@oslokommune/punkt-react 13.22.0 → 14.0.0

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,515 @@
1
+ import '@testing-library/jest-dom'
2
+ import { vi } from 'vitest'
3
+ import React from 'react'
4
+ import { render, screen, fireEvent } from '@testing-library/react'
5
+
6
+ //mock element width to control mobile/desktop behavior
7
+ let mockedWidth = 1280
8
+
9
+ vi.mock('../../hooks/useElementWidth', async () => {
10
+ return {
11
+ useElementWidth: () => mockedWidth,
12
+ }
13
+ })
14
+
15
+ import { PktHeaderService } from './HeaderService'
16
+
17
+ describe('PktHeaderService', () => {
18
+ // jsdom does not implement scrollTo; our scroll lock hook uses it
19
+ beforeAll(() => {
20
+ window.scrollTo = vi.fn()
21
+ })
22
+
23
+ beforeEach(() => {
24
+ mockedWidth = 1280
25
+ vi.clearAllMocks()
26
+ })
27
+
28
+ it('renders service name and link when provided', () => {
29
+ const { container } = render(<PktHeaderService serviceName="My Service" serviceLink="#" />)
30
+ const host = container.querySelector('.pkt-header-service__service-link') as HTMLElement | null
31
+ expect(host).toBeTruthy()
32
+ expect(screen.getByText('My Service')).toBeInTheDocument()
33
+ })
34
+
35
+ it('applies compact class when compact=true', () => {
36
+ const { container } = render(<PktHeaderService serviceName="Svc" compact />)
37
+ const header = container.querySelector('.pkt-header-service')
38
+ expect(header).toHaveClass('pkt-header-service--compact')
39
+ })
40
+
41
+ it('shows children inline on desktop, menu button on mobile', () => {
42
+ // Desktop
43
+ mockedWidth = 1440
44
+ const { rerender } = render(
45
+ <PktHeaderService serviceName="Svc">
46
+ <a className="pkt-header-service__slot-link" href="#">
47
+ Link
48
+ </a>
49
+ </PktHeaderService>,
50
+ )
51
+ expect(screen.getByText('Link')).toBeInTheDocument()
52
+
53
+ // Mobile
54
+ mockedWidth = 500
55
+ rerender(
56
+ <PktHeaderService serviceName="Svc">
57
+ <a className="pkt-header-service__slot-link" href="#">
58
+ Link
59
+ </a>
60
+ </PktHeaderService>,
61
+ )
62
+ expect(screen.getByRole('button', { name: 'Åpne meny' })).toBeInTheDocument()
63
+ })
64
+
65
+ it('renders desktop search input when showSearch is true (desktop)', () => {
66
+ mockedWidth = 1400
67
+ render(<PktHeaderService serviceName="Svc" showSearch={true} />)
68
+ const search = screen.getByRole('searchbox', { name: 'Søk' })
69
+ expect(search).toBeInTheDocument()
70
+ expect(search).toHaveAttribute('id', 'header-service-search')
71
+ })
72
+
73
+ it('renders mobile search toggle and opens search field when clicked (mobile)', () => {
74
+ mockedWidth = 500
75
+ render(<PktHeaderService serviceName="Svc" showSearch={true} />)
76
+ const btn = screen.getByRole('button', { name: 'Åpne søkefelt' })
77
+ fireEvent.click(btn)
78
+ // After opening, the mobile menu should show the search input
79
+ expect(screen.getByRole('searchbox', { name: 'Søk' })).toBeInTheDocument()
80
+ expect(screen.getByRole('searchbox')).toHaveAttribute('id', 'mobile-search-menu')
81
+ })
82
+
83
+ it('renders user button on desktop and opens user menu on click', () => {
84
+ mockedWidth = 1400
85
+ render(<PktHeaderService serviceName="Svc" user={{ name: 'Aksel' }} />)
86
+ // Button shows both fullname and shortname (both 'Aksel' when no shortname provided)
87
+ const userBtn = screen.getByRole('button', { name: /Aksel/ })
88
+ expect(userBtn).toBeInTheDocument()
89
+ fireEvent.click(userBtn)
90
+ // User menu should be mounted
91
+ const menu = screen.getByRole('navigation')
92
+ expect(menu).toHaveClass('pkt-user-menu')
93
+ })
94
+
95
+ // New tests for hideLogo prop
96
+ describe('hideLogo prop', () => {
97
+ it('shows oslo logo by default (hideLogo=false)', () => {
98
+ const { container } = render(<PktHeaderService serviceName="Svc" />)
99
+ const logos = container.querySelectorAll('.pkt-header-service__logo')
100
+ expect(logos.length).toBe(1)
101
+ })
102
+
103
+ it('hides oslo logo when hideLogo=true', () => {
104
+ const { container } = render(<PktHeaderService serviceName="Svc" hideLogo={true} />)
105
+ const logos = container.querySelectorAll('.pkt-header-service__logo')
106
+ expect(logos.length).toBe(0)
107
+ })
108
+ })
109
+
110
+ // New tests for logoLink prop
111
+ describe('logoLink prop', () => {
112
+ it('renders logo as link when logoLink is a string', () => {
113
+ const { container } = render(<PktHeaderService serviceName="Svc" logoLink="https://oslo.kommune.no" />)
114
+ const logoLink = container.querySelector('.pkt-header-service__logo a')
115
+ expect(logoLink).toBeTruthy()
116
+ expect(logoLink).toHaveAttribute('href', 'https://oslo.kommune.no')
117
+ })
118
+
119
+ it('renders logo without link/button when logoLink is not provided', () => {
120
+ const { container } = render(<PktHeaderService serviceName="Svc" />)
121
+ const logoLink = container.querySelector('.pkt-header-service__logo a')
122
+ const logoButton = container.querySelector('.pkt-header-service__logo button')
123
+ expect(logoLink).toBeNull()
124
+ expect(logoButton).toBeNull()
125
+ })
126
+ })
127
+
128
+ // New tests for position and scrollBehavior props
129
+ describe('position and scrollBehavior props', () => {
130
+ it('applies fixed class by default (position="fixed")', () => {
131
+ const { container } = render(<PktHeaderService serviceName="Svc" />)
132
+ const header = container.querySelector('.pkt-header-service')
133
+ expect(header).toHaveClass('pkt-header-service--fixed')
134
+ })
135
+
136
+ it('does not apply fixed class when position="relative"', () => {
137
+ const { container } = render(<PktHeaderService serviceName="Svc" position="relative" />)
138
+ const header = container.querySelector('.pkt-header-service')
139
+ expect(header).not.toHaveClass('pkt-header-service--fixed')
140
+ })
141
+
142
+ it('applies scroll-to-hide class by default (scrollBehavior="hide")', () => {
143
+ const { container } = render(<PktHeaderService serviceName="Svc" />)
144
+ const header = container.querySelector('.pkt-header-service')
145
+ expect(header).toHaveClass('pkt-header-service--scroll-to-hide')
146
+ })
147
+
148
+ it('does not apply scroll-to-hide class when scrollBehavior="none"', () => {
149
+ const { container } = render(<PktHeaderService serviceName="Svc" scrollBehavior="none" />)
150
+ const header = container.querySelector('.pkt-header-service')
151
+ expect(header).not.toHaveClass('pkt-header-service--scroll-to-hide')
152
+ })
153
+ })
154
+
155
+ // New tests for user with lastLoggedIn
156
+ describe('user props', () => {
157
+ it('displays user name in button', () => {
158
+ mockedWidth = 1400
159
+ render(<PktHeaderService serviceName="Svc" user={{ name: 'Aksel Olsen' }} />)
160
+ const userBtn = screen.getByRole('button', { name: /Aksel Olsen/ })
161
+ expect(userBtn).toBeInTheDocument()
162
+ })
163
+
164
+ it('logs deprecation warning when shortname is provided', () => {
165
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
166
+ mockedWidth = 1400
167
+ render(<PktHeaderService serviceName="Svc" user={{ name: 'Aksel', shortname: 'AO' }} />)
168
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('shortname'))
169
+ consoleSpy.mockRestore()
170
+ })
171
+
172
+ it('displays formatted lastLoggedIn in user menu', () => {
173
+ mockedWidth = 1400
174
+ render(<PktHeaderService serviceName="Svc" user={{ name: 'Aksel', lastLoggedIn: new Date('2025-09-19') }} />)
175
+ const userBtn = screen.getByRole('button', { name: /Aksel/ })
176
+ fireEvent.click(userBtn)
177
+ // Should format the date in Norwegian
178
+ expect(screen.getByText(/Sist pålogget:/)).toBeInTheDocument()
179
+ })
180
+
181
+ it('displays lastLoggedIn string as-is in user menu', () => {
182
+ mockedWidth = 1400
183
+ render(<PktHeaderService serviceName="Svc" user={{ name: 'Aksel', lastLoggedIn: '19. september 2025' }} />)
184
+ const userBtn = screen.getByRole('button', { name: /Aksel/ })
185
+ fireEvent.click(userBtn)
186
+ expect(screen.getByText('19. september 2025')).toBeInTheDocument()
187
+ })
188
+ })
189
+
190
+ // New tests for representing props
191
+ describe('representing props', () => {
192
+ it('displays representing name in user button when representing is provided', () => {
193
+ mockedWidth = 1400
194
+ render(<PktHeaderService serviceName="Svc" user={{ name: 'Aksel' }} representing={{ name: 'Oslo Kommune' }} />)
195
+ const userBtn = screen.getByRole('button', { name: /Oslo Kommune/ })
196
+ expect(userBtn).toBeInTheDocument()
197
+ })
198
+
199
+ it('logs deprecation warning when representing.shortname is provided', () => {
200
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
201
+ mockedWidth = 1400
202
+ render(
203
+ <PktHeaderService
204
+ serviceName="Svc"
205
+ user={{ name: 'Aksel' }}
206
+ representing={{ name: 'Oslo Kommune', shortname: 'OK' }}
207
+ />,
208
+ )
209
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('shortname'))
210
+ consoleSpy.mockRestore()
211
+ })
212
+
213
+ it('displays orgNumber in user menu when representing has orgNumber', () => {
214
+ mockedWidth = 1400
215
+ render(
216
+ <PktHeaderService
217
+ serviceName="Svc"
218
+ user={{ name: 'Aksel' }}
219
+ representing={{ name: 'Oslo Kommune', orgNumber: '911738066' }}
220
+ />,
221
+ )
222
+ const userBtn = screen.getByRole('button', { name: /Oslo Kommune/ })
223
+ fireEvent.click(userBtn)
224
+ expect(screen.getByText('Org.nr. 911738066')).toBeInTheDocument()
225
+ })
226
+ })
227
+
228
+ // New tests for canChangeRepresentation
229
+ describe('canChangeRepresentation prop', () => {
230
+ it('shows change representation button when canChangeRepresentation=true', () => {
231
+ mockedWidth = 1400
232
+ const handleChange = vi.fn()
233
+ render(
234
+ <PktHeaderService
235
+ serviceName="Svc"
236
+ user={{ name: 'Aksel' }}
237
+ representing={{ name: 'Oslo Kommune' }}
238
+ canChangeRepresentation={true}
239
+ changeRepresentation={handleChange}
240
+ />,
241
+ )
242
+ const userBtn = screen.getByRole('button', { name: /Oslo Kommune/ })
243
+ fireEvent.click(userBtn)
244
+ const changeBtn = screen.getByRole('button', { name: 'Endre organisasjon' })
245
+ expect(changeBtn).toBeInTheDocument()
246
+ fireEvent.click(changeBtn)
247
+ expect(handleChange).toHaveBeenCalledTimes(1)
248
+ })
249
+
250
+ it('does not show change representation button when canChangeRepresentation=false', () => {
251
+ mockedWidth = 1400
252
+ render(
253
+ <PktHeaderService
254
+ serviceName="Svc"
255
+ user={{ name: 'Aksel' }}
256
+ representing={{ name: 'Oslo Kommune' }}
257
+ canChangeRepresentation={false}
258
+ />,
259
+ )
260
+ const userBtn = screen.getByRole('button', { name: /Oslo Kommune/ })
261
+ fireEvent.click(userBtn)
262
+ expect(screen.queryByRole('button', { name: 'Endre organisasjon' })).not.toBeInTheDocument()
263
+ })
264
+ })
265
+
266
+ // New tests for userMenu prop
267
+ describe('userMenu prop', () => {
268
+ it('renders userMenu items in user menu dropdown', () => {
269
+ mockedWidth = 1400
270
+ const userMenu = [
271
+ { title: 'Mine bookinger', iconName: 'heart', target: '/bookinger' },
272
+ { title: 'Innstillinger', iconName: 'cogwheel', target: () => {} },
273
+ ]
274
+ render(<PktHeaderService serviceName="Svc" user={{ name: 'Aksel' }} userMenu={userMenu} />)
275
+ const userBtn = screen.getByRole('button', { name: /Aksel/ })
276
+ fireEvent.click(userBtn)
277
+ expect(screen.getByText('Mine bookinger')).toBeInTheDocument()
278
+ expect(screen.getByText('Innstillinger')).toBeInTheDocument()
279
+ })
280
+
281
+ it('renders userMenu link items as links', () => {
282
+ mockedWidth = 1400
283
+ const userMenu = [{ title: 'Min profil', target: '/profil' }]
284
+ const { container } = render(<PktHeaderService serviceName="Svc" user={{ name: 'Aksel' }} userMenu={userMenu} />)
285
+ const userBtn = screen.getByRole('button', { name: /Aksel/ })
286
+ fireEvent.click(userBtn)
287
+ // PktLink renders as an <a> tag with class pkt-link
288
+ const linkElement = container.querySelector('a.pkt-link[href="/profil"]')
289
+ expect(linkElement).toBeTruthy()
290
+ expect(screen.getByText('Min profil')).toBeInTheDocument()
291
+ })
292
+
293
+ it('calls userMenu button item onClick when clicked', () => {
294
+ mockedWidth = 1400
295
+ const handleClick = vi.fn()
296
+ const userMenu = [{ title: 'Custom Action', target: handleClick }]
297
+ render(<PktHeaderService serviceName="Svc" user={{ name: 'Aksel' }} userMenu={userMenu} />)
298
+ const userBtn = screen.getByRole('button', { name: /Aksel/ })
299
+ fireEvent.click(userBtn)
300
+ const actionBtn = screen.getByRole('button', { name: 'Custom Action' })
301
+ fireEvent.click(actionBtn)
302
+ expect(handleClick).toHaveBeenCalledTimes(1)
303
+ })
304
+ })
305
+
306
+ // New tests for logOut callback and placement
307
+ describe('logOut callback', () => {
308
+ it('shows logout button in header when logOutButtonPlacement="header"', () => {
309
+ mockedWidth = 1400
310
+ const handleLogout = vi.fn()
311
+ render(
312
+ <PktHeaderService
313
+ serviceName="Svc"
314
+ user={{ name: 'Aksel' }}
315
+ logOutButtonPlacement="header"
316
+ logOut={handleLogout}
317
+ />,
318
+ )
319
+ const logoutBtn = screen.getByRole('button', { name: 'Logg ut' })
320
+ expect(logoutBtn).toBeInTheDocument()
321
+ fireEvent.click(logoutBtn)
322
+ expect(handleLogout).toHaveBeenCalledTimes(1)
323
+ })
324
+
325
+ it('shows logout button in userMenu when logOutButtonPlacement="userMenu"', () => {
326
+ mockedWidth = 1400
327
+ const handleLogout = vi.fn()
328
+ render(
329
+ <PktHeaderService
330
+ serviceName="Svc"
331
+ user={{ name: 'Aksel' }}
332
+ logOutButtonPlacement="userMenu"
333
+ logOut={handleLogout}
334
+ />,
335
+ )
336
+ const userBtn = screen.getByRole('button', { name: /Aksel/ })
337
+ fireEvent.click(userBtn)
338
+ const logoutBtn = screen.getByRole('button', { name: 'Logg ut' })
339
+ expect(logoutBtn).toBeInTheDocument()
340
+ fireEvent.click(logoutBtn)
341
+ expect(handleLogout).toHaveBeenCalledTimes(1)
342
+ })
343
+
344
+ it('shows logout button in both header and userMenu when logOutButtonPlacement="both"', () => {
345
+ mockedWidth = 1400
346
+ const handleLogout = vi.fn()
347
+ render(
348
+ <PktHeaderService
349
+ serviceName="Svc"
350
+ user={{ name: 'Aksel' }}
351
+ logOutButtonPlacement="both"
352
+ logOut={handleLogout}
353
+ />,
354
+ )
355
+ // Header logout
356
+ const headerLogoutBtn = screen.getByRole('button', { name: 'Logg ut' })
357
+ expect(headerLogoutBtn).toBeInTheDocument()
358
+
359
+ // Open user menu
360
+ const userBtn = screen.getByRole('button', { name: /Aksel/ })
361
+ fireEvent.click(userBtn)
362
+ // Should have logout in menu too
363
+ const logoutBtns = screen.getAllByRole('button', { name: 'Logg ut' })
364
+ expect(logoutBtns.length).toBe(2)
365
+ })
366
+ })
367
+
368
+ // Test for userMenuFooter deprecation warning
369
+ describe('userMenuFooter deprecation', () => {
370
+ it('logs deprecation warning when userMenuFooter is provided', () => {
371
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
372
+ mockedWidth = 1400
373
+ render(
374
+ <PktHeaderService
375
+ serviceName="Svc"
376
+ user={{ name: 'Aksel' }}
377
+ userMenuFooter={[{ title: 'Footer Item', target: '#' }]}
378
+ />,
379
+ )
380
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('userMenuFooter'))
381
+ consoleSpy.mockRestore()
382
+ })
383
+ })
384
+
385
+ // Test for userOptions "no longer available" warning
386
+ describe('userOptions warning', () => {
387
+ it('logs warning when userOptions is provided', () => {
388
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
389
+ mockedWidth = 1400
390
+ render(
391
+ <PktHeaderService
392
+ serviceName="Svc"
393
+ user={{ name: 'Aksel' }}
394
+ userOptions={[{ title: 'Option', target: '#' }]}
395
+ />,
396
+ )
397
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('userOptions'))
398
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('no longer available'))
399
+ consoleSpy.mockRestore()
400
+ })
401
+ })
402
+
403
+ // Tests for serviceLink and serviceClick props
404
+ describe('serviceLink and serviceClick props', () => {
405
+ it('renders service name as link when serviceLink is a string', () => {
406
+ const { container } = render(<PktHeaderService serviceName="My Service" serviceLink="https://example.com" />)
407
+ const link = container.querySelector('a.pkt-link.pkt-header-service__service-link')
408
+ expect(link).toBeTruthy()
409
+ expect(link).toHaveAttribute('href', 'https://example.com')
410
+ })
411
+
412
+ it('renders service name as button when serviceClick is provided', () => {
413
+ const handleClick = vi.fn()
414
+ const { container } = render(<PktHeaderService serviceName="My Service" serviceClick={handleClick} />)
415
+ const button = container.querySelector('button.pkt-header-service__service-link')
416
+ expect(button).toBeTruthy()
417
+ fireEvent.click(button!)
418
+ expect(handleClick).toHaveBeenCalledTimes(1)
419
+ })
420
+
421
+ it('renders service name as span when neither serviceLink nor serviceClick is provided', () => {
422
+ const { container } = render(<PktHeaderService serviceName="My Service" />)
423
+ const span = container.querySelector('span.pkt-header-service__service-link')
424
+ expect(span).toBeTruthy()
425
+ expect(span?.tagName).toBe('SPAN')
426
+ })
427
+
428
+ it('prioritizes serviceLink over serviceClick when both are provided', () => {
429
+ const handleClick = vi.fn()
430
+ const { container } = render(
431
+ <PktHeaderService serviceName="My Service" serviceLink="https://example.com" serviceClick={handleClick} />,
432
+ )
433
+ const link = container.querySelector('a.pkt-link.pkt-header-service__service-link')
434
+ const button = container.querySelector('button.pkt-header-service__service-link')
435
+ expect(link).toBeTruthy()
436
+ expect(button).toBeNull()
437
+ })
438
+ })
439
+
440
+ // Tests for logoClick prop
441
+ describe('logoClick prop', () => {
442
+ it('renders logo as button when logoClick is provided', () => {
443
+ const handleClick = vi.fn()
444
+ const { container } = render(<PktHeaderService serviceName="Svc" logoClick={handleClick} />)
445
+ const logoButton = container.querySelector('.pkt-header-service__logo button')
446
+ expect(logoButton).toBeTruthy()
447
+ fireEvent.click(logoButton!)
448
+ expect(handleClick).toHaveBeenCalledTimes(1)
449
+ })
450
+
451
+ it('prioritizes logoLink over logoClick when both are provided', () => {
452
+ const handleClick = vi.fn()
453
+ const { container } = render(
454
+ <PktHeaderService serviceName="Svc" logoLink="https://oslo.kommune.no" logoClick={handleClick} />,
455
+ )
456
+ const logoLink = container.querySelector('.pkt-header-service__logo a')
457
+ const logoButton = container.querySelector('.pkt-header-service__logo button')
458
+ expect(logoLink).toBeTruthy()
459
+ expect(logoButton).toBeNull()
460
+ })
461
+ })
462
+
463
+ // Tests for logout button placement on mobile vs desktop
464
+ describe('logout button placement mobile vs desktop', () => {
465
+ it('shows logout button in user area on desktop when logOutButtonPlacement="header"', () => {
466
+ mockedWidth = 1400
467
+ const handleLogout = vi.fn()
468
+ render(
469
+ <PktHeaderService
470
+ serviceName="Svc"
471
+ user={{ name: 'Aksel' }}
472
+ logOutButtonPlacement="header"
473
+ logOut={handleLogout}
474
+ />,
475
+ )
476
+ // On desktop, logout button should be in user area
477
+ const logoutBtn = screen.getByRole('button', { name: 'Logg ut' })
478
+ expect(logoutBtn).toBeInTheDocument()
479
+ })
480
+
481
+ it('shows logout button in content area on mobile when logOutButtonPlacement="header"', () => {
482
+ mockedWidth = 500
483
+ const handleLogout = vi.fn()
484
+ const { container } = render(
485
+ <PktHeaderService
486
+ serviceName="Svc"
487
+ user={{ name: 'Aksel' }}
488
+ logOutButtonPlacement="header"
489
+ logOut={handleLogout}
490
+ />,
491
+ )
492
+ const contentArea = container.querySelector('.pkt-header-service__content')
493
+ const logoutBtn = contentArea?.querySelector('button')
494
+ expect(logoutBtn).toBeTruthy()
495
+ expect(screen.getByRole('button', { name: 'Logg ut' })).toBeInTheDocument()
496
+ })
497
+
498
+ it('does not show logout button in header areas when logOutButtonPlacement="userMenu"', () => {
499
+ mockedWidth = 1400
500
+ const handleLogout = vi.fn()
501
+ render(
502
+ <PktHeaderService
503
+ serviceName="Svc"
504
+ user={{ name: 'Aksel' }}
505
+ logOutButtonPlacement="userMenu"
506
+ logOut={handleLogout}
507
+ />,
508
+ )
509
+ // When logOutButtonPlacement is userMenu, logout button should not be visible outside the menu
510
+ // Only the user menu button should be visible, not a separate logout button
511
+ const logoutButtons = screen.queryAllByRole('button', { name: 'Logg ut' })
512
+ expect(logoutButtons.length).toBe(0)
513
+ })
514
+ })
515
+ })