comparison mrjunejune/test/theme_and_webp_test.js @ 273:e02e2036ef84 default tip

add Layer 2 JRPG component system Add reusable content and window modals, an isolated component sandbox, shared cyberpunk scroll areas, production-safe cache freshness, and server-rendered JRPG panel state. Co-authored-by: Copilot <[email protected]>
author MrJuneJune <me@mrjunejune.com>
date Sat, 08 Aug 2026 02:08:08 -0700
parents 41a49c29a28f
children
comparison
equal deleted inserted replaced
272:41a49c29a28f 273:e02e2036ef84
489 }); 489 });
490 assert.equal(await submit.getAttribute('aria-label'), 'Send message'); 490 assert.equal(await submit.getAttribute('aria-label'), 'Send message');
491 await context.close(); 491 await context.close();
492 } 492 }
493 493
494 async function testModalSandbox(browser) {
495 const context = await browser.newContext({
496 serviceWorkers: 'block',
497 viewport: { width: 1280, height: 900 },
498 });
499 const page = await context.newPage();
500 const errors = [];
501 page.on('pageerror', error => errors.push(error.message));
502 page.on('console', message => {
503 if (message.type() === 'error') errors.push(message.text());
504 });
505 await page.goto(`${baseUrl}/public/component-sandbox.html`, {
506 waitUntil: 'networkidle',
507 });
508 await page.waitForFunction(() =>
509 customElements.get('mjj-content-modal') &&
510 customElements.get('mjj-window-modal') &&
511 [...document.querySelectorAll('mjj-content-modal, mjj-window-modal')]
512 .every(modal => modal.hasAttribute('data-ready'))
513 );
514 await page.evaluate(() => document.fonts.ready);
515 const modalCss = await (
516 await fetch(`${baseUrl}/public/mjj-modal.css`, { cache: 'no-store' })
517 ).text();
518 assert.match(modalCss, /clip-path:\s*inset\(49% 45%\)/);
519 assert.match(modalCss, /clip-path:\s*inset\(49% 0\)/);
520 assert.match(modalCss, /scrollbar-gutter:\s*stable/);
521
522 assert.equal(
523 await page.locator('mjj-content-modal[data-invalid], mjj-window-modal[data-invalid]')
524 .count(),
525 0,
526 );
527
528 await page.getByRole('button', { name: 'Open article' }).click();
529 const contentDialog = page.locator('mjj-content-modal dialog');
530 await contentDialog.waitFor({ state: 'visible' });
531 await contentDialog.evaluate(dialog =>
532 Promise.all(dialog.getAnimations().map(animation => animation.finished))
533 );
534 const contentMetrics = await contentDialog.evaluate(dialog => {
535 const body = dialog.querySelector(':scope > [data-modal-body]');
536 const children = [...dialog.children].map(child => {
537 if (child.hasAttribute('data-modal-header')) return 'header';
538 if (child.hasAttribute('data-modal-body')) return 'body';
539 if (child.hasAttribute('data-modal-footer')) return 'footer';
540 return child.localName;
541 });
542 const style = getComputedStyle(dialog);
543 const bodyStyle = getComputedStyle(body);
544 return {
545 bodyScrollable: ['auto', 'scroll'].includes(bodyStyle.overflowY),
546 borderRadius: style.borderRadius,
547 children,
548 fontFamily: style.fontFamily,
549 labelled: dialog.getAttribute('aria-labelledby') ===
550 dialog.querySelector('[data-modal-title]').id,
551 overflowing: body.scrollHeight > body.clientHeight,
552 shadow: style.boxShadow,
553 scrollPrimitive: body.localName,
554 scrollbarColor: bodyStyle.scrollbarColor,
555 scrollbarGutter: bodyStyle.scrollbarGutter,
556 width: dialog.getBoundingClientRect().width,
557 };
558 });
559 assert.deepEqual(contentMetrics.children, ['header', 'body', 'footer']);
560 assert.equal(contentMetrics.bodyScrollable, true);
561 assert.equal(contentMetrics.borderRadius, '0px');
562 assert.equal(contentMetrics.labelled, true);
563 assert.equal(contentMetrics.fontFamily, '"Pixel Mplus"');
564 assert.equal(contentMetrics.overflowing, true);
565 assert.equal(contentMetrics.scrollPrimitive, 'zen-scroll-area');
566 assert.notEqual(contentMetrics.scrollbarColor, 'auto');
567 assert.match(contentMetrics.scrollbarGutter, /stable/);
568 assert.notEqual(contentMetrics.shadow, 'none');
569 assert.ok(contentMetrics.width <= 864);
570 await page.getByRole('button', { name: 'Close article' }).click();
571 await contentDialog.waitFor({ state: 'hidden' });
572
573 await page.getByRole('button', { name: 'Open workspace' }).click();
574 const windowDialog = page.locator('mjj-window-modal dialog');
575 await windowDialog.waitFor({ state: 'visible' });
576 await windowDialog.evaluate(dialog =>
577 Promise.all(dialog.getAnimations().map(animation => animation.finished))
578 );
579 const windowMetrics = await windowDialog.evaluate(dialog => {
580 const body = dialog.querySelector(':scope > [data-modal-body]');
581 const bodyStyle = getComputedStyle(body);
582 const primary = dialog.querySelector('[data-modal-pane="primary"]');
583 return {
584 bodyColumns: bodyStyle.gridTemplateColumns,
585 bodyScrollable: ['auto', 'scroll'].includes(bodyStyle.overflowY),
586 borderRadius: getComputedStyle(dialog).borderRadius,
587 hasStrictRegions:
588 Boolean(dialog.querySelector(':scope > [data-modal-header]')) &&
589 Boolean(body) &&
590 Boolean(dialog.querySelector(':scope > [data-modal-footer]')),
591 height: dialog.getBoundingClientRect().height,
592 primaryShadow: getComputedStyle(primary).boxShadow,
593 primaryOverflowing: primary.scrollHeight > primary.clientHeight,
594 primaryScrollColor: getComputedStyle(primary).scrollbarColor,
595 scrollPrimitive: body.localName,
596 scrollPrimitives:
597 dialog.querySelectorAll('zen-scroll-area[data-cyber-scroll]').length,
598 scrollbarColor: bodyStyle.scrollbarColor,
599 scrollbarGutter: bodyStyle.scrollbarGutter,
600 shadow: getComputedStyle(dialog).boxShadow,
601 };
602 });
603 assert.equal(windowMetrics.bodyScrollable, true);
604 assert.equal(windowMetrics.borderRadius, '0px');
605 assert.equal(windowMetrics.hasStrictRegions, true);
606 assert.equal(windowMetrics.scrollPrimitive, 'zen-scroll-area');
607 assert.notEqual(windowMetrics.scrollbarColor, 'auto');
608 assert.match(windowMetrics.scrollbarGutter, /stable/);
609 assert.match(windowMetrics.bodyColumns, /\d/);
610 assert.ok(windowMetrics.height <= 768);
611 assert.notEqual(windowMetrics.primaryShadow, 'none');
612 assert.equal(windowMetrics.primaryOverflowing, true);
613 assert.notEqual(windowMetrics.primaryScrollColor, 'auto');
614 assert.equal(windowMetrics.scrollPrimitives, 3);
615 assert.notEqual(windowMetrics.shadow, 'none');
616 await page.getByRole('button', { name: 'Close workspace' }).click();
617 await windowDialog.waitFor({ state: 'hidden' });
618 assert.deepEqual(errors, []);
619 await context.close();
620
621 const mobileContext = await browser.newContext({
622 serviceWorkers: 'block',
623 viewport: { width: 390, height: 844 },
624 });
625 const mobilePage = await mobileContext.newPage();
626 await mobilePage.goto(`${baseUrl}/public/component-sandbox.html`, {
627 waitUntil: 'networkidle',
628 });
629 await mobilePage.evaluate(() => document.fonts.ready);
630 await mobilePage.getByRole('button', { name: 'Open workspace' }).click();
631 const mobileDialog = mobilePage.locator('mjj-window-modal dialog');
632 await mobileDialog.waitFor({ state: 'visible' });
633 await mobileDialog.evaluate(dialog =>
634 Promise.all(dialog.getAnimations().map(animation => animation.finished))
635 );
636 const mobileMetrics = await mobileDialog.evaluate(dialog => {
637 const body = dialog.querySelector('[data-modal-body]');
638 const dialogBox = dialog.getBoundingClientRect();
639 return {
640 bodyColumns: getComputedStyle(body).gridTemplateColumns,
641 fits:
642 dialogBox.left >= 0 &&
643 dialogBox.top >= 0 &&
644 dialogBox.right <= innerWidth &&
645 dialogBox.bottom <= innerHeight,
646 pageFits: document.documentElement.scrollWidth <= innerWidth,
647 };
648 });
649 assert.equal(mobileMetrics.fits, true);
650 assert.equal(mobileMetrics.pageFits, true);
651 assert.doesNotMatch(mobileMetrics.bodyColumns, /\s/);
652 assert.equal(
653 await mobilePage.locator('mjj-window-modal').getAttribute(
654 'data-component-layer',
655 ),
656 '2',
657 );
658 await mobileContext.close();
659 }
660
494 async function testJrpgPage(browser) { 661 async function testJrpgPage(browser) {
495 const context = await browser.newContext({ 662 const context = await browser.newContext({
496 colorScheme: 'light', 663 colorScheme: 'light',
497 viewport: { width: 1280, height: 900 }, 664 viewport: { width: 1280, height: 900 },
498 }); 665 });
545 if (response.status() >= 400) { 712 if (response.status() >= 400) {
546 errors.push(`response: ${response.status()} ${response.url()}`); 713 errors.push(`response: ${response.status()} ${response.url()}`);
547 } 714 }
548 }); 715 });
549 716
717 for (const expected of [
718 { panel: 'resume', title: 'Resume' },
719 { panel: 'tools', title: 'Tools' },
720 { panel: 'blog', title: 'Blogs' },
721 ]) {
722 const html = await (
723 await fetch(`${baseUrl}/jrpg?panel=${expected.panel}`)
724 ).text();
725 assert.match(
726 html,
727 new RegExp(`data-initial-panel="${expected.panel}"`),
728 );
729 assert.match(html, new RegExp(`data-preview-title>${expected.title}<`));
730 assert.doesNotMatch(html, /__MJJ_[A-Z_]+__/);
731 }
732 const conversationHtml = await (
733 await fetch(`${baseUrl}/jrpg?panel=conversations`)
734 ).text();
735 assert.match(conversationHtml, /data-initial-panel="conversations"/);
736 assert.match(
737 conversationHtml,
738 /<mjj-conversation-archive\s+aria-label="Conversations"/,
739 );
740
550 await page.goto(`${baseUrl}/jrpg`, { waitUntil: 'networkidle' }); 741 await page.goto(`${baseUrl}/jrpg`, { waitUntil: 'networkidle' });
551 await page.waitForFunction(() => 742 await page.waitForFunction(() =>
552 customElements.get('mjj-composer') && 743 customElements.get('mjj-composer') &&
553 customElements.get('mjj-jrpg-menu') && 744 customElements.get('mjj-jrpg-menu') &&
554 customElements.get('mjj-jrpg-chat') && 745 customElements.get('mjj-jrpg-chat') &&
555 customElements.get('mjj-jrpg-preview') 746 customElements.get('mjj-jrpg-preview')
747 );
748 await page.waitForFunction(() =>
749 [...document.querySelectorAll('.jrpg-workspace dialog')].every(dialog => {
750 const owner = dialog.closest('mjj-content-modal, mjj-window-modal');
751 return owner?.hasAttribute('data-ready');
752 })
753 );
754 assert.deepEqual(
755 await page.evaluate(() =>
756 [...document.querySelectorAll('.jrpg-workspace dialog')]
757 .map(dialog =>
758 dialog.closest('mjj-content-modal, mjj-window-modal')?.localName)),
759 [
760 'mjj-window-modal',
761 'mjj-content-modal',
762 'mjj-content-modal',
763 'mjj-window-modal',
764 'mjj-window-modal',
765 ],
766 'every JRPG dialog has a Layer 2 owner',
556 ); 767 );
557 await page.evaluate(() => document.fonts.ready); 768 await page.evaluate(() => document.fonts.ready);
558 const desktop = await page.evaluate(() => { 769 const desktop = await page.evaluate(() => {
559 const scene = document.querySelector('.jrpg-scene').getBoundingClientRect(); 770 const scene = document.querySelector('.jrpg-scene').getBoundingClientRect();
560 const utility = document.querySelector('.jrpg-utility').getBoundingClientRect(); 771 const utility = document.querySelector('.jrpg-utility').getBoundingClientRect();
1065 // Conversations menu button should be pressed 1276 // Conversations menu button should be pressed
1066 assert.equal( 1277 assert.equal(
1067 await page.locator('button[data-preview="conversations"]').getAttribute('aria-pressed'), 1278 await page.locator('button[data-preview="conversations"]').getAttribute('aria-pressed'),
1068 'true', 1279 'true',
1069 ); 1280 );
1281 await page.evaluate(() => {
1282 document.querySelector('mjj-conversation-archive')?.addConversation({
1283 id: 'eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee',
1284 title: 'Modal migration fixture',
1285 turn_count: 2,
1286 });
1287 });
1288 await page.getByRole('button', {
1289 name: 'Rename "Modal migration fixture"',
1290 }).click();
1291 await page.locator('[data-archive-rename-dialog]').waitFor({ state: 'visible' });
1292 assert.equal(
1293 await page.locator('[data-archive-rename-dialog]').evaluate(dialog =>
1294 dialog.closest('mjj-window-modal')?.localName),
1295 'mjj-window-modal',
1296 );
1297 await page.locator('[data-archive-rename-cancel]').click();
1298 await page.locator('[data-archive-rename-dialog]').waitFor({ state: 'hidden' });
1299
1300 await page.getByRole('button', {
1301 name: 'Delete "Modal migration fixture"',
1302 }).click();
1303 await page.locator('[data-archive-delete-dialog]').waitFor({ state: 'visible' });
1304 assert.equal(
1305 await page.locator('[data-archive-delete-dialog]').evaluate(dialog =>
1306 dialog.closest('mjj-content-modal')?.localName),
1307 'mjj-content-modal',
1308 );
1309 await page.locator('[data-archive-delete-cancel]').click();
1310 await page.locator('[data-archive-delete-dialog]').waitFor({ state: 'hidden' });
1070 1311
1071 // Archive: close via close button, navigates back to resume panel 1312 // Archive: close via close button, navigates back to resume panel
1072 await page.locator('[data-archive-close]').click(); 1313 await page.locator('[data-archive-close]').click();
1073 await page.locator('.jrpg-utility mjj-conversation-archive').waitFor({ state: 'hidden' }); 1314 await page.locator('.jrpg-utility mjj-conversation-archive').waitFor({ state: 'hidden' });
1074 // mjj-jrpg-chat must remain visible (always in scene) 1315 // mjj-jrpg-chat must remain visible (always in scene)
1126 await page.getByRole('button', { name: 'Tools', exact: true }).click(); 1367 await page.getByRole('button', { name: 'Tools', exact: true }).click();
1127 assert.equal( 1368 assert.equal(
1128 await page.locator('[data-preview-title]').textContent(), 1369 await page.locator('[data-preview-title]').textContent(),
1129 'Tools', 1370 'Tools',
1130 ); 1371 );
1131 assert.equal( 1372 assert.equal(await page.locator('[data-preview-link]').count(), 0);
1132 await page.locator('[data-preview-link]').getAttribute('href'),
1133 '/tools',
1134 );
1135 assert.equal( 1373 assert.equal(
1136 await page.getByRole('button', { name: 'Tools', exact: true }) 1374 await page.getByRole('button', { name: 'Tools', exact: true })
1137 .getAttribute('aria-pressed'), 1375 .getAttribute('aria-pressed'),
1138 'true', 1376 'true',
1139 ); 1377 );
1149 /Personal Notes/, 1387 /Personal Notes/,
1150 ); 1388 );
1151 await page.locator('[data-latest-tool]').first().click(); 1389 await page.locator('[data-latest-tool]').first().click();
1152 await page.locator('[data-functional-tool="markdown"]').waitFor(); 1390 await page.locator('[data-functional-tool="markdown"]').waitFor();
1153 assert.equal( 1391 assert.equal(
1392 await page.locator('.jrpg-detail-dialog').evaluate(dialog =>
1393 dialog.closest('mjj-content-modal, mjj-window-modal')?.localName),
1394 'mjj-window-modal',
1395 'tool details use the window modal',
1396 );
1397 assert.equal(
1154 await page.locator('[data-dialog-title]').textContent(), 1398 await page.locator('[data-dialog-title]').textContent(),
1155 'MarkDown to HTML', 1399 'MarkDown to HTML',
1156 );
1157 assert.equal(
1158 await page.locator('[data-preview-link]').getAttribute('href'),
1159 '/tools/markdown_to_html',
1160 ); 1400 );
1161 await page.locator('[data-markdown-source]').fill('# Modal Markdown'); 1401 await page.locator('[data-markdown-source]').fill('# Modal Markdown');
1162 await page.waitForFunction(() => 1402 await page.waitForFunction(() =>
1163 document.querySelector('[data-markdown-output] h1')?.textContent === 1403 document.querySelector('[data-markdown-output] h1')?.textContent ===
1164 'Modal Markdown' 1404 'Modal Markdown'
1171 const dialog = dialogElement.getBoundingClientRect(); 1411 const dialog = dialogElement.getBoundingClientRect();
1172 const windows = [...document.querySelectorAll('.jrpg-tool-window')] 1412 const windows = [...document.querySelectorAll('.jrpg-tool-window')]
1173 .map(window => window.getBoundingClientRect()); 1413 .map(window => window.getBoundingClientRect());
1174 const windowShadows = [...document.querySelectorAll('.jrpg-tool-window')] 1414 const windowShadows = [...document.querySelectorAll('.jrpg-tool-window')]
1175 .map(window => getComputedStyle(window).boxShadow); 1415 .map(window => getComputedStyle(window).boxShadow);
1176 const rail = getComputedStyle(
1177 document.querySelector('.jrpg-tool-panes'),
1178 '::before',
1179 );
1180 return { 1416 return {
1181 dialogHeight: dialogElement.offsetHeight, 1417 dialogHeight: dialogElement.offsetHeight,
1182 dialogWidth: dialogElement.offsetWidth, 1418 dialogWidth: dialogElement.offsetWidth,
1183 fillsViewport: dialogElement.offsetWidth > innerWidth * 0.9 && 1419 compact: dialogElement.offsetWidth <= innerWidth * 0.9 &&
1184 dialogElement.offsetHeight > innerHeight * 0.9, 1420 dialogElement.offsetHeight <= innerHeight * 0.8,
1185 viewportHeight: innerHeight, 1421 viewportHeight: innerHeight,
1186 viewportWidth: innerWidth, 1422 viewportWidth: innerWidth,
1187 sideBySide: windows.length === 2 && 1423 stacked: windows.length === 2 &&
1188 windows[0].right < windows[1].left && 1424 windows[0].bottom < windows[1].top &&
1189 Math.abs(windows[0].top - windows[1].top) < 1, 1425 Math.abs(windows[0].left - windows[1].left) < 1,
1190 fullPageInHeader: Boolean( 1426 noOpenPageAction: !document.querySelector('[data-preview-link]'),
1191 document.querySelector('[data-preview-link]') 1427 noFooter: !document.querySelector('[data-dialog-actions]'),
1192 .closest('.jrpg-dialog-heading-actions'),
1193 ),
1194 footerHidden: document.querySelector('[data-dialog-actions]').hidden,
1195 metalRail: rail.content !== 'none' &&
1196 rail.backgroundImage.includes('bar-ink.webp'),
1197 pixelIcons: document.querySelectorAll('.jrpg-tool-window zen-icon').length >= 2 && 1428 pixelIcons: document.querySelectorAll('.jrpg-tool-window zen-icon').length >= 2 &&
1198 [...document.querySelectorAll('.jrpg-tool-window zen-icon svg')] 1429 [...document.querySelectorAll('.jrpg-tool-window zen-icon svg')]
1199 .every(icon => 1430 .every(icon =>
1200 getComputedStyle(icon).shapeRendering.toLowerCase() === 'crispedges' 1431 getComputedStyle(icon).shapeRendering.toLowerCase() === 'crispedges'
1201 ), 1432 ),
1202 separateGlows: windowShadows.length === 2 && 1433 separateGlows: windowShadows.length === 2 &&
1203 windowShadows.every(shadow => shadow !== 'none'), 1434 windowShadows.every(shadow => shadow !== 'none'),
1204 wideWindows: windows.every(window => window.width > dialog.width * 0.35), 1435 wideWindows: windows.every(window => window.width > dialog.width * 0.8),
1205 }; 1436 };
1206 }); 1437 });
1207 assert.equal( 1438 assert.equal(toolWorkspace.compact, true, JSON.stringify(toolWorkspace));
1208 toolWorkspace.fillsViewport, 1439 assert.equal(toolWorkspace.stacked, true);
1209 true, 1440 assert.equal(toolWorkspace.noOpenPageAction, true);
1210 JSON.stringify(toolWorkspace), 1441 assert.equal(toolWorkspace.noFooter, true);
1211 );
1212 assert.equal(toolWorkspace.sideBySide, true);
1213 assert.equal(toolWorkspace.fullPageInHeader, true);
1214 assert.equal(toolWorkspace.footerHidden, true);
1215 assert.equal(toolWorkspace.metalRail, true);
1216 assert.equal(toolWorkspace.pixelIcons, true); 1442 assert.equal(toolWorkspace.pixelIcons, true);
1217 assert.equal(toolWorkspace.separateGlows, true); 1443 assert.equal(toolWorkspace.separateGlows, true);
1218 assert.equal(toolWorkspace.wideWindows, true); 1444 assert.equal(toolWorkspace.wideWindows, true);
1445 await page.locator('.jrpg-detail-dialog').evaluate(dialog =>
1446 Promise.all(dialog.getAnimations().map(animation => animation.finished))
1447 );
1219 await page.getByRole('button', { 1448 await page.getByRole('button', {
1220 name: 'Close destination details', 1449 name: 'Close destination details',
1221 }).click(); 1450 }).click();
1222 await page.locator('.jrpg-preview-dialog dialog').waitFor({ state: 'hidden' }); 1451 await page.locator('.jrpg-preview-dialog dialog').waitFor({ state: 'hidden' });
1223 1452
1281 assert.equal((await lateCleanupResponse).status(), 204); 1510 assert.equal((await lateCleanupResponse).status(), 204);
1282 await page.locator('.jrpg-preview-dialog dialog').waitFor({ state: 'hidden' }); 1511 await page.locator('.jrpg-preview-dialog dialog').waitFor({ state: 'hidden' });
1283 1512
1284 await page.locator('[data-latest-tool]').nth(2).click(); 1513 await page.locator('[data-latest-tool]').nth(2).click();
1285 await page.locator('[data-functional-tool="hls"]').waitFor(); 1514 await page.locator('[data-functional-tool="hls"]').waitFor();
1286 assert.equal(
1287 await page.locator('[data-preview-link]').getAttribute('href'),
1288 '/tools/hls_player',
1289 );
1290 await page.locator('[data-hls-sample]').click(); 1515 await page.locator('[data-hls-sample]').click();
1291 await page.waitForFunction(() => 1516 await page.waitForFunction(() =>
1292 ['ready', 'error'].includes( 1517 ['ready', 'error'].includes(
1293 document.querySelector('[data-hls-status]')?.dataset.state, 1518 document.querySelector('[data-hls-status]')?.dataset.state,
1294 ) 1519 )
1334 ); 1559 );
1335 assert.match( 1560 assert.match(
1336 await page.locator('[data-tool-content]').textContent(), 1561 await page.locator('[data-tool-content]').textContent(),
1337 /private browser-based writing workspace/, 1562 /private browser-based writing workspace/,
1338 ); 1563 );
1339 assert.equal(
1340 await page.locator('[data-preview-link]').getAttribute('href'),
1341 '/notes',
1342 );
1343 await page.getByRole('button', { 1564 await page.getByRole('button', {
1344 name: 'Close destination details', 1565 name: 'Close destination details',
1345 }).click(); 1566 }).click();
1346 await page.locator('.jrpg-preview-dialog dialog').waitFor({ state: 'hidden' }); 1567 await page.locator('.jrpg-preview-dialog dialog').waitFor({ state: 'hidden' });
1347 1568
1348 await page.getByRole('button', { name: 'Resume', exact: true }).click(); 1569 await page.getByRole('button', { name: 'Resume', exact: true }).click();
1349 await page.locator('[data-resume-modal]').click(); 1570 await page.locator('[data-resume-modal]').click();
1571 assert.equal(
1572 await page.locator('.jrpg-detail-dialog').evaluate(dialog =>
1573 dialog.closest('mjj-content-modal, mjj-window-modal')?.localName),
1574 'mjj-content-modal',
1575 'resume details use the content modal',
1576 );
1350 await page.locator('[data-resume-content] .sub-header').first().waitFor(); 1577 await page.locator('[data-resume-content] .sub-header').first().waitFor();
1351 const resumeDossier = await page.locator('[data-resume-dossier]').evaluate( 1578 const resumeDossier = await page.locator('[data-resume-dossier]').evaluate(
1352 dossier => ({ 1579 dossier => ({
1353 fontFamily: getComputedStyle(dossier).fontFamily, 1580 fontFamily: getComputedStyle(dossier).fontFamily,
1354 text: dossier.textContent, 1581 text: dossier.textContent,
1356 ); 1583 );
1357 assert.equal(resumeDossier.fontFamily, '"Pixel Mplus"'); 1584 assert.equal(resumeDossier.fontFamily, '"Pixel Mplus"');
1358 assert.match(resumeDossier.text, /Copilot Tasks/); 1585 assert.match(resumeDossier.text, /Copilot Tasks/);
1359 assert.match(resumeDossier.text, /Microsoft/); 1586 assert.match(resumeDossier.text, /Microsoft/);
1360 assert.match(resumeDossier.text, /Meta/); 1587 assert.match(resumeDossier.text, /Meta/);
1588 const resumeModalGeometry = await page.locator('.jrpg-detail-dialog').evaluate(
1589 dialog => {
1590 const body = dialog.querySelector('[data-modal-body]').getBoundingClientRect();
1591 const dossier = dialog.querySelector('[data-resume-dossier]')
1592 .getBoundingClientRect();
1593 const buttons = [...dialog.querySelectorAll(
1594 '[data-modal-header] zen-button > :is(button, a)',
1595 )].map(control => control.getBoundingClientRect());
1596 return {
1597 bodyFilled:
1598 dossier.width / body.width > 0.9 &&
1599 Math.abs(dossier.left - body.left) < 32,
1600 equalHeaderControls:
1601 buttons.length === 2 &&
1602 Math.abs(buttons[0].height - buttons[1].height) < 1,
1603 headerControlHeights: buttons.map(button => button.height),
1604 footerAbsent: !dialog.querySelector('[data-modal-footer]'),
1605 headerPdfLinks: dialog.querySelectorAll(
1606 '[data-modal-header] a[href="/public/resume.pdf"]',
1607 ).length,
1608 visibleBodyPdfLinks: [...dialog.querySelectorAll(
1609 '[data-modal-body] a[href="/public/resume.pdf"]',
1610 )].filter(link => getComputedStyle(link).display !== 'none').length,
1611 pdfLinks: dialog.querySelectorAll('a[href="/public/resume.pdf"]').length,
1612 };
1613 },
1614 );
1615 assert.equal(resumeModalGeometry.bodyFilled, true);
1616 assert.equal(
1617 resumeModalGeometry.equalHeaderControls,
1618 true,
1619 JSON.stringify(resumeModalGeometry),
1620 );
1621 assert.equal(resumeModalGeometry.footerAbsent, true);
1622 assert.equal(resumeModalGeometry.headerPdfLinks, 1);
1623 assert.equal(resumeModalGeometry.visibleBodyPdfLinks, 0);
1624 assert.equal(resumeModalGeometry.pdfLinks, 1);
1625 await page.locator('.jrpg-detail-dialog').evaluate(dialog =>
1626 Promise.all(dialog.getAnimations().map(animation => animation.finished))
1627 );
1361 assert.equal( 1628 assert.equal(
1362 await page.locator('[data-resume-content] a[target="_blank"]').count() > 0, 1629 await page.locator('[data-resume-content] a[target="_blank"]').count() > 0,
1363 true, 1630 true,
1364 ); 1631 );
1365 await page.getByRole('button', { 1632 await page.getByRole('button', {
1384 assert.equal( 1651 assert.equal(
1385 await page.locator('[data-blog-content] h1').textContent(), 1652 await page.locator('[data-blog-content] h1').textContent(),
1386 'WebSocket Demystified', 1653 'WebSocket Demystified',
1387 ); 1654 );
1388 assert.equal( 1655 assert.equal(
1389 await page.locator('[data-preview-link]').getAttribute('href'),
1390 '/blog/websocket-demystified',
1391 );
1392 assert.equal(
1393 await page.locator('[data-blog-content]').evaluate( 1656 await page.locator('[data-blog-content]').evaluate(
1394 article => getComputedStyle(article).fontFamily, 1657 article => getComputedStyle(article).fontFamily,
1395 ), 1658 ),
1396 '"Pixel Mplus"', 1659 '"Pixel Mplus"',
1660 );
1661 const blogWidth = await page.locator('.jrpg-detail-dialog').evaluate(dialog => {
1662 const body = dialog.querySelector('[data-modal-body]').getBoundingClientRect();
1663 const browser = dialog.querySelector('[data-blog-browser]')
1664 .getBoundingClientRect();
1665 return browser.width / body.width;
1666 });
1667 assert.ok(blogWidth > 0.9);
1668 await page.locator('.jrpg-detail-dialog').evaluate(dialog =>
1669 Promise.all(dialog.getAnimations().map(animation => animation.finished))
1397 ); 1670 );
1398 await page.getByRole('button', { 1671 await page.getByRole('button', {
1399 name: 'Close destination details', 1672 name: 'Close destination details',
1400 }).click(); 1673 }).click();
1401 await page.locator('.jrpg-preview-dialog dialog').waitFor({ state: 'hidden' }); 1674 await page.locator('.jrpg-preview-dialog dialog').waitFor({ state: 'hidden' });
1407 assert.equal(await page.locator('[data-blog-entry]').count(), 9); 1680 assert.equal(await page.locator('[data-blog-entry]').count(), 9);
1408 await page.locator('[data-blog-entry]').nth(1).click(); 1681 await page.locator('[data-blog-entry]').nth(1).click();
1409 await page.waitForFunction(() => 1682 await page.waitForFunction(() =>
1410 document.querySelector('[data-blog-content] h1')?.textContent === 1683 document.querySelector('[data-blog-content] h1')?.textContent ===
1411 'Creating Network Library in C' 1684 'Creating Network Library in C'
1412 );
1413 assert.equal(
1414 await page.locator('[data-preview-link]').getAttribute('href'),
1415 '/blog/my-seobeo-journey',
1416 ); 1685 );
1417 await page.getByRole('button', { 1686 await page.getByRole('button', {
1418 name: 'Close destination details', 1687 name: 'Close destination details',
1419 }).click(); 1688 }).click();
1420 1689
1687 mediaMatches: matchMedia( 1956 mediaMatches: matchMedia(
1688 '(max-width: 52rem) and (orientation: portrait)', 1957 '(max-width: 52rem) and (orientation: portrait)',
1689 ).matches, 1958 ).matches,
1690 menuInModal: menuElement.parentElement === destinationContent, 1959 menuInModal: menuElement.parentElement === destinationContent,
1691 menuHidden: menu.width === 0 && menu.height === 0, 1960 menuHidden: menu.width === 0 && menu.height === 0,
1961 modalOwner: destinationContent.closest('mjj-window-modal')?.localName,
1692 mobileArtIsCorrect, 1962 mobileArtIsCorrect,
1693 noOverflow: document.documentElement.scrollWidth <= innerWidth, 1963 noOverflow: document.documentElement.scrollWidth <= innerWidth,
1694 sendButtonCompact: 1964 sendButtonCompact:
1695 Math.abs(sendButton.width - sendButton.height) < 1 && 1965 Math.abs(sendButton.width - sendButton.height) < 1 &&
1696 sendButton.width <= 44 && 1966 sendButton.width <= 44 &&
1718 topLeftClear: document.elementFromPoint(10, 10)?.classList 1988 topLeftClear: document.elementFromPoint(10, 10)?.classList
1719 .contains('jrpg-workspace'), 1989 .contains('jrpg-workspace'),
1720 utilityHidden: utility.width === 0 && utility.height === 0, 1990 utilityHidden: utility.width === 0 && utility.height === 0,
1721 utilityInModal: document.querySelector('.jrpg-utility').parentElement === 1991 utilityInModal: document.querySelector('.jrpg-utility').parentElement ===
1722 destinationContent, 1992 destinationContent,
1993 usesCyberScroll:
1994 destinationContent.localName === 'zen-scroll-area' &&
1995 destinationContent.hasAttribute('data-cyber-scroll') &&
1996 getComputedStyle(destinationContent).scrollbarColor !== 'auto',
1723 usesDesignSystemControls: 1997 usesDesignSystemControls:
1724 !composerField.hasAttribute('appearance') && 1998 !composerField.hasAttribute('appearance') &&
1725 !sendOwner.hasAttribute('appearance'), 1999 !sendOwner.hasAttribute('appearance'),
1726 workspaceFills: Math.abs(workspace.left) < 1 && 2000 workspaceFills: Math.abs(workspace.left) < 1 &&
1727 Math.abs(workspace.top) < 1 && 2001 Math.abs(workspace.top) < 1 &&
1760 true, 2034 true,
1761 `turn navigation is centered and level: ${JSON.stringify(mobile.turnNavigationGeometry)}`, 2035 `turn navigation is centered and level: ${JSON.stringify(mobile.turnNavigationGeometry)}`,
1762 ); 2036 );
1763 assert.equal(mobile.utilityHidden, true, 'destination view stays out of the frame'); 2037 assert.equal(mobile.utilityHidden, true, 'destination view stays out of the frame');
1764 assert.equal(mobile.utilityInModal, true, 'destination view is hosted by the mobile modal'); 2038 assert.equal(mobile.utilityInModal, true, 'destination view is hosted by the mobile modal');
2039 assert.equal(mobile.modalOwner, 'mjj-window-modal');
2040 assert.equal(mobile.usesCyberScroll, true, 'JRPG modal uses the shared cyber scroll area');
1765 assert.equal(mobile.usesDesignSystemControls, true, 'composer uses styled Zenbu controls'); 2041 assert.equal(mobile.usesDesignSystemControls, true, 'composer uses styled Zenbu controls');
1766 assert.equal(mobile.workspaceFills, true, 'workspace fills full viewport'); 2042 assert.equal(mobile.workspaceFills, true, 'workspace fills full viewport');
1767 assert.equal(mobile.topLeftClear, true, 'hidden dialog triggers do not cover frame art'); 2043 assert.equal(mobile.topLeftClear, true, 'hidden dialog triggers do not cover frame art');
1768 2044
1769 // The top-right trigger opens the modal menu. 2045 // The top-right trigger opens the modal menu.
1770 await mobilePage.locator('[data-mobile-menu-toggle]').click(); 2046 await mobilePage.locator('[data-mobile-menu-toggle]').click();
1771 await mobilePage.locator('[data-mobile-destination-dialog]').waitFor({ 2047 await mobilePage.locator('[data-mobile-destination-dialog]').waitFor({
1772 state: 'visible', 2048 state: 'visible',
1773 }); 2049 });
2050 await mobilePage.locator('[data-mobile-destination-dialog]').evaluate(dialog =>
2051 Promise.all(dialog.getAnimations().map(animation => animation.finished))
2052 );
1774 assert.equal( 2053 assert.equal(
1775 await mobilePage.locator('[data-mobile-menu-toggle]') 2054 await mobilePage.locator('[data-mobile-menu-toggle]')
1776 .getAttribute('aria-expanded'), 2055 .getAttribute('aria-expanded'),
1777 'true', 2056 'true',
1778 'menu trigger reflects the open modal', 2057 'menu trigger reflects the open modal',
2808 await fetch(`${baseUrl}/sw.js`) 3087 await fetch(`${baseUrl}/sw.js`)
2809 ).text(); 3088 ).text();
2810 for (const source of [home, dogGame, manifest]) { 3089 for (const source of [home, dogGame, manifest]) {
2811 assert.doesNotMatch(source, /\.png(?:["')]|$)/i); 3090 assert.doesNotMatch(source, /\.png(?:["')]|$)/i);
2812 } 3091 }
2813 assert.match(serviceWorker, /v35-role-greeting/); 3092 assert.match(serviceWorker, /v37-network-first/);
2814 assert.match( 3093 assert.match(
2815 await ( 3094 await (
2816 await fetch(`${baseUrl}/public/pwa-register.js`) 3095 await fetch(`${baseUrl}/public/pwa-register.js`)
2817 ).text(), 3096 ).text(),
2818 /register\('\/sw\.js', \{ scope: '\/' \}\)/, 3097 /register\('\/sw\.js', \{ scope: '\/' \}\)/,
2819 ); 3098 );
2820 assert.ok(serviceWorker.includes("startsWith('/tools/hls_player')")); 3099 assert.ok(serviceWorker.includes("startsWith('/tools/hls_player')"));
2821 assert.ok(serviceWorker.includes("url.hostname === 'localhost'")); 3100 assert.ok(serviceWorker.includes("url.hostname === 'localhost'"));
2822 assert.ok(serviceWorker.includes("url.hostname === '127.0.0.1'")); 3101 assert.ok(serviceWorker.includes("url.hostname === '127.0.0.1'"));
3102 assert.ok(serviceWorker.includes("DEVELOPMENT_SANDBOX_PATHS"));
3103 assert.ok(serviceWorker.includes("'/public/component-sandbox'"));
3104 assert.ok(serviceWorker.includes("'/public/mjj-modal.'"));
3105 assert.ok(serviceWorker.includes('networkFirst(request, acceptsHtml)'));
3106 assert.ok(serviceWorker.includes('staleWhileRevalidate(event, request)'));
3107 assert.ok(serviceWorker.includes("fetch(request, { cache: 'no-cache' })"));
3108 assert.doesNotMatch(
3109 await (
3110 await fetch(`${baseUrl}/public/pwa-register.js`)
3111 ).text(),
3112 /confirm\(/,
3113 );
2823 assert.ok(!serviceWorker.includes("startsWith('/jrpg')")); 3114 assert.ok(!serviceWorker.includes("startsWith('/jrpg')"));
2824 3115
2825 for (const asset of [ 3116 for (const asset of [
2826 'sprite_shiba0.webp', 3117 'sprite_shiba0.webp',
2827 'dog-treat.webp', 3118 'dog-treat.webp',
2875 await testPlainField(browser); 3166 await testPlainField(browser);
2876 await testButtonScale(browser); 3167 await testButtonScale(browser);
2877 await testDynamicButtonOwnership(browser); 3168 await testDynamicButtonOwnership(browser);
2878 await testResumePrint(browser); 3169 await testResumePrint(browser);
2879 await testComposerLab(browser); 3170 await testComposerLab(browser);
3171 await testModalSandbox(browser);
2880 } 3172 }
2881 if (runsSuite('jrpg')) await testJrpgPage(browser); 3173 if (runsSuite('jrpg')) await testJrpgPage(browser);
2882 if (runsSuite('routing')) await testConversationRouting(browser); 3174 if (runsSuite('routing')) await testConversationRouting(browser);
2883 if (runsSuite('greeting')) await testRoleGreetings(browser); 3175 if (runsSuite('greeting')) await testRoleGreetings(browser);
2884 if (runsSuite('hls')) await testHlsPlayer(browser, siteRoot); 3176 if (runsSuite('hls')) await testHlsPlayer(browser, siteRoot);