diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 51a8ed4d3a..14b084c811 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -53,7 +53,7 @@ yarn dev ### Unit Testing LWC -When developing LWC, utilize [jest](https://jestjs.io/en/) unit testing to provide test coverage for new functionality. To run the jest tests use the following command from the root directory: +When developing LWC, utilize [vitest](https://vitest.dev/) unit testing to provide test coverage for new functionality. To run the vitest tests use the following command from the root directory: ```bash yarn test @@ -84,11 +84,11 @@ If you want to debug these tests, you can do as follow: 3. Click on "Open dedicated DevTools for Node" 4. In your terminal, type the following command: `yarn test:debug ` -Your test should now be running in the Chrome debugger which you can use to poke around and explore. Now simply hit Enter in the terminal running your Jest process anytime you want to re-run your currently selected specs. You'll be dropped right back into the Chrome debugger. +Your test should now be running in the Chrome debugger which you can use to poke around and explore. Now simply hit Enter in the terminal running your Vitest process anytime you want to re-run your currently selected specs. You'll be dropped right back into the Chrome debugger. ### Debugging Test Fixtures LWC -Test fixtures are file-based tests that are executed using a helper called [`testFixtureDir`](./scripts/jest/utils/test-fixture-dir.ts). Because this helper does not list tests individually, jest's [`test.only`](https://jestjs.io/docs/api#testonlyname-fn-timeout) and [`test.skip`](https://jestjs.io/docs/api#testskipname-fn) cannot be used. Instead, to achieve the same behavior, "directive" files can be added to individual test fixtures. If a file called `.only` is found in a test fixture, that test will use `test.only`. Similarly, if a file called `.skip` is found, `test.skip` will be used. +Test fixtures are file-based tests that are executed using a helper called [`testFixtureDir`](./scripts/test-utils/test-fixture-dir.ts). Because this helper does not list tests individually, vitest's [`test.only`](https://vitest.dev/api/#test-only) and [`test.skip`](https://vitest.dev/api/#test-skip) cannot be used. Instead, to achieve the same behavior, "directive" files can be added to individual test fixtures. If a file called `.only` is found in a test fixture, that test will use `test.only`. Similarly, if a file called `.skip` is found, `test.skip` will be used. ### Integration Testing LWC diff --git a/package.json b/package.json index 52c495044e..d182362f3f 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "release:version": "./scripts/release/version.js" }, "//": { - "prettier": "v3 requires ESM, and we use prettier in our Jest tests. Jest does not support ESM yet." + "prettier": "Outdated since Jest has been replaced with vitest: v3 requires ESM, and we use prettier in our Jest tests. Jest does not support ESM yet." }, "devDependencies": { "@commitlint/cli": "^19.3.0", @@ -47,7 +47,9 @@ "@types/babel__core": "^7.20.5", "@types/node": "^22.1.0", "@vitest/coverage-v8": "^2.0.5", + "@vitest/expect": "^2.0.5", "@vitest/ui": "^2.0.5", + "@vitest/utils": "^2.0.5", "bytes": "^3.1.2", "es-module-lexer": "^1.5.4", "eslint": "^9.8.0", diff --git a/packages/@lwc/engine-core/src/shared/logger.ts b/packages/@lwc/engine-core/src/shared/logger.ts index e06725c63c..40ecb335ff 100644 --- a/packages/@lwc/engine-core/src/shared/logger.ts +++ b/packages/@lwc/engine-core/src/shared/logger.ts @@ -32,7 +32,7 @@ function log(method: 'warn' | 'error', message: string, vm: VM | undefined, once alreadyLoggedMessages.add(msg); } - // In Jest tests, reduce the warning and error verbosity by not printing the callstack + // In Vitest tests, reduce the warning and error verbosity by not printing the callstack if (process.env.NODE_ENV === 'test') { /* eslint-disable-next-line no-console */ console[method](msg); diff --git a/packages/@lwc/engine-server/src/__tests__/render-component.spec.ts b/packages/@lwc/engine-server/src/__tests__/render-component.spec.ts index bc896d38ef..9f004de179 100644 --- a/packages/@lwc/engine-server/src/__tests__/render-component.spec.ts +++ b/packages/@lwc/engine-server/src/__tests__/render-component.spec.ts @@ -4,7 +4,6 @@ * SPDX-License-Identifier: MIT * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ - import { renderComponent, LightningElement } from '../index'; class Test extends LightningElement {} diff --git a/packages/@lwc/errors/src/__tests__/errors.spec.ts b/packages/@lwc/errors/src/__tests__/errors.spec.ts index cdea08482a..5880bd9f52 100644 --- a/packages/@lwc/errors/src/__tests__/errors.spec.ts +++ b/packages/@lwc/errors/src/__tests__/errors.spec.ts @@ -17,19 +17,18 @@ const ERROR_CODE_RANGES = { }, }; -declare global { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace jest { - interface Matchers { - __type: R; // unused, but makes TypeScript happy - toBeUniqueCode: (key: string, seenErrorCodes: Set) => object; - toBeInRange: (min: number, max: number, key: string) => object; - } - } +interface CustomMatchers { + toBeUniqueCode: (key: string, seenErrorCodes: Set) => R; + toBeInRange: (range: { min: number; max: number }, key: string) => R; +} + +declare module 'vitest' { + interface Assertion extends CustomMatchers {} + interface AsymmetricMatchersContaining extends CustomMatchers {} } expect.extend({ - toBeInRange(code, min, max, key) { + toBeInRange(code, { min, max }, key) { const pass = Number.isInteger(code) && code >= min && code <= max; const message = () => `expected ${key}'s error code '${code}'${ @@ -68,11 +67,7 @@ function traverseErrorInfo( describe('error validation', () => { it('compiler error codes are in the correct range', () => { function validate(errorInfo: LWCErrorInfo, key: string) { - expect(errorInfo.code).toBeInRange( - ERROR_CODE_RANGES.compiler.min, - ERROR_CODE_RANGES.compiler.max, - key - ); + expect(errorInfo.code).toBeInRange(ERROR_CODE_RANGES.compiler, key); } traverseErrorInfo(CompilerErrors, validate, 'compiler'); diff --git a/packages/@lwc/errors/src/compiler/__tests__/error-info.spec.ts b/packages/@lwc/errors/src/compiler/__tests__/error-info.spec.ts index e882b02a56..36f2abfd8e 100644 --- a/packages/@lwc/errors/src/compiler/__tests__/error-info.spec.ts +++ b/packages/@lwc/errors/src/compiler/__tests__/error-info.spec.ts @@ -5,7 +5,6 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ import * as errorInfo from '../error-info'; - // All exported objects are maps of label/error info, except for GENERIC_COMPILER_ERROR, // which is a top-level error info object const { GENERIC_COMPILER_ERROR, ...errors } = errorInfo; @@ -14,7 +13,7 @@ const errorInfoMatcher = { code: expect.any(Number), message: expect.any(String), url: expect.any(String), - // Technically not *any* number, but jest doesn't have oneOf + // Technically not *any* number, but vitest doesn't have oneOf level: expect.any(Number), }; diff --git a/packages/@lwc/module-resolver/scripts/test/matchers/to-throw-error-with-code.ts b/packages/@lwc/module-resolver/scripts/test/matchers/to-throw-error-with-code.ts index 67d3ab92d3..5be128f343 100644 --- a/packages/@lwc/module-resolver/scripts/test/matchers/to-throw-error-with-code.ts +++ b/packages/@lwc/module-resolver/scripts/test/matchers/to-throw-error-with-code.ts @@ -5,11 +5,11 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ -import diff from 'jest-diff'; -import MatcherUtils = jest.MatcherUtils; +import diff from '@vitest/utils/diff'; +import type { MatcherState } from '@vitest/expect'; export function toThrowErrorWithCode( - this: MatcherUtils, + this: MatcherState, received: any, code: string, message?: string diff --git a/packages/@lwc/module-resolver/scripts/test/matchers/to-throw-error-with-type.ts b/packages/@lwc/module-resolver/scripts/test/matchers/to-throw-error-with-type.ts index bcf3ff6b7f..97c67ee898 100644 --- a/packages/@lwc/module-resolver/scripts/test/matchers/to-throw-error-with-type.ts +++ b/packages/@lwc/module-resolver/scripts/test/matchers/to-throw-error-with-type.ts @@ -5,11 +5,11 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ -import diff from 'jest-diff'; -import MatcherUtils = jest.MatcherUtils; +import diff from '@vitest/utils/diff'; +import type { MatcherState } from '@vitest/expect'; export function toThrowErrorWithType( - this: MatcherUtils, + this: MatcherState, received: any, ctor: any, message?: string diff --git a/packages/@lwc/module-resolver/scripts/test/types.ts b/packages/@lwc/module-resolver/scripts/test/types.ts index b2e664a775..83d514681d 100644 --- a/packages/@lwc/module-resolver/scripts/test/types.ts +++ b/packages/@lwc/module-resolver/scripts/test/types.ts @@ -4,15 +4,15 @@ * SPDX-License-Identifier: MIT * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ -export {}; // required to have a module with just `declare global` in it +/// +import 'vitest'; -declare global { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace jest { - interface Matchers { - __type: R; // unused, but makes TypeScript happy - toThrowErrorWithCode(received: any, ctor: any, message?: string): CustomMatcherResult; - toThrowErrorWithType(received: any, ctor: any, message?: string): CustomMatcherResult; - } - } +interface CustomMatchers { + toThrowErrorWithCode: (received: any, ctor: any, message?: string) => R; + toThrowErrorWithType: (received: any, ctor: any, message?: string) => R; +} + +declare module 'vitest' { + interface Assertion extends CustomMatchers {} + interface AsymmetricMatchersContaining extends CustomMatchers {} } diff --git a/packages/@lwc/template-compiler/src/__tests__/config.spec.ts b/packages/@lwc/template-compiler/src/__tests__/config.spec.ts index c47929a292..cd41fe0f5f 100644 --- a/packages/@lwc/template-compiler/src/__tests__/config.spec.ts +++ b/packages/@lwc/template-compiler/src/__tests__/config.spec.ts @@ -25,7 +25,7 @@ describe('customRendererConfig normalization', () => { }, }); expect(normalizedConfig.apiVersion).toBe(HIGHEST_API_VERSION); - normalizedConfig.apiVersion = -1; // avoid testing in inline snapshot so that Jest can easily update it + normalizedConfig.apiVersion = -1; // avoid testing in inline snapshot so that vitest can easily update it expect(normalizedConfig).toMatchInlineSnapshot(` { diff --git a/packages/lwc/__tests__/default-exports.spec.ts b/packages/lwc/__tests__/default-exports.spec.ts index 8f912a64ed..7ae79894de 100644 --- a/packages/lwc/__tests__/default-exports.spec.ts +++ b/packages/lwc/__tests__/default-exports.spec.ts @@ -20,6 +20,7 @@ const expectExportDefaultFromPackageInFile = (pkgName: string, ext: string) => { }; /* + * This comment needs to be updated: * Jest uses CommonJS, which means that packages with no explicit export statements actually export * the default `module.exports` empty object. That export is an empty object with the prototype set * to an empty object with null prototype. @@ -59,6 +60,7 @@ describe('default exports are not forgotten', () => { 'dist/index.js' ); const realModule = await import(pathToEsmDistFile); + // The commend below needs to be updated: // When jest properly supports ESM, this will be a lot simpler // const aliasedModule = await import(`lwc/${pkg}`); // expect(aliasedModule.default).toBe(realModule.default); diff --git a/scripts/test-utils/index.ts b/scripts/test-utils/index.ts index 0f30763115..3666f95c1e 100644 --- a/scripts/test-utils/index.ts +++ b/scripts/test-utils/index.ts @@ -4,6 +4,6 @@ * SPDX-License-Identifier: MIT * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT */ -import './matchers'; // File has no exports, but registers jest matchers +import './matchers'; // File has no exports, but registers vitest matchers export * from './test-fixture-dir'; export * from './format-html'; diff --git a/scripts/test-utils/matchers.ts b/scripts/test-utils/matchers.ts index c8acd3aac1..cfad043115 100644 --- a/scripts/test-utils/matchers.ts +++ b/scripts/test-utils/matchers.ts @@ -6,21 +6,16 @@ */ import fs from 'fs'; -import MatcherUtils = jest.MatcherUtils; -import CustomMatcherResult = jest.CustomMatcherResult; +import type { MatcherState } from '@vitest/expect'; /** - * Jest matcher to assert that the content received matches the content in fixture file. + * Vitest matcher to assert that the content received matches the content in fixture file. * @param receivedContent the fixture content * @param filename the fixture absolute path * @returns matcher result * @example expect(content).toMatchFile(outputPath) */ -function toMatchFile( - this: MatcherUtils, - receivedContent: string, - filename: string -): CustomMatcherResult { +function toMatchFile(this: MatcherState, receivedContent: string, filename: string) { const { snapshotState, expand, utils } = this; const fileExists = fs.existsSync(filename); @@ -29,10 +24,10 @@ function toMatchFile( const expectedContent = fs.readFileSync(filename, 'utf-8'); if (receivedContent === null || receivedContent === undefined) { - // If the file exists but the expected content is undefined or null. If the Jest is + // If the file exists but the expected content is undefined or null. If Vitest is // running with the update snapshot flag the file should be deleted. Otherwise fails // the assertion stating that the file is not expected to be there. - if (snapshotState._updateSnapshot === 'all') { + if (snapshotState['_updateSnapshot'] === 'all') { fs.unlinkSync(filename); snapshotState.updated++; @@ -57,10 +52,10 @@ function toMatchFile( // content everything is fine. return { pass: true, message: () => '' }; } else { - // If the expected file is present but the content is not matching. if Jest is running + // If the expected file is present but the content is not matching. if Vitest is running // with the update snapshot flag override the expected content. Otherwise fails the // assertion with a diff. - if (snapshotState._updateSnapshot === 'all') { + if (snapshotState['_updateSnapshot'] === 'all') { fs.writeFileSync(filename, receivedContent); snapshotState.updated++; @@ -93,7 +88,10 @@ function toMatchFile( // If expected file doesn't exists but got a received content and if the snapshots // should be updated, create the new snapshot. Otherwise fails the assertion. - if (snapshotState._updateSnapshot === 'new' || snapshotState._updateSnapshot === 'all') { + if ( + snapshotState['_updateSnapshot'] === 'new' || + snapshotState['_updateSnapshot'] === 'all' + ) { fs.writeFileSync(filename, receivedContent); snapshotState.added++; @@ -113,15 +111,16 @@ function toMatchFile( } } -declare global { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace jest { - interface Matchers { - __type: R; // unused, but makes TypeScript happy - toMatchFile(receivedContent: string, filename?: string): CustomMatcherResult; - } - } +import 'vitest'; + +interface CustomMatchers { + toMatchFile: (receivedContent: string, filename?: string) => R; +} + +declare module 'vitest' { + interface Assertion extends CustomMatchers {} + interface AsymmetricMatchersContaining extends CustomMatchers {} } -// Register jest matcher. +// Register vitest matcher. expect.extend({ toMatchFile }); diff --git a/scripts/test-utils/test-fixture-dir.ts b/scripts/test-utils/test-fixture-dir.ts index 800365f7e3..ab588f1415 100644 --- a/scripts/test-utils/test-fixture-dir.ts +++ b/scripts/test-utils/test-fixture-dir.ts @@ -13,13 +13,13 @@ const { globSync } = glob; type TestFixtureOutput = { [filename: string]: unknown }; /** - * Facilitates the use of jest's `test.only`/`test.skip` in fixture files. + * Facilitates the use of vitest's `test.only`/`test.skip` in fixture files. * @param dirname fixture directory to check for "directive" files * @returns `test.only` if `.only` exists, `test.skip` if `.skip` exists, otherwise `test` * @throws if you have both `.only` and `.skip` in the directory * @example getTestFunc('/fixtures/some-test') */ -function getTestFunc(dirname: string): jest.It { +function getTestFunc(dirname: string) { const isOnly = fs.existsSync(path.join(dirname, '.only')); const isSkip = fs.existsSync(path.join(dirname, '.skip')); if (isOnly && isSkip) { diff --git a/yarn.lock b/yarn.lock index 7b5ed01c39..0503ae46ef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1958,7 +1958,7 @@ test-exclude "^7.0.1" tinyrainbow "^1.2.0" -"@vitest/expect@2.0.5": +"@vitest/expect@2.0.5", "@vitest/expect@^2.0.5": version "2.0.5" resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-2.0.5.tgz#f3745a6a2c18acbea4d39f5935e913f40d26fa86" integrity sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA== @@ -2021,7 +2021,7 @@ sirv "^2.0.4" tinyrainbow "^1.2.0" -"@vitest/utils@2.0.5": +"@vitest/utils@2.0.5", "@vitest/utils@^2.0.5": version "2.0.5" resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-2.0.5.tgz#6f8307a4b6bc6ceb9270007f73c67c915944e926" integrity sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==