Skip to content

Commit 03a4957

Browse files
motiz88facebook-github-bot
authored andcommitted
Add integration tests for virtual-prefix URL routing (#1706)
Summary: Adds integration tests for `[metro-project]` and `[metro-watchFolders]` virtual URL prefixes: bundle requests, out-of-bounds index 404, and asset serving. Removes Server unit tests that tested private methods (`_resolveWatchFolderPrefix`, `_getEntryPointAbsolutePath`) directly. The behaviours they covered are now tested end-to-end by the new integration tests and will also be covered by `ProjectRouteMap` unit tests in the next diff. All tests pass without any production code changes. Reviewed By: huntie Differential Revision: D104259281
1 parent 0e9d7aa commit 03a4957

5 files changed

Lines changed: 133 additions & 91 deletions

File tree

packages/metro/src/Server/__tests__/Server-test.js

Lines changed: 0 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1436,87 +1436,4 @@ describe('processRequest', () => {
14361436
},
14371437
);
14381438
});
1439-
1440-
describe('watchFolder prefix resolution', () => {
1441-
let watchFolderServer: $FlowFixMe;
1442-
1443-
beforeEach(() => {
1444-
watchFolderServer = new Server(
1445-
mergeConfig(getDefaultValues('/'), {
1446-
projectRoot: '/project',
1447-
watchFolders: ['/project', '/external/packages'],
1448-
resolver: {blockList: []},
1449-
cacheVersion: '',
1450-
serializer: {
1451-
getRunModuleStatement: moduleId =>
1452-
`require(${JSON.stringify(moduleId)});`,
1453-
polyfillModuleNames: [],
1454-
getModulesRunBeforeMainModule: () => ['InitializeCore'],
1455-
},
1456-
reporter: require('../../lib/reporting').nullReporter,
1457-
} as InputConfigT),
1458-
);
1459-
});
1460-
1461-
test('resolves [metro-watchFolders]/N/ prefix against the Nth watch folder', () => {
1462-
expect(
1463-
watchFolderServer._resolveWatchFolderPrefix(
1464-
'./[metro-watchFolders]/1/expo-router/entry',
1465-
),
1466-
).toEqual({
1467-
rootDir: '/external/packages',
1468-
filePath: './expo-router/entry',
1469-
});
1470-
});
1471-
1472-
test('resolves [metro-watchFolders]/0/ prefix against the first watch folder', () => {
1473-
expect(
1474-
watchFolderServer._resolveWatchFolderPrefix(
1475-
'./[metro-watchFolders]/0/app/index',
1476-
),
1477-
).toEqual({
1478-
rootDir: '/project',
1479-
filePath: './app/index',
1480-
});
1481-
});
1482-
1483-
test('resolves [metro-project]/ prefix against projectRoot', () => {
1484-
expect(
1485-
watchFolderServer._resolveWatchFolderPrefix(
1486-
'./[metro-project]/src/App',
1487-
),
1488-
).toEqual({
1489-
rootDir: '/project',
1490-
filePath: './src/App',
1491-
});
1492-
});
1493-
1494-
test('returns null for paths without a recognized prefix', () => {
1495-
expect(
1496-
watchFolderServer._resolveWatchFolderPrefix('./mybundle'),
1497-
).toBeNull();
1498-
});
1499-
1500-
test('returns null for out-of-bounds watchFolder index', () => {
1501-
expect(
1502-
watchFolderServer._resolveWatchFolderPrefix(
1503-
'./[metro-watchFolders]/99/mybundle',
1504-
),
1505-
).toBeNull();
1506-
});
1507-
1508-
test('_getEntryPointAbsolutePath resolves prefixed entry against the corresponding watch folder', () => {
1509-
expect(
1510-
watchFolderServer._getEntryPointAbsolutePath(
1511-
'./[metro-watchFolders]/1/expo-router/entry',
1512-
),
1513-
).toBe('/external/packages/expo-router/entry');
1514-
});
1515-
1516-
test('_getEntryPointAbsolutePath resolves non-prefixed entry against server root', () => {
1517-
expect(watchFolderServer._getEntryPointAbsolutePath('./mybundle')).toBe(
1518-
'/project/mybundle',
1519-
);
1520-
});
1521-
});
15221439
});

packages/metro/src/integration_tests/__tests__/build-test.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,41 @@ test('allows specifying paths to save bundle and maps', async () => {
118118
);
119119
});
120120

