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) {