Skip to content

Commit 1652362

Browse files
committed
🐛 保持验证驱动器的活动页面身份
1 parent c0dd9ab commit 1652362

2 files changed

Lines changed: 59 additions & 28 deletions

File tree

e2e/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ None are required; each one only switches on when set. `.env` is **not** loaded
123123
| `E2E_PROXY` | [`fixtures.ts`](./fixtures.ts), [`agent-fixtures.ts`](./agent-fixtures.ts) | Chromium proxy for the launched context. Falls back to `https_proxy` / `http_proxy` / `HTTPS_PROXY` / `HTTP_PROXY`. Needed for the non-hermetic specs above on a restricted network. |
124124
| `E2E_RECORD_VIDEO_DIR` | [`fixtures.ts`](./fixtures.ts) **only** | Records video into that directory. Off by default. Point it at your scenario directory, e.g. `e2e/scratch/<scenario>/videos`. `server-fixtures.ts` / `agent-fixtures.ts`, and any spec that copies a fixture inline instead of importing it, do **not** honour this. |
125125
| `E2E_ONEDRIVE_TOKEN_FILE` | local scratch scripts only — **not referenced by any committed file** | Path to a OneDrive token JSON for real-provider cloud-sync verification, conventionally defaulting to `~/.config/scriptcat/e2e-onedrive-token.json`. Real account, real side effects — only with authorization. Recorded here because nothing in-tree can tell you it exists. |
126-
| `E2E_HEADED` | `headlessArgs()` in [`fixtures.ts`](./fixtures.ts), imported by every other fixture | Launches a **visible** window instead of the default `--headless=new`. For watching a run by eye; leave it unset otherwise. `session.mjs --headed` does the same for a session. |
126+
| `E2E_HEADED` | `headlessArgs()` in [`fixtures.ts`](./fixtures.ts), imported by every other fixture | Set to `1`, `true`, or `yes` to launch a **visible** window instead of the default `--headless=new`; unset it (or use `0`/`false`) otherwise. `session.mjs --headed` does the same for a session. |
127127
| `CI` | every fixture, plus [`playwright.config.ts`](../playwright.config.ts) | Disables the Chromium sandbox, and switches Playwright to 1 retry / 2 workers / HTML reporter / `forbidOnly`. Set by GitHub Actions; don't set it by hand. |
128128

129129
Secrets never belong in a committed spec or in `report.md` — see the redaction rules in
@@ -177,7 +177,7 @@ node e2e/session.mjs stop <scenario> | --all # 停止并清理 profile
177177
and then relaunches from the same profile, because `updateExtensionConfiguration` reloads the extension and its
178178
own pages answer `ERR_BLOCKED_BY_CLIENT` while that happens. It also sweeps the onboarding tabs the extension
179179
opens on install, so `pages` starts clean — one that arrives later simply stays, which is why the current page
180-
is tracked by URL rather than by index.
180+
is tracked by CDP target identity rather than by index; URL remains a fallback for old evidence files.
181181

182182
### Driving it
183183

@@ -201,7 +201,7 @@ node e2e/drive.mjs --scenario <scenario> <command> # 多会话时必须指名
201201
| `pages` / `use <i>` / `close` | Page management; `` marks the current page |
202202
| `console [n]` | Last `n` lines the session recorded, across all contexts (see below) |
203203

204-
The current page is tracked by URL, not by index — the extension opens pages of its own, and indices shift.
204+
The current page is tracked by CDP target identity, not by index — the extension opens pages of its own, and indices shift.
205205

206206
Clicking starts from `snapshot`, because the UI is Tailwind-classed and reading class strings tells you nothing
207207
about what is clickable. It prefers `data-testid`, falls back to `#id`, then to `text="…"`, and marks disabled

e2e/drive.mjs

