1/* eslint-env jest */
2import execa from 'execa';
3import fs from 'fs-extra';
4import klawSync from 'klaw-sync';
5import path from 'path';
6
7import { runExportSideEffects } from './export-side-effects';
8import { bin, getRouterE2ERoot, getPageHtml } from '../utils';
9
10runExportSideEffects();
11
12describe('exports with single-page', () => {
13  const projectRoot = getRouterE2ERoot();
14  const outputName = 'dist-spa';
15  const outputDir = path.join(projectRoot, outputName);
16
17  beforeAll(
18    async () => {
19      await execa('node', [bin, 'export', '-p', 'web', '--clear', '--output-dir', outputName], {
20        cwd: projectRoot,
21        env: {
22          NODE_ENV: 'production',
23          EXPO_USE_STATIC: '0',
24          E2E_ROUTER_SRC: 'static-rendering',
25          E2E_ROUTER_ASYNC: 'development',
26        },
27      });
28    },
29    // Could take 45s depending on how fast the bundler resolves
30    560 * 1000
31  );
32
33  it(
34    'has expected files',
35    async () => {
36      // List output files with sizes for snapshotting.
37      // This is to make sure that any changes to the output are intentional.
38      // Posix path formatting is used to make paths the same across OSes.
39      const files = klawSync(outputDir)
40        .map((entry) => {
41          if (entry.path.includes('node_modules') || !entry.stats.isFile()) {
42            return null;
43          }
44          return path.posix.relative(outputDir, entry.path);
45        })
46        .filter(Boolean);
47
48      // The wrapper should not be included as a route.
49      expect(files).not.toContain('+html.html');
50      expect(files).not.toContain('_layout.html');
51      expect(files).not.toContain('about.html');
52
53      // Single page
54      expect(files).toContain('index.html');
55
56      // expect(files).toBe([
57      //   '_expo/static/css/global-67b6bc5b348b2db946e81c5f0040f565.css',
58      //   '_expo/static/css/test.module-2e80684560f145dd976dab27f0bd1062.css',
59      //   'assets/35ba0eaec5a4f5ed12ca16fabeae451d',
60      //   'assets/369745d4a4a6fa62fa0ed495f89aa964',
61      //   'assets/4f355ba1efca4b9c0e7a6271af047f61',
62      //   'assets/5223c8d9b0d08b82a5670fb5f71faf78',
63      //   'assets/563d5e3294b67811d0a1aede6f601e30',
64      //   'assets/5974eb3e1c5314e8d5a822702d7d0740',
65      //   'assets/5b50965d3dfbc518fe50ce36c314a6ec',
66      //   'assets/817aca47ff3cea63020753d336e628a4',
67      //   'assets/9d9c5644f55c2f6e4b7f247c378b2fe9',
68      //   'assets/__packages/@expo/metro-runtime/assets/alert-triangle.png',
69      //   'assets/__packages/@expo/metro-runtime/assets/chevron-left.png',
70      //   'assets/__packages/@expo/metro-runtime/assets/chevron-right.png',
71      //   'assets/__packages/@expo/metro-runtime/assets/close.png',
72      //   'assets/__packages/@expo/metro-runtime/assets/loader.png',
73      //   'assets/__packages/expo-router/assets/error.png',
74      //   'assets/__packages/expo-router/assets/file.png',
75      //   'assets/__packages/expo-router/assets/forward.png',
76      //   'assets/__packages/expo-router/assets/pkg.png',
77      //   'assets/b6c297a501e289394b0bc5dc69c265e6',
78      //   'assets/e62addcde857ebdb7342e6b9f1095e97',
79      //   'bundles/web-e5c890886338c2296ecf737befa2cc66.js',
80      //   'index.html',
81      //   'metadata.json',
82      // ]);
83    },
84    2 * 1000
85  );
86
87  it(
88    'can use environment variables',
89    async () => {
90      const indexHtml = await getPageHtml(outputDir, 'index.html');
91
92      const queryMeta = (name: string) =>
93        indexHtml.querySelector(`html > head > meta[name="${name}"]`)?.attributes.content;
94
95      [
96        'expo-e2e-public-env-var',
97        'expo-e2e-private-env-var',
98        'expo-e2e-public-env-var-client',
99        'expo-e2e-private-env-var-client',
100      ].forEach((name) => {
101        expect(queryMeta(name)).not.toBeDefined();
102      });
103
104      indexHtml.querySelectorAll('script').forEach((script) => {
105        const jsBundle = fs.readFileSync(path.join(outputDir, script.attributes.src), 'utf8');
106
107        // Ensure the bundle is valid
108        expect(jsBundle).toMatch('__BUNDLE_START_TIME__');
109        // Ensure the non-public env var is not included in the bundle
110        expect(jsBundle).not.toMatch('not-public-value');
111      });
112    },
113    2 * 1000
114  );
115
116  it(
117    'static styles are injected',
118    async () => {
119      const indexHtml = await getPageHtml(outputDir, 'index.html');
120      expect(indexHtml.querySelectorAll('html > head > style')?.length).toBe(
121        // Whatever was in the template
122        1
123      );
124    },
125    2 * 1000
126  );
127
128  // Static CSS works mostly the same across static and SPA modes.
129  it(
130    'statically extracts CSS',
131    async () => {
132      // Unfortunately, the CSS is injected in every page for now since we don't have bundle splitting.
133      const indexHtml = await getPageHtml(outputDir, 'index.html');
134
135      const links = indexHtml.querySelectorAll('html > head > link');
136      expect(links.length).toBe(
137        // Global CSS and CSS Module
138        4
139      );
140
141      links.forEach((link) => {
142        // Linked to the expected static location
143        expect(link.attributes.href).toMatch(/^\/_expo\/static\/css\/.*\.css$/);
144      });
145
146      expect(links[0].toString()).toMatch(
147        /<link rel="preload" href="\/_expo\/static\/css\/global-[\d\w]+\.css" as="style">/
148      );
149      expect(links[1].toString()).toMatch(
150        /<link rel="stylesheet" href="\/_expo\/static\/css\/global-[\d\w]+\.css">/
151      );
152      // CSS Module
153      expect(links[2].toString()).toMatch(
154        /<link rel="preload" href="\/_expo\/static\/css\/test\.module-[\d\w]+\.css" as="style">/
155      );
156      expect(links[3].toString()).toMatch(
157        /<link rel="stylesheet" href="\/_expo\/static\/css\/test\.module-[\d\w]+\.css">/
158      );
159
160      expect(
161        fs.readFileSync(path.join(outputDir, links[0].attributes.href), 'utf-8')
162      ).toMatchInlineSnapshot(`"div{background:#0ff}"`);
163
164      // CSS Module
165      expect(
166        fs.readFileSync(path.join(outputDir, links[2].attributes.href), 'utf-8')
167      ).toMatchInlineSnapshot(`".HPV33q_text{color:#1e90ff}"`);
168    },
169    2 * 1000
170  );
171
172  it(
173    'does not statically render the head',
174    async () => {
175      const indexHtml = await getPageHtml(outputDir, 'index.html');
176      expect(indexHtml.querySelector('html > head > title')?.innerText).toBe('Router E2E');
177
178      expect(indexHtml.querySelector('html > head > meta[name="description"]')).toBeNull();
179      expect(
180        // Nested from app/_layout.js
181        indexHtml.querySelector('html > head > meta[name="expo-nested-layout"]')
182      ).toBeNull();
183    },
184    2 * 1000
185  );
186});
187