@saywright/vite 0.0.0-stage → 0.1.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.
@@ -0,0 +1,667 @@
1
+ /**
2
+ * Message protocol between the Saywright studio (parent window) and the
3
+ * bridge running inside the target app (iframe). Both sides check `ns` and the
4
+ * message source before trusting anything.
5
+ */
6
+ export declare const NS: 'saywright';
7
+ /**
8
+ * The plugin in an app and the studio are updated separately, so each side
9
+ * says which version of this protocol it speaks (`v`, in hello and ready) and
10
+ * the oldest of the other side's it still works with (`oldest`). Raise
11
+ * PROTOCOL_VERSION when a message changes in a way an older side can't follow,
12
+ * and OLDEST_PROTOCOL when this side stops understanding an older one; adding
13
+ * a message or an optional field needs neither (unknown ones are ignored).
14
+ */
15
+ export declare const PROTOCOL_VERSION = 1;
16
+ export declare const OLDEST_PROTOCOL = 1;
17
+ /** Which side has to be updated for a studio and an app's bridge to work together: the app's plugin, the studio, or neither. */
18
+ export declare function outdated(studio: {
19
+ v: number;
20
+ oldest?: number;
21
+ }, bridge: {
22
+ v: number;
23
+ oldest?: number;
24
+ }): 'plugin' | 'studio' | null;
25
+ /** Where the studio runs unless told otherwise. Not 5173: that is Vite's own default, where your app likely runs. */
26
+ export declare const STUDIO_PORT = 7357;
27
+ export declare const STUDIO_URL = "http://localhost:7357";
28
+ /** Attribute names injected by the Vite plugin at compile time. */
29
+ export declare const SRC_ATTR = "data-ds-src";
30
+ export declare const NAME_ATTR = "data-ds-name";
31
+ export interface Rect {
32
+ x: number;
33
+ y: number;
34
+ w: number;
35
+ h: number;
36
+ }
37
+ /** What the studio needs to know about an element in the app. Coordinates are iframe viewport CSS px. */
38
+ export interface ElementInfo {
39
+ /** Runtime id, stable while the DOM node stays connected. */
40
+ id: number;
41
+ /** "src/components/CouponField.tsx:18:9" relative to the app root, or null for unannotated nodes. */
42
+ src: string | null;
43
+ /** Component name for component instances ("Button"), otherwise the tag ("div"). */
44
+ name: string;
45
+ tag: string;
46
+ /** Short human label, e.g. "Button · Apply". */
47
+ label: string;
48
+ /** Visible text, whitespace-collapsed, at most 80 characters. */
49
+ text: string;
50
+ role: string | null;
51
+ rect: Rect;
52
+ /** Index of this node among elements sharing the same `src` (lists render one JSX site many times). */
53
+ index: number;
54
+ /** When `src` is null: what can find the element in the code. */
55
+ hint?: SourceHint;
56
+ /** Pages without source attributes: where React's development build says it came from (see SourceFrame). */
57
+ frames?: SourceFrame[];
58
+ }
59
+ /**
60
+ * A place React's development build gives for an element: its own JSX first, then where each component around it
61
+ * was used. The studio's server maps them to source, skipping those in libraries.
62
+ */
63
+ export interface SourceFrame {
64
+ /** As the page reports it: a file, or a module or bundle address and position to map ("http://…/chunk.js:3:120"). */
65
+ at: string;
66
+ /** What is written there: the element's tag for its own JSX, else the component ("Button"). */
67
+ name?: string;
68
+ /** React 19.0 says where a component is, not where its JSX is: `at` is a place inside the component that wrote `name`. */
69
+ inside?: boolean;
70
+ /** The element is this component's outermost element and gets the props set where it is used: an instance site. */
71
+ root?: boolean;
72
+ }
73
+ /** What identifies an element in its code, for pages that don't say where it was written. */
74
+ export interface SourceHint {
75
+ /** Telling attributes as the page has them: id, data-testid, name, aria-label, href, the first classes… */
76
+ attrs: Record<string, string>;
77
+ /** The component file a framework's development build names (Vue), absolute or relative to the app. */
78
+ file?: string;
79
+ }
80
+ export interface LocatorInfo {
81
+ /** Playwright locator expression, e.g. `page.getByTestId('summary-total')`. */
82
+ code: string;
83
+ /** Plain words for the recorder list, e.g. `the "Apply" button`. */
84
+ description: string;
85
+ /** Current visible text of the element. */
86
+ text: string;
87
+ /** The title of the card or row that tells it apart from others like it ("Celadon mug, 12 oz"). */
88
+ within?: string;
89
+ }
90
+ export type RecordedAction = {
91
+ kind: 'click';
92
+ locator: string;
93
+ description: string;
94
+ } | {
95
+ kind: 'fill';
96
+ locator: string;
97
+ value: string;
98
+ description: string;
99
+ } | {
100
+ kind: 'press';
101
+ locator: string;
102
+ key: string;
103
+ description: string;
104
+ };
105
+ /** An element as a test session remembers it. Cheap fields are read at once; `locator`, `description` and `path` when the browser is idle. */
106
+ export interface SessionTarget {
107
+ /** "src/components/CouponField.tsx:18:9", from the Vite plugin or React's dev info; null when unknown. */
108
+ src: string | null;
109
+ /** Component name ("Button") or tag ("div"). */
110
+ name: string;
111
+ tag: string;
112
+ role: string | null;
113
+ /** What a person would call it: its label, placeholder or text (≤ 60 characters). */
114
+ label: string;
115
+ /** Visible text (≤ 80 characters). */
116
+ text: string;
117
+ testId?: string;
118
+ /** Playwright locator, e.g. page.getByRole('button', { name: 'Apply' }). */
119
+ locator?: string;
120
+ /** Plain words, e.g. the “Apply” button. */
121
+ description?: string;
122
+ /** The title of the card or row that tells it apart from others like it ("Celadon mug, 12 oz"). */
123
+ within?: string;
124
+ /** When the locator goes by the element's place in the code (sessions recorded before locators stopped doing so): what it is made of (see shapeOf), to find it again once that place moves. */
125
+ shape?: string;
126
+ /** React components from the page down to this element, e.g. "Checkout › CouponField › Button". */
127
+ path?: string;
128
+ /** Viewport box: x, y, width, height (CSS px). */
129
+ rect: [number, number, number, number];
130
+ /** A link inside running text (exempt from minimum target sizes). */
131
+ inline?: boolean;
132
+ /** When `src` is null: what can find it in the code. */
133
+ hint?: SourceHint;
134
+ }
135
+ /**
136
+ * Everything a test session records about the app, one event at a time. `t` is
137
+ * wall-clock time in ms (Date.now() scale) so events from the app, the studio
138
+ * and the microphone line up; `seq` counts events within one page load (`page`).
139
+ */
140
+ export type SessionEventBody = {
141
+ type: 'page';
142
+ url: string;
143
+ title: string;
144
+ referrer: string;
145
+ viewport: [number, number, number];
146
+ ua: string;
147
+ lang: string;
148
+ reload: boolean;
149
+ timing?: {
150
+ ttfb: number;
151
+ dcl: number;
152
+ load: number;
153
+ };
154
+ }
155
+ /** `disabled`: the press landed on a disabled control, which ignores it. */
156
+ | {
157
+ type: 'click';
158
+ x: number;
159
+ y: number;
160
+ button: number;
161
+ pointer: string;
162
+ el: SessionTarget;
163
+ count: number;
164
+ disabled?: boolean;
165
+ } | {
166
+ type: 'input';
167
+ el: SessionTarget;
168
+ value: string;
169
+ masked: boolean;
170
+ } | {
171
+ type: 'change';
172
+ el: SessionTarget;
173
+ value: string;
174
+ checked?: boolean;
175
+ } | {
176
+ type: 'submit';
177
+ el: SessionTarget;
178
+ } | {
179
+ type: 'key';
180
+ key: string;
181
+ mods: string;
182
+ el: SessionTarget | null;
183
+ } | {
184
+ type: 'focus';
185
+ el: SessionTarget;
186
+ } | {
187
+ type: 'nav';
188
+ url: string;
189
+ title: string;
190
+ how: 'push' | 'replace' | 'pop' | 'hash';
191
+ } | {
192
+ type: 'scroll';
193
+ x: number;
194
+ y: number;
195
+ maxY: number;
196
+ el: SessionTarget | null;
197
+ } | {
198
+ type: 'resize';
199
+ w: number;
200
+ h: number;
201
+ }
202
+ /** The pointer rested on an element for `ms`. */
203
+ | {
204
+ type: 'hover';
205
+ el: SessionTarget;
206
+ ms: number;
207
+ }
208
+ /** Pointer positions sampled ten times a second: [dt ms, x, y, dt, x, y, …] after `t`. */
209
+ | {
210
+ type: 'trail';
211
+ pts: number[];
212
+ }
213
+ /** `repeat`: the event stands for itself and this many more of the same message within a second (a burst after its first one). */
214
+ | {
215
+ type: 'error';
216
+ kind: 'error' | 'rejection' | 'resource';
217
+ message: string;
218
+ stack?: string;
219
+ source?: string;
220
+ repeat?: number;
221
+ } | {
222
+ type: 'console';
223
+ level: 'error' | 'warn';
224
+ text: string;
225
+ stack?: string;
226
+ repeat?: number;
227
+ }
228
+ /**
229
+ * `ctype`: the response's content type (a data request answered with a web page is usually a missing route). `stack`: where the request was made, when it went wrong.
230
+ * `repeat`: the event stands for itself and this many more quick, successful requests to the same address, with the same status, within a
231
+ * second of one recorded on its own; `ms` is their average.
232
+ */
233
+ | {
234
+ type: 'net';
235
+ via: 'fetch' | 'xhr';
236
+ method: string;
237
+ url: string;
238
+ status: number;
239
+ ok: boolean;
240
+ ms: number;
241
+ size?: number;
242
+ error?: string;
243
+ body?: string;
244
+ ctype?: string;
245
+ stack?: string;
246
+ repeat?: number;
247
+ } | {
248
+ type: 'perf';
249
+ metric: 'lcp' | 'cls' | 'inp' | 'longtask' | 'fcp';
250
+ value: number;
251
+ el?: SessionTarget | null;
252
+ detail?: string;
253
+ }
254
+ /** Several fast clicks on the same spot: the user expected something to happen. */
255
+ | {
256
+ type: 'rage';
257
+ x: number;
258
+ y: number;
259
+ clicks: number;
260
+ el: SessionTarget;
261
+ }
262
+ /** A click that changed nothing: no DOM change, navigation, request or focus within a second. */
263
+ | {
264
+ type: 'dead';
265
+ x: number;
266
+ y: number;
267
+ el: SessionTarget;
268
+ } | {
269
+ type: 'vis';
270
+ hidden: boolean;
271
+ }
272
+ /**
273
+ * What the screen said at a key moment (start, reload, navigation, error, mark), and once it settled after an action
274
+ * when it changed (after): the main region's text and the dialogs and messages outside it, line by line.
275
+ */
276
+ | {
277
+ type: 'dom';
278
+ reason: string;
279
+ url: string;
280
+ title: string;
281
+ headings: string[];
282
+ text: string;
283
+ } | {
284
+ type: 'mark';
285
+ note: string;
286
+ via: string;
287
+ }
288
+ /** Events the page couldn't keep while the studio wasn't taking them (it holds a few thousand): how many, by type. */
289
+ | {
290
+ type: 'dropped';
291
+ count: number;
292
+ kinds: Record<string, number>;
293
+ }
294
+ /** Studio only: what the user pointed at with a hand or the mouse, and for how long. */
295
+ | {
296
+ type: 'point';
297
+ el: SessionTarget;
298
+ ms: number;
299
+ via: 'hand' | 'mouse';
300
+ }
301
+ /** Studio only: a voice or typed command, and what came of it. */
302
+ | {
303
+ type: 'command';
304
+ text: string;
305
+ input: 'voice' | 'typed';
306
+ result: string;
307
+ }
308
+ /** Studio only: a step Autopilot took (`did`), and why. */
309
+ | {
310
+ type: 'autopilot';
311
+ did: string;
312
+ why: string;
313
+ }
314
+ /** Studio only: a problem Autopilot saw, with what it expected. */
315
+ | {
316
+ type: 'observed';
317
+ severity: 'high' | 'medium' | 'low';
318
+ title: string;
319
+ expected: string;
320
+ actual: string;
321
+ el: SessionTarget | null;
322
+ };
323
+ export type SessionEvent = SessionEventBody & {
324
+ seq: number;
325
+ t: number;
326
+ };
327
+ /** What the bridge posts to the studio server: events from one page load, in order. */
328
+ export interface SessionBatch {
329
+ page: string;
330
+ events: SessionEvent[];
331
+ }
332
+ interface Base {
333
+ ns: typeof NS;
334
+ }
335
+ export type StudioToBridge = Base & ({
336
+ type: 'hello';
337
+ v: number;
338
+ oldest?: number;
339
+ }
340
+ /** Sent every animation frame while pointing. `track` lists ids whose rects should be reported. */
341
+ | {
342
+ type: 'probe';
343
+ seq: number;
344
+ x: number | null;
345
+ y: number | null;
346
+ radius: number;
347
+ deep: boolean;
348
+ track: number[];
349
+ sticky?: boolean;
350
+ } | {
351
+ type: 'describe';
352
+ reqId: number;
353
+ id: number;
354
+ } | {
355
+ type: 'locator';
356
+ reqId: number;
357
+ id: number;
358
+ }
359
+ /** Move from an element: out, in, to the next or previous one; or to the nearest one called `what` ("card"). `id` 0: from nothing. */
360
+ | {
361
+ type: 'step';
362
+ reqId: number;
363
+ id: number;
364
+ to: 'parent' | 'child' | 'next' | 'prev';
365
+ } | {
366
+ type: 'find';
367
+ reqId: number;
368
+ id: number;
369
+ what: string;
370
+ where: 'any' | 'out' | 'in';
371
+ } | {
372
+ type: 'resolve';
373
+ reqId: number;
374
+ src: string;
375
+ index: number;
376
+ }
377
+ /**
378
+ * Replaying a session: find the element a recorded Playwright locator points at, bring it into view, and do
379
+ * `step` on it (or nothing, just to show where it is). `read`: also answer with what it shows.
380
+ */
381
+ | {
382
+ type: 'replay';
383
+ reqId: number;
384
+ locator: string;
385
+ step?: {
386
+ kind: 'click' | 'fill' | 'press';
387
+ value?: string;
388
+ key?: string;
389
+ checked?: boolean;
390
+ };
391
+ read?: boolean;
392
+ like?: {
393
+ tag: string;
394
+ name: string;
395
+ text: string;
396
+ shape?: string;
397
+ };
398
+ }
399
+ /** Open another page of the app (same origin only), to replay a session from where it started. */
400
+ | {
401
+ type: 'goto';
402
+ url: string;
403
+ } | {
404
+ type: 'click';
405
+ reqId: number;
406
+ x: number;
407
+ y: number;
408
+ } | {
409
+ type: 'type';
410
+ reqId: number;
411
+ text: string;
412
+ submit: boolean;
413
+ } | {
414
+ type: 'scroll';
415
+ x: number;
416
+ y: number;
417
+ dx: number;
418
+ dy: number;
419
+ } | {
420
+ type: 'record';
421
+ on: boolean;
422
+ }
423
+ /** Report the real mouse position while the user interacts with the app directly (Use / Record modes). */
424
+ | {
425
+ type: 'mouse';
426
+ on: boolean;
427
+ }
428
+ /** Forward the studio's talk and cancel keys (Space outside text fields, Escape) while the app has focus. */
429
+ | {
430
+ type: 'keys';
431
+ on: boolean;
432
+ }
433
+ /** Start or stop recording a test session; events go straight to `endpoint` (the studio server). */
434
+ | {
435
+ type: 'session';
436
+ on: boolean;
437
+ id?: string;
438
+ endpoint?: string;
439
+ key?: string;
440
+ } | {
441
+ type: 'mark';
442
+ note: string;
443
+ via: string;
444
+ }
445
+ /** Check the page for the experts asked (SEO, content, UX, accessibility); `axe`: where to load axe-core from. */
446
+ | {
447
+ type: 'review';
448
+ reqId: number;
449
+ experts: ExpertId[];
450
+ axe?: string;
451
+ }
452
+ /** Bring an element into view (by id, or by where it was written when the page has changed) and describe it. */
453
+ | {
454
+ type: 'reveal';
455
+ reqId: number;
456
+ id: number;
457
+ src: string | null;
458
+ index: number;
459
+ }
460
+ /** Autopilot: what the page shows and what can be done on it. */
461
+ | {
462
+ type: 'explore';
463
+ reqId: number;
464
+ });
465
+ export type BridgeToStudio = Base & ({
466
+ type: 'ready';
467
+ v: number;
468
+ oldest?: number;
469
+ url: string;
470
+ title: string;
471
+ } | {
472
+ type: 'probe';
473
+ seq: number;
474
+ hit: ElementInfo | null;
475
+ snapped: boolean;
476
+ tracked: Record<string, Rect | null>;
477
+ } | {
478
+ type: 'reply';
479
+ reqId: number;
480
+ ok: boolean;
481
+ info?: ElementInfo | null;
482
+ locator?: LocatorInfo | null;
483
+ /** What an element shows (replay, with `read`). */
484
+ text?: string;
485
+ /** A page review's results. */
486
+ review?: PageReview;
487
+ /** What Autopilot sees of the page. */
488
+ snapshot?: PageSnapshot;
489
+ error?: string;
490
+ } | {
491
+ type: 'recorded';
492
+ action: RecordedAction;
493
+ }
494
+ /** How far a long request (a page review) has got, from 0 to 1. */
495
+ | {
496
+ type: 'progress';
497
+ reqId: number;
498
+ done: number;
499
+ } | {
500
+ type: 'navigated';
501
+ url: string;
502
+ title: string;
503
+ } | {
504
+ type: 'mouse';
505
+ x: number | null;
506
+ y: number | null;
507
+ } | {
508
+ type: 'key';
509
+ key: 'Space' | 'Escape' | 'Mark';
510
+ down: boolean;
511
+ }
512
+ /** The session recorder's state, so the studio can show it (a session can resume after a reload). */
513
+ | {
514
+ type: 'session';
515
+ id: string | null;
516
+ events: number;
517
+ issues: number;
518
+ }
519
+ /** Something worth a screenshot just happened in the app (navigation, an error, a click…). */
520
+ | {
521
+ type: 'moment';
522
+ reason: string;
523
+ });
524
+ /** What Autopilot sees of a page: where it is, what it says, and what can be done on it. */
525
+ export interface PageSnapshot {
526
+ url: string;
527
+ title: string;
528
+ headings: string[];
529
+ /** The visible text, shortened. */
530
+ text: string;
531
+ /** What can be clicked, filled or chosen: what is on screen first, then the rest in reading order. */
532
+ controls: PageControl[];
533
+ /** How many more there were than are listed. */
534
+ more: number;
535
+ /** An open dialog's name: only what is in it can be used until it closes. */
536
+ dialog: string | null;
537
+ /** The page's visible size, CSS px. */
538
+ viewport: [number, number];
539
+ /** The control that has the keyboard focus (its index in `controls`), where a key press goes. */
540
+ focused?: number | null;
541
+ }
542
+ export interface PageControl {
543
+ /** The Playwright locator that finds it again, as the recorder writes it. */
544
+ locator: string;
545
+ role: string;
546
+ /** What a person would call it: its label or its text. */
547
+ name: string;
548
+ /** A field: its type, what it holds now (never a password's value), whether it must be filled, its choices. */
549
+ field?: {
550
+ type: string;
551
+ value: string;
552
+ required: boolean;
553
+ checked?: boolean;
554
+ options?: string[];
555
+ };
556
+ disabled: boolean;
557
+ /** Outside what the page shows now: reached by scrolling. */
558
+ offscreen: boolean;
559
+ /** Where it was written ("src/components/Cart.tsx:18:9"), when known. */
560
+ src: string | null;
561
+ }
562
+ export type ExpertId = 'seo' | 'content' | 'ux' | 'a11y';
563
+ export type CheckSeverity = 'blocker' | 'high' | 'medium' | 'low' | 'info';
564
+ /** Something a page check found, with the elements it is about. */
565
+ export interface PageCheck {
566
+ /** The rule: "SEO-16", "CNT-06", "UX-01", "axe:color-contrast". */
567
+ rule: string;
568
+ expert: ExpertId;
569
+ severity: CheckSeverity;
570
+ title: string;
571
+ /** What was found, in words, with the numbers. */
572
+ what: string;
573
+ /** How to fix it. */
574
+ fix: string;
575
+ /** Up to six of the elements it is about. */
576
+ els?: ElementInfo[];
577
+ /** Their HTML, shortened. */
578
+ html?: string[];
579
+ /** How many there are in all (the elements are a sample). */
580
+ count?: number;
581
+ /** WCAG success criteria ("1.4.3"). */
582
+ wcag?: string[];
583
+ /** Where the rule comes from. */
584
+ source?: string;
585
+ }
586
+ /** What a page is, for the experts and the AI: its metadata, structure and text. */
587
+ export interface PageFacts {
588
+ url: string;
589
+ title: string;
590
+ description: string | null;
591
+ canonical: string | null;
592
+ robots: string | null;
593
+ lang: string | null;
594
+ dir: string | null;
595
+ viewport: string | null;
596
+ headings: {
597
+ level: number;
598
+ text: string;
599
+ }[];
600
+ og: Record<string, string>;
601
+ jsonLd: string[];
602
+ words: number;
603
+ /** The main text, shortened. */
604
+ text: string;
605
+ links: {
606
+ internal: number;
607
+ external: number;
608
+ };
609
+ images: number;
610
+ forms: {
611
+ label: string;
612
+ type: string;
613
+ autocomplete: string | null;
614
+ required: boolean;
615
+ }[];
616
+ buttons: string[];
617
+ /** Measured in this browser (lab, not field data). */
618
+ lcp: number | null;
619
+ cls: number | null;
620
+ readability: {
621
+ lang: 'en' | 'ar';
622
+ ease: number | null;
623
+ grade: number | null;
624
+ sentence: number;
625
+ } | null;
626
+ /** The elements that matter most for a review: main headings, the first buttons and links in view, form fields. */
627
+ elements: ElementInfo[];
628
+ /** Landmarks as a screen reader lists them (banner, navigation, main…), with their names. */
629
+ landmarks?: {
630
+ role: string;
631
+ name: string;
632
+ }[];
633
+ /** Where changes are announced (role="status", "alert", aria-live…) and what each says now. */
634
+ live?: {
635
+ role: string;
636
+ text: string;
637
+ }[];
638
+ /** Where the links go, as written in the page (a #/route stays one). */
639
+ linkTargets?: {
640
+ text: string;
641
+ href: string;
642
+ }[];
643
+ }
644
+ export interface PageReview {
645
+ facts: PageFacts;
646
+ checks: PageCheck[];
647
+ ms: number;
648
+ }
649
+ export declare function isStudioMessage(data: unknown): data is {
650
+ ns: typeof NS;
651
+ type: string;
652
+ };
653
+ /**
654
+ * Text from a page as a single-quoted JavaScript string, for the locators and
655
+ * tests Saywright writes: quotes, backslashes and every kind of line break are
656
+ * escaped, so no text can end the string or the line it is on.
657
+ */
658
+ export declare function jsString(s: string): string;
659
+ /** Text from a page in a `//` comment: on one line, whatever line breaks it had. */
660
+ export declare function jsComment(s: string): string;
661
+ /** Parse "path/to/File.tsx:12:5" into parts. Paths may contain colons on Windows drive letters, so split from the right. */
662
+ export declare function parseSrc(src: string): {
663
+ file: string;
664
+ line: number;
665
+ col: number;
666
+ } | null;
667
+ export {};