@nextclaw/x-harness 0.1.1 → 0.2.0

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.
Files changed (79) hide show
  1. package/LICENSE +231 -6
  2. package/README.md +15 -1
  3. package/SOURCE.md +9 -0
  4. package/cli/main.js +232 -200
  5. package/dist/api.d.ts +56 -2
  6. package/dist/api.js +130 -79
  7. package/dist/boundary.js +1 -0
  8. package/dist/browser-common.d.ts +11 -0
  9. package/dist/browser-common.js +34 -18
  10. package/dist/browser-dom-capabilities.d.ts +7 -2
  11. package/dist/browser-dom-capabilities.js +123 -102
  12. package/dist/browser-dom-dependencies.js +1 -0
  13. package/dist/browser-dom-editing.d.ts +20 -2
  14. package/dist/browser-dom-editing.js +148 -116
  15. package/dist/browser-dom-navigation.d.ts +11 -2
  16. package/dist/browser-dom-navigation.js +66 -50
  17. package/dist/browser-dom-submission.d.ts +6 -1
  18. package/dist/browser-dom-submission.js +37 -18
  19. package/dist/browser-dom.d.ts +13 -1
  20. package/dist/browser-dom.js +113 -66
  21. package/dist/browser-native.d.ts +14 -1
  22. package/dist/browser-native.js +161 -128
  23. package/dist/creator-native.d.ts +3 -0
  24. package/dist/creator-native.js +113 -0
  25. package/dist/diagnostics.d.ts +11 -0
  26. package/dist/diagnostics.js +10 -0
  27. package/dist/growth-batch.d.ts +6 -3
  28. package/dist/growth-batch.js +157 -135
  29. package/dist/growth-dependencies.js +1 -0
  30. package/dist/growth-execution.d.ts +8 -2
  31. package/dist/growth-execution.js +68 -37
  32. package/dist/growth-parallel.d.ts +6 -2
  33. package/dist/growth-parallel.js +78 -47
  34. package/dist/growth-plan.d.ts +8 -3
  35. package/dist/growth-plan.js +45 -23
  36. package/dist/growth-policy.d.ts +11 -2
  37. package/dist/growth-policy.js +80 -53
  38. package/dist/growth-publishing.d.ts +7 -2
  39. package/dist/growth-publishing.js +72 -46
  40. package/dist/growth-reading.d.ts +6 -2
  41. package/dist/growth-reading.js +47 -25
  42. package/dist/growth-recovery.d.ts +7 -4
  43. package/dist/growth-recovery.js +177 -148
  44. package/dist/growth-state.d.ts +10 -2
  45. package/dist/growth-state.js +67 -30
  46. package/dist/growth-tasks.d.ts +19 -16
  47. package/dist/growth-tasks.js +153 -90
  48. package/dist/growth.d.ts +25 -1
  49. package/dist/growth.js +207 -124
  50. package/dist/platform-capabilities.d.ts +6 -2
  51. package/dist/platform-capabilities.js +18 -8
  52. package/dist/platform-collections.d.ts +10 -3
  53. package/dist/platform-collections.js +62 -35
  54. package/dist/platform-dependencies.js +1 -0
  55. package/dist/platform-diagnostics.d.ts +7 -2
  56. package/dist/platform-diagnostics.js +31 -19
  57. package/dist/platform-observation.d.ts +10 -2
  58. package/dist/platform-observation.js +44 -23
  59. package/dist/platform-targets.d.ts +10 -3
  60. package/dist/platform-targets.js +39 -14
  61. package/dist/platform-writing.d.ts +10 -2
  62. package/dist/platform-writing.js +63 -32
  63. package/dist/platform.d.ts +22 -1
  64. package/dist/platform.js +120 -52
  65. package/dist/readers-ax-host.d.ts +11 -1
  66. package/dist/readers-ax-host.js +108 -99
  67. package/dist/readers-ax-program.js +5 -0
  68. package/dist/readers-ax-tree.js +1 -0
  69. package/dist/readers-cli.js +79 -0
  70. package/dist/readers.js +9 -0
  71. package/dist/round-state.js +18 -1
  72. package/dist/store.d.ts +22 -4
  73. package/dist/store.js +173 -151
  74. package/dist/types.js +1 -0
  75. package/generated/observation.mjs +1 -1
  76. package/package.json +23 -9
  77. package/skill/SKILL.md +2 -2
  78. package/skill/references/reads.md +1 -1
  79. package/skill/references/setup.md +1 -1
@@ -1,21 +1,65 @@
1
1
  import { attachImage } from "./readers-actions.js";
2
- export function createDOMEditing(dependencies) {
3
- const { session, unique, locator, tab, observe } = dependencies;
4
- async function dismissMenu(command) {
5
- const { query, result } = session.current;
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 function openControl(command) {
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 function openOwnPostMenu(command) {
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 function submitEditedPost(command) {
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 function expandPost(command) {
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 function nextPosts(command) {
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 function clearPreparedText(command) {
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 function closePreparedReply(command) {
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 { completed: true, value: observe({ view: "posts", limit: 5 }) };
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 function closePreparedMedia(command) {
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 function removePreparedMedia(command) {
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 function attachAssets(command) {
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 function follow(command) {
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 function openReply(command) {
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 function fillComposer(command) {
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 function submitPost(command) {
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
- return {
369
- dismissMenu,
370
- openControl,
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 function createDOMNavigation(dependencies: Pick<DOMDependencies, "session" | "tab" | "observe">): {
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;