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