Mercurial
diff design_system/test/storybook_test.js @ 252:7a7581f040e8
[ui] Add scoped notification component
Co-authored-by: Copilot <[email protected]>
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Tue, 04 Aug 2026 11:57:16 -0700 |
| parents | 117c4d53c9a4 |
| children | fdf3816959cb |
line wrap: on
line diff
--- a/design_system/test/storybook_test.js Tue Aug 04 09:14:57 2026 -0700 +++ b/design_system/test/storybook_test.js Tue Aug 04 11:57:16 2026 -0700 @@ -125,6 +125,7 @@ '/components/card', '/components/alert', '/components/field', + '/components/notifications', '/components/stack', ]) { const response = await fetch(`${baseUrl}${route}`); @@ -350,6 +351,386 @@ assert.equal(authoredDismissState.connected, true); assert.equal(authoredDismissState.events, 0); + await page.goto(`${baseUrl}/components/notifications`, { + waitUntil: 'networkidle', + }); + const notificationScope = page.locator('zen-notifications').first(); + const notificationPosition = await notificationScope.locator( + '[data-zen-notification-stack]', + ).evaluate(stack => { + const style = getComputedStyle(stack); + return { + bottom: style.bottom, + position: style.position, + right: style.right, + }; + }); + assert.equal(notificationPosition.position, 'fixed'); + assert.notEqual(notificationPosition.bottom, 'auto'); + assert.notEqual(notificationPosition.right, 'auto'); + + const dedupe = await notificationScope.evaluate(scope => { + const source = scope.querySelector('[data-notification-demo]'); + const send = detail => source.dispatchEvent( + new CustomEvent('zen-notify', { + bubbles: true, + composed: false, + detail, + }), + ); + send({ + version: 1, + id: 'dedupe', + tone: 'info', + message: 'First announcement', + announcement: 'polite', + persistent: true, + }); + send({ + version: 1, + id: 'dedupe', + tone: 'success', + message: 'Updated without duplication', + announcement: 'polite', + persistent: true, + }); + send({ version: 99, id: 'invalid' }); + return { + size: scope.size, + visible: scope.visibleCount, + }; + }); + assert.deepEqual(dedupe, { size: 1, visible: 1 }); + assert.equal( + await notificationScope.locator('article').count(), + 1, + ); + assert.match( + await notificationScope.locator('article').textContent(), + /Updated without duplication/, + ); + await page.waitForFunction(() => + document.querySelector( + 'zen-notifications [data-zen-live="polite"]', + )?.textContent === 'First announcement' + ); + + const bounded = await notificationScope.evaluate(scope => { + let accepted = 0; + for (let index = 0; index < 25; index++) { + if (scope.notify({ + version: 1, + id: `bounded-${index}`, + tone: 'info', + message: `Bounded ${index}`, + announcement: 'none', + durationMs: 120000, + })) accepted++; + } + return { + accepted, + size: scope.size, + visible: scope.visibleCount, + }; + }); + assert.deepEqual(bounded, { accepted: 25, size: 23, visible: 3 }); + assert.equal( + await notificationScope.locator('article').count(), + 3, + ); + + const actionScope = await page.evaluate(() => { + const scope = document.createElement('zen-notifications'); + scope.id = 'action-scope'; + const source = document.createElement('button'); + scope.append(source); + document.body.append(scope); + window.__notificationAction = null; + scope.addEventListener('zen-notification-action', event => { + window.__notificationAction = event.detail; + }); + source.dispatchEvent(new CustomEvent('zen-notify', { + bubbles: true, + composed: false, + detail: { + version: 1, + id: 'action', + tone: 'error', + message: 'Action required', + announcement: 'assertive', + persistent: true, + action: { + token: 'opaque-secret-token', + label: 'Retry', + }, + }, + })); + return scope.id; + }); + assert.equal( + await page.locator(`#${actionScope}`).evaluate( + scope => scope.records, + ), + undefined, + ); + const actionArticle = page.locator( + `#${actionScope} [data-zen-notification-id="action"]`, + ); + assert.doesNotMatch( + await actionArticle.evaluate(article => article.outerHTML), + /opaque-secret-token/, + ); + await actionArticle.locator('.zen-notification-action').focus(); + await page.locator(`#${actionScope}`).evaluate(scope => { + const source = scope.querySelector('button'); + source.dispatchEvent(new CustomEvent('zen-notify', { + bubbles: true, + composed: false, + detail: { + version: 1, + id: 'action-neighbor', + tone: 'info', + message: 'Neighbor', + announcement: 'none', + persistent: true, + }, + })); + }); + assert.equal( + await actionArticle.locator('.zen-notification-action').evaluate( + action => document.activeElement === action, + ), + true, + ); + await actionArticle.locator('.zen-notification-action').click(); + assert.deepEqual( + await page.evaluate(() => window.__notificationAction), + { + version: 1, + id: 'action', + token: 'opaque-secret-token', + }, + ); + await page.locator(`#${actionScope}`).evaluate(scope => { + const source = scope.querySelector('button'); + source.dispatchEvent(new CustomEvent( + 'zen-dismiss-notification', + { + bubbles: true, + composed: false, + detail: { version: 1, id: 'action' }, + }, + )); + }); + assert.equal(await actionArticle.count(), 0); + + await page.evaluate(() => { + const scope = document.createElement('zen-notifications'); + scope.id = 'announcement-scope'; + const source = document.createElement('button'); + scope.append(source); + document.body.append(scope); + window.__announcements = []; + const region = scope.querySelector('[data-zen-live="polite"]'); + new MutationObserver(() => { + if (region.textContent) { + window.__announcements.push(region.textContent); + } + }).observe(region, { childList: true }); + for (let index = 0; index < 3; index++) { + source.dispatchEvent(new CustomEvent('zen-notify', { + bubbles: true, + composed: false, + detail: { + version: 1, + id: `announcement-${index}`, + tone: 'info', + message: `Announcement ${index}`, + announcement: 'polite', + persistent: true, + }, + })); + } + }); + await page.waitForFunction(() => + window.__announcements?.length === 3 + ); + assert.deepEqual( + await page.evaluate(() => window.__announcements), + ['Announcement 0', 'Announcement 1', 'Announcement 2'], + ); + + const composedRejected = await page.evaluate(() => { + const scope = document.createElement('zen-notifications'); + const source = document.createElement('button'); + scope.append(source); + document.body.append(scope); + source.dispatchEvent(new CustomEvent('zen-notify', { + bubbles: true, + composed: true, + detail: { + version: 1, + id: 'composed', + tone: 'info', + message: 'Must be rejected', + announcement: 'none', + persistent: true, + }, + })); + return scope.size; + }); + assert.equal(composedRejected, 0); + + await page.evaluate(() => { + const scope = document.createElement('zen-notifications'); + scope.id = 'focus-timer-scope'; + const source = document.createElement('button'); + scope.append(source); + document.body.append(scope); + source.dispatchEvent(new CustomEvent('zen-notify', { + bubbles: true, + composed: false, + detail: { + version: 1, + id: 'focus-timer', + tone: 'warning', + message: 'Focus timer', + announcement: 'none', + durationMs: 300, + action: { token: 'focus-token', label: 'Keep focused' }, + }, + })); + }); + const focusTimer = page.locator( + '#focus-timer-scope [data-zen-notification-id="focus-timer"]', + ); + await focusTimer.locator('.zen-notification-action').focus(); + await page.locator('#focus-timer-scope').evaluate(scope => { + const source = scope.querySelector(':scope > button'); + source.dispatchEvent(new CustomEvent('zen-notify', { + bubbles: true, + composed: false, + detail: { + version: 1, + id: 'focus-neighbor', + tone: 'info', + message: 'Focus neighbor', + announcement: 'none', + persistent: true, + }, + })); + }); + await page.waitForTimeout(500); + assert.equal(await focusTimer.count(), 1); + assert.equal( + await focusTimer.locator('.zen-notification-action').evaluate( + action => document.activeElement === action, + ), + true, + ); + await page.mouse.move(0, 0); + await page.locator('#catalogMain').focus(); + await page.waitForFunction(() => + !document.querySelector( + '#focus-timer-scope [data-zen-notification-id="focus-timer"]', + ) + ); + + await page.evaluate(() => { + const scope = document.createElement('zen-notifications'); + scope.id = 'timer-scope'; + const source = document.createElement('button'); + scope.append(source); + document.body.append(scope); + source.dispatchEvent(new CustomEvent('zen-notify', { + bubbles: true, + composed: false, + detail: { + version: 1, + id: 'timer', + tone: 'info', + message: 'Paused timer', + announcement: 'none', + durationMs: 300, + }, + })); + }); + const timerArticle = page.locator('#timer-scope article'); + await timerArticle.hover(); + await page.waitForTimeout(500); + assert.equal(await timerArticle.count(), 1); + await page.mouse.move(0, 0); + await page.waitForFunction(() => + !document.querySelector('#timer-scope article') + ); + + await page.evaluate(() => { + const scope = document.createElement('zen-notifications'); + scope.id = 'hidden-timer-scope'; + const source = document.createElement('button'); + scope.append(source); + document.body.append(scope); + source.dispatchEvent(new CustomEvent('zen-notify', { + bubbles: true, + composed: false, + detail: { + version: 1, + id: 'hidden-timer', + tone: 'info', + message: 'Hidden timer', + announcement: 'none', + durationMs: 300, + }, + })); + Object.defineProperty(document, 'hidden', { + configurable: true, + value: true, + }); + document.dispatchEvent(new Event('visibilitychange')); + }); + await page.waitForTimeout(500); + assert.equal( + await page.locator('#hidden-timer-scope article').count(), + 1, + ); + await page.evaluate(() => { + Object.defineProperty(document, 'hidden', { + configurable: true, + value: false, + }); + document.dispatchEvent(new Event('visibilitychange')); + }); + await page.waitForFunction(() => + !document.querySelector('#hidden-timer-scope article') + ); + + const isolated = await page.evaluate(() => { + const makeScope = id => { + const scope = document.createElement('zen-notifications'); + scope.id = id; + const source = document.createElement('button'); + scope.append(source); + document.body.append(scope); + return { scope, source }; + }; + const first = makeScope('scope-one'); + const second = makeScope('scope-two'); + first.source.dispatchEvent(new CustomEvent('zen-notify', { + bubbles: true, + composed: false, + detail: { + version: 1, + id: 'isolated', + tone: 'success', + message: 'Only first scope', + announcement: 'none', + persistent: true, + }, + })); + return [first.scope.size, second.scope.size]; + }); + assert.deepEqual(isolated, [1, 0]); + const lightCanvas = await page.evaluate(() => getComputedStyle(document.documentElement) .getPropertyValue('--zen-color-canvas')