@ai-qa/workflow 2.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.
Files changed (48) hide show
  1. package/.github/agents/playwright-test-generator.agent.md +33 -0
  2. package/.github/agents/playwright-test-healer.agent.md +36 -0
  3. package/.github/agents/playwright-test-planner.agent.md +44 -0
  4. package/.opencode/agents/qa-generator.md +19 -0
  5. package/.opencode/agents/qa-healer.md +25 -0
  6. package/.opencode/agents/qa-planner.md +20 -0
  7. package/.qa-workflow.json +22 -0
  8. package/README.md +365 -0
  9. package/ai-qa-workflow.js +330 -0
  10. package/cli.js +7 -0
  11. package/docs/application-context.md +20 -0
  12. package/install.js +303 -0
  13. package/opencode.json +31 -0
  14. package/package.json +30 -0
  15. package/prompts/QAe2eprompt.md +513 -0
  16. package/prompts/general_prompt.md +13 -0
  17. package/qa-dashboard/.env +3 -0
  18. package/qa-dashboard/app.js +46 -0
  19. package/qa-dashboard/package.json +18 -0
  20. package/qa-dashboard/public/css/style.css +266 -0
  21. package/qa-dashboard/public/js/main.js +6 -0
  22. package/qa-dashboard/routes/analytics.js +52 -0
  23. package/qa-dashboard/routes/export.js +153 -0
  24. package/qa-dashboard/routes/index.js +10 -0
  25. package/qa-dashboard/routes/projects.js +92 -0
  26. package/qa-dashboard/routes/runs.js +66 -0
  27. package/qa-dashboard/routes/stories.js +101 -0
  28. package/qa-dashboard/routes/test-data.js +82 -0
  29. package/qa-dashboard/services/cli-bridge.js +143 -0
  30. package/qa-dashboard/services/project-manager.js +61 -0
  31. package/qa-dashboard/views/analytics.ejs +188 -0
  32. package/qa-dashboard/views/error.ejs +8 -0
  33. package/qa-dashboard/views/index.ejs +83 -0
  34. package/qa-dashboard/views/layouts/main.ejs +28 -0
  35. package/qa-dashboard/views/project-add.ejs +23 -0
  36. package/qa-dashboard/views/project.ejs +97 -0
  37. package/qa-dashboard/views/projects.ejs +29 -0
  38. package/qa-dashboard/views/run.ejs +84 -0
  39. package/qa-dashboard/views/runs.ejs +64 -0
  40. package/qa-dashboard/views/stories.ejs +53 -0
  41. package/qa-dashboard/views/story.ejs +63 -0
  42. package/qa-dashboard/views/test-data.ejs +117 -0
  43. package/scripts/executor.js +142 -0
  44. package/scripts/generator.js +130 -0
  45. package/scripts/healer.js +207 -0
  46. package/scripts/planner.js +142 -0
  47. package/scripts/reporter.js +190 -0
  48. package/scripts/utils.js +244 -0
