@oslokommune/punkt-elements 13.5.1 → 13.5.3
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.
- package/CHANGELOG.md +34 -0
- package/dist/{alert-DQNBDKjT.cjs → alert-7rUOhlNi.cjs} +2 -1
- package/dist/{alert-B07oUpkq.js → alert-cUBtwi2k.js} +12 -11
- package/dist/{linkcard-BVEsUPwG.js → linkcard-9CNlyT0S.js} +17 -16
- package/dist/{linkcard-BlMhPNry.cjs → linkcard-DqIvb54H.cjs} +2 -2
- package/dist/pkt-alert.cjs +1 -1
- package/dist/pkt-alert.js +1 -1
- package/dist/pkt-index.cjs +1 -1
- package/dist/pkt-index.js +2 -2
- package/dist/pkt-linkcard.cjs +1 -1
- package/dist/pkt-linkcard.js +1 -1
- package/package.json +6 -2
- package/src/components/alert/alert.test.ts +64 -79
- package/src/components/alert/alert.ts +1 -0
- package/src/components/backlink/backlink.test.ts +50 -96
- package/src/components/button/button.test.ts +211 -249
- package/src/components/calendar/calendar.accessibility.test.ts +30 -43
- package/src/components/card/card.test.ts +71 -121
- package/src/components/checkbox/checkbox.test.ts +231 -156
- package/src/components/consent/consent.test.ts +87 -91
- package/src/components/icon/icon.test.ts +368 -0
- package/src/components/input-wrapper/input-wrapper.test.ts +505 -0
- package/src/components/link/link.test.ts +224 -0
- package/src/components/linkcard/linkcard.test.ts +364 -0
- package/src/components/linkcard/linkcard.ts +3 -1
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import '@testing-library/jest-dom'
|
|
2
|
+
import { axe, toHaveNoViolations } from 'jest-axe'
|
|
3
|
+
import { fireEvent } from '@testing-library/dom'
|
|
4
|
+
import { createElementTest, BaseTestConfig } from '../../tests/test-framework'
|
|
5
|
+
import { CustomElementFor } from '../../tests/component-registry'
|
|
6
|
+
import './link'
|
|
7
|
+
|
|
8
|
+
expect.extend(toHaveNoViolations)
|
|
9
|
+
|
|
10
|
+
export interface LinkTestConfig extends BaseTestConfig {
|
|
11
|
+
href?: string
|
|
12
|
+
target?: string
|
|
13
|
+
skin?: string
|
|
14
|
+
variant?: string
|
|
15
|
+
iconName?: string
|
|
16
|
+
iconPosition?: string
|
|
17
|
+
openInNewTab?: boolean
|
|
18
|
+
external?: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Use shared framework
|
|
22
|
+
export const createLinkTest = async (config: LinkTestConfig = {}) => {
|
|
23
|
+
const { container, element } = await createElementTest<
|
|
24
|
+
CustomElementFor<'pkt-link'>,
|
|
25
|
+
LinkTestConfig
|
|
26
|
+
>('pkt-link', config)
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
container,
|
|
30
|
+
link: element,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('PktLink', () => {
|
|
35
|
+
describe('Rendering and basic functionality', () => {
|
|
36
|
+
test('renders without errors', async () => {
|
|
37
|
+
const { link } = await createLinkTest()
|
|
38
|
+
|
|
39
|
+
expect(link).toBeInTheDocument()
|
|
40
|
+
await link.updateComplete
|
|
41
|
+
expect(link).toBeTruthy()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('renders with correct structure', async () => {
|
|
45
|
+
const { link } = await createLinkTest({
|
|
46
|
+
href: 'https://example.com',
|
|
47
|
+
content: 'Click me',
|
|
48
|
+
})
|
|
49
|
+
await link.updateComplete
|
|
50
|
+
|
|
51
|
+
expect(link).toBeInTheDocument()
|
|
52
|
+
const anchor = link.querySelector('a')
|
|
53
|
+
expect(anchor).toBeInTheDocument()
|
|
54
|
+
expect(anchor?.href).toBe('https://example.com/')
|
|
55
|
+
expect(anchor?.textContent).toContain('Click me')
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
describe('Properties and attributes', () => {
|
|
60
|
+
test('applies default properties correctly', async () => {
|
|
61
|
+
const { link } = await createLinkTest()
|
|
62
|
+
await link.updateComplete
|
|
63
|
+
|
|
64
|
+
expect(link.href).toBe('#')
|
|
65
|
+
expect(link.external).toBe(false)
|
|
66
|
+
expect(link.iconName).toBeUndefined()
|
|
67
|
+
expect(link.target).toBe('_self')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('sets href property correctly', async () => {
|
|
71
|
+
const { link } = await createLinkTest({
|
|
72
|
+
href: 'https://example.com',
|
|
73
|
+
})
|
|
74
|
+
await link.updateComplete
|
|
75
|
+
|
|
76
|
+
expect(link.href).toBe('https://example.com')
|
|
77
|
+
const anchor = link.querySelector('a')
|
|
78
|
+
expect(anchor?.href).toBe('https://example.com/')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('sets external property correctly', async () => {
|
|
82
|
+
const { link } = await createLinkTest({
|
|
83
|
+
href: 'https://example.com',
|
|
84
|
+
external: true,
|
|
85
|
+
})
|
|
86
|
+
await link.updateComplete
|
|
87
|
+
|
|
88
|
+
expect(link.external).toBe(true)
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
describe('Icon functionality', () => {
|
|
93
|
+
test('renders icon when iconName is provided', async () => {
|
|
94
|
+
const { link } = await createLinkTest({
|
|
95
|
+
href: '#',
|
|
96
|
+
iconName: 'arrow-right',
|
|
97
|
+
})
|
|
98
|
+
await link.updateComplete
|
|
99
|
+
|
|
100
|
+
const icon = link.querySelector('pkt-icon')
|
|
101
|
+
expect(icon).toBeInTheDocument()
|
|
102
|
+
expect(icon?.getAttribute('name')).toBe('arrow-right')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test('positions icon correctly', async () => {
|
|
106
|
+
const { link } = await createLinkTest({
|
|
107
|
+
href: '#',
|
|
108
|
+
iconName: 'arrow-right',
|
|
109
|
+
iconPosition: 'right',
|
|
110
|
+
})
|
|
111
|
+
await link.updateComplete
|
|
112
|
+
|
|
113
|
+
const anchor = link.querySelector('a')
|
|
114
|
+
expect(anchor?.classList.contains('pkt-link--icon-right')).toBe(true)
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
describe('External link functionality', () => {
|
|
119
|
+
test('applies external class and rel attribute', async () => {
|
|
120
|
+
const { link } = await createLinkTest({
|
|
121
|
+
href: 'https://example.com',
|
|
122
|
+
external: true,
|
|
123
|
+
})
|
|
124
|
+
await link.updateComplete
|
|
125
|
+
|
|
126
|
+
const anchor = link.querySelector('a')
|
|
127
|
+
expect(anchor?.classList.contains('pkt-link--external')).toBe(true)
|
|
128
|
+
expect(anchor?.rel).toBe('noopener noreferrer')
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
test('does not set rel attribute for internal links', async () => {
|
|
132
|
+
const { link } = await createLinkTest({
|
|
133
|
+
href: '/internal-page',
|
|
134
|
+
})
|
|
135
|
+
await link.updateComplete
|
|
136
|
+
|
|
137
|
+
const anchor = link.querySelector('a')
|
|
138
|
+
expect(anchor?.rel).toBe('')
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
describe('Event handling', () => {
|
|
143
|
+
test('handles click events', async () => {
|
|
144
|
+
const { link } = await createLinkTest({
|
|
145
|
+
href: '#test',
|
|
146
|
+
})
|
|
147
|
+
await link.updateComplete
|
|
148
|
+
|
|
149
|
+
const anchor = link.querySelector('a')
|
|
150
|
+
const clickHandler = jest.fn()
|
|
151
|
+
anchor?.addEventListener('click', clickHandler)
|
|
152
|
+
|
|
153
|
+
fireEvent.click(anchor!)
|
|
154
|
+
expect(clickHandler).toHaveBeenCalled()
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
describe('Dynamic updates', () => {
|
|
159
|
+
test('updates href dynamically', async () => {
|
|
160
|
+
const { link } = await createLinkTest({
|
|
161
|
+
href: 'https://example.com',
|
|
162
|
+
})
|
|
163
|
+
await link.updateComplete
|
|
164
|
+
|
|
165
|
+
link.href = 'https://updated.com'
|
|
166
|
+
await link.updateComplete
|
|
167
|
+
|
|
168
|
+
expect(link.href).toBe('https://updated.com')
|
|
169
|
+
const anchor = link.querySelector('a')
|
|
170
|
+
expect(anchor?.href).toBe('https://updated.com/')
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
test('updates external property dynamically', async () => {
|
|
174
|
+
const { link } = await createLinkTest({
|
|
175
|
+
href: 'https://example.com',
|
|
176
|
+
})
|
|
177
|
+
await link.updateComplete
|
|
178
|
+
|
|
179
|
+
link.external = true
|
|
180
|
+
await link.updateComplete
|
|
181
|
+
|
|
182
|
+
expect(link.external).toBe(true)
|
|
183
|
+
const anchor = link.querySelector('a')
|
|
184
|
+
expect(anchor?.classList.contains('pkt-link--external')).toBe(true)
|
|
185
|
+
expect(anchor?.rel).toBe('noopener noreferrer')
|
|
186
|
+
})
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
describe('Accessibility', () => {
|
|
190
|
+
test('basic link is accessible', async () => {
|
|
191
|
+
const { container } = await createLinkTest({
|
|
192
|
+
href: 'https://example.com',
|
|
193
|
+
content: 'Accessible Link',
|
|
194
|
+
})
|
|
195
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
196
|
+
|
|
197
|
+
const results = await axe(container)
|
|
198
|
+
expect(results).toHaveNoViolations()
|
|
199
|
+
})
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
describe('Integration scenarios', () => {
|
|
203
|
+
test('works with complex configurations', async () => {
|
|
204
|
+
const { link } = await createLinkTest({
|
|
205
|
+
href: 'https://external-site.com',
|
|
206
|
+
iconName: 'external',
|
|
207
|
+
iconPosition: 'right',
|
|
208
|
+
external: true,
|
|
209
|
+
target: '_blank',
|
|
210
|
+
content: 'Complex External Link',
|
|
211
|
+
})
|
|
212
|
+
await link.updateComplete
|
|
213
|
+
|
|
214
|
+
expect(link.href).toBe('https://external-site.com')
|
|
215
|
+
expect(link.iconName).toBe('external')
|
|
216
|
+
expect(link.external).toBe(true)
|
|
217
|
+
expect(link.target).toBe('_blank')
|
|
218
|
+
|
|
219
|
+
const anchor = link.querySelector('a')
|
|
220
|
+
expect(anchor?.classList.contains('pkt-link--external')).toBe(true)
|
|
221
|
+
expect(anchor?.classList.contains('pkt-link--icon-right')).toBe(true)
|
|
222
|
+
})
|
|
223
|
+
})
|
|
224
|
+
})
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import '@testing-library/jest-dom'
|
|
2
|
+
import { axe, toHaveNoViolations } from 'jest-axe'
|
|
3
|
+
import { createElementTest, BaseTestConfig } from '../../tests/test-framework'
|
|
4
|
+
import { CustomElementFor } from '../../tests/component-registry'
|
|
5
|
+
import { PktLinkCard, type TLinkCardSkin } from './linkcard'
|
|
6
|
+
import type { IPktLinkCard } from './linkcard'
|
|
7
|
+
import './linkcard'
|
|
8
|
+
|
|
9
|
+
export interface LinkCardTestConfig extends IPktLinkCard, BaseTestConfig {}
|
|
10
|
+
|
|
11
|
+
// Use shared framework
|
|
12
|
+
export const createLinkCardTest = async (config: LinkCardTestConfig = {}) => {
|
|
13
|
+
const { container, element } = await createElementTest<
|
|
14
|
+
CustomElementFor<'pkt-linkcard'>,
|
|
15
|
+
LinkCardTestConfig
|
|
16
|
+
>('pkt-linkcard', config)
|
|
17
|
+
|
|
18
|
+
const link = element.querySelector('a') as HTMLAnchorElement
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
container,
|
|
22
|
+
linkCard: element,
|
|
23
|
+
link,
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
expect.extend(toHaveNoViolations)
|
|
27
|
+
|
|
28
|
+
// Test data constants
|
|
29
|
+
const VALID_SKINS: TLinkCardSkin[] = [
|
|
30
|
+
'normal',
|
|
31
|
+
'no-padding',
|
|
32
|
+
'blue',
|
|
33
|
+
'beige',
|
|
34
|
+
'green',
|
|
35
|
+
'gray',
|
|
36
|
+
'beige-outline',
|
|
37
|
+
'gray-outline',
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
const SAMPLE_ICONS = ['arrow-right', 'external-link', 'download', 'info']
|
|
41
|
+
|
|
42
|
+
// Cleanup after each test
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
document.body.innerHTML = ''
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
// ----------- THE TESTS -----------
|
|
48
|
+
|
|
49
|
+
describe('PktLinkCard', () => {
|
|
50
|
+
describe('Basic Rendering', () => {
|
|
51
|
+
test('renders without errors', async () => {
|
|
52
|
+
const { linkCard, link } = await createLinkCardTest()
|
|
53
|
+
|
|
54
|
+
expect(linkCard).toBeInTheDocument()
|
|
55
|
+
expect(link).toBeInTheDocument()
|
|
56
|
+
expect(link).toHaveClass('pkt-linkcard')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('renders with default properties', async () => {
|
|
60
|
+
const { linkCard, link } = await createLinkCardTest()
|
|
61
|
+
|
|
62
|
+
expect(linkCard.title).toBe('')
|
|
63
|
+
expect(linkCard.href).toBe('#')
|
|
64
|
+
expect(linkCard.iconName).toBe('')
|
|
65
|
+
expect(linkCard.openInNewTab).toBe(false)
|
|
66
|
+
expect(linkCard.skin).toBe('normal')
|
|
67
|
+
expect(linkCard.external).toBe(false)
|
|
68
|
+
|
|
69
|
+
expect(link.getAttribute('href')).toBe('#')
|
|
70
|
+
expect(link.getAttribute('target')).toBe('_self')
|
|
71
|
+
// When openInNewTab is false, rel should be null (ifDefined behavior)
|
|
72
|
+
expect(link.getAttribute('rel')).toBeNull()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('renders content in slot', async () => {
|
|
76
|
+
const content = '<p>Custom link card content</p>'
|
|
77
|
+
const { linkCard } = await createLinkCardTest({ content })
|
|
78
|
+
|
|
79
|
+
const textSlot = linkCard.querySelector('.pkt-linkcard__text')
|
|
80
|
+
expect(textSlot).toBeInTheDocument()
|
|
81
|
+
expect(textSlot?.innerHTML).toBe(content)
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
describe('Title Functionality', () => {
|
|
86
|
+
test('renders title when provided', async () => {
|
|
87
|
+
const title = 'Test Link Card Title'
|
|
88
|
+
const { linkCard } = await createLinkCardTest({ title })
|
|
89
|
+
|
|
90
|
+
expect(linkCard.title).toBe(title)
|
|
91
|
+
|
|
92
|
+
const titleElement = linkCard.querySelector('.pkt-linkcard__title')
|
|
93
|
+
expect(titleElement).toBeInTheDocument()
|
|
94
|
+
expect(titleElement?.textContent).toBe(title)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('does not render title when not provided', async () => {
|
|
98
|
+
const { linkCard } = await createLinkCardTest()
|
|
99
|
+
|
|
100
|
+
const titleElement = linkCard.querySelector('.pkt-linkcard__title')
|
|
101
|
+
expect(titleElement).not.toBeInTheDocument()
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('applies correct title classes', async () => {
|
|
105
|
+
const title = 'Test Title'
|
|
106
|
+
const { linkCard } = await createLinkCardTest({ title })
|
|
107
|
+
|
|
108
|
+
const titleElement = linkCard.querySelector('.pkt-linkcard__title')
|
|
109
|
+
expect(titleElement).toHaveClass('pkt-linkcard__title')
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
describe('Link Functionality', () => {
|
|
114
|
+
test('renders with custom href', async () => {
|
|
115
|
+
const href = '/custom-path'
|
|
116
|
+
const { linkCard, link } = await createLinkCardTest({ href })
|
|
117
|
+
|
|
118
|
+
expect(linkCard.href).toBe(href)
|
|
119
|
+
expect(link.getAttribute('href')).toBe(href)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('handles openInNewTab correctly', async () => {
|
|
123
|
+
const { linkCard, link } = await createLinkCardTest({ openInNewTab: true })
|
|
124
|
+
|
|
125
|
+
expect(linkCard.openInNewTab).toBe(true)
|
|
126
|
+
expect(link.getAttribute('target')).toBe('_blank')
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
test('handles openInNewTab false correctly', async () => {
|
|
130
|
+
const { linkCard, link } = await createLinkCardTest({ openInNewTab: false })
|
|
131
|
+
|
|
132
|
+
expect(linkCard.openInNewTab).toBe(false)
|
|
133
|
+
expect(link.getAttribute('target')).toBe('_self')
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
test('applies correct rel attribute for new tab', async () => {
|
|
137
|
+
const { link } = await createLinkCardTest({ openInNewTab: true })
|
|
138
|
+
expect(link.getAttribute('rel')).toBe('noopener noreferrer')
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
describe('Icon Functionality', () => {
|
|
143
|
+
test('renders icon when iconName provided', async () => {
|
|
144
|
+
const iconName = 'arrow-right'
|
|
145
|
+
const { linkCard } = await createLinkCardTest({ iconName })
|
|
146
|
+
|
|
147
|
+
expect(linkCard.iconName).toBe(iconName)
|
|
148
|
+
|
|
149
|
+
const icon = linkCard.querySelector('pkt-icon')
|
|
150
|
+
expect(icon).toBeInTheDocument()
|
|
151
|
+
expect(icon).toHaveClass('pkt-link__icon')
|
|
152
|
+
expect(icon?.getAttribute('name')).toBe(iconName)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
test('does not render icon when iconName not provided', async () => {
|
|
156
|
+
const { linkCard } = await createLinkCardTest()
|
|
157
|
+
|
|
158
|
+
const icon = linkCard.querySelector('pkt-icon')
|
|
159
|
+
expect(icon).not.toBeInTheDocument()
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
test('renders with different icon names', async () => {
|
|
163
|
+
for (const iconName of SAMPLE_ICONS) {
|
|
164
|
+
const { linkCard } = await createLinkCardTest({ iconName })
|
|
165
|
+
|
|
166
|
+
const icon = linkCard.querySelector('pkt-icon')
|
|
167
|
+
expect(icon?.getAttribute('name')).toBe(iconName)
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
describe('Skin Variations', () => {
|
|
173
|
+
test('applies default skin class', async () => {
|
|
174
|
+
const { link } = await createLinkCardTest()
|
|
175
|
+
expect(link).toHaveClass('pkt-linkcard')
|
|
176
|
+
expect(link).toHaveClass('pkt-linkcard--normal')
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
test('applies different skin classes correctly', async () => {
|
|
180
|
+
for (const skin of VALID_SKINS) {
|
|
181
|
+
const { link } = await createLinkCardTest({ skin })
|
|
182
|
+
|
|
183
|
+
expect(link).toHaveClass('pkt-linkcard')
|
|
184
|
+
expect(link).toHaveClass(`pkt-linkcard--${skin}`)
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
test('handles skin property changes', async () => {
|
|
189
|
+
const { linkCard, link } = await createLinkCardTest({ skin: 'blue' })
|
|
190
|
+
|
|
191
|
+
expect(link).toHaveClass('pkt-linkcard--blue')
|
|
192
|
+
|
|
193
|
+
// Change skin
|
|
194
|
+
linkCard.skin = 'green'
|
|
195
|
+
await linkCard.updateComplete
|
|
196
|
+
|
|
197
|
+
expect(link).toHaveClass('pkt-linkcard--green')
|
|
198
|
+
expect(link).not.toHaveClass('pkt-linkcard--blue')
|
|
199
|
+
})
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
describe('Property Updates', () => {
|
|
203
|
+
test('updates title dynamically', async () => {
|
|
204
|
+
const { linkCard } = await createLinkCardTest({ title: 'Initial Title' })
|
|
205
|
+
|
|
206
|
+
expect(linkCard.querySelector('.pkt-linkcard__title')?.textContent).toBe('Initial Title')
|
|
207
|
+
|
|
208
|
+
linkCard.title = 'Updated Title'
|
|
209
|
+
await linkCard.updateComplete
|
|
210
|
+
|
|
211
|
+
expect(linkCard.querySelector('.pkt-linkcard__title')?.textContent).toBe('Updated Title')
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
test('updates href dynamically', async () => {
|
|
215
|
+
const { linkCard, link } = await createLinkCardTest({ href: '/initial' })
|
|
216
|
+
|
|
217
|
+
expect(link.getAttribute('href')).toBe('/initial')
|
|
218
|
+
|
|
219
|
+
linkCard.href = '/updated'
|
|
220
|
+
await linkCard.updateComplete
|
|
221
|
+
|
|
222
|
+
expect(link.getAttribute('href')).toBe('/updated')
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
test('updates openInNewTab dynamically', async () => {
|
|
226
|
+
const { linkCard, link } = await createLinkCardTest({ openInNewTab: false })
|
|
227
|
+
|
|
228
|
+
expect(link.getAttribute('target')).toBe('_self')
|
|
229
|
+
|
|
230
|
+
linkCard.openInNewTab = true
|
|
231
|
+
await linkCard.updateComplete
|
|
232
|
+
|
|
233
|
+
expect(link.getAttribute('target')).toBe('_blank')
|
|
234
|
+
})
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
describe('Complex Configurations', () => {
|
|
238
|
+
test('renders with all properties set', async () => {
|
|
239
|
+
const config: LinkCardTestConfig = {
|
|
240
|
+
title: 'Complete Link Card',
|
|
241
|
+
href: '/complete-path',
|
|
242
|
+
iconName: 'arrow-right',
|
|
243
|
+
openInNewTab: true,
|
|
244
|
+
skin: 'blue',
|
|
245
|
+
content: '<p>Complete content</p>',
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const { linkCard, link } = await createLinkCardTest(config)
|
|
249
|
+
|
|
250
|
+
// Verify all properties
|
|
251
|
+
expect(linkCard.title).toBe(config.title)
|
|
252
|
+
expect(linkCard.href).toBe(config.href)
|
|
253
|
+
expect(linkCard.iconName).toBe(config.iconName)
|
|
254
|
+
expect(linkCard.openInNewTab).toBe(config.openInNewTab)
|
|
255
|
+
expect(linkCard.skin).toBe(config.skin)
|
|
256
|
+
|
|
257
|
+
// Verify DOM elements
|
|
258
|
+
expect(link.getAttribute('href')).toBe(config.href)
|
|
259
|
+
expect(link.getAttribute('target')).toBe('_blank')
|
|
260
|
+
expect(link).toHaveClass(`pkt-linkcard--${config.skin}`)
|
|
261
|
+
|
|
262
|
+
const titleElement = linkCard.querySelector('.pkt-linkcard__title')
|
|
263
|
+
expect(titleElement?.textContent).toBe(config.title)
|
|
264
|
+
|
|
265
|
+
const icon = linkCard.querySelector('pkt-icon')
|
|
266
|
+
expect(icon?.getAttribute('name')).toBe(config.iconName)
|
|
267
|
+
|
|
268
|
+
const textSlot = linkCard.querySelector('.pkt-linkcard__text')
|
|
269
|
+
expect(textSlot?.innerHTML).toBe(config.content)
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
test('renders minimal configuration', async () => {
|
|
273
|
+
const { linkCard, link } = await createLinkCardTest({ href: '/minimal' })
|
|
274
|
+
|
|
275
|
+
expect(linkCard.href).toBe('/minimal')
|
|
276
|
+
expect(link.getAttribute('href')).toBe('/minimal')
|
|
277
|
+
expect(link).toHaveClass('pkt-linkcard--normal')
|
|
278
|
+
|
|
279
|
+
// Should not have title or icon
|
|
280
|
+
expect(linkCard.querySelector('.pkt-linkcard__title')).not.toBeInTheDocument()
|
|
281
|
+
expect(linkCard.querySelector('pkt-icon')).not.toBeInTheDocument()
|
|
282
|
+
})
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
describe('Edge Cases', () => {
|
|
286
|
+
test('handles empty string values', async () => {
|
|
287
|
+
const { linkCard } = await createLinkCardTest({
|
|
288
|
+
title: '',
|
|
289
|
+
href: '',
|
|
290
|
+
iconName: '',
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
expect(linkCard.title).toBe('')
|
|
294
|
+
expect(linkCard.href).toBe('')
|
|
295
|
+
expect(linkCard.iconName).toBe('')
|
|
296
|
+
|
|
297
|
+
// Should not render title or icon with empty strings
|
|
298
|
+
expect(linkCard.querySelector('.pkt-linkcard__title')).not.toBeInTheDocument()
|
|
299
|
+
expect(linkCard.querySelector('pkt-icon')).not.toBeInTheDocument()
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
test('handles special characters in title', async () => {
|
|
303
|
+
const specialTitle = 'Title with "quotes" & <symbols>'
|
|
304
|
+
const { linkCard } = await createLinkCardTest()
|
|
305
|
+
|
|
306
|
+
// Set title via property to avoid HTML attribute encoding issues
|
|
307
|
+
linkCard.title = specialTitle
|
|
308
|
+
await linkCard.updateComplete
|
|
309
|
+
|
|
310
|
+
expect(linkCard.title).toBe(specialTitle)
|
|
311
|
+
|
|
312
|
+
const titleElement = linkCard.querySelector('.pkt-linkcard__title')
|
|
313
|
+
expect(titleElement).toBeInTheDocument()
|
|
314
|
+
expect(titleElement?.textContent).toBe(specialTitle)
|
|
315
|
+
})
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
describe('Accessibility', () => {
|
|
319
|
+
test('has no accessibility violations', async () => {
|
|
320
|
+
const { linkCard } = await createLinkCardTest({
|
|
321
|
+
title: 'Accessible Link Card',
|
|
322
|
+
href: '/accessible',
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
const results = await axe(linkCard)
|
|
326
|
+
expect(results).toHaveNoViolations()
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
test('maintains proper link semantics', async () => {
|
|
330
|
+
const { link } = await createLinkCardTest({
|
|
331
|
+
title: 'Semantic Link',
|
|
332
|
+
href: '/semantic',
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
expect(link.tagName).toBe('A')
|
|
336
|
+
expect(link.getAttribute('href')).toBe('/semantic')
|
|
337
|
+
expect(link.textContent).toContain('Semantic Link')
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
test('handles external link accessibility', async () => {
|
|
341
|
+
const { link } = await createLinkCardTest({
|
|
342
|
+
title: 'External Link',
|
|
343
|
+
href: 'https://example.com',
|
|
344
|
+
openInNewTab: true,
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
expect(link.getAttribute('target')).toBe('_blank')
|
|
348
|
+
expect(link.getAttribute('rel')).toBe('noopener noreferrer')
|
|
349
|
+
})
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
describe('Type Safety', () => {
|
|
353
|
+
test('validates interface implementation', () => {
|
|
354
|
+
const linkCard = new PktLinkCard()
|
|
355
|
+
|
|
356
|
+
// Check that all interface properties exist
|
|
357
|
+
expect(linkCard).toHaveProperty('title')
|
|
358
|
+
expect(linkCard).toHaveProperty('href')
|
|
359
|
+
expect(linkCard).toHaveProperty('iconName')
|
|
360
|
+
expect(linkCard).toHaveProperty('openInNewTab')
|
|
361
|
+
expect(linkCard).toHaveProperty('skin')
|
|
362
|
+
})
|
|
363
|
+
})
|
|
364
|
+
})
|
|
@@ -5,6 +5,7 @@ import { Ref, createRef } from 'lit/directives/ref.js'
|
|
|
5
5
|
import { PktElement } from '@/base-elements/element'
|
|
6
6
|
import { PktSlotController } from '@/controllers/pkt-slot-controller'
|
|
7
7
|
import '@/components/icon'
|
|
8
|
+
import { ifDefined } from 'lit/directives/if-defined.js'
|
|
8
9
|
|
|
9
10
|
export type TLinkCardSkin =
|
|
10
11
|
| 'normal'
|
|
@@ -15,6 +16,7 @@ export type TLinkCardSkin =
|
|
|
15
16
|
| 'gray'
|
|
16
17
|
| 'beige-outline'
|
|
17
18
|
| 'gray-outline';
|
|
19
|
+
|
|
18
20
|
export interface IPktLinkCard {
|
|
19
21
|
title?: string
|
|
20
22
|
href?: string
|
|
@@ -54,7 +56,7 @@ export class PktLinkCard extends PktElement implements IPktLinkCard {
|
|
|
54
56
|
href=${this.href}
|
|
55
57
|
class=${classes}
|
|
56
58
|
target=${this.openInNewTab ? '_blank' : '_self'}
|
|
57
|
-
rel=${this.openInNewTab
|
|
59
|
+
rel=${ifDefined(this.openInNewTab ? 'noopener noreferrer' : undefined)}
|
|
58
60
|
>
|
|
59
61
|
${this.iconName && html`<pkt-icon class="pkt-link__icon" name="${this.iconName}" />`}
|
|
60
62
|
${this.title && html`<div class=${titleClasses}>${this.title}</div>`}
|