Skip to content

Commit e29150b

Browse files
fix(weibo/publish): replace brittle CSS-module hash with placeholder selector (#1625)
* fix(weibo/publish): replace brittle CSS-module hash with placeholder selector `clis/weibo/publish.js` matched the compose textarea via `textarea._input_13iqr_8`, where `_input_13iqr_8` is the Vite CSS-module hash Weibo rebuilds on every frontend deploy. The hash drifted (current build emits `_input_1f5hn_8`), so step 4 of the publish flow throws "Weibo compose editor did not appear" before anything else can run. Reported in #1602. Replace the single hashed selector with a placeholder-text-based chain that survives Weibo's CSS-module rebuilds: textarea[placeholder*="有什么新鲜事"] textarea[placeholder*="新鲜事"] textarea._input_13iqr_8 // legacy hash kept last for older variants Two visible textareas can match on the home feed (the always-rendered "home-strip" prompt + the post-click modal compose). Pick the LAST visible candidate: the modal opens on top and is appended to DOM later, so the last-visible textarea is the modal. Both the editor-visibility poll (Step 4) and the text-insertion step (Step 6) use the same chain. Also drops `evaluateWithArgs` from Step 8 success polling. The IIFE there does not reference any outer args, but `evaluateWithArgs` injects its `const`-bound parameter names into the page context, and re-running on each iteration of the success-poll loop threw `Identifier 'maxIterations' has already been declared` after the first iteration. This was masked previously because Step 4 always failed first; with the selector fixed, the latent Step 8 bug surfaces. Switched to plain `page.evaluate` to avoid re-declaring per loop. Closes #1602. Verified live on macOS / opencli built locally / extension v1.0.15, weibo cookie session: - `opencli weibo publish "明洞那家店真不错"` returned `status: success, message: 发布成功, text: 明洞那家店真不错` - Confirmed via `/ajax/statuses/mymblog`: the post landed at `idstr=5299403716821218`, `mblogid=QFHWzsCvE`, text matches what was typed (proves selector chain picks the right textarea and the text insertion path works end-to-end) - Cleaned up: deleted via the same `/ajax/statuses/destroy` path that PR #1620 exposes as `weibo delete` Unit tests: 8 / 8 in `clis/weibo/publish.test.js` pass (mocks updated to reflect the new `evaluate`-vs-`evaluateWithArgs` split for Step 8 and the longer poll window). * test(weibo): lock publish placeholder selector path --------- Co-authored-by: jackwener <jakevingoo@gmail.com>
1 parent a50074d commit e29150b

2 files changed

Lines changed: 51 additions & 19 deletions

File tree

clis/weibo/publish.js

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,15 @@ const SUBMIT_POLL_MS = 500;
3030
const SUBMIT_TIMEOUT_MS = 20_000;
3131
const SUPPORTED_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp']);
3232

33-
// Weibo PC UI selectors
34-
const TEXTAREA_SELECTOR = 'textarea._input_13iqr_8';
33+
// Weibo PC UI selectors. The CSS-module hash drifts on every frontend
34+
// rebuild (#1602), so match on the stable placeholder text and keep the
35+
// legacy hash as a last-resort fallback. Callers pick the LAST visible
36+
// match because the compose modal renders on top of the home-feed strip.
37+
const TEXTAREA_SELECTORS = [
38+
'textarea[placeholder*="有什么新鲜事"]',
39+
'textarea[placeholder*="新鲜事"]',
40+
'textarea._input_13iqr_8',
41+
];
3542
const FILE_INPUT_SELECTOR = 'input[type="file"][class*="_file_"]';
3643

3744
function validateText(text) {
@@ -125,12 +132,19 @@ cli({
125132
let editorFound = false;
126133
for (let i = 0; i < Math.ceil(COMPOSE_TIMEOUT_MS / COMPOSE_POLL_MS); i++) {
127134
const result = await page.evaluate(`
128-
() => {
129-
const ta = document.querySelector('textarea._input_13iqr_8');
130-
if (!ta) return { found: false };
131-
const visible = ta.offsetParent !== null;
132-
return { found: true, visible, rectTop: visible ? ta.getBoundingClientRect().top : -1 };
133-
}
135+
(selectors => {
136+
// Pick the LAST visible match across all selectors so
137+
// the modal (rendered on top of the home-feed strip)
138+
// wins over earlier matches. See TEXTAREA_SELECTORS.
139+
let last = null;
140+
for (const sel of selectors) {
141+
for (const t of document.querySelectorAll(sel)) {
142+
if (t.offsetParent !== null) last = t;
143+
}
144+
}
145+
if (!last) return { found: false };
146+
return { found: true, visible: true, rectTop: last.getBoundingClientRect().top };
147+
})(${JSON.stringify(TEXTAREA_SELECTORS)})
134148
`);
135149
if (result?.found && result.visible && result.rectTop >= 0) {
136150
editorFound = true;
@@ -187,9 +201,14 @@ cli({
187201
// IMPORTANT: Using nativeSetter preserves the textarea's reactive/internal state.
188202
// Direct ta.value= assignment bypasses Weibo's Vue reactivity and causes "undefined" content.
189203
const insertResult = await page.evaluateWithArgs(`
190-
(() => {
191-
const ta = document.querySelector('textarea._input_13iqr_8');
192-
if (!ta || ta.offsetParent === null) return { ok: false, message: 'textarea not visible' };
204+
((selectors) => {
205+
let ta = null;
206+
for (const sel of selectors) {
207+
for (const t of document.querySelectorAll(sel)) {
208+
if (t.offsetParent !== null) ta = t;
209+
}
210+
}
211+
if (!ta) return { ok: false, message: 'textarea not visible' };
193212
ta.focus();
194213
const nativeSetter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value')?.set;
195214
if (nativeSetter) {
@@ -200,7 +219,7 @@ cli({
200219
ta.dispatchEvent(new Event('input', { bubbles: true }));
201220
ta.dispatchEvent(new Event('change', { bubbles: true }));
202221
return { ok: true, valueLength: ta.value.length };
203-
})()
222+
})(${JSON.stringify(TEXTAREA_SELECTORS)})
204223
`, { textContent: text });
205224

206225
if (!insertResult?.ok) {
@@ -233,10 +252,14 @@ cli({
233252
}
234253

235254
// Step 8: Wait for success/failure result
255+
// Use page.evaluate (not evaluateWithArgs): the IIFE doesn't reference
256+
// any outer args, and evaluateWithArgs would re-declare its const
257+
// bindings each loop iteration in the same page context, throwing
258+
// "Identifier already declared" after the first iteration.
236259
let finalResult = null;
237260
for (let i = 0; i < Math.ceil(SUBMIT_TIMEOUT_MS / SUBMIT_POLL_MS); i++) {
238261
await page.wait({ time: SUBMIT_POLL_MS / 1000 });
239-
finalResult = await page.evaluateWithArgs(`
262+
finalResult = await page.evaluate(`
240263
(() => {
241264
const successMarkers = ['发布成功', '已发布', '发送成功'];
242265
const errorMarkers = ['发布失败', '发送失败', '内容违规', '请稍后再试', '频繁'];
@@ -257,7 +280,7 @@ cli({
257280
}
258281
return null;
259282
})()
260-
`, { maxIterations: Math.ceil(SUBMIT_TIMEOUT_MS / SUBMIT_POLL_MS), currentIndex: i });
283+
`);
261284
if (finalResult !== null) break;
262285
}
263286

clis/weibo/publish.test.js

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,17 +61,20 @@ describe('weibo publish command', () => {
6161
{ ok: true },
6262
{ found: true, visible: true, rectTop: 100 },
6363
{ ok: true, label: '发送' },
64+
{ ok: true, message: '发送成功' },
6465
],
6566
evaluateWithArgsResults: [
6667
{ ok: true, valueLength: 5 },
67-
{ ok: true, message: '发送成功' },
6868
],
6969
});
7070

7171
const result = await command.func(page, { text: 'hello' });
7272

7373
expect(result).toEqual([{ status: 'success', message: '发送成功', text: 'hello' }]);
7474
expect(page.goto).toHaveBeenCalledWith('https://weibo.com', { waitUntil: 'load', settleMs: 2000 });
75+
expect(page.evaluate.mock.calls[2][0]).toContain('有什么新鲜事');
76+
expect(page.evaluate.mock.calls[2][0]).toContain('textarea._input_13iqr_8');
77+
expect(page.evaluateWithArgs.mock.calls[0][0]).toContain('有什么新鲜事');
7578
});
7679

7780
it('uploads up to nine images before publishing', async () => {
@@ -83,11 +86,11 @@ describe('weibo publish command', () => {
8386
{ found: true, visible: true, rectTop: 100 },
8487
true,
8588
{ ok: true, label: '发送' },
89+
{ ok: true, message: '发送成功' },
8690
],
8791
evaluateWithArgsResults: [
8892
{ ok: true, count: 2 },
8993
{ ok: true, valueLength: 11 },
90-
{ ok: true, message: '发送成功' },
9194
],
9295
});
9396

@@ -149,10 +152,10 @@ describe('weibo publish command', () => {
149152
{ ok: true },
150153
{ found: true, visible: true, rectTop: 100 },
151154
{ ok: true, label: '发送' },
155+
{ ok: false, message: '内容违规' },
152156
],
153157
evaluateWithArgsResults: [
154158
{ ok: true, valueLength: 5 },
155-
{ ok: false, message: '内容违规' },
156159
],
157160
});
158161

@@ -161,22 +164,28 @@ describe('weibo publish command', () => {
161164

162165
it('does not treat editor close as positive publish proof', async () => {
163166
const command = getCommand();
167+
// Step 8 polls up to SUBMIT_TIMEOUT_MS / SUBMIT_POLL_MS iterations
168+
// (= 20000 / 500 = 40 in upstream). Derive the window programmatically
169+
// so the test stays aligned with the implementation if the timeout
170+
// changes, and override the makePage default { ok: true } fallback that
171+
// would otherwise satisfy the success-marker break.
172+
const SUBMIT_POLL_ITERATIONS = Math.ceil(20_000 / 500);
164173
const page = makePage({
165174
evaluateResults: [
166175
'123456',
167176
{ ok: true },
168177
{ found: true, visible: true, rectTop: 100 },
169178
{ ok: true, label: '发送' },
179+
...Array(SUBMIT_POLL_ITERATIONS).fill(null),
170180
],
171181
evaluateWithArgsResults: [
172182
{ ok: true, valueLength: 5 },
173-
null,
174183
],
175184
});
176185

177186
await expect(command.func(page, { text: 'hello' })).rejects.toBeInstanceOf(CommandExecutionError);
178187

179-
const submitScript = page.evaluateWithArgs.mock.calls.at(-1)[0];
188+
const submitScript = page.evaluate.mock.calls.at(-1)[0];
180189
expect(submitScript).not.toContain('Editor closed after publish');
181190
expect(submitScript).toContain('发布成功');
182191
});

0 commit comments

Comments
 (0)