aztomiq 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +146 -0
- package/bin/aztomiq.js +336 -0
- package/bin/create-aztomiq.js +77 -0
- package/package.json +58 -0
- package/scripts/analyze-screenshots.js +217 -0
- package/scripts/build.js +39 -0
- package/scripts/builds/admin.js +17 -0
- package/scripts/builds/assets.js +167 -0
- package/scripts/builds/cache.js +48 -0
- package/scripts/builds/config.js +31 -0
- package/scripts/builds/data.js +210 -0
- package/scripts/builds/pages.js +288 -0
- package/scripts/builds/playground-examples.js +50 -0
- package/scripts/builds/templates.js +118 -0
- package/scripts/builds/utils.js +37 -0
- package/scripts/create-bug-tracker.js +277 -0
- package/scripts/deploy.js +135 -0
- package/scripts/feedback-generator.js +102 -0
- package/scripts/ui-test.js +624 -0
- package/scripts/utils/extract-examples.js +44 -0
- package/scripts/utils/migrate-icons.js +67 -0
- package/src/includes/breadcrumbs.ejs +100 -0
- package/src/includes/cloud-tags.ejs +120 -0
- package/src/includes/footer.ejs +37 -0
- package/src/includes/generator.ejs +226 -0
- package/src/includes/head.ejs +73 -0
- package/src/includes/header-data-only.ejs +43 -0
- package/src/includes/header.ejs +71 -0
- package/src/includes/layout.ejs +68 -0
- package/src/includes/legacy-banner.ejs +19 -0
- package/src/includes/mega-menu.ejs +80 -0
- package/src/includes/schema.ejs +20 -0
- package/src/templates/manifest.json.ejs +30 -0
- package/src/templates/readme-dist.md.ejs +58 -0
- package/src/templates/robots.txt.ejs +4 -0
- package/src/templates/sitemap.xml.ejs +69 -0
- package/src/templates/sw.js.ejs +78 -0
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* UI Testing Tool - Automated Visual & i18n Testing
|
|
5
|
+
*
|
|
6
|
+
* Features:
|
|
7
|
+
* - Crawls all pages from sitemap
|
|
8
|
+
* - Takes screenshots for visual review
|
|
9
|
+
* - Detects missing translations (i18n keys)
|
|
10
|
+
* - Checks for CSS layout issues
|
|
11
|
+
* - Generates HTML report
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const puppeteer = require('puppeteer');
|
|
15
|
+
const fs = require('fs').promises;
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const { parseStringPromise } = require('xml2js');
|
|
18
|
+
|
|
19
|
+
// Configuration
|
|
20
|
+
const CONFIG = {
|
|
21
|
+
baseUrl: process.env.BASE_URL || 'http://localhost:3000',
|
|
22
|
+
sitemapPath: './dist-dev/sitemap.xml',
|
|
23
|
+
outputDir: './ui-test-results',
|
|
24
|
+
screenshotDir: './ui-test-results/screenshots',
|
|
25
|
+
viewport: {
|
|
26
|
+
width: 1400,
|
|
27
|
+
height: 900
|
|
28
|
+
},
|
|
29
|
+
mobileViewport: {
|
|
30
|
+
width: 375,
|
|
31
|
+
height: 667
|
|
32
|
+
},
|
|
33
|
+
// Language filter: 'vi', 'en', or null for all
|
|
34
|
+
langFilter: process.argv.includes('--lang=vi') ? 'vi' :
|
|
35
|
+
process.argv.includes('--lang=en') ? 'en' : null,
|
|
36
|
+
// Theme filter: 'light' or 'dark'
|
|
37
|
+
theme: process.argv.includes('--theme=dark') ? 'dark' : 'light'
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Results storage
|
|
41
|
+
const results = {
|
|
42
|
+
timestamp: new Date().toISOString(),
|
|
43
|
+
summary: {
|
|
44
|
+
total: 0,
|
|
45
|
+
passed: 0,
|
|
46
|
+
failed: 0,
|
|
47
|
+
warnings: 0
|
|
48
|
+
},
|
|
49
|
+
pages: []
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Read and parse sitemap
|
|
54
|
+
*/
|
|
55
|
+
async function readSitemap() {
|
|
56
|
+
console.log('š Reading sitemap...');
|
|
57
|
+
const sitemapContent = await fs.readFile(CONFIG.sitemapPath, 'utf-8');
|
|
58
|
+
const sitemap = await parseStringPromise(sitemapContent);
|
|
59
|
+
|
|
60
|
+
// Map production URLs to localhost
|
|
61
|
+
let urls = sitemap.urlset.url
|
|
62
|
+
.map(entry => entry.loc[0].trim())
|
|
63
|
+
.map(url => url.replace('https://aztomiq.site', CONFIG.baseUrl));
|
|
64
|
+
|
|
65
|
+
// Filter by language if specified
|
|
66
|
+
if (CONFIG.langFilter) {
|
|
67
|
+
urls = urls.filter(url => url.includes(`/${CONFIG.langFilter}/`));
|
|
68
|
+
console.log(`š Filtering for language: ${CONFIG.langFilter}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
console.log(`ā
Found ${urls.length} URLs in sitemap`);
|
|
72
|
+
console.log(`š Testing against: ${CONFIG.baseUrl}`);
|
|
73
|
+
|
|
74
|
+
return urls;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Check for i18n issues (missing translation keys)
|
|
79
|
+
*/
|
|
80
|
+
async function checkI18nIssues(page, url) {
|
|
81
|
+
const issues = await page.evaluate(() => {
|
|
82
|
+
const found = [];
|
|
83
|
+
|
|
84
|
+
// Whitelist of known non-translation patterns
|
|
85
|
+
const whitelist = [
|
|
86
|
+
'AZtomiq.site',
|
|
87
|
+
'aztomiq.site',
|
|
88
|
+
'localhost.3000',
|
|
89
|
+
'github.com',
|
|
90
|
+
'npmjs.com'
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
const bodyText = document.body.innerText;
|
|
94
|
+
|
|
95
|
+
// Look for untranslated keys (camelCase.camelCase pattern)
|
|
96
|
+
const possibleKeys = bodyText.match(/[a-z][a-zA-Z]+\.[a-z][a-zA-Z]+/g) || [];
|
|
97
|
+
|
|
98
|
+
possibleKeys.forEach(key => {
|
|
99
|
+
// Skip if in whitelist
|
|
100
|
+
if (whitelist.some(w => key.toLowerCase().includes(w.toLowerCase()))) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Skip if it looks like a URL or domain
|
|
105
|
+
if (key.includes('http') || key.includes('www') || key.includes('.com') || key.includes('.site')) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Skip version numbers (e.g., v1.2.0)
|
|
110
|
+
if (/v?\d+\.\d+/.test(key)) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Find visible elements with this exact text
|
|
115
|
+
const elements = Array.from(document.querySelectorAll('*')).filter(el =>
|
|
116
|
+
el.textContent.trim() === key &&
|
|
117
|
+
el.offsetParent !== null && // visible
|
|
118
|
+
el.children.length === 0 // leaf node (not parent container)
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
if (elements.length > 0) {
|
|
122
|
+
found.push({
|
|
123
|
+
key: key,
|
|
124
|
+
count: elements.length,
|
|
125
|
+
selector: elements[0].tagName.toLowerCase()
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
return found;
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return issues;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Check for CSS layout issues
|
|
138
|
+
*/
|
|
139
|
+
async function checkCSSIssues(page) {
|
|
140
|
+
const issues = await page.evaluate(() => {
|
|
141
|
+
const problems = [];
|
|
142
|
+
|
|
143
|
+
// Check for horizontal overflow
|
|
144
|
+
if (document.body.scrollWidth > window.innerWidth) {
|
|
145
|
+
problems.push({
|
|
146
|
+
type: 'horizontal-overflow',
|
|
147
|
+
message: `Page width (${document.body.scrollWidth}px) exceeds viewport (${window.innerWidth}px)`,
|
|
148
|
+
severity: 'warning'
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Check for elements outside viewport
|
|
153
|
+
const allElements = document.querySelectorAll('*');
|
|
154
|
+
let elementsOutside = 0;
|
|
155
|
+
|
|
156
|
+
allElements.forEach(el => {
|
|
157
|
+
const rect = el.getBoundingClientRect();
|
|
158
|
+
if (rect.right > window.innerWidth + 50) { // 50px tolerance
|
|
159
|
+
elementsOutside++;
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
if (elementsOutside > 0) {
|
|
164
|
+
problems.push({
|
|
165
|
+
type: 'elements-overflow',
|
|
166
|
+
message: `${elementsOutside} elements extend beyond viewport`,
|
|
167
|
+
severity: 'warning'
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Check for invisible text (color too similar to background)
|
|
172
|
+
const textElements = Array.from(document.querySelectorAll('p, span, a, button, h1, h2, h3, h4, h5, h6, label'));
|
|
173
|
+
let lowContrastCount = 0;
|
|
174
|
+
|
|
175
|
+
textElements.forEach(el => {
|
|
176
|
+
const style = window.getComputedStyle(el);
|
|
177
|
+
const color = style.color;
|
|
178
|
+
const bgColor = style.backgroundColor;
|
|
179
|
+
|
|
180
|
+
// Simple check: if both are very similar (basic heuristic)
|
|
181
|
+
if (color && bgColor && color === bgColor) {
|
|
182
|
+
lowContrastCount++;
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
if (lowContrastCount > 0) {
|
|
187
|
+
problems.push({
|
|
188
|
+
type: 'low-contrast',
|
|
189
|
+
message: `${lowContrastCount} elements may have low contrast`,
|
|
190
|
+
severity: 'info'
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return problems;
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
return issues;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Check for console errors
|
|
202
|
+
*/
|
|
203
|
+
async function captureConsoleErrors(page) {
|
|
204
|
+
const errors = [];
|
|
205
|
+
|
|
206
|
+
page.on('console', msg => {
|
|
207
|
+
if (msg.type() === 'error') {
|
|
208
|
+
errors.push(msg.text());
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
page.on('pageerror', error => {
|
|
213
|
+
errors.push(error.message);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
return errors;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Test a single page
|
|
221
|
+
*/
|
|
222
|
+
async function testPage(browser, url, index, total) {
|
|
223
|
+
const pageName = url.replace(CONFIG.baseUrl, '').replace(/\//g, '_') || 'home';
|
|
224
|
+
console.log(`\n[${index + 1}/${total}] Testing: ${url}`);
|
|
225
|
+
|
|
226
|
+
const page = await browser.newPage();
|
|
227
|
+
const errors = [];
|
|
228
|
+
|
|
229
|
+
// Capture console errors
|
|
230
|
+
page.on('console', msg => {
|
|
231
|
+
if (msg.type() === 'error') {
|
|
232
|
+
errors.push(msg.text());
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
page.on('pageerror', error => {
|
|
237
|
+
errors.push(error.message);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const pageResult = {
|
|
241
|
+
url,
|
|
242
|
+
pageName,
|
|
243
|
+
status: 'passed',
|
|
244
|
+
issues: [],
|
|
245
|
+
screenshots: {}
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
// Desktop test
|
|
250
|
+
await page.setViewport(CONFIG.viewport);
|
|
251
|
+
|
|
252
|
+
// Force theme
|
|
253
|
+
await page.emulateMediaFeatures([{ name: 'prefers-color-scheme', value: CONFIG.theme }]);
|
|
254
|
+
|
|
255
|
+
await page.goto(url, { waitUntil: 'networkidle0', timeout: 30000 });
|
|
256
|
+
|
|
257
|
+
// Ensure theme is set in localStorage to avoid flashes or overrides
|
|
258
|
+
await page.evaluate((theme) => {
|
|
259
|
+
localStorage.setItem('theme', theme);
|
|
260
|
+
document.documentElement.setAttribute('data-theme', theme);
|
|
261
|
+
}, CONFIG.theme);
|
|
262
|
+
|
|
263
|
+
// Wait a bit for any animations
|
|
264
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
265
|
+
|
|
266
|
+
// Take desktop screenshot
|
|
267
|
+
const desktopScreenshot = path.join(CONFIG.screenshotDir, `${pageName}_desktop.png`);
|
|
268
|
+
await page.screenshot({
|
|
269
|
+
path: desktopScreenshot,
|
|
270
|
+
fullPage: true
|
|
271
|
+
});
|
|
272
|
+
pageResult.screenshots.desktop = desktopScreenshot;
|
|
273
|
+
console.log(` šø Desktop screenshot saved`);
|
|
274
|
+
|
|
275
|
+
// Check i18n issues
|
|
276
|
+
const i18nIssues = await checkI18nIssues(page, url);
|
|
277
|
+
if (i18nIssues.length > 0) {
|
|
278
|
+
pageResult.issues.push({
|
|
279
|
+
type: 'i18n',
|
|
280
|
+
severity: 'error',
|
|
281
|
+
count: i18nIssues.length,
|
|
282
|
+
details: i18nIssues
|
|
283
|
+
});
|
|
284
|
+
console.log(` ā ļø Found ${i18nIssues.length} i18n issues`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Check CSS issues
|
|
288
|
+
const cssIssues = await checkCSSIssues(page);
|
|
289
|
+
if (cssIssues.length > 0) {
|
|
290
|
+
pageResult.issues.push({
|
|
291
|
+
type: 'css',
|
|
292
|
+
severity: 'warning',
|
|
293
|
+
count: cssIssues.length,
|
|
294
|
+
details: cssIssues
|
|
295
|
+
});
|
|
296
|
+
console.log(` ā ļø Found ${cssIssues.length} CSS issues`);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Check console errors
|
|
300
|
+
if (errors.length > 0) {
|
|
301
|
+
pageResult.issues.push({
|
|
302
|
+
type: 'console-errors',
|
|
303
|
+
severity: 'error',
|
|
304
|
+
count: errors.length,
|
|
305
|
+
details: errors
|
|
306
|
+
});
|
|
307
|
+
console.log(` ā Found ${errors.length} console errors`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Mobile test
|
|
311
|
+
await page.setViewport(CONFIG.mobileViewport);
|
|
312
|
+
await page.reload({ waitUntil: 'networkidle0' });
|
|
313
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
314
|
+
|
|
315
|
+
const mobileScreenshot = path.join(CONFIG.screenshotDir, `${pageName}_mobile.png`);
|
|
316
|
+
await page.screenshot({
|
|
317
|
+
path: mobileScreenshot,
|
|
318
|
+
fullPage: true
|
|
319
|
+
});
|
|
320
|
+
pageResult.screenshots.mobile = mobileScreenshot;
|
|
321
|
+
console.log(` š± Mobile screenshot saved`);
|
|
322
|
+
|
|
323
|
+
// Determine status
|
|
324
|
+
const hasErrors = pageResult.issues.some(i => i.severity === 'error');
|
|
325
|
+
const hasWarnings = pageResult.issues.some(i => i.severity === 'warning');
|
|
326
|
+
|
|
327
|
+
if (hasErrors) {
|
|
328
|
+
pageResult.status = 'failed';
|
|
329
|
+
results.summary.failed++;
|
|
330
|
+
} else if (hasWarnings) {
|
|
331
|
+
pageResult.status = 'warning';
|
|
332
|
+
results.summary.warnings++;
|
|
333
|
+
} else {
|
|
334
|
+
pageResult.status = 'passed';
|
|
335
|
+
results.summary.passed++;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
console.log(` ā
Status: ${pageResult.status.toUpperCase()}`);
|
|
339
|
+
|
|
340
|
+
} catch (error) {
|
|
341
|
+
pageResult.status = 'error';
|
|
342
|
+
pageResult.error = error.message;
|
|
343
|
+
results.summary.failed++;
|
|
344
|
+
console.log(` ā Error: ${error.message}`);
|
|
345
|
+
} finally {
|
|
346
|
+
await page.close();
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return pageResult;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Generate HTML report
|
|
354
|
+
*/
|
|
355
|
+
async function generateReport() {
|
|
356
|
+
console.log('\nš Generating HTML report...');
|
|
357
|
+
|
|
358
|
+
const html = `
|
|
359
|
+
<!DOCTYPE html>
|
|
360
|
+
<html lang="en">
|
|
361
|
+
<head>
|
|
362
|
+
<meta charset="UTF-8">
|
|
363
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
364
|
+
<title>UI Test Report - ${new Date(results.timestamp).toLocaleString()}</title>
|
|
365
|
+
<style>
|
|
366
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
367
|
+
body {
|
|
368
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
369
|
+
background: #f5f5f5;
|
|
370
|
+
padding: 2rem;
|
|
371
|
+
line-height: 1.6;
|
|
372
|
+
}
|
|
373
|
+
.container { max-width: 1400px; margin: 0 auto; }
|
|
374
|
+
.header {
|
|
375
|
+
background: white;
|
|
376
|
+
padding: 2rem;
|
|
377
|
+
border-radius: 12px;
|
|
378
|
+
margin-bottom: 2rem;
|
|
379
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
380
|
+
}
|
|
381
|
+
h1 { color: #333; margin-bottom: 0.5rem; }
|
|
382
|
+
.timestamp { color: #666; font-size: 0.9rem; }
|
|
383
|
+
.summary {
|
|
384
|
+
display: grid;
|
|
385
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
386
|
+
gap: 1rem;
|
|
387
|
+
margin-bottom: 2rem;
|
|
388
|
+
}
|
|
389
|
+
.summary-card {
|
|
390
|
+
background: white;
|
|
391
|
+
padding: 1.5rem;
|
|
392
|
+
border-radius: 12px;
|
|
393
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
394
|
+
}
|
|
395
|
+
.summary-card h3 { font-size: 2rem; margin-bottom: 0.5rem; }
|
|
396
|
+
.summary-card p { color: #666; font-size: 0.9rem; }
|
|
397
|
+
.passed { color: #16a34a; }
|
|
398
|
+
.failed { color: #dc2626; }
|
|
399
|
+
.warning { color: #ea580c; }
|
|
400
|
+
.page-card {
|
|
401
|
+
background: white;
|
|
402
|
+
padding: 1.5rem;
|
|
403
|
+
border-radius: 12px;
|
|
404
|
+
margin-bottom: 1rem;
|
|
405
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
406
|
+
}
|
|
407
|
+
.page-header {
|
|
408
|
+
display: flex;
|
|
409
|
+
justify-content: space-between;
|
|
410
|
+
align-items: center;
|
|
411
|
+
margin-bottom: 1rem;
|
|
412
|
+
padding-bottom: 1rem;
|
|
413
|
+
border-bottom: 1px solid #e5e5e5;
|
|
414
|
+
}
|
|
415
|
+
.page-url { font-weight: 600; color: #333; }
|
|
416
|
+
.status-badge {
|
|
417
|
+
padding: 0.25rem 0.75rem;
|
|
418
|
+
border-radius: 6px;
|
|
419
|
+
font-size: 0.85rem;
|
|
420
|
+
font-weight: 600;
|
|
421
|
+
text-transform: uppercase;
|
|
422
|
+
}
|
|
423
|
+
.status-passed { background: #dcfce7; color: #16a34a; }
|
|
424
|
+
.status-failed { background: #fee2e2; color: #dc2626; }
|
|
425
|
+
.status-warning { background: #ffedd5; color: #ea580c; }
|
|
426
|
+
.screenshots {
|
|
427
|
+
display: grid;
|
|
428
|
+
grid-template-columns: 1fr 1fr;
|
|
429
|
+
gap: 1rem;
|
|
430
|
+
margin-top: 1rem;
|
|
431
|
+
}
|
|
432
|
+
.screenshot-box {
|
|
433
|
+
border: 1px solid #e5e5e5;
|
|
434
|
+
border-radius: 8px;
|
|
435
|
+
overflow: hidden;
|
|
436
|
+
}
|
|
437
|
+
.screenshot-box img {
|
|
438
|
+
width: 100%;
|
|
439
|
+
display: block;
|
|
440
|
+
}
|
|
441
|
+
.screenshot-label {
|
|
442
|
+
padding: 0.5rem;
|
|
443
|
+
background: #f5f5f5;
|
|
444
|
+
font-size: 0.85rem;
|
|
445
|
+
font-weight: 600;
|
|
446
|
+
text-align: center;
|
|
447
|
+
}
|
|
448
|
+
.issues {
|
|
449
|
+
margin-top: 1rem;
|
|
450
|
+
}
|
|
451
|
+
.issue {
|
|
452
|
+
background: #fef3c7;
|
|
453
|
+
border-left: 4px solid #f59e0b;
|
|
454
|
+
padding: 1rem;
|
|
455
|
+
margin-bottom: 0.5rem;
|
|
456
|
+
border-radius: 4px;
|
|
457
|
+
}
|
|
458
|
+
.issue.error {
|
|
459
|
+
background: #fee2e2;
|
|
460
|
+
border-left-color: #dc2626;
|
|
461
|
+
}
|
|
462
|
+
.issue-title {
|
|
463
|
+
font-weight: 600;
|
|
464
|
+
margin-bottom: 0.5rem;
|
|
465
|
+
}
|
|
466
|
+
.issue-details {
|
|
467
|
+
font-size: 0.9rem;
|
|
468
|
+
color: #666;
|
|
469
|
+
font-family: monospace;
|
|
470
|
+
background: white;
|
|
471
|
+
padding: 0.5rem;
|
|
472
|
+
border-radius: 4px;
|
|
473
|
+
margin-top: 0.5rem;
|
|
474
|
+
overflow-x: auto;
|
|
475
|
+
}
|
|
476
|
+
</style>
|
|
477
|
+
</head>
|
|
478
|
+
<body>
|
|
479
|
+
<div class="container">
|
|
480
|
+
<div class="header">
|
|
481
|
+
<h1>š§Ŗ UI Test Report</h1>
|
|
482
|
+
<p class="timestamp">Generated: ${new Date(results.timestamp).toLocaleString()}</p>
|
|
483
|
+
</div>
|
|
484
|
+
|
|
485
|
+
<div class="summary">
|
|
486
|
+
<div class="summary-card">
|
|
487
|
+
<h3>${results.summary.total}</h3>
|
|
488
|
+
<p>Total Pages</p>
|
|
489
|
+
</div>
|
|
490
|
+
<div class="summary-card">
|
|
491
|
+
<h3 class="passed">${results.summary.passed}</h3>
|
|
492
|
+
<p>Passed</p>
|
|
493
|
+
</div>
|
|
494
|
+
<div class="summary-card">
|
|
495
|
+
<h3 class="warning">${results.summary.warnings}</h3>
|
|
496
|
+
<p>Warnings</p>
|
|
497
|
+
</div>
|
|
498
|
+
<div class="summary-card">
|
|
499
|
+
<h3 class="failed">${results.summary.failed}</h3>
|
|
500
|
+
<p>Failed</p>
|
|
501
|
+
</div>
|
|
502
|
+
</div>
|
|
503
|
+
|
|
504
|
+
${results.pages.map(page => `
|
|
505
|
+
<div class="page-card">
|
|
506
|
+
<div class="page-header">
|
|
507
|
+
<div class="page-url">${page.url}</div>
|
|
508
|
+
<span class="status-badge status-${page.status}">${page.status}</span>
|
|
509
|
+
</div>
|
|
510
|
+
|
|
511
|
+
${page.issues.length > 0 ? `
|
|
512
|
+
<div class="issues">
|
|
513
|
+
<strong>Issues Found:</strong>
|
|
514
|
+
${page.issues.map(issue => `
|
|
515
|
+
<div class="issue ${issue.severity === 'error' ? 'error' : ''}">
|
|
516
|
+
<div class="issue-title">
|
|
517
|
+
${issue.type.toUpperCase()} (${issue.count} ${issue.count === 1 ? 'issue' : 'issues'})
|
|
518
|
+
</div>
|
|
519
|
+
<div class="issue-details">
|
|
520
|
+
${JSON.stringify(issue.details, null, 2)}
|
|
521
|
+
</div>
|
|
522
|
+
</div>
|
|
523
|
+
`).join('')}
|
|
524
|
+
</div>
|
|
525
|
+
` : '<p style="color: #16a34a;">ā
No issues found</p>'}
|
|
526
|
+
|
|
527
|
+
<div class="screenshots">
|
|
528
|
+
<div class="screenshot-box">
|
|
529
|
+
<div class="screenshot-label">š„ļø Desktop (1400x900)</div>
|
|
530
|
+
<img src="${path.relative(CONFIG.outputDir, page.screenshots.desktop)}" alt="Desktop screenshot">
|
|
531
|
+
</div>
|
|
532
|
+
<div class="screenshot-box">
|
|
533
|
+
<div class="screenshot-label">š± Mobile (375x667)</div>
|
|
534
|
+
<img src="${path.relative(CONFIG.outputDir, page.screenshots.mobile)}" alt="Mobile screenshot">
|
|
535
|
+
</div>
|
|
536
|
+
</div>
|
|
537
|
+
</div>
|
|
538
|
+
`).join('')}
|
|
539
|
+
</div>
|
|
540
|
+
</body>
|
|
541
|
+
</html>
|
|
542
|
+
`;
|
|
543
|
+
|
|
544
|
+
const reportPath = path.join(CONFIG.outputDir, 'report.html');
|
|
545
|
+
await fs.writeFile(reportPath, html);
|
|
546
|
+
console.log(`ā
Report saved to: ${reportPath}`);
|
|
547
|
+
|
|
548
|
+
return reportPath;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Main execution
|
|
553
|
+
*/
|
|
554
|
+
async function main() {
|
|
555
|
+
console.log('š Starting UI Testing Tool\n');
|
|
556
|
+
console.log(`Base URL: ${CONFIG.baseUrl}`);
|
|
557
|
+
console.log(`Output: ${CONFIG.outputDir}\n`);
|
|
558
|
+
|
|
559
|
+
// Clean up old screenshots
|
|
560
|
+
try {
|
|
561
|
+
const files = await fs.readdir(CONFIG.screenshotDir);
|
|
562
|
+
console.log(`šļø Cleaning up ${files.length} old screenshots...`);
|
|
563
|
+
for (const file of files) {
|
|
564
|
+
if (file.endsWith('.png')) {
|
|
565
|
+
await fs.unlink(path.join(CONFIG.screenshotDir, file));
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
console.log('ā
Cleanup complete\n');
|
|
569
|
+
} catch (err) {
|
|
570
|
+
// Directory doesn't exist yet, that's fine
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Create output directories
|
|
574
|
+
await fs.mkdir(CONFIG.outputDir, { recursive: true });
|
|
575
|
+
await fs.mkdir(CONFIG.screenshotDir, { recursive: true });
|
|
576
|
+
|
|
577
|
+
// Read sitemap
|
|
578
|
+
const urls = await readSitemap();
|
|
579
|
+
results.summary.total = urls.length;
|
|
580
|
+
|
|
581
|
+
// Launch browser
|
|
582
|
+
console.log('\nš Launching browser...');
|
|
583
|
+
const browser = await puppeteer.launch({
|
|
584
|
+
headless: 'new',
|
|
585
|
+
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
// Test each page
|
|
589
|
+
for (let i = 0; i < urls.length; i++) {
|
|
590
|
+
const pageResult = await testPage(browser, urls[i], i, urls.length);
|
|
591
|
+
results.pages.push(pageResult);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
await browser.close();
|
|
595
|
+
|
|
596
|
+
// Generate report
|
|
597
|
+
const reportPath = await generateReport();
|
|
598
|
+
|
|
599
|
+
// Save JSON results
|
|
600
|
+
const jsonPath = path.join(CONFIG.outputDir, 'results.json');
|
|
601
|
+
await fs.writeFile(jsonPath, JSON.stringify(results, null, 2));
|
|
602
|
+
console.log(`ā
JSON results saved to: ${jsonPath}`);
|
|
603
|
+
|
|
604
|
+
// Print summary
|
|
605
|
+
console.log('\n' + '='.repeat(60));
|
|
606
|
+
console.log('š TEST SUMMARY');
|
|
607
|
+
console.log('='.repeat(60));
|
|
608
|
+
console.log(`Total Pages: ${results.summary.total}`);
|
|
609
|
+
console.log(`ā
Passed: ${results.summary.passed}`);
|
|
610
|
+
console.log(`ā ļø Warnings: ${results.summary.warnings}`);
|
|
611
|
+
console.log(`ā Failed: ${results.summary.failed}`);
|
|
612
|
+
console.log('='.repeat(60));
|
|
613
|
+
console.log(`\nš Full report: ${reportPath}`);
|
|
614
|
+
console.log(`\nš” Open report: open ${reportPath}\n`);
|
|
615
|
+
|
|
616
|
+
// Exit with error code if there are failures
|
|
617
|
+
process.exit(results.summary.failed > 0 ? 1 : 0);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Run
|
|
621
|
+
main().catch(error => {
|
|
622
|
+
console.error('ā Fatal error:', error);
|
|
623
|
+
process.exit(1);
|
|
624
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const EXAMPLES_FILE = path.join(__dirname, '../../src/features/web-playground/examples.js');
|
|
5
|
+
const EXAMPLES_DIR = path.join(__dirname, '../../src/features/web-playground/examples');
|
|
6
|
+
|
|
7
|
+
async function extract() {
|
|
8
|
+
const content = await fs.readFile(EXAMPLES_FILE, 'utf8');
|
|
9
|
+
// Basic extraction using regex (not perfect but should work for this specific file structure)
|
|
10
|
+
// Actually, it's easier to just REQUIRE the file if it was a module, but it's a window property.
|
|
11
|
+
// Let's use a safer way: eval or just parse the JS.
|
|
12
|
+
// Since I know the structure, I'll just use a small hack to get the array.
|
|
13
|
+
|
|
14
|
+
const arrayStart = content.indexOf('window.PLAYGROUND_EXAMPLES = [');
|
|
15
|
+
const arrayEnd = content.lastIndexOf('];');
|
|
16
|
+
const arrayStr = content.substring(arrayStart + 'window.PLAYGROUND_EXAMPLES ='.length, arrayEnd + 1);
|
|
17
|
+
|
|
18
|
+
let examples = [];
|
|
19
|
+
try {
|
|
20
|
+
examples = eval(arrayStr);
|
|
21
|
+
} catch (e) {
|
|
22
|
+
console.error("Failed to eval examples array", e);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
await fs.ensureDir(EXAMPLES_DIR);
|
|
27
|
+
|
|
28
|
+
for (const ex of examples) {
|
|
29
|
+
const dir = path.join(EXAMPLES_DIR, ex.id);
|
|
30
|
+
await fs.ensureDir(dir);
|
|
31
|
+
|
|
32
|
+
await fs.writeFile(path.join(dir, 'index.html'), ex.html || '');
|
|
33
|
+
await fs.writeFile(path.join(dir, 'style.css'), ex.css || '');
|
|
34
|
+
await fs.writeFile(path.join(dir, 'script.js'), ex.js || '');
|
|
35
|
+
await fs.writeJson(path.join(dir, 'meta.json'), {
|
|
36
|
+
id: ex.id,
|
|
37
|
+
title: ex.title
|
|
38
|
+
}, { spaces: 2 });
|
|
39
|
+
|
|
40
|
+
console.log(`ā
Extracted: ${ex.id}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
extract();
|