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

2004 lines
68 KiB
JavaScript

"use strict";
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const NativeURLSearchParams = globalThis.URLSearchParams;
const root = path.resolve(__dirname, "..");
const routesPath = path.join(root, "utils", "navigation-routes.js");
const navigationPath = path.join(root, "utils", "navigation.js");
const exportedNavigationNames = [
"buildRouteUrl",
"openNoticeTarget",
"openPage",
"goBack",
"returnTo",
"finishPage",
"goRoot",
"consumeNavigationResult",
"resolveBackAction",
"runBackGuard",
"handleBackPress",
];
let moduleSequence = 0;
const toDataModuleUrl = (source) =>
`data:text/javascript;base64,${Buffer.from(source).toString("base64")}`;
const readRoutesSource = () => fs.readFileSync(routesPath, "utf8");
const routeSourceByRegistry = new WeakMap();
const importRoutes = async (source = readRoutesSource()) => {
const routeModule = await import(toDataModuleUrl(source));
routeSourceByRegistry.set(routeModule.ROUTES, source);
return routeModule;
};
// finishPage 的并发、生命周期和失败回滚属于导航网关通用算法,不能依赖某个
// 业务页面。这里向内存中的路由模块追加 TEST_* 中性路由;它们不会进入生产注册表、
// pages.json 或构建产物。真实 F 路由仍在正文开头用未注入源码验证拒绝旧结果。
const createResultFixtureRoutesSource = (productionSource) => {
const normalizedProductionSource = productionSource.replace(/\r\n/g, "\n");
const routeEndMarker = "\n});\n\nexport const ROOT_ROUTE_KEYS";
assert(normalizedProductionSource.includes(routeEndMarker), "生产路由模块结尾结构已变化");
const fixtureDefinitions = `
TEST_RESULT_ROOT: defineRoute({
path: "/__tests__/navigation-result-root",
kind: "root",
parent: null,
requiredParams: ["genealogyId"],
resultOperations: ["fixture-root-finished"],
}),
TEST_RESULT_LIST: defineRoute({
path: "/__tests__/navigation-result-list",
kind: "page",
parent: "TEST_RESULT_ROOT",
requiredParams: ["genealogyId"],
allowedSources: ["TEST_RESULT_ROOT"],
resultOperations: ["fixture-created"],
}),
TEST_RESULT_DETAIL: defineRoute({
path: "/__tests__/navigation-result-detail",
kind: "page",
parent: "TEST_RESULT_LIST",
requiredParams: ["genealogyId", "recordId"],
allowedSources: ["TEST_RESULT_LIST"],
resultOperations: ["fixture-updated"],
}),
TEST_RESULT_FLOW: defineRoute({
path: "/__tests__/navigation-result-flow",
kind: "flow",
parent: "TEST_RESULT_LIST",
requiredParams: ["genealogyId", "mode"],
optionalParams: ["recordId"],
allowedSources: ["TEST_RESULT_LIST", "TEST_RESULT_DETAIL"],
}),
TEST_SECOND_LIST: defineRoute({
path: "/__tests__/navigation-second-list",
kind: "page",
parent: "TEST_RESULT_ROOT",
requiredParams: ["genealogyId"],
allowedSources: ["TEST_RESULT_ROOT"],
resultOperations: ["fixture-second-created", "fixture-second-updated"],
}),
TEST_SECOND_FLOW: defineRoute({
path: "/__tests__/navigation-second-flow",
kind: "flow",
parent: "TEST_SECOND_LIST",
requiredParams: ["genealogyId"],
optionalParams: ["mode"],
allowedSources: ["TEST_SECOND_LIST"],
}),`;
return normalizedProductionSource
.replace(routeEndMarker, `${fixtureDefinitions}${routeEndMarker}`)
.replace(
'Object.freeze(["A01", "G01", "F01", "M01"])',
'Object.freeze(["A01", "G01", "F01", "M01", "TEST_RESULT_ROOT"])',
);
};
const createPage = (routes, routeKey, options = {}) => {
return {
route: routes[routeKey].path.replace(/^\//, ""),
options: { ...options },
$page: { options: { ...options } },
};
};
const createExternalPage = (route = "pages/external/direct-link", options = {}) => ({
route,
options: { ...options },
$page: { options: { ...options } },
});
const createHarness = async ({
routes,
stack,
manual = false,
failMethods = [],
throwMethods = [],
beforeSuccess = null,
deferComplete = false,
}) => {
const routeSource = routeSourceByRegistry.get(routes);
assert(routeSource, "createHarness 必须接收由 importRoutes 建立的同源路由注册表");
let pages = stack.slice();
const calls = [];
const pending = [];
const pendingCompletes = [];
const failures = new Set(failMethods);
const synchronousThrows = new Set(throwMethods);
const pageFromUrl = (url) => {
const [routePath, query = ""] = url.split("?");
return {
route: routePath.replace(/^\//, ""),
options: Object.fromEntries(new NativeURLSearchParams(query)),
$page: { options: Object.fromEntries(new NativeURLSearchParams(query)) },
};
};
const invoke = (method, payload) => {
calls.push({ method, payload });
if (synchronousThrows.delete(method)) {
throw new Error(`${method}:throw`);
}
const settle = (forcedFailure = failures.has(method)) => {
const complete = () => payload.complete?.();
const scheduleComplete = () => {
if (deferComplete) pendingCompletes.push(complete);
else complete();
};
if (forcedFailure) {
payload.fail?.({ errMsg: `${method}:fail` });
scheduleComplete();
return;
}
if (method === "navigateTo") pages = [...pages, pageFromUrl(payload.url)];
if (method === "redirectTo") pages = [...pages.slice(0, -1), pageFromUrl(payload.url)];
if (method === "reLaunch") pages = [pageFromUrl(payload.url)];
if (method === "navigateBack") pages = pages.slice(0, -payload.delta);
beforeSuccess?.({ method, pages: pages.slice() });
payload.success?.({});
scheduleComplete();
};
if (manual) pending.push(settle);
else queueMicrotask(() => settle());
};
globalThis.uni = {
navigateTo: (payload) => invoke("navigateTo", payload),
redirectTo: (payload) => invoke("redirectTo", payload),
reLaunch: (payload) => invoke("reLaunch", payload),
navigateBack: (payload) => invoke("navigateBack", payload),
};
globalThis.getCurrentPages = () => pages;
if (!fs.existsSync(navigationPath)) {
throw new Error("缺少唯一导航网关:utils/navigation.js");
}
const routesUrl = toDataModuleUrl(routeSource);
const navigationSource = fs
.readFileSync(navigationPath, "utf8")
.replace(
/from\s+["']\.\/navigation-routes\.js["']\s*;/,
`from "${routesUrl}";`,
);
assert.notStrictEqual(
navigationSource,
fs.readFileSync(navigationPath, "utf8"),
"navigation.js 必须从同目录 navigation-routes.js 消费唯一路由合同",
);
moduleSequence += 1;
const navigation = await import(
toDataModuleUrl(`${navigationSource}\n// runtime-smoke-instance:${moduleSequence}`)
);
assert.deepStrictEqual(
Object.keys(navigation).sort(),
exportedNavigationNames.slice().sort(),
"导航网关导出面必须精确",
);
return {
navigation,
calls,
get pages() {
return pages;
},
setPages(nextPages) {
pages = nextPages.slice();
},
settleNext(...args) {
const settle = pending.shift();
assert(settle, "没有待完成的 Uni 导航调用");
settle(...args);
},
completeNext() {
const complete = pendingCompletes.shift();
assert(complete, "没有待完成的 Uni complete 回调");
complete();
},
};
};
const assertRejectsMessage = async (promise, pattern) => {
await assert.rejects(promise, pattern);
};
// 未决 Promise 不会单独保持 Node 进程存活。先置失败,只允许完整跑到 PASS
// 后恢复为 0,防止错误锁实现让脚本静默提前退出却被误判为绿灯。
process.exitCode = 1;
(async () => {
const productionRoutesSource = readRoutesSource();
const { NOTICE_TARGETS, ROUTES: productionRoutes } = await importRoutes(productionRoutesSource);
assert.deepStrictEqual(Object.keys(NOTICE_TARGETS), ["GENEALOGY_REVIEW", "GENEALOGY_HOME"]);
for (const routeKey of Array.from({ length: 10 }, (_, index) => `F${String(index + 1).padStart(2, "0")}`)) {
assert.deepStrictEqual(
productionRoutes[routeKey].resultOperations,
[],
`${routeKey} 生产合同不得保留未落地写接口的成功结果`,
);
}
for (const routeKey of Array.from({ length: 11 }, (_, index) => `R${String(index + 1).padStart(2, "0")}`)) {
assert.deepStrictEqual(
productionRoutes[routeKey].resultOperations,
[],
`${routeKey} 生产合同不得保留未落地写接口的成功结果`,
);
}
assert.deepStrictEqual(
productionRoutes.G01.resultOperations,
["genealogy-created"],
"G01 必须只接受 G03 已落地的真实创建结果",
);
{
const targetPage = createPage(productionRoutes, "G01");
const productionHarness = await createHarness({
routes: productionRoutes,
stack: [targetPage, createPage(productionRoutes, "G03")],
});
const result = {
operation: "genealogy-created",
entityId: "genealogy-created-by-api",
refresh: true,
};
assert.strictEqual(
await productionHarness.navigation.finishPage("G01", {}, result),
true,
"G03 真实创建成功后必须能回流 G01",
);
assert.strictEqual(productionHarness.calls[0].method, "navigateBack");
assert.deepStrictEqual(
productionHarness.navigation.consumeNavigationResult("G01"),
result,
"G01 必须收到真实创建完成结果",
);
}
{
const rejectionCases = [
{
targetKey: "F01",
targetParams: { genealogyId: "g1" },
sourceKey: "F02",
sourceParams: { genealogyId: "g1" },
result: { operation: "feed-created", entityId: "feed-1", refresh: true },
},
{
targetKey: "F04",
targetParams: { genealogyId: "g1" },
sourceKey: "F06",
sourceParams: { genealogyId: "g1", mode: "create" },
result: { operation: "article-created", entityId: "article-1", refresh: true },
},
{
targetKey: "F05",
targetParams: { genealogyId: "g1", articleId: "article-1" },
sourceKey: "F06",
sourceParams: { genealogyId: "g1", articleId: "article-1", mode: "edit" },
result: { operation: "article-updated", entityId: "article-1", refresh: true },
},
{
targetKey: "F08",
targetParams: { genealogyId: "g1", albumId: "album-1" },
sourceKey: "F09",
sourceParams: { genealogyId: "g1", albumId: "album-1" },
result: { operation: "media-uploaded", entityId: "media-1", refresh: true },
},
{
targetKey: "R01",
targetParams: { genealogyId: "g1" },
sourceKey: "R02",
sourceParams: { genealogyId: "g1", mode: "create" },
result: { operation: "person-created", entityId: "person-1", refresh: true },
},
{
targetKey: "R03",
targetParams: { genealogyId: "g1" },
sourceKey: "R04",
sourceParams: { genealogyId: "g1", mode: "create" },
result: { operation: "gift-created", entityId: "relative-1", refresh: true },
},
{
targetKey: "R05",
targetParams: { genealogyId: "g1" },
sourceKey: "R07",
sourceParams: { genealogyId: "g1", mode: "create" },
result: { operation: "ritual-created", entityId: "ceremony-1", refresh: true },
},
];
for (const testCase of rejectionCases) {
const targetPage = createPage(
productionRoutes,
testCase.targetKey,
testCase.targetParams,
);
const productionHarness = await createHarness({
routes: productionRoutes,
stack: [
targetPage,
createPage(productionRoutes, testCase.sourceKey, testCase.sourceParams),
],
});
await assert.rejects(
productionHarness.navigation.finishPage(
testCase.targetKey,
testCase.targetParams,
testCase.result,
),
(error) =>
error?.message ===
`${testCase.targetKey} 不接受结果 operation=${testCase.result.operation}`,
`${testCase.targetKey} 必须精确拒绝旧完成结果`,
);
assert.strictEqual(productionHarness.calls.length, 0);
productionHarness.setPages([targetPage]);
assert.strictEqual(
productionHarness.navigation.consumeNavigationResult(testCase.targetKey),
null,
`${testCase.targetKey} 拒绝旧结果后不得残留一次性结果`,
);
}
}
{
const productionHarness = await createHarness({
routes: productionRoutes,
stack: [createPage(productionRoutes, "F01", { genealogyId: "g1" })],
});
assert.throws(
() => productionHarness.navigation.buildRouteUrl("F04", {}, "F01"),
/genealogyId/,
);
assert.throws(
() =>
productionHarness.navigation.buildRouteUrl(
"F05",
{ articleId: "article-1" },
"F04",
),
/genealogyId/,
);
assert.throws(
() => productionHarness.navigation.buildRouteUrl("R01", {}, "F01"),
/genealogyId/,
);
assert.throws(
() => productionHarness.navigation.buildRouteUrl("R06", { ceremonyId: "ceremony-1" }, "R05"),
/genealogyId/,
);
}
{
const reviewHarness = await createHarness({
routes: productionRoutes,
stack: [createPage(productionRoutes, "N02", { id: "review-1" })],
});
assert.strictEqual(
await reviewHarness.navigation.openNoticeTarget(
"GENEALOGY_REVIEW",
{ genealogyId: "1001" },
),
true,
);
assert.strictEqual(reviewHarness.calls.length, 1);
assert.strictEqual(reviewHarness.calls[0].method, "navigateTo");
assert.strictEqual(
reviewHarness.calls[0].payload.url,
"/pages/genealogy/g10-application-review?genealogyId=1001&sourceKey=N02",
);
const homeHarness = await createHarness({
routes: productionRoutes,
stack: [createPage(productionRoutes, "N02", { id: "approved" })],
});
assert.strictEqual(
await homeHarness.navigation.openNoticeTarget(
"GENEALOGY_HOME",
{ genealogyId: "1001" },
),
true,
);
assert.strictEqual(homeHarness.calls[0].method, "reLaunch");
assert.strictEqual(
homeHarness.calls[0].payload.url,
"/pages/genealogy/g01-my-genealogies?genealogyId=1001",
);
const rejectionHarness = await createHarness({
routes: productionRoutes,
stack: [createPage(productionRoutes, "N02", { id: "unsafe" })],
});
const invalidCalls = [
["__proto__", { genealogyId: "1001" }],
["toString", { genealogyId: "1001" }],
["/pages/genealogy/g10-application-review", { genealogyId: "1001" }],
["", { genealogyId: "1001" }],
[null, { genealogyId: "1001" }],
["GENEALOGY_REVIEW", null],
["GENEALOGY_REVIEW", []],
["GENEALOGY_REVIEW", {}],
["GENEALOGY_REVIEW", { genealogyId: 1001 }],
["GENEALOGY_REVIEW", { genealogyId: "1001", extra: "x" }],
];
for (const [targetType, params] of invalidCalls) {
await assert.rejects(
rejectionHarness.navigation.openNoticeTarget(targetType, params),
/通知目标|必须是对象|非空字符串|缺少|不接受/,
);
}
await assert.rejects(
rejectionHarness.navigation.openNoticeTarget(
"GENEALOGY_REVIEW",
{ genealogyId: "1001" },
"M01",
),
/通知来源|不能进入/,
);
assert.strictEqual(rejectionHarness.calls.length, 0, "非法通知目标不得触发任何 Uni 导航");
}
const resultFixtureRoutesSource = createResultFixtureRoutesSource(productionRoutesSource);
const { ROUTES } = await importRoutes(resultFixtureRoutesSource);
{
const harness = await createHarness({
routes: ROUTES,
stack: [createPage(ROUTES, "T01", { genealogyId: "1" })],
});
const { buildRouteUrl } = harness.navigation;
assert.strictEqual(
buildRouteUrl(
"T03",
{ genealogyId: "9007199254740993", personId: "p/1" },
"T01",
),
"/pages/tree/t03-member-profile?genealogyId=9007199254740993&personId=p%2F1&sourceKey=T01",
);
assert.strictEqual(
buildRouteUrl(
"T03",
{ genealogyId: "宗谱 1", personId: "p&=+#% 空格" },
"T01",
),
"/pages/tree/t03-member-profile?genealogyId=%E5%AE%97%E8%B0%B1%201&personId=p%26%3D%2B%23%25%20%E7%A9%BA%E6%A0%BC&sourceKey=T01",
"字段值必须完整 encodeURIComponent,不能允许查询注入",
);
assert.throws(
() => buildRouteUrl("T03", { genealogyId: "1", personId: 2 }, "T01"),
/personId/,
);
for (const invalidSelectedId of [2, "", undefined]) {
assert.throws(
() => buildRouteUrl(
"T01",
{ genealogyId: "1", selectedId: invalidSelectedId },
"G01",
),
/selectedId/,
"已声明的可选参数也必须遵守非空字符串合同",
);
}
assert.throws(
() => buildRouteUrl("T03", { genealogyId: "1", personId: "2", targetUrl: "/x" }, "T01"),
/targetUrl/,
);
const hiddenExtraParams = { genealogyId: "1", personId: "2" };
Object.defineProperty(hiddenExtraParams, "targetUrl", {
value: "/x",
enumerable: false,
});
assert.throws(
() => buildRouteUrl("T03", hiddenExtraParams, "T01"),
/targetUrl|可枚举/,
);
const accessorParams = { genealogyId: "1" };
Object.defineProperty(accessorParams, "personId", {
enumerable: true,
get: () => "p1",
});
assert.throws(
() => buildRouteUrl("T03", accessorParams, "T01"),
/personId|访问器/,
);
assert.throws(() => buildRouteUrl("T03", { personId: "2" }, "T01"), /genealogyId/);
assert.throws(
() => buildRouteUrl("T03", { genealogyId: "1", personId: "2" }, "G01"),
/G01.*T03|T03.*G01/,
);
assert.throws(() => buildRouteUrl("constructor"), /未知路由键/);
for (const invalidSource of [null, false, 0, Number.NaN]) {
assert.throws(
() => buildRouteUrl(
"T03",
{ genealogyId: "1", personId: "2" },
invalidSource,
),
/sourceKey/,
);
}
assert.strictEqual(
buildRouteUrl(
"G12",
{
genealogyId: "g1",
},
"G01",
),
"/pages/genealogy/g12-generation-poems?genealogyId=g1&sourceKey=G01",
"G12 批量接口没有局部起始代参数,路由不得发明后端无法表达的查询合同",
);
}
{
const savedURLSearchParams = globalThis.URLSearchParams;
try {
globalThis.URLSearchParams = undefined;
const harness = await createHarness({
routes: ROUTES,
stack: [createPage(ROUTES, "T01", { genealogyId: "g1" })],
});
assert.strictEqual(
harness.navigation.buildRouteUrl(
"T03",
{ personId: "p/1", genealogyId: "g1" },
"T01",
),
"/pages/tree/t03-member-profile?genealogyId=g1&personId=p%2F1&sourceKey=T01",
"模块求值时没有 URLSearchParams 仍须按注册表顺序编码",
);
} finally {
globalThis.URLSearchParams = savedURLSearchParams;
}
}
{
const harness = await createHarness({
routes: ROUTES,
stack: [createPage(ROUTES, "G01")],
});
await assert.rejects(
harness.navigation.openPage(
"T03",
{ genealogyId: "1", personId: "2" },
"T07",
),
/真实页面.*G01|G01.*T07/,
"合法但伪造的来源键不得绕过当前真实栈顶",
);
await assert.rejects(
harness.navigation.openPage("T03", { genealogyId: "1", personId: "2" }),
/sourceKey/,
"省略来源键不得跳过真实栈顶校验",
);
assert.strictEqual(harness.calls.length, 0);
}
{
const emptyHarness = await createHarness({ routes: ROUTES, stack: [] });
await assert.rejects(
emptyHarness.navigation.openPage(
"T03",
{ genealogyId: "1", personId: "2" },
"T01",
),
/UNKNOWN|真实页面/,
);
assert.strictEqual(emptyHarness.calls.length, 0);
const externalHarness = await createHarness({
routes: ROUTES,
stack: [createExternalPage()],
});
await assert.rejects(
externalHarness.navigation.openPage(
"T03",
{ genealogyId: "1", personId: "2" },
"T01",
),
/UNKNOWN|真实页面/,
);
assert.strictEqual(externalHarness.calls.length, 0);
}
{
const harness = await createHarness({
routes: ROUTES,
stack: [createPage(ROUTES, "T01", { genealogyId: "1" })],
manual: true,
});
const { openPage } = harness.navigation;
const params = { genealogyId: "1", personId: "2" };
const firstOpen = openPage("T03", params, "T01");
const repeatedOpen = openPage(
"T03",
{ personId: "2", genealogyId: "1" },
"T01",
);
assert.strictEqual(firstOpen, repeatedOpen, "同一转场必须复用同一个 Promise");
assert.strictEqual(harness.calls.length, 1, "同一转场只能调用一次 navigateTo");
assert.strictEqual(harness.calls[0].method, "navigateTo");
const refusedOpen = openPage("T04", { genealogyId: "1" }, "T01");
assert.strictEqual(typeof refusedOpen.then, "function", "锁拒绝也必须返回 Promise");
assert.strictEqual(
await refusedOpen,
false,
"不同转场在锁释放前必须被拒绝",
);
assert.strictEqual(harness.calls.length, 1, "被锁拒绝的转场不得触发 Uni API");
assert.strictEqual(await harness.navigation.goBack(), false);
assert.strictEqual(await harness.navigation.returnTo("G01", {}), false);
assert.strictEqual(await harness.navigation.goRoot("F01", {}), false);
assert.strictEqual(harness.calls.length, 1, "所有导航动作必须共享同一把转场锁");
harness.settleNext();
assert.strictEqual(await firstOpen, true);
harness.setPages([createPage(ROUTES, "T01", { genealogyId: "1" })]);
const retrySameOpen = openPage("T03", params, "T01");
assert.notStrictEqual(
retrySameOpen,
firstOpen,
"成功完成后相同 operation key 必须创建新的 Promise",
);
assert.strictEqual(harness.calls.length, 2);
harness.settleNext();
assert.strictEqual(await retrySameOpen, true);
const afterSuccess = openPage(
"T05",
{ genealogyId: "1", personId: "2" },
"T03",
);
assert.strictEqual(harness.calls.length, 3, "成功 complete 后必须释放转场锁");
harness.settleNext();
assert.strictEqual(await afterSuccess, true);
}
{
const harness = await createHarness({
routes: ROUTES,
stack: [createPage(ROUTES, "T01", { genealogyId: "g1" })],
manual: true,
deferComplete: true,
});
const firstOpen = harness.navigation.openPage(
"T03",
{ genealogyId: "g1", personId: "p1" },
"T01",
);
harness.settleNext();
assert.strictEqual(await firstOpen, true);
const secondOpen = harness.navigation.openPage(
"T05",
{ genealogyId: "g1", personId: "p1" },
"T03",
);
assert.strictEqual(
harness.calls.length,
2,
"success Promise 完成后必须立即允许下一转场,不依赖迟到 complete",
);
harness.completeNext();
const thirdNavigation = harness.navigation.goRoot("F01", {});
assert.strictEqual(
await thirdNavigation,
false,
"旧转场迟到的 complete 不能提前释放正在执行的第二把锁",
);
assert.strictEqual(harness.calls.length, 2);
harness.settleNext();
harness.completeNext();
assert.strictEqual(
await secondOpen,
true,
"旧转场迟到的 complete 不得清除新转场锁",
);
}
{
const harness = await createHarness({
routes: ROUTES,
stack: [createPage(ROUTES, "T01", { genealogyId: "1" })],
manual: true,
});
const failedOpen = harness.navigation.openPage(
"T03",
{ genealogyId: "1", personId: "2" },
"T01",
);
harness.settleNext(true);
await assert.rejects(failedOpen, /navigateTo:fail/);
const afterFailure = harness.navigation.openPage(
"T03",
{ genealogyId: "1", personId: "2" },
"T01",
);
assert.strictEqual(harness.calls.length, 2, "失败 complete 后相同 operation key 必须释放");
harness.settleNext();
assert.strictEqual(await afterFailure, true);
}
{
const harness = await createHarness({
routes: ROUTES,
stack: [createPage(ROUTES, "T01", { genealogyId: "1" })],
throwMethods: ["navigateTo"],
});
await assert.rejects(
harness.navigation.openPage(
"T03",
{ genealogyId: "1", personId: "2" },
"T01",
),
/navigateTo:throw/,
);
assert.strictEqual(
await harness.navigation.openPage(
"T03",
{ genealogyId: "1", personId: "2" },
"T01",
),
true,
"Uni API 同步抛错后相同 operation key 必须释放转场锁",
);
assert.strictEqual(harness.calls.length, 2);
}
{
const result = { operation: "fixture-created", entityId: "a9", refresh: true };
const harness = await createHarness({
routes: ROUTES,
stack: [createPage(ROUTES, "TEST_RESULT_LIST", { genealogyId: "g1" }), createPage(ROUTES, "TEST_RESULT_FLOW", { genealogyId: "g1", mode: "create" })],
});
const { finishPage, consumeNavigationResult } = harness.navigation;
assert.strictEqual(await finishPage("TEST_RESULT_LIST", { genealogyId: "g1" }, result), true);
assert.strictEqual(harness.calls[0].method, "navigateBack");
assert.strictEqual(harness.calls[0].payload.delta, 1);
assert.deepStrictEqual(consumeNavigationResult("TEST_RESULT_LIST"), result);
assert.strictEqual(consumeNavigationResult("TEST_RESULT_LIST"), null, "导航结果只能消费一次");
const invalidResults = [
[{ operation: "anything", refresh: true }, /operation|anything/],
[{ operation: "fixture-second-created", refresh: true }, /operation|fixture-second-created/],
[{ operation: "fixture-created", entityId: "", refresh: true }, /entityId/],
[{ operation: "fixture-created", entityId: 9, refresh: true }, /entityId/],
[{ operation: "fixture-created", refresh: "true" }, /refresh/],
[{ operation: "fixture-created", refresh: true, extra: "x" }, /extra/],
[{ operation: "fixture-created" }, /refresh/],
];
for (const [invalidResult, expectedError] of invalidResults) {
await assert.rejects(finishPage("TEST_RESULT_LIST", { genealogyId: "g1" }, invalidResult), expectedError);
assert.strictEqual(
consumeNavigationResult("TEST_RESULT_LIST"),
null,
"无效结果不得先写入再校验",
);
}
let operationReads = 0;
const hiddenResultField = { operation: "fixture-created", refresh: true };
Object.defineProperty(hiddenResultField, "extra", {
value: "x",
enumerable: false,
});
await assert.rejects(
finishPage("TEST_RESULT_LIST", { genealogyId: "g1" }, hiddenResultField),
/extra|可枚举/,
"不可枚举结果字段不得绕过白名单",
);
const accessorResult = { refresh: true };
Object.defineProperty(accessorResult, "operation", {
enumerable: true,
get: () => {
operationReads += 1;
return operationReads <= 2 ? "fixture-created" : "anything";
},
});
await assert.rejects(
finishPage("TEST_RESULT_LIST", { genealogyId: "g1" }, accessorResult),
/operation|访问器/,
"结果字段不得用变值 getter 绕过校验",
);
assert.strictEqual(consumeNavigationResult("TEST_RESULT_LIST"), null);
}
{
let lifecycleResult = null;
let harness;
harness = await createHarness({
routes: ROUTES,
stack: [createPage(ROUTES, "TEST_RESULT_LIST", { genealogyId: "g1" }), createPage(ROUTES, "TEST_RESULT_FLOW", { genealogyId: "g1", mode: "create" })],
manual: true,
beforeSuccess: ({ method }) => {
if (method === "navigateBack") {
lifecycleResult = harness.navigation.consumeNavigationResult("TEST_RESULT_LIST");
}
},
});
const result = { operation: "fixture-created", entityId: "a10", refresh: true };
const finishing = harness.navigation.finishPage("TEST_RESULT_LIST", { genealogyId: "g1" }, result);
result.operation = "invalid-after-start";
result.entityId = "mutated-after-start";
result.refresh = false;
assert.strictEqual(
harness.navigation.consumeNavigationResult("TEST_RESULT_LIST"),
null,
"源页不得提前偷取目标页的一次性结果",
);
harness.settleNext();
assert.strictEqual(await finishing, true);
assert.deepStrictEqual(
lifecycleResult,
{ operation: "fixture-created", entityId: "a10", refresh: true },
"目标页 onShow 时必须已能消费调用瞬间的结果快照",
);
assert.strictEqual(harness.navigation.consumeNavigationResult("TEST_RESULT_LIST"), null);
}
{
const harness = await createHarness({
routes: ROUTES,
stack: [createExternalPage()],
});
const result = { operation: "fixture-created", entityId: "a12", refresh: true };
assert.strictEqual(await harness.navigation.finishPage("TEST_RESULT_LIST", { genealogyId: "g1" }, result), true);
const createdTargetPage = harness.pages[0];
harness.setPages([createPage(ROUTES, "TEST_RESULT_LIST", { genealogyId: "g1" })]);
assert.strictEqual(
harness.navigation.consumeNavigationResult("TEST_RESULT_LIST"),
null,
"缺栈目标错过 onShow 后不得由未来同路由同参数实例消费",
);
harness.setPages([createdTargetPage]);
assert.deepStrictEqual(
harness.navigation.consumeNavigationResult("TEST_RESULT_LIST"),
result,
"原生 success 时必须把未消费结果绑定到实际新建目标实例",
);
}
{
let harness;
harness = await createHarness({
routes: ROUTES,
stack: [createExternalPage()],
beforeSuccess: ({ method }) => {
if (method === "redirectTo") {
harness.setPages([createPage(ROUTES, "G01")]);
}
},
});
await assert.rejects(
harness.navigation.finishPage(
"TEST_RESULT_LIST",
{ genealogyId: "g1" },
{ operation: "fixture-created", entityId: "a13", refresh: true },
),
/导航成功后未找到规范目标页|目标实例或参数不匹配/,
);
harness.setPages([createPage(ROUTES, "TEST_RESULT_LIST", { genealogyId: "g1" })]);
assert.strictEqual(
harness.navigation.consumeNavigationResult("TEST_RESULT_LIST"),
null,
"success 后目标绑定失败也必须同步回滚结果",
);
}
{
const targetPage = createPage(ROUTES, "TEST_RESULT_LIST", { genealogyId: "g1" });
let harness;
harness = await createHarness({
routes: ROUTES,
stack: [targetPage, createPage(ROUTES, "TEST_RESULT_FLOW", { genealogyId: "g1", mode: "create" })],
beforeSuccess: ({ method }) => {
if (method === "navigateBack") {
harness.setPages([createPage(ROUTES, "G01")]);
}
},
});
await assert.rejects(
harness.navigation.finishPage(
"TEST_RESULT_LIST",
{ genealogyId: "g1" },
{ operation: "fixture-created", entityId: "a14", refresh: true },
),
/导航成功后未找到规范目标页|目标实例或参数不匹配/,
);
harness.setPages([targetPage]);
assert.strictEqual(
harness.navigation.consumeNavigationResult("TEST_RESULT_LIST"),
null,
"既有目标转场 success 时未实际到达该实例也必须回滚结果",
);
}
{
const harness = await createHarness({
routes: ROUTES,
stack: [createPage(ROUTES, "TEST_RESULT_LIST", { genealogyId: "g1" }), createPage(ROUTES, "TEST_RESULT_FLOW", { genealogyId: "g1", mode: "create" })],
failMethods: ["navigateBack"],
});
const result = { operation: "fixture-created", refresh: true };
await assertRejectsMessage(harness.navigation.finishPage("TEST_RESULT_LIST", { genealogyId: "g1" }, result), /navigateBack:fail/);
harness.setPages([createPage(ROUTES, "TEST_RESULT_LIST", { genealogyId: "g1" })]);
assert.strictEqual(
harness.navigation.consumeNavigationResult("TEST_RESULT_LIST"),
null,
"原生导航失败后不得泄漏结果",
);
}
{
const harness = await createHarness({
routes: ROUTES,
stack: [createPage(ROUTES, "G01", { genealogyId: "g1" })],
});
assert.strictEqual(
await harness.navigation.goRoot("G01", { genealogyId: "g2" }),
true,
"同一根路由切换业务上下文时必须重新建栈",
);
assert.strictEqual(harness.calls[0].method, "reLaunch");
assert.strictEqual(
harness.calls[0].payload.url,
"/pages/genealogy/g01-my-genealogies?genealogyId=g2",
);
}
{
const harness = await createHarness({
routes: ROUTES,
stack: [createPage(ROUTES, "TEST_RESULT_LIST", { genealogyId: "g1" })],
manual: true,
});
const pending = harness.navigation.openPage("TEST_RESULT_DETAIL", { genealogyId: "g1", recordId: "a1" }, "TEST_RESULT_LIST");
const refused = await harness.navigation.finishPage(
"TEST_RESULT_LIST",
{ genealogyId: "g1" },
{ operation: "fixture-created", refresh: true },
);
assert.strictEqual(refused, false);
assert.strictEqual(harness.navigation.consumeNavigationResult("TEST_RESULT_LIST"), null);
harness.settleNext();
await pending;
}
{
const harness = await createHarness({
routes: ROUTES,
stack: [createPage(ROUTES, "TEST_SECOND_LIST", { genealogyId: "g1" }), createPage(ROUTES, "TEST_SECOND_FLOW", { genealogyId: "g1" })],
manual: true,
});
const firstResult = {
operation: "fixture-second-created",
entityId: "p1",
refresh: true,
};
const firstFinish = harness.navigation.finishPage("TEST_SECOND_LIST", { genealogyId: "g1" }, firstResult);
const repeatedFinish = harness.navigation.finishPage(
"TEST_SECOND_LIST",
{ genealogyId: "g1" },
{ refresh: true, entityId: "p1", operation: "fixture-second-created" },
);
assert.strictEqual(
firstFinish,
repeatedFinish,
"相同完成语义必须复用同一个在途 Promise",
);
const conflictingFinish = harness.navigation.finishPage(
"TEST_SECOND_LIST",
{ genealogyId: "g1" },
{ operation: "fixture-second-updated", entityId: "p2", refresh: true },
);
assert.strictEqual(harness.calls.length, 1);
harness.settleNext();
assert.strictEqual(await firstFinish, true);
assert.strictEqual(await conflictingFinish, false, "不同完成结果必须被转场锁拒绝");
assert.deepStrictEqual(
harness.navigation.consumeNavigationResult("TEST_SECOND_LIST"),
firstResult,
"被拒绝的结果不得覆盖首个在途结果",
);
}
{
const harness = await createHarness({
routes: ROUTES,
stack: [createPage(ROUTES, "TEST_SECOND_LIST", { genealogyId: "g1" }), createPage(ROUTES, "TEST_SECOND_FLOW", { genealogyId: "g1" })],
manual: true,
});
const result = {
operation: "fixture-second-created",
entityId: "p1",
refresh: true,
};
const firstFinish = harness.navigation.finishPage("TEST_SECOND_LIST", { genealogyId: "g1" }, result);
const repeatedFinish = harness.navigation.finishPage(
"TEST_SECOND_LIST",
{ genealogyId: "g1" },
{ refresh: true, entityId: "p1", operation: "fixture-second-created" },
);
assert.strictEqual(firstFinish, repeatedFinish);
harness.settleNext(true);
await assert.rejects(firstFinish, /navigateBack:fail/);
harness.setPages([createPage(ROUTES, "TEST_SECOND_LIST", { genealogyId: "g1" })]);
assert.strictEqual(
harness.navigation.consumeNavigationResult("TEST_SECOND_LIST"),
null,
"相同完成语义复用的转场失败后不得恢复幽灵结果",
);
}
{
const targetPage = createPage(ROUTES, "TEST_RESULT_LIST", { genealogyId: "g1" });
const harness = await createHarness({
routes: ROUTES,
stack: [targetPage, createPage(ROUTES, "TEST_RESULT_FLOW", { genealogyId: "g1", mode: "create" })],
manual: true,
});
const firstResult = { operation: "fixture-created", refresh: true };
const firstFinish = harness.navigation.finishPage("TEST_RESULT_LIST", { genealogyId: "g1" }, firstResult);
const collidingFinish = harness.navigation.finishPage(
"TEST_RESULT_LIST",
{ genealogyId: "g1" },
{ operation: "fixture-created", entityId: "-", refresh: true },
);
harness.settleNext();
assert.strictEqual(await firstFinish, true);
assert.strictEqual(
await collidingFinish,
false,
"缺省 entityId 与字符串 '-' 必须是不同完成语义",
);
assert.deepStrictEqual(harness.navigation.consumeNavigationResult("TEST_RESULT_LIST"), firstResult);
}
{
const harness = await createHarness({
routes: ROUTES,
stack: [createPage(ROUTES, "TEST_SECOND_LIST", { genealogyId: "g1" }), createPage(ROUTES, "TEST_SECOND_FLOW", { genealogyId: "g1" })],
manual: true,
});
const firstResult = {
operation: "fixture-second-created",
entityId: "p1",
refresh: true,
};
const firstFinish = harness.navigation.finishPage("TEST_SECOND_LIST", { genealogyId: "g1" }, firstResult);
const refreshConflict = harness.navigation.finishPage(
"TEST_SECOND_LIST",
{ genealogyId: "g1" },
{ operation: "fixture-second-created", entityId: "p1", refresh: false },
);
const operationConflict = harness.navigation.finishPage(
"TEST_SECOND_LIST",
{ genealogyId: "g1" },
{ operation: "fixture-second-updated", entityId: "p1", refresh: true },
);
harness.settleNext();
assert.strictEqual(await firstFinish, true);
assert.strictEqual(await refreshConflict, false, "refresh 必须参与完成语义键");
assert.strictEqual(await operationConflict, false, "operation 必须参与完成语义键");
assert.deepStrictEqual(harness.navigation.consumeNavigationResult("TEST_SECOND_LIST"), firstResult);
}
{
const harness = await createHarness({
routes: ROUTES,
stack: [createPage(ROUTES, "TEST_RESULT_LIST", { genealogyId: "g1" }), createPage(ROUTES, "TEST_RESULT_FLOW", { genealogyId: "g1", mode: "create" })],
manual: true,
});
const previousResult = { operation: "fixture-created", entityId: "a1", refresh: true };
const firstFinish = harness.navigation.finishPage("TEST_RESULT_LIST", { genealogyId: "g1" }, previousResult);
harness.settleNext();
assert.strictEqual(await firstFinish, true);
harness.setPages([createPage(ROUTES, "TEST_RESULT_LIST", { genealogyId: "g1" }), createPage(ROUTES, "TEST_RESULT_FLOW", { genealogyId: "g1", mode: "create" })]);
const failedReplacement = harness.navigation.finishPage(
"TEST_RESULT_LIST",
{ genealogyId: "g1" },
{ operation: "fixture-created", entityId: "a2", refresh: true },
);
harness.settleNext(true);
await assert.rejects(failedReplacement, /navigateBack:fail/);
harness.setPages([createPage(ROUTES, "TEST_RESULT_LIST", { genealogyId: "g1" })]);
assert.strictEqual(
harness.navigation.consumeNavigationResult("TEST_RESULT_LIST"),
null,
"新流程已开始后不得在失败时复活错过生命周期的陈旧结果",
);
}
{
const oldTargetPage = createPage(ROUTES, "TEST_RESULT_LIST", { genealogyId: "g1" });
const harness = await createHarness({
routes: ROUTES,
stack: [oldTargetPage, createPage(ROUTES, "TEST_RESULT_FLOW", { genealogyId: "g1", mode: "create" })],
manual: true,
});
const oldFinish = harness.navigation.finishPage(
"TEST_RESULT_LIST",
{ genealogyId: "g1" },
{ operation: "fixture-created", entityId: "a-old", refresh: true },
);
harness.settleNext();
assert.strictEqual(await oldFinish, true);
harness.setPages([
createPage(ROUTES, "TEST_SECOND_LIST", { genealogyId: "g1" }),
createPage(ROUTES, "TEST_SECOND_FLOW", { genealogyId: "g1" }),
]);
const newerFinish = harness.navigation.finishPage(
"TEST_SECOND_LIST",
{ genealogyId: "g1" },
{ operation: "fixture-second-created", entityId: "p-new", refresh: true },
);
harness.settleNext(true);
await assert.rejects(newerFinish, /navigateBack:fail/);
harness.setPages([oldTargetPage]);
assert.strictEqual(
harness.navigation.consumeNavigationResult("TEST_RESULT_LIST"),
null,
"任意新完成流程取得全局转场锁后都必须淘汰其他路由的陈旧结果",
);
}
{
const result = {
operation: "fixture-updated",
entityId: "a1",
refresh: true,
};
const targetPage = createPage(ROUTES, "TEST_RESULT_DETAIL", { genealogyId: "g1", recordId: "a1" });
const harness = await createHarness({
routes: ROUTES,
stack: [
targetPage,
createPage(ROUTES, "TEST_RESULT_FLOW", { genealogyId: "g1", recordId: "a1", mode: "edit" }),
],
});
assert.strictEqual(
await harness.navigation.finishPage("TEST_RESULT_DETAIL", { genealogyId: "g1", recordId: "a1" }, result),
true,
);
targetPage.options.recordId = "a2";
assert.strictEqual(
harness.navigation.consumeNavigationResult("TEST_RESULT_DETAIL"),
null,
"同一目标实例的业务上下文发生变化后不得消费结果",
);
targetPage.options.recordId = "a1";
assert.deepStrictEqual(
harness.navigation.consumeNavigationResult("TEST_RESULT_DETAIL"),
result,
"上下文不匹配时不得删除原目标结果",
);
}
{
const fartherTarget = createPage(ROUTES, "TEST_RESULT_LIST", { genealogyId: "g1" });
const nearestTarget = createPage(ROUTES, "TEST_RESULT_LIST", { genealogyId: "g1" });
const result = { operation: "fixture-created", entityId: "a3", refresh: true };
const harness = await createHarness({
routes: ROUTES,
stack: [fartherTarget, nearestTarget, createPage(ROUTES, "TEST_RESULT_FLOW", { genealogyId: "g1", mode: "create" })],
});
assert.strictEqual(await harness.navigation.finishPage("TEST_RESULT_LIST", { genealogyId: "g1" }, result), true);
harness.setPages([fartherTarget]);
assert.strictEqual(
harness.navigation.consumeNavigationResult("TEST_RESULT_LIST"),
null,
"同路由同参数的更远页面实例不得消费最近目标的结果",
);
harness.setPages([nearestTarget]);
assert.deepStrictEqual(harness.navigation.consumeNavigationResult("TEST_RESULT_LIST"), result);
}
{
const harness = await createHarness({
routes: ROUTES,
stack: [createExternalPage()],
failMethods: ["redirectTo"],
});
await assert.rejects(
harness.navigation.finishPage(
"TEST_RESULT_LIST",
{ genealogyId: "g1" },
{ operation: "fixture-created", refresh: true },
),
/redirectTo:fail/,
);
harness.setPages([createPage(ROUTES, "TEST_RESULT_LIST", { genealogyId: "g1" })]);
assert.strictEqual(
harness.navigation.consumeNavigationResult("TEST_RESULT_LIST"),
null,
"目标不在栈内且替换失败时必须回滚结果",
);
}
{
let lifecycleResult = null;
let harness;
harness = await createHarness({
routes: ROUTES,
stack: [createPage(ROUTES, "TEST_RESULT_LIST", { genealogyId: "g1" })],
manual: true,
beforeSuccess: ({ method }) => {
if (method === "redirectTo") {
lifecycleResult = harness.navigation.consumeNavigationResult("TEST_RESULT_LIST");
}
},
});
const result = { operation: "fixture-created", entityId: "a11", refresh: true };
const finishing = harness.navigation.finishPage("TEST_RESULT_LIST", { genealogyId: "g1" }, result);
assert.strictEqual(
harness.navigation.consumeNavigationResult("TEST_RESULT_LIST"),
null,
"缺栈逻辑中同路由发起页也不得提前消费目标结果",
);
harness.settleNext();
assert.strictEqual(await finishing, true);
assert.deepStrictEqual(
lifecycleResult,
result,
"缺栈 redirectTo 创建的目标页 onShow 必须能消费结果",
);
assert.strictEqual(harness.navigation.consumeNavigationResult("TEST_RESULT_LIST"), null);
}
{
const harness = await createHarness({
routes: ROUTES,
stack: [createExternalPage()],
failMethods: ["reLaunch"],
});
await assert.rejects(
harness.navigation.finishPage(
"TEST_RESULT_ROOT",
{ genealogyId: "g1" },
{ operation: "fixture-root-finished", refresh: true },
),
/reLaunch:fail/,
);
harness.setPages([createPage(ROUTES, "TEST_RESULT_ROOT", { genealogyId: "g1" })]);
assert.strictEqual(
harness.navigation.consumeNavigationResult("TEST_RESULT_ROOT"),
null,
"根目标重建失败时必须回滚结果",
);
}
{
let lifecycleResult = null;
let harness;
harness = await createHarness({
routes: ROUTES,
stack: [createExternalPage()],
beforeSuccess: ({ method }) => {
if (method === "reLaunch") {
lifecycleResult = harness.navigation.consumeNavigationResult("TEST_RESULT_ROOT");
}
},
});
const result = { operation: "fixture-root-finished", refresh: true };
assert.strictEqual(
await harness.navigation.finishPage(
"TEST_RESULT_ROOT",
{ genealogyId: "g1" },
result,
),
true,
);
assert.deepStrictEqual(
lifecycleResult,
result,
"缺栈 reLaunch 创建的根目标 onShow 必须能消费结果",
);
assert.strictEqual(harness.navigation.consumeNavigationResult("TEST_RESULT_ROOT"), null);
}
{
const harness = await createHarness({
routes: ROUTES,
stack: [createPage(ROUTES, "TEST_SECOND_LIST", { genealogyId: "g1" }), createPage(ROUTES, "TEST_SECOND_FLOW", { genealogyId: "g1" })],
throwMethods: ["navigateBack"],
});
await assert.rejects(
harness.navigation.finishPage(
"TEST_SECOND_LIST",
{ genealogyId: "g1" },
{ operation: "fixture-second-created", entityId: "p1", refresh: true },
),
/navigateBack:throw/,
);
harness.setPages([createPage(ROUTES, "TEST_SECOND_LIST", { genealogyId: "g1" })]);
assert.strictEqual(
harness.navigation.consumeNavigationResult("TEST_SECOND_LIST"),
null,
"结果写入后 Uni 同步抛错必须回滚",
);
}
{
const targetPage = createPage(ROUTES, "TEST_RESULT_LIST", { genealogyId: "g1" });
const harness = await createHarness({
routes: ROUTES,
stack: [targetPage, createPage(ROUTES, "TEST_RESULT_FLOW", { genealogyId: "g1", mode: "create" })],
throwMethods: ["navigateBack"],
failMethods: ["navigateBack"],
});
const synchronousFailure = harness.navigation.finishPage(
"TEST_RESULT_LIST",
{ genealogyId: "g1" },
{ operation: "fixture-created", entityId: "a1", refresh: true },
);
const immediateRetry = harness.navigation.finishPage(
"TEST_RESULT_LIST",
{ genealogyId: "g1" },
{ operation: "fixture-created", entityId: "a2", refresh: true },
);
await assert.rejects(synchronousFailure, /navigateBack:throw/);
await assert.rejects(immediateRetry, /navigateBack:fail/);
assert.strictEqual(harness.calls.length, 2);
harness.setPages([targetPage]);
assert.strictEqual(
harness.navigation.consumeNavigationResult("TEST_RESULT_LIST"),
null,
"同步抛错后立即重试再失败不得恢复首个幽灵结果",
);
}
{
const harness = await createHarness({
routes: ROUTES,
stack: [createExternalPage()],
});
await assert.rejects(
harness.navigation.returnTo(
"TEST_RESULT_LIST",
{ genealogyId: "g1" },
{ operation: "fixture-created", refresh: true },
),
/returnTo.*不接受.*结果/,
"旧的 returnTo 结果入口必须明确失败",
);
assert.strictEqual(harness.calls.length, 0);
harness.setPages([createPage(ROUTES, "TEST_RESULT_LIST", { genealogyId: "g1" })]);
assert.strictEqual(harness.navigation.consumeNavigationResult("TEST_RESULT_LIST"), null);
}
{
const harness = await createHarness({
routes: ROUTES,
stack: [
createPage(ROUTES, "TEST_RESULT_ROOT", { genealogyId: "g1" }),
createPage(ROUTES, "TEST_RESULT_LIST", { genealogyId: "g1" }),
createPage(ROUTES, "TEST_RESULT_DETAIL", { genealogyId: "g1", recordId: "a1" }),
createPage(ROUTES, "TEST_RESULT_LIST", { genealogyId: "g1" }),
createPage(ROUTES, "TEST_RESULT_DETAIL", { genealogyId: "g1", recordId: "a2" }),
createPage(ROUTES, "TEST_RESULT_FLOW", { genealogyId: "g1", recordId: "a1", mode: "edit" }),
],
});
assert.strictEqual(await harness.navigation.returnTo("TEST_RESULT_LIST", { genealogyId: "g1" }), true);
assert.strictEqual(harness.calls[0].method, "navigateBack");
assert.strictEqual(harness.calls[0].payload.delta, 2, "必须返回最近的既有目标实例");
}
{
const harness = await createHarness({
routes: ROUTES,
stack: [
createPage(ROUTES, "F01"),
createPage(ROUTES, "F03", { genealogyId: "g1", feedId: "feed-1" }),
],
});
assert.strictEqual(
await harness.navigation.returnTo("F01", { genealogyId: "g1" }),
true,
"根页未在 URL 保留可选上下文时,子页可按自身同一上下文返回",
);
assert.strictEqual(harness.calls[0].method, "navigateBack");
assert.strictEqual(harness.calls[0].payload.delta, 1);
}
{
const harness = await createHarness({
routes: ROUTES,
stack: [
createPage(ROUTES, "F01"),
createPage(ROUTES, "F03", { genealogyId: "g1", feedId: "feed-1" }),
],
});
await assert.rejects(
harness.navigation.returnTo("F01", { genealogyId: "g2" }),
/genealogyId.*不一致|最近实例/,
"根页隐式上下文仍不得接受与当前子页不同的家谱",
);
assert.strictEqual(harness.calls.length, 0);
}
{
const harness = await createHarness({
routes: ROUTES,
stack: [createExternalPage()],
});
await assertRejectsMessage(harness.navigation.returnTo("T03", {}), /genealogyId/);
assert.strictEqual(
await harness.navigation.returnTo("T03", { genealogyId: "g1", personId: "p1" }),
true,
);
assert.strictEqual(harness.calls[0].method, "redirectTo");
assert.strictEqual(
harness.calls[0].payload.url,
"/pages/tree/t03-member-profile?genealogyId=g1&personId=p1",
);
}
{
const harness = await createHarness({
routes: ROUTES,
stack: [createPage(ROUTES, "TEST_RESULT_ROOT", { genealogyId: "g1" })],
});
assert.strictEqual(await harness.navigation.returnTo("G01", { genealogyId: "g2" }), true);
assert.strictEqual(harness.calls[0].method, "reLaunch");
}
{
const harness = await createHarness({
routes: ROUTES,
stack: [createPage(ROUTES, "T01", { genealogyId: "g1" }), createPage(ROUTES, "T03", { genealogyId: "g1", personId: "p1" })],
});
await assert.rejects(
harness.navigation.returnTo("T01", { genealogyId: 2 }),
/genealogyId/,
"目标在栈内时仍须校验调用方实际提供的字段",
);
await assert.rejects(
harness.navigation.returnTo("T01", { genealogyId: "g2", targetUrl: "/x" }),
/targetUrl/,
);
const hiddenTargetParams = {};
Object.defineProperty(hiddenTargetParams, "genealogyId", {
value: "g2",
enumerable: false,
});
await assert.rejects(
harness.navigation.returnTo("T01", hiddenTargetParams),
/genealogyId|可枚举/,
);
assert.strictEqual(harness.calls.length, 0);
await assert.rejects(
harness.navigation.returnTo("T01", { genealogyId: "different-context" }),
/genealogyId.*不一致|上下文.*genealogyId/,
"最近实例与显式目标参数冲突时必须拒绝",
);
assert.strictEqual(harness.calls.length, 0);
harness.setPages([createPage(ROUTES, "T01", { genealogyId: "g1" })]);
assert.strictEqual(harness.navigation.consumeNavigationResult("T01"), null);
}
{
const harness = await createHarness({
routes: ROUTES,
stack: [
createPage(ROUTES, "TEST_RESULT_DETAIL", { genealogyId: "g1", recordId: "a1" }),
createPage(ROUTES, "TEST_RESULT_FLOW", { genealogyId: "g1", recordId: "a1", mode: "edit" }),
],
});
await assert.rejects(
harness.navigation.finishPage(
"TEST_RESULT_DETAIL",
{ genealogyId: "g1" },
{ operation: "fixture-updated", entityId: "a1", refresh: true },
),
/recordId/,
"finishPage 即使返回栈内目标也必须显式提供目标必填上下文",
);
assert.strictEqual(harness.calls.length, 0);
}
{
const harness = await createHarness({
routes: ROUTES,
stack: [
createPage(ROUTES, "T01", { genealogyId: "g1", sourceKey: "G05" }),
createPage(ROUTES, "T03", { genealogyId: "g1", personId: "p1" }),
],
});
assert.strictEqual(
await harness.navigation.returnTo("T01", {}),
true,
"目标已在栈内时不得要求重复提供目标必填参数",
);
assert.strictEqual(harness.calls[0].payload.delta, 1);
}
{
const harness = await createHarness({
routes: ROUTES,
stack: [
createPage(ROUTES, "T03", { genealogyId: "g1", personId: "pA" }),
createPage(ROUTES, "T05", { genealogyId: "g1", personId: "pB" }),
],
});
await assert.rejects(
harness.navigation.returnTo("T03", { genealogyId: "g1", personId: "pB" }),
/personId.*不一致|最近实例/,
"T05 当前成员不得覆盖宿主 T03 的初始路由身份",
);
assert.strictEqual(harness.calls.length, 0);
assert.strictEqual(await harness.navigation.goBack(), true);
assert.strictEqual(harness.calls[0].method, "navigateBack");
assert.strictEqual(harness.calls[0].payload.delta, 1);
}
{
const missingRequiredHarness = await createHarness({
routes: ROUTES,
stack: [
createPage(ROUTES, "T01"),
createPage(ROUTES, "T03", { genealogyId: "g1", personId: "p1" }),
],
});
await assert.rejects(
missingRequiredHarness.navigation.returnTo("T01", {}),
/genealogyId/,
"栈内目标实例自身缺少必填参数时不得返回",
);
assert.strictEqual(missingRequiredHarness.calls.length, 0);
const invalidTypeHarness = await createHarness({
routes: ROUTES,
stack: [
createPage(ROUTES, "T01", { genealogyId: 1 }),
createPage(ROUTES, "T03", { genealogyId: "g1", personId: "p1" }),
],
});
await assert.rejects(
invalidTypeHarness.navigation.returnTo("T01", {}),
/genealogyId/,
"栈内目标实例字段类型不合法时不得返回",
);
assert.strictEqual(invalidTypeHarness.calls.length, 0);
}
{
const harness = await createHarness({
routes: ROUTES,
stack: [
createPage(ROUTES, "T01", { genealogyId: "g2" }),
createPage(ROUTES, "T01", { genealogyId: "g1" }),
createPage(ROUTES, "T03", { genealogyId: "g1", personId: "p1" }),
],
});
await assert.rejects(
harness.navigation.returnTo("T01", { genealogyId: "g2" }),
/genealogyId.*不一致|最近实例/,
"不得越过最近实例寻找参数恰好匹配的更远同路由页面",
);
assert.strictEqual(harness.calls.length, 0);
}
{
const harness = await createHarness({
routes: ROUTES,
stack: [
createPage(ROUTES, "TEST_RESULT_DETAIL", { genealogyId: "g1", recordId: "a1" }),
createPage(ROUTES, "TEST_RESULT_FLOW", { genealogyId: "g1", recordId: "a2", mode: "edit" }),
],
});
await assert.rejects(
harness.navigation.finishPage(
"TEST_RESULT_DETAIL",
{ genealogyId: "g1", recordId: "a2" },
{ operation: "fixture-updated", entityId: "a2", refresh: true },
),
/recordId.*不一致|最近实例/,
"完成结果不得跨业务上下文写入最近的同路由目标",
);
await assert.rejects(
harness.navigation.finishPage(
"TEST_RESULT_DETAIL",
{ genealogyId: "g1", recordId: "a1" },
{ operation: "fixture-updated", entityId: "a1", refresh: true },
),
/来源.*recordId|recordId.*来源|上下文/,
"调用方不得用目标参数伪造与当前来源页不一致的完成上下文",
);
assert.strictEqual(harness.calls.length, 0);
harness.setPages([createPage(ROUTES, "TEST_RESULT_DETAIL", { genealogyId: "g1", recordId: "a1" })]);
assert.strictEqual(harness.navigation.consumeNavigationResult("TEST_RESULT_DETAIL"), null);
}
{
const harness = await createHarness({
routes: ROUTES,
stack: [createPage(ROUTES, "T01", { genealogyId: "g1" }), createPage(ROUTES, "T03", { genealogyId: "g1", personId: "p1" })],
});
assert.strictEqual(await harness.navigation.goBack(), true);
assert.strictEqual(harness.calls[0].method, "navigateBack");
assert.strictEqual(harness.calls[0].payload.delta, 1);
}
{
const harness = await createHarness({
routes: ROUTES,
stack: [createPage(ROUTES, "T03", { genealogyId: "g1", personId: "p/1", sourceKey: "T07" })],
});
assert.strictEqual(await harness.navigation.goBack(), true);
assert.strictEqual(harness.calls[0].method, "redirectTo");
assert.strictEqual(
harness.calls[0].payload.url,
"/pages/tree/t01-tree-overview?genealogyId=g1&selectedId=p%2F1",
"单页深链只能使用规范父页映射,不能信任 URL sourceKey",
);
}
{
const harness = await createHarness({
routes: ROUTES,
stack: [createPage(ROUTES, "T03", { personId: "p1", sourceKey: "T07" })],
});
assert.strictEqual(await harness.navigation.goBack(), true);
assert.strictEqual(harness.calls[0].method, "reLaunch");
assert.strictEqual(harness.calls[0].payload.url, "/pages/genealogy/g01-my-genealogies");
}
{
const harness = await createHarness({
routes: ROUTES,
stack: [createPage(ROUTES, "G01")],
});
const rootBack = harness.navigation.goBack();
assert.strictEqual(typeof rootBack.then, "function", "根页无操作也必须返回 Promise");
assert.strictEqual(await rootBack, false, "根页不得伪造跨根历史");
assert.strictEqual(harness.calls.length, 0);
const sameRoot = harness.navigation.goRoot("G01");
assert.strictEqual(typeof sameRoot.then, "function", "根目标无操作也必须返回 Promise");
assert.strictEqual(await sameRoot, false, "相同根目标不得重复 reLaunch");
await assert.rejects(
harness.navigation.goRoot("T01", { genealogyId: "g1" }),
/不是根语义/,
);
}
{
const harness = await createHarness({
routes: ROUTES,
stack: [{
options: {},
$page: {
route: ROUTES.T03.path.slice(1),
options: { genealogyId: "g1", personId: "p/1" },
},
}],
});
assert.strictEqual(await harness.navigation.goBack(), true);
assert.strictEqual(harness.calls[0].method, "redirectTo");
assert.strictEqual(
harness.calls[0].payload.url,
"/pages/tree/t01-tree-overview?genealogyId=g1&selectedId=p%2F1",
"空的顶层 options 不得遮蔽 $page.options,且路由须支持 $page.route",
);
}
{
const harness = await createHarness({
routes: ROUTES,
stack: [{
options: {},
$page: {
fullPath: `${ROUTES.T03.path}?genealogyId=g1&personId=p%2F1`,
query: { genealogyId: "g1", personId: "p/1" },
},
}],
});
assert.strictEqual(await harness.navigation.goBack(), true);
assert.strictEqual(harness.calls[0].method, "redirectTo");
assert.strictEqual(
harness.calls[0].payload.url,
"/pages/tree/t01-tree-overview?genealogyId=g1&selectedId=p%2F1",
"$page.fullPath 与 $page.query 必须支持深链父页回退",
);
}
{
const harness = await createHarness({
routes: ROUTES,
stack: [
createPage(ROUTES, "T01", { genealogyId: "g1" }),
createPage(ROUTES, "T03", { genealogyId: "g1", personId: "p1" }),
createPage(ROUTES, "F04", { genealogyId: "g1" }),
createPage(ROUTES, "T07", { genealogyId: "g1" }),
],
});
assert.strictEqual(
await harness.navigation.openPage("T03", { genealogyId: "g1", personId: "p2" }, "T07"),
true,
);
assert.deepStrictEqual(
harness.calls.map(({ method }) => method),
["navigateBack"],
"同一上下文的 single 页面必须激活既有实例而不是再次压栈",
);
assert.strictEqual(harness.calls[0].payload.delta, 2, "single 页面必须精确返回既有实例");
assert.strictEqual(
harness.pages.filter((page) => page.route === ROUTES.T03.path.slice(1)).length,
1,
"原生栈中只能有一个 T03",
);
assert.deepStrictEqual(harness.navigation.consumeNavigationResult("T03"), {
operation: "member-open-requested",
entityId: "p2",
refresh: false,
});
}
{
const harness = await createHarness({
routes: ROUTES,
stack: [
createPage(ROUTES, "T01", { genealogyId: "g1" }),
createPage(ROUTES, "T03", { genealogyId: "g1", personId: "p1" }),
createPage(ROUTES, "T07", { genealogyId: "g1" }),
],
failMethods: ["navigateBack"],
});
const firstActivation = harness.navigation.openPage(
"T03",
{ genealogyId: "g1", personId: "p2" },
"T07",
);
const repeatedActivation = harness.navigation.openPage(
"T03",
{ personId: "p2", genealogyId: "g1" },
"T07",
);
assert.strictEqual(firstActivation, repeatedActivation);
await assert.rejects(firstActivation, /navigateBack:fail/);
harness.setPages([
createPage(ROUTES, "T03", { genealogyId: "g1", personId: "p2" }),
]);
assert.strictEqual(
harness.navigation.consumeNavigationResult("T03"),
null,
"single 页面激活失败后不得残留成员打开结果",
);
}
{
const harness = await createHarness({
routes: ROUTES,
stack: [
createPage(ROUTES, "T01", { genealogyId: "g2" }),
createPage(ROUTES, "T03", { genealogyId: "g2", personId: "p1" }),
createPage(ROUTES, "T07", { genealogyId: "g1" }),
],
});
await assert.rejects(
harness.navigation.openPage("T03", { genealogyId: "g1", personId: "p2" }, "T07"),
(error) => error?.code === "T03_CONTEXT_CONFLICT",
);
assert.strictEqual(harness.calls.length, 0);
harness.setPages([
createPage(ROUTES, "T03", { genealogyId: "g1", personId: "p2" }),
]);
assert.strictEqual(
harness.navigation.consumeNavigationResult("T03"),
null,
"上下文冲突不得写入 single 页面结果",
);
}
{
for (const invalidPersonId of [undefined, 9]) {
const options = { genealogyId: "g1" };
if (invalidPersonId !== undefined) options.personId = invalidPersonId;
const harness = await createHarness({
routes: ROUTES,
stack: [
createPage(ROUTES, "T03", options),
createPage(ROUTES, "T07", { genealogyId: "g1" }),
],
});
await assert.rejects(
harness.navigation.openPage(
"T03",
{ genealogyId: "g1", personId: "p2" },
"T07",
),
/personId/,
"既有 single 实例自身必须满足完整路由合同",
);
assert.strictEqual(harness.calls.length, 0);
}
}
{
const harness = await createHarness({
routes: ROUTES,
stack: [
createPage(ROUTES, "T03", { genealogyId: "g1", personId: "p0" }),
createPage(ROUTES, "T03", { genealogyId: "g1", personId: "p1" }),
createPage(ROUTES, "T07", { genealogyId: "g1" }),
],
});
await assert.rejects(
harness.navigation.openPage(
"T03",
{ genealogyId: "g1", personId: "p2" },
"T07",
),
(error) => error?.code === "T03_STACK_CONFLICT",
"检测到历史重复 single 实例时必须明确失败,不能宣称栈已唯一",
);
assert.strictEqual(harness.calls.length, 0);
harness.setPages([
createPage(ROUTES, "T03", { genealogyId: "g1", personId: "p2" }),
]);
assert.strictEqual(harness.navigation.consumeNavigationResult("T03"), null);
}
{
const harness = await createHarness({
routes: ROUTES,
stack: [createPage(ROUTES, "G01")],
});
const { handleBackPress } = harness.navigation;
let requestCount = 0;
assert.strictEqual(
handleBackPress({ from: "navigateBack" }, () => {
requestCount += 1;
}),
false,
"网关发起的 navigateBack 再次触发 onBackPress 时必须放行原生返回",
);
assert.strictEqual(requestCount, 0);
assert.strictEqual(
handleBackPress({ from: "backbutton" }, () => {
requestCount += 1;
}),
true,
"Android 返回键必须同步被页面守卫消费",
);
await Promise.resolve();
assert.strictEqual(requestCount, 1);
assert.throws(
() => handleBackPress({}, null),
/requestBack|函数/,
"返回适配器必须拒绝缺失的页面守卫",
);
const { resolveBackAction } = harness.navigation;
assert.strictEqual(resolveBackAction({ transientOpen: true, internalTrail: true, submitting: true, dirty: true }), "close-transient");
assert.strictEqual(resolveBackAction({ transientOpen: false, internalTrail: true, submitting: true, dirty: true }), "pop-internal-trail");
assert.strictEqual(resolveBackAction({ transientOpen: false, internalTrail: false, submitting: true, dirty: true }), "block-submitting");
assert.strictEqual(resolveBackAction({ transientOpen: false, internalTrail: false, submitting: false, dirty: true }), "confirm-discard");
assert.strictEqual(resolveBackAction({}), "go-back");
assert.throws(
() => resolveBackAction({ transientOpen: "false" }),
/transientOpen|布尔/,
"返回守卫状态不得使用 truthy/falsy 多态",
);
await assert.rejects(
harness.navigation.runBackGuard({
submitting: 1,
"block-submitting": () => true,
}),
/submitting|布尔/,
);
}
{
const harness = await createHarness({
routes: ROUTES,
stack: [
createPage(ROUTES, "F01", { genealogyId: "g1" }),
createPage(ROUTES, "F02", { genealogyId: "g1" }),
],
});
const called = [];
const guardedContext = {
transientOpen: true,
internalTrail: true,
submitting: true,
dirty: true,
"close-transient": () => called.push("close-transient"),
"pop-internal-trail": () => called.push("pop-internal-trail"),
"block-submitting": () => called.push("block-submitting"),
"confirm-discard": () => {
called.push("confirm-discard");
return true;
},
};
assert.strictEqual(
await harness.navigation.runBackGuard(guardedContext),
true,
"关闭瞬态层成功后返回值必须稳定为 true",
);
assert.deepStrictEqual(called, ["close-transient"], "一次返回只能执行最高优先级回调");
assert.strictEqual(harness.calls.length, 0);
called.length = 0;
assert.strictEqual(
await harness.navigation.runBackGuard({
internalTrail: true,
"pop-internal-trail": () => called.push("pop-internal-trail"),
}),
true,
"内部轨迹回退成功后返回值必须稳定为 true",
);
assert.deepStrictEqual(called, ["pop-internal-trail"]);
assert.strictEqual(harness.calls.length, 0);
called.length = 0;
assert.strictEqual(
await harness.navigation.runBackGuard({
submitting: true,
"block-submitting": () => called.push("block-submitting"),
}),
true,
"提交中阻止返回后返回值必须稳定为 true",
);
assert.deepStrictEqual(called, ["block-submitting"]);
assert.strictEqual(harness.calls.length, 0);
called.length = 0;
assert.strictEqual(
await harness.navigation.runBackGuard({
dirty: true,
"confirm-discard": () => {
called.push("confirm-discard");
return false;
},
}),
false,
"拒绝放弃时必须留在当前页",
);
assert.deepStrictEqual(called, ["confirm-discard"]);
assert.strictEqual(harness.calls.length, 0);
assert.strictEqual(
await harness.navigation.runBackGuard({
dirty: true,
"confirm-discard": async () => true,
}),
true,
);
assert.strictEqual(harness.calls[0].method, "navigateBack");
}
console.log("NAVIGATION-GATEWAY-RUNTIME-SMOKE PASS");
process.exitCode = 0;
})().catch((error) => {
console.error(error);
process.exitCode = 1;
});