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.
Files changed (37) hide show
  1. package/package.json +1 -1
  2. package/skills/fix-pr/SKILL.md +99 -0
  3. package/skills/qa-strategy/SKILL.md +103 -21
  4. package/src/commands/fix-pr.ts +324 -0
  5. package/src/commands/qa.ts +232 -148
  6. package/src/commands/supi.ts +2 -1
  7. package/src/config/defaults.ts +1 -0
  8. package/src/config/schema.ts +1 -0
  9. package/src/fix-pr/config.ts +36 -0
  10. package/src/fix-pr/prompt-builder.ts +201 -0
  11. package/src/fix-pr/scripts/diff-comments.sh +33 -0
  12. package/src/fix-pr/scripts/fetch-pr-comments.sh +25 -0
  13. package/src/fix-pr/scripts/trigger-review.sh +36 -0
  14. package/src/fix-pr/scripts/wait-and-check.sh +37 -0
  15. package/src/fix-pr/types.ts +71 -0
  16. package/src/index.ts +2 -0
  17. package/src/qa/config.ts +43 -0
  18. package/src/qa/matrix.ts +84 -0
  19. package/src/qa/prompt-builder.ts +212 -0
  20. package/src/qa/scripts/detect-app-type.sh +68 -0
  21. package/src/qa/scripts/discover-routes.sh +143 -0
  22. package/src/qa/scripts/ensure-playwright.sh +38 -0
  23. package/src/qa/scripts/run-e2e-tests.sh +99 -0
  24. package/src/qa/scripts/start-dev-server.sh +46 -0
  25. package/src/qa/scripts/stop-dev-server.sh +36 -0
  26. package/src/qa/session.ts +39 -55
  27. package/src/qa/types.ts +97 -0
  28. package/src/storage/fix-pr-sessions.ts +59 -0
  29. package/src/storage/qa-sessions.ts +9 -9
  30. package/src/types.ts +1 -70
  31. package/src/qa/detector.ts +0 -61
  32. package/src/qa/phases/discovery.ts +0 -34
  33. package/src/qa/phases/execution.ts +0 -65
  34. package/src/qa/phases/matrix.ts +0 -41
  35. package/src/qa/phases/reporting.ts +0 -71
  36. package/src/qa/report.ts +0 -22
  37. 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