chore: expand rendering test capabilities (#1011)

* test: implement rendering tests with listbox

* test: keep headless

* chore: revert changing puppeteer version

* fix: clean up
This commit is contained in:
Johan Lahti
2022-11-25 12:57:47 +01:00
committed by GitHub
parent 7d95042a1d
commit e2208c6081
13 changed files with 537 additions and 11 deletions

1
.gitignore vendored
View File

@@ -14,6 +14,7 @@ dist/
temp/
test/**/__artifacts__/regression
test/**/__artifacts__/diff
test/**/__artifacts__/temp
apis/*/core/**/*.js
apis/*/core/**/*.js.map
apis/locale/all.json

View File

@@ -21,6 +21,7 @@
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:mashup": "aw puppet -c aw.config.js --testExt '*.int.js' --glob 'test/mashup/**/*.int.js'",
"test:rendering": "node ./scripts/run-rendering-tests.js",
"test:integration": "aw puppet -c aw.config.js --testExt '*.int.js' --glob 'test/integration/**/*.int.js'",
"test:component": "aw puppet -c aw.config.js --testExt '*.comp.js' --glob 'test/component/**/*.comp.js'",
"prepare": "husky install"
@@ -91,6 +92,7 @@
"picasso.js": "1.9.6",
"prettier": "2.7.1",
"pretty-quick": "3.1.3",
"puppeteer": "19.2.2",
"qix-faker": "0.3.0",
"rollup": "3.2.5",
"rollup-plugin-babel": "4.4.0",

View File

@@ -0,0 +1,4 @@
const { execSync } = require('child_process');
const cmd = 'mocha test/rendering/listbox/listbox.spec.js --bail false --timeout 30000';
execSync(cmd, { stdio: 'inherit' });

28
test/rendering/README.md Normal file
View File

@@ -0,0 +1,28 @@
# Rendering tests
These tests are aimed for more generic rendering tests than the **mashup** and **integration** counterparts. These tests do not rely on a Supernova or custom backend solution for producing screenshots. If you want to test a Supernova based viz, you should instead create either a [mashup](../mashup) or an [integration](../integration) test.
## Tests covered (so far)
- [Listboxes](./listbox)
## Create and run tests
1. Create a folder, e.g. `./test/rendering/listbox`
2. Inside that folder, create:
- listbox.spec.js - a test file which orchestrates interactions and when to take a screenshot
- listbox.html - constitutes the "site" that the test file interacts with and creates screenshots from
- listbox.js - (optional) if you don't want to add all JS code inside of the html file
3. Run tests from the nebula.js root with `yarn test:rendering`
4. The first time, the test will fail and create an image inside of the `__artifacts__/temp` folder. Drag this file to the `__artifacts__/baseline` folder and re-run the test to verify that it passes. Then `git push` the baseline.
Check the [listbox files](./listbox) for details on how to write the code in these files.
## Output folders
Screenshots are stored in the following folders:
- `temp` - new screenshots are stored here
- `baseline` - new screenshots are compared to the baseline version of the image, in order to check validity only update these if a change is expected
- `regression` - new screenshots are saved here when deviating too much (i.e. when they are invalid)
- `diff` - the difference between the baseline and the regression image

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

@@ -0,0 +1,239 @@
window.getFuncs = function getFuncs() {
return {
getMockData: () => [
{
qMatrix: [
[
{
qText: 'A',
qNum: 'NaN',
qElemNumber: 0,
qState: 'O',
},
],
[
{
qText: 'B',
qNum: 'NaN',
qElemNumber: 1,
qState: 'O',
},
],
[
{
qText: 'C',
qNum: 'NaN',
qElemNumber: 2,
qState: 'O',
},
],
[
{
qText: 'D',
qNum: 'NaN',
qElemNumber: 3,
qState: 'O',
},
],
[
{
qText: 'E',
qNum: 'NaN',
qElemNumber: 4,
qState: 'O',
},
],
[
{
qText: 'F',
qNum: 'NaN',
qElemNumber: 5,
qState: 'O',
},
],
[
{
qText: 'G',
qNum: 'NaN',
qElemNumber: 6,
qState: 'O',
},
],
[
{
qText: 'H',
qNum: 'NaN',
qElemNumber: 7,
qState: 'O',
},
],
[
{
qText: 'I',
qNum: 'NaN',
qElemNumber: 8,
qState: 'O',
},
],
[
{
qText: 'J',
qNum: 'NaN',
qElemNumber: 9,
qState: 'O',
},
],
[
{
qText: 'K',
qNum: 'NaN',
qElemNumber: 10,
qState: 'O',
},
],
[
{
qText: 'L',
qNum: 'NaN',
qElemNumber: 11,
qState: 'O',
},
],
],
qTails: [],
qArea: {
qLeft: 0,
qTop: 0,
qWidth: 1,
qHeight: 12,
},
},
{
qMatrix: [
[
{
qText: 'A',
qNum: 'NaN',
qElemNumber: 0,
qState: 'O',
},
],
[
{
qText: 'B',
qNum: 'NaN',
qElemNumber: 1,
qState: 'O',
},
],
[
{
qText: 'C',
qNum: 'NaN',
qElemNumber: 2,
qState: 'O',
},
],
[
{
qText: 'D',
qNum: 'NaN',
qElemNumber: 3,
qState: 'O',
},
],
[
{
qText: 'E',
qNum: 'NaN',
qElemNumber: 4,
qState: 'O',
},
],
[
{
qText: 'F',
qNum: 'NaN',
qElemNumber: 5,
qState: 'O',
},
],
[
{
qText: 'G',
qNum: 'NaN',
qElemNumber: 6,
qState: 'O',
},
],
[
{
qText: 'H',
qNum: 'NaN',
qElemNumber: 7,
qState: 'O',
},
],
[
{
qText: 'I',
qNum: 'NaN',
qElemNumber: 8,
qState: 'O',
},
],
[
{
qText: 'J',
qNum: 'NaN',
qElemNumber: 9,
qState: 'O',
},
],
[
{
qText: 'K',
qNum: 'NaN',
qElemNumber: 10,
qState: 'O',
},
],
[
{
qText: 'L',
qNum: 'NaN',
qElemNumber: 11,
qState: 'O',
},
],
],
qTails: [],
qArea: {
qLeft: 0,
qTop: 0,
qWidth: 1,
qHeight: 12,
},
},
],
getListboxLayout: () => ({
qInfo: {
qId: 'qId',
},
visualization: 'listbox',
qListObject: {
qDimensionInfo: {
qLocked: false,
},
qSize: {
qcy: 12,
},
qInitialDataFetch: [{ qLeft: 0, qWidth: 0, qTop: 0, qHeight: 0 }],
},
qSelectionInfo: {
qInSelections: false,
},
}),
};
};

View File

@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<script src="/apis/stardust/dist/stardust.js"></script>
<script src="./listbox-data.js"></script>
<script defer src="./listbox.js"></script>
<style>
html,
body {
margin: 0;
padding: 0;
overflow: hidden;
}
#object {
position: absolute;
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div id="object" data-type="listbox"></div>
</body>
</html>

View File

@@ -0,0 +1,40 @@
(() => {
function getMocks() {
const { getMockData, getListboxLayout } = window.getFuncs();
const obj = {
id: `listbox-${+new Date()}`,
getListObjectData: async () => getMockData(),
getLayout: async () => getListboxLayout(),
on() {},
once() {},
};
const app = {
id: `${+new Date()}`,
session: {},
createSessionObject: async () => obj,
getObject: async () => obj,
getAppLayout: async () => ({ qTitle: '', qLocaleInfo: {} }),
};
return {
obj,
app,
};
}
const init = async () => {
const element = window.document.querySelector('#object');
const { app } = getMocks();
const nebbie = window.stardust.embed(app);
const listboxOptions = {
dense: false,
};
const inst = await nebbie.field('Alpha');
inst.mount(element, listboxOptions);
return () => {
inst?.unmount(element);
};
};
return init();
})();

View File

@@ -0,0 +1,42 @@
const getPage = require('../setup');
const startServer = require('../server');
const { execSequence, looksLike } = require('../testUtils');
describe('listbox mashup rendering test', () => {
const object = '[data-type="listbox"]';
const listboxSelector = `${object} .listbox-container`;
let page;
let takeScreenshot;
let destroyServer;
let destroyBrowser;
let url;
const PAGE_OPTIONS = { width: 300, height: 500 };
beforeEach(async () => {
({ url, destroy: destroyServer } = await startServer());
({ page, takeScreenshot, destroy: destroyBrowser } = await getPage(PAGE_OPTIONS));
});
afterEach(async () => {
await Promise.all([destroyServer(), destroyBrowser()]);
});
it('selecting two values should result in two green rows', async () => {
const FILE_NAME = 'listbox_select_EH.png';
await page.goto(`${url}/listbox/listbox.html`);
await page.waitForSelector(listboxSelector, { visible: true });
const selectNumbers = [4, 7];
const action = async (nbr) => {
const rowSelector = `${listboxSelector} [data-n="${nbr}"]`;
await page.click(rowSelector);
};
await execSequence(selectNumbers, action);
const snapshotElement = await page.$(listboxSelector);
const { path: capturedPath } = await takeScreenshot(FILE_NAME, snapshotElement);
await looksLike(FILE_NAME, capturedPath);
});
});

31
test/rendering/server.js Normal file
View File

@@ -0,0 +1,31 @@
const express = require('express');
const path = require('path');
async function startServer() {
const app = express();
const port = 8050;
const url = `http://localhost:${port}`;
app.use(express.static(path.resolve(__dirname)));
app.use('/apis', express.static(path.resolve(__dirname, '../../apis')));
app.use('/node_modules', express.static(path.resolve(__dirname, '../../node_modules')));
let server;
await new Promise((resolve) => {
server = app.listen(port, () => {
console.log(`Running rendering server at ${url}`);
resolve();
});
});
const destroy = () => {
server.close();
};
return {
url,
destroy,
};
}
module.exports = startServer;

38
test/rendering/setup.js Normal file
View File

@@ -0,0 +1,38 @@
const puppeteer = require('puppeteer');
const path = require('path');
const artifactsPath = path.join(__dirname, './__artifacts__');
async function getPage(options = {}) {
const { width, height } = options;
const browser = await puppeteer.launch({
// Uncomment these for debugging the test visually.
// headless: false,
// slowMo: 200,
});
const page = await browser.newPage();
await page.setViewport({ width, height });
page.setDefaultNavigationTimeout(30000);
page.setDefaultTimeout(30000);
const destroy = async () => {
await browser.close();
};
/**
*
* @param {string} fileName The name of the output file, should match the baseline version's name.
* @param {HTMLElement} elm If you want to restrict the screenshot area to a certain element.
* @returns {Promise} Resolves the absolute path to the screenshot.
*/
const takeScreenshot = async (fileName, elm = undefined) => {
const screenshotPath = path.resolve(artifactsPath, './temp', fileName);
const clip = await elm?.boundingBox();
await page.screenshot({ clip, path: screenshotPath });
return { path: screenshotPath };
};
return { browser, page, destroy, takeScreenshot };
}
module.exports = getPage;

View File

@@ -0,0 +1,41 @@
const path = require('path');
const jimp = require('jimp');
async function looksLike(fileName, capturedPath) {
const artifactsPath = path.resolve(__dirname, './__artifacts__/');
const storedPath = path.resolve(artifactsPath, 'baseline', fileName);
const stored = await jimp.read(storedPath);
const captured = await jimp.read(capturedPath);
const distance = jimp.distance(stored, captured);
const diff = jimp.diff(stored, captured);
if (distance > 0.001 || diff.percent > 0.007) {
await captured.writeAsync(path.resolve(artifactsPath, 'regression', fileName));
await diff.image.writeAsync(path.resolve(artifactsPath, 'diff', fileName));
throw new Error(`Images differ too much - distance: ${distance}, percent: ${diff.percent}`);
}
}
/**
* Utility function for ensuring that each action is awaited before executing the next one.
* @param {function[]} items An array of items (e.g. selectors) that will be sent into the action function, iteratively.
* @returns {Promise} Resolves true when done.
*/
async function execSequence(items, action) {
const takeAction = async (index = 0) => {
if (index >= items.length) {
return true; // done
}
const nextItem = items[index];
await action(nextItem);
return takeAction(index + 1);
};
return takeAction();
}
module.exports = {
execSequence,
looksLike,
};

View File

@@ -9552,6 +9552,17 @@ cosmiconfig-typescript-loader@^4.0.0:
resolved "https://registry.yarnpkg.com/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-4.0.0.tgz#4a6d856c1281135197346a6f64dfa73a9cd9fefa"
integrity sha512-cVpucSc2Tf+VPwCCR7SZzmQTQkPbkk4O01yXsYqXBIbjE1bhwqSyAgYQkRK1un4i0OPziTleqFhdkmOc4RQ/9g==
cosmiconfig@7.0.1, cosmiconfig@^7.0.0:
version "7.0.1"
resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.1.tgz#714d756522cace867867ccb4474c5d01bbae5d6d"
integrity sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==
dependencies:
"@types/parse-json" "^4.0.0"
import-fresh "^3.2.1"
parse-json "^5.0.0"
path-type "^4.0.0"
yaml "^1.10.0"
cosmiconfig@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982"
@@ -9563,17 +9574,6 @@ cosmiconfig@^6.0.0:
path-type "^4.0.0"
yaml "^1.7.2"
cosmiconfig@^7.0.0:
version "7.0.1"
resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.1.tgz#714d756522cace867867ccb4474c5d01bbae5d6d"
integrity sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==
dependencies:
"@types/parse-json" "^4.0.0"
import-fresh "^3.2.1"
parse-json "^5.0.0"
path-type "^4.0.0"
yaml "^1.10.0"
cp-file@^6.2.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/cp-file/-/cp-file-6.2.0.tgz#40d5ea4a1def2a9acdd07ba5c0b0246ef73dc10d"
@@ -10245,6 +10245,11 @@ devtools-protocol@0.0.1001819:
resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.1001819.tgz#0a98f44cefdb02cc684f3d5e6bd898a1690231d9"
integrity sha512-G6OsIFnv/rDyxSqBa2lDLR6thp9oJioLsb2Gl+LbQlyoA9/OBAkrTU9jiCcQ8Pnh7z4d6slDiLaogR5hzgJLmQ==
devtools-protocol@0.0.1056733:
version "0.0.1056733"
resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.1056733.tgz#55bb1d56761014cc221131cca5e6bad94eefb2b9"
integrity sha512-CmTu6SQx2g3TbZzDCAV58+LTxVdKplS7xip0g5oDXpZ+isr0rv5dDP8ToyVRywzPHkCCPKgKgScEcwz4uPWDIA==
dezalgo@^1.0.0:
version "1.0.4"
resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.4.tgz#751235260469084c132157dfa857f386d4c33d81"
@@ -18039,6 +18044,22 @@ puppeteer-core@1.20.0:
rimraf "^2.6.1"
ws "^6.1.0"
puppeteer-core@19.2.2:
version "19.2.2"
resolved "https://registry.yarnpkg.com/puppeteer-core/-/puppeteer-core-19.2.2.tgz#a9b7b25099d87d550c224c660c205b7ebdd03eb4"
integrity sha512-faojf+1pZ/tHXSr4x1q+9MVd9FrL3rpdbC0w7qN7MNClMoLuCvMbpR4vzcjoiJYgclt1n+SOPUOmHQViTw6frw==
dependencies:
cross-fetch "3.1.5"
debug "4.3.4"
devtools-protocol "0.0.1056733"
extract-zip "2.0.1"
https-proxy-agent "5.0.1"
proxy-from-env "1.1.0"
rimraf "3.0.2"
tar-fs "2.1.1"
unbzip2-stream "1.4.3"
ws "8.10.0"
puppeteer@14.4.1:
version "14.4.1"
resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-14.4.1.tgz#6c7437a65f7ba98ef8ad7c2b0f1cf808e91617bb"
@@ -18057,6 +18078,18 @@ puppeteer@14.4.1:
unbzip2-stream "1.4.3"
ws "8.7.0"
puppeteer@19.2.2:
version "19.2.2"
resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-19.2.2.tgz#e6f7bc089ac9bffea78b2f792bf3affd93e16803"
integrity sha512-m1T5Mog5qu5+dMBptWYTn6pXRdnFbydbVUCthqwbfd8/kOiMlzZBR9ywjX79LpvI1Sj+/z8+FKeIsjnMul8ZYA==
dependencies:
cosmiconfig "7.0.1"
devtools-protocol "0.0.1056733"
https-proxy-agent "5.0.1"
progress "2.0.3"
proxy-from-env "1.1.0"
puppeteer-core "19.2.2"
q@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"