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.
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/mock-implementation.d.ts +1 -0
- package/dist/mock-implementation.d.ts.map +1 -0
- package/dist/mock-implementation.js +2 -0
- package/dist/mock-implementation.js.map +1 -0
- package/dist/mockproof/__tests__/import-graph-scanner.test.d.ts +5 -0
- package/dist/mockproof/__tests__/import-graph-scanner.test.d.ts.map +1 -0
- package/dist/mockproof/__tests__/import-graph-scanner.test.js +92 -0
- package/dist/mockproof/__tests__/import-graph-scanner.test.js.map +1 -0
- package/dist/mockproof/import-graph-scanner.d.ts +93 -0
- package/dist/mockproof/import-graph-scanner.d.ts.map +1 -0
- package/dist/mockproof/import-graph-scanner.js +411 -0
- package/dist/mockproof/import-graph-scanner.js.map +1 -0
- package/dist/mockproof/index.d.ts +10 -0
- package/dist/mockproof/index.d.ts.map +1 -0
- package/dist/mockproof/index.js +10 -0
- package/dist/mockproof/index.js.map +1 -0
- package/dist/reality-mode/auth-enforcer.d.ts +13 -0
- package/dist/reality-mode/auth-enforcer.d.ts.map +1 -0
- package/dist/reality-mode/auth-enforcer.js +90 -0
- package/dist/reality-mode/auth-enforcer.js.map +1 -0
- package/dist/reality-mode/explorer/critical-flows.d.ts +71 -0
- package/dist/reality-mode/explorer/critical-flows.d.ts.map +1 -0
- package/dist/reality-mode/explorer/critical-flows.js +463 -0
- package/dist/reality-mode/explorer/critical-flows.js.map +1 -0
- package/dist/reality-mode/explorer/flow-parser.d.ts +52 -0
- package/dist/reality-mode/explorer/flow-parser.d.ts.map +1 -0
- package/dist/reality-mode/explorer/flow-parser.js +250 -0
- package/dist/reality-mode/explorer/flow-parser.js.map +1 -0
- package/dist/reality-mode/explorer/index.d.ts +11 -0
- package/dist/reality-mode/explorer/index.d.ts.map +1 -0
- package/dist/reality-mode/explorer/index.js +11 -0
- package/dist/reality-mode/explorer/index.js.map +1 -0
- package/dist/reality-mode/explorer/runtime-explorer.d.ts +35 -0
- package/dist/reality-mode/explorer/runtime-explorer.d.ts.map +1 -0
- package/dist/reality-mode/explorer/runtime-explorer.js +688 -0
- package/dist/reality-mode/explorer/runtime-explorer.js.map +1 -0
- package/dist/reality-mode/explorer/surface-discovery.d.ts +60 -0
- package/dist/reality-mode/explorer/surface-discovery.d.ts.map +1 -0
- package/dist/reality-mode/explorer/surface-discovery.js +357 -0
- package/dist/reality-mode/explorer/surface-discovery.js.map +1 -0
- package/dist/reality-mode/explorer/types.d.ts +275 -0
- package/dist/reality-mode/explorer/types.d.ts.map +1 -0
- package/dist/reality-mode/explorer/types.js +8 -0
- package/dist/reality-mode/explorer/types.js.map +1 -0
- package/dist/reality-mode/fake-success-detector.d.ts +10 -0
- package/dist/reality-mode/fake-success-detector.d.ts.map +1 -0
- package/dist/reality-mode/fake-success-detector.js +76 -0
- package/dist/reality-mode/fake-success-detector.js.map +1 -0
- package/dist/reality-mode/index.d.ts +14 -0
- package/dist/reality-mode/index.d.ts.map +1 -0
- package/dist/reality-mode/index.js +14 -0
- package/dist/reality-mode/index.js.map +1 -0
- package/dist/reality-mode/reality-scanner.d.ts +48 -0
- package/dist/reality-mode/reality-scanner.d.ts.map +1 -0
- package/dist/reality-mode/reality-scanner.js +516 -0
- package/dist/reality-mode/reality-scanner.js.map +1 -0
- package/dist/reality-mode/report-generator.d.ts +11 -0
- package/dist/reality-mode/report-generator.d.ts.map +1 -0
- package/dist/reality-mode/report-generator.js +233 -0
- package/dist/reality-mode/report-generator.js.map +1 -0
- package/dist/reality-mode/traffic-classifier.d.ts +14 -0
- package/dist/reality-mode/traffic-classifier.d.ts.map +1 -0
- package/dist/reality-mode/traffic-classifier.js +131 -0
- package/dist/reality-mode/traffic-classifier.js.map +1 -0
- package/dist/reality-mode/types.d.ts +90 -0
- package/dist/reality-mode/types.d.ts.map +1 -0
- package/dist/reality-mode/types.js +2 -0
- package/dist/reality-mode/types.js.map +1 -0
- package/dist/ship-badge/__tests__/ship-badge-generator.test.d.ts +5 -0
- package/dist/ship-badge/__tests__/ship-badge-generator.test.d.ts.map +1 -0
- package/dist/ship-badge/__tests__/ship-badge-generator.test.js +146 -0
- package/dist/ship-badge/__tests__/ship-badge-generator.test.js.map +1 -0
- package/dist/ship-badge/index.d.ts +9 -0
- package/dist/ship-badge/index.d.ts.map +1 -0
- package/dist/ship-badge/index.js +9 -0
- package/dist/ship-badge/index.js.map +1 -0
- package/dist/ship-badge/ship-badge-generator.d.ts +136 -0
- package/dist/ship-badge/ship-badge-generator.d.ts.map +1 -0
- package/dist/ship-badge/ship-badge-generator.js +681 -0
- package/dist/ship-badge/ship-badge-generator.js.map +1 -0
- package/package.json +20 -0
- package/src/index.ts +7 -0
- package/src/mock-implementation.ts +0 -0
- package/src/mockproof/__tests__/import-graph-scanner.test.ts +115 -0
- package/src/mockproof/import-graph-scanner.d.ts +93 -0
- package/src/mockproof/import-graph-scanner.d.ts.map +1 -0
- package/src/mockproof/import-graph-scanner.js +482 -0
- package/src/mockproof/import-graph-scanner.ts +540 -0
- package/src/mockproof/index.ts +18 -0
- package/src/reality-mode/auth-enforcer.ts +97 -0
- package/src/reality-mode/explorer/critical-flows.ts +504 -0
- package/src/reality-mode/explorer/flow-parser.ts +293 -0
- package/src/reality-mode/explorer/index.ts +22 -0
- package/src/reality-mode/explorer/runtime-explorer.ts +715 -0
- package/src/reality-mode/explorer/surface-discovery.ts +498 -0
- package/src/reality-mode/explorer/templates/example-flows/auth-flow.yaml +41 -0
- package/src/reality-mode/explorer/templates/example-flows/checkout-flow.yaml +66 -0
- package/src/reality-mode/explorer/templates/example-flows/contact-form.yaml +43 -0
- package/src/reality-mode/explorer/templates/github-action.yml +132 -0
- package/src/reality-mode/explorer/types.ts +356 -0
- package/src/reality-mode/fake-success-detector.ts +89 -0
- package/src/reality-mode/index.ts +19 -0
- package/src/reality-mode/reality-scanner.d.ts +123 -0
- package/src/reality-mode/reality-scanner.d.ts.map +1 -0
- package/src/reality-mode/reality-scanner.js +526 -0
- package/src/reality-mode/reality-scanner.ts +576 -0
- package/src/reality-mode/report-generator.ts +253 -0
- package/src/reality-mode/traffic-classifier.ts +169 -0
- package/src/reality-mode/types.ts +95 -0
- package/src/ship-badge/__tests__/ship-badge-generator.test.ts +162 -0
- package/src/ship-badge/index.ts +16 -0
- package/src/ship-badge/ship-badge-generator.d.ts +136 -0
- package/src/ship-badge/ship-badge-generator.d.ts.map +1 -0
- package/src/ship-badge/ship-badge-generator.js +779 -0
- package/src/ship-badge/ship-badge-generator.ts +873 -0
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Critical Flows - Pre-built flow templates for common app patterns
|
|
3
|
+
*
|
|
4
|
+
* These are "known good" test sequences that Reality Mode can run
|
|
5
|
+
* against any app that follows standard patterns (login forms, signup, etc.)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { CriticalFlow, FlowStep, FlowAssertion } from "./types";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Standard authentication flow - tries common login patterns
|
|
12
|
+
*/
|
|
13
|
+
export const AUTH_LOGIN_FLOW: CriticalFlow = {
|
|
14
|
+
id: "auth-login",
|
|
15
|
+
name: "Authentication - Login",
|
|
16
|
+
description: "Tests standard email/password login flow",
|
|
17
|
+
steps: [
|
|
18
|
+
{ action: "navigate", target: "/login" },
|
|
19
|
+
{ action: "wait", timeout: 2000 },
|
|
20
|
+
{
|
|
21
|
+
action: "fill",
|
|
22
|
+
target: 'input[name="email"], input[type="email"], #email',
|
|
23
|
+
value: "{{email}}",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
action: "fill",
|
|
27
|
+
target: 'input[name="password"], input[type="password"], #password',
|
|
28
|
+
value: "{{password}}",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
action: "click",
|
|
32
|
+
target:
|
|
33
|
+
'button[type="submit"], input[type="submit"], button:has-text("Log in"), button:has-text("Sign in")',
|
|
34
|
+
},
|
|
35
|
+
{ action: "wait", timeout: 5000 },
|
|
36
|
+
],
|
|
37
|
+
assertions: [
|
|
38
|
+
{ type: "url-contains", value: "/dashboard|/home|/app", critical: true },
|
|
39
|
+
{ type: "no-errors", value: "", critical: true },
|
|
40
|
+
{
|
|
41
|
+
type: "element-hidden",
|
|
42
|
+
value: 'input[type="password"]',
|
|
43
|
+
critical: false,
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
required: true,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Standard signup/registration flow
|
|
51
|
+
*/
|
|
52
|
+
export const AUTH_SIGNUP_FLOW: CriticalFlow = {
|
|
53
|
+
id: "auth-signup",
|
|
54
|
+
name: "Authentication - Signup",
|
|
55
|
+
description: "Tests standard email/password registration flow",
|
|
56
|
+
steps: [
|
|
57
|
+
{ action: "navigate", target: "/signup|/register|/sign-up" },
|
|
58
|
+
{ action: "wait", timeout: 2000 },
|
|
59
|
+
{
|
|
60
|
+
action: "fill",
|
|
61
|
+
target: 'input[name="name"], input[name="fullName"], #name',
|
|
62
|
+
value: "{{name}}",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
action: "fill",
|
|
66
|
+
target: 'input[name="email"], input[type="email"], #email',
|
|
67
|
+
value: "{{email}}",
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
action: "fill",
|
|
71
|
+
target: 'input[name="password"], input[type="password"], #password',
|
|
72
|
+
value: "{{password}}",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
action: "fill",
|
|
76
|
+
target:
|
|
77
|
+
'input[name="confirmPassword"], input[name="password_confirmation"]',
|
|
78
|
+
value: "{{password}}",
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
action: "click",
|
|
82
|
+
target:
|
|
83
|
+
'button[type="submit"], button:has-text("Sign up"), button:has-text("Create account")',
|
|
84
|
+
},
|
|
85
|
+
{ action: "wait", timeout: 5000 },
|
|
86
|
+
],
|
|
87
|
+
assertions: [
|
|
88
|
+
{ type: "no-errors", value: "", critical: true },
|
|
89
|
+
{
|
|
90
|
+
type: "url-contains",
|
|
91
|
+
value: "/verify|/confirm|/dashboard|/welcome",
|
|
92
|
+
critical: false,
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
required: false,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Logout flow
|
|
100
|
+
*/
|
|
101
|
+
export const AUTH_LOGOUT_FLOW: CriticalFlow = {
|
|
102
|
+
id: "auth-logout",
|
|
103
|
+
name: "Authentication - Logout",
|
|
104
|
+
description: "Tests logout functionality",
|
|
105
|
+
steps: [
|
|
106
|
+
{
|
|
107
|
+
action: "click",
|
|
108
|
+
target:
|
|
109
|
+
'[data-testid="logout"], button:has-text("Log out"), button:has-text("Sign out"), a:has-text("Logout")',
|
|
110
|
+
},
|
|
111
|
+
{ action: "wait", timeout: 3000 },
|
|
112
|
+
],
|
|
113
|
+
assertions: [
|
|
114
|
+
{ type: "url-contains", value: "/login|/|/home", critical: true },
|
|
115
|
+
{ type: "no-errors", value: "", critical: true },
|
|
116
|
+
],
|
|
117
|
+
required: false,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Profile update flow
|
|
122
|
+
*/
|
|
123
|
+
export const PROFILE_UPDATE_FLOW: CriticalFlow = {
|
|
124
|
+
id: "profile-update",
|
|
125
|
+
name: "Profile - Update",
|
|
126
|
+
description: "Tests profile/settings update flow",
|
|
127
|
+
steps: [
|
|
128
|
+
{ action: "navigate", target: "/settings|/profile|/account" },
|
|
129
|
+
{ action: "wait", timeout: 2000 },
|
|
130
|
+
{
|
|
131
|
+
action: "fill",
|
|
132
|
+
target: 'input[name="name"], input[name="displayName"], #name',
|
|
133
|
+
value: "{{name}} Updated",
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
action: "click",
|
|
137
|
+
target:
|
|
138
|
+
'button[type="submit"], button:has-text("Save"), button:has-text("Update")',
|
|
139
|
+
},
|
|
140
|
+
{ action: "wait", timeout: 3000 },
|
|
141
|
+
],
|
|
142
|
+
assertions: [
|
|
143
|
+
{ type: "no-errors", value: "", critical: true },
|
|
144
|
+
{
|
|
145
|
+
type: "element-visible",
|
|
146
|
+
value: '[data-testid="success"], .toast, .notification',
|
|
147
|
+
critical: false,
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
required: false,
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Search functionality flow
|
|
155
|
+
*/
|
|
156
|
+
export const SEARCH_FLOW: CriticalFlow = {
|
|
157
|
+
id: "search",
|
|
158
|
+
name: "Search",
|
|
159
|
+
description: "Tests search functionality",
|
|
160
|
+
steps: [
|
|
161
|
+
{
|
|
162
|
+
action: "click",
|
|
163
|
+
target:
|
|
164
|
+
'[data-testid="search"], button[aria-label="Search"], input[type="search"], .search-input',
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
action: "fill",
|
|
168
|
+
target:
|
|
169
|
+
'input[type="search"], input[name="search"], input[name="q"], .search-input',
|
|
170
|
+
value: "test query",
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
action: "click",
|
|
174
|
+
target: 'button[type="submit"], button:has-text("Search")',
|
|
175
|
+
},
|
|
176
|
+
{ action: "wait", timeout: 3000 },
|
|
177
|
+
],
|
|
178
|
+
assertions: [
|
|
179
|
+
{ type: "no-errors", value: "", critical: true },
|
|
180
|
+
{
|
|
181
|
+
type: "element-visible",
|
|
182
|
+
value: '[data-testid="results"], .results, .search-results',
|
|
183
|
+
critical: false,
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
required: false,
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Modal interaction flow
|
|
191
|
+
*/
|
|
192
|
+
export const MODAL_FLOW: CriticalFlow = {
|
|
193
|
+
id: "modal-interaction",
|
|
194
|
+
name: "Modal - Open/Close",
|
|
195
|
+
description: "Tests modal dialog functionality",
|
|
196
|
+
steps: [
|
|
197
|
+
{
|
|
198
|
+
action: "click",
|
|
199
|
+
target:
|
|
200
|
+
'[data-modal-trigger], button[aria-haspopup="dialog"], [data-testid*="modal"]',
|
|
201
|
+
},
|
|
202
|
+
{ action: "wait", timeout: 1000 },
|
|
203
|
+
{
|
|
204
|
+
action: "assert",
|
|
205
|
+
target: '[role="dialog"], .modal, [data-state="open"]',
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
action: "click",
|
|
209
|
+
target:
|
|
210
|
+
'[data-testid="close"], button[aria-label="Close"], .modal-close, button:has-text("Cancel")',
|
|
211
|
+
},
|
|
212
|
+
{ action: "wait", timeout: 500 },
|
|
213
|
+
],
|
|
214
|
+
assertions: [
|
|
215
|
+
{ type: "element-hidden", value: '[role="dialog"]', critical: true },
|
|
216
|
+
{ type: "no-errors", value: "", critical: true },
|
|
217
|
+
],
|
|
218
|
+
required: false,
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Navigation flow - tests main nav links
|
|
223
|
+
*/
|
|
224
|
+
export const NAVIGATION_FLOW: CriticalFlow = {
|
|
225
|
+
id: "navigation",
|
|
226
|
+
name: "Navigation",
|
|
227
|
+
description: "Tests main navigation links",
|
|
228
|
+
steps: [
|
|
229
|
+
{ action: "click", target: "nav a:first-of-type, header a:first-of-type" },
|
|
230
|
+
{ action: "wait", timeout: 2000 },
|
|
231
|
+
{ action: "navigate", target: "/" },
|
|
232
|
+
{
|
|
233
|
+
action: "click",
|
|
234
|
+
target: "nav a:nth-of-type(2), header a:nth-of-type(2)",
|
|
235
|
+
},
|
|
236
|
+
{ action: "wait", timeout: 2000 },
|
|
237
|
+
],
|
|
238
|
+
assertions: [{ type: "no-errors", value: "", critical: true }],
|
|
239
|
+
required: false,
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Form validation flow - tests that validation works
|
|
244
|
+
*/
|
|
245
|
+
export const FORM_VALIDATION_FLOW: CriticalFlow = {
|
|
246
|
+
id: "form-validation",
|
|
247
|
+
name: "Form Validation",
|
|
248
|
+
description: "Tests that form validation prevents bad submissions",
|
|
249
|
+
steps: [
|
|
250
|
+
{ action: "navigate", target: "/contact|/signup|/register" },
|
|
251
|
+
{ action: "wait", timeout: 2000 },
|
|
252
|
+
// Submit empty form
|
|
253
|
+
{ action: "click", target: 'button[type="submit"]' },
|
|
254
|
+
{ action: "wait", timeout: 1000 },
|
|
255
|
+
],
|
|
256
|
+
assertions: [
|
|
257
|
+
// Should show validation errors, not submit
|
|
258
|
+
{
|
|
259
|
+
type: "element-visible",
|
|
260
|
+
value: '.error, [data-error], [aria-invalid="true"], .invalid-feedback',
|
|
261
|
+
critical: true,
|
|
262
|
+
},
|
|
263
|
+
{ type: "no-errors", value: "", critical: false },
|
|
264
|
+
],
|
|
265
|
+
required: false,
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Dark mode toggle flow
|
|
270
|
+
*/
|
|
271
|
+
export const DARK_MODE_FLOW: CriticalFlow = {
|
|
272
|
+
id: "dark-mode",
|
|
273
|
+
name: "Dark Mode Toggle",
|
|
274
|
+
description: "Tests dark/light mode switching",
|
|
275
|
+
steps: [
|
|
276
|
+
{
|
|
277
|
+
action: "click",
|
|
278
|
+
target:
|
|
279
|
+
'[data-testid="theme-toggle"], button[aria-label*="theme"], button[aria-label*="dark"], .theme-toggle',
|
|
280
|
+
},
|
|
281
|
+
{ action: "wait", timeout: 500 },
|
|
282
|
+
],
|
|
283
|
+
assertions: [{ type: "no-errors", value: "", critical: true }],
|
|
284
|
+
required: false,
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Checkout flow (e-commerce)
|
|
289
|
+
*/
|
|
290
|
+
export const CHECKOUT_FLOW: CriticalFlow = {
|
|
291
|
+
id: "checkout",
|
|
292
|
+
name: "E-commerce - Checkout",
|
|
293
|
+
description: "Tests basic checkout flow",
|
|
294
|
+
steps: [
|
|
295
|
+
{ action: "navigate", target: "/cart|/checkout" },
|
|
296
|
+
{ action: "wait", timeout: 2000 },
|
|
297
|
+
{
|
|
298
|
+
action: "click",
|
|
299
|
+
target:
|
|
300
|
+
'button:has-text("Checkout"), button:has-text("Continue"), a:has-text("Checkout")',
|
|
301
|
+
},
|
|
302
|
+
{ action: "wait", timeout: 3000 },
|
|
303
|
+
],
|
|
304
|
+
assertions: [
|
|
305
|
+
{ type: "no-errors", value: "", critical: true },
|
|
306
|
+
{ type: "url-contains", value: "/checkout|/payment", critical: false },
|
|
307
|
+
],
|
|
308
|
+
required: false,
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* All available flow packs
|
|
313
|
+
*/
|
|
314
|
+
export const FLOW_PACKS = {
|
|
315
|
+
auth: [AUTH_LOGIN_FLOW, AUTH_SIGNUP_FLOW, AUTH_LOGOUT_FLOW],
|
|
316
|
+
profile: [PROFILE_UPDATE_FLOW],
|
|
317
|
+
search: [SEARCH_FLOW],
|
|
318
|
+
ui: [MODAL_FLOW, NAVIGATION_FLOW, DARK_MODE_FLOW],
|
|
319
|
+
forms: [FORM_VALIDATION_FLOW],
|
|
320
|
+
ecommerce: [CHECKOUT_FLOW],
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Get all critical flows
|
|
325
|
+
*/
|
|
326
|
+
export function getAllFlows(): CriticalFlow[] {
|
|
327
|
+
return Object.values(FLOW_PACKS).flat();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Get flows by pack name
|
|
332
|
+
*/
|
|
333
|
+
export function getFlowPack(packName: keyof typeof FLOW_PACKS): CriticalFlow[] {
|
|
334
|
+
return FLOW_PACKS[packName] || [];
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Generate Playwright code for a critical flow
|
|
339
|
+
*/
|
|
340
|
+
export function generateFlowTest(
|
|
341
|
+
flow: CriticalFlow,
|
|
342
|
+
testData: Record<string, string>,
|
|
343
|
+
): string {
|
|
344
|
+
const steps = flow.steps
|
|
345
|
+
.map((step, idx) => {
|
|
346
|
+
let code = "";
|
|
347
|
+
|
|
348
|
+
// Replace template variables
|
|
349
|
+
const target = step.target?.replace(
|
|
350
|
+
/\{\{(\w+)\}\}/g,
|
|
351
|
+
(_, key) => testData[key] || "",
|
|
352
|
+
);
|
|
353
|
+
const value = step.value?.replace(
|
|
354
|
+
/\{\{(\w+)\}\}/g,
|
|
355
|
+
(_, key) => testData[key] || "",
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
switch (step.action) {
|
|
359
|
+
case "navigate":
|
|
360
|
+
// Handle multiple possible paths (pipe-separated)
|
|
361
|
+
if (target?.includes("|")) {
|
|
362
|
+
const paths = target.split("|");
|
|
363
|
+
code = `
|
|
364
|
+
// Step ${idx + 1}: Navigate to ${flow.name} page
|
|
365
|
+
let navigated = false;
|
|
366
|
+
for (const path of ${JSON.stringify(paths)}) {
|
|
367
|
+
try {
|
|
368
|
+
const response = await page.goto(CONFIG.baseUrl + path);
|
|
369
|
+
if (response?.status() < 400) { navigated = true; break; }
|
|
370
|
+
} catch {}
|
|
371
|
+
}
|
|
372
|
+
if (!navigated) { flowResult.failedAt = 'Navigation failed'; return; }`;
|
|
373
|
+
} else {
|
|
374
|
+
code = `
|
|
375
|
+
// Step ${idx + 1}: Navigate
|
|
376
|
+
await page.goto(CONFIG.baseUrl + '${target}');`;
|
|
377
|
+
}
|
|
378
|
+
break;
|
|
379
|
+
|
|
380
|
+
case "fill":
|
|
381
|
+
// Handle multiple possible selectors
|
|
382
|
+
if (target?.includes(",")) {
|
|
383
|
+
code = `
|
|
384
|
+
// Step ${idx + 1}: Fill field
|
|
385
|
+
const field${idx} = await page.$('${target}');
|
|
386
|
+
if (field${idx}) await field${idx}.fill('${value}');`;
|
|
387
|
+
} else {
|
|
388
|
+
code = `
|
|
389
|
+
// Step ${idx + 1}: Fill
|
|
390
|
+
await page.fill('${target}', '${value}').catch(() => {});`;
|
|
391
|
+
}
|
|
392
|
+
break;
|
|
393
|
+
|
|
394
|
+
case "click":
|
|
395
|
+
if (target?.includes(",") || target?.includes("|")) {
|
|
396
|
+
const selectors = target.split(/[,|]/).map((s) => s.trim());
|
|
397
|
+
code = `
|
|
398
|
+
// Step ${idx + 1}: Click
|
|
399
|
+
for (const sel of ${JSON.stringify(selectors)}) {
|
|
400
|
+
const el = await page.$(sel);
|
|
401
|
+
if (el && await el.isVisible()) { await el.click(); break; }
|
|
402
|
+
}`;
|
|
403
|
+
} else {
|
|
404
|
+
code = `
|
|
405
|
+
// Step ${idx + 1}: Click
|
|
406
|
+
await page.click('${target}').catch(() => {});`;
|
|
407
|
+
}
|
|
408
|
+
break;
|
|
409
|
+
|
|
410
|
+
case "wait":
|
|
411
|
+
code = `
|
|
412
|
+
// Step ${idx + 1}: Wait
|
|
413
|
+
await page.waitForTimeout(${step.timeout || 1000});
|
|
414
|
+
await page.waitForLoadState('networkidle').catch(() => {});`;
|
|
415
|
+
break;
|
|
416
|
+
|
|
417
|
+
case "assert":
|
|
418
|
+
code = `
|
|
419
|
+
// Step ${idx + 1}: Assert element exists
|
|
420
|
+
const exists${idx} = await page.$('${target}');
|
|
421
|
+
if (!exists${idx}) { flowResult.failedAt = 'Assertion failed: ${target}'; }`;
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return code;
|
|
426
|
+
})
|
|
427
|
+
.join("\n");
|
|
428
|
+
|
|
429
|
+
const assertions = flow.assertions
|
|
430
|
+
.map((assertion, idx) => {
|
|
431
|
+
switch (assertion.type) {
|
|
432
|
+
case "url-contains":
|
|
433
|
+
const patterns = assertion.value
|
|
434
|
+
.split("|")
|
|
435
|
+
.map((p) => `page.url().includes('${p}')`)
|
|
436
|
+
.join(" || ");
|
|
437
|
+
return `
|
|
438
|
+
// Assertion: URL contains ${assertion.value}
|
|
439
|
+
if (!(${patterns})) {
|
|
440
|
+
flowResult.assertionsFailed.push('URL should contain ${assertion.value}');
|
|
441
|
+
${assertion.critical ? "flowResult.success = false;" : ""}
|
|
442
|
+
}`;
|
|
443
|
+
case "element-visible":
|
|
444
|
+
return `
|
|
445
|
+
// Assertion: Element visible
|
|
446
|
+
const visible${idx} = await page.$('${assertion.value}');
|
|
447
|
+
if (!visible${idx} || !(await visible${idx}.isVisible())) {
|
|
448
|
+
flowResult.assertionsFailed.push('Element should be visible: ${assertion.value}');
|
|
449
|
+
${assertion.critical ? "flowResult.success = false;" : ""}
|
|
450
|
+
}`;
|
|
451
|
+
case "element-hidden":
|
|
452
|
+
return `
|
|
453
|
+
// Assertion: Element hidden
|
|
454
|
+
const hidden${idx} = await page.$('${assertion.value}');
|
|
455
|
+
if (hidden${idx} && await hidden${idx}.isVisible()) {
|
|
456
|
+
flowResult.assertionsFailed.push('Element should be hidden: ${assertion.value}');
|
|
457
|
+
${assertion.critical ? "flowResult.success = false;" : ""}
|
|
458
|
+
}`;
|
|
459
|
+
case "no-errors":
|
|
460
|
+
return `
|
|
461
|
+
// Assertion: No errors
|
|
462
|
+
// (errors are captured globally)`;
|
|
463
|
+
default:
|
|
464
|
+
return "";
|
|
465
|
+
}
|
|
466
|
+
})
|
|
467
|
+
.join("\n");
|
|
468
|
+
|
|
469
|
+
return `
|
|
470
|
+
test('Flow: ${flow.name}', async () => {
|
|
471
|
+
const flowResult = {
|
|
472
|
+
id: '${flow.id}',
|
|
473
|
+
name: '${flow.name}',
|
|
474
|
+
success: true,
|
|
475
|
+
stepsCompleted: 0,
|
|
476
|
+
failedAt: null as string | null,
|
|
477
|
+
assertionsFailed: [] as string[],
|
|
478
|
+
duration: 0,
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
const startTime = Date.now();
|
|
482
|
+
|
|
483
|
+
try {
|
|
484
|
+
${steps}
|
|
485
|
+
|
|
486
|
+
flowResult.stepsCompleted = ${flow.steps.length};
|
|
487
|
+
|
|
488
|
+
${assertions}
|
|
489
|
+
|
|
490
|
+
} catch (error: any) {
|
|
491
|
+
flowResult.success = false;
|
|
492
|
+
flowResult.failedAt = error.message;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
flowResult.duration = Date.now() - startTime;
|
|
496
|
+
results.flows.push(flowResult);
|
|
497
|
+
|
|
498
|
+
console.log(flowResult.success ?
|
|
499
|
+
'✅ Flow passed: ${flow.name}' :
|
|
500
|
+
'❌ Flow failed: ${flow.name} - ' + (flowResult.failedAt || flowResult.assertionsFailed.join(', '))
|
|
501
|
+
);
|
|
502
|
+
});
|
|
503
|
+
`;
|
|
504
|
+
}
|