guardrail-ship 1.0.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.
Files changed (119) hide show
  1. package/dist/index.d.ts +7 -0
  2. package/dist/index.d.ts.map +1 -0
  3. package/dist/index.js +7 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/mock-implementation.d.ts +1 -0
  6. package/dist/mock-implementation.d.ts.map +1 -0
  7. package/dist/mock-implementation.js +2 -0
  8. package/dist/mock-implementation.js.map +1 -0
  9. package/dist/mockproof/__tests__/import-graph-scanner.test.d.ts +5 -0
  10. package/dist/mockproof/__tests__/import-graph-scanner.test.d.ts.map +1 -0
  11. package/dist/mockproof/__tests__/import-graph-scanner.test.js +92 -0
  12. package/dist/mockproof/__tests__/import-graph-scanner.test.js.map +1 -0
  13. package/dist/mockproof/import-graph-scanner.d.ts +93 -0
  14. package/dist/mockproof/import-graph-scanner.d.ts.map +1 -0
  15. package/dist/mockproof/import-graph-scanner.js +411 -0
  16. package/dist/mockproof/import-graph-scanner.js.map +1 -0
  17. package/dist/mockproof/index.d.ts +10 -0
  18. package/dist/mockproof/index.d.ts.map +1 -0
  19. package/dist/mockproof/index.js +10 -0
  20. package/dist/mockproof/index.js.map +1 -0
  21. package/dist/reality-mode/auth-enforcer.d.ts +13 -0
  22. package/dist/reality-mode/auth-enforcer.d.ts.map +1 -0
  23. package/dist/reality-mode/auth-enforcer.js +90 -0
  24. package/dist/reality-mode/auth-enforcer.js.map +1 -0
  25. package/dist/reality-mode/explorer/critical-flows.d.ts +71 -0
  26. package/dist/reality-mode/explorer/critical-flows.d.ts.map +1 -0
  27. package/dist/reality-mode/explorer/critical-flows.js +463 -0
  28. package/dist/reality-mode/explorer/critical-flows.js.map +1 -0
  29. package/dist/reality-mode/explorer/flow-parser.d.ts +52 -0
  30. package/dist/reality-mode/explorer/flow-parser.d.ts.map +1 -0
  31. package/dist/reality-mode/explorer/flow-parser.js +250 -0
  32. package/dist/reality-mode/explorer/flow-parser.js.map +1 -0
  33. package/dist/reality-mode/explorer/index.d.ts +11 -0
  34. package/dist/reality-mode/explorer/index.d.ts.map +1 -0
  35. package/dist/reality-mode/explorer/index.js +11 -0
  36. package/dist/reality-mode/explorer/index.js.map +1 -0
  37. package/dist/reality-mode/explorer/runtime-explorer.d.ts +35 -0
  38. package/dist/reality-mode/explorer/runtime-explorer.d.ts.map +1 -0
  39. package/dist/reality-mode/explorer/runtime-explorer.js +688 -0
  40. package/dist/reality-mode/explorer/runtime-explorer.js.map +1 -0
  41. package/dist/reality-mode/explorer/surface-discovery.d.ts +60 -0
  42. package/dist/reality-mode/explorer/surface-discovery.d.ts.map +1 -0
  43. package/dist/reality-mode/explorer/surface-discovery.js +357 -0
  44. package/dist/reality-mode/explorer/surface-discovery.js.map +1 -0
  45. package/dist/reality-mode/explorer/types.d.ts +275 -0
  46. package/dist/reality-mode/explorer/types.d.ts.map +1 -0
  47. package/dist/reality-mode/explorer/types.js +8 -0
  48. package/dist/reality-mode/explorer/types.js.map +1 -0
  49. package/dist/reality-mode/fake-success-detector.d.ts +10 -0
  50. package/dist/reality-mode/fake-success-detector.d.ts.map +1 -0
  51. package/dist/reality-mode/fake-success-detector.js +76 -0
  52. package/dist/reality-mode/fake-success-detector.js.map +1 -0
  53. package/dist/reality-mode/index.d.ts +14 -0
  54. package/dist/reality-mode/index.d.ts.map +1 -0
  55. package/dist/reality-mode/index.js +14 -0
  56. package/dist/reality-mode/index.js.map +1 -0
  57. package/dist/reality-mode/reality-scanner.d.ts +48 -0
  58. package/dist/reality-mode/reality-scanner.d.ts.map +1 -0
  59. package/dist/reality-mode/reality-scanner.js +516 -0
  60. package/dist/reality-mode/reality-scanner.js.map +1 -0
  61. package/dist/reality-mode/report-generator.d.ts +11 -0
  62. package/dist/reality-mode/report-generator.d.ts.map +1 -0
  63. package/dist/reality-mode/report-generator.js +233 -0
  64. package/dist/reality-mode/report-generator.js.map +1 -0
  65. package/dist/reality-mode/traffic-classifier.d.ts +14 -0
  66. package/dist/reality-mode/traffic-classifier.d.ts.map +1 -0
  67. package/dist/reality-mode/traffic-classifier.js +131 -0
  68. package/dist/reality-mode/traffic-classifier.js.map +1 -0
  69. package/dist/reality-mode/types.d.ts +90 -0
  70. package/dist/reality-mode/types.d.ts.map +1 -0
  71. package/dist/reality-mode/types.js +2 -0
  72. package/dist/reality-mode/types.js.map +1 -0
  73. package/dist/ship-badge/__tests__/ship-badge-generator.test.d.ts +5 -0
  74. package/dist/ship-badge/__tests__/ship-badge-generator.test.d.ts.map +1 -0
  75. package/dist/ship-badge/__tests__/ship-badge-generator.test.js +146 -0
  76. package/dist/ship-badge/__tests__/ship-badge-generator.test.js.map +1 -0
  77. package/dist/ship-badge/index.d.ts +9 -0
  78. package/dist/ship-badge/index.d.ts.map +1 -0
  79. package/dist/ship-badge/index.js +9 -0
  80. package/dist/ship-badge/index.js.map +1 -0
  81. package/dist/ship-badge/ship-badge-generator.d.ts +136 -0
  82. package/dist/ship-badge/ship-badge-generator.d.ts.map +1 -0
  83. package/dist/ship-badge/ship-badge-generator.js +681 -0
  84. package/dist/ship-badge/ship-badge-generator.js.map +1 -0
  85. package/package.json +20 -0
  86. package/src/index.ts +7 -0
  87. package/src/mock-implementation.ts +0 -0
  88. package/src/mockproof/__tests__/import-graph-scanner.test.ts +115 -0
  89. package/src/mockproof/import-graph-scanner.d.ts +93 -0
  90. package/src/mockproof/import-graph-scanner.d.ts.map +1 -0
  91. package/src/mockproof/import-graph-scanner.js +482 -0
  92. package/src/mockproof/import-graph-scanner.ts +540 -0
  93. package/src/mockproof/index.ts +18 -0
  94. package/src/reality-mode/auth-enforcer.ts +97 -0
  95. package/src/reality-mode/explorer/critical-flows.ts +504 -0
  96. package/src/reality-mode/explorer/flow-parser.ts +293 -0
  97. package/src/reality-mode/explorer/index.ts +22 -0
  98. package/src/reality-mode/explorer/runtime-explorer.ts +715 -0
  99. package/src/reality-mode/explorer/surface-discovery.ts +498 -0
  100. package/src/reality-mode/explorer/templates/example-flows/auth-flow.yaml +41 -0
  101. package/src/reality-mode/explorer/templates/example-flows/checkout-flow.yaml +66 -0
  102. package/src/reality-mode/explorer/templates/example-flows/contact-form.yaml +43 -0
  103. package/src/reality-mode/explorer/templates/github-action.yml +132 -0
  104. package/src/reality-mode/explorer/types.ts +356 -0
  105. package/src/reality-mode/fake-success-detector.ts +89 -0
  106. package/src/reality-mode/index.ts +19 -0
  107. package/src/reality-mode/reality-scanner.d.ts +123 -0
  108. package/src/reality-mode/reality-scanner.d.ts.map +1 -0
  109. package/src/reality-mode/reality-scanner.js +526 -0
  110. package/src/reality-mode/reality-scanner.ts +576 -0
  111. package/src/reality-mode/report-generator.ts +253 -0
  112. package/src/reality-mode/traffic-classifier.ts +169 -0
  113. package/src/reality-mode/types.ts +95 -0
  114. package/src/ship-badge/__tests__/ship-badge-generator.test.ts +162 -0
  115. package/src/ship-badge/index.ts +16 -0
  116. package/src/ship-badge/ship-badge-generator.d.ts +136 -0
  117. package/src/ship-badge/ship-badge-generator.d.ts.map +1 -0
  118. package/src/ship-badge/ship-badge-generator.js +779 -0
  119. package/src/ship-badge/ship-badge-generator.ts +873 -0
