@ai-qa/workflow 2.0.2 → 2.0.3

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.
@@ -1,69 +1,116 @@
1
1
  <% layout = 'layouts/main' %>
2
- <% title = 'Run: ' + run.id %>
2
+ <% title = 'Run: ' + (typeof run !== 'undefined' && run && run.id ? run.id.substring(0, 16) : 'Details') %>
3
3
 
4
4
  <div class="page-header">
5
- <h1>Run: <%= run.id %></h1>
5
+ <h1>Run Details</h1>
6
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>
7
+ <% if (typeof run !== 'undefined' && run && run.result) { %>
8
+ <span class="badge <%= run.result.success ? 'badge-pass' : 'badge-fail' %>" style="font-size:0.85rem;padding:0.4rem 1rem;">
9
+ <%= run.result.success ? '✅ PASSED' : (run.result.success === null ? '⏳ RUNNING' : '❌ FAILED') %>
10
+ </span>
11
+ <% } %>
12
+ <a href="/runs?project=<%= (typeof project !== 'undefined' && project) ? project.id : '' %>" class="btn btn-sm">← Back to Runs</a>
9
13
  </div>
10
14
  </div>
11
15
 
16
+ <% if (typeof run === 'undefined' || !run || !run.result) { %>
17
+ <div class="empty-state">
18
+ <p>Aucun résultat d'exécution trouvé pour ce run.</p>
19
+ <a href="/runs?project=<%= (typeof project !== 'undefined' && project) ? project.id : '' %>" class="btn btn-primary" style="margin-top:0.5rem;">Retour aux runs</a>
20
+ </div>
21
+ <% } else { %>
22
+
12
23
  <div class="row">
24
+ <!-- Left Side: Summary & Actions -->
13
25
  <div class="col col-30">
14
- <div class="card">
15
- <h3>Summary</h3>
26
+ <div class="card card-cream">
27
+ <h3>📊 Run Summary</h3>
16
28
  <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>
29
+ <tr>
30
+ <td>Status</td>
31
+ <td><strong><%= run.result.success ? 'Passed' : (run.result.success === null ? 'In Progress' : 'Failed') %></strong></td>
32
+ </tr>
33
+ <tr>
34
+ <td>Duration</td>
35
+ <td><strong><%= run.result.duration ? (run.result.duration / 1000).toFixed(1) + 's' : 'N/A' %></strong></td>
36
+ </tr>
37
+ <tr>
38
+ <td>Test Suite</td>
39
+ <td><code style="font-family:var(--font-mono);font-size:0.78rem;"><%= run.result.test || 'All' %></code></td>
40
+ </tr>
41
+ <tr>
42
+ <td>Timestamp</td>
43
+ <td><%= run.result.timestamp ? new Date(run.result.timestamp).toLocaleString() : 'N/A' %></td>
44
+ </tr>
45
+ <tr>
46
+ <td>Failures</td>
47
+ <td>
48
+ <span style="font-weight:600;color:<%= run.result.failedTests && run.result.failedTests.length > 0 ? 'var(--danger)' : 'var(--success)' %>">
49
+ <%= run.result.failedTests ? run.result.failedTests.length : 0 %>
50
+ </span>
51
+ </td>
52
+ </tr>
22
53
  </table>
23
54
  </div>
24
55
 
25
56
  <div class="card">
26
- <h3>Report Actions</h3>
57
+ <h3>📂 Report Actions</h3>
27
58
  <div class="action-buttons-vertical">
28
59
  <% 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>
60
+ <a href="/export/report/<%= run.id %>?project=<%= project.id %>" class="btn btn-primary" target="_blank">Export HTML Report</a>
61
+ <a href="/export/pdf/<%= run.id %>?project=<%= project.id %>" class="btn">Download Raw (.txt)</a>
31
62
  <% } else { %>
32
- <p style="color:var(--text-muted);font-size:0.85rem;">No report available yet. Generate one from the pipeline.</p>
63
+ <p style="color:var(--text-disabled);font-size:0.85rem;margin-bottom:0.5rem;">No summary report generated yet.</p>
33
64
  <% } %>
