105863844SEvan Bacon/* eslint-env jest */
205863844SEvan Baconimport execa from 'execa';
305863844SEvan Baconimport fs from 'fs-extra';
405863844SEvan Baconimport klawSync from 'klaw-sync';
505863844SEvan Baconimport path from 'path';
605863844SEvan Bacon
705863844SEvan Baconimport { runExportSideEffects } from './export-side-effects';
805863844SEvan Baconimport { bin, getRouterE2ERoot, getPageHtml } from '../utils';
905863844SEvan Bacon
1005863844SEvan BaconrunExportSideEffects();
1105863844SEvan Bacon
1205863844SEvan Bacondescribe('exports with single-page', () => {
1305863844SEvan Bacon  const projectRoot = getRouterE2ERoot();
1405863844SEvan Bacon  const outputName = 'dist-spa';
1505863844SEvan Bacon  const outputDir = path.join(projectRoot, outputName);
1605863844SEvan Bacon
1705863844SEvan Bacon  beforeAll(
1805863844SEvan Bacon    async () => {
1905863844SEvan Bacon      await execa('node', [bin, 'export', '-p', 'web', '--clear', '--output-dir', outputName], {
2005863844SEvan Bacon        cwd: projectRoot,
2105863844SEvan Bacon        env: {
2205863844SEvan Bacon          NODE_ENV: 'production',
23*46f023faSEvan Bacon          EXPO_USE_STATIC: 'single',
2405863844SEvan Bacon          E2E_ROUTER_SRC: 'static-rendering',
2505863844SEvan Bacon          E2E_ROUTER_ASYNC: 'development',
2605863844SEvan Bacon        },
2705863844SEvan Bacon      });
2805863844SEvan Bacon    },
2905863844SEvan Bacon    // Could take 45s depending on how fast the bundler resolves
3005863844SEvan Bacon    560 * 1000
3105863844SEvan Bacon  );
3205863844SEvan Bacon
3305863844SEvan Bacon  it(
3405863844SEvan Bacon    'has expected files',
3505863844SEvan Bacon    async () => {
3605863844SEvan Bacon      // List output files with sizes for snapshotting.
3705863844SEvan Bacon      // This is to make sure that any changes to the output are intentional.
3805863844SEvan Bacon      // Posix path formatting is used to make paths the same across OSes.
3905863844SEvan Bacon      const files = klawSync(outputDir)
4005863844SEvan Bacon        .map((entry) => {
4105863844SEvan Bacon          if (entry.path.includes('node_modules') || !entry.stats.isFile()) {
4205863844SEvan Bacon            return null;
4305863844SEvan Bacon          }
4405863844SEvan Bacon          return path.posix.relative(outputDir, entry.path);
4505863844SEvan Bacon        })
4605863844SEvan Bacon        .filter(Boolean);
4705863844SEvan Bacon
4805863844SEvan Bacon      // The wrapper should not be included as a route.
4905863844SEvan Bacon      expect(files).not.toContain('+html.html');
5005863844SEvan Bacon      expect(files).not.toContain('_layout.html');
5105863844SEvan Bacon      expect(files).not.toContain('about.html');
5205863844SEvan Bacon
5305863844SEvan Bacon      // Single page
5405863844SEvan Bacon      expect(files).toContain('index.html');
5505863844SEvan Bacon
5605863844SEvan Bacon      // expect(files).toBe([
5705863844SEvan Bacon      //   '_expo/static/css/global-67b6bc5b348b2db946e81c5f0040f565.css',
5805863844SEvan Bacon      //   '_expo/static/css/test.module-2e80684560f145dd976dab27f0bd1062.css',
5905863844SEvan Bacon      //   'assets/35ba0eaec5a4f5ed12ca16fabeae451d',
6005863844SEvan Bacon      //   'assets/369745d4a4a6fa62fa0ed495f89aa964',
6105863844SEvan Bacon      //   'assets/4f355ba1efca4b9c0e7a6271af047f61',
6205863844SEvan Bacon      //   'assets/5223c8d9b0d08b82a5670fb5f71faf78',
6305863844SEvan Bacon      //   'assets/563d5e3294b67811d0a1aede6f601e30',
6405863844SEvan Bacon      //   'assets/5974eb3e1c5314e8d5a822702d7d0740',
6505863844SEvan Bacon      //   'assets/5b50965d3dfbc518fe50ce36c314a6ec',
6605863844SEvan Bacon      //   'assets/817aca47ff3cea63020753d336e628a4',
6705863844SEvan Bacon      //   'assets/9d9c5644f55c2f6e4b7f247c378b2fe9',
6805863844SEvan Bacon      //   'assets/__packages/@expo/metro-runtime/assets/alert-triangle.png',
6905863844SEvan Bacon      //   'assets/__packages/@expo/metro-runtime/assets/chevron-left.png',
7005863844SEvan Bacon      //   'assets/__packages/@expo/metro-runtime/assets/chevron-right.png',
7105863844SEvan Bacon      //   'assets/__packages/@expo/metro-runtime/assets/close.png',
7205863844SEvan Bacon      //   'assets/__packages/@expo/metro-runtime/assets/loader.png',
7305863844SEvan Bacon      //   'assets/__packages/expo-router/assets/error.png',
7405863844SEvan Bacon      //   'assets/__packages/expo-router/assets/file.png',
7505863844SEvan Bacon      //   'assets/__packages/expo-router/assets/forward.png',
7605863844SEvan Bacon      //   'assets/__packages/expo-router/assets/pkg.png',
7705863844SEvan Bacon      //   'assets/b6c297a501e289394b0bc5dc69c265e6',
7805863844SEvan Bacon      //   'assets/e62addcde857ebdb7342e6b9f1095e97',
7905863844SEvan Bacon      //   'bundles/web-e5c890886338c2296ecf737befa2cc66.js',
8005863844SEvan Bacon      //   'index.html',
8105863844SEvan Bacon      //   'metadata.json',
8205863844SEvan Bacon      // ]);
8305863844SEvan Bacon    },
8405863844SEvan Bacon    2 * 1000
8505863844SEvan Bacon  );
8605863844SEvan Bacon
8705863844SEvan Bacon  it(
8805863844SEvan Bacon    'can use environment variables',
8905863844SEvan Bacon    async () => {
9005863844SEvan Bacon      const indexHtml = await getPageHtml(outputDir, 'index.html');
9105863844SEvan Bacon
9205863844SEvan Bacon      const queryMeta = (name: string) =>
9305863844SEvan Bacon        indexHtml.querySelector(`html > head > meta[name="${name}"]`)?.attributes.content;
9405863844SEvan Bacon
9505863844SEvan Bacon      [
9605863844SEvan Bacon        'expo-e2e-public-env-var',
9705863844SEvan Bacon        'expo-e2e-private-env-var',
9805863844SEvan Bacon        'expo-e2e-public-env-var-client',
9905863844SEvan Bacon        'expo-e2e-private-env-var-client',
10005863844SEvan Bacon      ].forEach((name) => {
10105863844SEvan Bacon        expect(queryMeta(name)).not.toBeDefined();
10205863844SEvan Bacon      });
10305863844SEvan Bacon
10405863844SEvan Bacon      indexHtml.querySelectorAll('script').forEach((script) => {
10505863844SEvan Bacon        const jsBundle = fs.readFileSync(path.join(outputDir, script.attributes.src), 'utf8');
10605863844SEvan Bacon
10705863844SEvan Bacon        // Ensure the bundle is valid
10805863844SEvan Bacon        expect(jsBundle).toMatch('__BUNDLE_START_TIME__');
10905863844SEvan Bacon        // Ensure the non-public env var is not included in the bundle
11005863844SEvan Bacon        expect(jsBundle).not.toMatch('not-public-value');
11105863844SEvan Bacon      });
11205863844SEvan Bacon    },
11305863844SEvan Bacon    2 * 1000
11405863844SEvan Bacon  );
11505863844SEvan Bacon
11605863844SEvan Bacon  it(
11705863844SEvan Bacon    'static styles are injected',
11805863844SEvan Bacon    async () => {
11905863844SEvan Bacon      const indexHtml = await getPageHtml(outputDir, 'index.html');
12005863844SEvan Bacon      expect(indexHtml.querySelectorAll('html > head > style')?.length).toBe(
12105863844SEvan Bacon        // Whatever was in the template
12205863844SEvan Bacon        1
12305863844SEvan Bacon      );
12405863844SEvan Bacon    },
12505863844SEvan Bacon    2 * 1000
12605863844SEvan Bacon  );
12705863844SEvan Bacon
12805863844SEvan Bacon  // Static CSS works mostly the same across static and SPA modes.
12905863844SEvan Bacon  it(
13005863844SEvan Bacon    'statically extracts CSS',
13105863844SEvan Bacon    async () => {
13205863844SEvan Bacon      // Unfortunately, the CSS is injected in every page for now since we don't have bundle splitting.
13305863844SEvan Bacon      const indexHtml = await getPageHtml(outputDir, 'index.html');
13405863844SEvan Bacon
13505863844SEvan Bacon      const links = indexHtml.querySelectorAll('html > head > link');
13605863844SEvan Bacon      expect(links.length).toBe(
13705863844SEvan Bacon        // Global CSS and CSS Module
13805863844SEvan Bacon        4
13905863844SEvan Bacon      );
14005863844SEvan Bacon
14105863844SEvan Bacon      links.forEach((link) => {
14205863844SEvan Bacon        // Linked to the expected static location
14305863844SEvan Bacon        expect(link.attributes.href).toMatch(/^\/_expo\/static\/css\/.*\.css$/);
14405863844SEvan Bacon      });
14505863844SEvan Bacon
14605863844SEvan Bacon      expect(links[0].toString()).toMatch(
14705863844SEvan Bacon        /<link rel="preload" href="\/_expo\/static\/css\/global-[\d\w]+\.css" as="style">/
14805863844SEvan Bacon      );
14905863844SEvan Bacon      expect(links[1].toString()).toMatch(
15005863844SEvan Bacon        /<link rel="stylesheet" href="\/_expo\/static\/css\/global-[\d\w]+\.css">/
15105863844SEvan Bacon      );
15205863844SEvan Bacon      // CSS Module
15305863844SEvan Bacon      expect(links[2].toString()).toMatch(
15405863844SEvan Bacon        /<link rel="preload" href="\/_expo\/static\/css\/test\.module-[\d\w]+\.css" as="style">/
15505863844SEvan Bacon      );
15605863844SEvan Bacon      expect(links[3].toString()).toMatch(
15705863844SEvan Bacon        /<link rel="stylesheet" href="\/_expo\/static\/css\/test\.module-[\d\w]+\.css">/
15805863844SEvan Bacon      );
15905863844SEvan Bacon
16005863844SEvan Bacon      expect(
16105863844SEvan Bacon        fs.readFileSync(path.join(outputDir, links[0].attributes.href), 'utf-8')
16205863844SEvan Bacon      ).toMatchInlineSnapshot(`"div{background:#0ff}"`);
16305863844SEvan Bacon
16405863844SEvan Bacon      // CSS Module
16505863844SEvan Bacon      expect(
16605863844SEvan Bacon        fs.readFileSync(path.join(outputDir, links[2].attributes.href), 'utf-8')
16705863844SEvan Bacon      ).toMatchInlineSnapshot(`".HPV33q_text{color:#1e90ff}"`);
16805863844SEvan Bacon    },
16905863844SEvan Bacon    2 * 1000
17005863844SEvan Bacon  );
17105863844SEvan Bacon
17205863844SEvan Bacon  it(
17305863844SEvan Bacon    'does not statically render the head',
17405863844SEvan Bacon    async () => {
17505863844SEvan Bacon      const indexHtml = await getPageHtml(outputDir, 'index.html');
17605863844SEvan Bacon      expect(indexHtml.querySelector('html > head > title')?.innerText).toBe('Router E2E');
17705863844SEvan Bacon
17805863844SEvan Bacon      expect(indexHtml.querySelector('html > head > meta[name="description"]')).toBeNull();
17905863844SEvan Bacon      expect(
18005863844SEvan Bacon        // Nested from app/_layout.js
18105863844SEvan Bacon        indexHtml.querySelector('html > head > meta[name="expo-nested-layout"]')
18205863844SEvan Bacon      ).toBeNull();
18305863844SEvan Bacon    },
18405863844SEvan Bacon    2 * 1000
18505863844SEvan Bacon  );
18605863844SEvan Bacon});
187