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
3 changes: 2 additions & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ jobs:
run: >
pnpm exec vitest run
--coverage
--silent
--silent=passed-only
--reporter=blob
--shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}

Expand Down Expand Up @@ -158,6 +158,7 @@ jobs:
pnpm exec vitest
--merge-reports
--coverage
--silent=passed-only
--reporter=default
--reporter.default.summary=false

Expand Down
152 changes: 152 additions & 0 deletions packages/message/custom_event_message.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { describe, expect, it } from "vitest";
import { CustomEventMessage } from "./custom_event_message";
import { createMouseEvent, pageDispatchEvent } from "@Packages/message/common";

let flagCounter = 0;

function createMessagePair() {
const eventFlag = `custom-event-message-test-${++flagCounter}`;
const sender = new CustomEventMessage(eventFlag, false, "");
const receiver = new CustomEventMessage(eventFlag, true, "");

expect(sender.readyWrap.isReady).toBe(true);
expect(receiver.readyWrap.isReady).toBe(true);
expect(sender.relatedTarget).toHaveProperty("size", 0);
expect(receiver.relatedTarget).toHaveProperty("size", 0);
return { sender, receiver };
}

describe("CustomEventMessage relatedTarget lifecycle", () => {
it("stores a received target on the receiving message until it is consumed", () => {
const { sender, receiver } = createMessagePair();
const target = document.createElement("div");
const id = sender.sendRelatedTarget(target);

try {
expect(receiver.relatedTarget.get(id)).toBe(target);
expect(receiver.relatedTarget).toHaveProperty("size", 1);

expect(receiver.getAndDelRelatedTarget(id)).toBe(target);
expect(receiver.relatedTarget.has(id)).toBe(false);
} finally {
receiver.getAndDelRelatedTarget(id);
}
});

it("releases multiple targets after out-of-order consumption", () => {
const { sender, receiver } = createMessagePair();
const targets = [document.createElement("div"), document.createElement("span"), document.createElement("p")];
const ids = targets.map((target) => sender.sendRelatedTarget(target));

try {
expect(receiver.relatedTarget).toHaveProperty("size", targets.length);
expect(receiver.getAndDelRelatedTarget(ids[1])).toBe(targets[1]);
expect(receiver.relatedTarget).toHaveProperty("size", 2);
expect(receiver.getAndDelRelatedTarget(ids[1])).toBeUndefined();
expect(receiver.relatedTarget).toHaveProperty("size", 2);
expect(receiver.getAndDelRelatedTarget(ids[2])).toBe(targets[2]);
expect(receiver.getAndDelRelatedTarget(ids[0])).toBe(targets[0]);
expect(receiver.relatedTarget).toHaveProperty("size", 0);
} finally {
ids.forEach((id) => receiver.getAndDelRelatedTarget(id));
}
});

it("does not let an unknown target id affect queued targets", () => {
const { sender, receiver } = createMessagePair();
const target = document.createElement("div");
const id = sender.sendRelatedTarget(target);

try {
expect(receiver.getAndDelRelatedTarget(id + 1)).toBeUndefined();
expect(receiver.relatedTarget).toHaveProperty("size", 1);
expect(receiver.getAndDelRelatedTarget(id)).toBe(target);
expect(receiver.getAndDelRelatedTarget(id)).toBeUndefined();
expect(receiver.relatedTarget).toHaveProperty("size", 0);
} finally {
receiver.getAndDelRelatedTarget(id);
}
});

it("keeps targets from opposite message directions independently owned", () => {
const { sender, receiver } = createMessagePair();
const senderTarget = document.createElement("div");
const receiverTarget = document.createElement("span");
const senderTargetId = sender.sendRelatedTarget(senderTarget);
const receiverTargetId = receiver.sendRelatedTarget(receiverTarget);

try {
expect(receiver.relatedTarget.get(senderTargetId)).toBe(senderTarget);
expect(sender.relatedTarget.get(receiverTargetId)).toBe(receiverTarget);
expect(sender.relatedTarget.get(senderTargetId)).toBeUndefined();
expect(receiver.relatedTarget.get(receiverTargetId)).toBeUndefined();
expect(sender.getAndDelRelatedTarget(senderTargetId)).toBeUndefined();
expect(receiver.getAndDelRelatedTarget(receiverTargetId)).toBeUndefined();
expect(receiver.getAndDelRelatedTarget(senderTargetId)).toBe(senderTarget);
expect(sender.getAndDelRelatedTarget(receiverTargetId)).toBe(receiverTarget);
expect(sender.relatedTarget.has(senderTargetId)).toBe(false);
expect(receiver.relatedTarget.has(receiverTargetId)).toBe(false);
} finally {
sender.getAndDelRelatedTarget(senderTargetId);
receiver.getAndDelRelatedTarget(receiverTargetId);
}
});

it("keeps targets from different event channels independently owned", () => {
const first = createMessagePair();
const second = createMessagePair();
const firstTarget = document.createElement("div");
const secondTarget = document.createElement("span");
const firstId = first.sender.sendRelatedTarget(firstTarget);
const secondId = second.sender.sendRelatedTarget(secondTarget);

try {
expect(first.receiver.getAndDelRelatedTarget(secondId)).toBeUndefined();
expect(second.receiver.getAndDelRelatedTarget(firstId)).toBeUndefined();
expect(first.receiver.getAndDelRelatedTarget(firstId)).toBe(firstTarget);
expect(second.receiver.getAndDelRelatedTarget(secondId)).toBe(secondTarget);
expect(first.receiver.relatedTarget).toHaveProperty("size", 0);
expect(second.receiver.relatedTarget).toHaveProperty("size", 0);
} finally {
first.receiver.getAndDelRelatedTarget(firstId);
second.receiver.getAndDelRelatedTarget(secondId);
}
});

it("does not retain entries for ordinary messages or unrelated mouse events", async () => {
const { sender, receiver } = createMessagePair();
receiver.onMessage((_data, sendResponse) => sendResponse({ code: 0, data: "ok" }));

expect(
await sender.sendMessage({
action: "custom-event-message-test/ordinary-async",
data: {},
})
).toEqual({ code: 0, data: "ok" });
expect(sender.syncSendMessage({ action: "custom-event-message-test/ordinary-sync", data: {} })).toEqual({
code: 0,
data: "ok",
});

pageDispatchEvent(
createMouseEvent(receiver.receiveFlag, {
movementX: 0,
relatedTarget: document.createElement("div"),
cancelable: true,
})
);
pageDispatchEvent(createMouseEvent(receiver.receiveFlag, { movementX: 1, cancelable: true }));

expect(sender.relatedTarget).toHaveProperty("size", 0);
expect(receiver.relatedTarget).toHaveProperty("size", 0);
});

it("does not retain entries when sending before the channel is ready", () => {
const sender = new CustomEventMessage(`custom-event-message-test-${++flagCounter}`, false, "");
const target = document.createElement("div");

expect(sender.readyWrap.isReady).toBe(false);
expect(() => sender.sendRelatedTarget(target)).toThrow("custom_event_message is not ready.");
expect(sender.relatedTarget).toHaveProperty("size", 0);
});
});
25 changes: 17 additions & 8 deletions packages/message/custom_event_message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,19 @@ import { ReadyWrap } from "@App/pkg/utils/ready-wrap";
import type { ScriptEnvTag } from "@Packages/message/consts";

