Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/app/service/service_worker/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ export class ScriptClient extends Client {
}

async openUpdatePageByUUID(uuid: string) {
return this.do<void>("openUpdatePageByUUID", uuid);
return this.do<boolean>("openUpdatePageByUUID", uuid);
}

async openBatchUpdatePage(opts: TOpenBatchUpdatePageOption) {
Expand Down
109 changes: 109 additions & 0 deletions src/app/service/service_worker/script.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,24 @@ import { ScriptClient } from "./client";
import { SELF_METADATA_ONLY_RUN_ON_URL } from "@App/app/repo/metadata";
import { BatchUpdateListActionCode } from "./types";
import { stackAsyncTask } from "@App/pkg/utils/async_queue";
import type * as ScriptUtils from "@App/pkg/utils/script";
import type * as Utils from "@App/pkg/utils/utils";

// 打开更新详情页会真的发网络请求并开标签页;这两处替换成可断言的桩
const h = vi.hoisted(() => ({
fetchScriptBody: vi.fn(),
openInCurrentTab: vi.fn(),
}));

vi.mock("@App/pkg/utils/script", async (importOriginal) => ({
...(await importOriginal<typeof ScriptUtils>()),
fetchScriptBody: h.fetchScriptBody,
}));

vi.mock("@App/pkg/utils/utils", async (importOriginal) => ({
...(await importOriginal<typeof Utils>()),
openInCurrentTab: h.openInCurrentTab,
}));

initTestEnv();

Expand Down Expand Up @@ -1434,3 +1452,94 @@ describe("ScriptService._checkScriptUpdate —— 检查期间脚本被更新",
expect(service["scriptUpdateCheck"].cacheFull?.list?.find((e) => e.uuid === "u-stable")?.checkUpdate).toBe(true);
});
});

describe("ScriptService.openUpdatePageByUUID —— 打开单条更新详情", () => {
const URL = "https://example.test/open.user.js";
const userscript = (version: string) =>
[
"// ==UserScript==",
"// @name 更新详情目标",
"// @namespace scriptcat-test",
`// @version ${version}`,
"// ==/UserScript==",
"console.log(1);",
].join("\n");

const saveTarget = async (service: ScriptService, scriptDAO: ScriptDAO) => {
await scriptDAO.save(
makeScript({
uuid: "u-open",
name: "更新详情目标",
namespace: "scriptcat-test",
metadata: { name: ["更新详情目标"], namespace: ["scriptcat-test"], version: ["1.0.0"] },
downloadUrl: URL,
checkUpdateUrl: URL,
checkUpdate: true,
})
);
await service.scriptCodeDAO.save({ uuid: "u-open", code: userscript("1.0.0") });
};

const primeCache = (service: ScriptService, newCode: string) =>
service["scriptUpdateCheck"].setCacheFull({
checktime: Date.now(),
list: [
{
uuid: "u-open",
checkUpdate: true,
oldCode: userscript("1.0.0"),
newCode,
newMeta: { version: ["2.0.0"], connect: [] },
script: makeScript({ uuid: "u-open", name: "更新详情目标", namespace: "scriptcat-test" }),
codeSimilarity: 0.9,
sites: [],
withNewConnect: false,
},
],
});

beforeEach(() => {
h.fetchScriptBody.mockReset();
h.openInCurrentTab.mockReset();
});

it("检查记录里已带新版代码时直接打开安装页,不再重新拉取脚本", async () => {
const { service, scriptDAO } = buildService();
await saveTarget(service, scriptDAO);
primeCache(service, userscript("2.0.0"));

await expect(service.openUpdatePageByUUID("u-open")).resolves.toBe(true);

expect(h.fetchScriptBody).not.toHaveBeenCalled();
expect(h.openInCurrentTab).toHaveBeenCalledWith("/src/install.html?uuid=u-open");
});

it("检查记录已失效时回退到网络拉取并照常打开安装页", async () => {
const { service, scriptDAO } = buildService();
await saveTarget(service, scriptDAO);
h.fetchScriptBody.mockResolvedValue(userscript("2.0.0"));

await expect(service.openUpdatePageByUUID("u-open")).resolves.toBe(true);

expect(h.fetchScriptBody).toHaveBeenCalledWith(URL);
expect(h.openInCurrentTab).toHaveBeenCalledWith("/src/install.html?uuid=u-open");
});

it("拉取失败时回报 false,让更新页能给出失败反馈而不是一直转圈", async () => {
const { service, scriptDAO } = buildService();
await saveTarget(service, scriptDAO);
h.fetchScriptBody.mockRejectedValue(new Error("network error"));

await expect(service.openUpdatePageByUUID("u-open")).resolves.toBe(false);

expect(h.openInCurrentTab).not.toHaveBeenCalled();
});

it("脚本已不存在时回报 false 而不是静默无反应", async () => {
const { service } = buildService();

await expect(service.openUpdatePageByUUID("missing")).resolves.toBe(false);

expect(h.openInCurrentTab).not.toHaveBeenCalled();
});
});
40 changes: 29 additions & 11 deletions src/app/service/service_worker/script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1168,9 +1168,20 @@ export class ScriptService {
options: { source: InstallSource; byWebRequest?: boolean },
update: boolean,
logger?: Logger
) {
return this.prepareUpdateOrInstallPage(uuid, await fetchScriptBody(url), url, options, update, logger);
}

