@oslokommune/punkt-elements 13.5.5 → 13.5.6

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 CHANGED
@@ -5,6 +5,23 @@ og skriver commits ca etter [Conventional Commits](https://conventionalcommits.o
5
5
 
6
6
  ---
7
7
 
8
+ ## [13.5.6](https://github.com/oslokommune/punkt/compare/13.5.5...13.5.6) (2025-09-09)
9
+
10
+ ### ⚠ BREAKING CHANGES
11
+ Ingen
12
+
13
+ ### Features
14
+ Ingen
15
+
16
+ ### Bug Fixes
17
+ Ingen
18
+
19
+ ### Chores
20
+ Ingen
21
+
22
+ ---
23
+
24
+
8
25
  ## [13.5.5](https://github.com/oslokommune/punkt/compare/13.5.4...13.5.5) (2025-09-09)
9
26
 
10
27
  ### ⚠ BREAKING CHANGES
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oslokommune/punkt-elements",
3
- "version": "13.5.5",
3
+ "version": "13.5.6",
4
4
  "description": "Komponentbiblioteket til Punkt, et designsystem laget av Oslo Origo",
5
5
  "homepage": "https://punkt.oslo.kommune.no",
6
6
  "author": "Team Designsystem, Oslo Origo",
@@ -73,5 +73,5 @@
73
73
  "url": "https://github.com/oslokommune/punkt/issues"
74
74
  },
75
75
  "license": "MIT",
76
- "gitHead": "f65d3c81c0543cc360a34fe346b4e0585deb46ac"
76
+ "gitHead": "5f0019a262ea4b9086590f7b6d572f5a39142f31"
77
77
  }
