diff mrjunejune/test/theme_and_webp_test.js @ 259:667156fcd3e3

Add dev-only cyberpunk JRPG page
author MrJuneJune <me@mrjunejune.com>
date Wed, 05 Aug 2026 09:19:41 -0700
parents 60a876c4587a
children 1f9877b637e9
line wrap: on
line diff
--- a/mrjunejune/test/theme_and_webp_test.js	Wed Aug 05 05:25:40 2026 -0700
+++ b/mrjunejune/test/theme_and_webp_test.js	Wed Aug 05 09:19:41 2026 -0700
@@ -373,6 +373,284 @@
   await page.close();
 }
 
+async function testJrpgPage(browser) {
+  const context = await browser.newContext({
+    colorScheme: 'light',
+    viewport: { width: 1280, height: 900 },
+  });
+  const page = await context.newPage();
+  const errors = [];
+  page.on('pageerror', error => errors.push(`pageerror: ${error.message}`));
+  page.on('console', message => {
+    if (message.type() === 'error') errors.push(`console: ${message.text()}`);
+  });
+  page.on('requestfailed', request => {
+    errors.push(`requestfailed: ${request.url()} ${request.failure()?.errorText || ''}`);
+  });
+  page.on('response', response => {
+    if (response.status() >= 400) {
+      errors.push(`response: ${response.status()} ${response.url()}`);
+    }
+  });
+
+  await page.goto(`${baseUrl}/jrpg`, { waitUntil: 'networkidle' });
+  await page.waitForFunction(() =>
+    customElements.get('mjj-jrpg-composer') &&
+    customElements.get('mjj-jrpg-menu') &&
+    customElements.get('mjj-jrpg-chat') &&
+    customElements.get('mjj-jrpg-preview')
+  );
+  await page.evaluate(() => document.fonts.ready);
+  const desktop = await page.evaluate(() => {
+    const scene = document.querySelector('.jrpg-scene').getBoundingClientRect();
+    const utility = document.querySelector('.jrpg-utility').getBoundingClientRect();
+    const composer = document.querySelector('mjj-jrpg-composer')
+      .getBoundingClientRect();
+    const menu = document.querySelector('mjj-jrpg-menu').getBoundingClientRect();
+    const character = document.querySelector('[data-character]');
+    const shell = document.querySelector('.jrpg-shell').getBoundingClientRect();
+    const main = document.querySelector('.jrpg-main').getBoundingClientRect();
+    const colorProbe = document.createElement('span');
+    colorProbe.style.background = 'var(--zenbu-sys-color-surface-page)';
+    colorProbe.style.color =
+      'var(--zenbu-sys-color-action-primary-background)';
+    colorProbe.style.borderColor = 'var(--zenbu-sys-color-focus-outer)';
+    const surfaceProbe = document.createElement('span');
+    surfaceProbe.style.background =
+      'var(--zenbu-sys-color-surface-default)';
+    surfaceProbe.style.color = 'var(--zenbu-sys-color-text-primary)';
+    surfaceProbe.style.borderColor = 'var(--zenbu-sys-color-border-strong)';
+    const sunkenProbe = document.createElement('span');
+    sunkenProbe.style.background =
+      'var(--zenbu-sys-color-surface-sunken)';
+    const panelProbe = document.createElement('span');
+    panelProbe.style.background = 'var(--mjj-jrpg-panel-strong)';
+    document.body.append(colorProbe, surfaceProbe, sunkenProbe, panelProbe);
+    const probeStyle = getComputedStyle(colorProbe);
+    const surfaceProbeStyle = getComputedStyle(surfaceProbe);
+    const sunkenProbeStyle = getComputedStyle(sunkenProbe);
+    const panelProbeStyle = getComputedStyle(panelProbe);
+    const computedFonts = [
+      document.body,
+      document.querySelector('.jrpg-brand h1'),
+      document.querySelector('.jrpg-message p'),
+      document.querySelector('#jrpg-message'),
+      document.querySelector('.jrpg-composer button'),
+    ].map(element => getComputedStyle(element).fontFamily);
+    const nonPixelText = [document.body, ...document.body.querySelectorAll('*')]
+      .filter(element => [...element.childNodes].some(node =>
+        node.nodeType === Node.TEXT_NODE && node.textContent.trim()
+      ))
+      .filter(element =>
+        getComputedStyle(element).fontFamily !== '"Pixel Mplus"'
+      )
+      .map(element => element.localName);
+    const pageStyle = getComputedStyle(document.body);
+    const submitStyle = getComputedStyle(
+      document.querySelector('.jrpg-composer button'),
+    );
+    const colors = {
+      canvas: probeStyle.backgroundColor,
+      conversation: getComputedStyle(
+        document.querySelector('mjj-jrpg-chat'),
+      ).backgroundColor,
+      conversationBorder: getComputedStyle(
+        document.querySelector('mjj-jrpg-chat'),
+      ).borderTopColor,
+      conversationPanel: panelProbeStyle.backgroundColor,
+      conversationText: getComputedStyle(
+        document.querySelector('.jrpg-message p'),
+      ).color,
+      focus: probeStyle.borderColor,
+      frame: surfaceProbeStyle.borderColor,
+      menu: getComputedStyle(
+        document.querySelector('mjj-jrpg-menu'),
+      ).backgroundColor,
+      page: pageStyle.backgroundColor,
+      primary: probeStyle.color,
+      primaryText: getComputedStyle(
+        document.querySelector('.jrpg-preview-panel h2'),
+      ).color,
+      sunken: sunkenProbeStyle.backgroundColor,
+      surface: surfaceProbeStyle.backgroundColor,
+      submit: submitStyle.backgroundColor,
+      text: surfaceProbeStyle.color,
+      utility: getComputedStyle(
+        document.querySelector('.jrpg-utility'),
+      ).backgroundColor,
+    };
+    colorProbe.remove();
+    surfaceProbe.remove();
+    sunkenProbe.remove();
+    panelProbe.remove();
+    return {
+      art: getComputedStyle(document.querySelector('.jrpg-scene')).backgroundImage,
+      characterLoaded: character.complete && character.naturalWidth > 0,
+      colors,
+      composerBelowScene: composer.top >= scene.bottom,
+      fillsViewport: shell.left === 0 &&
+        shell.top === 0 &&
+        Math.abs(shell.right - innerWidth) < 1 &&
+        Math.abs(shell.bottom - innerHeight) < 1 &&
+        main.left === 0 &&
+        main.top === 0 &&
+        Math.abs(main.right - innerWidth) < 1 &&
+        Math.abs(main.bottom - innerHeight) < 1,
+      fonts: computedFonts,
+      fontLoaded: [...document.fonts].some(font =>
+        font.family.replaceAll('"', '') === 'Pixel Mplus' &&
+        font.status === 'loaded'
+      ),
+      menuRightOfComposer: menu.left >= composer.right,
+      noOverflow: document.documentElement.scrollWidth <= innerWidth,
+      nonPixelText,
+      theme: document.documentElement.dataset.zenTheme,
+      utilityRightOfScene: utility.left >= scene.right,
+    };
+  });
+  assert.match(desktop.art, /bar-ink\.webp/);
+  assert.equal(desktop.characterLoaded, true);
+  assert.equal(desktop.colors.page, desktop.colors.canvas);
+  assert.equal(
+    desktop.colors.conversation,
+    desktop.colors.conversationPanel,
+  );
+  assert.equal(desktop.colors.conversationBorder, desktop.colors.frame);
+  assert.equal(desktop.colors.conversationText, desktop.colors.text);
+  assert.equal(desktop.colors.menu, desktop.colors.sunken);
+  assert.equal(desktop.colors.primaryText, desktop.colors.primary);
+  assert.equal(desktop.colors.submit, desktop.colors.primary);
+  assert.equal(desktop.colors.utility, desktop.colors.surface);
+  assert.equal(desktop.composerBelowScene, true);
+  assert.equal(desktop.fillsViewport, true);
+  assert.ok(desktop.fonts.every(font => font === '"Pixel Mplus"'));
+  assert.equal(desktop.fontLoaded, true);
+  assert.equal(desktop.menuRightOfComposer, true);
+  assert.equal(desktop.noOverflow, true);
+  assert.deepEqual(desktop.nonPixelText, []);
+  assert.equal(desktop.theme, 'cyberpunk');
+  assert.equal(desktop.utilityRightOfScene, true);
+  await page.locator('.jrpg-brand a').focus();
+  assert.equal(
+    await page.locator('.jrpg-brand a').evaluate(element =>
+      getComputedStyle(element).outlineColor
+    ),
+    desktop.colors.focus,
+  );
+  await page.locator('.jrpg-composer button').focus();
+  assert.equal(
+    await page.locator('.jrpg-composer button').evaluate(element =>
+      getComputedStyle(element).outlineColor
+    ),
+    desktop.colors.focus,
+  );
+  assert.equal(
+    await page.locator('meta[name="theme-color"]').first().getAttribute('content'),
+    desktop.colors.canvas,
+  );
+
+  await page.getByRole('button', { name: 'Tools', exact: true }).click();
+  assert.equal(
+    await page.locator('[data-preview-title]').textContent(),
+    'Tools',
+  );
+  assert.equal(
+    await page.locator('[data-preview-link]').getAttribute('href'),
+    '/tools',
+  );
+  assert.equal(
+    await page.getByRole('button', { name: 'Tools', exact: true })
+      .getAttribute('aria-pressed'),
+    'true',
+  );
+
+  await page.getByRole('button', { name: /Inspect/ }).click();
+  await page.locator('.jrpg-preview-dialog dialog[open]').waitFor();
+  assert.equal(
+    await page.locator('[data-dialog-title]').textContent(),
+    'Tools',
+  );
+  await page.getByRole('button', {
+    name: 'Close destination details',
+  }).click();
+
+  await page.locator('#jrpg-message').fill('Where should I begin?');
+  await page.locator('#jrpg-message').press('Enter');
+  await page.locator('.jrpg-message[data-speaker="June"]').last().waitFor();
+  await page.getByRole('button', { name: 'Blogs', exact: true }).click();
+  assert.equal(
+    await page.locator('.jrpg-message[data-speaker="June"] p').last()
+      .textContent(),
+    'Where should I begin?',
+  );
+  await page.waitForFunction(() =>
+    document.querySelector('mjj-jrpg-character')?.getAttribute('state') ===
+      'default' &&
+    document.querySelectorAll('.jrpg-message[data-speaker="Epi"]').length > 1
+  );
+  assert.equal(await page.locator('#jrpg-message').isEnabled(), true);
+
+  await page.waitForFunction(() => navigator.serviceWorker?.controller);
+  await page.reload({ waitUntil: 'networkidle' });
+  await page.evaluate(() => document.fonts.ready);
+  const controlledReload = await page.evaluate(() => {
+    const shell = document.querySelector('.jrpg-shell').getBoundingClientRect();
+    return {
+      art: getComputedStyle(document.querySelector('.jrpg-scene'))
+        .backgroundImage,
+      fillsViewport: shell.left === 0 &&
+        shell.top === 0 &&
+        Math.abs(shell.right - innerWidth) < 1 &&
+        Math.abs(shell.bottom - innerHeight) < 1,
+      fontLoaded: document.fonts.check('16px "Pixel Mplus"'),
+      fontFamily: getComputedStyle(
+        document.querySelector('.jrpg-brand h1'),
+      ).fontFamily,
+      theme: document.documentElement.dataset.zenTheme,
+    };
+  });
+  assert.match(controlledReload.art, /bar-ink\.webp/);
+  assert.equal(controlledReload.fillsViewport, true);
+  assert.equal(controlledReload.fontLoaded, true);
+  assert.equal(controlledReload.fontFamily, '"Pixel Mplus"');
+  assert.equal(controlledReload.theme, 'cyberpunk');
+  assert.deepEqual(errors, []);
+  await context.close();
+
+  const mobileContext = await browser.newContext({
+    colorScheme: 'light',
+    viewport: { width: 360, height: 640 },
+  });
+  const mobilePage = await mobileContext.newPage();
+  await mobilePage.goto(`${baseUrl}/jrpg`, { waitUntil: 'networkidle' });
+  const mobile = await mobilePage.evaluate(() => {
+    const scene = document.querySelector('.jrpg-scene').getBoundingClientRect();
+    const utility = document.querySelector('.jrpg-utility').getBoundingClientRect();
+    const shell = document.querySelector('.jrpg-shell').getBoundingClientRect();
+    const menuButtons = [...document.querySelectorAll(
+      'mjj-jrpg-menu button[data-preview]',
+    )];
+    return {
+      buttonsFit: menuButtons.every(button =>
+        button.getBoundingClientRect().right <= innerWidth
+      ),
+      fillsWidth: shell.left === 0 &&
+        Math.abs(shell.right - innerWidth) < 1,
+      initialScroll: scrollY,
+      noOverflow: document.documentElement.scrollWidth <= innerWidth,
+      utilityBelowScene: utility.top >= scene.bottom,
+    };
+  });
+  assert.deepEqual(mobile, {
+    buttonsFit: true,
+    fillsWidth: true,
+    initialScroll: 0,
+    noOverflow: true,
+    utilityBelowScene: true,
+  });
+  await mobileContext.close();
+}
+
 async function testHlsPlayer(browser, siteRoot) {
   const page = await browser.newPage();
   const errors = [];
@@ -615,7 +893,7 @@
   const runfilesWorkspace = path.join(RUNFILES, WORKSPACE);
   const serverBinary = path.join(
     runfilesWorkspace,
-    'mrjunejune/mrjunejune_server',
+    'mrjunejune/mrjunejune_server_debug',
   );
   const siteRoot = path.join(runfilesWorkspace, 'mrjunejune/src');
   const chromiumPath = path.resolve(process.env.CHROMIUM_PATH);
@@ -679,7 +957,7 @@
     for (const source of [home, dogGame, manifest]) {
       assert.doesNotMatch(source, /\.png(?:["')]|$)/i);
     }
-    assert.match(serviceWorker, /v6-zenbu-headless/);
+    assert.match(serviceWorker, /v9-dev-page-split/);
     assert.match(
       await (
         await fetch(`${baseUrl}/public/pwa-register.js`)
@@ -687,6 +965,9 @@
       /register\('\/sw\.js', \{ scope: '\/' \}\)/,
     );
     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("startsWith('/jrpg')"));
 
     for (const asset of [
       'sprite_shiba0.webp',
@@ -741,6 +1022,7 @@
     await testButtonScale(browser);
     await testDynamicButtonOwnership(browser);
     await testResumePrint(browser);
+    await testJrpgPage(browser);
     await testHlsPlayer(browser, siteRoot);
   } finally {
     if (browser) await browser.close();