@rettangoli/vt 0.0.1 → 0.0.2-rc2
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 +95 -0
- package/package.json +1 -1
- package/src/cli/accept.js +2 -2
- package/src/cli/generate.js +6 -6
- package/src/cli/report.js +2 -2
- package/src/common.js +39 -39
- package/src/cli.js +0 -36
package/README.md
CHANGED
|
@@ -1,3 +1,98 @@
|
|
|
1
1
|
|
|
2
2
|
# Rettangoli Visual Testing
|
|
3
3
|
|
|
4
|
+
A visual testing framework for UI components using Playwright and screenshot comparison. Perfect for regression testing and ensuring UI consistency across changes.
|
|
5
|
+
|
|
6
|
+
**In production**, this package is typically used through the `rtgl` CLI tool. For development and testing of this package itself, you should call the local CLI directly.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- **Screenshot Generation** - Automatically generate screenshots from HTML specifications
|
|
11
|
+
- **Visual Comparison** - Compare screenshots to detect visual changes
|
|
12
|
+
- **Test Reports** - Generate detailed reports with diff highlights
|
|
13
|
+
- **Playwright Integration** - Uses Playwright for reliable cross-browser testing
|
|
14
|
+
- **Template System** - Liquid templates for flexible HTML generation
|
|
15
|
+
- **Configuration** - YAML-based configuration for easy customization
|
|
16
|
+
|
|
17
|
+
## Development
|
|
18
|
+
|
|
19
|
+
### Prerequisites
|
|
20
|
+
|
|
21
|
+
- Node.js 18+ or Bun
|
|
22
|
+
- Playwright browsers (automatically installed)
|
|
23
|
+
|
|
24
|
+
### Setup
|
|
25
|
+
|
|
26
|
+
1. **Install dependencies**:
|
|
27
|
+
```bash
|
|
28
|
+
bun install
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
2. **Install Playwright browsers** (if not already installed):
|
|
32
|
+
```bash
|
|
33
|
+
npx playwright install
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Project Structure
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
src/
|
|
40
|
+
├── cli/
|
|
41
|
+
│ ├── generate.js # Generate screenshots from specifications
|
|
42
|
+
│ ├── report.js # Generate visual comparison reports
|
|
43
|
+
│ ├── accept.js # Accept screenshot changes as new reference
|
|
44
|
+
│ ├── templates/ # HTML templates for reports
|
|
45
|
+
│ └── static/ # Static assets (CSS, etc.)
|
|
46
|
+
├── common.js # Shared utilities and functions
|
|
47
|
+
└── index.js # Main export (empty - CLI focused)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Core Functionality
|
|
51
|
+
|
|
52
|
+
The visual testing framework provides three main commands:
|
|
53
|
+
|
|
54
|
+
#### 1. Generate (`vt generate`)
|
|
55
|
+
- Reads HTML specifications from `vt/specs/` directory
|
|
56
|
+
- Generates screenshots using Playwright
|
|
57
|
+
- Saves candidate screenshots for comparison
|
|
58
|
+
- Creates a static site for viewing results
|
|
59
|
+
|
|
60
|
+
#### 2. Report (`vt report`)
|
|
61
|
+
- Compares candidate screenshots with reference screenshots
|
|
62
|
+
- Generates visual diff reports highlighting changes
|
|
63
|
+
- Creates an HTML report with before/after comparisons
|
|
64
|
+
- Uses `looks-same` library for pixel-perfect comparison
|
|
65
|
+
|
|
66
|
+
#### 3. Accept (`vt accept`)
|
|
67
|
+
- Accepts candidate screenshots as new reference images
|
|
68
|
+
- Updates the golden/reference screenshot directory
|
|
69
|
+
- Used when visual changes are intentional
|
|
70
|
+
|
|
71
|
+
### Configuration
|
|
72
|
+
|
|
73
|
+
The framework reads configuration from `rettangoli.config.yaml`:
|
|
74
|
+
|
|
75
|
+
```yaml
|
|
76
|
+
vt:
|
|
77
|
+
port: 3001
|
|
78
|
+
screenshotWaitTime: 500
|
|
79
|
+
skipScreenshots: false
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Testing Your Changes
|
|
83
|
+
|
|
84
|
+
**Note**: This package doesn't include example files in its directory. For testing during development, use examples from other packages (like `rettangoli-ui`) and call the CLI directly:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# Call the local CLI directly for development
|
|
88
|
+
node ../rettangoli-cli/cli.js vt generate
|
|
89
|
+
node ../rettangoli-cli/cli.js vt report
|
|
90
|
+
node ../rettangoli-cli/cli.js vt accept
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**Production usage** (when rtgl is installed globally):
|
|
94
|
+
```bash
|
|
95
|
+
rtgl vt generate
|
|
96
|
+
rtgl vt report
|
|
97
|
+
rtgl vt accept
|
|
98
|
+
```
|
package/package.json
CHANGED
package/src/cli/accept.js
CHANGED
|
@@ -34,10 +34,10 @@ async function copyWebpFiles(sourceDir, destDir) {
|
|
|
34
34
|
*/
|
|
35
35
|
async function acceptReference(options = {}) {
|
|
36
36
|
const {
|
|
37
|
-
|
|
37
|
+
vtPath = "./vt",
|
|
38
38
|
} = options;
|
|
39
39
|
|
|
40
|
-
const referenceDir = join(
|
|
40
|
+
const referenceDir = join(vtPath, "reference");
|
|
41
41
|
const siteOutputPath = join(".rettangoli", "vt", "_site");
|
|
42
42
|
const candidateDir = join(siteOutputPath, "candidate");
|
|
43
43
|
|
package/src/cli/generate.js
CHANGED
|
@@ -19,12 +19,12 @@ const libraryStaticPath = new URL('./static', import.meta.url).pathname;
|
|
|
19
19
|
async function main(options) {
|
|
20
20
|
const {
|
|
21
21
|
skipScreenshots = false,
|
|
22
|
-
|
|
22
|
+
vtPath = "./vt",
|
|
23
23
|
screenshotWaitTime = 0,
|
|
24
24
|
port = 3001
|
|
25
25
|
} = options;
|
|
26
26
|
|
|
27
|
-
const specsPath = join(
|
|
27
|
+
const specsPath = join(vtPath, "specs");
|
|
28
28
|
const mainConfigPath = "rettangoli.config.yaml";
|
|
29
29
|
const siteOutputPath = join(".rettangoli", "vt", "_site");
|
|
30
30
|
const candidatePath = join(siteOutputPath, "candidate");
|
|
@@ -46,13 +46,13 @@ async function main(options) {
|
|
|
46
46
|
await cp(libraryStaticPath, siteOutputPath, { recursive: true });
|
|
47
47
|
|
|
48
48
|
// Copy user's static files if they exist
|
|
49
|
-
const userStaticPath = join(
|
|
49
|
+
const userStaticPath = join(vtPath, "static");
|
|
50
50
|
if (existsSync(userStaticPath)) {
|
|
51
51
|
await cp(userStaticPath, siteOutputPath, { recursive: true });
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
// Check for local templates first, fallback to library templates
|
|
55
|
-
const localTemplatesPath = join(
|
|
55
|
+
const localTemplatesPath = join(vtPath, "templates");
|
|
56
56
|
const defaultTemplatePath = existsSync(join(localTemplatesPath, "default.html"))
|
|
57
57
|
? join(localTemplatesPath, "default.html")
|
|
58
58
|
: join(libraryTemplatesPath, "default.html");
|
|
@@ -80,7 +80,7 @@ async function main(options) {
|
|
|
80
80
|
// Start web server from site output path to serve both /public and /candidate
|
|
81
81
|
const server = startWebServer(
|
|
82
82
|
siteOutputPath,
|
|
83
|
-
|
|
83
|
+
vtPath,
|
|
84
84
|
port
|
|
85
85
|
);
|
|
86
86
|
try {
|
|
@@ -94,7 +94,7 @@ async function main(options) {
|
|
|
94
94
|
);
|
|
95
95
|
} finally {
|
|
96
96
|
// Stop server
|
|
97
|
-
server.
|
|
97
|
+
server.close();
|
|
98
98
|
console.log("Server stopped");
|
|
99
99
|
}
|
|
100
100
|
}
|
package/src/cli/report.js
CHANGED
|
@@ -79,11 +79,11 @@ async function generateReport({ results, templatePath, outputPath }) {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
async function main(options = {}) {
|
|
82
|
-
const {
|
|
82
|
+
const { vtPath = "./vt" } = options;
|
|
83
83
|
|
|
84
84
|
const siteOutputPath = path.join(".rettangoli", "vt", "_site");
|
|
85
85
|
const candidateDir = path.join(siteOutputPath, "candidate");
|
|
86
|
-
const originalReferenceDir = path.join(
|
|
86
|
+
const originalReferenceDir = path.join(vtPath, "reference");
|
|
87
87
|
const siteReferenceDir = path.join(siteOutputPath, "reference");
|
|
88
88
|
const templatePath = path.join(libraryTemplatesPath, "report.html");
|
|
89
89
|
const outputPath = path.join(siteOutputPath, "report.html");
|
package/src/common.js
CHANGED
|
@@ -5,8 +5,10 @@ import {
|
|
|
5
5
|
writeFileSync,
|
|
6
6
|
mkdirSync,
|
|
7
7
|
existsSync,
|
|
8
|
+
unlinkSync,
|
|
8
9
|
} from "fs";
|
|
9
10
|
import { join, dirname, resolve, extname } from "path";
|
|
11
|
+
import http from "http";
|
|
10
12
|
import { load as loadYaml } from "js-yaml";
|
|
11
13
|
import { Liquid } from "liquidjs";
|
|
12
14
|
import { chromium } from "playwright";
|
|
@@ -181,49 +183,47 @@ async function generateHtml(specsDir, templatePath, outputDir) {
|
|
|
181
183
|
* Start a web server to serve static files
|
|
182
184
|
*/
|
|
183
185
|
function startWebServer(artifactsDir, staticDir, port) {
|
|
184
|
-
const server =
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const url = new URL(req.url);
|
|
188
|
-
let path = url.pathname;
|
|
189
|
-
|
|
190
|
-
// Default to index.html for root path
|
|
191
|
-
if (path === "/") {
|
|
192
|
-
path = "/index.html";
|
|
193
|
-
}
|
|
186
|
+
const server = http.createServer((req, res) => {
|
|
187
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
188
|
+
let path = url.pathname;
|
|
194
189
|
|
|
195
|
-
|
|
196
|
-
|
|
190
|
+
// Default to index.html for root path
|
|
191
|
+
if (path === "/") {
|
|
192
|
+
path = "/index.html";
|
|
193
|
+
}
|
|
197
194
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
195
|
+
// Remove leading slash for file path
|
|
196
|
+
const filePath = path.startsWith("/") ? path.slice(1) : path;
|
|
197
|
+
|
|
198
|
+
// Try to serve from artifacts directory first
|
|
199
|
+
const artifactsPath = join(artifactsDir, filePath);
|
|
200
|
+
if (existsSync(artifactsPath) && statSync(artifactsPath).isFile()) {
|
|
201
|
+
const fileContent = readFileSync(artifactsPath);
|
|
202
|
+
const contentType = getContentType(artifactsPath);
|
|
203
|
+
res.writeHead(200, { "Content-Type": contentType });
|
|
204
|
+
res.end(fileContent);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
207
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
208
|
+
// Then try to serve from static directory
|
|
209
|
+
const staticPath = join(staticDir, filePath);
|
|
210
|
+
if (existsSync(staticPath) && statSync(staticPath).isFile()) {
|
|
211
|
+
const fileContent = readFileSync(staticPath);
|
|
212
|
+
const contentType = getContentType(staticPath);
|
|
213
|
+
res.writeHead(200, { "Content-Type": contentType });
|
|
214
|
+
res.end(fileContent);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
217
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
headers: { "Content-Type": "text/plain" },
|
|
222
|
-
});
|
|
223
|
-
},
|
|
218
|
+
// If not found in either directory, return 404
|
|
219
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
220
|
+
res.end("Not Found");
|
|
224
221
|
});
|
|
225
222
|
|
|
226
|
-
|
|
223
|
+
server.listen(port, () => {
|
|
224
|
+
console.log(`Server started at http://localhost:${port}`);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
227
|
return server;
|
|
228
228
|
}
|
|
229
229
|
|
|
@@ -311,7 +311,7 @@ async function takeScreenshots(
|
|
|
311
311
|
|
|
312
312
|
// Remove temporary PNG file
|
|
313
313
|
if (existsSync(tempPngPath)) {
|
|
314
|
-
|
|
314
|
+
unlinkSync(tempPngPath);
|
|
315
315
|
}
|
|
316
316
|
|
|
317
317
|
// example instructions:
|
|
@@ -350,7 +350,7 @@ async function takeScreenshots(
|
|
|
350
350
|
|
|
351
351
|
// Remove temporary PNG file
|
|
352
352
|
if (existsSync(tempAdditionalPngPath)) {
|
|
353
|
-
|
|
353
|
+
unlinkSync(tempAdditionalPngPath);
|
|
354
354
|
}
|
|
355
355
|
|
|
356
356
|
console.log(
|
package/src/cli.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
2
|
-
import generate from './generate.js';
|
|
3
|
-
import accept from './accept.js';
|
|
4
|
-
import report from './report.js';
|
|
5
|
-
|
|
6
|
-
const program = new Command();
|
|
7
|
-
|
|
8
|
-
program
|
|
9
|
-
.version('0.0.1')
|
|
10
|
-
.description('Rettangoli visualization CLI');
|
|
11
|
-
|
|
12
|
-
program
|
|
13
|
-
.command('generate')
|
|
14
|
-
.description('Generate visualizations')
|
|
15
|
-
.option('--skip-screenshots', 'Skip screenshot generation')
|
|
16
|
-
.option('--screenshot-wait-time <time>', 'Wait time between screenshots', '0')
|
|
17
|
-
.option('--viz-path <path>', 'Path to the viz directory', './viz')
|
|
18
|
-
.action((options) => {
|
|
19
|
-
generate(options);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
program
|
|
23
|
-
.command('report')
|
|
24
|
-
.description('Create reports')
|
|
25
|
-
.action(() => {
|
|
26
|
-
report();
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
program
|
|
30
|
-
.command('accept')
|
|
31
|
-
.description('Accept changes')
|
|
32
|
-
.action(() => {
|
|
33
|
-
accept();
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
program.parse(process.argv);
|