From cd80c51f8c3a2f7a1979852446e44787f455bfc7 Mon Sep 17 00:00:00 2001 From: testvalue Date: Thu, 2 Apr 2026 14:09:28 -0400 Subject: [PATCH 1/3] refactor(ui): replaces corvu Drawer with Kobalte Dialog --- package.json | 1 - pnpm-lock.yaml | 206 ------------------ .../components/shared/NotificationDrawer.tsx | 24 +- src/app/index.css | 22 +- tests/components/layout/Header.test.tsx | 2 +- .../shared/NotificationDrawer.test.tsx | 9 +- tsconfig.json | 3 +- 7 files changed, 28 insertions(+), 239 deletions(-) diff --git a/package.json b/package.json index 55ab11ac..598102a3 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,6 @@ "@octokit/plugin-throttling": "11.0.3", "@sentry/solid": "10.46.0", "@solidjs/router": "0.16.1", - "corvu": "0.7.2", "idb": "8.0.3", "solid-js": "1.9.11", "zod": "4.3.6" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 517be871..f69e6ddb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,9 +29,6 @@ importers: '@solidjs/router': specifier: 0.16.1 version: 0.16.1(solid-js@1.9.11) - corvu: - specifier: 0.7.2 - version: 0.7.2(solid-js@1.9.11) idb: specifier: 8.0.3 version: 8.0.3 @@ -231,56 +228,6 @@ packages: cpu: [x64] os: [win32] - '@corvu/accordion@0.2.5': - resolution: {integrity: sha512-oBZKDZZNN5mirmilSzWSrY+7PQGHQbXAlWm1SOvG1AAsIeX5osUdQnKjH0XzlkkAxCLKHb41178W4YNNPqH0PA==} - peerDependencies: - solid-js: ^1.8 - - '@corvu/calendar@0.1.2': - resolution: {integrity: sha512-Vbdns1iNNI0WTIwDZS7m9pHWtr/YTz7yrisXhHUCfl4mxDvb9W8MMmun651wiEjhL7+OrcNbbl2kTOmYSFIf4g==} - peerDependencies: - solid-js: ^1.8 - - '@corvu/dialog@0.2.4': - resolution: {integrity: sha512-n54vJq+fOy8GVrnYBdJpD6JXNuyx7LOeMrRxwzAvZnYGpW8+AA12tnb/P/2emJj/HjOO5otheGKb0breshdFlA==} - peerDependencies: - solid-js: ^1.8 - - '@corvu/disclosure@0.2.2': - resolution: {integrity: sha512-d2E9zS0w9peeOiBDc8/ZcqS56ZeOIiKJcwMFpLjuhlfrtVAepwyhbwFHNQTb48VeRHW8amfrGliymT//VQFF9g==} - peerDependencies: - solid-js: ^1.8 - - '@corvu/drawer@0.2.4': - resolution: {integrity: sha512-7jQoGZ8ROB9CmXam2nMY2wEskU3IoFwZQywkF/7vrBc/edGsPv7mOVQ1GN6G+4nd7nrMZ3UtqHAhmRh9V0azlw==} - peerDependencies: - solid-js: ^1.8 - - '@corvu/otp-field@0.1.4': - resolution: {integrity: sha512-3eG7OoUt6CfVqGujIYfqImdrhGR/s4DpKr5ZQT10zzw3nawIlcwVpqoHTam0v4cgv+NXXvl6I8DoA3J+WgW2YA==} - peerDependencies: - solid-js: ^1.8 - - '@corvu/popover@0.2.0': - resolution: {integrity: sha512-zynRWyRV7mk2uX7t/COQL4OEa6H6FBgOxxlXLQJ1BV8hTiBJwmfAN/yB/9jy+zlp0G9vp64E8VdsNn6gxUEUEQ==} - peerDependencies: - solid-js: ^1.8 - - '@corvu/resizable@0.2.5': - resolution: {integrity: sha512-psax38DraM9SXtaNh/sfvizGO6xLCL4sqT4rWYlMZP5ZsUM05Q30tvXgnTo2roQFNOz2e06WofhDsBXTjY6g4A==} - peerDependencies: - solid-js: ^1.8 - - '@corvu/tooltip@0.2.2': - resolution: {integrity: sha512-gyOBJ43GNAecDtmWc68A5YJvifXWwK0VZZdTOcFbX0rVQUA2ihBMltRE4oj7/lKidg2gXALjmFr71Blt5s3YuQ==} - peerDependencies: - solid-js: ^1.8 - - '@corvu/utils@0.3.2': - resolution: {integrity: sha512-ZWlyWEE8qV9+CB9OAyo2bTrZGXQN9ZeM+JfYv89zoR+lRACKTDuoOZEdiyL8Uc7U5dUSH1uTqKhTTnaHWb+wZA==} - peerDependencies: - solid-js: ^1.8 - '@corvu/utils@0.4.2': resolution: {integrity: sha512-Ox2kYyxy7NoXdKWdHeDEjZxClwzO4SKM8plAaVwmAJPxHMqA0rLOoAsa+hBDwRLpctf+ZRnAd/ykguuJidnaTA==} peerDependencies: @@ -879,11 +826,6 @@ packages: peerDependencies: solid-js: ^1.6.12 - '@solid-primitives/memo@1.4.5': - resolution: {integrity: sha512-dMfFShNsyX5virETyDv/Uoy2HP+PL4k8cUTTLb2r4TfoqJb010KIaOuURqp/Qbdznp4ZkDuP57b28d45kaOueQ==} - peerDependencies: - solid-js: ^1.6.12 - '@solid-primitives/props@3.2.3': resolution: {integrity: sha512-XzG6en9gSFwmvbKcATm2BxL63HegZ+BAG5fmHi8jyBppQHcaths7ffz+6vYvwYy3nlgLa20ufJLj7tst+PcHFA==} peerDependencies: @@ -904,11 +846,6 @@ packages: peerDependencies: solid-js: ^1.6.12 - '@solid-primitives/scheduled@1.5.3': - resolution: {integrity: sha512-oNwLE6E6lxJAWrc8QXuwM0k2oU1BnANnkChwMw82aK1j3+mWGJkG1IFe5gCwbV+afYmjI76t9JJV3md/8tLw+g==} - peerDependencies: - solid-js: ^1.6.12 - '@solid-primitives/static-store@0.1.3': resolution: {integrity: sha512-uxez7SXnr5GiRnzqO2IEDjOJRIXaG+0LZLBizmUA1FwSi+hrpuMzVBwyk70m4prcl8X6FDDXUl9O8hSq8wHbBQ==} peerDependencies: @@ -1182,11 +1119,6 @@ packages: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} - corvu@0.7.2: - resolution: {integrity: sha512-OpBHnX7NYceHif3/OPu4Tkv9JmoM+WJFI9SjHKciE/PnWKgADqbRhViX2GvfUH5WMNMlb3MVAYmfykYJDECwtA==} - peerDependencies: - solid-js: ^1.8 - csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} @@ -1496,34 +1428,14 @@ packages: siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - solid-dismissible@0.1.1: - resolution: {integrity: sha512-9kcKBJIMdS+586cA1g63HYWxKh3h89leeNHbPZ1csYjuni+NvPBtNr11l0iEX2AKKEt6FHk6qNhc/gjoYAW1pA==} - peerDependencies: - solid-js: ^1.8 - - solid-focus-trap@0.1.9: - resolution: {integrity: sha512-LTyNki6GUJPRLXV5uMWPkYClB07SUMubbr2EkAddiR0CJCF/I283txilMU9RURSr/P8EewMfXWu2o3aWrK7A5A==} - peerDependencies: - solid-js: ^1.8 - solid-js@1.9.11: resolution: {integrity: sha512-WEJtcc5mkh/BnHA6Yrg4whlF8g6QwpmXXRg4P2ztPmcKeHHlH4+djYecBLhSpecZY2RRECXYUwIc/C2r3yzQ4Q==} - solid-list@0.3.0: - resolution: {integrity: sha512-t4hx/F/l8Vmq+ib9HtZYl7Z9F1eKxq3eKJTXlvcm7P7yI4Z8O7QSOOEVHb/K6DD7M0RxzVRobK/BS5aSfLRwKg==} - peerDependencies: - solid-js: ^1.8 - solid-presence@0.1.8: resolution: {integrity: sha512-pWGtXUFWYYUZNbg5YpG5vkQJyOtzn2KXhxYaMx/4I+lylTLYkITOLevaCwMRN+liCVk0pqB6EayLWojNqBFECA==} peerDependencies: solid-js: ^1.8 - solid-presence@0.2.0: - resolution: {integrity: sha512-YM92o+jvpzX3XGaD4rLYmq/Kc2ZVh47GSCLEufHBFQQIurvZTs8SoGJxO8BJGNDxBKdcS8F3dYhW1SDXp4BNjA==} - peerDependencies: - solid-js: ^1.8 - solid-prevent-scroll@0.1.10: resolution: {integrity: sha512-KplGPX2GHiWJLZ6AXYRql4M127PdYzfwvLJJXMkO+CMb8Np4VxqDAg5S8jLdwlEuBis/ia9DKw2M8dFx5u8Mhw==} peerDependencies: @@ -1534,11 +1446,6 @@ packages: peerDependencies: solid-js: ^1.3 - solid-transition-size@0.1.4: - resolution: {integrity: sha512-ocHVnbfy23CgfaH4cEUR/AFg0Y3CEL8Oh3n9Qv8OHFJgPh+zkmERKZQfi/xH5XvxDCizg8VjPrVUhiHB1Gza8g==} - peerDependencies: - solid-js: ^1.8 - source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -1927,71 +1834,6 @@ snapshots: '@cloudflare/workerd-windows-64@1.20260317.1': optional: true - '@corvu/accordion@0.2.5(solid-js@1.9.11)': - dependencies: - '@corvu/disclosure': 0.2.2(solid-js@1.9.11) - '@corvu/utils': 0.4.2(solid-js@1.9.11) - solid-js: 1.9.11 - solid-list: 0.3.0(solid-js@1.9.11) - - '@corvu/calendar@0.1.2(solid-js@1.9.11)': - dependencies: - '@corvu/utils': 0.4.2(solid-js@1.9.11) - solid-js: 1.9.11 - - '@corvu/dialog@0.2.4(solid-js@1.9.11)': - dependencies: - '@corvu/utils': 0.4.2(solid-js@1.9.11) - solid-dismissible: 0.1.1(solid-js@1.9.11) - solid-focus-trap: 0.1.9(solid-js@1.9.11) - solid-js: 1.9.11 - solid-presence: 0.2.0(solid-js@1.9.11) - solid-prevent-scroll: 0.1.10(solid-js@1.9.11) - - '@corvu/disclosure@0.2.2(solid-js@1.9.11)': - dependencies: - '@corvu/utils': 0.4.2(solid-js@1.9.11) - solid-js: 1.9.11 - solid-presence: 0.2.0(solid-js@1.9.11) - - '@corvu/drawer@0.2.4(solid-js@1.9.11)': - dependencies: - '@corvu/dialog': 0.2.4(solid-js@1.9.11) - '@corvu/utils': 0.4.2(solid-js@1.9.11) - '@solid-primitives/memo': 1.4.5(solid-js@1.9.11) - solid-js: 1.9.11 - solid-transition-size: 0.1.4(solid-js@1.9.11) - - '@corvu/otp-field@0.1.4(solid-js@1.9.11)': - dependencies: - '@corvu/utils': 0.4.2(solid-js@1.9.11) - solid-js: 1.9.11 - - '@corvu/popover@0.2.0(solid-js@1.9.11)': - dependencies: - '@corvu/dialog': 0.2.4(solid-js@1.9.11) - '@corvu/utils': 0.3.2(solid-js@1.9.11) - '@floating-ui/dom': 1.7.6 - solid-js: 1.9.11 - - '@corvu/resizable@0.2.5(solid-js@1.9.11)': - dependencies: - '@corvu/utils': 0.4.2(solid-js@1.9.11) - solid-js: 1.9.11 - - '@corvu/tooltip@0.2.2(solid-js@1.9.11)': - dependencies: - '@corvu/utils': 0.4.2(solid-js@1.9.11) - '@floating-ui/dom': 1.7.6 - solid-dismissible: 0.1.1(solid-js@1.9.11) - solid-js: 1.9.11 - solid-presence: 0.2.0(solid-js@1.9.11) - - '@corvu/utils@0.3.2(solid-js@1.9.11)': - dependencies: - '@floating-ui/dom': 1.7.6 - solid-js: 1.9.11 - '@corvu/utils@0.4.2(solid-js@1.9.11)': dependencies: '@floating-ui/dom': 1.7.6 @@ -2451,12 +2293,6 @@ snapshots: '@solid-primitives/utils': 6.4.0(solid-js@1.9.11) solid-js: 1.9.11 - '@solid-primitives/memo@1.4.5(solid-js@1.9.11)': - dependencies: - '@solid-primitives/scheduled': 1.5.3(solid-js@1.9.11) - '@solid-primitives/utils': 6.4.0(solid-js@1.9.11) - solid-js: 1.9.11 - '@solid-primitives/props@3.2.3(solid-js@1.9.11)': dependencies: '@solid-primitives/utils': 6.4.0(solid-js@1.9.11) @@ -2480,10 +2316,6 @@ snapshots: '@solid-primitives/utils': 6.4.0(solid-js@1.9.11) solid-js: 1.9.11 - '@solid-primitives/scheduled@1.5.3(solid-js@1.9.11)': - dependencies: - solid-js: 1.9.11 - '@solid-primitives/static-store@0.1.3(solid-js@1.9.11)': dependencies: '@solid-primitives/utils': 6.4.0(solid-js@1.9.11) @@ -2740,19 +2572,6 @@ snapshots: cookie@1.1.1: {} - corvu@0.7.2(solid-js@1.9.11): - dependencies: - '@corvu/accordion': 0.2.5(solid-js@1.9.11) - '@corvu/calendar': 0.1.2(solid-js@1.9.11) - '@corvu/dialog': 0.2.4(solid-js@1.9.11) - '@corvu/disclosure': 0.2.2(solid-js@1.9.11) - '@corvu/drawer': 0.2.4(solid-js@1.9.11) - '@corvu/otp-field': 0.1.4(solid-js@1.9.11) - '@corvu/popover': 0.2.0(solid-js@1.9.11) - '@corvu/resizable': 0.2.5(solid-js@1.9.11) - '@corvu/tooltip': 0.2.2(solid-js@1.9.11) - solid-js: 1.9.11 - csstype@3.2.3: {} daisyui@5.5.19: {} @@ -3048,37 +2867,17 @@ snapshots: siginfo@2.0.0: {} - solid-dismissible@0.1.1(solid-js@1.9.11): - dependencies: - '@corvu/utils': 0.4.2(solid-js@1.9.11) - solid-js: 1.9.11 - - solid-focus-trap@0.1.9(solid-js@1.9.11): - dependencies: - '@corvu/utils': 0.4.2(solid-js@1.9.11) - solid-js: 1.9.11 - solid-js@1.9.11: dependencies: csstype: 3.2.3 seroval: 1.5.1 seroval-plugins: 1.5.1(seroval@1.5.1) - solid-list@0.3.0(solid-js@1.9.11): - dependencies: - '@corvu/utils': 0.4.2(solid-js@1.9.11) - solid-js: 1.9.11 - solid-presence@0.1.8(solid-js@1.9.11): dependencies: '@corvu/utils': 0.4.2(solid-js@1.9.11) solid-js: 1.9.11 - solid-presence@0.2.0(solid-js@1.9.11): - dependencies: - '@corvu/utils': 0.4.2(solid-js@1.9.11) - solid-js: 1.9.11 - solid-prevent-scroll@0.1.10(solid-js@1.9.11): dependencies: '@corvu/utils': 0.4.2(solid-js@1.9.11) @@ -3093,11 +2892,6 @@ snapshots: transitivePeerDependencies: - supports-color - solid-transition-size@0.1.4(solid-js@1.9.11): - dependencies: - '@corvu/utils': 0.3.2(solid-js@1.9.11) - solid-js: 1.9.11 - source-map-js@1.2.1: {} stackback@0.0.2: {} diff --git a/src/app/components/shared/NotificationDrawer.tsx b/src/app/components/shared/NotificationDrawer.tsx index 58d08e00..0cf34b94 100644 --- a/src/app/components/shared/NotificationDrawer.tsx +++ b/src/app/components/shared/NotificationDrawer.tsx @@ -1,5 +1,5 @@ import { createMemo, For, Show } from "solid-js"; -import Drawer from "corvu/drawer"; +import { Dialog } from "@kobalte/core/dialog"; import { getNotifications, markAllAsRead, @@ -25,15 +25,15 @@ export default function NotificationDrawer(props: NotificationDrawerProps) { } return ( - !open && props.onClose()} side="right"> - - - + !open && props.onClose()} modal> + + + {/* Header */}
- + Notifications - + - @@ -59,7 +59,7 @@ export default function NotificationDrawer(props: NotificationDrawerProps) { clip-rule="evenodd" /> - +
{/* Notification list */} @@ -129,8 +129,8 @@ export default function NotificationDrawer(props: NotificationDrawerProps) { -
-
-
+ + + ); } diff --git a/src/app/index.css b/src/app/index.css index 926851e2..b2c7d6cb 100644 --- a/src/app/index.css +++ b/src/app/index.css @@ -92,21 +92,19 @@ animation: toast-slide-out 0.3s ease-in forwards; } -@utility animate-drawer-in { - animation: drawer-slide-in 0.3s ease-out forwards; -} - -@utility animate-drawer-out { +/* Kobalte Dialog drawer animations */ +.drawer-content[data-closed] { animation: drawer-slide-out 0.3s ease-in forwards; } - -@utility animate-overlay-in { - animation: overlay-fade-in 0.3s ease-out forwards; +.drawer-content[data-expanded] { + animation: drawer-slide-in 0.3s ease-out forwards; } - -@utility animate-overlay-out { +.drawer-overlay[data-closed] { animation: overlay-fade-out 0.3s ease-in forwards; } +.drawer-overlay[data-expanded] { + animation: overlay-fade-in 0.3s ease-out forwards; +} /* ── Kobalte → daisyUI bridges ───────────────────────────────────────────── */ @@ -129,8 +127,8 @@ @media (prefers-reduced-motion: reduce) { .animate-slow-pulse, .animate-toast-in, .animate-toast-out, - .animate-drawer-in, .animate-drawer-out, - .animate-overlay-in, .animate-overlay-out, + .drawer-content[data-expanded], .drawer-content[data-closed], + .drawer-overlay[data-expanded], .drawer-overlay[data-closed], .animate-shimmer, .animate-flash, .animate-reorder-highlight, .loading { animation: none; diff --git a/tests/components/layout/Header.test.tsx b/tests/components/layout/Header.test.tsx index 79f6e3e6..d3bfb788 100644 --- a/tests/components/layout/Header.test.tsx +++ b/tests/components/layout/Header.test.tsx @@ -145,7 +145,7 @@ describe("Header", () => { const bellBtn = screen.getByLabelText("Notifications"); await user.click(bellBtn); expect(bellBtn.getAttribute("aria-expanded")).toBe("true"); - // Close via drawer close button (corvu sets pointer-events:none on body while open) + // Close via drawer close button (Kobalte Dialog modal sets pointer-events:none on body while open) const closeBtn = screen.getByLabelText("Close notifications"); fireEvent.click(closeBtn); expect(bellBtn.getAttribute("aria-expanded")).toBe("false"); diff --git a/tests/components/shared/NotificationDrawer.test.tsx b/tests/components/shared/NotificationDrawer.test.tsx index 5f6c5af0..2bb75a0c 100644 --- a/tests/components/shared/NotificationDrawer.test.tsx +++ b/tests/components/shared/NotificationDrawer.test.tsx @@ -1,6 +1,5 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { render, screen, fireEvent } from "@solidjs/testing-library"; -import userEvent from "@testing-library/user-event"; import { createSignal } from "solid-js"; import { pushNotification, @@ -108,15 +107,13 @@ describe("NotificationDrawer", () => { expect(isMuted("search")).toBe(true); }); - it("calls onClose when overlay backdrop is clicked", async () => { - const user = userEvent.setup({ delay: null }); + it("calls onClose when overlay backdrop is clicked", () => { const onClose = vi.fn(); render(() => ); vi.advanceTimersByTime(0); - // corvu drawer overlay - const overlay = document.body.querySelector("[data-corvu-drawer-overlay]") as HTMLElement; + const overlay = document.body.querySelector("[data-testid='notification-overlay']") as HTMLElement; expect(overlay).not.toBeNull(); - await user.click(overlay); + fireEvent.pointerDown(overlay); expect(onClose).toHaveBeenCalled(); }); diff --git a/tsconfig.json b/tsconfig.json index 1a1a216d..3dc63ab1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,8 @@ "noUnusedLocals": true, "noUnusedParameters": true, "types": ["vite/client"], - "lib": ["ESNext", "DOM", "DOM.Iterable"] + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "skipLibCheck": true }, "include": ["src/**/*", "tests/**/*"], "exclude": ["node_modules", "dist"] From 5fdc7a045daa188ca2486f2d3f892bd829101c86 Mon Sep 17 00:00:00 2001 From: testvalue Date: Thu, 2 Apr 2026 14:40:40 -0400 Subject: [PATCH 2/3] fix(a11y): adds Dialog.Description and idiomatic test query --- src/app/components/shared/NotificationDrawer.tsx | 1 + tests/components/shared/NotificationDrawer.test.tsx | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/components/shared/NotificationDrawer.tsx b/src/app/components/shared/NotificationDrawer.tsx index 0cf34b94..01ee0b48 100644 --- a/src/app/components/shared/NotificationDrawer.tsx +++ b/src/app/components/shared/NotificationDrawer.tsx @@ -29,6 +29,7 @@ export default function NotificationDrawer(props: NotificationDrawerProps) { + Recent system notifications and errors {/* Header */}
diff --git a/tests/components/shared/NotificationDrawer.test.tsx b/tests/components/shared/NotificationDrawer.test.tsx index 2bb75a0c..3f2c7d72 100644 --- a/tests/components/shared/NotificationDrawer.test.tsx +++ b/tests/components/shared/NotificationDrawer.test.tsx @@ -111,8 +111,7 @@ describe("NotificationDrawer", () => { const onClose = vi.fn(); render(() => ); vi.advanceTimersByTime(0); - const overlay = document.body.querySelector("[data-testid='notification-overlay']") as HTMLElement; - expect(overlay).not.toBeNull(); + const overlay = screen.getByTestId("notification-overlay"); fireEvent.pointerDown(overlay); expect(onClose).toHaveBeenCalled(); }); From e1ea995530834a7e78ff526a82df11adf16da16f Mon Sep 17 00:00:00 2001 From: testvalue Date: Thu, 2 Apr 2026 16:12:27 -0400 Subject: [PATCH 3/3] fix(test): tightens NotificationDrawer assertions and adds coverage --- src/app/index.css | 3 +- .../shared/NotificationDrawer.test.tsx | 39 +++++++++++++++++-- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/src/app/index.css b/src/app/index.css index b2c7d6cb..fd89cab7 100644 --- a/src/app/index.css +++ b/src/app/index.css @@ -92,7 +92,8 @@ animation: toast-slide-out 0.3s ease-in forwards; } -/* Kobalte Dialog drawer animations */ +/* Kobalte Dialog drawer animations — uses data-attribute selectors instead of @utility + because Kobalte sets [data-expanded]/[data-closed] based on open/closed state */ .drawer-content[data-closed] { animation: drawer-slide-out 0.3s ease-in forwards; } diff --git a/tests/components/shared/NotificationDrawer.test.tsx b/tests/components/shared/NotificationDrawer.test.tsx index 3f2c7d72..94f70dd2 100644 --- a/tests/components/shared/NotificationDrawer.test.tsx +++ b/tests/components/shared/NotificationDrawer.test.tsx @@ -53,6 +53,7 @@ describe("NotificationDrawer", () => { vi.advanceTimersByTime(0); expect(screen.getByText(/Something failed/)).toBeDefined(); expect(screen.getByText(/api/)).toBeDefined(); + expect(screen.queryByText("(will retry)")).toBeNull(); }); it("shows newest notification first in the list", () => { @@ -108,12 +109,15 @@ describe("NotificationDrawer", () => { }); it("calls onClose when overlay backdrop is clicked", () => { + // Kobalte dismisses via document-level capture-phase pointerdown (createInteractOutside), + // not an overlay click handler. data-testid targets the overlay because Dialog.Overlay + // has no ARIA role to query by. const onClose = vi.fn(); render(() => ); vi.advanceTimersByTime(0); const overlay = screen.getByTestId("notification-overlay"); fireEvent.pointerDown(overlay); - expect(onClose).toHaveBeenCalled(); + expect(onClose).toHaveBeenCalledTimes(1); }); it("calls onClose when X button is clicked", () => { @@ -121,7 +125,7 @@ describe("NotificationDrawer", () => { render(() => ); vi.advanceTimersByTime(0); fireEvent.click(screen.getByLabelText("Close notifications")); - expect(onClose).toHaveBeenCalled(); + expect(onClose).toHaveBeenCalledTimes(1); }); it("shows empty state text when no notifications", () => { @@ -159,6 +163,35 @@ describe("NotificationDrawer", () => { render(() => ); vi.advanceTimersByTime(0); fireEvent.keyDown(document, { key: "Escape" }); - expect(onClose).toHaveBeenCalled(); + expect(onClose).toHaveBeenCalledTimes(1); + }); + + it("shows retryable indicator when notification is retryable", () => { + pushNotification("api", "Transient failure", "error", true); + renderDrawer(true); + vi.advanceTimersByTime(0); + expect(screen.queryByText("(will retry)")).not.toBeNull(); + }); + + it("marks dialog as closed when open transitions from true to false", () => { + // solid-presence waits for animationend to unmount; happy-dom has no CSS engine + // so the element stays in DOM with data-closed instead of being removed + const { setIsOpen } = renderDrawer(true); + vi.advanceTimersByTime(0); + const dialog = screen.getByRole("dialog"); + expect(dialog.hasAttribute("data-closed")).toBe(false); + setIsOpen(false); + vi.advanceTimersByTime(0); + expect(dialog.hasAttribute("data-closed")).toBe(true); + }); + + it("has accessible description for screen readers", () => { + renderDrawer(true); + vi.advanceTimersByTime(0); + const dialog = screen.getByRole("dialog"); + const descId = dialog.getAttribute("aria-describedby"); + expect(descId).toBeTruthy(); + const descEl = document.getElementById(descId!); + expect(descEl?.textContent).toContain("Recent system notifications and errors"); }); });