Skip to content

Commit 65775cb

Browse files
authored
🔧 通用修复与基础设施改进 (#1328)
* 🔧 通用基础设施改进 - CI: build/test 流水线支持 feature/* 分支触发 - vitest: 修正 exclude 模式,确保子目录 node_modules/.claude 被排除 - message/server: SenderRuntime.getExtMessageSender 添加 null 检查, 防止 postMessage 通道(Offscreen→SW)因无 RuntimeMessageSender 崩溃 - message/window_message: connect() 使用 WindowPostMessage 包装, 确保 sandbox(origin:null)→offscreen 的消息不被丢弃 * 🐛 通用修复与代码规范化 - confirm/App.tsx: 定时器从 setTimeout 递归改为 useEffect+setInterval, 修复内存泄漏和渲染循环;提取 PermissionConfirmRequest 组件 - utils.ts: sourceMapTo 添加 chrome.runtime?.getURL 防御, sandbox 环境中 chrome.runtime 不可用时降级为脚本名 - 5 处 eslint-disable react-hooks/exhaustive-deps 注释 - prettier 格式化:CSS/HTML/JSON 文件统一缩进和引号 - .prettierignore: 忽略 *.md 和 example/ - manifest.json: 纯格式化(prettier) - playwright.config.ts: 本地也启用 retry * ⏪ 撤回 eslint-disable 注释和 playwright retry 改动 - 移除 6 处 react-hooks/exhaustive-deps 的 eslint-disable(仅 warn 无需抑制) - 还原 playwright retries 为 CI ? 1 : 0 * ⏪ 还原格式化改动,保持 release/v1.4 原有格式 CSS/HTML/JSON/manifest 等文件还原为 release/v1.4 原始格式 * 🧪 补充 message 包单测:sender 兜底 + connect targetOrigin 根据 PR #1328 Copilot 评审意见,补充两个单测: - SenderRuntime.getExtMessageSender() 在 sender 为 null/undefined/空对象时不崩溃并返回默认值 - WindowMessage.connect() 返回的连接 sendMessage 带 "*" targetOrigin * 🐛 修复权限确认页 beforeunload 监听器泄漏 - 拆分 useEffect:beforeunload 注册与 getPermissionInfo 请求独立 - 提取命名 handler,在 cleanup 中 removeEventListener - beforeunload effect 只依赖 [uuid],避免语言切换时重复注册 - .gitignore 忽略 .omc 本地开发目录 根据 PR #1328 @cyfung1031 评审意见处理 * 🐛 修复 UnoCSS 暗色模式默认边框色 暗色模式下 UnoCSS 的 --un-default-border-color 默认使用浅色值, 导致 border-* 类的边框在暗色主题下与背景对比不足。 将其绑定到 Arco CSS 变量 --color-border-2,让边框色随主题自适应。 * 🧪 navigation_handle: 用 resetAttachedForTest 替代 vi.resetModules vi.resetModules() 会清空全局模块缓存,导致后续测试重新加载 LoggerCore、 chrome mock 等模块时出错,在 CI 上表现为偶发失败。 改为导出 resetAttachedForTest() 重置模块级 attached 单例, 测试无需动态 import 即可验证多次调用的幂等性。
1 parent 3e975c0 commit 65775cb

File tree

14 files changed

+130
-38
lines changed

14 files changed

+130
-38
lines changed

.github/workflows/build.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ on:
55
branches:
66
- main
77
- release/*
8+
- feature/*
89
- dev
910
paths-ignore:
1011
- ".github/**"

.github/workflows/test.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ on:
55
branches:
66
- main
77
- release/*
8+
- feature/*
89
- dev
910
- develop/*
1011
pull_request:

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,4 @@ test-results
4141
playwright-report
4242

4343
superpowers
44+
.omc

.prettierignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,7 @@ yarn.lock
55

66
# Claude Code
77
.claude
8+
9+
# Docs & examples
10+
*.md
11+
example/

packages/message/server.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,32 @@ describe("Server", () => {
540540
const extSender = capturedSender!.getExtMessageSender();
541541
expect(extSender.tabId).toBe(-1);
542542
});
543+
544+
it.concurrent("sender 为 null/undefined 时不崩溃并返回默认值", async () => {
545+
// postMessage 通道(如 Offscreen→SW)传入空对象作为 sender,
546+
// SenderRuntime.getExtMessageSender() 应该返回默认兜底值
547+
const senderNull = new SenderRuntime(null as unknown as RuntimeMessageSender);
548+
const extNull = senderNull.getExtMessageSender();
549+
expect(extNull.windowId).toBe(-1);
550+
expect(extNull.tabId).toBe(-1);
551+
expect(extNull.frameId).toBeUndefined();
552+
expect(extNull.documentId).toBeUndefined();
553+
554+
const senderUndefined = new SenderRuntime(undefined as unknown as RuntimeMessageSender);
555+
const extUndefined = senderUndefined.getExtMessageSender();
556+
expect(extUndefined.windowId).toBe(-1);
557+
expect(extUndefined.tabId).toBe(-1);
558+
expect(extUndefined.frameId).toBeUndefined();
559+
expect(extUndefined.documentId).toBeUndefined();
560+
561+
// 空对象(ServiceWorkerMessageSend 实际传入的值)也应正常处理
562+
const senderEmpty = new SenderRuntime({} as RuntimeMessageSender);
563+
const extEmpty = senderEmpty.getExtMessageSender();
564+
expect(extEmpty.windowId).toBe(-1);
565+
expect(extEmpty.tabId).toBe(-1);
566+
expect(extEmpty.frameId).toBeUndefined();
567+
expect(extEmpty.documentId).toBeUndefined();
568+
});
543569
});
544570

545571
describe("Connect 功能测试", () => {

packages/message/server.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,15 @@ export class SenderRuntime {
8787

8888
getExtMessageSender(): ExtMessageSender {
8989
const sender = this.sender as RuntimeMessageSender;
90+
if (!sender) {
91+
// postMessage 通道(如 Offscreen→SW)没有 RuntimeMessageSender
92+
return {
93+
windowId: -1,
94+
tabId: -1,
95+
frameId: undefined,
96+
documentId: undefined,
97+
};
98+
}
9099
return {
91100
windowId: sender.tab?.windowId || -1, // -1表示后台脚本
92101
tabId: sender.tab?.id || -1, // -1表示后台脚本

packages/message/window_message.test.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
2-
import { ServiceWorkerMessageSend, ServiceWorkerClientMessage, type WindowMessageBody } from "./window_message";
2+
import {
3+
ServiceWorkerMessageSend,
4+
ServiceWorkerClientMessage,
5+
WindowMessage,
6+
type WindowMessageBody,
7+
} from "./window_message";
38
import { Server } from "./server";
49
import type { MessageConnect } from "./types";
510

@@ -160,6 +165,38 @@ describe("ServiceWorkerClientMessage", () => {
160165
});
161166
});
162167

168+
describe("WindowMessage.connect", () => {
169+
it("connect 返回的连接 sendMessage 应带 '*' targetOrigin", async () => {
170+
// 模拟 target window,验证 postMessage 被调用时带 "*"
171+
const targetPostMessage = vi.fn();
172+
const sourceWindow = {
173+
addEventListener: vi.fn(),
174+
} as unknown as Window;
175+
const targetWindow = {
176+
postMessage: targetPostMessage,
177+
} as unknown as Window;
178+
179+
const wm = new WindowMessage(sourceWindow, targetWindow);
180+
181+
const con = await wm.connect({ action: "test/connect", data: "init" });
182+
183+
// connect() 本身会调用一次 postMessage(发送 connect 消息)
184+
expect(targetPostMessage).toHaveBeenCalledTimes(1);
185+
expect(targetPostMessage).toHaveBeenCalledWith(expect.objectContaining({ type: "connect" }), "*");
186+
187+
targetPostMessage.mockClear();
188+
189+
// 通过返回的连接发送消息,也应该带 "*"
190+
con.sendMessage({ action: "test/msg", data: "hello" });
191+
192+
expect(targetPostMessage).toHaveBeenCalledTimes(1);
193+
expect(targetPostMessage).toHaveBeenCalledWith(
194+
expect.objectContaining({ type: "connectMessage", data: { action: "test/msg", data: "hello" } }),
195+
"*"
196+
);
197+
});
198+
});
199+
163200
describe("ServiceWorkerMessageSend ↔ ServiceWorkerClientMessage 双向通信", () => {
164201
// 辅助函数: 将两端连接起来,模拟 postMessage 通道
165202
function createWiredPair() {

packages/message/window_message.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,9 @@ export class WindowMessage implements Message {
100100
data,
101101
};
102102
this.target.postMessage(body, "*");
103-
resolve(new WindowMessageConnect(body.messageId, this.EE, this.target));
103+
// 使用 WindowPostMessage 包装,确保后续 sendMessage 也带 "*" targetOrigin
104+
// 否则沙箱(origin: null)→ offscreen(origin: chrome-extension://)的消息会被丢弃
105+
resolve(new WindowMessageConnect(body.messageId, this.EE, new WindowPostMessage(this.target)));
104106
});
105107
}
106108

src/app/service/content/gm_api/navigation_handle.test.ts

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,5 @@
11
import { describe, it, expect, vi, beforeEach } from "vitest";
2-
import { UrlChangeEvent } from "./navigation_handle.js";
3-
4-
// attachNavigateHandler 使用模块级 attached 单例,需要在每个测试前重置模块
5-
const importFresh = async () => {
6-
vi.resetModules();
7-
// vi.resetModules() 会清空模块缓存,后续 import 得到全新的 LoggerCore 类,
8-
// 需要重新初始化以免后续测试文件中 LoggerCore.getInstance() 返回 undefined
9-
// @ts-expect-error 动态 import 路径别名在 tsc nodenext 下无法解析
10-
const { default: LC, EmptyWriter: EW } = await import("@App/app/logger/core");
11-
new LC({ level: "trace", consoleLevel: "trace", writer: new EW(), labels: { env: "test" } });
12-
return await import("./navigation_handle.js");
13-
};
2+
import { UrlChangeEvent, attachNavigateHandler, resetAttachedForTest } from "./navigation_handle";
143

154
describe("UrlChangeEvent", () => {
165
it.concurrent("应包含 url 属性", () => {
@@ -64,10 +53,10 @@ describe("attachNavigateHandler", () => {
6453

6554
beforeEach(() => {
6655
vi.restoreAllMocks();
56+
resetAttachedForTest();
6757
});
6858

69-
it("不支持 Navigation API 时不应注册监听器", async () => {
70-
const { attachNavigateHandler } = await importFresh();
59+
it("不支持 Navigation API 时不应注册监听器", () => {
7160
const win = { location: { href: "https://example.com/" } } as any;
7261
attachNavigateHandler(win);
7362
// 没有 navigation 属性,不应报错也不应标记为 attached
@@ -77,16 +66,14 @@ describe("attachNavigateHandler", () => {
7766
expect(mock.win.navigation.addEventListener).toHaveBeenCalledWith("navigate", expect.any(Function), false);
7867
});
7968

80-
it("应在 win.navigation 上注册 navigate 监听器", async () => {
81-
const { attachNavigateHandler } = await importFresh();
69+
it("应在 win.navigation 上注册 navigate 监听器", () => {
8270
const mock = createMockWin();
8371
attachNavigateHandler(mock.win);
8472
expect(mock.win.navigation.addEventListener).toHaveBeenCalledTimes(1);
8573
expect(mock.win.navigation.addEventListener).toHaveBeenCalledWith("navigate", expect.any(Function), false);
8674
});
8775

88-
it("多次调用只注册一次", async () => {
89-
const { attachNavigateHandler } = await importFresh();
76+
it("多次调用只注册一次", () => {
9077
const mock = createMockWin();
9178
attachNavigateHandler(mock.win);
9279
attachNavigateHandler(mock.win);
@@ -95,7 +82,6 @@ describe("attachNavigateHandler", () => {
9582
});
9683

9784
it("URL 变化时应派发 urlchange 事件", async () => {
98-
const { attachNavigateHandler } = await importFresh();
9985
const mock = createMockWin("https://example.com/");
10086
attachNavigateHandler(mock.win);
10187
mock.fireNavigate("https://example.com/new");
@@ -109,7 +95,6 @@ describe("attachNavigateHandler", () => {
10995
});
11096

11197
it("URL 未变化时不应派发事件", async () => {
112-
const { attachNavigateHandler } = await importFresh();
11398
const mock = createMockWin("https://example.com/");
11499
attachNavigateHandler(mock.win);
115100
// destination.url 与当前 href 相同

src/app/service/content/gm_api/navigation_handle.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ export class UrlChangeEvent extends Event {
1010

1111
let attached = false;
1212

13+
// 仅供测试使用,重置 attached 标记
14+
export const resetAttachedForTest = () => {
15+
attached = false;
16+
};
17+
1318
const getPropGetter = <T>(obj: T, key: keyof T) => {
1419
// 避免直接 obj[key] 读取。或会被 hack
1520
for (let t = obj; t; t = Native.objectGetPrototypeOf(t)) {

0 commit comments

Comments
 (0)