34
65
  <% if (run.hasAllure) { %>
35
- <a href="/allure-report?project=<%= project.id %>" class="btn" target="_blank">Open Allure Report ↗</a>
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;">Open Allure Report ↗</a>
36
67
  <% } %>
37
68
  </div>
38
69
  </div>
39
70
 
40
71
  <% 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>
72
+ <div class="card card-lavender">
73
+ <h3>🛠️ Self-Healing Status</h3>
74
+ <table class="info-table" style="margin-bottom:1rem;">
75
+ <tr>
76
+ <td>Attempts</td>
77
+ <td><strong><%= run.healing.healingAttempts ? run.healing.healingAttempts.length : 0 %></strong></td>
78
+ </tr>
79
+ <tr>
80
+ <td>Healed</td>
81
+ <td><strong><%= run.healing.totalHealed || 0 %></strong></td>
82
+ </tr>
83
+ <tr>
84
+ <td>Remaining</td>
85
+ <td><strong><%= run.healing.totalRemaining || 0 %></strong></td>
86
+ </tr>
47
87
  </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
- <% }) %>
88
+
89
+ <% if (run.healing.healingAttempts && run.healing.healingAttempts.length > 0) { %>
90
+ <h4 style="font-size:0.9rem;margin-top:1rem;margin-bottom:0.5rem;font-weight:600;">Healing Log</h4>
91
+ <% run.healing.healingAttempts.forEach(a => { %>
92
+ <div class="heal-attempt" style="margin-bottom:0.5rem;font-size:0.85rem;">
93
+ <strong>Attempt <%= a.attempt %></strong>: <%= a.healed ? a.healed.length : 0 %> auto-fixed, <%= a.stillFailing ? a.stillFailing.length : 0 %> failing.
94
+ </div>
95
+ <% }) %>
55
96
  <% } %>
56
97
  </div>
57
98
  <% } %>
58
99
  </div>
59
100
 
101
+ <!-- Right Side: Failures, Reports, Output -->
60
102
  <div class="col col-70">
61
103
  <% if (run.result.failedTests && run.result.failedTests.length > 0) { %>
62
- <div class="card">
63
- <h3>Failed Tests</h3>
64
- <ul class="fail-list">
104
+ <div class="card card-peach">
105
+ <h3>❌ Failed Tests & Traces</h3>
106
+ <ul class="fail-list" style="padding-left:1.25rem;">
65
107
  <% run.result.failedTests.forEach(ft => { %>
66
- <li><strong><%= ft.test || ft.file %></strong><br><%= ft.error ? ft.error.substring(0, 200) : 'No details' %></li>
108
+ <li style="margin-bottom:1rem;">
109
+ <strong style="font-family:var(--font-mono);font-size:0.9rem;color:var(--danger);"><%= ft.test || ft.file || 'Test failure' %></strong>
110
+ <% if (ft.error) { %>
111
+ <pre style="margin-top:0.5rem;background:rgba(255,255,255,0.4);border:1px solid rgba(0,0,0,0.1);padding:0.75rem;border-radius:var(--radius-sm);font-family:var(--font-mono);font-size:0.78rem;overflow-x:auto;white-space:pre-wrap;color:var(--text);"><%= ft.error %></pre>
112
+ <% } %>
113
+ </li>
67
114
  <% }) %>
68
115
  </ul>
69
116
  </div>
@@ -71,14 +118,27 @@
71
118
 
72
119
  <% if (run.report) { %>
73
120
  <div class="card">
74
- <h3>Test Report</h3>
75
- <div class="markdown-content"><%- run.report.replace(/\n/g, '<br>') %></div>
121
+ <h3>📝 Test Execution Report</h3>
122
+ <div id="run-report-md" style="display:none;"><%= run.report %></div>
123
+ <div class="markdown-content" id="run-report-rendered"></div>
76
124
  </div>
77
125
  <% } %>
78
126
 
79
127
  <div class="card">
80
- <h3>Execution Output</h3>
81
- <pre class="code-block"><%= run.result.output || 'No output captured' %></pre>
128
+ <h3>💻 Raw Execution Console Output</h3>
129
+ <pre class="code-block" style="font-size:0.78rem;max-height:400px;overflow-y:auto;"><%= run.result.output || 'No console output captured.' %></pre>
82
130
  </div>
83
131
  </div>
84
132
  </div>
133
+
134
+ <script>
135
+ document.addEventListener('DOMContentLoaded', () => {
136
+ const reportRaw = document.getElementById('run-report-md');
137
+ const reportRendered = document.getElementById('run-report-rendered');
138
+ if (reportRaw && reportRendered && typeof marked !== 'undefined') {
139
+ reportRendered.innerHTML = marked.parse(reportRaw.textContent || '');
140
+ }
141
+ });
142
+ </script>
143
+
144
+ <% } %>
@@ -4,61 +4,52 @@
4
4
  <div class="page-header">
