refactor: client jest -> vitest (#62177)

This commit is contained in:
Oliver Eyton-Williams
2025-09-16 08:30:06 +02:00
committed by GitHub
parent c7354cff89
commit 881dfd8f78
86 changed files with 1100 additions and 964 deletions

View File

@@ -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<undefined | boolean>) =>
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
};

View File

@@ -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 }) =>
<Component t={() => ''} {...props} />
); */
// reactI18next.translate = translate;
reactI18next.withTranslation = withTranslation;
reactI18next.useTranslation = useTranslation;
reactI18next.Trans = Trans;
module.exports = reactI18next;
module.exports = { withTranslation, useTranslation, Trans };

View File

@@ -0,0 +1,4 @@
import React from 'react';
// eslint-disable-next-line react/display-name
export default () => <div>Spinner</div>;

View File

@@ -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
);
});
});
}
});

View File

@@ -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"
}
}

View File

@@ -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;

View File

@@ -1 +0,0 @@
export default {};

View File

@@ -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<undefined | boolean>) =>
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()
};

View File

@@ -1 +0,0 @@
export default {};

View File

@@ -1,3 +1,4 @@
import { describe, it, expect } from 'vitest';
import {
wrapHandledError,
unwrapHandledError

View File

@@ -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
});
});
});

View File

