Mercurial
diff 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 |
line wrap: on
line diff
--- a/mrjunejune/test/theme_and_webp_test.js Fri Aug 07 16:05:29 2026 -0700 +++ b/mrjunejune/test/theme_and_webp_test.js Sat Aug 08 02:08:08 2026 -0700 @@ -491,6 +491,173 @@ await context.close(); } +async function testModalSandbox(browser) { + const context = await browser.newContext({ + serviceWorkers: 'block', + viewport: { width: 1280, height: 900 }, + }); + const page = await context.newPage(); + const errors = []; + page.on('pageerror', error => errors.push(error.message)); + page.on('console', message => { + if (message.type() === 'error') errors.push(message.text()); + }); + await page.goto(`${baseUrl}/public/component-sandbox.html`, { + waitUntil: 'networkidle', + }); + await page.waitForFunction(() => + customElements.get('mjj-content-modal') && + customElements.get('mjj-window-modal') && + [...document.querySelectorAll('mjj-content-modal, mjj-window-modal')] + .every(modal => modal.hasAttribute('data-ready')) + ); + await page.evaluate(() => document.fonts.ready); + const modalCss = await ( + await fetch(`${baseUrl}/public/mjj-modal.css`, { cache: 'no-store' }) + ).text(); + assert.match(modalCss, /clip-path:\s*inset\(49% 45%\)/); + assert.match(modalCss, /clip-path:\s*inset\(49% 0\)/); + assert.match(modalCss, /scrollbar-gutter:\s*stable/); + + assert.equal( + await page.locator('mjj-content-modal[data-invalid], mjj-window-modal[data-invalid]') + .count(), + 0, + ); + + await page.getByRole('button', { name: 'Open article' }).click(); + const contentDialog = page.locator('mjj-content-modal dialog'); + await contentDialog.waitFor({ state: 'visible' }); + await contentDialog.evaluate(dialog => + Promise.all(dialog.getAnimations().map(animation => animation.finished)) + ); + const contentMetrics = await contentDialog.evaluate(dialog => { + const body = dialog.querySelector(':scope > [data-modal-body]'); + const children = [...dialog.children].map(child => { + if (child.hasAttribute('data-modal-header')) return 'header'; + if (child.hasAttribute('data-modal-body')) return 'body'; + if (child.hasAttribute('data-modal-footer')) return 'footer'; + return child.localName; + }); + const style = getComputedStyle(dialog); + const bodyStyle = getComputedStyle(body); + return { + bodyScrollable: ['auto', 'scroll'].includes(bodyStyle.overflowY), + borderRadius: style.borderRadius, + children, + fontFamily: style.fontFamily, + labelled: dialog.getAttribute('aria-labelledby') === + dialog.querySelector('[data-modal-title]').id, + overflowing: body.scrollHeight > body.clientHeight, + shadow: style.boxShadow, + scrollPrimitive: body.localName, + scrollbarColor: bodyStyle.scrollbarColor, + scrollbarGutter: bodyStyle.scrollbarGutter, + width: dialog.getBoundingClientRect().width, + }; + }); + assert.deepEqual(contentMetrics.children, ['header', 'body', 'footer']); + assert.equal(contentMetrics.bodyScrollable, true); + assert.equal(contentMetrics.borderRadius, '0px'); + assert.equal(contentMetrics.labelled, true); + assert.equal(contentMetrics.fontFamily, '"Pixel Mplus"'); + assert.equal(contentMetrics.overflowing, true); + assert.equal(contentMetrics.scrollPrimitive, 'zen-scroll-area'); + assert.notEqual(contentMetrics.scrollbarColor, 'auto'); + assert.match(contentMetrics.scrollbarGutter, /stable/); + assert.notEqual(contentMetrics.shadow, 'none'); + assert.ok(contentMetrics.width <= 864); + await page.getByRole('button', { name: 'Close article' }).click(); + await contentDialog.waitFor({ state: 'hidden' }); + + await page.getByRole('button', { name: 'Open workspace' }).click(); + const windowDialog = page.locator('mjj-window-modal dialog'); + await windowDialog.waitFor({ state: 'visible' }); + await windowDialog.evaluate(dialog => + Promise.all(dialog.getAnimations().map(animation => animation.finished)) + ); + const windowMetrics = await windowDialog.evaluate(dialog => { + const body = dialog.querySelector(':scope > [data-modal-body]'); + const bodyStyle = getComputedStyle(body); + const primary = dialog.querySelector('[data-modal-pane="primary"]'); + return { + bodyColumns: bodyStyle.gridTemplateColumns, + bodyScrollable: ['auto', 'scroll'].includes(bodyStyle.overflowY), + borderRadius: getComputedStyle(dialog).borderRadius, + hasStrictRegions: + Boolean(dialog.querySelector(':scope > [data-modal-header]')) && + Boolean(body) && + Boolean(dialog.querySelector(':scope > [data-modal-footer]')), + height: dialog.getBoundingClientRect().height, + primaryShadow: getComputedStyle(primary).boxShadow, + primaryOverflowing: primary.scrollHeight > primary.clientHeight, + primaryScrollColor: getComputedStyle(primary).scrollbarColor, + scrollPrimitive: body.localName, + scrollPrimitives: + dialog.querySelectorAll('zen-scroll-area[data-cyber-scroll]').length, + scrollbarColor: bodyStyle.scrollbarColor, + scrollbarGutter: bodyStyle.scrollbarGutter, + shadow: getComputedStyle(dialog).boxShadow, + }; + }); + assert.equal(windowMetrics.bodyScrollable, true); + assert.equal(windowMetrics.borderRadius, '0px'); + assert.equal(windowMetrics.hasStrictRegions, true); + assert.equal(windowMetrics.scrollPrimitive, 'zen-scroll-area'); + assert.notEqual(windowMetrics.scrollbarColor, 'auto'); + assert.match(windowMetrics.scrollbarGutter, /stable/); + assert.match(windowMetrics.bodyColumns, /\d/); + assert.ok(windowMetrics.height <= 768); + assert.notEqual(windowMetrics.primaryShadow, 'none'); + assert.equal(windowMetrics.primaryOverflowing, true); + assert.notEqual(windowMetrics.primaryScrollColor, 'auto'); + assert.equal(windowMetrics.scrollPrimitives, 3); + assert.notEqual(windowMetrics.shadow, 'none'); + await page.getByRole('button', { name: 'Close workspace' }).click(); + await windowDialog.waitFor({ state: 'hidden' }); + assert.deepEqual(errors, []); + await context.close(); + + const mobileContext = await browser.newContext({ + serviceWorkers: 'block', + viewport: { width: 390, height: 844 }, + }); + const mobilePage = await mobileContext.newPage(); + await mobilePage.goto(`${baseUrl}/public/component-sandbox.html`, { + waitUntil: 'networkidle', + }); + await mobilePage.evaluate(() => document.fonts.ready); + await mobilePage.getByRole('button', { name: 'Open workspace' }).click(); + const mobileDialog = mobilePage.locator('mjj-window-modal dialog'); + await mobileDialog.waitFor({ state: 'visible' }); + await mobileDialog.evaluate(dialog => + Promise.all(dialog.getAnimations().map(animation => animation.finished)) + ); + const mobileMetrics = await mobileDialog.evaluate(dialog => { + const body = dialog.querySelector('[data-modal-body]'); + const dialogBox = dialog.getBoundingClientRect(); + return { + bodyColumns: getComputedStyle(body).gridTemplateColumns, + fits: + dialogBox.left >= 0 && + dialogBox.top >= 0 && + dialogBox.right <= innerWidth && + dialogBox.bottom <= innerHeight, + pageFits: document.documentElement.scrollWidth <= innerWidth, + }; + }); + assert.equal(mobileMetrics.fits, true); + assert.equal(mobileMetrics.pageFits, true); + assert.doesNotMatch(mobileMetrics.bodyColumns, /\s/); + assert.equal( + await mobilePage.locator('mjj-window-modal').getAttribute( + 'data-component-layer', + ), + '2', + ); + await mobileContext.close(); +} + async function testJrpgPage(browser) { const context = await browser.newContext({ colorScheme: 'light', @@ -547,6 +714,30 @@ } }); + for (const expected of [ + { panel: 'resume', title: 'Resume' }, + { panel: 'tools', title: 'Tools' }, + { panel: 'blog', title: 'Blogs' }, + ]) { + const html = await ( + await fetch(`${baseUrl}/jrpg?panel=${expected.panel}`) + ).text(); + assert.match( + html, + new RegExp(`data-initial-panel="${expected.panel}"`), + ); + assert.match(html, new RegExp(`data-preview-title>${expected.title}<`)); + assert.doesNotMatch(html, /__MJJ_[A-Z_]+__/); + } + const conversationHtml = await ( + await fetch(`${baseUrl}/jrpg?panel=conversations`) + ).text(); + assert.match(conversationHtml, /data-initial-panel="conversations"/); + assert.match( + conversationHtml, + /<mjj-conversation-archive\s+aria-label="Conversations"/, + ); + await page.goto(`${baseUrl}/jrpg`, { waitUntil: 'networkidle' }); await page.waitForFunction(() => customElements.get('mjj-composer') && @@ -554,6 +745,26 @@ customElements.get('mjj-jrpg-chat') && customElements.get('mjj-jrpg-preview') ); + await page.waitForFunction(() => + [...document.querySelectorAll('.jrpg-workspace dialog')].every(dialog => { + const owner = dialog.closest('mjj-content-modal, mjj-window-modal'); + return owner?.hasAttribute('data-ready'); + }) + ); + assert.deepEqual( + await page.evaluate(() => + [...document.querySelectorAll('.jrpg-workspace dialog')] + .map(dialog => + dialog.closest('mjj-content-modal, mjj-window-modal')?.localName)), + [ + 'mjj-window-modal', + 'mjj-content-modal', + 'mjj-content-modal', + 'mjj-window-modal', + 'mjj-window-modal', + ], + 'every JRPG dialog has a Layer 2 owner', + ); await page.evaluate(() => document.fonts.ready); const desktop = await page.evaluate(() => { const scene = document.querySelector('.jrpg-scene').getBoundingClientRect(); @@ -1067,6 +1278,36 @@ await page.locator('button[data-preview="conversations"]').getAttribute('aria-pressed'), 'true', ); + await page.evaluate(() => { + document.querySelector('mjj-conversation-archive')?.addConversation({ + id: 'eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee', + title: 'Modal migration fixture', + turn_count: 2, + }); + }); + await page.getByRole('button', { + name: 'Rename "Modal migration fixture"', + }).click(); + await page.locator('[data-archive-rename-dialog]').waitFor({ state: 'visible' }); + assert.equal( + await page.locator('[data-archive-rename-dialog]').evaluate(dialog => + dialog.closest('mjj-window-modal')?.localName), + 'mjj-window-modal', + ); + await page.locator('[data-archive-rename-cancel]').click(); + await page.locator('[data-archive-rename-dialog]').waitFor({ state: 'hidden' }); + + await page.getByRole('button', { + name: 'Delete "Modal migration fixture"', + }).click(); + await page.locator('[data-archive-delete-dialog]').waitFor({ state: 'visible' }); + assert.equal( + await page.locator('[data-archive-delete-dialog]').evaluate(dialog => + dialog.closest('mjj-content-modal')?.localName), + 'mjj-content-modal', + ); + await page.locator('[data-archive-delete-cancel]').click(); + await page.locator('[data-archive-delete-dialog]').waitFor({ state: 'hidden' }); // Archive: close via close button, navigates back to resume panel await page.locator('[data-archive-close]').click(); @@ -1128,10 +1369,7 @@ await page.locator('[data-preview-title]').textContent(), 'Tools', ); - assert.equal( - await page.locator('[data-preview-link]').getAttribute('href'), - '/tools', - ); + assert.equal(await page.locator('[data-preview-link]').count(), 0); assert.equal( await page.getByRole('button', { name: 'Tools', exact: true }) .getAttribute('aria-pressed'), @@ -1151,13 +1389,15 @@ await page.locator('[data-latest-tool]').first().click(); await page.locator('[data-functional-tool="markdown"]').waitFor(); assert.equal( + await page.locator('.jrpg-detail-dialog').evaluate(dialog => + dialog.closest('mjj-content-modal, mjj-window-modal')?.localName), + 'mjj-window-modal', + 'tool details use the window modal', + ); + assert.equal( await page.locator('[data-dialog-title]').textContent(), 'MarkDown to HTML', ); - assert.equal( - await page.locator('[data-preview-link]').getAttribute('href'), - '/tools/markdown_to_html', - ); await page.locator('[data-markdown-source]').fill('# Modal Markdown'); await page.waitForFunction(() => document.querySelector('[data-markdown-output] h1')?.textContent === @@ -1173,27 +1413,18 @@ .map(window => window.getBoundingClientRect()); const windowShadows = [...document.querySelectorAll('.jrpg-tool-window')] .map(window => getComputedStyle(window).boxShadow); - const rail = getComputedStyle( - document.querySelector('.jrpg-tool-panes'), - '::before', - ); return { dialogHeight: dialogElement.offsetHeight, dialogWidth: dialogElement.offsetWidth, - fillsViewport: dialogElement.offsetWidth > innerWidth * 0.9 && - dialogElement.offsetHeight > innerHeight * 0.9, + compact: dialogElement.offsetWidth <= innerWidth * 0.9 && + dialogElement.offsetHeight <= innerHeight * 0.8, viewportHeight: innerHeight, viewportWidth: innerWidth, - sideBySide: windows.length === 2 && - windows[0].right < windows[1].left && - Math.abs(windows[0].top - windows[1].top) < 1, - fullPageInHeader: Boolean( - document.querySelector('[data-preview-link]') - .closest('.jrpg-dialog-heading-actions'), - ), - footerHidden: document.querySelector('[data-dialog-actions]').hidden, - metalRail: rail.content !== 'none' && - rail.backgroundImage.includes('bar-ink.webp'), + stacked: windows.length === 2 && + windows[0].bottom < windows[1].top && + Math.abs(windows[0].left - windows[1].left) < 1, + noOpenPageAction: !document.querySelector('[data-preview-link]'), + noFooter: !document.querySelector('[data-dialog-actions]'), pixelIcons: document.querySelectorAll('.jrpg-tool-window zen-icon').length >= 2 && [...document.querySelectorAll('.jrpg-tool-window zen-icon svg')] .every(icon => @@ -1201,21 +1432,19 @@ ), separateGlows: windowShadows.length === 2 && windowShadows.every(shadow => shadow !== 'none'), - wideWindows: windows.every(window => window.width > dialog.width * 0.35), + wideWindows: windows.every(window => window.width > dialog.width * 0.8), }; }); - assert.equal( - toolWorkspace.fillsViewport, - true, - JSON.stringify(toolWorkspace), - ); - assert.equal(toolWorkspace.sideBySide, true); - assert.equal(toolWorkspace.fullPageInHeader, true); - assert.equal(toolWorkspace.footerHidden, true); - assert.equal(toolWorkspace.metalRail, true); + assert.equal(toolWorkspace.compact, true, JSON.stringify(toolWorkspace)); + assert.equal(toolWorkspace.stacked, true); + assert.equal(toolWorkspace.noOpenPageAction, true); + assert.equal(toolWorkspace.noFooter, true); assert.equal(toolWorkspace.pixelIcons, true); assert.equal(toolWorkspace.separateGlows, true); assert.equal(toolWorkspace.wideWindows, true); + await page.locator('.jrpg-detail-dialog').evaluate(dialog => + Promise.all(dialog.getAnimations().map(animation => animation.finished)) + ); await page.getByRole('button', { name: 'Close destination details', }).click(); @@ -1283,10 +1512,6 @@ await page.locator('[data-latest-tool]').nth(2).click(); await page.locator('[data-functional-tool="hls"]').waitFor(); - assert.equal( - await page.locator('[data-preview-link]').getAttribute('href'), - '/tools/hls_player', - ); await page.locator('[data-hls-sample]').click(); await page.waitForFunction(() => ['ready', 'error'].includes( @@ -1336,10 +1561,6 @@ await page.locator('[data-tool-content]').textContent(), /private browser-based writing workspace/, ); - assert.equal( - await page.locator('[data-preview-link]').getAttribute('href'), - '/notes', - ); await page.getByRole('button', { name: 'Close destination details', }).click(); @@ -1347,6 +1568,12 @@ await page.getByRole('button', { name: 'Resume', exact: true }).click(); await page.locator('[data-resume-modal]').click(); + assert.equal( + await page.locator('.jrpg-detail-dialog').evaluate(dialog => + dialog.closest('mjj-content-modal, mjj-window-modal')?.localName), + 'mjj-content-modal', + 'resume details use the content modal', + ); await page.locator('[data-resume-content] .sub-header').first().waitFor(); const resumeDossier = await page.locator('[data-resume-dossier]').evaluate( dossier => ({ @@ -1358,6 +1585,46 @@ assert.match(resumeDossier.text, /Copilot Tasks/); assert.match(resumeDossier.text, /Microsoft/); assert.match(resumeDossier.text, /Meta/); + const resumeModalGeometry = await page.locator('.jrpg-detail-dialog').evaluate( + dialog => { + const body = dialog.querySelector('[data-modal-body]').getBoundingClientRect(); + const dossier = dialog.querySelector('[data-resume-dossier]') + .getBoundingClientRect(); + const buttons = [...dialog.querySelectorAll( + '[data-modal-header] zen-button > :is(button, a)', + )].map(control => control.getBoundingClientRect()); + return { + bodyFilled: + dossier.width / body.width > 0.9 && + Math.abs(dossier.left - body.left) < 32, + equalHeaderControls: + buttons.length === 2 && + Math.abs(buttons[0].height - buttons[1].height) < 1, + headerControlHeights: buttons.map(button => button.height), + footerAbsent: !dialog.querySelector('[data-modal-footer]'), + headerPdfLinks: dialog.querySelectorAll( + '[data-modal-header] a[href="/public/resume.pdf"]', + ).length, + visibleBodyPdfLinks: [...dialog.querySelectorAll( + '[data-modal-body] a[href="/public/resume.pdf"]', + )].filter(link => getComputedStyle(link).display !== 'none').length, + pdfLinks: dialog.querySelectorAll('a[href="/public/resume.pdf"]').length, + }; + }, + ); + assert.equal(resumeModalGeometry.bodyFilled, true); + assert.equal( + resumeModalGeometry.equalHeaderControls, + true, + JSON.stringify(resumeModalGeometry), + ); + assert.equal(resumeModalGeometry.footerAbsent, true); + assert.equal(resumeModalGeometry.headerPdfLinks, 1); + assert.equal(resumeModalGeometry.visibleBodyPdfLinks, 0); + assert.equal(resumeModalGeometry.pdfLinks, 1); + await page.locator('.jrpg-detail-dialog').evaluate(dialog => + Promise.all(dialog.getAnimations().map(animation => animation.finished)) + ); assert.equal( await page.locator('[data-resume-content] a[target="_blank"]').count() > 0, true, @@ -1386,15 +1653,21 @@ 'WebSocket Demystified', ); assert.equal( - await page.locator('[data-preview-link]').getAttribute('href'), - '/blog/websocket-demystified', - ); - assert.equal( await page.locator('[data-blog-content]').evaluate( article => getComputedStyle(article).fontFamily, ), '"Pixel Mplus"', ); + const blogWidth = await page.locator('.jrpg-detail-dialog').evaluate(dialog => { + const body = dialog.querySelector('[data-modal-body]').getBoundingClientRect(); + const browser = dialog.querySelector('[data-blog-browser]') + .getBoundingClientRect(); + return browser.width / body.width; + }); + assert.ok(blogWidth > 0.9); + await page.locator('.jrpg-detail-dialog').evaluate(dialog => + Promise.all(dialog.getAnimations().map(animation => animation.finished)) + ); await page.getByRole('button', { name: 'Close destination details', }).click(); @@ -1410,10 +1683,6 @@ document.querySelector('[data-blog-content] h1')?.textContent === 'Creating Network Library in C' ); - assert.equal( - await page.locator('[data-preview-link]').getAttribute('href'), - '/blog/my-seobeo-journey', - ); await page.getByRole('button', { name: 'Close destination details', }).click(); @@ -1689,6 +1958,7 @@ ).matches, menuInModal: menuElement.parentElement === destinationContent, menuHidden: menu.width === 0 && menu.height === 0, + modalOwner: destinationContent.closest('mjj-window-modal')?.localName, mobileArtIsCorrect, noOverflow: document.documentElement.scrollWidth <= innerWidth, sendButtonCompact: @@ -1720,6 +1990,10 @@ utilityHidden: utility.width === 0 && utility.height === 0, utilityInModal: document.querySelector('.jrpg-utility').parentElement === destinationContent, + usesCyberScroll: + destinationContent.localName === 'zen-scroll-area' && + destinationContent.hasAttribute('data-cyber-scroll') && + getComputedStyle(destinationContent).scrollbarColor !== 'auto', usesDesignSystemControls: !composerField.hasAttribute('appearance') && !sendOwner.hasAttribute('appearance'), @@ -1762,6 +2036,8 @@ ); assert.equal(mobile.utilityHidden, true, 'destination view stays out of the frame'); assert.equal(mobile.utilityInModal, true, 'destination view is hosted by the mobile modal'); + assert.equal(mobile.modalOwner, 'mjj-window-modal'); + assert.equal(mobile.usesCyberScroll, true, 'JRPG modal uses the shared cyber scroll area'); assert.equal(mobile.usesDesignSystemControls, true, 'composer uses styled Zenbu controls'); assert.equal(mobile.workspaceFills, true, 'workspace fills full viewport'); assert.equal(mobile.topLeftClear, true, 'hidden dialog triggers do not cover frame art'); @@ -1771,6 +2047,9 @@ await mobilePage.locator('[data-mobile-destination-dialog]').waitFor({ state: 'visible', }); + await mobilePage.locator('[data-mobile-destination-dialog]').evaluate(dialog => + Promise.all(dialog.getAnimations().map(animation => animation.finished)) + ); assert.equal( await mobilePage.locator('[data-mobile-menu-toggle]') .getAttribute('aria-expanded'), @@ -2810,7 +3089,7 @@ for (const source of [home, dogGame, manifest]) { assert.doesNotMatch(source, /\.png(?:["')]|$)/i); } - assert.match(serviceWorker, /v35-role-greeting/); + assert.match(serviceWorker, /v37-network-first/); assert.match( await ( await fetch(`${baseUrl}/public/pwa-register.js`) @@ -2820,6 +3099,18 @@ assert.ok(serviceWorker.includes("startsWith('/tools/hls_player')")); assert.ok(serviceWorker.includes("url.hostname === 'localhost'")); assert.ok(serviceWorker.includes("url.hostname === '127.0.0.1'")); + assert.ok(serviceWorker.includes("DEVELOPMENT_SANDBOX_PATHS")); + assert.ok(serviceWorker.includes("'/public/component-sandbox'")); + assert.ok(serviceWorker.includes("'/public/mjj-modal.'")); + assert.ok(serviceWorker.includes('networkFirst(request, acceptsHtml)')); + assert.ok(serviceWorker.includes('staleWhileRevalidate(event, request)')); + assert.ok(serviceWorker.includes("fetch(request, { cache: 'no-cache' })")); + assert.doesNotMatch( + await ( + await fetch(`${baseUrl}/public/pwa-register.js`) + ).text(), + /confirm\(/, + ); assert.ok(!serviceWorker.includes("startsWith('/jrpg')")); for (const asset of [ @@ -2877,6 +3168,7 @@ await testDynamicButtonOwnership(browser); await testResumePrint(browser); await testComposerLab(browser); + await testModalSandbox(browser); } if (runsSuite('jrpg')) await testJrpgPage(browser); if (runsSuite('routing')) await testConversationRouting(browser);