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 2 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
AlexVelezLl marked this conversation as resolved.
Show resolved Hide resolved
cliEnableJavaScript: true
disableShadowDOM: false
discovery:
allowedHostnames: []
disallowedHostnames: []
networkIdleTimeout: 100
captureMockedServiceWorker: false
upload:
files: "**/*.{png,jpg,jpeg}"
ignore: ""
stripExtensions: false
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',
};
2 changes: 2 additions & 0 deletions jest.conf/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import VueIntl from 'vue-intl';
import VueCompositionAPI from '@vue/composition-api';
import KThemePlugin from '../lib/KThemePlugin';

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

global.beforeEach(() => {
return new Promise(resolve => {
Aphrodite.StyleSheetTestUtils.suppressStyleInjection();
Expand Down
72 changes: 72 additions & 0 deletions jest.conf/setup.visual.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import 'regenerator-runtime/runtime';
import '@testing-library/jest-dom';
import * as Aphrodite from 'aphrodite';
import * as AphroditeNoImportant from 'aphrodite/no-important';

// eslint-disable-next-line import/no-unresolved
AlexVelezLl marked this conversation as resolved.
Show resolved Hide resolved
import 'mock-match-media/jest-setup';
AlexVelezLl marked this conversation as resolved.
Show resolved Hide resolved

import Vue from 'vue';
import VueRouter from 'vue-router';
import VueIntl from 'vue-intl';
import VueCompositionAPI from '@vue/composition-api';
import { percySnapshot } from '@percy/puppeteer';
import KThemePlugin from '../lib/KThemePlugin';

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

if (typeof window !== 'undefined') {
global.matchMedia =
global.matchMedia ||
function() {
return {
matches: false,
addListener: function() {},
removeListener: function() {},
};
};
}

global.beforeEach(() => {
return new Promise(resolve => {
Aphrodite.StyleSheetTestUtils.suppressStyleInjection();
AphroditeNoImportant.StyleSheetTestUtils.suppressStyleInjection();
return process.nextTick(resolve);
});
});

global.afterEach(() => {
return new Promise(resolve => {
Aphrodite.StyleSheetTestUtils.clearBufferAndResumeStyleInjection();
AphroditeNoImportant.StyleSheetTestUtils.clearBufferAndResumeStyleInjection();
return process.nextTick(resolve);
});
});

global.percySnapshot = percySnapshot;

// Register Vue plugins and components
Vue.use(VueRouter);
Vue.use(VueCompositionAPI);
Vue.use(KThemePlugin);
Vue.use(VueIntl);

Vue.config.silent = true;
Vue.config.devtools = false;
Vue.config.productionTip = false;

// Object.defineProperty(window, 'scrollTo', { value: () => {}, writable: true });

// Shows better NodeJS unhandled promise rejection errors
process.on('unhandledRejection', (reason, p) => {
/* eslint-disable no-console */
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
console.log(reason.stack);
});

// Copied from https://github.com/kentor/flush-promises/blob/f33ac564190c784019f1f689dd544187f4b77eb2/index.js
global.flushPromises = function flushPromises() {
return new Promise(function(resolve) {
setImmediate(resolve);
});
};
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 takeScreenshot() {
AlexVelezLl marked this conversation as resolved.
Show resolved Hide resolved
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
32 changes: 32 additions & 0 deletions jest.conf/visual.index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const path = require('node:path');

process.env.JEST_PUPPETEER_CONFIG = require.resolve('../jest-puppeteer.config.js');
AlexVelezLl marked this conversation as resolved.
Show resolved Hide resolved

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, './setup.visual')],
verbose: true,
};
140 changes: 76 additions & 64 deletions lib/KImg/__tests__/KImg.spec.js
Original file line number Diff line number Diff line change
@@ -1,92 +1,104 @@
import { shallowMount } from '@vue/test-utils';
import percySnapshot from '@percy/puppeteer';
import KImg from '../';
import { takeScreenshot } 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 (process.env.TEST_TYPE !== 'visual') {
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);
});
}
if (takeScreenshot()) {
AlexVelezLl marked this conversation as resolved.
Show resolved Hide resolved
describe('KImg Visual Tests', () => {
AlexVelezLl marked this conversation as resolved.
Show resolved Hide resolved
it('renders correctly (mock test)', async () => {
await global.page.goto('http://localhost:4000/', { waitUntil: 'networkidle2' });
AlexVelezLl marked this conversation as resolved.
Show resolved Hide resolved
await percySnapshot(page, 'HomePage');
});
});
}
});
2 changes: 1 addition & 1 deletion lib/composables/useKResponsiveWindow/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ function initProps() {
if (isNuxtServerSideRendering()) {
return;
}
if (window.matchMedia) {
if (typeof window !== 'undefined' && window.matchMedia) {
orientationQuery.eventHandler(orientationQuery.mediaQueryList);
heightQuery.eventHandler(heightQuery.mediaQueryList);
}
Expand Down
Loading
Loading