Skip to content

Commit 999d2cd

Browse files
authored
🐛 批量更新页打开更新详情复用检查缓存并挡住重复点击 (#1719)
* 🐛 批量更新页打开更新详情复用检查缓存并挡住重复点击 点击脚本名查看更新时,openUpdatePageByUUID 会重新 fetch 一次脚本代码, 而这份新版代码在检查更新阶段已经存进 scriptUpdateCheck 的记录缓存里 (行内「更新」按钮装的就是它)。用户因此要为每次点击白等一次网络往返, 期间页面又没有任何反馈,连点几下就会开出多个安装页。 - SW: 拆出 prepareUpdateOrInstallPage,openUpdatePage 命中缓存代码时 跳过 fetchScriptBody;openUpdatePageByUUID 改为回报 boolean - 页面: 打开期间行内转圈并同步挡住重复点击,失败弹 toast, 点击脚本名同样取消自动关闭倒计时 * 🐛 更新页与安装页补齐骨架屏与异步中间态,失败不再被渲染成成功 (#1721) * 🐛 打开更新详情区分静默更新,忽略动作补齐逐条回执 页面此前无从判断服务端到底做了什么:openUpdatePageByUUID 在命中静默更新时 不开安装页却同样返回 true,用户点完脚本名只看到转一圈、什么都没发生; IGNORE 分支根本没有返回值,页面只能 fire-and-forget。 - openUpdatePageByUUID / openUpdatePage 返回 "opened" | "silent" | "failed" - IGNORE 逐条回报结果。忽略写的是脚本自身的 ignoreVersion,与检查缓存无关, 因此缓存随 Service Worker 回收后忽略照样生效,这里如实回报而不是谎报失效 - checkScriptUpdate 的结果收敛成 TCheckScriptUpdateResult 并用 reason 区分 「已有检查在跑」与真正的失败,页面才能分别提示 * 🐛 安装页补齐加载分档、代码骨架与提交忙态 从批量更新页点脚本名进来的必然是「更新」,加载屏却把上下文 chip 写死成 「脚本安装」,几百毫秒后再闪成「脚本更新」;描述写着「正在从来源下载」, 但这条入口的代码 Service Worker 早已备好,根本不下载。 - 状态屏按来路分档,未确知场景不渲染 chip(不猜),并补一条与就绪态操作栏 等高的底部占位,避免就绪瞬间内容区高度再跳一次 - 暂存代码被定时清理回收时落到专属终态,出口换成「重新检查更新」—— 原来的「重试」在这个最常见的失败原因下重试多少次都是同一结果 - Monaco 实例就绪前渲染代码骨架,替代此前 340px 的纯空白 - toggleWatch / rejectExternalAccess 补忙态,install 加重入守卫: 这两个动作全程不置忙态,连点会发出两次安装/两次决定 * 🐛 批量更新页补齐取数失败、检查空窗期与忽略/批量的中间态 取数失败时记录仍是空的,页面直接走到空态,把一次加载失败渲染成 「所有脚本均为最新(已检查 0 个脚本)」这条与事实相反的成功终态; 点「检查更新」到服务端广播回来之间页面完全静止,期间可以连点。 - 取数失败落错误终态:等宽 detail 框 + 重试 / 脚本列表出口 - 主动检查由本地 pending 立刻接管忙态,并把服务端的「正忙」「结果够新已跳过」 「通道异常」三条回执分别说出来;跳过时就地清掉待反馈标记, 否则会在下一次后台检查完成时冒出一条用户没点过的 toast - 忽略复用与更新相同的行级阶段(working → success → 退场),不再 fire-and-forget - 批量进行中互斥(行内勾选、两个批量按钮、全部恢复),避免两条进度互相覆盖; 被「结果失效」中断时保留已完成条数,不把汇总抹掉 - 骨架补齐工具条(桌面)与顶部选择栏/底部操作栏(移动)占位,消除数据到达时的 布局跳动,并加 role="status" / aria-busy;空态下重新检查不再整页闪回骨架 - 脚本名改用 aria-disabled + onClick 早退:disabled 会让浏览器不派发指针事件, 正好在名字被截断、最需要看全名时把 tooltip 一起关掉,键盘触发后焦点还会掉到 body
1 parent 58391fb commit 999d2cd

28 files changed

Lines changed: 1711 additions & 198 deletions

src/app/service/service_worker/client.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,15 @@ import { type Resource } from "@App/app/repo/resource";
33
import { type Subscribe } from "@App/app/repo/subscribe";
44
import { type Logger } from "@App/app/repo/logger";
55
import { type Permission } from "@App/app/repo/permission";
6-
import type { InstallSource, ScriptMenu, ScriptMenuItem, TBatchUpdateListAction, TPopupPageStatus } from "./types";
6+
import type {
7+
InstallSource,
8+
ScriptMenu,
9+
ScriptMenuItem,
10+
TBatchUpdateListAction,
11+
TCheckScriptUpdateResult,
12+
TOpenUpdatePageResult,
13+
TPopupPageStatus,
14+
} from "./types";
715
import { Client } from "@Packages/message/client";
816
import type { MessageSend } from "@Packages/message/types";
917
import type PermissionVerify from "./permission_verify";
@@ -273,15 +281,15 @@ export class ScriptClient extends Client {
273281
}
274282

275283
async openUpdatePageByUUID(uuid: string) {
276-
return this.do<void>("openUpdatePageByUUID", uuid);
284+
return this.do<TOpenUpdatePageResult>("openUpdatePageByUUID", uuid);
277285
}
278286

279287
async openBatchUpdatePage(opts: TOpenBatchUpdatePageOption) {
280288
return this.do<boolean>("openBatchUpdatePage", opts);
281289
}
282290

283291
async checkScriptUpdate(opts: TCheckScriptUpdateOption) {
284-
return this.do<void>("checkScriptUpdate", opts);
292+
return this.do<TCheckScriptUpdateResult>("checkScriptUpdate", opts);
285293
}
286294
}
287295

src/app/service/service_worker/script.test.ts

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,24 @@ import { ScriptClient } from "./client";
2323
import { SELF_METADATA_ONLY_RUN_ON_URL } from "@App/app/repo/metadata";
2424
import { BatchUpdateListActionCode } from "./types";
2525
import { stackAsyncTask } from "@App/pkg/utils/async_queue";
26+
import type * as ScriptUtils from "@App/pkg/utils/script";
27+
import type * as Utils from "@App/pkg/utils/utils";
28+
29+
// 打开更新详情页会真的发网络请求并开标签页;这两处替换成可断言的桩
30+
const h = vi.hoisted(() => ({
31+
fetchScriptBody: vi.fn(),
32+
openInCurrentTab: vi.fn(),
33+
}));
34+
35+
vi.mock("@App/pkg/utils/script", async (importOriginal) => ({
36+
...(await importOriginal<typeof ScriptUtils>()),
37+
fetchScriptBody: h.fetchScriptBody,
38+
}));
39+
40+
vi.mock("@App/pkg/utils/utils", async (importOriginal) => ({
41+
...(await importOriginal<typeof Utils>()),
42+
openInCurrentTab: h.openInCurrentTab,
43+
}));
2644

2745
initTestEnv();
2846

@@ -1434,3 +1452,159 @@ describe("ScriptService._checkScriptUpdate —— 检查期间脚本被更新",
14341452
expect(service["scriptUpdateCheck"].cacheFull?.list?.find((e) => e.uuid === "u-stable")?.checkUpdate).toBe(true);
14351453
});
14361454
});
1455+
1456+
describe("ScriptService.openUpdatePageByUUID —— 打开单条更新详情", () => {
1457+
const URL = "https://example.test/open.user.js";
1458+
const userscript = (version: string) =>
1459+
[
1460+
"// ==UserScript==",
1461+
"// @name 更新详情目标",
1462+
"// @namespace scriptcat-test",
1463+
`// @version ${version}`,
1464+
"// ==/UserScript==",
1465+
"console.log(1);",
1466+
].join("\n");
1467+
1468+
const saveTarget = async (service: ScriptService, scriptDAO: ScriptDAO) => {
1469+
await scriptDAO.save(
1470+
makeScript({
1471+
uuid: "u-open",
1472+
name: "更新详情目标",
1473+
namespace: "scriptcat-test",
1474+
metadata: { name: ["更新详情目标"], namespace: ["scriptcat-test"], version: ["1.0.0"] },
1475+
downloadUrl: URL,
1476+
checkUpdateUrl: URL,
1477+
checkUpdate: true,
1478+
})
1479+
);
1480+
await service.scriptCodeDAO.save({ uuid: "u-open", code: userscript("1.0.0") });
1481+
};
1482+
1483+
const primeCache = (service: ScriptService, newCode: string) =>
1484+
service["scriptUpdateCheck"].setCacheFull({
1485+
checktime: Date.now(),
1486+
list: [
1487+
{
1488+
uuid: "u-open",
1489+
checkUpdate: true,
1490+
oldCode: userscript("1.0.0"),
1491+
newCode,
1492+
newMeta: { version: ["2.0.0"], connect: [] },
1493+
script: makeScript({ uuid: "u-open", name: "更新详情目标", namespace: "scriptcat-test" }),
1494+
codeSimilarity: 0.9,
1495+
sites: [],
1496+
withNewConnect: false,
1497+
},
1498+
],
1499+
});
1500+
1501+
beforeEach(() => {
1502+
h.fetchScriptBody.mockReset();
1503+
h.openInCurrentTab.mockReset();
1504+
});
1505+
1506+
it("检查记录里已带新版代码时直接打开安装页,不再重新拉取脚本", async () => {
1507+
const { service, scriptDAO } = buildService();
1508+
await saveTarget(service, scriptDAO);
1509+
primeCache(service, userscript("2.0.0"));
1510+
1511+
await expect(service.openUpdatePageByUUID("u-open")).resolves.toBe("opened");
1512+
1513+
expect(h.fetchScriptBody).not.toHaveBeenCalled();
1514+
expect(h.openInCurrentTab).toHaveBeenCalledWith("/src/install.html?uuid=u-open");
1515+
});
1516+
1517+
it("检查记录已失效时回退到网络拉取并照常打开安装页", async () => {
1518+
const { service, scriptDAO } = buildService();
1519+
await saveTarget(service, scriptDAO);
1520+
h.fetchScriptBody.mockResolvedValue(userscript("2.0.0"));
1521+
1522+
await expect(service.openUpdatePageByUUID("u-open")).resolves.toBe("opened");
1523+
1524+
expect(h.fetchScriptBody).toHaveBeenCalledWith(URL);
1525+
expect(h.openInCurrentTab).toHaveBeenCalledWith("/src/install.html?uuid=u-open");
1526+
});
1527+
1528+
it("拉取失败时回报 failed,让更新页能给出失败反馈而不是一直转圈", async () => {
1529+
const { service, scriptDAO } = buildService();
1530+
await saveTarget(service, scriptDAO);
1531+
h.fetchScriptBody.mockRejectedValue(new Error("network error"));
1532+
1533+
await expect(service.openUpdatePageByUUID("u-open")).resolves.toBe("failed");
1534+
1535+
expect(h.openInCurrentTab).not.toHaveBeenCalled();
1536+
});
1537+
1538+
it("脚本已不存在时回报 failed 而不是静默无反应", async () => {
1539+
const { service } = buildService();
1540+
1541+
await expect(service.openUpdatePageByUUID("missing")).resolves.toBe("failed");
1542+
1543+
expect(h.openInCurrentTab).not.toHaveBeenCalled();
1544+
});
1545+
1546+
it("命中静默更新时回报 silent:不开安装页,由调用方补一条反馈", async () => {
1547+
const { service, scriptDAO, systemConfig } = buildService();
1548+
await saveTarget(service, scriptDAO);
1549+
systemConfig.setSilenceUpdateScript(true);
1550+
primeCache(service, userscript("2.0.0"));
1551+
1552+
await expect(service.openUpdatePageByUUID("u-open")).resolves.toBe("silent");
1553+
1554+
// 静默更新是真的装了,只是页面上什么都不会发生
1555+
expect(h.openInCurrentTab).not.toHaveBeenCalled();
1556+
expect((await scriptDAO.get("u-open"))?.metadata.version?.[0]).toBe("2.0.0");
1557+
});
1558+
});
1559+
1560+
describe("ScriptService.batchUpdateListAction —— 忽略更新", () => {
1561+
const saveIgnoreTarget = (scriptDAO: ScriptDAO) =>
1562+
scriptDAO.save(
1563+
makeScript({
1564+
uuid: "u-ignore",
1565+
name: "忽略目标",
1566+
namespace: "scriptcat-test",
1567+
metadata: { name: ["忽略目标"], namespace: ["scriptcat-test"], version: ["1.0.0"] },
1568+
})
1569+
);
1570+
1571+
it("逐条回报忽略结果,页面据此收起该行", async () => {
1572+
const { service, scriptDAO } = buildService();
1573+
await saveIgnoreTarget(scriptDAO);
1574+
1575+
const res = await service.batchUpdateListAction({
1576+
actionCode: BatchUpdateListActionCode.IGNORE,
1577+
actionPayload: [{ uuid: "u-ignore", ignoreVersion: "2.0.0" }],
1578+
});
1579+
1580+
expect(res).toEqual({ ok: true, items: [{ uuid: "u-ignore", success: true }] });
1581+
expect((await scriptDAO.get("u-ignore"))?.ignoreVersion).toBe("2.0.0");
1582+
});
1583+
1584+
it("检查缓存已随 Service Worker 回收时,忽略照样生效并照常回报", async () => {
1585+
const { service, scriptDAO } = buildService();
1586+
await saveIgnoreTarget(scriptDAO);
1587+
// 忽略写的是脚本自身的 ignoreVersion,与检查缓存无关
1588+
expect(service["scriptUpdateCheck"].cacheFull).toBeFalsy();
1589+
1590+
const res = await service.batchUpdateListAction({
1591+
actionCode: BatchUpdateListActionCode.IGNORE,
1592+
actionPayload: [{ uuid: "u-ignore", ignoreVersion: "2.0.0" }],
1593+
});
1594+
1595+
expect(res).toEqual({ ok: true, items: [{ uuid: "u-ignore", success: true }] });
1596+
expect((await scriptDAO.get("u-ignore"))?.ignoreVersion).toBe("2.0.0");
1597+
});
1598+
1599+
it("脚本已不存在时该条回报失败,而不是静默当作成功", async () => {
1600+
const { service } = buildService();
1601+
1602+
const res = await service.batchUpdateListAction({
1603+
actionCode: BatchUpdateListActionCode.IGNORE,
1604+
actionPayload: [{ uuid: "missing", ignoreVersion: "2.0.0" }],
1605+
});
1606+
1607+
expect(res?.ok).toBe(true);
1608+
expect(res?.items[0]).toMatchObject({ uuid: "missing", success: false });
1609+
});
1610+
});

0 commit comments

Comments
 (0)