/** 已经拿到脚本代码之后的处理:静默更新判定 + 写入待安装的临时代码,由调用方决定是否打开安装页 */
async prepareUpdateOrInstallPage(
uuid: string,
code: string,
url: string,
options: { source: InstallSource; byWebRequest?: boolean },
update: boolean,
logger?: Logger
) {
const upsertBy = options.source;
const code = await fetchScriptBody(url);
if (update) {
try {
const { oldScript, script } = await prepareScriptByCode(code, url, uuid);
Expand Down Expand Up @@ -1221,8 +1232,11 @@ export class ScriptService {
return 1;
}

// 打开更新窗口
public async openUpdatePage(script: Script, source: "user" | "system") {
/**
* 打开更新窗口。cachedNewCode 是检查阶段已经拉到的新版代码,命中时直接复用。
* @returns 是否已处理(打开了安装页或完成了静默更新)
*/
public async openUpdatePage(script: Script, source: "user" | "system", cachedNewCode?: string) {
const { uuid, name, downloadUrl, checkUpdateUrl } = script;
const logger = this.logger.with({
uuid,
Expand All @@ -1232,12 +1246,16 @@ export class ScriptService {
});
const url = downloadUrl || checkUpdateUrl!;
try {
const ret = await this.openUpdateOrInstallPage(uuid, url, { source }, true, logger);
if (ret === 2) return; // slience update
const ret = cachedNewCode
? await this.prepareUpdateOrInstallPage(uuid, cachedNewCode, url, { source }, true, logger)
: await this.openUpdateOrInstallPage(uuid, url, { source }, true, logger);
if (ret === 2) return true; // slience update
// 打开安装页面
openInCurrentTab(`/src/install.html?uuid=${uuid}`);
return true;
} catch (e) {
logger.error("fetch script info failed", Logger.E(e));
return false;
}
}

Expand Down Expand Up @@ -1766,12 +1784,12 @@ export class ScriptService {

async openUpdatePageByUUID(uuid: string) {
const source = "user"; // TBC
const oldScript = await this.scriptDAO.get(uuid);
if (!oldScript || oldScript.uuid !== uuid) return;
const { name, downloadUrl, checkUpdateUrl } = oldScript;
//@ts-ignore
const script = { uuid, name, downloadUrl, checkUpdateUrl } as Script;
await this.openUpdatePage(script, source);
const script = await this.scriptDAO.get(uuid);
if (!script || script.uuid !== uuid) return false;
// 检查记录里已经带着这次要装的新版代码:复用它既省掉一次让用户干等的网络往返,
// 也保证打开的正是列表上展示的那一版
const cachedNewCode = this.scriptUpdateCheck.cacheFull?.list?.find((entry) => entry.uuid === uuid)?.newCode;
return await this.openUpdatePage(script, source, cachedNewCode || undefined);
}

init() {
Expand Down
3 changes: 2 additions & 1 deletion src/locales/de-DE/install.json
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,8 @@
"batch_done": "{{count}} Skripte aktualisiert",
"batch_done_partial": "{{updated}} aktualisiert, {{failed}} fehlgeschlagen",
"view_updated_scripts": "Aktualisierte Skripte anzeigen",
"record_expired": "Die Update-Daten sind abgelaufen. Bitte erneut nach Updates suchen."
"record_expired": "Die Update-Daten sind abgelaufen. Bitte erneut nach Updates suchen.",
"open_failed": "Update-Details konnten nicht geöffnet werden. Bitte erneut versuchen."
},
"importpage": {
"title": "Datenimport",
Expand Down
3 changes: 2 additions & 1 deletion src/locales/en-US/install.json
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,8 @@
"batch_done": "Updated {{count}} scripts",
"batch_done_partial": "Updated {{updated}}, {{failed}} failed",
"view_updated_scripts": "View updated scripts",
"record_expired": "Update data has expired. Please check for updates again."
"record_expired": "Update data has expired. Please check for updates again.",
"open_failed": "Failed to open update details. Please try again."
},
"importpage": {
"title": "Data Import",
Expand Down
3 changes: 2 additions & 1 deletion src/locales/ja-JP/install.json
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,8 @@
"batch_done": "{{count}} 件を更新しました",
"batch_done_partial": "{{updated}} 件を更新、{{failed}} 件が失敗",
"view_updated_scripts": "更新したスクリプトを表示",
"record_expired": "更新データの有効期限が切れました。もう一度更新を確認してください"
"record_expired": "更新データの有効期限が切れました。もう一度更新を確認してください",
"open_failed": "更新の詳細を開けませんでした。もう一度お試しください"
},
"importpage": {
"title": "データインポート",
Expand Down
3 changes: 2 additions & 1 deletion src/locales/ko-KR/install.json
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,8 @@
"batch_done": "{{count}}개를 업데이트했습니다",
"batch_done_partial": "{{updated}}개 업데이트, {{failed}}개 실패",
"view_updated_scripts": "업데이트된 스크립트 보기",
"record_expired": "업데이트 데이터가 만료되었습니다. 업데이트를 다시 확인하세요"
"record_expired": "업데이트 데이터가 만료되었습니다. 업데이트를 다시 확인하세요",
"open_failed": "업데이트 상세 정보를 열지 못했습니다. 다시 시도하세요"
},
"importpage": {
"title": "데이터 가져오기",
Expand Down
3 changes: 2 additions & 1 deletion src/locales/pt-BR/install.json
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,8 @@
"batch_done": "{{count}} scripts atualizados",
"batch_done_partial": "{{updated}} atualizados, {{failed}} com falha",
"view_updated_scripts": "Ver scripts atualizados",
"record_expired": "Os dados de atualização expiraram. Verifique as atualizações novamente."
"record_expired": "Os dados de atualização expiraram. Verifique as atualizações novamente.",
"open_failed": "Falha ao abrir os detalhes da atualização. Tente novamente."
},
"importpage": {
"title": "Importação de dados",
Expand Down
3 changes: 2 additions & 1 deletion src/locales/ru-RU/install.json
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,8 @@
"batch_done": "Обновлено скриптов: {{count}}",
"batch_done_partial": "Обновлено: {{updated}}, с ошибкой: {{failed}}",
"view_updated_scripts": "Показать обновлённые скрипты",
"record_expired": "Данные об обновлениях устарели. Проверьте обновления заново."
"record_expired": "Данные об обновлениях устарели. Проверьте обновления заново.",
"open_failed": "Не удалось открыть сведения об обновлении. Попробуйте ещё раз."
},
"importpage": {
"title": "Импорт данных",
Expand Down
3 changes: 2 additions & 1 deletion src/locales/tr-TR/install.json
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,8 @@
"batch_done": "{{count}} betik güncellendi",
"batch_done_partial": "{{updated}} güncellendi, {{failed}} başarısız",
"view_updated_scripts": "Güncellenen betikleri gör",
"record_expired": "Güncelleme verileri geçersiz oldu. Lütfen güncellemeleri yeniden denetleyin."
"record_expired": "Güncelleme verileri geçersiz oldu. Lütfen güncellemeleri yeniden denetleyin.",
"open_failed": "Güncelleme ayrıntıları açılamadı. Lütfen yeniden deneyin."
},
"importpage": {
"title": "Veri İçe Aktarma",
Expand Down
3 changes: 2 additions & 1 deletion src/locales/vi-VN/install.json
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,8 @@
"batch_done": "Đã cập nhật {{count}} tập lệnh",
"batch_done_partial": "Đã cập nhật {{updated}}, {{failed}} thất bại",
"view_updated_scripts": "Xem tập lệnh đã cập nhật",
"record_expired": "Dữ liệu cập nhật đã hết hiệu lực. Vui lòng kiểm tra cập nhật lại."
"record_expired": "Dữ liệu cập nhật đã hết hiệu lực. Vui lòng kiểm tra cập nhật lại.",
"open_failed": "Không mở được chi tiết cập nhật. Vui lòng thử lại."
},
"importpage": {
"title": "Nhập dữ liệu",
Expand Down
3 changes: 2 additions & 1 deletion src/locales/zh-CN/install.json
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,8 @@
"batch_done": "已更新 {{count}} 个",
"batch_done_partial": "已更新 {{updated}} 个,{{failed}} 个失败",
"view_updated_scripts": "查看更新的脚本",
"record_expired": "更新数据已过期,请重新检查更新"
"record_expired": "更新数据已过期,请重新检查更新",
"open_failed": "打开更新详情失败,请重试"
},
"importpage": {
"title": "数据导入",
Expand Down
3 changes: 2 additions & 1 deletion src/locales/zh-TW/install.json
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,8 @@
"batch_done": "已更新 {{count}} 個",
"batch_done_partial": "已更新 {{updated}} 個,{{failed}} 個失敗",
"view_updated_scripts": "檢視已更新的腳本",
"record_expired": "更新資料已過期,請重新檢查更新"
"record_expired": "更新資料已過期,請重新檢查更新",
"open_failed": "開啟更新詳情失敗,請重試"
},
"importpage": {
"title": "資料匯入",
Expand Down
30 changes: 30 additions & 0 deletions src/pages/batchupdate/components.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ function mkView(p: Partial<BatchUpdateViewProps> = {}): BatchUpdateViewProps {
autoClose: null,
autoCloseCancelled: false,
rowStates: {},
opening: new Set(),
batchProgress: null,
recordExpired: false,
onToggle: () => {},
Expand Down Expand Up @@ -358,3 +359,32 @@ describe("批量更新 全部恢复并更新的确认", () => {
expect(screen.getByTestId("ignored-restore-all")).toHaveTextContent(t("install:updatepage.restore_all"));
});
});

describe("批量更新 打开更新详情进行中", () => {
it("桌面行在打开期间转圈并拒绝再次点击", () => {
const onOpen = vi.fn();
renderDesktop({ updates: [mkItem()], opening: new Set(["u1"]), onOpen });

const name = screen.getByTestId("script-name");
expect(name).toHaveAttribute("aria-busy", "true");
expect(name.querySelector(".animate-spin")).toBeTruthy();

fireEvent.click(name);
expect(onOpen).not.toHaveBeenCalled();
});

it("未在打开时点击脚本名进入更新详情", () => {
const onOpen = vi.fn();
renderDesktop({ updates: [mkItem()], opening: new Set(), onOpen });

fireEvent.click(screen.getByTestId("script-name"));

expect(onOpen).toHaveBeenCalledWith("u1");
});

it("移动卡片在打开期间同样标记为进行中", () => {
renderMobile({ updates: [mkItem()], opening: new Set(["u1"]) });

expect(screen.getByTestId("script-name")).toHaveAttribute("aria-busy", "true");
});
});
Loading
Loading