@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rettangoli/vt",
3
- "version": "0.0.1",
3
+ "version": "0.0.2-rc2",
4
4
  "description": "Rettangoli Visual Testing",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
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
- vizPath = "./viz",
37
+ vtPath = "./vt",
38
38
  } = options;
39
39
 
40
- const referenceDir = join(vizPath, "reference");
40
+ const referenceDir = join(vtPath, "reference");
41
41
  const siteOutputPath = join(".rettangoli", "vt", "_site");
42
42
  const candidateDir = join(siteOutputPath, "candidate");
43
43
 
@@ -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
- vizPath = "./vt",
22
+ vtPath = "./vt",
23
23
  screenshotWaitTime = 0,
24
24
  port = 3001
25
25
  } = options;
26
26
 
27
- const specsPath = join(vizPath, "specs");
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(vizPath, "static");
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(vizPath, "templates");
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
- vizPath,
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.stop();
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 { vizPath = "./viz" } = options;
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(vizPath, "reference");
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 = Bun.serve({
185
- port: port,
186
- fetch(req) {
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
- // Remove leading slash for file path
196
- const filePath = path.startsWith("/") ? path.slice(1) : path;
190
+ // Default to index.html for root path
191
+ if (path === "/") {
192
+ path = "/index.html";
193
+ }
197
194
 
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
- return new Response(fileContent, {
204
- headers: { "Content-Type": contentType },
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
- // 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
- return new Response(fileContent, {
214
- headers: { "Content-Type": contentType },
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
- // If not found in either directory, return 404
219
- return new Response("Not Found", {
220
- status: 404,
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
- console.log(`Server started at http://localhost:${port}`);
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
- await import('fs').then(fs => fs.promises.unlink(tempPngPath));
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
- await import('fs').then(fs => fs.promises.unlink(tempAdditionalPngPath));
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);