5
5
  <h1>Execution Runs</h1>
6
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>
7
+ <span class="badge badge-project"><%= (typeof project !== 'undefined' && project && project.name) ? project.name : 'Unknown Project' %></span>
8
+ <a href="/analytics?project=<%= (typeof project !== 'undefined' && project) ? project.id : '' %>" class="btn btn-sm">Analytics</a>
9
9
  </div>
10
10
  </div>
11
11
 
12
- <% if (runs.length === 0) { %>
12
+ <% if (typeof runs === 'undefined' || !runs || runs.length === 0) { %>
13
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>
14
+ <p>No test executions yet. Run the pipeline from the Stories page to see executions here.</p>
15
+ <a href="/stories?project=<%= (typeof project !== 'undefined' && project) ? project.id : '' %>" class="btn btn-primary" style="margin-top:0.5rem;">View Stories</a>
16
16
  </div>
17
- <% } %>
17
+ <% } else { %>
18
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>
19
+ <div class="run-list" style="display:flex;flex-direction:column;gap:1rem;">
20
+ <% (runs || []).forEach(r => { %>
21
+ <div class="run-card-wrapper <%= r.success ? 'run-passed' : (r.success === null ? 'run-pending' : 'run-failed') %>"
22
+ style="display:flex;align-items:center;justify-content:space-between;background:var(--surface);border:1px solid var(--border);border-left:6px solid <%= r.success ? 'var(--success)' : (r.success === null ? 'var(--brand-ochre)' : 'var(--danger)') %>;border-radius:var(--radius-lg);padding:1.25rem;transition:transform 0.15s, border-color 0.15s;box-shadow:var(--shadow-1);"
23
+ onmouseover="this.style.transform='translateY(-2px)';this.style.borderColor='var(--border-strong)';"
24
+ onmouseout="this.style.transform='none';this.style.borderColor='var(--border)';">
25
+
26
+ <a href="/runs/<%= r.id %>?project=<%= (typeof project !== 'undefined' && project) ? project.id : '' %>" class="run-card-main" style="display:flex;align-items:center;gap:1rem;text-decoration:none;color:var(--text);flex:1;">
27
+ <div class="run-icon" style="font-size:1.75rem;"><%= r.success ? '✅' : (r.success === null ? '⏳' : '❌') %></div>
24
28
  <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' %>
29
+ <div class="run-name" style="font-family:var(--font-display);font-weight:600;font-size:1.15rem;letter-spacing:-0.01em;"><%= (r.id || '').substring(0, 24) %>...</div>
30
+ <div class="run-test" style="font-family:var(--font-mono);font-size:0.85rem;color:var(--text-muted);margin:0.2rem 0;"><%= r.testName || 'Unknown Test' %></div>
31
+ <div class="run-meta" style="font-size:0.8rem;color:var(--text-disabled);font-weight:500;">
32
+ Status: <strong><%= r.success ? 'Passed' : (r.success === null ? 'Running / Pending' : 'Failed') %></strong> ·
33
+ Duration: <strong><%= r.duration ? (r.duration / 1000).toFixed(1) + 's' : 'N/A' %></strong> ·
34
+ Errors: <strong><%= typeof r.failedCount !== 'undefined' ? r.failedCount : 0 %></strong> ·
35
+ Healing: <strong><%= (typeof r.healedCount !== 'undefined' && r.healedCount > 0) ? r.healedCount + ' healed' : 'none' %></strong>
32
36
  </div>
33
37
  </div>
34
38
  </a>
35
- <div class="run-actions">
39
+
40
+ <div class="run-actions" style="display:flex;gap:0.5rem;align-items:center;margin-left:1.5rem;">
36
41
  <% 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>
42
+ <a href="/export/report/<%= r.id %>?project=<%= (typeof project !== 'undefined' && project) ? project.id : '' %>" class="btn btn-sm btn-primary" target="_blank">HTML Report</a>
43
+ <a href="/export/pdf/<%= r.id %>?project=<%= (typeof project !== 'undefined' && project) ? project.id : '' %>" class="btn btn-sm">Raw TXT</a>
39
44
  <% } else { %>
40
- <span class="run-no-report">⏳ No report</span>
45
+ <span class="run-no-report" style="color:var(--text-disabled);font-size:0.8rem;font-weight:500;">⏳ No Report</span>
41
46
  <% } %>
42
47
  <% if (r.hasAllure) { %>
43
- <a href="/allure-report?project=<%= project.id %>" class="btn btn-sm" target="_blank">Allure Report</a>
48
+ <a href="/allure-report?project=<%= (typeof project !== 'undefined' && project) ? project.id : '' %>" class="btn btn-sm" target="_blank" style="background-color:var(--brand-ochre);color:var(--text);border-color:var(--border-strong);">Allure</a>
44
49
  <% } %>
45
50
  </div>
46
51
  </div>
47
52
  <% }) %>
