supipowers 0.3.0 → 0.5.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/package.json +1 -1
- package/skills/fix-pr/SKILL.md +99 -0
- package/skills/qa-strategy/SKILL.md +103 -21
- package/src/commands/fix-pr.ts +324 -0
- package/src/commands/qa.ts +232 -148
- package/src/commands/supi.ts +2 -1
- package/src/config/defaults.ts +1 -0
- package/src/config/schema.ts +1 -0
- package/src/fix-pr/config.ts +36 -0
- package/src/fix-pr/prompt-builder.ts +201 -0
- package/src/fix-pr/scripts/diff-comments.sh +33 -0
- package/src/fix-pr/scripts/fetch-pr-comments.sh +25 -0
- package/src/fix-pr/scripts/trigger-review.sh +36 -0
- package/src/fix-pr/scripts/wait-and-check.sh +37 -0
- package/src/fix-pr/types.ts +71 -0
- package/src/index.ts +2 -0
- package/src/qa/config.ts +43 -0
- package/src/qa/matrix.ts +84 -0
- package/src/qa/prompt-builder.ts +212 -0
- package/src/qa/scripts/detect-app-type.sh +68 -0
- package/src/qa/scripts/discover-routes.sh +143 -0
- package/src/qa/scripts/ensure-playwright.sh +38 -0
- package/src/qa/scripts/run-e2e-tests.sh +99 -0
- package/src/qa/scripts/start-dev-server.sh +46 -0
- package/src/qa/scripts/stop-dev-server.sh +36 -0
- package/src/qa/session.ts +39 -55
- package/src/qa/types.ts +97 -0
- package/src/storage/fix-pr-sessions.ts +59 -0
- package/src/storage/qa-sessions.ts +9 -9
- package/src/types.ts +1 -70
- package/src/qa/detector.ts +0 -61
- package/src/qa/phases/discovery.ts +0 -34
- package/src/qa/phases/execution.ts +0 -65
- package/src/qa/phases/matrix.ts +0 -41
- package/src/qa/phases/reporting.ts +0 -71
- package/src/qa/report.ts +0 -22
- package/src/qa/runner.ts +0 -46
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import type { AppTypeInfo, E2eQaConfig } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export interface E2ePromptOptions {
|
|
4
|
+
cwd: string;
|
|
5
|
+
appType: AppTypeInfo;
|
|
6
|
+
sessionDir: string;
|
|
7
|
+
scriptsDir: string;
|
|
8
|
+
config: E2eQaConfig;
|
|
9
|
+
discoveredRoutes: string;
|
|
10
|
+
previousMatrix: string | null;
|
|
11
|
+
skillContent: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function buildE2eOrchestratorPrompt(options: E2ePromptOptions): string {
|
|
15
|
+
const { appType, sessionDir, scriptsDir, config, discoveredRoutes, previousMatrix, skillContent } = options;
|
|
16
|
+
const { playwright, execution } = config;
|
|
17
|
+
|
|
18
|
+
const sections: string[] = [
|
|
19
|
+
"# E2E QA Pipeline — Autonomous Execution",
|
|
20
|
+
"",
|
|
21
|
+
`You are an autonomous E2E QA pipeline for a **${appType.type}** application.`,
|
|
22
|
+
"Run all phases sequentially without stopping. Use the provided scripts for heavy operations.",
|
|
23
|
+
"",
|
|
24
|
+
"## Session Context",
|
|
25
|
+
"",
|
|
26
|
+
`- Session dir: \`${sessionDir}\``,
|
|
27
|
+
`- Base URL: \`${appType.baseUrl}\``,
|
|
28
|
+
`- Dev command: \`${appType.devCommand}\``,
|
|
29
|
+
`- Port: ${appType.port}`,
|
|
30
|
+
`- Browser: ${playwright.browser}`,
|
|
31
|
+
`- Headless: ${playwright.headless}`,
|
|
32
|
+
`- Test timeout: ${playwright.timeout}ms`,
|
|
33
|
+
`- maxRetries: ${execution.maxRetries}`,
|
|
34
|
+
`- maxFlows: ${execution.maxFlows}`,
|
|
35
|
+
"",
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
// Previous matrix
|
|
39
|
+
if (previousMatrix) {
|
|
40
|
+
sections.push(
|
|
41
|
+
"## Previous Matrix",
|
|
42
|
+
"",
|
|
43
|
+
"Last-known flow states from `.omp/supipowers/e2e-matrix.json`:",
|
|
44
|
+
"",
|
|
45
|
+
"```json",
|
|
46
|
+
previousMatrix,
|
|
47
|
+
"```",
|
|
48
|
+
"",
|
|
49
|
+
"Compare your findings against this matrix to detect regressions, new flows, and removed flows.",
|
|
50
|
+
"",
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Discovered routes
|
|
55
|
+
sections.push(
|
|
56
|
+
"## Discovered Routes",
|
|
57
|
+
"",
|
|
58
|
+
"Pre-scanned routes/pages/forms from the codebase (JSONL):",
|
|
59
|
+
"",
|
|
60
|
+
"```jsonl",
|
|
61
|
+
discoveredRoutes,
|
|
62
|
+
"```",
|
|
63
|
+
"",
|
|
64
|
+
"Use these as a starting point. You may discover additional flows by reading the codebase.",
|
|
65
|
+
"",
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// Skill content
|
|
69
|
+
if (skillContent) {
|
|
70
|
+
sections.push(
|
|
71
|
+
"## E2E Testing Methodology",
|
|
72
|
+
"",
|
|
73
|
+
skillContent,
|
|
74
|
+
"",
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Step 1: Flow Discovery
|
|
79
|
+
sections.push(
|
|
80
|
+
"## Step 1: Flow Discovery",
|
|
81
|
+
"",
|
|
82
|
+
"Analyze the discovered routes and the codebase to identify user flows:",
|
|
83
|
+
"",
|
|
84
|
+
"1. Read the route scan output above",
|
|
85
|
+
"2. Explore the codebase for additional flows not captured by the scan (modals, multi-step wizards, etc.)",
|
|
86
|
+
"3. Identify forms, auth flows, CRUD operations, navigation patterns",
|
|
87
|
+
"4. Compare against the previous matrix (if any) to detect:",
|
|
88
|
+
" - **New flows**: routes that weren't in the matrix before",
|
|
89
|
+
" - **Removed flows**: routes in the matrix that no longer exist",
|
|
90
|
+
" - **Changed flows**: routes whose structure or behavior changed",
|
|
91
|
+
"5. Assign priority: critical (auth, payment), high (core CRUD), medium (secondary features), low (nice-to-have)",
|
|
92
|
+
`6. Write the flow manifest to \`${sessionDir}/flows.json\``,
|
|
93
|
+
"",
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
// Step 2: Test Generation
|
|
97
|
+
sections.push(
|
|
98
|
+
"## Step 2: Test Generation",
|
|
99
|
+
"",
|
|
100
|
+
"Write playwright test specs for each discovered flow:",
|
|
101
|
+
"",
|
|
102
|
+
`1. Create \`.spec.ts\` files in \`${sessionDir}/tests/\``,
|
|
103
|
+
"2. Each flow gets its own test file",
|
|
104
|
+
"3. Use playwright best practices:",
|
|
105
|
+
" - Use `page.getByRole()`, `page.getByText()`, `page.getByTestId()` for locators",
|
|
106
|
+
" - Use `expect(page).toHaveURL()`, `expect(locator).toBeVisible()` for assertions",
|
|
107
|
+
" - Use `page.waitForLoadState('networkidle')` or `page.waitForSelector()` before assertions",
|
|
108
|
+
" - Set meaningful test descriptions that describe the user journey",
|
|
109
|
+
`4. Import from \`@playwright/test\``,
|
|
110
|
+
`5. Each test should start with \`await page.goto('${appType.baseUrl}/...')\``,
|
|
111
|
+
"",
|
|
112
|
+
"Example test structure:",
|
|
113
|
+
"```typescript",
|
|
114
|
+
"import { test, expect } from '@playwright/test';",
|
|
115
|
+
"",
|
|
116
|
+
"test.describe('Login flow', () => {",
|
|
117
|
+
" test('should log in with valid credentials', async ({ page }) => {",
|
|
118
|
+
` await page.goto('${appType.baseUrl}/login');`,
|
|
119
|
+
" await page.getByLabel('Email').fill('user@example.com');",
|
|
120
|
+
" await page.getByLabel('Password').fill('password123');",
|
|
121
|
+
" await page.getByRole('button', { name: 'Sign in' }).click();",
|
|
122
|
+
` await expect(page).toHaveURL('${appType.baseUrl}/dashboard');`,
|
|
123
|
+
" });",
|
|
124
|
+
"});",
|
|
125
|
+
"```",
|
|
126
|
+
"",
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
// Step 3: Execution
|
|
130
|
+
sections.push(
|
|
131
|
+
"## Step 3: Execution",
|
|
132
|
+
"",
|
|
133
|
+
"Run the generated tests:",
|
|
134
|
+
"",
|
|
135
|
+
`1. Start the dev server:`,
|
|
136
|
+
"```bash",
|
|
137
|
+
`bash ${scriptsDir}/start-dev-server.sh "${options.cwd}" "${appType.devCommand}" ${appType.port} 60 "${sessionDir}"`,
|
|
138
|
+
"```",
|
|
139
|
+
" Read the JSON output. If `ready: false`, stop and report the error.",
|
|
140
|
+
"",
|
|
141
|
+
"2. Run the tests — **IMPORTANT: never run playwright directly. Always use the script:**",
|
|
142
|
+
"```bash",
|
|
143
|
+
`bash ${scriptsDir}/run-e2e-tests.sh "${sessionDir}/tests" "${appType.baseUrl}"`,
|
|
144
|
+
"```",
|
|
145
|
+
" Read the JSON output. It contains `total`, `passed`, `failed`, `failures[]`.",
|
|
146
|
+
"",
|
|
147
|
+
`3. If there are failures and retries remain (max ${execution.maxRetries}):`,
|
|
148
|
+
" - Read the `failures` array (do NOT read full test output)",
|
|
149
|
+
" - Analyze each failure: is it a test issue or a real app bug?",
|
|
150
|
+
" - If test issue: fix the test file and re-run",
|
|
151
|
+
" - If real app bug: note it for the report",
|
|
152
|
+
"",
|
|
153
|
+
"4. Stop the dev server:",
|
|
154
|
+
"```bash",
|
|
155
|
+
`bash ${scriptsDir}/stop-dev-server.sh "${sessionDir}"`,
|
|
156
|
+
"```",
|
|
157
|
+
"",
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
// Step 4: Regression Analysis & Reporting
|
|
161
|
+
sections.push(
|
|
162
|
+
"## Step 4: Regression Analysis & Reporting",
|
|
163
|
+
"",
|
|
164
|
+
"Compare results against the previous matrix to detect regressions:",
|
|
165
|
+
"",
|
|
166
|
+
"For each flow that was **passing** in the matrix but now **fails**:",
|
|
167
|
+
"- This is a **regression** — record it in the ledger's `regressions` array:",
|
|
168
|
+
' `{ "flowId": "...", "flowName": "...", "previousStatus": "pass", "currentStatus": "fail", "error": "..." }`',
|
|
169
|
+
"",
|
|
170
|
+
"Update the persistent matrix at `.omp/supipowers/e2e-matrix.json`:",
|
|
171
|
+
"- Update `lastStatus` and `lastTestedAt` for each tested flow",
|
|
172
|
+
"- Add new flows with `lastStatus: \"untested\"` and `addedAt` timestamp",
|
|
173
|
+
"- Mark removed flows with `removedAt` timestamp (don't delete them)",
|
|
174
|
+
"",
|
|
175
|
+
"Write the final report to the session ledger:",
|
|
176
|
+
"- Total flows tested, passed, failed",
|
|
177
|
+
"- List of regressions (if any)",
|
|
178
|
+
"- List of new flows discovered",
|
|
179
|
+
"- Coverage summary",
|
|
180
|
+
"",
|
|
181
|
+
`Update the session ledger at \`${sessionDir}/ledger.json\` with results and mark all phases completed.`,
|
|
182
|
+
"",
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
// Script paths
|
|
186
|
+
sections.push(
|
|
187
|
+
"## Script Paths",
|
|
188
|
+
"",
|
|
189
|
+
`- detect-app-type.sh: \`${scriptsDir}/detect-app-type.sh\``,
|
|
190
|
+
`- discover-routes.sh: \`${scriptsDir}/discover-routes.sh\``,
|
|
191
|
+
`- ensure-playwright.sh: \`${scriptsDir}/ensure-playwright.sh\``,
|
|
192
|
+
`- start-dev-server.sh: \`${scriptsDir}/start-dev-server.sh\``,
|
|
193
|
+
`- run-e2e-tests.sh: \`${scriptsDir}/run-e2e-tests.sh\``,
|
|
194
|
+
`- stop-dev-server.sh: \`${scriptsDir}/stop-dev-server.sh\``,
|
|
195
|
+
"",
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
// Token guidance
|
|
199
|
+
sections.push(
|
|
200
|
+
"## Token Guidance",
|
|
201
|
+
"",
|
|
202
|
+
"To minimize token usage:",
|
|
203
|
+
"- Always use `run-e2e-tests.sh` — never run playwright directly",
|
|
204
|
+
"- Only read the `failures` array from test results, skip passed tests",
|
|
205
|
+
"- Don't cat full test files when analyzing failures — read only the failing line range",
|
|
206
|
+
"- Write tests incrementally by flow group, run after each group to catch issues early",
|
|
207
|
+
"- Don't dump raw playwright output — the script produces a compact JSON summary",
|
|
208
|
+
"",
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
return sections.join("\n");
|
|
212
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Detect web app framework, dev command, and port.
|
|
3
|
+
# Usage: detect-app-type.sh <cwd>
|
|
4
|
+
# Output: JSON on stdout
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
CWD="${1:-.}"
|
|
8
|
+
|
|
9
|
+
type="generic"
|
|
10
|
+
devCommand="npm run dev"
|
|
11
|
+
port=3000
|
|
12
|
+
|
|
13
|
+
# Check for Next.js
|
|
14
|
+
if [ -f "$CWD/next.config.js" ] || [ -f "$CWD/next.config.mjs" ] || [ -f "$CWD/next.config.ts" ]; then
|
|
15
|
+
if [ -d "$CWD/app" ]; then
|
|
16
|
+
type="nextjs-app"
|
|
17
|
+
elif [ -d "$CWD/src/app" ]; then
|
|
18
|
+
type="nextjs-app"
|
|
19
|
+
elif [ -d "$CWD/pages" ] || [ -d "$CWD/src/pages" ]; then
|
|
20
|
+
type="nextjs-pages"
|
|
21
|
+
else
|
|
22
|
+
type="nextjs-app"
|
|
23
|
+
fi
|
|
24
|
+
port=3000
|
|
25
|
+
|
|
26
|
+
# Check for Vite
|
|
27
|
+
elif [ -f "$CWD/vite.config.ts" ] || [ -f "$CWD/vite.config.js" ] || [ -f "$CWD/vite.config.mjs" ]; then
|
|
28
|
+
type="vite"
|
|
29
|
+
port=5173
|
|
30
|
+
|
|
31
|
+
# Check for Angular
|
|
32
|
+
elif [ -f "$CWD/angular.json" ]; then
|
|
33
|
+
type="generic"
|
|
34
|
+
devCommand="npm start"
|
|
35
|
+
port=4200
|
|
36
|
+
|
|
37
|
+
# Check for Express (look for express in dependencies)
|
|
38
|
+
elif [ -f "$CWD/package.json" ]; then
|
|
39
|
+
if grep -q '"express"' "$CWD/package.json" 2>/dev/null; then
|
|
40
|
+
type="express"
|
|
41
|
+
port=3000
|
|
42
|
+
fi
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
# Try to detect dev command from package.json scripts
|
|
46
|
+
if [ -f "$CWD/package.json" ]; then
|
|
47
|
+
# Check for common dev script names
|
|
48
|
+
if node -e "const p=JSON.parse(require('fs').readFileSync('$CWD/package.json','utf8')); process.exit(p.scripts?.dev ? 0 : 1)" 2>/dev/null; then
|
|
49
|
+
devCommand="npm run dev"
|
|
50
|
+
elif node -e "const p=JSON.parse(require('fs').readFileSync('$CWD/package.json','utf8')); process.exit(p.scripts?.start ? 0 : 1)" 2>/dev/null; then
|
|
51
|
+
devCommand="npm start"
|
|
52
|
+
elif node -e "const p=JSON.parse(require('fs').readFileSync('$CWD/package.json','utf8')); process.exit(p.scripts?.serve ? 0 : 1)" 2>/dev/null; then
|
|
53
|
+
devCommand="npm run serve"
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
# Try to detect port from scripts
|
|
57
|
+
devScript=$(node -e "const p=JSON.parse(require('fs').readFileSync('$CWD/package.json','utf8')); console.log(p.scripts?.dev || p.scripts?.start || '')" 2>/dev/null || echo "")
|
|
58
|
+
portMatch=$(echo "$devScript" | grep -oE '(--port|PORT=)\s*([0-9]+)' | grep -oE '[0-9]+' | head -1 || echo "")
|
|
59
|
+
if [ -n "$portMatch" ]; then
|
|
60
|
+
port="$portMatch"
|
|
61
|
+
fi
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
baseUrl="http://localhost:$port"
|
|
65
|
+
|
|
66
|
+
cat <<EOF
|
|
67
|
+
{"type": "$type", "devCommand": "$devCommand", "port": $port, "baseUrl": "$baseUrl"}
|
|
68
|
+
EOF
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Scan project for routes, pages, forms, and auth flows.
|
|
3
|
+
# Usage: discover-routes.sh <cwd> <app_type>
|
|
4
|
+
# Output: JSONL on stdout (one JSON object per line)
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
CWD="${1:-.}"
|
|
8
|
+
APP_TYPE="${2:-generic}"
|
|
9
|
+
|
|
10
|
+
cd "$CWD"
|
|
11
|
+
|
|
12
|
+
# Helper: output a route as JSONL
|
|
13
|
+
emit() {
|
|
14
|
+
local routePath="$1" file="$2" type="$3" hasForm="${4:-false}" methods="${5:-}"
|
|
15
|
+
if [ -n "$methods" ]; then
|
|
16
|
+
echo "{\"path\": \"$routePath\", \"file\": \"$file\", \"type\": \"$type\", \"hasForm\": $hasForm, \"methods\": $methods}"
|
|
17
|
+
else
|
|
18
|
+
echo "{\"path\": \"$routePath\", \"file\": \"$file\", \"type\": \"$type\", \"hasForm\": $hasForm}"
|
|
19
|
+
fi
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
# Check if file likely contains a form
|
|
23
|
+
has_form() {
|
|
24
|
+
local file="$1"
|
|
25
|
+
grep -qE '(<form|onSubmit|handleSubmit|useForm|formik|react-hook-form)' "$file" 2>/dev/null && echo "true" || echo "false"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
case "$APP_TYPE" in
|
|
29
|
+
nextjs-app)
|
|
30
|
+
# Scan app/ directory for page.tsx/page.jsx/page.ts/page.js files
|
|
31
|
+
for dir in "app" "src/app"; do
|
|
32
|
+
if [ -d "$dir" ]; then
|
|
33
|
+
find "$dir" -name 'page.tsx' -o -name 'page.jsx' -o -name 'page.ts' -o -name 'page.js' 2>/dev/null | while read -r file; do
|
|
34
|
+
# Convert file path to route: app/login/page.tsx -> /login
|
|
35
|
+
route=$(echo "$file" | sed "s|^$dir||" | sed 's|/page\.\(tsx\|jsx\|ts\|js\)$||' | sed 's|^$|/|')
|
|
36
|
+
# Skip route groups (parenthesized segments)
|
|
37
|
+
if echo "$route" | grep -qE '\([^)]+\)'; then
|
|
38
|
+
route=$(echo "$route" | sed 's|/([^)]*)||g')
|
|
39
|
+
fi
|
|
40
|
+
[ -z "$route" ] && route="/"
|
|
41
|
+
formFlag=$(has_form "$file")
|
|
42
|
+
emit "$route" "$file" "page" "$formFlag"
|
|
43
|
+
done
|
|
44
|
+
|
|
45
|
+
# Scan for API routes
|
|
46
|
+
find "$dir" -name 'route.tsx' -o -name 'route.ts' -o -name 'route.js' 2>/dev/null | while read -r file; do
|
|
47
|
+
route=$(echo "$file" | sed "s|^$dir||" | sed 's|/route\.\(tsx\|ts\|js\)$||')
|
|
48
|
+
methods=$(grep -oE '(GET|POST|PUT|PATCH|DELETE)' "$file" 2>/dev/null | sort -u | awk 'BEGIN{ORS=""} NR>1{printf ","} {printf "\"%s\"", $0}' || echo "")
|
|
49
|
+
[ -n "$methods" ] && methods="[$methods]" || methods='["GET"]'
|
|
50
|
+
emit "$route" "$file" "api" "false" "$methods"
|
|
51
|
+
done
|
|
52
|
+
fi
|
|
53
|
+
done
|
|
54
|
+
;;
|
|
55
|
+
|
|
56
|
+
nextjs-pages)
|
|
57
|
+
# Scan pages/ directory
|
|
58
|
+
for dir in "pages" "src/pages"; do
|
|
59
|
+
if [ -d "$dir" ]; then
|
|
60
|
+
find "$dir" -name '*.tsx' -o -name '*.jsx' -o -name '*.ts' -o -name '*.js' 2>/dev/null | while read -r file; do
|
|
61
|
+
# Skip _app, _document, _error, api files
|
|
62
|
+
basename=$(basename "$file")
|
|
63
|
+
case "$basename" in _app.* | _document.* | _error.*) continue;; esac
|
|
64
|
+
|
|
65
|
+
route=$(echo "$file" | sed "s|^$dir||" | sed 's|\.\(tsx\|jsx\|ts\|js\)$||' | sed 's|/index$|/|')
|
|
66
|
+
[ -z "$route" ] && route="/"
|
|
67
|
+
|
|
68
|
+
# Check if it's an API route
|
|
69
|
+
if echo "$file" | grep -q '/api/'; then
|
|
70
|
+
methods=$(grep -oE '(GET|POST|PUT|PATCH|DELETE)' "$file" 2>/dev/null | sort -u | awk 'BEGIN{ORS=""} NR>1{printf ","} {printf "\"%s\"", $0}' || echo "")
|
|
71
|
+
[ -n "$methods" ] && methods="[$methods]" || methods='["GET"]'
|
|
72
|
+
emit "$route" "$file" "api" "false" "$methods"
|
|
73
|
+
else
|
|
74
|
+
formFlag=$(has_form "$file")
|
|
75
|
+
emit "$route" "$file" "page" "$formFlag"
|
|
76
|
+
fi
|
|
77
|
+
done
|
|
78
|
+
fi
|
|
79
|
+
done
|
|
80
|
+
;;
|
|
81
|
+
|
|
82
|
+
react-router)
|
|
83
|
+
# Grep for Route path= patterns
|
|
84
|
+
grep -rn --include='*.tsx' --include='*.jsx' --include='*.ts' --include='*.js' \
|
|
85
|
+
-E '<Route\s+.*path=' "$CWD/src" 2>/dev/null | while read -r line; do
|
|
86
|
+
file=$(echo "$line" | cut -d: -f1)
|
|
87
|
+
routePath=$(echo "$line" | grep -oE 'path="[^"]*"' | head -1 | sed 's/path="//;s/"//')
|
|
88
|
+
[ -z "$routePath" ] && continue
|
|
89
|
+
formFlag=$(has_form "$file")
|
|
90
|
+
emit "$routePath" "$file" "page" "$formFlag"
|
|
91
|
+
done || true
|
|
92
|
+
|
|
93
|
+
# Also check for createBrowserRouter patterns
|
|
94
|
+
grep -rn --include='*.tsx' --include='*.jsx' --include='*.ts' --include='*.js' \
|
|
95
|
+
-E 'path:\s*["\x27]' "$CWD/src" 2>/dev/null | while read -r line; do
|
|
96
|
+
file=$(echo "$line" | cut -d: -f1)
|
|
97
|
+
routePath=$(echo "$line" | grep -oE "path:\s*[\"'][^\"']*[\"']" | head -1 | sed "s/path:\s*[\"']//;s/[\"']//")
|
|
98
|
+
[ -z "$routePath" ] && continue
|
|
99
|
+
formFlag=$(has_form "$file")
|
|
100
|
+
emit "$routePath" "$file" "page" "$formFlag"
|
|
101
|
+
done || true
|
|
102
|
+
;;
|
|
103
|
+
|
|
104
|
+
express)
|
|
105
|
+
# Grep for app.get/post/put/delete/use patterns
|
|
106
|
+
grep -rn --include='*.ts' --include='*.js' \
|
|
107
|
+
-E '\.(get|post|put|patch|delete|use)\s*\(\s*["\x27/]' "$CWD/src" "$CWD/routes" "$CWD/server" 2>/dev/null | while read -r line; do
|
|
108
|
+
file=$(echo "$line" | cut -d: -f1)
|
|
109
|
+
method=$(echo "$line" | grep -oE '\.(get|post|put|patch|delete)' | head -1 | tr -d '.')
|
|
110
|
+
routePath=$(echo "$line" | grep -oE "[\"'][/][^\"']*[\"']" | head -1 | tr -d "\"'")
|
|
111
|
+
[ -z "$routePath" ] && continue
|
|
112
|
+
[ -z "$method" ] && method="GET"
|
|
113
|
+
methods="[\"$(echo "$method" | tr '[:lower:]' '[:upper:]')\"]"
|
|
114
|
+
emit "$routePath" "$file" "api" "false" "$methods"
|
|
115
|
+
done || true
|
|
116
|
+
;;
|
|
117
|
+
|
|
118
|
+
vite|generic)
|
|
119
|
+
# Generic: look for common patterns
|
|
120
|
+
if [ -d "src" ]; then
|
|
121
|
+
# Check for React Router in any form
|
|
122
|
+
grep -rln --include='*.tsx' --include='*.jsx' --include='*.ts' --include='*.js' \
|
|
123
|
+
-E '(<Route|createBrowserRouter|useRoutes)' src/ 2>/dev/null | while read -r file; do
|
|
124
|
+
grep -oE 'path[=:]\s*["\x27][^"\x27]*["\x27]' "$file" 2>/dev/null | while read -r match; do
|
|
125
|
+
routePath=$(echo "$match" | sed "s/path[=:]\s*[\"']//;s/[\"']//")
|
|
126
|
+
[ -z "$routePath" ] && continue
|
|
127
|
+
formFlag=$(has_form "$file")
|
|
128
|
+
emit "$routePath" "$file" "page" "$formFlag"
|
|
129
|
+
done
|
|
130
|
+
done || true
|
|
131
|
+
fi
|
|
132
|
+
;;
|
|
133
|
+
esac
|
|
134
|
+
|
|
135
|
+
# Always scan for auth-related files regardless of framework
|
|
136
|
+
find "$CWD/src" -type f \( -name '*auth*' -o -name '*login*' -o -name '*signup*' -o -name '*register*' \) \
|
|
137
|
+
-not -path '*/node_modules/*' -not -path '*/.next/*' -not -name '*.test.*' -not -name '*.spec.*' 2>/dev/null | while read -r file; do
|
|
138
|
+
# Only emit if not already covered by framework-specific scan
|
|
139
|
+
formFlag=$(has_form "$file")
|
|
140
|
+
# Use filename as hint for route
|
|
141
|
+
basename=$(basename "$file" | sed 's/\.\(tsx\|jsx\|ts\|js\)$//')
|
|
142
|
+
emit "/$basename" "$file" "auth" "$formFlag"
|
|
143
|
+
done || true
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Check if playwright is installed, install if needed.
|
|
3
|
+
# Usage: ensure-playwright.sh <cwd>
|
|
4
|
+
# Output: JSON on stdout
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
CWD="${1:-.}"
|
|
8
|
+
cd "$CWD"
|
|
9
|
+
|
|
10
|
+
installed=false
|
|
11
|
+
browsers="[]"
|
|
12
|
+
|
|
13
|
+
# Check if playwright is available
|
|
14
|
+
if npx playwright --version >/dev/null 2>&1; then
|
|
15
|
+
installed=true
|
|
16
|
+
else
|
|
17
|
+
# Try to install playwright
|
|
18
|
+
if npm install --save-dev @playwright/test >/dev/null 2>&1; then
|
|
19
|
+
installed=true
|
|
20
|
+
else
|
|
21
|
+
echo '{"installed": false, "browsers": [], "error": "Failed to install @playwright/test"}'
|
|
22
|
+
exit 1
|
|
23
|
+
fi
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
# Install chromium browser if not present
|
|
27
|
+
if $installed; then
|
|
28
|
+
if npx playwright install chromium >/dev/null 2>&1; then
|
|
29
|
+
browsers='["chromium"]'
|
|
30
|
+
else
|
|
31
|
+
echo '{"installed": true, "browsers": [], "error": "Failed to install chromium browser"}'
|
|
32
|
+
exit 1
|
|
33
|
+
fi
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
cat <<EOF
|
|
37
|
+
{"installed": $installed, "browsers": $browsers}
|
|
38
|
+
EOF
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Run playwright tests and produce a compact JSON summary.
|
|
3
|
+
# Usage: run-e2e-tests.sh <test_dir> <base_url> [test_filter]
|
|
4
|
+
# Output: Compact JSON summary on stdout
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
TEST_DIR="$1"
|
|
8
|
+
BASE_URL="$2"
|
|
9
|
+
TEST_FILTER="${3:-}"
|
|
10
|
+
RESULTS_DIR="${TEST_DIR}/../results"
|
|
11
|
+
SCREENSHOTS_DIR="${TEST_DIR}/../screenshots"
|
|
12
|
+
|
|
13
|
+
mkdir -p "$RESULTS_DIR" "$SCREENSHOTS_DIR"
|
|
14
|
+
|
|
15
|
+
# Build playwright command
|
|
16
|
+
PW_ARGS=(
|
|
17
|
+
test
|
|
18
|
+
"$TEST_DIR"
|
|
19
|
+
--reporter=json
|
|
20
|
+
--output="$RESULTS_DIR"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
if [ -n "$TEST_FILTER" ]; then
|
|
24
|
+
PW_ARGS+=(--grep "$TEST_FILTER")
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# Run playwright, capture JSON output
|
|
28
|
+
RAW_OUTPUT="$RESULTS_DIR/raw-results.json"
|
|
29
|
+
set +e
|
|
30
|
+
BASE_URL="$BASE_URL" npx playwright "${PW_ARGS[@]}" > "$RAW_OUTPUT" 2>/dev/null
|
|
31
|
+
PW_EXIT=$?
|
|
32
|
+
set -e
|
|
33
|
+
|
|
34
|
+
# If no JSON output was produced, create a minimal error report
|
|
35
|
+
if [ ! -s "$RAW_OUTPUT" ]; then
|
|
36
|
+
cat <<EOF
|
|
37
|
+
{"total": 0, "passed": 0, "failed": 0, "skipped": 0, "duration": 0, "failures": [], "error": "Playwright produced no output (exit code: $PW_EXIT)"}
|
|
38
|
+
EOF
|
|
39
|
+
exit 0
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
# Parse the JSON output into compact summary using node (more reliable than jq)
|
|
43
|
+
node -e "
|
|
44
|
+
const fs = require('fs');
|
|
45
|
+
const raw = JSON.parse(fs.readFileSync('$RAW_OUTPUT', 'utf-8'));
|
|
46
|
+
|
|
47
|
+
const suites = raw.suites || [];
|
|
48
|
+
const results = [];
|
|
49
|
+
|
|
50
|
+
function collectTests(suite, parentTitle) {
|
|
51
|
+
const title = parentTitle ? parentTitle + ' > ' + suite.title : suite.title;
|
|
52
|
+
for (const spec of (suite.specs || [])) {
|
|
53
|
+
for (const test of (spec.tests || [])) {
|
|
54
|
+
for (const result of (test.results || [])) {
|
|
55
|
+
results.push({
|
|
56
|
+
test: title + ' > ' + spec.title,
|
|
57
|
+
file: spec.file + (spec.line ? ':' + spec.line : ''),
|
|
58
|
+
status: result.status,
|
|
59
|
+
duration: result.duration || 0,
|
|
60
|
+
error: result.error?.message || null,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
for (const child of (suite.suites || [])) {
|
|
66
|
+
collectTests(child, title);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (const suite of suites) {
|
|
71
|
+
collectTests(suite, '');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const passed = results.filter(r => r.status === 'passed').length;
|
|
75
|
+
const failed = results.filter(r => r.status === 'failed' || r.status === 'timedOut').length;
|
|
76
|
+
const skipped = results.filter(r => r.status === 'skipped').length;
|
|
77
|
+
const totalDuration = results.reduce((sum, r) => sum + r.duration, 0);
|
|
78
|
+
|
|
79
|
+
const failures = results
|
|
80
|
+
.filter(r => r.status === 'failed' || r.status === 'timedOut')
|
|
81
|
+
.map(r => ({
|
|
82
|
+
test: r.test,
|
|
83
|
+
file: r.file,
|
|
84
|
+
error: r.error || 'Unknown error',
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
const summary = {
|
|
88
|
+
total: results.length,
|
|
89
|
+
passed,
|
|
90
|
+
failed,
|
|
91
|
+
skipped,
|
|
92
|
+
duration: totalDuration,
|
|
93
|
+
failures,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
console.log(JSON.stringify(summary));
|
|
97
|
+
" 2>/dev/null || cat <<EOF
|
|
98
|
+
{"total": 0, "passed": 0, "failed": 0, "skipped": 0, "duration": 0, "failures": [], "error": "Failed to parse playwright output"}
|
|
99
|
+
EOF
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Start the dev server in the background and wait for it to be ready.
|
|
3
|
+
# Usage: start-dev-server.sh <cwd> <dev_command> <port> <timeout_seconds> <session_dir>
|
|
4
|
+
# Output: JSON on stdout
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
CWD="$1"
|
|
8
|
+
DEV_COMMAND="$2"
|
|
9
|
+
PORT="$3"
|
|
10
|
+
TIMEOUT="${4:-60}"
|
|
11
|
+
SESSION_DIR="${5:-.}"
|
|
12
|
+
|
|
13
|
+
cd "$CWD"
|
|
14
|
+
|
|
15
|
+
# Check if port is already in use
|
|
16
|
+
if curl -s -o /dev/null -w "%{http_code}" "http://localhost:$PORT" 2>/dev/null | grep -qE '^[0-9]'; then
|
|
17
|
+
echo "{\"pid\": null, \"url\": \"http://localhost:$PORT\", \"ready\": true, \"note\": \"Server already running\"}"
|
|
18
|
+
exit 0
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
# Start dev server in background
|
|
22
|
+
eval "$DEV_COMMAND" > "$SESSION_DIR/dev-server.log" 2>&1 &
|
|
23
|
+
PID=$!
|
|
24
|
+
echo "$PID" > "$SESSION_DIR/dev-server.pid"
|
|
25
|
+
|
|
26
|
+
# Wait for server to be ready
|
|
27
|
+
for i in $(seq 1 "$TIMEOUT"); do
|
|
28
|
+
# Check if process is still alive
|
|
29
|
+
if ! kill -0 "$PID" 2>/dev/null; then
|
|
30
|
+
echo "{\"pid\": $PID, \"url\": \"http://localhost:$PORT\", \"ready\": false, \"error\": \"Server process exited\"}"
|
|
31
|
+
exit 1
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# Check if port responds
|
|
35
|
+
if curl -s -o /dev/null "http://localhost:$PORT" 2>/dev/null; then
|
|
36
|
+
echo "{\"pid\": $PID, \"url\": \"http://localhost:$PORT\", \"ready\": true}"
|
|
37
|
+
exit 0
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
sleep 1
|
|
41
|
+
done
|
|
42
|
+
|
|
43
|
+
# Timeout — kill the server
|
|
44
|
+
kill "$PID" 2>/dev/null || true
|
|
45
|
+
echo "{\"pid\": $PID, \"url\": \"http://localhost:$PORT\", \"ready\": false, \"error\": \"Timeout after ${TIMEOUT}s\"}"
|
|
46
|
+
exit 1
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Stop the dev server started by start-dev-server.sh.
|
|
3
|
+
# Usage: stop-dev-server.sh <session_dir>
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
SESSION_DIR="${1:-.}"
|
|
7
|
+
PID_FILE="$SESSION_DIR/dev-server.pid"
|
|
8
|
+
|
|
9
|
+
if [ ! -f "$PID_FILE" ]; then
|
|
10
|
+
echo '{"stopped": false, "error": "No PID file found"}'
|
|
11
|
+
exit 0
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
PID=$(cat "$PID_FILE")
|
|
15
|
+
|
|
16
|
+
if [ -z "$PID" ]; then
|
|
17
|
+
echo '{"stopped": false, "error": "Empty PID file"}'
|
|
18
|
+
exit 0
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
# Kill the process and its children
|
|
22
|
+
if kill -0 "$PID" 2>/dev/null; then
|
|
23
|
+
# Kill process group if possible
|
|
24
|
+
kill -- -"$PID" 2>/dev/null || kill "$PID" 2>/dev/null || true
|
|
25
|
+
# Wait briefly for cleanup
|
|
26
|
+
sleep 1
|
|
27
|
+
# Force kill if still alive
|
|
28
|
+
if kill -0 "$PID" 2>/dev/null; then
|
|
29
|
+
kill -9 "$PID" 2>/dev/null || true
|
|
30
|
+
fi
|
|
31
|
+
rm -f "$PID_FILE"
|
|
32
|
+
echo "{\"stopped\": true, \"pid\": $PID}"
|
|
33
|
+
else
|
|
34
|
+
rm -f "$PID_FILE"
|
|
35
|
+
echo "{\"stopped\": true, \"pid\": $PID, \"note\": \"Process was already dead\"}"
|
|
36
|
+
fi
|