@dile/crud 2.10.0 → 2.10.2
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/package.json +3 -3
- package/components/action/src/DileCrudSingleActions.spec.js +0 -126
- package/components/action/src/__screenshots__/DileCrudSingleActions.spec.js/DileCrudSingleActions-with-triggerAction-closing-dialog-with-ESC-and-reopening-without-data-shows-reset-form-1.png +0 -0
- package/components/action/src/__screenshots__/DileCrudSingleActions.spec.js/DileCrudSingleActions-with-triggerAction-initialData-is-cleared-after-being-applied-1.png +0 -0
- package/components/action/src/__screenshots__/DileCrudSingleActions.spec.js/DileCrudSingleActions-with-triggerAction-initialData-is-only-applied-once-and-cleared-after-opening-1.png +0 -0
- package/components/action/src/__screenshots__/DileCrudSingleActions.spec.js/DileCrudSingleActions-with-triggerAction-triggerAction-behavior-matches-original-when-no-initialData-1.png +0 -0
- package/components/action/src/__screenshots__/DileCrudSingleActions.spec.js/DileCrudSingleActions-with-triggerAction-triggerAction-opens-confirm-dialog-1.png +0 -0
- package/components/action/src/__screenshots__/DileCrudSingleActions.spec.js/DileCrudSingleActions-with-triggerAction-triggerAction-with-initialData-sets-form-values-1.png +0 -0
- package/components/action/src/__screenshots__/DileCrudSingleActions.spec.js/DileCrudSingleActions-with-triggerAction-triggerAction-with-initialData-sets-form-values-before-opening-dialog-1.png +0 -0
- package/components/action/src/__screenshots__/DileCrudSingleActions.spec.js/DileCrudSingleActions-with-triggerAction-triggerAction-without-initialData-resets-form-1.png +0 -0
- package/components/action/src/__screenshots__/DileCrudSingleActions.spec.js/DileCrudSingleActions-with-triggerAction-triggerAction-without-initialData-resets-form-to-empty-1.png +0 -0
- package/components/ajax/ajax.spec.js +0 -35
- package/components/ajax-select-crud/ajax-select-crud-overlay.spec.js +0 -186
- package/components/crud/__screenshots__/crud.spec.js/dile-crud-list-grid-view-switch-defaults-to-the-item-template-and-toggles-to-the-grid-on-click-when-both-views-are-configured-1.png +0 -0
- package/components/crud/__screenshots__/crud.spec.js/dile-crud-list-grid-view-switch-does-not-render-the-switch-button-when-customization-disableListWiewSwitch-is-true-1.png +0 -0
- package/components/crud/crud.spec.js +0 -453
- package/components/file-uploader/file-uploader.spec.js +0 -32
- package/components/list/__screenshots__/crud-data-grid.spec.js/dile-crud-data-grid-always-forces-sort-mode-to-external-regardless-of-config-1.png +0 -0
- package/components/list/__screenshots__/crud-data-grid.spec.js/dile-crud-data-grid-forwards-stickyFirstColumn--striped--responsiveMode-and-emptyMessage-with-sensible-defaults-1.png +0 -0
- package/components/list/__screenshots__/crud-data-grid.spec.js/dile-crud-data-grid-leaves-computeRowId-unset-when-config-computeItemId-is-not-a-function-1.png +0 -0
- package/components/list/__screenshots__/crud-list.spec.js/dile-crud-list-scope-changes-and-page-reset-keeps-the-current-page-on-refresh---and-goes-back-to-page-1-on-refresh---resetPage--true----1.png +0 -0
- package/components/list/__screenshots__/crud-list.spec.js/dile-crud-list-scope-changes-and-page-reset-reloads-from-the-first-page-with-the-new-scope-when-belongsTo-and-relationId-change-1.png +0 -0
- package/components/list/__screenshots__/crud-list.spec.js/dile-crud-list-scope-changes-and-page-reset-reloads-when-only-relationId-changes-1.png +0 -0
- package/components/list/crud-data-grid.spec.js +0 -207
- package/components/list/crud-item-actions.spec.js +0 -122
- package/components/list/crud-list.spec.js +0 -455
- package/components/many-relation/many-relation.spec.js +0 -154
- package/components/select-ajax-simple/select-ajax-simple.spec.js +0 -133
- package/components/single/src/DileCrudSingle.spec.js +0 -151
- package/components/single/src/__screenshots__/DileCrudSingle.spec.js/DileCrudSingle-triggerAction-and-triggerDirectAction-triggerAction-delegates-to-singleActionsElement-triggerAction-1.png +0 -0
- package/components/single/src/__screenshots__/DileCrudSingle.spec.js/DileCrudSingle-triggerAction-and-triggerDirectAction-triggerAction-without-initialData-works-1.png +0 -0
- package/components/single/src/__screenshots__/DileCrudSingle.spec.js/DileCrudSingle-triggerAction-and-triggerDirectAction-triggerDirectAction-delegates-to-singleActionsElement-triggerAction-1.png +0 -0
- package/components/single/src/__screenshots__/DileCrudSingle.spec.js/DileCrudSingle-triggerAction-and-triggerDirectAction-triggerDirectAction-without-initialData-still-works--backward-compatibility--1.png +0 -0
- package/components/steps-form/__screenshots__/steps-form.spec.js/dile-steps-form-translates-the-navigation-labels-with-the-language-property-1.png +0 -0
- package/components/steps-form/steps-form.spec.js +0 -230
- package/components/ui/__screenshots__/crud-sort-form.spec.js/dile-crud-sort-form-clicking-the-label-of-an-inactive-option-switches-to-it-using-its-own-configured-direction--without-flipping-it-1.png +0 -0
- package/components/ui/__screenshots__/crud-sort-form.spec.js/dile-crud-sort-form-clicking-the-label-of-the-already-active-option-toggles-its-direction-instead-of-doing-nothing-1.png +0 -0
- package/components/ui/__screenshots__/crud-sort-form.spec.js/dile-crud-sort-form-syncSort-suppresses-exactly-one-radio-group-echo--preserving-the-synced-direction-instead-of-falling-back-to-the-option-static-default-1.png +0 -0
- package/components/ui/crud-sort-form.spec.js +0 -132
- package/lib/CrudConfigBuilder.spec.js +0 -32
- package/lib/deepMerge.spec.js +0 -45
- package/lib/listViewMode.spec.js +0 -73
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
-
import './select-ajax-simple.js';
|
|
3
|
-
|
|
4
|
-
const MOCK_DATA = [
|
|
5
|
-
{ id: 1, name: 'Fantasy' },
|
|
6
|
-
{ id: 2, name: 'Strategy' },
|
|
7
|
-
];
|
|
8
|
-
|
|
9
|
-
function mockAxiosSuccess(data = MOCK_DATA) {
|
|
10
|
-
window.axiosInstance = {
|
|
11
|
-
get: vi.fn().mockResolvedValue({ status: 200, data: { data } }),
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function mockAxiosError() {
|
|
16
|
-
window.axiosInstance = {
|
|
17
|
-
get: vi.fn().mockRejectedValue(new Error('Network error')),
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async function renderEl(html) {
|
|
22
|
-
document.body.innerHTML = html;
|
|
23
|
-
const el = document.body.querySelector('dile-select-ajax-simple');
|
|
24
|
-
await el.updateComplete;
|
|
25
|
-
// Wait for async loadData to complete and re-render
|
|
26
|
-
await new Promise(resolve => setTimeout(resolve, 10));
|
|
27
|
-
await el.updateComplete;
|
|
28
|
-
return el;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
describe('dile-select-ajax-simple', () => {
|
|
32
|
-
beforeEach(() => {
|
|
33
|
-
mockAxiosSuccess();
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
afterEach(() => {
|
|
37
|
-
document.body.innerHTML = '';
|
|
38
|
-
delete window.axiosInstance;
|
|
39
|
-
vi.restoreAllMocks();
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('shows a spinner while loading', async () => {
|
|
43
|
-
document.body.innerHTML = '<dile-select-ajax-simple endpoint="/api/tags" name="tag"></dile-select-ajax-simple>';
|
|
44
|
-
const el = document.body.querySelector('dile-select-ajax-simple');
|
|
45
|
-
await el.updateComplete;
|
|
46
|
-
|
|
47
|
-
expect(el.shadowRoot.querySelector('dile-spinner-horizontal')).toBeTruthy();
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('renders options after successful data load', async () => {
|
|
51
|
-
const el = await renderEl('<dile-select-ajax-simple endpoint="/api/tags" name="tag"></dile-select-ajax-simple>');
|
|
52
|
-
|
|
53
|
-
const options = el.shadowRoot.querySelectorAll('option');
|
|
54
|
-
// placeholder + 2 data options
|
|
55
|
-
expect(options.length).toBe(3);
|
|
56
|
-
expect(options[1].textContent).toBe('Fantasy');
|
|
57
|
-
expect(options[2].textContent).toBe('Strategy');
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('uses displayProperty and idProperty for option rendering', async () => {
|
|
61
|
-
window.axiosInstance = {
|
|
62
|
-
get: vi.fn().mockResolvedValue({
|
|
63
|
-
status: 200,
|
|
64
|
-
data: { data: [{ code: 'FR', title: 'France' }] }
|
|
65
|
-
}),
|
|
66
|
-
};
|
|
67
|
-
const el = await renderEl(
|
|
68
|
-
'<dile-select-ajax-simple endpoint="/api/countries" name="country" displayProperty="title" idProperty="code"></dile-select-ajax-simple>'
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
const options = el.shadowRoot.querySelectorAll('option');
|
|
72
|
-
expect(options[1].value).toBe('FR');
|
|
73
|
-
expect(options[1].textContent).toBe('France');
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('shows error message when request fails', async () => {
|
|
77
|
-
mockAxiosError();
|
|
78
|
-
const el = await renderEl('<dile-select-ajax-simple endpoint="/api/tags" name="tag"></dile-select-ajax-simple>');
|
|
79
|
-
|
|
80
|
-
expect(el.shadowRoot.querySelector('p.error')).toBeTruthy();
|
|
81
|
-
expect(el.shadowRoot.querySelector('dile-select')).toBeNull();
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('shows empty message when response has no items', async () => {
|
|
85
|
-
window.axiosInstance = {
|
|
86
|
-
get: vi.fn().mockResolvedValue({ status: 200, data: { data: [] } }),
|
|
87
|
-
};
|
|
88
|
-
const el = await renderEl('<dile-select-ajax-simple endpoint="/api/tags" name="tag"></dile-select-ajax-simple>');
|
|
89
|
-
|
|
90
|
-
expect(el.shadowRoot.querySelector('p.empty')).toBeTruthy();
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('dispatches element-changed when an option is selected', async () => {
|
|
94
|
-
const el = await renderEl('<dile-select-ajax-simple endpoint="/api/tags" name="tag"></dile-select-ajax-simple>');
|
|
95
|
-
|
|
96
|
-
let detail = null;
|
|
97
|
-
el.addEventListener('element-changed', (e) => { detail = e.detail; });
|
|
98
|
-
|
|
99
|
-
el.value = '1';
|
|
100
|
-
await el.updateComplete;
|
|
101
|
-
|
|
102
|
-
expect(detail).not.toBeNull();
|
|
103
|
-
expect(detail.name).toBe('tag');
|
|
104
|
-
expect(detail.value).toBe('1');
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('supports getSelectResultList as custom extractor', async () => {
|
|
108
|
-
window.axiosInstance = {
|
|
109
|
-
get: vi.fn().mockResolvedValue({ status: 200, data: { result: { items: MOCK_DATA } } }),
|
|
110
|
-
};
|
|
111
|
-
// Set getSelectResultList before connectedCallback fires
|
|
112
|
-
const el = document.createElement('dile-select-ajax-simple');
|
|
113
|
-
el.getSelectResultList = (json) => json.result.items;
|
|
114
|
-
el.endpoint = '/api/tags';
|
|
115
|
-
el.name = 'tag';
|
|
116
|
-
document.body.appendChild(el);
|
|
117
|
-
await el.updateComplete;
|
|
118
|
-
await new Promise(resolve => setTimeout(resolve, 10));
|
|
119
|
-
await el.updateComplete;
|
|
120
|
-
|
|
121
|
-
const options = el.shadowRoot.querySelectorAll('option');
|
|
122
|
-
expect(options.length).toBe(3);
|
|
123
|
-
expect(options[1].textContent).toBe('Fantasy');
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it('renders label when provided', async () => {
|
|
127
|
-
const el = await renderEl('<dile-select-ajax-simple endpoint="/api/tags" name="tag" label="Category"></dile-select-ajax-simple>');
|
|
128
|
-
|
|
129
|
-
const label = el.shadowRoot.querySelector('label');
|
|
130
|
-
expect(label).toBeTruthy();
|
|
131
|
-
expect(label.textContent).toBe('Category');
|
|
132
|
-
});
|
|
133
|
-
});
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, afterEach, vi } from 'vitest';
|
|
2
|
-
import { html, LitElement } from 'lit';
|
|
3
|
-
import { DileForm } from '@dile/ui/mixins/form';
|
|
4
|
-
import '../crud-single.js';
|
|
5
|
-
|
|
6
|
-
// Create a test form component
|
|
7
|
-
class TestActionForm extends DileForm(LitElement) {
|
|
8
|
-
render() {
|
|
9
|
-
return html`
|
|
10
|
-
<input name="productUuid" />
|
|
11
|
-
<input name="units" type="number" value="1" />
|
|
12
|
-
`;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
if (!customElements.get('test-action-form')) {
|
|
17
|
-
customElements.define('test-action-form', TestActionForm);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
describe('DileCrudSingle.triggerAction and triggerDirectAction', () => {
|
|
21
|
-
afterEach(() => {
|
|
22
|
-
document.body.innerHTML = '';
|
|
23
|
-
delete window.axiosInstance;
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
function mockAxios({ get = vi.fn(() => Promise.resolve({ status: 200, data: {} })) } = {}) {
|
|
27
|
-
window.axiosInstance = { get };
|
|
28
|
-
return window.axiosInstance;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const testActions = [
|
|
32
|
-
{
|
|
33
|
-
name: 'CreateInvoiceAction',
|
|
34
|
-
label: 'Create Invoice',
|
|
35
|
-
destructive: false,
|
|
36
|
-
},
|
|
37
|
-
];
|
|
38
|
-
|
|
39
|
-
function createFormTemplate() {
|
|
40
|
-
return (selection, element) => html`
|
|
41
|
-
${selection === 'CreateInvoiceAction'
|
|
42
|
-
? html`<test-action-form action="CreateInvoiceAction"></test-action-form>`
|
|
43
|
-
: ''
|
|
44
|
-
}
|
|
45
|
-
`;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const testConfig = {
|
|
49
|
-
endpoint: 'https://example.test/api/items',
|
|
50
|
-
actions: {
|
|
51
|
-
single: testActions,
|
|
52
|
-
directSingleActions: [],
|
|
53
|
-
list: [],
|
|
54
|
-
},
|
|
55
|
-
customization: {
|
|
56
|
-
disableEdit: true,
|
|
57
|
-
disableListActionsOnSingleComponent: true,
|
|
58
|
-
},
|
|
59
|
-
labels: {
|
|
60
|
-
startUpdateAction: 'Edit',
|
|
61
|
-
},
|
|
62
|
-
templates: {
|
|
63
|
-
detail: (item) => html`<div class="item-detail">${item?.name || ''}</div>`,
|
|
64
|
-
formSingleActions: createFormTemplate(),
|
|
65
|
-
relations: () => html``,
|
|
66
|
-
updateForm: () => html``,
|
|
67
|
-
},
|
|
68
|
-
formIds: {
|
|
69
|
-
updateForm: 'update-form',
|
|
70
|
-
},
|
|
71
|
-
translations: {
|
|
72
|
-
element_actions: 'Actions',
|
|
73
|
-
},
|
|
74
|
-
responseAdapter: {
|
|
75
|
-
setResponse: () => {},
|
|
76
|
-
getData: () => ({ id: 1, name: 'Test Item' }),
|
|
77
|
-
getActionResponseMessage: () => 'Success',
|
|
78
|
-
getActionResponseActionName: () => 'CreateInvoiceAction',
|
|
79
|
-
getActionResponseActionData: () => ({}),
|
|
80
|
-
},
|
|
81
|
-
destructiveActionNames: [],
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
async function createCrudSingle() {
|
|
85
|
-
mockAxios({
|
|
86
|
-
get: vi.fn(() => Promise.resolve({
|
|
87
|
-
status: 200,
|
|
88
|
-
data: { data: { id: 1, name: 'Test Item' } }
|
|
89
|
-
}))
|
|
90
|
-
});
|
|
91
|
-
const el = document.createElement('dile-crud-single');
|
|
92
|
-
el.relatedId = '1';
|
|
93
|
-
el.config = testConfig;
|
|
94
|
-
el.element = { id: 1, name: 'Test Item' };
|
|
95
|
-
document.body.appendChild(el);
|
|
96
|
-
await el.updateComplete;
|
|
97
|
-
return el;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
it('triggerAction delegates to singleActionsElement.triggerAction', async () => {
|
|
101
|
-
const el = await createCrudSingle();
|
|
102
|
-
|
|
103
|
-
const singleActionsElement = el.singleActionsElement;
|
|
104
|
-
const triggerActionSpy = vi.spyOn(singleActionsElement, 'triggerAction');
|
|
105
|
-
|
|
106
|
-
const initialData = { productUuid: 'uuid-123', units: 5 };
|
|
107
|
-
el.triggerAction('CreateInvoiceAction', initialData);
|
|
108
|
-
|
|
109
|
-
expect(triggerActionSpy).toHaveBeenCalledWith('CreateInvoiceAction', initialData);
|
|
110
|
-
triggerActionSpy.mockRestore();
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('triggerDirectAction delegates to singleActionsElement.triggerAction', async () => {
|
|
114
|
-
const el = await createCrudSingle();
|
|
115
|
-
|
|
116
|
-
const singleActionsElement = el.singleActionsElement;
|
|
117
|
-
const triggerActionSpy = vi.spyOn(singleActionsElement, 'triggerAction');
|
|
118
|
-
|
|
119
|
-
const initialData = { productUuid: 'uuid-456', units: 2 };
|
|
120
|
-
el.triggerDirectAction('CreateInvoiceAction', initialData);
|
|
121
|
-
|
|
122
|
-
expect(triggerActionSpy).toHaveBeenCalledWith('CreateInvoiceAction', initialData);
|
|
123
|
-
triggerActionSpy.mockRestore();
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it('triggerDirectAction without initialData still works (backward compatibility)', async () => {
|
|
127
|
-
const el = await createCrudSingle();
|
|
128
|
-
|
|
129
|
-
const singleActionsElement = el.singleActionsElement;
|
|
130
|
-
const triggerActionSpy = vi.spyOn(singleActionsElement, 'triggerAction');
|
|
131
|
-
|
|
132
|
-
el.triggerDirectAction('CreateInvoiceAction');
|
|
133
|
-
|
|
134
|
-
expect(triggerActionSpy).toHaveBeenCalledWith('CreateInvoiceAction', undefined);
|
|
135
|
-
triggerActionSpy.mockRestore();
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it('triggerAction without initialData works', async () => {
|
|
139
|
-
const el = await createCrudSingle();
|
|
140
|
-
|
|
141
|
-
const singleActionsElement = el.singleActionsElement;
|
|
142
|
-
const triggerActionSpy = vi.spyOn(singleActionsElement, 'triggerAction');
|
|
143
|
-
|
|
144
|
-
el.triggerAction('CreateInvoiceAction');
|
|
145
|
-
|
|
146
|
-
expect(triggerActionSpy).toHaveBeenCalledWith('CreateInvoiceAction', undefined);
|
|
147
|
-
triggerActionSpy.mockRestore();
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
|
|
Binary file
|
|
Binary file
|
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, afterEach, beforeEach, vi } from 'vitest';
|
|
2
|
-
import './steps-form.js';
|
|
3
|
-
import './step-form.js';
|
|
4
|
-
|
|
5
|
-
class TestStepForm extends HTMLElement {
|
|
6
|
-
constructor() {
|
|
7
|
-
super();
|
|
8
|
-
this.value = '';
|
|
9
|
-
this.receivedData = [];
|
|
10
|
-
this.errors = null;
|
|
11
|
-
}
|
|
12
|
-
getData() {
|
|
13
|
-
return { name: this.value };
|
|
14
|
-
}
|
|
15
|
-
setData(data) {
|
|
16
|
-
this.receivedData.push(data);
|
|
17
|
-
}
|
|
18
|
-
showErrors(errors) {
|
|
19
|
-
this.errors = errors;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
if (!customElements.get('test-step-form')) {
|
|
23
|
-
customElements.define('test-step-form', TestStepForm);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const loadedResource = { id: 1, name: 'Loaded' };
|
|
27
|
-
|
|
28
|
-
function okResponse(data, message = 'Saved') {
|
|
29
|
-
return Promise.resolve({ status: 200, data: { message, data } });
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function validationError(errors) {
|
|
33
|
-
return Promise.reject({
|
|
34
|
-
response: { status: 422, data: { message: 'Invalid data', errors } },
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const flush = () => new Promise(resolve => setTimeout(resolve, 0));
|
|
39
|
-
|
|
40
|
-
describe('dile-steps-form', () => {
|
|
41
|
-
let get;
|
|
42
|
-
let patch;
|
|
43
|
-
|
|
44
|
-
beforeEach(() => {
|
|
45
|
-
get = vi.fn(() => okResponse(loadedResource));
|
|
46
|
-
patch = vi.fn((url, data) => okResponse({ ...loadedResource, ...data, url }));
|
|
47
|
-
window.axiosInstance = { get, patch };
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
afterEach(() => {
|
|
51
|
-
document.body.innerHTML = '';
|
|
52
|
-
delete window.axiosInstance;
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
async function renderStepsForm(attributes = '', thirdStepAttributes = '') {
|
|
56
|
-
document.body.innerHTML = `
|
|
57
|
-
<dile-steps-form endpoint="/api/resource" ${attributes}>
|
|
58
|
-
<dile-step-form label="First" endpoint="/api/resource/first">
|
|
59
|
-
<test-step-form slot="form"></test-step-form>
|
|
60
|
-
</dile-step-form>
|
|
61
|
-
<dile-step-form label="Second" endpoint="/api/resource/second" alternativeExit alternativeExitLabel="Save as draft">
|
|
62
|
-
<test-step-form slot="form"></test-step-form>
|
|
63
|
-
</dile-step-form>
|
|
64
|
-
<dile-step-form label="Third" endpoint="/api/resource/third" ${thirdStepAttributes}>
|
|
65
|
-
<test-step-form slot="form"></test-step-form>
|
|
66
|
-
</dile-step-form>
|
|
67
|
-
</dile-steps-form>
|
|
68
|
-
`;
|
|
69
|
-
const el = document.body.querySelector('dile-steps-form');
|
|
70
|
-
await el.updateComplete;
|
|
71
|
-
await flush();
|
|
72
|
-
await el.updateComplete;
|
|
73
|
-
return el;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function stepForms(el) {
|
|
77
|
-
return [...el.querySelectorAll('dile-step-form')];
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function forms(el) {
|
|
81
|
-
return [...el.querySelectorAll('test-step-form')];
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
it('takes the step labels from the label attribute of each step', async () => {
|
|
85
|
-
const el = await renderStepsForm();
|
|
86
|
-
|
|
87
|
-
expect(el.shadowRoot.querySelector('dile-steps').steps).toEqual(['First', 'Second', 'Third']);
|
|
88
|
-
expect(el.shadowRoot.querySelector('.step-label').textContent).toBe('First');
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('only shows the current step', async () => {
|
|
92
|
-
const el = await renderStepsForm();
|
|
93
|
-
|
|
94
|
-
expect(stepForms(el).map(step => step.active)).toEqual([true, false, false]);
|
|
95
|
-
expect(stepForms(el).map(step => getComputedStyle(step).display)).toEqual(['block', 'none', 'none']);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('loads the endpoint and sets the data in every form', async () => {
|
|
99
|
-
const el = await renderStepsForm();
|
|
100
|
-
let loadedDetail;
|
|
101
|
-
el.addEventListener('dile-steps-form-loaded', e => { loadedDetail = e.detail; });
|
|
102
|
-
|
|
103
|
-
el.reload();
|
|
104
|
-
await flush();
|
|
105
|
-
|
|
106
|
-
expect(get).toHaveBeenCalledWith('/api/resource', expect.anything());
|
|
107
|
-
expect(loadedDetail).toEqual({ data: loadedResource });
|
|
108
|
-
forms(el).forEach(form => expect(form.receivedData.at(-1)).toEqual(loadedResource));
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('saves the step with PATCH and moves to the next step', async () => {
|
|
112
|
-
const el = await renderStepsForm();
|
|
113
|
-
forms(el)[0].value = 'Acme';
|
|
114
|
-
let successDetail;
|
|
115
|
-
el.addEventListener('dile-step-form-success', e => { successDetail = e.detail; });
|
|
116
|
-
|
|
117
|
-
stepForms(el)[0].next();
|
|
118
|
-
await flush();
|
|
119
|
-
await el.updateComplete;
|
|
120
|
-
|
|
121
|
-
expect(patch).toHaveBeenCalledWith('/api/resource/first', { name: 'Acme' }, undefined);
|
|
122
|
-
expect(successDetail.message).toBe('Saved');
|
|
123
|
-
expect(el.current).toBe(1);
|
|
124
|
-
expect(stepForms(el).map(step => step.active)).toEqual([false, true, false]);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it('dispatches dile-steps-form-complete after saving the last step', async () => {
|
|
128
|
-
const el = await renderStepsForm();
|
|
129
|
-
let completeDetail;
|
|
130
|
-
el.addEventListener('dile-steps-form-complete', e => { completeDetail = e.detail; });
|
|
131
|
-
|
|
132
|
-
el.navigateTo(2);
|
|
133
|
-
stepForms(el)[2].next();
|
|
134
|
-
await flush();
|
|
135
|
-
|
|
136
|
-
expect(completeDetail.alternativeExit).toBe(false);
|
|
137
|
-
expect(completeDetail.data.url).toBe('/api/resource/third');
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it('completes with the alternative exit and sends the alternativeExit field', async () => {
|
|
141
|
-
const el = await renderStepsForm();
|
|
142
|
-
let completeDetail;
|
|
143
|
-
el.addEventListener('dile-steps-form-complete', e => { completeDetail = e.detail; });
|
|
144
|
-
|
|
145
|
-
el.navigateTo(1);
|
|
146
|
-
stepForms(el)[1].alternativeNext();
|
|
147
|
-
await flush();
|
|
148
|
-
|
|
149
|
-
expect(patch).toHaveBeenCalledWith('/api/resource/second', { name: '', alternativeExit: true }, undefined);
|
|
150
|
-
expect(completeDetail.alternativeExit).toBe(true);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it('does not keep the alternative exit after a failed save', async () => {
|
|
154
|
-
const el = await renderStepsForm();
|
|
155
|
-
let completed = false;
|
|
156
|
-
el.addEventListener('dile-steps-form-complete', () => { completed = true; });
|
|
157
|
-
el.navigateTo(1);
|
|
158
|
-
|
|
159
|
-
patch.mockImplementationOnce(() => validationError({ name: ['Required'] }));
|
|
160
|
-
stepForms(el)[1].alternativeNext();
|
|
161
|
-
await flush();
|
|
162
|
-
expect(forms(el)[1].errors).toEqual({ name: ['Required'] });
|
|
163
|
-
|
|
164
|
-
stepForms(el)[1].next();
|
|
165
|
-
await flush();
|
|
166
|
-
|
|
167
|
-
expect(completed).toBe(false);
|
|
168
|
-
expect(el.current).toBe(2);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it('dispatches dile-step-form-error with the validation errors and status', async () => {
|
|
172
|
-
const el = await renderStepsForm();
|
|
173
|
-
let errorDetail;
|
|
174
|
-
el.addEventListener('dile-step-form-error', e => { errorDetail = e.detail; });
|
|
175
|
-
patch.mockImplementationOnce(() => validationError({ name: ['Required'] }));
|
|
176
|
-
|
|
177
|
-
stepForms(el)[0].next();
|
|
178
|
-
await flush();
|
|
179
|
-
|
|
180
|
-
expect(errorDetail).toEqual({ message: 'Invalid data', errors: { name: ['Required'] }, status: 422 });
|
|
181
|
-
expect(el.current).toBe(0);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it('sends the saved data to the steps with refreshOnSave', async () => {
|
|
185
|
-
const el = await renderStepsForm('', 'refreshOnSave');
|
|
186
|
-
forms(el).forEach(form => { form.receivedData = []; });
|
|
187
|
-
|
|
188
|
-
stepForms(el)[0].next();
|
|
189
|
-
await flush();
|
|
190
|
-
|
|
191
|
-
expect(forms(el)[2].receivedData).toHaveLength(1);
|
|
192
|
-
expect(forms(el)[2].receivedData[0].url).toBe('/api/resource/first');
|
|
193
|
-
expect(forms(el)[1].receivedData).toHaveLength(0);
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it('dispatches dile-steps-form-error with the status when the load fails', async () => {
|
|
197
|
-
get.mockImplementation(() => Promise.reject({
|
|
198
|
-
response: { status: 404, data: { message: 'Not found' } },
|
|
199
|
-
}));
|
|
200
|
-
let errorDetail;
|
|
201
|
-
document.body.addEventListener('dile-steps-form-error', e => { errorDetail = e.detail; });
|
|
202
|
-
|
|
203
|
-
await renderStepsForm();
|
|
204
|
-
await flush();
|
|
205
|
-
|
|
206
|
-
expect(errorDetail.status).toBe(404);
|
|
207
|
-
expect(errorDetail.message).toBe('Not found');
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it('translates the navigation labels with the language property', async () => {
|
|
211
|
-
const el = await renderStepsForm('language="es"');
|
|
212
|
-
const nextPrev = stepForms(el)[0].shadowRoot.querySelector('dile-step-next-prev');
|
|
213
|
-
|
|
214
|
-
await vi.waitFor(() => {
|
|
215
|
-
expect(nextPrev.shadowRoot.querySelector('.next dile-button').textContent.trim()).toBe('Siguiente');
|
|
216
|
-
});
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
it('uses the labels defined in the step before the translations', async () => {
|
|
220
|
-
const el = await renderStepsForm('', 'endLabel="Create invoice"');
|
|
221
|
-
el.navigateTo(2);
|
|
222
|
-
await el.updateComplete;
|
|
223
|
-
await stepForms(el)[2].updateComplete;
|
|
224
|
-
const nextPrev = stepForms(el)[2].shadowRoot.querySelector('dile-step-next-prev');
|
|
225
|
-
await nextPrev.updateComplete;
|
|
226
|
-
|
|
227
|
-
expect(nextPrev.shadowRoot.querySelector('.next dile-button').textContent.trim()).toBe('Create invoice');
|
|
228
|
-
expect(nextPrev.shadowRoot.querySelector('.prev dile-button').textContent.trim()).toBe('Previous');
|
|
229
|
-
});
|
|
230
|
-
});
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
|
-
import './crud-sort-form.js';
|
|
3
|
-
|
|
4
|
-
describe('dile-crud-sort-form', () => {
|
|
5
|
-
afterEach(() => {
|
|
6
|
-
document.body.innerHTML = '';
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
const sortOptions = [
|
|
10
|
-
{ name: 'name', label: 'Name', direction: 'asc' },
|
|
11
|
-
{ name: 'id', label: 'Id', direction: 'asc' },
|
|
12
|
-
];
|
|
13
|
-
|
|
14
|
-
async function createSortForm(props = {}) {
|
|
15
|
-
const el = document.createElement('dile-crud-sort-form');
|
|
16
|
-
el.sortOptions = props.sortOptions ?? sortOptions;
|
|
17
|
-
if (props.sortField !== undefined) {
|
|
18
|
-
el.sortField = props.sortField;
|
|
19
|
-
}
|
|
20
|
-
document.body.appendChild(el);
|
|
21
|
-
await el.updateComplete;
|
|
22
|
-
return el;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
it('renders one radio/order-switch pair per configured sort option', async () => {
|
|
26
|
-
const el = await createSortForm();
|
|
27
|
-
expect(el.shadowRoot.querySelectorAll('dile-radio').length).toBe(2);
|
|
28
|
-
expect(el.shadowRoot.querySelectorAll('dile-order-switch').length).toBe(2);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('getDirection returns the configured direction for a known field', async () => {
|
|
32
|
-
const el = await createSortForm();
|
|
33
|
-
await el.shadowRoot.querySelector('dile-order-switch[name="name"]').updateComplete;
|
|
34
|
-
expect(el.getDirection('name')).toBe('asc');
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('getDirection returns undefined instead of throwing for a field with no matching order-switch', async () => {
|
|
38
|
-
// Reproduces a DataGrid column marked sortable whose field isn't listed in config.sort.options.
|
|
39
|
-
const el = await createSortForm();
|
|
40
|
-
expect(() => el.getDirection('continent')).not.toThrow();
|
|
41
|
-
expect(el.getDirection('continent')).toBeUndefined();
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('getDirection returns undefined for a falsy field without touching the DOM', async () => {
|
|
45
|
-
const el = await createSortForm();
|
|
46
|
-
expect(el.getDirection(null)).toBeUndefined();
|
|
47
|
-
expect(el.getDirection('')).toBeUndefined();
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
// "year" has a static configured direction of "desc" so it can expose the regression:
|
|
51
|
-
// syncing an "asc" sort must not get silently corrected back to "desc".
|
|
52
|
-
const yearOptions = [{ name: 'year', label: 'Year', direction: 'desc' }];
|
|
53
|
-
|
|
54
|
-
it('syncSort reflects an externally-applied sort without re-dispatching sort-changed', async () => {
|
|
55
|
-
const el = await createSortForm({ sortOptions: yearOptions });
|
|
56
|
-
|
|
57
|
-
let dispatchCount = 0;
|
|
58
|
-
el.addEventListener('sort-changed', () => { dispatchCount++; });
|
|
59
|
-
|
|
60
|
-
el.syncSort('year', 'asc');
|
|
61
|
-
await el.updateComplete;
|
|
62
|
-
|
|
63
|
-
expect(el.sortField).toBe('year');
|
|
64
|
-
expect(el.sortDirection).toBe('asc');
|
|
65
|
-
expect(dispatchCount).toBe(0);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('a genuine radio-group selection (not preceded by syncSort) still defaults to the option configured direction', async () => {
|
|
69
|
-
const el = await createSortForm({ sortOptions: yearOptions });
|
|
70
|
-
|
|
71
|
-
let receivedDetail = null;
|
|
72
|
-
el.addEventListener('sort-changed', (e) => { receivedDetail = e.detail; });
|
|
73
|
-
|
|
74
|
-
const radioGroup = el.shadowRoot.querySelector('dile-radio-group');
|
|
75
|
-
radioGroup.dispatchEvent(new CustomEvent('dile-radio-group-changed', {
|
|
76
|
-
detail: { name: 'selector', value: 'year' },
|
|
77
|
-
}));
|
|
78
|
-
await el.updateComplete;
|
|
79
|
-
|
|
80
|
-
expect(el.sortField).toBe('year');
|
|
81
|
-
expect(el.sortDirection).toBe('desc');
|
|
82
|
-
expect(receivedDetail).toEqual({ sortField: 'year', sortDirection: 'desc' });
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('clicking the label of an inactive option switches to it using its own configured direction, without flipping it', async () => {
|
|
86
|
-
const el = await createSortForm({
|
|
87
|
-
sortOptions: [
|
|
88
|
-
{ name: 'name', label: 'Name', direction: 'asc' },
|
|
89
|
-
{ name: 'year', label: 'Year', direction: 'desc' },
|
|
90
|
-
],
|
|
91
|
-
sortField: 'name',
|
|
92
|
-
});
|
|
93
|
-
el.sortDirection = 'asc';
|
|
94
|
-
await el.updateComplete;
|
|
95
|
-
|
|
96
|
-
let receivedDetail = null;
|
|
97
|
-
el.addEventListener('sort-changed', (e) => { receivedDetail = e.detail; });
|
|
98
|
-
|
|
99
|
-
el.shadowRoot.querySelector('dile-order-switch[name="year"]').shadowRoot.querySelector('a').click();
|
|
100
|
-
await el.updateComplete;
|
|
101
|
-
|
|
102
|
-
expect(el.sortField).toBe('year');
|
|
103
|
-
expect(el.sortDirection).toBe('desc'); // year's own configured default, not flipped
|
|
104
|
-
expect(receivedDetail).toEqual({ sortField: 'year', sortDirection: 'desc' });
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('clicking the label of the already-active option toggles its direction instead of doing nothing', async () => {
|
|
108
|
-
// Reproduces the reported confusion: clicking "Name" again while already sorted by
|
|
109
|
-
// name silently did nothing, because the label always re-announced the same value.
|
|
110
|
-
const el = await createSortForm({
|
|
111
|
-
sortOptions: [{ name: 'name', label: 'Name', direction: 'asc' }],
|
|
112
|
-
sortField: 'name',
|
|
113
|
-
});
|
|
114
|
-
el.sortDirection = 'asc';
|
|
115
|
-
await el.updateComplete;
|
|
116
|
-
|
|
117
|
-
let receivedDetail = null;
|
|
118
|
-
el.addEventListener('sort-changed', (e) => { receivedDetail = e.detail; });
|
|
119
|
-
|
|
120
|
-
const label = () => el.shadowRoot.querySelector('dile-order-switch[name="name"]').shadowRoot.querySelector('a');
|
|
121
|
-
|
|
122
|
-
label().click();
|
|
123
|
-
await el.updateComplete;
|
|
124
|
-
expect(el.sortField).toBe('name');
|
|
125
|
-
expect(el.sortDirection).toBe('desc'); // toggled, not a no-op
|
|
126
|
-
expect(receivedDetail).toEqual({ sortField: 'name', sortDirection: 'desc' });
|
|
127
|
-
|
|
128
|
-
label().click();
|
|
129
|
-
await el.updateComplete;
|
|
130
|
-
expect(el.sortDirection).toBe('asc'); // toggles back on a second click
|
|
131
|
-
});
|
|
132
|
-
});
|