@eidra-umain/greenlight 0.1.0 → 0.3.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/README.md +66 -3
- package/dist/browser/browser.d.ts +14 -4
- package/dist/browser/browser.d.ts.map +1 -1
- package/dist/browser/browser.js +89 -8
- package/dist/browser/browser.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +161 -118
- package/dist/cli/run.js.map +1 -1
- package/dist/map/adapters/maplibre.d.ts +23 -0
- package/dist/map/adapters/maplibre.d.ts.map +1 -0
- package/dist/map/adapters/maplibre.js +367 -0
- package/dist/map/adapters/maplibre.js.map +1 -0
- package/dist/map/index.d.ts +36 -0
- package/dist/map/index.d.ts.map +1 -0
- package/dist/map/index.js +82 -0
- package/dist/map/index.js.map +1 -0
- package/dist/map/types.d.ts +96 -0
- package/dist/map/types.d.ts.map +1 -0
- package/dist/map/types.js +14 -0
- package/dist/map/types.js.map +1 -0
- package/dist/pilot/a11y-parser.d.ts.map +1 -1
- package/dist/pilot/a11y-parser.js +6 -3
- package/dist/pilot/a11y-parser.js.map +1 -1
- package/dist/pilot/assertions.d.ts +6 -6
- package/dist/pilot/assertions.d.ts.map +1 -1
- package/dist/pilot/assertions.js +267 -40
- package/dist/pilot/assertions.js.map +1 -1
- package/dist/pilot/executor.d.ts +6 -2
- package/dist/pilot/executor.d.ts.map +1 -1
- package/dist/pilot/executor.js +71 -36
- package/dist/pilot/executor.js.map +1 -1
- package/dist/pilot/llm.d.ts.map +1 -1
- package/dist/pilot/llm.js +1 -0
- package/dist/pilot/llm.js.map +1 -1
- package/dist/pilot/locator.d.ts +14 -2
- package/dist/pilot/locator.d.ts.map +1 -1
- package/dist/pilot/locator.js +95 -35
- package/dist/pilot/locator.js.map +1 -1
- package/dist/pilot/message-builder.d.ts.map +1 -1
- package/dist/pilot/message-builder.js +19 -0
- package/dist/pilot/message-builder.js.map +1 -1
- package/dist/pilot/pilot.d.ts +3 -1
- package/dist/pilot/pilot.d.ts.map +1 -1
- package/dist/pilot/pilot.js +106 -14
- package/dist/pilot/pilot.js.map +1 -1
- package/dist/pilot/prompts.d.ts +37 -3
- package/dist/pilot/prompts.d.ts.map +1 -1
- package/dist/pilot/prompts.js +167 -93
- package/dist/pilot/prompts.js.map +1 -1
- package/dist/pilot/providers/anthropic.d.ts +1 -1
- package/dist/pilot/providers/anthropic.d.ts.map +1 -1
- package/dist/pilot/providers/anthropic.js +2 -1
- package/dist/pilot/providers/anthropic.js.map +1 -1
- package/dist/pilot/providers/gemini.d.ts +1 -1
- package/dist/pilot/providers/gemini.d.ts.map +1 -1
- package/dist/pilot/providers/gemini.js +2 -1
- package/dist/pilot/providers/gemini.js.map +1 -1
- package/dist/pilot/providers/index.d.ts +1 -0
- package/dist/pilot/providers/index.d.ts.map +1 -1
- package/dist/pilot/providers/index.js +1 -0
- package/dist/pilot/providers/index.js.map +1 -1
- package/dist/pilot/providers/openai-compatible.d.ts +1 -1
- package/dist/pilot/providers/openai-compatible.d.ts.map +1 -1
- package/dist/pilot/providers/openai-compatible.js +2 -1
- package/dist/pilot/providers/openai-compatible.js.map +1 -1
- package/dist/pilot/providers/types.d.ts +9 -0
- package/dist/pilot/providers/types.d.ts.map +1 -1
- package/dist/pilot/providers/types.js +13 -1
- package/dist/pilot/providers/types.js.map +1 -1
- package/dist/pilot/response-parser.d.ts +13 -1
- package/dist/pilot/response-parser.d.ts.map +1 -1
- package/dist/pilot/response-parser.js +85 -2
- package/dist/pilot/response-parser.js.map +1 -1
- package/dist/pilot/state.d.ts +2 -0
- package/dist/pilot/state.d.ts.map +1 -1
- package/dist/pilot/state.js +12 -1
- package/dist/pilot/state.js.map +1 -1
- package/dist/planner/plan-runner.d.ts +2 -1
- package/dist/planner/plan-runner.d.ts.map +1 -1
- package/dist/planner/plan-runner.js +118 -18
- package/dist/planner/plan-runner.js.map +1 -1
- package/dist/planner/plan-types.d.ts +1 -0
- package/dist/planner/plan-types.d.ts.map +1 -1
- package/dist/reporter/types.d.ts +37 -2
- package/dist/reporter/types.d.ts.map +1 -1
- package/package.json +4 -3
- package/dist/parser/steps.d.ts +0 -13
- package/dist/parser/steps.d.ts.map +0 -1
- package/dist/parser/steps.js +0 -44
- package/dist/parser/steps.js.map +0 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ No selectors. No XPaths. No test IDs, drivers or glue code. Just describe what a
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
-
**[How it works](#how-it-works)** | **[Quick start](#quick-start)** | **[Project configuration](#project-configuration)** | **[CLI](#cli)** | **[Test syntax](#test-syntax)** | **[Cached plans](#cached-plans)** | **[LLM setup](#llm-setup)** | **[Architecture](#architecture)** | **[CI/CD](#cicd)**
|
|
13
|
+
**[How it works](#how-it-works)** | **[Quick start](#quick-start)** | **[Project configuration](#project-configuration)** | **[CLI](#cli)** | **[Test syntax](#test-syntax)** | **[Cached plans](#cached-plans)** | **[LLM setup](#llm-setup)** | **[Architecture](#architecture)** | **[Avoiding side effects](#avoiding-side-effects-in-your-app)** | **[CI/CD](#cicd)**
|
|
14
14
|
|
|
15
15
|
---
|
|
16
16
|
|
|
@@ -34,7 +34,7 @@ tests:
|
|
|
34
34
|
- check that you see "Thanks for your inquiry"
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
GreenLight understands form wizards, custom dropdowns, autocomplete fields,
|
|
37
|
+
GreenLight understands form wizards, custom dropdowns, autocomplete fields, checkbox consent flows, and interactive maps. It fills in forms with realistic test data, handles before/after value comparisons, and works with any UI framework.
|
|
38
38
|
|
|
39
39
|
The first run uses an LLM to discover the right actions (the **discovery run**). After that, GreenLight caches a concrete action plan and replays it without LLM calls — making subsequent runs fast and deterministic.
|
|
40
40
|
|
|
@@ -185,6 +185,7 @@ Tests are plain English. The Pilot interprets intent, so phrasing is flexible. C
|
|
|
185
185
|
| Remember | `remember the number of search results` |
|
|
186
186
|
| Compare | `check that the number of results is less than before` |
|
|
187
187
|
| Assert | `check that page contains "Order Confirmed"` |
|
|
188
|
+
| Map assert | `check that the map shows "Stockholm"` or `check that zoom level is at least 10` |
|
|
188
189
|
| Multi-step | `Select Red - Green - Blue in the color picker` (auto-split into 3 clicks) |
|
|
189
190
|
|
|
190
191
|
### Form filling
|
|
@@ -202,6 +203,31 @@ steps:
|
|
|
202
203
|
- check that the total has decreased
|
|
203
204
|
```
|
|
204
205
|
|
|
206
|
+
### Map testing
|
|
207
|
+
|
|
208
|
+
GreenLight has built-in support for testing pages with interactive WebGL maps. When a test step mentions maps, markers, layers, or zoom levels, GreenLight automatically detects the map library, attaches to its instance, and queries the map's rendered features and viewport state directly — bypassing the DOM entirely, since WebGL canvas content is invisible to the accessibility tree.
|
|
209
|
+
|
|
210
|
+
Currently supported: **MapLibre GL JS**. The architecture is pluggable — Mapbox GL and Leaflet adapters can be added without changing test syntax.
|
|
211
|
+
|
|
212
|
+
```yaml
|
|
213
|
+
steps:
|
|
214
|
+
- navigate to the map view
|
|
215
|
+
- check that the map shows "Stockholm"
|
|
216
|
+
- check that the zoom level is at least 10
|
|
217
|
+
- check that the "hospitals" layer is visible on the map
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
**How it works:** When the planner sees map-related language in a step, it inserts a `MAP_DETECT` step that finds and attaches to the map instance. Subsequent map assertions query the map's actual rendered features (place names, road labels, etc. from vector tile data) and viewport state (center, zoom, bounds, layers). This means "check that the map shows Stockholm" searches for a feature with `name: "Stockholm"` among the thousands of features currently rendered on the canvas — it doesn't just check for the word in the page text.
|
|
221
|
+
|
|
222
|
+
**Map instance detection** works automatically for most setups:
|
|
223
|
+
|
|
224
|
+
1. **React apps** (react-map-gl, etc.) — walks the React fiber tree from the `.maplibregl-map` container to find the map instance in component refs and hook state
|
|
225
|
+
2. **Vue apps** — checks `__vue_app__` (Vue 3) and `__vue__` (Vue 2) component trees
|
|
226
|
+
3. **Global variables** — scans `window.map`, `window.mapInstance`, and similar common names
|
|
227
|
+
4. **Explicit exposure** — for maximum reliability, set `window.__greenlight_map = map` in your app
|
|
228
|
+
|
|
229
|
+
Map detection, state capture, and feature queries all work in both discovery and cached plan runs.
|
|
230
|
+
|
|
205
231
|
### Reusable steps
|
|
206
232
|
|
|
207
233
|
Define common sequences at the suite level and invoke by name:
|
|
@@ -325,7 +351,7 @@ greenlight run --provider openai --llm-base-url http://localhost:11434/v1 --mode
|
|
|
325
351
|
| Layer | Technology |
|
|
326
352
|
|-------|-----------|
|
|
327
353
|
| Browser automation | Playwright (Chromium) |
|
|
328
|
-
| Page representation | Accessibility tree with stable element refs |
|
|
354
|
+
| Page representation | Accessibility tree with stable element refs + map viewport state |
|
|
329
355
|
| AI | OpenRouter (any OpenAI-compatible provider) |
|
|
330
356
|
| Plan caching | SHA-256 hash-based invalidation, `.greenlight/plans/` |
|
|
331
357
|
| Test definitions | YAML |
|
|
@@ -379,6 +405,43 @@ flowchart TD
|
|
|
379
405
|
- [Specifications](docs/specifications.md) — full feature spec, technology decisions, MCP strategy
|
|
380
406
|
- [Implementation Plan](docs/implementation.md) — step-by-step build plan
|
|
381
407
|
|
|
408
|
+
## Avoiding side effects in your app
|
|
409
|
+
|
|
410
|
+
GreenLight provides two signals that your application can use to detect when it's running under a test and suppress side effects like analytics, chat widgets, cookie banners, or third-party scripts that interfere with testing.
|
|
411
|
+
|
|
412
|
+
### `window.__E2E_TEST__`
|
|
413
|
+
|
|
414
|
+
A global boolean set to `true` on every page before any app JavaScript runs. Use it in your frontend code:
|
|
415
|
+
|
|
416
|
+
```js
|
|
417
|
+
if (!window.__E2E_TEST__) {
|
|
418
|
+
initAnalytics()
|
|
419
|
+
loadIntercom()
|
|
420
|
+
showCookieBanner()
|
|
421
|
+
}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
### `X-E2E-Test` HTTP header
|
|
425
|
+
|
|
426
|
+
The header `X-E2E-Test: true` is sent on all same-origin requests (navigation, fetch, XHR). Use it server-side to skip side effects:
|
|
427
|
+
|
|
428
|
+
```js
|
|
429
|
+
// Express middleware example
|
|
430
|
+
app.use((req, res, next) => {
|
|
431
|
+
if (req.headers['x-e2e-test'] === 'true') {
|
|
432
|
+
req.isE2ETest = true
|
|
433
|
+
}
|
|
434
|
+
next()
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
// Skip sending emails during E2E tests
|
|
438
|
+
if (!req.isE2ETest) {
|
|
439
|
+
await sendConfirmationEmail(order)
|
|
440
|
+
}
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
The header is only added to same-origin requests — cross-origin requests to CDNs, tile servers, and third-party APIs are not affected. This avoids triggering CORS preflight requests on external services.
|
|
444
|
+
|
|
382
445
|
## CI/CD
|
|
383
446
|
|
|
384
447
|
```yaml
|
|
@@ -13,12 +13,22 @@ export interface BrowserOptions {
|
|
|
13
13
|
}
|
|
14
14
|
/** Launch a Chromium browser instance. */
|
|
15
15
|
export declare function launchBrowser(config: BrowserOptions): Promise<Browser>;
|
|
16
|
-
/** Create an isolated browser context with configured viewport
|
|
16
|
+
/** Create an isolated browser context with configured viewport. */
|
|
17
17
|
export declare function createContext(browser: Browser, config: BrowserOptions): Promise<BrowserContext>;
|
|
18
|
+
/**
|
|
19
|
+
* Launch a persistent browser context with the zoom extension loaded.
|
|
20
|
+
* Used in headed mode to get real 75% browser zoom.
|
|
21
|
+
* Returns a BrowserContext that the caller can create pages from.
|
|
22
|
+
*/
|
|
23
|
+
export declare function launchPersistentContextWithZoom(config: BrowserOptions): Promise<{
|
|
24
|
+
context: BrowserContext;
|
|
25
|
+
}>;
|
|
18
26
|
/** Create a new page within a browser context and inject test mode global. */
|
|
19
|
-
export declare function createPage(context: BrowserContext
|
|
20
|
-
|
|
21
|
-
|
|
27
|
+
export declare function createPage(context: BrowserContext, options?: {
|
|
28
|
+
headed?: boolean;
|
|
29
|
+
}): Promise<Page>;
|
|
30
|
+
/** Close a browser or persistent context. */
|
|
31
|
+
export declare function closeBrowser(browserOrContext: Browser | BrowserContext): Promise<void>;
|
|
22
32
|
/** Extract browser options from RunConfig. */
|
|
23
33
|
export declare function toBrowserOptions(config: RunConfig): BrowserOptions;
|
|
24
34
|
//# sourceMappingURL=browser.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../../src/browser/browser.ts"],"names":[],"mappings":"AAAA;;;GAGG;
|
|
1
|
+
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../../src/browser/browser.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,EAAY,KAAK,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,IAAI,EAAE,MAAM,YAAY,CAAA;AACnF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAE5C,MAAM,WAAW,cAAc;IAC9B,MAAM,EAAE,OAAO,CAAA;IACf,QAAQ,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAA;CAC3C;AAeD,0CAA0C;AAC1C,wBAAsB,aAAa,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,OAAO,CAAC,CAU5E;AAED,mEAAmE;AACnE,wBAAsB,aAAa,CAClC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,cAAc,GACpB,OAAO,CAAC,cAAc,CAAC,CAIzB;AAgBD;;;;GAIG;AACH,wBAAsB,+BAA+B,CACpD,MAAM,EAAE,cAAc,GACpB,OAAO,CAAC;IAAE,OAAO,EAAE,cAAc,CAAA;CAAE,CAAC,CAsBtC;AAED,8EAA8E;AAC9E,wBAAsB,UAAU,CAC/B,OAAO,EAAE,cAAc,EACvB,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,GAC5B,OAAO,CAAC,IAAI,CAAC,CAyCf;AAED,6CAA6C;AAC7C,wBAAsB,YAAY,CAAC,gBAAgB,EAAE,OAAO,GAAG,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAE5F;AAED,8CAA8C;AAC9C,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,SAAS,GAAG,cAAc,CAKlE"}
|
package/dist/browser/browser.js
CHANGED
|
@@ -2,24 +2,76 @@
|
|
|
2
2
|
* Playwright browser lifecycle management.
|
|
3
3
|
* Wraps launch, context creation, page creation, and cleanup.
|
|
4
4
|
*/
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { createRequire } from "module";
|
|
5
7
|
import { chromium } from "playwright";
|
|
8
|
+
/** Zoom level applied in headed mode (75%). */
|
|
9
|
+
const BROWSER_ZOOM = 75;
|
|
10
|
+
/**
|
|
11
|
+
* Resolve the path to the playwright-zoom extension directory.
|
|
12
|
+
* Uses createRequire because playwright-zoom is a CJS package.
|
|
13
|
+
*/
|
|
14
|
+
function zoomExtensionPath() {
|
|
15
|
+
const require = createRequire(import.meta.url);
|
|
16
|
+
const pwZoomDir = path.dirname(require.resolve("playwright-zoom"));
|
|
17
|
+
return path.join(pwZoomDir, "lib", "zoom-extension");
|
|
18
|
+
}
|
|
6
19
|
/** Launch a Chromium browser instance. */
|
|
7
20
|
export async function launchBrowser(config) {
|
|
8
21
|
return chromium.launch({
|
|
9
22
|
headless: !config.headed,
|
|
23
|
+
args: [
|
|
24
|
+
"--enable-webgl",
|
|
25
|
+
"--enable-webgl2-compute-context",
|
|
26
|
+
"--enable-gpu-rasterization",
|
|
27
|
+
"--ignore-gpu-blocklist",
|
|
28
|
+
],
|
|
10
29
|
});
|
|
11
30
|
}
|
|
12
|
-
/** Create an isolated browser context with configured viewport
|
|
31
|
+
/** Create an isolated browser context with configured viewport. */
|
|
13
32
|
export async function createContext(browser, config) {
|
|
14
33
|
return browser.newContext({
|
|
15
34
|
viewport: config.viewport,
|
|
16
|
-
extraHTTPHeaders: {
|
|
17
|
-
"X-E2E-Test": "true",
|
|
18
|
-
},
|
|
19
35
|
});
|
|
20
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Set browser zoom via the playwright-zoom extension.
|
|
39
|
+
* The extension's content script listens for a postMessage and relays
|
|
40
|
+
* it to its service worker which calls chrome.tabs.setZoom().
|
|
41
|
+
*/
|
|
42
|
+
async function applyBrowserZoom(page, zoom) {
|
|
43
|
+
await page.evaluate((browserZoom) => window.postMessage({ type: "setTabZoom", browserZoom }, "*"), zoom);
|
|
44
|
+
// Small delay for the extension round-trip
|
|
45
|
+
await page.waitForTimeout(200);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Launch a persistent browser context with the zoom extension loaded.
|
|
49
|
+
* Used in headed mode to get real 75% browser zoom.
|
|
50
|
+
* Returns a BrowserContext that the caller can create pages from.
|
|
51
|
+
*/
|
|
52
|
+
export async function launchPersistentContextWithZoom(config) {
|
|
53
|
+
const extPath = zoomExtensionPath();
|
|
54
|
+
const context = await chromium.launchPersistentContext("", {
|
|
55
|
+
headless: false,
|
|
56
|
+
viewport: config.viewport,
|
|
57
|
+
args: [
|
|
58
|
+
"--enable-webgl",
|
|
59
|
+
"--enable-webgl2-compute-context",
|
|
60
|
+
"--enable-gpu-rasterization",
|
|
61
|
+
"--ignore-gpu-blocklist",
|
|
62
|
+
`--disable-extensions-except=${extPath}`,
|
|
63
|
+
`--load-extension=${extPath}`,
|
|
64
|
+
],
|
|
65
|
+
});
|
|
66
|
+
// The zoom extension opens a tab on load — close it
|
|
67
|
+
const existingPages = context.pages();
|
|
68
|
+
for (const p of existingPages) {
|
|
69
|
+
await p.close().catch(() => { });
|
|
70
|
+
}
|
|
71
|
+
return { context };
|
|
72
|
+
}
|
|
21
73
|
/** Create a new page within a browser context and inject test mode global. */
|
|
22
|
-
export async function createPage(context) {
|
|
74
|
+
export async function createPage(context, options) {
|
|
23
75
|
const page = await context.newPage();
|
|
24
76
|
await page.addInitScript(() => {
|
|
25
77
|
Object.defineProperty(window, "__E2E_TEST__", {
|
|
@@ -28,11 +80,40 @@ export async function createPage(context) {
|
|
|
28
80
|
configurable: false,
|
|
29
81
|
});
|
|
30
82
|
});
|
|
83
|
+
// IMPORTANT: Custom headers must ONLY be added to same-origin requests.
|
|
84
|
+
// Never use extraHTTPHeaders on the browser context — it adds headers
|
|
85
|
+
// to ALL requests including cross-origin tile/CDN fetches (e.g.
|
|
86
|
+
// DigitalOcean Spaces PMTiles). Non-standard headers trigger CORS
|
|
87
|
+
// preflight (OPTIONS) requests that tile servers don't handle,
|
|
88
|
+
// breaking map tile loading entirely.
|
|
89
|
+
await page.route("**/*", async (route) => {
|
|
90
|
+
const request = route.request();
|
|
91
|
+
if (request.isNavigationRequest() || request.resourceType() === "fetch" || request.resourceType() === "xhr") {
|
|
92
|
+
try {
|
|
93
|
+
const reqUrl = new URL(request.url());
|
|
94
|
+
const pageUrl = new URL(page.url());
|
|
95
|
+
if (reqUrl.origin === pageUrl.origin) {
|
|
96
|
+
await route.continue({
|
|
97
|
+
headers: { ...request.headers(), "X-E2E-Test": "true" },
|
|
98
|
+
});
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch { /* page.url() can throw before first navigation */ }
|
|
103
|
+
}
|
|
104
|
+
await route.continue();
|
|
105
|
+
});
|
|
106
|
+
// Apply 75% zoom in headed mode after the first navigation
|
|
107
|
+
if (options?.headed) {
|
|
108
|
+
page.once("load", () => {
|
|
109
|
+
applyBrowserZoom(page, BROWSER_ZOOM).catch(() => { });
|
|
110
|
+
});
|
|
111
|
+
}
|
|
31
112
|
return page;
|
|
32
113
|
}
|
|
33
|
-
/** Close
|
|
34
|
-
export async function closeBrowser(
|
|
35
|
-
await
|
|
114
|
+
/** Close a browser or persistent context. */
|
|
115
|
+
export async function closeBrowser(browserOrContext) {
|
|
116
|
+
await browserOrContext.close();
|
|
36
117
|
}
|
|
37
118
|
/** Extract browser options from RunConfig. */
|
|
38
119
|
export function toBrowserOptions(config) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser.js","sourceRoot":"","sources":["../../src/browser/browser.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,QAAQ,EAAgD,MAAM,YAAY,CAAA;AAQnF,0CAA0C;AAC1C,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,MAAsB;IACzD,OAAO,QAAQ,CAAC,MAAM,CAAC;QACtB,QAAQ,EAAE,CAAC,MAAM,CAAC,MAAM;
|
|
1
|
+
{"version":3,"file":"browser.js","sourceRoot":"","sources":["../../src/browser/browser.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/run.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/cli/run.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/cli/run.ts"],"names":[],"mappings":"AA8BA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAsC5C,6DAA6D;AAC7D,wBAAsB,cAAc,CACnC,UAAU,EAAE,MAAM,EAAE,EACpB,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,SAAS,GACf,OAAO,CAAC,IAAI,CAAC,CAgDf;AAED;;;GAGG;AACH,wBAAsB,UAAU,CAC/B,MAAM,EAAE,SAAS,EACjB,aAAa,EAAE,MAAM,EAAE,GACrB,OAAO,CAAC,IAAI,CAAC,CAwSf"}
|
package/dist/cli/run.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { loadSuite } from "../parser/loader.js";
|
|
2
|
-
import { launchBrowser, createContext, createPage, closeBrowser, toBrowserOptions, } from "../browser/browser.js";
|
|
2
|
+
import { launchBrowser, launchPersistentContextWithZoom, createContext, createPage, closeBrowser, toBrowserOptions, } from "../browser/browser.js";
|
|
3
3
|
import { attachConsoleCollector, attachNetworkTracker, } from "../pilot/network.js";
|
|
4
4
|
import { resolveLLMConfig, createLLMClient, } from "../pilot/llm.js";
|
|
5
5
|
import { runTestCase } from "../pilot/pilot.js";
|
|
@@ -9,24 +9,23 @@ import { createPlanRecorder } from "../planner/plan-generator.js";
|
|
|
9
9
|
import { runCachedPlan } from "../planner/plan-runner.js";
|
|
10
10
|
import { resolveModelConfig } from "../types.js";
|
|
11
11
|
import { globals } from "../globals.js";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
12
|
+
import { LLMApiError } from "../pilot/providers/index.js";
|
|
13
|
+
/** Print a single step result as it completes. */
|
|
14
|
+
function printStepResult(stepResult) {
|
|
15
|
+
const icon = stepResult.status === "passed"
|
|
16
|
+
? "\x1b[32m\u2713\x1b[0m"
|
|
17
|
+
: "\x1b[31m\u2717\x1b[0m";
|
|
18
|
+
const dur = `${String(Math.round(stepResult.duration))}ms`;
|
|
19
|
+
const t = stepResult.timing;
|
|
20
|
+
const phases = t
|
|
21
|
+
? ` \x1b[90m[capture:${String(Math.round(t.capture))} llm:${String(Math.round(t.llm))} exec:${String(Math.round(t.execute))} post:${String(Math.round(t.postCapture))}ms]\x1b[0m`
|
|
22
|
+
: "";
|
|
23
|
+
console.log(` ${icon} ${stepResult.step} (${dur})${phases}`);
|
|
24
|
+
if (stepResult.error) {
|
|
25
|
+
console.log(` \x1b[31m${stepResult.error}\x1b[0m`);
|
|
26
|
+
}
|
|
27
|
+
if (globals.debug && stepResult.action) {
|
|
28
|
+
console.log(` Action: ${JSON.stringify(stepResult.action)}`);
|
|
30
29
|
}
|
|
31
30
|
}
|
|
32
31
|
/** Print pass/fail summary for a test case. */
|
|
@@ -132,11 +131,20 @@ export async function runCommand(config, resolvedFiles) {
|
|
|
132
131
|
}
|
|
133
132
|
return llm;
|
|
134
133
|
}
|
|
135
|
-
// Launch browser
|
|
134
|
+
// Launch browser — in headed mode use a persistent context with the
|
|
135
|
+
// zoom extension for real 75% browser zoom. In headless mode use the
|
|
136
|
+
// normal browser + context flow.
|
|
136
137
|
const browserOpts = toBrowserOptions(config);
|
|
137
|
-
let browser;
|
|
138
|
+
let browser = null;
|
|
139
|
+
let persistentContext = null;
|
|
138
140
|
try {
|
|
139
|
-
|
|
141
|
+
if (browserOpts.headed) {
|
|
142
|
+
const result = await launchPersistentContextWithZoom(browserOpts);
|
|
143
|
+
persistentContext = result.context;
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
browser = await launchBrowser(browserOpts);
|
|
147
|
+
}
|
|
140
148
|
}
|
|
141
149
|
catch (err) {
|
|
142
150
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -150,127 +158,162 @@ export async function runCommand(config, resolvedFiles) {
|
|
|
150
158
|
? suite.tests.filter((t) => t.name === config.testFilter)
|
|
151
159
|
: suite.tests;
|
|
152
160
|
for (const test of tests) {
|
|
153
|
-
const testSlug = slugify(test.name);
|
|
154
|
-
const testHash = computeTestHash(test);
|
|
155
|
-
const hashKey = `${suiteSlug}/${testSlug}`;
|
|
156
|
-
const cachedHash = hashIndex[hashKey];
|
|
157
|
-
// Determine execution mode
|
|
158
|
-
let useCachedPlan = false;
|
|
159
|
-
let cachedPlan = null;
|
|
160
|
-
if (!config.discover && cachedHash === testHash) {
|
|
161
|
-
cachedPlan = await loadPlan(cwd, suiteSlug, testSlug);
|
|
162
|
-
if (cachedPlan) {
|
|
163
|
-
useCachedPlan = true;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
const modeLabel = useCachedPlan
|
|
167
|
-
? "\x1b[36mcached\x1b[0m"
|
|
168
|
-
: "\x1b[33mdiscovery\x1b[0m";
|
|
169
|
-
console.log(`\n Test: ${test.name} [${modeLabel}]`);
|
|
170
|
-
if (!config.discover &&
|
|
171
|
-
cachedHash &&
|
|
172
|
-
cachedHash !== testHash) {
|
|
173
|
-
console.log(` \x1b[33mPlan stale, re-discovering\x1b[0m`);
|
|
174
|
-
}
|
|
175
|
-
// Fresh context per test case
|
|
176
|
-
const context = await createContext(browser, browserOpts);
|
|
177
|
-
const page = await createPage(context);
|
|
178
|
-
const { drain } = attachConsoleCollector(page);
|
|
179
|
-
const { waitForNetworkIdle } = attachNetworkTracker(page);
|
|
180
|
-
globals.trace.attachToPage(page);
|
|
181
161
|
try {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
162
|
+
const testSlug = slugify(test.name);
|
|
163
|
+
const testHash = computeTestHash(test);
|
|
164
|
+
const hashKey = `${suiteSlug}/${testSlug}`;
|
|
165
|
+
const cachedHash = hashIndex[hashKey];
|
|
166
|
+
// Determine execution mode
|
|
167
|
+
let useCachedPlan = false;
|
|
168
|
+
let cachedPlan = null;
|
|
169
|
+
if (!config.discover && cachedHash === testHash) {
|
|
170
|
+
cachedPlan = await loadPlan(cwd, suiteSlug, testSlug);
|
|
171
|
+
if (cachedPlan) {
|
|
172
|
+
useCachedPlan = true;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const modeLabel = useCachedPlan
|
|
176
|
+
? "\x1b[36mcached\x1b[0m"
|
|
177
|
+
: "\x1b[33mdiscovery\x1b[0m";
|
|
178
|
+
console.log(`\n Test: ${test.name} [${modeLabel}]`);
|
|
179
|
+
if (!config.discover &&
|
|
180
|
+
cachedHash &&
|
|
181
|
+
cachedHash !== testHash) {
|
|
182
|
+
console.log(` \x1b[33mPlan stale, re-discovering\x1b[0m`);
|
|
183
|
+
}
|
|
184
|
+
// Fresh context per test case (reuse persistent context in headed mode)
|
|
185
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
186
|
+
const context = persistentContext ?? await createContext(browser, browserOpts);
|
|
187
|
+
const page = await createPage(context, { headed: browserOpts.headed });
|
|
188
|
+
const { drain } = attachConsoleCollector(page);
|
|
189
|
+
const { waitForNetworkIdle } = attachNetworkTracker(page);
|
|
190
|
+
globals.trace.attachToPage(page);
|
|
191
|
+
try {
|
|
192
|
+
globals.trace.log("goto", suite.base_url);
|
|
193
|
+
await page.goto(suite.base_url);
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
197
|
+
console.error(`\n \x1b[31m\u2717\x1b[0m Failed to navigate to ${suite.base_url}: ${msg}`);
|
|
200
198
|
globals.trace.detachFromPage(page);
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
199
|
+
if (persistentContext) {
|
|
200
|
+
// Persistent context is shared — close pages only
|
|
201
|
+
for (const p of context.pages())
|
|
202
|
+
await p.close().catch(() => { });
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
await context.close();
|
|
206
|
+
}
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
let result;
|
|
210
|
+
if (useCachedPlan && cachedPlan) {
|
|
211
|
+
// Fast run — replay cached plan
|
|
212
|
+
result = await runCachedPlan(page, cachedPlan, test.name, { waitForNetworkIdle, onStepComplete: printStepResult });
|
|
213
|
+
// Handle plan drift
|
|
214
|
+
if (result.drifted && config.onDrift === "rerun") {
|
|
215
|
+
console.log(` \x1b[33mPlan drift detected, re-running with LLM\x1b[0m`);
|
|
216
|
+
// Close and re-create context for fresh state
|
|
217
|
+
globals.trace.detachFromPage(page);
|
|
218
|
+
if (persistentContext) {
|
|
219
|
+
// Persistent context is shared — close pages only
|
|
220
|
+
for (const p of context.pages())
|
|
221
|
+
await p.close().catch(() => { });
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
await context.close();
|
|
225
|
+
}
|
|
226
|
+
const ctx2 = persistentContext ?? await createContext(
|
|
227
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
228
|
+
browser, browserOpts);
|
|
229
|
+
const page2 = await createPage(ctx2, { headed: browserOpts.headed });
|
|
230
|
+
const { drain: drain2 } = attachConsoleCollector(page2);
|
|
231
|
+
const { waitForNetworkIdle: waitForNetworkIdle2 } = attachNetworkTracker(page2);
|
|
232
|
+
globals.trace.attachToPage(page2);
|
|
233
|
+
await page2.goto(suite.base_url);
|
|
234
|
+
const modelLabel = typeof effectiveModel === "string"
|
|
235
|
+
? effectiveModel
|
|
236
|
+
: `${effectiveModel.planner}/${effectiveModel.pilot}`;
|
|
237
|
+
const recorder = createPlanRecorder(suiteSlug, testSlug, testHash, modelLabel);
|
|
238
|
+
result = await runTestCase(page2, test, getOrCreateLLM(), {
|
|
239
|
+
timeout: config.timeout,
|
|
240
|
+
consoleDrain: drain2,
|
|
241
|
+
recorder,
|
|
242
|
+
waitForNetworkIdle: waitForNetworkIdle2,
|
|
243
|
+
onStepComplete: printStepResult,
|
|
244
|
+
});
|
|
245
|
+
result.mode = "discovery";
|
|
246
|
+
if (result.status === "passed") {
|
|
247
|
+
const plan = recorder.finalize();
|
|
248
|
+
await savePlan(cwd, plan);
|
|
249
|
+
hashIndex[hashKey] = testHash;
|
|
250
|
+
hashIndexDirty = true;
|
|
251
|
+
await ensureGitignore(cwd);
|
|
252
|
+
console.log(` \x1b[32mCached plan updated\x1b[0m`);
|
|
253
|
+
}
|
|
254
|
+
globals.trace.detachFromPage(page2);
|
|
255
|
+
await ctx2.close();
|
|
256
|
+
printTestSummary(result);
|
|
257
|
+
if (config.headed) {
|
|
258
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
259
|
+
}
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
// Discovery run — full LLM loop with recorder
|
|
265
|
+
const discoveryModelLabel = typeof effectiveModel === "string"
|
|
209
266
|
? effectiveModel
|
|
210
267
|
: `${effectiveModel.planner}/${effectiveModel.pilot}`;
|
|
211
|
-
const recorder = createPlanRecorder(suiteSlug, testSlug, testHash,
|
|
212
|
-
result = await runTestCase(
|
|
268
|
+
const recorder = createPlanRecorder(suiteSlug, testSlug, testHash, discoveryModelLabel);
|
|
269
|
+
result = await runTestCase(page, test, getOrCreateLLM(), {
|
|
213
270
|
timeout: config.timeout,
|
|
214
|
-
consoleDrain:
|
|
271
|
+
consoleDrain: drain,
|
|
215
272
|
recorder,
|
|
216
|
-
waitForNetworkIdle
|
|
273
|
+
waitForNetworkIdle,
|
|
274
|
+
onStepComplete: printStepResult,
|
|
217
275
|
});
|
|
218
276
|
result.mode = "discovery";
|
|
277
|
+
// Save plan only if the test passed
|
|
219
278
|
if (result.status === "passed") {
|
|
220
279
|
const plan = recorder.finalize();
|
|
221
280
|
await savePlan(cwd, plan);
|
|
222
281
|
hashIndex[hashKey] = testHash;
|
|
223
282
|
hashIndexDirty = true;
|
|
224
283
|
await ensureGitignore(cwd);
|
|
225
|
-
console.log(` \x1b[32mCached plan
|
|
284
|
+
console.log(` \x1b[32mCached plan generated for: ${test.name}\x1b[0m`);
|
|
226
285
|
}
|
|
227
|
-
globals.trace.detachFromPage(page2);
|
|
228
|
-
await ctx2.close();
|
|
229
|
-
printStepResults(result);
|
|
230
|
-
printTestSummary(result);
|
|
231
|
-
if (config.headed) {
|
|
232
|
-
await new Promise((r) => setTimeout(r, 2000));
|
|
233
|
-
}
|
|
234
|
-
continue;
|
|
235
286
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
});
|
|
249
|
-
result.mode = "discovery";
|
|
250
|
-
// Save plan only if the test passed
|
|
251
|
-
if (result.status === "passed") {
|
|
252
|
-
const plan = recorder.finalize();
|
|
253
|
-
await savePlan(cwd, plan);
|
|
254
|
-
hashIndex[hashKey] = testHash;
|
|
255
|
-
hashIndexDirty = true;
|
|
256
|
-
await ensureGitignore(cwd);
|
|
257
|
-
console.log(` \x1b[32mCached plan generated for: ${test.name}\x1b[0m`);
|
|
287
|
+
printTestSummary(result);
|
|
288
|
+
if (config.headed) {
|
|
289
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
290
|
+
}
|
|
291
|
+
globals.trace.detachFromPage(page);
|
|
292
|
+
if (persistentContext) {
|
|
293
|
+
// Persistent context is shared — close pages only
|
|
294
|
+
for (const p of context.pages())
|
|
295
|
+
await p.close().catch(() => { });
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
await context.close();
|
|
258
299
|
}
|
|
259
300
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
301
|
+
catch (err) {
|
|
302
|
+
if (err instanceof LLMApiError) {
|
|
303
|
+
console.error(`\n \x1b[31mLLM API returned ${String(err.status)} — aborting test run.\x1b[0m`);
|
|
304
|
+
console.error(` ${err.message}\n`);
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
throw err;
|
|
264
308
|
}
|
|
265
|
-
globals.trace.detachFromPage(page);
|
|
266
|
-
await context.close();
|
|
267
309
|
}
|
|
268
310
|
}
|
|
269
311
|
finally {
|
|
270
312
|
if (hashIndexDirty) {
|
|
271
313
|
await saveHashIndex(cwd, hashIndex);
|
|
272
314
|
}
|
|
273
|
-
|
|
315
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- browser is guaranteed non-null when persistentContext is null
|
|
316
|
+
await closeBrowser(persistentContext ?? browser);
|
|
274
317
|
}
|
|
275
318
|
}
|
|
276
319
|
}
|