@@ -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<string, string>;
jest.mock('../analytics');
vi.mock('../analytics');
describe('<ShowSettings />', () => {
it('renders to the DOM when user is logged in', () => {
@@ -34,24 +35,24 @@ describe('<ShowSettings />', () => {
});
});
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;

View File

@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<Footer /> matches snapshot 1`] = `
exports[`<Footer /> > matches snapshot 1`] = `
<footer
className="site-footer"
>
@@ -342,7 +342,7 @@ exports[`<Footer /> matches snapshot 1`] = `
<img
alt="Download on the App Store"
lang="en"
src={{}}
src="/src/assets/images/footer-ads/apple-store-badge.svg"
/>
</a>
</li>
@@ -355,7 +355,7 @@ exports[`<Footer /> matches snapshot 1`] = `
<img
alt="Get it on Google Play"
lang="en"
src={{}}
src="/src/assets/images/footer-ads/google-play-badge.svg"
/>
</a>
</li>

View File

@@ -1,5 +1,6 @@
import React from 'react';
import renderer from 'react-test-renderer';
import { describe, expect, it } from 'vitest';
import Footer from '.';

View File

@@ -1,5 +1,6 @@
import React from 'react';
import { create, ReactTestRendererJSON } from 'react-test-renderer';
import { describe, expect, it, vi } from 'vitest';
import AuthOrProfile from './components/auth-or-profile';
const defaultUserProps = {
@@ -37,7 +38,7 @@ const topDonatingContributorUserProps = {
}
};
jest.mock('../../analytics');
vi.mock('../../analytics');
describe('<AuthOrProfile />', () => {
it('has avatar with default border for default users', () => {

View File

@@ -1,11 +1,13 @@
import React from 'react';
import { Provider } from 'react-redux';
import { render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { createStore } from '../../redux/create-store';
import Intro from '.';
jest.mock('../../analytics');
vi.mock('../../analytics');
vi.mock('../../utils/get-words');
function renderWithRedux(
ui: JSX.Element,
@@ -45,12 +47,12 @@ const loggedInProps = {
completedChallengeCount: 0,
isSignedIn: true,
name: 'Development User',
navigate: () => jest.fn(),
navigate: () => vi.fn(),
pending: false,
slug: '/',
username: 'DevelopmentUser',
isDonating: false,
onLearnDonationAlertClick: () => jest.fn()
onLearnDonationAlertClick: () => vi.fn()
};
const loggedOutProps = {
@@ -58,10 +60,10 @@ const loggedOutProps = {
completedChallengeCount: 0,
isSignedIn: false,
name: '',
navigate: () => jest.fn(),
navigate: () => vi.fn(),
pending: false,
slug: '/',
username: '',
isDonating: false,
onLearnDonationAlertClick: () => jest.fn()
onLearnDonationAlertClick: () => vi.fn()
};

View File

@@ -1,16 +1,17 @@
import React from 'react';
import { render, act, screen } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import OfflineWarning from './offline-warning';
jest.useFakeTimers();
vi.useFakeTimers();
describe('<OfflineWarning />', () => {
it('renders null when isOnline, isServerOnline and isSignedIn are true', () => {
const { container } = render(
<OfflineWarning isOnline={true} isServerOnline={true} isSignedIn={true} />
);
act(() => jest.runAllTimers());
act(() => void vi.runAllTimers());
expect(container).toBeEmptyDOMElement();
});
@@ -22,7 +23,7 @@ describe('<OfflineWarning />', () => {
isSignedIn={true}
/>
);
act(() => jest.runAllTimers());
act(() => void vi.runAllTimers());
expect(screen.getByText('misc.offline')).toBeInTheDocument();
});
@@ -34,7 +35,7 @@ describe('<OfflineWarning />', () => {
isSignedIn={true}
/>
);
act(() => jest.runAllTimers());
act(() => void vi.runAllTimers());
expect(screen.getByText('placeholder').tagName).toBe('A');
expect(screen.getByText('placeholder')).toHaveAttribute(
'href',
@@ -50,7 +51,7 @@ describe('<OfflineWarning />', () => {
isSignedIn={false}
/>
);
act(() => jest.runAllTimers());
act(() => void vi.runAllTimers());
expect(container).toBeEmptyDOMElement();
});
@@ -62,7 +63,7 @@ describe('<OfflineWarning />', () => {
isSignedIn={true}
/>
);
act(() => jest.runAllTimers());
act(() => void vi.runAllTimers());
expect(screen.getByText('misc.offline')).toBeInTheDocument();
});
});

View File

@@ -2,13 +2,15 @@ import { render, waitFor } from '@testing-library/react';
import React from 'react';
import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux';
import { describe, vi, test, expect } from 'vitest';
import { i18nextCodes } from '../../../shared/config/i18n';
import i18nTestConfig from '../../i18n/config-for-tests';
import { createStore } from '../redux/create-store';
import AppMountNotifier from './app-mount-notifier';
jest.unmock('react-i18next');
vi.unmock('react-i18next');
vi.mock('../utils/get-words');
type Language = keyof typeof i18nextCodes;
type LanguagePair = [string, string];

View File

@@ -1,3 +1,5 @@
// @vitest-environment jsdom
import { beforeEach, describe, expect, it } from 'vitest';
import createLanguageRedirect from './create-language-redirect';
describe('createLanguageRedirect for clientLocale === english', () => {
@@ -17,8 +19,6 @@ describe('createLanguageRedirect for clientLocale === english', () => {
const dothrakiPageURL =
'https://www.freecodecamp.org/dothraki/learn/responsive-web-design/basic-html-and-html5/inform-with-the-paragraph-element';
const originalLocation = window.location;
beforeEach(() => {
Object.defineProperty(window, 'location', {
writable: true,
@@ -26,10 +26,6 @@ describe('createLanguageRedirect for clientLocale === english', () => {
});
});
afterEach(() => {
window.location = originalLocation;
});
[
{ lang: 'english', url: currentPageURL },
{ lang: 'chinese', url: chinesePageURL },
@@ -60,8 +56,6 @@ describe('createLanguageRedirect for clientLocale === english', () => {
'https://www.freecodecamp.org/chinese-traditional/settings';
const dothrakiPageURL = 'https://www.freecodecamp.org/dothraki/settings';
const originalLocation = window.location;
beforeEach(() => {
Object.defineProperty(window, 'location', {
writable: true,
@@ -69,10 +63,6 @@ describe('createLanguageRedirect for clientLocale === english', () => {
});
});
afterEach(() => {
window.location = originalLocation;
});
[
{ lang: 'english', url: currentPageURL },
{ lang: 'chinese', url: chinesePageURL },
@@ -113,8 +103,6 @@ describe('createLanguageRedirect for clientLocale === chinese', () => {
const dothrakiPageURL =
'https://www.freecodecamp.org/dothraki/learn/responsive-web-design/basic-html-and-html5/inform-with-the-paragraph-element';
const originalLocation = window.location;
beforeEach(() => {
Object.defineProperty(window, 'location', {
writable: true,
@@ -122,10 +110,6 @@ describe('createLanguageRedirect for clientLocale === chinese', () => {
});
});
afterEach(() => {
window.location = originalLocation;
});
[
{ lang: 'chinese', url: currentPageURL },
{ lang: 'english', url: englishPageURL },
@@ -156,8 +140,6 @@ describe('createLanguageRedirect for clientLocale === chinese', () => {
'https://www.freecodecamp.org/chinese-traditional/settings';
const dothrakiPageURL = 'https://www.freecodecamp.org/dothraki/settings';
const originalLocation = window.location;
beforeEach(() => {
Object.defineProperty(window, 'location', {
writable: true,
@@ -165,10 +147,6 @@ describe('createLanguageRedirect for clientLocale === chinese', () => {
});
});
afterEach(() => {
window.location = originalLocation;
});
[
{ lang: 'chinese', url: currentPageURL },
{ lang: 'english', url: englishPageURL },

View File

@@ -1,3 +1,4 @@
import { describe, test, expect } from 'vitest';
import {
editorValidator,
fCCValidator,
@@ -6,146 +7,148 @@ import {
pathValidator
} from './form-validators';
test('editorValidator', () => {
const editorAddresses = [
'repl.it/@username',
'repl.it/join/username',
'replit.com/@username',
'replit.com/join/username',
'glitch.com/edit/#!',
'codesandbox.io/s/',
'https://github.com',
'https://glitch.com/edit/#!/project',
'https://glitch.com/edit/#!'
];
describe('Form validators', () => {
test('editorValidator', () => {
const editorAddresses = [
'repl.it/@username',
'repl.it/join/username',
'replit.com/@username',
'replit.com/join/username',
'glitch.com/edit/#!',
'codesandbox.io/s/',
'https://github.com',
'https://glitch.com/edit/#!/project',
'https://glitch.com/edit/#!'
];
const nonEditorAddresses = [
'example.com',
'http://example.com',
'https://example.com',
'http://repl.it',
'http://glitch.com',
'https://repl.it/home'
];
const nonEditorAddresses = [
'example.com',
'http://example.com',
'https://example.com',
'http://repl.it',
'http://glitch.com',
'https://repl.it/home'
];
editorAddresses.forEach(address => {
expect(editorValidator(address)).not.toBeNull();
editorAddresses.forEach(address => {
expect(editorValidator(address)).not.toBeNull();
});
nonEditorAddresses.forEach(address => {
expect(editorValidator(address)).toBeNull();
});
});
nonEditorAddresses.forEach(address => {
expect(editorValidator(address)).toBeNull();
});
});
test('fCCValidator', () => {
const fCCAddresses = [
'codepen.io/freecodecamp',
'freecodecamp.rocks',
'github.com/freecodecamp',
'.freecodecamp.org',
'https://codepen.io/freecodecamp',
'https://freecodecamp.rocks',
'https://github.com/freeCodeCamp',
'https://www.freecodecamp.org'
];
const nonFCCAddresses = [
'example.com',
'http://example.com',
'https://example.com',
'http://codepen.io',
'https://github.com',
'https://codepen.io/editor',
'https://github.com/repo'
];
fCCAddresses.forEach(address => {
expect(fCCValidator(address)).not.toBeNull();
});
nonFCCAddresses.forEach(address => {
expect(fCCValidator(address)).toBeNull();
});
});
test('localhostValidator', () => {
const privateAddresses = [
'http://localhost:3000',
'https://localhost:3000',
'http://127.0.0.1',
'http://127.0.0.1:3000',
'https://127.0.0.1',
'https://127.0.0.1:3000',
'http://[::1]:3000'
];
const publicAddresses = [
'http://localhost.com',
'http://example.com',
'http://example.com:3000',
'https://example.com',
'https://example.com:3000'
];
privateAddresses.forEach(address => {
expect(localhostValidator(address)).not.toBeNull();
});
publicAddresses.forEach(address => {
expect(localhostValidator(address)).toBeNull();
});
});
test('httpValidator', () => {
const allowedHttpAddresses = ['http://[::1]:3000', 'http://localhost:3000'];
const disallowedHttpAddresses = [
'http://example.com',
'http://localhost.com'
];
const nonHttpAddresses = [
'ftp://example.com',
'file://localhost',
'ws://example.com',
'wss://localhost',
'https://example.com/test',
'https://localhost.com'
];
allowedHttpAddresses.forEach(address => {
expect(httpValidator(address)).toBeNull();
});
disallowedHttpAddresses.forEach(address => {
expect(httpValidator(address)).not.toBeNull();
});
nonHttpAddresses.forEach(address => {
expect(httpValidator(address)).toBeNull();
});
});
test('pathValidator', () => {
const rootAddresses = [
'http://example.com/',
'http://example.com',
'https://example.com/',
'https://localhost.com'
];
const nonRootAddresses = [
'http://example.com/path',
'http://example.com/path/',
'https://example.com/path',
'https://example.com/path/'
];
rootAddresses.forEach(address => {
expect(pathValidator(address)).toBeNull();
});
nonRootAddresses.forEach(address => {
expect(pathValidator(address)).not.toBeNull();
test('fCCValidator', () => {
const fCCAddresses = [
'codepen.io/freecodecamp',
'freecodecamp.rocks',
'github.com/freecodecamp',
'.freecodecamp.org',
'https://codepen.io/freecodecamp',
'https://freecodecamp.rocks',
'https://github.com/freeCodeCamp',
'https://www.freecodecamp.org'
];
const nonFCCAddresses = [
'example.com',
'http://example.com',
'https://example.com',
'http://codepen.io',
'https://github.com',
'https://codepen.io/editor',
'https://github.com/repo'
];
fCCAddresses.forEach(address => {
expect(fCCValidator(address)).not.toBeNull();
});
nonFCCAddresses.forEach(address => {
expect(fCCValidator(address)).toBeNull();
});
});
test('localhostValidator', () => {
const privateAddresses = [
'http://localhost:3000',
'https://localhost:3000',
'http://127.0.0.1',
'http://127.0.0.1:3000',
'https://127.0.0.1',
'https://127.0.0.1:3000',
'http://[::1]:3000'
];
const publicAddresses = [
'http://localhost.com',
'http://example.com',
'http://example.com:3000',
'https://example.com',
'https://example.com:3000'
];
privateAddresses.forEach(address => {
expect(localhostValidator(address)).not.toBeNull();
});
publicAddresses.forEach(address => {
expect(localhostValidator(address)).toBeNull();
});
});
test('httpValidator', () => {
const allowedHttpAddresses = ['http://[::1]:3000', 'http://localhost:3000'];
const disallowedHttpAddresses = [
'http://example.com',
'http://localhost.com'
];
const nonHttpAddresses = [
'ftp://example.com',
'file://localhost',
'ws://example.com',
'wss://localhost',
'https://example.com/test',
'https://localhost.com'
];
allowedHttpAddresses.forEach(address => {
expect(httpValidator(address)).toBeNull();
});
disallowedHttpAddresses.forEach(address => {
expect(httpValidator(address)).not.toBeNull();
});
nonHttpAddresses.forEach(address => {
expect(httpValidator(address)).toBeNull();
});
});
test('pathValidator', () => {
const rootAddresses = [
'http://example.com/',
'http://example.com',
'https://example.com/',
'https://localhost.com'
];
const nonRootAddresses = [
'http://example.com/path',
'http://example.com/path/',
'https://example.com/path',
'https://example.com/path/'
];
rootAddresses.forEach(address => {
expect(pathValidator(address)).toBeNull();
});
nonRootAddresses.forEach(address => {
expect(pathValidator(address)).not.toBeNull();
});
});
});

View File

@@ -1,3 +1,4 @@
import { describe, test, expect, vi } from 'vitest';
import { render, fireEvent, screen } from '@testing-library/react';
import React from 'react';
@@ -20,69 +21,71 @@ const defaultTestProps: StrictSolutionFormProps = {
submit: () => undefined
};
test('should render', () => {
render(<StrictSolutionForm {...defaultTestProps} />);
describe('<StrictSolutionForm />', () => {
test('should render', () => {
render(<StrictSolutionForm {...defaultTestProps} />);
const websiteInput = screen.getByLabelText(/WebSite label/);
expect(websiteInput).toBeRequired();
expect(websiteInput).toHaveAttribute('type', 'url');
const websiteInput = screen.getByLabelText(/WebSite label/);
expect(websiteInput).toBeRequired();
expect(websiteInput).toHaveAttribute('type', 'url');
const button = screen.getByText(/submit/i);
expect(button).toHaveAttribute('type', 'submit');
expect(button).toHaveAttribute('aria-disabled', 'true');
});
test('should render with default values', () => {
const websiteValue = 'http://mysite.com';
const nameValue = 'John';
render(
<StrictSolutionForm
{...defaultTestProps}
enableSubmit={true}
initialValues={{ name: nameValue, website: websiteValue }}
/>
);
const nameInput = screen.getByLabelText(/name Label/);
expect(nameInput).toHaveValue(nameValue);
const websiteInput = screen.getByLabelText(/WebSite label/);
expect(websiteInput).toHaveValue(websiteValue);
const button = screen.getByText(/submit/i);
expect(button).toBeEnabled();
});
test('should submit', () => {
const submit = jest.fn();
const props = {
...defaultTestProps,
submit
};
const websiteValue = 'http://mysite.com';
render(<StrictSolutionForm {...props} />);
const websiteInput = screen.getByLabelText(/WebSite label/);
fireEvent.change(websiteInput, { target: { value: websiteValue } });
expect(websiteInput).toHaveValue(websiteValue);
const button = screen.getByText(/submit/i);
expect(button).toBeEnabled();
fireEvent.click(button);
expect(submit).toHaveBeenCalledTimes(1);
expect((submit.mock.calls[0] as unknown[])[0]).toEqual(
expect.objectContaining({ values: { website: websiteValue } })
);
fireEvent.change(websiteInput, { target: { value: `${websiteValue}///` } });
expect(websiteInput).toHaveValue(`${websiteValue}///`);
fireEvent.click(button);
expect(submit).toHaveBeenCalledTimes(2);
expect((submit.mock.calls[1] as unknown[])[0]).toEqual(
expect.objectContaining({ values: { website: websiteValue } })
);
const button = screen.getByText(/submit/i);
expect(button).toHaveAttribute('type', 'submit');
expect(button).toHaveAttribute('aria-disabled', 'true');
});
test('should render with default values', () => {
const websiteValue = 'http://mysite.com';
const nameValue = 'John';
render(
<StrictSolutionForm
{...defaultTestProps}
enableSubmit={true}
initialValues={{ name: nameValue, website: websiteValue }}
/>
);
const nameInput = screen.getByLabelText(/name Label/);
expect(nameInput).toHaveValue(nameValue);
const websiteInput = screen.getByLabelText(/WebSite label/);
expect(websiteInput).toHaveValue(websiteValue);
const button = screen.getByText(/submit/i);
expect(button).toBeEnabled();
});
test('should submit', () => {
const submit = vi.fn();
const props = {
...defaultTestProps,
submit
};
const websiteValue = 'http://mysite.com';
render(<StrictSolutionForm {...props} />);
const websiteInput = screen.getByLabelText(/WebSite label/);
fireEvent.change(websiteInput, { target: { value: websiteValue } });
expect(websiteInput).toHaveValue(websiteValue);
const button = screen.getByText(/submit/i);
expect(button).toBeEnabled();
fireEvent.click(button);
expect(submit).toHaveBeenCalledTimes(1);
expect((submit.mock.calls[0] as unknown[])[0]).toEqual(
expect.objectContaining({ values: { website: websiteValue } })
);
fireEvent.change(websiteInput, { target: { value: `${websiteValue}///` } });
expect(websiteInput).toHaveValue(`${websiteValue}///`);
fireEvent.click(button);
expect(submit).toHaveBeenCalledTimes(2);
expect((submit.mock.calls[1] as unknown[])[0]).toEqual(
expect.objectContaining({ values: { website: websiteValue } })
);
});
});