48
53
  </div>
49
54
 
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>
55
+ <% } %>
@@ -6,48 +6,46 @@
6
6
  <span class="badge badge-project"><%= project.name %></span>
7
7
  </div>
8
8
 
9
+ <% if (stories.length === 0) { %>
10
+ <div class="empty-state">
11
+ <div style="font-size: 3rem; margin-bottom: 1rem;">📝</div>
12
+ <h3>Aucune User Story</h3>
13
+ <p>Ajoutez des fichiers Markdown <code>.md</code> dans le dossier <code>user-story/</code> de votre projet pour commencer.</p>
14
+ </div>
15
+ <% } else { %>
9
16
  <div class="story-list">
10
- <% stories.forEach(s => { %>
11
- <div class="story-card">
17
+ <% const cardColors = ['card-lavender', 'card-peach', 'card-cream']; %>
18
+ <% stories.forEach((s, idx) => { %>
19
+ <div class="story-card <%= cardColors[idx % cardColors.length] %>" style="margin-bottom:1.25rem;">
12
20
  <div class="story-header">
13
- <a href="/stories/<%= s.name %>?project=<%= project.id %>" class="story-title"><%= s.name %></a>
21
+ <a href="/stories/<%= s.name %>?project=<%= project.id %>" class="story-title" style="text-decoration:none;font-weight:600;"><%= s.name %></a>
14
22
  <div class="story-badges">
15
23
  <% if (s.hasPlan) { %><span class="badge badge-plan">Planned</span><% } %>
16
24
  <% if (s.hasSpec) { %><span class="badge badge-spec">Tested</span><% } %>
17
25
  <% if (!s.hasPlan) { %><span class="badge badge-draft">Draft</span><% } %>
18
26
  </div>
19
27
  </div>
20
- <div class="story-preview">
28
+ <div class="story-preview" style="margin:1rem 0;">
21
29
  <% 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><% }) %>
30
+ <% const lines = s.content.split('\n').filter(l => l.startsWith('##') || l.startsWith('**') || l.startsWith('-') || l.trim().length > 0) %>
31
+ <% lines.slice(0, 3).forEach(l => { %>
32
+ <p style="margin:0.25rem 0;font-size:0.9rem;opacity:0.85;"><%= l.replace(/\*\*|##/g, '').substring(0, 100) %></p>
33
+ <% }) %>
24
34
  <% } %>
25
35
  </div>
26
- <div class="story-actions">
27
- <button class="btn btn-sm" onclick="runPipeline('<%= project.id %>', '<%= s.name %>', this)">Full Pipeline</button>
36
+ <div class="story-actions" style="border-top:1px solid var(--border);padding-top:1rem;display:flex;align-items:center;">
37
+ <button class="btn btn-sm btn-primary" onclick="runPipeline('<%= project.id %>', '<%= s.name %>')">▶ Launch Pipeline</button>
38
+ <a href="/stories/<%= s.name %>?project=<%= project.id %>" class="btn btn-sm" style="margin-left:0.5rem;">Configure Spec</a>
28
39
  <span class="story-action-output"></span>
29
40
  </div>
30
41
  </div>
31
42
  <% }) %>
32
43
  </div>
44
+ <% } %>
33
45
 
