From 881dfd8f7890e72f2518f026a53cbd9f158d94a5 Mon Sep 17 00:00:00 2001 From: Oliver Eyton-Williams Date: Tue, 16 Sep 2025 08:30:06 +0200 Subject: [PATCH] refactor: client jest -> vitest (#62177) --- client/__mocks__/gatsby.ts | 33 ++ client/{src => }/__mocks__/react-i18next.js | 9 +- client/__mocks__/react-spinkit.tsx | 4 + client/i18n/locales.test.ts | 39 +- client/package.json | 5 + client/src/__mocks__/challenge-nodes.ts | 188 ------- client/src/__mocks__/file-mock.ts | 1 - client/src/__mocks__/gatsby.ts | 27 - client/src/__mocks__/style-mock.ts | 1 - .../integration/handled-error.test.ts | 1 + client/src/analytics/call-ga.test.ts | 24 - .../client-only-routes/show-settings.test.tsx | 23 +- .../Footer/__snapshots__/footer.test.tsx.snap | 8 +- client/src/components/Footer/footer.test.tsx | 1 + client/src/components/Header/header.test.tsx | 3 +- client/src/components/Intro/intro.test.tsx | 12 +- .../OfflineWarning/offline-warning.test.tsx | 13 +- .../components/app-mount-notifier.test.tsx | 4 +- .../create-language-redirect.test.ts | 26 +- .../formHelpers/form-validators.test.ts | 277 +++++----- .../src/components/formHelpers/form.test.tsx | 129 ++--- .../__snapshots__/loader.test.tsx.snap | 26 +- .../helpers/border-color-picker.test.ts | 1 + .../block-save-button.test.tsx.snap | 4 +- .../helpers/form/block-save-button.test.tsx | 35 +- client/src/components/helpers/link.test.tsx | 1 + client/src/components/helpers/loader.test.tsx | 1 + .../__snapshots__/profile.test.tsx.snap | 4 +- .../__fixtures__}/completed-challenges.json | 0 .../components/__fixtures__}/edges.json | 0 ...js.snap => timeline-buttons.test.jsx.snap} | 4 +- .../profile/components/heat-map.test.tsx | 14 +- .../profile/components/stats.test.tsx | 9 +- .../profile/components/time-line.test.tsx | 3 + .../components/timeline-buttons.test.js | 50 -- .../components/timeline-buttons.test.jsx | 55 ++ .../src/components/profile/profile.test.tsx | 3 +- .../search/searchBar/search-bar.test.tsx | 7 +- client/src/components/seo/seo.test.tsx | 19 +- .../__snapshots__/honesty.test.tsx.snap | 6 +- .../settings/certification.test.tsx | 6 +- .../src/components/settings/honesty.test.tsx | 3 +- .../components/share/share-template.test.tsx | 1 + .../src/components/share/use-share.test.tsx | 56 +- .../staging-warning-modal.test.tsx | 12 +- client/src/pages/challenges.test.tsx | 1 + client/src/redux/donation-saga.test.js | 12 +- client/src/redux/failed-updates-epic.test.js | 3 +- .../classic/saved-challenges.test.ts | 1 + .../challenge-title.test.tsx.snap | 4 +- .../components/challenge-title.test.tsx | 1 + .../components/challenge-transcript.test.tsx | 1 + .../components/completion-modal.test.tsx | 31 +- .../Challenges/components/help-modal.test.tsx | 3 + .../fill-in-the-blank/parse-blanks.test.ts | 1 + .../Challenges/redux/completion-epic.test.js | 1 + .../redux/create-question-epic.test.js | 1 + .../redux/execute-challenge-saga.test.js | 12 +- .../templates/Challenges/utils/index.test.ts | 1 + .../Challenges/utils/worker-executor.test.js | 37 +- .../__snapshots__/block-intros.test.tsx.snap | 4 +- .../components/block-intros.test.tsx | 1 + .../Introduction/components/block.test.tsx | 45 +- .../Introduction/components/block.tsx | 2 +- client/src/utils/__mocks__/get-words.ts | 7 + client/src/utils/analytics-strings.test.ts | 1 + client/src/utils/create-types.test.ts | 1 + client/src/utils/format.test.ts | 1 + client/src/utils/growthbook-cookie.test.ts | 26 +- client/src/utils/handled-error.test.ts | 1 + client/src/utils/is-contained.test.ts | 1 + client/src/utils/path-parsers.test.ts | 1 + client/src/utils/replace-apple-quotes.test.ts | 1 + client/src/utils/session-storage.test.ts | 26 +- .../src/utils/solution-display-type.test.ts | 1 + client/src/utils/to-learn-path.test.ts | 1 + .../tools/generate-search-placeholder.test.ts | 2 + client/tsconfig.json | 1 + client/utils/gatsby/layout-selector.test.tsx | 102 ++-- client/utils/sort-challengefiles.test.ts | 1 + client/vitest-setup.js | 13 + client/vitest.config.js | 27 + jest.config.js | 9 +- package.json | 2 +- pnpm-lock.yaml | 523 +++++++++++++----- pnpm-workspace.yaml | 6 + 86 files changed, 1100 insertions(+), 964 deletions(-) create mode 100644 client/__mocks__/gatsby.ts rename client/{src => }/__mocks__/react-i18next.js (85%) create mode 100644 client/__mocks__/react-spinkit.tsx delete mode 100644 client/src/__mocks__/challenge-nodes.ts delete mode 100644 client/src/__mocks__/file-mock.ts delete mode 100644 client/src/__mocks__/gatsby.ts delete mode 100644 client/src/__mocks__/style-mock.ts delete mode 100644 client/src/analytics/call-ga.test.ts rename client/src/{__mocks__ => components/profile/components/__fixtures__}/completed-challenges.json (100%) rename client/src/{__mocks__ => components/profile/components/__fixtures__}/edges.json (100%) rename client/src/components/profile/components/__snapshots__/{timeline-buttons.test.js.snap => timeline-buttons.test.jsx.snap} (98%) delete mode 100644 client/src/components/profile/components/timeline-buttons.test.js create mode 100644 client/src/components/profile/components/timeline-buttons.test.jsx create mode 100644 client/src/utils/__mocks__/get-words.ts create mode 100644 client/vitest-setup.js create mode 100644 client/vitest.config.js diff --git a/client/__mocks__/gatsby.ts b/client/__mocks__/gatsby.ts new file mode 100644 index 00000000000..b279e387259 --- /dev/null +++ b/client/__mocks__/gatsby.ts @@ -0,0 +1,33 @@ +import React from 'react'; +import type { GatsbyLinkProps } from 'gatsby'; +import { vi } from 'vitest'; +import gatsby from 'gatsby'; + +import envData from '../config/env.json'; +const { clientLocale } = envData; + +export const navigate = vi.fn(); +export const graphql = vi.fn(); +export const Link = vi + .fn() + .mockImplementation(({ to, ...rest }: GatsbyLinkProps) => + React.createElement('a', { ...rest, href: to }) + ); +export const withPrefix = vi.fn().mockImplementation((path: string) => { + const pathPrefix = clientLocale === 'english' ? '' : '/' + clientLocale; + return pathPrefix + path; +}); +export const StaticQuery = vi.fn(); +export const useStaticQuery = vi.fn(); + +export default { + // ...existing code... + // spread the actual gatsby module to keep other exports working + ...gatsby, + navigate, + graphql, + Link, + withPrefix, + StaticQuery, + useStaticQuery +}; diff --git a/client/src/__mocks__/react-i18next.js b/client/__mocks__/react-i18next.js similarity index 85% rename from client/src/__mocks__/react-i18next.js rename to client/__mocks__/react-i18next.js index be0857a798d..3157ba82a4c 100644 --- a/client/src/__mocks__/react-i18next.js +++ b/client/__mocks__/react-i18next.js @@ -1,7 +1,5 @@ import React from 'react'; -const reactI18next = jest.genMockFromModule('react-i18next'); - // modified from https://github.com/i18next/react-i18next/blob/master/example/test-jest/src/__mocks__/react-i18next.js const hasChildren = node => node && (node.children || (node.props && node.props.children)); @@ -58,9 +56,4 @@ const Trans = ({ children }) => ''} {...props} /> ); */ -// reactI18next.translate = translate; -reactI18next.withTranslation = withTranslation; -reactI18next.useTranslation = useTranslation; -reactI18next.Trans = Trans; - -module.exports = reactI18next; +module.exports = { withTranslation, useTranslation, Trans }; diff --git a/client/__mocks__/react-spinkit.tsx b/client/__mocks__/react-spinkit.tsx new file mode 100644 index 00000000000..982f64b2b5f --- /dev/null +++ b/client/__mocks__/react-spinkit.tsx @@ -0,0 +1,4 @@ +import React from 'react'; + +// eslint-disable-next-line react/display-name +export default () =>
Spinner
; diff --git a/client/i18n/locales.test.ts b/client/i18n/locales.test.ts index 622c4f23d0c..a353c1a7087 100644 --- a/client/i18n/locales.test.ts +++ b/client/i18n/locales.test.ts @@ -1,5 +1,9 @@ import fs from 'fs'; -import { setup } from 'jest-json-schema-extended'; +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; + +import { describe, test, expect } from 'vitest'; + import { availableLangs, LangNames, LangCodes } from '../../shared/config/i18n'; import { catalogSuperBlocks, @@ -7,8 +11,6 @@ import { } from '../../shared/config/curriculum'; import intro from './locales/english/intro.json'; -setup(); - interface Intro { [key: string]: { title: string; @@ -41,6 +43,9 @@ const filesThatShouldExist = [ } ]; +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + const path = `${__dirname}/locales`; describe('Locale tests:', () => { @@ -77,19 +82,25 @@ describe('Intro file structure tests:', () => { const typedIntro = intro as unknown as Intro; const superblocks = Object.values(SuperBlocks); for (const superBlock of superblocks) { - expect(typeof typedIntro[superBlock].title).toBe('string'); + test(`superBlock ${superBlock} has required properties`, () => { + expect(typeof typedIntro[superBlock].title).toBe('string'); + + // catalog superblocks should have a summary + if (catalogSuperBlocks.includes(superBlock)) { + expect(typedIntro[superBlock].intro).toBeInstanceOf(Array); + } - // catalog superblocks should have a summary - if (catalogSuperBlocks.includes(superBlock)) { expect(typedIntro[superBlock].intro).toBeInstanceOf(Array); - } - - expect(typedIntro[superBlock].intro).toBeInstanceOf(Array); - expect(typedIntro[superBlock].blocks).toBeInstanceOf(Object); - const blocks = Object.keys(typedIntro[superBlock].blocks); - blocks.forEach(block => { - expect(typeof typedIntro[superBlock].blocks[block].title).toBe('string'); - expect(typedIntro[superBlock].blocks[block].intro).toBeInstanceOf(Array); + expect(typedIntro[superBlock].blocks).toBeInstanceOf(Object); + const blocks = Object.keys(typedIntro[superBlock].blocks); + blocks.forEach(block => { + expect(typeof typedIntro[superBlock].blocks[block].title).toBe( + 'string' + ); + expect(typedIntro[superBlock].blocks[block].intro).toBeInstanceOf( + Array + ); + }); }); } }); diff --git a/client/package.json b/client/package.json index 4b3928b5344..de526f5ebd1 100644 --- a/client/package.json +++ b/client/package.json @@ -34,6 +34,7 @@ "serve-ci": "serve -l 8000 -c serve.json public", "prestand-alone": "pnpm run prebuild", "stand-alone": "gatsby develop", + "test": "vitest", "validate-keys": "tsx --tsconfig ../tsconfig.json ../tools/scripts/lint/validate-keys" }, "dependencies": { @@ -132,7 +133,9 @@ }, "devDependencies": { "@babel/plugin-syntax-dynamic-import": "7.8.3", + "@testing-library/jest-dom": "^6.8.0", "@testing-library/react": "12.1.5", + "@testing-library/react-hooks": "^8.0.1", "@total-typescript/ts-reset": "^0.5.0", "@types/canvas-confetti": "^1.6.0", "@types/gatsbyjs__reach-router": "1.3.0", @@ -156,6 +159,7 @@ "@types/sanitize-html": "^2.8.0", "@types/store": "^2.0.2", "@types/validator": "^13.7.12", + "@vitest/ui": "^3.2.4", "autoprefixer": "10.4.17", "babel-plugin-macros": "3.1.0", "core-js": "2.6.12", @@ -170,6 +174,7 @@ "react-test-renderer": "17.0.2", "redux-saga-test-plan": "4.0.6", "serve": "13.0.4", + "vitest": "^3.2.4", "webpack": "5.90.3" } } diff --git a/client/src/__mocks__/challenge-nodes.ts b/client/src/__mocks__/challenge-nodes.ts deleted file mode 100644 index 674af565418..00000000000 --- a/client/src/__mocks__/challenge-nodes.ts +++ /dev/null @@ -1,188 +0,0 @@ -interface MockChallengeNodes { - challenge: { - fields: { - slug: string; - blockName: string; - }; - id: string; - block: string; - title: string; - superBlock: string; - dashedName: string; - }; -} - -const mockChallengeNodes: MockChallengeNodes[] = [ - { - challenge: { - fields: { - slug: '/super-block-one/block-a/challenge-one', - blockName: 'Block A' - }, - id: 'a', - block: 'block-a', - title: 'Challenge One', - superBlock: 'super-block-one', - dashedName: 'challenge-one' - } - }, - { - challenge: { - fields: { - slug: '/super-block-one/block-a/challenge-two', - blockName: 'Block A' - }, - id: 'b', - block: 'block-a', - title: 'Challenge Two', - superBlock: 'super-block-one', - dashedName: 'challenge-two' - } - }, - { - challenge: { - fields: { - slug: '/super-block-one/block-b/challenge-one', - blockName: 'Block B' - }, - id: 'c', - block: 'block-b', - title: 'Challenge One', - superBlock: 'super-block-one', - dashedName: 'challenge-one' - } - }, - { - challenge: { - fields: { - slug: '/super-block-one/block-b/challenge-two', - blockName: 'Block B' - }, - - id: 'd', - block: 'block-b', - title: 'Challenge Two', - superBlock: 'super-block-one', - dashedName: 'challenge-two' - } - }, - { - challenge: { - fields: { - slug: '/super-block-one/block-c/challenge-one', - blockName: 'Block C' - }, - id: 'e', - block: 'block-c', - title: 'Challenge One', - superBlock: 'super-block-one', - dashedName: 'challenge-one' - } - }, - { - challenge: { - fields: { - slug: '/super-block-two/block-a/challenge-one', - blockName: 'Block A' - }, - id: 'f', - block: 'block-a', - title: 'Challenge One', - superBlock: 'super-block-two', - dashedName: 'challenge-one' - } - }, - { - challenge: { - fields: { - slug: '/super-block-two/block-a/challenge-two', - blockName: 'Block A' - }, - id: 'g', - block: 'block-a', - title: 'Challenge Two', - superBlock: 'super-block-two', - dashedName: 'challenge-two' - } - }, - { - challenge: { - fields: { - slug: '/super-block-two/block-b/challenge-one', - blockName: 'Block B' - }, - id: 'h', - block: 'block-b', - title: 'Challenge One', - superBlock: 'super-block-two', - dashedName: 'challenge-one' - } - }, - { - challenge: { - fields: { - slug: '/super-block-two/block-b/challenge-two', - blockName: 'Block B' - }, - id: 'i', - block: 'block-b', - title: 'Challenge Two', - superBlock: 'super-block-two', - dashedName: 'challenge-two' - } - }, - { - challenge: { - fields: { - slug: '/super-block-three/block-a/challenge-one', - blockName: 'Block A' - }, - id: 'j', - block: 'block-a', - title: 'Challenge One', - superBlock: 'super-block-three', - dashedName: 'challenge-one' - } - }, - { - challenge: { - fields: { - slug: '/super-block-three/block-c/challenge-two', - blockName: 'Block C' - }, - id: 'k', - block: 'block-c', - title: 'Challenge Two', - superBlock: 'super-block-three', - dashedName: 'challenge-two' - } - }, - { - challenge: { - fields: { - slug: '/super-block-three/block-c/challenge-three', - blockName: 'Block C' - }, - id: 'l', - block: 'block-c', - title: 'Challenge Three', - superBlock: 'super-block-three', - dashedName: 'challenge-three' - } - }, - { - challenge: { - fields: { - slug: '/super-block-four/block-a/challenge-one', - blockName: 'Block A' - }, - id: 'm', - block: 'block-a', - title: 'Challenge One', - superBlock: 'super-block-four', - dashedName: 'challenge-one' - } - } -]; - -export default mockChallengeNodes; diff --git a/client/src/__mocks__/file-mock.ts b/client/src/__mocks__/file-mock.ts deleted file mode 100644 index ff8b4c56321..00000000000 --- a/client/src/__mocks__/file-mock.ts +++ /dev/null @@ -1 +0,0 @@ -export default {}; diff --git a/client/src/__mocks__/gatsby.ts b/client/src/__mocks__/gatsby.ts deleted file mode 100644 index 362d802fad2..00000000000 --- a/client/src/__mocks__/gatsby.ts +++ /dev/null @@ -1,27 +0,0 @@ -import React from 'react'; -import { GatsbyLinkProps } from 'gatsby'; -const gatsby: NodeModule = jest.requireActual('gatsby'); - -import envData from '../../config/env.json'; - -const { clientLocale } = envData; - -module.exports = { - ...gatsby, - navigate: jest.fn(), - graphql: jest.fn(), - Link: jest.fn().mockImplementation( - // these props are invalid for an `a` tag - ({ to, ...rest }: GatsbyLinkProps) => - React.createElement('a', { - ...rest, - href: to - }) - ), - withPrefix: jest.fn().mockImplementation((path: string) => { - const pathPrefix = clientLocale === 'english' ? '' : '/' + clientLocale; - return pathPrefix + path; - }), - StaticQuery: jest.fn(), - useStaticQuery: jest.fn() -}; diff --git a/client/src/__mocks__/style-mock.ts b/client/src/__mocks__/style-mock.ts deleted file mode 100644 index ff8b4c56321..00000000000 --- a/client/src/__mocks__/style-mock.ts +++ /dev/null @@ -1 +0,0 @@ -export default {}; diff --git a/client/src/__tests__/integration/handled-error.test.ts b/client/src/__tests__/integration/handled-error.test.ts index 80b0c5d6ee5..3ba72b81a37 100644 --- a/client/src/__tests__/integration/handled-error.test.ts +++ b/client/src/__tests__/integration/handled-error.test.ts @@ -1,3 +1,4 @@ +import { describe, it, expect } from 'vitest'; import { wrapHandledError, unwrapHandledError diff --git a/client/src/analytics/call-ga.test.ts b/client/src/analytics/call-ga.test.ts deleted file mode 100644 index f9252dfcc02..00000000000 --- a/client/src/analytics/call-ga.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import callGA, { GAevent } from './call-ga'; -import TagManager from '.'; - -jest.mock('.', () => ({ - dataLayer: jest.fn() -})); - -describe('callGA function', () => { - it('calls TagManager dataLayer with the same arguments', () => { - const eventDataMock: GAevent = { - event: 'donation', - action: 'Donate Page Stripe Payment Submission', - duration: 'month', - amount: 500, - completed_challenges: 100, - completed_challenges_session: 10, - isSignedIn: true - }; - callGA(eventDataMock); - expect(TagManager.dataLayer).toHaveBeenCalledWith({ - dataLayer: eventDataMock - }); - }); -}); diff --git a/client/src/client-only-routes/show-settings.test.tsx b/client/src/client-only-routes/show-settings.test.tsx index 0a43c111f53..4925b579521 100644 --- a/client/src/client-only-routes/show-settings.test.tsx +++ b/client/src/client-only-routes/show-settings.test.tsx @@ -2,13 +2,14 @@ // @ts-nocheck Likely need to not use ShallowRenderer import React from 'react'; import ShallowRenderer from 'react-test-renderer/shallow'; +import { describe, it, expect, vi } from 'vitest'; import envData from '../../config/env.json'; import { ShowSettings } from './show-settings'; const { apiLocation } = envData as Record; -jest.mock('../analytics'); +vi.mock('../analytics'); describe('', () => { it('renders to the DOM when user is logged in', () => { @@ -34,24 +35,24 @@ describe('', () => { }); }); -const navigate = jest.fn(); +const navigate = vi.fn(); const loggedInProps = { - createFlashMessage: jest.fn(), - hardGoTo: jest.fn(), + createFlashMessage: vi.fn(), + hardGoTo: vi.fn(), isSignedIn: true, navigate: navigate, showLoading: false, - submitNewAbout: jest.fn(), - toggleTheme: jest.fn(), - updateSocials: jest.fn(), - updateIsHonest: jest.fn(), - updatePortfolio: jest.fn(), - updateQuincyEmail: jest.fn(), + submitNewAbout: vi.fn(), + toggleTheme: vi.fn(), + updateSocials: vi.fn(), + updateIsHonest: vi.fn(), + updatePortfolio: vi.fn(), + updateQuincyEmail: vi.fn(), user: { about: '', completedChallenges: [] }, - verifyCert: jest.fn() + verifyCert: vi.fn() }; const loggedOutProps = { ...loggedInProps }; loggedOutProps.isSignedIn = false; diff --git a/client/src/components/Footer/__snapshots__/footer.test.tsx.snap b/client/src/components/Footer/__snapshots__/footer.test.tsx.snap index f4e1518521a..f31b899e02a 100644 --- a/client/src/components/Footer/__snapshots__/footer.test.tsx.snap +++ b/client/src/components/Footer/__snapshots__/footer.test.tsx.snap @@ -1,6 +1,6 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`