1import execa from 'execa'; 2import klawSync from 'klaw-sync'; 3import path from 'path'; 4 5import { bin, ensurePortFreeAsync, getRouterE2ERoot } from '../utils'; 6import { runExportSideEffects } from './export-side-effects'; 7 8runExportSideEffects(); 9 10describe('server-output', () => { 11 const projectRoot = getRouterE2ERoot(); 12 const outputDir = path.join(projectRoot, 'dist-server'); 13 14 beforeAll( 15 async () => { 16 await execa('node', [bin, 'export', '-p', 'web', '--output-dir', 'dist-server'], { 17 cwd: projectRoot, 18 env: { 19 NODE_ENV: 'production', 20 EXPO_USE_STATIC: 'server', 21 E2E_ROUTER_SRC: 'server', 22 E2E_ROUTER_ASYNC: 'development', 23 }, 24 }); 25 }, 26 // Could take 45s depending on how fast the bundler resolves 27 560 * 1000 28 ); 29 30 describe('requests', () => { 31 beforeAll(async () => { 32 await ensurePortFreeAsync(3000); 33 // Start a server instance that we can test against then kill it. 34 server = execa('node', [nodeScript], { 35 cwd: projectRoot, 36 37 stderr: 'inherit', 38 39 env: { 40 NODE_ENV: 'production', 41 TEST_SECRET_KEY: 'test-secret-key', 42 }, 43 }); 44 // Wait for the server to start 45 await new Promise((resolve) => { 46 const listener = server!.stdout?.on('data', (data) => { 47 if (data.toString().includes('server listening')) { 48 console.log('Express server ready'); 49 resolve(null); 50 listener?.removeAllListeners(); 51 } 52 }); 53 }); 54 }, 120 * 1000); 55 const nodeScript = path.join(projectRoot, '__e2e__/server/express.js'); 56 let server: execa.ExecaChildProcess<string> | undefined; 57 58 afterAll(async () => { 59 server?.kill(); 60 }); 61 62 ['POST', 'GET', 'PUT', 'DELETE'].map(async (method) => { 63 it(`can make requests to ${method} routes`, async () => { 64 // Request missing route 65 expect( 66 await fetch('http://localhost:3000/methods', { 67 method: method, 68 }).then((res) => res.json()) 69 ).toEqual({ 70 method: method.toLowerCase(), 71 }); 72 }); 73 }); 74 75 it(`can serve up index html`, async () => { 76 expect(await fetch('http://localhost:3000').then((res) => res.text())).toMatch( 77 /<div id="root">/ 78 ); 79 }); 80 it(`can serve up dynamic html routes`, async () => { 81 expect(await fetch('http://localhost:3000/blog/123').then((res) => res.text())).toMatch( 82 /\[post\]/ 83 ); 84 }); 85 it(`can hit the 404 route`, async () => { 86 expect( 87 await fetch('http://localhost:3000/clearly-missing').then((res) => res.text()) 88 ).toMatch(/<div id="root">/); 89 }); 90 91 it( 92 'can use environment variables', 93 async () => { 94 expect(await fetch('http://localhost:3000/api/env-vars').then((res) => res.json())).toEqual( 95 { 96 // This is defined when we start the production server in `beforeAll`. 97 var: 'test-secret-key', 98 } 99 ); 100 }, 101 5 * 1000 102 ); 103 it( 104 'serves the empty route as 405', 105 async () => { 106 await expect(fetch('http://localhost:3000/api/empty').then((r) => r.status)).resolves.toBe( 107 405 108 ); 109 }, 110 5 * 1000 111 ); 112 it( 113 'serves not-found routes as 404', 114 async () => { 115 await expect(fetch('http://localhost:3000/missing').then((r) => r.status)).resolves.toBe( 116 404 117 ); 118 }, 119 5 * 1000 120 ); 121 it( 122 'automatically handles JS errors thrown inside of route handlers as 500', 123 async () => { 124 const res = await fetch('http://localhost:3000/api/problematic'); 125 expect(res.status).toBe(500); 126 expect(res.statusText).toBe('Internal Server Error'); 127 }, 128 5 * 1000 129 ); 130 it( 131 'can POST json to a route', 132 async () => { 133 const res = await fetch('http://localhost:3000/api/json', { 134 method: 'POST', 135 headers: { 136 'Content-Type': 'application/json', 137 }, 138 body: JSON.stringify({ hello: 'world' }), 139 }).then((r) => r.json()); 140 expect(res).toEqual({ hello: 'world' }); 141 }, 142 5 * 1000 143 ); 144 it( 145 'handles pinging routes with unsupported methods with 405 "Method Not Allowed"', 146 async () => { 147 const res = await fetch('http://localhost:3000/api/env-vars', { method: 'POST' }); 148 expect(res.status).toBe(405); 149 expect(res.statusText).toBe('Method Not Allowed'); 150 }, 151 5 * 1000 152 ); 153 it( 154 'supports accessing dynamic parameters using same convention as client-side Expo Router', 155 async () => { 156 await expect(fetch('http://localhost:3000/api/abc').then((r) => r.json())).resolves.toEqual( 157 { 158 hello: 'abc', 159 } 160 ); 161 }, 162 5 * 1000 163 ); 164 it( 165 'supports accessing deep dynamic parameters using different convention to client-side Expo Router', 166 async () => { 167 await expect( 168 fetch('http://localhost:3000/api/a/1/2/3').then((r) => r.json()) 169 ).resolves.toEqual({ 170 results: '1/2/3', 171 }); 172 }, 173 5 * 1000 174 ); 175 it( 176 'supports using Node.js externals to read local files', 177 async () => { 178 await expect( 179 fetch('http://localhost:3000/api/externals').then((r) => r.text()) 180 ).resolves.toEqual('a/b/c'); 181 }, 182 5 * 1000 183 ); 184 }); 185 186 it( 187 'has expected files', 188 async () => { 189 // Request HTML 190 191 // List output files with sizes for snapshotting. 192 // This is to make sure that any changes to the output are intentional. 193 // Posix path formatting is used to make paths the same across OSes. 194 const files = klawSync(outputDir) 195 .map((entry) => { 196 if (entry.path.includes('node_modules') || !entry.stats.isFile()) { 197 return null; 198 } 199 return path.posix.relative(outputDir, entry.path); 200 }) 201 .filter(Boolean); 202 203 // The wrapper should not be included as a route. 204 expect(files).not.toContain('+html.html'); 205 expect(files).not.toContain('_layout.html'); 206 207 // Has routes.json 208 expect(files).toContain('_expo/routes.json'); 209 210 // Has functions 211 expect(files).toContain('_expo/functions/methods+api.js'); 212 expect(files).toContain('_expo/functions/api/[dynamic]+api.js'); 213 expect(files).toContain('_expo/functions/api/externals+api.js'); 214 215 // TODO: We shouldn't export this 216 expect(files).toContain('_expo/functions/api/empty+api.js'); 217 218 // Injected by framework 219 expect(files).toContain('_sitemap.html'); 220 expect(files).toContain('[...404].html'); 221 222 // Normal routes 223 expect(files).toContain('index.html'); 224 expect(files).toContain('blog/[post].html'); 225 }, 226 5 * 1000 227 ); 228}); 229