34
46
  <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);
47
+ async function runPipeline(projectId, storyName) {
48
+ // Rediriger vers la page de détail avec l'instruction de lancement automatique
49
+ window.location.href = `/stories/${encodeURIComponent(storyName)}?project=${projectId}&autoRun=true`;
52
50
  }
53
51
  </script>
@@ -8,35 +8,48 @@
8
8
 
9
9
  <div class="row">
10
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>
11
+ <div class="card card-cream">
12
+ <h3>📝 User Story</h3>
13
+ <div id="story-content-md" style="display:none;"><%= story.content %></div>
14
+ <div class="markdown-content" id="story-content-rendered"></div>
14
15
  </div>
15
16
 
16
17
  <div class="card">
17
- <h3>Pipeline Controls</h3>
18
+ <h3>⚡ Pipeline Controls</h3>
19
+ <p style="color:var(--text-muted);font-size:0.85rem;margin-bottom:1rem;">Trigger step-by-step or launch the fully automated agentic sequence.</p>
18
20
  <div class="action-buttons-vertical">
19
21
  <button class="btn btn-sm" onclick="triggerPlan('<%= story.name %>')">1. Generate Plan</button>
20
22
  <button class="btn btn-sm" onclick="triggerGenerate('<%= story.name %>')">2. Generate Test Spec</button>
21
23
  <button class="btn btn-sm" onclick="triggerExecute('<%= story.name %>')">3. Execute Tests</button>
22
24
  <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>
25
+ <a href="/runs?project=<%= project.id %>" class="btn btn-sm" style="margin-top:0.5rem;justify-content:center;">View Test Results →</a>
26
+ </div>
27
+
28
+ <!-- Streaming console output box -->
29
+ <div id="pipeline-status" class="output-box" style="display:none;"></div>
30
+
31
+ <!-- Neon loader pulse indicator -->
32
+ <div id="pipeline-pulse" class="terminal-pulse" style="display:none;margin-top:1rem;">
33
+ <span class="pulse-dot"></span>
34
+ <span class="pulse-dot"></span>
35
+ <span class="pulse-dot"></span>
36
+ <span class="pulse-text">AI Agent is running...</span>
24
37
  </div>
25
- <div id="pipeline-status" class="output-box"></div>
26
38
  </div>
27
39
  </div>
28
40
 
29
41
  <div class="col col-60">
30
42
  <% if (story.plan) { %>
31
43
  <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>
44
+ <h3>📋 Test Plan <a href="/stories/<%= story.name %>?project=<%= project.id %>&tab=plan" class="btn btn-sm" style="margin-left:auto;">Refresh</a></h3>
45
+ <div id="story-plan-md" style="display:none;"><%= story.plan %></div>
46
+ <div class="markdown-content" id="story-plan-rendered"></div>
34
47
  </div>
35
48
  <% } %>
36
49
 
37
50
  <% if (story.spec) { %>
38
51
  <div class="card">
39
- <h3>Test Spec: <%= story.specName %></h3>
52
+ <h3>🧪 Test Spec: <%= story.specName %></h3>
40
53
  <pre class="code-block"><%= story.spec %></pre>
41
54
  </div>
42
55
  <% } %>
@@ -44,20 +57,86 @@
44
57
  </div>
45
58
 
46
59
  <script>
