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

[Bug]: Using @jest/globals expect breaks extensions #12267

Closed
johncrim opened this issue Jan 26, 2022 · 6 comments
Closed

[Bug]: Using @jest/globals expect breaks extensions #12267

johncrim opened this issue Jan 26, 2022 · 6 comments

Comments

@johncrim
Copy link

johncrim commented Jan 26, 2022

Version

27.4.7

Steps to reproduce

git clone git@github.com:johncrim/jest-bugs.git
yarn
yarn test-esm

There are two versions of the same test, one using

import { expect } from '@jest/globals';

the other using the ambient/global expect.

The test using import { expect } from '@jest/globals'; fails with:

 PASS  src/expect-extension-no-jest-globals.spec.ts
 FAIL  src/expect-extension-jest-globals.spec.ts
  ● Test suite failed to run

    src/expect-extension-jest-globals.spec.ts:6:18 - error TS2339: Property 'toIncludeSameMembers' does not exist on type 'Matchers<void, number[]>'.

    6   expect([1, 2]).toIncludeSameMembers([2, 1]);

because the type returned from expect() in '@jest/globals' (Matchers<void, T> in expect module) does not match the type returned from expect() in @types/jest (jest.JestMatchers<T>).

Expected behavior

expect extension typing continues to work whether you're importing from @jest/globals or not.

Actual behavior

expect extensions fail to compile when importing expect from @jest/globals. If you can get around the typing issue (via casting or any or creating a global type def file to fix the issue) the runtime behavior works as expected.

Additional context

This breaks all expect extensions in libraries I'm aware of, including:

  • jest-extended
  • ngneat/spectator
  • expect-more-jest

It also breaks anyone using private expect extensions using the approach that is widely documented and used - a file like:

// my-jest-matchers.d.ts
declare namespace jest {
  interface Matchers<R> {
    equalsFoo(message: string): R;
  }
}

will no longer enable compilation given expect(x).equalsFoo('message');.

The obvious workaround is "don't use @jest/globals", but that is not viable in many codebases - particularly if using ES Modules. In our codebase, we have jasmine types and jest types, and using @jest/globals is necessary now that we've changed to ES Modules.

I believe this is the same issue as #10642. #10642 was correctly reported, but it was incorrectly closed. This comment about creating a type def file for expect extensions is helpful, but is not a fix for #10642. Hopefully this issue provides more clarity.

Environment

System:
    OS: Windows 10 10.0.19044
    CPU: (12) x64 Intel(R) Core(TM) i9-8950HK CPU @ 2.90GHz
  Binaries:
    Node: 16.12.0 - C:\Program Files\nodejs\node.EXE
    Yarn: 1.22.15 - C:\Program Files (x86)\Yarn\bin\yarn.CMD
    npm: 8.1.0 - C:\Program Files\nodejs\npm.CMD
  npmPackages:
    jest: ^27.3.1 => 27.4.7
@johncrim
Copy link
Author

johncrim commented Jan 27, 2022

Here's the best workaround I've found so far. I've created a type declaration file, which combines the return types of import {expect} from 'expect'; and import { jest.Expect } from 'types/jest'. There may be issues with this approach which I haven't uncovered yet:

// File: types/jest-matcher-fix/index.d.ts

/// <reference types="jest" />

// This line is needed to pull in jest-extended. Remove if you're not using jest-extended,
// or if you're including its types another way.
/// <reference path="../../node_modules/jest-extended/types/index.d.ts" />

/**
 * Overrides the `@jest/globals` module so that `expect` returns a `Matcher<T>`
 * type that is compatible with matcher extensions.
 *
 * Without this, `@jest/globals expect()` returns a new type, which is not type-compatible with
 * `jest.Matchers<R>`, which all the extending libraries expect.
 */
declare module '@jest/globals' {

  // IMPORTANT: These imports have to be within the declared module,
  // otherwise the module patch doesn't work outside of this file.
  import type { Jest } from '@jest/environment';
  import type { Global } from '@jest/types';
  import type importedExpect from 'expect';

  export type CombinedMatchers<T> = jest.JestMatchers<T> & importedExpect.Matchers<void, T>;

  interface ExpectReturningCombinedMatchers {
    <T = unknown>(actual: T): CombinedMatchers<T>;
  }

  export type CombinedExpect = jest.Expect & typeof importedExpect & ExpectReturningCombinedMatchers;


  // I couldn't figure out a way to avoid duplicating all the globals/types in
  // ../../node_modules/@jest/globals/build/index.d.ts, so here they are again.
  //
  // The only change from `@jest/globals` is `expect: CombinedExpect`.
  export const jest: Jest;
  export const expect: CombinedExpect;
  export const it: Global.GlobalAdditions['it'];
  export const test: Global.GlobalAdditions['test'];
  export const fit: Global.GlobalAdditions['fit'];
  export const xit: Global.GlobalAdditions['xit'];
  export const xtest: Global.GlobalAdditions['xtest'];
  export const describe: Global.GlobalAdditions['describe'];
  export const xdescribe: Global.GlobalAdditions['xdescribe'];
  export const fdescribe: Global.GlobalAdditions['fdescribe'];
  export const beforeAll: Global.GlobalAdditions['beforeAll'];
  export const beforeEach: Global.GlobalAdditions['beforeEach'];
  export const afterEach: Global.GlobalAdditions['afterEach'];
  export const afterAll: Global.GlobalAdditions['afterAll'];
}

When this type declaration is included in the tsconfig via:

...
    "typeRoots": [
      "./node_modules/@types",
      "./types"
    ],
    "types": [
      "jest",
      "tslib",
      "node",
      "jest-matcher-fix"
    ]
...

using expect from @jest/globals no longer breaks existing expect extensions.

@mrazauskas
Copy link
Contributor

@johncrim Looking at your reproduction repo it seems like you were experimenting to find the best solution. I was just wondering to ask if you did try the latest alpha release too? After #12344, it should be trivial to extend Matchers interface. Here is working example: https://github.com/facebook/jest/tree/main/examples/expect-extend

I just though you might be interested to give it a try. It would be nice to hear if all works as expected in real world. Or perhaps any further improvement is necessary?

@johncrim
Copy link
Author

johncrim commented Feb 17, 2022

@mrazauskas - Thank you for the update. I did not see that there was a fix for this in the alpha until your comment, but I will try it out and follow up.

This example certainly looks great:
https://github.com/facebook/jest/blob/main/examples/expect-extend/toBeWithinRange.ts

One thing that I'll have to look out for is whether each of the libraries will need to import { expect } from '@jest/globals'; before calling expect.extend(), and whether doing so is backwards compatible for dependees that have not updated to ESM.

@SimenB
Copy link
Member

SimenB commented Feb 18, 2022

The plan is to make @types/jest import from expect instead of defining everything itself, at which point it should work seamlessly

@SimenB
Copy link
Member

SimenB commented Feb 18, 2022

Let's track in #12424

@github-actions
Copy link

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.
Please note this issue tracker is not a help forum. We recommend using StackOverflow or our discord channel for questions.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Mar 21, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

3 participants