@flowriki/agent 0.1.0-beta.23 → 0.1.0-beta.25
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/dist/{chunk-5J5D7IIT.js → chunk-WJ643WHC.js} +305 -33
- package/dist/cli.js +1 -1
- package/dist/engine.js +1 -1
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ import os from "os";
|
|
|
6
6
|
import path from "path";
|
|
7
7
|
import { createRequire } from "module";
|
|
8
8
|
import { spawnSync } from "child_process";
|
|
9
|
-
var AGENT_VERSION = "0.1.0-beta.
|
|
9
|
+
var AGENT_VERSION = "0.1.0-beta.25";
|
|
10
10
|
async function prepareBrowsers() {
|
|
11
11
|
if (browsersReady()) {
|
|
12
12
|
return { ok: true, already: true, message: "chromium is already installed" };
|
|
@@ -135,7 +135,7 @@ function forgetConnection(service, env = process.env) {
|
|
|
135
135
|
}
|
|
136
136
|
var fingerprint = (secret) => crypto2.createHash("sha256").update(secret).digest("hex").slice(0, 12);
|
|
137
137
|
|
|
138
|
-
// ../contracts/dist/chunk-
|
|
138
|
+
// ../contracts/dist/chunk-MQTBBCDK.js
|
|
139
139
|
import { z } from "zod";
|
|
140
140
|
var PROTOCOL_VERSION = 1;
|
|
141
141
|
var PAIRING_CODE_TTL_MS = 5 * 60 * 1e3;
|
|
@@ -290,7 +290,10 @@ var AGENT_FEATURES = /* @__PURE__ */ new Set([
|
|
|
290
290
|
"authentication.v1",
|
|
291
291
|
/** More than one participant, each with a browser session of its own —
|
|
292
292
|
* which also covers starting one over and sharing a captured output. */
|
|
293
|
-
"actors.v1"
|
|
293
|
+
"actors.v1",
|
|
294
|
+
/** Replaying other flows' steps before a recording starts capturing, so a
|
|
295
|
+
* flow can be recorded from where its prerequisites leave the browser. */
|
|
296
|
+
"prerequisites.v1"
|
|
294
297
|
]);
|
|
295
298
|
var AgentMessageSchema = z.discriminatedUnion("type", [
|
|
296
299
|
AckSchema,
|
|
@@ -458,7 +461,7 @@ import path7 from "path";
|
|
|
458
461
|
// ../engine-playwright/dist/index.js
|
|
459
462
|
import path3 from "path";
|
|
460
463
|
|
|
461
|
-
// ../test-model/dist/chunk-
|
|
464
|
+
// ../test-model/dist/chunk-TSFRTYLG.js
|
|
462
465
|
import { z as z3 } from "zod";
|
|
463
466
|
var LocatorOverrideSchema = z3.object({
|
|
464
467
|
strategy: z3.enum(["css", "xpath", "id", "testId", "text", "label", "placeholder", "altText", "title"]),
|
|
@@ -759,6 +762,12 @@ var ActorDefinitionSchema = z3.object({
|
|
|
759
762
|
name: z3.string().min(1),
|
|
760
763
|
startSignedIn: z3.object({ profileId: z3.string().min(1), accountId: z3.string().optional() }).strict().optional()
|
|
761
764
|
}).strict();
|
|
765
|
+
var FlowSignInSchema = z3.object({
|
|
766
|
+
profileId: z3.string().min(1),
|
|
767
|
+
accountId: z3.string().optional(),
|
|
768
|
+
from: z3.enum(["loginPage", "here"]).optional()
|
|
769
|
+
}).strict();
|
|
770
|
+
var FlowPrerequisitesSchema = z3.array(z3.object({ flowId: z3.string().min(1) }).strict());
|
|
762
771
|
var CURRENT_SCHEMA_VERSION = 5;
|
|
763
772
|
var FlowDocumentSchema = z3.object({
|
|
764
773
|
schemaVersion: z3.literal(CURRENT_SCHEMA_VERSION),
|
|
@@ -800,6 +809,24 @@ var FlowDocumentSchema = z3.object({
|
|
|
800
809
|
path: z3.string().optional(),
|
|
801
810
|
actors: z3.array(ActorDefinitionSchema).optional(),
|
|
802
811
|
defaultActorId: z3.string().optional(),
|
|
812
|
+
/** SIGN IN FIRST: the account this flow starts as, before anything else runs.
|
|
813
|
+
*
|
|
814
|
+
* A setting rather than step one, which is what it used to be saved as. As a
|
|
815
|
+
* step it could not be switched off without deleting it, and a flow reused
|
|
816
|
+
* as somebody else's prerequisite could not tell "my own sign-in" from a
|
|
817
|
+
* sign-in that is part of what it tests. `parseFlowDocument` lifts an old
|
|
818
|
+
* step one into this field, so both spellings read the same. A reference to
|
|
819
|
+
* a profile, never a copy of its steps. */
|
|
820
|
+
signIn: FlowSignInSchema.optional(),
|
|
821
|
+
/** Flows replayed, in this order, after signing in and before this flow's
|
|
822
|
+
* own steps — to reach the state it starts from.
|
|
823
|
+
*
|
|
824
|
+
* LINKS, NOT COPIES: re-record "Save basic info" once and every flow that
|
|
825
|
+
* starts after it follows. Each runs WITHOUT its own sign-in; this flow's
|
|
826
|
+
* is the only one. Explicit and flat — nothing here pulls in a
|
|
827
|
+
* prerequisite's prerequisites at run time, so what runs is exactly this
|
|
828
|
+
* list. The editor fills those in when a flow is added. */
|
|
829
|
+
prerequisites: FlowPrerequisitesSchema.optional(),
|
|
803
830
|
/** Bumped on every save. An edit carries the revision it was made against,
|
|
804
831
|
* so a save built on a stale copy is refused instead of overwriting work
|
|
805
832
|
* somebody else — or a debug session on the same machine — has since done.
|
|
@@ -895,8 +922,19 @@ function parseFlowDocument(input) {
|
|
|
895
922
|
version += 1;
|
|
896
923
|
raw.schemaVersion = version;
|
|
897
924
|
}
|
|
925
|
+
liftSignInFirst(raw);
|
|
898
926
|
return FlowDocumentSchema.parse(raw);
|
|
899
927
|
}
|
|
928
|
+
var SIGN_IN_FIRST_STEP_ID = "sign-in-first";
|
|
929
|
+
function liftSignInFirst(raw) {
|
|
930
|
+
const first = raw.steps?.[0];
|
|
931
|
+
const auth = first?.auth;
|
|
932
|
+
if (!first || first.type !== "signIn" || first.disabled === true || first.actorId !== void 0) return;
|
|
933
|
+
if (!auth || typeof auth.profileId !== "string" || !auth.profileId) return;
|
|
934
|
+
if (raw["signIn"] !== void 0 || Array.isArray(raw["actors"]) && raw["actors"].length > 0) return;
|
|
935
|
+
raw["signIn"] = auth;
|
|
936
|
+
raw.steps = raw.steps.slice(1);
|
|
937
|
+
}
|
|
900
938
|
|
|
901
939
|
// ../test-model/dist/index.js
|
|
902
940
|
import { z as z4 } from "zod";
|
|
@@ -1015,7 +1053,25 @@ var FlowOccurrenceSchema = z22.object({
|
|
|
1015
1053
|
* against the source can line the two up; nothing depends on it to run. */
|
|
1016
1054
|
stepIds: z22.record(z22.string(), z22.string()).default({}),
|
|
1017
1055
|
copiedAt: z22.string()
|
|
1018
|
-
})
|
|
1056
|
+
}),
|
|
1057
|
+
/** WHICH OF THIS ROW'S STEPS ARE WHAT RUNS BEFORE ITS FLOW — the flow's
|
|
1058
|
+
* sign-in and the prerequisites copied in with it — so the row can be shown
|
|
1059
|
+
* folded, the way the Flows screen shows the same flow.
|
|
1060
|
+
*
|
|
1061
|
+
* A description of steps the row already holds, and nothing more: they are
|
|
1062
|
+
* ordinary copies in `steps` and run in order whatever this says. Kept by
|
|
1063
|
+
* id, so editing or deleting one of them leaves the rest still grouped.
|
|
1064
|
+
* Absent on every row copied before this existed, which simply show flat.
|
|
1065
|
+
* Never sent to a machine — see `withoutFolding`. */
|
|
1066
|
+
before: z22.object({
|
|
1067
|
+
/** The flow's own sign-in, when the copy brought one. */
|
|
1068
|
+
signInStepId: z22.string().optional(),
|
|
1069
|
+
flows: z22.array(z22.object({
|
|
1070
|
+
flowId: z22.string(),
|
|
1071
|
+
name: z22.string(),
|
|
1072
|
+
stepIds: z22.array(z22.string())
|
|
1073
|
+
}).strict()).default([])
|
|
1074
|
+
}).strict().optional()
|
|
1019
1075
|
}).strict();
|
|
1020
1076
|
var CASE_SCHEMA_VERSION = 1;
|
|
1021
1077
|
var TestCaseSchema = z22.object({
|
|
@@ -1177,6 +1233,65 @@ function sessionLanes(occurrences, defaultActorId) {
|
|
|
1177
1233
|
}
|
|
1178
1234
|
return rows;
|
|
1179
1235
|
}
|
|
1236
|
+
function randomUUID() {
|
|
1237
|
+
const crypto5 = globalThis.crypto;
|
|
1238
|
+
if (crypto5?.randomUUID) return crypto5.randomUUID();
|
|
1239
|
+
return `${Date.now().toString(16)}-${Math.random().toString(16).slice(2, 14)}`;
|
|
1240
|
+
}
|
|
1241
|
+
var mintPage = (remap, pageId) => {
|
|
1242
|
+
if (pageId === "main") return "main";
|
|
1243
|
+
const existing = remap.pages.get(pageId);
|
|
1244
|
+
if (existing) return existing;
|
|
1245
|
+
const fresh = `pg_${randomUUID().replace(/-/g, "").slice(0, 12)}`;
|
|
1246
|
+
remap.pages.set(pageId, fresh);
|
|
1247
|
+
return fresh;
|
|
1248
|
+
};
|
|
1249
|
+
function retarget(target, remap) {
|
|
1250
|
+
if (!target.pageId) return target;
|
|
1251
|
+
return { ...target, pageId: mintPage(remap, target.pageId) };
|
|
1252
|
+
}
|
|
1253
|
+
function copyStep(step, remap) {
|
|
1254
|
+
const copied = retarget({ ...step, id: remap.steps.get(step.id) }, remap);
|
|
1255
|
+
if (copied.opensPage) copied.opensPage = mintPage(remap, copied.opensPage);
|
|
1256
|
+
if (copied.to) copied.to = retarget({ ...copied.to }, remap);
|
|
1257
|
+
if (copied.scroll?.container) {
|
|
1258
|
+
copied.scroll = { ...copied.scroll, container: retarget({ ...copied.scroll.container }, remap) };
|
|
1259
|
+
}
|
|
1260
|
+
copied.proposals = step.proposals.map((assertion) => ({ ...assertion }));
|
|
1261
|
+
return copied;
|
|
1262
|
+
}
|
|
1263
|
+
function copyFlowIntoCase(flow, context) {
|
|
1264
|
+
const remap = { steps: /* @__PURE__ */ new Map(), pages: /* @__PURE__ */ new Map() };
|
|
1265
|
+
for (const step of flow.steps) {
|
|
1266
|
+
remap.steps.set(step.id, `st_${randomUUID().replace(/-/g, "").slice(0, 12)}`);
|
|
1267
|
+
}
|
|
1268
|
+
const steps = flow.steps.map((step) => copyStep(step, remap));
|
|
1269
|
+
return {
|
|
1270
|
+
occurrenceId: `occ_${randomUUID().replace(/-/g, "").slice(0, 12)}`,
|
|
1271
|
+
name: flow.name,
|
|
1272
|
+
// WHO RUNS THIS COPY. The flow said which of its participants it belongs
|
|
1273
|
+
// to; dropping that here made every copy run as the main participant, so a
|
|
1274
|
+
// flow recorded as the approver silently became the buyer doing approvals.
|
|
1275
|
+
// The case has to be given the participant too — see `mergeParticipants`.
|
|
1276
|
+
...flow.defaultActorId && flow.defaultActorId !== "default" ? { actorId: flow.defaultActorId } : {},
|
|
1277
|
+
...flow.baseUrl ? { baseUrl: flow.baseUrl } : {},
|
|
1278
|
+
startPath: flow.startPath,
|
|
1279
|
+
navigateFirst: false,
|
|
1280
|
+
steps,
|
|
1281
|
+
...flow.environment ? { environment: flow.environment } : {},
|
|
1282
|
+
origin: {
|
|
1283
|
+
flowId: flow.id,
|
|
1284
|
+
schemaVersion: flow.schemaVersion,
|
|
1285
|
+
revision: flow.revision,
|
|
1286
|
+
...context.commit ? { commit: context.commit } : {},
|
|
1287
|
+
featureId: flow.featureId,
|
|
1288
|
+
featureName: context.featureName,
|
|
1289
|
+
flowName: flow.name,
|
|
1290
|
+
stepIds: Object.fromEntries(remap.steps),
|
|
1291
|
+
copiedAt: (context.at ?? /* @__PURE__ */ new Date()).toISOString()
|
|
1292
|
+
}
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1180
1295
|
function caseDebugPlan(testCase, baseUrl) {
|
|
1181
1296
|
const owners = {};
|
|
1182
1297
|
const steps = [];
|
|
@@ -1396,10 +1511,89 @@ function sameIdentity(a, b2) {
|
|
|
1396
1511
|
if (a.accountId && b2.accountId) return a.accountId === b2.accountId;
|
|
1397
1512
|
return true;
|
|
1398
1513
|
}
|
|
1514
|
+
var PrerequisiteProblem = class extends Error {
|
|
1515
|
+
constructor(message) {
|
|
1516
|
+
super(message);
|
|
1517
|
+
this.name = "PrerequisiteProblem";
|
|
1518
|
+
}
|
|
1519
|
+
};
|
|
1520
|
+
function signInFirstStep(flow, id = SIGN_IN_FIRST_STEP_ID) {
|
|
1521
|
+
if (!flow.signIn) return null;
|
|
1522
|
+
return {
|
|
1523
|
+
id,
|
|
1524
|
+
type: "signIn",
|
|
1525
|
+
auth: flow.signIn,
|
|
1526
|
+
name: "Sign in first",
|
|
1527
|
+
role: "",
|
|
1528
|
+
scope: "",
|
|
1529
|
+
value: "",
|
|
1530
|
+
secret: false,
|
|
1531
|
+
legacySelector: "",
|
|
1532
|
+
proposals: []
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
1535
|
+
function stepsWithoutSignIn(flow) {
|
|
1536
|
+
const first = flow.steps[0];
|
|
1537
|
+
return first && first.type === "signIn" && !first.disabled ? flow.steps.slice(1) : flow.steps;
|
|
1538
|
+
}
|
|
1539
|
+
var idsOf = (flow) => (flow.prerequisites ?? []).map((p) => p.flowId);
|
|
1540
|
+
function prerequisiteProblems(flow, lookup) {
|
|
1541
|
+
const problems = [];
|
|
1542
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1543
|
+
for (const id of idsOf(flow)) {
|
|
1544
|
+
if (id === flow.id) {
|
|
1545
|
+
problems.push(`"${flow.name}" cannot run before itself`);
|
|
1546
|
+
continue;
|
|
1547
|
+
}
|
|
1548
|
+
if (seen.has(id)) {
|
|
1549
|
+
problems.push(`"${lookup(id)?.name ?? id}" is listed twice before "${flow.name}"`);
|
|
1550
|
+
continue;
|
|
1551
|
+
}
|
|
1552
|
+
seen.add(id);
|
|
1553
|
+
const prerequisite = lookup(id);
|
|
1554
|
+
if (!prerequisite) {
|
|
1555
|
+
problems.push(`a flow "${flow.name}" runs after no longer exists \u2014 remove it from the list`);
|
|
1556
|
+
continue;
|
|
1557
|
+
}
|
|
1558
|
+
if (reaches(prerequisite, flow.id, lookup, /* @__PURE__ */ new Set())) {
|
|
1559
|
+
problems.push(`"${prerequisite.name}" already runs after "${flow.name}", so it cannot also run before it`);
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
return problems;
|
|
1563
|
+
}
|
|
1564
|
+
function reaches(from, target, lookup, visited) {
|
|
1565
|
+
if (visited.has(from.id)) return false;
|
|
1566
|
+
visited.add(from.id);
|
|
1567
|
+
return idsOf(from).some((id) => {
|
|
1568
|
+
if (id === target) return true;
|
|
1569
|
+
const next = lookup(id);
|
|
1570
|
+
return next ? reaches(next, target, lookup, visited) : false;
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
function prerequisitesOf(flow, lookup) {
|
|
1574
|
+
const problems = prerequisiteProblems(flow, lookup);
|
|
1575
|
+
if (problems.length > 0) throw new PrerequisiteProblem(problems.join("; "));
|
|
1576
|
+
return idsOf(flow).map((id) => lookup(id));
|
|
1577
|
+
}
|
|
1578
|
+
function stepsForHost(prerequisite) {
|
|
1579
|
+
return copyFlowIntoCase({ ...prerequisite, steps: stepsWithoutSignIn(prerequisite) }, { featureName: "" }).steps;
|
|
1580
|
+
}
|
|
1581
|
+
function preparationFor(flow, lookup) {
|
|
1582
|
+
return {
|
|
1583
|
+
signIn: signInFirstStep(flow),
|
|
1584
|
+
steps: prerequisitesOf(flow, lookup).flatMap(stepsForHost)
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
function expandForRun(flow, lookup) {
|
|
1588
|
+
const { signIn: _signIn, prerequisites: _prerequisites, ...rest } = flow;
|
|
1589
|
+
if (!flow.signIn && idsOf(flow).length === 0) return rest;
|
|
1590
|
+
const { signIn, steps } = preparationFor(flow, lookup);
|
|
1591
|
+
return { ...rest, steps: [...signIn ? [signIn] : [], ...steps, ...flow.steps] };
|
|
1592
|
+
}
|
|
1399
1593
|
|
|
1400
1594
|
// ../engine-playwright/dist/index.js
|
|
1401
1595
|
import { createHmac, randomBytes } from "crypto";
|
|
1402
|
-
import { errors } from "playwright";
|
|
1596
|
+
import { errors as errors2 } from "playwright";
|
|
1403
1597
|
|
|
1404
1598
|
// ../core/dist/index.js
|
|
1405
1599
|
function proposeAssertions(before, after) {
|
|
@@ -1720,7 +1914,7 @@ var STEP_PRESENTATION = {
|
|
|
1720
1914
|
// ../engine-playwright/dist/index.js
|
|
1721
1915
|
import fs4 from "fs";
|
|
1722
1916
|
import path22 from "path";
|
|
1723
|
-
import { chromium, firefox, webkit } from "playwright";
|
|
1917
|
+
import { chromium, errors, firefox, webkit } from "playwright";
|
|
1724
1918
|
|
|
1725
1919
|
// ../recorder/dist/index.js
|
|
1726
1920
|
function openShadowRoots() {
|
|
@@ -3747,38 +3941,63 @@ function resolveCapture(options = {}) {
|
|
|
3747
3941
|
};
|
|
3748
3942
|
}
|
|
3749
3943
|
var forcedHeadless = () => process.env["FLOWRIKI_FORCE_HEADLESS"] === "1";
|
|
3944
|
+
var LAUNCH_ATTEMPT_MS = () => Number(process.env["FLOWRIKI_LAUNCH_TIMEOUT_MS"] ?? 45e3);
|
|
3945
|
+
var isStartupFailure = (error) => error instanceof errors.TimeoutError || /Target page, context or browser has been closed|Browser has been closed|browser has disconnected/i.test(String(error?.message ?? error));
|
|
3946
|
+
async function launchWithRetry(launch, setUp, { attempts = 2, pauseMs = 1e3 } = {}) {
|
|
3947
|
+
let last;
|
|
3948
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
3949
|
+
let browser = null;
|
|
3950
|
+
try {
|
|
3951
|
+
browser = await launch(LAUNCH_ATTEMPT_MS());
|
|
3952
|
+
return { browser, value: await setUp(browser) };
|
|
3953
|
+
} catch (error) {
|
|
3954
|
+
last = error;
|
|
3955
|
+
await browser?.close().catch(() => {
|
|
3956
|
+
});
|
|
3957
|
+
if (!isStartupFailure(error)) throw error;
|
|
3958
|
+
if (attempt < attempts) await new Promise((r) => setTimeout(r, pauseMs));
|
|
3959
|
+
}
|
|
3960
|
+
}
|
|
3961
|
+
const seconds = Math.round(LAUNCH_ATTEMPT_MS() / 1e3);
|
|
3962
|
+
const why = String(last?.message ?? last).split("\n")[0];
|
|
3963
|
+
throw new Error(
|
|
3964
|
+
`the browser did not start after ${attempts} attempts of ${seconds}s each (${why}) \u2014 on a Mac this is usually a visible browser that could not open a window; run it again, or run it headless`
|
|
3965
|
+
);
|
|
3966
|
+
}
|
|
3750
3967
|
async function launchSession(environment = {}, headlessRequested = true, slowMo = 0, capture = {}, existingBrowser, storageState) {
|
|
3751
3968
|
const headless = headlessRequested || forcedHeadless();
|
|
3752
3969
|
const config = { ...DEFAULT_ENVIRONMENT, ...ExecutionEnvironmentSchema.parse(environment) };
|
|
3753
3970
|
const fullWindow = config.fullWindow === true && !headless;
|
|
3754
3971
|
const engine = { chromium, firefox, webkit }[config.browser];
|
|
3755
|
-
const
|
|
3972
|
+
const launch = (timeout) => engine.launch({
|
|
3756
3973
|
headless,
|
|
3757
3974
|
slowMo,
|
|
3975
|
+
timeout,
|
|
3758
3976
|
// Chromium's flag. Firefox and WebKit have no equivalent, and `viewport:
|
|
3759
3977
|
// null` still gives them a page that fills their default window — bigger
|
|
3760
3978
|
// than the fixed box, if not maximised.
|
|
3761
3979
|
...fullWindow && config.browser === "chromium" ? { args: ["--start-maximized"] } : {}
|
|
3762
3980
|
});
|
|
3981
|
+
const { viewport, locale, timezoneId, colorScheme } = config;
|
|
3982
|
+
const artifacts = resolveCapture(capture);
|
|
3983
|
+
const makeContext = (browser2) => browser2.newContext({
|
|
3984
|
+
viewport: fullWindow ? null : viewport,
|
|
3985
|
+
locale,
|
|
3986
|
+
timezoneId,
|
|
3987
|
+
colorScheme,
|
|
3988
|
+
...storageState ? { storageState } : {},
|
|
3989
|
+
// Playwright can only be told to record at context creation, so a video
|
|
3990
|
+
// wanted "on failure" is recorded for every run and thrown away when the
|
|
3991
|
+
// run passes. There is no way to start one after the fact.
|
|
3992
|
+
//
|
|
3993
|
+
// The video still needs a fixed size even when the page has none —
|
|
3994
|
+
// `size` is what the file is encoded at, and Playwright scales the frame
|
|
3995
|
+
// to fit it. The configured viewport is the closest thing to an intended
|
|
3996
|
+
// aspect ratio we have.
|
|
3997
|
+
...artifacts.video !== "off" && capture.dir ? { recordVideo: { dir: capture.dir, size: viewport } } : {}
|
|
3998
|
+
});
|
|
3999
|
+
const { browser, value: context } = existingBrowser ? { browser: existingBrowser, value: await makeContext(existingBrowser) } : await launchWithRetry(launch, makeContext);
|
|
3763
4000
|
try {
|
|
3764
|
-
const { viewport, locale, timezoneId, colorScheme } = config;
|
|
3765
|
-
const artifacts = resolveCapture(capture);
|
|
3766
|
-
const context = await browser.newContext({
|
|
3767
|
-
viewport: fullWindow ? null : viewport,
|
|
3768
|
-
locale,
|
|
3769
|
-
timezoneId,
|
|
3770
|
-
colorScheme,
|
|
3771
|
-
...storageState ? { storageState } : {},
|
|
3772
|
-
// Playwright can only be told to record at context creation, so a video
|
|
3773
|
-
// wanted "on failure" is recorded for every run and thrown away when the
|
|
3774
|
-
// run passes. There is no way to start one after the fact.
|
|
3775
|
-
//
|
|
3776
|
-
// The video still needs a fixed size even when the page has none —
|
|
3777
|
-
// `size` is what the file is encoded at, and Playwright scales the frame
|
|
3778
|
-
// to fit it. The configured viewport is the closest thing to an intended
|
|
3779
|
-
// aspect ratio we have.
|
|
3780
|
-
...artifacts.video !== "off" && capture.dir ? { recordVideo: { dir: capture.dir, size: viewport } } : {}
|
|
3781
|
-
});
|
|
3782
4001
|
if (artifacts.trace !== "off" && capture.dir) {
|
|
3783
4002
|
await context.tracing.start({ screenshots: true, snapshots: true, sources: false });
|
|
3784
4003
|
}
|
|
@@ -4388,6 +4607,26 @@ var checkStep = (id, proposals) => ({
|
|
|
4388
4607
|
legacySelector: "",
|
|
4389
4608
|
proposals
|
|
4390
4609
|
});
|
|
4610
|
+
async function replayInto(target, steps, opts) {
|
|
4611
|
+
const session = {
|
|
4612
|
+
ownsBrowser: false,
|
|
4613
|
+
browser: target.browser,
|
|
4614
|
+
context: target.context,
|
|
4615
|
+
page: target.page,
|
|
4616
|
+
pages: new ExecutionPages(target.context, target.page),
|
|
4617
|
+
config: target.config,
|
|
4618
|
+
artifacts: resolveCapture({ policy: { screenshot: "off", video: "off", trace: "off", console: false } }),
|
|
4619
|
+
consoleLines: [],
|
|
4620
|
+
runArtifacts: []
|
|
4621
|
+
};
|
|
4622
|
+
const outcome = await runStepsIn(session, steps, {
|
|
4623
|
+
startUrl: null,
|
|
4624
|
+
...opts.resolveSecret ? { resolveSecret: opts.resolveSecret } : {},
|
|
4625
|
+
...opts.resolveAuth ? { resolveAuth: opts.resolveAuth } : {},
|
|
4626
|
+
...opts.baseUrl ? { baseUrl: opts.baseUrl } : {}
|
|
4627
|
+
});
|
|
4628
|
+
return outcome.status === "passed" ? { ok: true } : { ok: false, error: outcome.error ?? "a step before recording did not complete" };
|
|
4629
|
+
}
|
|
4391
4630
|
function opensItsOwnEntry(steps, opts) {
|
|
4392
4631
|
const first = steps.find((step) => !step.disabled);
|
|
4393
4632
|
if (!first || first.type !== "signIn" || !first.auth) return false;
|
|
@@ -4643,7 +4882,7 @@ async function runStepsIn(session, steps, opts) {
|
|
|
4643
4882
|
}
|
|
4644
4883
|
} catch (err) {
|
|
4645
4884
|
result.status = "failed";
|
|
4646
|
-
result.failureKind = err.kind ?? (/strict mode violation/.test(String(err)) ? "ambiguous" : err instanceof
|
|
4885
|
+
result.failureKind = err.kind ?? (/strict mode violation/.test(String(err)) ? "ambiguous" : err instanceof errors2.TimeoutError ? "timeout" : "unknown");
|
|
4647
4886
|
result.detail = firstLine2(err);
|
|
4648
4887
|
}
|
|
4649
4888
|
const wantsShot = artifacts.screenshot === "always" || artifacts.screenshot === "on-failure" && result.status === "failed";
|
|
@@ -5441,6 +5680,25 @@ async function startRecordingSession(opts) {
|
|
|
5441
5680
|
throw new Error(`${firstLine2(error)} \u2014 the browser was on ${landed || "/"}${title ? ` ("${title}")` : ""}`);
|
|
5442
5681
|
}
|
|
5443
5682
|
}
|
|
5683
|
+
const prepared = Boolean(opts.preparation?.steps.length);
|
|
5684
|
+
if (opts.preparation && prepared) {
|
|
5685
|
+
try {
|
|
5686
|
+
if (!opts.authentication) await page.goto(url, { waitUntil: "domcontentloaded" });
|
|
5687
|
+
const replayed = await replayInto({ browser, context, page, config }, opts.preparation.steps, {
|
|
5688
|
+
resolveSecret: opts.preparation.resolveSecret,
|
|
5689
|
+
...opts.preparation.resolveAuth ? { resolveAuth: opts.preparation.resolveAuth } : {},
|
|
5690
|
+
baseUrl: url
|
|
5691
|
+
});
|
|
5692
|
+
if (!replayed.ok) throw new Error(replayed.error);
|
|
5693
|
+
} catch (error) {
|
|
5694
|
+
const landed = page.url().replace(/^https?:\/\/[^/]+/, "");
|
|
5695
|
+
const title = await page.title().catch(() => "");
|
|
5696
|
+
stopped = true;
|
|
5697
|
+
await browser.close().catch(() => {
|
|
5698
|
+
});
|
|
5699
|
+
throw new Error(`the flows this one starts after did not finish: ${firstLine2(error)} \u2014 the browser was on ${landed || "/"}${title ? ` ("${title}")` : ""}`);
|
|
5700
|
+
}
|
|
5701
|
+
}
|
|
5444
5702
|
await context.addInitScript({ content: INIT_SCRIPT });
|
|
5445
5703
|
const ensureInstalled = async (p) => {
|
|
5446
5704
|
try {
|
|
@@ -5493,7 +5751,7 @@ async function startRecordingSession(opts) {
|
|
|
5493
5751
|
});
|
|
5494
5752
|
});
|
|
5495
5753
|
try {
|
|
5496
|
-
if (!opts.authentication) await page.goto(url, { waitUntil: "domcontentloaded", timeout: 45e3 });
|
|
5754
|
+
if (!opts.authentication && !prepared) await page.goto(url, { waitUntil: "domcontentloaded", timeout: 45e3 });
|
|
5497
5755
|
else await ensureInstalled(page);
|
|
5498
5756
|
} catch (err) {
|
|
5499
5757
|
stopped = true;
|
|
@@ -7762,8 +8020,15 @@ var LOCAL_CHROMIUM = { id: "chromium", provider: "local", engine: "chromium" };
|
|
|
7762
8020
|
async function executeFlow(opts) {
|
|
7763
8021
|
const { store, storage = null, flowId, trigger, watch = false, onEvent } = opts;
|
|
7764
8022
|
const headless = watch ? false : opts.headless ?? store.readConfig().headless;
|
|
7765
|
-
const
|
|
7766
|
-
if (!
|
|
8023
|
+
const found = store.getFlow(flowId);
|
|
8024
|
+
if (!found) throw new FlowNotRunnable(`flow not found: ${flowId}`, "not_found");
|
|
8025
|
+
let flow = found;
|
|
8026
|
+
try {
|
|
8027
|
+
flow = expandForRun(found, (id) => store.getFlow(id));
|
|
8028
|
+
} catch (problem) {
|
|
8029
|
+
if (problem instanceof PrerequisiteProblem) throw new FlowNotRunnable(problem.message, "empty");
|
|
8030
|
+
throw problem;
|
|
8031
|
+
}
|
|
7767
8032
|
if (opts.expectedRevision !== void 0 && flow.revision !== opts.expectedRevision) {
|
|
7768
8033
|
throw new FlowConflict("the flow changed before execution \u2014 reload it before running", flow.revision);
|
|
7769
8034
|
}
|
|
@@ -8313,6 +8578,11 @@ var RecordingCoordinator = class {
|
|
|
8313
8578
|
resolveAuth: authenticationResolver(opts.authentications),
|
|
8314
8579
|
resolveSecret: (ref) => opts.secrets?.[ref]
|
|
8315
8580
|
} } : {},
|
|
8581
|
+
...opts.preparation?.length ? { preparation: {
|
|
8582
|
+
steps: opts.preparation,
|
|
8583
|
+
resolveAuth: authenticationResolver(opts.authentications),
|
|
8584
|
+
resolveSecret: (ref) => opts.secrets?.[ref]
|
|
8585
|
+
} } : {},
|
|
8316
8586
|
// The project's saved defaults, unless this recording asks for
|
|
8317
8587
|
// something else. Without the fallback the settings screen would write
|
|
8318
8588
|
// a file nothing ever read.
|
|
@@ -8757,7 +9027,7 @@ function resolveCheckouts(dirs) {
|
|
|
8757
9027
|
return { projects, skipped };
|
|
8758
9028
|
}
|
|
8759
9029
|
|
|
8760
|
-
// ../agent-core/dist/chunk-
|
|
9030
|
+
// ../agent-core/dist/chunk-4MOK25KG.js
|
|
8761
9031
|
import { WebSocket, WebSocketServer } from "ws";
|
|
8762
9032
|
var HEARTBEAT_MS = 3e4;
|
|
8763
9033
|
function adapt(socket) {
|
|
@@ -9827,8 +10097,10 @@ function recordingHandlers({ recorder, onActivity }) {
|
|
|
9827
10097
|
// returned on stop for the service to commit to the repository. A
|
|
9828
10098
|
// machine that has the project checked out still records into it.
|
|
9829
10099
|
root,
|
|
9830
|
-
...command.args["authentication"] ? {
|
|
9831
|
-
|
|
10100
|
+
...command.args["authentication"] ? { authentication: AuthReferenceSchema.parse(command.args["authentication"]) } : {},
|
|
10101
|
+
// The flows this recording starts after, replayed before capturing.
|
|
10102
|
+
...Array.isArray(command.args["preparation"]) && command.args["preparation"].length > 0 ? { preparation: RecordedStepSchema.array().parse(command.args["preparation"]) } : {},
|
|
10103
|
+
...command.args["authentication"] || command.args["preparation"] ? {
|
|
9832
10104
|
authentications: command.args["authentications"],
|
|
9833
10105
|
secrets: command.args["secrets"] ?? {}
|
|
9834
10106
|
} : {},
|
package/dist/cli.js
CHANGED
package/dist/engine.js
CHANGED
package/package.json
CHANGED