@iqual/playwright-vrt 0.1.0 → 0.1.2
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/{playwright.config.ts → dist/playwright.config.js} +3 -5
- package/dist/src/cli.d.ts +3 -0
- package/dist/src/cli.d.ts.map +1 -0
- package/dist/src/cli.js +301 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/collect.d.ts +11 -0
- package/dist/src/collect.d.ts.map +1 -0
- package/dist/src/collect.js +133 -0
- package/dist/src/collect.js.map +1 -0
- package/dist/src/config.d.ts +38 -0
- package/dist/src/config.d.ts.map +1 -0
- package/dist/src/config.js +79 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.d.ts.map +1 -0
- package/{src/index.ts → dist/src/index.js} +1 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/runner.d.ts +28 -0
- package/dist/src/runner.d.ts.map +1 -0
- package/dist/src/runner.js +182 -0
- package/dist/src/runner.js.map +1 -0
- package/dist/src/workspace.d.ts +18 -0
- package/dist/src/workspace.d.ts.map +1 -0
- package/dist/src/workspace.js +51 -0
- package/dist/src/workspace.js.map +1 -0
- package/package.json +8 -7
- package/src/cli.ts +0 -339
- package/src/collect.ts +0 -171
- package/src/config.ts +0 -119
- package/src/runner.ts +0 -232
- package/src/workspace.ts +0 -64
- /package/{tests → dist/tests}/vrt.css +0 -0
- /package/{tests → dist/tests}/vrt.spec.ts +0 -0
package/src/collect.ts
DELETED
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
|
|
3
|
-
import Sitemapper from 'sitemapper';
|
|
4
|
-
import micromatch from 'micromatch';
|
|
5
|
-
import { chromium } from 'playwright';
|
|
6
|
-
import type { VRTConfig } from './config';
|
|
7
|
-
|
|
8
|
-
export interface URLCollectionResult {
|
|
9
|
-
urls: string[];
|
|
10
|
-
source: 'sitemap' | 'crawl';
|
|
11
|
-
total: number;
|
|
12
|
-
filtered: number;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export async function collectURLs(config: VRTConfig): Promise<URLCollectionResult> {
|
|
16
|
-
let urls: string[] = [];
|
|
17
|
-
let source: 'sitemap' | 'crawl' = 'sitemap';
|
|
18
|
-
|
|
19
|
-
// Try sitemap first
|
|
20
|
-
try {
|
|
21
|
-
urls = await collectFromSitemap(config.referenceUrl, config.sitemapPath || '/sitemap.xml');
|
|
22
|
-
} catch (error) {
|
|
23
|
-
console.warn('⚠️ Sitemap fetch failed, falling back to crawler');
|
|
24
|
-
// Fallback to crawling
|
|
25
|
-
urls = await crawlWebsite(config.referenceUrl, config.crawlOptions);
|
|
26
|
-
source = 'crawl';
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const totalUrls = urls.length;
|
|
30
|
-
|
|
31
|
-
// Filter URLs
|
|
32
|
-
urls = filterURLs(urls, config.referenceUrl, config.include || ['*'], config.exclude || []);
|
|
33
|
-
|
|
34
|
-
// Remove duplicate URLs but keep order
|
|
35
|
-
urls = Array.from(new Set(urls));
|
|
36
|
-
|
|
37
|
-
// Limit to maxUrls
|
|
38
|
-
const maxUrls = config.maxUrls || 25;
|
|
39
|
-
const filteredCount = urls.length;
|
|
40
|
-
urls = urls.slice(0, maxUrls);
|
|
41
|
-
|
|
42
|
-
return {
|
|
43
|
-
urls,
|
|
44
|
-
source,
|
|
45
|
-
total: totalUrls,
|
|
46
|
-
filtered: filteredCount,
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async function collectFromSitemap(baseUrl: string, sitemapPath: string): Promise<string[]> {
|
|
51
|
-
const sitemapUrl = new URL(sitemapPath, baseUrl).toString();
|
|
52
|
-
|
|
53
|
-
const sitemap = new Sitemapper({
|
|
54
|
-
url: sitemapUrl,
|
|
55
|
-
timeout: 15000,
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
const { sites } = await sitemap.fetch();
|
|
59
|
-
|
|
60
|
-
if (!sites || sites.length === 0) {
|
|
61
|
-
throw new Error('No URLs found in sitemap');
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return sites;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async function crawlWebsite(
|
|
68
|
-
baseUrl: string,
|
|
69
|
-
options?: VRTConfig['crawlOptions']
|
|
70
|
-
): Promise<string[]> {
|
|
71
|
-
const urls = new Set<string>();
|
|
72
|
-
|
|
73
|
-
console.log(' Using crawler (homepage links only)...');
|
|
74
|
-
|
|
75
|
-
const browser = await chromium.launch({ headless: true });
|
|
76
|
-
const page = await browser.newPage();
|
|
77
|
-
|
|
78
|
-
const baseUrlObj = new URL(baseUrl);
|
|
79
|
-
|
|
80
|
-
try {
|
|
81
|
-
// Visit the homepage
|
|
82
|
-
await page.goto(baseUrl, {
|
|
83
|
-
waitUntil: 'networkidle',
|
|
84
|
-
timeout: 30000
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
// Add the homepage itself, by adding the current page URL
|
|
88
|
-
urls.add(page.url());
|
|
89
|
-
|
|
90
|
-
// Extract all links from the page
|
|
91
|
-
const links = await page.evaluate(() => {
|
|
92
|
-
// @ts-ignore - runs in browser context
|
|
93
|
-
const anchors = Array.from(document.querySelectorAll('a[href]'));
|
|
94
|
-
// @ts-ignore - runs in browser context
|
|
95
|
-
return anchors.map(a => (a as HTMLAnchorElement).href);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
// Filter links to same domain and add to set
|
|
99
|
-
for (const link of links) {
|
|
100
|
-
try {
|
|
101
|
-
const linkUrl = new URL(link);
|
|
102
|
-
|
|
103
|
-
// Only include links from the same domain
|
|
104
|
-
if (linkUrl.hostname === baseUrlObj.hostname) {
|
|
105
|
-
// Normalize URL (remove hash)
|
|
106
|
-
linkUrl.hash = '';
|
|
107
|
-
const normalizedUrl = linkUrl.toString();
|
|
108
|
-
|
|
109
|
-
if (options?.removeTrailingSlash) {
|
|
110
|
-
// Remove trailing slash for consistency (except for root)
|
|
111
|
-
const cleanUrl = normalizedUrl.endsWith('/') && normalizedUrl !== baseUrl + '/'
|
|
112
|
-
? normalizedUrl.slice(0, -1)
|
|
113
|
-
: normalizedUrl;
|
|
114
|
-
urls.add(cleanUrl);
|
|
115
|
-
} else {
|
|
116
|
-
urls.add(normalizedUrl);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
} catch {
|
|
120
|
-
// Skip invalid URLs
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
console.log(` Found ${urls.size} URLs from homepage`);
|
|
125
|
-
|
|
126
|
-
} catch (error) {
|
|
127
|
-
console.error(' Crawler error:', error instanceof Error ? error.message : error);
|
|
128
|
-
// At minimum, return the base URL
|
|
129
|
-
urls.add(baseUrl);
|
|
130
|
-
} finally {
|
|
131
|
-
await browser.close();
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return Array.from(urls);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function filterURLs(urls: string[], baseUrl: string, include: string[], exclude: string[]): string[] {
|
|
138
|
-
return urls.filter((url) => {
|
|
139
|
-
// Convert URL to path for pattern matching
|
|
140
|
-
const urlObj = new URL(url);
|
|
141
|
-
const path = urlObj.pathname + urlObj.search;
|
|
142
|
-
|
|
143
|
-
// Filter URLs that don't match the reference URL domain
|
|
144
|
-
const baseObj = new URL(baseUrl);
|
|
145
|
-
if (urlObj.hostname !== baseObj.hostname) {
|
|
146
|
-
return false;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Also match against full URL for more flexibility
|
|
150
|
-
const fullUrl = url;
|
|
151
|
-
|
|
152
|
-
// Check if excluded
|
|
153
|
-
if (exclude.length > 0) {
|
|
154
|
-
if (micromatch.isMatch(path, exclude, {bash: true})) {
|
|
155
|
-
return false;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Check if included
|
|
160
|
-
if (include.length > 0) {
|
|
161
|
-
return micromatch.isMatch(path, include, {bash: true});
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return true;
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
export function saveURLs(urls: string[], filepath: string): void {
|
|
169
|
-
const content = JSON.stringify(urls, null, 2);
|
|
170
|
-
Bun.write(filepath, content);
|
|
171
|
-
}
|
package/src/config.ts
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
|
|
3
|
-
export interface VRTConfig {
|
|
4
|
-
referenceUrl: string;
|
|
5
|
-
testUrl: string;
|
|
6
|
-
sitemapPath?: string;
|
|
7
|
-
maxUrls?: number;
|
|
8
|
-
exclude?: string[];
|
|
9
|
-
include?: string[];
|
|
10
|
-
crawlOptions?: {
|
|
11
|
-
maxDepth?: number;
|
|
12
|
-
removeTrailingSlash?: boolean;
|
|
13
|
-
};
|
|
14
|
-
viewports?: Array<{
|
|
15
|
-
name: string;
|
|
16
|
-
width: number;
|
|
17
|
-
height: number;
|
|
18
|
-
}>;
|
|
19
|
-
threshold?: {
|
|
20
|
-
maxDiffPixels?: number;
|
|
21
|
-
maxDiffPixelRatio?: number;
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface CLIOptions {
|
|
26
|
-
reference?: string;
|
|
27
|
-
test?: string;
|
|
28
|
-
config: string;
|
|
29
|
-
output?: string;
|
|
30
|
-
maxUrls?: number;
|
|
31
|
-
project?: string;
|
|
32
|
-
verbose?: boolean;
|
|
33
|
-
identifier?: string;
|
|
34
|
-
updateBaseline?: boolean;
|
|
35
|
-
headed?: boolean;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export const DEFAULT_CONFIG: Partial<VRTConfig> = {
|
|
39
|
-
sitemapPath: '/sitemap.xml',
|
|
40
|
-
maxUrls: 25,
|
|
41
|
-
exclude: [],
|
|
42
|
-
include: ['*'],
|
|
43
|
-
crawlOptions: {
|
|
44
|
-
maxDepth: 1,
|
|
45
|
-
removeTrailingSlash: true,
|
|
46
|
-
},
|
|
47
|
-
viewports: [
|
|
48
|
-
{ name: 'desktop', width: 1920, height: 1080 },
|
|
49
|
-
{ name: 'mobile', width: 375, height: 667 },
|
|
50
|
-
],
|
|
51
|
-
threshold: {
|
|
52
|
-
maxDiffPixels: 100,
|
|
53
|
-
maxDiffPixelRatio: 0.01,
|
|
54
|
-
},
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
export async function loadConfig(configPath: string): Promise<VRTConfig> {
|
|
58
|
-
try {
|
|
59
|
-
const file = Bun.file(configPath);
|
|
60
|
-
const config = await file.json();
|
|
61
|
-
|
|
62
|
-
// Merge with defaults
|
|
63
|
-
return {
|
|
64
|
-
...DEFAULT_CONFIG,
|
|
65
|
-
...config,
|
|
66
|
-
crawlOptions: {
|
|
67
|
-
...DEFAULT_CONFIG.crawlOptions,
|
|
68
|
-
...config.crawlOptions,
|
|
69
|
-
},
|
|
70
|
-
viewports: config.viewports || DEFAULT_CONFIG.viewports,
|
|
71
|
-
threshold: {
|
|
72
|
-
...DEFAULT_CONFIG.threshold,
|
|
73
|
-
...config.threshold,
|
|
74
|
-
},
|
|
75
|
-
};
|
|
76
|
-
} catch (error) {
|
|
77
|
-
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
|
|
78
|
-
throw new Error(`Config file not found: ${configPath}`);
|
|
79
|
-
}
|
|
80
|
-
throw error;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export function validateConfig(config: VRTConfig): void {
|
|
85
|
-
// Validate required URLs
|
|
86
|
-
if (!config.testUrl) {
|
|
87
|
-
throw new Error('testUrl is required');
|
|
88
|
-
}
|
|
89
|
-
if (!config.referenceUrl) {
|
|
90
|
-
throw new Error('referenceUrl is required');
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Validate URL format
|
|
94
|
-
try {
|
|
95
|
-
new URL(config.referenceUrl);
|
|
96
|
-
new URL(config.testUrl);
|
|
97
|
-
} catch {
|
|
98
|
-
throw new Error('Invalid URL format in referenceUrl or testUrl');
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Validate viewports
|
|
102
|
-
if (!config.viewports || config.viewports.length === 0) {
|
|
103
|
-
throw new Error('At least one viewport must be defined');
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
for (const vp of config.viewports) {
|
|
107
|
-
if (!vp.name || vp.width <= 0 || vp.height <= 0) {
|
|
108
|
-
throw new Error(`Invalid viewport configuration: ${JSON.stringify(vp)}`);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Validate threshold
|
|
113
|
-
if (config.threshold) {
|
|
114
|
-
if (config.threshold.maxDiffPixelRatio &&
|
|
115
|
-
(config.threshold.maxDiffPixelRatio < 0 || config.threshold.maxDiffPixelRatio > 1)) {
|
|
116
|
-
throw new Error('maxDiffPixelRatio must be between 0 and 1');
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
package/src/runner.ts
DELETED
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
|
|
3
|
-
import { spawn } from 'child_process';
|
|
4
|
-
import * as path from 'path';
|
|
5
|
-
import * as fs from 'fs';
|
|
6
|
-
import type { VRTConfig } from './config';
|
|
7
|
-
|
|
8
|
-
export interface TestResults {
|
|
9
|
-
passed: number;
|
|
10
|
-
failed: number;
|
|
11
|
-
total: number;
|
|
12
|
-
exitCode: number;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface RunnerOptions {
|
|
16
|
-
config: VRTConfig;
|
|
17
|
-
outputDir: string;
|
|
18
|
-
verbose?: boolean;
|
|
19
|
-
project?: string;
|
|
20
|
-
updateBaseline?: boolean;
|
|
21
|
-
hasExplicitReference?: boolean;
|
|
22
|
-
headed?: boolean;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Check if baseline snapshots already exist and are valid
|
|
27
|
-
*/
|
|
28
|
-
export function hasExistingSnapshots(snapshotDir: string): boolean {
|
|
29
|
-
if (!fs.existsSync(snapshotDir)) {
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Recursively check for any .png files
|
|
34
|
-
function hasSnapshotFiles(dir: string): boolean {
|
|
35
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
36
|
-
|
|
37
|
-
for (const entry of entries) {
|
|
38
|
-
const fullPath = path.join(dir, entry.name);
|
|
39
|
-
|
|
40
|
-
if (entry.isDirectory()) {
|
|
41
|
-
if (hasSnapshotFiles(fullPath)) {
|
|
42
|
-
return true;
|
|
43
|
-
}
|
|
44
|
-
} else if (entry.name.endsWith('.png')) {
|
|
45
|
-
return true;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return false;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return hasSnapshotFiles(snapshotDir);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Run Playwright tests using the shipped config and test files
|
|
57
|
-
* Much simpler than the old approach - just exec playwright
|
|
58
|
-
*/
|
|
59
|
-
export async function runVisualTests(options: RunnerOptions): Promise<TestResults> {
|
|
60
|
-
const { config, outputDir, verbose, project, updateBaseline, hasExplicitReference, headed } = options;
|
|
61
|
-
|
|
62
|
-
// Find the playwright-vrt package directory
|
|
63
|
-
const packageDir = path.join(__dirname, '..');
|
|
64
|
-
const playwrightConfigPath = path.join(packageDir, 'playwright.config.ts');
|
|
65
|
-
const snapshotDir = path.join(process.cwd(), 'playwright-snapshots');
|
|
66
|
-
|
|
67
|
-
// Check if baseline snapshots already exist (look for any .png files in snapshots)
|
|
68
|
-
const hasBaseline = !updateBaseline && hasExistingSnapshots(snapshotDir);
|
|
69
|
-
|
|
70
|
-
if (hasBaseline) {
|
|
71
|
-
console.log('\n📸 Using existing baseline snapshots');
|
|
72
|
-
if (verbose) {
|
|
73
|
-
console.log(' (Use --update-baseline to regenerate from reference URL)');
|
|
74
|
-
}
|
|
75
|
-
} else {
|
|
76
|
-
if (updateBaseline) {
|
|
77
|
-
console.log('\n🔄 Updating baseline snapshots...');
|
|
78
|
-
} else {
|
|
79
|
-
console.log('\n📸 Creating baseline snapshots (first run)...');
|
|
80
|
-
}
|
|
81
|
-
console.log(` Source: ${config.referenceUrl}`);
|
|
82
|
-
|
|
83
|
-
// Step 1: Create baseline screenshots
|
|
84
|
-
await runPlaywright({
|
|
85
|
-
configPath: playwrightConfigPath,
|
|
86
|
-
baseURL: config.referenceUrl,
|
|
87
|
-
vrtConfig: config,
|
|
88
|
-
outputDir,
|
|
89
|
-
updateSnapshots: true,
|
|
90
|
-
verbose: true,
|
|
91
|
-
project,
|
|
92
|
-
headed,
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
console.log('✓ Baseline created');
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
console.log(`\n🧪 Testing ${config.testUrl}`);
|
|
99
|
-
|
|
100
|
-
// Step 2: Run tests against test URL
|
|
101
|
-
const exitCode = await runPlaywright({
|
|
102
|
-
configPath: playwrightConfigPath,
|
|
103
|
-
baseURL: config.testUrl,
|
|
104
|
-
vrtConfig: config,
|
|
105
|
-
outputDir,
|
|
106
|
-
updateSnapshots: false,
|
|
107
|
-
verbose: true,
|
|
108
|
-
project,
|
|
109
|
-
headed,
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
// Parse results
|
|
113
|
-
const results = await parseResults(outputDir);
|
|
114
|
-
results.exitCode = exitCode;
|
|
115
|
-
|
|
116
|
-
return results;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
interface PlaywrightRunOptions {
|
|
120
|
-
configPath: string;
|
|
121
|
-
baseURL: string;
|
|
122
|
-
vrtConfig: VRTConfig;
|
|
123
|
-
outputDir: string;
|
|
124
|
-
updateSnapshots: boolean;
|
|
125
|
-
verbose?: boolean;
|
|
126
|
-
project?: string;
|
|
127
|
-
headed?: boolean;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
async function runPlaywright(options: PlaywrightRunOptions): Promise<number> {
|
|
131
|
-
return new Promise((resolve, reject) => {
|
|
132
|
-
const args = ['playwright', 'test', '--config', options.configPath];
|
|
133
|
-
|
|
134
|
-
if (options.updateSnapshots) {
|
|
135
|
-
args.push('--update-snapshots');
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (options.project) {
|
|
139
|
-
args.push('--project', options.project);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (options.headed) {
|
|
143
|
-
args.push('--headed');
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const env = {
|
|
147
|
-
...process.env,
|
|
148
|
-
BASE_URL: options.baseURL,
|
|
149
|
-
VRT_CONFIG: JSON.stringify(options.vrtConfig),
|
|
150
|
-
OUTPUT_DIR: options.outputDir,
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
const proc = spawn('bunx', args, {
|
|
154
|
-
env,
|
|
155
|
-
stdio: options.verbose ? 'inherit' : 'pipe',
|
|
156
|
-
shell: true,
|
|
157
|
-
cwd: process.cwd(),
|
|
158
|
-
}); let stdout = '';
|
|
159
|
-
let stderr = '';
|
|
160
|
-
|
|
161
|
-
if (!options.verbose) {
|
|
162
|
-
proc.stdout?.on('data', (data) => {
|
|
163
|
-
stdout += data.toString();
|
|
164
|
-
});
|
|
165
|
-
proc.stderr?.on('data', (data) => {
|
|
166
|
-
stderr += data.toString();
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
proc.on('close', (code) => {
|
|
171
|
-
const exitCode = code || 0;
|
|
172
|
-
|
|
173
|
-
// For baseline creation (update-snapshots), always succeed
|
|
174
|
-
if (options.updateSnapshots) {
|
|
175
|
-
resolve(0);
|
|
176
|
-
} else {
|
|
177
|
-
// For actual tests, return the exit code
|
|
178
|
-
resolve(exitCode);
|
|
179
|
-
}
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
proc.on('error', (error) => {
|
|
183
|
-
reject(new Error(`Failed to run Playwright: ${error.message}`));
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
async function parseResults(outputDir: string): Promise<TestResults> {
|
|
189
|
-
const resultsPath = path.join(outputDir, 'results.json');
|
|
190
|
-
|
|
191
|
-
try {
|
|
192
|
-
const file = Bun.file(resultsPath);
|
|
193
|
-
const results = await file.json();
|
|
194
|
-
|
|
195
|
-
let passed = 0;
|
|
196
|
-
let failed = 0;
|
|
197
|
-
let total = 0;
|
|
198
|
-
|
|
199
|
-
// Parse Playwright JSON results
|
|
200
|
-
if (results.suites) {
|
|
201
|
-
for (const suite of results.suites) {
|
|
202
|
-
if (suite.specs) {
|
|
203
|
-
for (const spec of suite.specs) {
|
|
204
|
-
total++;
|
|
205
|
-
if (spec.ok) {
|
|
206
|
-
passed++;
|
|
207
|
-
} else {
|
|
208
|
-
failed++;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
return { passed, failed, total, exitCode: 0 };
|
|
216
|
-
} catch {
|
|
217
|
-
return { passed: 0, failed: 0, total: 0, exitCode: 1 };
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
export function printResults(results: TestResults, config: VRTConfig): void {
|
|
222
|
-
console.log('\n📊 Test Results:');
|
|
223
|
-
console.log(` Total: ${results.total}`);
|
|
224
|
-
console.log(` Passed: ${results.passed}`);
|
|
225
|
-
console.log(` Failed: ${results.failed}`);
|
|
226
|
-
|
|
227
|
-
if (results.failed > 0) {
|
|
228
|
-
console.log(`\n❌ ${results.failed} visual difference(s) detected`);
|
|
229
|
-
} else if (results.total > 0) {
|
|
230
|
-
console.log('\n✅ All visual tests passed');
|
|
231
|
-
}
|
|
232
|
-
}
|
package/src/workspace.ts
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
|
|
3
|
-
import * as fs from 'fs';
|
|
4
|
-
import * as path from 'path';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Workspace manages the .playwright-vrt/ directory
|
|
8
|
-
* This is persistent between runs for debugging and caching
|
|
9
|
-
*/
|
|
10
|
-
export class Workspace {
|
|
11
|
-
private readonly dir: string;
|
|
12
|
-
|
|
13
|
-
constructor(baseDir?: string) {
|
|
14
|
-
// Use .playwright-vrt in current directory (or specified base)
|
|
15
|
-
this.dir = path.join(baseDir || process.cwd(), '.playwright-vrt');
|
|
16
|
-
|
|
17
|
-
// Create workspace directory
|
|
18
|
-
fs.mkdirSync(this.dir, { recursive: true });
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
getPath(file: string = ''): string {
|
|
22
|
-
return file ? path.join(this.dir, file) : this.dir;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
writeFile(filename: string, content: string): void {
|
|
26
|
-
const filePath = this.getPath(filename);
|
|
27
|
-
const dir = path.dirname(filePath);
|
|
28
|
-
|
|
29
|
-
if (!fs.existsSync(dir)) {
|
|
30
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
fs.writeFileSync(filePath, content, 'utf-8');
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
readFile(filename: string): string {
|
|
37
|
-
const filePath = this.getPath(filename);
|
|
38
|
-
return fs.readFileSync(filePath, 'utf-8');
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
exists(filename: string): boolean {
|
|
42
|
-
return fs.existsSync(this.getPath(filename));
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
writeJSON(filename: string, data: any): void {
|
|
46
|
-
this.writeFile(filename, JSON.stringify(data, null, 2));
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
readJSON(filename: string): any {
|
|
50
|
-
return JSON.parse(this.readFile(filename));
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Manual cleanup - workspace is persistent by default
|
|
54
|
-
clean(): void {
|
|
55
|
-
if (fs.existsSync(this.dir)) {
|
|
56
|
-
fs.rmSync(this.dir, { recursive: true, force: true });
|
|
57
|
-
console.log(`🗑️ Cleaned workspace: ${this.dir}`);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
info(): void {
|
|
62
|
-
console.log(`📁 Workspace: ${this.dir}`);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
File without changes
|
|
File without changes
|