192 lines
8.8 KiB
JavaScript
192 lines
8.8 KiB
JavaScript
const assert = require("node:assert/strict");
|
|
const { readFileSync } = require("node:fs");
|
|
const path = require("node:path");
|
|
const vm = require("node:vm");
|
|
const { execFileSync } = require("node:child_process");
|
|
const { test } = require("node:test");
|
|
|
|
const root = path.resolve(__dirname, "..");
|
|
const read = (file) => readFileSync(path.join(root, file), "utf8");
|
|
|
|
// Execute the actual page handlers while substituting only platform/lifecycle APIs.
|
|
function loadScript(file, names, dependencies = {}) {
|
|
const source = read(file);
|
|
const script = file.endsWith(".vue")
|
|
? source.match(/<script setup[^>]*>([\s\S]*?)<\/script>/)[1]
|
|
: source;
|
|
const executable = script
|
|
.replace(/^import\s[\s\S]*?;\s*/gm, "")
|
|
.replace(/^export default .*;\s*$/gm, "")
|
|
.replace(/^export (?=const |function )/gm, "");
|
|
const context = vm.createContext({
|
|
console: { log() {}, warn() {}, error() {} },
|
|
ref: (value) => ({ value }),
|
|
computed: (getter) => ({ get value() { return getter(); } }),
|
|
onMounted() {},
|
|
...dependencies,
|
|
});
|
|
return vm.runInContext(
|
|
`${executable}\n;({${names.map((name) => `${name}: typeof ${name} === 'undefined' ? undefined : ${name}`).join(",")}})`,
|
|
context,
|
|
{ filename: file },
|
|
);
|
|
}
|
|
|
|
const { Command } = loadScript("src/model/ChatModel.js", ["Command"]);
|
|
const normalize = (value) => JSON.parse(JSON.stringify(value));
|
|
|
|
test("旅行记录 waits for authentication before opening the local AIGC home", async () => {
|
|
const destinations = [];
|
|
let loginChecks = 0;
|
|
let allowLogin;
|
|
const login = new Promise((resolve) => { allowLogin = resolve; });
|
|
const quick = loadScript("src/pages/ChatMain/ChatQuickAccess/index.vue", ["itemList", "sendReply"], {
|
|
Command,
|
|
SEND_MESSAGE_COMMAND_TYPE: "send-command",
|
|
checkToken: () => { loginChecks++; return login; },
|
|
uni: { navigateTo: ({ url }) => destinations.push(url), $emit() {} },
|
|
});
|
|
assert.equal(quick.itemList.value[0].title, "旅行记录");
|
|
assert.equal(quick.itemList.value[0].type, "Command.travelAIGC");
|
|
quick.sendReply(quick.itemList.value[0]);
|
|
assert.equal(loginChecks, 1);
|
|
assert.deepEqual(destinations, []);
|
|
allowLogin();
|
|
await login;
|
|
await Promise.resolve();
|
|
assert.deepEqual(destinations, ["/pages-aigc/home/home"]);
|
|
assert.deepEqual(quick.itemList.value.slice(1).map((item) => item.title).join(","), "探索发现,呼叫服务,更多");
|
|
});
|
|
|
|
function loadGenerator(toolCall) {
|
|
const navigation = [];
|
|
const component = loadScript("src/pages/ChatModule/GeneratorPhotoComponent/index.vue", ["componentDataMap", "jumpClick"], {
|
|
defineProps: () => ({ toolCall }),
|
|
getAccessToken: () => "test-token",
|
|
navigateTo: (...args) => navigation.push(args),
|
|
});
|
|
return { ...component, navigation };
|
|
}
|
|
|
|
test("generator card accepts both server payload formats and preserves their jump URL", () => {
|
|
for (const field of ["componentNameParams", "aigc"]) {
|
|
const generator = loadGenerator({ [field]: { title: "title.png", jumpUrl: "/pages-aigc/home/home" } });
|
|
assert.ok(generator.componentDataMap, "card needs normalized data for both server payloads");
|
|
assert.equal(generator.componentDataMap.value.title, "title.png");
|
|
generator.jumpClick();
|
|
assert.deepEqual(normalize(generator.navigation), [["/pages-aigc/home/home", { token: "test-token" }]]);
|
|
}
|
|
});
|
|
|
|
test("generator merges field fallbacks and safely ignores a missing link", () => {
|
|
const generator = loadGenerator({ componentNameParams: { title: "new.png" }, aigc: { title: "old.png", description: "description" } });
|
|
assert.ok(generator.componentDataMap);
|
|
assert.equal(generator.componentDataMap.value.title, "new.png");
|
|
assert.equal(generator.componentDataMap.value.description, "description");
|
|
generator.jumpClick();
|
|
assert.deepEqual(generator.navigation, []);
|
|
const empty = loadGenerator({});
|
|
assert.doesNotThrow(() => empty.jumpClick());
|
|
});
|
|
|
|
test("the actual router opens AIGC locally and keeps external generator pages in WebView", () => {
|
|
const { objectToUrlParams } = loadScript("src/utils/UrlParams.js", ["objectToUrlParams"]);
|
|
const destinations = [];
|
|
const { navigateTo } = loadScript("src/router/index.js", ["navigateTo"], {
|
|
objectToUrlParams,
|
|
uni: { navigateTo: ({ url }) => destinations.push(url) },
|
|
});
|
|
navigateTo("/pages-aigc/home/home", { token: "test-token" });
|
|
navigateTo("https://example.invalid/aigc/#/home", { token: "test-token" });
|
|
assert.equal(destinations[0], "/pages-aigc/home/home?token=test-token");
|
|
assert.equal(destinations[1], "/pages/webview/index?url=" + encodeURIComponent("https://example.invalid/aigc/#/home?token=test-token"));
|
|
});
|
|
|
|
test("shared work renders its initial empty state before the request completes", () => {
|
|
const { compile } = require("@vue/compiler-dom");
|
|
const Vue = require("vue");
|
|
const template = read("src/pages-aigc/sharedWork/sharedWork.vue").split("<template>")[1].split("<script setup>")[0].replace(/<\/template>\s*$/, "");
|
|
const render = new Function("Vue", compile(template).code)({ ...Vue, resolveComponent: (name) => name });
|
|
assert.doesNotThrow(() => render({
|
|
shareDetail: null, isVideo: false, videoResultUrl: "", imageResultUrl: "",
|
|
generatorTypeText: "", handleBack() {}, handleCreateOwn() {},
|
|
}, []));
|
|
});
|
|
|
|
test("cold share entry waits for the target service address before loading a public work", async () => {
|
|
let finishEnvironment;
|
|
const ready = new Promise((resolve) => { finishEnvironment = resolve; });
|
|
let requests = 0;
|
|
const page = loadScript("src/pages-aigc/sharedWork/sharedWork.vue", ["fetchShareDetail"], {
|
|
onLoad() {},
|
|
getEvnUrl: () => ready,
|
|
getAigcGeneratorTaskShareDetail: async () => { requests++; return { code: 0, data: { templateName: "旅行作品" } }; },
|
|
uni: { showLoading() {}, hideLoading() {}, showToast() {} },
|
|
});
|
|
const pending = page.fetchShareDetail();
|
|
assert.equal(requests, 0, "do not send the request while the app is still resolving its service address");
|
|
finishEnvironment();
|
|
await pending;
|
|
assert.equal(requests, 1);
|
|
});
|
|
|
|
function loadRequest({ statusCode = 200 } = {}) {
|
|
const logs = [];
|
|
const sent = [];
|
|
let removed = 0;
|
|
let logins = 0;
|
|
const { request } = loadScript("src/request/base/request.js", ["request"], {
|
|
getCurrentConfig: () => ({ clientId: "yangmadao-test-client" }),
|
|
useAppStore: () => ({ serverConfig: { baseUrl: "https://target.invalid" } }),
|
|
getAccessToken: () => "PRIVATE_TOKEN",
|
|
removeAccessToken: () => { removed++; },
|
|
goLogin: () => { logins++; },
|
|
NOTICE_EVENT_LOGOUT: "logout",
|
|
setTimeout: (callback) => callback(),
|
|
console: { log: (...args) => logs.push(args.join(" ")), error() {} },
|
|
uni: {
|
|
$emit() {},
|
|
request: (config) => {
|
|
sent.push(config);
|
|
config.success({ statusCode, data: { code: 0, data: { paySig: "PRIVATE_SIGNATURE" } } });
|
|
},
|
|
},
|
|
});
|
|
return { request, logs, sent, removed: () => removed, logins: () => logins };
|
|
}
|
|
|
|
test("AIGC sensitive requests retain target identity without logging credentials", async () => {
|
|
const client = loadRequest();
|
|
await client.request.post("/hotelBiz/credit/virtual-recharge/orders", { wxLoginCode: "PRIVATE_LOGIN_CODE" }, { sensitive: true });
|
|
assert.equal(client.sent[0].url, "https://target.invalid/hotelBiz/credit/virtual-recharge/orders");
|
|
assert.equal(client.sent[0].header.clientId, "yangmadao-test-client");
|
|
assert.equal(client.sent[0].header.Authorization, "Bearer PRIVATE_TOKEN");
|
|
assert.equal(client.sent[0].data.wxLoginCode, "PRIVATE_LOGIN_CODE");
|
|
assert.doesNotMatch(client.logs.join("\n"), /PRIVATE_TOKEN|PRIVATE_LOGIN_CODE|PRIVATE_SIGNATURE/);
|
|
assert.equal(client.sent[0].sensitive, undefined);
|
|
});
|
|
|
|
test("public share requests omit authentication and expiry still invokes target login", async () => {
|
|
const client = loadRequest();
|
|
await client.request.post("/hotelBiz/aigcGeneratorTask/share/detail", { shareKey: "public-share" }, { noToken: true });
|
|
assert.equal(client.sent[0].header.Authorization, undefined);
|
|
assert.equal(client.sent[0].header.clientId, "yangmadao-test-client");
|
|
const expired = loadRequest({ statusCode: 424 });
|
|
await expired.request.get("/hotelBiz/credit/current");
|
|
assert.equal(expired.removed(), 1);
|
|
assert.equal(expired.logins(), 1);
|
|
});
|
|
|
|
test("migration preserves the independent target client configuration byte for byte", () => {
|
|
const baseline = "340ecc9165684467c01a1f803e21040f86ba3a8f";
|
|
const files = [
|
|
"src/manifest.json", "project.config.json", "src/constant/base.js",
|
|
"src/request/base/config.js", "src/request/base/baseUrl.js", "src/uni.scss",
|
|
"client-configs.json", "scripts/update-appid.js", "src/constant/token.js",
|
|
];
|
|
for (const file of files) {
|
|
const original = execFileSync("git", ["show", `${baseline}:${file}`], { cwd: root });
|
|
assert.deepEqual(readFileSync(path.join(root, file)), original, `${file} must keep target client configuration`);
|
|
}
|
|
});
|