@@ -0,0 +1,498 @@
1
+ /**
2
+ * App Surface Discovery
3
+ *
4
+ * Discovers all testable elements in the app:
5
+ * - Routes (from links, router config, redirects)
6
+ * - Interactive elements (buttons, links, tabs, accordions)
7
+ * - Forms (with their fields and validation)
8
+ * - API endpoints (from network interception)
9
+ */
10
+
11
+ import type { Page } from "@playwright/test";
12
+
13
+ // Type for element info extracted from browser context
14
+ interface ElementInfo {
15
+ tag: string;
16
+ text: string;
17
+ id: string;
18
+ className?: string;
19
+ type?: string;
20
+ ariaLabel: string;
21
+ dataTestId?: string;
22
+ disabled?: boolean;
23
+ idx: number;
24
+ }
25
+
26
+ interface LinkInfo {
27
+ href: string;
28
+ text?: string;
29
+ }
30
+
31
+ interface FormInfo {
32
+ id: string;
33
+ action: string;
34
+ method: string;
35
+ fields: Array<{
36
+ name: string;
37
+ type: string;
38
+ required: boolean;
39
+ placeholder: string;
40
+ pattern: string;
41
+ selector: string;
42
+ }>;
43
+ submitSelector?: string;
44
+ idx: number;
45
+ }
46
+ import type {
47
+ AppSurface,
48
+ DiscoveredRoute,
49
+ DiscoveredElement,
50
+ DiscoveredForm,
51
+ DiscoveredAPI,
52
+ FormField,
53
+ } from "./types";
54
+
55
+ // Patterns that indicate destructive actions
56
+ const DESTRUCTIVE_PATTERNS = [
57
+ /delete/i,
58
+ /remove/i,
59
+ /destroy/i,
60
+ /cancel.*subscription/i,
61
+ /deactivate/i,
62
+ /terminate/i,
63
+ /close.*account/i,
64
+ /reset.*all/i,
65
+ ];
66
+
67
+ // Patterns that indicate auth-required routes
68
+ const AUTH_ROUTE_PATTERNS = [
69
+ /\/admin/i,
70
+ /\/dashboard/i,
71
+ /\/settings/i,
72
+ /\/profile/i,
73
+ /\/account/i,
74
+ /\/billing/i,
75
+ /\/api\/.*private/i,
76
+ ];
77
+
78
+ export class SurfaceDiscovery {
79
+ private surface: AppSurface = {
80
+ routes: [],
81
+ elements: [],
82
+ forms: [],
83
+ apis: [],
84
+ timestamp: new Date().toISOString(),
85
+ };
86
+
87
+ private visitedUrls = new Set<string>();
88
+ private discoveredSelectors = new Set<string>();
89
+ private baseUrl: string;
90
+
91
+ constructor(baseUrl: string) {
92
+ this.baseUrl = baseUrl;
93
+ }
94
+
95
+ /**
96
+ * Main discovery entry point - crawls the page and discovers everything
97
+ */
98
+ async discoverPage(page: Page): Promise<AppSurface> {
99
+ const currentUrl = page.url();
100
+
101
+ // Discover routes from links
102
+ await this.discoverRoutes(page);
103
+
104
+ // Discover interactive elements
105
+ await this.discoverElements(page, currentUrl);
106
+
107
+ // Discover forms
108
+ await this.discoverForms(page, currentUrl);
109
+
110
+ // API discovery happens via network interception (setup separately)
111
+
112
+ this.surface.timestamp = new Date().toISOString();
113
+ return this.surface;
114
+ }
115
+
116
+ /**
117
+ * Discover all navigable routes from the current page
118
+ */
119
+ private async discoverRoutes(page: Page): Promise<void> {
120
+ // Get all links
121
+ const links: LinkInfo[] = await page.$$eval("a[href]", (anchors) =>
122
+ anchors.map((a) => ({
123
+ href: a.getAttribute("href") || "",
124
+ text: a.textContent?.trim() || "",
125
+ })),
126
+ );
127
+
128
+ for (const link of links) {
129
+ const href = link.href;
130
+
131
+ // Skip external links, anchors, javascript
132
+ if (
133
+ !href ||
134
+ href.startsWith("#") ||
135
+ href.startsWith("javascript:") ||
136
+ href.startsWith("mailto:") ||
137
+ href.startsWith("tel:")
138
+ ) {
139
+ continue;
140
+ }
141
+
142
+ // Normalize URL
143
+ let fullUrl: string;
144
+ try {
145
+ fullUrl = new URL(href, this.baseUrl).pathname;
146
+ } catch {
147
+ continue;
148
+ }
149
+
150
+ // Check if already discovered
151
+ if (this.visitedUrls.has(fullUrl)) continue;
152
+ this.visitedUrls.add(fullUrl);
153
+
154
+ const route: DiscoveredRoute = {
155
+ path: fullUrl,
156
+ method: "GET",
157
+ source: "link",
158
+ requiresAuth: AUTH_ROUTE_PATTERNS.some((p) => p.test(fullUrl)),
159
+ visited: false,
160
+ };
161
+
162
+ this.surface.routes.push(route);
163
+ }
164
+
165
+ // Also check for Next.js/React Router links
166
+ const routerLinks: LinkInfo[] = await page.$$eval(
167
+ '[data-href], [href^="/"]',
168
+ (elements) =>
169
+ elements.map((el) => ({
170
+ href: el.getAttribute("data-href") || el.getAttribute("href") || "",
171
+ })),
172
+ );
173
+
174
+ for (const link of routerLinks) {
175
+ const path = link.href;
176
+ if (!path || this.visitedUrls.has(path)) continue;
177
+
178
+ this.visitedUrls.add(path);
179
+ this.surface.routes.push({
180
+ path,
181
+ method: "GET",
182
+ source: "router",
183
+ requiresAuth: AUTH_ROUTE_PATTERNS.some((p) => p.test(path)),
184
+ visited: false,
185
+ });
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Discover all interactive elements on the page
191
+ */
192
+ private async discoverElements(
193
+ page: Page,
194
+ currentPage: string,
195
+ ): Promise<void> {
196
+ // Buttons
197
+ const buttons: ElementInfo[] = await page.$$eval(
198
+ 'button, [role="button"], input[type="submit"], input[type="button"]',
199
+ (elements) =>
200
+ elements.map((el, idx) => ({
201
+ tag: el.tagName.toLowerCase(),
202
+ text: el.textContent?.trim() || el.getAttribute("value") || "",
203
+ id: el.id || "",
204
+ className: (el.className as string) || "",
205
+ type: el.getAttribute("type") || "",
206
+ ariaLabel: el.getAttribute("aria-label") || "",
207
+ dataTestId: el.getAttribute("data-testid") || "",
208
+ disabled: (el as HTMLButtonElement).disabled,
209
+ idx,
210
+ })),
211
+ );
212
+
213
+ for (const btn of buttons) {
214
+ if (btn.disabled) continue;
215
+
216
+ const selector = this.buildSelector(btn);
217
+ if (this.discoveredSelectors.has(selector)) continue;
218
+ this.discoveredSelectors.add(selector);
219
+
220
+ const text =
221
+ btn.text || btn.ariaLabel || btn.dataTestId || `Button ${btn.idx}`;
222
+ const isDestructive = DESTRUCTIVE_PATTERNS.some((p) => p.test(text));
223
+
224
+ this.surface.elements.push({
225
+ id: `btn-${this.surface.elements.length}`,
226
+ selector,
227
+ type: "button",
228
+ text,
229
+ page: currentPage,
230
+ isDestructive,
231
+ tested: false,
232
+ });
233
+ }
234
+
235
+ // Modal triggers
236
+ const modalTriggers: ElementInfo[] = await page.$$eval(
237
+ '[data-toggle="modal"], [aria-haspopup="dialog"], [aria-controls*="modal"]',
238
+ (elements) =>
239
+ elements.map((el, idx) => ({
240
+ tag: el.tagName.toLowerCase(),
241
+ text: el.textContent?.trim() || "",
242
+ id: el.id || "",
243
+ ariaLabel: el.getAttribute("aria-label") || "",
244
+ dataTestId: el.getAttribute("data-testid") || "",
245
+ idx,
246
+ })),
247
+ );
248
+
249
+ for (const trigger of modalTriggers) {
250
+ const selector = this.buildSelector(trigger);
251
+ if (this.discoveredSelectors.has(selector)) continue;
252
+ this.discoveredSelectors.add(selector);
253
+
254
+ this.surface.elements.push({
255
+ id: `modal-${this.surface.elements.length}`,
256
+ selector,
257
+ type: "modal-trigger",
258
+ text: trigger.text || trigger.ariaLabel || "Modal trigger",
259
+ page: currentPage,
260
+ isDestructive: false,
261
+ tested: false,
262
+ });
263
+ }
264
+
265
+ // Dropdowns
266
+ const dropdowns: ElementInfo[] = await page.$$eval(
267
+ '[role="combobox"], [aria-haspopup="listbox"], select, [data-dropdown]',
268
+ (elements) =>
269
+ elements.map((el, idx) => ({
270
+ tag: el.tagName.toLowerCase(),
271
+ text: el.textContent?.trim().slice(0, 50) || "",
272
+ id: el.id || "",
273
+ ariaLabel: el.getAttribute("aria-label") || "",
274
+ idx,
275
+ })),
276
+ );
277
+
278
+ for (const dropdown of dropdowns) {
279
+ const selector = this.buildSelector(dropdown);
280
+ if (this.discoveredSelectors.has(selector)) continue;
281
+ this.discoveredSelectors.add(selector);
282
+
283
+ this.surface.elements.push({
284
+ id: `dropdown-${this.surface.elements.length}`,
285
+ selector,
286
+ type: "dropdown",
287
+ text: dropdown.ariaLabel || dropdown.text || "Dropdown",
288
+ page: currentPage,
289
+ isDestructive: false,
290
+ tested: false,
291
+ });
292
+ }
293
+
294
+ // Tabs
295
+ const tabs: ElementInfo[] = await page.$$eval(
296
+ '[role="tab"], [data-tab], .tab',
297
+ (elements) =>
298
+ elements.map((el, idx) => ({
299
+ tag: el.tagName.toLowerCase(),
300
+ text: el.textContent?.trim() || "",
301
+ id: el.id || "",
302
+ ariaLabel: el.getAttribute("aria-label") || "",
303
+ idx,
304
+ })),
305
+ );
306
+
307
+ for (const tab of tabs) {
308
+ const selector = this.buildSelector(tab);
309
+ if (this.discoveredSelectors.has(selector)) continue;
310
+ this.discoveredSelectors.add(selector);
311
+
312
+ this.surface.elements.push({
313
+ id: `tab-${this.surface.elements.length}`,
314
+ selector,
315
+ type: "tab",
316
+ text: tab.text || tab.ariaLabel || "Tab",
317
+ page: currentPage,
318
+ isDestructive: false,
319
+ tested: false,
320
+ });
321
+ }
322
+
323
+ // Accordions
324
+ const accordions: ElementInfo[] = await page.$$eval(
325
+ "[data-accordion], [aria-expanded], details > summary",
326
+ (elements) =>
327
+ elements.map((el, idx) => ({
328
+ tag: el.tagName.toLowerCase(),
329
+ text: el.textContent?.trim().slice(0, 50) || "",
330
+ id: el.id || "",
331
+ ariaLabel: el.getAttribute("aria-label") || "",
332
+ idx,
333
+ })),
334
+ );
335
+
336
+ for (const accordion of accordions) {
337
+ const selector = this.buildSelector(accordion);
338
+ if (this.discoveredSelectors.has(selector)) continue;
339
+ this.discoveredSelectors.add(selector);
340
+
341
+ this.surface.elements.push({
342
+ id: `accordion-${this.surface.elements.length}`,
343
+ selector,
344
+ type: "accordion",
345
+ text: accordion.text || "Accordion",
346
+ page: currentPage,
347
+ isDestructive: false,
348
+ tested: false,
349
+ });
350
+ }
351
+ }
352
+
353
+ /**
354
+ * Discover all forms on the page
355
+ */
356
+ private async discoverForms(page: Page, currentPage: string): Promise<void> {
357
+ const forms: FormInfo[] = await page.$$eval("form", (formElements) =>
358
+ formElements.map((form, idx) => {
359
+ const fields: Array<{
360
+ name: string;
361
+ type: string;
362
+ required: boolean;
363
+ placeholder: string;
364
+ pattern: string;
365
+ selector: string;
366
+ }> = [];
367
+
368
+ // Get all input fields
369
+ form
370
+ .querySelectorAll("input, textarea, select")
371
+ .forEach((field, fieldIdx) => {
372
+ const input = field as HTMLInputElement;
373
+ fields.push({
374
+ name: input.name || input.id || `field-${fieldIdx}`,
375
+ type: input.type || field.tagName.toLowerCase(),
376
+ required: input.required,
377
+ placeholder: input.placeholder || "",
378
+ pattern: input.pattern || "",
379
+ selector: input.id ? `#${input.id}` : `[name="${input.name}"]`,
380
+ });
381
+ });
382
+
383
+ // Find submit button
384
+ const submitBtn = form.querySelector(
385
+ 'button[type="submit"], input[type="submit"]',
386
+ );
387
+
388
+ return {
389
+ id: form.id || `form-${idx}`,
390
+ action: form.action || "",
391
+ method: form.method?.toUpperCase() || "POST",
392
+ fields,
393
+ submitSelector: submitBtn
394
+ ? submitBtn.id
395
+ ? `#${submitBtn.id}`
396
+ : 'button[type="submit"]'
397
+ : undefined,
398
+ idx,
399
+ };
400
+ }),
401
+ );
402
+
403
+ for (const formData of forms) {
404
+ const selector = formData.id
405
+ ? `#${formData.id}`
406
+ : `form:nth-of-type(${formData.idx + 1})`;
407
+
408
+ const form: DiscoveredForm = {
409
+ id: `form-${this.surface.forms.length}`,
410
+ selector,
411
+ page: currentPage,
412
+ action: formData.action,
413
+ method: formData.method,
414
+ fields: formData.fields.map(
415
+ (f): FormField => ({
416
+ name: f.name,
417
+ type: f.type,
418
+ required: f.required,
419
+ selector: f.selector,
420
+ placeholder: f.placeholder,
421
+ pattern: f.pattern,
422
+ }),
423
+ ),
424
+ submitButton: formData.submitSelector,
425
+ tested: false,
426
+ };
427
+
428
+ this.surface.forms.push(form);
429
+ }
430
+ }
431
+
432
+ /**
433
+ * Add an API call discovered via network interception
434
+ */
435
+ addDiscoveredAPI(api: DiscoveredAPI): void {
436
+ // Avoid duplicates
437
+ const exists = this.surface.apis.some(
438
+ (a) => a.url === api.url && a.method === api.method,
439
+ );
440
+ if (!exists) {
441
+ this.surface.apis.push(api);
442
+ }
443
+ }
444
+
445
+ /**
446
+ * Add a route discovered via redirect or API call
447
+ */
448
+ addDiscoveredRoute(route: DiscoveredRoute): void {
449
+ if (!this.visitedUrls.has(route.path)) {
450
+ this.visitedUrls.add(route.path);
451
+ this.surface.routes.push(route);
452
+ }
453
+ }
454
+
455
+ /**
456
+ * Build a stable selector for an element
457
+ */
458
+ private buildSelector(el: {
459
+ id?: string;
460
+ dataTestId?: string;
461
+ ariaLabel?: string;
462
+ className?: string;
463
+ tag?: string;
464
+ idx?: number;
465
+ }): string {
466
+ // Prefer stable selectors
467
+ if (el.dataTestId) return `[data-testid="${el.dataTestId}"]`;
468
+ if (el.id) return `#${el.id}`;
469
+ if (el.ariaLabel) return `[aria-label="${el.ariaLabel}"]`;
470
+
471
+ // Fallback to nth-of-type
472
+ return `${el.tag || "button"}:nth-of-type(${(el.idx || 0) + 1})`;
473
+ }
474
+
475
+ /**
476
+ * Get the current surface
477
+ */
478
+ getSurface(): AppSurface {
479
+ return this.surface;
480
+ }
481
+
482
+ /**
483
+ * Get discovery stats
484
+ */
485
+ getStats(): {
486
+ routes: number;
487
+ elements: number;
488
+ forms: number;
489
+ apis: number;
490
+ } {
491
+ return {
492
+ routes: this.surface.routes.length,
493
+ elements: this.surface.elements.length,
494
+ forms: this.surface.forms.length,
495
+ apis: this.surface.apis.length,
496
+ };
497
+ }
498
+ }
@@ -0,0 +1,41 @@
1
+ # Authentication Flow - Login/Signup Testing
2
+ # Copy to .guardrail/flows/auth-flow.yaml and customize
3
+
4
+ id: auth-login
5
+ name: User Login
6
+ description: Tests the standard email/password login flow
7
+ required: true
8
+
9
+ steps:
10
+ - action: navigate
11
+ target: /login
12
+
13
+ - action: wait
14
+ timeout: 2000
15
+
16
+ - action: fill
17
+ target: input[name="email"], input[type="email"], #email
18
+ value: "{{email}}"
19
+
20
+ - action: fill
21
+ target: input[name="password"], input[type="password"], #password
22
+ value: "{{password}}"
23
+
24
+ - action: click
25
+ target: button[type="submit"], button:has-text("Log in"), button:has-text("Sign in")
26
+
27
+ - action: wait
28
+ timeout: 5000
29
+
30
+ assertions:
31
+ - type: url-contains
32
+ value: /dashboard|/home|/app
33
+ critical: true
34
+
35
+ - type: no-errors
36
+ value: ""
37
+ critical: true
38
+
39
+ - type: element-hidden
40
+ value: input[type="password"]
41
+ critical: false
@@ -0,0 +1,66 @@
1
+ # E-commerce Checkout Flow
2
+ # Copy to .guardrail/flows/checkout-flow.yaml and customize
3
+
4
+ id: ecommerce-checkout
5
+ name: Checkout Flow
6
+ description: Tests the complete checkout process
7
+ required: false
8
+
9
+ steps:
10
+ - action: navigate
11
+ target: /products
12
+
13
+ - action: wait
14
+ timeout: 2000
15
+
16
+ - action: click
17
+ target: button:has-text("Add to Cart"), [data-testid="add-to-cart"], .add-to-cart
18
+
19
+ - action: wait
20
+ timeout: 1000
21
+
22
+ - action: navigate
23
+ target: /cart
24
+
25
+ - action: wait
26
+ timeout: 2000
27
+
28
+ - action: click
29
+ target: button:has-text("Checkout"), a:has-text("Checkout"), [data-testid="checkout"]
30
+
31
+ - action: wait
32
+ timeout: 3000
33
+
34
+ # Fill shipping info
35
+ - action: fill
36
+ target: input[name="email"], #email
37
+ value: "{{email}}"
38
+
39
+ - action: fill
40
+ target: input[name="name"], input[name="fullName"], #name
41
+ value: "{{name}}"
42
+
43
+ - action: fill
44
+ target: input[name="address"], #address
45
+ value: "123 Test Street"
46
+
47
+ - action: fill
48
+ target: input[name="city"], #city
49
+ value: "Test City"
50
+
51
+ - action: fill
52
+ target: input[name="zip"], input[name="postalCode"], #zip
53
+ value: "12345"
54
+
55
+ assertions:
56
+ - type: url-contains
57
+ value: /checkout|/payment
58
+ critical: true
59
+
60
+ - type: no-errors
61
+ value: ""
62
+ critical: true
63
+
64
+ - type: element-visible
65
+ value: button:has-text("Pay"), button:has-text("Place Order")
66
+ critical: false
@@ -0,0 +1,43 @@
1
+ # Contact Form Flow
2
+ # Copy to .guardrail/flows/contact-form.yaml and customize
3
+
4
+ id: contact-form
5
+ name: Contact Form Submission
6
+ description: Tests the contact form submission process
7
+ required: false
8
+
9
+ steps:
10
+ - action: navigate
11
+ target: /contact
12
+
13
+ - action: wait
14
+ timeout: 2000
15
+
16
+ - action: fill
17
+ target: input[name="name"], #name
18
+ value: "{{name}}"
19
+
20
+ - action: fill
21
+ target: input[name="email"], #email
22
+ value: "{{email}}"
23
+
24
+ - action: fill
25
+ target: input[name="subject"], #subject
26
+ value: "Test inquiry from Reality Mode"
27
+
28
+ - action: fill
29
+ target: textarea[name="message"], #message, textarea
30
+ value: "This is an automated test message from Guardrail Reality Mode. Please disregard."
31
+
32
+ # Don't actually submit in test mode to avoid spam
33
+ # - action: click
34
+ # target: button[type="submit"]
35
+
36
+ assertions:
37
+ - type: no-errors
38
+ value: ""
39
+ critical: true
40
+
41
+ - type: element-visible
42
+ value: button[type="submit"], input[type="submit"]
43
+ critical: true