350 lines
9.3 KiB
Plaintext
350 lines
9.3 KiB
Plaintext
/**
|
|
* This traverses all json files located on the examples folder, then iterates
|
|
* over each file and opens a puppeteer page to a screenshot of all frames
|
|
* combined in a single page.
|
|
*/
|
|
|
|
const puppeteer = require('puppeteer');
|
|
const express = require('express');
|
|
const fs = require('fs');
|
|
const { promises: { readFile } } = require('fs');
|
|
const commandLineArgs = require('command-line-args');
|
|
const PNG = require('pngjs').PNG;
|
|
const pixelmatch = require('pixelmatch');
|
|
|
|
const examplesDirectory = '/test/animations/';
|
|
const createDirectory = 'screenshots/create';
|
|
const compareDirectory = 'screenshots/compare';
|
|
|
|
function createDirectoryPath(path) {
|
|
const directories = path.split('/');
|
|
directories.reduce((acc, current) => {
|
|
let dir = acc + '/' + current
|
|
if (!fs.existsSync(dir)) {
|
|
fs.mkdirSync(dir);
|
|
}
|
|
return dir
|
|
}, '.')
|
|
}
|
|
|
|
const animations = [
|
|
{
|
|
fileName: 'pigeon.json',
|
|
renderer: 'svg',
|
|
},
|
|
{
|
|
fileName: 'banner.json',
|
|
renderer: 'svg',
|
|
},
|
|
{
|
|
fileName: 'adrock.json',
|
|
renderer: 'canvas',
|
|
},
|
|
{
|
|
fileName: 'bm_ronda.json',
|
|
renderer: 'svg',
|
|
},
|
|
{
|
|
fileName: 'bodymovin.json',
|
|
renderer: 'svg',
|
|
},
|
|
{
|
|
fileName: 'bodymovin.json',
|
|
renderer: 'canvas',
|
|
},
|
|
{
|
|
fileName: 'dalek.json',
|
|
renderer: 'svg',
|
|
},
|
|
{
|
|
fileName: 'navidad.json',
|
|
renderer: 'svg',
|
|
},
|
|
{
|
|
fileName: 'monster.json',
|
|
renderer: 'svg',
|
|
},
|
|
{
|
|
fileName: 'bacon.json',
|
|
renderer: 'svg',
|
|
},
|
|
{
|
|
fileName: 'lights.json',
|
|
renderer: 'svg',
|
|
},
|
|
{
|
|
fileName: 'ripple.json',
|
|
renderer: 'svg',
|
|
},
|
|
{
|
|
fileName: 'starfish.json',
|
|
renderer: 'svg',
|
|
},
|
|
{
|
|
directory: 'footage',
|
|
fileName: 'data.json',
|
|
renderer: 'svg',
|
|
},
|
|
]
|
|
|
|
const getSettings = async () => {
|
|
const defaultValues = {
|
|
step: 'create',
|
|
}
|
|
const opts = [
|
|
{
|
|
name: 'step',
|
|
alias: 's',
|
|
type: (val) => {
|
|
|
|
return val === 'compare' ? 'compare' : 'create';
|
|
},
|
|
description: 'Whether it is the create or the compare step',
|
|
}
|
|
];
|
|
const settings = {
|
|
...defaultValues,
|
|
...commandLineArgs(opts),
|
|
};
|
|
return settings;
|
|
};
|
|
|
|
const wait = (time) => new Promise((resolve) => setTimeout(resolve, time));
|
|
|
|
const filesData = [
|
|
{
|
|
path: '/test/index.html',
|
|
filePath: './test/index.html',
|
|
type: 'html',
|
|
},
|
|
{
|
|
path: '/lottie.min.js',
|
|
filePath: './build/player/lottie.min.js',
|
|
type: 'js',
|
|
},
|
|
];
|
|
|
|
const getEncoding = (() => {
|
|
const encodingMap = {
|
|
js: 'utf8',
|
|
json: 'utf8',
|
|
html: 'utf8',
|
|
};
|
|
return (fileType) => encodingMap[fileType];
|
|
})();
|
|
|
|
const getContentTypeHeader = (() => {
|
|
const contentTypeMap = {
|
|
js: { 'Content-Type': 'application/javascript' },
|
|
json: { 'Content-Type': 'application/json' },
|
|
html: { 'Content-Type': 'text/html; charset=utf-8' },
|
|
wasm: { 'Content-Type': 'application/wasm' },
|
|
};
|
|
return (fileType) => contentTypeMap[fileType];
|
|
})();
|
|
|
|
const startServer = async () => {
|
|
const app = express();
|
|
await Promise.all(filesData.map(async (file) => {
|
|
const fileData = await readFile(file.filePath, getEncoding(file.type));
|
|
app.get(file.path, async (req, res) => {
|
|
res.writeHead(200, getContentTypeHeader(file.type));
|
|
// TODO: comment line. Only for live updates.
|
|
const fileData = await readFile(file.filePath, getEncoding(file.type));
|
|
res.end(fileData);
|
|
});
|
|
return file;
|
|
}));
|
|
|
|
app.get('/*', async (req, res) => {
|
|
try {
|
|
if (req.originalUrl.indexOf('.json') !== -1) {
|
|
const file = await readFile(`.${req.originalUrl}`, 'utf8');
|
|
res.send(file);
|
|
} else {
|
|
const data = await readFile(`.${req.originalUrl}`);
|
|
res.writeHead(200, { 'Content-Type': 'image/jpeg' });
|
|
res.end(data);
|
|
}
|
|
} catch (err) {
|
|
res.send('');
|
|
}
|
|
});
|
|
app.listen('9999');
|
|
};
|
|
|
|
const getBrowser = async () => puppeteer.launch({ defaultViewport: null });
|
|
|
|
const startPage = async (browser, path, renderer) => {
|
|
const targetURL = `http://localhost:9999/test/index.html\
|
|
?path=${encodeURIComponent(path)}&renderer=${renderer}`;
|
|
const page = await browser.newPage();
|
|
page.on('console', (msg) => console.log('PAGE LOG:', msg.text())); // eslint-disable-line no-console
|
|
await page.setViewport({
|
|
width: 1024,
|
|
height: 768,
|
|
});
|
|
await page.goto(targetURL);
|
|
return page;
|
|
};
|
|
|
|
const createBridgeHelper = async (page) => {
|
|
let resolveScoped;
|
|
let animationLoadedPromise;
|
|
const messageHandler = (event) => {
|
|
resolveScoped(event);
|
|
};
|
|
const onAnimationLoaded = () => {
|
|
if (animationLoadedPromise) {
|
|
animationLoadedPromise()
|
|
}
|
|
}
|
|
await page.exposeFunction('onAnimationLoaded', onAnimationLoaded);
|
|
await page.exposeFunction('onMessageReceivedEvent', messageHandler);
|
|
const waitForMessage = () => new Promise((resolve) => {
|
|
resolveScoped = resolve;
|
|
});
|
|
const waitForAnimationLoaded = () => new Promise((resolve) => {
|
|
animationLoadedPromise = resolve;
|
|
});
|
|
const continueExecution = async () => {
|
|
page.evaluate(() => {
|
|
window.continueExecution();
|
|
});
|
|
};
|
|
return {
|
|
waitForAnimationLoaded,
|
|
waitForMessage,
|
|
continueExecution,
|
|
};
|
|
};
|
|
|
|
const compareFiles = (folderName, fileName) => {
|
|
const createPath = `${createDirectory}/${folderName}/${fileName}`;
|
|
const comparePath = `${compareDirectory}/${folderName}/${fileName}`;
|
|
const img1 = PNG.sync.read(fs.readFileSync(createPath));
|
|
const img2 = PNG.sync.read(fs.readFileSync(comparePath));
|
|
const {width, height} = img1;
|
|
const diff = new PNG({width, height});
|
|
|
|
const result = pixelmatch(img1.data, img2.data, diff.data, width, height, {threshold: 0.1});
|
|
// Using 50 as threshold because it should be an acceptable difference
|
|
// that doesn't raise false positives
|
|
if (result > 200) {
|
|
console.log('RESULT NOT ZERO: ', result);
|
|
throw new Error(`Animation failed: ${folderName} at frame: ${fileName}`)
|
|
}
|
|
}
|
|
|
|
const createIndividualAssets = async (page, folderName, settings) => {
|
|
const filePath = `${settings.step === 'create' ? createDirectory : compareDirectory}/${folderName}`;
|
|
createDirectoryPath(filePath);
|
|
let isLastFrame = false;
|
|
const bridgeHelper = await (createBridgeHelper(page));
|
|
page.evaluate(() => {
|
|
window.startProcess();
|
|
});
|
|
await bridgeHelper.waitForAnimationLoaded();
|
|
while (!isLastFrame) {
|
|
// Disabling rule because execution can't be parallelized
|
|
/* eslint-disable no-await-in-loop */
|
|
await wait(1);
|
|
page.evaluate(() => {
|
|
window.continueExecution();
|
|
});
|
|
const message = await bridgeHelper.waitForMessage();
|
|
const fileNumber = message.currentFrame.toString().padStart(5, '0');
|
|
const fileName = `image_${fileNumber}.png`;
|
|
const localDestinationPath = `${filePath}/${fileName}`;
|
|
await page.screenshot({
|
|
path: localDestinationPath,
|
|
fullPage: false,
|
|
});
|
|
if (settings.step === 'compare') {
|
|
try {
|
|
compareFiles(folderName, fileName);
|
|
} catch (err) {
|
|
console.log('FAILED AT FRAME: ', message.currentFrame);
|
|
throw err;
|
|
}
|
|
}
|
|
isLastFrame = message.isLast;
|
|
}
|
|
};
|
|
|
|
const getDirFiles = async (directory) => (
|
|
new Promise((resolve, reject) => {
|
|
fs.readdir(directory, (err, files) => {
|
|
if (err) {
|
|
reject(err);
|
|
} else {
|
|
resolve(files);
|
|
}
|
|
});
|
|
})
|
|
);
|
|
|
|
async function processPage(browser, settings, directory, animation) {
|
|
let fullDirectory = `${directory}`;
|
|
if (animation.directory) {
|
|
fullDirectory += `${animation.directory}/`;
|
|
}
|
|
const fileName = animation.fileName;
|
|
const page = await startPage(browser, fullDirectory + fileName, animation.renderer);
|
|
const fileNameWithoutExtension = fileName.replace(/\.[^/.]+$/, '');
|
|
let fullName = `${fileNameWithoutExtension}_${animation.renderer}`
|
|
if (animation.directory) {
|
|
fullName = `${animation.directory}_` + fullName;
|
|
}
|
|
await createIndividualAssets(page, fullName, settings);
|
|
}
|
|
|
|
const iteratePages = async (browser, settings) => {
|
|
const failedAnimations = [];
|
|
for (let i = 0; i < animations.length; i += 1) {
|
|
const animation = animations[i];
|
|
let fileName = `${animation.renderer}_${animation.fileName}`;
|
|
if (animation.directory) {
|
|
fileName = `${animation.directory}_` + fileName;
|
|
}
|
|
try {
|
|
// eslint-disable-next-line no-await-in-loop
|
|
await processPage(browser, settings, examplesDirectory, animation);
|
|
if (settings.step === 'create') {
|
|
console.log(`Creating animation: ${fileName}`);
|
|
}
|
|
if (settings.step === 'compare') {
|
|
console.log(`Animation passed: ${fileName}`);
|
|
}
|
|
} catch (error) {
|
|
if (settings.step === 'compare') {
|
|
failedAnimations.push({
|
|
fileName: fileName
|
|
})
|
|
}
|
|
}
|
|
}
|
|
if (failedAnimations.length) {
|
|
failedAnimations.forEach(animation => {
|
|
console.log(`Animation failed: ${animation.fileName}`);
|
|
})
|
|
throw new Error('Animations failed');
|
|
}
|
|
};
|
|
|
|
|
|
const takeImageStrip = async () => {
|
|
try {
|
|
await startServer();
|
|
const settings = await getSettings();
|
|
await wait(1);
|
|
const browser = await getBrowser();
|
|
await iteratePages(browser, settings);
|
|
process.exit(0);
|
|
} catch (error) {
|
|
console.log(error); // eslint-disable-line no-console
|
|
process.exit(1);
|
|
}
|
|
};
|
|
|
|
takeImageStrip(); |