Files
jiapuapp/tests/tac-renderjs-runtime-smoke.js
T
2026-07-27 06:50:23 +08:00

243 lines
6.5 KiB
JavaScript

"use strict";
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const vm = require("vm");
const componentSource = fs.readFileSync(
path.join(__dirname, "../components/TacVerification.vue"),
"utf8",
);
const renderScript = componentSource.match(
/<script module="tacRenderer" lang="renderjs">([\s\S]*?)<\/script>/,
);
assert(renderScript, "必须保留 TacVerification 的 renderjs 桥接入口");
let latestXhr = null;
class FakeXMLHttpRequest {
static DONE = 4;
constructor() {
this.readyState = 0;
this.status = 0;
this.responseText = "";
this.headers = {};
this.aborted = false;
latestXhr = this;
}
open(method, url) {
this.method = method;
this.url = url;
}
setRequestHeader(name, value) {
this.headers[name] = value;
}
send(payload) {
this.payload = payload;
}
abort() {
this.aborted = true;
}
respond(status, body) {
this.status = status;
this.responseText = body;
this.readyState = FakeXMLHttpRequest.DONE;
if (this.onreadystatechange) this.onreadystatechange();
}
timeoutNow() {
if (this.ontimeout) this.ontimeout();
}
}
let latestTac = null;
class FakeCaptchaConfig {
constructor(options) {
this.options = options;
}
addRequestChain(chain) {
this.chain = chain;
}
}
class FakeTac {
constructor(config) {
this.config = config;
this.destroyCount = 0;
this.reloadCount = 0;
latestTac = this;
}
init() {
this.initialized = true;
}
destroyWindow() {
this.destroyCount += 1;
}
reloadCaptcha() {
this.reloadCount += 1;
}
}
const host = { innerHTML: "rendered" };
const loadedStyle = { dataset: { jiapuState: "loaded" }, sheet: {} };
const document = {
head: { appendChild() {} },
querySelector(selector) {
if (selector === "#jiapu-tac-host") return host;
if (selector.startsWith("link[data-jiapu-tac=")) return loadedStyle;
return null;
},
createElement() {
throw new Error("本测试中的本地 TAC 资源应已就绪");
},
};
const window = {
CaptchaConfig: FakeCaptchaConfig,
TAC: FakeTac,
JiapuTacAdapter: {
buildVerifyBody: (data) => data,
normalizeChallengeResponse: () => ({ challenge: {}, sdkResponse: {} }),
normalizeVerifyResponse: (response) => response,
toSdkFailure: (error) => ({ code: 500, msg: error.message }),
},
};
const sandbox = {
console,
document,
window,
XMLHttpRequest: FakeXMLHttpRequest,
setTimeout: (callback) => {
callback();
return 1;
},
clearTimeout() {},
Promise,
Error,
Object,
};
sandbox.globalThis = sandbox;
vm.runInNewContext(
renderScript[1].replace("export default", "globalThis.__renderModule ="),
sandbox,
{ filename: "TacVerification.renderjs.js" },
);
const definition = sandbox.__renderModule;
assert(definition && definition.methods, "renderjs 必须导出可执行桥接模块");
const ownerCalls = [];
const instance = {
...definition.data(),
$ownerInstance: {
callMethod(name, payload) {
ownerCalls.push({ name, payload });
},
},
};
for (const [name, method] of Object.entries(definition.methods)) {
instance[name] = method.bind(instance);
}
const strictRequest = () => instance.sendStrictRequest({
method: "POST",
url: "https://backend-api.ddxcjp.cn/genealogy/app/auth/verification/register/verify",
headers: { clientid: "client-1" },
data: { challengeId: "challenge-1" },
});
const run = async () => {
let pending = strictRequest();
latestXhr.respond(500, "");
assert.strictEqual((await pending).code, 500, "HTTP 500 空体不得被供应商 SDK 当作成功");
pending = strictRequest();
latestXhr.respond(200, "");
assert.strictEqual((await pending).code, 502, "HTTP 200 空体必须失败关闭");
pending = strictRequest();
latestXhr.respond(200, "null");
assert.strictEqual((await pending).code, 502, "HTTP 200 非对象 JSON 必须失败关闭");
pending = strictRequest();
latestXhr.timeoutNow();
assert.strictEqual((await pending).code, 504, "验证码请求超时必须返回可识别失败码");
strictRequest();
const abortedXhr = latestXhr;
instance.destroyTac();
assert.strictEqual(abortedXhr.aborted, true, "关闭验证层必须中止在途请求");
assert.strictEqual(instance.activeXhr, null);
const context = {
visible: true,
requestId: "request-1",
challengeUrl: "https://backend-api.ddxcjp.cn/genealogy/app/auth/verification/register/challenge",
verifyUrl: "https://backend-api.ddxcjp.cn/genealogy/app/auth/verification/register/verify",
clientId: "client-1",
tenantId: "000000",
operationCode: "register",
subject: "13800138000",
};
instance.requestContext = context;
instance.generation += 1;
instance.createTac();
assert.strictEqual(latestTac.initialized, true);
assert.strictEqual(typeof latestTac.config.doSendRequest, "function");
latestTac.config.options.btnRefreshFun(null, latestTac);
assert.strictEqual(latestTac.reloadCount, 1, "必须保留供应商原生刷新操作");
const firstSuccess = latestTac.config.options.validSuccess;
firstSuccess(
{ data: { validToken: "ticket-1", expireSeconds: 300 } },
null,
latestTac,
);
firstSuccess(
{ data: { validToken: "ticket-duplicate", expireSeconds: 300 } },
null,
latestTac,
);
assert.strictEqual(
ownerCalls.filter(({ name }) => name === "handleTacSuccess").length,
1,
"同一轮供应商重复成功回调只能向逻辑层提交一次",
);
const staleSuccess = firstSuccess;
instance.generation += 1;
instance.requestContext = { ...context, requestId: "request-2" };
staleSuccess({ data: { validToken: "stale", expireSeconds: 300 } }, null, latestTac);
assert.strictEqual(ownerCalls.length, 1, "换轮后的陈旧回调不得污染新页面状态");
instance.createTac();
const reopenedTac = latestTac;
reopenedTac.config.options.btnCloseFun(null, reopenedTac);
reopenedTac.config.options.btnCloseFun(null, reopenedTac);
assert.strictEqual(
ownerCalls.filter(({ name }) => name === "handleTacCancel").length,
1,
"关闭按钮重复回调只能提交一次取消",
);
instance.requestContext = { ...context, requestId: "request-3" };
await instance.onContextChange({ visible: false });
assert.strictEqual(instance.tac, null, "隐藏或卸载验证层必须销毁 SDK 实例");
assert.strictEqual(host.innerHTML, "", "隐藏或卸载验证层必须清空宿主节点");
console.log("TAC-RENDERJS-RUNTIME-SMOKE PASS");
};
run().catch((error) => {
console.error(error);
process.exitCode = 1;
});