@hover-dev/core 0.20.0 → 0.21.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.
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"humanSteps.d.ts","sourceRoot":"","sources":["../../src/specs/humanSteps.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAEtD,uEAAuE;AACvE,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"humanSteps.d.ts","sourceRoot":"","sources":["../../src/specs/humanSteps.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAEtD,uEAAuE;AACvE,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAoExE;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,MAAM,EAAE,CAmBvD"}
|
package/dist/specs/humanSteps.js
CHANGED
|
@@ -40,6 +40,18 @@ export function humanStep(tool, rawInput) {
|
|
|
40
40
|
const key = String(input.key ?? '');
|
|
41
41
|
return key ? `Press ${key}` : null;
|
|
42
42
|
}
|
|
43
|
+
// Grounded control tools (MCP-first) — the target is role+name/testId/text
|
|
44
|
+
// ON the input itself, not an `element` string.
|
|
45
|
+
case 'click_control':
|
|
46
|
+
return `Click ${describeGrounded(input)}`;
|
|
47
|
+
case 'fill_control':
|
|
48
|
+
return `Fill ${describeGrounded(input)} with ${quote(String(input.value ?? ''))}`;
|
|
49
|
+
case 'select_control':
|
|
50
|
+
return `Select ${quote(String(input.value ?? ''))} in ${describeGrounded(input)}`;
|
|
51
|
+
case 'check_control':
|
|
52
|
+
return `${input.checked === false ? 'Uncheck' : 'Check'} ${describeGrounded(input)}`;
|
|
53
|
+
case 'assert_visible':
|
|
54
|
+
return `Expect ${describeGrounded(input)} to be visible`;
|
|
43
55
|
// Diagnostic / read-only — same skip list as writeSpec.translateStep.
|
|
44
56
|
case 'browser_wait_for':
|
|
45
57
|
case 'browser_tabs':
|
|
@@ -91,6 +103,23 @@ function describe(raw) {
|
|
|
91
103
|
const s = String(raw ?? '').trim();
|
|
92
104
|
return s.length > 0 ? s : 'the target element';
|
|
93
105
|
}
|
|
106
|
+
/** Human phrase for a GROUNDED target ({ role, name, testId, text }) — the shape
|
|
107
|
+
* the *_control tools carry, vs the old browser_* tools' `element` string. */
|
|
108
|
+
function describeGrounded(input) {
|
|
109
|
+
const role = typeof input.role === 'string' ? input.role : '';
|
|
110
|
+
const name = typeof input.name === 'string' ? input.name : '';
|
|
111
|
+
const testId = typeof input.testId === 'string' ? input.testId : '';
|
|
112
|
+
const text = typeof input.text === 'string' ? input.text : '';
|
|
113
|
+
if (role && name)
|
|
114
|
+
return `${role} "${name}"`;
|
|
115
|
+
if (name)
|
|
116
|
+
return `"${name}"`;
|
|
117
|
+
if (testId)
|
|
118
|
+
return `testId "${testId}"`;
|
|
119
|
+
if (text)
|
|
120
|
+
return `"${text}"`;
|
|
121
|
+
return 'the target element';
|
|
122
|
+
}
|
|
94
123
|
/** Wrap in double-quotes for prose; escape internal quotes. A redacted
|
|
95
124
|
* credential (stored as a `process.env.X …` expression) shows as the masked
|
|
96
125
|
* `$X` instead — the prose, like the code, never reveals the secret. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"writeSpec.d.ts","sourceRoot":"","sources":["../../src/specs/writeSpec.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAatD,MAAM,MAAM,QAAQ,GAAG,SAAS,CAAC;AAEjC;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,yBAAyB,CAAC;AAEzD;;0DAE0D;AAC1D,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAE9D;AA2CD,MAAM,WAAW,aAAa;IAC5B,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAC;IACb,qDAAqD;IACrD,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,qBAAa,eAAgB,SAAQ,KAAK;aACZ,IAAI,EAAE,MAAM;aAAkB,IAAI,EAAE,MAAM;gBAA1C,IAAI,EAAE,MAAM,EAAkB,IAAI,EAAE,MAAM;CAIvE;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,UAAU,CAAC,EAAE,aAAa,EAAE,CAAC;IAC7B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,0EAA0E;IAC1E,UAAU,CAAC,EAAE,SAAS,EAAE,CAAC;IACzB;;;;yEAIqE;IACrE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;sFAIkF;IAClF,WAAW,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACtE;;;;qEAIiE;IACjE,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAiDD,MAAM,WAAW,eAAe;IAC9B,8EAA8E;IAC9E,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,+EAA+E;IAC/E,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACtD;;;;;2DAKuD;IACvD,gBAAgB,CAAC,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAA;KAAE,CAAC;CACnE;AAQD,wBAAsB,SAAS,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,CAchF;
|
|
1
|
+
{"version":3,"file":"writeSpec.d.ts","sourceRoot":"","sources":["../../src/specs/writeSpec.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAatD,MAAM,MAAM,QAAQ,GAAG,SAAS,CAAC;AAEjC;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,yBAAyB,CAAC;AAEzD;;0DAE0D;AAC1D,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAE9D;AA2CD,MAAM,WAAW,aAAa;IAC5B,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAC;IACb,qDAAqD;IACrD,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,qBAAa,eAAgB,SAAQ,KAAK;aACZ,IAAI,EAAE,MAAM;aAAkB,IAAI,EAAE,MAAM;gBAA1C,IAAI,EAAE,MAAM,EAAkB,IAAI,EAAE,MAAM;CAIvE;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,UAAU,CAAC,EAAE,aAAa,EAAE,CAAC;IAC7B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,0EAA0E;IAC1E,UAAU,CAAC,EAAE,SAAS,EAAE,CAAC;IACzB;;;;yEAIqE;IACrE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;sFAIkF;IAClF,WAAW,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACtE;;;;qEAIiE;IACjE,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAiDD,MAAM,WAAW,eAAe;IAC9B,8EAA8E;IAC9E,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,+EAA+E;IAC/E,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACtD;;;;;2DAKuD;IACvD,gBAAgB,CAAC,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAA;KAAE,CAAC;CACnE;AAQD,wBAAsB,SAAS,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,CAchF;AAonBD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,eAAe,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAM9E;AAED;;oCAEoC;AACpC,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAEpD;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,SAAS,GAAG,MAAM,CAqB9E;AA0DD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,SAAS,GAAG,MAAM,CAMxE;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,OAAO,SAAS,GAAG,MAAM,CAQ1F"}
|
package/dist/specs/writeSpec.js
CHANGED
|
@@ -201,13 +201,17 @@ async function writeOneSpec(opts, slug, displayName, rawSteps) {
|
|
|
201
201
|
const envVars = (opts.redactions ?? []).map(r => r.envVar);
|
|
202
202
|
const detectedPrefix = authPrefixLength(cleanActions, envVars);
|
|
203
203
|
const userConfigName = PLAYWRIGHT_CONFIG_NAMES.find(n => existsSync(join(opts.devRoot, n)));
|
|
204
|
+
// A config Hover scaffolded earlier this run (e.g. a non-login spec wrote a
|
|
205
|
+
// plain one first) is OURS to upgrade — treat it like "no user config", not a
|
|
206
|
+
// hands-off user file. ensurePlaywrightConfig adds the setup project to it.
|
|
207
|
+
const ownScaffold = !!userConfigName && readFileSync(join(opts.devRoot, userConfigName), 'utf-8').includes(SCAFFOLD_MARKER);
|
|
204
208
|
// Already opted in: auth.setup.ts exists from a prior approval (and the config
|
|
205
209
|
// already registers it), so engage AUTOMATICALLY — don't re-ask or re-edit.
|
|
206
210
|
const authSetupExists = existsSync(join(dir, 'auth.setup.ts'));
|
|
207
211
|
// Engage the fixture when a login is detected AND we can register the setup
|
|
208
|
-
// project: we scaffold the config
|
|
209
|
-
//
|
|
210
|
-
const engage = detectedPrefix > 0 && (!userConfigName || opts.authFixture === true || authSetupExists);
|
|
212
|
+
// project: we scaffold/own the config, the caller approved editing a user
|
|
213
|
+
// config (opts.authFixture, Stage 4), or the fixture was already set up earlier.
|
|
214
|
+
const engage = detectedPrefix > 0 && (!userConfigName || ownScaffold || opts.authFixture === true || authSetupExists);
|
|
211
215
|
const authPrefix = engage ? detectedPrefix : 0;
|
|
212
216
|
const authFile = engage ? AUTH_STATE_FILE : undefined;
|
|
213
217
|
let authFixtureOffer;
|
|
@@ -1005,28 +1009,24 @@ async function ensureResetStateHelper(devRoot, keys) {
|
|
|
1005
1009
|
].join('\n');
|
|
1006
1010
|
await writeFile(join(dir, 'resetState.ts'), source, 'utf-8');
|
|
1007
1011
|
}
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
return;
|
|
1014
|
-
// Auth-as-fixture: register a `setup` project (matches auth.setup.ts) that the
|
|
1015
|
-
// main project depends on, so login runs ONCE before the specs. Only emitted
|
|
1016
|
-
// when scaffolding our own config (we never touch a user's existing one).
|
|
1012
|
+
const SCAFFOLD_MARKER = 'Scaffolded by Hover';
|
|
1013
|
+
/** The scaffolded config source. When `authFile` is set, a `setup` project runs
|
|
1014
|
+
* auth.setup.ts ONCE and the main `chromium` project reuses the saved
|
|
1015
|
+
* storageState — so EVERY spec starts authenticated, not just the login flow. */
|
|
1016
|
+
function renderScaffoldConfig(origin, authFile) {
|
|
1017
1017
|
const projects = authFile
|
|
1018
1018
|
? [
|
|
1019
1019
|
` projects: [`,
|
|
1020
1020
|
` { name: 'setup', testMatch: /.*\\.setup\\.ts$/ },`,
|
|
1021
|
-
` { name: 'chromium', dependencies: ['setup'] },`,
|
|
1021
|
+
` { name: 'chromium', dependencies: ['setup'], use: { storageState: ${JSON.stringify(authFile)} } },`,
|
|
1022
1022
|
` ],`,
|
|
1023
1023
|
]
|
|
1024
1024
|
: [];
|
|
1025
|
-
|
|
1025
|
+
return [
|
|
1026
1026
|
`import { defineConfig } from '@playwright/test';`,
|
|
1027
1027
|
``,
|
|
1028
1028
|
`/**`,
|
|
1029
|
-
` *
|
|
1029
|
+
` * ${SCAFFOLD_MARKER} so crystallized specs (which use relative URLs like`,
|
|
1030
1030
|
` * page.goto("/")) resolve against a base. Override HOVER_BASE_URL in CI to`,
|
|
1031
1031
|
` * point the same specs at staging/prod.`,
|
|
1032
1032
|
` */`,
|
|
@@ -1039,7 +1039,50 @@ async function ensurePlaywrightConfig(devRoot, steps, startUrl, authFile) {
|
|
|
1039
1039
|
`});`,
|
|
1040
1040
|
``,
|
|
1041
1041
|
].join('\n');
|
|
1042
|
-
|
|
1042
|
+
}
|
|
1043
|
+
async function ensurePlaywrightConfig(devRoot, steps, startUrl, authFile) {
|
|
1044
|
+
const origin = firstNavigateOrigin(steps) ?? originOf(startUrl);
|
|
1045
|
+
if (!origin)
|
|
1046
|
+
return;
|
|
1047
|
+
const existingName = PLAYWRIGHT_CONFIG_NAMES.find(n => existsSync(join(devRoot, n)));
|
|
1048
|
+
if (existingName) {
|
|
1049
|
+
// A config already exists. Only ever UPGRADE our OWN scaffold — and only to
|
|
1050
|
+
// add the auth `setup` project when a login was just lifted (authFile) and
|
|
1051
|
+
// it isn't there yet. This makes auth order-independent: whichever spec in
|
|
1052
|
+
// the run triggers auth-fixture upgrades the config, even if a non-login
|
|
1053
|
+
// spec scaffolded a plain config first. A user's own config is never touched
|
|
1054
|
+
// (that's the Stage-4 approval flow via authFixtureOffer).
|
|
1055
|
+
if (!authFile)
|
|
1056
|
+
return;
|
|
1057
|
+
try {
|
|
1058
|
+
const cur = readFileSync(join(devRoot, existingName), 'utf-8');
|
|
1059
|
+
if (!cur.includes(SCAFFOLD_MARKER) || cur.includes(`name: 'setup'`))
|
|
1060
|
+
return;
|
|
1061
|
+
await writeFile(join(devRoot, existingName), renderScaffoldConfig(origin, authFile), 'utf-8');
|
|
1062
|
+
}
|
|
1063
|
+
catch { /* upgrade is best-effort */ }
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
await writeFile(join(devRoot, 'playwright.config.ts'), renderScaffoldConfig(origin, authFile), 'utf-8');
|
|
1067
|
+
await ensurePlaywrightDep(devRoot);
|
|
1068
|
+
}
|
|
1069
|
+
/** When Hover scaffolds the config it also ensures `@playwright/test` is a
|
|
1070
|
+
* devDependency — otherwise `npx playwright test` can't run the specs locally.
|
|
1071
|
+
* Best-effort + idempotent: skips if already present (either dep list) or if
|
|
1072
|
+
* there's no package.json. Reformats to 2-space JSON (the npm norm). */
|
|
1073
|
+
const PLAYWRIGHT_TEST_RANGE = '^1.50.0';
|
|
1074
|
+
async function ensurePlaywrightDep(devRoot) {
|
|
1075
|
+
const pkgPath = join(devRoot, 'package.json');
|
|
1076
|
+
if (!existsSync(pkgPath))
|
|
1077
|
+
return;
|
|
1078
|
+
try {
|
|
1079
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
1080
|
+
if (pkg.dependencies?.['@playwright/test'] || pkg.devDependencies?.['@playwright/test'])
|
|
1081
|
+
return;
|
|
1082
|
+
pkg.devDependencies = { ...(pkg.devDependencies ?? {}), '@playwright/test': PLAYWRIGHT_TEST_RANGE };
|
|
1083
|
+
await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
|
|
1084
|
+
}
|
|
1085
|
+
catch { /* best-effort — never break Save */ }
|
|
1043
1086
|
}
|
|
1044
1087
|
function stripBaseUrl(url) {
|
|
1045
1088
|
// http://localhost:5173/checkout → /checkout, http://localhost:5173/ → /
|