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