259 lines
8.1 KiB
JavaScript
259 lines
8.1 KiB
JavaScript
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)
|
|
})
|