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
7 changes: 7 additions & 0 deletions packages/eslint/compat-headers.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// compat-headers.js 是给 rspack alias 用的 CommonJS 覆盖文件(见 rspack.config.ts),
// 这里只声明测试与类型检查需要的形状。
export declare const compatMap: {
localized: Record<string, unknown>;
unlocalized: Record<string, unknown>;
nonFunctional: Record<string, unknown>;
};
1 change: 1 addition & 0 deletions packages/eslint/compat-headers.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const compatMap = {
storageName: [],
"early-start": [],
"require-css": [],
definition: [],
allFrames: [],
},
};
Expand Down
6 changes: 6 additions & 0 deletions src/app/service/content/gm_api/gm_context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ export function GMContextApiGet(name: string): ApiValue[] | undefined {
return apis.get(name);
}

// 已注册的全部 @grant 名。注册表由装饰器在模块载入时填充,无法静态推导,
// 供 script_compat.ts 的静态支持表做一致性守卫(新增 GM API 若漏进表会被测出来)。
export function GMContextApiNames(): string[] {
return [...apis.keys()];
}

function GMContextApiSet(grant: string, fnKey: string, api: any, param: ApiParam): void {
// 一个 @grant 可以扩充多个 API 函数
let m: ApiValue[] | undefined = apis.get(grant);
Expand Down
10 changes: 9 additions & 1 deletion src/locales/de-DE/install.json
Original file line number Diff line number Diff line change
Expand Up @@ -254,5 +254,13 @@
"expired_title": "Update-Inhalt ist abgelaufen",
"expired_desc": "Der für dieses Update vorbereitete Code wurde bereinigt. Bitte erneut nach Updates suchen.",
"expired_recheck": "Erneut nach Updates suchen",
"code_loading": "Code wird geladen"
"code_loading": "Code wird geladen",
"compat_ineffective": "ohne Wirkung",
"compat_tag_desc": "ScriptCat unterstützt diese Deklaration nicht; sie wird nach der Installation ignoriert.",
"compat_grant_desc": "ScriptCat implementiert diese API nicht; Aufrufe schlagen fehl und darauf aufbauende Funktionen arbeiten nicht.",
"compat_jump": "Zu Zeile {{line}} springen",
"compat_docs": "Kompatibilitätsdokumentation",
"compat_count": "{{count}} ohne Wirkung",
"perm_other_label": "Weitere Deklarationen",
"perm_other_summary": "Vom Skript deklariert, wird von ScriptCat aber nicht ausgeführt"
}
10 changes: 9 additions & 1 deletion src/locales/en-US/install.json
Original file line number Diff line number Diff line change
Expand Up @@ -254,5 +254,13 @@
"expired_title": "Update content has expired",
"expired_desc": "The code prepared for this update has been cleaned up. Please check for updates again.",
"expired_recheck": "Check for updates again",
"code_loading": "Loading code"
"code_loading": "Loading code",
"compat_ineffective": "no effect",
"compat_tag_desc": "ScriptCat does not support this declaration; it is ignored after installation.",
"compat_grant_desc": "ScriptCat has not implemented this API; calls to it fail and features relying on it won't work.",
"compat_jump": "Go to line {{line}}",
"compat_docs": "Compatibility docs",
"compat_count": "{{count}} with no effect",
"perm_other_label": "Other declarations",
"perm_other_summary": "Declared by the script but not executed by ScriptCat"
}
10 changes: 9 additions & 1 deletion src/locales/ja-JP/install.json
Original file line number Diff line number Diff line change
Expand Up @@ -254,5 +254,13 @@
"expired_title": "更新内容の有効期限が切れました",
"expired_desc": "この更新のために用意されたコードは削除されました。もう一度更新を確認してください。",
"expired_recheck": "更新を再確認",
"code_loading": "コードを読み込み中"
"code_loading": "コードを読み込み中",
"compat_ineffective": "無効",
"compat_tag_desc": "ScriptCat はこの宣言に対応していないため、インストール後は無視されます。",
"compat_grant_desc": "ScriptCat はこの API を実装していないため、スクリプトから呼び出すとエラーになり、これに依存する機能は動作しません。",
"compat_jump": "{{line}} 行目へ移動",
"compat_docs": "互換性ドキュメント",
"compat_count": "無効な項目 {{count}} 件",
"perm_other_label": "その他の宣言",
"perm_other_summary": "宣言されていますが ScriptCat では実行されません"
}
10 changes: 9 additions & 1 deletion src/locales/ko-KR/install.json
Original file line number Diff line number Diff line change
Expand Up @@ -254,5 +254,13 @@
"expired_title": "업데이트 내용이 만료되었습니다",
"expired_desc": "이번 업데이트를 위해 준비한 코드가 정리되었습니다. 업데이트를 다시 확인해 주세요.",
"expired_recheck": "업데이트 다시 확인",
"code_loading": "코드를 불러오는 중"
"code_loading": "코드를 불러오는 중",
"compat_ineffective": "적용되지 않음",
"compat_tag_desc": "ScriptCat은 이 선언을 지원하지 않으므로 설치 후 무시됩니다.",
"compat_grant_desc": "ScriptCat은 이 API를 구현하지 않았습니다. 스크립트에서 호출하면 오류가 발생하고 이에 의존하는 기능은 동작하지 않습니다.",
"compat_jump": "{{line}}번째 줄로 이동",
"compat_docs": "호환성 문서",
"compat_count": "적용되지 않는 항목 {{count}}개",
"perm_other_label": "기타 선언",
"perm_other_summary": "선언되었지만 ScriptCat이 실행하지 않습니다"
}
10 changes: 9 additions & 1 deletion src/locales/pt-BR/install.json
Original file line number Diff line number Diff line change
Expand Up @@ -254,5 +254,13 @@
"expired_title": "O conteúdo da atualização expirou",
"expired_desc": "O código preparado para esta atualização foi removido. Verifique as atualizações novamente.",
"expired_recheck": "Verificar atualizações novamente",
"code_loading": "Carregando código"
"code_loading": "Carregando código",
"compat_ineffective": "sem efeito",
"compat_tag_desc": "O ScriptCat não oferece suporte a esta declaração; ela é ignorada após a instalação.",
"compat_grant_desc": "O ScriptCat não implementou esta API; as chamadas falham e os recursos que dependem dela não funcionam.",
"compat_jump": "Ir para a linha {{line}}",
"compat_docs": "Documentação de compatibilidade",
"compat_count": "{{count}} sem efeito",
"perm_other_label": "Outras declarações",
"perm_other_summary": "Declarado pelo script, mas não executado pelo ScriptCat"
}
10 changes: 9 additions & 1 deletion src/locales/ru-RU/install.json
Original file line number Diff line number Diff line change
Expand Up @@ -254,5 +254,13 @@
"expired_title": "Содержимое обновления устарело",
"expired_desc": "Код, подготовленный для этого обновления, был очищен. Проверьте обновления ещё раз.",
"expired_recheck": "Проверить обновления ещё раз",
"code_loading": "Загрузка кода"
"code_loading": "Загрузка кода",
"compat_ineffective": "не действует",
"compat_tag_desc": "ScriptCat не поддерживает это объявление — после установки оно игнорируется.",
"compat_grant_desc": "ScriptCat не реализует этот API: вызов из скрипта завершится ошибкой, а зависящие от него функции работать не будут.",
"compat_jump": "Перейти к строке {{line}}",
"compat_docs": "Документация по совместимости",
"compat_count": "Не действует: {{count}}",
"perm_other_label": "Прочие объявления",
"perm_other_summary": "Объявлено скриптом, но ScriptCat это не выполняет"
}
10 changes: 9 additions & 1 deletion src/locales/tr-TR/install.json
Original file line number Diff line number Diff line change
Expand Up @@ -254,5 +254,13 @@
"expired_title": "Güncelleme içeriğinin süresi doldu",
"expired_desc": "Bu güncelleme için hazırlanan kod temizlendi. Lütfen güncellemeleri yeniden denetleyin.",
"expired_recheck": "Güncellemeleri yeniden denetle",
"code_loading": "Kod yükleniyor"
"code_loading": "Kod yükleniyor",
"compat_ineffective": "etkisiz",
"compat_tag_desc": "ScriptCat bu bildirimi desteklemiyor; kurulumdan sonra yok sayılır.",
"compat_grant_desc": "ScriptCat bu API'yi uygulamadı; betik çağırdığında hata verir ve buna dayanan özellikler çalışmaz.",
"compat_jump": "{{line}}. satıra git",
"compat_docs": "Uyumluluk belgeleri",
"compat_count": "{{count}} etkisiz",
"perm_other_label": "Diğer bildirimler",
"perm_other_summary": "Betikte bildirildi ancak ScriptCat tarafından çalıştırılmıyor"
}
10 changes: 9 additions & 1 deletion src/locales/vi-VN/install.json
Original file line number Diff line number Diff line change
Expand Up @@ -254,5 +254,13 @@
"expired_title": "Nội dung cập nhật đã hết hạn",
"expired_desc": "Mã đã chuẩn bị cho lần cập nhật này đã bị dọn dẹp. Vui lòng kiểm tra cập nhật lại.",
"expired_recheck": "Kiểm tra cập nhật lại",
"code_loading": "Đang tải mã"
"code_loading": "Đang tải mã",
"compat_ineffective": "không có tác dụng",
"compat_tag_desc": "ScriptCat không hỗ trợ khai báo này; nó sẽ bị bỏ qua sau khi cài đặt.",
"compat_grant_desc": "ScriptCat chưa triển khai API này; script gọi đến sẽ báo lỗi và các tính năng phụ thuộc vào nó sẽ không hoạt động.",
"compat_jump": "Đến dòng {{line}}",
"compat_docs": "Tài liệu tương thích",
"compat_count": "{{count}} mục không có tác dụng",
"perm_other_label": "Khai báo khác",
"perm_other_summary": "Script có khai báo nhưng ScriptCat không thực thi"
}
10 changes: 9 additions & 1 deletion src/locales/zh-CN/install.json
Original file line number Diff line number Diff line change
Expand Up @@ -254,5 +254,13 @@
"expired_title": "更新内容已过期",
"expired_desc": "这次更新准备好的代码已被清理,需要重新检查一次更新。",
"expired_recheck": "重新检查更新",
"code_loading": "正在加载代码"
"code_loading": "正在加载代码",
"compat_ineffective": "不生效",
"compat_tag_desc": "脚本猫不支持该声明,安装后会被忽略。",
"compat_grant_desc": "脚本猫未实现该 API,脚本调用时会报错,依赖它的功能不可用。",
"compat_jump": "跳到第 {{line}} 行",
"compat_docs": "兼容性文档",
"compat_count": "{{count}} 项不生效",
"perm_other_label": "其他声明",
"perm_other_summary": "已声明,但脚本猫不会执行"
}
10 changes: 9 additions & 1 deletion src/locales/zh-TW/install.json
Original file line number Diff line number Diff line change
Expand Up @@ -254,5 +254,13 @@
"expired_title": "更新內容已過期",
"expired_desc": "這次更新準備好的程式碼已被清理,需要重新檢查一次更新。",
"expired_recheck": "重新檢查更新",
"code_loading": "正在載入程式碼"
"code_loading": "正在載入程式碼",
"compat_ineffective": "不生效",
"compat_tag_desc": "腳本貓不支援此宣告,安裝後會被忽略。",
"compat_grant_desc": "腳本貓尚未實作此 API,腳本呼叫時會出錯,依賴它的功能無法使用。",
"compat_jump": "跳至第 {{line}} 行",
"compat_docs": "相容性文件",
"compat_count": "{{count}} 項不生效",
"perm_other_label": "其他宣告",
"perm_other_summary": "已宣告,但腳本貓不會執行"
}
53 changes: 51 additions & 2 deletions src/pages/components/CodeEditor/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { render, cleanup, act, waitFor } from "@testing-library/react";
// 用 hoisted 持有可在测试内变更的主题与 monaco 桩,供被提升的 vi.mock 工厂引用
const h = vi.hoisted(() => {
const makeEditor = () => ({
revealLineInCenter: vi.fn(),
setSelection: vi.fn(),
setModel: vi.fn(),
setValue: vi.fn(),
updateOptions: vi.fn(),
Expand All @@ -18,7 +20,10 @@ const h = vi.hoisted(() => {
return {
resolvedTheme: "light" as string,
setTheme: vi.fn(),
createDiffEditor: vi.fn((_container?: unknown, _options?: any) => makeEditor()),
createDiffEditor: vi.fn((_container?: unknown, _options?: any) => {
const modified = makeEditor();
return { ...makeEditor(), getModifiedEditor: () => modified, __modified: modified };
}),
create: vi.fn((_container?: unknown, _options?: any) => makeEditor()),
createModel: vi.fn(() => ({
dispose: vi.fn(),
Expand All @@ -36,7 +41,14 @@ vi.mock("monaco-editor", () => ({
setTheme: h.setTheme,
setModelMarkers: vi.fn(),
},
Range: class {},
Range: class {
constructor(
public startLineNumber: number,
public startColumn: number,
public endLineNumber: number,
public endColumn: number
) {}
},
}));
vi.mock("./theme", () => ({ resolveMonacoTheme: (t: string) => t }));
vi.mock("@App/pkg/utils/monaco-editor", () => ({
Expand Down Expand Up @@ -126,3 +138,40 @@ describe("CodeEditor 可访问性与主题", () => {
expect(h.setTheme).toHaveBeenCalledWith("dark");
});
});

describe("CodeEditor 就绪信号与定位", () => {
it("内联 diff 也报告就绪——否则加载占位会一直留在无障碍树里说「正在加载」", async () => {
const onReady = vi.fn();
render(<CodeEditor id="ed-ready-diff" code="const a = 2;" diffCode="const a = 1;" onReady={onReady} />);
await waitFor(() => expect(onReady).toHaveBeenCalled());
});

it("普通编辑器同样报告就绪", async () => {
const onReady = vi.fn();
await act(async () => {
render(<CodeEditor id="ed-ready-plain" code="const a = 1;" diffCode="" onReady={onReady} />);
});
await waitFor(() => expect(onReady).toHaveBeenCalled());
});

it("revealLine 在普通编辑器上滚动并选中整行", async () => {
const ref = createRef<ComponentRef<typeof CodeEditor>>();
await act(async () => {
render(<CodeEditor ref={ref} id="ed-reveal" code={"a\nb\nc"} diffCode="" />);
});
await waitFor(() => expect(h.create).toHaveBeenCalled());
act(() => ref.current?.revealLine(2));
const instance = h.create.mock.results[0].value;
expect(instance.revealLineInCenter).toHaveBeenCalledWith(2);
expect(instance.setSelection).toHaveBeenCalled();
});

it("revealLine 在 diff 预览里定位到修改侧——诊断说的是新版本的那一行", async () => {
const ref = createRef<ComponentRef<typeof CodeEditor>>();
render(<CodeEditor ref={ref} id="ed-reveal-diff" code={"a\nb"} diffCode={"a"} />);
await waitFor(() => expect(h.createDiffEditor).toHaveBeenCalled());
act(() => ref.current?.revealLine(2));
const modified = (h.createDiffEditor.mock.results[0].value as any).__modified;
expect(modified.revealLineInCenter).toHaveBeenCalledWith(2);
});
});
31 changes: 28 additions & 3 deletions src/pages/components/CodeEditor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,25 @@ import { clearModelEslintFixes, getModelEslintFixKey } from "@App/pkg/utils/mona
import { useTheme } from "@App/pages/components/theme-provider";
import { resolveMonacoTheme } from "./theme";

export interface CodeEditorHandle {
/** 普通编辑器实例;diff 预览没有可编辑实例,为 undefined */
editor: editor.IStandaloneCodeEditor | undefined;
/** 滚动到指定行并选中整行;diff 预览定位到修改侧 */
revealLine: (line: number) => void;
}

type Props = {
ref?: Ref<{ editor: editor.IStandaloneCodeEditor | undefined }>;
ref?: Ref<CodeEditorHandle>;
className?: string;
diffCode?: string; // 代码加载是异步的:undefined=不确定(不加载),""=无 diff,有值=diff
editable?: boolean;
id: string;
code?: string;
onChange?: (code: string) => void;
/** 普通编辑器实例就绪;diff 预览没有可编辑实例,不会触发 */
onEditorMount?: (editor: editor.IStandaloneCodeEditor) => void;
/** 编辑器已创建(含 diff 预览);用于收起加载占位 */
onReady?: () => void;
};

type TMarker = {
Expand Down Expand Up @@ -44,7 +54,7 @@ function toMonacoEditorPreferenceOptions(
} satisfies editor.IEditorOptions;
}

function CodeEditor({ id, className, code, diffCode, editable, onChange, onEditorMount, ref }: Props) {
function CodeEditor({ id, className, code, diffCode, editable, onChange, onEditorMount, onReady, ref }: Props) {
const [monacoEditor, setEditor] = useState<editor.IStandaloneCodeEditor>();
const editorInstanceRef = useRef<editor.IStandaloneCodeEditor | editor.IStandaloneDiffEditor | undefined>(undefined);
// 普通 editor 与 diff editor 都会置位,供主题切换 effect 判断实例是否就绪
Expand All @@ -58,14 +68,27 @@ function CodeEditor({ id, className, code, diffCode, editable, onChange, onEdito
// 用 ref 保存最新回调,避免 stale closure 同时不让创建 effect 重跑
const onChangeRef = useRef(onChange);
const onEditorMountRef = useRef(onEditorMount);
const onReadyRef = useRef(onReady);
// ref 赋值须在创建 effect 之前,确保 mount 时创建 effect 同步读到最新 onEditorMount
useEffect(() => {
onChangeRef.current = onChange;
onEditorMountRef.current = onEditorMount;
onReadyRef.current = onReady;
});

const divRef = useRef<HTMLDivElement>(null);
useImperativeHandle(ref, () => ({ editor: monacoEditor }));
useImperativeHandle(ref, () => ({
editor: monacoEditor,
revealLine: (line: number) => {
const instance = editorInstanceRef.current;
if (!instance) return;
// diff 预览的行号说的是新版本,定位到修改侧
const target = "getModifiedEditor" in instance ? instance.getModifiedEditor() : instance;
target.revealLineInCenter(line);
const maxColumn = target.getModel()?.getLineMaxColumn(line) ?? 1;
target.setSelection(new Range(line, 1, line, maxColumn));
},
}));

// 注册 monaco 全局环境(只需执行一次)
useEffect(() => {
Expand Down Expand Up @@ -180,6 +203,7 @@ function CodeEditor({ id, className, code, diffCode, editable, onChange, onEdito
});
editorInstanceRef.current = edit;
editorReadyRef.current = true;
onReadyRef.current?.();
} else {
const standaloneEdit = editor.create(container, {
language: "javascript",
Expand All @@ -199,6 +223,7 @@ function CodeEditor({ id, className, code, diffCode, editable, onChange, onEdito
editorInstanceRef.current = standaloneEdit;
editorReadyRef.current = true;
onEditorMountRef.current?.(standaloneEdit);
onReadyRef.current?.();
}
});

Expand Down
32 changes: 32 additions & 0 deletions src/pages/install/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ vi.mock("./useInstallData", () => ({ useInstallData: vi.fn() }));
// Monaco 编辑器无法在 DOM 测试环境中渲染(需 worker + ThemeProvider),用桩替换
vi.mock("@App/pages/components/CodeEditor", () => import("@Tests/mocks/CodeEditor.tsx"));

import { revealLine } from "@Tests/mocks/CodeEditor";
import { useInstallData, type InstallView } from "./useInstallData";
import App from "./App";

Expand Down Expand Up @@ -41,11 +42,13 @@ const readyView = (over: Partial<InstallView> = {}): InstallView => ({
schedule: null,
code: "// a\n// b",
subscribeScripts: [],
compat: { grants: new Map(), tags: [] },
...over,
});

beforeEach(() => {
mockMatchMedia();
revealLine.calls.length = 0;
});

beforeAll(() => initTestLanguage("zh-CN"));
Expand Down Expand Up @@ -386,3 +389,32 @@ describe("Install App 不再渲染安全警示条", () => {
expect(screen.queryByTestId("install-warning-risk")).not.toBeInTheDocument();
});
});

describe("安装页的不生效标记", () => {
it("把不受支持的 GM 能力标在权限行上,点击跳到代码对应行", () => {
mockHook.mockReturnValue({
...baseHook(),
state: {
status: "ready",
view: readyView({
permissions: [{ kind: "grant", risk: "warn", values: ["GM_setValue", "GM_audio"], sensitive: [] }],
code: "// ==UserScript==\n// @name X\n// @grant GM_audio\n// ==/UserScript==",
compat: { grants: new Map([["GM_audio", 3]]), tags: [] },
}),
},
});
render(<App />);

const chip = screen.getByTestId("compat-chip");
expect(chip).toHaveTextContent("GM_audio");
fireEvent.click(chip);
expect(revealLine.calls).toEqual([3]);
});

it("没有不生效项时权限区一字不改", () => {
mockHook.mockReturnValue({ ...baseHook(), state: { status: "ready", view: readyView() } });
render(<App />);
expect(screen.queryByTestId("compat-chip")).not.toBeInTheDocument();
expect(screen.queryByTestId("permission-row-other")).not.toBeInTheDocument();
});
});
Loading
Loading