2004 lines
68 KiB
JavaScript
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;
|
|
});
|