Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Percy and jest-puppeteer environment setup for visual testing #670

Merged
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,19 @@ esLintConfig.settings['import/resolver'].nuxt = {
nuxtSrcDir: 'docs',
};

// Remove linting errors for the globals defined in the jest-puppeteer package
esLintConfig.env = {
...esLintConfig.env,
'jest': true,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont think we should add this here, because this lint config is for the whole package, including the development environment where we are not running jest, and I think this could cause unwanted behaviours. Probably we could set these settings in a specific file for tests?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @KshitijThareja! why do we need this property here in the esLintConfig env?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had followed troubleshooting guidelines as specified here: Jest-Puppeteer
This was basically to recognize jest's global variables and not flag their use while running linting tests.
But on taking a closer look now, I can see that this property has already been included in the main esLint config import (kolibri-tools/.eslintrc). I'll make the required changes.

};

esLintConfig.globals = {
AlexVelezLl marked this conversation as resolved.
Show resolved Hide resolved
...esLintConfig.globals,
page: true,
browser: true,
context: true,
puppeteerConfig: true,
jestPuppeteer: true,
};

module.exports = esLintConfig;
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,6 @@ kolibri-design-system.iml
# env
.envrc
.python-version

# local
myenv
AlexVelezLl marked this conversation as resolved.
Show resolved Hide resolved
19 changes: 19 additions & 0 deletions .percy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
version: 2
snapshot:
widths:
- 375
- 1280
minHeight: 1024
percyCSS: ""
enableJavaScript: false #disable Javascript by default to capture initial page state without JS-driven changes
cliEnableJavaScript: true #enable Javascript when running Percy through CLI, for dynamic content
disableShadowDOM: false
discovery:
allowedHostnames: []
disallowedHostnames: []
networkIdleTimeout: 100
captureMockedServiceWorker: false
upload:
files: "**/*.{png,jpg,jpeg}"
ignore: ""
stripExtensions: false
42 changes: 42 additions & 0 deletions docs/pages/testing-playground.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<template>

<!--
Testing Playground: A dedicated page for component visual testing
*****************************************************

Please do not modify the contents of this file.
-->
<div id="testing-playground" style="padding: 24px">
<component :is="component" v-bind="componentProps" />
</div>

</template>


<script>

export default {
name: 'TestingPlayground',
AlexVelezLl marked this conversation as resolved.
Show resolved Hide resolved
data() {
return {
component: null,
componentProps: {},
};
},
mounted() {
window.addEventListener('message', this.handleMessage);
},
beforeDestroy() {
window.removeEventListener('message', this.handleMessage);
},
methods: {
handleMessage(event) {
if (event.data.type === 'RENDER_COMPONENT') {
this.component = event.data.component;
this.componentProps = event.data.props;
}
},
},
};

</script>
7 changes: 7 additions & 0 deletions jest-puppeteer.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = {
launch: {
headless: true,
timeout: 180000,
},
browserContext: 'default',
};
4 changes: 3 additions & 1 deletion jest.conf/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ Vue.config.silent = true;
Vue.config.devtools = false;
Vue.config.productionTip = false;

Object.defineProperty(window, 'scrollTo', { value: () => {}, writable: true });
if (process.env.TEST_TYPE !== 'visual' && typeof window !== 'undefined') {
Object.defineProperty(window, 'scrollTo', { value: () => {}, writable: true });
}

// Shows better NodeJS unhandled promise rejection errors
process.on('unhandledRejection', (reason, p) => {
Expand Down
11 changes: 11 additions & 0 deletions jest.conf/testUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@ export const resizeWindow = (width, height = 768) => {
});
};

export function canTakeScreenshot() {
const percyToken = process.env.PERCY_TOKEN;
const runVisualTests = process.env.TEST_TYPE === 'visual';
if (runVisualTests && !percyToken) {
throw new Error(
'Error: Visual tests cannot be run because PERCY_TOKEN environment variable is not set.'
);
}
return runVisualTests && percyToken;
}

