Skip to content

Commit

Permalink
feat: create new @jest/expect package (#12404)
Browse files Browse the repository at this point in the history
  • Loading branch information
mrazauskas committed Feb 16, 2022
1 parent 199f981 commit 755640d
Show file tree
Hide file tree
Showing 29 changed files with 308 additions and 171 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -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))
Expand Down
7 changes: 5 additions & 2 deletions packages/expect/__typetests__/expect.test.ts
Expand Up @@ -16,8 +16,7 @@ import {
} from 'expect';
import type * as jestMatcherUtils from 'jest-matcher-utils';

type M = Matchers<void, unknown>;
type N = Matchers<void>;
type M = Matchers<void>;

expectError(() => {
type E = Matchers;
Expand Down Expand Up @@ -211,3 +210,7 @@ const customStateAndExpected: MatcherFunctionWithState<
};

expectAssignable<CustomStateAndExpected>(customStateAndExpected);

expectError(() => {
expect({}).toMatchSnapshot();
});
10 changes: 4 additions & 6 deletions packages/expect/src/asymmetricMatchers.ts
Expand Up @@ -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<T>
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;
Expand Down
9 changes: 4 additions & 5 deletions packages/expect/src/index.ts
Expand Up @@ -51,6 +51,7 @@ import type {

export type {
AsymmetricMatchers,
BaseExpect,
Expect,
MatcherFunction,
MatcherFunctionWithState,
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -363,9 +364,8 @@ const makeThrowingMatcher = (
}
};

expect.extend = <T extends MatcherState = MatcherState>(
matchers: MatchersObject<T>,
) => setMatchers(matchers, false, expect);
expect.extend = (matchers: MatchersObject) =>
setMatchers(matchers, false, expect);

expect.anything = anything;
expect.any = any;
Expand Down Expand Up @@ -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;
Expand Down
12 changes: 5 additions & 7 deletions packages/expect/src/jestMatchersObject.ts
Expand Up @@ -46,12 +46,11 @@ export const setState = <State extends MatcherState = MatcherState>(
Object.assign((global as any)[JEST_MATCHERS_OBJECT].state, state);
};

export const getMatchers = <
State extends MatcherState = MatcherState,
>(): MatchersObject<State> => (global as any)[JEST_MATCHERS_OBJECT].matchers;
export const getMatchers = (): MatchersObject =>
(global as any)[JEST_MATCHERS_OBJECT].matchers;

export const setMatchers = <State extends MatcherState = MatcherState>(
matchers: MatchersObject<State>,
export const setMatchers = (
matchers: MatchersObject,
isInternal: boolean,
expect: Expect,
): void => {
Expand All @@ -65,8 +64,7 @@ export const setMatchers = <State extends MatcherState = MatcherState>(
// expect is defined

class CustomMatcher extends AsymmetricMatcher<
[unknown, ...Array<unknown>],
State
[unknown, ...Array<unknown>]
> {
constructor(inverse = false, ...sample: [unknown, ...Array<unknown>]) {
super(sample, inverse);
Expand Down
92 changes: 23 additions & 69 deletions packages/expect/src/types.ts
Expand Up @@ -34,8 +34,8 @@ export type RawMatcherFn<State extends MatcherState = MatcherState> = {
[INTERNAL_MATCHER_FLAG]?: boolean;
};

export type MatchersObject<T extends MatcherState = MatcherState> = {
[name: string]: RawMatcherFn<T>;
export type MatchersObject = {
[name: string]: RawMatcherFn;
};

export type ThrowingMatcherFn = (actual: any) => void;
Expand All @@ -62,40 +62,41 @@ 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;
error: Error;
expected: string;
}>;

export type Expect<State extends MatcherState = MatcherState> = {
<T = unknown>(actual: T): Matchers<void, T> &
InverseMatchers<void, T> &
PromiseMatchers<T>;
// 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<T extends MatcherState = State>(matchers: MatchersObject<T>): void;
extend(matchers: MatchersObject): void;
extractExpectedAssertionsErrors(): ExpectedAssertionsErrors;
getState(): State;
getState(): MatcherState;
hasAssertions(): void;
setState(state: Partial<State>): void;
} & AsymmetricMatchers &
InverseAsymmetricMatchers;
setState(state: Partial<MatcherState>): void;
}

type InverseAsymmetricMatchers = {
export type Expect = {
<T = unknown>(actual: T): Matchers<void> &
Inverse<Matchers<void>> &
PromiseMatchers;
} & BaseExpect &
AsymmetricMatchers &
Inverse<Omit<AsymmetricMatchers, 'any' | 'anything'>>;

type Inverse<Matchers> = {
/**
* Inverse next matcher. If you know how to test something, `.not` lets you test its opposite.
*/
not: Omit<AsymmetricMatchers, 'any' | 'anything'>;
not: Matchers;
};

export interface AsymmetricMatchers {
Expand All @@ -108,28 +109,21 @@ export interface AsymmetricMatchers {
stringMatching(sample: string | RegExp): AsymmetricMatcher;
}

type PromiseMatchers<T = unknown> = {
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<Promise<void>, T> & InverseMatchers<Promise<void>, T>;
rejects: Matchers<Promise<void>> & Inverse<Matchers<Promise<void>>>;
/**
* Unwraps the value of a fulfilled promise so any other matcher can be chained.
* If the promise is rejected the assertion fails.
*/
resolves: Matchers<Promise<void>, T> & InverseMatchers<Promise<void>, T>;
};

type InverseMatchers<R extends void | Promise<void>, T = unknown> = {
/**
* Inverse next matcher. If you know how to test something, `.not` lets you test its opposite.
*/
not: Matchers<R, T>;
resolves: Matchers<Promise<void>> & Inverse<Matchers<Promise<void>>>;
};

// This is a copy from https://github.com/DefinitelyTyped/DefinitelyTyped/blob/de6730f4463cba69904698035fafd906a72b9664/types/jest/index.d.ts#L570-L817
export interface Matchers<R extends void | Promise<void>, T = unknown> {
export interface Matchers<R extends void | Promise<void>> {
/**
* Ensures the last call to a mock function was provided specific args.
*/
Expand Down Expand Up @@ -339,44 +333,4 @@ export interface Matchers<R extends void | Promise<void>, 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<U extends Record<keyof T, unknown>>(
propertyMatchers: Partial<U>,
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<U extends Record<keyof T, unknown>>(
propertyMatchers: Partial<U>,
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 */
}
2 changes: 1 addition & 1 deletion packages/jest-circus/package.json
Expand Up @@ -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",
Expand Down
Expand Up @@ -7,6 +7,7 @@

import throat from 'throat';
import type {JestEnvironment} from '@jest/environment';
import {type JestExpect, jestExpect} from '@jest/expect';
import {
AssertionResult,
Status,
Expand All @@ -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 {
Expand All @@ -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 ({
Expand All @@ -56,9 +53,9 @@ export const initialize = async ({
globalConfig: Config.GlobalConfig;
localRequire: <T = unknown>(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;
Expand Down Expand Up @@ -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);

Expand All @@ -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) {
Expand Down Expand Up @@ -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': {
Expand All @@ -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);
};
Expand All @@ -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);
}
Expand Down

0 comments on commit 755640d

Please sign in to comment.