// Offline checks: execute the real composable and page setup code with stubbed APIs. import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { createRequire } from "node:module"; import vm from "node:vm"; import test from "node:test"; import { compileScript, parse } from "@vue/compiler-sfc"; import ts from "typescript"; import * as vue from "vue"; const require = createRequire(import.meta.url); const root = new URL("../", import.meta.url); const read = path => readFileSync(new URL(path, root), "utf8"); const deferred = () => { let resolve, reject; const promise = new Promise((yes, no) => { resolve = yes; reject = no; }); return { promise, resolve, reject }; }; function harness(overrides = {}) { const calls = []; const api = new Proxy(overrides, { get(target, method) { return (...args) => { calls.push({ method, args }); if (method in target) return target[method](...args); if (method === "listCompanies") return Promise.resolve({ data: [{ id: 1, companyName: "A" }, { id: 2, companyName: "B" }] }); if (method === "listDepartments") return Promise.resolve({ data: [{ id: args[0] * 10, departmentName: "部门" }] }); if (method === "listPositionsByDepartment") return Promise.resolve({ data: [{ id: args[0] * 10, positionName: "岗位" }] }); if (method === "listMaterialTypes") return Promise.resolve({ data: [{ id: 1, status: "enable" }] }); if (method === "onboardingTemplatePage") return Promise.resolve({ data: { list: [], total: 0 } }); throw new Error("Unexpected API: " + method); }; } }); function evaluate(source, imports = {}) { const output = ts.transpileModule(source.replaceAll("import.meta.env", "({})"), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022, esModuleInterop: true } }).outputText; const module = { exports: {} }; vm.runInNewContext(output, { module, exports: module.exports, console, require(name) { if (name in imports) return imports[name]; if (name === "vue") return vue; if (name === "@/api/modules/biz/onboarding") return { onboardingTaskApi: api }; if (name === "@/hooks/useOnboardingScopeOptions") return scopeModule; if (name === "@/utils/onboardingPermission") return { useOnboardingPermission: () => ({ canManage: () => true, disabledReason: () => "", hasAction: () => true }) }; if (name === "@/utils/messageBox" || name === "@/utils") return {}; if (name === "element-plus") return { ElMessage: { success() {}, warning() {}, error() {} }, ElMessageBox: {} }; if (name === "@element-plus/icons-vue" || name.endsWith(".vue")) return {}; return require(name); } }); return module.exports; } const scopeModule = evaluate(read("src/hooks/useOnboardingScopeOptions.ts")); function page(path, props = {}) { const mounted = []; const descriptor = parse(read(path), { filename: path }).descriptor; const code = compileScript(descriptor, { id: path }).content; const component = evaluate(code, { vue: { ...vue, onMounted: callback => mounted.push(callback), onBeforeUnmount() {} } }).default; const effects = vue.effectScope(); const state = effects.run(() => component.setup(props, { expose() {} })); return { state, mount: () => Promise.all(mounted.map(callback => callback())), stop: () => effects.stop() }; } return { api, calls, scopeModule, page }; } test("dependent options load only after selecting their parent, and reset makes no request", async () => { const h = harness(); const selection = vue.reactive({}); const options = h.scopeModule.useOnboardingScopeOptions(() => selection); assert.equal(h.calls.length, 0); await options.onDepartmentChange(10); assert.equal(h.calls.length, 0); selection.companyId = 1; await options.onCompanyChange(1); assert.equal(h.calls.length, 1); assert.equal(h.calls[0].method, "listDepartments"); assert.equal(options.positions.value.length, 0); selection.departmentId = 10; await options.onDepartmentChange(10); assert.equal(h.calls[1].method, "listPositionsByDepartment"); selection.positionId = 100; selection.companyId = undefined; options.reset(); assert.equal(selection.departmentId, undefined); assert.equal(selection.positionId, undefined); assert.equal(options.departments.value.length, 0); assert.equal(options.positions.value.length, 0); assert.equal(h.calls.length, 2); }); test("slow department responses cannot restore a previous company or survive reset", async () => { const a = deferred(), b = deferred(), c = deferred(); const h = harness({ listDepartments: id => ({ 1: a, 2: b, 3: c })[id].promise }); const selection = { companyId: 1 }; const options = h.scopeModule.useOnboardingScopeOptions(() => selection); const first = options.onCompanyChange(1); selection.companyId = 2; const second = options.onCompanyChange(2); b.resolve({ data: [{ id: 20 }] }); await second; a.resolve({ data: [{ id: 10 }] }); await first; assert.equal(options.departments.value[0].id, 20); selection.companyId = 3; const third = options.onCompanyChange(3); options.reset(); c.resolve({ data: [{ id: 30 }] }); await third; assert.equal(options.departments.value.length, 0); }); test("slow position responses cannot overwrite a new department; failure leaves options empty", async () => { const a = deferred(), b = deferred(); const h = harness({ listPositionsByDepartment: id => (id === 10 ? a : b).promise }); const selection = { companyId: 1, departmentId: 10 }; const options = h.scopeModule.useOnboardingScopeOptions(() => selection); const first = options.onDepartmentChange(10); selection.departmentId = 20; const second = options.onDepartmentChange(20); b.resolve({ data: [{ id: 200 }] }); await second; a.resolve({ data: [{ id: 100 }] }); await first; assert.equal(options.positions.value[0].id, 200); const failed = harness({ listDepartments: () => Promise.reject(new Error("offline")) }); const empty = failed.scopeModule.useOnboardingScopeOptions(() => ({ companyId: 1 })); await empty.onCompanyChange(1); assert.equal(empty.departments.value.length, 0); }); for (const [name, tableName, columnName] of [ ["contract", "templateTable", "templateColumns"], ["task", "proTable", "columns"], ["employee", "proTable", "columns"] ]) { test(name + ": initialization, filter cascade and reset use only necessary requests", async () => { const h = harness(); const page = h.page("src/views/biz/onboarding/" + name + "/index.vue"); const state = page.state; state[tableName].value = { searchParam: {}, enumMap: new Map() }; await page.mount(); assert.equal(h.calls.length, 1); assert.equal(h.calls[0].method, "listCompanies"); const department = state[columnName].find(item => item.prop === "departmentId"); const position = state[columnName].find(item => item.prop === "positionId"); assert.equal(department.search.props.disabled, true); assert.equal(position.search.props.disabled, true); state[tableName].value.searchParam.companyId = 1; await state.onSearchCompanyChange(1); assert.equal(department.search.props.disabled, false); assert.equal(position.search.props.disabled, true); assert.equal(state[tableName].value.enumMap.get("departmentId")[0].value, 10); state[tableName].value.searchParam.departmentId = 10; await state.onSearchDepartmentChange(10); assert.equal(position.search.props.disabled, false); assert.equal(state[tableName].value.enumMap.get("positionId")[0].value, 100); state[tableName].value.searchParam = {}; state.resetSearchScope(); assert.equal(h.calls.length, 3); assert.equal(state[tableName].value.enumMap.get("departmentId").length, 0); assert.equal(state[tableName].value.enumMap.get("positionId").length, 0); page.stop(); }); } test("entry config loads no hidden data; material loads share one in-flight request", async () => { const pending = deferred(); const h = harness({ listMaterialTypes: () => pending.promise }); const page = h.page("src/views/biz/onboarding/entry-config/index.vue"); await page.mount(); assert.equal(h.calls.length, 0); page.state.activeTab.value = "materials"; await vue.nextTick(); const shared = page.state.loadMaterials(); assert.equal(h.calls.length, 1); pending.resolve({ data: [{ id: 5 }] }); await shared; assert.equal(page.state.materialTypes.value[0].id, 5); page.stop(); }); test("task form fetches only the selected template's materials and preserves names and HR fields", async () => { const h = harness({ onboardingTemplateDetail: () => Promise.resolve({ data: { contracts: [{ contractTemplateId: 11, contractTemplateVersionId: 12, required: true }] } }), templateDetail: () => Promise.resolve({ data: { id: 11, companyId: 1, departmentId: 10, status: "enable", templateName: "劳动合同" } }), variablePage: () => Promise.resolve({ data: { list: [{ variableCode: "hr.Salary", variableName: "薪资", variableType: "contract", hrEditable: true, defaultValue: "5000", dataType: "number", required: true }] } }) }); const page = h.page("src/views/biz/onboarding/task/index.vue"); await page.mount(); page.state.form.companyId = 1; page.state.form.departmentId = 10; await page.state.onOnboardingTemplateChange(7); assert.equal(h.calls.length, 4); assert.equal(h.calls[1].method, "onboardingTemplateDetail"); assert.equal(h.calls[2].method, "templateDetail"); assert.equal(h.calls[2].args[0].id, 11); assert.equal(h.calls[3].method, "variablePage"); assert.equal(page.state.contractPreviews.value[0].contractName, "劳动合同"); assert.equal(page.state.form.contractVariables[0].variableValue, "5000"); assert.equal(page.state.form.contractVariables[0].contractName, "劳动合同"); assert.equal(page.state.contractConfigurationError.value, ""); page.stop(); }); test("entry template list loads just companies and its page; form data is deferred", async () => { const h = harness(); let materials = 0; const page = h.page("src/views/biz/onboarding/entry-config/components/OnboardingTemplatePanel.vue", { loadMaterials: async () => { materials++; return [{ id: 7, status: "enable" }]; } }); await page.mount(); assert.equal(h.calls.length, 2); assert.equal(materials, 0); page.state.query.companyId = 1; await page.state.onQueryCompanyChange(1); assert.equal(h.calls.length, 4); assert.equal(h.calls[2].method, "listDepartments"); assert.equal(h.calls[3].method, "onboardingTemplatePage"); page.state.resetQuery(); await vue.nextTick(); assert.equal(h.calls.length, 5); assert.equal(page.state.queryDepartments.value.length, 0); await page.state.openCreate(); assert.equal(materials, 1); assert.equal(page.state.materialTypes.value[0].id, 7); assert.equal(h.calls.length, 5); page.stop(); }); test("entry template list ignores older search responses", async () => { const a = deferred(), b = deferred(); let count = 0; const h = harness({ onboardingTemplatePage: () => (++count === 1 ? a : b).promise }); const page = h.page("src/views/biz/onboarding/entry-config/components/OnboardingTemplatePanel.vue", { loadMaterials: async () => [] }); const first = page.state.loadTemplates(); page.state.query.keyword = "new"; const second = page.state.loadTemplates(); b.resolve({ data: { list: [{ id: 2 }], total: 1 } }); await second; a.resolve({ data: { list: [{ id: 1 }], total: 1 } }); await first; assert.equal(page.state.rows.value[0].id, 2); assert.equal(page.state.loading.value, false); page.stop(); }); test("company setup defers options until needed and shares loading across fast tab changes", async () => { const pending = deferred(); const h = harness({ listCompanies: () => pending.promise }); const page = h.page("src/views/biz/onboarding/setup/index.vue"); await page.mount(); assert.equal(h.calls.length, 0); page.state.activeTab.value = "department"; await vue.nextTick(); page.state.activeTab.value = "position"; await vue.nextTick(); assert.equal(h.calls.length, 1); pending.resolve({ data: [{ id: 1, companyName: "A" }] }); await page.state.loadCompanies(); page.state.activeTab.value = "department"; await vue.nextTick(); assert.equal(h.calls.length, 1); page.stop(); });