60
+ // Parse Markdown on Client-Side for extremely high-fidelity tables, checkmarks, list layout
61
+ document.addEventListener('DOMContentLoaded', () => {
62
+ const contentRaw = document.getElementById('story-content-md');
63
+ const contentRendered = document.getElementById('story-content-rendered');
64
+ if (contentRaw && contentRendered) {
65
+ contentRendered.innerHTML = marked.parse(contentRaw.textContent || '');
66
+ }
67
+
68
+ const planRaw = document.getElementById('story-plan-md');
69
+ const planRendered = document.getElementById('story-plan-rendered');
70
+ if (planRaw && planRendered) {
71
+ planRendered.innerHTML = marked.parse(planRaw.textContent || '');
72
+ }
73
+ });
74
+
47
75
  async function callPipeline(action, storyName) {
48
76
  const status = document.getElementById('pipeline-status');
49
- status.textContent = `${action}...`;
77
+ const pulse = document.getElementById('pipeline-pulse');
78
+
79
+ // Reset and show containers
80
+ status.style.display = 'block';
81
+ status.textContent = `🚀 Initializing QA agent action [${action}]...\n\n`;
50
82
  status.className = 'output-box output-running';
83
+ pulse.style.display = 'flex';
84
+
85
+ try {
86
+ const res = await fetch(`/stories/${encodeURIComponent(storyName)}/${action}?project=<%= project.id %>`, { method: 'POST' });
87
+
88
+ if (!res.body) {
89
+ status.textContent += "\nError: The server did not return a readable stream.";
90
+ pulse.style.display = 'none';
91
+ return;
92
+ }
51
93
 
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);
94
+ const reader = res.body.getReader();
95
+ const decoder = new TextDecoder('utf-8');
96
+
97
+ while (true) {
98
+ const { done, value } = await reader.read();
99
+ if (done) break;
100
+
101
+ const chunk = decoder.decode(value, { stream: true });
102
+ status.textContent += chunk;
103
+ status.scrollTop = status.scrollHeight; // Force Autoscroll
104
+ }
105
+
106
+ // Hide pulse loader once done
107
+ pulse.style.display = 'none';
108
+
109
+ // Parse final status
110
+ const finishedText = status.textContent.toLowerCase();
111
+ if (finishedText.includes('[finished: success]')) {
112
+ status.className = 'output-box output-success';
113
+ // Reload page to reflect new plans/specs in EJS
114
+ setTimeout(() => { location.reload(); }, 2000);
115
+ } else {
116
+ status.className = 'output-box output-error';
117
+ }
118
+
119
+ } catch (error) {
120
+ status.textContent += `\n\nNetwork or server connection error: ${error.message}`;
121
+ status.className = 'output-box output-error';
122
+ pulse.style.display = 'none';
123
+ }
57
124
  }
58
125
 
59
126
  function triggerPlan(s) { callPipeline('plan', s); }
60
127
  function triggerGenerate(s) { callPipeline('generate', s); }
61
128
  function triggerExecute(s) { callPipeline('execute', s); }
62
129
  function triggerFull(s) { callPipeline('full-pipeline', s); }
130
+
131
+ document.addEventListener('DOMContentLoaded', () => {
132
+ const urlParams = new URLSearchParams(window.location.search);
133
+ if (urlParams.get('autoRun') === 'true') {
134
+ // Nettoyer l'URL afin qu'un rechargement manuel ne relance pas le processus
135
+ const newUrl = window.location.pathname + '?project=' + urlParams.get('project');
136
+ window.history.replaceState({}, '', newUrl);
137
+
138
+ // Déclencher le pipeline complet
139
+ triggerFull('<%= story.name %>');
140
+ }
141
+ });
63
142
  </script>
@@ -19,43 +19,43 @@
19
19
 
20
20
  <div class="alerts" id="alert-container"></div>
21
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.
22
+ <div class="row" style="margin-bottom:2rem;">
23
+ <div class="col col-40">
24
+ <div class="card card-lavender">
25
+ <h3>⚡ Generate Test Data</h3>
26
+ <p style="color: var(--text-muted); font-size: 0.85rem; margin-bottom: 1.25rem;">
27
+ Auto-generate realistic synthetic mock datasets for your QA pipeline.
28
28
  </p>
29
- <form id="data-form">
29
+ <form id="data-form" class="form">
30
30
  <div class="form-group">
