Mercurial
comparison hg-web/e2e/app_e2e_test.js @ 224:3007ef5fc0ed hg-web
[hg-web] Simplify visuals and preview static files
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Sun, 02 Aug 2026 13:51:11 -0700 |
| parents | 0e7b9464248d |
| children | 70de0c80d093 |
comparison
equal
deleted
inserted
replaced
| 223:0e7b9464248d | 224:3007ef5fc0ed |
|---|---|
| 71 path.join(root, 'README.md'), | 71 path.join(root, 'README.md'), |
| 72 '# Fixture Repository\n\n<script>window.__hgWebXss = true</script>\n', | 72 '# Fixture Repository\n\n<script>window.__hgWebXss = true</script>\n', |
| 73 ); | 73 ); |
| 74 fs.writeFileSync(path.join(root, 'docs', 'README.md'), '# Documentation\n\nNested README.\n'); | 74 fs.writeFileSync(path.join(root, 'docs', 'README.md'), '# Documentation\n\nNested README.\n'); |
| 75 fs.writeFileSync(path.join(root, 'src', 'main.c'), 'int main(void) { return 0; }\n'); | 75 fs.writeFileSync(path.join(root, 'src', 'main.c'), 'int main(void) { return 0; }\n'); |
| 76 fs.writeFileSync( | |
| 77 path.join(root, 'pixel.png'), | |
| 78 Buffer.from( | |
| 79 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Wl2nWQAAAAASUVORK5CYII=', | |
| 80 'base64', | |
| 81 ), | |
| 82 ); | |
| 83 fs.writeFileSync(path.join(root, 'movie.mp4'), Buffer.from('00000018667479706d703432', 'hex')); | |
| 84 fs.writeFileSync(path.join(root, 'sound.mp3'), Buffer.from('ID3')); | |
| 85 fs.writeFileSync(path.join(root, 'document.pdf'), Buffer.from('%PDF-1.4\n%%EOF\n')); | |
| 86 fs.writeFileSync(path.join(root, 'archive.bin'), Buffer.from([0, 1, 2, 3])); | |
| 76 run('hg', ['--repository', root, 'add'], options); | 87 run('hg', ['--repository', root, 'add'], options); |
| 77 run('hg', ['--repository', root, 'commit', '-u', 'Test User <[email protected]>', '-m', 'Initial fixture'], options); | 88 run('hg', ['--repository', root, 'commit', '-u', 'Test User <[email protected]>', '-m', 'Initial fixture'], options); |
| 78 const firstNode = run('hg', ['--repository', root, 'log', '-r', '.', '-T', '{node}'], options); | 89 const firstNode = run('hg', ['--repository', root, 'log', '-r', '.', '-T', '{node}'], options); |
| 79 | 90 |
| 80 fs.appendFileSync(path.join(root, 'README.md'), '\nSecond revision.\n'); | 91 fs.appendFileSync(path.join(root, 'README.md'), '\nSecond revision.\n'); |
| 176 for (const pathname of ['/page.js', '/index.css', '/base.css', '/pencil_lines.png', '/panda.png']) { | 187 for (const pathname of ['/page.js', '/index.css', '/base.css', '/pencil_lines.png', '/panda.png']) { |
| 177 const response = await fetch(`${BASE_URL}${pathname}`); | 188 const response = await fetch(`${BASE_URL}${pathname}`); |
| 178 assert.equal(response.status, 200, `${pathname} static asset`); | 189 assert.equal(response.status, 200, `${pathname} static asset`); |
| 179 assert.ok((await response.arrayBuffer()).byteLength > 0, `${pathname} is non-empty`); | 190 assert.ok((await response.arrayBuffer()).byteLength > 0, `${pathname} is non-empty`); |
| 180 } | 191 } |
| 192 for (const pathname of ['/hg-web-background.jpg', '/pencil_texture.png']) { | |
| 193 assert.equal((await fetch(`${BASE_URL}${pathname}`)).status, 404); | |
| 194 } | |
| 181 | 195 |
| 182 const rootList = await assertJson('/api/repo/list'); | 196 const rootList = await assertJson('/api/repo/list'); |
| 183 assert.ok(rootList.directories.some(entry => entry.basename === 'docs')); | 197 assert.ok(rootList.directories.some(entry => entry.basename === 'docs')); |
| 184 assert.ok(rootList.files.some(entry => entry.basename === 'README.md')); | 198 assert.ok(rootList.files.some(entry => entry.basename === 'README.md')); |
| 185 | 199 |
| 187 assert.ok(nestedList.files.some(entry => entry.basename === 'README.md')); | 201 assert.ok(nestedList.files.some(entry => entry.basename === 'README.md')); |
| 188 | 202 |
| 189 const fileResponse = await fetch(`${BASE_URL}/api/repo/file?path=src%2Fmain.c`); | 203 const fileResponse = await fetch(`${BASE_URL}/api/repo/file?path=src%2Fmain.c`); |
| 190 assert.equal(fileResponse.status, 200); | 204 assert.equal(fileResponse.status, 200); |
| 191 assert.match(await fileResponse.text(), /int main/); | 205 assert.match(await fileResponse.text(), /int main/); |
| 206 | |
| 207 for (const [filename, contentType, disposition] of [ | |
| 208 ['pixel.png', 'image/png', 'inline'], | |
| 209 ['movie.mp4', 'video/mp4', 'inline'], | |
| 210 ['sound.mp3', 'audio/mpeg', 'inline'], | |
| 211 ['document.pdf', 'application/pdf', 'inline'], | |
| 212 ['archive.bin', 'application/octet-stream', 'attachment'], | |
| 213 ]) { | |
| 214 const response = await fetch( | |
| 215 `${BASE_URL}/api/repo/file?path=${encodeURIComponent(filename)}`, | |
| 216 ); | |
| 217 assert.equal(response.status, 200, `${filename} response`); | |
| 218 assert.match(response.headers.get('content-type') || '', new RegExp(`^${contentType}`)); | |
| 219 assert.equal(response.headers.get('content-disposition'), disposition); | |
| 220 assert.equal(response.headers.get('x-content-type-options'), 'nosniff'); | |
| 221 assert.ok((await response.arrayBuffer()).byteLength > 0); | |
| 222 } | |
| 223 const binaryResponse = await fetch(`${BASE_URL}/api/repo/file?path=archive.bin`); | |
| 224 assert.deepEqual( | |
| 225 Buffer.from(await binaryResponse.arrayBuffer()), | |
| 226 Buffer.from([0, 1, 2, 3]), | |
| 227 ); | |
| 192 | 228 |
| 193 const readmeResponse = await fetch(`${BASE_URL}/api/repo/readme?path=docs`); | 229 const readmeResponse = await fetch(`${BASE_URL}/api/repo/readme?path=docs`); |
| 194 assert.equal(readmeResponse.status, 200); | 230 assert.equal(readmeResponse.status, 200); |
| 195 assert.match(await readmeResponse.text(), /Nested README/); | 231 assert.match(await readmeResponse.text(), /Nested README/); |
| 196 assert.equal((await fetch(`${BASE_URL}/api/repo/readme?path=src`)).status, 204); | 232 assert.equal((await fetch(`${BASE_URL}/api/repo/readme?path=src`)).status, 204); |
| 232 await assertPageHasNoBrowserErrors(browser, '/', async page => { | 268 await assertPageHasNoBrowserErrors(browser, '/', async page => { |
| 233 await page.getByRole('heading', { name: 'Zenbu Repository' }).waitFor(); | 269 await page.getByRole('heading', { name: 'Zenbu Repository' }).waitFor(); |
| 234 await page.getByText('Recent Commits').waitFor(); | 270 await page.getByText('Recent Commits').waitFor(); |
| 235 await page.getByText('Repository Files').waitFor(); | 271 await page.getByText('Repository Files').waitFor(); |
| 236 assert.equal(await page.evaluate(() => window.__hgWebXss), undefined); | 272 assert.equal(await page.evaluate(() => window.__hgWebXss), undefined); |
| 273 assert.equal( | |
| 274 await page.locator('.graph-container').evaluate( | |
| 275 element => getComputedStyle(element).backgroundImage, | |
| 276 ), | |
| 277 'none', | |
| 278 ); | |
| 237 await page.locator('.theme-toggle').click(); | 279 await page.locator('.theme-toggle').click(); |
| 238 }); | 280 }); |
| 239 | 281 |
| 240 await assertPageHasNoBrowserErrors(browser, '/directory', async page => { | 282 await assertPageHasNoBrowserErrors(browser, '/directory', async page => { |
| 241 await page.getByText('Repository Files').waitFor(); | 283 await page.getByText('Repository Files').waitFor(); |
| 266 await page.getByText('Fixture Repository').waitFor(); | 308 await page.getByText('Fixture Repository').waitFor(); |
| 267 await page.waitForTimeout(600); | 309 await page.waitForTimeout(600); |
| 268 assert.equal(await page.getByText('Documentation').count(), 0); | 310 assert.equal(await page.getByText('Documentation').count(), 0); |
| 269 assert.equal(delayedReadmeRequested, true); | 311 assert.equal(delayedReadmeRequested, true); |
| 270 | 312 |
| 313 await page.getByRole('link', { name: 'pixel.png' }).click(); | |
| 314 const image = page.locator('.static-file-image'); | |
| 315 await image.waitFor(); | |
| 316 await image.evaluate(element => { | |
| 317 const imageElement = element; | |
| 318 if (imageElement.complete) return; | |
| 319 return new Promise((resolve, reject) => { | |
| 320 imageElement.addEventListener('load', resolve, { once: true }); | |
| 321 imageElement.addEventListener('error', reject, { once: true }); | |
| 322 }); | |
| 323 }); | |
| 324 assert.equal(await image.evaluate(element => element.naturalWidth), 1); | |
| 325 await page.getByRole('dialog', { name: 'Preview pixel.png' }).waitFor(); | |
| 326 await page.keyboard.press('Escape'); | |
| 327 | |
| 271 await page.getByRole('link', { name: 'src' }).click(); | 328 await page.getByRole('link', { name: 'src' }).click(); |
| 272 await page.getByRole('link', { name: 'main.c' }).click(); | 329 await page.getByRole('link', { name: 'main.c' }).click(); |
| 273 await page.getByText('int main(void)').waitFor(); | 330 await page.getByText('int main(void)').waitFor(); |
| 274 await page.keyboard.press('Escape'); | 331 await page.keyboard.press('Escape'); |
| 275 }); | 332 }); |
| 296 await assertPageHasNoBrowserErrors(browser, `/changeset/${tipNode}`, async page => { | 353 await assertPageHasNoBrowserErrors(browser, `/changeset/${tipNode}`, async page => { |
| 297 await page.getByRole('heading', { name: 'Update fixture' }).waitFor(); | 354 await page.getByRole('heading', { name: 'Update fixture' }).waitFor(); |
| 298 await page.locator('.changeset-files code').filter({ hasText: 'new-file.txt' }).waitFor(); | 355 await page.locator('.changeset-files code').filter({ hasText: 'new-file.txt' }).waitFor(); |
| 299 await page.getByText('added', { exact: true }).waitFor(); | 356 await page.getByText('added', { exact: true }).waitFor(); |
| 300 await page.getByRole('region', { name: 'Changeset diff' }).waitFor(); | 357 await page.getByRole('region', { name: 'Changeset diff' }).waitFor(); |
| 358 assert.equal( | |
| 359 await page.locator('.changeset-paper').evaluate( | |
| 360 element => getComputedStyle(element).backgroundImage, | |
| 361 ), | |
| 362 'none', | |
| 363 ); | |
| 301 await page.getByRole('button', { name: firstNode.slice(0, 12) }).click(); | 364 await page.getByRole('button', { name: firstNode.slice(0, 12) }).click(); |
| 302 await page.waitForFunction( | 365 await page.waitForFunction( |
| 303 expectedPath => window.location.pathname === expectedPath, | 366 expectedPath => window.location.pathname === expectedPath, |
| 304 `/changeset/${firstNode}`, | 367 `/changeset/${firstNode}`, |
| 305 ); | 368 ); |