export const testAfterResize = testFunction => {
let animationFrameId;
const assertAfterResize = () => {
Expand Down
30 changes: 30 additions & 0 deletions jest.conf/visual.index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const path = require('node:path');

const moduleNameMapper = {
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga|css)$': path.resolve(
__dirname,
'./fileMock.js'
),
};

module.exports = {
rootDir: path.resolve(__dirname, '..'),
preset: 'jest-puppeteer',
testTimeout: 50000,
moduleFileExtensions: ['js', 'json', 'vue'],
moduleNameMapper,
transform: {
'^.+\\.js$': require.resolve('babel-jest'),
'^.+\\.vue$': require.resolve('vue-jest'),
},
snapshotSerializers: ['jest-serializer-vue'],
globals: {
HOST: 'http://localhost:4000/',
'vue-jest': {
hideStyleWarn: true,
experimentalCSSCompile: true,
},
},
setupFilesAfterEnv: [path.resolve(__dirname, './visual.setup')],
verbose: true,
};
7 changes: 7 additions & 0 deletions jest.conf/visual.setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import './setup';
import { percySnapshot } from '@percy/puppeteer';

// Set the test type to visual
process.env.TEST_TYPE = 'visual';

global.percySnapshot = percySnapshot;
139 changes: 75 additions & 64 deletions lib/KImg/__tests__/KImg.spec.js
Original file line number Diff line number Diff line change
@@ -1,92 +1,103 @@
import { shallowMount } from '@vue/test-utils';
import percySnapshot from '@percy/puppeteer';
import KImg from '../';
import { canTakeScreenshot } from '../../../jest.conf/testUtils';

function makeWrapper(opts) {
return shallowMount(KImg, opts);
}

