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,688 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime Explorer
|
|
3
|
+
*
|
|
4
|
+
* The "robot user" that actually clicks through the app using Playwright.
|
|
5
|
+
* - Navigates to all discovered routes
|
|
6
|
+
* - Clicks buttons, opens modals, interacts with dropdowns
|
|
7
|
+
* - Fills and submits forms with test data
|
|
8
|
+
* - Captures errors, network calls, state changes
|
|
9
|
+
*/
|
|
10
|
+
import { SurfaceDiscovery } from "./surface-discovery";
|
|
11
|
+
// Test data generators for form filling
|
|
12
|
+
const TEST_DATA = {
|
|
13
|
+
email: "test-reality@guardrail.dev",
|
|
14
|
+
password: "TestPass123!",
|
|
15
|
+
name: "Reality Test User",
|
|
16
|
+
phone: "+1234567890",
|
|
17
|
+
address: "123 Test Street",
|
|
18
|
+
city: "Test City",
|
|
19
|
+
zip: "12345",
|
|
20
|
+
text: "This is a test input from Guardrail Reality Mode",
|
|
21
|
+
number: "42",
|
|
22
|
+
url: "https://example.com",
|
|
23
|
+
date: "2024-01-15",
|
|
24
|
+
};
|
|
25
|
+
export class RuntimeExplorer {
|
|
26
|
+
config;
|
|
27
|
+
surfaceDiscovery;
|
|
28
|
+
errors = [];
|
|
29
|
+
networkCalls = [];
|
|
30
|
+
isAuthenticated = false;
|
|
31
|
+
constructor(config) {
|
|
32
|
+
this.config = config;
|
|
33
|
+
this.surfaceDiscovery = new SurfaceDiscovery(config.baseUrl);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Generate the complete Playwright test file for exploration
|
|
37
|
+
*/
|
|
38
|
+
generateExplorerTest() {
|
|
39
|
+
const { baseUrl, outputDir, headless, allowDestructive, timeout } = this.config;
|
|
40
|
+
const authConfig = this.config.auth;
|
|
41
|
+
return `/**
|
|
42
|
+
* Reality Explorer - Auto-generated Playwright Test
|
|
43
|
+
*
|
|
44
|
+
* This test ACTUALLY explores your app:
|
|
45
|
+
* - Visits every discoverable route
|
|
46
|
+
* - Clicks every safe button and element
|
|
47
|
+
* - Fills and submits forms
|
|
48
|
+
* - Captures what works and what breaks
|
|
49
|
+
*
|
|
50
|
+
* Generated by Guardrail Reality Mode
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
import { test, expect, Page, BrowserContext } from '@playwright/test';
|
|
54
|
+
import * as fs from 'fs';
|
|
55
|
+
import * as path from 'path';
|
|
56
|
+
|
|
57
|
+
// Configuration
|
|
58
|
+
const CONFIG = {
|
|
59
|
+
baseUrl: '${baseUrl}',
|
|
60
|
+
outputDir: '${outputDir.replace(/\\/g, "\\\\")}',
|
|
61
|
+
timeout: ${timeout},
|
|
62
|
+
allowDestructive: ${allowDestructive},
|
|
63
|
+
maxActionsPerPage: ${this.config.maxActionsPerPage || 50},
|
|
64
|
+
maxPages: ${this.config.maxPages || 20},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Test data for form filling
|
|
68
|
+
const TEST_DATA = ${JSON.stringify(TEST_DATA, null, 2)};
|
|
69
|
+
|
|
70
|
+
// Destructive action patterns to avoid
|
|
71
|
+
const DESTRUCTIVE_PATTERNS = [
|
|
72
|
+
/delete/i, /remove/i, /destroy/i, /cancel.*subscription/i,
|
|
73
|
+
/deactivate/i, /terminate/i, /close.*account/i, /reset.*all/i,
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
// Results storage
|
|
77
|
+
interface ExplorerResults {
|
|
78
|
+
routes: { path: string; status: string; error?: string; responseTime?: number }[];
|
|
79
|
+
elements: { selector: string; text: string; status: string; error?: string; changes: string[] }[];
|
|
80
|
+
forms: { selector: string; status: string; error?: string; fieldsFilledCount: number }[];
|
|
81
|
+
errors: { type: string; message: string; url: string; timestamp: number }[];
|
|
82
|
+
networkCalls: { url: string; method: string; status: number; duration: number }[];
|
|
83
|
+
coverage: { routes: number; elements: number; forms: number };
|
|
84
|
+
score: number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const results: ExplorerResults = {
|
|
88
|
+
routes: [],
|
|
89
|
+
elements: [],
|
|
90
|
+
forms: [],
|
|
91
|
+
errors: [],
|
|
92
|
+
networkCalls: [],
|
|
93
|
+
coverage: { routes: 0, elements: 0, forms: 0 },
|
|
94
|
+
score: 0,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Discovered surface
|
|
98
|
+
let discoveredRoutes: string[] = [];
|
|
99
|
+
let discoveredElements: any[] = [];
|
|
100
|
+
let discoveredForms: any[] = [];
|
|
101
|
+
|
|
102
|
+
test.describe('Reality Explorer', () => {
|
|
103
|
+
let context: BrowserContext;
|
|
104
|
+
let page: Page;
|
|
105
|
+
|
|
106
|
+
test.beforeAll(async ({ browser }) => {
|
|
107
|
+
// Create context with tracing
|
|
108
|
+
context = await browser.newContext({
|
|
109
|
+
viewport: { width: 1280, height: 720 },
|
|
110
|
+
recordVideo: { dir: path.join(CONFIG.outputDir, 'videos') },
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Start tracing
|
|
114
|
+
await context.tracing.start({ screenshots: true, snapshots: true });
|
|
115
|
+
|
|
116
|
+
page = await context.newPage();
|
|
117
|
+
|
|
118
|
+
// Setup error capture
|
|
119
|
+
page.on('console', msg => {
|
|
120
|
+
if (msg.type() === 'error') {
|
|
121
|
+
results.errors.push({
|
|
122
|
+
type: 'console',
|
|
123
|
+
message: msg.text(),
|
|
124
|
+
url: page.url(),
|
|
125
|
+
timestamp: Date.now(),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
page.on('pageerror', error => {
|
|
131
|
+
results.errors.push({
|
|
132
|
+
type: 'uncaught',
|
|
133
|
+
message: error.message,
|
|
134
|
+
url: page.url(),
|
|
135
|
+
timestamp: Date.now(),
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Setup network capture
|
|
140
|
+
page.on('response', async response => {
|
|
141
|
+
const url = response.url();
|
|
142
|
+
if (url.includes('/api/') || url.includes('/graphql')) {
|
|
143
|
+
results.networkCalls.push({
|
|
144
|
+
url,
|
|
145
|
+
method: response.request().method(),
|
|
146
|
+
status: response.status(),
|
|
147
|
+
duration: 0, // Would need timing API for accurate duration
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Ensure output directory exists
|
|
153
|
+
if (!fs.existsSync(CONFIG.outputDir)) {
|
|
154
|
+
fs.mkdirSync(CONFIG.outputDir, { recursive: true });
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test.afterAll(async () => {
|
|
159
|
+
// Save trace
|
|
160
|
+
await context.tracing.stop({
|
|
161
|
+
path: path.join(CONFIG.outputDir, 'trace.zip')
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Calculate score
|
|
165
|
+
results.score = calculateScore(results);
|
|
166
|
+
|
|
167
|
+
// Save results
|
|
168
|
+
fs.writeFileSync(
|
|
169
|
+
path.join(CONFIG.outputDir, 'explorer-results.json'),
|
|
170
|
+
JSON.stringify(results, null, 2)
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
// Generate HTML report
|
|
174
|
+
const html = generateHTMLReport(results);
|
|
175
|
+
fs.writeFileSync(
|
|
176
|
+
path.join(CONFIG.outputDir, 'reality-report.html'),
|
|
177
|
+
html
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
await context.close();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
${authConfig ? this.generateAuthTest(authConfig) : ""}
|
|
184
|
+
|
|
185
|
+
test('01 - Discover App Surface', async () => {
|
|
186
|
+
await page.goto(CONFIG.baseUrl);
|
|
187
|
+
await page.waitForLoadState('networkidle');
|
|
188
|
+
|
|
189
|
+
// Discover all routes from links
|
|
190
|
+
discoveredRoutes = await page.$$eval('a[href]', anchors =>
|
|
191
|
+
anchors
|
|
192
|
+
.map(a => a.getAttribute('href'))
|
|
193
|
+
.filter(href => href && href.startsWith('/') && !href.startsWith('//'))
|
|
194
|
+
.filter((v, i, a) => a.indexOf(v) === i) // unique
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
console.log(\`π Discovered \${discoveredRoutes.length} routes\`);
|
|
198
|
+
|
|
199
|
+
// Discover interactive elements
|
|
200
|
+
discoveredElements = await discoverElements(page);
|
|
201
|
+
console.log(\`π Discovered \${discoveredElements.length} interactive elements\`);
|
|
202
|
+
|
|
203
|
+
// Discover forms
|
|
204
|
+
discoveredForms = await discoverForms(page);
|
|
205
|
+
console.log(\`π Discovered \${discoveredForms.length} forms\`);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test('02 - Visit All Routes', async () => {
|
|
209
|
+
const routesToVisit = discoveredRoutes.slice(0, CONFIG.maxPages);
|
|
210
|
+
|
|
211
|
+
for (const route of routesToVisit) {
|
|
212
|
+
const startTime = Date.now();
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
const response = await page.goto(CONFIG.baseUrl + route, {
|
|
216
|
+
waitUntil: 'networkidle',
|
|
217
|
+
timeout: CONFIG.timeout
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const status = response?.status() || 0;
|
|
221
|
+
const responseTime = Date.now() - startTime;
|
|
222
|
+
|
|
223
|
+
results.routes.push({
|
|
224
|
+
path: route,
|
|
225
|
+
status: status >= 200 && status < 400 ? 'success' : 'error',
|
|
226
|
+
responseTime,
|
|
227
|
+
error: status >= 400 ? \`HTTP \${status}\` : undefined,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Re-discover elements on each page
|
|
231
|
+
const pageElements = await discoverElements(page);
|
|
232
|
+
const pageForms = await discoverForms(page);
|
|
233
|
+
|
|
234
|
+
// Merge newly discovered elements
|
|
235
|
+
for (const el of pageElements) {
|
|
236
|
+
if (!discoveredElements.find(e => e.selector === el.selector)) {
|
|
237
|
+
discoveredElements.push({ ...el, page: route });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
for (const form of pageForms) {
|
|
241
|
+
if (!discoveredForms.find(f => f.selector === form.selector)) {
|
|
242
|
+
discoveredForms.push({ ...form, page: route });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
await page.screenshot({
|
|
247
|
+
path: path.join(CONFIG.outputDir, \`route-\${route.replace(/\\//g, '_')}.png\`)
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
} catch (error: any) {
|
|
251
|
+
results.routes.push({
|
|
252
|
+
path: route,
|
|
253
|
+
status: 'error',
|
|
254
|
+
error: error.message,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
results.coverage.routes = results.routes.filter(r => r.status === 'success').length;
|
|
260
|
+
console.log(\`β
Visited \${results.coverage.routes}/\${routesToVisit.length} routes successfully\`);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test('03 - Test Interactive Elements', async () => {
|
|
264
|
+
// Go back to home first
|
|
265
|
+
await page.goto(CONFIG.baseUrl);
|
|
266
|
+
await page.waitForLoadState('networkidle');
|
|
267
|
+
|
|
268
|
+
const elementsToTest = discoveredElements
|
|
269
|
+
.filter(el => !el.isDestructive || CONFIG.allowDestructive)
|
|
270
|
+
.slice(0, CONFIG.maxActionsPerPage);
|
|
271
|
+
|
|
272
|
+
for (const element of elementsToTest) {
|
|
273
|
+
const result = await testElement(page, element);
|
|
274
|
+
results.elements.push(result);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
results.coverage.elements = results.elements.filter(e => e.status === 'success').length;
|
|
278
|
+
console.log(\`β
Tested \${results.coverage.elements}/\${elementsToTest.length} elements successfully\`);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test('04 - Test Forms', async () => {
|
|
282
|
+
// Go back to home first
|
|
283
|
+
await page.goto(CONFIG.baseUrl);
|
|
284
|
+
await page.waitForLoadState('networkidle');
|
|
285
|
+
|
|
286
|
+
for (const form of discoveredForms) {
|
|
287
|
+
const result = await testForm(page, form);
|
|
288
|
+
results.forms.push(result);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
results.coverage.forms = results.forms.filter(f => f.status === 'success').length;
|
|
292
|
+
console.log(\`β
Tested \${results.coverage.forms}/\${discoveredForms.length} forms successfully\`);
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// Helper: Discover interactive elements
|
|
297
|
+
async function discoverElements(page: Page) {
|
|
298
|
+
return page.$$eval(
|
|
299
|
+
'button, [role="button"], a[href], [data-testid], [aria-haspopup], input[type="submit"]',
|
|
300
|
+
elements => elements.map((el, idx) => {
|
|
301
|
+
const text = el.textContent?.trim().slice(0, 50) || '';
|
|
302
|
+
const isDestructive = /delete|remove|destroy|cancel|deactivate/i.test(text);
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
selector: el.id ? \`#\${el.id}\` :
|
|
306
|
+
el.getAttribute('data-testid') ? \`[data-testid="\${el.getAttribute('data-testid')}"]\` :
|
|
307
|
+
\`\${el.tagName.toLowerCase()}:nth-of-type(\${idx + 1})\`,
|
|
308
|
+
text,
|
|
309
|
+
type: el.tagName.toLowerCase(),
|
|
310
|
+
isDestructive,
|
|
311
|
+
page: '',
|
|
312
|
+
};
|
|
313
|
+
}).filter(el => el.text.length > 0)
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Helper: Discover forms
|
|
318
|
+
async function discoverForms(page: Page) {
|
|
319
|
+
return page.$$eval('form', forms =>
|
|
320
|
+
forms.map((form, idx) => ({
|
|
321
|
+
selector: form.id ? \`#\${form.id}\` : \`form:nth-of-type(\${idx + 1})\`,
|
|
322
|
+
action: form.action,
|
|
323
|
+
method: form.method,
|
|
324
|
+
fields: Array.from(form.querySelectorAll('input, textarea, select')).map((field: any) => ({
|
|
325
|
+
name: field.name || field.id,
|
|
326
|
+
type: field.type || 'text',
|
|
327
|
+
required: field.required,
|
|
328
|
+
selector: field.id ? \`#\${field.id}\` : \`[name="\${field.name}"]\`,
|
|
329
|
+
})),
|
|
330
|
+
page: '',
|
|
331
|
+
}))
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Helper: Test an element
|
|
336
|
+
async function testElement(page: Page, element: any) {
|
|
337
|
+
const beforeUrl = page.url();
|
|
338
|
+
const beforeHtml = await page.content();
|
|
339
|
+
const changes: string[] = [];
|
|
340
|
+
|
|
341
|
+
try {
|
|
342
|
+
// Check if element exists and is visible
|
|
343
|
+
const el = await page.$(element.selector);
|
|
344
|
+
if (!el) {
|
|
345
|
+
return { selector: element.selector, text: element.text, status: 'not-found', changes: [] };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const isVisible = await el.isVisible();
|
|
349
|
+
if (!isVisible) {
|
|
350
|
+
return { selector: element.selector, text: element.text, status: 'hidden', changes: [] };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Click the element
|
|
354
|
+
await el.click({ timeout: 5000 });
|
|
355
|
+
|
|
356
|
+
// Wait for potential navigation or state change
|
|
357
|
+
await page.waitForTimeout(500);
|
|
358
|
+
await page.waitForLoadState('networkidle').catch(() => {});
|
|
359
|
+
|
|
360
|
+
// Check for changes
|
|
361
|
+
const afterUrl = page.url();
|
|
362
|
+
const afterHtml = await page.content();
|
|
363
|
+
|
|
364
|
+
if (afterUrl !== beforeUrl) {
|
|
365
|
+
changes.push(\`URL changed to \${afterUrl}\`);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (afterHtml !== beforeHtml) {
|
|
369
|
+
changes.push('DOM changed');
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Check for modals
|
|
373
|
+
const modals = await page.$$('[role="dialog"], .modal, [data-modal]');
|
|
374
|
+
if (modals.length > 0) {
|
|
375
|
+
changes.push('Modal opened');
|
|
376
|
+
// Close modal if possible
|
|
377
|
+
await page.keyboard.press('Escape');
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return {
|
|
381
|
+
selector: element.selector,
|
|
382
|
+
text: element.text,
|
|
383
|
+
status: changes.length > 0 ? 'success' : 'no-change',
|
|
384
|
+
changes,
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
} catch (error: any) {
|
|
388
|
+
return {
|
|
389
|
+
selector: element.selector,
|
|
390
|
+
text: element.text,
|
|
391
|
+
status: 'error',
|
|
392
|
+
error: error.message,
|
|
393
|
+
changes: [],
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Helper: Test a form
|
|
399
|
+
async function testForm(page: Page, form: any) {
|
|
400
|
+
let fieldsFilledCount = 0;
|
|
401
|
+
|
|
402
|
+
try {
|
|
403
|
+
// Navigate to the page with the form if needed
|
|
404
|
+
if (form.page && form.page !== page.url()) {
|
|
405
|
+
await page.goto(CONFIG.baseUrl + form.page);
|
|
406
|
+
await page.waitForLoadState('networkidle');
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Check if form exists
|
|
410
|
+
const formEl = await page.$(form.selector);
|
|
411
|
+
if (!formEl) {
|
|
412
|
+
return { selector: form.selector, status: 'not-found', fieldsFilledCount: 0 };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Fill fields
|
|
416
|
+
for (const field of form.fields) {
|
|
417
|
+
try {
|
|
418
|
+
const fieldEl = await page.$(field.selector);
|
|
419
|
+
if (!fieldEl) continue;
|
|
420
|
+
|
|
421
|
+
const value = getTestValue(field.type, field.name);
|
|
422
|
+
|
|
423
|
+
if (field.type === 'checkbox' || field.type === 'radio') {
|
|
424
|
+
await fieldEl.check();
|
|
425
|
+
} else if (field.type === 'select') {
|
|
426
|
+
// Select first option
|
|
427
|
+
await page.selectOption(field.selector, { index: 1 });
|
|
428
|
+
} else {
|
|
429
|
+
await fieldEl.fill(value);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
fieldsFilledCount++;
|
|
433
|
+
} catch (e) {
|
|
434
|
+
// Field couldn't be filled, continue
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Try to submit (but don't actually submit to avoid side effects)
|
|
439
|
+
// Just validate that the form is fillable
|
|
440
|
+
|
|
441
|
+
return {
|
|
442
|
+
selector: form.selector,
|
|
443
|
+
status: fieldsFilledCount > 0 ? 'success' : 'no-fields',
|
|
444
|
+
fieldsFilledCount,
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
} catch (error: any) {
|
|
448
|
+
return {
|
|
449
|
+
selector: form.selector,
|
|
450
|
+
status: 'error',
|
|
451
|
+
error: error.message,
|
|
452
|
+
fieldsFilledCount,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Helper: Get appropriate test value for field type
|
|
458
|
+
function getTestValue(type: string, name: string): string {
|
|
459
|
+
const nameLower = name?.toLowerCase() || '';
|
|
460
|
+
|
|
461
|
+
if (nameLower.includes('email')) return TEST_DATA.email;
|
|
462
|
+
if (nameLower.includes('password')) return TEST_DATA.password;
|
|
463
|
+
if (nameLower.includes('phone') || nameLower.includes('tel')) return TEST_DATA.phone;
|
|
464
|
+
if (nameLower.includes('name')) return TEST_DATA.name;
|
|
465
|
+
if (nameLower.includes('address')) return TEST_DATA.address;
|
|
466
|
+
if (nameLower.includes('city')) return TEST_DATA.city;
|
|
467
|
+
if (nameLower.includes('zip') || nameLower.includes('postal')) return TEST_DATA.zip;
|
|
468
|
+
if (nameLower.includes('url') || nameLower.includes('website')) return TEST_DATA.url;
|
|
469
|
+
|
|
470
|
+
switch (type) {
|
|
471
|
+
case 'email': return TEST_DATA.email;
|
|
472
|
+
case 'password': return TEST_DATA.password;
|
|
473
|
+
case 'tel': return TEST_DATA.phone;
|
|
474
|
+
case 'number': return TEST_DATA.number;
|
|
475
|
+
case 'url': return TEST_DATA.url;
|
|
476
|
+
case 'date': return TEST_DATA.date;
|
|
477
|
+
default: return TEST_DATA.text;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Helper: Calculate reality score
|
|
482
|
+
function calculateScore(results: ExplorerResults): number {
|
|
483
|
+
const totalRoutes = results.routes.length || 1;
|
|
484
|
+
const totalElements = results.elements.length || 1;
|
|
485
|
+
const totalForms = results.forms.length || 1;
|
|
486
|
+
|
|
487
|
+
// Coverage (40 points)
|
|
488
|
+
const routeCoverage = (results.coverage.routes / totalRoutes) * 15;
|
|
489
|
+
const elementCoverage = (results.coverage.elements / totalElements) * 15;
|
|
490
|
+
const formCoverage = (results.coverage.forms / totalForms) * 10;
|
|
491
|
+
const coverageScore = routeCoverage + elementCoverage + formCoverage;
|
|
492
|
+
|
|
493
|
+
// Functionality (35 points)
|
|
494
|
+
const successfulActions = results.elements.filter(e => e.status === 'success').length;
|
|
495
|
+
const successfulForms = results.forms.filter(f => f.status === 'success').length;
|
|
496
|
+
const functionalityScore =
|
|
497
|
+
((successfulActions / totalElements) * 20) +
|
|
498
|
+
((successfulForms / totalForms) * 15);
|
|
499
|
+
|
|
500
|
+
// Stability (15 points) - penalize errors
|
|
501
|
+
const errorPenalty = Math.min(results.errors.length * 3, 15);
|
|
502
|
+
const stabilityScore = 15 - errorPenalty;
|
|
503
|
+
|
|
504
|
+
// UX (10 points) - bonus for interactive elements that work
|
|
505
|
+
const interactiveElements = results.elements.filter(e => e.changes && e.changes.length > 0);
|
|
506
|
+
const uxScore = Math.min((interactiveElements.length / totalElements) * 10, 10);
|
|
507
|
+
|
|
508
|
+
return Math.round(Math.max(0, coverageScore + functionalityScore + stabilityScore + uxScore));
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Helper: Generate HTML report
|
|
512
|
+
function generateHTMLReport(results: ExplorerResults): string {
|
|
513
|
+
const score = results.score;
|
|
514
|
+
const grade = score >= 90 ? 'A' : score >= 80 ? 'B' : score >= 70 ? 'C' : score >= 60 ? 'D' : 'F';
|
|
515
|
+
const color = score >= 80 ? '#22c55e' : score >= 60 ? '#eab308' : '#ef4444';
|
|
516
|
+
|
|
517
|
+
return \`<!DOCTYPE html>
|
|
518
|
+
<html>
|
|
519
|
+
<head>
|
|
520
|
+
<title>Reality Mode Report - Guardrail</title>
|
|
521
|
+
<style>
|
|
522
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
523
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0a0a0a; color: #fff; padding: 2rem; }
|
|
524
|
+
.container { max-width: 1200px; margin: 0 auto; }
|
|
525
|
+
h1 { font-size: 2rem; margin-bottom: 0.5rem; }
|
|
526
|
+
.subtitle { color: #888; margin-bottom: 2rem; }
|
|
527
|
+
.score-card { background: linear-gradient(135deg, #1a1a1a, #0d0d0d); border: 1px solid #333; border-radius: 1rem; padding: 2rem; text-align: center; margin-bottom: 2rem; }
|
|
528
|
+
.score { font-size: 6rem; font-weight: bold; color: \${color}; }
|
|
529
|
+
.grade { font-size: 2rem; color: \${color}; }
|
|
530
|
+
.metrics { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; margin-bottom: 2rem; }
|
|
531
|
+
.metric { background: #1a1a1a; border: 1px solid #333; border-radius: 0.5rem; padding: 1rem; text-align: center; }
|
|
532
|
+
.metric-value { font-size: 2rem; font-weight: bold; color: #fff; }
|
|
533
|
+
.metric-label { color: #888; font-size: 0.875rem; }
|
|
534
|
+
.section { background: #1a1a1a; border: 1px solid #333; border-radius: 0.5rem; padding: 1.5rem; margin-bottom: 1rem; }
|
|
535
|
+
.section h2 { font-size: 1.25rem; margin-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem; }
|
|
536
|
+
.item { display: flex; justify-content: space-between; align-items: center; padding: 0.75rem 0; border-bottom: 1px solid #333; }
|
|
537
|
+
.item:last-child { border-bottom: none; }
|
|
538
|
+
.status { padding: 0.25rem 0.75rem; border-radius: 1rem; font-size: 0.75rem; font-weight: 600; }
|
|
539
|
+
.status-success { background: rgba(34, 197, 94, 0.2); color: #22c55e; }
|
|
540
|
+
.status-error { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
|
|
541
|
+
.status-warning { background: rgba(234, 179, 8, 0.2); color: #eab308; }
|
|
542
|
+
.errors { background: rgba(239, 68, 68, 0.1); border-color: #ef4444; }
|
|
543
|
+
.error-item { color: #ef4444; padding: 0.5rem 0; font-family: monospace; font-size: 0.875rem; }
|
|
544
|
+
</style>
|
|
545
|
+
</head>
|
|
546
|
+
<body>
|
|
547
|
+
<div class="container">
|
|
548
|
+
<h1>π Reality Mode Report</h1>
|
|
549
|
+
<p class="subtitle">Generated by Guardrail - \${new Date().toLocaleString()}</p>
|
|
550
|
+
|
|
551
|
+
<div class="score-card">
|
|
552
|
+
<div class="score">\${score}</div>
|
|
553
|
+
<div class="grade">Grade: \${grade}</div>
|
|
554
|
+
<p style="color: #888; margin-top: 1rem;">
|
|
555
|
+
\${score >= 80 ? 'β
Ready to ship!' : score >= 60 ? 'β οΈ Needs some work' : 'β Critical issues found'}
|
|
556
|
+
</p>
|
|
557
|
+
</div>
|
|
558
|
+
|
|
559
|
+
<div class="metrics">
|
|
560
|
+
<div class="metric">
|
|
561
|
+
<div class="metric-value">\${results.coverage.routes}/\${results.routes.length}</div>
|
|
562
|
+
<div class="metric-label">Routes Working</div>
|
|
563
|
+
</div>
|
|
564
|
+
<div class="metric">
|
|
565
|
+
<div class="metric-value">\${results.coverage.elements}/\${results.elements.length}</div>
|
|
566
|
+
<div class="metric-label">Elements Working</div>
|
|
567
|
+
</div>
|
|
568
|
+
<div class="metric">
|
|
569
|
+
<div class="metric-value">\${results.coverage.forms}/\${results.forms.length}</div>
|
|
570
|
+
<div class="metric-label">Forms Working</div>
|
|
571
|
+
</div>
|
|
572
|
+
<div class="metric">
|
|
573
|
+
<div class="metric-value">\${results.errors.length}</div>
|
|
574
|
+
<div class="metric-label">Errors Found</div>
|
|
575
|
+
</div>
|
|
576
|
+
</div>
|
|
577
|
+
|
|
578
|
+
<div class="section">
|
|
579
|
+
<h2>πΊοΈ Routes</h2>
|
|
580
|
+
\${results.routes.map(r => \`
|
|
581
|
+
<div class="item">
|
|
582
|
+
<span>\${r.path}</span>
|
|
583
|
+
<span class="status status-\${r.status === 'success' ? 'success' : 'error'}">\${r.status}\${r.responseTime ? \` (\${r.responseTime}ms)\` : ''}</span>
|
|
584
|
+
</div>
|
|
585
|
+
\`).join('')}
|
|
586
|
+
</div>
|
|
587
|
+
|
|
588
|
+
<div class="section">
|
|
589
|
+
<h2>π Interactive Elements</h2>
|
|
590
|
+
\${results.elements.slice(0, 20).map(e => \`
|
|
591
|
+
<div class="item">
|
|
592
|
+
<span>\${e.text || e.selector}</span>
|
|
593
|
+
<span class="status status-\${e.status === 'success' ? 'success' : e.status === 'no-change' ? 'warning' : 'error'}">\${e.status}</span>
|
|
594
|
+
</div>
|
|
595
|
+
\`).join('')}
|
|
596
|
+
\${results.elements.length > 20 ? \`<p style="color: #888; padding-top: 1rem;">... and \${results.elements.length - 20} more</p>\` : ''}
|
|
597
|
+
</div>
|
|
598
|
+
|
|
599
|
+
<div class="section">
|
|
600
|
+
<h2>π Forms</h2>
|
|
601
|
+
\${results.forms.map(f => \`
|
|
602
|
+
<div class="item">
|
|
603
|
+
<span>\${f.selector}</span>
|
|
604
|
+
<span class="status status-\${f.status === 'success' ? 'success' : 'error'}">\${f.status} (\${f.fieldsFilledCount} fields)</span>
|
|
605
|
+
</div>
|
|
606
|
+
\`).join('')}
|
|
607
|
+
</div>
|
|
608
|
+
|
|
609
|
+
\${results.errors.length > 0 ? \`
|
|
610
|
+
<div class="section errors">
|
|
611
|
+
<h2>β Errors Captured</h2>
|
|
612
|
+
\${results.errors.slice(0, 10).map(e => \`
|
|
613
|
+
<div class="error-item">[\${e.type}] \${e.message}</div>
|
|
614
|
+
\`).join('')}
|
|
615
|
+
</div>
|
|
616
|
+
\` : ''}
|
|
617
|
+
|
|
618
|
+
<div class="section">
|
|
619
|
+
<h2>π API Calls</h2>
|
|
620
|
+
\${results.networkCalls.slice(0, 15).map(n => \`
|
|
621
|
+
<div class="item">
|
|
622
|
+
<span>\${n.method} \${n.url.split('?')[0]}</span>
|
|
623
|
+
<span class="status status-\${n.status < 400 ? 'success' : 'error'}">\${n.status}</span>
|
|
624
|
+
</div>
|
|
625
|
+
\`).join('')}
|
|
626
|
+
</div>
|
|
627
|
+
</div>
|
|
628
|
+
</body>
|
|
629
|
+
</html>\`;
|
|
630
|
+
}
|
|
631
|
+
`;
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Generate auth test section if auth config is provided
|
|
635
|
+
*/
|
|
636
|
+
generateAuthTest(authConfig) {
|
|
637
|
+
return `
|
|
638
|
+
test('00 - Authenticate', async () => {
|
|
639
|
+
await page.goto('${authConfig.loginUrl}');
|
|
640
|
+
await page.waitForLoadState('networkidle');
|
|
641
|
+
|
|
642
|
+
// Fill credentials
|
|
643
|
+
await page.fill('${authConfig.credentials.emailField}', '${authConfig.credentials.email}');
|
|
644
|
+
await page.fill('${authConfig.credentials.passwordField}', '${authConfig.credentials.password}');
|
|
645
|
+
|
|
646
|
+
// Submit
|
|
647
|
+
await page.click('button[type="submit"]');
|
|
648
|
+
|
|
649
|
+
// Wait for success indicator
|
|
650
|
+
await page.waitForSelector('${authConfig.successIndicator}', { timeout: 10000 });
|
|
651
|
+
|
|
652
|
+
console.log('β
Authentication successful');
|
|
653
|
+
});
|
|
654
|
+
`;
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Get the explorer configuration
|
|
658
|
+
*/
|
|
659
|
+
getConfig() {
|
|
660
|
+
return this.config;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Create default explorer config
|
|
665
|
+
*/
|
|
666
|
+
export function createDefaultConfig(baseUrl, outputDir) {
|
|
667
|
+
return {
|
|
668
|
+
baseUrl,
|
|
669
|
+
maxPages: 20,
|
|
670
|
+
maxActionsPerPage: 50,
|
|
671
|
+
timeout: 30000,
|
|
672
|
+
headless: true,
|
|
673
|
+
allowDestructive: false,
|
|
674
|
+
destructivePatterns: [
|
|
675
|
+
"delete",
|
|
676
|
+
"remove",
|
|
677
|
+
"destroy",
|
|
678
|
+
"cancel",
|
|
679
|
+
"deactivate",
|
|
680
|
+
"terminate",
|
|
681
|
+
],
|
|
682
|
+
outputDir,
|
|
683
|
+
captureVideo: true,
|
|
684
|
+
captureTrace: true,
|
|
685
|
+
captureScreenshots: true,
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
//# sourceMappingURL=runtime-explorer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime-explorer.js","sourceRoot":"","sources":["../../../src/reality-mode/explorer/runtime-explorer.ts"],"names":[],"mappings":"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"}
|