146f023faSEvan Baconimport execa from 'execa'; 246f023faSEvan Baconimport klawSync from 'klaw-sync'; 346f023faSEvan Baconimport path from 'path'; 446f023faSEvan Bacon 546f023faSEvan Baconimport { bin, ensurePortFreeAsync, getRouterE2ERoot } from '../utils'; 646f023faSEvan Baconimport { runExportSideEffects } from './export-side-effects'; 746f023faSEvan Bacon 846f023faSEvan BaconrunExportSideEffects(); 946f023faSEvan Bacon 1046f023faSEvan Bacondescribe('server-output', () => { 1146f023faSEvan Bacon const projectRoot = getRouterE2ERoot(); 1246f023faSEvan Bacon const outputDir = path.join(projectRoot, 'dist-server'); 1346f023faSEvan Bacon 1446f023faSEvan Bacon beforeAll( 1546f023faSEvan Bacon async () => { 1646f023faSEvan Bacon await execa('node', [bin, 'export', '-p', 'web', '--output-dir', 'dist-server'], { 1746f023faSEvan Bacon cwd: projectRoot, 1846f023faSEvan Bacon env: { 1946f023faSEvan Bacon NODE_ENV: 'production', 2046f023faSEvan Bacon EXPO_USE_STATIC: 'server', 2146f023faSEvan Bacon E2E_ROUTER_SRC: 'server', 2246f023faSEvan Bacon E2E_ROUTER_ASYNC: 'development', 2346f023faSEvan Bacon }, 2446f023faSEvan Bacon }); 2546f023faSEvan Bacon }, 2646f023faSEvan Bacon // Could take 45s depending on how fast the bundler resolves 2746f023faSEvan Bacon 560 * 1000 2846f023faSEvan Bacon ); 2946f023faSEvan Bacon 3046f023faSEvan Bacon describe('requests', () => { 3146f023faSEvan Bacon beforeAll(async () => { 3246f023faSEvan Bacon await ensurePortFreeAsync(3000); 3346f023faSEvan Bacon // Start a server instance that we can test against then kill it. 3446f023faSEvan Bacon server = execa('node', [nodeScript], { 3546f023faSEvan Bacon cwd: projectRoot, 3646f023faSEvan Bacon 3746f023faSEvan Bacon stderr: 'inherit', 3846f023faSEvan Bacon 3946f023faSEvan Bacon env: { 4046f023faSEvan Bacon NODE_ENV: 'production', 4146f023faSEvan Bacon TEST_SECRET_KEY: 'test-secret-key', 4246f023faSEvan Bacon }, 4346f023faSEvan Bacon }); 4446f023faSEvan Bacon // Wait for the server to start 4546f023faSEvan Bacon await new Promise((resolve) => { 4646f023faSEvan Bacon const listener = server!.stdout?.on('data', (data) => { 4746f023faSEvan Bacon if (data.toString().includes('server listening')) { 4846f023faSEvan Bacon console.log('Express server ready'); 4946f023faSEvan Bacon resolve(null); 5046f023faSEvan Bacon listener?.removeAllListeners(); 5146f023faSEvan Bacon } 5246f023faSEvan Bacon }); 5346f023faSEvan Bacon }); 5446f023faSEvan Bacon }, 120 * 1000); 5546f023faSEvan Bacon const nodeScript = path.join(projectRoot, '__e2e__/server/express.js'); 5646f023faSEvan Bacon let server: execa.ExecaChildProcess<string> | undefined; 5746f023faSEvan Bacon 5846f023faSEvan Bacon afterAll(async () => { 5946f023faSEvan Bacon server?.kill(); 6046f023faSEvan Bacon }); 6146f023faSEvan Bacon 6246f023faSEvan Bacon ['POST', 'GET', 'PUT', 'DELETE'].map(async (method) => { 6346f023faSEvan Bacon it(`can make requests to ${method} routes`, async () => { 6446f023faSEvan Bacon // Request missing route 6546f023faSEvan Bacon expect( 6646f023faSEvan Bacon await fetch('http://localhost:3000/methods', { 6746f023faSEvan Bacon method: method, 6846f023faSEvan Bacon }).then((res) => res.json()) 6946f023faSEvan Bacon ).toEqual({ 7046f023faSEvan Bacon method: method.toLowerCase(), 7146f023faSEvan Bacon }); 7246f023faSEvan Bacon }); 7346f023faSEvan Bacon }); 7446f023faSEvan Bacon 7546f023faSEvan Bacon it(`can serve up index html`, async () => { 7646f023faSEvan Bacon expect(await fetch('http://localhost:3000').then((res) => res.text())).toMatch( 7746f023faSEvan Bacon /<div id="root">/ 7846f023faSEvan Bacon ); 7946f023faSEvan Bacon }); 80*e015d41cSEvan Bacon 81*e015d41cSEvan Bacon it(`can serve up group routes`, async () => { 82*e015d41cSEvan Bacon // Can access the same route from different paths 83*e015d41cSEvan Bacon expect(await fetch('http://localhost:3000/beta').then((res) => res.text())).toMatch( 84*e015d41cSEvan Bacon /<div data-testid="alpha-beta-text">/ 85*e015d41cSEvan Bacon ); 86*e015d41cSEvan Bacon expect(await fetch('http://localhost:3000/(alpha)/beta').then((res) => res.text())).toMatch( 87*e015d41cSEvan Bacon /<div data-testid="alpha-beta-text">/ 88*e015d41cSEvan Bacon ); 89*e015d41cSEvan Bacon }); 9046f023faSEvan Bacon it(`can serve up dynamic html routes`, async () => { 9146f023faSEvan Bacon expect(await fetch('http://localhost:3000/blog/123').then((res) => res.text())).toMatch( 9246f023faSEvan Bacon /\[post\]/ 9346f023faSEvan Bacon ); 9446f023faSEvan Bacon }); 9546f023faSEvan Bacon it(`can hit the 404 route`, async () => { 9646f023faSEvan Bacon expect( 9746f023faSEvan Bacon await fetch('http://localhost:3000/clearly-missing').then((res) => res.text()) 9846f023faSEvan Bacon ).toMatch(/<div id="root">/); 9946f023faSEvan Bacon }); 10046f023faSEvan Bacon 10146f023faSEvan Bacon it( 10246f023faSEvan Bacon 'can use environment variables', 10346f023faSEvan Bacon async () => { 10446f023faSEvan Bacon expect(await fetch('http://localhost:3000/api/env-vars').then((res) => res.json())).toEqual( 10546f023faSEvan Bacon { 10646f023faSEvan Bacon // This is defined when we start the production server in `beforeAll`. 10746f023faSEvan Bacon var: 'test-secret-key', 10846f023faSEvan Bacon } 10946f023faSEvan Bacon ); 11046f023faSEvan Bacon }, 11146f023faSEvan Bacon 5 * 1000 11246f023faSEvan Bacon ); 11346f023faSEvan Bacon it( 11446f023faSEvan Bacon 'serves the empty route as 405', 11546f023faSEvan Bacon async () => { 11646f023faSEvan Bacon await expect(fetch('http://localhost:3000/api/empty').then((r) => r.status)).resolves.toBe( 11746f023faSEvan Bacon 405 11846f023faSEvan Bacon ); 11946f023faSEvan Bacon }, 12046f023faSEvan Bacon 5 * 1000 12146f023faSEvan Bacon ); 12246f023faSEvan Bacon it( 12346f023faSEvan Bacon 'serves not-found routes as 404', 12446f023faSEvan Bacon async () => { 12546f023faSEvan Bacon await expect(fetch('http://localhost:3000/missing').then((r) => r.status)).resolves.toBe( 12646f023faSEvan Bacon 404 12746f023faSEvan Bacon ); 12846f023faSEvan Bacon }, 12946f023faSEvan Bacon 5 * 1000 13046f023faSEvan Bacon ); 13146f023faSEvan Bacon it( 13246f023faSEvan Bacon 'automatically handles JS errors thrown inside of route handlers as 500', 13346f023faSEvan Bacon async () => { 13446f023faSEvan Bacon const res = await fetch('http://localhost:3000/api/problematic'); 13546f023faSEvan Bacon expect(res.status).toBe(500); 13646f023faSEvan Bacon expect(res.statusText).toBe('Internal Server Error'); 13746f023faSEvan Bacon }, 13846f023faSEvan Bacon 5 * 1000 13946f023faSEvan Bacon ); 14046f023faSEvan Bacon it( 14146f023faSEvan Bacon 'can POST json to a route', 14246f023faSEvan Bacon async () => { 14346f023faSEvan Bacon const res = await fetch('http://localhost:3000/api/json', { 14446f023faSEvan Bacon method: 'POST', 14546f023faSEvan Bacon headers: { 14646f023faSEvan Bacon 'Content-Type': 'application/json', 14746f023faSEvan Bacon }, 14846f023faSEvan Bacon body: JSON.stringify({ hello: 'world' }), 14946f023faSEvan Bacon }).then((r) => r.json()); 15046f023faSEvan Bacon expect(res).toEqual({ hello: 'world' }); 15146f023faSEvan Bacon }, 15246f023faSEvan Bacon 5 * 1000 15346f023faSEvan Bacon ); 15446f023faSEvan Bacon it( 15546f023faSEvan Bacon 'handles pinging routes with unsupported methods with 405 "Method Not Allowed"', 15646f023faSEvan Bacon async () => { 15746f023faSEvan Bacon const res = await fetch('http://localhost:3000/api/env-vars', { method: 'POST' }); 15846f023faSEvan Bacon expect(res.status).toBe(405); 15946f023faSEvan Bacon expect(res.statusText).toBe('Method Not Allowed'); 16046f023faSEvan Bacon }, 16146f023faSEvan Bacon 5 * 1000 16246f023faSEvan Bacon ); 16346f023faSEvan Bacon it( 16446f023faSEvan Bacon 'supports accessing dynamic parameters using same convention as client-side Expo Router', 16546f023faSEvan Bacon async () => { 16646f023faSEvan Bacon await expect(fetch('http://localhost:3000/api/abc').then((r) => r.json())).resolves.toEqual( 16746f023faSEvan Bacon { 16846f023faSEvan Bacon hello: 'abc', 16946f023faSEvan Bacon } 17046f023faSEvan Bacon ); 17146f023faSEvan Bacon }, 17246f023faSEvan Bacon 5 * 1000 17346f023faSEvan Bacon ); 17446f023faSEvan Bacon it( 17546f023faSEvan Bacon 'supports accessing deep dynamic parameters using different convention to client-side Expo Router', 17646f023faSEvan Bacon async () => { 17746f023faSEvan Bacon await expect( 17846f023faSEvan Bacon fetch('http://localhost:3000/api/a/1/2/3').then((r) => r.json()) 17946f023faSEvan Bacon ).resolves.toEqual({ 18046f023faSEvan Bacon results: '1/2/3', 18146f023faSEvan Bacon }); 18246f023faSEvan Bacon }, 18346f023faSEvan Bacon 5 * 1000 18446f023faSEvan Bacon ); 18546f023faSEvan Bacon it( 18646f023faSEvan Bacon 'supports using Node.js externals to read local files', 18746f023faSEvan Bacon async () => { 18846f023faSEvan Bacon await expect( 18946f023faSEvan Bacon fetch('http://localhost:3000/api/externals').then((r) => r.text()) 19046f023faSEvan Bacon ).resolves.toEqual('a/b/c'); 19146f023faSEvan Bacon }, 19246f023faSEvan Bacon 5 * 1000 19346f023faSEvan Bacon ); 19446f023faSEvan Bacon }); 19546f023faSEvan Bacon 19646f023faSEvan Bacon it( 19746f023faSEvan Bacon 'has expected files', 19846f023faSEvan Bacon async () => { 19946f023faSEvan Bacon // Request HTML 20046f023faSEvan Bacon 20146f023faSEvan Bacon // List output files with sizes for snapshotting. 20246f023faSEvan Bacon // This is to make sure that any changes to the output are intentional. 20346f023faSEvan Bacon // Posix path formatting is used to make paths the same across OSes. 20446f023faSEvan Bacon const files = klawSync(outputDir) 20546f023faSEvan Bacon .map((entry) => { 20646f023faSEvan Bacon if (entry.path.includes('node_modules') || !entry.stats.isFile()) { 20746f023faSEvan Bacon return null; 20846f023faSEvan Bacon } 20946f023faSEvan Bacon return path.posix.relative(outputDir, entry.path); 21046f023faSEvan Bacon }) 21146f023faSEvan Bacon .filter(Boolean); 21246f023faSEvan Bacon 21346f023faSEvan Bacon // The wrapper should not be included as a route. 21446f023faSEvan Bacon expect(files).not.toContain('+html.html'); 21546f023faSEvan Bacon expect(files).not.toContain('_layout.html'); 21646f023faSEvan Bacon 21746f023faSEvan Bacon // Has routes.json 21846f023faSEvan Bacon expect(files).toContain('_expo/routes.json'); 21946f023faSEvan Bacon 22046f023faSEvan Bacon // Has functions 22146f023faSEvan Bacon expect(files).toContain('_expo/functions/methods+api.js'); 22246f023faSEvan Bacon expect(files).toContain('_expo/functions/api/[dynamic]+api.js'); 22346f023faSEvan Bacon expect(files).toContain('_expo/functions/api/externals+api.js'); 22446f023faSEvan Bacon 22546f023faSEvan Bacon // TODO: We shouldn't export this 22646f023faSEvan Bacon expect(files).toContain('_expo/functions/api/empty+api.js'); 22746f023faSEvan Bacon 228*e015d41cSEvan Bacon // Has single variation of group file 229*e015d41cSEvan Bacon expect(files).toContain('(alpha)/beta.html'); 230*e015d41cSEvan Bacon expect(files).not.toContain('beta.html'); 231*e015d41cSEvan Bacon 23246f023faSEvan Bacon // Injected by framework 23346f023faSEvan Bacon expect(files).toContain('_sitemap.html'); 23446f023faSEvan Bacon expect(files).toContain('[...404].html'); 23546f023faSEvan Bacon 23646f023faSEvan Bacon // Normal routes 23746f023faSEvan Bacon expect(files).toContain('index.html'); 23846f023faSEvan Bacon expect(files).toContain('blog/[post].html'); 23946f023faSEvan Bacon }, 24046f023faSEvan Bacon 5 * 1000 24146f023faSEvan Bacon ); 24246f023faSEvan Bacon}); 243