From 755640d947d7080258671cf70ab72cbef199884e Mon Sep 17 00:00:00 2001 From: Tom Mrazauskas Date: Wed, 16 Feb 2022 18:42:02 +0200 Subject: [PATCH] feat: create new `@jest/expect` package (#12404) --- CHANGELOG.md | 1 + packages/expect/__typetests__/expect.test.ts | 7 +- packages/expect/src/asymmetricMatchers.ts | 10 +- packages/expect/src/index.ts | 9 +- packages/expect/src/jestMatchersObject.ts | 12 +-- packages/expect/src/types.ts | 92 +++++-------------- packages/jest-circus/package.json | 2 +- .../jestAdapterInit.ts | 36 ++++---- packages/jest-circus/src/types.ts | 2 - packages/jest-circus/tsconfig.json | 6 +- packages/jest-expect/.npmignore | 5 + packages/jest-expect/README.md | 5 + .../__typetests__/jest-expect.test.ts | 31 +++++++ .../jest-expect/__typetests__/tsconfig.json | 9 ++ packages/jest-expect/package.json | 33 +++++++ .../src/index.ts} | 15 +-- packages/jest-expect/src/types.ts | 57 ++++++++++++ packages/jest-expect/tsconfig.json | 10 ++ packages/jest-globals/package.json | 4 +- packages/jest-globals/src/index.ts | 4 +- packages/jest-globals/tsconfig.json | 2 +- packages/jest-jasmine2/package.json | 2 +- packages/jest-jasmine2/src/jestExpect.ts | 39 +++----- .../jest-jasmine2/src/setup_jest_globals.ts | 14 +-- packages/jest-jasmine2/src/types.ts | 6 +- packages/jest-jasmine2/tsconfig.json | 8 +- packages/jest-snapshot/src/index.ts | 1 + packages/jest-snapshot/src/types.ts | 40 ++++++++ yarn.lock | 17 +++- 29 files changed, 308 insertions(+), 171 deletions(-) create mode 100644 packages/jest-expect/.npmignore create mode 100644 packages/jest-expect/README.md create mode 100644 packages/jest-expect/__typetests__/jest-expect.test.ts create mode 100644 packages/jest-expect/__typetests__/tsconfig.json create mode 100644 packages/jest-expect/package.json rename packages/{jest-circus/src/legacy-code-todo-rewrite/jestExpect.ts => jest-expect/src/index.ts} (70%) create mode 100644 packages/jest-expect/src/types.ts create mode 100644 packages/jest-expect/tsconfig.json diff --git a/CHANGELOG.md b/CHANGELOG.md index ce1923b04bec..74ecb66b586f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - `[jest-environment-jsdom]` [**BREAKING**] Upgrade jsdom to 19.0.0 ([#12290](https://github.com/facebook/jest/pull/12290)) - `[jest-environment-jsdom]` [**BREAKING**] Add default `browser` condition to `exportConditions` for `jsdom` environment ([#11924](https://github.com/facebook/jest/pull/11924)) - `[jest-environment-node]` [**BREAKING**] Add default `node` and `node-addon` conditions to `exportConditions` for `node` environment ([#11924](https://github.com/facebook/jest/pull/11924)) +- `[@jest/expect]` [**BREAKING**] New module which extends `expect` with `jest-snapshot` matchers ([#12404](https://github.com/facebook/jest/pull/12404)) - `[@jest/expect-utils]` New module exporting utils for `expect` ([#12323](https://github.com/facebook/jest/pull/12323)) - `[jest-resolver]` [**BREAKING**] Add support for `package.json` `exports` ([11961](https://github.com/facebook/jest/pull/11961)) - `[jest-resolve, jest-runtime]` Add support for `data:` URI import and mock ([#12392](https://github.com/facebook/jest/pull/12392)) diff --git a/packages/expect/__typetests__/expect.test.ts b/packages/expect/__typetests__/expect.test.ts index 6f5d73bcced0..36e94f6272e5 100644 --- a/packages/expect/__typetests__/expect.test.ts +++ b/packages/expect/__typetests__/expect.test.ts @@ -16,8 +16,7 @@ import { } from 'expect'; import type * as jestMatcherUtils from 'jest-matcher-utils'; -type M = Matchers; -type N = Matchers; +type M = Matchers; expectError(() => { type E = Matchers; @@ -211,3 +210,7 @@ const customStateAndExpected: MatcherFunctionWithState< }; expectAssignable(customStateAndExpected); + +expectError(() => { + expect({}).toMatchSnapshot(); +}); diff --git a/packages/expect/src/asymmetricMatchers.ts b/packages/expect/src/asymmetricMatchers.ts index fc8e043be269..874925b9fb97 100644 --- a/packages/expect/src/asymmetricMatchers.ts +++ b/packages/expect/src/asymmetricMatchers.ts @@ -62,22 +62,20 @@ export function hasProperty(obj: object | null, property: string): boolean { return hasProperty(getPrototype(obj), property); } -export abstract class AsymmetricMatcher< - T, - State extends MatcherState = MatcherState, -> implements AsymmetricMatcherInterface +export abstract class AsymmetricMatcher + implements AsymmetricMatcherInterface { $$typeof = Symbol.for('jest.asymmetricMatcher'); constructor(protected sample: T, protected inverse = false) {} - protected getMatcherContext(): State { + protected getMatcherContext(): MatcherState { return { ...getState(), equals, isNot: this.inverse, utils, - } as State; + }; } abstract asymmetricMatch(other: unknown): boolean; diff --git a/packages/expect/src/index.ts b/packages/expect/src/index.ts index 855339906134..c459e0a5ad99 100644 --- a/packages/expect/src/index.ts +++ b/packages/expect/src/index.ts @@ -51,6 +51,7 @@ import type { export type { AsymmetricMatchers, + BaseExpect, Expect, MatcherFunction, MatcherFunctionWithState, @@ -79,7 +80,7 @@ const createToThrowErrorMatchingSnapshotMatcher = function ( }; }; -const getPromiseMatcher = (name: string, matcher: any) => { +const getPromiseMatcher = (name: string, matcher: RawMatcherFn) => { if (name === 'toThrow' || name === 'toThrowError') { return createThrowMatcher(name, true); } else if ( @@ -363,9 +364,8 @@ const makeThrowingMatcher = ( } }; -expect.extend = ( - matchers: MatchersObject, -) => setMatchers(matchers, false, expect); +expect.extend = (matchers: MatchersObject) => + setMatchers(matchers, false, expect); expect.anything = anything; expect.any = any; @@ -431,7 +431,6 @@ setMatchers(matchers, true, expect); setMatchers(spyMatchers, true, expect); setMatchers(toThrowMatchers, true, expect); -expect.addSnapshotSerializer = () => void 0; expect.assertions = assertions; expect.hasAssertions = hasAssertions; expect.getState = getState; diff --git a/packages/expect/src/jestMatchersObject.ts b/packages/expect/src/jestMatchersObject.ts index 544530bbe398..758909b40909 100644 --- a/packages/expect/src/jestMatchersObject.ts +++ b/packages/expect/src/jestMatchersObject.ts @@ -46,12 +46,11 @@ export const setState = ( Object.assign((global as any)[JEST_MATCHERS_OBJECT].state, state); }; -export const getMatchers = < - State extends MatcherState = MatcherState, ->(): MatchersObject => (global as any)[JEST_MATCHERS_OBJECT].matchers; +export const getMatchers = (): MatchersObject => + (global as any)[JEST_MATCHERS_OBJECT].matchers; -export const setMatchers = ( - matchers: MatchersObject, +export const setMatchers = ( + matchers: MatchersObject, isInternal: boolean, expect: Expect, ): void => { @@ -65,8 +64,7 @@ export const setMatchers = ( // expect is defined class CustomMatcher extends AsymmetricMatcher< - [unknown, ...Array], - State + [unknown, ...Array] > { constructor(inverse = false, ...sample: [unknown, ...Array]) { super(sample, inverse); diff --git a/packages/expect/src/types.ts b/packages/expect/src/types.ts index 486b3a8a4f07..2b3e8c6ac613 100644 --- a/packages/expect/src/types.ts +++ b/packages/expect/src/types.ts @@ -34,8 +34,8 @@ export type RawMatcherFn = { [INTERNAL_MATCHER_FLAG]?: boolean; }; -export type MatchersObject = { - [name: string]: RawMatcherFn; +export type MatchersObject = { + [name: string]: RawMatcherFn; }; export type ThrowingMatcherFn = (actual: any) => void; @@ -62,12 +62,12 @@ export interface MatcherState { }; } -export interface AsymmetricMatcher { +export type AsymmetricMatcher = { asymmetricMatch(other: unknown): boolean; toString(): string; getExpectedType?(): string; toAsymmetricMatcher?(): string; -} +}; export type ExpectedAssertionsErrors = Array<{ actual: string | number; @@ -75,27 +75,28 @@ export type ExpectedAssertionsErrors = Array<{ expected: string; }>; -export type Expect = { - (actual: T): Matchers & - InverseMatchers & - PromiseMatchers; - // TODO: this is added by test runners, not `expect` itself - addSnapshotSerializer(serializer: unknown): void; +export interface BaseExpect { assertions(numberOfAssertions: number): void; - // TODO: remove this `T extends` - should get from some interface merging - extend(matchers: MatchersObject): void; + extend(matchers: MatchersObject): void; extractExpectedAssertionsErrors(): ExpectedAssertionsErrors; - getState(): State; + getState(): MatcherState; hasAssertions(): void; - setState(state: Partial): void; -} & AsymmetricMatchers & - InverseAsymmetricMatchers; + setState(state: Partial): void; +} -type InverseAsymmetricMatchers = { +export type Expect = { + (actual: T): Matchers & + Inverse> & + PromiseMatchers; +} & BaseExpect & + AsymmetricMatchers & + Inverse>; + +type Inverse = { /** * Inverse next matcher. If you know how to test something, `.not` lets you test its opposite. */ - not: Omit; + not: Matchers; }; export interface AsymmetricMatchers { @@ -108,28 +109,21 @@ export interface AsymmetricMatchers { stringMatching(sample: string | RegExp): AsymmetricMatcher; } -type PromiseMatchers = { +type PromiseMatchers = { /** * Unwraps the reason of a rejected promise so any other matcher can be chained. * If the promise is fulfilled the assertion fails. */ - rejects: Matchers, T> & InverseMatchers, T>; + rejects: Matchers> & Inverse>>; /** * Unwraps the value of a fulfilled promise so any other matcher can be chained. * If the promise is rejected the assertion fails. */ - resolves: Matchers, T> & InverseMatchers, T>; -}; - -type InverseMatchers, T = unknown> = { - /** - * Inverse next matcher. If you know how to test something, `.not` lets you test its opposite. - */ - not: Matchers; + resolves: Matchers> & Inverse>>; }; // This is a copy from https://github.com/DefinitelyTyped/DefinitelyTyped/blob/de6730f4463cba69904698035fafd906a72b9664/types/jest/index.d.ts#L570-L817 -export interface Matchers, T = unknown> { +export interface Matchers> { /** * Ensures the last call to a mock function was provided specific args. */ @@ -339,44 +333,4 @@ export interface Matchers, T = unknown> { * If you want to test that a specific error is thrown inside a function. */ toThrowError(expected?: unknown): R; - - /* TODO: START snapshot matchers are not from `expect`, the types should not be here */ - /** - * This ensures that a value matches the most recent snapshot with property matchers. - * Check out [the Snapshot Testing guide](https://jestjs.io/docs/snapshot-testing) for more information. - */ - toMatchSnapshot(hint?: string): R; - /** - * This ensures that a value matches the most recent snapshot. - * Check out [the Snapshot Testing guide](https://jestjs.io/docs/snapshot-testing) for more information. - */ - toMatchSnapshot>( - propertyMatchers: Partial, - hint?: string, - ): R; - /** - * This ensures that a value matches the most recent snapshot with property matchers. - * Instead of writing the snapshot value to a .snap file, it will be written into the source code automatically. - * Check out [the Snapshot Testing guide](https://jestjs.io/docs/snapshot-testing) for more information. - */ - toMatchInlineSnapshot(snapshot?: string): R; - /** - * This ensures that a value matches the most recent snapshot with property matchers. - * Instead of writing the snapshot value to a .snap file, it will be written into the source code automatically. - * Check out [the Snapshot Testing guide](https://jestjs.io/docs/snapshot-testing) for more information. - */ - toMatchInlineSnapshot>( - propertyMatchers: Partial, - snapshot?: string, - ): R; - /** - * Used to test that a function throws a error matching the most recent snapshot when it is called. - */ - toThrowErrorMatchingSnapshot(hint?: string): R; - /** - * Used to test that a function throws a error matching the most recent snapshot when it is called. - * Instead of writing the snapshot value to a .snap file, it will be written into the source code automatically. - */ - toThrowErrorMatchingInlineSnapshot(snapshot?: string): R; - /* TODO: END snapshot matchers are not from `expect`, the types should not be here */ } diff --git a/packages/jest-circus/package.json b/packages/jest-circus/package.json index 3f5d8c933a21..110f2aaebec2 100644 --- a/packages/jest-circus/package.json +++ b/packages/jest-circus/package.json @@ -19,13 +19,13 @@ }, "dependencies": { "@jest/environment": "^28.0.0-alpha.1", + "@jest/expect": "^28.0.0-alpha.1", "@jest/test-result": "^28.0.0-alpha.1", "@jest/types": "^28.0.0-alpha.1", "@types/node": "*", "chalk": "^4.0.0", "co": "^4.6.0", "dedent": "^0.7.0", - "expect": "^28.0.0-alpha.1", "is-generator-fn": "^2.0.0", "jest-each": "^28.0.0-alpha.1", "jest-matcher-utils": "^28.0.0-alpha.1", diff --git a/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts b/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts index f7698576c396..6723ae7d74ab 100644 --- a/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts +++ b/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts @@ -7,6 +7,7 @@ import throat from 'throat'; import type {JestEnvironment} from '@jest/environment'; +import {type JestExpect, jestExpect} from '@jest/expect'; import { AssertionResult, Status, @@ -15,7 +16,6 @@ import { createEmptyTestResult, } from '@jest/test-result'; import type {Circus, Config, Global} from '@jest/types'; -import {Expect, expect} from 'expect'; import {bind} from 'jest-each'; import {formatExecError, formatResultsErrors} from 'jest-message-util'; import { @@ -33,12 +33,9 @@ import { } from '../state'; import testCaseReportHandler from '../testCaseReportHandler'; import {getTestID} from '../utils'; -import createExpect from './jestExpect'; -type Process = NodeJS.Process; - -interface JestGlobals extends Global.TestFrameworkGlobals { - expect: Expect; +interface RuntimeGlobals extends Global.TestFrameworkGlobals { + expect: JestExpect; } export const initialize = async ({ @@ -56,9 +53,9 @@ export const initialize = async ({ globalConfig: Config.GlobalConfig; localRequire: (path: string) => T; testPath: string; - parentProcess: Process; + parentProcess: NodeJS.Process; sendMessageToJest?: TestFileEvent; - setGlobalsForRuntime: (globals: JestGlobals) => void; + setGlobalsForRuntime: (globals: RuntimeGlobals) => void; }): Promise<{ globals: Global.TestFrameworkGlobals; snapshotState: SnapshotState; @@ -124,9 +121,11 @@ export const initialize = async ({ addEventHandler(environment.handleTestEvent.bind(environment)); } - const runtimeGlobals: JestGlobals = { + jestExpect.setState({expand: globalConfig.expand}); + + const runtimeGlobals: RuntimeGlobals = { ...globalsObject, - expect: createExpect(globalConfig), + expect: jestExpect, }; setGlobalsForRuntime(runtimeGlobals); @@ -152,17 +151,16 @@ export const initialize = async ({ .reverse() .forEach(path => addSerializer(localRequire(path))); - const {expand, updateSnapshot} = globalConfig; const snapshotResolver = await buildSnapshotResolver(config, localRequire); const snapshotPath = snapshotResolver.resolveSnapshotPath(testPath); const snapshotState = new SnapshotState(snapshotPath, { - expand, + expand: globalConfig.expand, prettierPath: config.prettierPath, snapshotFormat: config.snapshotFormat, - updateSnapshot, + updateSnapshot: globalConfig.updateSnapshot, }); - // @ts-expect-error: snapshotState is a jest extension of `expect` - expect.setState({snapshotState, testPath}); + + jestExpect.setState({snapshotState, testPath}); addEventHandler(handleSnapshotStateAfterRetry(snapshotState)); if (sendMessageToJest) { @@ -279,7 +277,7 @@ const handleSnapshotStateAfterRetry = const eventHandler = async (event: Circus.Event) => { switch (event.name) { case 'test_start': { - expect.setState({currentTestName: getTestID(event.test)}); + jestExpect.setState({currentTestName: getTestID(event.test)}); break; } case 'test_done': { @@ -291,7 +289,7 @@ const eventHandler = async (event: Circus.Event) => { }; const _addExpectedAssertionErrors = (test: Circus.TestEntry) => { - const failures = expect.extractExpectedAssertionsErrors(); + const failures = jestExpect.extractExpectedAssertionsErrors(); const errors = failures.map(failure => failure.error); test.errors = test.errors.concat(errors); }; @@ -300,8 +298,8 @@ const _addExpectedAssertionErrors = (test: Circus.TestEntry) => { // test execution and add them to the test result, potentially failing // a passing test. const _addSuppressedErrors = (test: Circus.TestEntry) => { - const {suppressedErrors} = expect.getState(); - expect.setState({suppressedErrors: []}); + const {suppressedErrors} = jestExpect.getState(); + jestExpect.setState({suppressedErrors: []}); if (suppressedErrors.length) { test.errors = test.errors.concat(suppressedErrors); } diff --git a/packages/jest-circus/src/types.ts b/packages/jest-circus/src/types.ts index 92d47eb16983..babde014441c 100644 --- a/packages/jest-circus/src/types.ts +++ b/packages/jest-circus/src/types.ts @@ -6,7 +6,6 @@ */ import type {Circus} from '@jest/types'; -import type {Expect} from 'expect'; export const STATE_SYM = Symbol( 'JEST_STATE_SYMBOL', @@ -25,7 +24,6 @@ declare global { STATE_SYM_SYMBOL: Circus.State; RETRY_TIMES_SYMBOL: string; TEST_TIMEOUT_SYMBOL: number; - expect: Expect; } } } diff --git a/packages/jest-circus/tsconfig.json b/packages/jest-circus/tsconfig.json index 4e6535abac08..67941a37790e 100644 --- a/packages/jest-circus/tsconfig.json +++ b/packages/jest-circus/tsconfig.json @@ -2,14 +2,16 @@ "extends": "../../tsconfig", "compilerOptions": { "outDir": "build", - "rootDir": "src" + "rootDir": "src", + // we don't want `@types/jest` to be referenced + "types": [] }, "include": ["./src/**/*"], "exclude": ["./**/__mocks__/**/*", "./**/__tests__/**/*"], "references": [ - {"path": "../expect"}, {"path": "../jest-each"}, {"path": "../jest-environment"}, + {"path": "../jest-expect"}, {"path": "../jest-matcher-utils"}, {"path": "../jest-message-util"}, {"path": "../jest-runtime"}, diff --git a/packages/jest-expect/.npmignore b/packages/jest-expect/.npmignore new file mode 100644 index 000000000000..8ea2d7d53d29 --- /dev/null +++ b/packages/jest-expect/.npmignore @@ -0,0 +1,5 @@ +**/__tests__/** +src +tsconfig.json +tsconfig.tsbuildinfo +api-extractor.json diff --git a/packages/jest-expect/README.md b/packages/jest-expect/README.md new file mode 100644 index 000000000000..ed84274db617 --- /dev/null +++ b/packages/jest-expect/README.md @@ -0,0 +1,5 @@ +# @jest/expect + +This package extends `expect` library with `jest-snapshot` matchers. It export `jestExpect` object, which can be used as standalone replacement of `expect`, and `createJestExpect` factory function, which returns an instance of `jestExpect`. + +The `jestExpect` function used in [Jest](https://jestjs.io/). You can find its documentation [on Jest's website](https://jestjs.io/docs/expect). diff --git a/packages/jest-expect/__typetests__/jest-expect.test.ts b/packages/jest-expect/__typetests__/jest-expect.test.ts new file mode 100644 index 000000000000..ac1d0193565c --- /dev/null +++ b/packages/jest-expect/__typetests__/jest-expect.test.ts @@ -0,0 +1,31 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { + expectAssignable, + expectError, + expectNotAssignable, + expectType, +} from 'tsd-lite'; +import {jestExpect} from '@jest/expect'; +import {expect} from 'expect'; +import type {Plugin} from 'pretty-format'; + +expectType(jestExpect({}).toMatchSnapshot()); + +expectError(() => { + expect({}).toMatchSnapshot(); +}); + +expectType(jestExpect.addSnapshotSerializer({} as Plugin)); + +expectError(() => { + expect.addSnapshotSerializer(); +}); + +expectAssignable(jestExpect); +expectNotAssignable(expect); diff --git a/packages/jest-expect/__typetests__/tsconfig.json b/packages/jest-expect/__typetests__/tsconfig.json new file mode 100644 index 000000000000..b7577781b384 --- /dev/null +++ b/packages/jest-expect/__typetests__/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "noUnusedLocals": false, + "noUnusedParameters": false, + "skipLibCheck": true + }, + "include": ["./**/*"] +} diff --git a/packages/jest-expect/package.json b/packages/jest-expect/package.json new file mode 100644 index 000000000000..a8a938f03479 --- /dev/null +++ b/packages/jest-expect/package.json @@ -0,0 +1,33 @@ +{ + "name": "@jest/expect", + "version": "28.0.0-alpha.1", + "repository": { + "type": "git", + "url": "https://github.com/facebook/jest.git", + "directory": "packages/jest-expect" + }, + "license": "MIT", + "main": "./build/index.js", + "types": "./build/index.d.ts", + "exports": { + ".": { + "types": "./build/index.d.ts", + "default": "./build/index.js" + }, + "./package.json": "./package.json" + }, + "dependencies": { + "expect": "^28.0.0-alpha.1", + "jest-snapshot": "^28.0.0-alpha.1" + }, + "devDependencies": { + "@tsd/typescript": "~4.5.5", + "tsd-lite": "^0.5.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.13.0 || >=17.0.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/jest-circus/src/legacy-code-todo-rewrite/jestExpect.ts b/packages/jest-expect/src/index.ts similarity index 70% rename from packages/jest-circus/src/legacy-code-todo-rewrite/jestExpect.ts rename to packages/jest-expect/src/index.ts index 6f6332f72490..3086a8fe447a 100644 --- a/packages/jest-circus/src/legacy-code-todo-rewrite/jestExpect.ts +++ b/packages/jest-expect/src/index.ts @@ -5,8 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import type {Config} from '@jest/types'; -import {Expect, expect} from 'expect'; +import {expect} from 'expect'; import { addSerializer, toMatchInlineSnapshot, @@ -14,11 +13,11 @@ import { toThrowErrorMatchingInlineSnapshot, toThrowErrorMatchingSnapshot, } from 'jest-snapshot'; +import type {JestExpect} from './types'; -export default function jestExpect( - config: Pick, -): Expect { - expect.setState({expand: config.expand}); +export type {JestExpect} from './types'; + +export function createJestExpect(): JestExpect { expect.extend({ toMatchInlineSnapshot, toMatchSnapshot, @@ -28,5 +27,7 @@ export default function jestExpect( expect.addSnapshotSerializer = addSerializer; - return expect; + return expect as JestExpect; } + +export const jestExpect = createJestExpect(); diff --git a/packages/jest-expect/src/types.ts b/packages/jest-expect/src/types.ts new file mode 100644 index 000000000000..10739da8481e --- /dev/null +++ b/packages/jest-expect/src/types.ts @@ -0,0 +1,57 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import type {AsymmetricMatchers, BaseExpect, Matchers} from 'expect'; +import type { + SnapshotMatchers, + SnapshotState, + addSerializer, +} from 'jest-snapshot'; + +export type JestExpect = { + (actual: T): JestMatchers & + Inverse> & + PromiseMatchers; + // Duplicated due to https://github.com/microsoft/rushstack/issues/1709 + addSnapshotSerializer: typeof addSerializer; +} & BaseExpect & + AsymmetricMatchers & + Inverse>; + +type Inverse = { + /** + * Inverse next matcher. If you know how to test something, `.not` lets you test its opposite. + */ + not: Matchers; +}; + +type JestMatchers, T> = Matchers & + SnapshotMatchers; + +type PromiseMatchers = { + /** + * Unwraps the reason of a rejected promise so any other matcher can be chained. + * If the promise is fulfilled the assertion fails. + */ + rejects: JestMatchers, T> & + Inverse, T>>; + /** + * Unwraps the value of a fulfilled promise so any other matcher can be chained. + * If the promise is rejected the assertion fails. + */ + resolves: JestMatchers, T> & + Inverse, T>>; +}; + +declare module 'expect' { + interface MatcherState { + snapshotState: SnapshotState; + } + interface BaseExpect { + addSnapshotSerializer: typeof addSerializer; + } +} diff --git a/packages/jest-expect/tsconfig.json b/packages/jest-expect/tsconfig.json new file mode 100644 index 000000000000..b04915822b89 --- /dev/null +++ b/packages/jest-expect/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig", + "compilerOptions": { + "outDir": "build", + "rootDir": "src" + }, + "include": ["./src/**/*"], + "exclude": ["./**/__tests__/**/*"], + "references": [{"path": "../expect"}, {"path": "../jest-snapshot"}] +} diff --git a/packages/jest-globals/package.json b/packages/jest-globals/package.json index ebc40a85f2ad..4f64d0a8cd1d 100644 --- a/packages/jest-globals/package.json +++ b/packages/jest-globals/package.json @@ -21,8 +21,8 @@ }, "dependencies": { "@jest/environment": "^28.0.0-alpha.1", - "@jest/types": "^28.0.0-alpha.1", - "expect": "^28.0.0-alpha.1" + "@jest/expect": "^28.0.0-alpha.1", + "@jest/types": "^28.0.0-alpha.1" }, "publishConfig": { "access": "public" diff --git a/packages/jest-globals/src/index.ts b/packages/jest-globals/src/index.ts index 5d84d89125d1..90f50d9665ec 100644 --- a/packages/jest-globals/src/index.ts +++ b/packages/jest-globals/src/index.ts @@ -6,12 +6,12 @@ */ import type {Jest} from '@jest/environment'; +import type {JestExpect} from '@jest/expect'; import type {Global} from '@jest/types'; -import type {Expect} from 'expect'; export declare const jest: Jest; -export declare const expect: Expect; +export declare const expect: JestExpect; export declare const it: Global.GlobalAdditions['it']; export declare const test: Global.GlobalAdditions['test']; diff --git a/packages/jest-globals/tsconfig.json b/packages/jest-globals/tsconfig.json index ef7d4e629e02..16a7adbc92db 100644 --- a/packages/jest-globals/tsconfig.json +++ b/packages/jest-globals/tsconfig.json @@ -9,8 +9,8 @@ "include": ["./src/**/*"], "exclude": ["./**/__tests__/**/*"], "references": [ - {"path": "../expect"}, {"path": "../jest-environment"}, + {"path": "../jest-expect"}, {"path": "../jest-types"} ] } diff --git a/packages/jest-jasmine2/package.json b/packages/jest-jasmine2/package.json index 7e74e8cf5d9e..1935dff545e4 100644 --- a/packages/jest-jasmine2/package.json +++ b/packages/jest-jasmine2/package.json @@ -18,13 +18,13 @@ }, "dependencies": { "@jest/environment": "^28.0.0-alpha.1", + "@jest/expect": "^28.0.0-alpha.1", "@jest/source-map": "^28.0.0-alpha.0", "@jest/test-result": "^28.0.0-alpha.1", "@jest/types": "^28.0.0-alpha.1", "@types/node": "*", "chalk": "^4.0.0", "co": "^4.6.0", - "expect": "^28.0.0-alpha.1", "is-generator-fn": "^2.0.0", "jest-each": "^28.0.0-alpha.1", "jest-matcher-utils": "^28.0.0-alpha.1", diff --git a/packages/jest-jasmine2/src/jestExpect.ts b/packages/jest-jasmine2/src/jestExpect.ts index 579b6a7de605..b95a661206b2 100644 --- a/packages/jest-jasmine2/src/jestExpect.ts +++ b/packages/jest-jasmine2/src/jestExpect.ts @@ -7,41 +7,24 @@ /* eslint-disable local/prefer-spread-eventually */ -import {type MatcherState, expect} from 'expect'; -import { - addSerializer, - toMatchInlineSnapshot, - toMatchSnapshot, - toThrowErrorMatchingInlineSnapshot, - toThrowErrorMatchingSnapshot, -} from 'jest-snapshot'; +import {jestExpect} from '@jest/expect'; import type {JasmineMatchersObject} from './types'; -export default function jestExpect(config: {expand: boolean}): void { - global.expect = expect; - expect.setState({expand: config.expand}); - expect.extend({ - toMatchInlineSnapshot, - toMatchSnapshot, - toThrowErrorMatchingInlineSnapshot, - toThrowErrorMatchingSnapshot, - }); - expect.addSnapshotSerializer = addSerializer; +export default function jestExpectAdapter(config: {expand: boolean}): void { + global.expect = jestExpect; + jestExpect.setState({expand: config.expand}); const jasmine = global.jasmine; - jasmine.anything = expect.anything; - jasmine.any = expect.any; - jasmine.objectContaining = expect.objectContaining; - jasmine.arrayContaining = expect.arrayContaining; - jasmine.stringMatching = expect.stringMatching; + jasmine.anything = jestExpect.anything; + jasmine.any = jestExpect.any; + jasmine.objectContaining = jestExpect.objectContaining; + jasmine.arrayContaining = jestExpect.arrayContaining; + jasmine.stringMatching = jestExpect.stringMatching; jasmine.addMatchers = (jasmineMatchersObject: JasmineMatchersObject) => { const jestMatchersObject = Object.create(null); Object.keys(jasmineMatchersObject).forEach(name => { - jestMatchersObject[name] = function ( - this: MatcherState, - ...args: Array - ) { + jestMatchersObject[name] = function (...args: Array) { // use "expect.extend" if you need to use equality testers (via this.equal) const result = jasmineMatchersObject[name](null, null); // if there is no 'negativeCompare', both should be handled by `compare` @@ -53,6 +36,6 @@ export default function jestExpect(config: {expand: boolean}): void { }; }); - expect.extend(jestMatchersObject); + jestExpect.extend(jestMatchersObject); }; } diff --git a/packages/jest-jasmine2/src/setup_jest_globals.ts b/packages/jest-jasmine2/src/setup_jest_globals.ts index d16c34685009..0265b547c4d3 100644 --- a/packages/jest-jasmine2/src/setup_jest_globals.ts +++ b/packages/jest-jasmine2/src/setup_jest_globals.ts @@ -5,8 +5,8 @@ * LICENSE file in the root directory of this source tree. */ +import {jestExpect} from '@jest/expect'; import type {Config} from '@jest/types'; -import {expect} from 'expect'; import { SnapshotState, addSerializer, @@ -30,8 +30,8 @@ export type SetupOptions = { // test execution and add them to the test result, potentially failing // a passing test. const addSuppressedErrors = (result: SpecResult) => { - const {suppressedErrors} = expect.getState(); - expect.setState({suppressedErrors: []}); + const {suppressedErrors} = jestExpect.getState(); + jestExpect.setState({suppressedErrors: []}); if (suppressedErrors.length) { result.status = 'failed'; @@ -49,7 +49,7 @@ const addSuppressedErrors = (result: SpecResult) => { }; const addAssertionErrors = (result: SpecResult) => { - const assertionErrors = expect.extractExpectedAssertionsErrors(); + const assertionErrors = jestExpect.extractExpectedAssertionsErrors(); if (assertionErrors.length) { const jasmineErrors = assertionErrors.map(({actual, error, expected}) => ({ actual, @@ -74,7 +74,7 @@ const patchJasmine = () => { }; const onStart = attr.onStart; attr.onStart = (context: JasmineSpec) => { - expect.setState({currentTestName: context.getFullName()}); + jestExpect.setState({currentTestName: context.getFullName()}); onStart && onStart.call(attr, context); }; super(attr); @@ -111,8 +111,8 @@ export default async function setupJestGlobals({ snapshotFormat, updateSnapshot, }); - // @ts-expect-error: snapshotState is a jest extension of `expect` - expect.setState({snapshotState, testPath}); + + jestExpect.setState({snapshotState, testPath}); // Return it back to the outer scope (test runner outside the VM). return snapshotState; } diff --git a/packages/jest-jasmine2/src/types.ts b/packages/jest-jasmine2/src/types.ts index 4f42b280fb96..b6f3e147b8bb 100644 --- a/packages/jest-jasmine2/src/types.ts +++ b/packages/jest-jasmine2/src/types.ts @@ -6,7 +6,7 @@ */ import type {AssertionError} from 'assert'; -import type {Expect} from 'expect'; +import type {JestExpect} from '@jest/expect'; import type CallTracker from './jasmine/CallTracker'; import type Env from './jasmine/Env'; import type JsApiReporter from './jasmine/JsApiReporter'; @@ -69,13 +69,13 @@ export type Jasmine = { version: string; testPath: string; addMatchers: (matchers: JasmineMatchersObject) => void; -} & Expect & +} & JestExpect & typeof globalThis; declare global { namespace NodeJS { interface Global { - expect: Expect; + expect: JestExpect; jasmine: Jasmine; } } diff --git a/packages/jest-jasmine2/tsconfig.json b/packages/jest-jasmine2/tsconfig.json index f7813a30ad7a..3d2268f04774 100644 --- a/packages/jest-jasmine2/tsconfig.json +++ b/packages/jest-jasmine2/tsconfig.json @@ -1,17 +1,17 @@ { "extends": "../../tsconfig", "compilerOptions": { - // we don't want `@types/jest` to be referenced - "types": [], "rootDir": "src", - "outDir": "build" + "outDir": "build", + // we don't want `@types/jest` to be referenced + "types": [] }, "include": ["./src/**/*"], "exclude": ["./**/__tests__/**/*"], "references": [ - {"path": "../expect"}, {"path": "../jest-each"}, {"path": "../jest-environment"}, + {"path": "../jest-expect"}, {"path": "../jest-matcher-utils"}, {"path": "../jest-message-util"}, {"path": "../jest-runtime"}, diff --git a/packages/jest-snapshot/src/index.ts b/packages/jest-snapshot/src/index.ts index c0e229e257b1..ca990b208785 100644 --- a/packages/jest-snapshot/src/index.ts +++ b/packages/jest-snapshot/src/index.ts @@ -42,6 +42,7 @@ export { } from './SnapshotResolver'; export type {SnapshotResolver} from './SnapshotResolver'; export {default as SnapshotState} from './State'; +export type {SnapshotMatchers} from './types'; const DID_NOT_THROW = 'Received function did not throw'; // same as toThrow const NOT_SNAPSHOT_MATCHERS = `Snapshot matchers cannot be used with ${BOLD_WEIGHT( diff --git a/packages/jest-snapshot/src/types.ts b/packages/jest-snapshot/src/types.ts index 0013a08d081f..2f47b96a3ed0 100644 --- a/packages/jest-snapshot/src/types.ts +++ b/packages/jest-snapshot/src/types.ts @@ -23,3 +23,43 @@ export type MatchSnapshotConfig = { }; export type SnapshotData = Record; + +export interface SnapshotMatchers, T> { + /** + * This ensures that a value matches the most recent snapshot with property matchers. + * Check out [the Snapshot Testing guide](https://jestjs.io/docs/snapshot-testing) for more information. + */ + toMatchSnapshot(hint?: string): R; + /** + * This ensures that a value matches the most recent snapshot. + * Check out [the Snapshot Testing guide](https://jestjs.io/docs/snapshot-testing) for more information. + */ + toMatchSnapshot>( + propertyMatchers: Partial, + hint?: string, + ): R; + /** + * This ensures that a value matches the most recent snapshot with property matchers. + * Instead of writing the snapshot value to a .snap file, it will be written into the source code automatically. + * Check out [the Snapshot Testing guide](https://jestjs.io/docs/snapshot-testing) for more information. + */ + toMatchInlineSnapshot(snapshot?: string): R; + /** + * This ensures that a value matches the most recent snapshot with property matchers. + * Instead of writing the snapshot value to a .snap file, it will be written into the source code automatically. + * Check out [the Snapshot Testing guide](https://jestjs.io/docs/snapshot-testing) for more information. + */ + toMatchInlineSnapshot>( + propertyMatchers: Partial, + snapshot?: string, + ): R; + /** + * Used to test that a function throws a error matching the most recent snapshot when it is called. + */ + toThrowErrorMatchingSnapshot(hint?: string): R; + /** + * Used to test that a function throws a error matching the most recent snapshot when it is called. + * Instead of writing the snapshot value to a .snap file, it will be written into the source code automatically. + */ + toThrowErrorMatchingInlineSnapshot(snapshot?: string): R; +} diff --git a/yarn.lock b/yarn.lock index f2a4eab9107f..ca22e60a5e6b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2589,6 +2589,17 @@ __metadata: languageName: unknown linkType: soft +"@jest/expect@^28.0.0-alpha.1, @jest/expect@workspace:packages/jest-expect": + version: 0.0.0-use.local + resolution: "@jest/expect@workspace:packages/jest-expect" + dependencies: + "@tsd/typescript": ~4.5.5 + expect: ^28.0.0-alpha.1 + jest-snapshot: ^28.0.0-alpha.1 + tsd-lite: ^0.5.1 + languageName: unknown + linkType: soft + "@jest/fake-timers@^28.0.0-alpha.1, @jest/fake-timers@workspace:packages/jest-fake-timers": version: 0.0.0-use.local resolution: "@jest/fake-timers@workspace:packages/jest-fake-timers" @@ -2608,8 +2619,8 @@ __metadata: resolution: "@jest/globals@workspace:packages/jest-globals" dependencies: "@jest/environment": ^28.0.0-alpha.1 + "@jest/expect": ^28.0.0-alpha.1 "@jest/types": ^28.0.0-alpha.1 - expect: ^28.0.0-alpha.1 languageName: unknown linkType: soft @@ -12851,6 +12862,7 @@ __metadata: "@babel/core": ^7.1.0 "@babel/register": ^7.0.0 "@jest/environment": ^28.0.0-alpha.1 + "@jest/expect": ^28.0.0-alpha.1 "@jest/test-result": ^28.0.0-alpha.1 "@jest/types": ^28.0.0-alpha.1 "@types/co": ^4.6.0 @@ -12862,7 +12874,6 @@ __metadata: co: ^4.6.0 dedent: ^0.7.0 execa: ^5.0.0 - expect: ^28.0.0-alpha.1 graceful-fs: ^4.2.9 is-generator-fn: ^2.0.0 jest-each: ^28.0.0-alpha.1 @@ -13093,6 +13104,7 @@ __metadata: resolution: "jest-jasmine2@workspace:packages/jest-jasmine2" dependencies: "@jest/environment": ^28.0.0-alpha.1 + "@jest/expect": ^28.0.0-alpha.1 "@jest/source-map": ^28.0.0-alpha.0 "@jest/test-result": ^28.0.0-alpha.1 "@jest/types": ^28.0.0-alpha.1 @@ -13100,7 +13112,6 @@ __metadata: "@types/node": "*" chalk: ^4.0.0 co: ^4.6.0 - expect: ^28.0.0-alpha.1 is-generator-fn: ^2.0.0 jest-each: ^28.0.0-alpha.1 jest-matcher-utils: ^28.0.0-alpha.1