From 4f68670aa706b330fbc06896bd5afd7f5a80574f Mon Sep 17 00:00:00 2001 From: jelveh Date: Wed, 24 Jun 2026 11:29:45 -0700 Subject: [PATCH] Add theme test & refactor context menu items Extract shared mediaMenuItems to remove duplication and simplify context menu calls. Add a light-theme context menu handler and a "Run theme test" button that executes runContextMenuThemeTest(), which programmatically renders menus to verify the theme attribute and .puter-theme-dark class for dark, light, and default cases (mirrors existing Playwright assertions). Update testContextMenu and testContextMenuDark to use the new item factory and clean up rendered menus after checks. UI text is updated to show pass/fail results. --- src/puter-js/test/web-components-test.html | 122 +++++++++++++-------- 1 file changed, 79 insertions(+), 43 deletions(-) diff --git a/src/puter-js/test/web-components-test.html b/src/puter-js/test/web-components-test.html index 47ceb0179..a8e2337d6 100644 --- a/src/puter-js/test/web-components-test.html +++ b/src/puter-js/test/web-components-test.html @@ -79,6 +79,8 @@ + + @@ -195,53 +197,87 @@ paste: svg(''), }; + // Shared item list so the default/dark/light triggers stay identical + // apart from the `theme` they request. + const mediaMenuItems = () => [ + { label: 'Get Info', icon: ICONS.info, action: () => log('menu', 'Get Info') }, + '-', + { label: 'Play Next', icon: ICONS.play, action: () => log('menu', 'Play Next') }, + { label: 'Add to Queue', icon: ICONS.queue, action: () => log('menu', 'Add to Queue') }, + '-', + { label: 'Go to Artist', icon: ICONS.user, action: () => log('menu', 'Go to Artist') }, + { label: 'Go to Album', icon: ICONS.disc, action: () => log('menu', 'Go to Album') }, + '-', + { label: 'Add to Playlist', icon: ICONS.plus, items: [ + { label: 'Favorites', action: () => log('menu', 'Add to Favorites') }, + { label: 'Workout Mix', action: () => log('menu', 'Add to Workout Mix') }, + { label: 'Chill Vibes', action: () => log('menu', 'Add to Chill Vibes') }, + ]}, + { label: 'Download', icon: ICONS.download, action: () => log('menu', 'Download') }, + { label: 'Delete', icon: ICONS.trash, type: 'danger', action: () => log('menu', 'Delete') }, + ]; + function testContextMenu(e) { - puter.ui.contextMenu({ - x: e.clientX, - y: e.clientY, - items: [ - { label: 'Get Info', icon: ICONS.info, action: () => log('menu', 'Get Info') }, - '-', - { label: 'Play Next', icon: ICONS.play, action: () => log('menu', 'Play Next') }, - { label: 'Add to Queue', icon: ICONS.queue, action: () => log('menu', 'Add to Queue') }, - '-', - { label: 'Go to Artist', icon: ICONS.user, action: () => log('menu', 'Go to Artist') }, - { label: 'Go to Album', icon: ICONS.disc, action: () => log('menu', 'Go to Album') }, - '-', - { label: 'Add to Playlist', icon: ICONS.plus, items: [ - { label: 'Favorites', action: () => log('menu', 'Add to Favorites') }, - { label: 'Workout Mix', action: () => log('menu', 'Add to Workout Mix') }, - { label: 'Chill Vibes', action: () => log('menu', 'Add to Chill Vibes') }, - ]}, - { label: 'Download', icon: ICONS.download, action: () => log('menu', 'Download') }, - { label: 'Delete', icon: ICONS.trash, type: 'danger', action: () => log('menu', 'Delete') }, - ], - }); + puter.ui.contextMenu({ x: e.clientX, y: e.clientY, items: mediaMenuItems() }); } function testContextMenuDark(e) { - puter.ui.contextMenu({ - x: e.clientX, - y: e.clientY, - theme: 'dark', - items: [ - { label: 'Get Info', icon: ICONS.info, action: () => log('menu', 'Get Info') }, - '-', - { label: 'Play Next', icon: ICONS.play, action: () => log('menu', 'Play Next') }, - { label: 'Add to Queue', icon: ICONS.queue, action: () => log('menu', 'Add to Queue') }, - '-', - { label: 'Go to Artist', icon: ICONS.user, action: () => log('menu', 'Go to Artist') }, - { label: 'Go to Album', icon: ICONS.disc, action: () => log('menu', 'Go to Album') }, - '-', - { label: 'Add to Playlist', icon: ICONS.plus, items: [ - { label: 'Favorites', action: () => log('menu', 'Add to Favorites') }, - { label: 'Workout Mix', action: () => log('menu', 'Add to Workout Mix') }, - { label: 'Chill Vibes', action: () => log('menu', 'Add to Chill Vibes') }, - ]}, - { label: 'Download', icon: ICONS.download, action: () => log('menu', 'Download') }, - { label: 'Delete', icon: ICONS.trash, type: 'danger', action: () => log('menu', 'Delete') }, - ], - }); + puter.ui.contextMenu({ x: e.clientX, y: e.clientY, theme: 'dark', items: mediaMenuItems() }); + } + + function testContextMenuLight(e) { + puter.ui.contextMenu({ x: e.clientX, y: e.clientY, theme: 'light', items: mediaMenuItems() }); + } + + // Programmatic check that the `theme` option is forwarded onto the + // rendered . Mirrors the Playwright assertions in + // tests/e2e/specs/menubar-contextmenu-theme.spec.js, but runs in-page so + // the plumbing can be verified manually without the e2e harness. + // Note: the base component toggles `.puter-theme-dark` on the host; + // "light" is the absence of that class (there is no .puter-theme-light). + function runContextMenuThemeTest() { + const resultEl = document.getElementById('menu-result'); + const failures = []; + const check = (cond, msg) => { if ( ! cond ) failures.push(msg); }; + + // Render one menu for `theme`, then return the rendered host element. + // connectedCallback resolves the theme synchronously on append, so the + // attribute and class are already settled by the time we query. + const spawn = (theme) => { + document.querySelectorAll('puter-context-menu').forEach(el => el.remove()); + puter.ui.contextMenu({ + x: 40, y: 220, + ...(theme ? { theme } : {}), + items: [{ label: 'Probe', action: () => {} }], + }); + return document.querySelector('puter-context-menu'); + }; + + // theme: 'dark' → theme attribute set + .puter-theme-dark on host. + const dark = spawn('dark'); + check(dark, 'dark: no was rendered'); + check(dark?.getAttribute('theme') === 'dark', 'dark: theme attribute is not "dark"'); + check(dark?.classList.contains('puter-theme-dark'), 'dark: missing .puter-theme-dark class'); + + // theme: 'light' → theme attribute set + .puter-theme-dark absent. + const light = spawn('light'); + check(light?.getAttribute('theme') === 'light', 'light: theme attribute is not "light"'); + check( ! light?.classList.contains('puter-theme-dark'), 'light: .puter-theme-dark class should be absent'); + + // No theme → no forced attribute; component follows system preference. + const auto = spawn(null); + check( ! auto?.hasAttribute('theme'), 'default: theme attribute should be absent'); + + // Leave the page clean. + document.querySelectorAll('puter-context-menu').forEach(el => el.remove()); + + if ( failures.length === 0 ) { + resultEl.textContent = '✓ theme test passed (dark / light / default)'; + resultEl.style.color = 'green'; + } else { + resultEl.textContent = '✗ theme test failed: ' + failures.join('; '); + resultEl.style.color = 'crimson'; + } } function testContextMenuClick(e) {