@arcadialdev/arcality 2.2.8 → 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
package/playwright.config.ts
CHANGED
|
@@ -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.
|
|
6
|
+
['./tests/_helpers/ArcalityReporter.js', { outputDir: process.env.REPORTS_DIR || 'tests-report' }]
|
|
7
7
|
],
|
|
8
8
|
use: {
|
|
9
9
|
video: 'on',
|
package/scripts/gen-and-run.mjs
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import 'dotenv/config';
|
|
7
7
|
import fs from "node:fs";
|
|
8
8
|
import path from "node:path";
|
|
9
|
+
import { createRequire } from 'node:module';
|
|
9
10
|
import { spawn, exec } from "node:child_process";
|
|
10
11
|
import chalk from "chalk";
|
|
11
12
|
import { fileURLToPath } from 'url';
|
|
@@ -711,9 +712,23 @@ async function main() {
|
|
|
711
712
|
console.log(chalk.gray(`\n>> ARCALITY_PROJECT_ID: ${finalProjectId}`));
|
|
712
713
|
}
|
|
713
714
|
|
|
714
|
-
//
|
|
715
|
-
//
|
|
716
|
-
|
|
715
|
+
// Resolve playwright CLI using Node's module resolution algorithm.
|
|
716
|
+
// This handles BOTH cases:
|
|
717
|
+
// - Global install: playwright is inside arcality's own node_modules
|
|
718
|
+
// - Local install: playwright is hoisted to the project's root node_modules
|
|
719
|
+
let playwrightCli;
|
|
720
|
+
try {
|
|
721
|
+
const arcalityRequire = createRequire(path.join(PROJECT_ROOT, 'package.json'));
|
|
722
|
+
playwrightCli = arcalityRequire.resolve('playwright/cli');
|
|
723
|
+
} catch {
|
|
724
|
+
// Fallback: try local hoisted path (project_root/../../playwright)
|
|
725
|
+
const hoistedPath = path.join(PROJECT_ROOT, '..', '..', 'playwright', 'cli.js');
|
|
726
|
+
if (fs.existsSync(hoistedPath)) {
|
|
727
|
+
playwrightCli = hoistedPath;
|
|
728
|
+
} else {
|
|
729
|
+
playwrightCli = path.join(PROJECT_ROOT, 'node_modules', 'playwright', 'cli.js');
|
|
730
|
+
}
|
|
731
|
+
}
|
|
717
732
|
const configFile = path.join(PROJECT_ROOT, 'playwright.config.ts');
|
|
718
733
|
|
|
719
734
|
try {
|
|
@@ -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 & 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, '&')
|
|
272
|
+
.replace(/</g, '<')
|
|
273
|
+
.replace(/>/g, '>')
|
|
274
|
+
.replace(/"/g, '"')
|
|
275
|
+
.replace(/'/g, ''');
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
module.exports = ArcalityReporter;
|