@nextclaw/x-harness 0.1.1 → 0.2.1
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 +231 -6
- package/README.md +15 -1
- package/SOURCE.md +9 -0
- package/cli/main.js +232 -200
- package/dist/api.d.ts +56 -2
- package/dist/api.js +130 -79
- package/dist/boundary.js +1 -0
- package/dist/browser-common.d.ts +11 -0
- package/dist/browser-common.js +34 -18
- package/dist/browser-dom-capabilities.d.ts +7 -2
- package/dist/browser-dom-capabilities.js +123 -102
- package/dist/browser-dom-dependencies.js +1 -0
- package/dist/browser-dom-editing.d.ts +20 -2
- package/dist/browser-dom-editing.js +148 -116
- package/dist/browser-dom-navigation.d.ts +11 -2
- package/dist/browser-dom-navigation.js +66 -50
- package/dist/browser-dom-submission.d.ts +6 -1
- package/dist/browser-dom-submission.js +37 -18
- package/dist/browser-dom.d.ts +13 -1
- package/dist/browser-dom.js +113 -66
- package/dist/browser-native.d.ts +14 -1
- package/dist/browser-native.js +161 -128
- package/dist/creator-native.d.ts +3 -0
- package/dist/creator-native.js +113 -0
- package/dist/diagnostics.d.ts +11 -0
- package/dist/diagnostics.js +10 -0
- package/dist/growth-batch.d.ts +6 -3
- package/dist/growth-batch.js +157 -135
- package/dist/growth-dependencies.js +1 -0
- package/dist/growth-execution.d.ts +8 -2
- package/dist/growth-execution.js +68 -37
- package/dist/growth-parallel.d.ts +6 -2
- package/dist/growth-parallel.js +78 -47
- package/dist/growth-plan.d.ts +8 -3
- package/dist/growth-plan.js +45 -23
- package/dist/growth-policy.d.ts +11 -2
- package/dist/growth-policy.js +80 -53
- package/dist/growth-publishing.d.ts +7 -2
- package/dist/growth-publishing.js +72 -46
- package/dist/growth-reading.d.ts +6 -2
- package/dist/growth-reading.js +47 -25
- package/dist/growth-recovery.d.ts +7 -4
- package/dist/growth-recovery.js +177 -148
- package/dist/growth-state.d.ts +10 -2
- package/dist/growth-state.js +67 -30
- package/dist/growth-tasks.d.ts +19 -16
- package/dist/growth-tasks.js +153 -90
- package/dist/growth.d.ts +25 -1
- package/dist/growth.js +207 -124
- package/dist/platform-capabilities.d.ts +6 -2
- package/dist/platform-capabilities.js +18 -8
- package/dist/platform-collections.d.ts +10 -3
- package/dist/platform-collections.js +62 -35
- package/dist/platform-dependencies.js +1 -0
- package/dist/platform-diagnostics.d.ts +7 -2
- package/dist/platform-diagnostics.js +31 -19
- package/dist/platform-observation.d.ts +10 -2
- package/dist/platform-observation.js +44 -23
- package/dist/platform-targets.d.ts +10 -3
- package/dist/platform-targets.js +39 -14
- package/dist/platform-writing.d.ts +10 -2
- package/dist/platform-writing.js +63 -32
- package/dist/platform.d.ts +22 -1
- package/dist/platform.js +120 -52
- package/dist/readers-ax-host.d.ts +11 -1
- package/dist/readers-ax-host.js +108 -99
- package/dist/readers-ax-program.js +5 -0
- package/dist/readers-ax-tree.js +1 -0
- package/dist/readers-cli.js +79 -0
- package/dist/readers.js +9 -0
- package/dist/round-state.js +18 -1
- package/dist/store.d.ts +22 -4
- package/dist/store.js +173 -151
- package/dist/types.js +1 -0
- package/generated/observation.mjs +1 -1
- package/package.json +23 -9
- package/skill/SKILL.md +2 -2
- package/skill/references/reads.md +1 -1
- package/skill/references/recovery.md +4 -4
- package/skill/references/setup.md +1 -1
|
@@ -1,21 +1,65 @@
|
|
|
1
1
|
import { attachImage } from "./readers-actions.js";
|
|
2
|
-
export
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
export class DOMEditing {
|
|
3
|
+
#session;
|
|
4
|
+
#unique;
|
|
5
|
+
#locator;
|
|
6
|
+
#tab;
|
|
7
|
+
#observe;
|
|
8
|
+
dismissMenu;
|
|
9
|
+
openControl;
|
|
10
|
+
openOwnPostMenu;
|
|
11
|
+
submitEditedPost;
|
|
12
|
+
expandPost;
|
|
13
|
+
nextPosts;
|
|
14
|
+
clearPreparedText;
|
|
15
|
+
closePreparedReply;
|
|
16
|
+
closePreparedMedia;
|
|
17
|
+
removePreparedMedia;
|
|
18
|
+
attachAssets;
|
|
19
|
+
follow;
|
|
20
|
+
openReply;
|
|
21
|
+
fillComposer;
|
|
22
|
+
submitPost;
|
|
23
|
+
submitReply;
|
|
24
|
+
constructor(dependencies) {
|
|
25
|
+
const { session, unique, locator, tab, observe } = dependencies;
|
|
26
|
+
this.#session = session;
|
|
27
|
+
this.#unique = unique;
|
|
28
|
+
this.#locator = locator;
|
|
29
|
+
this.#tab = tab;
|
|
30
|
+
this.#observe = observe;
|
|
31
|
+
this.dismissMenu = this.handleDismissMenu.bind(this);
|
|
32
|
+
this.openControl = this.handleOpenControl.bind(this);
|
|
33
|
+
this.openOwnPostMenu = this.handleOpenOwnPostMenu.bind(this);
|
|
34
|
+
this.submitEditedPost = this.handleSubmitEditedPost.bind(this);
|
|
35
|
+
this.expandPost = this.handleExpandPost.bind(this);
|
|
36
|
+
this.nextPosts = this.handleNextPosts.bind(this);
|
|
37
|
+
this.clearPreparedText = this.handleClearPreparedText.bind(this);
|
|
38
|
+
this.closePreparedReply = this.handleClosePreparedReply.bind(this);
|
|
39
|
+
this.closePreparedMedia = this.handleClosePreparedMedia.bind(this);
|
|
40
|
+
this.removePreparedMedia = this.handleRemovePreparedMedia.bind(this);
|
|
41
|
+
this.attachAssets = this.handleAttachAssets.bind(this);
|
|
42
|
+
this.follow = this.handleFollow.bind(this);
|
|
43
|
+
this.openReply = this.handleOpenReply.bind(this);
|
|
44
|
+
this.fillComposer = this.handleFillComposer.bind(this);
|
|
45
|
+
this.submitPost = this.handleSubmitPost.bind(this);
|
|
46
|
+
this.submitReply = this.handleSubmitPost.bind(this);
|
|
47
|
+
}
|
|
48
|
+
async handleDismissMenu(command) {
|
|
49
|
+
const { query, result } = this.#session.current;
|
|
6
50
|
let loc, submittedEditor;
|
|
7
51
|
if (query.view !== "menu" ||
|
|
8
52
|
result.needsStateCheck ||
|
|
9
53
|
!result.items?.length)
|
|
10
54
|
throw new Error("关闭菜单需已有菜单证据");
|
|
11
55
|
const item = result.items[0];
|
|
12
|
-
session.current = null;
|
|
13
|
-
await (await unique(locator(item.role, item.label))).press("Escape");
|
|
56
|
+
this.#session.current = null;
|
|
57
|
+
await (await this.#unique(this.#locator(item.role, item.label))).press("Escape");
|
|
14
58
|
return { completed: true };
|
|
15
59
|
return { completed: false, loc, submittedEditor };
|
|
16
60
|
}
|
|
17
|
-
async
|
|
18
|
-
const { query, result } = session.current;
|
|
61
|
+
async handleOpenControl(command) {
|
|
62
|
+
const { query, result } = this.#session.current;
|
|
19
63
|
let loc, submittedEditor;
|
|
20
64
|
if (!["navigation", "menu"].includes(query.view) || result.needsStateCheck)
|
|
21
65
|
throw new Error("导航控件须先唯一观察");
|
|
@@ -25,19 +69,19 @@ export function createDOMEditing(dependencies) {
|
|
|
25
69
|
!c.disabled);
|
|
26
70
|
if (matches.length !== 1)
|
|
27
71
|
throw new Error("导航控件未唯一观察");
|
|
28
|
-
session.current = null;
|
|
29
|
-
await (await unique(command.role === "text"
|
|
30
|
-
? tab.playwright.getByText(command.label, { exact: true })
|
|
31
|
-
: locator(command.role, command.label))).click();
|
|
72
|
+
this.#session.current = null;
|
|
73
|
+
await (await this.#unique(command.role === "text"
|
|
74
|
+
? this.#tab.playwright.getByText(command.label, { exact: true })
|
|
75
|
+
: this.#locator(command.role, command.label))).click();
|
|
32
76
|
if (command.href?.startsWith("/"))
|
|
33
|
-
await tab.playwright.waitForURL(`https://x.com${command.href}`, {
|
|
77
|
+
await this.#tab.playwright.waitForURL(`https://x.com${command.href}`, {
|
|
34
78
|
timeoutMs: 5000,
|
|
35
79
|
});
|
|
36
80
|
return { completed: true };
|
|
37
81
|
return { completed: false, loc, submittedEditor };
|
|
38
82
|
}
|
|
39
|
-
async
|
|
40
|
-
const { query, result } = session.current;
|
|
83
|
+
async handleOpenOwnPostMenu(command) {
|
|
84
|
+
const { query, result } = this.#session.current;
|
|
41
85
|
let loc, submittedEditor;
|
|
42
86
|
const post = result.posts?.[0];
|
|
43
87
|
if (query.view !== "posts" ||
|
|
@@ -50,32 +94,32 @@ export function createDOMEditing(dependencies) {
|
|
|
50
94
|
const more = post.buttons.filter((b) => /^(更多|More)$/.test(b.label) && !b.disabled);
|
|
51
95
|
if (more.length !== 1)
|
|
52
96
|
throw new Error("原帖更多按钮未唯一观察");
|
|
53
|
-
const article = locator("article").filter({
|
|
54
|
-
has: tab.playwright.locator(`a[href=${JSON.stringify(post.primary)}]`),
|
|
97
|
+
const article = this.#locator("article").filter({
|
|
98
|
+
has: this.#tab.playwright.locator(`a[href=${JSON.stringify(post.primary)}]`),
|
|
55
99
|
visible: true,
|
|
56
100
|
});
|
|
57
|
-
session.editBinding = {
|
|
101
|
+
this.#session.editBinding = {
|
|
58
102
|
postId: post.id,
|
|
59
103
|
accountHandle: result.accountHandle,
|
|
60
104
|
body: post.body,
|
|
61
105
|
};
|
|
62
|
-
session.current = null;
|
|
63
|
-
await (await unique((await unique(article)).getByRole("button", {
|
|
106
|
+
this.#session.current = null;
|
|
107
|
+
await (await this.#unique((await this.#unique(article)).getByRole("button", {
|
|
64
108
|
name: more[0].label,
|
|
65
109
|
exact: true,
|
|
66
110
|
}))).click();
|
|
67
111
|
return { completed: true };
|
|
68
112
|
return { completed: false, loc, submittedEditor };
|
|
69
113
|
}
|
|
70
|
-
async
|
|
71
|
-
const { query, result } = session.current;
|
|
114
|
+
async handleSubmitEditedPost(command) {
|
|
115
|
+
const { query, result } = this.#session.current;
|
|
72
116
|
let loc, submittedEditor;
|
|
73
117
|
if (query.view !== "composer" ||
|
|
74
118
|
result.needsStateCheck ||
|
|
75
119
|
result.alerts?.length ||
|
|
76
|
-
!session.editBinding ||
|
|
77
|
-
session.editBinding.postId !== command.postId ||
|
|
78
|
-
result.accountHandle !== session.editBinding.accountHandle ||
|
|
120
|
+
!this.#session.editBinding ||
|
|
121
|
+
this.#session.editBinding.postId !== command.postId ||
|
|
122
|
+
result.accountHandle !== this.#session.editBinding.accountHandle ||
|
|
79
123
|
!command.evidenceRef ||
|
|
80
124
|
!command.approvedText?.trim() ||
|
|
81
125
|
result.editorText !== command.approvedText ||
|
|
@@ -84,57 +128,57 @@ export function createDOMEditing(dependencies) {
|
|
|
84
128
|
const updates = result.buttons.filter((b) => /^(更新|Update)$/.test(b.label) && !b.disabled);
|
|
85
129
|
if (updates.length !== 1)
|
|
86
130
|
throw new Error("编辑更新按钮未唯一观察");
|
|
87
|
-
const update = await unique(locator("button", updates[0].label));
|
|
131
|
+
const update = await this.#unique(this.#locator("button", updates[0].label));
|
|
88
132
|
await command.beforeSubmit?.();
|
|
89
|
-
session.current = null;
|
|
90
|
-
session.editBinding = null;
|
|
133
|
+
this.#session.current = null;
|
|
134
|
+
this.#session.editBinding = null;
|
|
91
135
|
if (command.activation === "keyboard")
|
|
92
136
|
await update.press("Enter");
|
|
93
137
|
else
|
|
94
138
|
await update.click();
|
|
95
|
-
await tab.playwright.waitForLoadState({
|
|
139
|
+
await this.#tab.playwright.waitForLoadState({
|
|
96
140
|
state: "networkidle",
|
|
97
141
|
timeoutMs: 8000,
|
|
98
142
|
});
|
|
99
143
|
return { completed: true };
|
|
100
144
|
return { completed: false, loc, submittedEditor };
|
|
101
145
|
}
|
|
102
|
-
async
|
|
103
|
-
const { query, result } = session.current;
|
|
146
|
+
async handleExpandPost(command) {
|
|
147
|
+
const { query, result } = this.#session.current;
|
|
104
148
|
let loc, submittedEditor;
|
|
105
149
|
if (query.view !== "posts" ||
|
|
106
150
|
result.posts?.length !== 1 ||
|
|
107
151
|
result.posts[0].id !== command.postId)
|
|
108
152
|
throw new Error("展开需唯一原帖");
|
|
109
153
|
const primary = result.posts[0].primary;
|
|
110
|
-
const article = locator("article").filter({
|
|
111
|
-
has: tab.playwright.locator(`a[href=${JSON.stringify(primary)}]`),
|
|
154
|
+
const article = this.#locator("article").filter({
|
|
155
|
+
has: this.#tab.playwright.locator(`a[href=${JSON.stringify(primary)}]`),
|
|
112
156
|
visible: true,
|
|
113
157
|
});
|
|
114
|
-
const control = (await unique(article)).getByText(/^(显示更多|Show more)$/i);
|
|
115
|
-
session.current = null;
|
|
116
|
-
await (await unique(control)).click();
|
|
158
|
+
const control = (await this.#unique(article)).getByText(/^(显示更多|Show more)$/i);
|
|
159
|
+
this.#session.current = null;
|
|
160
|
+
await (await this.#unique(control)).click();
|
|
117
161
|
return { completed: true };
|
|
118
162
|
return { completed: false, loc, submittedEditor };
|
|
119
163
|
}
|
|
120
|
-
async
|
|
121
|
-
const { query, result } = session.current;
|
|
164
|
+
async handleNextPosts(command) {
|
|
165
|
+
const { query, result } = this.#session.current;
|
|
122
166
|
let loc, submittedEditor;
|
|
123
167
|
if (query.view !== "posts" || result.needsStateCheck)
|
|
124
168
|
throw new Error("翻页须先观察原列表");
|
|
125
169
|
const primary = result.posts?.at(-1)?.primary;
|
|
126
170
|
if (!primary)
|
|
127
171
|
throw new Error("翻页须有已观察原帖链接");
|
|
128
|
-
const anchor = await unique(tab.playwright
|
|
172
|
+
const anchor = await this.#unique(this.#tab.playwright
|
|
129
173
|
.locator(`a[href=${JSON.stringify(primary)}]`)
|
|
130
174
|
.filter({ visible: true }));
|
|
131
|
-
session.current = null;
|
|
175
|
+
this.#session.current = null;
|
|
132
176
|
await anchor.press("PageDown");
|
|
133
177
|
return { completed: true };
|
|
134
178
|
return { completed: false, loc, submittedEditor };
|
|
135
179
|
}
|
|
136
|
-
async
|
|
137
|
-
const { query, result } = session.current;
|
|
180
|
+
async handleClearPreparedText(command) {
|
|
181
|
+
const { query, result } = this.#session.current;
|
|
138
182
|
let loc, submittedEditor;
|
|
139
183
|
if (query.view !== "composer" ||
|
|
140
184
|
result.needsStateCheck ||
|
|
@@ -144,27 +188,30 @@ export function createDOMEditing(dependencies) {
|
|
|
144
188
|
command.text.replace(/\n{3,}/g, "\n\n") ||
|
|
145
189
|
result.editors?.length !== 1)
|
|
146
190
|
throw new Error("仅恢复本次已知稿件准备");
|
|
147
|
-
const editor = await unique(locator("textbox", result.editors[0].label));
|
|
191
|
+
const editor = await this.#unique(this.#locator("textbox", result.editors[0].label));
|
|
148
192
|
await editor.press("Meta+A");
|
|
149
193
|
await editor.press("Backspace");
|
|
150
|
-
return { completed: true, value: observe({ view: "composer" }) };
|
|
194
|
+
return { completed: true, value: this.#observe({ view: "composer" }) };
|
|
151
195
|
return { completed: false, loc, submittedEditor };
|
|
152
196
|
}
|
|
153
|
-
async
|
|
154
|
-
const { query, result } = session.current;
|
|
197
|
+
async handleClosePreparedReply(command) {
|
|
198
|
+
const { query, result } = this.#session.current;
|
|
155
199
|
let loc, submittedEditor;
|
|
156
200
|
if (query.view !== "composer" ||
|
|
157
201
|
result.needsStateCheck ||
|
|
158
202
|
result.editorText?.trim() !== "" ||
|
|
159
203
|
!command.evidenceRef)
|
|
160
204
|
throw new Error("仅关闭有准备失败证据的空回复");
|
|
161
|
-
await (await unique(locator("button", "关闭"))).click();
|
|
162
|
-
session.replyBinding = null;
|
|
163
|
-
return {
|
|
205
|
+
await (await this.#unique(this.#locator("button", "关闭"))).click();
|
|
206
|
+
this.#session.replyBinding = null;
|
|
207
|
+
return {
|
|
208
|
+
completed: true,
|
|
209
|
+
value: this.#observe({ view: "posts", limit: 5 }),
|
|
210
|
+
};
|
|
164
211
|
return { completed: false, loc, submittedEditor };
|
|
165
212
|
}
|
|
166
|
-
async
|
|
167
|
-
const { query, result } = session.current;
|
|
213
|
+
async handleClosePreparedMedia(command) {
|
|
214
|
+
const { query, result } = this.#session.current;
|
|
168
215
|
let loc, submittedEditor;
|
|
169
216
|
if (query.view !== "media" ||
|
|
170
217
|
result.needsStateCheck ||
|
|
@@ -173,15 +220,15 @@ export function createDOMEditing(dependencies) {
|
|
|
173
220
|
const backs = result.controls.filter((b) => b.role === "button" && /^(返回|Back)$/i.test(b.label) && !b.disabled);
|
|
174
221
|
if (backs.length !== 1)
|
|
175
222
|
throw new Error("媒体返回入口不唯一");
|
|
176
|
-
await (await unique(locator("button", backs[0].label))).click();
|
|
177
|
-
await tab.playwright
|
|
223
|
+
await (await this.#unique(this.#locator("button", backs[0].label))).click();
|
|
224
|
+
await this.#tab.playwright
|
|
178
225
|
.getByRole("textbox", { name: /^(帖子文本|Post text)$/i })
|
|
179
226
|
.waitFor({ state: "visible", timeoutMs: 5000 });
|
|
180
|
-
return { completed: true, value: observe({ view: "composer" }) };
|
|
227
|
+
return { completed: true, value: this.#observe({ view: "composer" }) };
|
|
181
228
|
return { completed: false, loc, submittedEditor };
|
|
182
229
|
}
|
|
183
|
-
async
|
|
184
|
-
const { query, result } = session.current;
|
|
230
|
+
async handleRemovePreparedMedia(command) {
|
|
231
|
+
const { query, result } = this.#session.current;
|
|
185
232
|
let loc, submittedEditor;
|
|
186
233
|
if (query.view !== "composer" ||
|
|
187
234
|
result.needsStateCheck ||
|
|
@@ -191,12 +238,12 @@ export function createDOMEditing(dependencies) {
|
|
|
191
238
|
const removes = result.buttons.filter((b) => /^(移除媒体|Remove media)$/i.test(b.label) && !b.disabled);
|
|
192
239
|
if (removes.length !== 1)
|
|
193
240
|
throw new Error("待恢复媒体不唯一");
|
|
194
|
-
await (await unique(locator("button", removes[0].label))).click();
|
|
195
|
-
return { completed: true, value: observe({ view: "composer" }) };
|
|
241
|
+
await (await this.#unique(this.#locator("button", removes[0].label))).click();
|
|
242
|
+
return { completed: true, value: this.#observe({ view: "composer" }) };
|
|
196
243
|
return { completed: false, loc, submittedEditor };
|
|
197
244
|
}
|
|
198
|
-
async
|
|
199
|
-
const { query, result } = session.current;
|
|
245
|
+
async handleAttachAssets(command) {
|
|
246
|
+
const { query, result } = this.#session.current;
|
|
200
247
|
let loc, submittedEditor;
|
|
201
248
|
if (query.view !== "composer" ||
|
|
202
249
|
result.needsStateCheck ||
|
|
@@ -205,49 +252,49 @@ export function createDOMEditing(dependencies) {
|
|
|
205
252
|
const asset = command.assets[0];
|
|
206
253
|
if (!asset.alt?.trim())
|
|
207
254
|
throw new Error("图片须有完整ALT");
|
|
208
|
-
session.current = null;
|
|
209
|
-
await attachImage(tab, { path: asset.path });
|
|
210
|
-
await tab.playwright
|
|
255
|
+
this.#session.current = null;
|
|
256
|
+
await attachImage(this.#tab, { path: asset.path });
|
|
257
|
+
await this.#tab.playwright
|
|
211
258
|
.getByRole("button", {
|
|
212
259
|
name: /^(?:编辑|Edit)(?:图片|照片|媒体| image| photo| media)?(?: \d+)?$/i,
|
|
213
260
|
})
|
|
214
261
|
.waitFor({ state: "visible", timeoutMs: 5000 });
|
|
215
|
-
const composer = await observe({ view: "composer" });
|
|
262
|
+
const composer = await this.#observe({ view: "composer" });
|
|
216
263
|
const edits = composer.buttons.filter((b) => /^(?:编辑|Edit)(?:图片|照片|媒体| image| photo| media)?(?: \d+)?$/i.test(b.label) && !b.disabled);
|
|
217
264
|
if (edits.length !== 1)
|
|
218
265
|
throw new Error("图片编辑入口未唯一观察");
|
|
219
|
-
await (await unique(locator("button", edits[0].label))).click();
|
|
220
|
-
await tab.playwright
|
|
266
|
+
await (await this.#unique(this.#locator("button", edits[0].label))).click();
|
|
267
|
+
await this.#tab.playwright
|
|
221
268
|
.getByRole("tab", {
|
|
222
269
|
name: /^(编辑图片描述|Edit image description)$/i,
|
|
223
270
|
})
|
|
224
271
|
.waitFor({ state: "visible", timeoutMs: 5000 });
|
|
225
|
-
let media = await observe({ view: "media" });
|
|
272
|
+
let media = await this.#observe({ view: "media" });
|
|
226
273
|
const tabControl = media.controls.find((c) => c.role === "tab" &&
|
|
227
274
|
/^(编辑图片描述|Edit image description)$/i.test(c.label));
|
|
228
275
|
if (!tabControl)
|
|
229
276
|
throw new Error("ALT页签未观察");
|
|
230
|
-
await (await unique(locator("tab", tabControl.label))).click();
|
|
231
|
-
media = await observe({ view: "media" });
|
|
277
|
+
await (await this.#unique(this.#locator("tab", tabControl.label))).click();
|
|
278
|
+
media = await this.#observe({ view: "media" });
|
|
232
279
|
if (media.needsStateCheck || media.fields.length !== 1)
|
|
233
280
|
throw new Error("ALT输入未唯一观察");
|
|
234
281
|
const field = media.fields[0];
|
|
235
282
|
const input = field.label
|
|
236
|
-
? tab.playwright.getByLabel(field.label, { exact: true })
|
|
283
|
+
? this.#tab.playwright.getByLabel(field.label, { exact: true })
|
|
237
284
|
: field.placeholder
|
|
238
|
-
? tab.playwright.getByPlaceholder(field.placeholder, {
|
|
285
|
+
? this.#tab.playwright.getByPlaceholder(field.placeholder, {
|
|
239
286
|
exact: true,
|
|
240
287
|
})
|
|
241
|
-
: locator("textbox");
|
|
242
|
-
await (await unique(input)).fill(asset.alt);
|
|
243
|
-
media = await observe({ view: "media" });
|
|
288
|
+
: this.#locator("textbox");
|
|
289
|
+
await (await this.#unique(input)).fill(asset.alt);
|
|
290
|
+
media = await this.#observe({ view: "media" });
|
|
244
291
|
if (media.fields[0]?.value !== asset.alt)
|
|
245
292
|
throw new Error("ALT全文不匹配");
|
|
246
293
|
const save = media.controls.filter((c) => c.role === "button" && /^(保存|Save)$/i.test(c.label) && !c.disabled);
|
|
247
294
|
if (save.length !== 1)
|
|
248
295
|
throw new Error("ALT保存入口不唯一");
|
|
249
|
-
await (await unique(locator("button", save[0].label))).click();
|
|
250
|
-
const after = await observe({ view: "composer" });
|
|
296
|
+
await (await this.#unique(this.#locator("button", save[0].label))).click();
|
|
297
|
+
const after = await this.#observe({ view: "composer" });
|
|
251
298
|
if (after.needsStateCheck || !after.images?.length)
|
|
252
299
|
throw new Error("上传图片未回到编辑器");
|
|
253
300
|
return {
|
|
@@ -260,8 +307,8 @@ export function createDOMEditing(dependencies) {
|
|
|
260
307
|
};
|
|
261
308
|
return { completed: false, loc, submittedEditor };
|
|
262
309
|
}
|
|
263
|
-
async
|
|
264
|
-
const { query, result } = session.current;
|
|
310
|
+
async handleFollow(command) {
|
|
311
|
+
const { query, result } = this.#session.current;
|
|
265
312
|
let loc, submittedEditor;
|
|
266
313
|
if (query.view !== "person" ||
|
|
267
314
|
query.handle !== command.handle ||
|
|
@@ -270,13 +317,13 @@ export function createDOMEditing(dependencies) {
|
|
|
270
317
|
const name = result.relationship === "follows-you"
|
|
271
318
|
? `回关 @${command.handle}`
|
|
272
319
|
: `关注 @${command.handle}`;
|
|
273
|
-
loc = await unique(locator("button", name));
|
|
320
|
+
loc = await this.#unique(this.#locator("button", name));
|
|
274
321
|
return { completed: false, loc, submittedEditor };
|
|
275
322
|
}
|
|
276
|
-
async
|
|
277
|
-
const { query, result } = session.current;
|
|
323
|
+
async handleOpenReply(command) {
|
|
324
|
+
const { query, result } = this.#session.current;
|
|
278
325
|
let loc, submittedEditor;
|
|
279
|
-
session.replyBinding = null;
|
|
326
|
+
this.#session.replyBinding = null;
|
|
280
327
|
if (query.targetId !== command.postId || result.posts?.length !== 1)
|
|
281
328
|
throw new Error("回复主帖未唯一绑定");
|
|
282
329
|
const names = result.posts[0].buttons.filter((b) => /回复|Reply/i.test(b.label));
|
|
@@ -285,35 +332,35 @@ export function createDOMEditing(dependencies) {
|
|
|
285
332
|
const primary = result.posts[0].primary;
|
|
286
333
|
if (!primary || primary.match(/\/status\/(\d+)$/)?.[1] !== command.postId)
|
|
287
334
|
throw new Error("回复主帖永久链接未绑定");
|
|
288
|
-
const article = locator("article").filter({
|
|
289
|
-
has: tab.playwright.locator(`a[href=${JSON.stringify(primary)}]`),
|
|
335
|
+
const article = this.#locator("article").filter({
|
|
336
|
+
has: this.#tab.playwright.locator(`a[href=${JSON.stringify(primary)}]`),
|
|
290
337
|
visible: true,
|
|
291
338
|
});
|
|
292
|
-
loc = await unique((await unique(article)).getByRole("button", {
|
|
339
|
+
loc = await this.#unique((await this.#unique(article)).getByRole("button", {
|
|
293
340
|
name: names[0].label,
|
|
294
341
|
exact: true,
|
|
295
342
|
}));
|
|
296
343
|
return { completed: false, loc, submittedEditor };
|
|
297
344
|
}
|
|
298
|
-
async
|
|
299
|
-
const { query, result } = session.current;
|
|
345
|
+
async handleFillComposer(command) {
|
|
346
|
+
const { query, result } = this.#session.current;
|
|
300
347
|
let loc, submittedEditor;
|
|
301
348
|
if (query.view !== "composer" || result.editors?.length !== 1)
|
|
302
349
|
throw new Error("编辑器未唯一观察");
|
|
303
|
-
loc = await unique(locator("textbox", result.editors[0].label));
|
|
304
|
-
session.current = null;
|
|
350
|
+
loc = await this.#unique(this.#locator("textbox", result.editors[0].label));
|
|
351
|
+
this.#session.current = null;
|
|
305
352
|
await loc.fill(command.text);
|
|
306
353
|
const submits = result.buttons.filter((b) => /^(回复|Reply|发帖|发布|Post|更新|Update)$/i.test(b.label));
|
|
307
354
|
if (submits.length !== 1)
|
|
308
355
|
throw new Error("编辑后提交入口不唯一");
|
|
309
|
-
await locator("button", submits[0].label)
|
|
310
|
-
.and(tab.playwright.locator(':not([disabled]):not([aria-disabled="true"])'))
|
|
356
|
+
await this.#locator("button", submits[0].label)
|
|
357
|
+
.and(this.#tab.playwright.locator(':not([disabled]):not([aria-disabled="true"])'))
|
|
311
358
|
.waitFor({ state: "visible", timeoutMs: 5000 });
|
|
312
359
|
return { completed: true };
|
|
313
360
|
return { completed: false, loc, submittedEditor };
|
|
314
361
|
}
|
|
315
|
-
async
|
|
316
|
-
const { query, result } = session.current;
|
|
362
|
+
async handleSubmitPost(command) {
|
|
363
|
+
const { query, result } = this.#session.current;
|
|
317
364
|
let loc, submittedEditor;
|
|
318
365
|
if (query.view !== "composer")
|
|
319
366
|
throw new Error("未观察编辑器");
|
|
@@ -323,7 +370,7 @@ export function createDOMEditing(dependencies) {
|
|
|
323
370
|
const buttons = result.buttons?.filter((b) => pattern.test(b.label) && !b.disabled) ?? [];
|
|
324
371
|
if (buttons.length !== 1)
|
|
325
372
|
throw new Error("提交按钮未唯一观察");
|
|
326
|
-
loc = await unique(locator("button", buttons[0].label));
|
|
373
|
+
loc = await this.#unique(this.#locator("button", buttons[0].label));
|
|
327
374
|
if (command.activation !== undefined &&
|
|
328
375
|
!["click", "keyboard"].includes(command.activation))
|
|
329
376
|
throw new Error("提交激活方式无效");
|
|
@@ -332,7 +379,7 @@ export function createDOMEditing(dependencies) {
|
|
|
332
379
|
if (command.operation === "submitReply") {
|
|
333
380
|
if (result.editors?.length !== 1 || !result.editorText?.trim())
|
|
334
381
|
throw new Error("回复提交缺唯一完整编辑器");
|
|
335
|
-
submittedEditor = locator("textbox", result.editors[0].label).filter({
|
|
382
|
+
submittedEditor = this.#locator("textbox", result.editors[0].label).filter({
|
|
336
383
|
hasText: new RegExp(
|
|
337
384
|
// Full text and actor are checked by platform.compose before this act.
|
|
338
385
|
// Use a short observed anchor for the retained-text wait: host locator
|
|
@@ -343,14 +390,14 @@ export function createDOMEditing(dependencies) {
|
|
|
343
390
|
.slice(0, 64)
|
|
344
391
|
.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")),
|
|
345
392
|
});
|
|
346
|
-
await unique(submittedEditor);
|
|
393
|
+
await this.#unique(submittedEditor);
|
|
347
394
|
}
|
|
348
395
|
else {
|
|
349
396
|
if (result.editors?.length !== 1 || !result.editorText?.trim())
|
|
350
397
|
throw new Error("发帖提交缺唯一完整编辑器");
|
|
351
398
|
// Publication with media may stay pending after the click. Do not let
|
|
352
399
|
// receipt navigation interrupt it while the submitted text is retained.
|
|
353
|
-
submittedEditor = locator("textbox", result.editors[0].label).filter({
|
|
400
|
+
submittedEditor = this.#locator("textbox", result.editors[0].label).filter({
|
|
354
401
|
hasText: new RegExp(
|
|
355
402
|
// Full text and actor are checked by platform.compose before this act.
|
|
356
403
|
// Use a short observed anchor for the retained-text wait: host locator
|
|
@@ -361,26 +408,11 @@ export function createDOMEditing(dependencies) {
|
|
|
361
408
|
.slice(0, 64)
|
|
362
409
|
.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")),
|
|
363
410
|
});
|
|
364
|
-
await unique(submittedEditor);
|
|
411
|
+
await this.#unique(submittedEditor);
|
|
365
412
|
}
|
|
366
413
|
return { completed: false, loc, submittedEditor };
|
|
367
414
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
openOwnPostMenu,
|
|
372
|
-
submitEditedPost,
|
|
373
|
-
expandPost,
|
|
374
|
-
nextPosts,
|
|
375
|
-
clearPreparedText,
|
|
376
|
-
closePreparedReply,
|
|
377
|
-
closePreparedMedia,
|
|
378
|
-
removePreparedMedia,
|
|
379
|
-
attachAssets,
|
|
380
|
-
follow,
|
|
381
|
-
openReply,
|
|
382
|
-
fillComposer,
|
|
383
|
-
submitPost,
|
|
384
|
-
submitReply: submitPost,
|
|
385
|
-
};
|
|
415
|
+
}
|
|
416
|
+
export function createDOMEditing(...args) {
|
|
417
|
+
return new DOMEditing(...args);
|
|
386
418
|
}
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import type { DOMDependencies } from "./browser-dom-dependencies.js";
|
|
2
|
-
export declare
|
|
2
|
+
export declare class DOMNavigation {
|
|
3
|
+
#private;
|
|
3
4
|
searchSources: (command: any) => Promise<void>;
|
|
4
5
|
waitForFolder: (command: any) => Promise<void>;
|
|
5
6
|
waitForPosts: (command: any) => Promise<void>;
|
|
6
7
|
openObserved: (command: any) => Promise<void>;
|
|
7
8
|
refreshPerson: (command: any) => Promise<void>;
|
|
8
9
|
refreshPosts: (command: any) => Promise<void>;
|
|
9
|
-
|
|
10
|
+
constructor(dependencies: Pick<DOMDependencies, "session" | "tab" | "observe">);
|
|
11
|
+
private handleSearchSources;
|
|
12
|
+
private handleWaitForFolder;
|
|
13
|
+
private handleWaitForPosts;
|
|
14
|
+
private handleOpenObserved;
|
|
15
|
+
private handleRefreshPerson;
|
|
16
|
+
private handleRefreshPosts;
|
|
17
|
+
}
|
|
18
|
+
export declare function createDOMNavigation(...args: ConstructorParameters<typeof DOMNavigation>): DOMNavigation;
|