@agent-scope/playwright 1.17.1 → 1.17.3
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 +416 -0
- package/package.json +5 -4
package/README.md
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
# `@agent-scope/playwright`
|
|
2
|
+
|
|
3
|
+
Playwright fixture + browser-entry bundle for capturing React component trees in end-to-end tests. Extends `@playwright/test` with a `scope.capture()` fixture method that serialises the live React fibre tree into a `PageReport` — no source maps, no instrumented builds required beyond the Babel plugin.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @agent-scope/playwright
|
|
9
|
+
# or
|
|
10
|
+
bun add @agent-scope/playwright
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Peer dependencies: `@playwright/test`.
|
|
14
|
+
|
|
15
|
+
**Prerequisites**: The package ships a pre-built browser IIFE bundle (`browser-bundle.iife.js`). Build it first if it doesn't exist:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bun run build # in packages/playwright
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## What it does / when to use
|
|
24
|
+
|
|
25
|
+
| Use case | What to use |
|
|
26
|
+
|---|---|
|
|
27
|
+
| Capture React tree in a Playwright test | `test` + `scope.capture()` fixture |
|
|
28
|
+
| Navigate then capture in one call | `scope.captureUrl(url)` |
|
|
29
|
+
| Capture from a second page object | `scope.capture(otherPage)` |
|
|
30
|
+
| Wait for async data to finish loading | `scope.capture({ waitForStable: true })` |
|
|
31
|
+
| Inject the browser script manually (Vite/custom setup) | `getBrowserEntryScript()` |
|
|
32
|
+
| Load a `PageReport` from JSON | `loadTrace(rawJson)` |
|
|
33
|
+
| Generate a Playwright test skeleton from a trace | `generateTest(trace)` |
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Architecture
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
Browser (page) Node.js (test)
|
|
41
|
+
───────────────────────────────── ───────────────────────────
|
|
42
|
+
page.addInitScript(bundle) fixture.ts
|
|
43
|
+
└── browser-entry.ts (IIFE) ← BrowserPool / SatoriRenderer
|
|
44
|
+
1. installHook() scope.capture()
|
|
45
|
+
2. Vite react-refresh compat └── evaluateCapture(page)
|
|
46
|
+
3. await firstCommit └── page.evaluate(
|
|
47
|
+
4. window.__SCOPE_CAPTURE__() __SCOPE_CAPTURE_JSON__()
|
|
48
|
+
5. window.__SCOPE_CAPTURE_JSON__() )
|
|
49
|
+
→ JSON string
|
|
50
|
+
→ JSON.parse()
|
|
51
|
+
→ PageReport
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Browser-entry injection pattern
|
|
55
|
+
|
|
56
|
+
The fixture calls `page.addInitScript({ path: bundlePath })` before any navigation. This ensures the bundle runs in the browser before React initialises — critical because the DevTools hook (`installHook()`) must be installed before React evaluates its own bootstrap code.
|
|
57
|
+
|
|
58
|
+
### Vite react-refresh compatibility
|
|
59
|
+
|
|
60
|
+
Vite's `react-refresh` preamble (`injectIntoGlobalHook`) accesses `hook.renderers.forEach(...)` (a `Map<number, RendererObj>`). The Scope devtools hook stores renderers in `_renderers` (with wrapper objects). `browser-entry.ts` creates a proxy `renderers` Map that exposes only the raw renderer objects, patching `inject()` to keep it in sync. Without this, the preamble throws mid-execution and React commits never reach the hook.
|
|
61
|
+
|
|
62
|
+
### Async React 18 first-commit
|
|
63
|
+
|
|
64
|
+
React 18's concurrent scheduler may call `inject()` before the first `onCommitFiberRoot`. The browser entry wraps `onCommitFiberRoot` to set `hasCommitted = true` and resolve a `firstCommit` Promise. `window.__SCOPE_CAPTURE__()` awaits this promise if `hasCommitted` is false — preventing "Execution context was destroyed" races in SPAs that navigate before the first render completes.
|
|
65
|
+
|
|
66
|
+
### CDP structured-clone bypass
|
|
67
|
+
|
|
68
|
+
`window.__SCOPE_CAPTURE_JSON__()` serialises the `PageReport` to a JSON string inside the browser, bypassing Playwright's CDP structured-clone limit. `evaluateCapture()` prefers `__SCOPE_CAPTURE_JSON__` and falls back to `__SCOPE_CAPTURE__` for older runtime versions.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## API Reference
|
|
73
|
+
|
|
74
|
+
### `test` and `expect`
|
|
75
|
+
|
|
76
|
+
Drop-in replacements for `@playwright/test`'s `test` and `expect` that add the `scope` fixture.
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
import { test, expect } from '@agent-scope/playwright';
|
|
80
|
+
|
|
81
|
+
test('captures React tree', async ({ scope, page }) => {
|
|
82
|
+
await page.goto('http://localhost:5173');
|
|
83
|
+
const report = await scope.capture();
|
|
84
|
+
expect(report.tree.name).toBeTruthy();
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
### `ScopeFixture`
|
|
91
|
+
|
|
92
|
+
The `scope` object available inside tests.
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
interface ScopeFixture {
|
|
96
|
+
scope: {
|
|
97
|
+
capture(options?: CaptureOptions): Promise<PageReport>;
|
|
98
|
+
capture(targetPage: Page, options?: CaptureOptions): Promise<PageReport>;
|
|
99
|
+
captureUrl(url: string): Promise<PageReport>;
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
#### `scope.capture(options?)`
|
|
105
|
+
|
|
106
|
+
Captures the React component tree from the fixture's default `page`. Safe to call immediately after `page.goto()` — the browser bundle awaits the first React commit internally.
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
const report = await scope.capture();
|
|
110
|
+
const report = await scope.capture({ waitForStable: true, stableMs: 500 });
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
#### `scope.capture(targetPage, options?)`
|
|
114
|
+
|
|
115
|
+
Captures from an alternative Playwright `Page` object. The browser bundle is injected into `targetPage` automatically.
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
const secondPage = await context.newPage();
|
|
119
|
+
await secondPage.goto('http://localhost:5173/modal');
|
|
120
|
+
const report = await scope.capture(secondPage);
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
#### `scope.captureUrl(url)`
|
|
124
|
+
|
|
125
|
+
Navigates to `url` on the fixture page then immediately captures.
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
const report = await scope.captureUrl('http://localhost:5173');
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
### `CaptureOptions`
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
interface CaptureOptions {
|
|
137
|
+
/**
|
|
138
|
+
* Poll until component count is stable for `stableMs` ms.
|
|
139
|
+
* Use when the page performs async data loading after initial render.
|
|
140
|
+
* Default: false.
|
|
141
|
+
*/
|
|
142
|
+
waitForStable?: boolean;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* How long (ms) component count must remain unchanged before capture
|
|
146
|
+
* is considered stable. Only used with waitForStable: true.
|
|
147
|
+
* Default: 1000.
|
|
148
|
+
*/
|
|
149
|
+
stableMs?: number;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Maximum time (ms) to spend polling before returning the last
|
|
153
|
+
* successful capture. Does NOT throw on timeout.
|
|
154
|
+
* Only used with waitForStable: true.
|
|
155
|
+
* Default: 15000.
|
|
156
|
+
*/
|
|
157
|
+
timeoutMs?: number;
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Use lightweight captures (structure only, no props/state/hooks) during
|
|
161
|
+
* stability polling. A single full capture is performed once stability
|
|
162
|
+
* is confirmed. Reduces CDP payload cost per poll tick.
|
|
163
|
+
* Only used with waitForStable: true.
|
|
164
|
+
* Default: false.
|
|
165
|
+
*/
|
|
166
|
+
lightweight?: boolean;
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
### `PageReport`
|
|
173
|
+
|
|
174
|
+
The return type of `scope.capture()` — defined in `@agent-scope/core`.
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
interface PageReport {
|
|
178
|
+
url: string;
|
|
179
|
+
route: null;
|
|
180
|
+
timestamp: number;
|
|
181
|
+
capturedIn: number; // ms to capture
|
|
182
|
+
tree: ComponentNode; // root of the React component tree
|
|
183
|
+
errors: unknown[];
|
|
184
|
+
suspenseBoundaries: unknown[];
|
|
185
|
+
consoleEntries: unknown[];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
interface ComponentNode {
|
|
189
|
+
id: number;
|
|
190
|
+
name: string;
|
|
191
|
+
type: 'function' | 'class' | 'other';
|
|
192
|
+
source: SourceLocation | null;
|
|
193
|
+
props: SerializedValue;
|
|
194
|
+
state: SerializedValue[];
|
|
195
|
+
context: SerializedValue[];
|
|
196
|
+
renderCount: number;
|
|
197
|
+
renderDuration: number;
|
|
198
|
+
children: ComponentNode[];
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
### `getBrowserEntryScript()`
|
|
205
|
+
|
|
206
|
+
Returns the pre-built browser IIFE bundle as a string. Use this to inject the Scope runtime into a Playwright page manually — useful in Vite-based setups or when you need to inject it outside of the fixture.
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
import { getBrowserEntryScript } from '@agent-scope/playwright';
|
|
210
|
+
|
|
211
|
+
// In a Playwright globalSetup or a manual test:
|
|
212
|
+
const script = getBrowserEntryScript();
|
|
213
|
+
await page.addInitScript({ content: script });
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
The bundle is searched in:
|
|
217
|
+
1. `dist/browser-bundle.iife.js` (relative to the package's dist directory, when running from an installed package)
|
|
218
|
+
2. `../dist/browser-bundle.iife.js` (when running from source)
|
|
219
|
+
|
|
220
|
+
Throws with a clear message if the bundle is not found, instructing you to run `bun run build`.
|
|
221
|
+
|
|
222
|
+
**Why use this instead of `{ path: bundlePath }` directly?**
|
|
223
|
+
|
|
224
|
+
`getBrowserEntryScript()` returns the bundle content as a string, which is required when:
|
|
225
|
+
- You cannot resolve the bundle file path reliably (e.g. in bundled test environments)
|
|
226
|
+
- You need to modify or prefix the script before injection
|
|
227
|
+
- You want to inject into multiple pages without file-path resolution per page
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
### `evaluateCapture(page)` / `captureUntilStable(page, stableMs, timeoutMs, lightweight?)`
|
|
232
|
+
|
|
233
|
+
Low-level capture utilities (exported from `capture-utils.ts`).
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
import { evaluateCapture, captureUntilStable, countNodes } from '@agent-scope/playwright';
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
**`evaluateCapture(page)`** — single capture with retry logic.
|
|
240
|
+
|
|
241
|
+
Calls `page.evaluate(() => window.__SCOPE_CAPTURE_JSON__())`. Retries up to `MAX_RETRIES` (3) times on "Execution context was destroyed" errors (caused by navigations racing with `evaluate`). Each retry waits `RETRY_DELAY_MS` (500 ms). Non-retriable errors are re-thrown immediately.
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
// Succeeds after 1 context-destroyed error:
|
|
245
|
+
let callCount = 0;
|
|
246
|
+
const page = makePage(async () => {
|
|
247
|
+
if (++callCount === 1) throw new Error('Execution context was destroyed');
|
|
248
|
+
return JSON.stringify(report);
|
|
249
|
+
});
|
|
250
|
+
const result = await evaluateCapture(page); // callCount === 2
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
**`captureUntilStable(page, stableMs, timeoutMs, lightweight?)`** — stability polling.
|
|
254
|
+
|
|
255
|
+
Polls `evaluateCapture` every `POLL_INTERVAL_MS` (300 ms). Returns when the component node count (via `countNodes(report.tree)`) has been unchanged for `stableMs` ms. On timeout, returns the last successful capture instead of throwing.
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
// Tree grows then stabilises at 5 nodes:
|
|
259
|
+
const result = await captureUntilStable(page, 500, 10000);
|
|
260
|
+
// countNodes(result.tree) === 5
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
When `lightweight: true`, polls with `evaluateLightweightCapture` (calls `__SCOPE_CAPTURE_JSON__({ lightweight: true })`) and performs a single full capture once stability is confirmed.
|
|
264
|
+
|
|
265
|
+
**`countNodes(node)`** — recursive node counter.
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
countNodes(makeNode(1));
|
|
269
|
+
// → 1
|
|
270
|
+
|
|
271
|
+
countNodes(makeNode(1, [makeNode(2, [makeNode(4), makeNode(5)]), makeNode(3)]));
|
|
272
|
+
// → 5
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
### `loadTrace(rawJson)` / `generateTest(trace, options?)`
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
import { loadTrace, generateTest } from '@agent-scope/playwright';
|
|
281
|
+
|
|
282
|
+
// Parse a raw PageReport JSON string into a CaptureTrace:
|
|
283
|
+
const trace = loadTrace(fs.readFileSync('capture.json', 'utf-8'));
|
|
284
|
+
// trace.report: PageReport
|
|
285
|
+
// trace.capturedAt: number (Date.now() at load time)
|
|
286
|
+
|
|
287
|
+
// Generate a Playwright test skeleton:
|
|
288
|
+
const source = generateTest(trace, {
|
|
289
|
+
description: 'Button renders in primary variant',
|
|
290
|
+
outputPath: 'button.spec.ts',
|
|
291
|
+
});
|
|
292
|
+
// Returns a TypeScript test file string
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## Real test payloads
|
|
298
|
+
|
|
299
|
+
### Basic capture (from `fixture.test.ts`)
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
test('captures React tree from basic-tree fixture', async ({ scope, page }) => {
|
|
303
|
+
await page.goto('http://localhost:5173');
|
|
304
|
+
const report = await scope.capture();
|
|
305
|
+
|
|
306
|
+
expect(report.url).toContain('localhost:5173');
|
|
307
|
+
expect(typeof report.timestamp).toBe('number');
|
|
308
|
+
expect(report.tree.name).toBeTruthy();
|
|
309
|
+
expect(Array.isArray(report.errors)).toBe(true);
|
|
310
|
+
expect(report.errors).toHaveLength(0); // no intentional errors in basic-tree fixture
|
|
311
|
+
expect(report.route).toBeNull();
|
|
312
|
+
});
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### `captureUrl` shorthand (from `fixture.test.ts`)
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
test('captureUrl navigates and captures', async ({ scope }) => {
|
|
319
|
+
const report = await scope.captureUrl('http://localhost:5173');
|
|
320
|
+
expect(report.url).toContain('localhost:5173');
|
|
321
|
+
expect(report.tree.children.length).toBeGreaterThanOrEqual(0);
|
|
322
|
+
});
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Context-destroyed retry (from `capture-utils.test.ts`)
|
|
326
|
+
|
|
327
|
+
```typescript
|
|
328
|
+
// evaluateCapture retries once on context-destroyed, then succeeds:
|
|
329
|
+
let callCount = 0;
|
|
330
|
+
const page = makePage(async () => {
|
|
331
|
+
if (++callCount === 1) {
|
|
332
|
+
throw new Error('Execution context was destroyed, most likely because of a navigation.');
|
|
333
|
+
}
|
|
334
|
+
return JSON.stringify(report);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
const result = await evaluateCapture(page);
|
|
338
|
+
// callCount === 2
|
|
339
|
+
expect(result.url).toBe(report.url);
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### Stability polling (from `capture-utils.test.ts`)
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
// Tree grows across captures then stabilises at 5 nodes:
|
|
346
|
+
const reports = [
|
|
347
|
+
makeReport(makeNode(1)), // 1 node
|
|
348
|
+
makeReport(makeNode(1, [makeNode(2), makeNode(3)])), // 3 nodes
|
|
349
|
+
makeReport(makeNode(1, [makeNode(2, [makeNode(4)]),
|
|
350
|
+
makeNode(3, [makeNode(5)])])), // 5 nodes
|
|
351
|
+
// ... same 5-node tree repeats → stable
|
|
352
|
+
];
|
|
353
|
+
|
|
354
|
+
const result = await captureUntilStable(page, 500, 10000);
|
|
355
|
+
// countNodes(result.tree) === 5
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### Timeout: returns last capture without throwing (from `capture-utils.test.ts`)
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
// stableMs > timeoutMs: forced timeout
|
|
362
|
+
const resultPromise = captureUntilStable(page, 5000, 500);
|
|
363
|
+
await vi.advanceTimersByTimeAsync(2000);
|
|
364
|
+
// resolves (not rejects) with a PageReport
|
|
365
|
+
await expect(resultPromise).resolves.toBeDefined();
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
## Configuration examples
|
|
371
|
+
|
|
372
|
+
### Standard Playwright config
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
// playwright.config.ts
|
|
376
|
+
import { defineConfig } from '@playwright/test';
|
|
377
|
+
|
|
378
|
+
export default defineConfig({
|
|
379
|
+
use: { baseURL: 'http://localhost:5173' },
|
|
380
|
+
webServer: { command: 'bun run dev', port: 5173 },
|
|
381
|
+
});
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
// tests/scope.spec.ts
|
|
386
|
+
import { test, expect } from '@agent-scope/playwright';
|
|
387
|
+
|
|
388
|
+
test('component tree is stable after data load', async ({ scope, page }) => {
|
|
389
|
+
await page.goto('/dashboard');
|
|
390
|
+
const report = await scope.capture({ waitForStable: true, stableMs: 1000 });
|
|
391
|
+
expect(report.tree.name).toBe('App');
|
|
392
|
+
});
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### Manual injection (no fixture)
|
|
396
|
+
|
|
397
|
+
```typescript
|
|
398
|
+
import { getBrowserEntryScript } from '@agent-scope/playwright';
|
|
399
|
+
import { chromium } from 'playwright';
|
|
400
|
+
|
|
401
|
+
const browser = await chromium.launch();
|
|
402
|
+
const page = await browser.newPage();
|
|
403
|
+
await page.addInitScript({ content: getBrowserEntryScript() });
|
|
404
|
+
await page.goto('http://localhost:5173');
|
|
405
|
+
|
|
406
|
+
const json = await page.evaluate(() => window.__SCOPE_CAPTURE_JSON__());
|
|
407
|
+
const report = JSON.parse(json);
|
|
408
|
+
await browser.close();
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
---
|
|
412
|
+
|
|
413
|
+
## Used by
|
|
414
|
+
|
|
415
|
+
- `@agent-scope/cli` — uses `getBrowserEntryScript()` to inject the browser bundle into Vite dev-server pages during `scope capture` runs
|
|
416
|
+
- CI E2E test suites — `test` and `expect` replace the base Playwright exports for all Scope integration tests
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-scope/playwright",
|
|
3
|
-
"version": "1.17.
|
|
3
|
+
"version": "1.17.3",
|
|
4
4
|
"description": "Playwright integration for Scope — replay traces and generate tests",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"module": "./dist/index.js",
|
|
21
21
|
"types": "./dist/index.d.ts",
|
|
22
22
|
"files": [
|
|
23
|
-
"dist"
|
|
23
|
+
"dist",
|
|
24
|
+
"README.md"
|
|
24
25
|
],
|
|
25
26
|
"scripts": {
|
|
26
27
|
"build": "tsup",
|
|
@@ -31,8 +32,8 @@
|
|
|
31
32
|
},
|
|
32
33
|
"dependencies": {
|
|
33
34
|
"@playwright/test": "^1.58.2",
|
|
34
|
-
"@agent-scope/core": "1.17.
|
|
35
|
-
"@agent-scope/runtime": "1.17.
|
|
35
|
+
"@agent-scope/core": "1.17.3",
|
|
36
|
+
"@agent-scope/runtime": "1.17.3"
|
|
36
37
|
},
|
|
37
38
|
"devDependencies": {
|
|
38
39
|
"@types/node": "*",
|