fix: some
All checks were successful
构建并部署前端到测试环境 / deploy (push) Successful in 55s

This commit is contained in:
luo
2026-10-14 17:08:32 +08:00
parent 16effb69f3
commit 0d77a7d55b
23 changed files with 1113 additions and 89 deletions

View 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)
})