Lines changed: 56 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,37 @@ function activeFile(session) {
4444

4545
function readActive(session) {
4646
try {
47-
return fs.readFileSync(activeFile(session), "utf8").trim();
47+
const value = fs.readFileSync(activeFile(session), "utf8").trim();
48+
if (!value) return null;
49+
try {
50+
return JSON.parse(value);
51+
} catch {
52+
return { url: value };
53+
}
4854
} catch {
49-
return "";
55+
return null;
56+
}
57+
}
58+
59+
function clearActive(session) {
60+
fs.rmSync(activeFile(session), { force: true });
61+
}
62+
63+
async function pageTargetId(context, page) {
64+
const cdp = await context.newCDPSession(page);
65+
try {
66+
const { targetInfo } = await cdp.send("Target.getTargetInfo");
67+
return targetInfo.targetId;
68+
} finally {
69+
await cdp.detach().catch(() => {});
5070
}
5171
}
5272

53-
function writeActive(session, url) {
54-
fs.writeFileSync(activeFile(session), url);
73+
async function writeActive(context, session, page) {
74+
fs.writeFileSync(
75+
activeFile(session),
76+
`${JSON.stringify({ targetId: await pageTargetId(context, page), url: page.url() })}\n`
77+
);
5578
}
5679

5780
function logAction(session, line) {
@@ -72,17 +95,22 @@ async function anyExtensionPage(context, session) {
7295
}
7396

7497
/**
75-
* 按 URL 而不是下标定位当前页:扩展自己会开引导页,下标随时会错位。
76-
* 依次退让到「同源同路径」(hash 路由跳转)和最后一个页面。
98+
* 按 CDP targetId 而不是下标定位当前页:扩展自己会开引导页,下标随时会错位。
99+
* 旧的 URL 标记仍可读取,并依次退让到「同源同路径」(hash 路由跳转)和最后一个页面。
77100
*/
78-
function activePage(context, session) {
101+
async function activePage(context, session) {
79102
const pages = context.pages();
80103
if (!pages.length) fail("会话里没有打开的页面,先 open 或 goto");
81104
const wanted = readActive(session);
82105
if (!wanted) return pages[pages.length - 1];
83-
const exact = pages.filter((page) => page.url() === wanted).pop();
106+
if (wanted.targetId) {
107+
for (const page of pages) {
108+
if ((await pageTargetId(context, page)) === wanted.targetId) return page;
109+
}
110+
}
111+
const exact = pages.filter((page) => page.url() === wanted.url).pop();
84112
if (exact) return exact;
85-
const base = wanted.split("#")[0];
113+
const base = wanted.url.split("#")[0];
86114
const sameDocument = pages.filter((page) => page.url().split("#")[0] === base).pop();
87115
return sameDocument ?? pages[pages.length - 1];
88116
}
@@ -129,9 +157,10 @@ async function run() {
129157
// console 只读会话记录的日志文件,不必附着浏览器
130158
if (command === "console") {
131159
const file = path.join(dir, "console.log");
160+
const count = parseInt(args[0], 10) || 50;
161+
logAction(session, `console ${count}`);
132162
if (!fs.existsSync(file)) return console.log("(还没有 console 输出)");
133163
const lines = fs.readFileSync(file, "utf8").trimEnd().split("\n");
134-
const count = parseInt(args[0], 10) || 50;
135164
return console.log(lines.slice(-count).join("\n"));
136165
}
137166

@@ -146,43 +175,43 @@ async function run() {
146175
const url = `chrome-extension://${session.extensionId}/${suffix}`;
147176
const page = await context.newPage();
148177
await page.goto(url, { waitUntil: "domcontentloaded" });
149-
writeActive(session, page.url());
178+
await writeActive(context, session, page);
150179
logAction(session, `open ${target}${url}`);
151180
console.log(`✓ ${await page.title()}${url}`);
152181
break;
153182
}
154183
case "goto": {
155184
if (!args[0]) fail("goto 需要一个 URL");
156-
const page = activePage(context, session);
185+
const page = await activePage(context, session);
157186
await page.goto(args[0], { waitUntil: "domcontentloaded" });
158-
writeActive(session, page.url());
187+
await writeActive(context, session, page);
159188
logAction(session, `goto ${args[0]}`);
160189
console.log(`✓ ${await page.title()}${page.url()}`);
161190
break;
162191
}
163192
case "click": {
164-
const page = activePage(context, session);
193+
const page = await activePage(context, session);
165194
await page.locator(args[0]).first().click({ timeout: 10_000 });
166195
logAction(session, `click ${args[0]}`);
167196
console.log(`✓ clicked ${args[0]}`);
168197
break;
169198
}
170199
case "fill": {
171-
const page = activePage(context, session);
200+
const page = await activePage(context, session);
172201
await page.locator(args[0]).first().fill(args.slice(1).join(" "), { timeout: 10_000 });
173202
logAction(session, `fill ${args[0]}`);
174203
console.log(`✓ filled ${args[0]}`);
175204
break;
176205
}
177206
case "press": {
178-
const page = activePage(context, session);
207+
const page = await activePage(context, session);
179208
await page.keyboard.press(args[0]);
180209
logAction(session, `press ${args[0]}`);
181210
console.log(`✓ pressed ${args[0]}`);
182211
break;
183212
}
184213
case "wait": {
185-
const page = activePage(context, session);
214+
const page = await activePage(context, session);
186215
await page
187216
.locator(args[0])
188217
.first()
@@ -192,7 +221,7 @@ async function run() {
192221
break;
193222
}
194223
case "text": {
195-
const page = activePage(context, session);
224+
const page = await activePage(context, session);
196225
const text = await page
197226
.locator(args[0] ?? "body")
198227
.first()
@@ -202,7 +231,7 @@ async function run() {
202231
break;
203232
}
204233
case "shot": {
205-
const page = activePage(context, session);
234+
const page = await activePage(context, session);
206235
const shots = path.join(dir, "shots");
207236
fs.mkdirSync(shots, { recursive: true });
208237
const seq = String(fs.readdirSync(shots).filter((f) => f.endsWith(".png")).length + 1).padStart(2, "0");
@@ -213,7 +242,7 @@ async function run() {
213242
break;
214243
}
215244
case "eval": {
216-
const page = activePage(context, session);
245+
const page = await activePage(context, session);
217246
const result = await page.evaluate(wrapEvalSource(args.join(" ")));
218247
logAction(session, `eval ${args.join(" ").slice(0, 80)}`);
219248
print(result);
@@ -253,7 +282,7 @@ async function run() {
253282
break;
254283
}
255284
case "snapshot": {
256-
const page = activePage(context, session);
285+
const page = await activePage(context, session);
257286
const items = await page.evaluate((scopeSelector) => {
258287
const root = document.querySelector(scopeSelector) ?? document.body;
259288
const INTERACTIVE =
@@ -302,22 +331,24 @@ async function run() {
302331
break;
303332
}
304333
case "pages": {
305-
const current = activePage(context, session);
334+
const current = await activePage(context, session);
306335
context.pages().forEach((page, i) => console.log(`${page === current ? "→" : " "} ${i} ${page.url()}`));
336+
logAction(session, "pages");
307337
break;
308338
}
309339
case "use": {
310340
const index = parseInt(args[0], 10);
311341
const target = context.pages()[index];
312342
if (Number.isNaN(index) || !target) fail(`没有第 ${args[0]} 个页面,先看 pages`);
313-
writeActive(session, target.url());
343+
await writeActive(context, session, target);
344+
logAction(session, `use ${index}`);
314345
console.log(`✓ 当前页 ${index}${target.url()}`);
315346
break;
316347
}
317348
case "close": {
318-
const page = activePage(context, session);
349+
const page = await activePage(context, session);
319350
await page.close();
320-
writeActive(session, "");
351+
clearActive(session);
321352
logAction(session, "close");
322353
console.log("✓ closed");
323354
break;

0 commit comments

Comments
 (0)