Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: create new @jest/expect package #12404

Merged
merged 21 commits into from Feb 16, 2022
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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;
SimenB marked this conversation as resolved.
Show resolved Hide resolved
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