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

fix: .resolves and .rejects expectations #1300

Merged
merged 3 commits into from May 13, 2022

Conversation

davidmyersdev
Copy link
Contributor

Summary

The promise helpers, .resolves and .rejects, were throwing errors when the returned object could not be serialized. Additionally, the .resolves helper did not support a function argument even though a function was expected.

Examples

1. Promise resolution

With a happy-path test for resolving a promise...

// example1.test.ts
await expect(() => Promise.resolve(1)).resolves.toEqual(1)
Before

The following error occurs.

TypeError: obj.then is not a function
After

The test passes.

1 passed (1)

2. Dynamic imports

With an alternate-path test expecting a dynamic import (promise) to error...

// example2.ts
export default {}
// example2.test.ts
await expect(() => import('./example')).rejects.toThrowError()
Before

The following error occurs.

TypeError: Cannot convert object to primitive value
After

The test passes.

1 passed (1)

@netlify
Copy link

netlify bot commented May 12, 2022

Deploy Preview for vitest-dev ready!

Name Link
🔨 Latest commit 05eedef
🔍 Latest deploy log https://app.netlify.com/sites/vitest-dev/deploys/627e5f92d87f20000994c014
😎 Deploy Preview https://deploy-preview-1300--vitest-dev.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site settings.

@sheremet-va
Copy link
Member

Additionally, the .resolves helper did not support a function argument even though a function was expected.

What do you mean a function was expected? Promise is expected. Function can be expected for .rejects, because it's a common pattern to wrap throwing in a function, and jest allows that, but I don't like putting it in .resolves.

@davidmyersdev
Copy link
Contributor Author

@sheremet-va my apologies. I meant to say a function should be allowed to maintain jest compatibility.

@sheremet-va
Copy link
Member

sheremet-va commented May 12, 2022

@sheremet-va my apologies. I meant to say a function should be allowed to maintain jest compatibility.

But your example will fail in jest. resolves expects a Promise, and should always expect a promise, not a function that returns a Promise. This is just extra steps, that don't do any favours.

The error in Jest is:

Matcher error: received value must be a promise

@davidmyersdev
Copy link
Contributor Author

@sheremet-va should it be removed from .rejects then? Either way, the two methods are currently inconsistent (I pulled that line from .rejects).

@sheremet-va
Copy link
Member

@sheremet-va should it be removed from .rejects then? Either way, the two methods are currently inconsistent (I pulled that line from .rejects).

I know that you pulled that line from there. It is there for jest compatibility. As I said, it's a very common pattern that jest supports, and by extension, Vitest. Just remove the line from .resolves and PR can be merged.

two methods are currently inconsistent

Being consistent is behaving the same as Jest does at this point of Vitest development. Maybe later, when we will want to diverge from that we will talk.

@sheremet-va
Copy link
Member

If you really want to help, I would advise adding a type guard inside .resolves/.rejects that makes sure that passed value is a Promise. To not get a weird error "obj.then is not a function".

@davidmyersdev
Copy link
Contributor Author

@sheremet-va I see the difference now. To be consistent with Jest, do you want me to just add that type guard to .resolves for now?

@sheremet-va
Copy link
Member

@sheremet-va To be consistent with Jest, do you want me to just add that type guard to .resolves for now?

We can check inside both. In .resolves check the obj value, and in .rejects check that wrapper is a promise.

@davidmyersdev davidmyersdev force-pushed the fix-async-expectations branch 2 times, most recently from 984be7f to 490989e Compare May 12, 2022 19:42
The promise helpers, .resolves and .rejects, were throwing errors when the returned object could not be serialized. Specifically, this would happen when using dynamic imports (e.g. `expect(() => import('./some_module')).resolves`). Additionally, the .resolves helper did not support a function argument even though a function was expected.
@davidmyersdev
Copy link
Contributor Author

@sheremet-va I just pushed up another related commit. The reason I thought a function was required was due to the specific test I had written.

// make sure promise resolves
await expect(Promise.resolve(1)).resolves.not.toThrow()

This test caused the following error (hence why I thought it was required originally).

AssertionError: expected 1 to be a function

@antfu antfu changed the title Fix .resolves and .rejects expectations fix: .resolves and .rejects expectations May 13, 2022
@sheremet-va
Copy link
Member

The reason I thought a function was required was due to the specific test I had written.

.resolves is just a hellper that passes awaited value down to the matcher, so you wouldn't have to do expect(await promise). So, in your case it should be expect(Promise.resolve(() => ({}))).

I think it was created before async/await syntax, so it was easier to do then doing promise.then(v => expect(v)). This exist mostly for compatibility with jest.

.rejects is the most useful, because you don't need to write try/catch block.

@sheremet-va
Copy link
Member

I wanted to do some changes to your PR, but cannot commit. Can you grant access? Specifically, you didn't cover this cases:

    await expect((async () => new Error('msg'))()).resolves.not.toThrow() // calls chai assertion
    await expect((async () => new Error('msg'))()).resolves.not.toThrow(Error) // calls our assertion
    await expect((async () => () => {
      throw new Error('msg')
    })()).resolves.toThrow()
    await expect((async () => () => {
      return new Error('msg')
    })()).resolves.not.toThrow()
    await expect((async () => () => {
      return new Error('msg')
    })()).resolves.not.toThrow(Error)
  })

  it('resolves trows chai', async () => {
    const assertion = async () => {
      await expect((async () => new Error('msg'))()).resolves.toThrow()
    }

    await expect(assertion).rejects.toThrowError('expected promise to throw an error, but it didn\'t')
  })

  it('resolves trows jest', async () => {
    const assertion = async () => {
      await expect((async () => new Error('msg'))()).resolves.toThrow(Error)
    }

    await expect(assertion).rejects.toThrowError('expected promise to throw an error, but it didn\'t')
  })

@davidmyersdev
Copy link
Contributor Author

@sheremet-va you should be able to commit now.

@antfu antfu merged commit 789cc93 into vitest-dev:main May 13, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants