@ai-qa/workflow 2.0.17 β 2.0.18
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/.opencode/qa-workflow-skill.md +232 -0
- package/PROJECT_GUIDE.md +114 -74
- package/install.js +43 -6
- package/opencode.json +6 -0
- package/package.json +9 -3
- package/playwright.config.ts +10 -0
- package/qa-dashboard/app.js +6 -2
- package/qa-dashboard/package.json +4 -1
- package/qa-dashboard/routes/index.js +26 -4
- package/qa-dashboard/routes/projects.js +25 -1
- package/qa-dashboard/routes/runs.js +38 -0
- package/qa-dashboard/routes/stories.js +18 -1
- package/qa-dashboard/routes/terminal.js +51 -0
- package/qa-dashboard/routes/test-data.js +46 -2
- package/qa-dashboard/services/cli-bridge.js +4 -0
- package/qa-dashboard/views/index.ejs +250 -238
- package/qa-dashboard/views/layouts/main.ejs +3 -2
- package/qa-dashboard/views/project.ejs +78 -51
- package/qa-dashboard/views/run.ejs +63 -3
- package/qa-dashboard/views/stories.ejs +36 -12
- package/qa-dashboard/views/story.ejs +125 -86
- package/qa-dashboard/views/terminal.ejs +101 -0
- package/qa-dashboard/views/test-data.ejs +59 -22
- package/router.md +27 -17
- package/test-results/run-2026-05-18T14-51-36/final-test-report.md +10 -2
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
</div>
|
|
12
12
|
</div>
|
|
13
13
|
|
|
14
|
-
<div
|
|
15
|
-
<p
|
|
14
|
+
<div style="margin-bottom:2rem;">
|
|
15
|
+
<p style="font-family:var(--font-mono);color:var(--text-disabled);font-size:0.88rem;"><%= project.path %></p>
|
|
16
16
|
</div>
|
|
17
17
|
|
|
18
18
|
<div class="row" style="margin-bottom:2rem;">
|
|
@@ -20,7 +20,6 @@
|
|
|
20
20
|
<div class="card card-lavender">
|
|
21
21
|
<h3>π User Stories</h3>
|
|
22
22
|
<div class="big-number"><%= list.stories.length %></div>
|
|
23
|
-
<a href="/stories?project=<%= project.id %>" class="btn btn-sm" style="margin-top:1rem;">View Stories</a>
|
|
24
23
|
</div>
|
|
25
24
|
</div>
|
|
26
25
|
<div class="col">
|
|
@@ -37,81 +36,109 @@
|
|
|
37
36
|
</div>
|
|
38
37
|
</div>
|
|
39
38
|
|
|
39
|
+
<!-- βββ Liste des Stories avec Pipeline Status βββ -->
|
|
40
40
|
<div class="card">
|
|
41
|
-
<h3
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
<
|
|
46
|
-
<
|
|
47
|
-
|
|
48
|
-
<
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
41
|
+
<h3>π Stories Pipeline</h3>
|
|
42
|
+
<% if (storiesWithStatus && storiesWithStatus.length > 0) { %>
|
|
43
|
+
<table class="info-table">
|
|
44
|
+
<thead>
|
|
45
|
+
<tr>
|
|
46
|
+
<th>User Story</th>
|
|
47
|
+
<th>Plan</th>
|
|
48
|
+
<th>Test</th>
|
|
49
|
+
<th>Run</th>
|
|
50
|
+
<th>Actions</th>
|
|
51
|
+
</tr>
|
|
52
|
+
</thead>
|
|
53
|
+
<tbody>
|
|
54
|
+
<% storiesWithStatus.forEach(s => { %>
|
|
55
|
+
<tr>
|
|
56
|
+
<td><a href="/stories/<%= s.name %>?project=<%= project.id %>" style="font-weight:600;"><%= s.baseName %></a></td>
|
|
57
|
+
<td><% if (s.hasPlan) { %><span class="badge badge-plan">β
</span><% } else { %><span class="badge badge-draft">β</span><% } %></td>
|
|
58
|
+
<td><% if (s.hasSpec) { %><span class="badge badge-spec">β
</span><% } else { %><span class="badge badge-draft">β</span><% } %></td>
|
|
59
|
+
<td><% if (s.hasRun) { %><span class="badge badge-pass">β
</span><% } else { %><span class="badge badge-draft">β</span><% } %></td>
|
|
60
|
+
<td>
|
|
61
|
+
<div style="display:flex;gap:0.4rem;flex-wrap:wrap;">
|
|
62
|
+
<button class="btn btn-sm" onclick="runPipelineAction('<%= project.id %>', '<%= s.name %>', 'plan')" <%= s.hasPlan ? 'disabled style="opacity:0.5;"' : '' %>>π Plan</button>
|
|
63
|
+
<button class="btn btn-sm" onclick="runPipelineAction('<%= project.id %>', '<%= s.name %>', 'generate')" <%= !s.hasPlan || s.hasSpec ? 'disabled style="opacity:0.5;"' : '' %>>π§ͺ Generate</button>
|
|
64
|
+
<button class="btn btn-sm btn-primary" onclick="runPipelineAction('<%= project.id %>', '<%= s.name %>', 'execute')" <%= !s.hasSpec ? 'disabled style="opacity:0.5;"' : '' %>>βΆ Execute</button>
|
|
65
|
+
<a href="/stories/<%= s.name %>?project=<%= project.id %>" class="btn btn-sm">View β</a>
|
|
66
|
+
</div>
|
|
67
|
+
</td>
|
|
68
|
+
</tr>
|
|
69
|
+
<% }) %>
|
|
70
|
+
</tbody>
|
|
71
|
+
</table>
|
|
72
|
+
<% } else { %>
|
|
73
|
+
<p style="color:var(--text-muted);">Aucune user story trouvΓ©e. Ajoutez des fichiers <code>.md</code> dans <code>user-story/</code>.</p>
|
|
74
|
+
<% } %>
|
|
75
|
+
<div id="pipeline-stream-output" class="output-box" style="display:none;margin-top:1rem;"></div>
|
|
76
|
+
<div id="pipeline-stream-pulse" class="terminal-pulse" style="display:none;margin-top:0.75rem;">
|
|
77
|
+
<span class="pulse-dot"></span><span class="pulse-dot"></span><span class="pulse-dot"></span>
|
|
78
|
+
<span class="pulse-text">Running...</span>
|
|
79
|
+
</div>
|
|
55
80
|
</div>
|
|
56
81
|
|
|
82
|
+
<!-- βββ Quick Actions βββ -->
|
|
57
83
|
<div class="card">
|
|
58
|
-
<h3
|
|
59
|
-
<
|
|
60
|
-
|
|
61
|
-
<button class="btn btn-sm" onclick="
|
|
62
|
-
|
|
63
|
-
<
|
|
84
|
+
<h3>β‘ Orchestrator Actions</h3>
|
|
85
|
+
<div class="action-buttons" style="margin-bottom:1rem;">
|
|
86
|
+
<button class="btn btn-sm" onclick="runQuickAction('<%= project.id %>', 'status')">π Status</button>
|
|
87
|
+
<button class="btn btn-sm btn-primary" onclick="runQuickAction('<%= project.id %>', 'report')">π Generate Report</button>
|
|
88
|
+
<% if (hasAllure) { %>
|
|
89
|
+
<a href="/allure-report?project=<%= project.id %>" class="btn btn-sm" target="_blank">π Allure Report</a>
|
|
90
|
+
<% } %>
|
|
91
|
+
<a href="/export/report/latest?project=<%= project.id %>" class="btn btn-sm" target="_blank">π₯ Download Latest Report</a>
|
|
64
92
|
</div>
|
|
65
93
|
<div id="quick-output" class="output-box" style="display:none;"></div>
|
|
66
94
|
</div>
|
|
67
95
|
|
|
68
96
|
<script>
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
const projectId = form.dataset.project;
|
|
73
|
-
const formData = new FormData(form);
|
|
74
|
-
const output = document.getElementById('pipeline-output');
|
|
75
|
-
|
|
97
|
+
async function runPipelineAction(projectId, storyName, action) {
|
|
98
|
+
const output = document.getElementById('pipeline-stream-output');
|
|
99
|
+
const pulse = document.getElementById('pipeline-stream-pulse');
|
|
76
100
|
output.style.display = 'block';
|
|
77
|
-
output.textContent =
|
|
101
|
+
output.textContent = `π Running ${action} for ${storyName}...\n`;
|
|
78
102
|
output.className = 'output-box output-running';
|
|
103
|
+
pulse.style.display = 'flex';
|
|
79
104
|
|
|
80
105
|
try {
|
|
81
|
-
const res = await fetch(`/
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
106
|
+
const res = await fetch(`/stories/${encodeURIComponent(storyName)}/${action}?project=${projectId}`, { method: 'POST' });
|
|
107
|
+
if (!res.body) { output.textContent += '\nError: No response stream'; pulse.style.display = 'none'; return; }
|
|
108
|
+
const reader = res.body.getReader();
|
|
109
|
+
const decoder = new TextDecoder('utf-8');
|
|
110
|
+
while (true) {
|
|
111
|
+
const { done, value } = await reader.read();
|
|
112
|
+
if (done) break;
|
|
113
|
+
output.textContent += decoder.decode(value, { stream: true });
|
|
114
|
+
output.scrollTop = output.scrollHeight;
|
|
115
|
+
}
|
|
116
|
+
pulse.style.display = 'none';
|
|
117
|
+
output.className = output.textContent.toLowerCase().includes('finished: success') ? 'output-box output-success' : 'output-box output-error';
|
|
118
|
+
if (output.textContent.toLowerCase().includes('finished: success')) {
|
|
119
|
+
setTimeout(() => location.reload(), 2000);
|
|
120
|
+
}
|
|
90
121
|
} catch (err) {
|
|
91
|
-
output.textContent
|
|
122
|
+
output.textContent += `\nError: ${err.message}`;
|
|
92
123
|
output.className = 'output-box output-error';
|
|
124
|
+
pulse.style.display = 'none';
|
|
93
125
|
}
|
|
94
|
-
}
|
|
126
|
+
}
|
|
95
127
|
|
|
96
|
-
async function
|
|
128
|
+
async function runQuickAction(projectId, action) {
|
|
97
129
|
const output = document.getElementById('quick-output');
|
|
98
130
|
output.style.display = 'block';
|
|
99
131
|
output.textContent = `Running ${action}...`;
|
|
100
132
|
output.className = 'output-box output-running';
|
|
101
|
-
|
|
102
133
|
try {
|
|
103
|
-
const res = await fetch(`/projects/${projectId}/${action}`, {
|
|
104
|
-
method: 'POST',
|
|
105
|
-
headers: { 'Content-Type': 'application/json' },
|
|
106
|
-
body: '{}'
|
|
107
|
-
});
|
|
134
|
+
const res = await fetch(`/projects/${projectId}/${action}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' });
|
|
108
135
|
if (!res.ok) throw new Error(`HTTP Error ${res.status}`);
|
|
109
136
|
const data = await res.json();
|
|
110
137
|
output.textContent = data.output || data.error || 'Done';
|
|
111
138
|
output.className = `output-box ${data.success ? 'output-success' : 'output-error'}`;
|
|
112
139
|
} catch (err) {
|
|
113
|
-
output.textContent = '
|
|
140
|
+
output.textContent = 'Error: ' + err.message;
|
|
114
141
|
output.className = 'output-box output-error';
|
|
115
142
|
}
|
|
116
143
|
}
|
|
117
|
-
</script>
|
|
144
|
+
</script>
|
|
@@ -56,14 +56,19 @@
|
|
|
56
56
|
<div class="card">
|
|
57
57
|
<h3>π Report Actions</h3>
|
|
58
58
|
<div class="action-buttons-vertical">
|
|
59
|
+
<button class="btn btn-sm btn-primary" onclick="rerunRun('<%= project.id %>', '<%= run.id %>')" style="margin-bottom:0.5rem;">π Rerun This Test</button>
|
|
59
60
|
<% if (run.report) { %>
|
|
60
|
-
<a href="/export/report/<%= run.id %>?project=<%= project.id %>" class="btn btn-
|
|
61
|
-
<a href="/export/pdf/<%= run.id %>?project=<%= project.id %>" class="btn"
|
|
61
|
+
<a href="/export/report/<%= run.id %>?project=<%= project.id %>" class="btn btn-sm" target="_blank">π₯ Export HTML Report</a>
|
|
62
|
+
<a href="/export/pdf/<%= run.id %>?project=<%= project.id %>" class="btn btn-sm">π₯ Download Raw (.txt)</a>
|
|
62
63
|
<% } else { %>
|
|
63
64
|
<p style="color:var(--text-disabled);font-size:0.85rem;margin-bottom:0.5rem;">No summary report generated yet.</p>
|
|
64
65
|
<% } %>
|
|
66
|
+
<button class="btn btn-sm" onclick="generateReport('<%= project.id %>', '<%= run.id %>')" style="margin-top:0.25rem;">π Generate Report</button>
|
|
65
67
|
<% if (run.hasAllure) { %>
|
|
66
|
-
<a href="/allure-report?project=<%= project.id %>" class="btn" target="_blank" style="background-color:var(--brand-ochre);color:var(--text);border-color:var(--border-strong);margin-top:0.5rem;"
|
|
68
|
+
<a href="/allure-report?project=<%= project.id %>" class="btn btn-sm" target="_blank" style="background-color:var(--brand-ochre);color:var(--text);border-color:var(--border-strong);margin-top:0.5rem;">π Open Allure Report β</a>
|
|
69
|
+
<button class="btn btn-sm" onclick="regenerateAllure('<%= project.id %>')" style="margin-top:0.25rem;">π Regenerate Allure</button>
|
|
70
|
+
<% } else { %>
|
|
71
|
+
<button class="btn btn-sm" onclick="regenerateAllure('<%= project.id %>')" style="margin-top:0.25rem;">π Generate Allure Report</button>
|
|
67
72
|
<% } %>
|
|
68
73
|
</div>
|
|
69
74
|
</div>
|
|
@@ -139,6 +144,61 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
139
144
|
reportRendered.innerHTML = marked.parse(reportRaw.textContent || '');
|
|
140
145
|
}
|
|
141
146
|
});
|
|
147
|
+
|
|
148
|
+
async function rerunRun(projectId, runId) {
|
|
149
|
+
if (!confirm('Rerun this test?')) return;
|
|
150
|
+
const output = document.querySelector('.output-box') || document.createElement('div');
|
|
151
|
+
output.textContent = 'π Rerunning...';
|
|
152
|
+
output.className = 'output-box output-running';
|
|
153
|
+
output.style.display = 'block';
|
|
154
|
+
try {
|
|
155
|
+
const res = await fetch(`/runs/${runId}/rerun?project=${projectId}`, { method: 'POST' });
|
|
156
|
+
const data = await res.json();
|
|
157
|
+
output.textContent = data.output || data.error || 'Done';
|
|
158
|
+
output.className = `output-box ${data.success ? 'output-success' : 'output-error'}`;
|
|
159
|
+
if (data.success && data.runId) {
|
|
160
|
+
output.textContent += `\n\nNew run: ${data.runId}`;
|
|
161
|
+
setTimeout(() => { window.location.href = `/runs/${data.runId}?project=${projectId}`; }, 2000);
|
|
162
|
+
}
|
|
163
|
+
} catch (err) {
|
|
164
|
+
output.textContent = 'Error: ' + err.message;
|
|
165
|
+
output.className = 'output-box output-error';
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function generateReport(projectId, runId) {
|
|
170
|
+
const output = document.querySelector('.output-box') || document.createElement('div');
|
|
171
|
+
output.textContent = 'π Generating report...';
|
|
172
|
+
output.className = 'output-box output-running';
|
|
173
|
+
output.style.display = 'block';
|
|
174
|
+
try {
|
|
175
|
+
const res = await fetch(`/projects/${projectId}/report`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ runId }) });
|
|
176
|
+
const data = await res.json();
|
|
177
|
+
output.textContent = data.output || data.error || 'Done';
|
|
178
|
+
output.className = `output-box ${data.success ? 'output-success' : 'output-error'}`;
|
|
179
|
+
if (data.success) setTimeout(() => location.reload(), 1500);
|
|
180
|
+
} catch (err) {
|
|
181
|
+
output.textContent = 'Error: ' + err.message;
|
|
182
|
+
output.className = 'output-box output-error';
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function regenerateAllure(projectId) {
|
|
187
|
+
const output = document.querySelector('.output-box') || document.createElement('div');
|
|
188
|
+
output.textContent = 'π Regenerating Allure report...';
|
|
189
|
+
output.className = 'output-box output-running';
|
|
190
|
+
output.style.display = 'block';
|
|
191
|
+
try {
|
|
192
|
+
const res = await fetch(`/runs/allure-regenerate?project=${projectId}`, { method: 'POST' });
|
|
193
|
+
const data = await res.json();
|
|
194
|
+
output.textContent = data.output || data.error || 'Done';
|
|
195
|
+
output.className = `output-box ${data.success ? 'output-success' : 'output-error'}`;
|
|
196
|
+
if (data.success) setTimeout(() => location.reload(), 1500);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
output.textContent = 'Error: ' + err.message;
|
|
199
|
+
output.className = 'output-box output-error';
|
|
200
|
+
}
|
|
201
|
+
}
|
|
142
202
|
</script>
|
|
143
203
|
|
|
144
204
|
<% } %>
|
|
@@ -18,11 +18,11 @@
|
|
|
18
18
|
<% stories.forEach((s, idx) => { %>
|
|
19
19
|
<div class="story-card <%= cardColors[idx % cardColors.length] %>" style="margin-bottom:1.25rem;">
|
|
20
20
|
<div class="story-header">
|
|
21
|
-
<a href="/stories/<%= s.name %>?project=<%= project.id %>" class="story-title" style="text-decoration:none;font-weight:600;"><%= s.name %></a>
|
|
21
|
+
<a href="/stories/<%= s.name %>?project=<%= project.id %>" class="story-title" style="text-decoration:none;font-weight:600;"><%= s.name.replace('.md', '') %></a>
|
|
22
22
|
<div class="story-badges">
|
|
23
|
-
<% if (s.hasPlan) { %><span class="badge badge-
|
|
24
|
-
<% if (s.
|
|
25
|
-
<%
|
|
23
|
+
<% if (s.hasPlan && s.hasSpec) { %><span class="badge badge-spec">β
Planned & Tested</span>
|
|
24
|
+
<% } else if (s.hasPlan) { %><span class="badge badge-plan">π Planned</span>
|
|
25
|
+
<% } else { %><span class="badge badge-draft">π Draft</span><% } %>
|
|
26
26
|
</div>
|
|
27
27
|
</div>
|
|
28
28
|
<div class="story-preview" style="margin:1rem 0;">
|
|
@@ -33,10 +33,14 @@
|
|
|
33
33
|
<% }) %>
|
|
34
34
|
<% } %>
|
|
35
35
|
</div>
|
|
36
|
-
<div class="story-actions" style="border-top:1px solid var(--border);padding-top:1rem;
|
|
37
|
-
<
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
<div class="story-actions" style="border-top:1px solid var(--border);padding-top:1rem;">
|
|
37
|
+
<div style="display:flex;align-items:center;gap:0.5rem;flex-wrap:wrap;">
|
|
38
|
+
<button class="btn btn-sm" onclick="callPipelineAction('<%= project.id %>', '<%= s.name %>', 'plan')" <%= s.hasPlan ? 'disabled style="opacity:0.5;"' : '' %>>π Plan</button>
|
|
39
|
+
<button class="btn btn-sm" onclick="callPipelineAction('<%= project.id %>', '<%= s.name %>', 'generate')" <%= !s.hasPlan || s.hasSpec ? 'disabled style="opacity:0.5;"' : '' %>>π§ͺ Generate</button>
|
|
40
|
+
<button class="btn btn-sm btn-primary" onclick="callPipelineAction('<%= project.id %>', '<%= s.name %>', 'execute')" <%= !s.hasSpec ? 'disabled style="opacity:0.5;"' : '' %>>βΆ Execute</button>
|
|
41
|
+
<a href="/stories/<%= s.name %>?project=<%= project.id %>" class="btn btn-sm">View Details β</a>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="story-action-output" style="margin-top:0.5rem;"></div>
|
|
40
44
|
</div>
|
|
41
45
|
</div>
|
|
42
46
|
<% }) %>
|
|
@@ -44,8 +48,28 @@
|
|
|
44
48
|
<% } %>
|
|
45
49
|
|
|
46
50
|
<script>
|
|
47
|
-
async function
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
async function callPipelineAction(projectId, storyName, action) {
|
|
52
|
+
const card = event.target.closest('.story-card');
|
|
53
|
+
const outputDiv = card.querySelector('.story-action-output');
|
|
54
|
+
outputDiv.innerHTML = `<div class="output-box output-running" style="margin-top:0.5rem;padding:0.75rem;font-size:0.8rem;max-height:200px;">π Running ${action}...</div>`;
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const res = await fetch(`/stories/${encodeURIComponent(storyName)}/${action}?project=${projectId}`, { method: 'POST' });
|
|
58
|
+
if (!res.body) { outputDiv.innerHTML = '<div class="output-box output-error" style="margin-top:0.5rem;padding:0.75rem;font-size:0.8rem;">Error: No stream</div>'; return; }
|
|
59
|
+
const reader = res.body.getReader();
|
|
60
|
+
const decoder = new TextDecoder('utf-8');
|
|
61
|
+
let text = '';
|
|
62
|
+
while (true) {
|
|
63
|
+
const { done, value } = await reader.read();
|
|
64
|
+
if (done) break;
|
|
65
|
+
text += decoder.decode(value, { stream: true });
|
|
66
|
+
outputDiv.innerHTML = `<div class="output-box output-running" style="margin-top:0.5rem;padding:0.75rem;font-size:0.8rem;max-height:200px;overflow-y:auto;">${text.replace(/\n/g, '<br>')}</div>`;
|
|
67
|
+
}
|
|
68
|
+
const isSuccess = text.toLowerCase().includes('finished: success');
|
|
69
|
+
outputDiv.innerHTML = `<div class="output-box ${isSuccess ? 'output-success' : 'output-error'}" style="margin-top:0.5rem;padding:0.75rem;font-size:0.8rem;max-height:200px;overflow-y:auto;">${text.replace(/\n/g, '<br>')}</div>`;
|
|
70
|
+
if (isSuccess) setTimeout(() => location.reload(), 2000);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
outputDiv.innerHTML = `<div class="output-box output-error" style="margin-top:0.5rem;padding:0.75rem;font-size:0.8rem;">Error: ${err.message}</div>`;
|
|
73
|
+
}
|
|
50
74
|
}
|
|
51
|
-
</script>
|
|
75
|
+
</script>
|
|
@@ -2,138 +2,177 @@
|
|
|
2
2
|
<% title = story.name %>
|
|
3
3
|
|
|
4
4
|
<div class="page-header">
|
|
5
|
-
<h1><%= story.name %></h1>
|
|
5
|
+
<h1><%= story.name.replace('.md', '') %></h1>
|
|
6
6
|
<span class="badge badge-project"><%= project.name %></span>
|
|
7
7
|
</div>
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
<!-- βββ Phase Workflow Buttons βββ -->
|
|
10
|
+
<div class="card" style="margin-bottom:1.5rem;">
|
|
11
|
+
<h3>β‘ Pipeline Workflow</h3>
|
|
12
|
+
<p style="color:var(--text-muted);font-size:0.85rem;margin-bottom:1rem;">Execute each phase in order. Verify the output before proceeding to the next step.</p>
|
|
13
|
+
<div style="display:flex;gap:0.75rem;flex-wrap:wrap;align-items:center;">
|
|
14
|
+
<button class="btn btn-sm" id="btn-plan" onclick="triggerPhase('plan')" <%= story.plan ? 'disabled style="opacity:0.5;"' : '' %>>
|
|
15
|
+
<%= story.plan ? 'β
Planned' : 'π 1. Plan' %>
|
|
16
|
+
</button>
|
|
17
|
+
<span style="color:var(--text-disabled);">β</span>
|
|
18
|
+
<button class="btn btn-sm" id="btn-generate" onclick="triggerPhase('generate')" <%= !story.plan || story.spec ? 'disabled style="opacity:0.5;"' : '' %>>
|
|
19
|
+
<%= story.spec ? 'β
Generated' : 'π§ͺ 2. Generate' %>
|
|
20
|
+
</button>
|
|
21
|
+
<span style="color:var(--text-disabled);">β</span>
|
|
22
|
+
<button class="btn btn-sm btn-primary" id="btn-execute" onclick="triggerPhase('execute')" <%= !story.spec ? 'disabled style="opacity:0.5;"' : '' %>>
|
|
23
|
+
βΆ 3. Execute
|
|
24
|
+
</button>
|
|
25
|
+
<span style="color:var(--text-disabled);">β</span>
|
|
26
|
+
<button class="btn btn-sm" id="btn-report" onclick="triggerPhase('report')" <%= !story.hasRun ? 'disabled style="opacity:0.5;"' : '' %>>
|
|
27
|
+
π 4. Report
|
|
28
|
+
</button>
|
|
29
|
+
<% if (story.hasAllure) { %>
|
|
30
|
+
<a href="/allure-report?project=<%= project.id %>" class="btn btn-sm" target="_blank" style="background-color:var(--brand-ochre);">π Allure</a>
|
|
31
|
+
<% } %>
|
|
32
|
+
</div>
|
|
33
|
+
<div id="pulse-indicator" class="terminal-pulse" style="display:none;margin-top:1rem;">
|
|
34
|
+
<span class="pulse-dot"></span><span class="pulse-dot"></span><span class="pulse-dot"></span>
|
|
35
|
+
<span class="pulse-text">AI Agent running...</span>
|
|
36
|
+
</div>
|
|
37
|
+
<div id="phase-output" class="output-box" style="display:none;margin-top:1rem;"></div>
|
|
38
|
+
</div>
|
|
16
39
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
<span class="pulse-dot"></span>
|
|
31
|
-
<span class="pulse-dot"></span>
|
|
32
|
-
<span class="pulse-dot"></span>
|
|
33
|
-
<span class="pulse-text">AI Agent is running...</span>
|
|
34
|
-
</div>
|
|
35
|
-
</div>
|
|
40
|
+
<!-- βββ Tabs: Story | Test Plan | Test Code | Results βββ -->
|
|
41
|
+
<div class="card">
|
|
42
|
+
<div style="display:flex;gap:1rem;border-bottom:2px solid var(--border);margin-bottom:1.5rem;padding-bottom:0.5rem;">
|
|
43
|
+
<button class="btn btn-sm tab-btn active-tab" data-tab="story-tab" style="border-bottom:2px solid var(--primary);border-radius:0;">π Story</button>
|
|
44
|
+
<button class="btn btn-sm tab-btn" data-tab="plan-tab" style="border-radius:0;">π Test Plan</button>
|
|
45
|
+
<button class="btn btn-sm tab-btn" data-tab="spec-tab" style="border-radius:0;">π§ͺ Test Code</button>
|
|
46
|
+
<button class="btn btn-sm tab-btn" data-tab="results-tab" style="border-radius:0;">β‘ Results</button>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<!-- Tab: Story Content -->
|
|
50
|
+
<div id="story-tab" class="tab-content">
|
|
51
|
+
<div id="story-content-md" style="display:none;"><%= story.content %></div>
|
|
52
|
+
<div class="markdown-content" id="story-content-rendered"></div>
|
|
36
53
|
</div>
|
|
37
54
|
|
|
38
|
-
|
|
55
|
+
<!-- Tab: Test Plan -->
|
|
56
|
+
<div id="plan-tab" class="tab-content" style="display:none;">
|
|
39
57
|
<% if (story.plan) { %>
|
|
40
|
-
<div class="card">
|
|
41
|
-
<h3>π Test Plan <a href="/stories/<%= story.name %>?project=<%= project.id %>&tab=plan" class="btn btn-sm" style="margin-left:auto;">Refresh</a></h3>
|
|
42
58
|
<div id="story-plan-md" style="display:none;"><%= story.plan %></div>
|
|
43
59
|
<div class="markdown-content" id="story-plan-rendered"></div>
|
|
44
|
-
|
|
60
|
+
<% } else { %>
|
|
61
|
+
<div class="empty-state" style="padding:2rem;">
|
|
62
|
+
<p>No test plan yet. Click <strong>"π 1. Plan"</strong> above to generate one.</p>
|
|
63
|
+
</div>
|
|
45
64
|
<% } %>
|
|
65
|
+
</div>
|
|
46
66
|
|
|
67
|
+
<!-- Tab: Test Code -->
|
|
68
|
+
<div id="spec-tab" class="tab-content" style="display:none;">
|
|
47
69
|
<% if (story.spec) { %>
|
|
48
|
-
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
|
|
70
|
+
<pre class="code-block" style="max-height:500px;"><%= story.spec %></pre>
|
|
71
|
+
<div style="margin-top:1rem;">
|
|
72
|
+
<a href="/data?project=<%= project.id %>" class="btn btn-sm">View All Test Files</a>
|
|
73
|
+
</div>
|
|
74
|
+
<% } else { %>
|
|
75
|
+
<div class="empty-state" style="padding:2rem;">
|
|
76
|
+
<p>No test code yet. Complete the <strong>Plan</strong> phase first, then click <strong>"π§ͺ 2. Generate"</strong>.</p>
|
|
77
|
+
</div>
|
|
78
|
+
<% } %>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<!-- Tab: Results -->
|
|
82
|
+
<div id="results-tab" class="tab-content" style="display:none;">
|
|
83
|
+
<% if (story.results && story.results.length > 0) { %>
|
|
84
|
+
<table class="info-table">
|
|
85
|
+
<thead><tr><th>Run</th><th>Status</th><th>Duration</th><th>Passed</th><th>Failed</th><th>Actions</th></tr></thead>
|
|
86
|
+
<tbody>
|
|
87
|
+
<% story.results.slice(0, 5).forEach(r => { %>
|
|
88
|
+
<tr>
|
|
89
|
+
<td><%= r.id.substring(0, 20) %></td>
|
|
90
|
+
<td><%= r.success ? 'β
Passed' : (r.success === null ? 'β³ Running' : 'β Failed') %></td>
|
|
91
|
+
<td><%= r.duration ? (r.duration / 1000).toFixed(1) + 's' : 'N/A' %></td>
|
|
92
|
+
<td><%= r.passedCount %></td>
|
|
93
|
+
<td><%= r.failedCount %></td>
|
|
94
|
+
<td><a href="/runs/<%= r.id %>?project=<%= project.id %>" class="btn btn-sm">View</a></td>
|
|
95
|
+
</tr>
|
|
96
|
+
<% }) %>
|
|
97
|
+
</tbody>
|
|
98
|
+
</table>
|
|
99
|
+
<div style="margin-top:1rem;">
|
|
100
|
+
<a href="/runs?project=<%= project.id %>" class="btn btn-sm">All Runs β</a>
|
|
101
|
+
<a href="/export/report/latest?project=<%= project.id %>" class="btn btn-sm" target="_blank">π₯ Download Report</a>
|
|
102
|
+
</div>
|
|
103
|
+
<% } else { %>
|
|
104
|
+
<div class="empty-state" style="padding:2rem;">
|
|
105
|
+
<p>No execution results yet. Click <strong>"βΆ 3. Execute"</strong> above to run the tests.</p>
|
|
106
|
+
</div>
|
|
52
107
|
<% } %>
|
|
53
108
|
</div>
|
|
54
109
|
</div>
|
|
55
110
|
|
|
56
111
|
<script>
|
|
57
|
-
//
|
|
112
|
+
// βββ Tab switching βββ
|
|
58
113
|
document.addEventListener('DOMContentLoaded', () => {
|
|
114
|
+
// Render markdown
|
|
59
115
|
const contentRaw = document.getElementById('story-content-md');
|
|
60
116
|
const contentRendered = document.getElementById('story-content-rendered');
|
|
61
|
-
if (contentRaw && contentRendered) {
|
|
117
|
+
if (contentRaw && contentRendered && typeof marked !== 'undefined') {
|
|
62
118
|
contentRendered.innerHTML = marked.parse(contentRaw.textContent || '');
|
|
63
119
|
}
|
|
64
|
-
|
|
65
120
|
const planRaw = document.getElementById('story-plan-md');
|
|
66
121
|
const planRendered = document.getElementById('story-plan-rendered');
|
|
67
|
-
if (planRaw && planRendered) {
|
|
122
|
+
if (planRaw && planRendered && typeof marked !== 'undefined') {
|
|
68
123
|
planRendered.innerHTML = marked.parse(planRaw.textContent || '');
|
|
69
124
|
}
|
|
125
|
+
|
|
126
|
+
// Tab click handlers
|
|
127
|
+
document.querySelectorAll('.tab-btn').forEach(btn => {
|
|
128
|
+
btn.addEventListener('click', () => {
|
|
129
|
+
document.querySelectorAll('.tab-btn').forEach(b => { b.style.borderBottom = '2px solid transparent'; b.style.color = 'var(--text-muted)'; });
|
|
130
|
+
btn.style.borderBottom = '2px solid var(--primary)';
|
|
131
|
+
btn.style.color = 'var(--text)';
|
|
132
|
+
document.querySelectorAll('.tab-content').forEach(tc => tc.style.display = 'none');
|
|
133
|
+
document.getElementById(btn.dataset.tab).style.display = 'block';
|
|
134
|
+
});
|
|
135
|
+
});
|
|
70
136
|
});
|
|
71
137
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
status.className = 'output-box output-running';
|
|
138
|
+
// βββ Phase execution βββ
|
|
139
|
+
async function triggerPhase(action) {
|
|
140
|
+
const output = document.getElementById('phase-output');
|
|
141
|
+
const pulse = document.getElementById('pulse-indicator');
|
|
142
|
+
output.style.display = 'block';
|
|
143
|
+
output.textContent = `π Running ${action}...\n`;
|
|
144
|
+
output.className = 'output-box output-running';
|
|
80
145
|
pulse.style.display = 'flex';
|
|
81
146
|
|
|
82
147
|
try {
|
|
83
|
-
const res = await fetch(`/stories
|
|
84
|
-
|
|
85
|
-
if (!res.body) {
|
|
86
|
-
status.textContent += "\nError: The server did not return a readable stream.";
|
|
87
|
-
pulse.style.display = 'none';
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
|
|
148
|
+
const res = await fetch(`/stories/<%= story.name %>/${action}?project=<%= project.id %>`, { method: 'POST' });
|
|
149
|
+
if (!res.body) { output.textContent += '\nError: No stream'; pulse.style.display = 'none'; return; }
|
|
91
150
|
const reader = res.body.getReader();
|
|
92
151
|
const decoder = new TextDecoder('utf-8');
|
|
93
|
-
|
|
94
152
|
while (true) {
|
|
95
153
|
const { done, value } = await reader.read();
|
|
96
154
|
if (done) break;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
status.textContent += chunk;
|
|
100
|
-
status.scrollTop = status.scrollHeight; // Force Autoscroll
|
|
155
|
+
output.textContent += decoder.decode(value, { stream: true });
|
|
156
|
+
output.scrollTop = output.scrollHeight;
|
|
101
157
|
}
|
|
102
|
-
|
|
103
|
-
// Hide pulse loader once done
|
|
104
158
|
pulse.style.display = 'none';
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
setTimeout(() => { location.reload(); }, 2000);
|
|
112
|
-
} else {
|
|
113
|
-
status.className = 'output-box output-error';
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
} catch (error) {
|
|
117
|
-
status.textContent += `\n\nNetwork or server connection error: ${error.message}`;
|
|
118
|
-
status.className = 'output-box output-error';
|
|
159
|
+
const isSuccess = output.textContent.toLowerCase().includes('finished: success');
|
|
160
|
+
output.className = `output-box ${isSuccess ? 'output-success' : 'output-error'}`;
|
|
161
|
+
if (isSuccess) setTimeout(() => location.reload(), 2000);
|
|
162
|
+
} catch (err) {
|
|
163
|
+
output.textContent += `\nError: ${err.message}`;
|
|
164
|
+
output.className = 'output-box output-error';
|
|
119
165
|
pulse.style.display = 'none';
|
|
120
166
|
}
|
|
121
167
|
}
|
|
122
168
|
|
|
123
|
-
|
|
124
|
-
function triggerGenerate(s) { callPipeline('generate', s); }
|
|
125
|
-
function triggerExecute(s) { callPipeline('execute', s); }
|
|
126
|
-
function triggerFull(s) { callPipeline('full-pipeline', s); }
|
|
127
|
-
|
|
169
|
+
// βββ Auto-execute on load βββ
|
|
128
170
|
document.addEventListener('DOMContentLoaded', () => {
|
|
129
171
|
const urlParams = new URLSearchParams(window.location.search);
|
|
130
172
|
if (urlParams.get('autoRun') === 'true') {
|
|
131
|
-
// Nettoyer l'URL afin qu'un rechargement manuel ne relance pas le processus
|
|
132
173
|
const newUrl = window.location.pathname + '?project=' + urlParams.get('project');
|
|
133
174
|
window.history.replaceState({}, '', newUrl);
|
|
134
|
-
|
|
135
|
-
// DΓ©clencher l'exΓ©cution des tests
|
|
136
|
-
triggerExecute('<%= story.name %>');
|
|
175
|
+
triggerPhase('execute');
|
|
137
176
|
}
|
|
138
177
|
});
|
|
139
|
-
</script>
|
|
178
|
+
</script>
|