diff --git a/jest.conf/visual.index.js b/jest.conf/visual.index.js index d982a5cff..f041c1326 100644 --- a/jest.conf/visual.index.js +++ b/jest.conf/visual.index.js @@ -1,30 +1,122 @@ const path = require('node:path'); +const http = require('http'); +const puppeteer = require('puppeteer'); -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' - ), +/* 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); + } }; -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, +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); + } }; diff --git a/package.json b/package.json index a99176d1f..963feee0d 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,9 @@ "precompile-svgs": "node utils/precompileSvgs/index.js && yarn run pregenerate", "precompile-custom-svgs": "node utils/precompileSvgs/index.js --custom && yarn run pregenerate", "_lint-watch-fix": "yarn lint -w -m", + "test:percy": "PERCY_LOGLEVEL=error npx percy exec -v -- jest --config jest.conf/visual.index.js -i ./lib/buttons-and-links/__tests__/KButton.spec.js", "test": "jest --config=jest.conf/index.js", - "test:visual": "JEST_PUPPETEER_CONFIG=./jest-puppeteer.config.js node startVisualTests.js", + "test:visual": "concurrently --kill-others --success first --names \"SERVER,TEST\" -c \"bgCyan.bold,bgYellow.bold\" \"yarn dev-only > /dev/null 2>&1\" \"yarn test:percy\"", "_api-watch": "chokidar \"**/lib/**\" -c \"node utils/extractApi.js\"" }, "files": [ @@ -48,6 +49,7 @@ "babel-jest": "^29.7.0", "browserslist-config-kolibri": "0.16.0-dev.7", "chokidar-cli": "^3.0.0", + "concurrently": "^8.2.2", "consola": "^2.15.3", "eslint-import-resolver-nuxt": "^1.0.1", "globby": "^6.1.0", @@ -63,7 +65,6 @@ "npm-run-all": "^4.1.5", "nuxt": "2.15.8", "prismjs": "^1.27.0", - "ps-tree": "^1.2.0", "puppeteer": "^22.11.0", "raw-loader": "0.5.1", "sass-loader": "^10.5.2", diff --git a/startVisualTests.js b/startVisualTests.js deleted file mode 100644 index c7819cf30..000000000 --- a/startVisualTests.js +++ /dev/null @@ -1,171 +0,0 @@ -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(); diff --git a/yarn.lock b/yarn.lock index def7ac347..10c582cd3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1029,6 +1029,13 @@ dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.21.0": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.7.tgz#f4f0d5530e8dbdf59b3451b9b3e594b6ba082e12" + integrity sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": version "7.15.4" resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.4.tgz" @@ -4594,6 +4601,21 @@ concat-stream@^1.5.0: readable-stream "^2.2.2" typedarray "^0.0.6" +concurrently@^8.2.2: + version "8.2.2" + resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-8.2.2.tgz#353141985c198cfa5e4a3ef90082c336b5851784" + integrity sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg== + dependencies: + chalk "^4.1.2" + date-fns "^2.30.0" + lodash "^4.17.21" + rxjs "^7.8.1" + shell-quote "^1.8.1" + spawn-command "0.0.2" + supports-color "^8.1.1" + tree-kill "^1.2.2" + yargs "^17.7.2" + condense-newlines@^0.2.1: version "0.2.1" resolved "https://registry.npmjs.org/condense-newlines/-/condense-newlines-0.2.1.tgz" @@ -5320,6 +5342,13 @@ date-fns@1.30.1: resolved "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz" integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw== +date-fns@^2.30.0: + version "2.30.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0" + integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw== + dependencies: + "@babel/runtime" "^7.21.0" + de-indent@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz" @@ -13410,6 +13439,11 @@ sourcemap-codec@^1.4.4: resolved "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz" integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== +spawn-command@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2.tgz#9544e1a43ca045f8531aac1a48cb29bdae62338e" + integrity sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ== + spawnd@^10.0.0: version "10.0.0" resolved "https://registry.yarnpkg.com/spawnd/-/spawnd-10.0.0.tgz#cf1b222831272f4bef7e2abc9f98cd31c380a4dc" @@ -13964,7 +13998,7 @@ supports-color@^7.0.0, supports-color@^7.1.0: dependencies: has-flag "^4.0.0" -supports-color@^8.0.0: +supports-color@^8.0.0, supports-color@^8.1.1: version "8.1.1" resolved "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz" integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== @@ -15605,7 +15639,7 @@ yargs-parser@^21.1.1: resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== -yargs@17.7.2, yargs@^17.2.1, yargs@^17.3.1: +yargs@17.7.2, yargs@^17.2.1, yargs@^17.3.1, yargs@^17.7.2: version "17.7.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==