"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} 生产合同不得保留未落地写接口的成功结果`, ); } { 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: [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; });