@odavl/guardian 0.1.0-rc1
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/CHANGELOG.md +20 -0
- package/LICENSE +21 -0
- package/README.md +141 -0
- package/bin/guardian.js +690 -0
- package/flows/example-login-flow.json +36 -0
- package/flows/example-signup-flow.json +44 -0
- package/guardian-contract-v1.md +149 -0
- package/guardian.config.json +54 -0
- package/guardian.policy.json +12 -0
- package/guardian.profile.docs.yaml +18 -0
- package/guardian.profile.ecommerce.yaml +17 -0
- package/guardian.profile.marketing.yaml +18 -0
- package/guardian.profile.saas.yaml +21 -0
- package/package.json +69 -0
- package/policies/enterprise.json +12 -0
- package/policies/saas.json +12 -0
- package/policies/startup.json +12 -0
- package/src/guardian/attempt-engine.js +454 -0
- package/src/guardian/attempt-registry.js +227 -0
- package/src/guardian/attempt-reporter.js +507 -0
- package/src/guardian/attempt.js +227 -0
- package/src/guardian/auto-attempt-builder.js +283 -0
- package/src/guardian/baseline-reporter.js +143 -0
- package/src/guardian/baseline-storage.js +285 -0
- package/src/guardian/baseline.js +492 -0
- package/src/guardian/behavioral-signals.js +261 -0
- package/src/guardian/breakage-intelligence.js +223 -0
- package/src/guardian/browser.js +92 -0
- package/src/guardian/cli-summary.js +141 -0
- package/src/guardian/crawler.js +142 -0
- package/src/guardian/discovery-engine.js +661 -0
- package/src/guardian/enhanced-html-reporter.js +305 -0
- package/src/guardian/failure-taxonomy.js +169 -0
- package/src/guardian/flow-executor.js +374 -0
- package/src/guardian/flow-registry.js +67 -0
- package/src/guardian/html-reporter.js +414 -0
- package/src/guardian/index.js +218 -0
- package/src/guardian/init-command.js +139 -0
- package/src/guardian/junit-reporter.js +264 -0
- package/src/guardian/market-criticality.js +335 -0
- package/src/guardian/market-reporter.js +305 -0
- package/src/guardian/network-trace.js +178 -0
- package/src/guardian/policy.js +357 -0
- package/src/guardian/preset-loader.js +148 -0
- package/src/guardian/reality.js +547 -0
- package/src/guardian/reporter.js +181 -0
- package/src/guardian/root-cause-analysis.js +171 -0
- package/src/guardian/safety.js +248 -0
- package/src/guardian/scan-presets.js +60 -0
- package/src/guardian/screenshot.js +152 -0
- package/src/guardian/sitemap.js +225 -0
- package/src/guardian/snapshot-schema.js +266 -0
- package/src/guardian/snapshot.js +327 -0
- package/src/guardian/validators.js +323 -0
- package/src/guardian/visual-diff.js +247 -0
- package/src/guardian/webhook.js +206 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guardian Flow Execution Module
|
|
3
|
+
* Executes predefined user interaction flows (click, type, submit, etc.)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
class GuardianFlowExecutor {
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
this.timeout = options.timeout || 30000; // 30 seconds per step
|
|
12
|
+
this.screenshotOnStep = options.screenshotOnStep !== false; // Screenshot each step by default
|
|
13
|
+
this.safety = options.safety || null; // Safety guard instance (optional)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Load flow definition from JSON file
|
|
18
|
+
* @param {string} flowPath - Path to flow JSON file
|
|
19
|
+
* @returns {object|null} Flow definition
|
|
20
|
+
*/
|
|
21
|
+
loadFlow(flowPath) {
|
|
22
|
+
try {
|
|
23
|
+
const content = fs.readFileSync(flowPath, 'utf8');
|
|
24
|
+
return JSON.parse(content);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
console.error(`❌ Failed to load flow: ${error.message}`);
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Execute a single flow step
|
|
33
|
+
* @param {Page} page - Playwright page
|
|
34
|
+
* @param {object} step - Step definition
|
|
35
|
+
* @param {number} stepIndex - Step index for logging
|
|
36
|
+
* @returns {Promise<object>} { success: boolean, error: string|null }
|
|
37
|
+
*/
|
|
38
|
+
async executeStep(page, step, stepIndex) {
|
|
39
|
+
try {
|
|
40
|
+
console.log(` Step ${stepIndex + 1}: ${step.type} ${step.target || ''}`);
|
|
41
|
+
|
|
42
|
+
// Safety check for destructive actions
|
|
43
|
+
if (this.safety) {
|
|
44
|
+
const safetyCheck = this.checkStepSafety(step);
|
|
45
|
+
if (!safetyCheck.safe) {
|
|
46
|
+
return {
|
|
47
|
+
success: false,
|
|
48
|
+
error: `Safety guard blocked step: ${safetyCheck.reason}`,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
switch (step.type) {
|
|
54
|
+
case 'navigate':
|
|
55
|
+
return await this.stepNavigate(page, step);
|
|
56
|
+
|
|
57
|
+
case 'click':
|
|
58
|
+
return await this.stepClick(page, step);
|
|
59
|
+
|
|
60
|
+
case 'type':
|
|
61
|
+
return await this.stepType(page, step);
|
|
62
|
+
|
|
63
|
+
case 'submit':
|
|
64
|
+
return await this.stepSubmit(page, step);
|
|
65
|
+
|
|
66
|
+
case 'waitFor':
|
|
67
|
+
return await this.stepWaitFor(page, step);
|
|
68
|
+
|
|
69
|
+
case 'wait':
|
|
70
|
+
return await this.stepWait(page, step);
|
|
71
|
+
|
|
72
|
+
default:
|
|
73
|
+
return {
|
|
74
|
+
success: false,
|
|
75
|
+
error: `Unknown step type: ${step.type}`,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
} catch (error) {
|
|
79
|
+
return {
|
|
80
|
+
success: false,
|
|
81
|
+
error: error.message,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Navigate to URL
|
|
88
|
+
* @param {Page} page - Playwright page
|
|
89
|
+
* @param {object} step - Step definition
|
|
90
|
+
* @returns {Promise<object>} Result
|
|
91
|
+
*/
|
|
92
|
+
async stepNavigate(page, step) {
|
|
93
|
+
try {
|
|
94
|
+
await page.goto(step.target, {
|
|
95
|
+
timeout: this.timeout,
|
|
96
|
+
waitUntil: 'domcontentloaded',
|
|
97
|
+
});
|
|
98
|
+
return { success: true, error: null };
|
|
99
|
+
} catch (error) {
|
|
100
|
+
return { success: false, error: error.message };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Click element
|
|
106
|
+
* @param {Page} page - Playwright page
|
|
107
|
+
* @param {object} step - Step definition
|
|
108
|
+
* @returns {Promise<object>} Result
|
|
109
|
+
*/
|
|
110
|
+
async stepClick(page, step) {
|
|
111
|
+
try {
|
|
112
|
+
await page.click(step.target, { timeout: this.timeout });
|
|
113
|
+
|
|
114
|
+
// Wait for navigation if expected
|
|
115
|
+
if (step.waitForNavigation) {
|
|
116
|
+
await page.waitForLoadState('domcontentloaded');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { success: true, error: null };
|
|
120
|
+
} catch (error) {
|
|
121
|
+
return { success: false, error: error.message };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Type into input field
|
|
127
|
+
* @param {Page} page - Playwright page
|
|
128
|
+
* @param {object} step - Step definition
|
|
129
|
+
* @returns {Promise<object>} Result
|
|
130
|
+
*/
|
|
131
|
+
async stepType(page, step) {
|
|
132
|
+
try {
|
|
133
|
+
// Clear field first if specified
|
|
134
|
+
if (step.clear !== false) {
|
|
135
|
+
await page.fill(step.target, '');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
await page.type(step.target, step.value, {
|
|
139
|
+
timeout: this.timeout,
|
|
140
|
+
delay: step.delay || 50, // Simulate human typing
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
return { success: true, error: null };
|
|
144
|
+
} catch (error) {
|
|
145
|
+
return { success: false, error: error.message };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Submit form
|
|
151
|
+
* @param {Page} page - Playwright page
|
|
152
|
+
* @param {object} step - Step definition
|
|
153
|
+
* @returns {Promise<object>} Result
|
|
154
|
+
*/
|
|
155
|
+
async stepSubmit(page, step) {
|
|
156
|
+
try {
|
|
157
|
+
// Find submit button within form or use provided selector
|
|
158
|
+
if (step.target) {
|
|
159
|
+
await page.click(step.target, { timeout: this.timeout });
|
|
160
|
+
} else {
|
|
161
|
+
// Find first submit button
|
|
162
|
+
await page.click('button[type="submit"]', { timeout: this.timeout });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Wait for navigation
|
|
166
|
+
await page.waitForLoadState('domcontentloaded');
|
|
167
|
+
|
|
168
|
+
return { success: true, error: null };
|
|
169
|
+
} catch (error) {
|
|
170
|
+
return { success: false, error: error.message };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Wait for element to appear
|
|
176
|
+
* @param {Page} page - Playwright page
|
|
177
|
+
* @param {object} step - Step definition
|
|
178
|
+
* @returns {Promise<object>} Result
|
|
179
|
+
*/
|
|
180
|
+
async stepWaitFor(page, step) {
|
|
181
|
+
try {
|
|
182
|
+
await page.waitForSelector(step.target, {
|
|
183
|
+
timeout: step.timeout || this.timeout,
|
|
184
|
+
state: step.state || 'visible',
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
return { success: true, error: null };
|
|
188
|
+
} catch (error) {
|
|
189
|
+
return { success: false, error: error.message };
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Wait for specified time
|
|
195
|
+
* @param {Page} page - Playwright page
|
|
196
|
+
* @param {object} step - Step definition
|
|
197
|
+
* @returns {Promise<object>} Result
|
|
198
|
+
*/
|
|
199
|
+
async stepWait(page, step) {
|
|
200
|
+
try {
|
|
201
|
+
const duration = step.duration || 1000;
|
|
202
|
+
await page.waitForTimeout(duration);
|
|
203
|
+
return { success: true, error: null };
|
|
204
|
+
} catch (error) {
|
|
205
|
+
return { success: false, error: error.message };
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Check if step is safe to execute
|
|
211
|
+
* @param {object} step - Step definition
|
|
212
|
+
* @returns {object} { safe: boolean, reason: string|null }
|
|
213
|
+
*/
|
|
214
|
+
checkStepSafety(step) {
|
|
215
|
+
if (!this.safety) {
|
|
216
|
+
return { safe: true, reason: null };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Check URL safety for navigate steps
|
|
220
|
+
if (step.type === 'navigate') {
|
|
221
|
+
return this.safety.isUrlSafe(step.target);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Check selector safety for click/type steps
|
|
225
|
+
if (step.type === 'click' || step.type === 'type') {
|
|
226
|
+
return this.safety.isSelectorSafe(step.target);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Check form submission safety
|
|
230
|
+
if (step.type === 'submit') {
|
|
231
|
+
return this.safety.isFormSubmitSafe(step.target);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return { safe: true, reason: null };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Execute complete flow
|
|
239
|
+
* @param {Page} page - Playwright page
|
|
240
|
+
* @param {object} flow - Flow definition
|
|
241
|
+
* @param {string} artifactsDir - Directory for screenshots
|
|
242
|
+
* @returns {Promise<object>} Flow result
|
|
243
|
+
*/
|
|
244
|
+
async executeFlow(page, flow, artifactsDir, baseUrl = null) {
|
|
245
|
+
const result = {
|
|
246
|
+
flowId: flow.id,
|
|
247
|
+
flowName: flow.name,
|
|
248
|
+
success: false,
|
|
249
|
+
stepsExecuted: 0,
|
|
250
|
+
stepsTotal: flow.steps.length,
|
|
251
|
+
failedStep: null,
|
|
252
|
+
error: null,
|
|
253
|
+
screenshots: [],
|
|
254
|
+
durationMs: 0
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// Normalize steps with optional baseUrl substitution
|
|
258
|
+
const steps = (flow.steps || []).map((step) => {
|
|
259
|
+
if (baseUrl && typeof step.target === 'string' && step.target.includes('$BASEURL')) {
|
|
260
|
+
return { ...step, target: step.target.replace('$BASEURL', baseUrl) };
|
|
261
|
+
}
|
|
262
|
+
return step;
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Ensure artifact subfolder exists for screenshots
|
|
266
|
+
if (artifactsDir) {
|
|
267
|
+
const pagesDir = path.join(artifactsDir, 'pages');
|
|
268
|
+
fs.mkdirSync(pagesDir, { recursive: true });
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
let startedAt = Date.now();
|
|
272
|
+
try {
|
|
273
|
+
console.log(`\n🎬 Executing flow: ${flow.name}`);
|
|
274
|
+
console.log(`📋 Steps: ${steps.length}`);
|
|
275
|
+
|
|
276
|
+
for (let i = 0; i < steps.length; i++) {
|
|
277
|
+
const step = steps[i];
|
|
278
|
+
|
|
279
|
+
// Execute step
|
|
280
|
+
const stepResult = await this.executeStep(page, step, i);
|
|
281
|
+
|
|
282
|
+
// Capture screenshot after step
|
|
283
|
+
if (this.screenshotOnStep && artifactsDir) {
|
|
284
|
+
const screenshotPath = path.join(
|
|
285
|
+
artifactsDir,
|
|
286
|
+
'pages',
|
|
287
|
+
`flow-step-${i + 1}.jpeg`
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
await page.screenshot({ path: screenshotPath, type: 'jpeg', quality: 80 });
|
|
292
|
+
result.screenshots.push(`flow-step-${i + 1}.jpeg`);
|
|
293
|
+
} catch (error) {
|
|
294
|
+
console.error(`⚠️ Failed to capture screenshot: ${error.message}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Check if step failed
|
|
299
|
+
if (!stepResult.success) {
|
|
300
|
+
result.failedStep = i + 1;
|
|
301
|
+
result.error = stepResult.error;
|
|
302
|
+
console.log(` ❌ Step failed: ${stepResult.error}`);
|
|
303
|
+
return result;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
result.stepsExecuted++;
|
|
307
|
+
console.log(` ✅ Step ${i + 1} completed`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
result.success = true;
|
|
311
|
+
result.durationMs = Date.now() - startedAt;
|
|
312
|
+
console.log(`✅ Flow completed successfully`);
|
|
313
|
+
|
|
314
|
+
return result;
|
|
315
|
+
} catch (error) {
|
|
316
|
+
if (!result.durationMs) {
|
|
317
|
+
result.durationMs = Date.now() - (startedAt || Date.now());
|
|
318
|
+
}
|
|
319
|
+
result.error = error.message;
|
|
320
|
+
console.error(`❌ Flow execution failed: ${error.message}`);
|
|
321
|
+
return result;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Validate flow definition
|
|
327
|
+
* @param {object} flow - Flow definition
|
|
328
|
+
* @returns {object} { valid: boolean, errors: string[] }
|
|
329
|
+
*/
|
|
330
|
+
validateFlow(flow) {
|
|
331
|
+
const errors = [];
|
|
332
|
+
|
|
333
|
+
if (!flow.id) {
|
|
334
|
+
errors.push('Flow missing required field: id');
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (!flow.name) {
|
|
338
|
+
errors.push('Flow missing required field: name');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (!flow.steps || !Array.isArray(flow.steps)) {
|
|
342
|
+
errors.push('Flow missing required field: steps (must be array)');
|
|
343
|
+
} else if (flow.steps.length === 0) {
|
|
344
|
+
errors.push('Flow has no steps');
|
|
345
|
+
} else {
|
|
346
|
+
// Validate each step
|
|
347
|
+
flow.steps.forEach((step, index) => {
|
|
348
|
+
if (!step.type) {
|
|
349
|
+
errors.push(`Step ${index + 1}: missing type`);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const validTypes = ['navigate', 'click', 'type', 'submit', 'waitFor', 'wait'];
|
|
353
|
+
if (step.type && !validTypes.includes(step.type)) {
|
|
354
|
+
errors.push(`Step ${index + 1}: invalid type "${step.type}"`);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if ((step.type === 'navigate' || step.type === 'click' || step.type === 'type' || step.type === 'waitFor') && !step.target) {
|
|
358
|
+
errors.push(`Step ${index + 1}: missing target`);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (step.type === 'type' && !step.value) {
|
|
362
|
+
errors.push(`Step ${index + 1}: missing value for type step`);
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
valid: errors.length === 0,
|
|
369
|
+
errors,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
module.exports = { GuardianFlowExecutor };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guardian Flow Registry (Phase 3)
|
|
3
|
+
* Curated intent flows executed via Flow Executor
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const DEFAULT_FLOWS = ['signup_flow', 'login_flow', 'checkout_flow'];
|
|
7
|
+
|
|
8
|
+
const flowDefinitions = {
|
|
9
|
+
signup_flow: {
|
|
10
|
+
id: 'signup_flow',
|
|
11
|
+
name: 'Intent: Account Signup',
|
|
12
|
+
description: 'User creates an account successfully',
|
|
13
|
+
riskCategory: 'LEAD',
|
|
14
|
+
steps: [
|
|
15
|
+
{ id: 'navigate_home', type: 'navigate', target: '$BASEURL', description: 'Navigate to home page' },
|
|
16
|
+
{ id: 'open_signup', type: 'click', target: 'a[data-guardian="account-signup-link"], a:has-text("Account Signup")', description: 'Open account signup page', waitForNavigation: true },
|
|
17
|
+
{ id: 'fill_email', type: 'type', target: '[data-guardian="signup-email"]', value: 'newuser@example.com', description: 'Enter signup email' },
|
|
18
|
+
{ id: 'fill_password', type: 'type', target: '[data-guardian="signup-password"]', value: 'P@ssword123', description: 'Enter signup password' },
|
|
19
|
+
{ id: 'submit', type: 'click', target: '[data-guardian="signup-account-submit"], button:has-text("Sign up")', description: 'Submit signup form' },
|
|
20
|
+
{ id: 'wait_success', type: 'waitFor', target: '[data-guardian="signup-account-success"]', description: 'Wait for signup success', timeout: 7000 }
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
login_flow: {
|
|
24
|
+
id: 'login_flow',
|
|
25
|
+
name: 'Intent: Account Login',
|
|
26
|
+
description: 'User logs in successfully',
|
|
27
|
+
riskCategory: 'TRUST/UX',
|
|
28
|
+
steps: [
|
|
29
|
+
{ id: 'navigate_home', type: 'navigate', target: '$BASEURL', description: 'Navigate to home page' },
|
|
30
|
+
{ id: 'open_login', type: 'click', target: 'a[data-guardian="account-login-link"], a:has-text("Login"), a[href*="login"]', description: 'Open login page', waitForNavigation: true },
|
|
31
|
+
{ id: 'fill_email', type: 'type', target: '[data-guardian="login-email"]', value: 'user@example.com', description: 'Enter login email' },
|
|
32
|
+
{ id: 'fill_password', type: 'type', target: '[data-guardian="login-password"]', value: 'password123', description: 'Enter login password' },
|
|
33
|
+
{ id: 'submit', type: 'click', target: '[data-guardian="login-submit"], button:has-text("Login")', description: 'Submit login form' },
|
|
34
|
+
{ id: 'wait_success', type: 'waitFor', target: '[data-guardian="login-success"]', description: 'Wait for login success', timeout: 7000 }
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
checkout_flow: {
|
|
38
|
+
id: 'checkout_flow',
|
|
39
|
+
name: 'Intent: Checkout',
|
|
40
|
+
description: 'User reviews cart and places order (no payment)',
|
|
41
|
+
riskCategory: 'REVENUE',
|
|
42
|
+
steps: [
|
|
43
|
+
{ id: 'navigate_home', type: 'navigate', target: '$BASEURL', description: 'Navigate to home page' },
|
|
44
|
+
{ id: 'open_checkout', type: 'click', target: 'a[data-guardian="checkout-link"], a:has-text("Checkout"), a[href*="checkout"]', description: 'Open checkout page', waitForNavigation: true },
|
|
45
|
+
{ id: 'place_order', type: 'click', target: '[data-guardian="checkout-place-order"], button:has-text("Place order"), button:has-text("Place Order")', description: 'Place order' },
|
|
46
|
+
{ id: 'wait_success', type: 'waitFor', target: '[data-guardian="checkout-success"]', description: 'Wait for order confirmation', timeout: 7000 }
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function getFlowDefinition(flowId) {
|
|
52
|
+
return flowDefinitions[flowId] || null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getDefaultFlowIds() {
|
|
56
|
+
return DEFAULT_FLOWS.slice();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function listFlowDefinitions() {
|
|
60
|
+
return Object.values(flowDefinitions);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = {
|
|
64
|
+
getFlowDefinition,
|
|
65
|
+
getDefaultFlowIds,
|
|
66
|
+
listFlowDefinitions
|
|
67
|
+
};
|