describe('KImg', () => {
it(`renders without any errors when a valid 'src' and 'altText' are provided`, () => {
const error = jest.fn();
const wrapper = makeWrapper({
propsData: { src: '/le-logo.svg', altText: 'LE logo' },
listeners: { error },
});

expect(wrapper.exists()).toBe(true);
expect(error).not.toHaveBeenCalled();
if (!canTakeScreenshot()) {
it(`renders without any errors when a valid 'src' and 'altText' are provided`, () => {
const error = jest.fn();
const wrapper = makeWrapper({
propsData: { src: '/le-logo.svg', altText: 'LE logo' },
listeners: { error },
});

const img = wrapper.find('img');
expect(img.exists()).toBe(true);
expect(img.attributes('src')).toBe('/le-logo.svg');
expect(img.attributes('alt')).toBe('LE logo');
});
expect(wrapper.exists()).toBe(true);
expect(error).not.toHaveBeenCalled();

it(`throws an error when no 'altText' is provided`, () => {
const error = jest.fn();
makeWrapper({
propsData: { src: '/le-logo.svg', altText: undefined },
listeners: { error },
const img = wrapper.find('img');
expect(img.exists()).toBe(true);
expect(img.attributes('src')).toBe('/le-logo.svg');
expect(img.attributes('alt')).toBe('LE logo');
});
expect(error).toHaveBeenCalled();
expect(error.mock.calls[0][0]).toBeInstanceOf(Error);
expect(error.mock.calls[0][0].message).toBe(
'Missing required prop - provide altText or indicate isDecorative'
);
});

describe(`when no 'altText' is provided and it is a decorative image`, () => {
it(`does not throw an error`, () => {
it(`throws an error when no 'altText' is provided`, () => {
const error = jest.fn();
makeWrapper({
propsData: { src: '/le-logo.svg', altText: undefined, isDecorative: true },
propsData: { src: '/le-logo.svg', altText: undefined },
listeners: { error },
});
expect(error).not.toHaveBeenCalled();
expect(error).toHaveBeenCalled();
expect(error.mock.calls[0][0]).toBeInstanceOf(Error);
expect(error.mock.calls[0][0].message).toBe(
'Missing required prop - provide altText or indicate isDecorative'
);
});

it(`sets 'alt' attribute to an empty string`, () => {
const wrapper = makeWrapper({
propsData: { src: '/le-logo.svg', altText: undefined, isDecorative: true },
describe(`when no 'altText' is provided and it is a decorative image`, () => {
it(`does not throw an error`, () => {
const error = jest.fn();
makeWrapper({
propsData: { src: '/le-logo.svg', altText: undefined, isDecorative: true },
listeners: { error },
});
expect(error).not.toHaveBeenCalled();
});
expect(wrapper.exists()).toBe(true);
expect(wrapper.find('img').attributes('alt')).toBe('');
});
});

it(`throws an error when 'aspectRatio' has an invalid format`, () => {
const error = jest.fn();
makeWrapper({
propsData: { src: '/le-logo.svg', altText: 'LE logo', aspectRatio: '16/9' },
listeners: { error },
it(`sets 'alt' attribute to an empty string`, () => {
const wrapper = makeWrapper({
propsData: { src: '/le-logo.svg', altText: undefined, isDecorative: true },
});
expect(wrapper.exists()).toBe(true);
expect(wrapper.find('img').attributes('alt')).toBe('');
});
});
expect(error).toHaveBeenCalled();
expect(error.mock.calls[0][0]).toBeInstanceOf(Error);
expect(error.mock.calls[0][0].message).toBe('Invalid aspect ratio provided: 16/9');
});

it(`doesn't throw an error when 'aspectRatio' has a valid format`, () => {
const error = jest.fn();
makeWrapper({
propsData: { src: '/le-logo.svg', altText: 'LE logo', aspectRatio: '16:9' },
listeners: { error },
it(`throws an error when 'aspectRatio' has an invalid format`, () => {
const error = jest.fn();
makeWrapper({
propsData: { src: '/le-logo.svg', altText: 'LE logo', aspectRatio: '16/9' },
listeners: { error },
});
expect(error).toHaveBeenCalled();
expect(error.mock.calls[0][0]).toBeInstanceOf(Error);
expect(error.mock.calls[0][0].message).toBe('Invalid aspect ratio provided: 16/9');
});
expect(error).not.toHaveBeenCalled();
});

it(`emits an 'error' event when there is an error in loading the image`, async () => {
const error = jest.fn();
const wrapper = makeWrapper({
propsData: { src: '/le-logo.svg', altText: 'LE logo' },
listeners: { error },
it(`doesn't throw an error when 'aspectRatio' has a valid format`, () => {
const error = jest.fn();
makeWrapper({
propsData: { src: '/le-logo.svg', altText: 'LE logo', aspectRatio: '16:9' },
listeners: { error },
});
expect(error).not.toHaveBeenCalled();
});

// Manually trigger the onError method to simulate the image load failure
const e = new Event('error');
wrapper.vm.onError(e);
it(`emits an 'error' event when there is an error in loading the image`, async () => {
const error = jest.fn();
const wrapper = makeWrapper({
propsData: { src: '/le-logo.svg', altText: 'LE logo' },
listeners: { error },
});

// Manually trigger the onError method to simulate the image load failure
const e = new Event('error');
wrapper.vm.onError(e);

expect(error).toHaveBeenCalled();
expect(error.mock.calls[0][0]).toBeInstanceOf(Event);
expect(error.mock.calls[0][0]).toEqual(e);
});
expect(error).toHaveBeenCalled();
expect(error.mock.calls[0][0]).toBeInstanceOf(Event);
expect(error.mock.calls[0][0]).toEqual(e);
});
} else {
describe('KImg Visual Tests', () => {
AlexVelezLl marked this conversation as resolved.
Show resolved Hide resolved
it('renders correctly (mock test)', async () => {
await page.goto('http://localhost:4000/', { waitUntil: 'networkidle2' });
await percySnapshot(page, 'HomePage');
});
});
}
});
Loading
Loading