regressionbot 0.0.1
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/README.md +125 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +236 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.js +164 -0
- package/dist/types.d.ts +89 -0
- package/dist/types.js +9 -0
- package/package.json +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# RegressionBot SDK
|
|
2
|
+
|
|
3
|
+
The declarative visual regression testing SDK.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Fluent Manifest Builder**: Chainable methods to define your test scope.
|
|
8
|
+
- **Matrix Testing**: Test multiple devices and viewports in a single job.
|
|
9
|
+
- **Auto-Discovery**: Scan sitemaps with glob patterns and limits.
|
|
10
|
+
- **Project-Based Baselines**: Share visual history across different environments (Preview vs Prod).
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install regressionbot
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
### Basic Example
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { Visual } from 'regressionbot';
|
|
24
|
+
|
|
25
|
+
const visual = new Visual();
|
|
26
|
+
|
|
27
|
+
const job = await visual
|
|
28
|
+
.test('https://preview.myapp.com')
|
|
29
|
+
.forProject('my-app-web')
|
|
30
|
+
.run();
|
|
31
|
+
|
|
32
|
+
const status = await job.waitForCompletion();
|
|
33
|
+
console.log(`Stability Score: ${status.overallScore}/100`);
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Full Matrix Example
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import { Visual } from 'regressionbot';
|
|
40
|
+
|
|
41
|
+
const visual = new Visual(process.env.API_KEY);
|
|
42
|
+
|
|
43
|
+
const job = await visual
|
|
44
|
+
.test(process.env.VERCEL_PREVIEW_URL) // The Candidate (Test Origin)
|
|
45
|
+
.against('https://production-app.com') // The Source of Truth (Base Origin)
|
|
46
|
+
.forProject('marketing-site-v2') // Context: Links to Baselines & History
|
|
47
|
+
|
|
48
|
+
// Matrix Configuration: Run all checks on both Desktop and Mobile
|
|
49
|
+
.on(['Desktop Chrome', 'iPhone 13'])
|
|
50
|
+
|
|
51
|
+
// Sitemap: Explicitly provide a sitemap location (optional)
|
|
52
|
+
.sitemap('https://production-app.com/sitemap_index.xml')
|
|
53
|
+
|
|
54
|
+
// Scope: Explicitly check critical paths
|
|
55
|
+
.check('/', 'Homepage')
|
|
56
|
+
.check('/pricing', 'Pricing Table')
|
|
57
|
+
|
|
58
|
+
// Discovery: Auto-discover up to 20 blog posts
|
|
59
|
+
.scan('/blog/**', { limit: 20 })
|
|
60
|
+
|
|
61
|
+
// Concurrency: Max parallel browser instances
|
|
62
|
+
.concurrency(10)
|
|
63
|
+
|
|
64
|
+
// Masking: Automatic and manual masking
|
|
65
|
+
.mask(['.ads', '#modal']) // Manual selectors
|
|
66
|
+
// Tip: Adding 'data-vr-mask' to your HTML elements masks them automatically!
|
|
67
|
+
|
|
68
|
+
// Execute: Compiles manifest and triggers the API
|
|
69
|
+
.run();
|
|
70
|
+
|
|
71
|
+
const result = await job.waitForCompletion();
|
|
72
|
+
const summary = await job.getSummary();
|
|
73
|
+
|
|
74
|
+
console.log(`Job ${job.jobId} finished. Overall Score: ${summary.overallScore}`);
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## CLI Usage
|
|
78
|
+
|
|
79
|
+
The `regressionbot` CLI is the easiest way to interact with the API from your terminal or CI scripts.
|
|
80
|
+
|
|
81
|
+
### Authentication
|
|
82
|
+
|
|
83
|
+
The CLI looks for the following environment variables:
|
|
84
|
+
- `REGRESSIONBOT_API_KEY`: Your project API key.
|
|
85
|
+
- `REGRESSIONBOT_API_URL`: (Optional) Override the default API endpoint.
|
|
86
|
+
|
|
87
|
+
### Commands
|
|
88
|
+
|
|
89
|
+
#### 1. Quick Check
|
|
90
|
+
Test a single URL against its established baseline.
|
|
91
|
+
```bash
|
|
92
|
+
npx regressionbot https://example.com --project my-site --on "Desktop Chrome, iPhone 12"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
#### 2. Sitemap Scan
|
|
96
|
+
Test an entire site using glob patterns.
|
|
97
|
+
```bash
|
|
98
|
+
npx regressionbot https://example.com --scan "/**" --exclude "/admin/**" --concurrency 20
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
#### 3. Job Summary
|
|
102
|
+
Get detailed results and diff URLs for a completed job.
|
|
103
|
+
```bash
|
|
104
|
+
npx regressionbot summary <jobId>
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Add the `--download` flag to save regression images locally:
|
|
108
|
+
```bash
|
|
109
|
+
npx regressionbot summary <jobId> --download
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
#### 4. Approve Changes
|
|
113
|
+
Promote the current screenshots of a job to be the new baselines.
|
|
114
|
+
```bash
|
|
115
|
+
npx regressionbot approve <jobId>
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Development
|
|
119
|
+
|
|
120
|
+
### Versioning
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
npm version patch # or minor/major
|
|
124
|
+
npm publish
|
|
125
|
+
```
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const index_1 = require("./index");
|
|
5
|
+
function parseArgs(args) {
|
|
6
|
+
const options = { _: [] };
|
|
7
|
+
for (let i = 0; i < args.length; i++) {
|
|
8
|
+
const arg = args[i];
|
|
9
|
+
if (arg.startsWith('--')) {
|
|
10
|
+
const key = arg.slice(2);
|
|
11
|
+
const value = args[i + 1];
|
|
12
|
+
if (value && !value.startsWith('--')) {
|
|
13
|
+
options[key] = value;
|
|
14
|
+
i++;
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
options[key] = true;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
options._.push(arg);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return options;
|
|
25
|
+
}
|
|
26
|
+
const argv = parseArgs(process.argv.slice(2));
|
|
27
|
+
const command = argv._[0];
|
|
28
|
+
const param = argv._[1];
|
|
29
|
+
const sdk = new index_1.Visual();
|
|
30
|
+
async function main() {
|
|
31
|
+
if (!command || command === 'help' || command === '--help' || command === '-h') {
|
|
32
|
+
showHelp();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
if (command === 'status') {
|
|
37
|
+
if (!param)
|
|
38
|
+
throw new Error('Job ID is required for status command.');
|
|
39
|
+
await checkStatus(param);
|
|
40
|
+
}
|
|
41
|
+
else if (command === 'summary') {
|
|
42
|
+
if (!param)
|
|
43
|
+
throw new Error('Job ID is required for summary command.');
|
|
44
|
+
await showSummary(param, argv);
|
|
45
|
+
}
|
|
46
|
+
else if (command === 'approve') {
|
|
47
|
+
if (!param)
|
|
48
|
+
throw new Error('Job ID is required for approve command.');
|
|
49
|
+
await approveJob(param);
|
|
50
|
+
}
|
|
51
|
+
else if (command.startsWith('http')) {
|
|
52
|
+
// Implicit test command
|
|
53
|
+
await startJob(command, argv);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
console.error(`Unknown command: ${command}`);
|
|
57
|
+
showHelp();
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
console.error(`
|
|
63
|
+
Error: ${error.message}`);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function showHelp() {
|
|
68
|
+
console.log(`
|
|
69
|
+
RegressionBot CLI
|
|
70
|
+
|
|
71
|
+
Usage:
|
|
72
|
+
npx regressionbot <url> Quick test a URL.
|
|
73
|
+
npx regressionbot status <jobId> Check the status of a specific job.
|
|
74
|
+
npx regressionbot summary <jobId> Get detailed results and diff URLs.
|
|
75
|
+
Use --download to save images locally.
|
|
76
|
+
npx regressionbot approve <jobId> Approve a job's results as new baselines.
|
|
77
|
+
|
|
78
|
+
Options for <url>:
|
|
79
|
+
--project <id> Required project ID.
|
|
80
|
+
--against <url> Base origin to compare against.
|
|
81
|
+
--sitemap <url> Explicit sitemap.xml location.
|
|
82
|
+
--on <devices> Comma-separated device names (e.g. "Desktop Chrome,iPhone 12").
|
|
83
|
+
--scan <pattern> Glob pattern to scan in sitemap (e.g. "/blog/**").
|
|
84
|
+
--exclude <patterns> Comma-separated glob patterns to exclude.
|
|
85
|
+
--concurrency <n> Max concurrent workers (default 10).
|
|
86
|
+
--auto-approve Automatically approve results as new baselines.
|
|
87
|
+
--mask <selectors> Comma-separated CSS selectors to hide (e.g. ".ad,#popup").
|
|
88
|
+
|
|
89
|
+
Environment Variables:
|
|
90
|
+
REGRESSIONBOT_API_KEY Override the API Key.
|
|
91
|
+
REGRESSIONBOT_API_URL Override the API URL.
|
|
92
|
+
`);
|
|
93
|
+
}
|
|
94
|
+
async function startJob(url, options) {
|
|
95
|
+
console.log(`🚀 Initializing visual test...`);
|
|
96
|
+
const projectId = options.project || new URL(url).hostname;
|
|
97
|
+
const devices = options.on ? options.on.split(',').map((s) => s.trim()) : ['Desktop Chrome'];
|
|
98
|
+
const builder = sdk.test(url)
|
|
99
|
+
.forProject(projectId)
|
|
100
|
+
.on(devices)
|
|
101
|
+
.concurrency(Number(options.concurrency) || 10);
|
|
102
|
+
if (options.against) {
|
|
103
|
+
builder.against(options.against);
|
|
104
|
+
}
|
|
105
|
+
if (options.sitemap) {
|
|
106
|
+
builder.sitemap(options.sitemap);
|
|
107
|
+
}
|
|
108
|
+
if (options.scan) {
|
|
109
|
+
const exclude = options.exclude ? options.exclude.split(',').map((s) => s.trim()) : [];
|
|
110
|
+
builder.scan(options.scan, { exclude });
|
|
111
|
+
}
|
|
112
|
+
if (options['auto-approve']) {
|
|
113
|
+
builder.autoApprove(true);
|
|
114
|
+
}
|
|
115
|
+
if (options.mask) {
|
|
116
|
+
const selectors = options.mask.split(',').map((s) => s.trim());
|
|
117
|
+
builder.mask(selectors);
|
|
118
|
+
}
|
|
119
|
+
const job = await builder.run();
|
|
120
|
+
console.log(`✅ Job started! ID: ${job.jobId}`);
|
|
121
|
+
console.log(`📊 Project: ${projectId}`);
|
|
122
|
+
console.log(`📱 Matrix: ${devices.join(', ')}`);
|
|
123
|
+
if (options.scan) {
|
|
124
|
+
console.log(`🔍 Scan: ${options.scan} (Exclude: ${options.exclude || 'none'})`);
|
|
125
|
+
}
|
|
126
|
+
console.log(`
|
|
127
|
+
Waiting for completion...
|
|
128
|
+
`);
|
|
129
|
+
const result = await job.waitForCompletion(2000, (status) => {
|
|
130
|
+
const progress = status.progress || { percent: '0' };
|
|
131
|
+
process.stdout.write(`\r Status: ${status.status} (${progress.percent}%)`);
|
|
132
|
+
});
|
|
133
|
+
console.log('\n\n✅ Job Completed.');
|
|
134
|
+
const summary = await job.getSummary();
|
|
135
|
+
console.log(`Overall Stability Score: ${summary.overallScore}/100`);
|
|
136
|
+
console.log(`Total Tasks: ${summary.totalUrls}`);
|
|
137
|
+
console.log(`Regressions: ${summary.regressionCount}`);
|
|
138
|
+
console.log(`New Baselines: ${summary.newBaselineCount}`);
|
|
139
|
+
console.log(`Errors: ${summary.errorCount}`);
|
|
140
|
+
if (summary.newBaselineCount > 0) {
|
|
141
|
+
console.log('\n✨ New Baselines Created:');
|
|
142
|
+
summary.newBaselines.forEach((nb) => {
|
|
143
|
+
console.log(`- ${nb.url} [${nb.variantName}]`);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
if (summary.regressionCount > 0) {
|
|
147
|
+
console.log('\n❌ Regressions found:');
|
|
148
|
+
summary.regressions.forEach((r) => {
|
|
149
|
+
console.log(`- ${r.url} [${r.variantName}] (Score: ${r.score.toFixed(2)})`);
|
|
150
|
+
console.log(` Diff: ${r.diffUrl}`);
|
|
151
|
+
});
|
|
152
|
+
console.log(`\nTo approve these changes, run:\n npx regressionbot approve ${job.jobId}`);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
else if (summary.errorCount > 0) {
|
|
156
|
+
console.log('\n⚠️ Errors encountered:');
|
|
157
|
+
summary.errors.forEach((e) => {
|
|
158
|
+
console.log(`- ${e.url}: ${e.errorMessage}`);
|
|
159
|
+
});
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
console.log('\n✨ No regressions found. All good!');
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
async function checkStatus(jobId) {
|
|
167
|
+
const job = sdk.job(jobId);
|
|
168
|
+
const status = await job.getStatus();
|
|
169
|
+
console.log(JSON.stringify(status, null, 2));
|
|
170
|
+
}
|
|
171
|
+
async function showSummary(jobId, options = {}) {
|
|
172
|
+
const job = sdk.job(jobId);
|
|
173
|
+
const summary = await job.getSummary();
|
|
174
|
+
console.log(`
|
|
175
|
+
Job Summary: ${jobId}
|
|
176
|
+
Status: ${summary.status}
|
|
177
|
+
Overall Score: ${summary.overallScore}/100
|
|
178
|
+
Execution Time: ${summary.executionTime}s
|
|
179
|
+
Total Tasks: ${summary.totalUrls}
|
|
180
|
+
Regressions: ${summary.regressionCount}
|
|
181
|
+
Matches: ${summary.matchCount}
|
|
182
|
+
Errors: ${summary.errorCount}
|
|
183
|
+
`);
|
|
184
|
+
if (summary.collageUrl) {
|
|
185
|
+
console.log(`Collage: ${summary.collageUrl}`);
|
|
186
|
+
if (options.download) {
|
|
187
|
+
const fs = require('fs');
|
|
188
|
+
const path = require('path');
|
|
189
|
+
const dir = path.join(process.cwd(), 'regressions', jobId);
|
|
190
|
+
if (!fs.existsSync(dir))
|
|
191
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
192
|
+
const res = await fetch(summary.collageUrl);
|
|
193
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
194
|
+
const filePath = path.join(dir, 'collage.jpg');
|
|
195
|
+
fs.writeFileSync(filePath, buffer);
|
|
196
|
+
console.log(`💾 Downloaded collage to: ${filePath}\n`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (summary.regressionCount > 0) {
|
|
200
|
+
console.log('❌ Regressions found:');
|
|
201
|
+
for (const r of summary.regressions) {
|
|
202
|
+
console.log(`- ${r.url} [${r.variantName}] (Score: ${r.score.toFixed(2)})`);
|
|
203
|
+
console.log(` Diff: ${r.diffUrl}`);
|
|
204
|
+
if (options.download) {
|
|
205
|
+
const fs = require('fs');
|
|
206
|
+
const path = require('path');
|
|
207
|
+
const dir = path.join(process.cwd(), 'regressions', jobId, r.variantName);
|
|
208
|
+
if (!fs.existsSync(dir))
|
|
209
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
210
|
+
const download = async (url, name) => {
|
|
211
|
+
const res = await fetch(url);
|
|
212
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
213
|
+
const filePath = path.join(dir, name);
|
|
214
|
+
fs.writeFileSync(filePath, buffer);
|
|
215
|
+
console.log(` 💾 Downloaded: ${name}`);
|
|
216
|
+
};
|
|
217
|
+
await download(r.baselineUrl, 'baseline.png');
|
|
218
|
+
await download(r.currentUrl, 'current.png');
|
|
219
|
+
await download(r.diffUrl, 'diff.png');
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (summary.errorCount > 0) {
|
|
224
|
+
console.log('\n⚠️ Errors encountered:');
|
|
225
|
+
summary.errors.forEach((e) => {
|
|
226
|
+
console.log(`- ${e.url}: ${e.errorMessage}`);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
async function approveJob(jobId) {
|
|
231
|
+
console.log(`Approving baselines for job: ${jobId}...`);
|
|
232
|
+
const job = sdk.job(jobId);
|
|
233
|
+
const res = await job.approve();
|
|
234
|
+
console.log(`Success! ${res.message}`);
|
|
235
|
+
}
|
|
236
|
+
main();
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { JobStatus, JobSummary } from './types';
|
|
2
|
+
export declare class Visual {
|
|
3
|
+
private apiKey;
|
|
4
|
+
private apiUrl;
|
|
5
|
+
constructor(apiKey?: string, apiUrl?: string);
|
|
6
|
+
/**
|
|
7
|
+
* Set the candidate URL/Origin to test.
|
|
8
|
+
*/
|
|
9
|
+
test(origin: string): JobBuilder;
|
|
10
|
+
/**
|
|
11
|
+
* Get a handle to an existing job.
|
|
12
|
+
*/
|
|
13
|
+
job(jobId: string): JobHandle;
|
|
14
|
+
_request<T>(path: string, method?: string, body?: any): Promise<T>;
|
|
15
|
+
}
|
|
16
|
+
export declare class JobBuilder {
|
|
17
|
+
private sdk;
|
|
18
|
+
private manifest;
|
|
19
|
+
constructor(sdk: Visual, testOrigin: string);
|
|
20
|
+
against(origin: string): this;
|
|
21
|
+
sitemap(url: string): this;
|
|
22
|
+
forProject(id: string): this;
|
|
23
|
+
/**
|
|
24
|
+
* Define the matrix: list of Playwright devices or viewport names.
|
|
25
|
+
*/
|
|
26
|
+
on(variants: string[]): this;
|
|
27
|
+
/**
|
|
28
|
+
* Add a specific page to the test scope.
|
|
29
|
+
*/
|
|
30
|
+
check(path: string, label?: string): this;
|
|
31
|
+
/**
|
|
32
|
+
* Add a discovery rule to scan the sitemap.
|
|
33
|
+
*/
|
|
34
|
+
scan(pattern: string, options?: {
|
|
35
|
+
limit?: number;
|
|
36
|
+
exclude?: string[];
|
|
37
|
+
}): this;
|
|
38
|
+
concurrency(n: number): this;
|
|
39
|
+
autoApprove(val?: boolean): this;
|
|
40
|
+
mask(selectors: string[]): this;
|
|
41
|
+
/**
|
|
42
|
+
* Compiles the manifest and triggers the API.
|
|
43
|
+
*/
|
|
44
|
+
run(): Promise<JobHandle>;
|
|
45
|
+
}
|
|
46
|
+
export declare class JobHandle {
|
|
47
|
+
private sdk;
|
|
48
|
+
jobId: string;
|
|
49
|
+
constructor(sdk: Visual, jobId: string);
|
|
50
|
+
getStatus(): Promise<JobStatus>;
|
|
51
|
+
getSummary(): Promise<JobSummary>;
|
|
52
|
+
approve(): Promise<{
|
|
53
|
+
message: string;
|
|
54
|
+
}>;
|
|
55
|
+
waitForCompletion(intervalMs?: number, callback?: (status: JobStatus) => void): Promise<JobStatus>;
|
|
56
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.JobHandle = exports.JobBuilder = exports.Visual = void 0;
|
|
4
|
+
class Visual {
|
|
5
|
+
apiKey;
|
|
6
|
+
apiUrl;
|
|
7
|
+
constructor(apiKey, apiUrl) {
|
|
8
|
+
this.apiKey = apiKey || process.env.REGRESSIONBOT_API_KEY || "";
|
|
9
|
+
this.apiUrl = apiUrl || process.env.REGRESSIONBOT_API_URL || "https://api.regressionbot.com";
|
|
10
|
+
if (!this.apiKey) {
|
|
11
|
+
console.warn("Warning: No API Key provided. Set REGRESSIONBOT_API_KEY environment variable or pass it to the constructor.");
|
|
12
|
+
}
|
|
13
|
+
if (this.apiUrl.endsWith('/')) {
|
|
14
|
+
this.apiUrl = this.apiUrl.slice(0, -1);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Set the candidate URL/Origin to test.
|
|
19
|
+
*/
|
|
20
|
+
test(origin) {
|
|
21
|
+
return new JobBuilder(this, origin);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Get a handle to an existing job.
|
|
25
|
+
*/
|
|
26
|
+
job(jobId) {
|
|
27
|
+
return new JobHandle(this, jobId);
|
|
28
|
+
}
|
|
29
|
+
// Internal fetch wrapper
|
|
30
|
+
async _request(path, method = 'GET', body) {
|
|
31
|
+
const headers = {
|
|
32
|
+
'Content-Type': 'application/json',
|
|
33
|
+
'x-api-key': this.apiKey
|
|
34
|
+
};
|
|
35
|
+
const response = await fetch(`${this.apiUrl}${path}`, {
|
|
36
|
+
method,
|
|
37
|
+
headers,
|
|
38
|
+
body: body ? JSON.stringify(body) : undefined
|
|
39
|
+
});
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
const errorText = await response.text();
|
|
42
|
+
throw new Error(`API Error ${response.status}: ${errorText}`);
|
|
43
|
+
}
|
|
44
|
+
return response.json();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
exports.Visual = Visual;
|
|
48
|
+
class JobBuilder {
|
|
49
|
+
sdk;
|
|
50
|
+
manifest;
|
|
51
|
+
constructor(sdk, testOrigin) {
|
|
52
|
+
this.sdk = sdk;
|
|
53
|
+
this.manifest = {
|
|
54
|
+
testOrigin: testOrigin.replace(/\/$/, ''),
|
|
55
|
+
variants: [],
|
|
56
|
+
checks: [],
|
|
57
|
+
scans: [],
|
|
58
|
+
concurrency: 10
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
against(origin) {
|
|
62
|
+
this.manifest.baseOrigin = origin.replace(/\/$/, '');
|
|
63
|
+
return this;
|
|
64
|
+
}
|
|
65
|
+
sitemap(url) {
|
|
66
|
+
this.manifest.sitemapUrl = url;
|
|
67
|
+
return this;
|
|
68
|
+
}
|
|
69
|
+
forProject(id) {
|
|
70
|
+
this.manifest.projectId = id;
|
|
71
|
+
return this;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Define the matrix: list of Playwright devices or viewport names.
|
|
75
|
+
*/
|
|
76
|
+
on(variants) {
|
|
77
|
+
this.manifest.variants.push(...variants);
|
|
78
|
+
return this;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Add a specific page to the test scope.
|
|
82
|
+
*/
|
|
83
|
+
check(path, label) {
|
|
84
|
+
this.manifest.checks.push({ path, label });
|
|
85
|
+
return this;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Add a discovery rule to scan the sitemap.
|
|
89
|
+
*/
|
|
90
|
+
scan(pattern, options) {
|
|
91
|
+
this.manifest.scans.push({ pattern, options });
|
|
92
|
+
return this;
|
|
93
|
+
}
|
|
94
|
+
concurrency(n) {
|
|
95
|
+
this.manifest.concurrency = n;
|
|
96
|
+
return this;
|
|
97
|
+
}
|
|
98
|
+
autoApprove(val = true) {
|
|
99
|
+
this.manifest.autoApprove = val;
|
|
100
|
+
return this;
|
|
101
|
+
}
|
|
102
|
+
mask(selectors) {
|
|
103
|
+
this.manifest.masks = selectors;
|
|
104
|
+
return this;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Compiles the manifest and triggers the API.
|
|
108
|
+
*/
|
|
109
|
+
async run() {
|
|
110
|
+
if (!this.manifest.projectId) {
|
|
111
|
+
throw new Error('Project ID is required. Use .forProject("id")');
|
|
112
|
+
}
|
|
113
|
+
// If no checks/scans provided, default to root
|
|
114
|
+
if (this.manifest.checks.length === 0 && this.manifest.scans.length === 0) {
|
|
115
|
+
this.manifest.checks.push({ path: '/', label: 'Home' });
|
|
116
|
+
}
|
|
117
|
+
const payload = {
|
|
118
|
+
project: this.manifest.projectId,
|
|
119
|
+
testOrigin: this.manifest.testOrigin,
|
|
120
|
+
sitemapUrl: this.manifest.sitemapUrl,
|
|
121
|
+
baseOrigin: this.manifest.baseOrigin,
|
|
122
|
+
devices: this.manifest.variants,
|
|
123
|
+
paths: this.manifest.checks,
|
|
124
|
+
scans: this.manifest.scans,
|
|
125
|
+
concurrency: this.manifest.concurrency,
|
|
126
|
+
autoApprove: this.manifest.autoApprove,
|
|
127
|
+
masks: this.manifest.masks
|
|
128
|
+
};
|
|
129
|
+
const res = await this.sdk._request('/crawl', 'POST', payload);
|
|
130
|
+
return new JobHandle(this.sdk, res.jobId);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
exports.JobBuilder = JobBuilder;
|
|
134
|
+
class JobHandle {
|
|
135
|
+
sdk;
|
|
136
|
+
jobId;
|
|
137
|
+
constructor(sdk, jobId) {
|
|
138
|
+
this.sdk = sdk;
|
|
139
|
+
this.jobId = jobId;
|
|
140
|
+
}
|
|
141
|
+
async getStatus() {
|
|
142
|
+
return this.sdk._request(`/job/${this.jobId}`);
|
|
143
|
+
}
|
|
144
|
+
async getSummary() {
|
|
145
|
+
return this.sdk._request(`/job/${this.jobId}/summary`);
|
|
146
|
+
}
|
|
147
|
+
async approve() {
|
|
148
|
+
return this.sdk._request('/approve', 'POST', { jobId: this.jobId });
|
|
149
|
+
}
|
|
150
|
+
async waitForCompletion(intervalMs = 2000, callback) {
|
|
151
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
152
|
+
while (true) {
|
|
153
|
+
const status = await this.getStatus();
|
|
154
|
+
if (callback)
|
|
155
|
+
callback(status);
|
|
156
|
+
if (status.status === 'COMPLETED' || status.status === 'APPROVED')
|
|
157
|
+
return status;
|
|
158
|
+
if (status.status === 'FAILED')
|
|
159
|
+
throw new Error(`Job Failed: ${status.error}`);
|
|
160
|
+
await new Promise(resolve => setTimeout(resolve, intervalMs));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
exports.JobHandle = JobHandle;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
export interface VRConfig {
|
|
2
|
+
apiKey?: string;
|
|
3
|
+
apiUrl?: string;
|
|
4
|
+
}
|
|
5
|
+
export interface Viewport {
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
}
|
|
9
|
+
export declare const Viewports: {
|
|
10
|
+
readonly DESKTOP: {
|
|
11
|
+
readonly width: 1920;
|
|
12
|
+
readonly height: 1080;
|
|
13
|
+
};
|
|
14
|
+
readonly LAPTOP: {
|
|
15
|
+
readonly width: 1366;
|
|
16
|
+
readonly height: 768;
|
|
17
|
+
};
|
|
18
|
+
readonly TABLET: {
|
|
19
|
+
readonly width: 768;
|
|
20
|
+
readonly height: 1024;
|
|
21
|
+
};
|
|
22
|
+
readonly MOBILE: {
|
|
23
|
+
readonly width: 375;
|
|
24
|
+
readonly height: 667;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
export interface JobResult {
|
|
28
|
+
url: string;
|
|
29
|
+
status: 'SUCCESS' | 'ERROR';
|
|
30
|
+
diffCount?: number;
|
|
31
|
+
diffPercentage?: number;
|
|
32
|
+
score?: number;
|
|
33
|
+
currentKey?: string;
|
|
34
|
+
baselineKey?: string;
|
|
35
|
+
diffKey?: string;
|
|
36
|
+
isNewBaseline?: boolean;
|
|
37
|
+
}
|
|
38
|
+
export interface JobStatus {
|
|
39
|
+
jobId: string;
|
|
40
|
+
status: 'PROCESSING' | 'COMPLETED' | 'APPROVED' | 'FAILED' | 'INITIALIZING';
|
|
41
|
+
error?: string;
|
|
42
|
+
progress?: {
|
|
43
|
+
total: number;
|
|
44
|
+
completed: number;
|
|
45
|
+
percent: string;
|
|
46
|
+
};
|
|
47
|
+
executionTime?: number;
|
|
48
|
+
results?: JobResult[];
|
|
49
|
+
createdAt?: string;
|
|
50
|
+
collageKey?: string;
|
|
51
|
+
}
|
|
52
|
+
export interface JobSummary {
|
|
53
|
+
jobId: string;
|
|
54
|
+
status: string;
|
|
55
|
+
totalUrls: number;
|
|
56
|
+
completedCount: number;
|
|
57
|
+
overallScore: number;
|
|
58
|
+
executionTime: number;
|
|
59
|
+
regressionCount: number;
|
|
60
|
+
matchCount: number;
|
|
61
|
+
newBaselineCount: number;
|
|
62
|
+
errorCount: number;
|
|
63
|
+
collageUrl?: string;
|
|
64
|
+
regressions: Array<{
|
|
65
|
+
url: string;
|
|
66
|
+
variantName: string;
|
|
67
|
+
diffCount: number;
|
|
68
|
+
score: number;
|
|
69
|
+
baselineUrl: string;
|
|
70
|
+
currentUrl: string;
|
|
71
|
+
diffUrl: string;
|
|
72
|
+
}>;
|
|
73
|
+
matches: Array<{
|
|
74
|
+
url: string;
|
|
75
|
+
variantName: string;
|
|
76
|
+
score: number;
|
|
77
|
+
baselineUrl: string;
|
|
78
|
+
currentUrl: string;
|
|
79
|
+
}>;
|
|
80
|
+
newBaselines: Array<{
|
|
81
|
+
url: string;
|
|
82
|
+
variantName: string;
|
|
83
|
+
}>;
|
|
84
|
+
errors: Array<{
|
|
85
|
+
url: string;
|
|
86
|
+
errorMessage: string;
|
|
87
|
+
score: number;
|
|
88
|
+
}>;
|
|
89
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Viewports = void 0;
|
|
4
|
+
exports.Viewports = {
|
|
5
|
+
DESKTOP: { width: 1920, height: 1080 },
|
|
6
|
+
LAPTOP: { width: 1366, height: 768 },
|
|
7
|
+
TABLET: { width: 768, height: 1024 },
|
|
8
|
+
MOBILE: { width: 375, height: 667 }
|
|
9
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "regressionbot",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "The official SDK for regressionbot.com - the simplest way to automate visual regression testing.",
|
|
5
|
+
"homepage": "https://regressionbot.com",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/cbestall/vr-api.git",
|
|
9
|
+
"directory": "sdk"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/cbestall/vr-api/issues"
|
|
13
|
+
},
|
|
14
|
+
"main": "dist/index.js",
|
|
15
|
+
"types": "dist/index.d.ts",
|
|
16
|
+
"bin": {
|
|
17
|
+
"regressionbot": "dist/cli.js"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc",
|
|
24
|
+
"dev": "tsc -w",
|
|
25
|
+
"prepublishOnly": "npm run build"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"visual-regression",
|
|
29
|
+
"testing",
|
|
30
|
+
"sdk",
|
|
31
|
+
"cli"
|
|
32
|
+
],
|
|
33
|
+
"author": "RegressionBot (https://regressionbot.com)",
|
|
34
|
+
"license": "ISC",
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^20.0.0",
|
|
37
|
+
"typescript": "^5.0.0"
|
|
38
|
+
}
|
|
39
|
+
}
|