@arcadialdev/arcality 2.2.9 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcadialdev/arcality",
3
- "version": "2.2.9",
3
+ "version": "2.3.0",
4
4
  "description": "AI-powered QA testing tool — Autonomous web testing agent by Arcadial",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -3,7 +3,7 @@ import { defineConfig } from '@playwright/test';
3
3
  export default defineConfig({
4
4
  testDir: './tests',
5
5
  reporter: [
6
- ['./tests/_helpers/ArcalityReporter.ts', { outputDir: process.env.REPORTS_DIR || 'tests-report' }]
6
+ ['./tests/_helpers/ArcalityReporter.js', { outputDir: process.env.REPORTS_DIR || 'tests-report' }]
7
7
  ],
8
8
  use: {
9
9
  video: 'on',
@@ -0,0 +1,279 @@
1
+ // tests/_helpers/ArcalityReporter.js
2
+ // Plain JavaScript CJS version — compatible with any Playwright version
3
+ // Avoids TypeScript compilation issues when loaded as a reporter
4
+
5
+ 'use strict';
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ class ArcalityReporter {
11
+ constructor(options = {}) {
12
+ this.outputDir = options.outputDir || 'arcality-report';
13
+ this.results = [];
14
+ this.totalDuration = 0;
15
+ this.startTime = new Date();
16
+ }
17
+
18
+ onBegin(_config, _suite) {
19
+ if (fs.existsSync(this.outputDir)) {
20
+ try {
21
+ fs.rmSync(this.outputDir, { recursive: true, force: true });
22
+ } catch (err) {
23
+ // ignore
24
+ }
25
+ }
26
+ if (!fs.existsSync(this.outputDir)) {
27
+ fs.mkdirSync(this.outputDir, { recursive: true });
28
+ }
29
+ this.startTime = new Date();
30
+ }
31
+
32
+ onTestEnd(test, result) {
33
+ const stepsHtml = result.steps.map(step => this.renderStep(step, 0)).join('');
34
+
35
+ const processedAttachments = result.attachments.map(att => {
36
+ const fileName = att.path ? path.basename(att.path) : `${att.name || 'attachment'}-${Date.now()}.png`;
37
+ const attachmentsDir = path.join(this.outputDir, 'attachments');
38
+ const destPath = path.join(attachmentsDir, fileName);
39
+
40
+ if (!fs.existsSync(attachmentsDir)) {
41
+ fs.mkdirSync(attachmentsDir, { recursive: true });
42
+ }
43
+
44
+ try {
45
+ if (att.path && fs.existsSync(att.path)) {
46
+ fs.copyFileSync(att.path, destPath);
47
+ } else if (att.body) {
48
+ fs.writeFileSync(destPath, att.body);
49
+ }
50
+ return { ...att, path: `attachments/${fileName}` };
51
+ } catch (e) {
52
+ return att;
53
+ }
54
+ });
55
+
56
+ this.results.push({ test, result, stepsHtml, attachments: processedAttachments });
57
+ }
58
+
59
+ async onEnd(result) {
60
+ this.totalDuration = result.duration;
61
+ const htmlContent = this.generateHtml();
62
+ const reportPath = path.join(this.outputDir, 'index.html');
63
+ fs.writeFileSync(reportPath, htmlContent);
64
+ console.log(`✨ Arcality Mission Report generated at: ${reportPath}`);
65
+ }
66
+
67
+ renderStep(step, depth) {
68
+ // Humanize: hide internal Playwright hooks
69
+ if (step.category === 'hook' || step.category === 'fixture') return '';
70
+
71
+ const padding = depth * 16;
72
+ const statusClass = step.error ? 'step-error' : 'step-passed';
73
+ const icon = step.error ? '✕' : '✓';
74
+ const duration = step.duration >= 0 ? `<span class="step-duration">${step.duration}ms</span>` : '';
75
+
76
+ let title = this.escapeHtml(step.title);
77
+ title = title.replace(/^(click|fill|navigate|wait|expect|goto|press)/i, '<span class="step-action">$1</span>');
78
+
79
+ let children = '';
80
+ if (step.steps && step.steps.length > 0) {
81
+ children = `<div class="step-children">${step.steps.map(s => this.renderStep(s, depth + 1)).join('')}</div>`;
82
+ }
83
+
84
+ return `
85
+ <div class="step-wrapper ${statusClass}" style="margin-left: ${padding}px">
86
+ <div class="step-header">
87
+ <span class="step-icon">${icon}</span>
88
+ <span class="step-title">${title}</span>
89
+ ${duration}
90
+ </div>
91
+ ${children}
92
+ </div>
93
+ `;
94
+ }
95
+
96
+ getEngineVersion() {
97
+ try {
98
+ // Check Arcality's own package.json first, then cwd fallback
99
+ const toolPkg = path.join(__dirname, '..', '..', 'package.json');
100
+ const cwdPkg = path.join(process.cwd(), 'package.json');
101
+ const pkgPath = fs.existsSync(toolPkg) ? toolPkg : cwdPkg;
102
+ if (fs.existsSync(pkgPath)) {
103
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
104
+ return pkg.version || '2.2.9';
105
+ }
106
+ } catch (e) { /* ignore */ }
107
+ return '2.2.9';
108
+ }
109
+
110
+ generateHtml() {
111
+ const stats = {
112
+ total: this.results.length,
113
+ passed: this.results.filter(r => r.result.status === 'passed').length,
114
+ failed: this.results.filter(r => r.result.status === 'failed' || r.result.status === 'timedOut').length,
115
+ flaky: this.results.filter(r => r.result.status === 'passed' && r.result.retry > 0).length,
116
+ };
117
+
118
+ const testCards = this.results.map((r, i) => {
119
+ const statusClass = `status-${r.result.status}`;
120
+ const projectName = (r.test.parent && r.test.parent.project && r.test.parent.project()) ? r.test.parent.project().name : 'default';
121
+
122
+ const successSummaryAtt = r.attachments.find(a => a.name === 'success_summary');
123
+ const successBlock = (r.result.status === 'passed' && successSummaryAtt && successSummaryAtt.body)
124
+ ? `<div class="success-block">${this.escapeHtml(successSummaryAtt.body.toString())}</div>`
125
+ : '';
126
+
127
+ const errorBlock = r.result.error
128
+ ? `<div class="steps-section"><span class="section-title">Analysis of the Obstacle</span><div class="error-block">${this.escapeHtml(r.result.error.message || 'Unknown Failure')}</div></div>`
129
+ : '';
130
+
131
+ const mediaAttachments = r.attachments.filter(a => a.contentType && (a.contentType.startsWith('image/') || a.contentType.startsWith('video/')));
132
+ const mediaGrid = mediaAttachments.length > 0 ? `
133
+ <div class="steps-section">
134
+ <span class="section-title">Visual Evidence</span>
135
+ <div class="attachments-grid">
136
+ ${mediaAttachments.map(att => {
137
+ if (att.contentType.startsWith('image/')) {
138
+ return `<div class="attachment-item"><img src="${att.path}" alt="${att.name}" onclick="window.open('${att.path}')"><a href="${att.path}" target="_blank" class="att-label">${att.name}</a></div>`;
139
+ }
140
+ if (att.contentType.startsWith('video/')) {
141
+ return `<div class="attachment-item"><video controls src="${att.path}"></video><a href="${att.path}" target="_blank" class="att-label">${att.name}</a></div>`;
142
+ }
143
+ return '';
144
+ }).join('')}
145
+ </div>
146
+ </div>` : '';
147
+
148
+ return `
149
+ <div class="test-entry ${statusClass}">
150
+ <div class="test-header" onclick="toggleDetails(${i})">
151
+ <div class="test-info">
152
+ <span class="status-dot"></span>
153
+ <div>
154
+ <div class="test-name">${this.escapeHtml(r.test.title)}</div>
155
+ <div class="test-meta">
156
+ <span class="tag tag-project">${projectName}</span>
157
+ ${r.result.retry > 0 ? `<span class="tag tag-retry">Retry #${r.result.retry}</span>` : ''}
158
+ <span>${(r.result.duration / 1000).toFixed(2)}s</span>
159
+ </div>
160
+ </div>
161
+ </div>
162
+ <span style="opacity:0.5; font-size: 1.2rem">▼</span>
163
+ </div>
164
+ <div class="test-details" id="details-${i}">
165
+ ${successBlock}
166
+ ${errorBlock}
167
+ <div class="steps-section">
168
+ <span class="section-title">🧠 Agent Cognitive Process</span>
169
+ <div class="steps-container">${r.stepsHtml || '<p style="opacity:0.5">No steps recorded.</p>'}</div>
170
+ </div>
171
+ ${mediaGrid}
172
+ </div>
173
+ </div>`;
174
+ }).join('');
175
+
176
+ return `<!DOCTYPE html>
177
+ <html lang="en">
178
+ <head>
179
+ <meta charset="UTF-8">
180
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
181
+ <title>Arcality Mission Report</title>
182
+ <style>
183
+ @import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap');
184
+ :root {
185
+ --bg-deep: #020617; --bg-card: #0f172a; --text-main: #f8fafc; --text-muted: #94a3b8;
186
+ --primary: #6366f1; --success: #10b981; --error: #f43f5e; --warning: #f59e0b;
187
+ --border: #1e293b; --accent: #c084fc;
188
+ }
189
+ * { box-sizing: border-box; }
190
+ body { font-family: 'Plus Jakarta Sans', sans-serif; background-color: var(--bg-deep); color: var(--text-main); margin: 0; line-height: 1.6; }
191
+ .container { max-width: 1400px; margin: 0 auto; padding: 2rem; }
192
+ .main-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 3rem; padding-bottom: 2rem; border-bottom: 1px solid var(--border); }
193
+ .brand-section h1 { font-size: 2.5rem; font-weight: 800; margin: 0; background: linear-gradient(to right, #818cf8, #c084fc); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
194
+ .mission-id { font-family: 'JetBrains Mono', monospace; font-size: 0.8rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 3px; }
195
+ .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1.5rem; margin-bottom: 3rem; }
196
+ .stat-card { background: var(--bg-card); padding: 1.5rem; border-radius: 16px; border: 1px solid var(--border); text-align: center; }
197
+ .stat-value { font-size: 2rem; font-weight: 800; display: block; }
198
+ .stat-label { font-size: 0.75rem; color: var(--text-muted); text-transform: uppercase; font-weight: 700; }
199
+ .val-total { color: var(--text-main); } .val-passed { color: var(--success); } .val-failed { color: var(--error); } .val-flaky { color: var(--warning); }
200
+ .test-entry { background: var(--bg-card); border: 1px solid var(--border); border-radius: 20px; margin-bottom: 2rem; overflow: hidden; }
201
+ .test-entry:hover { border-color: var(--primary); }
202
+ .test-header { padding: 1.5rem 2rem; display: flex; justify-content: space-between; align-items: center; cursor: pointer; background: rgba(255,255,255,0.02); }
203
+ .test-info { display: flex; align-items: center; gap: 1rem; }
204
+ .status-dot { width: 12px; height: 12px; border-radius: 50%; display: inline-block; }
205
+ .status-passed .status-dot { background: var(--success); box-shadow: 0 0 10px var(--success); }
206
+ .status-failed .status-dot { background: var(--error); box-shadow: 0 0 10px var(--error); }
207
+ .test-name { font-weight: 700; font-size: 1.1rem; }
208
+ .test-meta { font-size: 0.8rem; color: var(--text-muted); font-family: 'JetBrains Mono', monospace; }
209
+ .test-details { padding: 0 2rem 2rem; }
210
+ .error-block { background: rgba(244,63,94,0.1); border-left: 4px solid var(--error); padding: 1.5rem; border-radius: 8px; margin: 1rem 0; font-family: 'JetBrains Mono', monospace; font-size: 0.9rem; color: #fda4af; white-space: pre-wrap; }
211
+ .success-block { background: rgba(16,185,129,0.1); border-left: 4px solid var(--success); padding: 1.2rem; border-radius: 12px; margin: 1rem 0; font-size: 1.1rem; font-weight: 600; color: #6ee7b7; display: flex; align-items: center; gap: 12px; }
212
+ .success-block::before { content: "✨"; font-size: 1.4rem; }
213
+ .steps-section { margin-top: 2rem; }
214
+ .section-title { font-size: 0.9rem; text-transform: uppercase; letter-spacing: 2px; color: var(--accent); margin-bottom: 1rem; display: block; }
215
+ .step-wrapper { border-left: 1px solid var(--border); padding: 8px 16px; position: relative; }
216
+ .step-header { display: flex; align-items: flex-start; gap: 10px; font-size: 0.9rem; }
217
+ .step-icon { width: 16px; font-size: 0.8rem; opacity: 0.7; }
218
+ .step-action { color: var(--primary); font-weight: 700; }
219
+ .step-duration { font-size: 0.7rem; color: var(--text-muted); margin-left: auto; }
220
+ .step-error { border-left-color: var(--error); color: #fb7185; }
221
+ .attachments-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 1.5rem; margin-top: 2rem; }
222
+ .attachment-item { background: var(--bg-deep); border-radius: 12px; overflow: hidden; border: 1px solid var(--border); }
223
+ .attachment-item img, .attachment-item video { width: 100%; height: 180px; object-fit: cover; display: block; cursor: zoom-in; }
224
+ .att-label { padding: 0.75rem; font-size: 0.75rem; font-weight: 600; text-align: center; background: rgba(255,255,255,0.05); display: block; text-decoration: none; color: var(--text-muted); }
225
+ .tag { padding: 4px 8px; border-radius: 4px; font-size: 0.7rem; font-weight: 800; text-transform: uppercase; }
226
+ .tag-project { background: rgba(192,132,252,0.2); color: var(--accent); }
227
+ .tag-retry { background: rgba(245,158,11,0.2); color: var(--warning); }
228
+ footer { margin-top: 5rem; text-align: center; padding: 2rem; font-size: 0.8rem; color: var(--text-muted); border-top: 1px solid var(--border); }
229
+ </style>
230
+ </head>
231
+ <body>
232
+ <div class="container">
233
+ <div class="main-header">
234
+ <div class="brand-section">
235
+ <span class="mission-id">Mission Alpha System</span>
236
+ <h1>Arcality Mission Report</h1>
237
+ <p style="color: var(--text-muted)">Testing Intelligence &amp; Diagnostics Console</p>
238
+ </div>
239
+ <div style="text-align: right">
240
+ <div style="color: var(--accent); font-weight: 700;">${this.startTime.toLocaleDateString()}</div>
241
+ <div style="font-family: 'JetBrains Mono'; font-size: 0.8rem;">ENGINE VERSION ${this.getEngineVersion()}</div>
242
+ </div>
243
+ </div>
244
+
245
+ <div class="stats-grid">
246
+ <div class="stat-card"><span class="stat-value val-total">${stats.total}</span><span class="stat-label">Total Missions</span></div>
247
+ <div class="stat-card"><span class="stat-value val-passed">${stats.passed}</span><span class="stat-label">Successful</span></div>
248
+ <div class="stat-card"><span class="stat-value val-failed">${stats.failed}</span><span class="stat-label">Fatal Errors</span></div>
249
+ <div class="stat-card"><span class="stat-value val-flaky">${stats.flaky}</span><span class="stat-label">Flaky Tests</span></div>
250
+ <div class="stat-card"><span class="stat-value val-total">${(this.totalDuration / 1000).toFixed(2)}s</span><span class="stat-label">Total Duration</span></div>
251
+ </div>
252
+
253
+ <div class="test-list">${testCards}</div>
254
+
255
+ <footer>ARCALITY ENGINE v${this.getEngineVersion()} • SYSTEM TIME [${new Date().toISOString()}] • GENERATED FOR ARCADIAL</footer>
256
+ </div>
257
+ <script>
258
+ function toggleDetails(id) {
259
+ const el = document.getElementById('details-' + id);
260
+ el.style.display = (el.style.display === 'none' || el.style.display === '') ? 'block' : 'none';
261
+ }
262
+ document.querySelectorAll('.test-details').forEach(d => d.style.display = 'none');
263
+ </script>
264
+ </body>
265
+ </html>`;
266
+ }
267
+
268
+ escapeHtml(unsafe) {
269
+ if (!unsafe) return '';
270
+ return String(unsafe)
271
+ .replace(/&/g, '&amp;')
272
+ .replace(/</g, '&lt;')
273
+ .replace(/>/g, '&gt;')
274
+ .replace(/"/g, '&quot;')
275
+ .replace(/'/g, '&#039;');
276
+ }
277
+ }
278
+
279
+ module.exports = ArcalityReporter;