This commit is contained in:
258
scripts/tests/tableHeaderDragLifecycle.test.mjs
Normal file
258
scripts/tests/tableHeaderDragLifecycle.test.mjs
Normal file
@@ -0,0 +1,258 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import ts from 'typescript'
|
||||
import * as vue from 'vue'
|
||||
import { compileScript, parse } from '@vue/compiler-sfc'
|
||||
|
||||
const { descriptor } = parse(readFileSync('src/components/core/tables/ArtTableHeader.vue', 'utf8'))
|
||||
const componentSource = compileScript(descriptor, {
|
||||
id: 'table-header-drag-test',
|
||||
inlineTemplate: true
|
||||
}).content
|
||||
|
||||
class HostElement {
|
||||
constructor(tag, text = '') {
|
||||
this.tag = tag
|
||||
this.text = text
|
||||
this.props = {}
|
||||
this.children = []
|
||||
this.parentNode = null
|
||||
}
|
||||
|
||||
get isConnected() {
|
||||
return this.tag === 'root' || Boolean(this.parentNode?.isConnected)
|
||||
}
|
||||
|
||||
removeChild(child) {
|
||||
const index = this.children.indexOf(child)
|
||||
assert.notEqual(index, -1)
|
||||
this.children.splice(index, 1)
|
||||
child.parentNode = null
|
||||
}
|
||||
}
|
||||
|
||||
function evaluate(source, imports) {
|
||||
const compiled = ts.transpileModule(source, {
|
||||
compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS }
|
||||
}).outputText
|
||||
const module = { exports: {} }
|
||||
new Function('require', 'module', 'exports', compiled)(
|
||||
(name) => {
|
||||
assert.ok(imports[name], `Unexpected dependency: ${name}`)
|
||||
return imports[name]
|
||||
},
|
||||
module,
|
||||
module.exports
|
||||
)
|
||||
return module.exports
|
||||
}
|
||||
|
||||
function mountHeader(t) {
|
||||
const previousGlobals = { HTMLElement: globalThis.HTMLElement, document: globalThis.document }
|
||||
globalThis.HTMLElement = HostElement
|
||||
globalThis.document = { addEventListener() {}, removeEventListener() {} }
|
||||
const controls = []
|
||||
const store = vue.reactive({
|
||||
tableSize: 'default',
|
||||
isZebra: false,
|
||||
isBorder: false,
|
||||
isHeaderBackground: false
|
||||
})
|
||||
const slotComponent = (name) =>
|
||||
vue.defineComponent({
|
||||
name,
|
||||
setup:
|
||||
(_, { slots, attrs }) =>
|
||||
() =>
|
||||
vue.h(name, attrs, [slots.reference?.(), slots.default?.(), slots.dropdown?.()])
|
||||
})
|
||||
const elements = Object.fromEntries(
|
||||
['ElPopover', 'ElCheckbox', 'ElDropdown', 'ElDropdownMenu', 'ElDropdownItem'].map((name) => [
|
||||
name,
|
||||
slotComponent(name)
|
||||
])
|
||||
)
|
||||
const component = evaluate(componentSource, {
|
||||
vue,
|
||||
pinia: { storeToRefs: (value) => vue.toRefs(value) },
|
||||
'element-plus': elements,
|
||||
'vue-i18n': { useI18n: () => ({ t: (key) => key }) },
|
||||
'@/enums/formEnum': { TableSizeEnum: { SMALL: 'small', DEFAULT: 'default', LARGE: 'large' } },
|
||||
'@/store/modules/table': { useTableStore: () => store },
|
||||
'vue-draggable-plus': {
|
||||
useDraggable: (target, model, options) => {
|
||||
const control = {
|
||||
target,
|
||||
model,
|
||||
options,
|
||||
starts: [],
|
||||
active: false,
|
||||
destroys: 0,
|
||||
start(element = target.value) {
|
||||
assert.ok(
|
||||
element instanceof HostElement && element.isConnected,
|
||||
'Sortable needs a connected native element'
|
||||
)
|
||||
this.starts.push(element)
|
||||
this.active = true
|
||||
},
|
||||
destroy() {
|
||||
this.destroys += 1
|
||||
this.active = false
|
||||
}
|
||||
}
|
||||
controls.push(control)
|
||||
vue.onMounted(() => {
|
||||
if (options.immediate !== false) control.start()
|
||||
})
|
||||
return control
|
||||
}
|
||||
}
|
||||
}).default
|
||||
const renderer = vue.createRenderer({
|
||||
createElement: (tag) => new HostElement(tag),
|
||||
createText: (text) => new HostElement('#text', text),
|
||||
createComment: (text) => new HostElement('#comment', text),
|
||||
setText: (node, text) => {
|
||||
node.text = text
|
||||
},
|
||||
setElementText: (node, text) => {
|
||||
node.text = text
|
||||
node.children = []
|
||||
},
|
||||
parentNode: (node) => node.parentNode,
|
||||
nextSibling: (node) => {
|
||||
const siblings = node.parentNode?.children || []
|
||||
return siblings[siblings.indexOf(node) + 1] || null
|
||||
},
|
||||
patchProp: (node, key, _, value) => {
|
||||
node.props[key] = value
|
||||
},
|
||||
insert: (node, parent, anchor) => {
|
||||
node.parentNode?.removeChild(node)
|
||||
const index = anchor ? parent.children.indexOf(anchor) : parent.children.length
|
||||
assert.notEqual(index, -1)
|
||||
parent.children.splice(index, 0, node)
|
||||
node.parentNode = parent
|
||||
},
|
||||
remove: (node) => node.parentNode?.removeChild(node)
|
||||
})
|
||||
const columns = vue.ref([
|
||||
{ prop: 'phone', label: '手机号', checked: true },
|
||||
{ prop: 'shop_name', label: '店铺', checked: false }
|
||||
])
|
||||
const active = vue.ref(true)
|
||||
const root = new HostElement('root')
|
||||
const app = renderer.createApp({
|
||||
render: () =>
|
||||
vue.h(vue.KeepAlive, null, {
|
||||
default: () =>
|
||||
active.value
|
||||
? vue.h(component, {
|
||||
columns: columns.value,
|
||||
'onUpdate:columns': (value) => {
|
||||
columns.value = value
|
||||
}
|
||||
})
|
||||
: vue.h('placeholder')
|
||||
})
|
||||
})
|
||||
for (const [name, element] of Object.entries(elements)) app.component(name, element)
|
||||
app.mount(root)
|
||||
let mounted = true
|
||||
const unmount = () => {
|
||||
if (mounted) {
|
||||
app.unmount()
|
||||
mounted = false
|
||||
}
|
||||
}
|
||||
t.after(() => {
|
||||
unmount()
|
||||
for (const [key, value] of Object.entries(previousGlobals)) {
|
||||
if (value === undefined) delete globalThis[key]
|
||||
else globalThis[key] = value
|
||||
}
|
||||
})
|
||||
const find = (node, tag) =>
|
||||
node.tag === tag ? node : node.children.map((child) => find(child, tag)).find(Boolean)
|
||||
return { control: controls[0], columns, active, unmount, popover: find(root, 'ElPopover') }
|
||||
}
|
||||
|
||||
const openPopover = (popover) => {
|
||||
popover.props['onUpdate:visible'](true)
|
||||
popover.props.onAfterEnter()
|
||||
}
|
||||
|
||||
test('table header does not auto-start Sortable until the column popover has entered', async (t) => {
|
||||
const { control, popover } = mountHeader(t)
|
||||
await vue.nextTick()
|
||||
assert.equal(control.options.immediate, false)
|
||||
assert.equal(control.starts.length, 0)
|
||||
popover.props.onAfterEnter()
|
||||
assert.equal(control.starts.length, 0, 'a stale enter without opening is ignored')
|
||||
openPopover(popover)
|
||||
assert.equal(control.starts.length, 1)
|
||||
assert.equal(control.starts[0], control.target.value)
|
||||
})
|
||||
|
||||
test('closing and reopening column settings destroys the drag instance and rejects a late enter', async (t) => {
|
||||
const { control, popover } = mountHeader(t)
|
||||
openPopover(popover)
|
||||
popover.props.onBeforeLeave()
|
||||
assert.equal(control.active, false)
|
||||
assert.equal(control.destroys, 1)
|
||||
popover.props.onAfterEnter()
|
||||
assert.equal(control.starts.length, 1)
|
||||
openPopover(popover)
|
||||
assert.equal(control.starts.length, 2)
|
||||
})
|
||||
|
||||
test('missing, non-element and detached column targets never start Sortable', async (t) => {
|
||||
const { control, popover } = mountHeader(t)
|
||||
const original = control.target.value
|
||||
for (const target of [null, undefined, {}, new HostElement('detached')]) {
|
||||
control.target.value = target
|
||||
openPopover(popover)
|
||||
assert.equal(control.starts.length, 0)
|
||||
}
|
||||
control.target.value = original
|
||||
openPopover(popover)
|
||||
assert.equal(control.starts.length, 1)
|
||||
})
|
||||
|
||||
test('unmounting before or after opening safely cleans up and rejects queued popover callbacks', async (t) => {
|
||||
for (const opened of [false, true]) {
|
||||
const { control, popover, unmount } = mountHeader(t)
|
||||
if (opened) openPopover(popover)
|
||||
const count = control.starts.length
|
||||
unmount()
|
||||
await vue.nextTick()
|
||||
assert.equal(control.active, false)
|
||||
assert.ok(control.destroys > 0)
|
||||
popover.props.onAfterEnter()
|
||||
assert.equal(control.starts.length, count)
|
||||
}
|
||||
})
|
||||
|
||||
test('cached page deactivation cleans up; returning preserves column order and checked state', async (t) => {
|
||||
const { control, popover, active, columns } = mountHeader(t)
|
||||
openPopover(popover)
|
||||
control.model.value = [...control.model.value].reverse()
|
||||
await vue.nextTick()
|
||||
assert.deepEqual(
|
||||
columns.value.map((item) => item.prop),
|
||||
['shop_name', 'phone']
|
||||
)
|
||||
active.value = false
|
||||
await vue.nextTick()
|
||||
assert.equal(control.active, false)
|
||||
popover.props.onAfterEnter()
|
||||
assert.equal(control.starts.length, 1)
|
||||
active.value = true
|
||||
await vue.nextTick()
|
||||
openPopover(popover)
|
||||
assert.equal(control.starts.length, 2)
|
||||
assert.equal(columns.value[0].checked, false)
|
||||
assert.equal(columns.value[1].checked, true)
|
||||
})
|
||||
Reference in New Issue
Block a user