@open-mercato/ui 0.6.6-develop.6314.1.c7b8291aa2 → 0.6.6-develop.6330.1.a261878aa8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,124 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+
5
+ import * as React from 'react'
6
+ import { render, screen, fireEvent, within, act } from '@testing-library/react'
7
+ import { NotificationPanel } from '../NotificationPanel'
8
+ import type { NotificationDto } from '@open-mercato/shared/modules/notifications/types'
9
+ import type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'
10
+
11
+ // NotificationItem (rendered per row) calls next/navigation's useRouter, which
12
+ // throws outside an App Router provider. Stub it so the panel can render rows.
13
+ jest.mock('next/navigation', () => ({
14
+ useRouter: () => ({
15
+ push: jest.fn(),
16
+ replace: jest.fn(),
17
+ prefetch: jest.fn(),
18
+ refresh: jest.fn(),
19
+ back: jest.fn(),
20
+ forward: jest.fn(),
21
+ }),
22
+ }))
23
+
24
+ const t = ((key: string, fallback?: unknown) =>
25
+ typeof fallback === 'string' ? fallback : key) as TranslateFn
26
+
27
+ function buildNotification(overrides: Partial<NotificationDto>): NotificationDto {
28
+ return {
29
+ id: 'n',
30
+ type: 'system.generic',
31
+ title: 'Title',
32
+ severity: 'info',
33
+ status: 'unread',
34
+ actions: [],
35
+ createdAt: '2026-06-18T00:00:00.000Z',
36
+ ...overrides,
37
+ }
38
+ }
39
+
40
+ function buildProps(overrides: Partial<React.ComponentProps<typeof NotificationPanel>> = {}) {
41
+ return {
42
+ open: true,
43
+ onOpenChange: jest.fn(),
44
+ notifications: [] as NotificationDto[],
45
+ unreadCount: 0,
46
+ onMarkAsRead: jest.fn().mockResolvedValue(undefined),
47
+ onExecuteAction: jest.fn().mockResolvedValue({}),
48
+ onDismiss: jest.fn().mockResolvedValue(undefined),
49
+ onMarkAllRead: jest.fn().mockResolvedValue(undefined),
50
+ t,
51
+ ...overrides,
52
+ } satisfies React.ComponentProps<typeof NotificationPanel>
53
+ }
54
+
55
+ describe('NotificationPanel controls use shared primitives', () => {
56
+ it('renders the filter tabs through the Tabs primitive, not raw buttons', () => {
57
+ render(<NotificationPanel {...buildProps()} />)
58
+
59
+ const tablist = screen.getByRole('tablist')
60
+ const tabs = within(tablist).getAllByRole('tab')
61
+
62
+ expect(tabs).toHaveLength(3)
63
+ // The Tabs primitive stamps data-slot on every trigger; a raw <button>
64
+ // would not, so this guards against regressing back to hand-rolled markup.
65
+ tabs.forEach((tab) => expect(tab).toHaveAttribute('data-slot', 'tabs-trigger'))
66
+ })
67
+
68
+ it('marks the active filter with aria-selected and switches on click', () => {
69
+ render(<NotificationPanel {...buildProps()} />)
70
+
71
+ const allTab = screen.getByRole('tab', { name: /All/i })
72
+ const unreadTab = screen.getByRole('tab', { name: /Unread/i })
73
+
74
+ expect(allTab).toHaveAttribute('aria-selected', 'true')
75
+ expect(unreadTab).toHaveAttribute('aria-selected', 'false')
76
+
77
+ fireEvent.click(unreadTab)
78
+
79
+ expect(unreadTab).toHaveAttribute('aria-selected', 'true')
80
+ expect(allTab).toHaveAttribute('aria-selected', 'false')
81
+ })
82
+
83
+ it('shows the unread count badge on the Unread tab and caps it at 99+', () => {
84
+ const { rerender } = render(<NotificationPanel {...buildProps({ unreadCount: 5 })} />)
85
+ expect(within(screen.getByRole('tab', { name: /Unread/i })).getByText('5')).toBeInTheDocument()
86
+
87
+ rerender(<NotificationPanel {...buildProps({ unreadCount: 150 })} />)
88
+ expect(within(screen.getByRole('tab', { name: /Unread/i })).getByText('99+')).toBeInTheDocument()
89
+ })
90
+
91
+ it('renders Mark all read as a Button primitive and invokes the handler', async () => {
92
+ const onMarkAllRead = jest.fn().mockResolvedValue(undefined)
93
+ render(<NotificationPanel {...buildProps({ unreadCount: 3, onMarkAllRead })} />)
94
+
95
+ const markAllButton = screen.getByRole('button', { name: /Mark all read/i })
96
+ expect(markAllButton).toHaveAttribute('data-slot', 'button')
97
+
98
+ await act(async () => {
99
+ fireEvent.click(markAllButton)
100
+ })
101
+ expect(onMarkAllRead).toHaveBeenCalledTimes(1)
102
+ })
103
+
104
+ it('hides Mark all read when there are no unread notifications', () => {
105
+ render(<NotificationPanel {...buildProps({ unreadCount: 0 })} />)
106
+ expect(screen.queryByRole('button', { name: /Mark all read/i })).toBeNull()
107
+ })
108
+
109
+ it('keeps filtering behavior when switching tabs', () => {
110
+ const notifications = [
111
+ buildNotification({ id: 'a', title: 'Unread one', status: 'unread' }),
112
+ buildNotification({ id: 'b', title: 'Read one', status: 'read' }),
113
+ ]
114
+ render(<NotificationPanel {...buildProps({ notifications, unreadCount: 1 })} />)
115
+
116
+ expect(screen.getByText('Unread one')).toBeInTheDocument()
117
+ expect(screen.getByText('Read one')).toBeInTheDocument()
118
+
119
+ fireEvent.click(screen.getByRole('tab', { name: /Unread/i }))
120
+
121
+ expect(screen.getByText('Unread one')).toBeInTheDocument()
122
+ expect(screen.queryByText('Read one')).toBeNull()
123
+ })
124
+ })