279 lines
12 KiB
JavaScript
279 lines
12 KiB
JavaScript
// 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();
|
|
});
|
|
|