Skip to content

Commit 2c408ce

Browse files
authored
refactor: improve regression testing (#1898)
1 parent 16c6977 commit 2c408ce

File tree

6 files changed

+118
-293
lines changed

6 files changed

+118
-293
lines changed

.github/workflows/main.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ jobs:
3636
node-version: ${{ env.NODE }}
3737
cache: yarn
3838
- run: yarn install
39+
- run: yarn playwright install --with-deps chromium
3940
- run: yarn test-regression
4041
test:
4142
name: ${{ matrix.os }} Node.js ${{ matrix.node-version }}
@@ -58,5 +59,6 @@ jobs:
5859
node-version: ${{ matrix.node-version }}
5960
cache: yarn
6061
- run: yarn install
62+
- run: yarn playwright install --with-deps chromium
6163
- run: yarn test
6264
- run: yarn test-browser

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,9 @@
9696
"eslint": "^8.55.0",
9797
"jest": "^29.5.5",
9898
"node-fetch": "^2.7.0",
99-
"pixelmatch": "^5.2.1",
100-
"playwright": "^1.14.1",
101-
"pngjs": "^6.0.0",
99+
"pixelmatch": "^5.3.0",
100+
"playwright": "^1.40.1",
101+
"pngjs": "^7.0.0",
102102
"prettier": "^3.1.1",
103103
"rollup": "^2.79.1",
104104
"rollup-plugin-terser": "^7.0.2",

test/browser.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
const fs = require('fs');
2-
const http = require('http');
31
const assert = require('assert');
2+
const fs = require('node:fs/promises');
3+
const http = require('http');
44
const { chromium } = require('playwright');
55

66
const fixture = `<svg xmlns="http://www.w3.org/2000/svg">
@@ -33,14 +33,14 @@ globalThis.result = result.data;
3333
</script>
3434
`;
3535

36-
const server = http.createServer((req, res) => {
36+
const server = http.createServer(async (req, res) => {
3737
if (req.url === '/') {
3838
res.setHeader('Content-Type', 'text/html');
3939
res.end(content);
4040
}
4141
if (req.url === '/svgo.browser.js') {
4242
res.setHeader('Content-Type', 'application/javascript');
43-
res.end(fs.readFileSync('./dist/svgo.browser.js'));
43+
res.end(await fs.readFile('./dist/svgo.browser.js'));
4444
}
4545
res.end();
4646
});

test/regression-extract.js

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,33 @@
22

33
const fs = require('fs');
44
const path = require('path');
5+
const stream = require('stream');
56
const util = require('util');
67
const zlib = require('zlib');
7-
const stream = require('stream');
88
const { default: fetch } = require('node-fetch');
99
const tarStream = require('tar-stream');
1010

1111
const pipeline = util.promisify(stream.pipeline);
1212

13+
const exclude = [
14+
// animated
15+
'svg/filters-light-04-f.svg',
16+
'svg/filters-composite-05-f.svg',
17+
// messed gradients
18+
'svg/pservers-grad-18-b.svg',
19+
// removing wrapping <g> breaks :first-child pseudo-class
20+
'svg/styling-pres-04-f.svg',
21+
// rect is converted to path which matches wrong styles
22+
'svg/styling-css-08-f.svg',
23+
// complex selectors are messed because of converting shapes to paths
24+
'svg/struct-use-10-f.svg',
25+
'svg/struct-use-11-f.svg',
26+
'svg/styling-css-01-b.svg',
27+
'svg/styling-css-04-f.svg',
28+
// strange artifact breaks inconsistently breaks regression tests
29+
'svg/filters-conv-05-f.svg',
30+
];
31+
1332
/**
1433
* @param {string} url
1534
* @param {string} baseDir
@@ -18,15 +37,21 @@ const pipeline = util.promisify(stream.pipeline);
1837
const extractTarGz = async (url, baseDir, include) => {
1938
const extract = tarStream.extract();
2039
extract.on('entry', async (header, stream, next) => {
40+
const name = header.name;
41+
2142
try {
22-
if (include == null || include.test(header.name)) {
23-
if (header.name.endsWith('.svg')) {
24-
const file = path.join(baseDir, header.name);
43+
if (include == null || include.test(name)) {
44+
if (
45+
name.endsWith('.svg') &&
46+
!exclude.includes(name) &&
47+
!name.startsWith('svg/animate-')
48+
) {
49+
const file = path.join(baseDir, name);
2550
await fs.promises.mkdir(path.dirname(file), { recursive: true });
2651
await pipeline(stream, fs.createWriteStream(file));
27-
} else if (header.name.endsWith('.svgz')) {
52+
} else if (name.endsWith('.svgz')) {
2853
// .svgz -> .svg
29-
const file = path.join(baseDir, header.name.slice(0, -1));
54+
const file = path.join(baseDir, name.slice(0, -1));
3055
await fs.promises.mkdir(path.dirname(file), { recursive: true });
3156
await pipeline(
3257
stream,
@@ -48,7 +73,7 @@ const extractTarGz = async (url, baseDir, include) => {
4873

4974
(async () => {
5075
try {
51-
console.info('Download W3C SVG 1.1 Test Suite and extract svg files');
76+
console.info('Download W3C SVG 1.1 Test Suite and extract SVG files');
5277
await extractTarGz(
5378
'https://www.w3.org/Graphics/SVG/Test/20110816/archives/W3C_SVG_11_TestSuite.tar.gz',
5479
path.join(__dirname, 'regression-fixtures', 'w3c-svg-11-test-suite'),

test/regression.js

Lines changed: 41 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,49 @@
11
'use strict';
22

3-
const fs = require('fs');
4-
const path = require('path');
3+
/**
4+
* @typedef {import('playwright').Page} Page
5+
* @typedef {import('playwright').PageScreenshotOptions} PageScreenshotOptions
6+
*/
7+
8+
const fs = require('node:fs/promises');
59
const http = require('http');
610
const os = require('os');
11+
const path = require('path');
12+
const pixelmatch = require('pixelmatch');
713
const { chromium } = require('playwright');
814
const { PNG } = require('pngjs');
9-
const pixelmatch = require('pixelmatch');
1015
const { optimize } = require('../lib/svgo.js');
1116

12-
const runTests = async ({ list }) => {
13-
let skipped = 0;
17+
const width = 960;
18+
const height = 720;
19+
20+
/** @type {PageScreenshotOptions} */
21+
const screenshotOptions = {
22+
omitBackground: true,
23+
clip: { x: 0, y: 0, width, height },
24+
animations: 'disabled',
25+
};
26+
27+
/**
28+
* @param {string[]} list
29+
* @returns {Promise<boolean>}
30+
*/
31+
const runTests = async (list) => {
1432
let mismatched = 0;
1533
let passed = 0;
16-
list.reverse();
17-
console.info('Start browser...');
34+
console.info('Start browser…');
35+
/**
36+
* @param {Page} page
37+
* @param {string} name
38+
*/
1839
const processFile = async (page, name) => {
19-
if (
20-
// animated
21-
name.startsWith('w3c-svg-11-test-suite/svg/animate-') ||
22-
name === 'w3c-svg-11-test-suite/svg/filters-light-04-f.svg' ||
23-
name === 'w3c-svg-11-test-suite/svg/filters-composite-05-f.svg' ||
24-
// messed gradients
25-
name === 'w3c-svg-11-test-suite/svg/pservers-grad-18-b.svg' ||
26-
// removing wrapping <g> breaks :first-child pseudo-class
27-
name === 'w3c-svg-11-test-suite/svg/styling-pres-04-f.svg' ||
28-
// rect is converted to path which matches wrong styles
29-
name === 'w3c-svg-11-test-suite/svg/styling-css-08-f.svg' ||
30-
// complex selectors are messed because of converting shapes to paths
31-
name === 'w3c-svg-11-test-suite/svg/struct-use-10-f.svg' ||
32-
name === 'w3c-svg-11-test-suite/svg/struct-use-11-f.svg' ||
33-
name === 'w3c-svg-11-test-suite/svg/styling-css-01-b.svg' ||
34-
name === 'w3c-svg-11-test-suite/svg/styling-css-04-f.svg' ||
35-
// strange artifact breaks inconsistently breaks regression tests
36-
name === 'w3c-svg-11-test-suite/svg/filters-conv-05-f.svg'
37-
) {
38-
console.info(`${name} is skipped`);
39-
skipped += 1;
40-
return;
41-
}
4240
await page.goto(`http://localhost:5000/original/${name}`);
43-
await page.setViewportSize({ width, height });
44-
const originalBuffer = await page.screenshot({
45-
omitBackground: true,
46-
clip: { x: 0, y: 0, width, height },
47-
});
41+
const originalBuffer = await page.screenshot(screenshotOptions);
4842
await page.goto(`http://localhost:5000/optimized/${name}`);
49-
const optimizedBuffer = await page.screenshot({
50-
omitBackground: true,
51-
clip: { x: 0, y: 0, width, height },
52-
});
43+
const optimizedBufferPromise = page.screenshot(screenshotOptions);
44+
5345
const originalPng = PNG.sync.read(originalBuffer);
54-
const optimizedPng = PNG.sync.read(optimizedBuffer);
46+
const optimizedPng = PNG.sync.read(await optimizedBufferPromise);
5547
const diff = new PNG({ width, height });
5648
const matched = pixelmatch(
5749
originalPng.data,
@@ -63,24 +55,25 @@ const runTests = async ({ list }) => {
6355
// ignore small aliasing issues
6456
if (matched <= 4) {
6557
console.info(`${name} is passed`);
66-
passed += 1;
58+
passed++;
6759
} else {
68-
mismatched += 1;
60+
mismatched++;
6961
console.error(`${name} is mismatched`);
7062
if (process.env.NO_DIFF == null) {
7163
const file = path.join(
7264
__dirname,
7365
'regression-diffs',
7466
`${name}.diff.png`,
7567
);
76-
await fs.promises.mkdir(path.dirname(file), { recursive: true });
77-
await fs.promises.writeFile(file, PNG.sync.write(diff));
68+
await fs.mkdir(path.dirname(file), { recursive: true });
69+
await fs.writeFile(file, PNG.sync.write(diff));
7870
}
7971
}
8072
};
8173
const worker = async () => {
8274
let item;
8375
const page = await context.newPage();
76+
await page.setViewportSize({ width, height });
8477
while ((item = list.pop())) {
8578
await processFile(page, item);
8679
}
@@ -93,44 +86,21 @@ const runTests = async ({ list }) => {
9386
Array.from(new Array(os.cpus().length * 2), () => worker()),
9487
);
9588
await browser.close();
96-
console.info(`Skipped: ${skipped}`);
9789
console.info(`Mismatched: ${mismatched}`);
9890
console.info(`Passed: ${passed}`);
9991
return mismatched === 0;
10092
};
10193

102-
const readdirRecursive = async (absolute, relative = '') => {
103-
let result = [];
104-
const list = await fs.promises.readdir(absolute, { withFileTypes: true });
105-
for (const item of list) {
106-
const itemAbsolute = path.join(absolute, item.name);
107-
const itemRelative = path.join(relative, item.name);
108-
if (item.isDirectory()) {
109-
const itemList = await readdirRecursive(itemAbsolute, itemRelative);
110-
result = [...result, ...itemList];
111-
} else if (item.name.endsWith('.svg')) {
112-
result = [...result, itemRelative];
113-
}
114-
}
115-
return result;
116-
};
117-
118-
const width = 960;
119-
const height = 720;
12094
(async () => {
12195
try {
12296
const start = process.hrtime.bigint();
12397
const fixturesDir = path.join(__dirname, 'regression-fixtures');
124-
const list = await readdirRecursive(fixturesDir);
125-
// setup server
98+
const filesPromise = fs.readdir(fixturesDir, { recursive: true });
12699
const server = http.createServer(async (req, res) => {
127100
const name = req.url.slice(req.url.indexOf('/', 1));
128101
let file;
129102
try {
130-
file = await fs.promises.readFile(
131-
path.join(fixturesDir, name),
132-
'utf-8',
133-
);
103+
file = await fs.readFile(path.join(fixturesDir, name), 'utf-8');
134104
} catch (error) {
135105
res.statusCode = 404;
136106
res.end();
@@ -144,14 +114,8 @@ const height = 720;
144114
}
145115
if (req.url.startsWith('/optimized/')) {
146116
const optimized = optimize(file, {
147-
path: name,
148117
floatPrecision: 4,
149118
});
150-
if (optimized.error) {
151-
throw new Error(`Failed to optimize ${name}`, {
152-
cause: optimized.error,
153-
});
154-
}
155119
res.setHeader('Content-Type', 'image/svg+xml');
156120
res.end(optimized.data);
157121
return;
@@ -161,9 +125,9 @@ const height = 720;
161125
await new Promise((resolve) => {
162126
server.listen(5000, resolve);
163127
});
164-
const passed = await runTests({ list });
128+
const list = (await filesPromise).filter((name) => name.endsWith('.svg'));
129+
const passed = await runTests(list);
165130
server.close();
166-
// compute time
167131
const end = process.hrtime.bigint();
168132
const diff = (end - start) / BigInt(1e6);
169133
if (passed) {

0 commit comments

Comments
 (0)