@@ -0,0 +1,28 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title><%= title || 'QA Dashboard' %> — AI QA Pipeline</title>
7
+ <link rel="stylesheet" href="/css/style.css">
8
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
9
+ </head>
10
+ <body>
11
+ <nav class="navbar">
12
+ <div class="nav-inner">
13
+ <a href="/" class="nav-brand">AI QA Pipeline</a>
14
+ <div class="nav-links">
15
+ <a href="/projects">Projects</a>
16
+ <a href="/stories">Stories</a>
17
+ <a href="/runs">Runs</a>
18
+ <a href="/analytics">Analytics</a>
19
+ <a href="/data">Test Data</a>
20
+ </div>
21
+ </div>
22
+ </nav>
23
+ <main class="container">
24
+ <%- body %>
25
+ </main>
26
+ <script src="/js/main.js"></script>
27
+ </body>
28
+ </html>
@@ -0,0 +1,23 @@
1
+ <% layout = 'layouts/main' %>
2
+ <% title = 'Add Project' %>
3
+
4
+ <div class="page-header">
5
+ <h1>Add Project</h1>
6
+ </div>
7
+
8
+ <form method="POST" class="form">
9
+ <% if (typeof error !== 'undefined') { %>
10
+ <div class="alert alert-error"><%= error %></div>
11
+ <% } %>
12
+ <div class="form-group">
13
+ <label>Project Name</label>
14
+ <input type="text" name="name" placeholder="e.g. test_wakil" required>
15
+ </div>
16
+ <div class="form-group">
17
+ <label>Project Path</label>
18
+ <input type="text" name="path" placeholder="e.g. C:\Users\aitbe\test_wakil" required>
19
+ <small>Full path to the test project root (where ai-qa-workflow.js lives)</small>
20
+ </div>
21
+ <button type="submit" class="btn btn-primary">Add Project</button>
22
+ <a href="/projects" class="btn">Cancel</a>
23
+ </form>
@@ -0,0 +1,97 @@
1
+ <% layout = 'layouts/main' %>
2
+ <% title = project.name %>
3
+
4
+ <div class="page-header">
5
+ <h1><%= project.name %></h1>
6
+ <div class="header-actions">
7
+ <a href="/stories?project=<%= project.id %>" class="btn">Stories</a>
8
+ <a href="/runs?project=<%= project.id %>" class="btn">Runs</a>
9
+ <a href="/analytics?project=<%= project.id %>" class="btn">Analytics</a>
10
+ <a href="/data?project=<%= project.id %>" class="btn">Test Data</a>
11
+ </div>
12
+ </div>
13
+
14
+ <div class="project-detail">
15
+ <p class="project-path"><%= project.path %></p>
16
+ </div>
17
+
18
+ <div class="row">
19
+ <div class="col">
20
+ <div class="card">
21
+ <h3>User Stories</h3>
22
+ <div class="big-number"><%= list.stories.length %></div>
23
+ <a href="/stories?project=<%= project.id %>" class="btn btn-sm">View</a>
24
+ </div>
25
+ </div>
26
+ <div class="col">
27
+ <div class="card">
28
+ <h3>Test Plans</h3>
29
+ <div class="big-number"><%= list.plans.length %></div>
30
+ </div>
31
+ </div>
32
+ <div class="col">
33
+ <div class="card">
34
+ <h3>Test Specs</h3>
35
+ <div class="big-number"><%= list.specs.length %></div>
36
+ </div>
37
+ </div>
38
+ </div>
39
+
40
+ <div class="card">
41
+ <h3>Pipeline Execution</h3>
42
+ <form id="pipeline-form" data-project="<%= project.id %>">
43
+ <div class="form-group">
44
+ <label>User Story</label>
45
+ <select name="storyName" class="form-input">
46
+ <option value="">— Run status check —</option>
47
+ <% list.stories.forEach(s => { %>
48
+ <option value="<%= s %>"><%= s %></option>
49
+ <% }) %>
50
+ </select>
51
+ </div>
52
+ <button type="submit" class="btn btn-primary">Run Pipeline</button>
53
+ </form>
54
+ <div id="pipeline-output" class="output-box"></div>
55
+ </div>
56
+
57
+ <div class="card">
58
+ <h3>Quick Actions</h3>
59
+ <div class="action-buttons">
60
+ <button class="btn btn-sm" onclick="runAction('<%= project.id %>', 'status')">Status</button>
61
+ <button class="btn btn-sm" onclick="runAction('<%= project.id %>', 'execute')">Execute All Tests</button>
62
+ <button class="btn btn-sm" onclick="runAction('<%= project.id %>', 'report')">Generate Report</button>
63
+ </div>
64
+ <div id="quick-output" class="output-box"></div>
65
+ </div>
66
+
67
+ <script>
68
+ document.getElementById('pipeline-form').addEventListener('submit', async (e) => {
69
+ e.preventDefault();
70
+ const form = e.target;
71
+ const projectId = form.dataset.project;
72
+ const formData = new FormData(form);
73
+ const output = document.getElementById('pipeline-output');
74
+ output.textContent = 'Running pipeline...';
75
+ output.className = 'output-box output-running';
76
+
77
+ const res = await fetch(`/projects/${projectId}/run-pipeline`, {
78
+ method: 'POST',
79
+ headers: { 'Content-Type': 'application/json' },
80
+ body: JSON.stringify({ storyName: formData.get('storyName') })
81
+ });
82
+ const data = await res.json();
83
+ output.textContent = data.output || data.error || 'Done';
84
+ output.className = `output-box ${data.success ? 'output-success' : 'output-error'}`;
85
+ });
86
+
87
+ async function runAction(projectId, action) {
88
+ const output = document.getElementById('quick-output');
89
+ output.textContent = `Running ${action}...`;
90
+ output.className = 'output-box output-running';
91
+
92
+ const res = await fetch(`/projects/${projectId}/${action}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' });
93
+ const data = await res.json();
94
+ output.textContent = data.output || data.error || 'Done';
95
+ output.className = `output-box ${data.success ? 'output-success' : 'output-error'}`;
96
+ }
97
+ </script>
@@ -0,0 +1,29 @@
1
+ <% layout = 'layouts/main' %>
2
+ <% title = 'Projects' %>
3
+
4
+ <div class="page-header">
5
+ <h1>Projects</h1>
6
+ <a href="/projects/add" class="btn btn-primary">+ Add Project</a>
7
+ </div>
8
+
9
+ <% if (projects.length === 0) { %>
10
+ <div class="empty-state">
11
+ <p>No projects yet. Add your test project (e.g. test_wakil) to connect the AI QA pipeline.</p>
12
+ </div>
13
+ <% } %>
14
+
15
+ <div class="project-grid">
16
+ <% projects.forEach(p => { %>
17
+ <div class="project-card">
18
+ <div class="project-name"><%= p.name %></div>
19
+ <div class="project-path"><%= p.path %></div>
20
+ <div class="project-meta">Added: <%= new Date(p.createdAt).toLocaleDateString() %></div>
21
+ <div class="project-actions">
22
+ <a href="/projects/<%= p.id %>" class="btn btn-sm">Open</a>
23
+ <form action="/projects/<%= p.id %>/remove" method="POST" style="display:inline">
24
+ <button class="btn btn-sm btn-danger">Remove</button>
25
+ </form>
26
+ </div>
27
+ </div>
28
+ <% }) %>
29
+ </div>
@@ -0,0 +1,84 @@
1
+ <% layout = 'layouts/main' %>
2
+ <% title = 'Run: ' + run.id %>
3
+
4
+ <div class="page-header">
5
+ <h1>Run: <%= run.id %></h1>
6
+ <div class="header-actions">
7
+ <span class="badge <%= run.result.success ? 'badge-pass' : 'badge-fail' %>"><%= run.result.success ? 'PASSED' : 'FAILED' %></span>
8
+ <a href="/runs?project=<%= project.id %>" class="btn btn-sm">← Back</a>
9
+ </div>
10
+ </div>
11
+
12
+ <div class="row">
13
+ <div class="col col-30">
14
+ <div class="card">
15
+ <h3>Summary</h3>
16
+ <table class="info-table">
17
+ <tr><td>Status</td><td><%= run.result.success ? '✅ Passed' : '❌ Failed' %></td></tr>
18
+ <tr><td>Duration</td><td><%= (run.result.duration / 1000).toFixed(1) %>s</td></tr>
19
+ <tr><td>Test</td><td><%= run.result.test %></td></tr>
20
+ <tr><td>Timestamp</td><td><%= new Date(run.result.timestamp).toLocaleString() %></td></tr>
21
+ <tr><td>Failures</td><td><%= run.result.failedTests ? run.result.failedTests.length : 0 %></td></tr>
22
+ </table>
23
+ </div>
24
+
25
+ <div class="card">
26
+ <h3>Report Actions</h3>
27
+ <div class="action-buttons-vertical">
28
+ <% if (run.report) { %>
29
+ <a href="/export/report/<%= run.id %>?project=<%= project.id %>" class="btn btn-primary" target="_blank">View Full Report</a>
30
+ <a href="/export/pdf/<%= run.id %>?project=<%= project.id %>" class="btn">Download Report (.txt)</a>
31
+ <% } else { %>
32
+ <p style="color:var(--text-muted);font-size:0.85rem;">No report available yet. Generate one from the pipeline.</p>
33
+ <% } %>
34
+ <% if (run.hasAllure) { %>
35
+ <a href="/allure-report?project=<%= project.id %>" class="btn" target="_blank">Open Allure Report ↗</a>
36
+ <% } %>
37
+ </div>
38
+ </div>
39
+
40
+ <% if (run.healing) { %>
41
+ <div class="card">
42
+ <h3>Self-Healing</h3>
43
+ <table class="info-table">
44
+ <tr><td>Attempts</td><td><%= run.healing.healingAttempts ? run.healing.healingAttempts.length : 0 %></td></tr>
45
+ <tr><td>Healed</td><td><%= run.healing.totalHealed || 0 %></td></tr>
46
+ <tr><td>Remaining</td><td><%= run.healing.totalRemaining || 0 %></td></tr>
47
+ </table>
48
+ <% if (run.healing.healingAttempts) { %>
49
+ <h4>Attempts</h4>
50
+ <% run.healing.healingAttempts.forEach(a => { %>
51
+ <div class="heal-attempt">
52
+ Attempt <%= a.attempt %>: <%= a.healed.length %> healed, <%= a.stillFailing.length %> remaining
53
+ </div>
54
+ <% }) %>
55
+ <% } %>
56
+ </div>
57
+ <% } %>
58
+ </div>
59
+
60
+ <div class="col col-70">
61
+ <% if (run.result.failedTests && run.result.failedTests.length > 0) { %>
62
+ <div class="card">
63
+ <h3>Failed Tests</h3>
64
+ <ul class="fail-list">
65
+ <% run.result.failedTests.forEach(ft => { %>
66
+ <li><strong><%= ft.test || ft.file %></strong><br><%= ft.error ? ft.error.substring(0, 200) : 'No details' %></li>
67
+ <% }) %>
68
+ </ul>
69
+ </div>
70
+ <% } %>
71
+
72
+ <% if (run.report) { %>
73
+ <div class="card">
74
+ <h3>Test Report</h3>
75
+ <div class="markdown-content"><%- run.report.replace(/\n/g, '<br>') %></div>
76
+ </div>
77
+ <% } %>
78
+
79
+ <div class="card">
80
+ <h3>Execution Output</h3>
81
+ <pre class="code-block"><%= run.result.output || 'No output captured' %></pre>
82
+ </div>
83
+ </div>
84
+ </div>
@@ -0,0 +1,64 @@
1
+ <% layout = 'layouts/main' %>
2
+ <% title = 'Execution Runs' %>
3
+
4
+ <div class="page-header">
5
+ <h1>Execution Runs</h1>
6
+ <div class="header-actions">
7
+ <span class="badge badge-project"><%= project.name %></span>
8
+ <a href="/analytics?project=<%= project.id %>" class="btn btn-sm">Analytics</a>
9
+ </div>
10
+ </div>
11
+
12
+ <% if (runs.length === 0) { %>
13
+ <div class="empty-state">
14
+ <p>No test executions yet. Run the pipeline from the Stories page.</p>
15
+ <a href="/stories?project=<%= project.id %>" class="btn btn-primary">View Stories</a>
16
+ </div>
17
+ <% } %>
18
+
19
+ <div class="run-list">
20
+ <% runs.forEach(r => { %>
21
+ <div class="run-card-wrapper <%= r.success ? 'run-passed' : 'run-failed' %>">
22
+ <a href="/runs/<%= r.id %>?project=<%= project.id %>" class="run-card-main">
23
+ <div class="run-icon"><%= r.success ? '✅' : '❌' %></div>
24
+ <div class="run-info">
25
+ <div class="run-name"><%= r.id %></div>
26
+ <div class="run-test"><%= r.testName %></div>
27
+ <div class="run-meta">
28
+ <%= r.success ? 'Passed' : 'Failed' %> ·
29
+ <%= r.duration ? (r.duration / 1000).toFixed(1) + 's' : 'N/A' %> ·
30
+ <%= r.failedCount %> failure(s) ·
31
+ <%= r.healedCount > 0 ? r.healedCount + ' healed' : 'no healing' %>
32
+ </div>
33
+ </div>
34
+ </a>
35
+ <div class="run-actions">
36
+ <% if (r.hasReport) { %>
37
+ <a href="/export/report/<%= r.id %>?project=<%= project.id %>" class="btn btn-sm btn-primary" target="_blank">View Report</a>
38
+ <a href="/export/pdf/<%= r.id %>?project=<%= project.id %>" class="btn btn-sm">Download</a>
39
+ <% } else { %>
40
+ <span class="run-no-report">⏳ No report</span>
41
+ <% } %>
42
+ <% if (r.hasAllure) { %>
43
+ <a href="/allure-report?project=<%= project.id %>" class="btn btn-sm" target="_blank">Allure Report</a>
44
+ <% } %>
45
+ </div>
46
+ </div>
47
+ <% }) %>
48
+ </div>
49
+
50
+ <style>
51
+ .run-card-wrapper {
52
+ display: flex; align-items: center; gap: 1rem;
53
+ background: var(--surface); border: 1px solid var(--border);
54
+ border-radius: var(--radius); padding: 0.75rem 1.25rem;
55
+ transition: border-color 0.2s;
56
+ }
57
+ .run-card-wrapper:hover { border-color: var(--primary); }
58
+ .run-card-main {
59
+ display: flex; align-items: center; gap: 1rem;
60
+ text-decoration: none; color: var(--text); flex: 1;
61
+ }
62
+ .run-actions { display: flex; gap: 0.5rem; align-items: center; white-space: nowrap; }
63
+ .run-no-report { color: var(--text-muted); font-size: 0.8rem; }
64
+ </style>
@@ -0,0 +1,53 @@
1
+ <% layout = 'layouts/main' %>
2
+ <% title = 'User Stories' %>
3
+
4
+ <div class="page-header">
5
+ <h1>User Stories</h1>
6
+ <span class="badge badge-project"><%= project.name %></span>
7
+ </div>
8
+
9
+ <div class="story-list">
10
+ <% stories.forEach(s => { %>
11
+ <div class="story-card">
12
+ <div class="story-header">
13
+ <a href="/stories/<%= s.name %>?project=<%= project.id %>" class="story-title"><%= s.name %></a>
14
+ <div class="story-badges">
15
+ <% if (s.hasPlan) { %><span class="badge badge-plan">Planned</span><% } %>
16
+ <% if (s.hasSpec) { %><span class="badge badge-spec">Tested</span><% } %>
17
+ <% if (!s.hasPlan) { %><span class="badge badge-draft">Draft</span><% } %>
18
+ </div>
19
+ </div>
20
+ <div class="story-preview">
21
+ <% if (s.content) { %>
22
+ <% const lines = s.content.split('\n').filter(l => l.startsWith('##') || l.startsWith('**')) %>
23
+ <% lines.slice(0, 3).forEach(l => { %><p><%= l.replace(/\*\*/g, '') %></p><% }) %>
24
+ <% } %>
25
+ </div>
26
+ <div class="story-actions">
27
+ <button class="btn btn-sm" onclick="runPipeline('<%= project.id %>', '<%= s.name %>', this)">Full Pipeline</button>
28
+ <span class="story-action-output"></span>
29
+ </div>
30
+ </div>
31
+ <% }) %>
32
+ </div>
33
+
34
+ <script>
35
+ async function runPipeline(projectId, storyName, btn) {
36
+ btn.disabled = true;
37
+ btn.textContent = 'Running...';
38
+ const outputSpan = btn.parentElement.querySelector('.story-action-output');
39
+
40
+ const res = await fetch(`/stories/${encodeURIComponent(storyName)}/full-pipeline?project=${projectId}`, {
41
+ method: 'POST',
42
+ headers: { 'Content-Type': 'application/json' },
43
+ });
44
+ const data = await res.json();
45
+
46
+ outputSpan.textContent = data.success ? '✓ Done' : '✗ Failed';
47
+ outputSpan.className = data.success ? 'output-success' : 'output-error';
48
+ btn.disabled = false;
49
+ btn.textContent = 'Full Pipeline';
50
+
51
+ setTimeout(() => { outputSpan.textContent = ''; }, 5000);
52
+ }
53
+ </script>
@@ -0,0 +1,63 @@
1
+ <% layout = 'layouts/main' %>
2
+ <% title = story.name %>
3
+
4
+ <div class="page-header">
5
+ <h1><%= story.name %></h1>
6
+ <span class="badge badge-project"><%= project.name %></span>
7
+ </div>
8
+
9
+ <div class="row">
10
+ <div class="col col-40">
11
+ <div class="card">
12
+ <h3>User Story</h3>
13
+ <div class="markdown-content"><%- story.content.replace(/\n/g, '<br>') %></div>
14
+ </div>
15
+
16
+ <div class="card">
17
+ <h3>Pipeline Controls</h3>
18
+ <div class="action-buttons-vertical">
19
+ <button class="btn btn-sm" onclick="triggerPlan('<%= story.name %>')">1. Generate Plan</button>
20
+ <button class="btn btn-sm" onclick="triggerGenerate('<%= story.name %>')">2. Generate Test Spec</button>
21
+ <button class="btn btn-sm" onclick="triggerExecute('<%= story.name %>')">3. Execute Tests</button>
22
+ <button class="btn btn-sm btn-primary" onclick="triggerFull('<%= story.name %>')">▶ Full Pipeline</button>
23
+ <a href="/runs?project=<%= project.id %>" class="btn btn-sm" style="margin-top:0.5rem;">View Test Results →</a>
24
+ </div>
25
+ <div id="pipeline-status" class="output-box"></div>
26
+ </div>
27
+ </div>
28
+
29
+ <div class="col col-60">
30
+ <% if (story.plan) { %>
31
+ <div class="card">
32
+ <h3>Test Plan <a href="/stories/<%= story.name %>?project=<%= project.id %>&tab=plan" class="btn btn-sm">Refresh</a></h3>
33
+ <div class="markdown-content"><%- story.plan.replace(/\n/g, '<br>') %></div>
34
+ </div>
35
+ <% } %>
36
+
37
+ <% if (story.spec) { %>
38
+ <div class="card">
39
+ <h3>Test Spec: <%= story.specName %></h3>
40
+ <pre class="code-block"><%= story.spec %></pre>
41
+ </div>
42
+ <% } %>
43
+ </div>
44
+ </div>
45
+
46
+ <script>
47
+ async function callPipeline(action, storyName) {
48
+ const status = document.getElementById('pipeline-status');
49
+ status.textContent = `${action}...`;
50
+ status.className = 'output-box output-running';
51
+
52
+ const res = await fetch(`/stories/${encodeURIComponent(storyName)}/${action}?project=<%= project.id %>`, { method: 'POST' });
53
+ const data = await res.json();
54
+ status.textContent = data.output || data.error || 'Done';
55
+ status.className = `output-box ${data.success ? 'output-success' : 'output-error'}`;
56
+ setTimeout(() => { if (data.success) location.reload(); }, 1500);
57
+ }
58
+
59
+ function triggerPlan(s) { callPipeline('plan', s); }
60
+ function triggerGenerate(s) { callPipeline('generate', s); }
61
+ function triggerExecute(s) { callPipeline('execute', s); }
62
+ function triggerFull(s) { callPipeline('full-pipeline', s); }
63
+ </script>
@@ -0,0 +1,117 @@
1
+ <% layout = 'layouts/main' %>
2
+ <% title = project ? 'Test Data - ' + project.name : 'Test Data Management' %>
3
+
4
+ <div class="page-header">
5
+ <h1>Test Data Management</h1>
6
+ <div class="header-actions">
7
+ <% if (typeof projects !== 'undefined' && projects.length > 1) { %>
8
+ <select id="project-select" class="form-input" style="width:auto;" onchange="window.location='/data?project='+this.value">
9
+ <% projects.forEach(p => { %>
10
+ <option value="<%= p.id %>" <%= project && project.id === p.id ? 'selected' : '' %>><%= p.name %></option>
11
+ <% }) %>
12
+ </select>
13
+ <% } %>
14
+ <% if (project) { %>
15
+ <a href="/projects/<%= project.id %>" class="btn">Back</a>
16
+ <% } %>
17
+ </div>
18
+ </div>
19
+
20
+ <div class="alerts" id="alert-container"></div>
21
+
22
+ <div class="row">
23
+ <div class="col-40">
24
+ <div class="card">
25
+ <h3>Generate Test Data</h3>
26
+ <p style="color: var(--text-muted); font-size: 0.85rem; margin-bottom: 1rem;">
27
+ Auto-generate realistic test data for your QA pipeline.
28
+ </p>
29
+ <form id="data-form">
30
+ <div class="form-group">
31
+ <label>Data Type</label>
32
+ <select id="data-type" class="form-input">
33
+ <option value="users">Users</option>
34
+ <option value="transactions">Transactions</option>
35
+ </select>
36
+ </div>
37
+ <div class="form-group">
38
+ <label>Record Count</label>
39
+ <input type="number" id="data-count" class="form-input" value="5" min="1" max="100">
40
+ </div>
41
+ <button type="submit" class="btn btn-primary">Generate</button>
42
+ </form>
43
+ <div id="data-output" class="output-box" style="display:none;"></div>
44
+ </div>
45
+ </div>
46
+ <div class="col-60">
47
+ <div class="card">
48
+ <h3>Generated Data Files</h3>
49
+ <% if (dataFiles.length === 0) { %>
50
+ <div class="empty-state" style="padding:2rem 0;">
51
+ <p>No data files generated yet</p>
52
+ </div>
53
+ <% } else { %>
54
+ <table class="info-table">
55
+ <tr><th>File</th><th>Size</th><th>Modified</th><th></th></tr>
56
+ <% dataFiles.sort((a,b) => new Date(b.modified) - new Date(a.modified)).forEach(f => { %>
57
+ <tr>
58
+ <td><code><%= f.name %></code></td>
59
+ <td><%= f.size > 1024 ? (f.size / 1024).toFixed(1) + ' KB' : f.size + ' B' %></td>
60
+ <td><%= new Date(f.modified).toLocaleString() %></td>
61
+ <td><button class="btn btn-sm btn-danger" onclick="deleteFile('<%= f.name %>')">Delete</button></td>
62
+ </tr>
63
+ <% }) %>
64
+ </table>
65
+ <% } %>
66
+ </div>
67
+ </div>
68
+ </div>
69
+
70
+ <div class="card">
71
+ <h3>About Test Data Management</h3>
72
+ <p style="color: var(--text-muted); font-size: 0.85rem;">
73
+ Generated test data is stored in <code>data/</code> as JSON files. These can be:
74
+ </p>
75
+ <ul style="color: var(--text-muted); font-size: 0.85rem; padding-left: 1.5rem; margin-top: 0.5rem;">
76
+ <li>Used by Playwright tests via <code>fs.readFileSync()</code></li>
77
+ <li>Imported into your test database for seeding</li>
78
+ <li>Downloaded and shared with your team</li>
79
+ </ul>
80
+ </div>
81
+
82
+ <script>
83
+ document.getElementById('data-form').addEventListener('submit', async (e) => {
84
+ e.preventDefault();
85
+ const type = document.getElementById('data-type').value;
86
+ const count = document.getElementById('data-count').value;
87
+ const output = document.getElementById('data-output');
88
+
89
+ output.style.display = 'block';
90
+ output.textContent = 'Generating...';
91
+ output.className = 'output-box output-running';
92
+
93
+ const res = await fetch('/data/generate', {
94
+ method: 'POST',
95
+ headers: { 'Content-Type': 'application/json' },
96
+ body: JSON.stringify({ type, count })
97
+ });
98
+ const data = await res.json();
99
+
100
+ if (data.success) {
101
+ output.textContent = `Generated ${data.count} records → ${data.file}`;
102
+ output.className = 'output-box output-success';
103
+ setTimeout(() => location.reload(), 1500);
104
+ } else {
105
+ output.textContent = 'Error: ' + data.error;
106
+ output.className = 'output-box output-error';
107
+ }
108
+ });
109
+
110
+ async function deleteFile(name) {
111
+ if (!confirm(`Delete ${name}?`)) return;
112
+ const res = await fetch('/data/' + encodeURIComponent(name), { method: 'DELETE' });
113
+ const data = await res.json();
114
+ if (data.success) location.reload();
115
+ else alert('Delete failed');
116
+ }
117
+ </script>