const test = require("node:test"); const assert = require("node:assert/strict"); const fs = require("node:fs"); const vm = require("node:vm"); const tick = () => new Promise((resolve) => setImmediate(resolve)); function runtime(cachedToken = "") { let app; const storage = new Map(cachedToken ? [["pet_token", cachedToken]] : []); const requests = [], logins = []; const wx = { getStorageSync: (key) => storage.get(key), setStorageSync: (key, value) => storage.set(key, value), removeStorageSync: (key) => storage.delete(key), login: (options) => logins.push(options), request: (options) => { requests.push(options); if (options.url.endsWith("/auth/login")) options.success({ statusCode: 200, data: { code: 0, data: { token: "fresh-token", user: { nickname: "毛孩家长9527" } }, }, }); else if (options.header.Authorization === "Bearer fresh-token") options.success({ statusCode: 200, data: { code: 0, data: "ok" } }); }, }; const module = { exports: {} }; vm.runInNewContext(fs.readFileSync("pets-fe/utils/api.js", "utf8"), { wx, getApp: () => app, require: () => ({ baseURL: "https://test.local/api" }), module, }); vm.runInNewContext(fs.readFileSync("pets-fe/app.js", "utf8"), { wx, App: (value) => { app = value; }, require: () => module.exports, }); return { app, api: module.exports, requests, logins, storage }; } test("启动即登录,多个首页请求共用登录任务并携带 token", async () => { const r = runtime(); r.app.onLaunch(); const pending = [ r.api.secure("/pets"), r.api.secure("/tasks"), r.api.secure("/me"), ]; assert.equal(r.logins.length, 1); assert.equal(r.requests.length, 0); r.logins[0].success({ code: "wechat-code" }); assert.deepEqual(await Promise.all(pending), ["ok", "ok", "ok"]); assert.equal( r.requests.filter((req) => req.url.endsWith("/auth/login")).length, 1, ); assert.ok( r.requests .slice(1) .every((req) => req.header.Authorization === "Bearer fresh-token"), ); assert.equal(r.storage.get("pet_token"), "fresh-token"); }); test("缓存 token 过期时只重新登录一次,延迟的旧 401 不清除新 token", async () => { const r = runtime("expired"); r.app.onLaunch(); const first = r.api.secure("/pets"), second = r.api.secure("/tasks"); await tick(); const old = r.requests.slice(); assert.equal(old.length, 2); old[0].success({ statusCode: 401, data: { message: "expired" } }); await tick(); assert.equal(r.logins.length, 1); r.logins[0].success({ code: "new-code" }); await first; old[1].success({ statusCode: 401, data: { message: "expired" } }); await second; assert.equal(r.app.globalData.token, "fresh-token"); assert.equal(r.logins.length, 1); }); function home(login, secure, app = { login }) { let definition; const wx = { getStorageSync: () => [], navigateTo: () => {} }; vm.runInNewContext(fs.readFileSync("pets-fe/pages/home/home.js", "utf8"), { wx, Date, getApp: () => app, Page: (value) => { definition = value; }, require: (name) => name.includes("environment") ? { cached: () => null } : name.includes("date") ? { localDate: () => "2026-10-05" } : { secure, toast: () => {} }, }); const page = { ...definition, data: { ...definition.data }, setData(value) { Object.assign(this.data, value); }, }; page.refreshEnvironment = () => new Promise(() => {}); return page; } function data(path) { return Promise.resolve( path === "/pets" || path === "/walks" ? [] : path === "/tasks" ? { items: [] } : { nickname: "毛孩家长9527" }, ); } test("登录和数据都准备好才显示首页,天气未完成也能进入", async () => { let finishLogin, calls = 0; const login = new Promise((resolve) => { finishLogin = resolve; }); const page = home( () => login, (path) => { calls++; return data(path); }, ); const loading = page.load(); page.load(); assert.equal(page.data.phase, "auth"); assert.equal(page.data.ready, false); assert.equal(calls, 0); finishLogin("token"); await loading; assert.equal(calls, 4); assert.equal(page.data.ready, true); assert.equal(page.data.loading, false); assert.equal(page.data.phase, "ready"); assert.equal(page.data.pets.length, 0); }); test("首次登录失败可重试,失败期间不请求业务数据", async () => { let attempts = 0, calls = 0; const page = home( async () => { if (++attempts === 1) throw new Error("无法连接"); return "token"; }, (path) => { calls++; return data(path); }, ); await page.load(); assert.equal(page.data.phase, "auth-error"); assert.equal(page.data.ready, false); assert.equal(calls, 0); await page.load(); assert.equal(page.data.ready, true); assert.equal(page.data.error, ""); }); test("首页请求失败不显示空数据状态,刷新失败保留已有记录", async () => { let fail = true; const page = home( async () => "token", (path) => (fail ? Promise.reject(new Error("网络异常")) : data(path)), ); await page.load(); assert.equal(page.data.phase, "data-error"); assert.equal(page.data.ready, false); fail = false; await page.load(); assert.equal(page.data.ready, true); page.setData({ pets: [{ id: "existing" }] }); fail = true; await page.load(); assert.equal(page.data.ready, true); assert.equal(page.data.pets[0].id, "existing"); assert.equal(page.data.error, "网络异常"); }); test("从其他页面回到日常立即显示已有数据,后台刷新不清空内容", async () => { const app = { login: async () => "token", globalData: { token: "token", user: { id: "user-1" } } }; const first = home(app.login, data, app); await first.load(); first.setData({ pets: [{ id: "pet-1" }], tasks: [{ id: "task-1", completed: true }], undoneCount: 0 }); first.onUnload(); let finish; const pending = new Promise((resolve) => { finish = resolve; }); const next = home(app.login, async (route) => { await pending; return data(route); }, app); next.onLoad(); assert.equal(next.data.ready, true); assert.equal(next.data.phase, "ready"); assert.equal(next.data.pets[0].id, "pet-1"); assert.equal(next.data.tasks[0].completed, true); const refreshing = next.load(); await tick(); assert.equal(next.data.ready, true); assert.equal(next.data.pets[0].id, "pet-1"); finish(); await refreshing; assert.equal(next.data.loading, false); }); test("日常缓存不跨账号、不显示上一天的任务", async () => { const app = { login: async () => "token", globalData: { user: { id: "new-user" } }, homeSnapshot: { account: "old-user", date: "2026-10-05", data: { pets: [{ id: "private" }] }, } }; const page = home(app.login, data, app); page.onLoad(); assert.equal(page.data.ready, false); assert.equal(page.data.pets.length, 0); app.homeSnapshot.account = "new-user"; app.homeSnapshot.date = "2026-10-04"; page.onLoad(); assert.equal(page.data.ready, false); });