Skip to content

Commit 65d900a

Browse files
committed
Merge branch 'release/v1.4' into develop/add-no-csp-option
2 parents 3f06fda + eccc118 commit 65d900a

28 files changed

Lines changed: 269 additions & 69 deletions

.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)