umy-ui-table 1.2.5 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "umy-ui-table",
3
- "version": "1.2.5",
3
+ "version": "1.2.6",
4
4
  "description": "一个基于 vue 的 PC 端表格UI库,解决万级数据渲染卡顿问题,过万数据点击全选卡顿,等等问题",
5
5
  "author": {
6
6
  "name": "foriacus"
@@ -304,6 +304,17 @@ function sameCascaderIdentity(left, right) {
304
304
  return left.length === right.length && left.every((value, index) => value === right[index]);
305
305
  }
306
306
 
307
+ function getCascaderRenderIdentity(options, config, identity = []) {
308
+ if (!Array.isArray(options)) return identity;
309
+ identity.push(options, options.length);
310
+ options.forEach((option) => {
311
+ identity.push(option);
312
+ Object.keys(option).forEach((key) => identity.push(key, option[key]));
313
+ getCascaderRenderIdentity(option[config.children || "children"], config, identity);
314
+ });
315
+ return identity;
316
+ }
317
+
307
318
  function getCascaderCache(params, create, renderOpts) {
308
319
  if (!params || !params.row || !params.column) return;
309
320
  let columns = cascaderLabelCache.get(params.row);
@@ -430,7 +441,21 @@ function cascaderEditRender(h, renderOpts, params) {
430
441
  // hook runs in that same mount turn, before asynchronous results can arrive.
431
442
  const hooks = nodes[0].data.hook || {};
432
443
  const onInsert = hooks.insert;
444
+ const onPrepatch = hooks.prepatch;
445
+ nodes[0].data.umyCascaderCache = cache;
446
+ nodes[0].data.umyCascaderOptionsIdentity = getCascaderRenderIdentity(props.options || renderOpts.options, config);
433
447
  nodes[0].data.hook = Object.assign({}, hooks, {
448
+ prepatch(oldVnode, vnode) {
449
+ // Hydration is for a new editor. Feeding newly loaded children back as
450
+ // options into the same live Element panel resets its store/expanded
451
+ // menus on an unrelated table render (e.g. popup mousedown).
452
+ // Genuine external option edits must still reach the panel unchanged.
453
+ if (oldVnode.data.umyCascaderCache === cache &&
454
+ sameCascaderIdentity(oldVnode.data.umyCascaderOptionsIdentity || [], vnode.data.umyCascaderOptionsIdentity)) {
455
+ vnode.componentOptions.propsData.options = oldVnode.componentOptions.propsData.options;
456
+ }
457
+ if (onPrepatch) onPrepatch.apply(this, arguments);
458
+ },
434
459
  insert(...args) {
435
460
  if (onInsert) onInsert.apply(this, args);
436
461
  pendingMountRequests.forEach((request) => {
@@ -54,12 +54,22 @@ export function observeGridHeight(vm) {
54
54
  // Pagination can appear/disappear after mount; updated() reconnects when its
55
55
  // element identity changes. Root observation also covers hidden -> visible.
56
56
  const pager = vm.$refs.myPagination;
57
- if (pager) observer.observe(pager);
57
+ if (pager) observer.observe(pager, { box: "border-box" });
58
58
  const view = root.ownerDocument.defaultView;
59
+ // Native ResizeObserver supports border-box; older polyfills ignore it.
60
+ // Padding/border-only changes still change offsetHeight in that fallback.
61
+ const pagerObserver = pager && view && view.MutationObserver
62
+ ? new view.MutationObserver(() => vm.scheduleHeight())
63
+ : null;
64
+ if (pagerObserver) pagerObserver.observe(pager, {
65
+ attributes: true, attributeFilter: ["style", "class"], childList: true,
66
+ characterData: true, subtree: true
67
+ });
59
68
  const onResize = () => vm.scheduleHeight();
60
69
  if (view) view.addEventListener("resize", onResize);
61
70
  return () => {
62
71
  observer.disconnect();
72
+ if (pagerObserver) pagerObserver.disconnect();
63
73
  if (view) view.removeEventListener("resize", onResize);
64
74
  };
65
75
  }
@@ -0,0 +1,105 @@
1
+ // Real Chrome geometry, not jsdom. Uses only installed dependencies, isolated assets/profile.
2
+ const fs = require('fs'), os = require('os'), path = require('path'), http = require('http');
3
+ const { spawn } = require('child_process');
4
+ const assert = require('assert');
5
+ const webpack = require('webpack'), WebSocket = require('ws');
6
+ const VueLoaderPlugin = require('vue-loader/lib/plugin');
7
+ const root = path.resolve(__dirname, '../../..');
8
+ const consumer = process.env.UX_GRID_TEST_DEPENDENCIES;
9
+ if (!consumer) throw Error('Set UX_GRID_TEST_DEPENDENCIES');
10
+ const output = fs.mkdtempSync(path.join(os.tmpdir(), 'ux-grid-layout-browser-'));
11
+ const results = {};
12
+ results.observerMode = process.env.UX_GRID_LAYOUT_POLYFILL === '1' ? 'polyfill' : 'native';
13
+ results.manifestVersion = require('vxe-table/package.json').version;
14
+ let chrome, server, ws, nextId = 0;
15
+ const pending = new Map();
16
+ function call(method, params = {}, sessionId) {
17
+ return new Promise((resolve, reject) => {
18
+ const id = ++nextId, timer = setTimeout(() => { pending.delete(id); reject(Error('Timeout: ' + method)); }, 60000);
19
+ pending.set(id, { resolve: result => { clearTimeout(timer); resolve(result); }, reject: error => { clearTimeout(timer); reject(error); } });
20
+ ws.send(JSON.stringify({ id, method, params, sessionId }));
21
+ });
22
+ }
23
+ function check(m, limit, exact = true) {
24
+ assert.deepStrictEqual(m.errors, []);
25
+ assert(m.table.height > 0 && m.body.height > 0, 'actual table/body visible');
26
+ const end = m.pager ? m.pager.bottom : m.table.bottom;
27
+ if (m.pager) assert(m.table.bottom <= m.pager.top + 1, 'pager overlaps table');
28
+ assert(end <= m.root.top + limit + 1, 'height budget exceeded: ' + JSON.stringify(m));
29
+ if (exact) assert(Math.abs(m.root.height - limit) <= 1, 'root height differs');
30
+ if (exact) assert(Math.abs(end - (m.root.bottom - m.bottomInset)) <= 1, 'unused height budget');
31
+ assert(m.calls < 30, 'unbounded observer callbacks');
32
+ }
33
+ (async () => {
34
+ await new Promise((resolve, reject) => webpack({ mode:'development', devtool:false,
35
+ entry:path.join(__dirname, 'browser-layout-entry.js'), output:{ path:output, filename:'bundle.js' },
36
+ resolve:{ extensions:['.js','.vue','.json'], alias:{ 'vue$':require.resolve('vue/dist/vue.runtime.esm.js'), 'element-ui':path.join(consumer,'node_modules/element-ui'), 'umy-ui':root }, modules:[path.join(root,'node_modules'),path.join(consumer,'node_modules')] },
37
+ module:{ rules:[{test:/\.vue$/,loader:'vue-loader'}, {test:/\.js$/,exclude:/node_modules/,loader:'babel-loader'}, {test:/\.css$/,use:['style-loader','css-loader']}, {test:/\.(woff|ttf)$/,loader:'file-loader'}] },
38
+ plugins:[new VueLoaderPlugin()], performance:{hints:false}
39
+ }, (error, stats) => error || stats.hasErrors() ? reject(error || Error(stats.toString({all:false, errors:true}))) : resolve()));
40
+ server = http.createServer((request, response) => {
41
+ const asset = path.join(output, path.basename(request.url));
42
+ if (fs.existsSync(asset) && fs.statSync(asset).isFile()) { response.setHeader('Content-Type', request.url.endsWith('.js') ? 'application/javascript; charset=utf-8' : 'application/octet-stream'); fs.createReadStream(asset).pipe(response); }
43
+ else { response.setHeader('Content-Type','text/html; charset=utf-8'); response.end('<!doctype html><html><head><meta charset="utf-8"></head><body><script>' + (results.observerMode === 'polyfill' ? 'window.ResizeObserver=undefined;' : '') + 'window.bootErrors=[];window.onerror=function(m,u,l,c,e){bootErrors.push(String(e&&e.stack||m))}</script><script src="/bundle.js"></script></body></html>'); }
44
+ });
45
+ await new Promise((resolve, reject) => { server.once('error', reject); server.listen(0,'127.0.0.1',resolve); });
46
+ const endpoint = await new Promise((resolve, reject) => {
47
+ chrome = spawn(process.env.UX_GRID_TEST_CHROME || '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', ['--headless=new','--disable-gpu','--no-first-run','--no-default-browser-check','--remote-debugging-port=0','--user-data-dir=' + path.join(output,'profile'),'about:blank']);
48
+ chrome.on('error',reject); chrome.on('exit',code => reject(Error('Chrome exited: ' + code)));
49
+ chrome.stderr.on('data',data => { const m = String(data).match(/DevTools listening on (ws:\/\/[^\s]+)/); if(m) resolve(m[1]); });
50
+ });
51
+ ws = new WebSocket(endpoint); await new Promise(resolve => ws.on('open',resolve));
52
+ ws.on('message',message => { const data = JSON.parse(message), entry = pending.get(data.id); if(entry) { pending.delete(data.id); data.error ? entry.reject(Error(JSON.stringify(data.error))) : entry.resolve(data.result); } });
53
+ const {targetId} = await call('Target.createTarget',{url:'about:blank'});
54
+ const {sessionId} = await call('Target.attachToTarget',{targetId,flatten:true});
55
+ await call('Emulation.setDeviceMetricsOverride',{width:1200,height:900,deviceScaleFactor:1,mobile:false},sessionId);
56
+ const evaluate = async expression => { const r = await call('Runtime.evaluate',{expression,awaitPromise:true,returnByValue:true},sessionId); if(r.exceptionDetails) throw Error(JSON.stringify(r.exceptionDetails)); return r.result.value; };
57
+ await call('Page.navigate',{url:'http://127.0.0.1:' + server.address().port},sessionId);
58
+ for(let i=0;i<100;i++) { if(await evaluate('Boolean(window.layout)')) break; await new Promise(resolve=>setTimeout(resolve,100)); }
59
+ results.boot = await evaluate('window.bootErrors'); console.log('boot',results.boot);
60
+ assert.deepStrictEqual(results.boot, []);
61
+ for(const [name,config,limit,exact] of [
62
+ ['numeric',{height:400},400,true], ['px',{height:'400px'},400,true],
63
+ ['percent',{height:'80%'},400,true], ['auto',{height:'auto'},500,true],
64
+ ['calc',{height:'calc(100% - 100px)'},400,true],
65
+ ['border-box',{height:400,rootStyle:{boxSizing:'border-box',padding:'10px',border:'2px solid red'}},400,true],
66
+ ['flex',{height:'100%',parent:{display:'flex',flexDirection:'column'}},500,true],
67
+ ['grid',{height:'100%',parent:{display:'grid',gridTemplateRows:'500px'}},500,true],
68
+ ['flex-header',{height:'auto',parent:{display:'flex',flexDirection:'column'},sibling:{height:'100px',flex:'none'},rootStyle:{flex:'1',minHeight:0}},400,true],
69
+ ['flex-two-grow',{height:'50%',parent:{display:'flex',flexDirection:'column'},sibling:{height:'50%',flex:'1',minHeight:0},rootStyle:{flex:'1',minHeight:0}},250,true],
70
+ ['grid-second-track',{height:'100%',parent:{display:'grid',gridTemplateRows:'100px 1fr'},sibling:{},rootStyle:{minHeight:0}},400,true],
71
+ ['grid-second-track-half',{height:'50%',parent:{display:'grid',gridTemplateRows:'100px 1fr'},sibling:{},rootStyle:{minHeight:0}},200,true],
72
+ ['max-many',{maxHeight:'80%'},400,false], ['max-few',{maxHeight:'80%',rows:1},400,false],
73
+ ['max-calc',{maxHeight:'calc(100% - 100px)'},400,false], ['no-pager',{height:400,pager:false},400,true]
74
+ ]) { const m = results[name] = await evaluate('layout.mount(' + JSON.stringify(config) + ')'); check(m,limit,exact); console.log('PASS ' + name); }
75
+ assert(results['max-few'].root.height < results['max-many'].root.height - 100, 'maxHeight preserves short content natural height');
76
+ results.priority = await evaluate('layout.mount({height:400,maxHeight:200})'); check(results.priority,400); assert.strictEqual(results.priority.maxHeight,null);
77
+ results.fallbackMax = await evaluate('layout.patch({height:null})'); check(results.fallbackMax,200,false); assert.strictEqual(results.fallbackMax.maxHeight,200-results.fallbackMax.pager.height);
78
+ results.resizeMount = await evaluate('layout.mount({height:"80%"})');
79
+ results.resize = await evaluate('layout.patch({parent:{height:"600px"}})'); check(results.resize,480);
80
+ results.pagerOff = await evaluate('layout.patch({pager:false})'); check(results.pagerOff,480);
81
+ results.pagerOn = await evaluate('layout.patch({pager:true})'); check(results.pagerOn,480);
82
+ results.pagerSize = await evaluate('layout.pagerSize()'); check(results.pagerSize,480);
83
+ assert(results.pagerSize.pager.height > results.pagerOn.pager.height && results.pagerSize.table.height < results.pagerOn.table.height);
84
+ results.pagerShrink = await evaluate('layout.pagerSize(0)'); check(results.pagerShrink,480); assert.strictEqual(results.pagerShrink.table.height,results.pagerOn.table.height);
85
+ if (results.observerMode === 'native') {
86
+ results.parentTheme = await evaluate('layout.parentTheme(true)'); check(results.parentTheme,480); assert(results.parentTheme.pager.height > results.pagerOn.pager.height);
87
+ results.parentThemeReset = await evaluate('layout.parentTheme(false)'); check(results.parentThemeReset,480);
88
+ }
89
+ await evaluate('layout.hide(true)'); results.show = await evaluate('layout.hide(false)'); check(results.show,480);
90
+ results.deactivated = await evaluate('layout.active(false)'); assert(!results.deactivated.observing);
91
+ results.inactiveMutation = await evaluate('layout.inactiveMutation()'); assert.strictEqual(results.inactiveMutation,0);
92
+ results.activated = await evaluate('layout.active(true)'); check(results.activated,480);
93
+ results.stable = await evaluate('layout.stable()'); assert(results.stable.delta <= 2);
94
+ results.clearHeight = await evaluate('layout.patch({height:null})'); assert.strictEqual(results.clearHeight.height,null); assert(!results.clearHeight.observing);
95
+ results.destroyed = await evaluate('layout.destroy()'); assert.deepStrictEqual(results.destroyed,{delta:0,scheduleDelta:0,observing:false});
96
+ results.natural = await evaluate('layout.mount({height:"100%",rows:2,parent:{height:"auto"}})'); assert.strictEqual(results.natural.height,null); assert(results.natural.observing);
97
+ results.naturalStable = await evaluate('layout.stable()'); assert(results.naturalStable.delta <= 2);
98
+ results.naturalDestroyed = await evaluate('layout.destroy()'); assert.deepStrictEqual(results.naturalDestroyed,{delta:0,scheduleDelta:0,observing:false});
99
+ results.finalErrors = await evaluate('({boot:window.bootErrors,vue:window.layoutErrors})'); assert.deepStrictEqual(results.finalErrors,{boot:[],vue:[]});
100
+ console.log('PASS lifecycle/resize/pager/stability/destruction');
101
+ })().catch(error => { results.failure = error.stack || String(error); console.error(error); process.exitCode=1; }).finally(() => {
102
+ fs.writeFileSync(path.join(output,'results.json'),JSON.stringify(results,null,2)); console.log('Result directory: ' + output);
103
+ if(ws) ws.close(); if(chrome) chrome.kill(); if(server) server.close();
104
+ setTimeout(()=>process.exit(process.exitCode || 0),500);
105
+ });
@@ -0,0 +1,66 @@
1
+ import 'babel-polyfill';
2
+ import Vue from 'vue';
3
+ import Element from 'element-ui';
4
+ import 'element-ui/lib/theme-chalk/index.css';
5
+ import Grid from '../src/ux-grid.vue';
6
+ import { version as runtimeVersion } from 'vxe-table/lib/ui';
7
+ import '../../../lib/theme-chalk/ux-grid.css';
8
+ Vue.use(Element);
9
+ window.layoutErrors = [];
10
+ Vue.config.errorHandler = error => layoutErrors.push(String(error.stack || error));
11
+ const theme = document.createElement('style');
12
+ theme.textContent = '.layout-padded-pager .myPagination { padding-block: 12px !important; }';
13
+ document.head.appendChild(theme);
14
+ const wait = (ms = 350) => new Promise(resolve => setTimeout(resolve, ms));
15
+ const rect = el => {
16
+ if (!el) return null;
17
+ const r = el.getBoundingClientRect();
18
+ return { top: r.top, bottom: r.bottom, height: r.height, width: r.width };
19
+ };
20
+ let vm, wrapper, calls, schedules;
21
+ window.layout = {
22
+ async mount(config) {
23
+ if (vm) { vm.$destroy(); vm.$el.remove(); }
24
+ calls = 0;
25
+ schedules = 0;
26
+ vm = new Vue({
27
+ data: () => ({ config, active: true, hidden: false }),
28
+ render(h) {
29
+ const c = this.config;
30
+ return h('div', { style: Object.assign({ width: '800px', height: '500px' }, c.parent, this.hidden ? { display: 'none' } : {}) }, [
31
+ c.sibling ? h('div', { style: c.sibling }, ['Sibling']) : null,
32
+ h('keep-alive', [this.active ? h(Grid, { ref: 'grid', style: c.rootStyle || {}, props: {
33
+ data: Array.from({ length: c.rows === undefined ? 100 : c.rows }, (_, i) => ({ id: i, name: 'Row ' + i })),
34
+ columns: [{ field: 'name', title: 'Name', width: 160 }], height: c.height, maxHeight: c.maxHeight,
35
+ paginationShow: c.pager !== false, total: 100, pageSize: 10
36
+ } }) : h('div')])
37
+ ]);
38
+ }
39
+ }).$mount();
40
+ document.body.appendChild(vm.$el);
41
+ wrapper = vm.$refs.grid;
42
+ const original = wrapper.setHeight;
43
+ wrapper.setHeight = function () { calls++; return original.apply(this, arguments); };
44
+ const schedule = wrapper.scheduleHeight;
45
+ wrapper.scheduleHeight = function () { schedules++; return schedule.apply(this, arguments); };
46
+ await wait();
47
+ return this.measure();
48
+ },
49
+ measure() {
50
+ const root = wrapper.$el;
51
+ const style = getComputedStyle(root);
52
+ return { root: rect(root), parent: rect(vm.$el), table: rect(wrapper.$refs.singleTable.$el),
53
+ body: rect(root.querySelector('.vxe-table--main-wrapper .vxe-table--body-wrapper')),
54
+ pager: rect(wrapper.$refs.myPagination), height: wrapper.newHeight, maxHeight: wrapper.newMaxHeight,
55
+ bottomInset: parseFloat(style.paddingBottom) + parseFloat(style.borderBottomWidth),
56
+ runtimeVersion, calls, observing: !!wrapper._stopHeightObserver, errors: layoutErrors.slice() };
57
+ },
58
+ async patch(config) { vm.config = Object.assign({}, vm.config, config); await wait(); return this.measure(); },
59
+ async hide(value) { vm.hidden = value; await wait(); return this.measure(); },
60
+ async active(value) { vm.active = value; await wait(); return this.measure(); },
61
+ async stable() { const before = calls; await wait(650); return { delta: calls - before, measurement: this.measure() }; },
62
+ async pagerSize(size = 20) { wrapper.$refs.myPagination.style.padding = size + 'px 0'; await wait(); return this.measure(); },
63
+ async parentTheme(value) { vm.$el.classList.toggle('layout-padded-pager', value); await wait(); return this.measure(); },
64
+ async inactiveMutation() { const before = schedules; wrapper.$refs.myPagination.style.padding = '10px'; await wait(); return schedules - before; },
65
+ async destroy() { const root = vm.$el, pager = wrapper.$refs.myPagination; vm.$destroy(); root.remove(); const before = calls, beforeSchedules = schedules; if (pager) pager.style.padding = '15px'; window.dispatchEvent(new Event('resize')); await wait(); return { delta: calls - before, scheduleDelta: schedules - beforeSchedules, observing: !!wrapper._stopHeightObserver }; }
66
+ };
@@ -168,12 +168,16 @@ function call(method, params = {}, sessionId) {
168
168
  const label={select:'Beta',remote:'Beta',multiple:'Zero, False',cascader:'Parent / Child',cascaderLazy:'Parent / Child',cascaderMultiple:'Parent / Child, Parent / Other',date:'2026-09-19',dateObject:'2026-09-19',datetime:'2026-09-19 13:24:56',dateRange:'2026-09-20 - 2026-09-22',time:'14:24:56',timeObject:'14:24:56',timeAmPm:'02:24:56 PM',timeRange:'12:24:56 - 14:25:57'}[kind];
169
169
  assert(suite.display.text.includes(label),kind+' closed display retains correct label/time format');
170
170
  }
171
- if(kind==='cascaderLazy') {
171
+ if(kind==='cascaderLazy' && version==='legacy') {
172
172
  assert.deepStrictEqual(suite.display.errors,[]);
173
173
  console.log(version+' '+kind+' selection/display passed');
174
- continue; // Lazy node rehydration on editor remount is a separate Element concern.
174
+ continue; // Legacy has no remount cache; current must verify its added restoration.
175
175
  }
176
176
  await evaluate('rendererTest.activate()');
177
+ if(kind==='cascaderLazy') {
178
+ const restored = await evaluate("document.querySelector('tbody .el-cascader input').value");
179
+ assert.equal(restored,'Parent / Child','lazy editor restores selected path on remount');
180
+ }
177
181
  if(kind==='multiple') {
178
182
  await evaluate('rendererTest.resetCallbacks()');
179
183
  await click('tbody .el-tag__close');
@@ -201,6 +205,18 @@ function call(method, params = {}, sessionId) {
201
205
  assert.equal(suite.reselected.callbacks.filter(e=>e.type==='change').length,1);
202
206
  assert.equal(suite.reselected.validationCalls.length,1);
203
207
  }
208
+ if(kind==='cascaderLazy') {
209
+ await evaluate('rendererTest.resetCallbacks()');
210
+ if(!await evaluate("rendererTest.hasVisible('.el-cascader__dropdown')")) await click('tbody .el-input__inner');
211
+ await click('.el-cascader-node__label','Parent');
212
+ await click('.el-cascader-node__label','Child');
213
+ suite.reselected=await evaluate('rendererTest.state()');persist();
214
+ assert.deepStrictEqual(suite.reselected.value,['p','c']);
215
+ assert(suite.reselected.active);
216
+ assert.equal(suite.reselected.callbacks.filter(e=>e.type==='change').length,1);
217
+ assert.equal(suite.reselected.validationCalls.length,1);
218
+ assert.equal(suite.reselected.lazyCalls,suite.selected.lazyCalls,'remount reuses loaded children without refetch');
219
+ }
204
220
  await outside();
205
221
  suite.outside=await evaluate('rendererTest.state()');persist();
206
222
  assert(!suite.outside.active,kind+' outside click closes edit');
@@ -0,0 +1,48 @@
1
+ // Run after npm run lib. Exercise the actual script-tag UMD product, without
2
+ // CommonJS/AMD globals or a bundler hiding missing browser runtime dependencies.
3
+ const assert = require('assert');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const root = path.resolve(__dirname, '../../..');
7
+ const dependencyRoot = process.env.UX_GRID_TEST_DEPENDENCIES || root;
8
+ const { JSDOM, VirtualConsole } = require(require.resolve('jsdom', { paths: [dependencyRoot] }));
9
+ const errors = [];
10
+ const virtualConsole = new VirtualConsole();
11
+ virtualConsole.on('jsdomError', error => errors.push(error));
12
+ const dom = new JSDOM('<html><body><script src="/lib/index.js"></script></body></html>', {
13
+ url: 'http://localhost/', runScripts: 'outside-only', pretendToBeVisual: true, virtualConsole
14
+ });
15
+ const { window } = dom;
16
+ let vm;
17
+ (async () => {
18
+ window.eval(fs.readFileSync(require.resolve('vue/dist/vue.js'), 'utf8'));
19
+ window.Vue.config.productionTip = false;
20
+ window.Vue.config.devtools = false;
21
+ window.Vue.config.errorHandler = error => errors.push(error);
22
+ window.eval(fs.readFileSync(path.join(root, 'lib/index.js'), 'utf8'));
23
+ const product = window.UMYUI;
24
+ assert(product && product.UxGrid && product.UxTableColumn && product.interceptor);
25
+ assert.strictEqual(product.version, require(path.join(root, 'package.json')).version);
26
+ assert(window.Vue.options.components.UxGrid, 'script tag automatically installs UxGrid');
27
+ vm = new window.Vue({
28
+ render(h) { return h(product.UxGrid, { props: {
29
+ data: [{ name: 'umd-row' }], columns: [{ field: 'name' }], height: 240,
30
+ paginationShow: true, total: 42, layout: 'total'
31
+ } }); }
32
+ }).$mount();
33
+ window.document.body.appendChild(vm.$el);
34
+ await new Promise(resolve => setTimeout(resolve, 150));
35
+ assert(vm.$el.textContent.includes('umd-row'), 'UMD product mounts real VXE row');
36
+ window.eval(fs.readFileSync(path.join(root, 'lib/umd/locale/en.js'), 'utf8'));
37
+ assert(product.lang && product.lang.en, 'UMD locale script registers language');
38
+ product.locale(product.lang.en);
39
+ const forceTree = node => { node.$forceUpdate(); node.$children.forEach(forceTree); };
40
+ forceTree(vm);
41
+ await new Promise(resolve => setTimeout(resolve, 80));
42
+ assert(vm.$el.textContent.includes('Total 42'), 'UMD locale changes pagination labels');
43
+ assert.deepStrictEqual(errors, []);
44
+ console.log('delivery UMD: browser-global product, auto-install, real rows and locale script passed');
45
+ })().catch(error => { console.error(error); process.exitCode = 1; }).finally(() => {
46
+ if (vm) vm.$destroy();
47
+ dom.window.close();
48
+ });
@@ -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(); });
@@ -37,7 +37,7 @@ let validations = 0;
37
37
  columns: [
38
38
  { field: 'choice', editRender: { name: 'ElSelect', options, props: { clearable: true } } },
39
39
  { field: 'cascade', editRender: { name: 'ElCascader', options: [{ value: 'root', label: 'Root', children: [{ value: 'leaf', label: 'Leaf' }] }] } },
40
- { field: 'lazy', editRender: { name: 'ElCascader', props: { options: [{ value: 'parent', label: 'Lazy Parent' }], props: { lazy: true, lazyLoad(node, resolve) { setTimeout(() => resolve([{ value: 'child', label: 'Lazy Child', leaf: true }]), 0); } } } } },
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
41
  { field: 'time', editRender: { name: 'ElTimePicker', props: { valueFormat: 'HH:mm:ss' } } }
42
42
  ]
43
43
  } }); } }).$mount();
@@ -120,6 +120,7 @@ let validations = 0;
120
120
  vm.$el.querySelector('.el-cascader input').click(); await wait();
121
121
  const parent = Array.from(document.querySelectorAll('.el-cascader-node')).find(el => el.textContent.trim() === 'Lazy Parent');
122
122
  assert(parent); parent.click(); await wait();
123
+ table.$forceUpdate(); await wait();
123
124
  const child = Array.from(document.querySelectorAll('.el-cascader-node')).find(el => el.textContent.trim() === 'Lazy Child');
124
125
  assert(child); child.click(); await wait();
125
126
  assert.deepStrictEqual(errors, []);
@@ -132,12 +133,27 @@ let validations = 0;
132
133
  const lazyRenderer = renderer.get('ElCascader');
133
134
  assert.equal(lazyRenderer.tableExportMethod(lazyParams), 'Lazy Parent / Lazy Child');
134
135
  const lazyOptions = lazyColumn.editRender;
135
- lazyOptions.props.options = [{ value: 'parent', label: 'Renamed Parent' }];
136
+ lazyOptions.props.options[0].label = 'Renamed Parent';
136
137
  assert.equal(lazyRenderer.renderTableCell(null, lazyOptions, lazyParams)[0], 'Renamed Parent / Lazy Child', 'latest static prefix wins over lazy cache');
137
138
  lazyOptions.props.separator = '>';
138
139
  assert.equal(lazyRenderer.renderTableCell(null, lazyOptions, lazyParams)[0], 'Renamed Parent > Lazy Child');
139
140
  lazyOptions.props.showAllLevels = false;
140
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');
141
157
  lazyOptions.props.options = [{ value: 'parent', label: 'New Parent', children: [{ value: 'child', label: 'New Child' }] }];
142
158
  assert.equal(lazyRenderer.renderTableCell(null, lazyOptions, lazyParams)[0], 'New Child', 'fresh complete options win over cached labels');
143
159
  row.lazy = [];
@@ -160,6 +176,21 @@ let validations = 0;
160
176
  const dateFilter = renderer.get('ElDatePicker').tableFilterDefaultMethod;
161
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' } } } });
162
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' }));
163
194
  assert.deepStrictEqual(errors, []);
164
195
  console.log('renderer boundaries passed: dynamic real registry, veto/priority/reload, Element Select/Cascader DOM, date/time formats and dates filter');
165
196
  })().catch(error => { console.error(error); process.exitCode = 1; }).finally(() => { if (vm) vm.$destroy(); dom.window.close(); });