@dile/ui 3.17.1 → 3.18.1
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/components/steps/index.js +2 -0
- package/components/steps/src/DileStep.js +90 -0
- package/components/steps/src/DileSteps.js +171 -0
- package/components/steps/step.js +5 -0
- package/components/steps/steps.js +5 -0
- package/components/toast/src/DileToast.js +2 -1
- package/components/toast/src/DileToastPersistent.js +1 -1
- package/package.json +4 -4
- package/components/accordion/accordion.spec.js +0 -253
- package/components/app-drawer/app-drawer.spec.js +0 -262
- package/components/avatar/avatar.spec.js +0 -25
- package/components/bottom-nav/__screenshots__/bottom-nav.spec.js/dile-bottom-nav-places-the-sheet-and-the-overlay-above-the-bar-1.png +0 -0
- package/components/bottom-nav/bottom-nav.spec.js +0 -340
- package/components/box-selector/box-selector-item.spec.js +0 -27
- package/components/box-selector/box-selector.spec.js +0 -27
- package/components/breadcrumbs/breadcrumbs-item.spec.js +0 -28
- package/components/breadcrumbs/breadcrumbs.spec.js +0 -33
- package/components/button/button.spec.js +0 -234
- package/components/card/card-slide.spec.js +0 -30
- package/components/card/card.spec.js +0 -26
- package/components/checkbox/checkbox.spec.js +0 -235
- package/components/chip/chip.spec.js +0 -31
- package/components/color-picker/color-picker.spec.js +0 -133
- package/components/confirm/confirm.spec.js +0 -57
- package/components/confirm-text/confirm-text.spec.js +0 -29
- package/components/copy-markdown-url/copy-markdown-url.spec.js +0 -27
- package/components/copy-text/copy-text.spec.js +0 -38
- package/components/data-grid/__screenshots__/data-grid.spec.js/dile-data-grid-dispatches-item-checkbox-changed-and-dile-data-grid-item-selected-when-a-checkbox-is-toggled-1.png +0 -0
- package/components/data-grid/data-grid.spec.js +0 -421
- package/components/drop-file/drop-file.spec.js +0 -214
- package/components/drop-file/file-preview.spec.js +0 -35
- package/components/floating-feedback/floating-feedback.spec.js +0 -30
- package/components/floating-menu/floating-menu-item.spec.js +0 -27
- package/components/floating-menu/floating-menu.spec.js +0 -31
- package/components/form-separator/form-separator.spec.js +0 -28
- package/components/hamburger/hamburger.spec.js +0 -28
- package/components/icon/icon.spec.js +0 -31
- package/components/icon-switch/icon-switch-group.spec.js +0 -46
- package/components/icon-switch/icon-switch.spec.js +0 -35
- package/components/info-box/info-box.spec.js +0 -38
- package/components/inline-feedback/inline-feedback.spec.js +0 -46
- package/components/input/input-contact.spec.js +0 -32
- package/components/input/input-icon.spec.js +0 -166
- package/components/input/input-integer.spec.js +0 -31
- package/components/input/input-mask.spec.js +0 -314
- package/components/input/input-message.spec.js +0 -27
- package/components/input/input-money.spec.js +0 -30
- package/components/input/input-number-mask.spec.js +0 -48
- package/components/input/input-number.spec.js +0 -109
- package/components/input/input-percentage.spec.js +0 -31
- package/components/input/input-search.spec.js +0 -91
- package/components/input/input.spec.js +0 -47
- package/components/light-mode-switch/light-mode-switch.spec.js +0 -34
- package/components/menu-hamburger/menu-hamburger.spec.js +0 -34
- package/components/menu-overlay/menu-overlay.spec.js +0 -44
- package/components/message/message.spec.js +0 -42
- package/components/modal/__screenshots__/modal.spec.js/dile-modal-closes-only-the-topmost-modal-when-Esc-is-pressed-1.png +0 -0
- package/components/modal/__screenshots__/modal.spec.js/dile-modal-dispatches-dile-modal-closed-and-animates-when-the-opened-property-is-set-to-false-1.png +0 -0
- package/components/modal/__screenshots__/modal.spec.js/dile-modal-does-not-close-a-blocking-modal-when-Esc-is-pressed-1.png +0 -0
- package/components/modal/__screenshots__/modal.spec.js/dile-modal-does-not-close-the-modal-below-when-the-topmost-modal-is-blocking-1.png +0 -0
- package/components/modal/__screenshots__/modal.spec.js/dile-modal-ignores-transitionend-events-coming-from-inner-elements-1.png +0 -0
- package/components/modal/__screenshots__/modal.spec.js/dile-modal-keeps-the-focus-on-content-focused-by-the-user-when-opening-1.png +0 -0
- package/components/modal/__screenshots__/modal.spec.js/dile-modal-opens-and-closes-through-the-opened-attribute-1.png +0 -0
- package/components/modal/__screenshots__/modal.spec.js/dile-modal-reflects-the-opened-property-to-the-attribute-1.png +0 -0
- package/components/modal/__screenshots__/modal.spec.js/dile-modal-releases-the-background-layer-after-the-close-animation-even-without-transitionend-1.png +0 -0
- package/components/modal/__screenshots__/modal.spec.js/dile-modal-releases-the-background-layer-immediately-when-the-animation-duration-is-0s-1.png +0 -0
- package/components/modal/modal-help.spec.js +0 -32
- package/components/modal/modal.spec.js +0 -250
- package/components/nav/nav.spec.js +0 -33
- package/components/number-picker/number-picker-element.spec.js +0 -30
- package/components/number-picker/number-picker.spec.js +0 -30
- package/components/order-switch/__screenshots__/order-switch.spec.js/dile-order-switch-clicking-the-label-re-announces-the-current-value-without-toggling-when-not-selected-1.png +0 -0
- package/components/order-switch/__screenshots__/order-switch.spec.js/dile-order-switch-clicking-the-label-toggles-the-order-when-selected-1.png +0 -0
- package/components/order-switch/__screenshots__/order-switch.spec.js/dile-order-switch-defaults-to-ascending-order-1.png +0 -0
- package/components/order-switch/order-switch.spec.js +0 -57
- package/components/otp-input/otp-input.spec.js +0 -41
- package/components/pages/pages.spec.js +0 -44
- package/components/password/password.spec.js +0 -35
- package/components/progress-bar/progress-bar.spec.js +0 -28
- package/components/radio-group/__screenshots__/radio-group-dialog.spec.js/dile-radio-group-dialog-renders-dialog-items-with-labels-and-values-1.png +0 -0
- package/components/radio-group/__screenshots__/radio-group-dialog.spec.js/dile-radio-group-dialog-renders-items-with-optional-description-1.png +0 -0
- package/components/radio-group/__screenshots__/radio-group-dialog.spec.js/dile-radio-group-dialog-updates-value-when-an-option-is-selected-1.png +0 -0
- package/components/radio-group/radio-group-dialog.spec.js +0 -126
- package/components/radio-group/radio-group.spec.js +0 -128
- package/components/radio-group/radio.spec.js +0 -32
- package/components/range/range.spec.js +0 -36
- package/components/rating/rating.spec.js +0 -34
- package/components/rating/star.spec.js +0 -30
- package/components/rating-scale-question/rating-scale-option.spec.js +0 -32
- package/components/rating-scale-question/rating-scale-question.spec.js +0 -35
- package/components/select/__screenshots__/select-search.spec.js/dile-select-search-Value-API-uses-the-label-as-accessible-name-of-the-selected-field-1.png +0 -0
- package/components/select/select-ajax-overlay.spec.js +0 -469
- package/components/select/select-overlay.spec.js +0 -612
- package/components/select/select-search.spec.js +0 -318
- package/components/select/select.spec.js +0 -574
- package/components/selector/selector-item.spec.js +0 -40
- package/components/selector/selector-overlay.spec.js +0 -139
- package/components/selector/selector.spec.js +0 -45
- package/components/slide-menu/slide-menu.spec.js +0 -38
- package/components/slide-show/slide-show.spec.js +0 -34
- package/components/social-icon/social-icon.spec.js +0 -26
- package/components/spinner/spinner-bar.spec.js +0 -26
- package/components/spinner/spinner-block.spec.js +0 -21
- package/components/spinner/spinner-horizontal.spec.js +0 -25
- package/components/spinner/spinner-icon.spec.js +0 -25
- package/components/spinner/spinner-modal.spec.js +0 -26
- package/components/spinner/spinner.spec.js +0 -26
- package/components/switch/switch.spec.js +0 -33
- package/components/tabs/tabs.spec.js +0 -85
- package/components/textarea/textarea.spec.js +0 -39
- package/components/time-picker/time-picker.spec.js +0 -26
- package/components/timer/timer.spec.js +0 -31
- package/components/toast/toast.spec.js +0 -86
- package/components/tooltip/chip-tooltip.spec.js +0 -29
- package/components/tooltip/tooltip.spec.js +0 -32
- package/components/web-visor/web-visor.spec.js +0 -32
- package/mixins/form/src/DileForm.spec.js +0 -29
|
@@ -1,421 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
|
-
import { html } from 'lit';
|
|
3
|
-
import './data-grid.js';
|
|
4
|
-
|
|
5
|
-
describe('dile-data-grid', () => {
|
|
6
|
-
afterEach(() => {
|
|
7
|
-
document.body.innerHTML = '';
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
async function createDataGrid(props = {}) {
|
|
11
|
-
const el = document.createElement('dile-data-grid');
|
|
12
|
-
Object.assign(el, props);
|
|
13
|
-
document.body.appendChild(el);
|
|
14
|
-
await el.updateComplete;
|
|
15
|
-
return el;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const sampleColumns = [
|
|
19
|
-
{ field: 'id', header: 'ID', sortable: true, width: '80px', align: 'center' },
|
|
20
|
-
{ field: 'name', header: 'Name', sortable: true },
|
|
21
|
-
{ field: 'age', header: 'Age', sortable: true, align: 'right' },
|
|
22
|
-
{
|
|
23
|
-
header: 'Custom',
|
|
24
|
-
render: (row) => html`<span class="custom-badge">${row.name} (${row.age})</span>`,
|
|
25
|
-
},
|
|
26
|
-
];
|
|
27
|
-
|
|
28
|
-
const sampleItems = [
|
|
29
|
-
{ id: 1, name: 'Carlos', age: 30 },
|
|
30
|
-
{ id: 2, name: 'Ana', age: 25 },
|
|
31
|
-
{ id: 3, name: 'Beatriz', age: 35 },
|
|
32
|
-
];
|
|
33
|
-
|
|
34
|
-
it('renders default empty message when items is empty', async () => {
|
|
35
|
-
const el = await createDataGrid({ columns: sampleColumns, items: [] });
|
|
36
|
-
const emptyCell = el.shadowRoot.querySelector('.empty-cell');
|
|
37
|
-
expect(emptyCell).toBeTruthy();
|
|
38
|
-
expect(emptyCell.textContent.trim()).toBe('No data available');
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('renders customized empty message', async () => {
|
|
42
|
-
const el = await createDataGrid({
|
|
43
|
-
columns: sampleColumns,
|
|
44
|
-
items: [],
|
|
45
|
-
emptyMessage: 'Nothing found',
|
|
46
|
-
});
|
|
47
|
-
const emptyCell = el.shadowRoot.querySelector('.empty-cell');
|
|
48
|
-
expect(emptyCell).toBeTruthy();
|
|
49
|
-
expect(emptyCell.textContent.trim()).toBe('Nothing found');
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('renders column headers and data rows correctly', async () => {
|
|
53
|
-
const el = await createDataGrid({
|
|
54
|
-
columns: sampleColumns,
|
|
55
|
-
items: sampleItems,
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
const headers = el.shadowRoot.querySelectorAll('th');
|
|
59
|
-
expect(headers.length).toBe(4);
|
|
60
|
-
expect(headers[0].textContent).toContain('ID');
|
|
61
|
-
expect(headers[1].textContent).toContain('Name');
|
|
62
|
-
expect(headers[2].textContent).toContain('Age');
|
|
63
|
-
expect(headers[3].textContent).toContain('Custom');
|
|
64
|
-
|
|
65
|
-
const rows = el.shadowRoot.querySelectorAll('tbody tr');
|
|
66
|
-
expect(rows.length).toBe(3);
|
|
67
|
-
|
|
68
|
-
const firstRowCells = rows[0].querySelectorAll('td');
|
|
69
|
-
expect(firstRowCells[0].textContent.trim()).toBe('1');
|
|
70
|
-
expect(firstRowCells[1].textContent.trim()).toBe('Carlos');
|
|
71
|
-
expect(firstRowCells[2].textContent.trim()).toBe('30');
|
|
72
|
-
expect(firstRowCells[3].querySelector('.custom-badge')).toBeTruthy();
|
|
73
|
-
expect(firstRowCells[3].textContent.trim()).toBe('Carlos (30)');
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('sorts strings ascending and descending on client sortMode', async () => {
|
|
77
|
-
const el = await createDataGrid({
|
|
78
|
-
columns: sampleColumns,
|
|
79
|
-
items: sampleItems,
|
|
80
|
-
sortMode: 'client',
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
const nameHeader = el.shadowRoot.querySelectorAll('th')[1];
|
|
84
|
-
|
|
85
|
-
// Click 1: sort asc by name
|
|
86
|
-
nameHeader.click();
|
|
87
|
-
await el.updateComplete;
|
|
88
|
-
|
|
89
|
-
let rows = el.shadowRoot.querySelectorAll('tbody tr');
|
|
90
|
-
expect(rows[0].querySelectorAll('td')[1].textContent.trim()).toBe('Ana');
|
|
91
|
-
expect(rows[1].querySelectorAll('td')[1].textContent.trim()).toBe('Beatriz');
|
|
92
|
-
expect(rows[2].querySelectorAll('td')[1].textContent.trim()).toBe('Carlos');
|
|
93
|
-
|
|
94
|
-
// Click 2: sort desc by name
|
|
95
|
-
nameHeader.click();
|
|
96
|
-
await el.updateComplete;
|
|
97
|
-
|
|
98
|
-
rows = el.shadowRoot.querySelectorAll('tbody tr');
|
|
99
|
-
expect(rows[0].querySelectorAll('td')[1].textContent.trim()).toBe('Carlos');
|
|
100
|
-
expect(rows[1].querySelectorAll('td')[1].textContent.trim()).toBe('Beatriz');
|
|
101
|
-
expect(rows[2].querySelectorAll('td')[1].textContent.trim()).toBe('Ana');
|
|
102
|
-
|
|
103
|
-
// Click 3: reset sort
|
|
104
|
-
nameHeader.click();
|
|
105
|
-
await el.updateComplete;
|
|
106
|
-
|
|
107
|
-
rows = el.shadowRoot.querySelectorAll('tbody tr');
|
|
108
|
-
expect(rows[0].querySelectorAll('td')[1].textContent.trim()).toBe('Carlos');
|
|
109
|
-
expect(rows[1].querySelectorAll('td')[1].textContent.trim()).toBe('Ana');
|
|
110
|
-
expect(rows[2].querySelectorAll('td')[1].textContent.trim()).toBe('Beatriz');
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('sorts numbers properly in client mode', async () => {
|
|
114
|
-
const el = await createDataGrid({
|
|
115
|
-
columns: sampleColumns,
|
|
116
|
-
items: sampleItems,
|
|
117
|
-
sortMode: 'client',
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
const ageHeader = el.shadowRoot.querySelectorAll('th')[2];
|
|
121
|
-
|
|
122
|
-
// Click: sort asc by age
|
|
123
|
-
ageHeader.click();
|
|
124
|
-
await el.updateComplete;
|
|
125
|
-
|
|
126
|
-
let rows = el.shadowRoot.querySelectorAll('tbody tr');
|
|
127
|
-
expect(rows[0].querySelectorAll('td')[2].textContent.trim()).toBe('25');
|
|
128
|
-
expect(rows[1].querySelectorAll('td')[2].textContent.trim()).toBe('30');
|
|
129
|
-
expect(rows[2].querySelectorAll('td')[2].textContent.trim()).toBe('35');
|
|
130
|
-
|
|
131
|
-
// Click: sort desc by age
|
|
132
|
-
ageHeader.click();
|
|
133
|
-
await el.updateComplete;
|
|
134
|
-
|
|
135
|
-
rows = el.shadowRoot.querySelectorAll('tbody tr');
|
|
136
|
-
expect(rows[0].querySelectorAll('td')[2].textContent.trim()).toBe('35');
|
|
137
|
-
expect(rows[1].querySelectorAll('td')[2].textContent.trim()).toBe('30');
|
|
138
|
-
expect(rows[2].querySelectorAll('td')[2].textContent.trim()).toBe('25');
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it('supports custom column comparators', async () => {
|
|
142
|
-
const customColumns = [
|
|
143
|
-
{
|
|
144
|
-
field: 'date',
|
|
145
|
-
header: 'Date',
|
|
146
|
-
sortable: true,
|
|
147
|
-
comparator: (valA, valB) => new Date(valA).getTime() - new Date(valB).getTime(),
|
|
148
|
-
},
|
|
149
|
-
];
|
|
150
|
-
const items = [
|
|
151
|
-
{ date: '2026-05-10' },
|
|
152
|
-
{ date: '2025-01-01' },
|
|
153
|
-
{ date: '2026-01-15' },
|
|
154
|
-
];
|
|
155
|
-
|
|
156
|
-
const el = await createDataGrid({
|
|
157
|
-
columns: customColumns,
|
|
158
|
-
items,
|
|
159
|
-
sortMode: 'client',
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
const dateHeader = el.shadowRoot.querySelector('th');
|
|
163
|
-
dateHeader.click();
|
|
164
|
-
await el.updateComplete;
|
|
165
|
-
|
|
166
|
-
let rows = el.shadowRoot.querySelectorAll('tbody tr');
|
|
167
|
-
expect(rows[0].querySelector('td').textContent.trim()).toBe('2025-01-01');
|
|
168
|
-
expect(rows[1].querySelector('td').textContent.trim()).toBe('2026-01-15');
|
|
169
|
-
expect(rows[2].querySelector('td').textContent.trim()).toBe('2026-05-10');
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
it('dispatches dile-data-grid-sort event on header click', async () => {
|
|
173
|
-
const el = await createDataGrid({
|
|
174
|
-
columns: sampleColumns,
|
|
175
|
-
items: sampleItems,
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
let sortEventDetail = null;
|
|
179
|
-
el.addEventListener('dile-data-grid-sort', (e) => {
|
|
180
|
-
sortEventDetail = e.detail;
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
const nameHeader = el.shadowRoot.querySelectorAll('th')[1];
|
|
184
|
-
nameHeader.click();
|
|
185
|
-
|
|
186
|
-
expect(sortEventDetail).toBeTruthy();
|
|
187
|
-
expect(sortEventDetail.field).toBe('name');
|
|
188
|
-
expect(sortEventDetail.direction).toBe('asc');
|
|
189
|
-
expect(sortEventDetail.column.field).toBe('name');
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it('dispatches dile-data-grid-row-click event on row click', async () => {
|
|
193
|
-
const el = await createDataGrid({
|
|
194
|
-
columns: sampleColumns,
|
|
195
|
-
items: sampleItems,
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
let rowClickDetail = null;
|
|
199
|
-
el.addEventListener('dile-data-grid-row-click', (e) => {
|
|
200
|
-
rowClickDetail = e.detail;
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
const secondRow = el.shadowRoot.querySelectorAll('tbody tr')[1];
|
|
204
|
-
secondRow.click();
|
|
205
|
-
|
|
206
|
-
expect(rowClickDetail).toBeTruthy();
|
|
207
|
-
expect(rowClickDetail.row.id).toBe(2);
|
|
208
|
-
expect(rowClickDetail.row.name).toBe('Ana');
|
|
209
|
-
expect(rowClickDetail.index).toBe(1);
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
it('does not re-order displayed items internally when sortMode is external', async () => {
|
|
213
|
-
const el = await createDataGrid({
|
|
214
|
-
columns: sampleColumns,
|
|
215
|
-
items: sampleItems,
|
|
216
|
-
sortMode: 'external',
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
const nameHeader = el.shadowRoot.querySelectorAll('th')[1];
|
|
220
|
-
nameHeader.click();
|
|
221
|
-
await el.updateComplete;
|
|
222
|
-
|
|
223
|
-
// Items order should remain unchanged
|
|
224
|
-
const rows = el.shadowRoot.querySelectorAll('tbody tr');
|
|
225
|
-
expect(rows[0].querySelectorAll('td')[1].textContent.trim()).toBe('Carlos');
|
|
226
|
-
expect(rows[1].querySelectorAll('td')[1].textContent.trim()).toBe('Ana');
|
|
227
|
-
expect(rows[2].querySelectorAll('td')[1].textContent.trim()).toBe('Beatriz');
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
it('sets data-label attribute for responsive card mode', async () => {
|
|
231
|
-
const el = await createDataGrid({
|
|
232
|
-
columns: [
|
|
233
|
-
{ field: 'name', header: 'Full Name' },
|
|
234
|
-
{ field: 'role', header: 'Role', hideCardLabel: true },
|
|
235
|
-
{ field: 'notes', header: 'Notes', hideOnCard: true },
|
|
236
|
-
],
|
|
237
|
-
items: [{ name: 'Elena', role: 'Dev', notes: 'Secret' }],
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
const cells = el.shadowRoot.querySelectorAll('tbody tr td');
|
|
241
|
-
expect(cells[0].getAttribute('data-label')).toBe('Full Name');
|
|
242
|
-
expect(cells[1].getAttribute('data-label')).toBe('');
|
|
243
|
-
expect(cells[1].classList.contains('no-card-label')).toBe(true);
|
|
244
|
-
expect(cells[2].classList.contains('hide-on-card')).toBe(true);
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
it('reflects responsiveMode attribute', async () => {
|
|
248
|
-
const el = await createDataGrid({
|
|
249
|
-
columns: sampleColumns,
|
|
250
|
-
items: sampleItems,
|
|
251
|
-
responsiveMode: 'cards',
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
expect(el.getAttribute('responsive-mode')).toBe('cards');
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
it('applies sticky-left class when stickyFirstColumn is true or column.sticky is set', async () => {
|
|
258
|
-
const el = await createDataGrid({
|
|
259
|
-
columns: [
|
|
260
|
-
{ field: 'name', header: 'Name', sticky: true },
|
|
261
|
-
{ field: 'city', header: 'City' },
|
|
262
|
-
{ field: 'action', header: 'Action', sticky: 'right' },
|
|
263
|
-
],
|
|
264
|
-
items: [{ name: 'Carlos', city: 'Madrid', action: 'Edit' }],
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
const ths = el.shadowRoot.querySelectorAll('th');
|
|
268
|
-
const tds = el.shadowRoot.querySelectorAll('tbody tr td');
|
|
269
|
-
|
|
270
|
-
expect(ths[0].classList.contains('sticky-left')).toBe(true);
|
|
271
|
-
expect(tds[0].classList.contains('sticky-left')).toBe(true);
|
|
272
|
-
|
|
273
|
-
expect(ths[1].classList.contains('sticky-left')).toBe(false);
|
|
274
|
-
expect(tds[1].classList.contains('sticky-left')).toBe(false);
|
|
275
|
-
|
|
276
|
-
expect(ths[2].classList.contains('sticky-right')).toBe(true);
|
|
277
|
-
expect(tds[2].classList.contains('sticky-right')).toBe(true);
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
it('applies sticky-left to the first column when stickyFirstColumn property is set without selection', async () => {
|
|
281
|
-
const el = await createDataGrid({
|
|
282
|
-
columns: sampleColumns,
|
|
283
|
-
items: sampleItems,
|
|
284
|
-
stickyFirstColumn: true,
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
const ths = el.shadowRoot.querySelectorAll('th');
|
|
288
|
-
const tds = el.shadowRoot.querySelectorAll('tbody tr td');
|
|
289
|
-
|
|
290
|
-
expect(ths[0].classList.contains('sticky-left')).toBe(true);
|
|
291
|
-
expect(tds[0].classList.contains('sticky-left')).toBe(true);
|
|
292
|
-
expect(ths[1].classList.contains('sticky-left')).toBe(false);
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
it('handles sticky columns properly when selectable is true and stickyFirstColumn is true', async () => {
|
|
296
|
-
const el = await createDataGrid({
|
|
297
|
-
columns: sampleColumns,
|
|
298
|
-
items: sampleItems,
|
|
299
|
-
selectable: true,
|
|
300
|
-
stickyFirstColumn: true,
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
const ths = el.shadowRoot.querySelectorAll('th');
|
|
304
|
-
const tds = el.shadowRoot.querySelectorAll('tbody tr td');
|
|
305
|
-
|
|
306
|
-
// th[0] is selection cell -> sticky-left
|
|
307
|
-
expect(ths[0].classList.contains('selection-cell')).toBe(true);
|
|
308
|
-
expect(ths[0].classList.contains('sticky-left')).toBe(true);
|
|
309
|
-
expect(tds[0].classList.contains('selection-cell')).toBe(true);
|
|
310
|
-
expect(tds[0].classList.contains('sticky-left')).toBe(true);
|
|
311
|
-
|
|
312
|
-
// th[1] is first data column -> sticky-left-after-selection
|
|
313
|
-
expect(ths[1].classList.contains('sticky-left-after-selection')).toBe(true);
|
|
314
|
-
expect(tds[1].classList.contains('sticky-left-after-selection')).toBe(true);
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
it('renders selection column when selectable is true and checks items matching selectedIds', async () => {
|
|
318
|
-
const el = await createDataGrid({
|
|
319
|
-
columns: sampleColumns,
|
|
320
|
-
items: sampleItems,
|
|
321
|
-
selectable: true,
|
|
322
|
-
selectedIds: [2],
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
const ths = el.shadowRoot.querySelectorAll('th');
|
|
326
|
-
expect(ths.length).toBe(5);
|
|
327
|
-
expect(ths[0].classList.contains('selection-cell')).toBe(true);
|
|
328
|
-
|
|
329
|
-
const rows = el.shadowRoot.querySelectorAll('tbody tr');
|
|
330
|
-
expect(rows.length).toBe(3);
|
|
331
|
-
|
|
332
|
-
const checkboxes = el.shadowRoot.querySelectorAll('dile-checkbox');
|
|
333
|
-
expect(checkboxes.length).toBe(3);
|
|
334
|
-
expect(checkboxes[0].checked).toBe(false);
|
|
335
|
-
expect(checkboxes[1].checked).toBe(true);
|
|
336
|
-
expect(checkboxes[2].checked).toBe(false);
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
it('dispatches item-checkbox-changed and dile-data-grid-item-selected when a checkbox is toggled', async () => {
|
|
340
|
-
const el = await createDataGrid({
|
|
341
|
-
columns: sampleColumns,
|
|
342
|
-
items: sampleItems,
|
|
343
|
-
selectable: true,
|
|
344
|
-
selectedIds: [],
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
let itemCheckboxDetail = null;
|
|
348
|
-
let gridItemSelectedDetail = null;
|
|
349
|
-
|
|
350
|
-
el.addEventListener('item-checkbox-changed', (e) => {
|
|
351
|
-
itemCheckboxDetail = e.detail;
|
|
352
|
-
});
|
|
353
|
-
el.addEventListener('dile-data-grid-item-selected', (e) => {
|
|
354
|
-
gridItemSelectedDetail = e.detail;
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
const firstCheckbox = el.shadowRoot.querySelectorAll('dile-checkbox')[0];
|
|
358
|
-
await firstCheckbox.updateComplete;
|
|
359
|
-
const clickTarget = firstCheckbox.shadowRoot?.querySelector('div') || firstCheckbox;
|
|
360
|
-
clickTarget.click();
|
|
361
|
-
await el.updateComplete;
|
|
362
|
-
|
|
363
|
-
expect(itemCheckboxDetail).toBeTruthy();
|
|
364
|
-
expect(itemCheckboxDetail.checked).toBe(true);
|
|
365
|
-
expect(itemCheckboxDetail.itemId).toBe(1);
|
|
366
|
-
expect(itemCheckboxDetail.index).toBe(0);
|
|
367
|
-
|
|
368
|
-
expect(gridItemSelectedDetail).toBeTruthy();
|
|
369
|
-
expect(gridItemSelectedDetail.checked).toBe(true);
|
|
370
|
-
expect(gridItemSelectedDetail.itemId).toBe(1);
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
it('applies rowClass custom class to table rows', async () => {
|
|
374
|
-
const el = await createDataGrid({
|
|
375
|
-
columns: sampleColumns,
|
|
376
|
-
items: [
|
|
377
|
-
{ id: 1, name: 'Carlos', deleted_at: null },
|
|
378
|
-
{ id: 2, name: 'Ana', deleted_at: '2026-01-01' },
|
|
379
|
-
],
|
|
380
|
-
rowClass: (row) => (row.deleted_at ? 'is-deleted' : ''),
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
const rows = el.shadowRoot.querySelectorAll('tbody tr');
|
|
384
|
-
expect(rows[0].classList.contains('is-deleted')).toBe(false);
|
|
385
|
-
expect(rows[1].classList.contains('is-deleted')).toBe(true);
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
it('supports custom computeRowId function', async () => {
|
|
389
|
-
const customItems = [
|
|
390
|
-
{ uuid: 'abc-1', name: 'Carlos' },
|
|
391
|
-
{ uuid: 'xyz-2', name: 'Ana' },
|
|
392
|
-
];
|
|
393
|
-
const el = await createDataGrid({
|
|
394
|
-
columns: [{ field: 'name', header: 'Name' }],
|
|
395
|
-
items: customItems,
|
|
396
|
-
selectable: true,
|
|
397
|
-
computeRowId: (row) => row.uuid,
|
|
398
|
-
selectedIds: ['xyz-2'],
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
const checkboxes = el.shadowRoot.querySelectorAll('dile-checkbox');
|
|
402
|
-
expect(checkboxes[0].checked).toBe(false);
|
|
403
|
-
expect(checkboxes[1].checked).toBe(true);
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
it('reads sortMode and emptyMessage from their kebab-case HTML attributes', async () => {
|
|
407
|
-
document.body.innerHTML = `
|
|
408
|
-
<dile-data-grid sort-mode="external" empty-message="Nothing here"></dile-data-grid>
|
|
409
|
-
`;
|
|
410
|
-
const el = document.body.querySelector('dile-data-grid');
|
|
411
|
-
await el.updateComplete;
|
|
412
|
-
|
|
413
|
-
expect(el.sortMode).toBe('external');
|
|
414
|
-
expect(el.emptyMessage).toBe('Nothing here');
|
|
415
|
-
|
|
416
|
-
el.items = [];
|
|
417
|
-
await el.updateComplete;
|
|
418
|
-
const emptyCell = el.shadowRoot.querySelector('.empty-cell');
|
|
419
|
-
expect(emptyCell.textContent.trim()).toBe('Nothing here');
|
|
420
|
-
});
|
|
421
|
-
});
|
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
|
-
import './drop-file.js';
|
|
3
|
-
|
|
4
|
-
describe('dile-drop-file', () => {
|
|
5
|
-
afterEach(() => {
|
|
6
|
-
document.body.innerHTML = '';
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
async function renderDropFile(html) {
|
|
10
|
-
document.body.innerHTML = html;
|
|
11
|
-
const el = document.body.querySelector('dile-drop-file');
|
|
12
|
-
await el.updateComplete;
|
|
13
|
-
return el;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
it('renders the drop zone with label and file input', async () => {
|
|
17
|
-
const el = await renderDropFile('<dile-drop-file label="Upload"></dile-drop-file>');
|
|
18
|
-
expect(el.shadowRoot.querySelector('#dropZone')).toBeTruthy();
|
|
19
|
-
expect(el.shadowRoot.querySelector('label').textContent.trim()).toBe('Upload');
|
|
20
|
-
expect(el.shadowRoot.querySelector('input[type="file"]')).toBeTruthy();
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('clears the selected file name', async () => {
|
|
24
|
-
const el = await renderDropFile('<dile-drop-file></dile-drop-file>');
|
|
25
|
-
el.fileName = 'test.png';
|
|
26
|
-
await el.updateComplete;
|
|
27
|
-
|
|
28
|
-
el.clear();
|
|
29
|
-
await el.updateComplete;
|
|
30
|
-
|
|
31
|
-
expect(el.fileName).toBe('');
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('renders accept attribute when accept property is defined', async () => {
|
|
35
|
-
const el = await renderDropFile('<dile-drop-file accept="application/pdf,image/*"></dile-drop-file>');
|
|
36
|
-
const fileInput = el.shadowRoot.querySelector('input[type="file"]');
|
|
37
|
-
expect(fileInput.getAttribute('accept')).toBe('application/pdf,image/*');
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('does not render accept attribute when accept property is empty', async () => {
|
|
41
|
-
const el = await renderDropFile('<dile-drop-file accept=""></dile-drop-file>');
|
|
42
|
-
const fileInput = el.shadowRoot.querySelector('input[type="file"]');
|
|
43
|
-
expect(fileInput.getAttribute('accept')).toBeNull();
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('renders capture attribute when capture property is defined', async () => {
|
|
47
|
-
const el = await renderDropFile('<dile-drop-file capture="environment"></dile-drop-file>');
|
|
48
|
-
const fileInput = el.shadowRoot.querySelector('input[type="file"]');
|
|
49
|
-
expect(fileInput.getAttribute('capture')).toBe('environment');
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('does not render capture attribute when capture property is empty', async () => {
|
|
53
|
-
const el = await renderDropFile('<dile-drop-file capture=""></dile-drop-file>');
|
|
54
|
-
const fileInput = el.shadowRoot.querySelector('input[type="file"]');
|
|
55
|
-
expect(fileInput.getAttribute('capture')).toBeNull();
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('updates accept attribute when property changes after render', async () => {
|
|
59
|
-
const el = await renderDropFile('<dile-drop-file></dile-drop-file>');
|
|
60
|
-
let fileInput = el.shadowRoot.querySelector('input[type="file"]');
|
|
61
|
-
expect(fileInput.getAttribute('accept')).toBeNull();
|
|
62
|
-
|
|
63
|
-
el.accept = 'image/*';
|
|
64
|
-
await el.updateComplete;
|
|
65
|
-
|
|
66
|
-
fileInput = el.shadowRoot.querySelector('input[type="file"]');
|
|
67
|
-
expect(fileInput.getAttribute('accept')).toBe('image/*');
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('updates capture attribute when property changes after render', async () => {
|
|
71
|
-
const el = await renderDropFile('<dile-drop-file></dile-drop-file>');
|
|
72
|
-
let fileInput = el.shadowRoot.querySelector('input[type="file"]');
|
|
73
|
-
expect(fileInput.getAttribute('capture')).toBeNull();
|
|
74
|
-
|
|
75
|
-
el.capture = 'user';
|
|
76
|
-
await el.updateComplete;
|
|
77
|
-
|
|
78
|
-
fileInput = el.shadowRoot.querySelector('input[type="file"]');
|
|
79
|
-
expect(fileInput.getAttribute('capture')).toBe('user');
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('emits dile-drop-file-changed event when file is selected', async () => {
|
|
83
|
-
const el = await renderDropFile('<dile-drop-file></dile-drop-file>');
|
|
84
|
-
let eventDetail = null;
|
|
85
|
-
el.addEventListener('dile-drop-file-changed', (e) => { eventDetail = e.detail; });
|
|
86
|
-
|
|
87
|
-
const fileInput = el.shadowRoot.querySelector('input[type="file"]');
|
|
88
|
-
const file = new File(['content'], 'test.pdf', { type: 'application/pdf' });
|
|
89
|
-
Object.defineProperty(fileInput, 'files', {
|
|
90
|
-
value: [file],
|
|
91
|
-
writable: false,
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
fileInput.dispatchEvent(new Event('change', { bubbles: true }));
|
|
95
|
-
await el.updateComplete;
|
|
96
|
-
|
|
97
|
-
expect(eventDetail).toBeTruthy();
|
|
98
|
-
expect(eventDetail.file).toEqual(file);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('emits dile-drop-file-changed event with null when file is cleared', async () => {
|
|
102
|
-
const el = await renderDropFile('<dile-drop-file></dile-drop-file>');
|
|
103
|
-
let eventDetail = null;
|
|
104
|
-
el.addEventListener('dile-drop-file-changed', (e) => { eventDetail = e.detail; });
|
|
105
|
-
|
|
106
|
-
el.clear();
|
|
107
|
-
await el.updateComplete;
|
|
108
|
-
|
|
109
|
-
expect(eventDetail).toBeTruthy();
|
|
110
|
-
expect(eventDetail.file).toBeNull();
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
describe('multiple mode', () => {
|
|
114
|
-
function createFile(name, content = 'content') {
|
|
115
|
-
return new File([content], name, { lastModified: 1 });
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function dropFiles(el, files) {
|
|
119
|
-
const dataTransfer = new DataTransfer();
|
|
120
|
-
files.forEach(file => dataTransfer.items.add(file));
|
|
121
|
-
const dropZone = el.shadowRoot.querySelector('#dropZone');
|
|
122
|
-
dropZone.dispatchEvent(new DragEvent('drop', { dataTransfer, bubbles: true, cancelable: true }));
|
|
123
|
-
return el.updateComplete;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
it('renders the file input with the multiple attribute', async () => {
|
|
127
|
-
const el = await renderDropFile('<dile-drop-file multiple></dile-drop-file>');
|
|
128
|
-
expect(el.shadowRoot.querySelector('input[type="file"]').hasAttribute('multiple')).toBe(true);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it('does not render the multiple attribute by default', async () => {
|
|
132
|
-
const el = await renderDropFile('<dile-drop-file></dile-drop-file>');
|
|
133
|
-
expect(el.shadowRoot.querySelector('input[type="file"]').hasAttribute('multiple')).toBe(false);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it('accepts several dropped files and renders a preview for each one', async () => {
|
|
137
|
-
const el = await renderDropFile('<dile-drop-file multiple></dile-drop-file>');
|
|
138
|
-
let eventDetail = null;
|
|
139
|
-
el.addEventListener('dile-drop-file-changed', (e) => { eventDetail = e.detail; });
|
|
140
|
-
|
|
141
|
-
await dropFiles(el, [createFile('a.txt'), createFile('b.txt')]);
|
|
142
|
-
|
|
143
|
-
expect(el.shadowRoot.querySelectorAll('dile-file-preview').length).toBe(2);
|
|
144
|
-
expect(el.value.map(file => file.name)).toEqual(['a.txt', 'b.txt']);
|
|
145
|
-
expect(el.getFiles().length).toBe(2);
|
|
146
|
-
expect(el.shadowRoot.querySelector('#fileInput').files.length).toBe(2);
|
|
147
|
-
expect(eventDetail.files.map(file => file.name)).toEqual(['a.txt', 'b.txt']);
|
|
148
|
-
expect(eventDetail.file.name).toBe('a.txt');
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it('accumulates files between selections without duplicates', async () => {
|
|
152
|
-
const el = await renderDropFile('<dile-drop-file multiple></dile-drop-file>');
|
|
153
|
-
await dropFiles(el, [createFile('a.txt')]);
|
|
154
|
-
await dropFiles(el, [createFile('a.txt'), createFile('b.txt')]);
|
|
155
|
-
|
|
156
|
-
expect(el.value.map(file => file.name)).toEqual(['a.txt', 'b.txt']);
|
|
157
|
-
expect(el.shadowRoot.querySelectorAll('dile-file-preview').length).toBe(2);
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it('removes a single file with removeFile', async () => {
|
|
161
|
-
const el = await renderDropFile('<dile-drop-file multiple></dile-drop-file>');
|
|
162
|
-
await dropFiles(el, [createFile('a.txt'), createFile('b.txt'), createFile('c.txt')]);
|
|
163
|
-
|
|
164
|
-
el.removeFile(1);
|
|
165
|
-
await el.updateComplete;
|
|
166
|
-
|
|
167
|
-
expect(el.value.map(file => file.name)).toEqual(['a.txt', 'c.txt']);
|
|
168
|
-
expect(el.shadowRoot.querySelector('#fileInput').files.length).toBe(2);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it('removes a file when its preview clear link is clicked', async () => {
|
|
172
|
-
const el = await renderDropFile('<dile-drop-file multiple></dile-drop-file>');
|
|
173
|
-
await dropFiles(el, [createFile('a.txt'), createFile('b.txt')]);
|
|
174
|
-
|
|
175
|
-
const preview = el.shadowRoot.querySelectorAll('dile-file-preview')[0];
|
|
176
|
-
await preview.updateComplete;
|
|
177
|
-
preview.shadowRoot.querySelector('a').click();
|
|
178
|
-
await el.updateComplete;
|
|
179
|
-
|
|
180
|
-
expect(el.value.map(file => file.name)).toEqual(['b.txt']);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it('discards files with invalid extensions and keeps the valid ones', async () => {
|
|
184
|
-
const el = await renderDropFile('<dile-drop-file multiple></dile-drop-file>');
|
|
185
|
-
el.allowedExtensions = ['pdf'];
|
|
186
|
-
await dropFiles(el, [createFile('a.pdf'), createFile('b.exe')]);
|
|
187
|
-
|
|
188
|
-
expect(el.value.map(file => file.name)).toEqual(['a.pdf']);
|
|
189
|
-
expect(el.errored).toBe(true);
|
|
190
|
-
expect(el.shadowRoot.querySelector('.message.errored-msg')).toBeTruthy();
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
it('clears all the files', async () => {
|
|
194
|
-
const el = await renderDropFile('<dile-drop-file multiple></dile-drop-file>');
|
|
195
|
-
await dropFiles(el, [createFile('a.txt'), createFile('b.txt')]);
|
|
196
|
-
|
|
197
|
-
el.clear();
|
|
198
|
-
await el.updateComplete;
|
|
199
|
-
|
|
200
|
-
expect(el.value).toEqual([]);
|
|
201
|
-
expect(el.shadowRoot.querySelectorAll('dile-file-preview').length).toBe(0);
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
it('sets all the files as form value', async () => {
|
|
205
|
-
document.body.innerHTML = '<form><dile-drop-file name="docs" multiple></dile-drop-file></form>';
|
|
206
|
-
const el = document.querySelector('dile-drop-file');
|
|
207
|
-
await el.updateComplete;
|
|
208
|
-
await dropFiles(el, [createFile('a.txt'), createFile('b.txt')]);
|
|
209
|
-
|
|
210
|
-
const formData = new FormData(document.querySelector('form'));
|
|
211
|
-
expect(formData.getAll('docs').map(file => file.name)).toEqual(['a.txt', 'b.txt']);
|
|
212
|
-
});
|
|
213
|
-
});
|
|
214
|
-
});
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
|
-
import './file-preview.js';
|
|
3
|
-
|
|
4
|
-
describe('dile-file-preview', () => {
|
|
5
|
-
afterEach(() => {
|
|
6
|
-
document.body.innerHTML = '';
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
async function renderFilePreview(html) {
|
|
10
|
-
document.body.innerHTML = html;
|
|
11
|
-
const el = document.body.querySelector('dile-file-preview');
|
|
12
|
-
await el.updateComplete;
|
|
13
|
-
return el;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
it('renders nothing when there is no fileName', async () => {
|
|
17
|
-
const el = await renderFilePreview('<dile-file-preview></dile-file-preview>');
|
|
18
|
-
expect(el.shadowRoot.querySelector('div')).toBeNull();
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it('renders the file name and a clear link when fileName is set', async () => {
|
|
22
|
-
const el = await renderFilePreview('<dile-file-preview fileName="report.pdf" selectedFileLabel="Selected"></dile-file-preview>');
|
|
23
|
-
expect(el.shadowRoot.querySelector('div span').textContent).toContain('report.pdf');
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it('dispatches dile-file-clear when the clear link is clicked', async () => {
|
|
27
|
-
const el = await renderFilePreview('<dile-file-preview fileName="report.pdf"></dile-file-preview>');
|
|
28
|
-
let detail = null;
|
|
29
|
-
el.addEventListener('dile-file-clear', (e) => { detail = e.detail; });
|
|
30
|
-
|
|
31
|
-
el.shadowRoot.querySelector('a').click();
|
|
32
|
-
|
|
33
|
-
expect(detail).toEqual({ fileName: 'report.pdf' });
|
|
34
|
-
});
|
|
35
|
-
});
|