diff --git a/.eslintrc.js b/.eslintrc.js index b8b42b712..6f77eee99 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -20,7 +20,7 @@ esLintConfig.settings['import/resolver'].nuxt = { nuxtSrcDir: 'docs', }; -// Remove linting errors for the globals defined in the jest-puppeteer package +// Remove linting errors for the globals defined in the jest-puppeteer package and testUtils esLintConfig.globals = { ...esLintConfig.globals, page: true, diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 347627c03..4b1de12be 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -38,4 +38,4 @@ jobs: yarn --frozen-lockfile npm rebuild node-sass - name: Run tests - run: yarn test + run: yarn test \ No newline at end of file diff --git a/jest.conf/index.js b/jest.conf/index.js index d534833b3..61b06d735 100644 --- a/jest.conf/index.js +++ b/jest.conf/index.js @@ -10,6 +10,7 @@ const moduleNameMapper = { module.exports = { rootDir: path.resolve(__dirname, '..'), moduleFileExtensions: ['js', 'json', 'vue'], + testNamePattern: '^(?!.*\\[Visual\\])', moduleNameMapper, testEnvironment: 'jsdom', testEnvironmentOptions: { diff --git a/jest.conf/setup.js b/jest.conf/setup.js index d62049c5d..f600b044d 100644 --- a/jest.conf/setup.js +++ b/jest.conf/setup.js @@ -28,6 +28,15 @@ global.afterEach(() => { }); }); +// Configure special test blocks for visual tests +global.describe.visual = (name, fn) => { + global.describe(`[Visual] ${name}`, fn); +}; + +global.it.visual = (name, fn) => { + global.it(`[Visual] ${name}`, fn); +}; + // Register Vue plugins and components Vue.use(VueRouter); Vue.use(VueCompositionAPI); diff --git a/jest.conf/testUtils.js b/jest.conf/testUtils.js index 81a23645e..0023aaf00 100644 --- a/jest.conf/testUtils.js +++ b/jest.conf/testUtils.js @@ -10,17 +10,6 @@ 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 = () => { diff --git a/jest.conf/visual.index.js b/jest.conf/visual.index.js index f041c1326..c6b5dba88 100644 --- a/jest.conf/visual.index.js +++ b/jest.conf/visual.index.js @@ -94,6 +94,7 @@ module.exports = async () => { preset: 'jest-puppeteer', testTimeout: 50000, moduleFileExtensions: ['js', 'json', 'vue'], + testNamePattern: '\\[Visual\\]', moduleNameMapper: { '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga|css)$': path.resolve( __dirname, diff --git a/jest.conf/visual.setup.js b/jest.conf/visual.setup.js index 0e983657c..6b80ecc30 100644 --- a/jest.conf/visual.setup.js +++ b/jest.conf/visual.setup.js @@ -4,4 +4,9 @@ import { percySnapshot } from '@percy/puppeteer'; // Set the test type to visual process.env.TEST_TYPE = 'visual'; +const TESTING_PLAYGROUND_URL = 'http://localhost:4000/testing-playground'; global.percySnapshot = percySnapshot; + +global.beforeAll(async () => { + await page.goto(TESTING_PLAYGROUND_URL, { waitUntil: 'networkidle2' }); +}); diff --git a/jest.conf/visual.testUtils.js b/jest.conf/visual.testUtils.js new file mode 100644 index 000000000..d7b87db37 --- /dev/null +++ b/jest.conf/visual.testUtils.js @@ -0,0 +1,47 @@ +import percySnapshot from '@percy/puppeteer'; + +export async function renderComponent(component, props) { + const beforeRenderState = await page.evaluate(() => { + const testing_playground = document.querySelector('#testing-playground'); + return testing_playground ? testing_playground.innerHTML : ''; + }); + + await page.evaluate( + ({ component, props }) => { + window.postMessage( + { + type: 'RENDER_COMPONENT', + component: component, + props: props, + }, + '*' + ); + }, + { component, props } + ); + await page.waitForSelector('#testing-playground'); + + // Wait until the innerHTML of the testing playground changes, indicating that the component has been rendered. + await page.waitForFunction( + initialState => { + const testing_playground = document.querySelector('#testing-playground'); + return testing_playground && testing_playground.innerHTML !== initialState; + }, + {}, + beforeRenderState + ); + + // Check if the component has been rendered by comparing the initial state with the current state. + const isComponentRendered = await page.evaluate(initialState => { + const testing_playground = document.querySelector('#testing-playground'); + return testing_playground && testing_playground.innerHTML !== initialState; + }, beforeRenderState); + + global.expect(isComponentRendered).toBe(true); +} + +export async function takeSnapshot(name) { + if (process.env.TEST_TYPE == 'visual') { + await percySnapshot(page, name); + } +} diff --git a/lib/buttons-and-links/__tests__/KButton.spec.js b/lib/buttons-and-links/__tests__/KButton.spec.js index ca1fa251a..cd359f727 100644 --- a/lib/buttons-and-links/__tests__/KButton.spec.js +++ b/lib/buttons-and-links/__tests__/KButton.spec.js @@ -1,109 +1,75 @@ import { shallowMount } from '@vue/test-utils'; -import percySnapshot from '@percy/puppeteer'; import KButton from '../KButton.vue'; -import { canTakeScreenshot } from '../../../jest.conf/testUtils'; +import { renderComponent, takeSnapshot } from '../../../jest.conf/visual.testUtils'; describe('KButton', () => { - if (!canTakeScreenshot()) { - describe('icon related props', () => { - it('should render an icon before the text with the icon string passed to the `icon` prop', () => { - const wrapper = shallowMount(KButton, { - propsData: { - icon: 'add', - }, - }); - expect(wrapper.find('[data-test="iconBefore"]').exists()).toBe(true); + describe('icon related props', () => { + it('should render an icon before the text with the icon string passed to the `icon` prop', () => { + const wrapper = shallowMount(KButton, { + propsData: { + icon: 'add', + }, }); - it('should render an icon after the text with the icon string pased to the `iconAfter` prop', () => { - const wrapper = shallowMount(KButton, { - propsData: { - iconAfter: 'video', - }, - }); - expect(wrapper.find('[data-test="iconAfter"]').exists()).toBe(true); + expect(wrapper.find('[data-test="iconBefore"]').exists()).toBe(true); + }); + it('should render an icon after the text with the icon string pased to the `iconAfter` prop', () => { + const wrapper = shallowMount(KButton, { + propsData: { + iconAfter: 'video', + }, }); - it('should render a dropdown icon when hasDropdown is true', () => { - const wrapper = shallowMount(KButton, { - propsData: { - hasDropdown: true, - }, - }); - expect(wrapper.find('[data-test="dropdownIcon"]').exists()).toBe(true); + expect(wrapper.find('[data-test="iconAfter"]').exists()).toBe(true); + }); + it('should render a dropdown icon when hasDropdown is true', () => { + const wrapper = shallowMount(KButton, { + propsData: { + hasDropdown: true, + }, }); + expect(wrapper.find('[data-test="dropdownIcon"]').exists()).toBe(true); }); + }); - describe('text prop and slots', () => { - it('should render the text prop if nothing is in the default slot', () => { - const wrapper = shallowMount(KButton, { - propsData: { - text: 'test', - }, - }); - expect(wrapper.text()).toContain('test'); + describe('text prop and slots', () => { + it('should render the text prop if nothing is in the default slot', () => { + const wrapper = shallowMount(KButton, { + propsData: { + text: 'test', + }, }); + expect(wrapper.text()).toContain('test'); + }); - it('should render the slot when the slot has content', () => { - const wrapper = shallowMount(KButton, { - propsData: { - text: 'test', - }, - slots: { - default: 'slot', - }, - }); - expect(wrapper.text()).toContain('slot'); - expect(wrapper.text()).toContain('test'); + it('should render the slot when the slot has content', () => { + const wrapper = shallowMount(KButton, { + propsData: { + text: 'test', + }, + slots: { + default: 'slot', + }, }); + expect(wrapper.text()).toContain('slot'); + expect(wrapper.text()).toContain('test'); }); + }); - describe('event handling', () => { - it('should emit a click event when clicked', () => { - const wrapper = shallowMount(KButton, { - propsData: { - text: 'test', - }, - }); - wrapper.trigger('click'); - expect(wrapper.emitted().click).toBeTruthy(); + describe('event handling', () => { + it('should emit a click event when clicked', () => { + const wrapper = shallowMount(KButton, { + propsData: { + text: 'test', + }, }); + wrapper.trigger('click'); + expect(wrapper.emitted().click).toBeTruthy(); }); - } else { - describe('KButton Visual Tests', () => { - beforeAll(async () => { - await page.goto('http://localhost:4000/testing-playground', { waitUntil: 'networkidle2' }); - }); + }); - async function renderComponent(component, props) { - await page.evaluate( - ({ component, props }) => { - window.postMessage( - { - type: 'RENDER_COMPONENT', - component: component, - props: props, - }, - '*' - ); - }, - { component, props } - ); - await page.waitForSelector('#testing-playground'); - - const isComponentRendered = await page.evaluate(() => { - const testing_playground = document.querySelector('#testing-playground'); - return testing_playground && testing_playground.children.length > 0; - }); - - if (!isComponentRendered) { - // eslint-disable-next-line no-console - console.error('Component did not render in the testing playground'); - } - } - - it('renders correctly with default props', async () => { - await renderComponent('KButton', { text: 'Test Button' }); - await percySnapshot(page, 'KButton - Default'); - }); + describe.visual('KButton Visual Tests', () => { + it('renders correctly with default props', async () => { + await renderComponent('KButton', { text: 'Test Button' }); + await takeSnapshot('KButton - Default'); }); - } + }); }); diff --git a/package.json b/package.json index 963feee0d..a711214e3 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "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:percy": "PERCY_LOGLEVEL=info 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": "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\"" diff --git a/yarn.lock b/yarn.lock index 10c582cd3..ba5628288 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5778,7 +5778,7 @@ dotenv@^9.0.2: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-9.0.2.tgz#dacc20160935a37dea6364aa1bef819fb9b6ab05" integrity sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg== -duplexer@^0.1.2, duplexer@~0.1.1: +duplexer@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== @@ -6338,19 +6338,6 @@ etag@^1.8.1, etag@~1.8.1: resolved "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz" integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== -event-stream@=3.3.4: - version "3.3.4" - resolved "https://registry.yarnpkg.com/event-stream/-/event-stream-3.3.4.tgz#4ab4c9a0f5a54db9338b4c34d86bfce8f4b35571" - integrity sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g== - dependencies: - duplexer "~0.1.1" - from "~0" - map-stream "~0.1.0" - pause-stream "0.0.11" - split "0.3" - stream-combiner "~0.0.4" - through "~2.3.1" - eventemitter3@^4.0.0: version "4.0.7" resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz" @@ -6890,11 +6877,6 @@ from2@^2.1.0: inherits "^2.0.1" readable-stream "^2.0.0" -from@~0: - version "0.1.7" - resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe" - integrity sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g== - fs-exists-sync@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz#982d6893af918e72d08dec9e8673ff2b5a8d6add" @@ -9604,11 +9586,6 @@ map-obj@^4.0.0: resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.3.0.tgz#9304f906e93faae70880da102a9f1df0ea8bb05a" integrity sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ== -map-stream@~0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194" - integrity sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g== - map-values@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/map-values/-/map-values-1.0.1.tgz" @@ -10881,13 +10858,6 @@ path-type@^4.0.0: resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== -pause-stream@0.0.11: - version "0.0.11" - resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445" - integrity sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A== - dependencies: - through "~2.3" - pbkdf2@^3.0.3, pbkdf2@^3.1.2: version "3.1.2" resolved "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz" @@ -12083,13 +12053,6 @@ prr@~1.0.1: resolved "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz" integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY= -ps-tree@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/ps-tree/-/ps-tree-1.2.0.tgz#5e7425b89508736cdd4f2224d028f7bb3f722ebd" - integrity sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA== - dependencies: - event-stream "=3.3.4" - pseudomap@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz" @@ -13508,13 +13471,6 @@ split-string@^3.0.1, split-string@^3.0.2: dependencies: extend-shallow "^3.0.0" -split@0.3: - version "0.3.3" - resolved "https://registry.yarnpkg.com/split/-/split-0.3.3.tgz#cd0eea5e63a211dfff7eb0f091c4133e2d0dd28f" - integrity sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA== - dependencies: - through "2" - sprintf-js@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a" @@ -13620,13 +13576,6 @@ stream-browserify@^2.0.1: inherits "~2.0.1" readable-stream "^2.0.2" -stream-combiner@~0.0.4: - version "0.0.4" - resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.0.4.tgz#4d5e433c185261dde623ca3f44c586bcf5c4ad14" - integrity sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw== - dependencies: - duplexer "~0.1.1" - stream-each@^1.1.0: version "1.2.3" resolved "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz" @@ -14254,7 +14203,7 @@ through2@^2.0.0: readable-stream "~2.3.6" xtend "~4.0.1" -through@2, through@^2.3.6, through@^2.3.8, through@~2.3, through@~2.3.1: +through@^2.3.6, through@^2.3.8: version "2.3.8" resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=