ectrol 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-PRESENT Cee Vee X <https://github.com/ceeveex>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # Ectrol = Electron + Control
2
+
3
+ 一个基于 Electron WebContents 的轻量级自动化助手,用于在你的应用内对网页内容进行交互(点击、输入、聚焦、键盘事件等)。
4
+
5
+ - 专注于 Electron 环境:无需引入完整的浏览器自动化运行时。
6
+ - 轻量且易用:API 风格参考了部分 Playwright 用法,学习成本低。
7
+ - 支持 iframe 穿越选择器:使用 `|>` 分隔层级,轻松访问嵌套内容。
8
+
9
+ ## 安装
10
+
11
+ 你可以使用任意包管理器进行安装:
12
+
13
+ ```powershell
14
+ # PNPM
15
+ pnpm add ectrol
16
+
17
+ # NPM
18
+ npm install ectrol
19
+
20
+ # Yarn
21
+ yarn add ectrol
22
+ ```
23
+
24
+ ## 快速开始
25
+
26
+ 在主进程中创建 `Ectrol` 实例,并传入目标 `WebContents`:
27
+
28
+ ```ts
29
+ import Ectrol from 'ectrol'
30
+ import { app, BrowserWindow } from 'electron'
31
+
32
+ let win: BrowserWindow
33
+
34
+ app.whenReady().then(async () => {
35
+ win = new BrowserWindow({
36
+ webPreferences: {
37
+ nodeIntegration: false,
38
+ contextIsolation: true,
39
+ },
40
+ })
41
+
42
+ await win.loadURL('https://example.com')
43
+
44
+ const ctl = new Ectrol(win.webContents)
45
+
46
+ // 基本查询与点击
47
+ const exist = await ctl.$('button.submit').exist(2000)
48
+ if (exist) {
49
+ await ctl.$('button.submit').click()
50
+ }
51
+
52
+ // 输入文本(支持 input / textarea / contenteditable)
53
+ await ctl.$('input[name="username"]').fill('hello-world', { timeout: 2000 })
54
+
55
+ // 组合键:Ctrl+Shift+T(Windows/Linux 下为 Control,macOS 可用 Meta)
56
+ await ctl.$('input[name="username"]').press('Control+Shift+T', { delay: 50 })
57
+
58
+ // 读写存储
59
+ await ctl.localStorage.setItem('token', 'abc123')
60
+ const token = await ctl.localStorage.getItem('token')
61
+ console.log('token:', token)
62
+ })
63
+ ```
64
+
65
+ ### iframe 选择器
66
+
67
+ 当页面包含 iframe 时,使用 `|>` 逐层穿越:
68
+
69
+ ```ts
70
+ // 选择外层 iframe,再选择里层的输入框
71
+ await ctl.$('iframe#login |> input[name="username"]').fill('user')
72
+
73
+ // 更深层级示例
74
+ await ctl.$('iframe#outer |> iframe#inner |> button.go').click()
75
+ ```
76
+
77
+ ### 常用 API
78
+
79
+ - `ctl.$(selector).exist(timeout?)`: 检查元素是否存在(支持超时轮询)。
80
+ - `ctl.$(selector).click(options?)`: 点击元素(可配置按钮、次数、修饰键)。
81
+ - `ctl.$(selector).dblclick(options?)`: 双击元素。
82
+ - `ctl.$(selector).fill(value, options?)`: 填充文本并触发 `input` 事件。
83
+ - `ctl.$(selector).press(keys, options?)`: 在元素上发送组合键(自动聚焦)。
84
+ - `ctl.$(selector).focus(options?)`: 聚焦输入型元素。
85
+ - `ctl.$(selector).getBoundingClientRect(timeout?)`: 获取包含中心点坐标的位置信息。
86
+ - `ctl.localStorage.* / ctl.sessionStorage.*`: 操作存储(字符串键值)。
87
+
88
+ ## 为什么不是 Playwright?
89
+
90
+ 编写该库时尚不熟悉 Playwright。后续了解后发现其功能全面,但在 Electron 应用内做少量、直接的页面交互,往往只需要一个轻量方案。Electrol 的设计目标是:在不引入额外浏览器驱动与上下文的前提下,以最小侵入实现 Electron 内的页面自动化。
91
+
92
+ ## 许可
93
+
94
+ 详见 `LICENSE.md`。
@@ -0,0 +1,196 @@
1
+ import { WebContents } from "electron";
2
+
3
+ //#region src/element.d.ts
4
+
5
+ /**
6
+ * 扩展版的元素位置信息,基于 DOMRect 并补充中心点坐标。
7
+ */
8
+ interface IBoundingClientRect {
9
+ x: number;
10
+ y: number;
11
+ width: number;
12
+ height: number;
13
+ top: number;
14
+ right: number;
15
+ bottom: number;
16
+ left: number;
17
+ centerX: number;
18
+ centerY: number;
19
+ }
20
+ /**
21
+ * 二维向量坐标。
22
+ */
23
+ interface IVector2D {
24
+ x: number;
25
+ y: number;
26
+ }
27
+ /**
28
+ * 尺寸信息。
29
+ */
30
+ interface ISize {
31
+ width: number;
32
+ height: number;
33
+ }
34
+ /**
35
+ * ElementHandle
36
+ *
37
+ * - 封装了在指定 WebContents 中对单个元素进行操作的能力
38
+ * - 选择器支持 iframe 穿越,形如:`iframe.sel |> .inner |> button.submit`
39
+ * - 大多数 API 接受 `timeout`,轮询查找元素直到超时
40
+ */
41
+ declare class ElementHandle {
42
+ readonly contents: WebContents;
43
+ readonly selector: string;
44
+ isNotFound: boolean;
45
+ constructor(contents: WebContents, selector: string);
46
+ /**
47
+ * 生成在 renderer 端执行的查找脚本。
48
+ * - 通过 `|>` 支持逐层进入 iframe。
49
+ * - 在 `window.__ELECTROL__` 上缓存找到的元素及其 rect,以减少后续访问成本。
50
+ * - 当传入 `timeout > 0` 时会在 renderer 端做原地轮询,直到找到或超时。
51
+ */
52
+ private _getElement;
53
+ /**
54
+ * 触发元素 mouseover 事件(不移动真实鼠标)。
55
+ */
56
+ hover(): Promise<any>;
57
+ /**
58
+ * 点击
59
+ * - 通过 WebContents.sendInputEvent 发送 mouseDown/mouseUp 事件
60
+ * - 默认在元素中心点点击
61
+ * - 支持修饰键(Alt/Control/Meta/Shift)按下期间点击
62
+ * - 注意:此操作会触发鼠标悬浮事件
63
+ */
64
+ click(options?: {
65
+ button?: 'left' | 'right' | 'middle';
66
+ clickCount?: number;
67
+ delay?: number;
68
+ modifiers?: ('Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift')[];
69
+ position?: IVector2D;
70
+ timeout?: number;
71
+ }): Promise<void>;
72
+ /**
73
+ * 双击
74
+ * - 底层复用 click,并将 `clickCount` 设为 2。
75
+ */
76
+ dblclick(options?: {
77
+ button?: 'left' | 'right' | 'middle';
78
+ delay?: number;
79
+ modifiers?: ('Alt' | 'Control' | 'ControlOrMeta' | 'Meta' | 'Shift')[];
80
+ position?: IVector2D;
81
+ timeout?: number;
82
+ }): Promise<void>;
83
+ /**
84
+ * 将复选框或单选按钮设为选中,并派发 change 事件。
85
+ */
86
+ check(): Promise<any>;
87
+ /**
88
+ * 填充输入框内容。
89
+ * - 适用于 `<input>`、`<textarea>` 或 `contenteditable` 元素。
90
+ * - 会触发 `input` 事件(冒泡)。
91
+ */
92
+ fill(value: string, options?: {
93
+ timeout?: number;
94
+ }): Promise<void>;
95
+ /**
96
+ * 在元素上发送按键事件。
97
+ * - 先调用 `focus()`,随后按下并抬起指定组合键。
98
+ * - 组合键用 `+` 连接,如 `Control+Shift+A`。
99
+ * @example
100
+ * press('a', { delay: 100, timeout: 1000 })
101
+ * press('Control+Shift+T')
102
+ */
103
+ press(key: string, options?: {
104
+ delay?: number;
105
+ timeout?: number;
106
+ }): Promise<void>;
107
+ /**
108
+ * 聚焦输入型元素(input/textarea/contenteditable)。
109
+ */
110
+ focus(options?: {
111
+ timeout?: number;
112
+ }): Promise<any>;
113
+ /**
114
+ * 检查元素是否存在(支持超时轮询)。
115
+ */
116
+ exist(timeout?: number): Promise<boolean>;
117
+ /**
118
+ * 获取元素的位置信息。
119
+ * - 返回扩展的 DOMRect 信息并包含元素中心点坐标。
120
+ */
121
+ getBoundingClientRect(timeout?: number): Promise<IBoundingClientRect | null>;
122
+ }
123
+ //#endregion
124
+ //#region src/localStorage.d.ts
125
+ /**
126
+ * LocalStorage 封装
127
+ *
128
+ * 通过 WebContents 在渲染进程中直接读取/写入 window.localStorage。
129
+ * 注意:键值均按字符串处理。
130
+ */
131
+ declare class LocalStorage {
132
+ readonly contents: WebContents;
133
+ constructor(contents: WebContents);
134
+ /**
135
+ * 获取指定键的值。
136
+ */
137
+ getItem(key: string): Promise<string | null>;
138
+ /**
139
+ * 写入指定键的值。
140
+ */
141
+ setItem(key: string, value: string): Promise<void>;
142
+ /**
143
+ * 删除指定键。
144
+ */
145
+ removeItem(key: string): Promise<void>;
146
+ }
147
+ //#endregion
148
+ //#region src/sessionStorage.d.ts
149
+ /**
150
+ * SessionStorage 封装
151
+ *
152
+ * 通过 WebContents 在渲染进程中直接读取/写入 window.sessionStorage。
153
+ * 注意:键值均按字符串处理;生命周期为会话级别。
154
+ */
155
+ declare class SessionStorage {
156
+ readonly contents: WebContents;
157
+ constructor(contents: WebContents);
158
+ /**
159
+ * 获取指定键的值。
160
+ */
161
+ getItem(key: string): Promise<string | null>;
162
+ /**
163
+ * 写入指定键的值。
164
+ */
165
+ setItem(key: string, value: string): Promise<void>;
166
+ /**
167
+ * 删除指定键。
168
+ */
169
+ removeItem(key: string): Promise<void>;
170
+ }
171
+ //#endregion
172
+ //#region src/index.d.ts
173
+ /**
174
+ * Ectrol
175
+ *
176
+ * 一个针对 Electron WebContents 的轻量级自动化助手。
177
+ * - 提供 `localStorage` / `sessionStorage` 封装
178
+ * - 通过 `$(selector)` 获取元素句柄进行点击、输入、聚焦等操作
179
+ *
180
+ * 选择器支持穿越 iframe:
181
+ * 使用 `|>` 分隔层级,如:`iframe#login |> input[name="username"]`
182
+ */
183
+ declare class Ectrol {
184
+ readonly contents: WebContents;
185
+ readonly localStorage: LocalStorage;
186
+ readonly sessionStorage: SessionStorage;
187
+ constructor(contents: WebContents);
188
+ /**
189
+ * 获取一个元素句柄,用于后续交互。
190
+ * @param selector CSS 选择器;支持通过 `|>` 穿越 iframe
191
+ * @returns `ElementHandle` 实例
192
+ */
193
+ $(selector: string): ElementHandle;
194
+ }
195
+ //#endregion
196
+ export { Ectrol, Ectrol as default, ElementHandle, IBoundingClientRect, ISize, IVector2D, LocalStorage, SessionStorage };
package/dist/index.mjs ADDED
@@ -0,0 +1,390 @@
1
+ //#region src/element.ts
2
+ /**
3
+ * ElementHandle
4
+ *
5
+ * - 封装了在指定 WebContents 中对单个元素进行操作的能力
6
+ * - 选择器支持 iframe 穿越,形如:`iframe.sel |> .inner |> button.submit`
7
+ * - 大多数 API 接受 `timeout`,轮询查找元素直到超时
8
+ */
9
+ var ElementHandle = class {
10
+ isNotFound = false;
11
+ constructor(contents, selector) {
12
+ this.contents = contents;
13
+ this.selector = selector;
14
+ }
15
+ /**
16
+ * 生成在 renderer 端执行的查找脚本。
17
+ * - 通过 `|>` 支持逐层进入 iframe。
18
+ * - 在 `window.__ELECTROL__` 上缓存找到的元素及其 rect,以减少后续访问成本。
19
+ * - 当传入 `timeout > 0` 时会在 renderer 端做原地轮询,直到找到或超时。
20
+ */
21
+ _getElement(timeout = 0) {
22
+ return `
23
+ (()=>{
24
+ function getElement() {
25
+ const selector = "${this.selector}";
26
+
27
+ if (!selector) return null;
28
+
29
+ window.__ELECTROL__ = window.__ELECTROL__ || {};
30
+ if (window.__ELECTROL__[selector]) {
31
+ return window.__ELECTROL__[selector];
32
+ }
33
+
34
+ // 分层选择器,支持 iframe 穿越
35
+ const parts = selector.split('|>').map(s => s.trim());
36
+ if (!parts.length) return null;
37
+
38
+ let currentDocument = document;
39
+ let offsetLeft = 0;
40
+ let offsetTop = 0;
41
+ let element = null;
42
+
43
+ for (let i = 0; i < parts.length; i++) {
44
+ if (!currentDocument) return null;
45
+
46
+ const part = parts[i];
47
+ // 在当前文档查找该层选择器
48
+ const el = currentDocument.querySelector(part);
49
+
50
+ if (!el) {
51
+ console.warn('未找到元素:', part);
52
+ return null;
53
+ }
54
+
55
+ const rect = el.getBoundingClientRect();
56
+
57
+ // 累加当前层级偏移
58
+ offsetLeft += rect.left;
59
+ offsetTop += rect.top;
60
+
61
+ element = el;
62
+
63
+ const isLast = i === parts.length - 1;
64
+ if (!isLast) {
65
+ if (el.tagName !== 'IFRAME') {
66
+ console.warn('非 iframe 元素却尝试进入下一层:', part);
67
+ return null;
68
+ }
69
+
70
+ const nextDoc = el.contentDocument || el.contentWindow?.document;
71
+ if (!nextDoc) {
72
+ console.warn('无法访问 iframe.contentDocument(可能跨域)');
73
+ return null;
74
+ }
75
+
76
+ currentDocument = nextDoc;
77
+ }
78
+ }
79
+
80
+ if (!element) return null;
81
+
82
+ const finalRect = element.getBoundingClientRect();
83
+
84
+ // 缓存元素
85
+ // 缓存以避免重复查找
86
+ window.__ELECTROL__[selector] = {
87
+ element,
88
+ rect: new DOMRect(
89
+ offsetLeft,
90
+ offsetTop,
91
+ finalRect.width,
92
+ finalRect.height
93
+ )
94
+ };
95
+
96
+ return window.__ELECTROL__[selector]
97
+ }
98
+
99
+ const timeout = ${timeout}
100
+
101
+ if(timeout <= 0) {
102
+ return getElement();
103
+ }
104
+
105
+ const startTime = Date.now()
106
+ while (Date.now() - startTime < timeout) {
107
+ const element = getElement();
108
+ if (element) return element;
109
+ }
110
+ return null;
111
+ })()
112
+ `;
113
+ }
114
+ /**
115
+ * 触发元素 mouseover 事件(不移动真实鼠标)。
116
+ */
117
+ hover() {
118
+ return this.contents.executeJavaScript(`
119
+ (function() {
120
+ const target = ${this._getElement()};
121
+ if (!target) return null;
122
+
123
+ const event = new MouseEvent('mouseover', {
124
+ bubbles: true,
125
+ cancelable: true,
126
+ view: window
127
+ });
128
+ target.element.dispatchEvent(event);
129
+ })()
130
+ `);
131
+ }
132
+ /**
133
+ * 点击
134
+ * - 通过 WebContents.sendInputEvent 发送 mouseDown/mouseUp 事件
135
+ * - 默认在元素中心点点击
136
+ * - 支持修饰键(Alt/Control/Meta/Shift)按下期间点击
137
+ * - 注意:此操作会触发鼠标悬浮事件
138
+ */
139
+ async click(options) {
140
+ const rect = await this.getBoundingClientRect(options?.timeout);
141
+ if (!rect) return;
142
+ const _handle = async () => {
143
+ this.contents.sendInputEvent({
144
+ type: "mouseDown",
145
+ x: rect.centerX,
146
+ y: rect.centerY,
147
+ button: options?.button || "left",
148
+ clickCount: 1
149
+ });
150
+ await new Promise((resolve) => setTimeout(resolve, options?.delay || 10));
151
+ this.contents.sendInputEvent({
152
+ type: "mouseUp",
153
+ x: rect.centerX,
154
+ y: rect.centerY,
155
+ button: options?.button || "left",
156
+ clickCount: 1
157
+ });
158
+ };
159
+ if (options?.modifiers) for (const modifier of options.modifiers) this.contents.sendInputEvent({
160
+ type: "keyDown",
161
+ keyCode: modifier
162
+ });
163
+ if (options?.clickCount) for (let i = 0; i < options.clickCount; i++) await _handle();
164
+ else await _handle();
165
+ if (options?.modifiers) for (const modifier of options.modifiers) this.contents.sendInputEvent({
166
+ type: "keyUp",
167
+ keyCode: modifier
168
+ });
169
+ }
170
+ /**
171
+ * 双击
172
+ * - 底层复用 click,并将 `clickCount` 设为 2。
173
+ */
174
+ async dblclick(options) {
175
+ return this.click({
176
+ ...options,
177
+ clickCount: 2
178
+ });
179
+ }
180
+ /**
181
+ * 将复选框或单选按钮设为选中,并派发 change 事件。
182
+ */
183
+ check() {
184
+ return this.contents.executeJavaScript(`
185
+ (function() {
186
+ const target = ${this._getElement()};
187
+ if (!target) return null;
188
+
189
+ // 验证输入框是否是复选框或单选按钮
190
+ if (target.element.tagName !== 'INPUT' || (target.element.type !== 'checkbox' && target.element.type !== 'radio')) return null;
191
+
192
+ target.element.checked = true;
193
+ target.element.dispatchEvent(new Event('change', { bubbles: true }));
194
+ })()
195
+ `);
196
+ }
197
+ /**
198
+ * 填充输入框内容。
199
+ * - 适用于 `<input>`、`<textarea>` 或 `contenteditable` 元素。
200
+ * - 会触发 `input` 事件(冒泡)。
201
+ */
202
+ async fill(value, options) {
203
+ await this.contents.executeJavaScript(`
204
+ (function() {
205
+ const target = ${this._getElement(options?.timeout)};
206
+ if (!target) return null;
207
+
208
+ // 验证输入框是否是文本类型
209
+ if (target.element.tagName !== 'INPUT' && target.element.tagName !== 'TEXTAREA' && !target.element.isContentEditable) return null;
210
+
211
+ target.element.focus();
212
+
213
+ target.element.value = ${JSON.stringify(value)};
214
+ target.element.dispatchEvent(new Event('input', { bubbles: true }));
215
+ })()
216
+ `);
217
+ }
218
+ /**
219
+ * 在元素上发送按键事件。
220
+ * - 先调用 `focus()`,随后按下并抬起指定组合键。
221
+ * - 组合键用 `+` 连接,如 `Control+Shift+A`。
222
+ * @example
223
+ * press('a', { delay: 100, timeout: 1000 })
224
+ * press('Control+Shift+T')
225
+ */
226
+ async press(key, options) {
227
+ await this.focus(options);
228
+ const keys = key.split("+");
229
+ for (const key$1 of keys) this.contents.sendInputEvent({
230
+ type: "keyDown",
231
+ keyCode: key$1
232
+ });
233
+ await new Promise((resolve) => setTimeout(resolve, options?.delay || 10));
234
+ for (const key$1 of keys) this.contents.sendInputEvent({
235
+ type: "keyUp",
236
+ keyCode: key$1
237
+ });
238
+ }
239
+ /**
240
+ * 聚焦输入型元素(input/textarea/contenteditable)。
241
+ */
242
+ async focus(options) {
243
+ return this.contents.executeJavaScript(`
244
+ (function() {
245
+ const target = ${this._getElement(options?.timeout)};
246
+ if (!target) return null;
247
+
248
+ // 验证输入框是否是文本类型
249
+ if (target.element.tagName !== 'INPUT' && target.element.tagName !== 'TEXTAREA' && !target.element.isContentEditable) return null;
250
+
251
+ target.element.focus();
252
+ })()
253
+ `);
254
+ }
255
+ /**
256
+ * 检查元素是否存在(支持超时轮询)。
257
+ */
258
+ async exist(timeout) {
259
+ return await this.contents.executeJavaScript(`!!${this._getElement(timeout)}`);
260
+ }
261
+ /**
262
+ * 获取元素的位置信息。
263
+ * - 返回扩展的 DOMRect 信息并包含元素中心点坐标。
264
+ */
265
+ getBoundingClientRect(timeout = 0) {
266
+ if (this.isNotFound) return Promise.resolve(null);
267
+ return this.contents.executeJavaScript(`
268
+ (function() {
269
+ const target = ${this._getElement(timeout)};
270
+
271
+ if (!target) return null;
272
+
273
+ const rect = target.rect;
274
+ return {
275
+ left: rect.left,
276
+ top: rect.top,
277
+ right: rect.right,
278
+ bottom: rect.bottom,
279
+ width: rect.width,
280
+ height: rect.height,
281
+ x: rect.x,
282
+ y: rect.y,
283
+ centerX: rect.x + rect.width / 2,
284
+ centerY: rect.y + rect.height / 2
285
+ };
286
+ })()
287
+ `);
288
+ }
289
+ };
290
+ var element_default = ElementHandle;
291
+
292
+ //#endregion
293
+ //#region src/localStorage.ts
294
+ /**
295
+ * LocalStorage 封装
296
+ *
297
+ * 通过 WebContents 在渲染进程中直接读取/写入 window.localStorage。
298
+ * 注意:键值均按字符串处理。
299
+ */
300
+ var LocalStorage = class {
301
+ constructor(contents) {
302
+ this.contents = contents;
303
+ }
304
+ /**
305
+ * 获取指定键的值。
306
+ */
307
+ async getItem(key) {
308
+ return this.contents.executeJavaScript(`localStorage.getItem("${key}")`);
309
+ }
310
+ /**
311
+ * 写入指定键的值。
312
+ */
313
+ async setItem(key, value) {
314
+ await this.contents.executeJavaScript(`localStorage.setItem("${key}", "${value}")`);
315
+ }
316
+ /**
317
+ * 删除指定键。
318
+ */
319
+ async removeItem(key) {
320
+ await this.contents.executeJavaScript(`localStorage.removeItem("${key}")`);
321
+ }
322
+ };
323
+ var localStorage_default = LocalStorage;
324
+
325
+ //#endregion
326
+ //#region src/sessionStorage.ts
327
+ /**
328
+ * SessionStorage 封装
329
+ *
330
+ * 通过 WebContents 在渲染进程中直接读取/写入 window.sessionStorage。
331
+ * 注意:键值均按字符串处理;生命周期为会话级别。
332
+ */
333
+ var SessionStorage = class {
334
+ constructor(contents) {
335
+ this.contents = contents;
336
+ }
337
+ /**
338
+ * 获取指定键的值。
339
+ */
340
+ async getItem(key) {
341
+ return this.contents.executeJavaScript(`sessionStorage.getItem("${key}")`);
342
+ }
343
+ /**
344
+ * 写入指定键的值。
345
+ */
346
+ async setItem(key, value) {
347
+ await this.contents.executeJavaScript(`sessionStorage.setItem("${key}", "${value}")`);
348
+ }
349
+ /**
350
+ * 删除指定键。
351
+ */
352
+ async removeItem(key) {
353
+ await this.contents.executeJavaScript(`sessionStorage.removeItem("${key}")`);
354
+ }
355
+ };
356
+ var sessionStorage_default = SessionStorage;
357
+
358
+ //#endregion
359
+ //#region src/index.ts
360
+ /**
361
+ * Ectrol
362
+ *
363
+ * 一个针对 Electron WebContents 的轻量级自动化助手。
364
+ * - 提供 `localStorage` / `sessionStorage` 封装
365
+ * - 通过 `$(selector)` 获取元素句柄进行点击、输入、聚焦等操作
366
+ *
367
+ * 选择器支持穿越 iframe:
368
+ * 使用 `|>` 分隔层级,如:`iframe#login |> input[name="username"]`
369
+ */
370
+ var Ectrol = class {
371
+ localStorage;
372
+ sessionStorage;
373
+ constructor(contents) {
374
+ this.contents = contents;
375
+ this.localStorage = new localStorage_default(contents);
376
+ this.sessionStorage = new sessionStorage_default(contents);
377
+ }
378
+ /**
379
+ * 获取一个元素句柄,用于后续交互。
380
+ * @param selector CSS 选择器;支持通过 `|>` 穿越 iframe
381
+ * @returns `ElementHandle` 实例
382
+ */
383
+ $(selector) {
384
+ return new element_default(this.contents, selector);
385
+ }
386
+ };
387
+ var src_default = Ectrol;
388
+
389
+ //#endregion
390
+ export { Ectrol, ElementHandle, LocalStorage, SessionStorage, src_default as default };
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "ectrol",
3
+ "type": "module",
4
+ "version": "0.0.2",
5
+ "description": "一个基于 Electron WebContents 的轻量级自动化助手,用于在你的应用内对网页内容进行交互(点击、输入、聚焦、键盘事件等)。",
6
+ "author": "Cee Vee X <ceeveex@hotmail.com>",
7
+ "license": "MIT",
8
+ "funding": "https://github.com/sponsors/ceeveex",
9
+ "homepage": "https://github.com/ceeveex/ectrol#readme",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/ceeveex/ectrol.git"
13
+ },
14
+ "bugs": "https://github.com/ceeveex/ectrol/issues",
15
+ "keywords": [],
16
+ "sideEffects": false,
17
+ "exports": {
18
+ ".": "./dist/index.mjs",
19
+ "./package.json": "./package.json"
20
+ },
21
+ "main": "./dist/index.mjs",
22
+ "module": "./dist/index.mjs",
23
+ "types": "./dist/index.d.mts",
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "devDependencies": {
28
+ "@antfu/eslint-config": "^6.6.1",
29
+ "@antfu/ni": "^28.0.0",
30
+ "@antfu/utils": "^9.3.0",
31
+ "@types/node": "^25.0.1",
32
+ "bumpp": "^10.3.2",
33
+ "electron": "^34.2.0",
34
+ "eslint": "^9.39.2",
35
+ "lint-staged": "^16.2.7",
36
+ "publint": "^0.3.16",
37
+ "simple-git-hooks": "^2.13.1",
38
+ "tinyexec": "^1.0.2",
39
+ "tsdown": "^0.17.3",
40
+ "tsx": "^4.21.0",
41
+ "typescript": "^5.9.3",
42
+ "vite": "^7.2.7",
43
+ "vitest": "^4.0.15",
44
+ "vitest-package-exports": "^0.1.1",
45
+ "yaml": "^2.8.2"
46
+ },
47
+ "simple-git-hooks": {
48
+ "pre-commit": "pnpm i --frozen-lockfile --ignore-scripts --offline && npx lint-staged"
49
+ },
50
+ "lint-staged": {
51
+ "*": "eslint --fix"
52
+ },
53
+ "scripts": {
54
+ "build": "tsdown",
55
+ "dev": "tsdown --watch",
56
+ "lint": "eslint",
57
+ "release": "bumpp",
58
+ "start": "tsx src/index.ts",
59
+ "test": "vitest",
60
+ "typecheck": "tsc"
61
+ }
62
+ }