31
31
  <label>Data Type</label>
32
32
  <select id="data-type" class="form-input">
33
- <option value="users">Users</option>
34
- <option value="transactions">Transactions</option>
33
+ <option value="users">Users Accounts</option>
34
+ <option value="transactions">Transactions Logs</option>
35
35
  </select>
36
36
  </div>
37
37
  <div class="form-group">
38
38
  <label>Record Count</label>
39
39
  <input type="number" id="data-count" class="form-input" value="5" min="1" max="100">
40
40
  </div>
41
- <button type="submit" class="btn btn-primary">Generate</button>
41
+ <button type="submit" class="btn btn-primary" style="width:100%;margin-top:0.5rem;">Generate Dataset</button>
42
42
  </form>
43
- <div id="data-output" class="output-box" style="display:none;"></div>
43
+ <div id="data-output" class="output-box" style="display:none;margin-top:1rem;"></div>
44
44
  </div>
45
45
  </div>
46
- <div class="col-60">
46
+ <div class="col col-60">
47
47
  <div class="card">
48
- <h3>Generated Data Files</h3>
48
+ <h3>📂 Generated Data Files</h3>
49
49
  <% if (dataFiles.length === 0) { %>
50
50
  <div class="empty-state" style="padding:2rem 0;">
51
51
  <p>No data files generated yet</p>
52
52
  </div>
53
53
  <% } else { %>
54
54
  <table class="info-table">
55
- <tr><th>File</th><th>Size</th><th>Modified</th><th></th></tr>
55
+ <tr><th>File Name</th><th>Size</th><th>Last Modified</th><th>Action</th></tr>
56
56
  <% dataFiles.sort((a,b) => new Date(b.modified) - new Date(a.modified)).forEach(f => { %>
57
57
  <tr>
58
- <td><code><%= f.name %></code></td>
58
+ <td><code style="font-family:var(--font-mono);font-size:0.8rem;"><%= f.name %></code></td>
59
59
  <td><%= f.size > 1024 ? (f.size / 1024).toFixed(1) + ' KB' : f.size + ' B' %></td>
60
60
  <td><%= new Date(f.modified).toLocaleString() %></td>
61
61
  <td><button class="btn btn-sm btn-danger" onclick="deleteFile('<%= f.name %>')">Delete</button></td>
@@ -68,14 +68,14 @@
68
68
  </div>
69
69
 
70
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:
71
+ <h3>ℹ️ About Test Data Management</h3>
72
+ <p style="color: var(--text-muted); font-size: 0.88rem; line-height:1.5;">
73
+ Generated test data is stored directly in your connected project's <code>data/</code> folder as structured JSON files. These datasets can be easily:
74
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>
75
+ <ul style="color: var(--text-muted); font-size: 0.88rem; padding-left: 1.5rem; margin-top: 0.5rem; line-height:1.6;">
76
+ <li>Read dynamically inside Playwright test scenarios using <code>fs.readFileSync()</code></li>
77
+ <li>Injected directly into a localized sandbox database before test suite runs</li>
78
+ <li>Exported or synced for cross-team quality assurance consistency</li>
79
79
  </ul>
80
80
  </div>
81
81
 
@@ -87,7 +87,7 @@ document.getElementById('data-form').addEventListener('submit', async (e) => {
87
87
  const output = document.getElementById('data-output');
88
88
 
89
89
  output.style.display = 'block';
90
- output.textContent = 'Generating...';
90
+ output.textContent = 'Generating realistic mock data...';
91
91
  output.className = 'output-box output-running';
92
92
 
93
93
  const res = await fetch('/data/generate', {
@@ -108,7 +108,7 @@ document.getElementById('data-form').addEventListener('submit', async (e) => {
108
108
  });
109
109
 
110
110
  async function deleteFile(name) {
111
- if (!confirm(`Delete ${name}?`)) return;
111
+ if (!confirm(`Are you sure you want to delete ${name}?`)) return;
112
112
  const res = await fetch('/data/' + encodeURIComponent(name), { method: 'DELETE' });
113
113
  const data = await res.json();
114
114
  if (data.success) location.reload();