// 避免页面载入后改动 Map.prototype 导致消息传递失败
const relatedTargetMap = new Map<number, EventTarget>();
relatedTargetMap.set = Map.prototype.set;
relatedTargetMap.get = Map.prototype.get;
relatedTargetMap.delete = Map.prototype.delete;
const relatedTargetMapMethods = {
set: Map.prototype.set,
get: Map.prototype.get,
delete: Map.prototype.delete,
};

function createRelatedTargetMap() {
const map = new Map<number, EventTarget>();
map.set = relatedTargetMapMethods.set;
map.get = relatedTargetMapMethods.get;
map.delete = relatedTargetMapMethods.delete;
return map;
}

let relateId = 0;
const maxInteger = Number.MAX_SAFE_INTEGER;
Expand All @@ -38,7 +47,7 @@ export class CustomEventMessage implements Message {
readonly sendFlag: string;

// 关联dom目标
relatedTarget: Map<number, EventTarget> = new Map();
relatedTarget: Map<number, EventTarget> = createRelatedTargetMap();
readyWrap: ReadyWrap = new ReadyWrap();

constructor(
Expand All @@ -55,7 +64,7 @@ export class CustomEventMessage implements Message {
this.readyWrap.setReady(); // 两端已准备好,则 setReady()
} else if (event instanceof MouseEventClone && event.movementX && event.relatedTarget) {
if (event.cancelable) event.preventDefault(); // 告知另一端
relatedTargetMap.set(event.movementX, event.relatedTarget);
this.relatedTarget.set(event.movementX, event.relatedTarget);
} else if (event instanceof CustomEventClone) {
this.messageHandle(event.detail, new CustomEventPostMessage(this));
}
Expand Down Expand Up @@ -185,8 +194,8 @@ export class CustomEventMessage implements Message {
}

getAndDelRelatedTarget(id: number) {
const target = relatedTargetMap.get(id);
relatedTargetMap.delete(id);
const target = this.relatedTarget.get(id);
this.relatedTarget.delete(id);
return target;
}
}
75 changes: 75 additions & 0 deletions src/app/service/content/gm_api/related_target_lifecycle.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { describe, expect, it } from "vitest";
import { ScriptEnvTag } from "@Packages/message/consts";
import { CustomEventMessage } from "@Packages/message/custom_event_message";
import { Server } from "@Packages/message/server";
import type { ScriptRunResource } from "@App/app/repo/scripts";
import { ScriptExecutor } from "../script_executor";
import { ScriptRuntime } from "../script_runtime";
import GMApi from "./gm_api";

let flagCounter = 0;

function createApiWithContentRuntime() {
const eventFlag = `related-target-lifecycle-test-${++flagCounter}`;
const sender = new CustomEventMessage(eventFlag, false, "");
const receiver = new CustomEventMessage(eventFlag, true, "");
const server = new Server("content", receiver);
const runtime = new ScriptRuntime(
ScriptEnvTag.content,
server,
receiver,
new ScriptExecutor(receiver, receiver),
undefined
);
runtime.contentInit();

const scriptRes: ScriptRunResource = {
uuid: "related-target-lifecycle-test",
name: "related-target-lifecycle-test",
namespace: "test",
metadata: {},
sort: 0,
type: 1,
status: 1,
runStatus: "complete",
createtime: 0,
checktime: 0,
code: "",
value: {},
flag: "related-target-lifecycle-test",
resource: {},
originalMetadata: {},
};
const api = new GMApi("test", sender, sender, scriptRes);
return { api, sender, receiver };
}

describe("relatedTarget lifecycle across content runtime callers", () => {
it("consumes returned elements and parent nodes for GM DOM operations", () => {
const { api, sender, receiver } = createApiWithContentRuntime();
const parent = document.createElement("section");

try {
const style = api.GM_addStyle("body { color: red; }");
expect(style?.tagName).toBe("STYLE");
expect(style?.textContent).toBe("body { color: red; }");
expect(sender.relatedTarget).toHaveProperty("size", 0);
expect(receiver.relatedTarget).toHaveProperty("size", 0);

const child = api.GM_addElement(parent, "span", { id: "child" });
expect(child?.parentNode).toBe(parent);
expect(child?.id).toBe("child");
expect(sender.relatedTarget).toHaveProperty("size", 0);
expect(receiver.relatedTarget).toHaveProperty("size", 0);

const root = api.GM_addElement("div", { id: "root" });
expect(root?.tagName).toBe("DIV");
expect(root?.id).toBe("root");
expect(sender.relatedTarget).toHaveProperty("size", 0);
expect(receiver.relatedTarget).toHaveProperty("size", 0);
} finally {
sender.relatedTarget.clear();
receiver.relatedTarget.clear();
}
});
});
Loading