Mercurial
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 ); |