@rettangoli/vt 1.0.3 → 1.0.5

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 CHANGED
@@ -21,6 +21,7 @@ Behavior split:
21
21
  - `--concurrency <number>`
22
22
  - `--timeout <ms>`
23
23
  - `--wait-event <name>`
24
+ - `--isolation <fast|strict>`
24
25
  - `--folder <path>` (repeatable)
25
26
  - `--group <section-key>` (repeatable)
26
27
  - `--item <spec-path>` (repeatable)
@@ -69,7 +70,7 @@ rtgl vt report --group components-basic
69
70
  rtgl vt report --item components/forms/login
70
71
  ```
71
72
 
72
- Everything else in capture is internal and intentionally not user-configurable.
73
+ Other capture tuning remains internal and intentionally not user-configurable.
73
74
 
74
75
  ## Config
75
76
 
@@ -85,6 +86,7 @@ vt:
85
86
  concurrency: 4
86
87
  timeout: 30000
87
88
  waitEvent: vt:ready
89
+ isolationMode: strict
88
90
  viewport:
89
91
  id: desktop
90
92
  width: 1280
@@ -99,6 +101,8 @@ Notes:
99
101
  - `vt.sections` is required.
100
102
  - `vt.service` is optional. When set, VT starts the command before capture, waits for `vt.url`, then stops it after capture.
101
103
  - when `vt.service` is omitted and `vt.url` is set, VT expects that URL to already be running.
104
+ - `vt.isolationMode` is optional: `fast` or `strict`. Default is `fast`.
105
+ - `strict` uses a fresh browser context per captured spec and is recommended for real app routes or IndexedDB-backed state.
102
106
  - Section page keys are derived as `kebab-case(title)` for flat sections and group `items[].title`.
103
107
  - Derived section page keys must be unique case-insensitively.
104
108
  - `vt.viewport` supports object or array; each viewport requires `id`, `width`, `height`.
@@ -142,15 +146,15 @@ Screenshot naming:
142
146
  A pre-built Docker image with `rtgl` and Playwright browsers is available:
143
147
 
144
148
  ```bash
145
- docker pull han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.10
149
+ docker pull han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.12
146
150
  ```
147
151
 
148
152
  Run commands against a local project:
149
153
 
150
154
  ```bash