View File

@@ -1,35 +1,23 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<Loader /> matches the fullScreen render snapshot 1`] = `
exports[`<Loader /> > matches the fullScreen render snapshot 1`] = `
<div
class="fcc-loader full-screen-wrapper"
data-testid="fcc-loader"
>
<div
class="sk-fade-in sk-spinner line-scale-pulse-out"
>
<div />
<div />
<div />
<div />
<div />
<div>
Spinner
</div>
</div>
`;
exports[`<Loader /> matches to the default render snapshot 1`] = `
exports[`<Loader /> > matches to the default render snapshot 1`] = `
<div
class="fcc-loader "
data-testid="fcc-loader"
>
<div
class="sk-fade-in sk-spinner line-scale-pulse-out"
>
<div />
<div />
<div />
<div />
<div />
<div>
Spinner
</div>
</div>
`;

View File

@@ -1,3 +1,4 @@
import { describe, it, expect } from 'vitest';
import borderColorPicker from './border-color-picker';
describe('Border color picker helper', () => {

View File

@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<BlockSaveButton /> snapshot 1`] = `
exports[`<BlockSaveButton /> > <BlockSaveButton /> snapshot 1`] = `
<div>
<button
class=" relative inline-block mt-[0.5px] border-solid border-3 active:before:w-full active:before:h-full active:before:absolute active:before:inset-0 active:before:border-3 active:before:border-transparent active:before:bg-gray-900 active:before:opacity-20 text-center cursor-pointer no-underline block w-full bg-background-quaternary text-foreground-secondary border-foreground-secondary hover:bg-foreground-primary hover:text-background-primary hover:border-foreground-secondary dark:hover:bg-background-primary dark:hover:text-foreground-primary px-3 py-1.5 text-md"

View File

