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 );