151
- docker run --rm -v "$(pwd):/workspace" han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.10 rtgl vt screenshot
152
- docker run --rm -v "$(pwd):/workspace" han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.10 rtgl vt report
153
- docker run --rm -v "$(pwd):/workspace" han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.10 rtgl vt accept
155
+ docker run --rm -v "$(pwd):/workspace" han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.12 rtgl vt screenshot
156
+ docker run --rm -v "$(pwd):/workspace" han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.12 rtgl vt report
157
+ docker run --rm -v "$(pwd):/workspace" han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.12 rtgl vt accept
154
158
  ```
155
159
 
156
160
  Note:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rettangoli/vt",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Rettangoli Visual Testing",
5
5
  "type": "module",
6
6
  "repository": {
@@ -56,15 +56,11 @@ export class PlaywrightRunner {
56
56
  this.envVarPrefix = envVarPrefix;
57
57
  this.envVars = collectEnvVars(this.envVarPrefix);
58
58
  this.sharedContext = null;
59
- this.sharedPage = null;
60
- this.sharedRegisteredReadyEvents = new Set();
61
59
  }
62
60
 
63
61
  async initialize() {
64
62
  if (this.isolationMode === "fast") {
65
63
  this.sharedContext = await this.createContext();
66
- this.sharedPage = await this.sharedContext.newPage();
67
- await this.configurePage(this.sharedPage);
68
64
  }
69
65
  }
70
66
 
@@ -72,8 +68,6 @@ export class PlaywrightRunner {
72
68
  if (this.sharedContext) {
73
69
  await this.sharedContext.close();
74
70
  this.sharedContext = null;
75
- this.sharedPage = null;
76
- this.sharedRegisteredReadyEvents.clear();
77
71
  }
78
72
  }
79
73
 
@@ -83,8 +77,6 @@ export class PlaywrightRunner {
83
77
  }
84
78
  await this.dispose();
85
79
  this.sharedContext = await this.createContext();
86
- this.sharedPage = await this.sharedContext.newPage();
87
- await this.configurePage(this.sharedPage);
88
80
  }
89
81
 
90
82
  async createContext() {
@@ -138,32 +130,13 @@ export class PlaywrightRunner {
138
130
  });
139
131
  }
140
132
 
141
- async acquireSession() {
142
- if (this.isolationMode === "strict") {
143
- const context = await this.createContext();
144
- const page = await context.newPage();
145
- await this.configurePage(page);
146
- return {
147
- page,
148
- resetSession: async () => 0,
149
- registeredReadyEvents: new Set(),
150
- cleanup: async () => {
151
- await context.close();
152
- },
153
- };
133
+ async clearOriginRuntimeState(page) {
134
+ if (!page || page.isClosed()) {
135
+ return;
154
136
  }
155
137
 
156
- if (!this.sharedContext) {
157
- this.sharedContext = await this.createContext();
158
- }
159
- if (!this.sharedPage || this.sharedPage.isClosed()) {
160
- this.sharedPage = await this.sharedContext.newPage();
161
- await this.configurePage(this.sharedPage);
162
- }
163
- const resetSession = async () => {
164
- const resetStart = nowMs();
165
- // Clear origin-scoped runtime state before switching away.
166
- await this.sharedPage.evaluate(async () => {
138
+ try {
139
+ await page.evaluate(async () => {
167
140
  try {
168
141
  localStorage.clear();
169
142
  } catch {}
@@ -183,18 +156,48 @@ export class PlaywrightRunner {
183
156
  }
184
157
  } catch {}
185
158
  });
186
- await this.sharedPage.goto("about:blank", { waitUntil: "domcontentloaded" });
159
+ } catch {}
160
+ }
161
+
162
+ async acquireSession() {
163
+ if (this.isolationMode === "strict") {
164
+ const context = await this.createContext();
165
+ const page = await context.newPage();
166
+ await this.configurePage(page);
167
+ return {
168
+ page,
169
+ resetSession: async () => 0,
170
+ registeredReadyEvents: new Set(),
171
+ cleanup: async () => {
172
+ await context.close();
173
+ },
174
+ };
175
+ }
176
+
177
+ if (!this.sharedContext) {
178
+ this.sharedContext = await this.createContext();
179
+ }
180
+
181
+ const page = await this.sharedContext.newPage();
182
+ await this.configurePage(page);
183
+
184
+ const resetSession = async () => {
185
+ const resetStart = nowMs();
187
186
  await this.sharedContext.clearCookies();
188
187
  await this.sharedContext.clearPermissions();
189
- await this.configurePage(this.sharedPage);
190
188
  return nowMs() - resetStart;
191
189
  };
192
190
 
193
191
  return {
194
- page: this.sharedPage,
192
+ page,
195
193
  resetSession,
196
- registeredReadyEvents: this.sharedRegisteredReadyEvents,
197
- cleanup: async () => {},
194
+ registeredReadyEvents: new Set(),
195
+ cleanup: async () => {
196
+ await this.clearOriginRuntimeState(page);
197
+ if (!page.isClosed()) {
198
+ await page.close();
199
+ }
200
+ },
198
201
  };
199
202
  }
200
203
 
@@ -11,6 +11,7 @@ export function resolveGenerateOptions(options = {}, configData = {}) {
11
11
  concurrency: cliConcurrency,
12
12
  timeout: cliTimeout,
13
13
  waitEvent: cliWaitEvent,
14
+ isolation: cliIsolation,
14
15
  folder: cliFolder,
15
16
  group: cliGroup,
16
17
  item: cliItem,
@@ -20,6 +21,7 @@ export function resolveGenerateOptions(options = {}, configData = {}) {
20
21
 
21
22
  const waitEvent = cliWaitEvent ?? configData.waitEvent;
22
23
  const timeout = cliTimeout ?? configData.timeout ?? 30000;
24
+ const isolationMode = cliIsolation ?? configData.isolationMode ?? "fast";
23
25
  const selectors = normalizeSelectors({
24
26
  folder: cliFolder,
25
27
  group: cliGroup,
@@ -52,7 +54,7 @@ export function resolveGenerateOptions(options = {}, configData = {}) {
52
54
  screenshotWaitTime: 0,
53
55
  waitStrategy: waitEvent ? "event" : "load",
54
56
  workerCount: cliConcurrency ?? configData.concurrency ?? undefined, // adaptive worker planning
55
- isolationMode: "fast",
57
+ isolationMode,
56
58
  navigationTimeout: timeout,
57
59
  readyTimeout: timeout,
58
60
  screenshotTimeout: timeout,
@@ -84,6 +86,11 @@ export function resolveGenerateOptions(options = {}, configData = {}) {
84
86
  throw new Error(`Invalid waitEvent: expected a non-empty string, got ${typeof resolvedOptions.waitEvent}.`);
85
87
  }
86
88
  }
89
+ if (!["fast", "strict"].includes(resolvedOptions.isolationMode)) {
90
+ throw new Error(
91
+ `Invalid isolation: expected "fast" or "strict", got "${resolvedOptions.isolationMode}".`,
92
+ );
93
+ }
87
94
  if (resolvedOptions.configUrl !== undefined && resolvedOptions.configUrl !== null) {
88
95
  if (typeof resolvedOptions.configUrl !== "string" || resolvedOptions.configUrl.trim().length === 0) {
89
96
  throw new Error(`Invalid url: expected a non-empty string, got ${typeof resolvedOptions.configUrl}.`);
package/src/validation.js CHANGED
@@ -112,7 +112,6 @@ const LEGACY_CAPTURE_FIELDS = {
112
112
  waitSelector: true,
113
113
  waitStrategy: true,
114
114
  workerCount: true,
115
- isolationMode: true,
116
115
  navigationTimeout: true,
117
116
  readyTimeout: true,
118
117
  screenshotTimeout: true,
@@ -557,6 +556,7 @@ export function validateVtConfig(vtConfig, sourcePath = "rettangoli.config.yaml"
557
556
  validateOptionalNumber(vtConfig.concurrency, "vt.concurrency", { integer: true, min: 1 });
558
557
  validateOptionalNumber(vtConfig.timeout, "vt.timeout", { integer: true, min: 1 });
559
558
  validateOptionalString(vtConfig.waitEvent, "vt.waitEvent");
559
+ validateOptionalEnum(vtConfig.isolationMode, "vt.isolationMode", ["fast", "strict"]);
560
560
  normalizeViewportField(vtConfig.viewport, "vt.viewport");
561
561
  validateOptionalNumber(vtConfig.colorThreshold, "vt.colorThreshold", { min: 0, max: 1 });
562
562
  validateOptionalNumber(vtConfig.diffThreshold, "vt.diffThreshold", { min: 0, max: 100 });