@@ -0,0 +1,211 @@
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 { type IPktTag } from './tag'
7
+ import './tag'
8
+
9
+ export interface TagTestConfig extends Partial<IPktTag>, BaseTestConfig {}
10
+
11
+ // Use shared framework
12
+ export const createTagTest = async (config: TagTestConfig = {}) => {
13
+ const { container, element } = await createElementTest<
14
+ CustomElementFor<'pkt-tag'>,
15
+ TagTestConfig
16
+ >('pkt-tag', config)
17
+
18
+ return {
19
+ container,
20
+ tag: element,
21
+ }
22
+ }
23
+
24
+ expect.extend(toHaveNoViolations)
25
+
26
+ // Test data constants
27
+ const VALID_SKINS = [
28
+ 'blue',
29
+ 'blue-dark',
30
+ 'blue-light',
31
+ 'green',
32
+ 'red',
33
+ 'yellow',
34
+ 'beige',
35
+ 'gray',
36
+ 'grey',
37
+ ] as const
38
+ const VALID_SIZES = ['small', 'medium', 'large'] as const
39
+ const VALID_TYPES = ['button', 'reset', 'submit'] as const
40
+
41
+ afterEach(() => {
42
+ document.body.innerHTML = ''
43
+ })
44
+
45
+ describe('PktTag', () => {
46
+ describe('Basic Rendering', () => {
47
+ test('renders without errors', async () => {
48
+ const { tag } = await createTagTest()
49
+ expect(tag).toBeInTheDocument()
50
+ })
51
+
52
+ test('renders with default properties', async () => {
53
+ const { tag } = await createTagTest()
54
+ expect(tag.closeTag).toBe(false)
55
+ expect(tag.size).toBe('medium')
56
+ expect(tag.skin).toBe('blue')
57
+ expect(tag.type).toBe('button')
58
+ })
59
+
60
+ test('renders content in slot', async () => {
61
+ const content = 'Test Tag Content'
62
+ const { tag } = await createTagTest({ content })
63
+ expect(tag.textContent?.trim()).toBe(content)
64
+ })
65
+ })
66
+
67
+ describe('Skin Variations', () => {
68
+ test('applies different skin classes correctly', async () => {
69
+ for (const skin of VALID_SKINS) {
70
+ const { tag } = await createTagTest({ skin })
71
+ // For non-closeable tags, check the span element
72
+ const tagElement = tag.querySelector('span') || tag.querySelector('button')
73
+ expect(tagElement).toHaveClass(`pkt-tag--${skin}`)
74
+ }
75
+ })
76
+ })
77
+
78
+ describe('Size Variations', () => {
79
+ test('applies different size classes correctly', async () => {
80
+ for (const size of VALID_SIZES) {
81
+ const { tag } = await createTagTest({ size })
82
+ // For non-closeable tags, check the span element
83
+ const tagElement = tag.querySelector('span') || tag.querySelector('button')
84
+ expect(tagElement).toHaveClass(`pkt-tag--${size}`)
85
+ }
86
+ })
87
+ })
88
+
89
+ describe('Type Variations', () => {
90
+ test('sets correct type attribute', async () => {
91
+ for (const type of VALID_TYPES) {
92
+ const { tag } = await createTagTest({ type })
93
+ expect(tag.type).toBe(type)
94
+ }
95
+ })
96
+ })
97
+
98
+ describe('Icon Functionality', () => {
99
+ test('renders icon when iconName provided', async () => {
100
+ const { tag } = await createTagTest({ iconName: 'arrow-right' })
101
+ const icon = tag.querySelector('pkt-icon')
102
+ expect(icon).toBeInTheDocument()
103
+ expect(icon?.getAttribute('name')).toBe('arrow-right')
104
+ })
105
+
106
+ test('does not render icon when iconName not provided', async () => {
107
+ const { tag } = await createTagTest()
108
+ const icon = tag.querySelector('pkt-icon')
109
+ expect(icon).not.toBeInTheDocument()
110
+ })
111
+ })
112
+
113
+ describe('Close Functionality', () => {
114
+ test('renders close button when closeTag is true', async () => {
115
+ const { tag } = await createTagTest({ closeTag: true })
116
+ const closeIcon = tag.querySelector('.pkt-tag__close-btn')
117
+ expect(closeIcon).toBeInTheDocument()
118
+ })
119
+
120
+ test('does not render close button when closeTag is false', async () => {
121
+ const { tag } = await createTagTest({ closeTag: false })
122
+ const closeIcon = tag.querySelector('.pkt-tag__close-btn')
123
+ expect(closeIcon).not.toBeInTheDocument()
124
+ })
125
+
126
+ test('dispatches close event when close button is clicked', async () => {
127
+ const { tag } = await createTagTest({ closeTag: true })
128
+ const closeSpy = jest.fn()
129
+ tag.addEventListener('close', closeSpy)
130
+
131
+ const button = tag.querySelector('button') as HTMLButtonElement
132
+ fireEvent.click(button)
133
+
134
+ expect(closeSpy).toHaveBeenCalled()
135
+ })
136
+
137
+ test('hides tag when closed', async () => {
138
+ const { tag } = await createTagTest({ closeTag: true })
139
+ const button = tag.querySelector('button') as HTMLButtonElement
140
+
141
+ fireEvent.click(button)
142
+ await tag.updateComplete
143
+
144
+ expect(button).toHaveClass('pkt-hide')
145
+ })
146
+ })
147
+
148
+ describe('Text Style', () => {
149
+ test('applies text style class when provided', async () => {
150
+ const { tag } = await createTagTest({ textStyle: 'thin-text' })
151
+ const tagElement = tag.querySelector('span') || tag.querySelector('button')
152
+ expect(tagElement).toHaveClass('pkt-tag--thin-text')
153
+ })
154
+ })
155
+
156
+ describe('Accessibility', () => {
157
+ test('applies aria-label when provided', async () => {
158
+ const { tag } = await createTagTest({ closeTag: true, ariaLabel: 'Close tag' })
159
+ const button = tag.querySelector('button')
160
+ expect(button?.getAttribute('aria-label')).toBe('Close tag')
161
+ })
162
+
163
+ test('tag is accessible', async () => {
164
+ const { tag } = await createTagTest({
165
+ content: 'Accessible Tag',
166
+ closeTag: true,
167
+ ariaLabel: 'Close tag',
168
+ })
169
+
170
+ const results = await axe(tag)
171
+ expect(results).toHaveNoViolations()
172
+ })
173
+ })
174
+
175
+ describe('Complex Configurations', () => {
176
+ test('renders with all properties set', async () => {
177
+ const config: TagTestConfig = {
178
+ content: 'Complete Tag',
179
+ closeTag: true,
180
+ size: 'large',
181
+ skin: 'green',
182
+ iconName: 'check',
183
+ type: 'submit',
184
+ textStyle: 'thin-text',
185
+ ariaLabel: 'Complete tag',
186
+ }
187
+
188
+ const { tag } = await createTagTest(config)
189
+
190
+ expect(tag.textContent?.trim()).toBe(config.content)
191
+ expect(tag.closeTag).toBe(config.closeTag)
192
+ expect(tag.size).toBe(config.size)
193
+ expect(tag.skin).toBe(config.skin)
194
+ expect(tag.iconName).toBe(config.iconName)
195
+ expect(tag.type).toBe(config.type)
196
+ expect(tag.textStyle).toBe(config.textStyle)
197
+ expect(tag.ariaLabel).toBe(config.ariaLabel)
198
+
199
+ const button = tag.querySelector('button')
200
+ expect(button).toHaveClass(`pkt-tag--${config.size}`)
201
+ expect(button).toHaveClass(`pkt-tag--${config.skin}`)
202
+ expect(button).toHaveClass(`pkt-tag--${config.textStyle}`)
203
+
204
+ const icon = tag.querySelector('pkt-icon')
205
+ expect(icon?.getAttribute('name')).toBe(config.iconName)
206
+
207
+ const closeIcon = tag.querySelector('.pkt-tag__close-btn')
208
+ expect(closeIcon).toBeInTheDocument()
209
+ })
210
+ })
211
+ })
@@ -0,0 +1,289 @@
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 './textarea'
7
+
8
+ export interface TextareaTestConfig extends BaseTestConfig {
9
+ // From PktTextarea specific properties
10
+ value?: string
11
+ autocomplete?: string
12
+ rows?: number | null
13
+
14
+ // From PktInputElement base class (commonly used ones)
15
+ id?: string
16
+ label?: string
17
+ name?: string
18
+ disabled?: boolean
19
+ readonly?: boolean
20
+ required?: boolean
21
+ placeholder?: string | null
22
+ maxlength?: number | null
23
+ minlength?: number | null
24
+ hasError?: boolean
25
+ errorMessage?: string
26
+ helptext?: string
27
+ fullwidth?: boolean
28
+ counter?: boolean
29
+ inline?: boolean
30
+ ariaLabelledby?: string | null
31
+ ariaDescribedBy?: string | null
32
+ }
33
+
34
+ // Use shared framework
35
+ export const createTextareaTest = async (config: TextareaTestConfig = {}) => {
36
+ const { container, element } = await createElementTest<
37
+ CustomElementFor<'pkt-textarea'>,
38
+ TextareaTestConfig
39
+ >('pkt-textarea', config)
40
+
41
+ return {
42
+ container,
43
+ textarea: element,
44
+ }
45
+ }
46
+
47
+ expect.extend(toHaveNoViolations)
48
+
49
+ afterEach(() => {
50
+ document.body.innerHTML = ''
51
+ })
52
+
53
+ describe('PktTextarea', () => {
54
+ describe('Basic Rendering', () => {
55
+ test('renders without errors', async () => {
56
+ const { textarea } = await createTextareaTest()
57
+ expect(textarea).toBeInTheDocument()
58
+ })
59
+
60
+ test('renders with default properties', async () => {
61
+ const { textarea } = await createTextareaTest()
62
+ expect(textarea.value).toBe('')
63
+ expect(textarea.autocomplete).toBe('off')
64
+ expect(textarea.rows).toBe(null)
65
+ })
66
+
67
+ test('renders textarea element', async () => {
68
+ const { textarea } = await createTextareaTest()
69
+ const textareaElement = textarea.querySelector('textarea')
70
+ expect(textareaElement).toBeInTheDocument()
71
+ })
72
+ })
73
+
74
+ describe('Properties and Attributes', () => {
75
+ test('sets value correctly', async () => {
76
+ const value = 'Test textarea content'
77
+ const { textarea } = await createTextareaTest({ value })
78
+
79
+ expect(textarea.value).toBe(value)
80
+ const textareaElement = textarea.querySelector('textarea') as HTMLTextAreaElement
81
+ expect(textareaElement.value).toBe(value)
82
+ })
83
+
84
+ test('sets rows correctly', async () => {
85
+ const { textarea } = await createTextareaTest({ rows: 5 })
86
+
87
+ expect(textarea.rows).toBe(5)
88
+ const textareaElement = textarea.querySelector('textarea')
89
+ expect(textareaElement?.getAttribute('rows')).toBe('5')
90
+ })
91
+
92
+ test('sets autocomplete correctly', async () => {
93
+ const { textarea } = await createTextareaTest({ autocomplete: 'on' })
94
+
95
+ expect(textarea.autocomplete).toBe('on')
96
+ const textareaElement = textarea.querySelector('textarea')
97
+ expect(textareaElement?.getAttribute('autocomplete')).toBe('on')
98
+ })
99
+
100
+ test('handles disabled state', async () => {
101
+ const { textarea } = await createTextareaTest({ disabled: true })
102
+
103
+ const textareaElement = textarea.querySelector('textarea')
104
+ expect(textareaElement?.hasAttribute('disabled')).toBe(true)
105
+ })
106
+
107
+ test('handles readonly state', async () => {
108
+ const { textarea } = await createTextareaTest({ readonly: true })
109
+
110
+ const textareaElement = textarea.querySelector('textarea')
111
+ expect(textareaElement?.hasAttribute('readonly')).toBe(true)
112
+ })
113
+
114
+ test('handles required state', async () => {
115
+ const { textarea } = await createTextareaTest({ required: true })
116
+
117
+ const textareaElement = textarea.querySelector('textarea')
118
+ expect(textareaElement?.hasAttribute('required')).toBe(false) // Not set as attribute on textarea
119
+
120
+ const inputWrapper = textarea.querySelector('pkt-input-wrapper')
121
+ expect(inputWrapper?.hasAttribute('required')).toBe(true) // But passed to wrapper
122
+ })
123
+ })
124
+
125
+ describe('Input Wrapper Integration', () => {
126
+ test('displays label correctly', async () => {
127
+ const { textarea } = await createTextareaTest({ label: 'Comment' })
128
+
129
+ const inputWrapper = textarea.querySelector('pkt-input-wrapper')
130
+ expect(inputWrapper?.getAttribute('label')).toBe('Comment')
131
+ })
132
+
133
+ test('displays helptext correctly', async () => {
134
+ const { textarea } = await createTextareaTest({ helptext: 'Enter your message' })
135
+
136
+ // helptext is passed as a property, not attribute to input-wrapper
137
+ expect(textarea.helptext).toBe('Enter your message')
138
+ })
139
+
140
+ test('handles error state', async () => {
141
+ const { textarea } = await createTextareaTest({
142
+ hasError: true,
143
+ errorMessage: 'This field is required',
144
+ })
145
+
146
+ expect(textarea.hasError).toBe(true)
147
+ expect(textarea.errorMessage).toBe('This field is required')
148
+
149
+ const inputWrapper = textarea.querySelector('pkt-input-wrapper')
150
+ expect(inputWrapper?.hasAttribute('hasError')).toBe(true)
151
+
152
+ const textareaElement = textarea.querySelector('textarea')
153
+ expect(textareaElement?.getAttribute('aria-invalid')).toBe('true')
154
+ })
155
+
156
+ test('handles fullwidth styling', async () => {
157
+ const { textarea } = await createTextareaTest({ fullwidth: true })
158
+
159
+ const textareaElement = textarea.querySelector('textarea')
160
+ expect(textareaElement?.className).toContain('pkt-input--fullwidth')
161
+ })
162
+ })
163
+
164
+ describe('Character Counter', () => {
165
+ test('shows counter when enabled', async () => {
166
+ const { textarea } = await createTextareaTest({
167
+ counter: true,
168
+ maxlength: 100,
169
+ })
170
+
171
+ const inputWrapper = textarea.querySelector('pkt-input-wrapper')
172
+ expect(inputWrapper?.hasAttribute('counter')).toBe(true)
173
+ })
174
+
175
+ test('updates counter on value change', async () => {
176
+ const { textarea } = await createTextareaTest({
177
+ counter: true,
178
+ maxlength: 100,
179
+ value: 'Hello',
180
+ })
181
+
182
+ expect(textarea.counterCurrent).toBe(5)
183
+ })
184
+ })
185
+
186
+ describe('User Interaction', () => {
187
+ test('updates value on user input', async () => {
188
+ const { textarea } = await createTextareaTest()
189
+ const textareaElement = textarea.querySelector('textarea') as HTMLTextAreaElement
190
+
191
+ fireEvent.input(textareaElement, { target: { value: 'New content' } })
192
+ await textarea.updateComplete
193
+
194
+ expect(textarea.value).toBe('New content')
195
+ expect(textarea.touched).toBe(true)
196
+ })
197
+
198
+ test('handles focus and blur events', async () => {
199
+ const { textarea } = await createTextareaTest()
200
+ const textareaElement = textarea.querySelector('textarea') as HTMLTextAreaElement
201
+
202
+ // Focus and input to trigger touched state
203
+ fireEvent.focus(textareaElement)
204
+ fireEvent.input(textareaElement, { target: { value: 'test input' } })
205
+ await textarea.updateComplete
206
+ fireEvent.blur(textareaElement)
207
+ await textarea.updateComplete
208
+
209
+ // Test that input with value change sets touched state
210
+ expect(textarea.touched).toBe(true)
211
+ })
212
+ })
213
+
214
+ describe('Validation', () => {
215
+ test('respects maxlength constraint', async () => {
216
+ const { textarea } = await createTextareaTest({ maxlength: 10 })
217
+
218
+ const textareaElement = textarea.querySelector('textarea')
219
+ expect(textareaElement?.getAttribute('maxlength')).toBe('10')
220
+ })
221
+
222
+ test('respects minlength constraint', async () => {
223
+ const { textarea } = await createTextareaTest({ minlength: 5 })
224
+
225
+ const textareaElement = textarea.querySelector('textarea')
226
+ expect(textareaElement?.getAttribute('minlength')).toBe('5')
227
+ })
228
+ })
229
+
230
+ describe('Accessibility', () => {
231
+ test('passes through accessibility attributes', async () => {
232
+ const { textarea } = await createTextareaTest({
233
+ ariaLabelledby: 'external-label',
234
+ ariaDescribedBy: 'external-description',
235
+ })
236
+
237
+ const textareaElement = textarea.querySelector('textarea')
238
+ expect(textareaElement?.getAttribute('aria-labelledby')).toBe('external-label')
239
+
240
+ // ariaDescribedBy is passed as property to input-wrapper
241
+ expect(textarea.ariaDescribedBy).toBe('external-description')
242
+ })
243
+
244
+ test('textarea is accessible', async () => {
245
+ const { textarea } = await createTextareaTest({
246
+ label: 'Message',
247
+ helptext: 'Enter your message here',
248
+ required: true,
249
+ })
250
+
251
+ const results = await axe(textarea)
252
+ expect(results).toHaveNoViolations()
253
+ })
254
+ })
255
+
256
+ describe('Complex Configuration', () => {
257
+ test('renders with all properties set', async () => {
258
+ const config: TextareaTestConfig = {
259
+ label: 'Feedback',
260
+ value: 'Initial feedback text',
261
+ placeholder: 'Enter your feedback...',
262
+ rows: 6,
263
+ maxlength: 500,
264
+ counter: true,
265
+ required: true,
266
+ helptext: 'Please provide detailed feedback',
267
+ }
268
+
269
+ const { textarea } = await createTextareaTest(config)
270
+
271
+ expect(textarea.value).toBe(config.value)
272
+ expect(textarea.rows).toBe(config.rows)
273
+ expect(textarea.maxlength).toBe(config.maxlength)
274
+ expect(textarea.required).toBe(config.required)
275
+
276
+ const inputWrapper = textarea.querySelector('pkt-input-wrapper')
277
+ expect(inputWrapper?.getAttribute('label')).toBe(config.label)
278
+ expect(textarea.helptext).toBe(config.helptext) // Property, not attribute
279
+ expect(inputWrapper?.hasAttribute('counter')).toBe(true)
280
+
281
+ const textareaElement = textarea.querySelector('textarea')
282
+ expect(textareaElement?.getAttribute('placeholder')).toBe(config.placeholder)
283
+ expect(textareaElement?.getAttribute('rows')).toBe(String(config.rows))
284
+ expect(textareaElement?.getAttribute('maxlength')).toBe(String(config.maxlength))
285
+ // required is handled by input-wrapper, not set directly on textarea
286
+ expect(textarea.required).toBe(true)
287
+ })
288
+ })
289
+ })
@@ -0,0 +1,421 @@
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 './textinput'
7
+
8
+ export interface TextinputTestConfig extends BaseTestConfig {
9
+ // From PktTextinput specific properties
10
+ type?: string
11
+ value?: string
12
+ autocomplete?: string | null
13
+ iconNameRight?: string | null
14
+ prefix?: string | null
15
+ suffix?: string | null
16
+ size?: number | null
17
+ omitSearchIcon?: boolean
18
+
19
+ // From PktInputElement base class (commonly used ones)
20
+ id?: string
21
+ label?: string
22
+ name?: string
23
+ disabled?: boolean
24
+ readonly?: boolean
25
+ required?: boolean
26
+ placeholder?: string | null
27
+ maxlength?: number | null
28
+ minlength?: number | null
29
+ hasError?: boolean
30
+ errorMessage?: string
31
+ helptext?: string
32
+ fullwidth?: boolean
33
+ counter?: boolean
34
+ inline?: boolean
35
+ ariaLabelledby?: string | null
36
+ ariaDescribedBy?: string | null
37
+ }
38
+
39
+ // Use shared framework
40
+ export const createTextinputTest = async (config: TextinputTestConfig = {}) => {
41
+ const { container, element } = await createElementTest<
42
+ CustomElementFor<'pkt-textinput'>,
43
+ TextinputTestConfig
44
+ >('pkt-textinput', config)
45
+
46
+ return {
47
+ container,
48
+ textinput: element,
49
+ }
50
+ }
51
+
52
+ expect.extend(toHaveNoViolations)
53
+
54
+ afterEach(() => {
55
+ document.body.innerHTML = ''
56
+ })
57
+
58
+ describe('PktTextinput', () => {
59
+ describe('Basic Rendering', () => {
60
+ test('renders without errors', async () => {
61
+ const { textinput } = await createTextinputTest()
62
+ expect(textinput).toBeInTheDocument()
63
+ })
64
+
65
+ test('renders with default properties', async () => {
66
+ const { textinput } = await createTextinputTest()
67
+ expect(textinput.type).toBe('text')
68
+ expect(textinput.value).toBe('')
69
+ expect(textinput.autocomplete).toBe(null) // Property defaults to null, template sets 'off'
70
+ })
71
+
72
+ test('renders input element', async () => {
73
+ const { textinput } = await createTextinputTest()
74
+ const inputElement = textinput.querySelector('input')
75
+ expect(inputElement).toBeInTheDocument()
76
+ })
77
+ })
78
+
79
+ describe('Input Types', () => {
80
+ test('renders text input by default', async () => {
81
+ const { textinput } = await createTextinputTest()
82
+ const inputElement = textinput.querySelector('input')
83
+ expect(inputElement?.getAttribute('type')).toBe('text')
84
+ })
85
+
86
+ test('renders email input type', async () => {
87
+ const { textinput } = await createTextinputTest({ type: 'email' })
88
+ const inputElement = textinput.querySelector('input')
89
+ expect(inputElement?.getAttribute('type')).toBe('email')
90
+ })
91
+
92
+ test('renders password input type', async () => {
93
+ const { textinput } = await createTextinputTest({ type: 'password' })
94
+ const inputElement = textinput.querySelector('input')
95
+ expect(inputElement?.getAttribute('type')).toBe('password')
96
+ })
97
+
98
+ test('renders tel input type', async () => {
99
+ const { textinput } = await createTextinputTest({ type: 'tel' })
100
+ const inputElement = textinput.querySelector('input')
101
+ expect(inputElement?.getAttribute('type')).toBe('tel')
102
+ })
103
+
104
+ test('renders url input type', async () => {
105
+ const { textinput } = await createTextinputTest({ type: 'url' })
106
+ const inputElement = textinput.querySelector('input')
107
+ expect(inputElement?.getAttribute('type')).toBe('url')
108
+ })
109
+
110
+ test('renders search input type', async () => {
111
+ const { textinput } = await createTextinputTest({ type: 'search' })
112
+ const inputElement = textinput.querySelector('input')
113
+ expect(inputElement?.getAttribute('type')).toBe('search')
114
+ })
115
+ })
116
+
117
+ describe('Properties and Attributes', () => {
118
+ test('sets value correctly', async () => {
119
+ const value = 'Test input value'
120
+ const { textinput } = await createTextinputTest({ value })
121
+
122
+ expect(textinput.value).toBe(value)
123
+ const inputElement = textinput.querySelector('input') as HTMLInputElement
124
+ expect(inputElement.value).toBe(value)
125
+ })
126
+
127
+ test('sets autocomplete correctly', async () => {
128
+ const { textinput } = await createTextinputTest({ autocomplete: 'email' })
129
+
130
+ expect(textinput.autocomplete).toBe('email')
131
+ const inputElement = textinput.querySelector('input')
132
+ expect(inputElement?.getAttribute('autocomplete')).toBe('email')
133
+ })
134
+
135
+ test('handles disabled state', async () => {
136
+ const { textinput } = await createTextinputTest({ disabled: true })
137
+
138
+ const inputElement = textinput.querySelector('input')
139
+ expect(inputElement?.hasAttribute('disabled')).toBe(true)
140
+ })
141
+
142
+ test('handles readonly state', async () => {
143
+ const { textinput } = await createTextinputTest({ readonly: true })
144
+
145
+ const inputElement = textinput.querySelector('input')
146
+ expect(inputElement?.hasAttribute('readonly')).toBe(true)
147
+ })
148
+
149
+ test('handles required state', async () => {
150
+ const { textinput } = await createTextinputTest({ required: true })
151
+
152
+ const inputElement = textinput.querySelector('input')
153
+ expect(inputElement?.hasAttribute('required')).toBe(false) // Not set as attribute on input
154
+
155
+ const inputWrapper = textinput.querySelector('pkt-input-wrapper')
156
+ expect(inputWrapper?.hasAttribute('required')).toBe(true) // But passed to wrapper
157
+ })
158
+ })
159
+
160
+ describe('Icons', () => {
161
+ test('renders right icon', async () => {
162
+ const { textinput } = await createTextinputTest({
163
+ iconNameRight: 'search',
164
+ })
165
+
166
+ expect(textinput.iconNameRight).toBe('search')
167
+
168
+ const icon = textinput.querySelector('pkt-icon')
169
+ expect(icon).toBeInTheDocument()
170
+ expect(icon?.getAttribute('name')).toBe('search')
171
+ })
172
+
173
+ test('renders search icon for search type by default', async () => {
174
+ const { textinput } = await createTextinputTest({ type: 'search' })
175
+
176
+ const icon = textinput.querySelector('pkt-icon')
177
+ expect(icon).toBeInTheDocument()
178
+ expect(icon?.getAttribute('name')).toBe('magnifying-glass-big')
179
+ })
180
+
181
+ test('can omit search icon for search type', async () => {
182
+ const { textinput } = await createTextinputTest({
183
+ type: 'search',
184
+ omitSearchIcon: true,
185
+ })
186
+
187
+ const icon = textinput.querySelector('pkt-icon')
188
+ expect(icon).not.toBeInTheDocument()
189
+ })
190
+ })
191
+
192
+ describe('Prefix and Suffix', () => {
193
+ test('renders prefix text', async () => {
194
+ const { textinput } = await createTextinputTest({ prefix: 'https://' })
195
+
196
+ expect(textinput.prefix).toBe('https://')
197
+ const prefixElement = textinput.querySelector('.pkt-input-prefix')
198
+ expect(prefixElement).toBeInTheDocument()
199
+ expect(prefixElement?.textContent).toBe('https://')
200
+ })
201
+
202
+ test('renders suffix text', async () => {
203
+ const { textinput } = await createTextinputTest({ suffix: '.com' })
204
+
205
+ expect(textinput.suffix).toBe('.com')
206
+ const suffixElement = textinput.querySelector('.pkt-input-suffix')
207
+ expect(suffixElement).toBeInTheDocument()
208
+ expect(suffixElement?.textContent?.trim()).toBe('.com')
209
+ })
210
+
211
+ test('renders both prefix and suffix', async () => {
212
+ const { textinput } = await createTextinputTest({
213
+ prefix: '$',
214
+ suffix: 'USD',
215
+ })
216
+
217
+ const prefixElement = textinput.querySelector('.pkt-input-prefix')
218
+ const suffixElement = textinput.querySelector('.pkt-input-suffix')
219
+
220
+ expect(prefixElement?.textContent).toBe('$')
221
+ expect(suffixElement?.textContent?.trim()).toBe('USD')
222
+ })
223
+ })
224
+
225
+ describe('Input Wrapper Integration', () => {
226
+ test('displays label correctly', async () => {
227
+ const { textinput } = await createTextinputTest({ label: 'Email Address' })
228
+
229
+ const inputWrapper = textinput.querySelector('pkt-input-wrapper')
230
+ expect(inputWrapper?.getAttribute('label')).toBe('Email Address')
231
+ })
232
+
233
+ test('displays helptext correctly', async () => {
234
+ const { textinput } = await createTextinputTest({ helptext: 'Enter a valid email' })
235
+
236
+ // helptext is passed as a property, not attribute to input-wrapper
237
+ expect(textinput.helptext).toBe('Enter a valid email')
238
+ })
239
+
240
+ test('handles error state', async () => {
241
+ const { textinput } = await createTextinputTest({
242
+ hasError: true,
243
+ errorMessage: 'Email is required',
244
+ })
245
+
246
+ expect(textinput.hasError).toBe(true)
247
+ expect(textinput.errorMessage).toBe('Email is required')
248
+
249
+ const inputWrapper = textinput.querySelector('pkt-input-wrapper')
250
+ expect(inputWrapper?.hasAttribute('hasError')).toBe(true)
251
+
252
+ const inputElement = textinput.querySelector('input')
253
+ expect(inputElement?.getAttribute('aria-invalid')).toBe('true')
254
+ })
255
+
256
+ test('handles fullwidth styling', async () => {
257
+ const { textinput } = await createTextinputTest({ fullwidth: true })
258
+
259
+ const inputElement = textinput.querySelector('input')
260
+ expect(inputElement?.className).toContain('pkt-input--fullwidth')
261
+ })
262
+ })
263
+
264
+ describe('Character Counter', () => {
265
+ test('shows counter when enabled', async () => {
266
+ const { textinput } = await createTextinputTest({
267
+ counter: true,
268
+ maxlength: 50,
269
+ })
270
+
271
+ const inputWrapper = textinput.querySelector('pkt-input-wrapper')
272
+ expect(inputWrapper?.hasAttribute('counter')).toBe(true)
273
+ })
274
+
275
+ test('updates counter on value change', async () => {
276
+ const { textinput } = await createTextinputTest({
277
+ counter: true,
278
+ maxlength: 50,
279
+ value: 'Hello',
280
+ })
281
+
282
+ expect(textinput.counterCurrent).toBe(5)
283
+ })
284
+ })
285
+
286
+ describe('User Interaction', () => {
287
+ test('updates value on user input', async () => {
288
+ const { textinput } = await createTextinputTest()
289
+ const inputElement = textinput.querySelector('input') as HTMLInputElement
290
+
291
+ fireEvent.input(inputElement, { target: { value: 'new value' } })
292
+ await textinput.updateComplete
293
+
294
+ expect(textinput.value).toBe('new value')
295
+ expect(textinput.touched).toBe(true)
296
+ })
297
+
298
+ test('handles focus and blur events', async () => {
299
+ const { textinput } = await createTextinputTest()
300
+ const inputElement = textinput.querySelector('input') as HTMLInputElement
301
+
302
+ // Focus and input to trigger touched state
303
+ fireEvent.focus(inputElement)
304
+ fireEvent.input(inputElement, { target: { value: 'test input' } })
305
+ await textinput.updateComplete
306
+ fireEvent.blur(inputElement)
307
+ await textinput.updateComplete
308
+
309
+ // Test that input with value change sets touched state
310
+ expect(textinput.touched).toBe(true)
311
+ })
312
+ })
313
+
314
+ describe('Validation', () => {
315
+ test('respects maxlength constraint', async () => {
316
+ const { textinput } = await createTextinputTest({ maxlength: 20 })
317
+
318
+ const inputElement = textinput.querySelector('input')
319
+ expect(inputElement?.getAttribute('maxlength')).toBe('20')
320
+ })
321
+
322
+ test('respects minlength constraint', async () => {
323
+ const { textinput } = await createTextinputTest({ minlength: 3 })
324
+
325
+ const inputElement = textinput.querySelector('input')
326
+ expect(inputElement?.getAttribute('minlength')).toBe('3')
327
+ })
328
+ })
329
+
330
+ describe('Accessibility', () => {
331
+ test('passes through accessibility attributes', async () => {
332
+ const { textinput } = await createTextinputTest({
333
+ ariaLabelledby: 'external-label',
334
+ ariaDescribedBy: 'external-description',
335
+ })
336
+
337
+ const inputElement = textinput.querySelector('input')
338
+ expect(inputElement?.getAttribute('aria-labelledby')).toBe('external-label')
339
+
340
+ // ariaDescribedBy is passed as property to input-wrapper
341
+ expect(textinput.ariaDescribedBy).toBe('external-description')
342
+ })
343
+
344
+ test('textinput is accessible', async () => {
345
+ const { textinput } = await createTextinputTest({
346
+ label: 'Email',
347
+ type: 'email',
348
+ helptext: 'Enter your email address',
349
+ required: true,
350
+ })
351
+
352
+ const results = await axe(textinput)
353
+ expect(results).toHaveNoViolations()
354
+ })
355
+ })
356
+
357
+ describe('Complex Configuration', () => {
358
+ test('renders email input with all features', async () => {
359
+ const config: TextinputTestConfig = {
360
+ type: 'email',
361
+ label: 'Email Address',
362
+ value: 'user@example.com',
363
+ placeholder: 'Enter your email...',
364
+ iconNameRight: 'mail',
365
+ maxlength: 100,
366
+ counter: true,
367
+ required: true,
368
+ autocomplete: 'email',
369
+ helptext: 'We will never share your email',
370
+ }
371
+
372
+ const { textinput } = await createTextinputTest(config)
373
+
374
+ expect(textinput.type).toBe(config.type)
375
+ expect(textinput.value).toBe(config.value)
376
+ expect(textinput.iconNameRight).toBe(config.iconNameRight)
377
+ expect(textinput.maxlength).toBe(config.maxlength)
378
+ expect(textinput.required).toBe(config.required)
379
+ expect(textinput.autocomplete).toBe(config.autocomplete)
380
+
381
+ const inputWrapper = textinput.querySelector('pkt-input-wrapper')
382
+ expect(inputWrapper?.getAttribute('label')).toBe(config.label)
383
+ expect(textinput.helptext).toBe(config.helptext) // Property, not attribute
384
+ expect(inputWrapper?.hasAttribute('counter')).toBe(true)
385
+
386
+ const inputElement = textinput.querySelector('input')
387
+ expect(inputElement?.getAttribute('type')).toBe(config.type)
388
+ expect(inputElement?.getAttribute('placeholder')).toBe(config.placeholder)
389
+ expect(inputElement?.getAttribute('maxlength')).toBe(String(config.maxlength))
390
+ expect(inputElement?.getAttribute('autocomplete')).toBe(config.autocomplete)
391
+
392
+ const icon = textinput.querySelector('pkt-icon')
393
+ expect(icon?.getAttribute('name')).toBe(config.iconNameRight)
394
+ })
395
+
396
+ test('renders URL input with prefix and suffix', async () => {
397
+ const config: TextinputTestConfig = {
398
+ type: 'url',
399
+ label: 'Website URL',
400
+ prefix: 'https://',
401
+ suffix: '.com',
402
+ placeholder: 'example',
403
+ fullwidth: true,
404
+ }
405
+
406
+ const { textinput } = await createTextinputTest(config)
407
+
408
+ expect(textinput.prefix).toBe(config.prefix)
409
+ expect(textinput.suffix).toBe(config.suffix)
410
+
411
+ const prefixElement = textinput.querySelector('.pkt-input-prefix')
412
+ const suffixElement = textinput.querySelector('.pkt-input-suffix')
413
+
414
+ expect(prefixElement?.textContent).toBe(config.prefix)
415
+ expect(suffixElement?.textContent?.trim()).toBe(config.suffix)
416
+
417
+ const inputElement = textinput.querySelector('input')
418
+ expect(inputElement?.className).toContain('pkt-input--fullwidth')
419
+ })
420
+ })
421
+ })