Use cypress for visual testing
- Ditch Casper.js / PhantomCSS - Set up Cypress and cypress-image-snapshot - Implement visual tests covering most of the same ground as what I had before (and more), but with some caveats - Some tests are flaky right now due to resource loading, and some have areas blotched out in order to not depend on resource loading - TODO: set up continuous integration, add more tests, etc.main
|
@ -1,4 +1,10 @@
|
||||||
|
|
||||||
|
# cypress-image-snapshot visual diffs
|
||||||
|
__diff_output__
|
||||||
|
|
||||||
|
# cypress-image-snapshot images of the whole cypress UI
|
||||||
|
*(failed).snap.png
|
||||||
|
|
||||||
# electron forge output
|
# electron forge output
|
||||||
out/
|
out/
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"baseUrl": "http://localhost:11822",
|
||||||
|
"video": false
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
/// <reference types="Cypress" />
|
||||||
|
|
||||||
|
context('visual tests', () => {
|
||||||
|
it('main screenshot', () => {
|
||||||
|
cy.visit('/');
|
||||||
|
cy.matchImageSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('brush selected', () => {
|
||||||
|
cy.get('.tool[title="Brush"]').click();
|
||||||
|
cy.get('.Tools-component').matchImageSnapshot();
|
||||||
|
});
|
||||||
|
it('select selected', () => {
|
||||||
|
cy.get('.tool[title="Select"]').click();
|
||||||
|
cy.get('.Tools-component').matchImageSnapshot();
|
||||||
|
});
|
||||||
|
it('magnifier selected', () => {
|
||||||
|
cy.get('.tool[title="Magnifier"]').click();
|
||||||
|
cy.get('.Tools-component').matchImageSnapshot();
|
||||||
|
});
|
||||||
|
it('airbrush selected', () => {
|
||||||
|
cy.get('.tool[title="Airbrush"]').click();
|
||||||
|
cy.get('.Tools-component').matchImageSnapshot();
|
||||||
|
});
|
||||||
|
it('eraser selected', () => {
|
||||||
|
cy.get('.tool[title="Eraser/Color Eraser"]').click();
|
||||||
|
cy.get('.Tools-component').matchImageSnapshot();
|
||||||
|
});
|
||||||
|
it('line selected', () => {
|
||||||
|
cy.get('.tool[title="Line"]').click();
|
||||||
|
cy.get('.Tools-component').matchImageSnapshot();
|
||||||
|
});
|
||||||
|
it('rectangle selected', () => {
|
||||||
|
cy.get('.tool[title="Rectangle"]').click();
|
||||||
|
cy.get('.Tools-component').matchImageSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('image attributes window', () => {
|
||||||
|
cy.get('body').type('{ctrl}e');
|
||||||
|
cy.get('.window:visible').matchImageSnapshot();
|
||||||
|
cy.get('.window:visible .window-close-button').click();
|
||||||
|
cy.get('.window').should('not.be.visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('flip and rotate window', () => {
|
||||||
|
// TODO: make menus more testable, with IDs
|
||||||
|
cy.get('.menus > .menu-container:nth-child(4) > .menu-button > .menu-hotkey').click();
|
||||||
|
cy.get('.menus > .menu-container:nth-child(4) > .menu-popup > table > tr:nth-child(1)').click();
|
||||||
|
cy.get('.window:visible').matchImageSnapshot();
|
||||||
|
cy.get('.window:visible .window-close-button').click();
|
||||||
|
cy.get('.window').should('not.be.visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stretch and skew window', () => {
|
||||||
|
// TODO: make menus more testable, with IDs
|
||||||
|
cy.get('.menus > .menu-container:nth-child(4) > .menu-button > .menu-hotkey').click();
|
||||||
|
cy.get('.menus > .menu-container:nth-child(4) > .menu-popup > table > tr:nth-child(2)').click();
|
||||||
|
// TODO: wait for images to load and include images?
|
||||||
|
cy.get('.window:visible').matchImageSnapshot({ blackout: ["img"] });
|
||||||
|
cy.get('.window:visible .window-close-button').click();
|
||||||
|
cy.get('.window').should('not.be.visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('help window', () => {
|
||||||
|
// TODO: make menus more testable, with IDs
|
||||||
|
cy.get('.menus > .menu-container:nth-child(6) > .menu-button > .menu-hotkey').click();
|
||||||
|
cy.get('.menus > .menu-container:nth-child(6) > .menu-popup > table > tr:nth-child(1)').click();
|
||||||
|
cy.get('.window:visible .folder', {timeout: 10000}); // wait for sidebar contents to load
|
||||||
|
// TODO: wait for iframe to load
|
||||||
|
cy.get('.window:visible').matchImageSnapshot({ blackout: ["iframe"] });
|
||||||
|
cy.get('.window:visible .window-close-button').click();
|
||||||
|
cy.get('.window').should('not.be.visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('about window', () => {
|
||||||
|
// TODO: make menus more testable, with IDs
|
||||||
|
cy.get('.menus > .menu-container:nth-child(6) > .menu-button > .menu-hotkey').click();
|
||||||
|
cy.get('.menus > .menu-container:nth-child(6) > .menu-popup > table > tr:nth-child(3)').click();
|
||||||
|
cy.get('.window:visible').matchImageSnapshot({ blackout: ["img"] });
|
||||||
|
cy.get('.window:visible .window-close-button').click();
|
||||||
|
cy.get('.window').should('not.be.visible');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,11 @@
|
||||||
|
// This function is called when a project is opened or re-opened (e.g. due to
|
||||||
|
// the project's config changing)
|
||||||
|
|
||||||
|
const {
|
||||||
|
addMatchImageSnapshotPlugin,
|
||||||
|
} = require("cypress-image-snapshot/plugin");
|
||||||
|
module.exports = (on, config) => {
|
||||||
|
// `on` is used to hook into various events Cypress emits
|
||||||
|
// `config` is the resolved Cypress config
|
||||||
|
addMatchImageSnapshotPlugin(on, config);
|
||||||
|
};
|
After Width: | Height: | Size: 47 KiB |
After Width: | Height: | Size: 4.0 KiB |
After Width: | Height: | Size: 3.5 KiB |
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 33 KiB |
After Width: | Height: | Size: 33 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 3.5 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 4.3 KiB |
After Width: | Height: | Size: 20 KiB |
|
@ -0,0 +1,14 @@
|
||||||
|
import { addMatchImageSnapshotCommand } from 'cypress-image-snapshot/command';
|
||||||
|
addMatchImageSnapshotCommand({
|
||||||
|
failureThreshold: 0.00,
|
||||||
|
failureThresholdType: 'percent',
|
||||||
|
customDiffConfig: { threshold: 0.0 },
|
||||||
|
capture: 'viewport',
|
||||||
|
});
|
||||||
|
Cypress.Commands.add("setResolution", (size) => {
|
||||||
|
if (Cypress._.isArray(size)) {
|
||||||
|
cy.viewport(size[0], size[1]);
|
||||||
|
} else {
|
||||||
|
cy.viewport(size);
|
||||||
|
}
|
||||||
|
})
|
|
@ -0,0 +1,7 @@
|
||||||
|
// ***********************************************************
|
||||||
|
// This support/index.js is processed and
|
||||||
|
// loaded automatically before your test files.
|
||||||
|
//
|
||||||
|
// https://on.cypress.io/configuration
|
||||||
|
|
||||||
|
import './commands'
|
22
package.json
|
@ -64,14 +64,15 @@
|
||||||
"@electron-forge/maker-squirrel": "6.0.0-beta.45",
|
"@electron-forge/maker-squirrel": "6.0.0-beta.45",
|
||||||
"@electron-forge/maker-zip": "6.0.0-beta.45",
|
"@electron-forge/maker-zip": "6.0.0-beta.45",
|
||||||
"cross-spawn": "^7.0.0",
|
"cross-spawn": "^7.0.0",
|
||||||
|
"cypress": "3.6.0",
|
||||||
|
"cypress-image-snapshot": "3.1.1",
|
||||||
"devtron": "^1.4.0",
|
"devtron": "^1.4.0",
|
||||||
"electron": "6.0.10",
|
"electron": "6.0.10",
|
||||||
"electron-debug": "^3.0.1",
|
"electron-debug": "^3.0.1",
|
||||||
"eslint": "6.4.0",
|
"eslint": "6.4.0",
|
||||||
"live-server": "^1.2.1",
|
"live-server": "^1.2.1",
|
||||||
"npm-run-all": "4.1.5",
|
"serve": "11.2.0",
|
||||||
"phantomcss": "^1.6.0",
|
"start-server-and-test": "1.10.6"
|
||||||
"serve": "11.2.0"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "electron-forge start",
|
"start": "electron-forge start",
|
||||||
|
@ -80,14 +81,13 @@
|
||||||
"make": "electron-forge make",
|
"make": "electron-forge make",
|
||||||
"publish": "electron-forge publish",
|
"publish": "electron-forge publish",
|
||||||
"lint": "eslint src/",
|
"lint": "eslint src/",
|
||||||
"dev": "live-server",
|
"dev": "live-server --ignorePattern=\"(node_modules|cypress|out)[/\\\\]|package\\.json|cypress\\.json\"",
|
||||||
"test": "run-p test:server test:runner",
|
"test:start-server": "live-server --port=11822 --no-browser --ignorePattern=\"(node_modules|cypress|out)[/\\\\]|package\\.json|cypress\\.json\"",
|
||||||
"test-verbose": "run-p test:server test:runner:verbose",
|
"cy:open": "cypress open",
|
||||||
"test:server": "serve --listen 11822 --no-clipboard",
|
"cy:run": "cypress run",
|
||||||
"test:runner": "casperjs test test.js --verbose --log-level=debug",
|
"cy:accept": "cypress run --env updateSnapshots=true",
|
||||||
"test:runner:verbose": "casperjs test test.js --verbose --log-level=debug",
|
"test": "start-server-and-test test:start-server http://localhost:11822 cy:run",
|
||||||
"test:runner:firefox": "casperjs test test.js --engine=slimerjs --verbose --log-level=debug",
|
"accept": "start-server-and-test test:start-server http://localhost:11822 cy:accept"
|
||||||
"test:runner:firefox:verbose": "casperjs test test.js --engine=slimerjs --verbose --log-level=debug"
|
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 731 B |
Before Width: | Height: | Size: 731 B |
Before Width: | Height: | Size: 7.4 KiB |
Before Width: | Height: | Size: 7.4 KiB |
Before Width: | Height: | Size: 68 KiB |
Before Width: | Height: | Size: 68 KiB |
Before Width: | Height: | Size: 982 B |
Before Width: | Height: | Size: 982 B |
Before Width: | Height: | Size: 8.2 KiB |
Before Width: | Height: | Size: 8.2 KiB |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.4 KiB |
112
test.js
|
@ -1,112 +0,0 @@
|
||||||
|
|
||||||
var fs = require('fs');
|
|
||||||
var phantomcss = require('phantomcss');
|
|
||||||
|
|
||||||
casper.test.begin('jspaint visual tests', function(test){
|
|
||||||
|
|
||||||
phantomcss.init({
|
|
||||||
rebase: casper.cli.get('rebase'),
|
|
||||||
// SlimerJS needs explicit knowledge of this Casper, and lots of absolute paths
|
|
||||||
casper: casper,
|
|
||||||
libraryRoot: fs.absolute(fs.workingDirectory + '/node_modules/phantomcss'), //module._getFilename('phantomcss'), //require.resolve('phantomcss'),
|
|
||||||
screenshotRoot: fs.absolute(fs.workingDirectory + '/screenshots'),
|
|
||||||
failedComparisonsRoot: fs.absolute(fs.workingDirectory + '/screenshots/failures'),
|
|
||||||
addLabelToFailedImage: false,
|
|
||||||
/*
|
|
||||||
fileNameGetter: function overide_file_naming(){},
|
|
||||||
onPass: function pass_callback(){},
|
|
||||||
onFail: function fail_callback(){},
|
|
||||||
onTimeout: function timeout_callback(){},
|
|
||||||
onComplete: function complete_callback(){},
|
|
||||||
hideElements: '#thing.selector',
|
|
||||||
addLabelToFailedImage: true,
|
|
||||||
outputSettings: {
|
|
||||||
errorColor: {
|
|
||||||
red: 255,
|
|
||||||
green: 255,
|
|
||||||
blue: 0
|
|
||||||
},
|
|
||||||
errorType: 'movement',
|
|
||||||
transparency: 0.3
|
|
||||||
}*/
|
|
||||||
});
|
|
||||||
|
|
||||||
casper.on('remote.message', function(msg){
|
|
||||||
this.echo("[page] " + msg);
|
|
||||||
});
|
|
||||||
|
|
||||||
casper.on('error', function(err){
|
|
||||||
this.die("PhantomJS has errored: " + err);
|
|
||||||
});
|
|
||||||
|
|
||||||
casper.on('resource.error', function(err){
|
|
||||||
casper.log('Resource load error: ' + err, 'warning');
|
|
||||||
});
|
|
||||||
/*
|
|
||||||
The test scenario
|
|
||||||
*/
|
|
||||||
|
|
||||||
casper.start('http://localhost:11822');
|
|
||||||
|
|
||||||
casper.viewport(1024, 768);
|
|
||||||
|
|
||||||
casper.then(function(){
|
|
||||||
phantomcss.screenshot('.jspaint', 'app screen initial');
|
|
||||||
phantomcss.screenshot('.menus', 'menu bar initial');
|
|
||||||
phantomcss.screenshot('.Tools-component', 'toolbox initial');
|
|
||||||
phantomcss.screenshot('.Colors-component', 'color box initial');
|
|
||||||
});
|
|
||||||
|
|
||||||
var screenshot_and_close_window = function(screenshot_name){
|
|
||||||
// var window_title = "Attributes";
|
|
||||||
// var selector = {
|
|
||||||
// type: "xpath",
|
|
||||||
// path: "//div[contains(concat(' ', normalize-space(@class), ' '), ' window ')][//span[contains(concat(' ', normalize-space(@class), ' '), ' window-title ')][.='" + window_title + "']]"
|
|
||||||
// };
|
|
||||||
var selector = ".window:not([style*='display: none'])";
|
|
||||||
|
|
||||||
casper.then(function(){
|
|
||||||
casper.waitUntilVisible(selector,
|
|
||||||
function success(){
|
|
||||||
phantomcss.screenshot(selector, screenshot_name);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
// casper.thenEvaluate(function(selector){
|
|
||||||
// $(selector).find(".window-close-button").trigger("click");
|
|
||||||
// }, selector);
|
|
||||||
casper.then(function close_the_window(){
|
|
||||||
casper.click(selector + " .window-close-button");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
casper.thenEvaluate(function(){
|
|
||||||
image_attributes();
|
|
||||||
});
|
|
||||||
screenshot_and_close_window('attributes window');
|
|
||||||
|
|
||||||
casper.thenEvaluate(function(){
|
|
||||||
image_flip_and_rotate();
|
|
||||||
});
|
|
||||||
screenshot_and_close_window('flip and rotate window');
|
|
||||||
|
|
||||||
casper.thenEvaluate(function(){
|
|
||||||
image_stretch_and_skew();
|
|
||||||
});
|
|
||||||
screenshot_and_close_window('stretch and skew window');
|
|
||||||
|
|
||||||
casper.thenEvaluate(function(){
|
|
||||||
show_help();
|
|
||||||
});
|
|
||||||
screenshot_and_close_window('help window');
|
|
||||||
|
|
||||||
casper.then(function now_check_the_screenshots(){
|
|
||||||
phantomcss.compareAll();
|
|
||||||
});
|
|
||||||
|
|
||||||
casper.run(function(){
|
|
||||||
console.log('\nTHE END.');
|
|
||||||
// phantomcss.getExitStatus() // pass or fail?
|
|
||||||
casper.test.done();
|
|
||||||
});
|
|
||||||
});
|
|