121+
// $FlowFixMe[prop-missing] - test.failing is not in Flow's Jest types
122+
test.failing(
123+
'builds a bundle from a file in a directory literally named [metro-project]',
124+
async () => {
125+
const config = await Metro.loadConfig({
126+
config: require.resolve('../metro.config.js'),
127+
});
128+
129+
const result = await Metro.runBuild(config, {
130+
entry: './[metro-project]/LiteralDir.js',
131+
});
132+
133+
expect(execBundle(result.code)).toBe('from-literal-dir');
134+
},
135+
);
136+
137+
// $FlowFixMe[prop-missing] - test.failing is not in Flow's Jest types
138+
test.failing(
139+
'runBuild resolves entry against projectRoot, not unstable_serverRoot',
140+
async () => {
141+
const baseConfig = await Metro.loadConfig({
142+
config: require.resolve('../metro.config.js'),
143+
});
144+
const config = MetroConfig.mergeConfig(baseConfig, {
145+
server: {unstable_serverRoot: path.resolve(INPUT_PATH, '..')},
146+
});
147+
148+
const result = await Metro.runBuild(config, {
149+
entry: 'TestBundle.js',
150+
});
151+
152+
expect(execBundle(result.code)).toBeDefined();
153+
},
154+
);
155+
121156
test('(unstable) allows specifying a transform profile', async () => {
122157
const config = await Metro.loadConfig({
123158
config: require.resolve('../metro.config.js'),

packages/metro/src/integration_tests/__tests__/rambundle-test.js

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,32 +19,46 @@ const vm = require('vm');
1919

2020
jest.setTimeout(30 * 1000);
2121

22-
test('builds and executes a RAM bundle', async () => {
23-
const config = await Metro.loadConfig({
22+
let config;
23+
24+
beforeAll(async () => {
25+
config = await Metro.loadConfig({
2426
config: require.resolve('../metro.config.js'),
2527
});
26-
const bundlePath = path.join(os.tmpdir(), 'rambundle.js');
28+
});
2729

30+
async function buildAndExecRamBundle(entry: string): mixed {
31+
const bundlePath = path.join(os.tmpdir(), `rambundle-${Date.now()}.js`);
2832
try {
2933
await Metro.runBuild(config, {
30-
entry: 'TestBundle.js',
34+
entry,
3135
output: ramBundleOutput,
3236
out: bundlePath,
3337
});
3438

3539
const bundleBuffer = fs.readFileSync(bundlePath);
3640
const parser = new RamBundleParser(bundleBuffer);
3741

38-
// Create a context with a global nativeRequire function, which reads the
39-
// module code from the RAM bundle and injects it into the VM.
4042
const context = vm.createContext({
4143
nativeRequire(id) {
4244
vm.runInContext(parser.getModule(id), context);
4345
},
4446
});
4547

46-
expect(vm.runInContext(parser.getStartupCode(), context)).toMatchSnapshot();
48+
return vm.runInContext(parser.getStartupCode(), context);
4749
} finally {
48-
fs.unlinkSync(bundlePath);
50+
if (fs.existsSync(bundlePath)) {
51+
fs.unlinkSync(bundlePath);
52+
}
4953
}
54+
}
55+
56+
test('builds and executes a RAM bundle', async () => {
57+
expect(await buildAndExecRamBundle('TestBundle.js')).toMatchSnapshot();
58+
});
59+
60+
test('rejects [metro-project] virtual prefix in runBuild entry', async () => {
61+
await expect(
62+
buildAndExecRamBundle('./[metro-project]/TestBundle.js'),
63+
).rejects.toThrow('was not found');
5064
});

packages/metro/src/integration_tests/__tests__/server-test.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,71 @@ describe('Metro development server serves bundles via HTTP', () => {
122122
);
123123
});
124124

125+
test('should serve bundles with [metro-watchFolders] entry point', async () => {
126+
expect(
127+
await downloadAndExec(
128+
'/[metro-watchFolders]/1/metro/src/integration_tests/basic_bundle/TestBundle.bundle?platform=ios&dev=true&minify=false',
129+
),
130+
).toBeDefined();
131+
});
132+
133+
test('should serve bundles with [metro-project] entry point', async () => {
134+
expect(
135+
await downloadAndExec(
136+
'/[metro-project]/TestBundle.bundle?platform=ios&dev=true&minify=false',
137+
),
138+
).toBeDefined();
139+
});
140+
141+
test('[metro-project] source map resolves same modules as non-prefixed', async () => {
142+
const directResponse = await fetchAndClose(
143+
'http://localhost:' +
144+
httpServer.address().port +
145+
'/TestBundle.map?platform=ios&dev=true&minify=false',
146+
);
147+
const prefixedResponse = await fetchAndClose(
148+
'http://localhost:' +
149+
httpServer.address().port +
150+
'/[metro-project]/TestBundle.map?platform=ios&dev=true&minify=false',
151+
);
152+
expect(directResponse.ok).toBe(true);
153+
expect(prefixedResponse.ok).toBe(true);
154+
const directMap = await directResponse.json();
155+
const prefixedMap = await prefixedResponse.json();
156+
expect([...prefixedMap.sources].sort()).toEqual(
157+
[...directMap.sources].sort(),
158+
);
159+
});
160+
161+
test('[metro-watchFolders] source map resolves same modules as non-prefixed', async () => {
162+
const directResponse = await fetchAndClose(
163+
'http://localhost:' +
164+
httpServer.address().port +
165+
'/TestBundle.map?platform=ios&dev=true&minify=false',
166+
);
167+
const watchFolderResponse = await fetchAndClose(
168+
'http://localhost:' +
169+
httpServer.address().port +
170+
'/[metro-watchFolders]/1/metro/src/integration_tests/basic_bundle/TestBundle.map?platform=ios&dev=true&minify=false',
171+
);
172+
expect(directResponse.ok).toBe(true);
173+
expect(watchFolderResponse.ok).toBe(true);
174+
const directMap = await directResponse.json();
175+
const watchFolderMap = await watchFolderResponse.json();
176+
expect([...watchFolderMap.sources].sort()).toEqual(
177+
[...directMap.sources].sort(),
178+
);
179+
});
180+
181+
test('responds with 404 for [metro-watchFolders] with out-of-bounds index', async () => {
182+
const response = await fetchAndClose(
183+
'http://localhost:' +
184+
httpServer.address().port +
185+
'/[metro-watchFolders]/99/TestBundle.bundle?platform=ios&dev=true&minify=false',
186+
);
187+
expect(response.status).toBe(404);
188+
});
189+
125190
test('responds with 404 when the bundle cannot be resolved', async () => {
126191
const response = await fetchAndClose(
127192
'http://localhost:' + httpServer.address().port + '/doesnotexist.bundle',
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
module.exports = 'from-literal-dir';

0 commit comments

Comments
 (0)