@@ -1,23 +1,26 @@
import { describe, test, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import React from 'react';
import BlockSaveButton from './block-save-button';
test('<BlockSaveButton /> snapshot', () => {
const { container } = render(<BlockSaveButton />);
describe('<BlockSaveButton />', () => {
test('<BlockSaveButton /> snapshot', () => {
const { container } = render(<BlockSaveButton />);
expect(container).toMatchSnapshot();
});
test('Button text should default to the correct translation key', () => {
render(<BlockSaveButton />);
expect(screen.getByRole('button')).toHaveTextContent('buttons.save');
});
test('Button text should match "children"', () => {
const testText = 'My Text Here';
render(<BlockSaveButton>{testText}</BlockSaveButton>);
expect(screen.getByRole('button')).toHaveTextContent(testText);
expect(container).toMatchSnapshot();
});
test('Button text should default to the correct translation key', () => {
render(<BlockSaveButton />);
expect(screen.getByRole('button')).toHaveTextContent('buttons.save');
});
test('Button text should match "children"', () => {
const testText = 'My Text Here';
render(<BlockSaveButton>{testText}</BlockSaveButton>);
expect(screen.getByRole('button')).toHaveTextContent(testText);
});
});

View File

@@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { describe, it, expect } from 'vitest';
import React from 'react';
import { create } from 'react-test-renderer';

View File

@@ -1,3 +1,4 @@
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import React from 'react';

View File

@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<Profile/> renders correctly 1`] = `
exports[`<Profile/> > renders correctly 1`] = `
<div>
<div
class="h-[30px]"

View File

@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`should check certification page consistency 1`] = `
exports[`Timeline buttons test > should check certification page consistency 1`] = `
<div
className="mx-[-15px] "
>

View File

@@ -1,3 +1,12 @@
import {
describe,
it,
expect,
beforeEach,
afterEach,
vi,
MockInstance
} from 'vitest';
import { render, screen } from '@testing-library/react';
import React from 'react';
@@ -24,11 +33,10 @@ props.calendar[date1] = 1;
props.calendar[date2] = 1;
props.calendar[date3] = 1;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let dateNowMockFn: jest.MockInstance<any, unknown[]>;
let dateNowMockFn: MockInstance;
beforeEach(() => {
dateNowMockFn = jest.spyOn(Date, 'now').mockImplementation(() => now);
dateNowMockFn = vi.spyOn(Date, 'now').mockImplementation(() => now);
});
afterEach(() => {

View File

@@ -1,3 +1,4 @@
import { describe, it, test, expect, beforeEach, vi } from 'vitest';
import { render, screen, within } from '@testing-library/react';
import React from 'react';
import Stats, { calculateStreaks } from './stats';
@@ -69,10 +70,10 @@ const multipleEntriesInOneDay = {
'1736946300': 1 // 2025-01-15 13:05:00 UTC
};
jest.useFakeTimers();
vi.useFakeTimers();
describe('calculateStreaks', () => {
beforeEach(() => jest.setSystemTime(new Date(2025, 0, 15)));
beforeEach(() => vi.setSystemTime(new Date(2025, 0, 15)));
test('Should return 0 for the current streak if the user has not made progress today', () => {
const { longestStreak, currentStreak } =
calculateStreaks(oldStreakCalendar);
@@ -82,7 +83,7 @@ describe('calculateStreaks', () => {
});
test('Should calculate longest streak, regardless of how long ago they were', () => {
jest.setSystemTime(new Date(2030, 0, 15));
vi.setSystemTime(new Date(2030, 0, 15));
const { longestStreak, currentStreak } =
calculateStreaks(oldStreakCalendar);
@@ -91,7 +92,7 @@ describe('calculateStreaks', () => {
});
test('Should return a longest streak of 3 days when the current streak is 3 days', () => {
jest.setSystemTime(new Date(2025, 0, 14));
vi.setSystemTime(new Date(2025, 0, 14));
const { longestStreak, currentStreak } =
calculateStreaks(recentStreakCalendar);

View File

@@ -2,6 +2,9 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
import { useStaticQuery } from 'gatsby';
import React from 'react';
import { beforeEach, describe, it, expect, vi } from 'vitest';
vi.mock('../../../utils/get-words');
import { render, screen } from '../../../../utils/test-utils';
import { createStore } from '../../../redux/create-store';

View File

@@ -1,50 +0,0 @@
import React from 'react';
import renderer from 'react-test-renderer';
import { Provider } from 'react-redux';
import { createStore } from '../../../redux/create-store';
import completedChallenges from '../../../__mocks__/completed-challenges.json';
import Timeline from './time-line';
Date.prototype.toLocaleString = jest.fn(() => 'Dec 29, 2022');
Date.prototype.toISOString = jest.fn(() => '2016-09-28T20:31:56.730Z');
jest.mock('../../../analytics');
jest.mock('gatsby', () => {
const edges = require('../../../__mocks__/edges.json');
const React = require('react');
const gatsby = jest.requireActual('gatsby');
return {
...gatsby,
useStaticQuery: jest.fn().mockReturnValue({
allChallengeNode: {
edges: edges
}
}),
graphql: jest.fn(),
Link: jest.fn().mockImplementation(({ to, ...rest }) =>
React.createElement('a', {
...rest,
href: to,
gatsby: 'true'
})
)
};
});
it('should check certification page consistency', () => {
const tree = renderer
.create(
<Provider store={createStore()}>
<Timeline
completedMap={completedChallenges}
username='CeritifedUser'
onPress={() => {}}
/>
</Provider>
)
.toJSON();
expect(tree).toMatchSnapshot();
});

View File

@@ -0,0 +1,55 @@
import React from 'react';
import renderer from 'react-test-renderer';
import { Provider } from 'react-redux';
import { describe, it, expect, vi } from 'vitest';
import { createStore } from '../../../redux/create-store';
import completedChallenges from './__fixtures__/completed-challenges.json';
import Timeline from './time-line';
Date.prototype.toLocaleString = vi.fn(() => 'Dec 29, 2022');
Date.prototype.toISOString = vi.fn(() => '2016-09-28T20:31:56.730Z');
vi.mock('../../../analytics');
vi.mock('../../../utils/get-words');
vi.mock('gatsby', async () => {
const edges = require('./__fixtures__/edges.json');
const React = require('react');
const gatsby = await vi.importActual('gatsby');
return {
...gatsby,
useStaticQuery: vi.fn().mockReturnValue({
allChallengeNode: {
edges: edges
}
}),
graphql: vi.fn(),
Link: vi.fn().mockImplementation(({ to, ...rest }) =>
React.createElement('a', {
...rest,
href: to,
gatsby: 'true'
})
)
};
});
describe('Timeline buttons test', () => {
it('should check certification page consistency', () => {
const tree = renderer
.create(
<Provider store={createStore()}>
<Timeline
completedMap={completedChallenges}
username='CeritifedUser'
onPress={() => {}}
/>
</Provider>
)
.toJSON();
expect(tree).toMatchSnapshot();
});
});

View File

@@ -1,3 +1,4 @@
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import React from 'react';
import { Provider } from 'react-redux';
@@ -5,7 +6,7 @@ import { createStore } from 'redux';
import { UserThemes } from '../../redux/types';
import Profile from './profile';
jest.mock('../../analytics');
vi.mock('../../analytics');
//workaround to avoid some strange gatsby error:
window.___loader = { enqueue: () => {}, hovering: () => {} };

View File

@@ -1,5 +1,6 @@
import React from 'react';
import ShallowRenderer from 'react-test-renderer/shallow';
import { describe, it, expect, vi } from 'vitest';
import { SearchBar } from './search-bar';
@@ -19,7 +20,7 @@ describe('<SearchBar />', () => {
});
const searchBarProps = {
toggleSearchDropdown: jest.fn(),
toggleSearchFocused: jest.fn(),
updateSearchQuery: jest.fn()
toggleSearchDropdown: vi.fn(),
toggleSearchFocused: vi.fn(),
updateSearchQuery: vi.fn()
};

View File

@@ -1,21 +1,24 @@
import React from 'react';
import * as Gatsby from 'gatsby';
import { render } from '@testing-library/react';
import Helmet from 'react-helmet';
import { describe, it, expect, vi, afterEach } from 'vitest';
import SEO from './index';
const useStaticQuery = jest.spyOn(Gatsby, `useStaticQuery`);
const mockUseStaticQuery = {
site: {
siteMetadata: {
title: 'freeCodeCamp',
siteUrl: 'freeCodeCamp.org'
title: 'freeCodeCamp'
}
}
};
jest.mock('react-i18next', () => ({
vi.mock('gatsby', () => ({
useStaticQuery: vi.fn(() => mockUseStaticQuery),
graphql: vi.fn()
}));
vi.mock('react-i18next', () => ({
useTranslation: () => {
return {
t: (str: string) => ({
@@ -27,12 +30,8 @@ jest.mock('react-i18next', () => ({
}));
describe('<SEO />', () => {
beforeEach(() => {
useStaticQuery.mockImplementation(() => mockUseStaticQuery);
});
afterEach(() => {
jest.restoreAllMocks();
vi.restoreAllMocks();
});
it('renders', () => {

View File

@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<Honesty /> <Honesty /> snapshot when isHonest is false: Honesty 1`] = `
exports[`<Honesty /> > <Honesty /> snapshot when isHonest is false > Honesty 1`] = `
<DocumentFragment>
<section
id="honesty-policy"
@@ -67,7 +67,7 @@ exports[`<Honesty /> <Honesty /> snapshot when isHonest is false: Honesty 1`] =
</DocumentFragment>
`;
exports[`<Honesty /> <Honesty /> snapshot when isHonest is true: HonestyAccepted 1`] = `
exports[`<Honesty /> > <Honesty /> snapshot when isHonest is true > HonestyAccepted 1`] = `
<DocumentFragment>
<section
id="honesty-policy"

View File

@@ -1,6 +1,10 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import { Provider } from 'react-redux';
import { describe, it, expect, vi } from 'vitest';
vi.mock('../../utils/get-words');
import { createStore } from '../../redux/create-store';
import { Ext } from '../../redux/prop-types';
import { verifyCert } from '../../redux/settings/actions';
@@ -8,7 +12,7 @@ import { createFlashMessage } from '../Flash/redux';
import CertificationSettings from './certification';
jest.mock('../../analytics');
vi.mock('../../analytics');
function renderWithRedux(ui: JSX.Element) {
return render(<Provider store={createStore()}>{ui}</Provider>);

View File

@@ -1,9 +1,10 @@
import React from 'react';
import { render, fireEvent, screen } from '@testing-library/react';
import { describe, test, expect, vi } from 'vitest';
import Honesty from './honesty';
describe('<Honesty />', () => {
const updateIsHonestMock = jest.fn();
const updateIsHonestMock = vi.fn();
test('<Honesty /> snapshot when isHonest is false', () => {
const { asFragment } = render(

View File

@@ -1,5 +1,6 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { describe, test, expect } from 'vitest';
import { ShareTemplate } from './share-template';
const redirectURL = 'string';

View File

@@ -1,4 +1,6 @@
import { renderHook } from '@testing-library/react-hooks';
import { useTranslation } from 'react-i18next';
import { describe, test, expect } from 'vitest';
import {
hastag,
nextLine,
@@ -9,29 +11,37 @@ import {
threadsData
} from './use-share';
test('useShare testing', () => {
const superBlock = 'testSuperBlock';
const block = 'testBlock';
const { t } = useTranslation();
describe('useShare', () => {
test('useShare hook returns correct social media URLs', () => {
const superBlock = 'testSuperBlock';
const block = 'testBlock';
const redirectURL = useShare({
superBlock: superBlock,
block: block
const { result: translationResult } = renderHook(() => useTranslation());
const { t } = translationResult.current;
// Test useShare hook
const { result: shareResult } = renderHook(() =>
useShare({
superBlock,
block
})
);
const freecodecampLearnDomain = 'www.freecodecamp.org/learn';
const i18nSupportedBlock = t(`intro:${superBlock}.blocks.${block}.title`);
const tweetMessage = `I${space}have${space}completed${space}${i18nSupportedBlock}${space}${hastag}freecodecamp`;
const redirectFreeCodeCampLearnURL = `https://${freecodecampLearnDomain}/${superBlock}/${hastag}${block}`;
expect(shareResult.current.xUrl).toBe(
`https://${twitterData.domain}/${twitterData.action}?original_referer=${twitterData.developerDomainURL}&text=${tweetMessage}${nextLine}&url=${redirectFreeCodeCampLearnURL}`
);
expect(shareResult.current.blueSkyUrl).toBe(
`https://${blueSkyData.domain}/${blueSkyData.action}?original_referer=${blueSkyData.developerDomainURL}&text=${tweetMessage}${nextLine}&url=${redirectFreeCodeCampLearnURL}`
);
expect(shareResult.current.threadsURL).toBe(
`https://${threadsData.domain}/${threadsData.action}?original_referer=${threadsData.developerDomainURL}&text=${tweetMessage}${nextLine}&url=${redirectFreeCodeCampLearnURL}`
);
});
const freecodecampLearnDomain = 'www.freecodecamp.org/learn';
const i18nSupportedBlock = t(`intro:${superBlock}.blocks.${block}.title`);
const tweetMessage = `I${space}have${space}completed${space}${i18nSupportedBlock}${space}%23freecodecamp`;
const redirectFreeCodeCampLearnURL = `https://${freecodecampLearnDomain}/${superBlock}/${hastag}${block}`;
expect(redirectURL.xUrl).toBe(
`https://${twitterData.domain}/${twitterData.action}?original_referer=${twitterData.developerDomainURL}&text=${tweetMessage}${nextLine}&url=${redirectFreeCodeCampLearnURL}`
);
expect(redirectURL.blueSkyUrl).toBe(
`https://${blueSkyData.domain}/${blueSkyData.action}?original_referer=${blueSkyData.developerDomainURL}&text=${tweetMessage}${nextLine}&url=${redirectFreeCodeCampLearnURL}`
);
expect(redirectURL.threadsURL).toBe(
`https://${threadsData.domain}/${threadsData.action}?original_referer=${threadsData.developerDomainURL}&text=${tweetMessage}${nextLine}&url=${redirectFreeCodeCampLearnURL}`
);
});

