comparison 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
comparison
equal deleted inserted replaced
251:117c4d53c9a4 252:7a7581f040e8
123 '/components', 123 '/components',
124 '/components/button', 124 '/components/button',
125 '/components/card', 125 '/components/card',
126 '/components/alert', 126 '/components/alert',
127 '/components/field', 127 '/components/field',
128 '/components/notifications',
128 '/components/stack', 129 '/components/stack',
129 ]) { 130 ]) {
130 const response = await fetch(`${baseUrl}${route}`); 131 const response = await fetch(`${baseUrl}${route}`);
131 assert.equal(response.status, 200, route); 132 assert.equal(response.status, 200, route);
132 assert.match( 133 assert.match(
348 }; 349 };
349 }); 350 });
350 assert.equal(authoredDismissState.connected, true); 351 assert.equal(authoredDismissState.connected, true);
351 assert.equal(authoredDismissState.events, 0); 352 assert.equal(authoredDismissState.events, 0);
352 353
354 await page.goto(`${baseUrl}/components/notifications`, {
355 waitUntil: 'networkidle',
356 });
357 const notificationScope = page.locator('zen-notifications').first();
358 const notificationPosition = await notificationScope.locator(
359 '[data-zen-notification-stack]',
360 ).evaluate(stack => {
361 const style = getComputedStyle(stack);
362 return {
363 bottom: style.bottom,
364 position: style.position,
365 right: style.right,
366 };
367 });
368 assert.equal(notificationPosition.position, 'fixed');
369 assert.notEqual(notificationPosition.bottom, 'auto');
370 assert.notEqual(notificationPosition.right, 'auto');
371
372 const dedupe = await notificationScope.evaluate(scope => {
373 const source = scope.querySelector('[data-notification-demo]');
374 const send = detail => source.dispatchEvent(
375 new CustomEvent('zen-notify', {
376 bubbles: true,
377 composed: false,
378 detail,
379 }),
380 );
381 send({
382 version: 1,
383 id: 'dedupe',
384 tone: 'info',
385 message: 'First announcement',
386 announcement: 'polite',
387 persistent: true,
388 });
389 send({
390 version: 1,
391 id: 'dedupe',
392 tone: 'success',
393 message: 'Updated without duplication',
394 announcement: 'polite',
395 persistent: true,
396 });
397 send({ version: 99, id: 'invalid' });
398 return {
399 size: scope.size,
400 visible: scope.visibleCount,
401 };
402 });
403 assert.deepEqual(dedupe, { size: 1, visible: 1 });
404 assert.equal(
405 await notificationScope.locator('article').count(),
406 1,
407 );
408 assert.match(
409 await notificationScope.locator('article').textContent(),
410 /Updated without duplication/,
411 );
412 await page.waitForFunction(() =>
413 document.querySelector(
414 'zen-notifications [data-zen-live="polite"]',
415 )?.textContent === 'First announcement'
416 );
417
418 const bounded = await notificationScope.evaluate(scope => {
419 let accepted = 0;
420 for (let index = 0; index < 25; index++) {
421 if (scope.notify({
422 version: 1,
423 id: `bounded-${index}`,
424 tone: 'info',
425 message: `Bounded ${index}`,
426 announcement: 'none',
427 durationMs: 120000,
428 })) accepted++;
429 }
430 return {
431 accepted,
432 size: scope.size,
433 visible: scope.visibleCount,
434 };
435 });
436 assert.deepEqual(bounded, { accepted: 25, size: 23, visible: 3 });
437 assert.equal(
438 await notificationScope.locator('article').count(),
439 3,
440 );
441
442 const actionScope = await page.evaluate(() => {
443 const scope = document.createElement('zen-notifications');
444 scope.id = 'action-scope';
445 const source = document.createElement('button');
446 scope.append(source);
447 document.body.append(scope);
448 window.__notificationAction = null;
449 scope.addEventListener('zen-notification-action', event => {
450 window.__notificationAction = event.detail;
451 });
452 source.dispatchEvent(new CustomEvent('zen-notify', {
453 bubbles: true,
454 composed: false,
455 detail: {
456 version: 1,
457 id: 'action',
458 tone: 'error',
459 message: 'Action required',
460 announcement: 'assertive',
461 persistent: true,
462 action: {
463 token: 'opaque-secret-token',
464 label: 'Retry',
465 },
466 },
467 }));
468 return scope.id;
469 });
470 assert.equal(
471 await page.locator(`#${actionScope}`).evaluate(
472 scope => scope.records,
473 ),
474 undefined,
475 );
476 const actionArticle = page.locator(
477 `#${actionScope} [data-zen-notification-id="action"]`,
478 );
479 assert.doesNotMatch(
480 await actionArticle.evaluate(article => article.outerHTML),
481 /opaque-secret-token/,
482 );
483 await actionArticle.locator('.zen-notification-action').focus();
484 await page.locator(`#${actionScope}`).evaluate(scope => {
485 const source = scope.querySelector('button');
486 source.dispatchEvent(new CustomEvent('zen-notify', {
487 bubbles: true,
488 composed: false,
489 detail: {
490 version: 1,
491 id: 'action-neighbor',
492 tone: 'info',
493 message: 'Neighbor',
494 announcement: 'none',
495 persistent: true,
496 },
497 }));
498 });
499 assert.equal(
500 await actionArticle.locator('.zen-notification-action').evaluate(
501 action => document.activeElement === action,
502 ),
503 true,
504 );
505 await actionArticle.locator('.zen-notification-action').click();
506 assert.deepEqual(
507 await page.evaluate(() => window.__notificationAction),
508 {
509 version: 1,
510 id: 'action',
511 token: 'opaque-secret-token',
512 },
513 );
514 await page.locator(`#${actionScope}`).evaluate(scope => {
515 const source = scope.querySelector('button');
516 source.dispatchEvent(new CustomEvent(
517 'zen-dismiss-notification',
518 {
519 bubbles: true,
520 composed: false,
521 detail: { version: 1, id: 'action' },
522 },
523 ));
524 });
525 assert.equal(await actionArticle.count(), 0);
526
527 await page.evaluate(() => {
528 const scope = document.createElement('zen-notifications');
529 scope.id = 'announcement-scope';
530 const source = document.createElement('button');
531 scope.append(source);
532 document.body.append(scope);
533 window.__announcements = [];
534 const region = scope.querySelector('[data-zen-live="polite"]');
535 new MutationObserver(() => {
536 if (region.textContent) {
537 window.__announcements.push(region.textContent);
538 }
539 }).observe(region, { childList: true });
540 for (let index = 0; index < 3; index++) {
541 source.dispatchEvent(new CustomEvent('zen-notify', {
542 bubbles: true,
543 composed: false,
544 detail: {
545 version: 1,
546 id: `announcement-${index}`,
547 tone: 'info',
548 message: `Announcement ${index}`,
549 announcement: 'polite',
550 persistent: true,
551 },
552 }));
553 }
554 });
555 await page.waitForFunction(() =>
556 window.__announcements?.length === 3
557 );
558 assert.deepEqual(
559 await page.evaluate(() => window.__announcements),
560 ['Announcement 0', 'Announcement 1', 'Announcement 2'],
561 );
562
563 const composedRejected = await page.evaluate(() => {
564 const scope = document.createElement('zen-notifications');
565 const source = document.createElement('button');
566 scope.append(source);
567 document.body.append(scope);
568 source.dispatchEvent(new CustomEvent('zen-notify', {
569 bubbles: true,
570 composed: true,
571 detail: {
572 version: 1,
573 id: 'composed',
574 tone: 'info',
575 message: 'Must be rejected',
576 announcement: 'none',
577 persistent: true,
578 },
579 }));
580 return scope.size;
581 });
582 assert.equal(composedRejected, 0);
583
584 await page.evaluate(() => {
585 const scope = document.createElement('zen-notifications');
586 scope.id = 'focus-timer-scope';
587 const source = document.createElement('button');
588 scope.append(source);
589 document.body.append(scope);
590 source.dispatchEvent(new CustomEvent('zen-notify', {
591 bubbles: true,
592 composed: false,
593 detail: {
594 version: 1,
595 id: 'focus-timer',
596 tone: 'warning',
597 message: 'Focus timer',
598 announcement: 'none',
599 durationMs: 300,
600 action: { token: 'focus-token', label: 'Keep focused' },
601 },
602 }));
603 });
604 const focusTimer = page.locator(
605 '#focus-timer-scope [data-zen-notification-id="focus-timer"]',
606 );
607 await focusTimer.locator('.zen-notification-action').focus();
608 await page.locator('#focus-timer-scope').evaluate(scope => {
609 const source = scope.querySelector(':scope > button');
610 source.dispatchEvent(new CustomEvent('zen-notify', {
611 bubbles: true,
612 composed: false,
613 detail: {
614 version: 1,
615 id: 'focus-neighbor',
616 tone: 'info',
617 message: 'Focus neighbor',
618 announcement: 'none',
619 persistent: true,
620 },
621 }));
622 });
623 await page.waitForTimeout(500);
624 assert.equal(await focusTimer.count(), 1);
625 assert.equal(
626 await focusTimer.locator('.zen-notification-action').evaluate(
627 action => document.activeElement === action,
628 ),
629 true,
630 );
631 await page.mouse.move(0, 0);
632 await page.locator('#catalogMain').focus();
633 await page.waitForFunction(() =>
634 !document.querySelector(
635 '#focus-timer-scope [data-zen-notification-id="focus-timer"]',
636 )
637 );
638
639 await page.evaluate(() => {
640 const scope = document.createElement('zen-notifications');
641 scope.id = 'timer-scope';
642 const source = document.createElement('button');
643 scope.append(source);
644 document.body.append(scope);
645 source.dispatchEvent(new CustomEvent('zen-notify', {
646 bubbles: true,
647 composed: false,
648 detail: {
649 version: 1,
650 id: 'timer',
651 tone: 'info',
652 message: 'Paused timer',
653 announcement: 'none',
654 durationMs: 300,
655 },
656 }));
657 });
658 const timerArticle = page.locator('#timer-scope article');
659 await timerArticle.hover();
660 await page.waitForTimeout(500);
661 assert.equal(await timerArticle.count(), 1);
662 await page.mouse.move(0, 0);
663 await page.waitForFunction(() =>
664 !document.querySelector('#timer-scope article')
665 );
666
667 await page.evaluate(() => {
668 const scope = document.createElement('zen-notifications');
669 scope.id = 'hidden-timer-scope';
670 const source = document.createElement('button');
671 scope.append(source);
672 document.body.append(scope);
673 source.dispatchEvent(new CustomEvent('zen-notify', {
674 bubbles: true,
675 composed: false,
676 detail: {
677 version: 1,
678 id: 'hidden-timer',
679 tone: 'info',
680 message: 'Hidden timer',
681 announcement: 'none',
682 durationMs: 300,
683 },
684 }));
685 Object.defineProperty(document, 'hidden', {
686 configurable: true,
687 value: true,
688 });
689 document.dispatchEvent(new Event('visibilitychange'));
690 });
691 await page.waitForTimeout(500);
692 assert.equal(
693 await page.locator('#hidden-timer-scope article').count(),
694 1,
695 );
696 await page.evaluate(() => {
697 Object.defineProperty(document, 'hidden', {
698 configurable: true,
699 value: false,
700 });
701 document.dispatchEvent(new Event('visibilitychange'));
702 });
703 await page.waitForFunction(() =>
704 !document.querySelector('#hidden-timer-scope article')
705 );
706
707 const isolated = await page.evaluate(() => {
708 const makeScope = id => {
709 const scope = document.createElement('zen-notifications');
710 scope.id = id;
711 const source = document.createElement('button');
712 scope.append(source);
713 document.body.append(scope);
714 return { scope, source };
715 };
716 const first = makeScope('scope-one');
717 const second = makeScope('scope-two');
718 first.source.dispatchEvent(new CustomEvent('zen-notify', {
719 bubbles: true,
720 composed: false,
721 detail: {
722 version: 1,
723 id: 'isolated',
724 tone: 'success',
725 message: 'Only first scope',
726 announcement: 'none',
727 persistent: true,
728 },
729 }));
730 return [first.scope.size, second.scope.size];
731 });
732 assert.deepEqual(isolated, [1, 0]);
733
353 const lightCanvas = await page.evaluate(() => 734 const lightCanvas = await page.evaluate(() =>
354 getComputedStyle(document.documentElement) 735 getComputedStyle(document.documentElement)
355 .getPropertyValue('--zen-color-canvas') 736 .getPropertyValue('--zen-color-canvas')
356 .trim() 737 .trim()
357 ); 738 );