Repository navigation
Expand file tree
/
Copy pathverify-content.js
More file actions
174 lines (155 loc) · 5.7 KB
/
Copy pathverify-content.js
File metadata and controls
174 lines (155 loc) · 5.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
"use strict";
const fs = require("fs");
const vm = require("vm");
// content.js をブラウザなしで実行するための、入力欄判定に必要な最小限の Element モック。
class ElementMock {
constructor(tagName, attributes = {}) {
this.tagName = tagName;
this.attributes = attributes;
this.isContentEditable = Boolean(attributes.contenteditable);
}
getAttribute(name) {
return this.attributes[name] ?? null;
}
}
// querySelector で返すクリック対象。クリック回数を記録して期待する要素だけが動くかを確認する。
function createClickableElement({ disabled = false, ariaDisabled = false } = {}) {
return {
disabled,
clicks: 0,
getAttribute(name) {
return name === "aria-disabled" && ariaDisabled ? "true" : null;
},
click() {
this.clicks += 1;
}
};
}
// 親 URL 用と子 URL 用のボタンを分け、具体的な URL 条件の優先順位を検証する。
const targets = {
"#site-prev": createClickableElement(),
"#site-next": createClickableElement(),
"#gallery-prev": createClickableElement(),
"#gallery-next": createClickableElement(),
"#disabled-next": createClickableElement({ disabled: true })
};
const listeners = {};
let storageChangeListener;
// 初期設定と storage.onChanged による設定更新だけを再現する Chrome API モック。
const chromeMock = {
runtime: {
lastError: null,
onMessage: { addListener() {} }
},
storage: {
sync: {
get(defaults, callback) {
callback({
...defaults,
enabled: true,
mappings: [
{
id: "site",
urlPattern: "https://e-hentai.org/*",
leftSelector: "#site-prev",
rightSelector: "#site-next"
},
{
id: "gallery",
urlPattern: "https://e-hentai.org/g/",
leftSelector: "#gallery-prev",
rightSelector: "#gallery-next"
}
]
});
}
},
onChanged: {
addListener(listener) {
storageChangeListener = listener;
}
}
}
};
const context = {
chrome: chromeMock,
Element: ElementMock,
console,
document: {
querySelector(selector) {
if (selector === "[invalid") throw new Error("Invalid selector");
return targets[selector] || null;
}
},
window: {
location: { href: "https://e-hentai.org/g/4126828/98c7949b6f/?p=1" },
addEventListener(type, listener) {
listeners[type] = listener;
}
}
};
// 本物の content.js を実行し、登録された keydown リスナーを直接テストする。
vm.runInNewContext(fs.readFileSync("content.js", "utf8"), context, { filename: "content.js" });
function createKeyEvent(key, path = []) {
return {
key,
ctrlKey: false,
metaKey: false,
altKey: false,
isComposing: false,
defaultPrevented: false,
preventDefault() { this.defaultPrevented = true; },
stopImmediatePropagation() { this.propagationStopped = true; },
composedPath() { return path; }
};
}
function assert(condition, message) {
if (!condition) throw new Error(message);
}
assert(typeof listeners.keydown === "function", "keydown のキャプチャリスナーが登録されていません。");
// 子 URL の設定が、より広い親 URL の設定より優先される。
const galleryRight = createKeyEvent("ArrowRight");
listeners.keydown(galleryRight);
assert(targets["#gallery-next"].clicks === 1, "子 URL 条件の → ボタンがクリックされていません。");
assert(targets["#site-next"].clicks === 0, "親 URL 条件が子 URL 条件より優先されています。");
assert(galleryRight.defaultPrevented && galleryRight.propagationStopped, "クリック成功時にキーイベントが抑制されていません。");
const galleryLeft = createKeyEvent("ArrowLeft");
listeners.keydown(galleryLeft);
assert(targets["#gallery-prev"].clicks === 1, "子 URL 条件の ← ボタンがクリックされていません。");
context.window.location.href = "https://e-hentai.org/tag/to-love-ru?next=4129455";
const siteRight = createKeyEvent("ArrowRight");
listeners.keydown(siteRight);
assert(targets["#site-next"].clicks === 1, "親 URL 条件がギャラリー外の URL に適用されていません。");
// * を書かない URL 条件も前方一致となり、配下ページに適用される。
storageChangeListener({
mappings: {
newValue: [{
id: "prefix-only",
urlPattern: "https://e-hentai.org/",
leftSelector: "#site-prev",
rightSelector: "#site-next"
}]
}
}, "sync");
const prefixOnly = createKeyEvent("ArrowRight");
listeners.keydown(prefixOnly);
assert(targets["#site-next"].clicks === 2, "末尾の * がない URL 条件で配下 URL に一致していません。");
// テキスト入力と無効化済み要素では、通常のキー操作を奪わない。
const input = new ElementMock("INPUT", { type: "text" });
const inputEvent = createKeyEvent("ArrowRight", [input]);
listeners.keydown(inputEvent);
assert(targets["#site-next"].clicks === 2 && !inputEvent.defaultPrevented, "入力欄での → キーを奪っています。");
storageChangeListener({
mappings: {
newValue: [{
id: "disabled",
urlPattern: "https://e-hentai.org/",
leftSelector: "",
rightSelector: "#disabled-next"
}]
}
}, "sync");
const disabled = createKeyEvent("ArrowRight");
listeners.keydown(disabled);
assert(targets["#disabled-next"].clicks === 0 && !disabled.defaultPrevented, "無効化された要素をクリックまたはイベント抑制しています。");
console.log("content.js の前方一致、親子 URL 条件の優先順位、左右クリックを検証しました。");