View File

@@ -1,7 +1,9 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import store from 'store';
import { describe, it, expect, beforeAll, afterEach, vi } from 'vitest';
import StagingWarningModal from '.';
describe('StagingWarningModal', () => {
beforeAll(() => {
// The Modal component uses `ResizeObserver` under the hood.
@@ -9,16 +11,16 @@ describe('StagingWarningModal', () => {
// Ref: https://github.com/jsdom/jsdom/issues/3368
Object.defineProperty(window, 'ResizeObserver', {
writable: true,
value: jest.fn(() => ({
observe: jest.fn(),
unobserve: jest.fn(),
disconnect: jest.fn()
value: vi.fn(() => ({
observe: vi.fn(),
unobserve: vi.fn(),
disconnect: vi.fn()
}))
});
});
afterEach(() => {
jest.clearAllMocks();
vi.clearAllMocks();
});
it('renders the modal successfully', () => {

View File

@@ -6,6 +6,7 @@ import {
import { render } from '@testing-library/react';
import { navigate, withPrefix } from 'gatsby';
import React from 'react';
import { describe, it, expect } from 'vitest';
import Challenges from './challenges';

View File

@@ -1,4 +1,6 @@
// @vitest-environment jsdom
import { expectSaga } from 'redux-saga-test-plan';
import { describe, it, vi } from 'vitest';
import {
postChargeStripe,
postChargeStripeCard,
@@ -18,11 +20,11 @@ import {
updateCardError
} from './actions';
jest.mock('../utils/ajax');
jest.mock('../analytics/call-ga');
jest.mock('../utils/stripe', () => ({
vi.mock('../utils/ajax');
vi.mock('../analytics/call-ga');
vi.mock('../utils/stripe', () => ({
stripe: Promise.resolve({
redirectToCheckout: jest.fn()
redirectToCheckout: vi.fn()
})
}));
@@ -32,7 +34,7 @@ const postChargeDataMock = {
paymentContext: 'donate page',
amount: '500',
duration: 'month',
handleAuthentication: jest.fn(),
handleAuthentication: vi.fn(),
paymentMethodId: '123456'
}
};

View File

@@ -1,10 +1,11 @@
import { ActionsObservable, StateObservable } from 'redux-observable';
import { Subject } from 'rxjs';
import store from 'store';
import { describe, it, expect, vi } from 'vitest';
import { actionTypes } from './action-types';
import failedUpdatesEpic from './failed-updates-epic';
jest.mock('../analytics');
vi.mock('../analytics');
const key = 'fcc-failed-updates';

View File

@@ -1,3 +1,4 @@
import { describe, it, expect } from 'vitest';
import type {
ChallengeFile,
SavedChallengeFile

View File

@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<ChallengeTitle/> renders correctly 1`] = `
exports[`<ChallengeTitle/> > renders correctly 1`] = `
<div
className="challenge-title-wrap"
>

View File

@@ -1,5 +1,6 @@
import React from 'react';
import renderer from 'react-test-renderer';
import { describe, it, expect } from 'vitest';
import ChallengeTitle from './challenge-title';

View File

@@ -1,6 +1,7 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import store from 'store';
import { describe, it, expect, beforeEach } from 'vitest';
import ChallengeTranscript from './challenge-transcript';
const baseProps = {

View File

@@ -1,5 +1,6 @@
import React from 'react';
import { runSaga } from 'redux-saga';
import { describe, test, it, expect, beforeEach, vi, Mock } from 'vitest';
import { render } from '../../../../utils/test-utils';
import { getCompletedPercentage } from '../../../utils/get-completion-percentage';
@@ -15,21 +16,21 @@ import {
} from '../redux/selectors';
import { buildChallenge, getTestRunner } from '../utils/build';
import CompletionModal, { combineFileData } from './completion-modal';
jest.mock('../../../analytics');
jest.mock('../../../utils/fire-confetti');
jest.mock('../../../components/Progress');
jest.mock('../redux/selectors');
jest.mock('../utils/build');
const mockFireConfetti = fireConfetti as jest.Mock;
const mockTestRunner = jest.fn().mockReturnValue({ pass: true });
const mockBuildEnabledSelector = isBuildEnabledSelector as jest.Mock;
const mockChallengeTestsSelector = challengeTestsSelector as jest.Mock;
const mockChallengeMetaSelector = challengeMetaSelector as jest.Mock;
const mockChallengeDataSelector = challengeDataSelector as jest.Mock;
const mockIsBlockNewlyCompletedSelector =
isBlockNewlyCompletedSelector as jest.Mock;
const mockBuildChallenge = buildChallenge as jest.Mock;
const mockGetTestRunner = getTestRunner as jest.Mock;
vi.mock('../../../analytics');
vi.mock('../../../utils/fire-confetti');
vi.mock('../../../components/Progress');
vi.mock('../redux/selectors');
vi.mock('../utils/build');
vi.mock('../../../utils/get-words');
const mockFireConfetti = fireConfetti as Mock;
const mockTestRunner = vi.fn().mockReturnValue({ pass: true });
const mockBuildEnabledSelector = isBuildEnabledSelector as Mock;
const mockChallengeTestsSelector = challengeTestsSelector as Mock;
const mockChallengeMetaSelector = challengeMetaSelector as Mock;
const mockChallengeDataSelector = challengeDataSelector as Mock;
const mockIsBlockNewlyCompletedSelector = isBlockNewlyCompletedSelector as Mock;
const mockBuildChallenge = buildChallenge as Mock;
const mockGetTestRunner = getTestRunner as Mock;
mockBuildEnabledSelector.mockReturnValue(true);
mockChallengeTestsSelector.mockReturnValue([
{ text: 'Test 1', testString: 'mock test code' }

View File

@@ -1,6 +1,9 @@
import { describe, it, expect, vi } from 'vitest';
import i18n from '../../../../i18n/config-for-tests';
import { generateSearchLink } from './help-modal';
vi.unmock('react-i18next');
describe('generateSearchLink', () => {
it("should return a link with search query containing block name and challenge title if the title includes 'step'", async () => {
await i18n.reloadResources('en', 'intro');

View File

@@ -1,3 +1,4 @@
import { describe, it, expect } from 'vitest';
import { parseBlanks } from './parse-blanks';
describe('parseBlanks', () => {

View File

@@ -1,4 +1,5 @@
import { TestScheduler } from 'rxjs/testing';
import { describe, it, expect } from 'vitest';
import completionEpic from './completion-epic';
import { submitChallenge, submitChallengeComplete } from './actions';

View File

@@ -1,3 +1,4 @@
import { describe, it, expect } from 'vitest';
import { transformEditorLink } from '../utils';
import { insertEditableRegions } from './create-question-epic';

View File

@@ -1,9 +1,13 @@
import { expectSaga } from 'redux-saga-test-plan';
import { describe, it, vi } from 'vitest';
jest.mock('redux-saga/effects', () => ({
...jest.requireActual('redux-saga/effects'),
delay: jest.fn()
}));
vi.mock('redux-saga/effects', async importOriginal => {
const actual = await importOriginal();
return {
...actual,
delay: vi.fn()
};
});
const initialState = {
challenge: { isBuildEnabled: true, isExecuting: false, challengeMeta: {} }

View File

@@ -1,3 +1,4 @@
import { describe, it, expect } from 'vitest';
import envData from '../../../../config/env.json';
import { getGuideUrl } from './index';

View File

@@ -1,11 +1,12 @@
/**
* @jest-environment node
* @vitest-environment node
*/
import { it, expect, afterEach, vi } from 'vitest';
import { WorkerExecutor } from './worker-executor';
function mockWorker({ init, postMessage, terminate } = {}) {
global.Worker = jest.fn(function () {
global.Worker = vi.fn(function () {
setImmediate(
(init && init(this)) ||
(() =>
@@ -29,7 +30,7 @@ afterEach(() => {
});
it('Worker executor should successfully execute one task', async () => {
const terminateHandler = jest.fn();
const terminateHandler = vi.fn();
mockWorker({ terminate: terminateHandler });
const testWorker = new WorkerExecutor('test');
expect(testWorker).not.toBeUndefined();
@@ -37,9 +38,9 @@ it('Worker executor should successfully execute one task', async () => {
const task = testWorker.execute('test');
expect(task).not.toBeUndefined();
expect(task.done).not.toBeUndefined();
const handler = jest.fn();
const handler = vi.fn();
task.on('done', handler);
const errorHandler = jest.fn();
const errorHandler = vi.fn();
task.on('error', errorHandler);
await expect(task.done).resolves.toBe('test processed');
@@ -55,20 +56,20 @@ it('Worker executor should successfully execute one task', async () => {
});
it('Worker executor should successfully execute two tasks in parallel', async () => {
const terminateHandler = jest.fn();
const terminateHandler = vi.fn();
mockWorker({ terminate: terminateHandler });
const testWorker = new WorkerExecutor('test');
const task1 = testWorker.execute('test1');
const handler1 = jest.fn();
const handler1 = vi.fn();
task1.on('done', handler1);
const errorHandler1 = jest.fn();
const errorHandler1 = vi.fn();
task1.on('error', errorHandler1);
const task2 = testWorker.execute('test2');
const handler2 = jest.fn();
const handler2 = vi.fn();
task2.on('done', handler2);
const errorHandler2 = jest.fn();
const errorHandler2 = vi.fn();
task2.on('error', errorHandler2);
await expect(Promise.all([task1.done, task2.done])).resolves.toEqual([
@@ -103,7 +104,7 @@ it('Worker executor should successfully execute 3 tasks in parallel and use two
});
it('Worker executor should successfully execute 3 tasks, use 3 workers and terminate each worker', async () => {
const terminateHandler = jest.fn();
const terminateHandler = vi.fn();
mockWorker({ terminate: terminateHandler });
const testWorker = new WorkerExecutor('test', { terminateWorker: true });
@@ -156,7 +157,7 @@ it('Worker executor should reject task', async () => {
const testWorker = new WorkerExecutor('test');
const task = testWorker.execute('test');
const errorHandler = jest.fn();
const errorHandler = vi.fn();
task.on('error', errorHandler);
await expect(task.done).rejects.toBe(error.message);
@@ -184,11 +185,11 @@ it('Worker executor should emit LOG events', async () => {
const testWorker = new WorkerExecutor('test');
const task = testWorker.execute('test');
const handler = jest.fn();
const handler = vi.fn();
task.on('done', handler);
const errorHandler = jest.fn();
const errorHandler = vi.fn();
task.on('error', errorHandler);
const logHandler = jest.fn();
const logHandler = vi.fn();
task.on('LOG', logHandler);
await expect(task.done).resolves.toBe('test processed');
@@ -204,7 +205,7 @@ it('Worker executor should emit LOG events', async () => {
});
it('Worker executor should reject task on timeout', async () => {
const terminateHandler = jest.fn();
const terminateHandler = vi.fn();
mockWorker({
postMessage: () => {},
terminate: terminateHandler
@@ -212,7 +213,7 @@ it('Worker executor should reject task on timeout', async () => {
const testWorker = new WorkerExecutor('test');
const task = testWorker.execute('test', 0);
const errorHandler = jest.fn();
const errorHandler = vi.fn();
task.on('error', errorHandler);
await expect(task.done).rejects.toBe('timeout');
@@ -239,7 +240,7 @@ it('Task should only emit handler once', () => {
mockWorker();
const testWorker = new WorkerExecutor('test');
const task = testWorker.execute('test');
const handler = jest.fn();
const handler = vi.fn();
task.once('testOnce', handler);
task.emit('testOnce', handler);

View File

@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<BlockIntros /> matches snapshot 1`] = `
exports[`<BlockIntros /> > matches snapshot 1`] = `
<div
className="block-description"
>

View File

@@ -1,5 +1,6 @@
import React from 'react';
import renderer from 'react-test-renderer';
import { describe, it, expect } from 'vitest';
import BlockIntros from './block-intros';
const arrString = ['first paragraph', 'second paragraph'];

View File

@@ -1,8 +1,8 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { Provider } from 'react-redux';
import { describe, it, expect, afterEach, vi, Mock } from 'vitest';
import type { TFunction } from 'i18next';
import { SuperBlocks } from '../../../../../shared/config/curriculum';
import { createStore } from '../../../redux/create-store';
import {
ChallengeFiles,
PrerequisiteChallenge,
@@ -14,18 +14,13 @@ import {
} from '../../../redux/prop-types';
import { isAuditedSuperBlock } from '../../../../../shared/utils/is-audited';
import { BlockLayouts, BlockTypes } from '../../../../../shared/config/blocks';
import Block from './block';
import { Block } from './block';
jest.mock('../../../../../shared/utils/is-audited', () => ({
isAuditedSuperBlock: jest.fn().mockReturnValueOnce(true)
vi.mock('../../../../../shared/utils/is-audited', () => ({
isAuditedSuperBlock: vi.fn().mockReturnValueOnce(true)
}));
jest.mock('../redux', () => ({
makeExpandedBlockSelector: jest.fn(() => jest.fn(() => true)),
completedChallengesSelector: jest.fn(() => [
{ id: 'mockId', title: 'mockTitle' }
])
}));
vi.mock('../../../utils/get-words');
const defaultProps = {
block: 'test-block',
@@ -91,46 +86,34 @@ const defaultProps = {
],
completedChallengeIds: ['testchallengeIds'],
isExpanded: true,
t: jest.fn((key: string) => [key]),
t: vi.fn((key: string) => [key]) as unknown as TFunction,
superBlock: SuperBlocks.FullStackDeveloper,
toggleBlock: jest.fn()
toggleBlock: vi.fn()
};
describe('<Block />', () => {
afterEach(() => {
jest.clearAllMocks();
vi.clearAllMocks();
});
it('The "Help us translate" badge does not appear on any English blocks', () => {
render(
<Provider store={createStore()}>
<Block {...defaultProps} />
</Provider>
);
render(<Block {...defaultProps} />);
expect(
screen.queryByText(/misc.translation-pending/)
).not.toBeInTheDocument();
});
it(`The "Help us translate" badge does not appear on any i18n blocks when the superblock is audited`, () => {
(isAuditedSuperBlock as jest.Mock).mockReturnValue(true);
render(
<Provider store={createStore()}>
<Block {...defaultProps} />
</Provider>
);
(isAuditedSuperBlock as Mock).mockReturnValue(true);
render(<Block {...defaultProps} />);
expect(
screen.queryByText(/misc.translation-pending/)
).not.toBeInTheDocument();
});
it(`The "Help us translate" badge does appear on i18n blocks when the superblock is not audited`, () => {
(isAuditedSuperBlock as jest.Mock).mockReturnValue(false);
render(
<Provider store={createStore()}>
<Block {...defaultProps} />
</Provider>
);
(isAuditedSuperBlock as Mock).mockReturnValue(false);
render(<Block {...defaultProps} />);
expect(screen.getByText(/misc.translation-pending/)).toBeInTheDocument();
});
});

View File

@@ -69,7 +69,7 @@ interface BlockProps {
toggleBlock: typeof toggleBlock;
}
class Block extends Component<BlockProps> {
export class Block extends Component<BlockProps> {
static displayName: string;
constructor(props: BlockProps) {
super(props);

View File

@@ -0,0 +1,7 @@
import { vi } from 'vitest';
export const randomQuote = vi.fn(() => ({
quote: 'Test quote',
author: 'Test author'
}));
export const randomCompliment = vi.fn(() => 'Great job!');

View File

@@ -1,3 +1,4 @@
import { describe, it, expect } from 'vitest';
import { stringifyDonationEvents } from './analytics-strings';
describe('Analytics donation strings', () => {

View File

@@ -1,3 +1,4 @@
import { describe, it, expect } from 'vitest';
import { createTypes, createAsyncTypes } from './create-types';
describe('Create types utility (createTypes)', () => {

View File

@@ -1,3 +1,4 @@
import { describe, it, expect } from 'vitest';
import { format } from './format';
function simpleFun() {

View File

@@ -1,30 +1,8 @@
// @vitest-environment jsdom
import { describe, it, expect } from 'vitest';
import { getUUID } from './growthbook-cookie';
describe('getUUID', () => {
let originalCookie: string;
beforeEach(() => {
global.crypto = {
...global.crypto,
randomUUID: () => '123e4567-e89b-12d3-a456-426614174000'
};
// Save original cookie
originalCookie = document.cookie;
// Clear the cookie before each test
document.cookie.split(';').forEach(c => {
document.cookie = c
.replace(/^ +/, '')
.replace(/=.*/, '=;expires=' + new Date().toUTCString() + ';path=/');
});
});
afterEach(() => {
// Restore original cookie
document.cookie = originalCookie;
});
it('should generate a new UUID if none exists', () => {
const uuid = getUUID();
expect(uuid).toBeDefined();

View File

@@ -1,3 +1,4 @@
import { describe, it, expect } from 'vitest';
import { isObject } from 'lodash-es';
import {

View File

@@ -1,3 +1,4 @@
import { describe, it, expect } from 'vitest';
import { isContained } from './is-contained';
describe('client/src isContained', () => {

View File

@@ -1,3 +1,4 @@
import { describe, it, expect } from 'vitest';
import { isLanding } from './path-parsers';
const pathnames = {

View File

@@ -1,3 +1,4 @@
import { describe, it, expect } from 'vitest';
import { replaceAppleQuotes } from './replace-apple-quotes';
describe('replaceAppleQuotes()', () => {

View File

@@ -1,4 +1,6 @@
// @vitest-environment jsdom
/* eslint-disable @typescript-eslint/unbound-method */
import { describe, test, expect, beforeEach, afterEach, vi } from 'vitest';
import {
CURRENT_COUNT_KEY,
SAVED_COUNT_KEY,
@@ -17,24 +19,24 @@ describe('Session Storage', () => {
const mockStorage: Storage = {
length: 0,
clear: jest.fn(() => {
clear: vi.fn(() => {
store = {};
}),
getItem: jest.fn((key: string) => {
getItem: vi.fn((key: string) => {
return store[key] || null;
}),
key: jest.fn((index: number) => {
key: vi.fn((index: number) => {
const keys = Object.keys(store);
return keys[index] || null;
}),
removeItem: jest.fn((key: string) => {
removeItem: vi.fn((key: string) => {
delete store[key];
}),
setItem: jest.fn((key: string, value: string) => {
setItem: vi.fn((key: string, value: string) => {
store[key] = value;
})
};
@@ -54,16 +56,16 @@ describe('Session Storage', () => {
afterEach(() => {
sessionStorage.clear();
jest.clearAllMocks();
vi.clearAllMocks();
});
describe('getSessionChallengeData', () => {
describe('countSinceSave', () => {
it('is not included if nothing has been saved', () => {
test('is not included if nothing has been saved', () => {
expect(getSessionChallengeData()).not.toHaveProperty('countSinceSave');
});
it('is included if the count has been saved', () => {
test('is included if the count has been saved', () => {
sessionStorage.setItem(SAVED_COUNT_KEY, '7');
sessionStorage.setItem(CURRENT_COUNT_KEY, '10');
expect(getSessionChallengeData()).toMatchObject({
@@ -73,13 +75,13 @@ describe('Session Storage', () => {
});
describe('currentCount', () => {
it('defaults to 0 if no challenges have been completed', () => {
test('defaults to 0 if no challenges have been completed', () => {
expect(getSessionChallengeData()).toMatchObject({
currentCount: 0
});
});
it('returns the stored number if it exists', () => {
test('returns the stored number if test exists', () => {
sessionStorage.setItem(CURRENT_COUNT_KEY, '5');
expect(getSessionChallengeData()).toMatchObject({
currentCount: 5
@@ -88,13 +90,13 @@ describe('Session Storage', () => {
});
describe('isSaved', () => {
it('is false if we haved saved the count', () => {
test('is false if we haved saved the count', () => {
expect(getSessionChallengeData()).toMatchObject({
isSaved: false
});
});
it('is true if we have saved something', () => {
test('is true if we have saved something', () => {
sessionStorage.setItem(SAVED_COUNT_KEY, '7');
expect(getSessionChallengeData()).toMatchObject({
isSaved: true

View File

@@ -1,3 +1,4 @@
import { describe, it, expect } from 'vitest';
import {
bothLinks,
invalidGithubLink,

View File

@@ -1,4 +1,5 @@
import { withPrefix } from 'gatsby';
import { describe, it, expect } from 'vitest';
import toLearnPath from './to-learn-path';

View File

@@ -1,3 +1,5 @@
import { describe, test, expect } from 'vitest';
import { clientLocale } from '../config/env.json';
import {
convertToLocalizedString,

View File

@@ -19,6 +19,7 @@
"i18n/**/*",
"plugins/**/*",
"src/**/*",
"__mocks__/**/*",
"utils/**/*",
"tools/**/*",
"config/**/*"

View File

@@ -2,6 +2,7 @@
import React from 'react';
import { Provider } from 'react-redux';
import ShallowRenderer from 'react-test-renderer/shallow';
import { describe, test, expect, vi } from 'vitest';
import FourOhFourPage from '../../src/pages/404';
import Certification from '../../src/pages/certification';
@@ -9,7 +10,9 @@ import Learn from '../../src/pages/learn';
import { createStore } from '../../src/redux/create-store';
import layoutSelector from './layout-selector';
jest.mock('../../src/analytics');
vi.mock('../../src/analytics');
vi.mock('../../src/utils/get-words');
const store = createStore();
@@ -60,55 +63,60 @@ const challengePageContext = {
}
};
test('Challenges should have DefaultLayout and no footer', () => {
const challengePath =
'/learn/responsive-web-design/basic-html-and-html5/say-hello-to-html-elements';
const componentObj = getComponentNameAndProps(
Learn,
challengePath,
challengePageContext
);
expect(componentObj.name).toEqual('DefaultLayout');
expect(componentObj.props.showFooter).toEqual(false);
});
describe('Layout selector', () => {
test('Challenges should have DefaultLayout and no footer', () => {
const challengePath =
'/learn/responsive-web-design/basic-html-and-html5/say-hello-to-html-elements';
const componentObj = getComponentNameAndProps(
Learn,
challengePath,
challengePageContext
);
expect(componentObj.name).toEqual('DefaultLayout');
expect(componentObj.props.showFooter).toEqual(false);
});
test('SuperBlock path should have DefaultLayout and footer', () => {
const superBlockPath = '/learn/responsive-web-design/';
const componentObj = getComponentNameAndProps(Learn, superBlockPath);
expect(componentObj.name).toEqual('DefaultLayout');
expect(componentObj.props.showFooter).toEqual(true);
});
test('SuperBlock path should have DefaultLayout and footer', () => {
const superBlockPath = '/learn/responsive-web-design/';
const componentObj = getComponentNameAndProps(Learn, superBlockPath);
expect(componentObj.name).toEqual('DefaultLayout');
expect(componentObj.props.showFooter).toEqual(true);
});
test('i18n challenge path should have DefaultLayout and no footer', () => {
const challengePath =
'espanol/learn/responsive-web-design/basic-html-and-html5/say-hello-to-html-elements/';
const componentObj = getComponentNameAndProps(
Learn,
challengePath,
challengePageContext
);
expect(componentObj.name).toEqual('DefaultLayout');
expect(componentObj.props.showFooter).toEqual(false);
});
test('i18n challenge path should have DefaultLayout and no footer', () => {
const challengePath =
'espanol/learn/responsive-web-design/basic-html-and-html5/say-hello-to-html-elements/';
const componentObj = getComponentNameAndProps(
Learn,
challengePath,
challengePageContext
);
expect(componentObj.name).toEqual('DefaultLayout');
expect(componentObj.props.showFooter).toEqual(false);
});
test('i18n superBlock path should have DefaultLayout and footer', () => {
const superBlockPath = '/learn/responsive-web-design/';
const componentObj = getComponentNameAndProps(Learn, superBlockPath);
expect(componentObj.name).toEqual('DefaultLayout');
expect(componentObj.props.showFooter).toEqual(true);
});
test('i18n superBlock path should have DefaultLayout and footer', () => {
const superBlockPath = '/learn/responsive-web-design/';
const componentObj = getComponentNameAndProps(Learn, superBlockPath);
expect(componentObj.name).toEqual('DefaultLayout');
expect(componentObj.props.showFooter).toEqual(true);
});
test('404 page should have DefaultLayout and footer', () => {
const challengePath =
'/espanol/learn/responsive-web-design/basic-html-and-html5/say-hello-to-html-elements/';
const componentObj = getComponentNameAndProps(FourOhFourPage, challengePath);
expect(componentObj.name).toEqual('DefaultLayout');
expect(componentObj.props.showFooter).toEqual(true);
});
test('404 page should have DefaultLayout and footer', () => {
const challengePath =
'/espanol/learn/responsive-web-design/basic-html-and-html5/say-hello-to-html-elements/';
const componentObj = getComponentNameAndProps(
FourOhFourPage,
challengePath
);
expect(componentObj.name).toEqual('DefaultLayout');
expect(componentObj.props.showFooter).toEqual(true);
});
test('Certification path should have CertificationLayout', () => {
const challengePath =
'/certification/mot01/javascript-algorithms-and-data-structures/';
const componentObj = getComponentNameAndProps(Certification, challengePath);
expect(componentObj.name).toEqual('CertificationLayout');
test('Certification path should have CertificationLayout', () => {
const challengePath =
'/certification/mot01/javascript-algorithms-and-data-structures/';
const componentObj = getComponentNameAndProps(Certification, challengePath);
expect(componentObj.name).toEqual('CertificationLayout');
});
});

View File

@@ -1,3 +1,4 @@
import { describe, it, expect } from 'vitest';
import { challengeFiles } from './__fixtures__/challenges';
import { sortChallengeFiles } from './sort-challengefiles';

13
client/vitest-setup.js Normal file
View File

@@ -0,0 +1,13 @@
import { vi, afterEach } from 'vitest';
import '@testing-library/jest-dom/vitest';
import { cleanup } from '@testing-library/react';
vi.mock('react-spinkit');
vi.mock('gatsby');
vi.mock('react-i18next');
afterEach(() => {
// Vitest doesn't automatically call cleanup. Without it the rendered
// components are never removed and so tests are not independent.
cleanup();
});

27
client/vitest.config.js Normal file
View File

@@ -0,0 +1,27 @@
/// <reference types="vitest/config" />
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
setupFiles: 'vitest-setup.js',
exclude: 'node_modules',
projects: [
{
extends: true,
test: {
include: '**/*.test.{jsx,tsx}',
name: 'react',
environment: 'jsdom'
}
},
{
extends: true,
test: {
include: '**/*.test.{js,ts}',
name: 'js',
environment: 'node'
}
}
]
}
});

View File

@@ -6,15 +6,10 @@ module.exports = {
'tools/challenge-helper-scripts/',
'tools/challenge-parser/',
'tools/scripts/build/',
'curriculum'
'curriculum',
'client'
],
moduleNameMapper: {
'\\.(jpg|jpeg|png|svg|woff|woff2)$':
'<rootDir>/client/src/__mocks__/file-mock.ts',
// Plain CSS - match css files that don't end with
// '.module.css' https://regex101.com/r/VzwrKH/4
'^(?!.*\\.module\\.css$).*\\.css$':
'<rootDir>/client/src/__mocks__/style-mock.ts',
// CSS Modules - match files that end with 'module.css'
'\\.module\\.css$': 'identity-obj-proxy',
'^lodash-es$': 'lodash'

View File

@@ -78,7 +78,7 @@
"test:curriculum:content": "cd ./curriculum && pnpm test",
"test:curriculum:tooling": "cd ./curriculum && pnpm vitest run",
"test-curriculum-full-output": "cd ./curriculum && pnpm run test:full-output",
"test-client": "jest client",
"test:client": "cd ./client && pnpm test run",
"test-config": "jest config",
"test-tools": "jest tools",
"test-utils": "jest utils",

523
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,3 +13,9 @@ packages:
- 'tools/scripts/build'
- 'tools/scripts/seed'
- 'tools/scripts/seed-exams'
packageExtensions:
'@testing-library/jest-dom':
peerDependencies:
jest: '*'
vitest: '*'