@oslokommune/punkt-elements 16.13.2 → 16.15.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.
- package/CHANGELOG.md +35 -1
- package/dist/calendar-Ch1dgnBo.js +656 -0
- package/dist/calendar-DkjXla0I.cjs +90 -0
- package/dist/{card-CQYxrtL0.cjs → card-C3POnP3O.cjs} +1 -1
- package/dist/{card-Bir3-pl2.js → card-CEDbo1t7.js} +2 -2
- package/dist/{checkbox-Cspxcvt_.js → checkbox-Bjp43NxA.js} +1 -1
- package/dist/{checkbox-BARDb9iV.cjs → checkbox-nTbihMxW.cjs} +1 -1
- package/dist/{combobox-CGbFTWq_.cjs → combobox-C4Q6kQEs.cjs} +1 -1
- package/dist/{combobox-BcdLZRDU.js → combobox-CL9BLuxt.js} +6 -6
- package/dist/components/fileupload/fileupload-base.d.ts +116 -0
- package/dist/components/fileupload/fileupload-operations.d.ts +4 -0
- package/dist/components/fileupload/fileupload-renderers.d.ts +28 -0
- package/dist/components/fileupload/fileupload-types.d.ts +70 -0
- package/dist/components/fileupload/fileupload.d.ts +25 -0
- package/dist/components/fileupload/fileupload.test.d.ts +0 -0
- package/dist/components/fileupload/index.d.ts +4 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/input-wrapper/input-wrapper.d.ts +2 -0
- package/dist/controllers/focus-modality-controller.d.ts +31 -0
- package/dist/converters-Bffq_YDm.cjs +1 -0
- package/dist/converters-jauYkvNh.js +1378 -0
- package/dist/{datepicker-OVd8pmnZ.js → datepicker-BUK1Qanw.js} +22 -21
- package/dist/{datepicker-DpmlsnH_.cjs → datepicker-DYd3JCs_.cjs} +10 -10
- package/dist/docs/components/fileupload.d.ts +27 -0
- package/dist/fileupload-BRqioKSY.cjs +396 -0
- package/dist/fileupload-Bnl303X0.js +1372 -0
- package/dist/fileupload.d.ts +1 -0
- package/dist/{header-DWgPgGfS.cjs → header-Dps8o-ZY.cjs} +1 -1
- package/dist/{header-a6ztVNp3.js → header-T5O6sfCN.js} +3 -3
- package/dist/{helptext-CCBKKLus.js → helptext-C5rG4VkA.js} +2 -2
- package/dist/{helptext-D-3QfZv5.cjs → helptext-DQgStO4D.cjs} +1 -1
- package/dist/{input-element-Bw1s6NlF.js → input-element-CjebcQd1.js} +3 -3
- package/dist/{input-element-B1yxcfz4.cjs → input-element-FGcr5hGd.cjs} +1 -1
- package/dist/input-wrapper-B_XFi211.cjs +55 -0
- package/dist/{input-wrapper-YfTmpSSY.js → input-wrapper-DBpMVSxt.js} +41 -15
- package/dist/{listbox-DuBgTFvH.js → listbox-0NHJvNjz.js} +2 -2
- package/dist/{listbox-DCA2JoPV.cjs → listbox-CcThiNBY.cjs} +1 -1
- package/dist/{modal-CBEWrAi2.cjs → modal-B6591sWa.cjs} +1 -1
- package/dist/{modal-pPp8nia6.js → modal-DyIC3uhs.js} +1 -1
- package/dist/pkt-calendar.cjs +1 -1
- package/dist/pkt-calendar.js +1 -1
- package/dist/pkt-card.cjs +1 -1
- package/dist/pkt-card.js +1 -1
- package/dist/pkt-checkbox.cjs +1 -1
- package/dist/pkt-checkbox.js +1 -1
- package/dist/pkt-combobox.cjs +1 -1
- package/dist/pkt-combobox.js +1 -1
- package/dist/pkt-consent.cjs +1 -1
- package/dist/pkt-consent.js +1 -1
- package/dist/pkt-datepicker.cjs +1 -1
- package/dist/pkt-datepicker.js +1 -1
- package/dist/pkt-fileupload.cjs +1 -0
- package/dist/pkt-fileupload.js +2 -0
- package/dist/pkt-header.cjs +1 -1
- package/dist/pkt-header.js +1 -1
- package/dist/pkt-heading.cjs +1 -1
- package/dist/pkt-heading.js +1 -1
- package/dist/pkt-helptext.cjs +1 -1
- package/dist/pkt-helptext.js +1 -1
- package/dist/pkt-index.cjs +1 -1
- package/dist/pkt-index.js +25 -24
- package/dist/pkt-input-wrapper.cjs +1 -1
- package/dist/pkt-input-wrapper.js +1 -1
- package/dist/pkt-link.cjs +1 -1
- package/dist/pkt-link.js +1 -1
- package/dist/pkt-linkcard.cjs +1 -1
- package/dist/pkt-linkcard.js +1 -1
- package/dist/pkt-listbox.cjs +1 -1
- package/dist/pkt-listbox.js +1 -1
- package/dist/pkt-loader.cjs +1 -1
- package/dist/pkt-loader.js +1 -1
- package/dist/pkt-messagebox.cjs +1 -1
- package/dist/pkt-messagebox.js +1 -1
- package/dist/pkt-modal.cjs +1 -1
- package/dist/pkt-modal.js +1 -1
- package/dist/pkt-progressbar.cjs +1 -1
- package/dist/pkt-progressbar.js +1 -1
- package/dist/pkt-radiobutton.cjs +1 -1
- package/dist/pkt-radiobutton.js +1 -1
- package/dist/pkt-select.cjs +1 -1
- package/dist/pkt-select.js +1 -1
- package/dist/pkt-tabs.cjs +1 -1
- package/dist/pkt-tabs.js +1 -1
- package/dist/pkt-tag.cjs +1 -1
- package/dist/pkt-tag.js +1 -1
- package/dist/pkt-textarea.cjs +1 -1
- package/dist/pkt-textarea.js +1 -1
- package/dist/pkt-textinput.cjs +1 -1
- package/dist/pkt-textinput.js +1 -1
- package/dist/pkt-timepicker.cjs +1 -1
- package/dist/pkt-timepicker.js +1 -1
- package/dist/{progressbar-CdXtmOBb.cjs → progressbar-CrwpnIkv.cjs} +1 -1
- package/dist/{progressbar-Dy9AiYQz.js → progressbar-uC-0W6o4.js} +1 -1
- package/dist/{radiobutton-V_KPbup_.js → radiobutton-B-8-wWFf.js} +1 -1
- package/dist/{radiobutton-GOnpUbCW.cjs → radiobutton-DvXr7tDk.cjs} +1 -1
- package/dist/{select-C2i8sOk5.cjs → select-B75wzvyz.cjs} +1 -1
- package/dist/{select-BSAFd8Wa.js → select-D9B8Ighz.js} +3 -3
- package/dist/shared-types/fileupload.d.ts +73 -0
- package/dist/shared-types/index.d.ts +2 -0
- package/dist/shared-utils/fileupload/announcements.d.ts +11 -0
- package/dist/shared-utils/fileupload/filename.d.ts +13 -0
- package/dist/shared-utils/fileupload/focus-trap.d.ts +12 -0
- package/dist/shared-utils/fileupload/formats.d.ts +30 -0
- package/dist/shared-utils/fileupload/index.d.ts +15 -0
- package/dist/shared-utils/fileupload/navigation.d.ts +5 -0
- package/dist/shared-utils/fileupload/size.d.ts +12 -0
- package/dist/shared-utils/fileupload/transfers.d.ts +17 -0
- package/dist/shared-utils/fileupload/truncate.d.ts +13 -0
- package/dist/shared-utils/fileupload/validation.d.ts +24 -0
- package/dist/tests/component-registry.d.ts +2 -0
- package/dist/{textarea-DYOBhJrN.js → textarea-CIWXMaAD.js} +2 -2
- package/dist/{textarea-D7aGOyDf.cjs → textarea-CjqprL6Y.cjs} +1 -1
- package/dist/{textinput-DFh8_9cx.cjs → textinput-B0VWmVIF.cjs} +1 -1
- package/dist/{textinput-BUgntXnM.js → textinput-CxzAxM8E.js} +2 -2
- package/dist/{timepicker-BIfE7KKJ.cjs → timepicker-Cv9juQeB.cjs} +1 -1
- package/dist/{timepicker-DaSwn1FA.js → timepicker-DXrpg5C7.js} +2 -2
- package/package.json +2 -2
- package/src/components/fileupload/fileupload-base.ts +557 -0
- package/src/components/fileupload/fileupload-operations.ts +213 -0
- package/src/components/fileupload/fileupload-renderers.ts +769 -0
- package/src/components/fileupload/fileupload-types.ts +88 -0
- package/src/components/fileupload/fileupload.test.ts +949 -0
- package/src/components/fileupload/fileupload.ts +469 -0
- package/src/components/fileupload/index.ts +34 -0
- package/src/components/index.ts +16 -0
- package/src/components/input-wrapper/input-wrapper.ts +10 -3
- package/dist/calendar-BzEtOJla.js +0 -2031
- package/dist/calendar-E5jJLt8D.cjs +0 -90
- package/dist/input-wrapper-BRuVELpK.cjs +0 -54
- /package/dist/{booleanish-jJLlnr6-.cjs → booleanish-B5EFqFpJ.cjs} +0 -0
- /package/dist/{booleanish-BmcOU-43.js → booleanish-CRaKZ75t.js} +0 -0
- /package/dist/{consent-DeB_5sWH.js → consent-f_IGyHss.js} +0 -0
- /package/dist/{consent-hwu7pp3S.cjs → consent-zh2npq2K.cjs} +0 -0
- /package/dist/{dialog-polyfill.esm-1hPr0gl9.js → dialog-polyfill.esm-BmVHGRTX.js} +0 -0
- /package/dist/{dialog-polyfill.esm-D3Oinvk0.cjs → dialog-polyfill.esm-CXDsb3l-.cjs} +0 -0
- /package/dist/{heading-DiQLPxdf.js → heading-BxvE80bb.js} +0 -0
- /package/dist/{heading-BgSWIRii.cjs → heading-DRJTS27k.cjs} +0 -0
- /package/dist/{input-wrapper-Dwd-_sKm.cjs → input-wrapper-CYCCx8MP.cjs} +0 -0
- /package/dist/{input-wrapper-CDyHTR1V.js → input-wrapper-kw2y6Jp_.js} +0 -0
- /package/dist/{link-CyTCpI0k.cjs → link-B8zYc5WK.cjs} +0 -0
- /package/dist/{link-CKAFMz9j.js → link-CjJcwcxR.js} +0 -0
- /package/dist/{linkcard-BE4-FewK.cjs → linkcard-C4x-NJMi.cjs} +0 -0
- /package/dist/{linkcard-B0KOGaGj.js → linkcard-Ddh5R_L-.js} +0 -0
- /package/dist/{loader-CX2rsC01.cjs → loader-CGQXAuyM.cjs} +0 -0
- /package/dist/{loader-6kFZUErT.js → loader-DeizpX45.js} +0 -0
- /package/dist/{messagebox-Din9X53b.cjs → messagebox-Ccu29rDv.cjs} +0 -0
- /package/dist/{messagebox-Cwlg5sOr.js → messagebox-DqoPqo4K.js} +0 -0
- /package/dist/{pkt-options-controller-CXUGx134.cjs → pkt-options-controller-BPBb_O2K.cjs} +0 -0
- /package/dist/{pkt-options-controller-CRrI6WMk.js → pkt-options-controller-DbLrdRwH.js} +0 -0
- /package/dist/{repeat-Cepr3469.js → repeat-DV1O43YU.js} +0 -0
- /package/dist/{repeat-CqcDYBxr.cjs → repeat-DaDFvx86.cjs} +0 -0
- /package/dist/{tabs-Rc9N9HWa.js → tabs-4eWA7CsK.js} +0 -0
- /package/dist/{tabs-BfurI6C3.cjs → tabs-RnDtIFv3.cjs} +0 -0
- /package/dist/{tag-DLthw5b4.cjs → tag-DWtajZk6.cjs} +0 -0
- /package/dist/{tag-VwykmuQR.js → tag-hl59gziI.js} +0 -0
- /package/dist/{utils-CUqCKGvp.js → utils-BOOZ0ppt.js} +0 -0
- /package/dist/{utils-CjAbIttF.cjs → utils-CM67Oudc.cjs} +0 -0
|
@@ -0,0 +1,949 @@
|
|
|
1
|
+
import '@testing-library/jest-dom'
|
|
2
|
+
import { html } from 'lit'
|
|
3
|
+
import { createElementTest, BaseTestConfig } from '@/tests/test-framework'
|
|
4
|
+
import { CustomElementFor } from '@/tests/component-registry'
|
|
5
|
+
import './fileupload'
|
|
6
|
+
import { IPktFileUpload, FileItem, TFileTransfer, TQueueItemOperation } from './fileupload-types'
|
|
7
|
+
|
|
8
|
+
interface FileUploadTestConfig extends Partial<IPktFileUpload>, BaseTestConfig {}
|
|
9
|
+
|
|
10
|
+
const createFileUploadTest = async (config: FileUploadTestConfig = {}) => {
|
|
11
|
+
// Default empty light-DOM child: stray "Test content" breaks Lit render on `pkt-fileupload` (createRenderRoot = this).
|
|
12
|
+
const { content = '', ...attributeConfig } = config
|
|
13
|
+
const { container, element } = await createElementTest<
|
|
14
|
+
CustomElementFor<'pkt-fileupload'>,
|
|
15
|
+
BaseTestConfig
|
|
16
|
+
>('pkt-fileupload', { content })
|
|
17
|
+
|
|
18
|
+
Object.entries(attributeConfig).forEach(([key, value]) => {
|
|
19
|
+
;(element as any)[key] = value
|
|
20
|
+
})
|
|
21
|
+
await element.updateComplete
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
container,
|
|
25
|
+
fileupload: element,
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Drive the component through its public input — same pattern as React's
|
|
31
|
+
* `fireEvent.change(fileInput, { target: { files } })`. Tests should prefer
|
|
32
|
+
* this over poking `.addFiles()` directly so the assertions stay close to
|
|
33
|
+
* the user-visible flow.
|
|
34
|
+
*/
|
|
35
|
+
const selectFiles = async (
|
|
36
|
+
fileupload: CustomElementFor<'pkt-fileupload'>,
|
|
37
|
+
files: File[],
|
|
38
|
+
): Promise<void> => {
|
|
39
|
+
const input = fileupload.querySelector<HTMLInputElement>('input[type="file"]')
|
|
40
|
+
if (!input) throw new Error('Expected pkt-fileupload to render a native file input')
|
|
41
|
+
Object.defineProperty(input, 'files', {
|
|
42
|
+
configurable: true,
|
|
43
|
+
value: files,
|
|
44
|
+
})
|
|
45
|
+
input.dispatchEvent(new Event('change', { bubbles: true }))
|
|
46
|
+
await fileupload.updateComplete
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Convenience to track every `files-changed` event the component emits. */
|
|
50
|
+
const recordFilesChanged = (fileupload: CustomElementFor<'pkt-fileupload'>) => {
|
|
51
|
+
const events: Array<{ files: FileItem[]; reason: string; changedFileIds?: string[] }> = []
|
|
52
|
+
fileupload.addEventListener('files-changed', (event) => {
|
|
53
|
+
events.push((event as CustomEvent).detail)
|
|
54
|
+
})
|
|
55
|
+
return events
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Returns the most recent file list as observed via `files-changed` events. */
|
|
59
|
+
const lastFilesFromEvents = (
|
|
60
|
+
events: Array<{ files: FileItem[] }>,
|
|
61
|
+
): FileItem[] => (events.length === 0 ? [] : events[events.length - 1].files)
|
|
62
|
+
|
|
63
|
+
afterEach(() => {
|
|
64
|
+
document.body.innerHTML = ''
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
describe('PktFileUpload', () => {
|
|
68
|
+
const createSeedFileItem = (
|
|
69
|
+
fileId: string,
|
|
70
|
+
name: string,
|
|
71
|
+
type: string = 'application/pdf',
|
|
72
|
+
): FileItem => ({
|
|
73
|
+
fileId,
|
|
74
|
+
file: new File(['seed'], name, { type }),
|
|
75
|
+
attributes: { targetFilename: name },
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('adds files with stable ids and preserves order', async () => {
|
|
79
|
+
const { fileupload } = await createFileUploadTest({ multiple: true })
|
|
80
|
+
const events = recordFilesChanged(fileupload)
|
|
81
|
+
|
|
82
|
+
await selectFiles(fileupload, [
|
|
83
|
+
new File(['a'], 'first.pdf', { type: 'application/pdf' }),
|
|
84
|
+
new File(['b'], 'second.pdf', { type: 'application/pdf' }),
|
|
85
|
+
])
|
|
86
|
+
|
|
87
|
+
const detail = events.at(-1)!
|
|
88
|
+
expect(detail.reason).toBe('add')
|
|
89
|
+
expect(detail.files).toHaveLength(2)
|
|
90
|
+
expect(detail.files[0].attributes.targetFilename).toBe('first.pdf')
|
|
91
|
+
expect(detail.files[1].attributes.targetFilename).toBe('second.pdf')
|
|
92
|
+
const ids = detail.files.map((item) => item.fileId)
|
|
93
|
+
expect(new Set(ids).size).toBe(2)
|
|
94
|
+
|
|
95
|
+
// Re-render should not change anything user-visible: queue still has both files in order
|
|
96
|
+
fileupload.disabled = true
|
|
97
|
+
await fileupload.updateComplete
|
|
98
|
+
const titles = Array.from(
|
|
99
|
+
fileupload.querySelectorAll('.pkt-fileupload__queue-display__item__title'),
|
|
100
|
+
).map((el) => {
|
|
101
|
+
const head = el.querySelector('[data-pkt-truncate-part="first"]')?.textContent ?? ''
|
|
102
|
+
const tail = el.querySelector('[data-pkt-truncate-part="tail"]')?.textContent ?? ''
|
|
103
|
+
return (head + tail).trim()
|
|
104
|
+
})
|
|
105
|
+
expect(titles).toEqual(['first.pdf', 'second.pdf'])
|
|
106
|
+
// No new files-changed event should fire from a property toggle
|
|
107
|
+
expect(events).toHaveLength(1)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test('remove only affects selected file and keeps other items untouched', async () => {
|
|
111
|
+
const { fileupload } = await createFileUploadTest({ multiple: true })
|
|
112
|
+
const events = recordFilesChanged(fileupload)
|
|
113
|
+
|
|
114
|
+
await selectFiles(fileupload, [
|
|
115
|
+
new File(['a'], 'remove-me.pdf', { type: 'application/pdf' }),
|
|
116
|
+
new File(['b'], 'keep-me.pdf', { type: 'application/pdf' }),
|
|
117
|
+
])
|
|
118
|
+
const beforeRemove = lastFilesFromEvents(events)
|
|
119
|
+
|
|
120
|
+
const removeButtons = fileupload.querySelectorAll<HTMLButtonElement>(
|
|
121
|
+
'button[aria-label="Slett fil"]',
|
|
122
|
+
)
|
|
123
|
+
expect(removeButtons.length).toBe(2)
|
|
124
|
+
removeButtons[0].click()
|
|
125
|
+
await fileupload.updateComplete
|
|
126
|
+
|
|
127
|
+
const detail = events.at(-1)!
|
|
128
|
+
expect(detail.reason).toBe('remove')
|
|
129
|
+
expect(detail.changedFileIds).toEqual([beforeRemove[0].fileId])
|
|
130
|
+
expect(detail.files).toHaveLength(1)
|
|
131
|
+
expect(detail.files[0].fileId).toBe(beforeRemove[1].fileId)
|
|
132
|
+
expect(detail.files[0].attributes.targetFilename).toBe('keep-me.pdf')
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test('update operation keeps ids stable and emits update reason', async () => {
|
|
136
|
+
const { fileupload } = await createFileUploadTest({ multiple: true })
|
|
137
|
+
const events = recordFilesChanged(fileupload)
|
|
138
|
+
|
|
139
|
+
await selectFiles(fileupload, [
|
|
140
|
+
new File(['a'], 'old-name.pdf', { type: 'application/pdf' }),
|
|
141
|
+
new File(['b'], 'other.pdf', { type: 'application/pdf' }),
|
|
142
|
+
])
|
|
143
|
+
const beforeUpdate = lastFilesFromEvents(events)
|
|
144
|
+
|
|
145
|
+
// updateFileItem is the documented public method for programmatic updates.
|
|
146
|
+
fileupload.updateFileItem(beforeUpdate[0].fileId, {
|
|
147
|
+
attributes: {
|
|
148
|
+
...beforeUpdate[0].attributes,
|
|
149
|
+
targetFilename: 'new-name.pdf',
|
|
150
|
+
},
|
|
151
|
+
})
|
|
152
|
+
await fileupload.updateComplete
|
|
153
|
+
|
|
154
|
+
const detail = events.at(-1)!
|
|
155
|
+
expect(detail.reason).toBe('update')
|
|
156
|
+
expect(detail.changedFileIds).toEqual([beforeUpdate[0].fileId])
|
|
157
|
+
expect(detail.files).toHaveLength(2)
|
|
158
|
+
expect(detail.files[0].fileId).toBe(beforeUpdate[0].fileId)
|
|
159
|
+
expect(detail.files[0].attributes.targetFilename).toBe('new-name.pdf')
|
|
160
|
+
expect(detail.files[1].fileId).toBe(beforeUpdate[1].fileId)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
test('controlled mode does not adopt the new list until the parent re-supplies value', async () => {
|
|
164
|
+
const controlledValue: FileItem[] = [
|
|
165
|
+
{
|
|
166
|
+
fileId: 'seed-id',
|
|
167
|
+
attributes: { targetFilename: 'seed.pdf' },
|
|
168
|
+
},
|
|
169
|
+
]
|
|
170
|
+
const { fileupload } = await createFileUploadTest({ value: controlledValue, multiple: true })
|
|
171
|
+
const events = recordFilesChanged(fileupload)
|
|
172
|
+
|
|
173
|
+
await selectFiles(fileupload, [new File(['a'], 'new.pdf', { type: 'application/pdf' })])
|
|
174
|
+
|
|
175
|
+
// Component emits a `files-changed` with the proposed next list…
|
|
176
|
+
const detail = events.at(-1)!
|
|
177
|
+
expect(detail.reason).toBe('add')
|
|
178
|
+
expect(detail.files).toHaveLength(2)
|
|
179
|
+
|
|
180
|
+
// …but the queue still reflects the controlled value (parent hasn't updated yet).
|
|
181
|
+
const titles = Array.from(
|
|
182
|
+
fileupload.querySelectorAll('.pkt-fileupload__queue-display__item__title'),
|
|
183
|
+
).map((el) => el.textContent?.trim())
|
|
184
|
+
expect(titles).toEqual(['seed.pdf'])
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
test('renders remove operation for both filename and thumbnail modes', async () => {
|
|
188
|
+
const { fileupload: filenameUpload } = await createFileUploadTest({ itemRenderer: 'filename' })
|
|
189
|
+
await selectFiles(filenameUpload, [
|
|
190
|
+
new File(['a'], 'filename-mode.pdf', { type: 'application/pdf' }),
|
|
191
|
+
])
|
|
192
|
+
expect(
|
|
193
|
+
filenameUpload.querySelector('button[aria-label="Slett fil"]'),
|
|
194
|
+
).toBeInTheDocument()
|
|
195
|
+
|
|
196
|
+
const { fileupload: thumbnailUpload } = await createFileUploadTest()
|
|
197
|
+
thumbnailUpload.itemRenderer = 'thumbnail'
|
|
198
|
+
await thumbnailUpload.updateComplete
|
|
199
|
+
await selectFiles(thumbnailUpload, [
|
|
200
|
+
new File(['b'], 'thumbnail-mode.png', { type: 'image/png' }),
|
|
201
|
+
])
|
|
202
|
+
expect(
|
|
203
|
+
thumbnailUpload.querySelector('.pkt-fileupload__queue-display__item__thumbnail'),
|
|
204
|
+
).toBeInTheDocument()
|
|
205
|
+
expect(
|
|
206
|
+
thumbnailUpload.querySelector('button[aria-label="Slett fil"]'),
|
|
207
|
+
).toBeInTheDocument()
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
test('host flow: native input change + remove emits expected files-changed payload', async () => {
|
|
211
|
+
const { fileupload } = await createFileUploadTest({ multiple: true })
|
|
212
|
+
const events: Array<any> = []
|
|
213
|
+
fileupload.addEventListener('files-changed', (event) =>
|
|
214
|
+
events.push((event as CustomEvent).detail),
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
const input = fileupload.querySelector<HTMLInputElement>('input[type="file"]')
|
|
218
|
+
expect(input).toBeInTheDocument()
|
|
219
|
+
|
|
220
|
+
const selectedFiles = [
|
|
221
|
+
new File(['a'], 'dom-first.pdf', { type: 'application/pdf' }),
|
|
222
|
+
new File(['b'], 'dom-second.pdf', { type: 'application/pdf' }),
|
|
223
|
+
]
|
|
224
|
+
|
|
225
|
+
Object.defineProperty(input as HTMLInputElement, 'files', {
|
|
226
|
+
value: selectedFiles as unknown as FileList,
|
|
227
|
+
configurable: true,
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
input?.dispatchEvent(new Event('change', { bubbles: true }))
|
|
231
|
+
await fileupload.updateComplete
|
|
232
|
+
|
|
233
|
+
const addDetail = events.at(-1)
|
|
234
|
+
expect(addDetail.reason).toBe('add')
|
|
235
|
+
expect(addDetail.files).toHaveLength(2)
|
|
236
|
+
|
|
237
|
+
const removeButtons = fileupload.querySelectorAll<HTMLButtonElement>(
|
|
238
|
+
'.pkt-fileupload__queue-display__item__operation',
|
|
239
|
+
)
|
|
240
|
+
expect(removeButtons).toHaveLength(2)
|
|
241
|
+
removeButtons[0].click()
|
|
242
|
+
await fileupload.updateComplete
|
|
243
|
+
|
|
244
|
+
const removeDetail = events.at(-1)
|
|
245
|
+
expect(removeDetail.reason).toBe('remove')
|
|
246
|
+
expect(removeDetail.files).toHaveLength(1)
|
|
247
|
+
expect(removeDetail.files[0].attributes.targetFilename).toBe('dom-second.pdf')
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
test('disabled mode blocks remove interaction from queue action', async () => {
|
|
251
|
+
const { fileupload } = await createFileUploadTest({ multiple: true })
|
|
252
|
+
const events = recordFilesChanged(fileupload)
|
|
253
|
+
await selectFiles(fileupload, [new File(['a'], 'locked.pdf', { type: 'application/pdf' })])
|
|
254
|
+
expect(events.at(-1)!.reason).toBe('add')
|
|
255
|
+
|
|
256
|
+
fileupload.disabled = true
|
|
257
|
+
await fileupload.updateComplete
|
|
258
|
+
|
|
259
|
+
const removeButton = fileupload.querySelector<HTMLButtonElement>('button[aria-label="Slett fil"]')
|
|
260
|
+
expect(removeButton?.disabled).toBe(true)
|
|
261
|
+
removeButton?.click()
|
|
262
|
+
await fileupload.updateComplete
|
|
263
|
+
|
|
264
|
+
expect(events).toHaveLength(1)
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
test('rejects unsupported formats and shows format error placeholder', async () => {
|
|
268
|
+
const { fileupload } = await createFileUploadTest({
|
|
269
|
+
allowedFormats: ['pdf'],
|
|
270
|
+
formatErrorMessage: 'Kun lovlige formater: {formats}',
|
|
271
|
+
})
|
|
272
|
+
const events = recordFilesChanged(fileupload)
|
|
273
|
+
|
|
274
|
+
await selectFiles(fileupload, [new File(['a'], 'photo.jpg', { type: 'image/jpeg' })])
|
|
275
|
+
|
|
276
|
+
expect(events).toHaveLength(0)
|
|
277
|
+
expect(fileupload.querySelector('pkt-alert')?.textContent).toContain(
|
|
278
|
+
'Kun lovlige formater: pdf',
|
|
279
|
+
)
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
test('accepts MIME wildcards like image/* in allowedFormats', async () => {
|
|
283
|
+
const { fileupload } = await createFileUploadTest({
|
|
284
|
+
allowedFormats: ['image/*'],
|
|
285
|
+
})
|
|
286
|
+
const events = recordFilesChanged(fileupload)
|
|
287
|
+
|
|
288
|
+
await selectFiles(fileupload, [new File(['a'], 'good.png', { type: 'image/png' })])
|
|
289
|
+
expect(events.at(-1)?.reason).toBe('add')
|
|
290
|
+
|
|
291
|
+
await selectFiles(fileupload, [new File(['b'], 'bad.pdf', { type: 'application/pdf' })])
|
|
292
|
+
expect(fileupload.querySelector('pkt-alert')?.textContent).toContain('Ugyldig filtype')
|
|
293
|
+
// Bad file did not trigger another `files-changed`
|
|
294
|
+
expect(events).toHaveLength(1)
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
test('rejects oversized files and applies {maxSize} placeholder', async () => {
|
|
298
|
+
const { fileupload } = await createFileUploadTest({
|
|
299
|
+
maxFileSize: '500KB',
|
|
300
|
+
sizeErrorMessage: 'Maks størrelse er {maxSize}',
|
|
301
|
+
})
|
|
302
|
+
const events = recordFilesChanged(fileupload)
|
|
303
|
+
|
|
304
|
+
const tooLarge = new File([new Uint8Array(600 * 1024)], 'large.pdf', {
|
|
305
|
+
type: 'application/pdf',
|
|
306
|
+
})
|
|
307
|
+
await selectFiles(fileupload, [tooLarge])
|
|
308
|
+
|
|
309
|
+
expect(events).toHaveLength(0)
|
|
310
|
+
expect(fileupload.querySelector('pkt-alert')?.textContent).toContain('Maks størrelse er 500 KB')
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
test('supports onFileValidation callback for custom validation', async () => {
|
|
314
|
+
const { fileupload } = await createFileUploadTest({
|
|
315
|
+
onFileValidation: (file) =>
|
|
316
|
+
file.name.includes('callback-blocked') ? 'Blokkert av callback' : null,
|
|
317
|
+
})
|
|
318
|
+
const events = recordFilesChanged(fileupload)
|
|
319
|
+
|
|
320
|
+
await selectFiles(fileupload, [
|
|
321
|
+
new File(['a'], 'callback-blocked.pdf', { type: 'application/pdf' }),
|
|
322
|
+
])
|
|
323
|
+
|
|
324
|
+
expect(events).toHaveLength(0)
|
|
325
|
+
expect(fileupload.querySelector('pkt-alert')?.textContent).toContain('Blokkert av callback')
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
test('supports file-validate event for custom validation', async () => {
|
|
329
|
+
const { fileupload } = await createFileUploadTest({
|
|
330
|
+
onFileValidation: () => null,
|
|
331
|
+
})
|
|
332
|
+
const events = recordFilesChanged(fileupload)
|
|
333
|
+
fileupload.addEventListener('file-validate', (event) => {
|
|
334
|
+
const detail = (event as CustomEvent<{ file: File; errorMessage: string | null }>).detail
|
|
335
|
+
if (detail.file.name.endsWith('.exe')) detail.errorMessage = 'Blokkert av event-hook'
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
await selectFiles(fileupload, [
|
|
339
|
+
new File(['b'], 'event-blocked.exe', { type: 'application/octet-stream' }),
|
|
340
|
+
])
|
|
341
|
+
|
|
342
|
+
expect(events).toHaveLength(0)
|
|
343
|
+
expect(fileupload.querySelector('pkt-alert')?.textContent).toContain('Blokkert av event-hook')
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
test('dropzone format text follows allowedFormats first, then accept', async () => {
|
|
347
|
+
const { fileupload: noRestrictions } = await createFileUploadTest()
|
|
348
|
+
expect(
|
|
349
|
+
noRestrictions.querySelector('.pkt-fileupload__drop-zone__placeholder__formats'),
|
|
350
|
+
).not.toBeInTheDocument()
|
|
351
|
+
|
|
352
|
+
const { fileupload: withAllowed } = await createFileUploadTest({
|
|
353
|
+
allowedFormats: ['pdf', 'image/*'],
|
|
354
|
+
})
|
|
355
|
+
const withAllowedText = withAllowed
|
|
356
|
+
.querySelector('.pkt-fileupload__drop-zone__placeholder__formats')
|
|
357
|
+
?.textContent?.replace(/\s+/g, ' ')
|
|
358
|
+
.trim()
|
|
359
|
+
expect(withAllowedText).toContain('Format: PDF, image/*')
|
|
360
|
+
|
|
361
|
+
const { fileupload: withAccept } = await createFileUploadTest({
|
|
362
|
+
accept: '.png,application/pdf',
|
|
363
|
+
})
|
|
364
|
+
const withAcceptText = withAccept
|
|
365
|
+
.querySelector('.pkt-fileupload__drop-zone__placeholder__formats')
|
|
366
|
+
?.textContent?.replace(/\s+/g, ' ')
|
|
367
|
+
.trim()
|
|
368
|
+
expect(withAcceptText).toContain('Format: PNG, application/pdf')
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
test('sets required only on native file input in form strategy', async () => {
|
|
372
|
+
const { fileupload: formUpload } = await createFileUploadTest({
|
|
373
|
+
required: true,
|
|
374
|
+
uploadStrategy: 'form',
|
|
375
|
+
})
|
|
376
|
+
const formInput = formUpload.querySelector<HTMLInputElement>('input[type="file"]')
|
|
377
|
+
expect(formInput?.required).toBe(true)
|
|
378
|
+
expect(formInput?.getAttribute('aria-required')).toBe('true')
|
|
379
|
+
|
|
380
|
+
const { fileupload: customUpload } = await createFileUploadTest({
|
|
381
|
+
required: true,
|
|
382
|
+
uploadStrategy: 'custom',
|
|
383
|
+
})
|
|
384
|
+
const customInput = customUpload.querySelector<HTMLInputElement>('input[type="file"]')
|
|
385
|
+
expect(customInput?.required).toBe(false)
|
|
386
|
+
expect(customInput?.getAttribute('aria-required')).toBe('true')
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
test('custom strategy blocks form submit when required and no files are selected', async () => {
|
|
390
|
+
const { fileupload } = await createFileUploadTest({
|
|
391
|
+
required: true,
|
|
392
|
+
uploadStrategy: 'custom',
|
|
393
|
+
name: 'attachments',
|
|
394
|
+
})
|
|
395
|
+
const form = document.createElement('form')
|
|
396
|
+
document.body.appendChild(form)
|
|
397
|
+
form.appendChild(fileupload)
|
|
398
|
+
await fileupload.updateComplete
|
|
399
|
+
|
|
400
|
+
const submitEvent = new Event('submit', { bubbles: true, cancelable: true })
|
|
401
|
+
form.dispatchEvent(submitEvent)
|
|
402
|
+
await fileupload.updateComplete
|
|
403
|
+
|
|
404
|
+
expect(submitEvent.defaultPrevented).toBe(true)
|
|
405
|
+
expect(fileupload.querySelector('pkt-alert')?.textContent).toContain('Du må laste opp minst én fil.')
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
test('custom strategy allows submit when required and at least one file exists', async () => {
|
|
409
|
+
const { fileupload } = await createFileUploadTest({
|
|
410
|
+
required: true,
|
|
411
|
+
uploadStrategy: 'custom',
|
|
412
|
+
multiple: true,
|
|
413
|
+
})
|
|
414
|
+
const form = document.createElement('form')
|
|
415
|
+
document.body.appendChild(form)
|
|
416
|
+
form.appendChild(fileupload)
|
|
417
|
+
await fileupload.updateComplete
|
|
418
|
+
|
|
419
|
+
await selectFiles(fileupload, [new File(['a'], 'ok.pdf', { type: 'application/pdf' })])
|
|
420
|
+
|
|
421
|
+
const submitEvent = new Event('submit', { bubbles: true, cancelable: true })
|
|
422
|
+
form.dispatchEvent(submitEvent)
|
|
423
|
+
await fileupload.updateComplete
|
|
424
|
+
|
|
425
|
+
expect(submitEvent.defaultPrevented).toBe(false)
|
|
426
|
+
expect(fileupload.querySelector('pkt-alert')?.textContent || '').not.toContain(
|
|
427
|
+
'Du må laste opp minst én fil.',
|
|
428
|
+
)
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
test('rename operation updates target filename and emits update event', async () => {
|
|
432
|
+
const { fileupload } = await createFileUploadTest({
|
|
433
|
+
value: [createSeedFileItem('rename-1', 'old-name.pdf')],
|
|
434
|
+
renameFilesEnabled: true,
|
|
435
|
+
})
|
|
436
|
+
const events = recordFilesChanged(fileupload)
|
|
437
|
+
|
|
438
|
+
const renameButton = fileupload.querySelector<HTMLButtonElement>(
|
|
439
|
+
'button[aria-label="Rediger filnavn"]',
|
|
440
|
+
)
|
|
441
|
+
expect(renameButton).toBeInTheDocument()
|
|
442
|
+
renameButton?.click()
|
|
443
|
+
await fileupload.updateComplete
|
|
444
|
+
|
|
445
|
+
const renameInput = fileupload.querySelector<HTMLInputElement>(
|
|
446
|
+
'.pkt-fileupload__queue-display__item__rename-input',
|
|
447
|
+
)
|
|
448
|
+
expect(renameInput).toBeInTheDocument()
|
|
449
|
+
if (renameInput) {
|
|
450
|
+
renameInput.value = 'new-name.pdf'
|
|
451
|
+
renameInput.dispatchEvent(
|
|
452
|
+
new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }),
|
|
453
|
+
)
|
|
454
|
+
}
|
|
455
|
+
await fileupload.updateComplete
|
|
456
|
+
|
|
457
|
+
expect(events.at(-1)?.reason).toBe('update')
|
|
458
|
+
expect(events.at(-1)?.files[0].attributes.targetFilename).toBe('new-name.pdf')
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
test('comment operation supports add and remove comment', async () => {
|
|
462
|
+
const { fileupload } = await createFileUploadTest({ addCommentsEnabled: true })
|
|
463
|
+
await selectFiles(fileupload, [new File(['seed'], 'comment.pdf', { type: 'application/pdf' })])
|
|
464
|
+
|
|
465
|
+
const addCommentButton = Array.from(
|
|
466
|
+
fileupload.querySelectorAll<HTMLButtonElement>('.pkt-fileupload__queue-display__item__operation'),
|
|
467
|
+
).find((button) => button.textContent?.includes('Legg til kommentar'))
|
|
468
|
+
expect(addCommentButton).toBeInTheDocument()
|
|
469
|
+
addCommentButton?.click()
|
|
470
|
+
await fileupload.updateComplete
|
|
471
|
+
|
|
472
|
+
const commentInput = fileupload.querySelector<HTMLTextAreaElement>(
|
|
473
|
+
'.pkt-fileupload__queue-display__item__comment-input',
|
|
474
|
+
)
|
|
475
|
+
expect(commentInput).toBeInTheDocument()
|
|
476
|
+
if (commentInput) {
|
|
477
|
+
commentInput.value = 'Dette er en kommentar'
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const saveCommentButton = Array.from(fileupload.querySelectorAll<HTMLButtonElement>('.pkt-btn')).find(
|
|
481
|
+
(button) => button.textContent?.includes('Legg til kommentar'),
|
|
482
|
+
)
|
|
483
|
+
saveCommentButton?.click()
|
|
484
|
+
await fileupload.updateComplete
|
|
485
|
+
|
|
486
|
+
expect(
|
|
487
|
+
fileupload.querySelector('.pkt-fileupload__queue-display__item__comment__text')?.textContent,
|
|
488
|
+
).toContain('Dette er en kommentar')
|
|
489
|
+
|
|
490
|
+
const deleteCommentButton = fileupload.querySelector<HTMLButtonElement>(
|
|
491
|
+
'button[aria-label="Slett kommentar"]',
|
|
492
|
+
)
|
|
493
|
+
expect(deleteCommentButton).toBeInTheDocument()
|
|
494
|
+
deleteCommentButton?.click()
|
|
495
|
+
await fileupload.updateComplete
|
|
496
|
+
|
|
497
|
+
expect(fileupload.querySelector('.pkt-fileupload__queue-display__item__comment__text')).not.toBeInTheDocument()
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
test('rename input closes on escape for keyboard users', async () => {
|
|
501
|
+
const { fileupload } = await createFileUploadTest({
|
|
502
|
+
value: [createSeedFileItem('rename-2', 'escape.pdf')],
|
|
503
|
+
renameFilesEnabled: true,
|
|
504
|
+
})
|
|
505
|
+
|
|
506
|
+
const renameButton = fileupload.querySelector<HTMLButtonElement>(
|
|
507
|
+
'button[aria-label="Rediger filnavn"]',
|
|
508
|
+
)
|
|
509
|
+
renameButton?.click()
|
|
510
|
+
await fileupload.updateComplete
|
|
511
|
+
|
|
512
|
+
const renameInput = fileupload.querySelector<HTMLInputElement>(
|
|
513
|
+
'.pkt-fileupload__queue-display__item__rename-input',
|
|
514
|
+
)
|
|
515
|
+
expect(renameInput).toBeInTheDocument()
|
|
516
|
+
renameInput?.dispatchEvent(
|
|
517
|
+
new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true }),
|
|
518
|
+
)
|
|
519
|
+
await fileupload.updateComplete
|
|
520
|
+
|
|
521
|
+
expect(
|
|
522
|
+
fileupload.querySelector('.pkt-fileupload__queue-display__item__rename-input'),
|
|
523
|
+
).not.toBeInTheDocument()
|
|
524
|
+
})
|
|
525
|
+
|
|
526
|
+
test('custom operation renders inline ui', async () => {
|
|
527
|
+
const inlineOperation: TQueueItemOperation = {
|
|
528
|
+
id: 'inline-custom',
|
|
529
|
+
title: 'Inline custom',
|
|
530
|
+
renderInlineUI: ({ close }) =>
|
|
531
|
+
html`<span class="custom-inline-ui">Inline UI</span>
|
|
532
|
+
<button type="button" @click=${close}>Lukk</button>`,
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const { fileupload } = await createFileUploadTest({
|
|
536
|
+
value: [createSeedFileItem('custom-inline-1', 'custom.pdf')],
|
|
537
|
+
extraOperations: [inlineOperation],
|
|
538
|
+
})
|
|
539
|
+
|
|
540
|
+
const button = Array.from(
|
|
541
|
+
fileupload.querySelectorAll<HTMLButtonElement>('.pkt-fileupload__queue-display__item__operation'),
|
|
542
|
+
).find((candidate) => candidate.textContent?.includes('Inline custom'))
|
|
543
|
+
button?.click()
|
|
544
|
+
await fileupload.updateComplete
|
|
545
|
+
|
|
546
|
+
expect(fileupload.querySelector('.custom-inline-ui')).toBeInTheDocument()
|
|
547
|
+
})
|
|
548
|
+
|
|
549
|
+
test('custom operation renders extended ui', async () => {
|
|
550
|
+
const extendedOperation: TQueueItemOperation = {
|
|
551
|
+
id: 'extended-custom',
|
|
552
|
+
title: 'Extended custom',
|
|
553
|
+
renderExtendedUI: () => html`<div class="custom-extended-ui">Utvidet UI</div>`,
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const { fileupload } = await createFileUploadTest({
|
|
557
|
+
value: [createSeedFileItem('custom-extended-1', 'custom.pdf')],
|
|
558
|
+
extraOperations: [extendedOperation],
|
|
559
|
+
})
|
|
560
|
+
|
|
561
|
+
const button = Array.from(
|
|
562
|
+
fileupload.querySelectorAll<HTMLButtonElement>('.pkt-fileupload__queue-display__item__operation'),
|
|
563
|
+
).find((candidate) => candidate.textContent?.includes('Extended custom'))
|
|
564
|
+
button?.click()
|
|
565
|
+
await fileupload.updateComplete
|
|
566
|
+
|
|
567
|
+
expect(fileupload.querySelector('.custom-extended-ui')).toBeInTheDocument()
|
|
568
|
+
})
|
|
569
|
+
|
|
570
|
+
test('custom operation onClick receives the queue context', async () => {
|
|
571
|
+
const seen: Array<{ fileId: string; isActive: boolean }> = []
|
|
572
|
+
const clickOperation: TQueueItemOperation = {
|
|
573
|
+
id: 'click-custom',
|
|
574
|
+
title: 'Klikk',
|
|
575
|
+
onClick: ({ file, isActive }) => seen.push({ fileId: file.fileId, isActive }),
|
|
576
|
+
}
|
|
577
|
+
const { fileupload } = await createFileUploadTest({
|
|
578
|
+
value: [createSeedFileItem('click-1', 'click.pdf')],
|
|
579
|
+
extraOperations: [clickOperation],
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
const button = Array.from(
|
|
583
|
+
fileupload.querySelectorAll<HTMLButtonElement>('.pkt-fileupload__queue-display__item__operation'),
|
|
584
|
+
).find((candidate) => candidate.textContent?.includes('Klikk'))
|
|
585
|
+
button?.click()
|
|
586
|
+
await fileupload.updateComplete
|
|
587
|
+
|
|
588
|
+
expect(seen).toEqual([{ fileId: 'click-1', isActive: false }])
|
|
589
|
+
})
|
|
590
|
+
|
|
591
|
+
test('renders transfer progress state with progress bar for filename and thumbnail', async () => {
|
|
592
|
+
const seed = createSeedFileItem('progress-1', 'progress.pdf')
|
|
593
|
+
const transfers: TFileTransfer[] = [
|
|
594
|
+
{ fileId: 'progress-1', progress: 0.42, showProgress: true },
|
|
595
|
+
]
|
|
596
|
+
|
|
597
|
+
const { fileupload: filenameUpload } = await createFileUploadTest({
|
|
598
|
+
value: [seed],
|
|
599
|
+
transfers,
|
|
600
|
+
itemRenderer: 'filename',
|
|
601
|
+
})
|
|
602
|
+
expect(
|
|
603
|
+
filenameUpload.querySelector('.pkt-fileupload__queue-display__item--in-progress'),
|
|
604
|
+
).toBeInTheDocument()
|
|
605
|
+
expect(
|
|
606
|
+
filenameUpload.querySelector('.pkt-fileupload__queue-display__item__progress'),
|
|
607
|
+
).toBeInTheDocument()
|
|
608
|
+
|
|
609
|
+
const { fileupload: thumbnailUpload } = await createFileUploadTest({
|
|
610
|
+
value: [seed],
|
|
611
|
+
transfers,
|
|
612
|
+
itemRenderer: 'thumbnail',
|
|
613
|
+
})
|
|
614
|
+
expect(
|
|
615
|
+
thumbnailUpload.querySelector('.pkt-fileupload__queue-display__item--in-progress'),
|
|
616
|
+
).toBeInTheDocument()
|
|
617
|
+
expect(
|
|
618
|
+
thumbnailUpload.querySelector('.pkt-fileupload__queue-display__item__progress'),
|
|
619
|
+
).toBeInTheDocument()
|
|
620
|
+
})
|
|
621
|
+
|
|
622
|
+
test('renders uploading text state when progress bar is hidden', async () => {
|
|
623
|
+
const seed = createSeedFileItem('uploading-text-1', 'uploading.pdf')
|
|
624
|
+
const { fileupload } = await createFileUploadTest({
|
|
625
|
+
value: [seed],
|
|
626
|
+
transfers: [{ fileId: 'uploading-text-1', progress: 0, showProgress: false }],
|
|
627
|
+
itemRenderer: 'filename',
|
|
628
|
+
})
|
|
629
|
+
|
|
630
|
+
expect(
|
|
631
|
+
fileupload.querySelector('.pkt-fileupload__queue-display__item__loading-text')?.textContent,
|
|
632
|
+
).toContain('Laster opp')
|
|
633
|
+
})
|
|
634
|
+
|
|
635
|
+
test('renders error transfer state with expected class and message', async () => {
|
|
636
|
+
const seed = createSeedFileItem('error-1', 'failed.pdf')
|
|
637
|
+
const { fileupload } = await createFileUploadTest({
|
|
638
|
+
value: [seed],
|
|
639
|
+
transfers: [
|
|
640
|
+
{
|
|
641
|
+
fileId: 'error-1',
|
|
642
|
+
progress: 'error',
|
|
643
|
+
errorMessage: 'Opplastingen feilet',
|
|
644
|
+
showProgress: true,
|
|
645
|
+
lastProgress: 0.9,
|
|
646
|
+
},
|
|
647
|
+
],
|
|
648
|
+
})
|
|
649
|
+
|
|
650
|
+
expect(
|
|
651
|
+
fileupload.querySelector('.pkt-fileupload__queue-display__item--error'),
|
|
652
|
+
).toBeInTheDocument()
|
|
653
|
+
expect(
|
|
654
|
+
fileupload.querySelector('.pkt-fileupload__queue-display__item__error-message')?.textContent,
|
|
655
|
+
).toContain('Opplastingen feilet')
|
|
656
|
+
})
|
|
657
|
+
|
|
658
|
+
test('cancel button removes queue item and emits transfer-cancelled event', async () => {
|
|
659
|
+
const { fileupload } = await createFileUploadTest({ multiple: true })
|
|
660
|
+
|
|
661
|
+
const transferCancelledEvents: Array<any> = []
|
|
662
|
+
fileupload.addEventListener('transfer-cancelled', (event) =>
|
|
663
|
+
transferCancelledEvents.push((event as CustomEvent).detail),
|
|
664
|
+
)
|
|
665
|
+
const filesChangedEvents = recordFilesChanged(fileupload)
|
|
666
|
+
|
|
667
|
+
await selectFiles(fileupload, [new File(['seed'], 'cancel.pdf', { type: 'application/pdf' })])
|
|
668
|
+
const addedFileId = lastFilesFromEvents(filesChangedEvents)[0].fileId
|
|
669
|
+
fileupload.transfers = [{ fileId: addedFileId, progress: 0.5, showProgress: true }]
|
|
670
|
+
await fileupload.updateComplete
|
|
671
|
+
|
|
672
|
+
const cancelButton = fileupload.querySelector<HTMLButtonElement>(
|
|
673
|
+
'button[aria-label="Avbryt opplasting"]',
|
|
674
|
+
)
|
|
675
|
+
expect(cancelButton).toBeInTheDocument()
|
|
676
|
+
cancelButton?.click()
|
|
677
|
+
await fileupload.updateComplete
|
|
678
|
+
|
|
679
|
+
expect(filesChangedEvents.at(-1)?.reason).toBe('remove')
|
|
680
|
+
expect(fileupload.querySelectorAll('.pkt-fileupload__queue-display__item')).toHaveLength(0)
|
|
681
|
+
expect(transferCancelledEvents).toHaveLength(1)
|
|
682
|
+
expect(transferCancelledEvents[0].fileId).toBe(addedFileId)
|
|
683
|
+
})
|
|
684
|
+
|
|
685
|
+
test('renders canceled and done transfer state class names', async () => {
|
|
686
|
+
const canceled = createSeedFileItem('cancelled-file', 'cancelled.pdf')
|
|
687
|
+
const done = createSeedFileItem('done-file', 'done.pdf')
|
|
688
|
+
const { fileupload } = await createFileUploadTest({
|
|
689
|
+
value: [canceled, done],
|
|
690
|
+
transfers: [
|
|
691
|
+
{ fileId: 'cancelled-file', progress: 'canceled' },
|
|
692
|
+
{ fileId: 'done-file', progress: 'done' },
|
|
693
|
+
],
|
|
694
|
+
itemRenderer: 'thumbnail',
|
|
695
|
+
})
|
|
696
|
+
|
|
697
|
+
expect(
|
|
698
|
+
fileupload.querySelector('.pkt-fileupload__queue-display__item--canceled'),
|
|
699
|
+
).toBeInTheDocument()
|
|
700
|
+
expect(
|
|
701
|
+
fileupload.querySelector('.pkt-fileupload__queue-display__item--done'),
|
|
702
|
+
).toBeInTheDocument()
|
|
703
|
+
})
|
|
704
|
+
|
|
705
|
+
test('preview modal opens only for done previewable images in thumbnail mode', async () => {
|
|
706
|
+
const image = createSeedFileItem('preview-image-1', 'preview.png', 'image/png')
|
|
707
|
+
const doc = createSeedFileItem('preview-doc-1', 'not-previewable.pdf', 'application/pdf')
|
|
708
|
+
const { fileupload } = await createFileUploadTest({
|
|
709
|
+
value: [image, doc],
|
|
710
|
+
itemRenderer: 'thumbnail',
|
|
711
|
+
enableImagePreview: true,
|
|
712
|
+
transfers: [
|
|
713
|
+
{ fileId: 'preview-image-1', progress: 'done' },
|
|
714
|
+
{ fileId: 'preview-doc-1', progress: 'done' },
|
|
715
|
+
],
|
|
716
|
+
})
|
|
717
|
+
|
|
718
|
+
const previewButtons = fileupload.querySelectorAll<HTMLButtonElement>(
|
|
719
|
+
'.pkt-fileupload__queue-display__item__thumbnail__image-wrapper',
|
|
720
|
+
)
|
|
721
|
+
expect(previewButtons).toHaveLength(2)
|
|
722
|
+
expect(previewButtons[0].disabled).toBe(false)
|
|
723
|
+
expect(previewButtons[1].disabled).toBe(true)
|
|
724
|
+
|
|
725
|
+
previewButtons[0].click()
|
|
726
|
+
await fileupload.updateComplete
|
|
727
|
+
|
|
728
|
+
const modal = fileupload.querySelector<HTMLElement & { open?: boolean }>('pkt-modal')
|
|
729
|
+
expect(modal).toBeInTheDocument()
|
|
730
|
+
expect(modal?.open).toBe(true)
|
|
731
|
+
})
|
|
732
|
+
|
|
733
|
+
test('preview modal supports arrow navigation and escape', async () => {
|
|
734
|
+
const imageOne = createSeedFileItem('preview-nav-1', 'first.png', 'image/png')
|
|
735
|
+
const imageTwo = createSeedFileItem('preview-nav-2', 'second.png', 'image/png')
|
|
736
|
+
const { fileupload } = await createFileUploadTest({
|
|
737
|
+
value: [imageOne, imageTwo],
|
|
738
|
+
itemRenderer: 'thumbnail',
|
|
739
|
+
enableImagePreview: true,
|
|
740
|
+
transfers: [
|
|
741
|
+
{ fileId: 'preview-nav-1', progress: 'done' },
|
|
742
|
+
{ fileId: 'preview-nav-2', progress: 'done' },
|
|
743
|
+
],
|
|
744
|
+
})
|
|
745
|
+
|
|
746
|
+
const previewButton = fileupload.querySelector<HTMLButtonElement>(
|
|
747
|
+
'.pkt-fileupload__queue-display__item__thumbnail__image-wrapper',
|
|
748
|
+
)
|
|
749
|
+
previewButton?.click()
|
|
750
|
+
await fileupload.updateComplete
|
|
751
|
+
|
|
752
|
+
const modal = fileupload.querySelector<HTMLElement & { open?: boolean; headingText?: string }>(
|
|
753
|
+
'pkt-modal',
|
|
754
|
+
)
|
|
755
|
+
expect(modal?.headingText).toBe('first.png')
|
|
756
|
+
|
|
757
|
+
modal?.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }))
|
|
758
|
+
await fileupload.updateComplete
|
|
759
|
+
expect(modal?.headingText).toBe('second.png')
|
|
760
|
+
|
|
761
|
+
modal?.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
|
|
762
|
+
await fileupload.updateComplete
|
|
763
|
+
expect(modal?.open).toBe(false)
|
|
764
|
+
})
|
|
765
|
+
|
|
766
|
+
test('preview modal traps tab focus inside modal controls', async () => {
|
|
767
|
+
const imageOne = createSeedFileItem('preview-trap-1', 'first.png', 'image/png')
|
|
768
|
+
const imageTwo = createSeedFileItem('preview-trap-2', 'second.png', 'image/png')
|
|
769
|
+
const { fileupload } = await createFileUploadTest({
|
|
770
|
+
value: [imageOne, imageTwo],
|
|
771
|
+
itemRenderer: 'thumbnail',
|
|
772
|
+
enableImagePreview: true,
|
|
773
|
+
transfers: [
|
|
774
|
+
{ fileId: 'preview-trap-1', progress: 'done' },
|
|
775
|
+
{ fileId: 'preview-trap-2', progress: 'done' },
|
|
776
|
+
],
|
|
777
|
+
})
|
|
778
|
+
|
|
779
|
+
const previewButton = fileupload.querySelector<HTMLButtonElement>(
|
|
780
|
+
'.pkt-fileupload__queue-display__item__thumbnail__image-wrapper',
|
|
781
|
+
)
|
|
782
|
+
previewButton?.click()
|
|
783
|
+
await fileupload.updateComplete
|
|
784
|
+
|
|
785
|
+
const closeButton = fileupload.querySelector<HTMLButtonElement>('pkt-modal .pkt-modal__closeButton .pkt-btn')
|
|
786
|
+
const nextButton = fileupload.querySelector<HTMLButtonElement>('.pkt-fileupload__image-preview__nav--next')
|
|
787
|
+
expect(closeButton).toBeInTheDocument()
|
|
788
|
+
expect(nextButton).toBeInTheDocument()
|
|
789
|
+
if (!closeButton || !nextButton) throw new Error('Expected preview modal controls to exist')
|
|
790
|
+
|
|
791
|
+
nextButton.focus()
|
|
792
|
+
nextButton.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', bubbles: true, cancelable: true }))
|
|
793
|
+
expect(document.activeElement).toBe(closeButton)
|
|
794
|
+
|
|
795
|
+
closeButton.focus()
|
|
796
|
+
closeButton.dispatchEvent(
|
|
797
|
+
new KeyboardEvent('keydown', { key: 'Tab', shiftKey: true, bubbles: true, cancelable: true }),
|
|
798
|
+
)
|
|
799
|
+
expect(document.activeElement).toBe(nextButton)
|
|
800
|
+
})
|
|
801
|
+
|
|
802
|
+
test('falls back to icon when thumbnail or preview image fails', async () => {
|
|
803
|
+
const image = createSeedFileItem('preview-error-1', 'broken.png', 'image/png')
|
|
804
|
+
const { fileupload } = await createFileUploadTest({
|
|
805
|
+
value: [image],
|
|
806
|
+
itemRenderer: 'thumbnail',
|
|
807
|
+
enableImagePreview: true,
|
|
808
|
+
transfers: [{ fileId: 'preview-error-1', progress: 'done' }],
|
|
809
|
+
})
|
|
810
|
+
|
|
811
|
+
const thumbnailImage = fileupload.querySelector<HTMLImageElement>(
|
|
812
|
+
'.pkt-fileupload__queue-display__item__thumbnail__image-wrapper img',
|
|
813
|
+
)
|
|
814
|
+
expect(thumbnailImage).toBeInTheDocument()
|
|
815
|
+
if (!thumbnailImage) throw new Error('Expected thumbnail image to exist')
|
|
816
|
+
thumbnailImage.dispatchEvent(new Event('error'))
|
|
817
|
+
await fileupload.updateComplete
|
|
818
|
+
expect(
|
|
819
|
+
fileupload.querySelector('.pkt-fileupload__queue-display__item__thumbnail .pkt-icon'),
|
|
820
|
+
).toBeInTheDocument()
|
|
821
|
+
|
|
822
|
+
const previewButton = fileupload.querySelector<HTMLButtonElement>(
|
|
823
|
+
'.pkt-fileupload__queue-display__item__thumbnail__image-wrapper',
|
|
824
|
+
)
|
|
825
|
+
previewButton?.click()
|
|
826
|
+
await fileupload.updateComplete
|
|
827
|
+
|
|
828
|
+
const previewImage = fileupload.querySelector<HTMLImageElement>('.pkt-fileupload__image-preview__image')
|
|
829
|
+
previewImage?.dispatchEvent(new Event('error'))
|
|
830
|
+
await fileupload.updateComplete
|
|
831
|
+
expect(
|
|
832
|
+
fileupload.querySelector('.pkt-fileupload__image-preview .pkt-icon'),
|
|
833
|
+
).toBeInTheDocument()
|
|
834
|
+
})
|
|
835
|
+
|
|
836
|
+
test('renders externally-supplied errorMessage in the alert and flags the input', async () => {
|
|
837
|
+
const { fileupload } = await createFileUploadTest({
|
|
838
|
+
hasError: true,
|
|
839
|
+
errorMessage: 'Opplastingen feilet hos tjenesten',
|
|
840
|
+
})
|
|
841
|
+
|
|
842
|
+
const alert = fileupload.querySelector('pkt-alert')
|
|
843
|
+
expect(alert?.textContent).toContain('Opplastingen feilet hos tjenesten')
|
|
844
|
+
|
|
845
|
+
const input = fileupload.querySelector<HTMLInputElement>('input[type="file"]')
|
|
846
|
+
expect(input?.getAttribute('aria-invalid')).toBe('true')
|
|
847
|
+
expect(input?.getAttribute('aria-describedby')).toContain(`${fileupload.id}-error`)
|
|
848
|
+
})
|
|
849
|
+
|
|
850
|
+
test('internal validation error still shown when no external error is set', async () => {
|
|
851
|
+
const { fileupload } = await createFileUploadTest({ allowedFormats: ['pdf'] })
|
|
852
|
+
|
|
853
|
+
await selectFiles(fileupload, [new File(['x'], 'bad.png', { type: 'image/png' })])
|
|
854
|
+
|
|
855
|
+
const alert = fileupload.querySelector('pkt-alert')
|
|
856
|
+
expect(alert?.textContent).toContain('Ugyldig filtype')
|
|
857
|
+
expect(
|
|
858
|
+
fileupload.querySelector<HTMLInputElement>('input[type="file"]')?.getAttribute('aria-invalid'),
|
|
859
|
+
).toBe('true')
|
|
860
|
+
})
|
|
861
|
+
|
|
862
|
+
test('forwards optionalTag and requiredTag to pkt-input-wrapper', async () => {
|
|
863
|
+
const { fileupload: optional } = await createFileUploadTest({
|
|
864
|
+
label: 'Vedlegg',
|
|
865
|
+
optionalTag: true,
|
|
866
|
+
})
|
|
867
|
+
const optionalWrapper = optional.querySelector<HTMLElement & { optionalTag?: boolean }>('pkt-input-wrapper')
|
|
868
|
+
expect(optionalWrapper?.optionalTag).toBe(true)
|
|
869
|
+
|
|
870
|
+
const { fileupload: required } = await createFileUploadTest({
|
|
871
|
+
label: 'Vedlegg',
|
|
872
|
+
requiredTag: true,
|
|
873
|
+
})
|
|
874
|
+
const requiredWrapper = required.querySelector<HTMLElement & { requiredTag?: boolean }>('pkt-input-wrapper')
|
|
875
|
+
expect(requiredWrapper?.requiredTag).toBe(true)
|
|
876
|
+
})
|
|
877
|
+
|
|
878
|
+
test('default id is unique per instance', async () => {
|
|
879
|
+
const { fileupload: first } = await createFileUploadTest()
|
|
880
|
+
const { fileupload: second } = await createFileUploadTest()
|
|
881
|
+
|
|
882
|
+
expect(first.id).toMatch(/^pkt-fileupload-/)
|
|
883
|
+
expect(second.id).toMatch(/^pkt-fileupload-/)
|
|
884
|
+
expect(first.id).not.toBe(second.id)
|
|
885
|
+
})
|
|
886
|
+
|
|
887
|
+
test('renders queue items in the order: in-progress → error → done/queued', async () => {
|
|
888
|
+
const value: FileItem[] = [
|
|
889
|
+
createSeedFileItem('done-1', 'done.pdf'),
|
|
890
|
+
createSeedFileItem('error-1', 'failed.pdf'),
|
|
891
|
+
createSeedFileItem('progress-1', 'uploading.pdf'),
|
|
892
|
+
createSeedFileItem('queued-1', 'queued.pdf'),
|
|
893
|
+
]
|
|
894
|
+
const transfers: TFileTransfer[] = [
|
|
895
|
+
{ fileId: 'done-1', progress: 'done' },
|
|
896
|
+
{ fileId: 'error-1', progress: 'error', errorMessage: 'failed' },
|
|
897
|
+
{ fileId: 'progress-1', progress: 0.5, showProgress: true },
|
|
898
|
+
{ fileId: 'queued-1', progress: 'queued' },
|
|
899
|
+
]
|
|
900
|
+
const { fileupload } = await createFileUploadTest({
|
|
901
|
+
uploadStrategy: 'custom',
|
|
902
|
+
multiple: true,
|
|
903
|
+
value,
|
|
904
|
+
transfers,
|
|
905
|
+
})
|
|
906
|
+
|
|
907
|
+
const titles = Array.from(
|
|
908
|
+
fileupload.querySelectorAll('.pkt-fileupload__queue-display__item__title'),
|
|
909
|
+
).map((el) => {
|
|
910
|
+
const head = el.querySelector('[data-pkt-truncate-part="first"]')?.textContent ?? ''
|
|
911
|
+
const tail = el.querySelector('[data-pkt-truncate-part="tail"]')?.textContent ?? ''
|
|
912
|
+
return (head + tail).trim()
|
|
913
|
+
})
|
|
914
|
+
expect(titles).toEqual(['uploading.pdf', 'failed.pdf', 'done.pdf', 'queued.pdf'])
|
|
915
|
+
})
|
|
916
|
+
|
|
917
|
+
test('truncateTail splits long filenames into head + tail spans and leaves short ones whole', async () => {
|
|
918
|
+
const longName = 'a-very-long-filename-that-needs-truncation.pdf'
|
|
919
|
+
const longSeed = {
|
|
920
|
+
fileId: 'truncate-long',
|
|
921
|
+
file: new File(['x'], longName, { type: 'application/pdf' }),
|
|
922
|
+
attributes: { targetFilename: longName },
|
|
923
|
+
}
|
|
924
|
+
const shortSeed = {
|
|
925
|
+
fileId: 'truncate-short',
|
|
926
|
+
file: new File(['x'], 'short.pdf', { type: 'application/pdf' }),
|
|
927
|
+
attributes: { targetFilename: 'short.pdf' },
|
|
928
|
+
}
|
|
929
|
+
const { fileupload } = await createFileUploadTest({
|
|
930
|
+
value: [longSeed, shortSeed],
|
|
931
|
+
truncateTail: 6,
|
|
932
|
+
})
|
|
933
|
+
|
|
934
|
+
const titles = fileupload.querySelectorAll('.pkt-fileupload__queue-display__item__title')
|
|
935
|
+
const longTitle = titles[0] as HTMLElement
|
|
936
|
+
const head = longTitle.querySelector('.pkt-fileupload__queue-display__item__title__head')
|
|
937
|
+
const tail = longTitle.querySelector('.pkt-fileupload__queue-display__item__title__tail')
|
|
938
|
+
expect(head?.textContent).toBe(longName.slice(0, longName.length - 6))
|
|
939
|
+
expect(tail?.textContent).toBe(longName.slice(-6))
|
|
940
|
+
// head + tail reconstruct the full filename (screen readers read both)
|
|
941
|
+
expect((head?.textContent ?? '') + (tail?.textContent ?? '')).toBe(longName)
|
|
942
|
+
|
|
943
|
+
const shortTitle = titles[1] as HTMLElement
|
|
944
|
+
expect(shortTitle.querySelector('.pkt-fileupload__queue-display__item__title__head')).toBeNull()
|
|
945
|
+
expect(
|
|
946
|
+
shortTitle.querySelector('[data-pkt-truncate-part="first"]')?.textContent?.trim(),
|
|
947
|
+
).toBe('short.pdf')
|
|
948
|
+
})
|
|
949
|
+
})
|