Skip to content

Commit

Permalink
Merge pull request learningequality#670 from KshitijThareja/visual-te…
Browse files Browse the repository at this point in the history
…sting

Percy and jest-puppeteer environment setup for visual testing
  • Loading branch information
bjester authored and KshitijThareja committed Jan 18, 2025
1 parent fc09854 commit cf41d8c
Show file tree
Hide file tree
Showing 5 changed files with 1,138 additions and 205 deletions.
142 changes: 25 additions & 117 deletions jest.conf/visual.index.js
Original file line number Diff line number Diff line change
@@ -1,122 +1,30 @@
const path = require('node:path');
const http = require('http');
const puppeteer = require('puppeteer');

/* eslint-disable no-console */

const SERVER_URL = 'http://localhost:4000/testing-playground';
const SERVER_TIMEOUT = 360000;
const WAIT_FOR_SELECTOR = '#testing-playground';
let setupDone = false;

const waitForServer = async (url, timeout = 30000) => {
const start = Date.now();
let waitingLogged = false;

const checkServer = () => {
return new Promise((resolve, reject) => {
const req = http.get(url, res => {
if (res.statusCode === 200) {
resolve(true);
} else {
reject(new Error(`Server responded with status code: ${res.statusCode}`));
}
});

req.on('error', () => {
if (!waitingLogged) {
console.error('Waiting for server to respond.');
waitingLogged = true;
}
resolve(false);
});

req.end();
});
};

while (Date.now() - start < timeout) {
try {
const isServerUp = await checkServer();
if (isServerUp) {
return;
}
} catch (err) {
console.error(err.message);
}
await new Promise(resolve => setTimeout(resolve, 1000));
}
throw new Error('Server did not start within the timeout period');
};

const checkPageLoad = async (url, timeout = 30000) => {
const browser = await puppeteer.launch();
const page = await browser.newPage();

try {
await page.goto(url, { waitUntil: 'networkidle2', timeout });
await page.waitForSelector(WAIT_FOR_SELECTOR, { timeout });
console.log('Visual testing playground is loaded.');
} catch (error) {
throw new Error('Failed to load visual testing playground.');
} finally {
await browser.close();
}
};

const validatePercyToken = () => {
if (!process.env.PERCY_TOKEN) {
throw new Error(
'PERCY_TOKEN environment variable is not set. Please set it to run visual tests.'
);
}
};

const runServerChecks = async () => {
if (setupDone) return;
setupDone = true;
try {
await waitForServer(SERVER_URL, SERVER_TIMEOUT);
await checkPageLoad(SERVER_URL, SERVER_TIMEOUT);
console.log('Server and testing playground are up and running');
} catch (error) {
console.error(error);
process.exit(1);
}
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 = async () => {
try {
validatePercyToken();
await runServerChecks();
return {
rootDir: path.resolve(__dirname, '..'),
preset: 'jest-puppeteer',
testTimeout: 50000,
moduleFileExtensions: ['js', 'json', 'vue'],
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'
),
},
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,
};
} catch (error) {
console.error(error);
process.exit(1);
}
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,
};
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"normalize.css": "^8.0.1",
"nuxt": "2.18.1",
"prismjs": "^1.27.0",
"ps-tree": "^1.2.0",
"puppeteer": "^22.11.0",
"raw-loader": "0.5.1",
"sass-loader": "^10.5.2",
Expand Down
171 changes: 171 additions & 0 deletions startVisualTests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
const { spawn } = require('child_process');
const net = require('net');
const fetch = require('node-fetch');
const psTree = require('ps-tree');
const puppeteer = require('puppeteer');

/* eslint-disable no-console */

const SERVER_URL = 'http://localhost:4000/testing-playground';
const SERVER_TIMEOUT = 360000;
const CHECK_ELEMENT_SELECTOR = '#testing-playground';

const waitForServer = async (url, timeout = 30000) => {
const start = Date.now();
let waitingLogged = false;
while (Date.now() - start < timeout) {
try {
const response = await fetch(url);
if (response.ok) {
return;
}
} catch (err) {
if (!waitingLogged) {
console.error('Waiting for server to respond.');
waitingLogged = true;
}
}
await new Promise(resolve => setTimeout(resolve, 1000));
}
throw new Error('Server did not start within the timeout period');
};

const checkPortInUse = port => {
return new Promise((resolve, reject) => {
const server = net.createServer();
server.once('error', err => {
if (err.code === 'EADDRINUSE') {
reject(new Error(`Port ${port} is already in use.`));
} else {
reject(err);
}
});

server.once('listening', () => {
server.close();
resolve();
});

server.listen(port);
});
};

const checkPageLoad = async (url, timeout = 30000) => {
const browser = await puppeteer.launch();
const page = await browser.newPage();

try {
await page.goto(url, { waitUntil: 'networkidle2', timeout });
await page.waitForSelector(CHECK_ELEMENT_SELECTOR, { timeout });
console.log('Page is fully loaded.');
} catch (error) {
throw new Error('Page did not load correctly.');
} finally {
await browser.close();
}
};

const startServer = () => {
return new Promise((resolve, reject) => {
const server = spawn('yarn', ['dev-only'], { shell: true });

server.on('error', err => {
reject(new Error(`Failed to start server: ${err.message}`));
});

server.on('close', code => {
console.log(`Server process exited with code ${code}`);
if (code !== 0) {
reject(new Error('Server failed to start'));
}
});

waitForServer(SERVER_URL, SERVER_TIMEOUT)
.then(() => checkPageLoad(SERVER_URL, SERVER_TIMEOUT))
.then(() => {
console.log('Server and page are up and running');
resolve(server);
})
.catch(error => {
server.kill('SIGINT');
reject(error);
});
});
};

const runTests = () => {
return new Promise((resolve, reject) => {
const tests = spawn(
'npx',
[
'percy',
'exec',
'-v',
'--',
'jest',
'--config',
'jest.conf/visual.index.js',
'-i',
'./lib/buttons-and-links/__tests__/KButton.spec.js',
],
{ stdio: 'inherit' }
);

tests.on('close', code => {
console.log(`Tests process exited with code ${code}`);
resolve(code);
});

tests.on('error', error => {
reject(error);
});
});
};

const stopServer = server => {
return new Promise((resolve, reject) => {
psTree(server.pid, (err, children) => {
if (err) {
return reject(err);
}
[server.pid, ...children.map(p => p.PID)].forEach(pid => {
try {
process.kill(pid, 'SIGINT');
} catch (e) {
if (e.code !== 'ESRCH') {
reject(e);
}
}
});
resolve();
});
});
};

const validateTestRun = () => {
if (!process.env.PERCY_TOKEN) {
throw new Error(
'PERCY_TOKEN environment variable is not set. Please set it to run visual tests.'
);
}
};

const start = async () => {
validateTestRun();
let server;
try {
await checkPortInUse(4000);
server = await startServer();
const testExitCode = await runTests();
await stopServer(server);
process.exit(testExitCode);
} catch (error) {
console.error(error);
if (server) {
await stopServer(server);
}
process.exit(1);
}
};

start();
Loading

0 comments on commit cf41d8c

Please sign in to comment.