umy-ui-table 1.2.4 → 1.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/button.js +1 -1
- package/lib/icon.js +1 -1
- package/lib/index.js +1 -1
- package/lib/locale/format.js +1 -1
- package/lib/locale/index.js +1 -72
- package/lib/mixins/locale.js +1 -1
- package/lib/theme-chalk/button.css +1 -1
- package/lib/theme-chalk/icon.css +1 -1
- package/lib/theme-chalk/iconfont.1785242763644.ttf +0 -0
- package/lib/theme-chalk/iconfont.1785242763644.woff +0 -0
- package/lib/theme-chalk/iconfont.1785242763644.woff2 +0 -0
- package/lib/theme-chalk/index.css +1 -1
- package/lib/theme-chalk/u-table.css +1 -1
- package/lib/theme-chalk/ux-grid.css +1 -1
- package/lib/u-table-column.js +1 -1
- package/lib/u-table.js +1 -1
- package/lib/umy-ui.common.js +1 -1
- package/lib/utils/util.js +1 -1
- package/lib/ux-grid.js +1 -1
- package/lib/ux-table-column.js +1 -1
- package/package.json +4 -3
- package/packages/.DS_Store +0 -0
- package/packages/input/.DS_Store +0 -0
- package/packages/pagination/.DS_Store +0 -0
- package/packages/scrollbar/.DS_Store +0 -0
- package/packages/select/.DS_Store +0 -0
- package/packages/table/.DS_Store +0 -0
- package/packages/tag/.DS_Store +0 -0
- package/packages/tools/.DS_Store +0 -0
- package/packages/tools/locale/index.js +2 -48
- package/packages/tools/utils/.DS_Store +0 -0
- package/packages/tooltip/.DS_Store +0 -0
- package/packages/u-table/.DS_Store +0 -0
- package/packages/ux-grid/.DS_Store +0 -0
- package/packages/ux-grid/mixins/index.js +133 -25
- package/packages/ux-grid/src/dialog-draft.js +75 -0
- package/packages/ux-grid/src/element-renderer.js +809 -0
- package/packages/ux-grid/src/grid-height.js +75 -0
- package/packages/ux-grid/src/grid-kernel.js +14 -0
- package/packages/ux-grid/src/setup.js +42 -0
- package/packages/ux-grid/src/ux-grid.vue +215 -57
- package/packages/ux-grid/test/browser-acceptance.cjs +192 -0
- package/packages/ux-grid/test/browser-baseline-loader.cjs +10 -0
- package/packages/ux-grid/test/browser-entry.js +242 -0
- package/packages/ux-grid/test/browser-layout-contracts.cjs +105 -0
- package/packages/ux-grid/test/browser-layout-entry.js +66 -0
- package/packages/ux-grid/test/browser-renderer-contracts.cjs +262 -0
- package/packages/ux-grid/test/browser-renderer-entry.js +129 -0
- package/packages/ux-grid/test/browser-resize-contracts.cjs +150 -0
- package/packages/ux-grid/test/compatibility.cjs +176 -0
- package/packages/ux-grid/test/contracts.cjs +281 -0
- package/packages/ux-grid/test/delivery-umd.cjs +48 -0
- package/packages/ux-grid/test/delivery.cjs +103 -0
- package/packages/ux-grid/test/integration.cjs +132 -0
- package/packages/ux-grid/test/real-kernel-contracts.cjs +335 -0
- package/packages/ux-grid/test/real-kernel-events.cjs +233 -0
- package/packages/ux-grid/test/renderer-async.cjs +140 -0
- package/packages/ux-grid/test/renderer-boundaries.cjs +196 -0
- package/packages/ux-grid/test/ui-components.cjs +103 -0
- package/tools/locale/format.js +1 -1
- package/tools/locale/index.js +1 -7
- package/tools/mixins/locale.js +1 -1
- package/tools/utils/util.js +1 -1
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// Real Vue/Element/OLD or NEW kernel and actual DOM dispatched events.
|
|
2
|
+
// jsdom is not evidence of layout, resize or physical pointer hit testing.
|
|
3
|
+
const assert = require('assert');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const root = process.env.UX_GRID_TEST_DEPENDENCIES;
|
|
6
|
+
if (!root) throw Error('Set UX_GRID_TEST_DEPENDENCIES to the legacy consumer');
|
|
7
|
+
const mode = process.argv[2];
|
|
8
|
+
if (!mode) {
|
|
9
|
+
const results = {};
|
|
10
|
+
const traces = {};
|
|
11
|
+
for (const name of ['old', 'new']) {
|
|
12
|
+
const child = require('child_process').spawnSync(process.execPath, [__filename, name], { env: process.env, encoding: 'utf8' });
|
|
13
|
+
assert.equal(child.status, 0, name + '\n' + child.stdout + child.stderr);
|
|
14
|
+
results[name] = JSON.parse(child.stdout.split('\n').find(line => line.startsWith('RESULT ')).slice(7));
|
|
15
|
+
const timingLine = child.stdout.split('\n').find(line => line.startsWith('TIMING '));
|
|
16
|
+
assert(timingLine); traces[name] = JSON.parse(timingLine.slice(7));
|
|
17
|
+
console.log(name + ' ' + timingLine);
|
|
18
|
+
console.log(name + ' EVENTS ' + JSON.stringify(results[name]));
|
|
19
|
+
}
|
|
20
|
+
assert.deepStrictEqual(results.new, results.old);
|
|
21
|
+
for (const label of Object.keys(traces.old)) {
|
|
22
|
+
assert.deepStrictEqual(traces.new[label].filter(phase => phase !== 'nextTick'), traces.old[label].filter(phase => phase !== 'nextTick'), label + ' same event order relative to synchronous DOM dispatch return');
|
|
23
|
+
}
|
|
24
|
+
console.log('real-kernel DOM events passed');
|
|
25
|
+
process.exit(0);
|
|
26
|
+
}
|
|
27
|
+
const external = name => require(require.resolve(name, { paths: [root] }));
|
|
28
|
+
require('babel-polyfill');
|
|
29
|
+
const dom = new (external('jsdom').JSDOM)('<html><body><script></script></body></html>', { url: 'http://localhost/' });
|
|
30
|
+
for (const key of ['window', 'document', 'Element', 'HTMLElement', 'HTMLTableElement', 'Node', 'MutationObserver', 'getComputedStyle', 'name', 'Image']) global[key] = dom.window[key];
|
|
31
|
+
Object.defineProperty(global, 'navigator', { configurable: true, value: dom.window.navigator });
|
|
32
|
+
global.requestAnimationFrame = cb => setTimeout(cb, 0);
|
|
33
|
+
global.cancelAnimationFrame = clearTimeout;
|
|
34
|
+
// jsdom has no layout engine and returns null for every offsetParent. The
|
|
35
|
+
// legacy filter positioning assumes one exists; supply ancestry only, never
|
|
36
|
+
// claim these zero-size DOM tests verify popup placement or scrolling.
|
|
37
|
+
Object.defineProperty(HTMLElement.prototype, 'offsetParent', { configurable: true, get() { return this.parentElement; } });
|
|
38
|
+
const Vue = require('vue');
|
|
39
|
+
for (const location of [root, require.resolve('vxe-table'), require.resolve('vxe-pc-ui')]) require.cache[require.resolve('vue', { paths: [location] })] = require.cache[require.resolve('vue')];
|
|
40
|
+
Vue.config.productionTip = false; Vue.config.devtools = false;
|
|
41
|
+
const errors = [];
|
|
42
|
+
Vue.config.errorHandler = error => errors.push(error.stack || String(error));
|
|
43
|
+
Vue.use(external('element-ui'));
|
|
44
|
+
let Grid;
|
|
45
|
+
if (mode === 'old') {
|
|
46
|
+
const old = external('umy-table/lib/umy-table.common.js'); Vue.use(old.default || old); Grid = Vue.options.components.UxGrid;
|
|
47
|
+
} else { const current = require('../../../lib/ux-grid.js'); Grid = current.default || current; }
|
|
48
|
+
const settle = () => new Promise(resolve => setTimeout(resolve, 80));
|
|
49
|
+
const observations = {};
|
|
50
|
+
const eventTiming = {};
|
|
51
|
+
let vm;
|
|
52
|
+
(async () => {
|
|
53
|
+
vm = new Vue({ data: () => ({ allowEdit: true }), render(h) { return h(Grid, { ref: 'grid', props: {
|
|
54
|
+
height: 300, data: [{ id: 1, name: 'One' }, { id: 2, name: 'Two' }], rowId: 'id',
|
|
55
|
+
paginationShow: true, total: 30, pageSize: 10, currentPage: 1, layout: 'prev,pager,next',
|
|
56
|
+
highlightCurrentRow: true,
|
|
57
|
+
editConfig: { mode: 'cell', trigger: 'click', activeMethod: () => this.allowEdit, beforeEditMethod: () => this.allowEdit },
|
|
58
|
+
columns: [{ type: 'checkbox' }, { type: 'expand' },
|
|
59
|
+
{ field: 'id', title: 'ID', sortable: true, filters: [{ label: 'One', value: 1 }] },
|
|
60
|
+
{ field: 'name', title: 'Name', filters: [{ label: 'One', value: 'One' }], editRender: { name: 'ElInput' } },
|
|
61
|
+
{ field: 'hidden', visible: false, filters: [{ label: 'Hidden', value: 'hidden' }] }]
|
|
62
|
+
} }); } }).$mount();
|
|
63
|
+
document.body.appendChild(vm.$el); await settle();
|
|
64
|
+
const wrapper = vm.$refs.grid;
|
|
65
|
+
let emitted = []; let sequence = [];
|
|
66
|
+
const names = ['selection-change', 'select', 'select-all', 'cell-click', 'row-click', 'cell-dblclick', 'row-dblclick', 'row-contextmenu', 'header-click', 'header-contextmenu', 'sort-change', 'filter-change', 'edit-actived', 'edit-closed', 'edit-disabled', 'expand-change', 'current-change', 'radio-change', 'cell-mouse-enter', 'cell-mouse-leave'];
|
|
67
|
+
for (const name of names) wrapper.$on(name, (...args) => { emitted.push({ name, args }); sequence.push(name); });
|
|
68
|
+
async function dispatch(label, selector, type, expected) {
|
|
69
|
+
emitted = []; sequence = ['before-dispatch'];
|
|
70
|
+
const element = vm.$el.querySelector(selector);
|
|
71
|
+
assert(element, label + ': control ' + selector);
|
|
72
|
+
const domEvent = new window.MouseEvent(type, { bubbles: true, cancelable: true });
|
|
73
|
+
element.dispatchEvent(domEvent);
|
|
74
|
+
if (type === 'contextmenu') assert.equal(domEvent.defaultPrevented, false, 'empty menu retains native menu');
|
|
75
|
+
sequence.push('returned'); await Vue.nextTick(); sequence.push('nextTick'); await settle();
|
|
76
|
+
expected.forEach(name => assert(emitted.some(event => event.name === name), label + ' emits ' + name));
|
|
77
|
+
const cell = element.closest('td,th');
|
|
78
|
+
const kernel = wrapper.tableExample();
|
|
79
|
+
const columnNode = cell && kernel.getColumnNode(cell);
|
|
80
|
+
const rowNode = cell && cell.tagName === 'TD' && kernel.getRowNode(cell.parentElement);
|
|
81
|
+
const column = columnNode && columnNode.item;
|
|
82
|
+
const row = rowNode && rowNode.item;
|
|
83
|
+
for (const event of emitted) {
|
|
84
|
+
const args = event.args;
|
|
85
|
+
if (['cell-click', 'cell-dblclick', 'cell-mouse-enter', 'cell-mouse-leave'].includes(event.name)) {
|
|
86
|
+
assert.strictEqual(args[0], row); assert.strictEqual(args[1], column);
|
|
87
|
+
assert.strictEqual(args[2], cell); assert.strictEqual(args[3], domEvent);
|
|
88
|
+
} else if (['row-click', 'row-dblclick', 'row-contextmenu'].includes(event.name)) {
|
|
89
|
+
assert.strictEqual(args[0], row); assert.strictEqual(args[1], column); assert.strictEqual(args[2], domEvent);
|
|
90
|
+
} else if (['header-click', 'header-contextmenu'].includes(event.name)) {
|
|
91
|
+
assert.strictEqual(args[0], column); assert.strictEqual(args[1], domEvent);
|
|
92
|
+
} else if (['edit-actived', 'edit-disabled', 'expand-change'].includes(event.name)) {
|
|
93
|
+
assert.strictEqual(args[0].row, row); assert.strictEqual(args[0].column, column);
|
|
94
|
+
assert.equal(typeof args[0].rowIndex, 'number'); assert.equal(typeof args[0].columnIndex, 'number');
|
|
95
|
+
if (event.name === 'expand-change') { assert.equal(typeof args[0].expanded, 'boolean'); assert.strictEqual(args[0].$event, domEvent); }
|
|
96
|
+
} else if (event.name === 'current-change' || event.name === 'radio-change') {
|
|
97
|
+
assert.strictEqual(args[0], row);
|
|
98
|
+
if (event.name === 'radio-change') assert.strictEqual(args[1], column);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
eventTiming[label] = sequence.slice();
|
|
102
|
+
observations[label] = emitted.map(event => event.name);
|
|
103
|
+
return emitted;
|
|
104
|
+
}
|
|
105
|
+
await dispatch('checkbox', 'tbody tr .elx-cell--checkbox,tbody tr .vxe-cell--checkbox', 'click', ['select', 'selection-change']);
|
|
106
|
+
assert.deepStrictEqual(emitted.filter(event => ['select', 'selection-change'].includes(event.name)).map(event => event.name), ['select', 'selection-change']);
|
|
107
|
+
assert(!emitted.some(event => event.name === 'row-click'));
|
|
108
|
+
assert.equal(emitted[0].args[1].id, 1); assert(Array.isArray(emitted[1].args[0]));
|
|
109
|
+
assert(sequence.indexOf('selection-change') < sequence.indexOf('returned'));
|
|
110
|
+
await dispatch('checkbox-all', 'thead .elx-cell--checkbox,thead .vxe-cell--checkbox', 'click', ['select-all', 'selection-change']);
|
|
111
|
+
await dispatch('cell-click', 'tbody tr td:nth-child(3)', 'click', ['cell-click', 'row-click']);
|
|
112
|
+
await dispatch('cell-dblclick', 'tbody tr td:nth-child(3)', 'dblclick', ['cell-dblclick', 'row-dblclick']);
|
|
113
|
+
await dispatch('row-contextmenu', 'tbody tr td:nth-child(3)', 'contextmenu', ['row-contextmenu']);
|
|
114
|
+
await dispatch('header-click', 'thead th:nth-child(3)', 'click', ['header-click']);
|
|
115
|
+
await dispatch('header-contextmenu', 'thead th:nth-child(3)', 'contextmenu', ['header-contextmenu']);
|
|
116
|
+
await dispatch('sort', 'thead .elx-sort--desc-btn,thead .vxe-sort--desc-btn', 'click', ['sort-change']);
|
|
117
|
+
assert.equal(emitted.find(event => event.name === 'sort-change').args[0].prop, 'id');
|
|
118
|
+
await dispatch('filter-open', 'thead th:nth-child(3) .elx-filter--btn,thead th:nth-child(3) .vxe-filter--btn', 'click', []);
|
|
119
|
+
await dispatch('filter-option', '.elx-table--filter-body .elx-table--filter-option,.vxe-table--filter-body .vxe-table--filter-option', 'click', []);
|
|
120
|
+
await dispatch('filter-confirm', '.elx-table--filter-footer button:first-child,.vxe-table--filter-footer button:first-child', 'click', ['filter-change']);
|
|
121
|
+
const filterPayload = emitted.find(event => event.name === 'filter-change').args[0];
|
|
122
|
+
observations.filters = filterPayload.filters.map(item => ({ property: item.property || item.field, values: item.values }));
|
|
123
|
+
assert.deepStrictEqual(observations.filters, [{ property: 'id', values: [1] }, { property: 'name', values: [] }]);
|
|
124
|
+
await dispatch('filter-reopen', 'thead th:nth-child(3) .elx-filter--btn,thead th:nth-child(3) .vxe-filter--btn', 'click', []);
|
|
125
|
+
await dispatch('filter-reset', '.elx-table--filter-footer button:last-child,.vxe-table--filter-footer button:last-child', 'click', ['filter-change']);
|
|
126
|
+
observations.resetFilters = emitted.find(event => event.name === 'filter-change').args[0].filters.map(item => ({ property: item.property || item.field, values: item.values }));
|
|
127
|
+
assert.deepStrictEqual(observations.resetFilters, [{ property: 'id', values: [] }, { property: 'name', values: [] }]);
|
|
128
|
+
emitted = [];
|
|
129
|
+
assert.strictEqual(wrapper.clearFilter(), undefined); await settle();
|
|
130
|
+
assert(!emitted.some(event => event.name === 'filter-change'), 'programmatic clearFilter is silent');
|
|
131
|
+
await dispatch('edit', 'tbody tr td:nth-child(4)', 'click', ['edit-actived']);
|
|
132
|
+
assert.equal(emitted.filter(event => event.name === 'edit-actived').length, 1);
|
|
133
|
+
emitted = []; await wrapper.clearActived(); await settle();
|
|
134
|
+
assert.equal(emitted.filter(event => event.name === 'edit-closed').length, 1);
|
|
135
|
+
vm.allowEdit = false; await Vue.nextTick();
|
|
136
|
+
await dispatch('edit-disabled', 'tbody tr td:nth-child(4)', 'click', ['edit-disabled']);
|
|
137
|
+
assert.equal(emitted.filter(event => event.name === 'edit-disabled').length, 1);
|
|
138
|
+
await dispatch('mouseenter', 'tbody tr td:nth-child(3)', mode === 'old' ? 'mouseenter' : 'mouseover', ['cell-mouse-enter']);
|
|
139
|
+
await dispatch('mouseleave', 'tbody tr td:nth-child(3)', 'mouseleave', ['cell-mouse-leave']);
|
|
140
|
+
await dispatch('expand-row', 'tbody tr .elx-table--expanded,tbody tr .vxe-table--expand-btn', 'click', ['expand-change']);
|
|
141
|
+
wrapper.clearSelection(); wrapper.toggleRowSelection([{ row: wrapper.getRowById(1), selected: true }]);
|
|
142
|
+
assert.strictEqual(wrapper.removeCheckboxRow(), undefined); await settle();
|
|
143
|
+
assert.deepStrictEqual(wrapper.getTableData().fullData.map(row => row.id), [2]);
|
|
144
|
+
await wrapper.loadColumn([{ type: 'radio' }, { field: 'name' }]); await settle();
|
|
145
|
+
await dispatch('radio', 'tbody tr .elx-cell--radio,tbody tr .vxe-cell--radio', 'click', ['radio-change']);
|
|
146
|
+
assert.equal(emitted.find(event => event.name === 'radio-change').args[0].id, 2);
|
|
147
|
+
for (const name of ['row-contextmenu', 'header-contextmenu']) wrapper.$off(name);
|
|
148
|
+
await dispatch('unobserved-contextmenu', 'tbody tr td:nth-child(2)', 'contextmenu', []);
|
|
149
|
+
// Auxiliary wrapper-direct contracts: no claim that these are user DOM input.
|
|
150
|
+
const auxiliary = [];
|
|
151
|
+
for (const name of ['handlePageSize', 'show-field', 'reset', 'amend-field', 'handle-field']) wrapper.$on(name, (...args) => auxiliary.push({ name, args }));
|
|
152
|
+
vm.$el.querySelector('.el-pagination .btn-next').click(); await settle();
|
|
153
|
+
assert.deepStrictEqual(auxiliary.map(event => event.args[0]), [{ size: 10, page: 2 }]);
|
|
154
|
+
observations['pagination DOM'] = auxiliary[0].args[0]; auxiliary.length = 0;
|
|
155
|
+
wrapper.handleSizeChange(25); wrapper.handleCurrentChange(3);
|
|
156
|
+
assert.deepStrictEqual(auxiliary.slice(0, 2).map(event => event.args[0]), [{ size: 25, page: 2 }, { size: 25, page: 3 }]);
|
|
157
|
+
wrapper.plDialogOpens(); await new Promise(resolve => setTimeout(resolve, 240));
|
|
158
|
+
assert.equal(wrapper.$refs.plDialog.style.width, '300px');
|
|
159
|
+
Array.from(vm.$el.querySelectorAll('.table-cus-footer button')).find(button => button.textContent.trim() === '确定').click(); assert.equal(auxiliary[2].name, 'show-field'); assert(Array.isArray(auxiliary[2].args[0]));
|
|
160
|
+
wrapper.plDialogOpens(); await new Promise(resolve => setTimeout(resolve, 240));
|
|
161
|
+
Array.from(vm.$el.querySelectorAll('.table-cus-footer button')).find(button => button.textContent.trim() === '重置').click(); assert.equal(auxiliary[3].name, 'reset');
|
|
162
|
+
const field = { name: 'Auxiliary' };
|
|
163
|
+
wrapper.amendField(field, 0); assert.strictEqual(auxiliary[4].args[0], field); assert.equal(auxiliary[4].args[1], 0);
|
|
164
|
+
wrapper.handleField('下移', 0, [field, { name: 'Second' }]);
|
|
165
|
+
assert.equal(auxiliary[5].name, 'handle-field');
|
|
166
|
+
assert.deepStrictEqual(auxiliary[5].args, ['下移', 0]);
|
|
167
|
+
wrapper.plDialogOpens(); wrapper.closeModal(); assert.equal(wrapper.$refs.plDialog.style.width, '0px');
|
|
168
|
+
observations['wrapper-direct auxiliary'] = auxiliary.map(event => event.name);
|
|
169
|
+
vm.$destroy(); vm.$el.remove();
|
|
170
|
+
const parentRow = { id: 10, name: 'Tree', children: [{ id: 11, name: 'Child' }] };
|
|
171
|
+
vm = new Vue({ render: h => h(Grid, { ref: 'grid', props: {
|
|
172
|
+
height: 240, data: [parentRow], treeConfig: { children: 'children' },
|
|
173
|
+
keyboardConfig: { isArrow: true, isTab: true }, mouseConfig: { selected: true },
|
|
174
|
+
columns: [{ field: 'name', treeNode: true }]
|
|
175
|
+
} }) }).$mount();
|
|
176
|
+
document.body.appendChild(vm.$el); await settle();
|
|
177
|
+
let treePayload;
|
|
178
|
+
const treeEvents = [];
|
|
179
|
+
vm.$refs.grid.$on('toggle-tree-expand', value => { treePayload = value; treeEvents.push('toggle-tree-expand'); });
|
|
180
|
+
vm.$refs.grid.$on('cell-click', (row, column, cell, event) => {
|
|
181
|
+
assert.strictEqual(row, parentRow); assert.strictEqual(event, treeEvent);
|
|
182
|
+
assert.strictEqual(column, vm.$refs.grid.getColumnByField('name')); assert(cell instanceof HTMLElement);
|
|
183
|
+
treeEvents.push('cell-click');
|
|
184
|
+
});
|
|
185
|
+
vm.$refs.grid.$on('row-click', (row, column, event) => { assert.strictEqual(row, parentRow); assert.strictEqual(event, treeEvent); treeEvents.push('row-click'); });
|
|
186
|
+
const treeButton = vm.$el.querySelector('.elx-tree--node-btn,.vxe-cell--tree-btn');
|
|
187
|
+
assert(treeButton, 'real tree expand control');
|
|
188
|
+
const treeEvent = new window.MouseEvent('click', { bubbles: true });
|
|
189
|
+
treeButton.dispatchEvent(treeEvent); await settle();
|
|
190
|
+
assert(treePayload); assert.strictEqual(treePayload.row, parentRow);
|
|
191
|
+
assert.strictEqual(treePayload.$event, treeEvent); assert.equal(treePayload.expanded, true);
|
|
192
|
+
assert(vm.$refs.grid.isTreeExpandByRow(parentRow));
|
|
193
|
+
observations['tree DOM'] = { expanded: treePayload.expanded, row: treePayload.row.id };
|
|
194
|
+
observations['tree DOM events'] = treeEvents;
|
|
195
|
+
let keyPayload;
|
|
196
|
+
vm.$refs.grid.$on('keydown', payload => { keyPayload = payload; });
|
|
197
|
+
const firstCell = vm.$el.querySelector('tbody td');
|
|
198
|
+
firstCell.dispatchEvent(new window.MouseEvent('mousedown', { bubbles: true, button: 0 }));
|
|
199
|
+
const keyEvent = new window.KeyboardEvent('keydown', { bubbles: true, key: 'ArrowDown', code: 'ArrowDown', keyCode: 40, which: 40 });
|
|
200
|
+
document.dispatchEvent(keyEvent); await settle();
|
|
201
|
+
assert(keyPayload); assert.strictEqual(keyPayload.$event, keyEvent);
|
|
202
|
+
observations['keydown DOM'] = Object.keys(keyPayload);
|
|
203
|
+
let scrollPayload; let scrollSource;
|
|
204
|
+
vm.$refs.grid.$on('table-body-scroll', (payload, event) => { scrollPayload = payload; scrollSource = event; });
|
|
205
|
+
const scrollBody = vm.$el.querySelector('.vxe-table--scroll-y-handle') || vm.$el.querySelector('.elx-table--body-wrapper,.vxe-table--body-wrapper');
|
|
206
|
+
scrollBody.scrollTop = 30;
|
|
207
|
+
const scrollEvent = new window.Event('scroll'); scrollBody.dispatchEvent(scrollEvent); await settle();
|
|
208
|
+
assert(scrollPayload); assert.strictEqual(scrollSource, scrollEvent);
|
|
209
|
+
assert.equal(scrollPayload.scrollTop, 30); assert.equal(scrollPayload.isY, true);
|
|
210
|
+
observations['scroll DOM fields'] = Object.keys(scrollPayload).sort();
|
|
211
|
+
for (const kind of ['expand', 'tree']) {
|
|
212
|
+
vm.$destroy(); vm.$el.remove();
|
|
213
|
+
vm = new Vue({ render: h => h(Grid, { ref: 'grid', props: {
|
|
214
|
+
height: 240, data: [parentRow], highlightCurrentRow: true,
|
|
215
|
+
expandConfig: kind === 'expand' ? { trigger: 'row' } : null,
|
|
216
|
+
treeConfig: kind === 'tree' ? { children: 'children', trigger: 'row' } : null,
|
|
217
|
+
columns: kind === 'expand' ? [{ type: 'expand' }, { field: 'name' }] : [{ field: 'name', treeNode: true }]
|
|
218
|
+
} }) }).$mount();
|
|
219
|
+
document.body.appendChild(vm.$el); await settle();
|
|
220
|
+
const eventName = kind === 'expand' ? 'expand-change' : 'toggle-tree-expand';
|
|
221
|
+
const order = [];
|
|
222
|
+
for (const name of [eventName, 'current-change', 'cell-click', 'row-click']) vm.$refs.grid.$on(name, () => order.push(name));
|
|
223
|
+
vm.$el.querySelector(kind === 'expand' ? 'tbody td:nth-child(2)' : 'tbody td').click();
|
|
224
|
+
order.push('returned'); await settle();
|
|
225
|
+
assert.deepStrictEqual(order, [eventName, 'current-change', 'cell-click', 'row-click', 'returned'], kind + ' row-trigger retains native event ordering');
|
|
226
|
+
observations[kind + ' row-trigger'] = order;
|
|
227
|
+
}
|
|
228
|
+
assert.deepStrictEqual(errors, []);
|
|
229
|
+
console.log('RESULT ' + JSON.stringify(observations));
|
|
230
|
+
console.log('TIMING ' + JSON.stringify(eventTiming));
|
|
231
|
+
})().catch(error => { console.error(error.stack); process.exitCode = 1; }).finally(() => {
|
|
232
|
+
if (vm) vm.$destroy(); dom.window.close(); setImmediate(() => process.exit(process.exitCode || 0));
|
|
233
|
+
});
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// Real Element Cascader lifecycle/store behavior in jsdom. Direct wrapper calls
|
|
2
|
+
// below separately verify loader return/this/throw contracts, not UI interaction.
|
|
3
|
+
const assert = require('assert');
|
|
4
|
+
const root = process.env.UX_GRID_TEST_DEPENDENCIES;
|
|
5
|
+
if (!root) throw Error('Set UX_GRID_TEST_DEPENDENCIES');
|
|
6
|
+
const external = name => require(require.resolve(name, { paths: [root] }));
|
|
7
|
+
const { JSDOM } = external('jsdom');
|
|
8
|
+
const dom = new JSDOM('<html><body><script></script></body></html>', { url: 'http://localhost/' });
|
|
9
|
+
for (const key of ['window', 'document', 'Element', 'HTMLElement', 'Node', 'MutationObserver', 'getComputedStyle', 'name', 'Image']) global[key] = dom.window[key];
|
|
10
|
+
Object.defineProperty(global, 'navigator', { configurable: true, value: dom.window.navigator });
|
|
11
|
+
global.requestAnimationFrame = cb => setTimeout(cb, 0);
|
|
12
|
+
global.cancelAnimationFrame = clearTimeout;
|
|
13
|
+
const Vue = require('vue');
|
|
14
|
+
for (const base of [root, require.resolve('vxe-table'), require.resolve('vxe-pc-ui')]) require.cache[require.resolve('vue', { paths: [base] })] = require.cache[require.resolve('vue')];
|
|
15
|
+
Vue.config.productionTip = false;
|
|
16
|
+
Vue.config.devtools = false;
|
|
17
|
+
const errors = [];
|
|
18
|
+
Vue.config.errorHandler = error => errors.push(error.stack || String(error));
|
|
19
|
+
Vue.use(external('element-ui'));
|
|
20
|
+
require('../../../lib/ux-grid.js');
|
|
21
|
+
const { renderer } = require('vxe-table/lib/ui');
|
|
22
|
+
const cascader = renderer.get('ElCascader');
|
|
23
|
+
const wait = () => new Promise(resolve => setTimeout(resolve, 40));
|
|
24
|
+
const mounted = [];
|
|
25
|
+
function fixture(options, loader) {
|
|
26
|
+
const requests = [];
|
|
27
|
+
const row = { value: [] };
|
|
28
|
+
const column = { field: 'value' };
|
|
29
|
+
const renderOpts = { name: 'ElCascader', props: { clearable: true, options, props: { lazy: true, lazyLoad: loader || function(node, resolve) { requests.push({ node, resolve, receiver: this }); } } } };
|
|
30
|
+
const params = { row, column, $table: { updateStatus() {} } };
|
|
31
|
+
const vm = new Vue({ data: { renderOpts, row, shown: true }, render(h) {
|
|
32
|
+
return h('div', this.shown ? cascader.renderTableEdit(h, this.renderOpts, params) : []);
|
|
33
|
+
} }).$mount();
|
|
34
|
+
document.body.appendChild(vm.$el);
|
|
35
|
+
mounted.push(vm);
|
|
36
|
+
return { vm, requests, renderOpts, component: () => vm.$children[0] };
|
|
37
|
+
}
|
|
38
|
+
function nodes(component) { return component.$refs.panel.store.getFlattedNodes(false, false); }
|
|
39
|
+
async function expand(f, label) {
|
|
40
|
+
f.vm.$el.querySelector('input').click(); await wait();
|
|
41
|
+
const el = Array.from(document.querySelectorAll('.el-cascader-node')).find(el => el.textContent.trim() === label);
|
|
42
|
+
assert(el, 'real cascader option ' + label); el.click(); await wait();
|
|
43
|
+
}
|
|
44
|
+
(async () => {
|
|
45
|
+
// Initial root request binds to the mounted panel; two callbacks cannot append twice.
|
|
46
|
+
const rootLoad = fixture([]); await wait();
|
|
47
|
+
assert.equal(rootLoad.requests.length, 1);
|
|
48
|
+
const rootRequest = rootLoad.requests[0];
|
|
49
|
+
rootRequest.resolve([{ value: 'a', label: 'A', leaf: true }]); await wait();
|
|
50
|
+
rootRequest.resolve([{ value: 'duplicate', label: 'Duplicate', leaf: true }]); await wait();
|
|
51
|
+
assert.deepStrictEqual(nodes(rootLoad.component()).map(n => n.value), ['a']);
|
|
52
|
+
rootLoad.vm.$destroy();
|
|
53
|
+
// Replacing options invalidates the pending root callback without touching current store.
|
|
54
|
+
const rootStale = fixture([]); await wait();
|
|
55
|
+
const staleRoot = rootStale.requests[0];
|
|
56
|
+
rootStale.renderOpts.props.options = [{ value: 'replacement', label: 'Replacement', leaf: true }]; await wait();
|
|
57
|
+
staleRoot.resolve([{ value: 'stale', label: 'Stale', leaf: true }]); await wait();
|
|
58
|
+
assert.deepStrictEqual(nodes(rootStale.component()).map(n => n.value), ['replacement']);
|
|
59
|
+
rootStale.vm.$destroy();
|
|
60
|
+
// Two live generations, B resolves before A: obsolete A cannot contaminate
|
|
61
|
+
// either Element's replacement store or the renderer's hydrated remount cache.
|
|
62
|
+
const generations = fixture([]); await wait();
|
|
63
|
+
const requestA = generations.requests[0];
|
|
64
|
+
generations.renderOpts.props.options = []; await wait();
|
|
65
|
+
assert.equal(generations.requests.length, 2, 'replacing empty options starts generation B');
|
|
66
|
+
generations.requests[1].resolve([{ value: 'B', label: 'Current B', leaf: true }]); await wait();
|
|
67
|
+
requestA.resolve([{ value: 'A', label: 'Obsolete A', leaf: true }]); await wait();
|
|
68
|
+
assert.deepStrictEqual(nodes(generations.component()).map(n => n.value), ['B']);
|
|
69
|
+
generations.vm.shown = false; await wait();
|
|
70
|
+
generations.vm.shown = true; await wait();
|
|
71
|
+
assert.deepStrictEqual(nodes(generations.component()).map(n => n.value), ['B'], 'remount hydrates only the winning generation');
|
|
72
|
+
assert.equal(generations.requests.length, 2, 'hydrated root does not issue a redundant loader request');
|
|
73
|
+
generations.vm.$destroy();
|
|
74
|
+
// Unmount must leave the destroyed Element store untouched.
|
|
75
|
+
const destroyed = fixture([]); await wait();
|
|
76
|
+
const oldComponent = destroyed.component();
|
|
77
|
+
const oldStore = oldComponent.$refs.panel.store;
|
|
78
|
+
destroyed.vm.shown = false; await wait();
|
|
79
|
+
destroyed.requests[0].resolve([{ value: 'late', label: 'Late', leaf: true }]); await wait();
|
|
80
|
+
assert.equal(oldComponent._isDestroyed, true);
|
|
81
|
+
assert.deepStrictEqual(oldStore.getFlattedNodes(false, false), []);
|
|
82
|
+
destroyed.vm.$destroy();
|
|
83
|
+
// Child request and options replacement, with a real expand click.
|
|
84
|
+
const child = fixture([{ value: 'parent', label: 'Parent' }]); await wait();
|
|
85
|
+
await expand(child, 'Parent');
|
|
86
|
+
assert.equal(child.requests.length, 1);
|
|
87
|
+
assert.equal(child.requests[0].node.value, 'parent');
|
|
88
|
+
child.renderOpts.props.options = [{ value: 'fresh', label: 'Fresh', leaf: true }]; await wait();
|
|
89
|
+
child.requests[0].resolve([{ value: 'lateChild', label: 'Late Child', leaf: true }]); await wait();
|
|
90
|
+
assert.deepStrictEqual(nodes(child.component()).map(n => n.value), ['fresh']);
|
|
91
|
+
child.vm.$destroy();
|
|
92
|
+
const childOnce = fixture([{ value: 'parent', label: 'Once Parent' }]); await wait();
|
|
93
|
+
await expand(childOnce, 'Once Parent');
|
|
94
|
+
childOnce.requests[0].resolve([{ value: 'child', label: 'Once Child', leaf: true }]); await wait();
|
|
95
|
+
const activeStore = childOnce.component().$refs.panel.store;
|
|
96
|
+
childOnce.vm.$forceUpdate(); await wait();
|
|
97
|
+
assert.strictEqual(childOnce.component().$refs.panel.store, activeStore, 'unrelated renderer rerender must preserve active lazy store');
|
|
98
|
+
childOnce.requests[0].resolve([{ value: 'duplicate', label: 'Duplicate', leaf: true }]); await wait();
|
|
99
|
+
assert.deepStrictEqual(nodes(childOnce.component()).map(n => n.value), ['parent', 'child']);
|
|
100
|
+
childOnce.vm.shown = false; await wait();
|
|
101
|
+
childOnce.vm.shown = true; await wait();
|
|
102
|
+
childOnce.renderOpts.props.options[0].label = 'Renamed';
|
|
103
|
+
Vue.set(childOnce.renderOpts.props.options[0], 'disabled', true); await wait();
|
|
104
|
+
assert.equal(nodes(childOnce.component())[0].label, 'Renamed', 'external label edits reach hydrated nodes');
|
|
105
|
+
assert.equal(nodes(childOnce.component())[0].isDisabled, true, 'external disabled edits reach hydrated nodes');
|
|
106
|
+
Vue.set(childOnce.renderOpts.props.options[0], 'children', [{ value: 'newChild', label: 'New Child', leaf: true }]); await wait();
|
|
107
|
+
assert.deepStrictEqual(nodes(childOnce.component()).map(n => n.value), ['parent', 'newChild'], 'external children edits replace cached children');
|
|
108
|
+
childOnce.vm.$destroy();
|
|
109
|
+
const childDestroyed = fixture([{ value: 'parent', label: 'Doomed Parent' }]); await wait();
|
|
110
|
+
await expand(childDestroyed, 'Doomed Parent');
|
|
111
|
+
const oldChildStore = childDestroyed.component().$refs.panel.store;
|
|
112
|
+
childDestroyed.vm.shown = false; await wait();
|
|
113
|
+
childDestroyed.requests[0].resolve([{ value: 'late', label: 'Late', leaf: true }]); await wait();
|
|
114
|
+
assert.deepStrictEqual(oldChildStore.getFlattedNodes(false, false).map(n => n.value), ['parent']);
|
|
115
|
+
childDestroyed.vm.$destroy();
|
|
116
|
+
// Same options identity, but Element resets its store after deep label change.
|
|
117
|
+
const sameOptions = fixture([{ value: 'parent', label: 'Before' }]); await wait();
|
|
118
|
+
await expand(sameOptions, 'Before');
|
|
119
|
+
sameOptions.renderOpts.props.options[0].label = 'After'; await wait();
|
|
120
|
+
sameOptions.requests[0].resolve([{ value: 'obsolete', label: 'Obsolete', leaf: true }]); await wait();
|
|
121
|
+
assert.deepStrictEqual(nodes(sameOptions.component()).map(n => n.value), ['parent']);
|
|
122
|
+
sameOptions.vm.$destroy();
|
|
123
|
+
// The wrapper itself retains the exact business loader call contract.
|
|
124
|
+
const host = new Vue();
|
|
125
|
+
const row = { value: [] }, column = { field: 'value' };
|
|
126
|
+
const params = { row, column, $table: { updateStatus() {} } };
|
|
127
|
+
const receiver = {}, returned = {}, failure = Error('business failure');
|
|
128
|
+
let captured;
|
|
129
|
+
const options = { name: 'ElCascader', props: { options: [{ value: 'root' }], props: { lazy: true, lazyLoad(node, resolve) { assert.strictEqual(this, receiver); captured = resolve; return returned; } } } };
|
|
130
|
+
let vnode = cascader.renderTableEdit(host.$createElement, options, params)[0];
|
|
131
|
+
let resolutions = 0;
|
|
132
|
+
assert.strictEqual(vnode.componentOptions.propsData.props.lazyLoad.call(receiver, { root: true }, () => resolutions++), returned);
|
|
133
|
+
captured([]); captured([]); assert.equal(resolutions, 1);
|
|
134
|
+
options.props.props.lazyLoad = () => { throw failure; };
|
|
135
|
+
vnode = cascader.renderTableEdit(host.$createElement, options, params)[0];
|
|
136
|
+
assert.throws(() => vnode.componentOptions.propsData.props.lazyLoad({ root: true }, () => {}), error => error === failure);
|
|
137
|
+
host.$destroy();
|
|
138
|
+
assert.deepStrictEqual(errors, []);
|
|
139
|
+
console.log('renderer async passed: real Element root/child/stale/store/destroy lifecycle; direct loader this/return/throw/once contracts');
|
|
140
|
+
})().catch(error => { console.error(error); process.exitCode = 1; }).finally(() => { mounted.forEach(vm => vm.$destroy()); dom.window.close(); });
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// Real Vue/Element/VXE registry and DOM events; layout is covered by Chrome.
|
|
2
|
+
const assert = require('assert');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const dependencyRoot = process.env.UX_GRID_TEST_DEPENDENCIES;
|
|
6
|
+
if (!dependencyRoot) throw Error('Set UX_GRID_TEST_DEPENDENCIES');
|
|
7
|
+
const external = name => require(require.resolve(name, { paths: [dependencyRoot] }));
|
|
8
|
+
const { JSDOM } = external('jsdom');
|
|
9
|
+
const dom = new JSDOM('<html><body><script></script></body></html>', { url: 'http://localhost/' });
|
|
10
|
+
for (const key of ['window', 'document', 'Element', 'HTMLElement', 'HTMLTableElement', 'Node', 'MutationObserver', 'getComputedStyle', 'name', 'Image']) global[key] = dom.window[key];
|
|
11
|
+
Object.defineProperty(global, 'navigator', { configurable: true, value: dom.window.navigator });
|
|
12
|
+
global.requestAnimationFrame = cb => setTimeout(cb, 0);
|
|
13
|
+
global.cancelAnimationFrame = clearTimeout;
|
|
14
|
+
const Vue = require('vue');
|
|
15
|
+
for (const root of [dependencyRoot, require.resolve('vxe-table'), require.resolve('vxe-pc-ui')]) require.cache[require.resolve('vue', { paths: [root] })] = require.cache[require.resolve('vue')];
|
|
16
|
+
Vue.config.productionTip = false;
|
|
17
|
+
Vue.config.devtools = false;
|
|
18
|
+
const errors = [];
|
|
19
|
+
Vue.config.errorHandler = error => errors.push(error.stack || String(error));
|
|
20
|
+
Vue.use(external('element-ui'));
|
|
21
|
+
const { renderer, interceptor } = require('vxe-table/lib/ui');
|
|
22
|
+
const GridModule = require('../../../lib/ux-grid.js');
|
|
23
|
+
const Grid = GridModule.default || GridModule;
|
|
24
|
+
const wait = () => new Promise(resolve => setTimeout(resolve, 90));
|
|
25
|
+
let vm;
|
|
26
|
+
let validations = 0;
|
|
27
|
+
(async () => {
|
|
28
|
+
const row = { id: 1, choice: 'a', cascade: [], lazy: [], time: '14:24:56' };
|
|
29
|
+
const options = [{ value: 'a', label: 'Alpha' }, { value: 'b', label: 'Beta' }];
|
|
30
|
+
vm = new Vue({ render(h) { return h(Grid, { ref: 'grid', props: {
|
|
31
|
+
height: 280, data: [row], editConfig: { trigger: 'click', mode: 'cell' },
|
|
32
|
+
validConfig: { message: 'inline' },
|
|
33
|
+
rules: { choice: [{ trigger: 'change', validator({ cellValue }) {
|
|
34
|
+
validations++;
|
|
35
|
+
return cellValue === 'b' ? Promise.reject(new Error('Beta rejected')) : Promise.resolve();
|
|
36
|
+
} }] },
|
|
37
|
+
columns: [
|
|
38
|
+
{ field: 'choice', editRender: { name: 'ElSelect', options, props: { clearable: true } } },
|
|
39
|
+
{ field: 'cascade', editRender: { name: 'ElCascader', options: [{ value: 'root', label: 'Root', children: [{ value: 'leaf', label: 'Leaf' }] }] } },
|
|
40
|
+
{ field: 'lazy', editRender: { name: 'ElCascader', props: { clearable: true, options: [{ value: 'parent', label: 'Lazy Parent' }], props: { lazy: true, lazyLoad(node, resolve) { setTimeout(() => resolve([{ value: 'child', label: 'Lazy Child', leaf: true }]), 0); } } } } },
|
|
41
|
+
{ field: 'time', editRender: { name: 'ElTimePicker', props: { valueFormat: 'HH:mm:ss' } } }
|
|
42
|
+
]
|
|
43
|
+
} }); } }).$mount();
|
|
44
|
+
document.body.appendChild(vm.$el);
|
|
45
|
+
await wait();
|
|
46
|
+
const grid = vm.$refs.grid;
|
|
47
|
+
const table = grid.tableExample();
|
|
48
|
+
const event = new window.MouseEvent('mousedown', { bubbles: true });
|
|
49
|
+
const trace = [];
|
|
50
|
+
const first = params => { trace.push('first'); params.marker = 'mutated'; };
|
|
51
|
+
const second = params => { trace.push('second'); assert.equal(params.marker, 'original'); };
|
|
52
|
+
interceptor.add('event.clearActived', first);
|
|
53
|
+
interceptor.add('event.clearActived', second);
|
|
54
|
+
const invoke = () => table.preventEvent(event, 'event.clearEdit', { marker: 'original' }, () => { trace.push('next'); return 'next-result'; });
|
|
55
|
+
assert.equal(invoke(), 'next-result');
|
|
56
|
+
assert.deepStrictEqual(trace.splice(0), ['first', 'second', 'next']);
|
|
57
|
+
const veto = () => false;
|
|
58
|
+
interceptor.add('event.clearActived', veto);
|
|
59
|
+
assert.equal(invoke(), null);
|
|
60
|
+
assert.deepStrictEqual(trace.splice(0), ['first', 'second']);
|
|
61
|
+
interceptor.delete('event.clearActived', veto);
|
|
62
|
+
const result = { exact: true };
|
|
63
|
+
const resultVeto = () => ({ status: false, result });
|
|
64
|
+
interceptor.add('event.clearActived', resultVeto);
|
|
65
|
+
assert.strictEqual(invoke(), result);
|
|
66
|
+
trace.length = 0;
|
|
67
|
+
interceptor.delete('event.clearActived', resultVeto);
|
|
68
|
+
const modern = () => { trace.push('modern'); };
|
|
69
|
+
interceptor.add('event.clearEdit', modern);
|
|
70
|
+
invoke();
|
|
71
|
+
assert.deepStrictEqual(trace.splice(0), ['modern', 'next']);
|
|
72
|
+
// Actual Element dropdown DOM receives mousedown/click while modern callbacks exist.
|
|
73
|
+
await grid.setActiveCell(row, 'choice'); await wait();
|
|
74
|
+
vm.$el.querySelector('.el-select input').dispatchEvent(new window.MouseEvent('mousedown', { bubbles: true }));
|
|
75
|
+
vm.$el.querySelector('.el-select input').click(); await wait();
|
|
76
|
+
const beta = Array.from(document.querySelectorAll('.el-select-dropdown__item')).find(el => el.textContent.trim() === 'Beta');
|
|
77
|
+
assert(beta, 'actual Element option mounted');
|
|
78
|
+
vm.$el.querySelector('.el-select input').focus();
|
|
79
|
+
const focusedInput = document.activeElement;
|
|
80
|
+
validations = 0;
|
|
81
|
+
beta.dispatchEvent(new window.MouseEvent('mousedown', { bubbles: true }));
|
|
82
|
+
beta.click(); await wait();
|
|
83
|
+
assert.equal(row.choice, 'b');
|
|
84
|
+
assert.strictEqual(document.activeElement, focusedInput, 'validation activation does not steal DOM focus');
|
|
85
|
+
assert.equal(validations, 1, 'one real kernel validation per Element change');
|
|
86
|
+
assert(vm.$el.querySelector('.col--valid-error'), 'validator rejection marks invalid cell');
|
|
87
|
+
assert(table.getEditRecord(), 'popup preserves active edit with modern interceptor');
|
|
88
|
+
vm.$el.querySelector('.el-select .el-input').dispatchEvent(new window.MouseEvent('mouseenter', { bubbles: false }));
|
|
89
|
+
await wait();
|
|
90
|
+
const clear = vm.$el.querySelector('.el-select .el-icon-circle-close');
|
|
91
|
+
assert(clear, 'real Element clear control'); clear.click(); await wait();
|
|
92
|
+
assert.equal(row.choice, '');
|
|
93
|
+
assert.equal(validations, 2, 'clear triggers exactly one additional kernel validation');
|
|
94
|
+
assert(!vm.$el.querySelector('.col--valid-error'), 'clear removes rejected value validation state');
|
|
95
|
+
document.body.dispatchEvent(new window.MouseEvent('mousedown', { bubbles: true }));
|
|
96
|
+
await wait();
|
|
97
|
+
assert(!table.getEditRecord(), 'outside click closes editor');
|
|
98
|
+
interceptor.delete('event.clearEdit', modern);
|
|
99
|
+
trace.length = 0;
|
|
100
|
+
invoke(); assert.deepStrictEqual(trace.splice(0), ['first', 'second', 'next']);
|
|
101
|
+
// Simulate another separately bundled renderer module sharing the actual registry.
|
|
102
|
+
const source = fs.readFileSync(path.join(__dirname, '../src/element-renderer.js'), 'utf8').replace(/^import .*;$/gm, '');
|
|
103
|
+
const counts = ['event.clearEdit', 'event.clearActived', 'event.clearFilter'].map(name => interceptor.get(name).length);
|
|
104
|
+
require('vm').runInNewContext(source, { XEUtils: require('xe-utils'), renderer, interceptor, document });
|
|
105
|
+
assert.deepStrictEqual(['event.clearEdit', 'event.clearActived', 'event.clearFilter'].map(name => interceptor.get(name).length), counts);
|
|
106
|
+
invoke(); assert.deepStrictEqual(trace.splice(0), ['first', 'second', 'next']);
|
|
107
|
+
interceptor.delete('event.clearActived', first);
|
|
108
|
+
interceptor.delete('event.clearActived', second);
|
|
109
|
+
await grid.setActiveCell(row, 'cascade'); await wait();
|
|
110
|
+
vm.$el.querySelector('.el-cascader input').click(); await wait();
|
|
111
|
+
const root = Array.from(document.querySelectorAll('.el-cascader-node')).find(el => el.textContent.trim() === 'Root');
|
|
112
|
+
assert(root, 'top-level renderer options reach real Cascader');
|
|
113
|
+
root.click(); await wait();
|
|
114
|
+
const leaf = Array.from(document.querySelectorAll('.el-cascader-node')).find(el => el.textContent.trim() === 'Leaf');
|
|
115
|
+
assert(leaf); leaf.click(); await wait();
|
|
116
|
+
assert.deepStrictEqual(Array.from(row.cascade), ['root', 'leaf']);
|
|
117
|
+
await grid.clearActived(); await wait();
|
|
118
|
+
assert(vm.$el.textContent.includes('Root / Leaf'));
|
|
119
|
+
await grid.setActiveCell(row, 'lazy'); await wait();
|
|
120
|
+
vm.$el.querySelector('.el-cascader input').click(); await wait();
|
|
121
|
+
const parent = Array.from(document.querySelectorAll('.el-cascader-node')).find(el => el.textContent.trim() === 'Lazy Parent');
|
|
122
|
+
assert(parent); parent.click(); await wait();
|
|
123
|
+
table.$forceUpdate(); await wait();
|
|
124
|
+
const child = Array.from(document.querySelectorAll('.el-cascader-node')).find(el => el.textContent.trim() === 'Lazy Child');
|
|
125
|
+
assert(child); child.click(); await wait();
|
|
126
|
+
assert.deepStrictEqual(errors, []);
|
|
127
|
+
assert.deepStrictEqual(Array.from(row.lazy), ['parent', 'child']);
|
|
128
|
+
await grid.clearActived(); await wait();
|
|
129
|
+
assert(vm.$el.textContent.includes('Lazy Parent / Lazy Child'), 'lazy checked node label survives closing edit');
|
|
130
|
+
// Cache uses selected values and unformatted path labels; export shares it.
|
|
131
|
+
const lazyColumn = table.getColumnByField('lazy');
|
|
132
|
+
const lazyParams = { row, column: lazyColumn };
|
|
133
|
+
const lazyRenderer = renderer.get('ElCascader');
|
|
134
|
+
assert.equal(lazyRenderer.tableExportMethod(lazyParams), 'Lazy Parent / Lazy Child');
|
|
135
|
+
const lazyOptions = lazyColumn.editRender;
|
|
136
|
+
lazyOptions.props.options[0].label = 'Renamed Parent';
|
|
137
|
+
assert.equal(lazyRenderer.renderTableCell(null, lazyOptions, lazyParams)[0], 'Renamed Parent / Lazy Child', 'latest static prefix wins over lazy cache');
|
|
138
|
+
lazyOptions.props.separator = '>';
|
|
139
|
+
assert.equal(lazyRenderer.renderTableCell(null, lazyOptions, lazyParams)[0], 'Renamed Parent > Lazy Child');
|
|
140
|
+
lazyOptions.props.showAllLevels = false;
|
|
141
|
+
assert.equal(lazyRenderer.renderTableCell(null, lazyOptions, lazyParams)[0], 'Lazy Child');
|
|
142
|
+
await grid.setActiveCell(row, 'lazy'); await wait();
|
|
143
|
+
assert.equal(vm.$el.querySelector('.el-cascader input').value, 'Lazy Child', 'lazy selection restored after editor remount');
|
|
144
|
+
vm.$el.querySelector('.el-cascader').dispatchEvent(new window.MouseEvent('mouseenter', { bubbles: false }));
|
|
145
|
+
await wait();
|
|
146
|
+
const lazyClear = vm.$el.querySelector('.el-cascader .el-icon-circle-close');
|
|
147
|
+
assert(lazyClear, 'restored lazy selection has a real clear control');
|
|
148
|
+
lazyClear.click(); await wait();
|
|
149
|
+
assert.deepStrictEqual(Array.from(row.lazy), [], 'real clear updates lazy model after remount');
|
|
150
|
+
vm.$el.querySelector('.el-cascader input').click(); await wait();
|
|
151
|
+
Array.from(document.querySelectorAll('.el-cascader-node')).find(el => el.textContent.trim() === 'Renamed Parent').click(); await wait();
|
|
152
|
+
Array.from(document.querySelectorAll('.el-cascader-node')).find(el => el.textContent.trim() === 'Lazy Child').click(); await wait();
|
|
153
|
+
assert.deepStrictEqual(Array.from(row.lazy), ['parent', 'child'], 'restored lazy children remain selectable after clear');
|
|
154
|
+
await grid.clearActived(); await wait();
|
|
155
|
+
lazyOptions.props.options = [{ value: 'parent', label: 'Replacement Parent' }];
|
|
156
|
+
assert.equal(lazyRenderer.renderTableCell(null, lazyOptions, lazyParams)[0], 'Replacement Parent', 'replaced option identity cannot reuse obsolete lazy labels');
|
|
157
|
+
lazyOptions.props.options = [{ value: 'parent', label: 'New Parent', children: [{ value: 'child', label: 'New Child' }] }];
|
|
158
|
+
assert.equal(lazyRenderer.renderTableCell(null, lazyOptions, lazyParams)[0], 'New Child', 'fresh complete options win over cached labels');
|
|
159
|
+
row.lazy = [];
|
|
160
|
+
assert.equal(lazyRenderer.renderTableCell(null, lazyOptions, lazyParams)[0], '', 'empty selection cannot resurrect cached label');
|
|
161
|
+
const display = (name, value, props) => renderer.get(name).renderTableCell(null, { name, props }, { row: { value }, column: { field: 'value' } })[0];
|
|
162
|
+
assert.equal(display('ElTimePicker', '14:24:56', { valueFormat: 'HH:mm:ss' }), '14:24:56');
|
|
163
|
+
assert.equal(display('ElTimePicker', ['14:24:56', '18:25:57'], { valueFormat: 'HH:mm:ss', rangeSeparator: '' }), '14:24:56 18:25:57');
|
|
164
|
+
assert.equal(display('ElTimePicker', '14:24', { valueFormat: 'HH:mm', format: 'HH:mm' }), '14:24');
|
|
165
|
+
assert.equal(display('ElTimePicker', '02:24:56 PM', { valueFormat: 'hh:mm:ss A' }), '14:24:56');
|
|
166
|
+
assert.equal(display('ElTimePicker', '12:24:56 AM', { valueFormat: 'hh:mm:ss A' }), '00:24:56');
|
|
167
|
+
assert.equal(display('ElTimePicker', '12:24:56 PM', { valueFormat: 'hh:mm:ss A' }), '12:24:56');
|
|
168
|
+
assert.equal(display('ElTimePicker', 'PM 02:24:56', { valueFormat: 'A hh:mm:ss' }), '14:24:56');
|
|
169
|
+
assert.equal(display('ElTimePicker', '24:56 02 PM', { valueFormat: 'mm:ss hh A' }), '14:24:56');
|
|
170
|
+
assert.equal(display('ElTimePicker', '02:24:56 PM', { valueFormat: 'hh:mm:ss A', format: 'hh:mm:ss A' }), '02:24:56 PM');
|
|
171
|
+
const stamp = String(new Date(2026, 8, 17, 14, 24, 56).getTime());
|
|
172
|
+
assert.equal(display('ElDatePicker', stamp, { valueFormat: 'timestamp' }), '2026-09-17');
|
|
173
|
+
assert.equal(display('ElTimePicker', stamp, { valueFormat: 'timestamp' }), '14:24:56');
|
|
174
|
+
assert.equal(display('ElDatePicker', ['2026-09-01', '2026-09-17'], { type: 'daterange', valueFormat: 'yyyy-MM-dd', rangeSeparator: '' }), '2026-09-01 2026-09-17');
|
|
175
|
+
for (const value of [null, undefined, '']) assert.equal(display('ElTimePicker', value, {}), '');
|
|
176
|
+
const dateFilter = renderer.get('ElDatePicker').tableFilterDefaultMethod;
|
|
177
|
+
const match = value => dateFilter({ option: { data: ['2026-09-01', '2026-09-17'] }, row: { value }, column: { field: 'value', filterRender: { props: { type: 'dates', valueFormat: 'yyyy-MM-dd' } } } });
|
|
178
|
+
assert(match('2026-09-01')); assert(match('2026-09-17')); assert(!match('2026-09-10')); assert(!match(null));
|
|
179
|
+
const filter = (name, value, data, props) => renderer.get(name).tableFilterDefaultMethod({ option: { data }, row: { value }, column: { field: 'value', filterRender: { props } } });
|
|
180
|
+
assert(filter('ElDatePicker', new Date(2026, 8, 17, 21), new Date(2026, 8, 17, 8), { type: 'date' }), 'date filter ignores time on Date models');
|
|
181
|
+
assert(filter('ElDatePicker', '17/09/2026', ['01/09/2026', '30/09/2026'], { type: 'daterange', valueFormat: 'dd/MM/yyyy' }), 'custom formatted range compares chronological dates');
|
|
182
|
+
assert(!filter('ElDatePicker', '01/10/2026', ['01/09/2026', '30/09/2026'], { type: 'daterange', valueFormat: 'dd/MM/yyyy' }));
|
|
183
|
+
assert(filter('ElDatePicker', '2026-09-17 14:25:00', ['2026-09-17 14:24:59', '2026-09-17 14:25:00'], { type: 'datetimerange', valueFormat: 'yyyy-MM-dd HH:mm:ss' }));
|
|
184
|
+
assert(!filter('ElDatePicker', '2026-09-17 14:25:01', ['2026-09-17 14:24:59', '2026-09-17 14:25:00'], { type: 'datetimerange', valueFormat: 'yyyy-MM-dd HH:mm:ss' }));
|
|
185
|
+
assert(filter('ElDatePicker', '2026-09', ['2026-08', '2026-10'], { type: 'monthrange', valueFormat: 'yyyy-MM' }));
|
|
186
|
+
for (const invalid of [null, undefined, '', 'invalid']) {
|
|
187
|
+
assert(!filter('ElDatePicker', invalid, invalid, { type: 'date' }), 'invalid date cannot equal invalid date');
|
|
188
|
+
assert(!filter('ElTimePicker', invalid, invalid, { valueFormat: 'HH:mm:ss' }), 'invalid time cannot equal invalid time');
|
|
189
|
+
assert(!filter('ElTimePicker', '14:24:56', [invalid, '18:00:00'], { isRange: true, valueFormat: 'HH:mm:ss' }));
|
|
190
|
+
}
|
|
191
|
+
assert(filter('ElTimePicker', '02:24:56 PM', ['01:00:00 PM', '02:24:56 PM'], { isRange: true, valueFormat: 'hh:mm:ss A' }));
|
|
192
|
+
assert(!filter('ElTimePicker', '02:24:57 PM', ['01:00:00 PM', '02:24:56 PM'], { isRange: true, valueFormat: 'hh:mm:ss A' }));
|
|
193
|
+
assert(!filter('ElTimePicker', '14:24:56', ['18:00:00', '13:00:00'], { isRange: true, valueFormat: 'HH:mm:ss' }));
|
|
194
|
+
assert.deepStrictEqual(errors, []);
|
|
195
|
+
console.log('renderer boundaries passed: dynamic real registry, veto/priority/reload, Element Select/Cascader DOM, date/time formats and dates filter');
|
|
196
|
+
})().catch(error => { console.error(error); process.exitCode = 1; }).finally(() => { if (vm) vm.$destroy(); dom.window.close(); });
|