Files
Friendship-and-chat-pc/tests/member-user-brokerage.test.cjs
T

353 lines
12 KiB
JavaScript

const { test } = require('node:test')
const assert = require('node:assert/strict')
const fs = require('node:fs')
const path = require('node:path')
const vm = require('node:vm')
const ts = require('typescript')
const vue = require('vue')
const { parse, compileTemplate } = require('vue/compiler-sfc')
const detailDirectory = path.join(__dirname, '../src/views/member/user/detail')
const hookSource = fs.readFileSync(path.join(detailDirectory, 'useBrokerageTree.ts'), 'utf8')
const transpile = (source) =>
ts.transpileModule(source, {
compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS }
}).outputText
const child = (id, bindUserId, directChildCount = 0) => ({
id,
bindUserId,
directChildCount,
userCode: `U${id}`,
nickname: `会员${id}`,
memberExists: true
})
const member = (id = 1, inviterId = 90) => ({
id,
userCode: `U${id}`,
nickname: `会员${id}`,
inviter: inviterId
? { id: inviterId, nickname: '邀请人', userCode: `U${inviterId}`, memberExists: true }
: null
})
const deferred = () => {
let resolve, reject
const promise = new Promise((done, fail) => {
resolve = done
reject = fail
})
return { promise, resolve, reject }
}
const flush = async () => {
await vue.nextTick()
for (let index = 0; index < 8; index++) await Promise.resolve()
}
function harness(overrides = {}, initial = member()) {
const calls = []
const api = {
getBrokerageChildrenPage: async () => ({ list: [], total: 0 }),
getUser: async (id) => member(id),
...overrides
}
const childQuery = api.getBrokerageChildrenPage
api.getBrokerageChildrenPage = (query) => {
calls.push({ ...query })
return childQuery(query)
}
let unmount
const runtimeVue = {
...vue,
onBeforeUnmount: (fn) => {
unmount = fn
}
}
const module = { exports: {} }
vm.runInNewContext(transpile(hookSource), {
exports: module.exports,
require: (name) => {
if (name === 'vue') return runtimeVue
if (name === '@/api/member/user') return api
throw new Error(`Unexpected import ${name}`)
}
})
const user = vue.ref(initial)
const scope = vue.effectScope()
const tree = scope.run(() => module.exports.useBrokerageTree(user))
return {
tree,
user,
api,
calls,
stop: () => {
unmount()
scope.stop()
}
}
}
const nodeIds = (tree) => Array.from(tree.graph.value.nodes, ({ member }) => member.id)
test('会员列表与详情模板可编译,邀请人紧跟禁用列,关系树使用懒加载页签', () => {
for (const file of [
path.join(detailDirectory, '../index.vue'),
path.join(detailDirectory, 'index.vue'),
path.join(detailDirectory, 'UserBrokerageTree.vue')
]) {
const { descriptor } = parse(fs.readFileSync(file, 'utf8'))
assert.deepEqual(
compileTemplate({ source: descriptor.template.content, filename: file, id: 'brokerage' })
.errors,
[]
)
}
const list = fs.readFileSync(path.join(detailDirectory, '../index.vue'), 'utf8')
assert.match(
list,
/label="是否禁用"[\s\S]*?<\/el-table-column>\s*<el-table-column label="邀请人"/
)
assert.match(list, /openDetail\(row.inviter.id\)/)
const detail = fs.readFileSync(path.join(detailDirectory, 'index.vue'), 'utf8')
assert.match(detail, /label="分销关系" name="brokerage" lazy/)
assert.match(detail, /defineAsyncComponent\(\(\) => import\('\.\/UserBrokerageTree.vue'\)\)/)
assert.match(detail, /label="推广用户" lazy/)
})
test('等待会员资料时不查询,未绑定邀请人仍展示当前会员', async () => {
const h = harness({}, null)
assert.equal(h.calls.length, 0)
h.user.value = member(1, null)
await flush()
assert.deepEqual(nodeIds(h.tree), [1])
assert.equal(h.tree.branches[1].total, 0)
h.stop()
})
test('直属下级按20人分页,去重后继续正确推进页码', async () => {
const h = harness({
getBrokerageChildrenPage: async ({ pageNo }) =>
pageNo === 1
? { list: Array.from({ length: 20 }, (_, index) => child(30 - index, 1)), total: 24 }
: { list: [child(11, 1), child(10, 1), child(9, 1), child(8, 1)], total: 24 }
})
await flush()
assert.deepEqual(nodeIds(h.tree).slice(0, 3), [90, 1, 30])
await h.tree.loadChildren(1)
assert.equal(h.tree.branches[1].children.length, 23)
assert.equal(h.tree.hasMore(h.tree.branches[1]), false)
await h.tree.loadChildren(1)
assert.deepEqual(h.calls, [
{ bindUserId: 1, pageNo: 1, pageSize: 20 },
{ bindUserId: 1, pageNo: 2, pageSize: 20 }
])
h.stop()
})
test('可展开超过三级,收起后再次展开复用已加载的分支与展开状态', async () => {
const h = harness({
getBrokerageChildrenPage: async ({ bindUserId }) => ({
list: bindUserId < 4 ? [child(bindUserId + 1, bindUserId, 1)] : [],
total: bindUserId < 4 ? 1 : 0
})
})
await flush()
await h.tree.toggleBranch(2)
await h.tree.toggleBranch(3)
assert.deepEqual(nodeIds(h.tree), [90, 1, 2, 3, 4])
await h.tree.toggleBranch(2)
assert.deepEqual(nodeIds(h.tree), [90, 1, 2])
await h.tree.toggleBranch(2)
assert.deepEqual(nodeIds(h.tree), [90, 1, 2, 3, 4])
assert.equal(h.calls.length, 3)
h.stop()
})
test('分支失败后可重试原页,已有节点继续显示', async () => {
let attempts = 0
const h = harness({
getBrokerageChildrenPage: async ({ pageNo }) => {
if (pageNo === 1) return { list: [child(2, 1)], total: 21 }
if (++attempts === 1) throw new Error('offline')
return { list: [child(3, 1)], total: 21 }
}
})
await flush()
await h.tree.loadChildren(1)
assert.equal(h.tree.branches[1].pageNo, 1)
assert.match(h.tree.branches[1].error, /重试/)
assert.deepEqual(nodeIds(h.tree), [90, 1, 2])
await h.tree.loadChildren(1)
assert.equal(h.tree.branches[1].error, '')
assert.deepEqual(nodeIds(h.tree), [90, 1, 2, 3])
assert.equal(h.calls.at(-1).pageNo, 2)
h.stop()
})
test('初次加载失败可重试,并发重复点击只发出一次请求', async () => {
const pending = deferred()
const h = harness({ getBrokerageChildrenPage: () => pending.promise })
await h.tree.loadChildren(1)
assert.equal(h.calls.length, 1)
pending.reject(new Error('offline'))
await flush()
assert.equal(h.tree.branches[1].loading, false)
h.api.getBrokerageChildrenPage = async () => ({ list: [child(2, 1)], total: 1 })
await h.tree.loadChildren(1)
assert.deepEqual(nodeIds(h.tree), [90, 1, 2])
h.stop()
})
test('切换会员后忽略旧分支的迟到响应', async () => {
const old = deferred()
const h = harness({
getBrokerageChildrenPage: ({ bindUserId }) =>
bindUserId === 1 ? old.promise : Promise.resolve({ list: [child(20, 10)], total: 1 })
})
h.user.value = member(10, 99)
await flush()
old.resolve({ list: [child(2, 1)], total: 1 })
await flush()
assert.deepEqual(nodeIds(h.tree), [99, 10, 20])
assert.equal(h.tree.branches[1], undefined)
assert.equal(h.tree.branches[2], undefined)
h.stop()
})
test('刷新重新读取邀请人并清空旧分支,连续点击只刷新一次', async () => {
const pending = deferred()
let reads = 0
const h = harness({
getUser: () => {
reads++
return pending.promise
}
})
await flush()
const task = h.tree.refresh()
await h.tree.refresh()
assert.equal(reads, 1)
pending.resolve(member(1, 99))
await task
assert.deepEqual(nodeIds(h.tree), [99, 1])
assert.equal(h.tree.refreshing.value, false)
h.stop()
})
test('刷新失败保留已加载关系并允许再次刷新,切换会员会清除错误', async () => {
const h = harness({
getUser: async () => {
throw new Error('offline')
}
})
await flush()
await h.tree.refresh()
assert.match(h.tree.refreshError.value, /重试/)
assert.deepEqual(nodeIds(h.tree), [90, 1])
h.api.getUser = async () => member(1, 99)
await h.tree.refresh()
assert.equal(h.tree.refreshError.value, '')
assert.deepEqual(nodeIds(h.tree), [99, 1])
h.stop()
})
test('历史循环、错误父节点和重复数据不会形成环或重复节点,缺失会员资料仍可查看关系', async () => {
const h = harness({
getBrokerageChildrenPage: async ({ bindUserId }) =>
bindUserId === 1
? {
list: [
child(1, 1),
child(90, 1),
child(8, 999),
{ ...child(2, 1, 1), memberExists: false },
child(2, 1)
],
total: 5
}
: { list: [child(1, 2), child(3, 2)], total: 2 }
})
await flush()
assert.deepEqual(nodeIds(h.tree), [90, 1, 2])
assert.match(h.tree.branches[1].warning, /异常/)
assert.equal(h.tree.graph.value.nodes.at(-1).member.memberExists, false)
await h.tree.toggleBranch(2)
assert.deepEqual(nodeIds(h.tree), [90, 1, 2, 3])
assert.match(h.tree.branches[2].warning, /异常/)
h.stop()
})
test('会员邀请自己时显示异常提示,组件卸载后忽略请求结果', async () => {
const pending = deferred()
const h = harness({ getBrokerageChildrenPage: () => pending.promise }, member(1, 1))
assert.deepEqual(nodeIds(h.tree), [1])
assert.match(h.tree.branches[1].warning, /自己/)
h.stop()
pending.resolve({ list: [child(2, 1)], total: 1 })
await flush()
assert.equal(h.tree.branches[2], undefined)
})
test('实际Dagre布局在宽分支和三级关系中无节点重叠,节点详情跳转使用内部编号', async () => {
const h = harness({
getBrokerageChildrenPage: async () => ({ list: [child(2, 1, 1), child(3, 1)], total: 2 })
})
await flush()
const { descriptor } = parse(
fs.readFileSync(path.join(detailDirectory, 'UserBrokerageTree.vue'), 'utf8')
)
const props = vue.reactive({ user: member(), active: true })
const routes = []
const context = {
exports: {},
defineProps: () => props,
useRouter: () => ({ push: (route) => routes.push(route) }),
require: (name) => {
if (name === 'vue') return { ...vue, useId: () => 'test' }
if (name === '@vue-flow/core')
return {
Position: { Top: 'top', Bottom: 'bottom' },
MarkerType: { ArrowClosed: 'arrowclosed' },
ConnectionMode: { Strict: 'strict' },
useVueFlow: () => ({
onNodesInitialized: () => {},
fitView: async () => {},
zoomIn: () => {},
zoomOut: () => {}
})
}
if (name === '@vue-flow/controls' || name.endsWith('.css')) return {}
if (name === '@dagrejs/dagre') return require('@dagrejs/dagre')
if (name === '@/utils/constants') return { CommonStatusEnum: { DISABLE: 1 } }
if (name === '@/utils/formatTime') return { formatDate: (value) => value.toISOString() }
if (name === './useBrokerageTree') return { useBrokerageTree: () => h.tree }
throw new Error(`Unexpected import ${name}`)
}
}
const scope = vue.effectScope()
scope.run(() =>
vm.runInNewContext(
transpile(descriptor.scriptSetup.content) + '\nglobalThis.state = { layout, openDetail };',
context
)
)
const nodes = context.state.layout.value.nodes
assert.equal(nodes.length, 4)
assert.ok(nodes.every((node) => node.style.pointerEvents === 'all'))
assert.ok(
context.state.layout.value.edges.every(
(edge) => edge.sourceHandle === 'out' && edge.targetHandle === 'in'
)
)
for (let index = 0; index < nodes.length; index++) {
for (let other = index + 1; other < nodes.length; other++) {
assert.ok(
Math.abs(nodes[index].position.x - nodes[other].position.x) >= 248 ||
Math.abs(nodes[index].position.y - nodes[other].position.y) >= 174
)
}
}
context.state.openDetail(2)
assert.equal(routes[0].params.id, 2)
assert.equal(routes[0].name, 'MemberUserDetail')
scope.stop()
h.stop()
})