scenescout 3.15.0 → 3.17.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.
- package/CHANGELOG.md +87 -0
- package/README.md +70 -18
- package/dist/browsers.js +28 -0
- package/dist/check-run.js +191 -14
- package/dist/ci-run.js +268 -52
- package/dist/cli.js +107 -47
- package/dist/commands.js +3 -2
- package/dist/engine/baseline.js +377 -0
- package/dist/engine/brief.js +16 -7
- package/dist/engine/browser.js +1147 -286
- package/dist/engine/calibration.js +61 -30
- package/dist/engine/capture.js +164 -0
- package/dist/engine/check.js +244 -42
- package/dist/engine/ci-lanes.js +215 -0
- package/dist/engine/ci.js +136 -18
- package/dist/engine/claims.js +159 -3
- package/dist/engine/collector.js +561 -30
- package/dist/engine/crawl.js +49 -0
- package/dist/engine/design.js +281 -38
- package/dist/engine/export.js +877 -0
- package/dist/engine/fingerprint.js +92 -4
- package/dist/engine/flow.js +18 -6
- package/dist/engine/forms.js +181 -18
- package/dist/engine/journey.js +29 -1
- package/dist/engine/lane.js +13 -3
- package/dist/engine/launch.js +45 -6
- package/dist/engine/limits.js +7 -0
- package/dist/engine/live-page.js +49 -2
- package/dist/engine/live.js +4 -1
- package/dist/engine/memory.js +501 -47
- package/dist/engine/open.js +118 -0
- package/dist/engine/oracles.js +41 -1
- package/dist/engine/plain.js +268 -0
- package/dist/engine/png.js +127 -0
- package/dist/engine/policy.js +379 -9
- package/dist/engine/probes.js +3 -2
- package/dist/engine/profiles.js +45 -9
- package/dist/engine/project-folder.js +191 -0
- package/dist/engine/refresh.js +68 -3
- package/dist/engine/replay.js +63 -10
- package/dist/engine/report.js +241 -40
- package/dist/engine/request.js +317 -23
- package/dist/engine/sarif.js +120 -0
- package/dist/engine/settle.js +67 -0
- package/dist/engine/signed-in.js +256 -0
- package/dist/engine/status-pane-page.js +441 -0
- package/dist/engine/status-pane.js +128 -0
- package/dist/engine/tickets.js +671 -0
- package/dist/engine/unload.js +3 -2
- package/dist/export-run.js +633 -0
- package/dist/first-run.js +5 -0
- package/dist/installer.js +378 -8
- package/dist/intake.js +104 -0
- package/dist/login-run.js +250 -36
- package/dist/mcp-server.js +660 -65
- package/dist/playbook.js +5 -0
- package/dist/prompts.js +106 -0
- package/package.json +8 -5
- package/skills/scenescout/SKILL.md +49 -16
package/dist/login-run.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Runs `scenescout login <url> --role <name
|
|
3
|
-
* URL, lets the person sign in however the app asks
|
|
4
|
-
* manager), and
|
|
5
|
-
* storage state as that role's profile.
|
|
2
|
+
* Runs `scenescout login <url> --role <name>` and scout_login: opens a
|
|
3
|
+
* visible browser at the URL, lets the person sign in however the app asks
|
|
4
|
+
* (SSO, MFA, a password manager), and once they are signed in saves the
|
|
5
|
+
* browser's storage state as that role's profile.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* The window saves by itself once engine/signed-in.ts says the sign-in has
|
|
8
|
+
* finished; Enter in the terminal saves at once, and `--save enter` makes it
|
|
9
|
+
* the only way, as before. Closing the window or Ctrl+C saves nothing: by the
|
|
10
|
+
* time a window is closed its state can no longer be read, and a profile saved
|
|
11
|
+
* by accident half-way through a sign-in would be attached later as if it
|
|
12
|
+
* worked.
|
|
11
13
|
*
|
|
12
14
|
* The rules (role names, where the file goes, its mode, what may be printed)
|
|
13
15
|
* are in engine/profiles.ts; this file only drives the browser and the terminal.
|
|
@@ -21,6 +23,7 @@ import { redactRoute } from "./engine/check.js";
|
|
|
21
23
|
import { describeLifetime, readLifetime } from "./engine/expiry.js";
|
|
22
24
|
import { describeSaved, mergeSessionStorage, withSessionStorage, writeProfile } from "./engine/profiles.js";
|
|
23
25
|
import { afterTyping, chooseFields, describeStep, fieldIdentity, filledNames, nextStep, otpBoxes, quotable, secondsLeft, splitCode, totp as totpCode, TOTP_MIN_SECONDS_LEFT, urlMatches, } from "./engine/scripted-login.js";
|
|
26
|
+
import { DEFAULT_SAVE_MODE, judgeSignIn, LOOK_EVERY_MS, onApp, startWatch, } from "./engine/signed-in.js";
|
|
24
27
|
/**
|
|
25
28
|
* Read what the profile keeps from a signed-in context: cookies, localStorage
|
|
26
29
|
* and IndexedDB through Playwright's storage state, and sessionStorage, which
|
|
@@ -62,8 +65,13 @@ export async function captureState(context) {
|
|
|
62
65
|
}
|
|
63
66
|
/** How long after the prompt input still counts as typed before it. */
|
|
64
67
|
const EARLY_INPUT_MS = 300;
|
|
65
|
-
/**
|
|
66
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Wait for Enter on the terminal, the input ending or Ctrl+C. With `keepOnEnd`
|
|
70
|
+
* (the window saves by itself), input that ends is not an answer: a client
|
|
71
|
+
* that starts the command with no terminal closes it at once, and the window
|
|
72
|
+
* is still the way to finish.
|
|
73
|
+
*/
|
|
74
|
+
function waitForEnter(keepOnEnd) {
|
|
67
75
|
const rl = readline.createInterface({ input: process.stdin, terminal: false });
|
|
68
76
|
// Lines typed before the prompt (while the browser launched or the page
|
|
69
77
|
// loaded) arrive at once, buffered; they are not an answer to it.
|
|
@@ -74,10 +82,10 @@ function waitForEnd(browserClosed) {
|
|
|
74
82
|
if (Date.now() >= listening)
|
|
75
83
|
resolve("enter");
|
|
76
84
|
});
|
|
77
|
-
|
|
85
|
+
if (!keepOnEnd)
|
|
86
|
+
rl.once("close", () => resolve("input-ended"));
|
|
78
87
|
onSigint = () => resolve("interrupted");
|
|
79
88
|
process.once("SIGINT", onSigint);
|
|
80
|
-
void browserClosed.then(() => resolve("window-closed"));
|
|
81
89
|
});
|
|
82
90
|
return {
|
|
83
91
|
done,
|
|
@@ -90,28 +98,30 @@ function waitForEnd(browserClosed) {
|
|
|
90
98
|
};
|
|
91
99
|
}
|
|
92
100
|
const NOT_SAVED = {
|
|
93
|
-
"window-closed": "The browser was closed before
|
|
94
|
-
"input-ended": "The terminal's input ended before Enter was pressed, so nothing was saved. Run this command in an interactive terminal.",
|
|
101
|
+
"window-closed": "The browser was closed before the sign-in finished, so nothing was saved. Run the command again and sign in in the window it opens.",
|
|
102
|
+
"input-ended": "The terminal's input ended before Enter was pressed, so nothing was saved. Run this command in an interactive terminal, or leave out --save enter so the window saves by itself once you are signed in.",
|
|
95
103
|
interrupted: "Interrupted, so nothing was saved.",
|
|
104
|
+
"timed-out": "The sign-in did not finish in time, so the window was closed and nothing was saved.",
|
|
96
105
|
};
|
|
97
|
-
|
|
106
|
+
/** Open the window a person signs in in. Throws a sentence when the browser cannot start or the page cannot load. */
|
|
107
|
+
export async function openLoginWindow(options, opts = {}) {
|
|
98
108
|
const engine = options.browser ?? defaultEngine(process.env);
|
|
99
109
|
const types = { chromium, firefox, webkit };
|
|
100
110
|
// Checked before a window opens: a limit out of bounds is a sentence, not a browser left behind.
|
|
101
111
|
const navMs = explicitLimits({}, process.env).navMs ?? LOGIN_NAV_MS;
|
|
112
|
+
const headless = opts.headless ?? false;
|
|
102
113
|
let browser;
|
|
103
114
|
try {
|
|
104
|
-
browser = await types[engine].launch({ headless
|
|
115
|
+
browser = await types[engine].launch({ headless });
|
|
105
116
|
}
|
|
106
117
|
catch (err) {
|
|
107
|
-
throw new Error(explainLaunchFailure(err instanceof Error ? err.message : String(err), 0, { engine, headed:
|
|
118
|
+
throw new Error(explainLaunchFailure(err instanceof Error ? err.message : String(err), 0, { engine, headed: !headless }));
|
|
108
119
|
}
|
|
109
|
-
const
|
|
110
|
-
let end;
|
|
120
|
+
const closed = new Promise((resolve) => browser.once("disconnected", () => resolve()));
|
|
111
121
|
try {
|
|
112
122
|
const context = await browser.newContext();
|
|
113
123
|
const page = await context.newPage();
|
|
114
|
-
// Closing the
|
|
124
|
+
// Closing the sign-in tab is how most people close the window; treat it the
|
|
115
125
|
// same. The browser may already be closing, and either way it ends up closed.
|
|
116
126
|
page.once("close", () => void browser.close().catch(() => { }));
|
|
117
127
|
try {
|
|
@@ -121,23 +131,223 @@ export async function runLogin(options, log) {
|
|
|
121
131
|
const explained = loginTimeout(err, "nav", navMs);
|
|
122
132
|
throw new Error(`could not open ${options.url}: ${explained instanceof Error ? explained.message.split("\n")[0] : String(explained)}`);
|
|
123
133
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
134
|
+
return { browser, context, page, engine, closed };
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
// The error being thrown is the one to report; a browser that fails to close as well adds nothing to it.
|
|
138
|
+
await browser.close().catch(() => { });
|
|
139
|
+
throw err;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** Longest storage value read for a look; a longer one is compared by its start. */
|
|
143
|
+
const MAX_HELD_VALUE = 64 * 1024;
|
|
144
|
+
/** The cookies the browser would send to `url` and the top document's storage: what a look compares with the baseline. */
|
|
145
|
+
async function readHeld(context, page, url) {
|
|
146
|
+
const held = (await context.cookies([url])).map((c) => ({
|
|
147
|
+
kind: "cookie",
|
|
148
|
+
key: `cookie|${c.domain}|${c.path}|${c.name}`,
|
|
149
|
+
name: c.name,
|
|
150
|
+
value: c.value,
|
|
151
|
+
}));
|
|
152
|
+
const storage = await page.evaluate((max) => {
|
|
153
|
+
const out = [];
|
|
154
|
+
for (const [kind, store] of [
|
|
155
|
+
["local", localStorage],
|
|
156
|
+
["session", sessionStorage],
|
|
157
|
+
]) {
|
|
158
|
+
for (let i = 0; i < store.length; i++) {
|
|
159
|
+
const name = store.key(i);
|
|
160
|
+
if (name !== null)
|
|
161
|
+
out.push({ kind, name, value: (store.getItem(name) ?? "").slice(0, max) });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return { origin: location.origin, out };
|
|
165
|
+
}, MAX_HELD_VALUE);
|
|
166
|
+
for (const e of storage.out)
|
|
167
|
+
held.push({ kind: e.kind, key: `${e.kind}|${storage.origin}|${e.name}`, name: e.name, value: e.value });
|
|
168
|
+
return held;
|
|
169
|
+
}
|
|
170
|
+
/** A read the page's own navigation interrupted: the look is skipped and taken again. */
|
|
171
|
+
const betweenPages = (err) => /context was destroyed|navigat|detached|target closed|has been closed/i.test(err.message);
|
|
172
|
+
/** One look at the window, or null when the page was between documents and the look should be taken again. */
|
|
173
|
+
async function lookAt(win, watch) {
|
|
174
|
+
const url = win.page.url();
|
|
175
|
+
const popupAway = win.context.pages().some((p) => p !== win.page && !p.isClosed() && /^https?:/i.test(p.url()) && !onApp(watch, p.url()));
|
|
176
|
+
// Off the app nothing is read, except at an absolute success URL, where a sign-in field still says it is not done.
|
|
177
|
+
const atSuccess = watch.successUrl !== undefined && /^https?:\/\//i.test(watch.successUrl) && urlMatches(url, watch.successUrl);
|
|
178
|
+
if (!onApp(watch, url) && !atSuccess)
|
|
179
|
+
return { url, signInField: false, held: [], popupAway };
|
|
180
|
+
try {
|
|
181
|
+
const fields = await win.page.evaluate(collectFields, { selectors: {}, tag: false });
|
|
182
|
+
if (!Array.isArray(fields))
|
|
183
|
+
return null;
|
|
184
|
+
const chosen = chooseFields(fields);
|
|
185
|
+
const held = await readHeld(win.context, win.page, url);
|
|
186
|
+
// The page moved while it was read: what was read belongs to two pages.
|
|
187
|
+
if (win.page.url() !== url)
|
|
188
|
+
return null;
|
|
189
|
+
return { url, signInField: Boolean(chosen.password || chosen.otp), held, popupAway };
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
if (err instanceof Error && betweenPages(err))
|
|
193
|
+
return null;
|
|
194
|
+
throw err;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Watch the window until the person is signed in (engine/signed-in.ts says
|
|
199
|
+
* when), the window closes, or `signal` aborts. Resolves with how it ended;
|
|
200
|
+
* the caller saves and closes.
|
|
201
|
+
*/
|
|
202
|
+
export async function watchForSignIn(win, options, opts = {}) {
|
|
203
|
+
let closed = false;
|
|
204
|
+
void win.closed.then(() => {
|
|
205
|
+
closed = true;
|
|
206
|
+
});
|
|
207
|
+
// The baseline is what the window holds once the first page is there: whatever the app set before anyone signed in.
|
|
208
|
+
const firstUrl = win.page.url();
|
|
209
|
+
let watch = startWatch(options.url, [], options.successUrl, firstUrl);
|
|
210
|
+
const baseline = [];
|
|
211
|
+
try {
|
|
212
|
+
for (const c of await win.context.cookies())
|
|
213
|
+
baseline.push({ kind: "cookie", key: `cookie|${c.domain}|${c.path}|${c.name}`, name: c.name, value: c.value });
|
|
214
|
+
if (onApp(watch, firstUrl)) {
|
|
215
|
+
const first = await readHeld(win.context, win.page, firstUrl).catch((err) => {
|
|
216
|
+
if (betweenPages(err))
|
|
217
|
+
return [];
|
|
218
|
+
throw err;
|
|
219
|
+
});
|
|
220
|
+
baseline.push(...first.filter((h) => h.kind !== "cookie"));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
if (opts.signal?.aborted)
|
|
225
|
+
return { kind: "aborted" };
|
|
226
|
+
if (closed || win.page.isClosed())
|
|
227
|
+
return { kind: "closed" };
|
|
228
|
+
throw err;
|
|
229
|
+
}
|
|
230
|
+
watch = startWatch(options.url, baseline, options.successUrl, firstUrl);
|
|
231
|
+
opts.progress?.({ reason: "starting", url: firstUrl });
|
|
232
|
+
for (;;) {
|
|
233
|
+
if (closed || win.page.isClosed())
|
|
234
|
+
return { kind: "closed" };
|
|
235
|
+
if (opts.signal?.aborted)
|
|
236
|
+
return { kind: "aborted" };
|
|
237
|
+
let look;
|
|
238
|
+
try {
|
|
239
|
+
look = await lookAt(win, watch);
|
|
240
|
+
}
|
|
241
|
+
catch (err) {
|
|
242
|
+
// The window closing under a look is the window closing, not an error; nor is a look the caller already stopped
|
|
243
|
+
// (Enter was pressed and the window is being closed to save).
|
|
244
|
+
if (opts.signal?.aborted)
|
|
245
|
+
return { kind: "aborted" };
|
|
246
|
+
if (closed || win.page.isClosed())
|
|
247
|
+
return { kind: "closed" };
|
|
248
|
+
throw err;
|
|
249
|
+
}
|
|
250
|
+
if (look !== null) {
|
|
251
|
+
const judged = judgeSignIn(watch, look);
|
|
252
|
+
watch = judged.watch;
|
|
253
|
+
if (judged.verdict.kind === "signed-in")
|
|
254
|
+
return { kind: "signed-in", verdict: judged.verdict, url: look.url };
|
|
255
|
+
opts.progress?.({ reason: judged.verdict.reason, url: look.url });
|
|
256
|
+
}
|
|
257
|
+
await new Promise((resolve) => setTimeout(resolve, LOOK_EVERY_MS));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/** What the window says once the sign-in is seen: where, and by what. */
|
|
261
|
+
function sayDetected(verdict, url) {
|
|
262
|
+
const how = verdict.via === "success-url" ? "reached the success URL" : `the app holds a new session (${verdict.credential})`;
|
|
263
|
+
return `Signed in: back on ${redactRoute(url)} and ${how}. Saving and closing the window.`;
|
|
264
|
+
}
|
|
265
|
+
export async function runLogin(options, log) {
|
|
266
|
+
const mode = options.save ?? DEFAULT_SAVE_MODE;
|
|
267
|
+
const win = await openLoginWindow(options);
|
|
268
|
+
let enter;
|
|
269
|
+
const stop = new AbortController();
|
|
270
|
+
try {
|
|
271
|
+
log(`A ${win.engine} window is open at ${options.url}.`);
|
|
272
|
+
if (mode === "auto") {
|
|
273
|
+
log(`Sign in as "${options.role}" there — SSO, MFA, whatever the app asks. The window saves and closes by itself once you are signed in.`);
|
|
274
|
+
log(`(To save sooner, press Enter here. Closing the window or pressing Ctrl+C saves nothing.)`);
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
log(`Sign in as "${options.role}" there — SSO, MFA, whatever the app asks — then come back here and press Enter to save.`);
|
|
278
|
+
log(`(Closing the window or pressing Ctrl+C saves nothing.)`);
|
|
279
|
+
}
|
|
127
280
|
// Listening starts only now, after the prompt: an Enter pressed while the
|
|
128
281
|
// page was still loading must not save a signed-out profile.
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
if (
|
|
282
|
+
enter = waitForEnter(mode === "auto");
|
|
283
|
+
const racers = [enter.done, win.closed.then(() => "window-closed")];
|
|
284
|
+
if (mode === "auto") {
|
|
285
|
+
racers.push(watchForSignIn(win, options, { signal: stop.signal }).then((r) => {
|
|
286
|
+
if (r.kind === "signed-in") {
|
|
287
|
+
log(sayDetected(r.verdict, r.url));
|
|
288
|
+
return "signed-in";
|
|
289
|
+
}
|
|
290
|
+
return r.kind === "closed" ? "window-closed" : "interrupted";
|
|
291
|
+
}));
|
|
292
|
+
}
|
|
293
|
+
const how = await Promise.race(racers);
|
|
294
|
+
stop.abort();
|
|
295
|
+
if (how !== "enter" && how !== "signed-in")
|
|
132
296
|
throw new Error(NOT_SAVED[how]);
|
|
133
|
-
return await saveLogin(context, options);
|
|
297
|
+
return await saveLogin(win.context, options);
|
|
134
298
|
}
|
|
135
299
|
finally {
|
|
136
|
-
|
|
300
|
+
stop.abort();
|
|
301
|
+
enter?.dispose();
|
|
137
302
|
// Closing a browser the person already closed fails; nothing is left to clean up then.
|
|
138
|
-
await browser.close().catch(() => { });
|
|
303
|
+
await win.browser.close().catch(() => { });
|
|
139
304
|
}
|
|
140
305
|
}
|
|
306
|
+
/** How long a sign-in window opened from the conversation stays open, at most, before it closes saving nothing. */
|
|
307
|
+
export const LOGIN_WINDOW_MAX_MS = 15 * 60_000;
|
|
308
|
+
/**
|
|
309
|
+
* Open a window for a person to sign in, watch it, and save the profile once
|
|
310
|
+
* they are signed in: what scout_login does. Never waits for Enter; the
|
|
311
|
+
* window closing, `cancel`, or LOGIN_WINDOW_MAX_MS passing saves nothing.
|
|
312
|
+
*/
|
|
313
|
+
export async function startLoginWindow(options, opts = {}) {
|
|
314
|
+
const win = await openLoginWindow(options, opts);
|
|
315
|
+
const stop = new AbortController();
|
|
316
|
+
let last = { reason: "starting", url: options.url };
|
|
317
|
+
const timer = setTimeout(() => stop.abort(), opts.maxMs ?? LOGIN_WINDOW_MAX_MS);
|
|
318
|
+
const done = (async () => {
|
|
319
|
+
try {
|
|
320
|
+
const r = await watchForSignIn(win, options, { signal: stop.signal, progress: (p) => (last = p) });
|
|
321
|
+
if (r.kind === "closed")
|
|
322
|
+
return { ok: false, error: NOT_SAVED["window-closed"] };
|
|
323
|
+
if (r.kind === "aborted")
|
|
324
|
+
return { ok: false, error: NOT_SAVED["timed-out"] };
|
|
325
|
+
const saved = await saveLogin(win.context, options);
|
|
326
|
+
return { ok: true, saved, detected: sayDetected(r.verdict, r.url).replace(/ Saving and closing the window\.$/, "") };
|
|
327
|
+
}
|
|
328
|
+
catch (err) {
|
|
329
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
330
|
+
}
|
|
331
|
+
finally {
|
|
332
|
+
clearTimeout(timer);
|
|
333
|
+
// The outcome is decided; a window the person already closed fails to close again, and nothing is left then.
|
|
334
|
+
await win.browser.close().catch(() => { });
|
|
335
|
+
}
|
|
336
|
+
})();
|
|
337
|
+
return {
|
|
338
|
+
role: options.role,
|
|
339
|
+
url: options.url,
|
|
340
|
+
window: win,
|
|
341
|
+
done,
|
|
342
|
+
progress: () => last,
|
|
343
|
+
cancel: async () => {
|
|
344
|
+
stop.abort();
|
|
345
|
+
// Closed either way: a window already gone fails to close again. `done` settles once the watch sees it.
|
|
346
|
+
await win.browser.close().catch(() => { });
|
|
347
|
+
await done;
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
}
|
|
141
351
|
/**
|
|
142
352
|
* Save a signed-in context as the role's profile and read how long it lasts:
|
|
143
353
|
* what `scenescout login` does once Enter is pressed, apart so it can be run
|
|
@@ -179,10 +389,12 @@ const LOGIN_ACTION_MS = 10_000;
|
|
|
179
389
|
const MAX_SUBMITS = 5;
|
|
180
390
|
/**
|
|
181
391
|
* Read every visible form control on the page (the top document only), tag
|
|
182
|
-
* each with its index
|
|
183
|
-
*
|
|
392
|
+
* each with its index when asked (a scripted sign-in acts on them; watching a
|
|
393
|
+
* person sign in only reads them, so leaves the page as it is), and mark those
|
|
394
|
+
* matching a configured selector. Runs in the page, so it may use only what
|
|
395
|
+
* the page has.
|
|
184
396
|
*/
|
|
185
|
-
function collectFields(selectors) {
|
|
397
|
+
function collectFields({ selectors, tag: tagging }) {
|
|
186
398
|
const visible = (el) => {
|
|
187
399
|
const r = el.getBoundingClientRect();
|
|
188
400
|
const st = getComputedStyle(el);
|
|
@@ -220,8 +432,9 @@ function collectFields(selectors) {
|
|
|
220
432
|
return parts.join(" ").replace(/\s+/g, " ").trim();
|
|
221
433
|
};
|
|
222
434
|
// Tags from an earlier read name other elements now: one index, one element.
|
|
223
|
-
|
|
224
|
-
old.
|
|
435
|
+
if (tagging)
|
|
436
|
+
for (const old of Array.from(document.querySelectorAll("[data-scenescout-login]")))
|
|
437
|
+
old.removeAttribute("data-scenescout-login");
|
|
225
438
|
const out = [];
|
|
226
439
|
const all = Array.from(document.querySelectorAll("input, textarea, select, button"));
|
|
227
440
|
for (const el of all) {
|
|
@@ -229,7 +442,8 @@ function collectFields(selectors) {
|
|
|
229
442
|
if (type === "hidden" || !visible(el))
|
|
230
443
|
continue;
|
|
231
444
|
const index = out.length;
|
|
232
|
-
|
|
445
|
+
if (tagging)
|
|
446
|
+
el.setAttribute("data-scenescout-login", String(index));
|
|
233
447
|
const tag = el.tagName.toLowerCase();
|
|
234
448
|
const input = el;
|
|
235
449
|
out.push({
|
|
@@ -342,7 +556,7 @@ export async function runScriptedLogin(options, config, redactor, log, now = ()
|
|
|
342
556
|
return true;
|
|
343
557
|
};
|
|
344
558
|
const read = async () => {
|
|
345
|
-
const got = await page.evaluate(collectFields, config.selectors).catch((err) => {
|
|
559
|
+
const got = await page.evaluate(collectFields, { selectors: config.selectors, tag: true }).catch((err) => {
|
|
346
560
|
if (midNavigation(err))
|
|
347
561
|
return null;
|
|
348
562
|
throw fail(err.message);
|