Promise Rejections

The first thing to realize is that with the .rejects.toThrow() API we are creating a chain. The rejects part becomes a property that returns another object with the .toThrow() function. I reflect this in the Assertions type straight away:
interface Assertions {
	toBe(expected: unknown): void
	rejects: {
		toThrow(expected: Error): Promise<void>
	}
}
Note that the .rejects.toThrow() assertion will be returning a Promise. Annotate its return type accordingly (Promise<void>).
Next, I extend the object returned from the globalThis.expect function to have the rejects property. In that property, I define a new toThrow function.
globalThis.expect = function (actual: unknown) {
	return {
		toBe(expected: unknown) {
			if (actual !== expected) {
				throw new Error(`Expected ${actual} to equal to ${expected}`)
			}
		},
		rejects: {
			toThrow(expected) {
Although the actual value will be a Promise in our case, we can pass anything to the expect() function. If we assume it's a Promise, and write actual.catch(), TypeScript will kindly warn us that actual is unknown, and it doesn't necessarily have the .then()/.catch() methods a Promise has.
I make sure that the passed actual value is the instance of Promise:
		rejects: {
			toThrow(expected) {
				if (!(actual instanceof Promise)) {
					throw new Error(`Expected ${actual} to be a promise`)
				}
Now that we are always asserting on a Promise, I will make sure it rejects, and compare the expected and the actual error messages once it does.
rejects: {
  toThrow(expected) {
    if (!(actual instanceof Promise)) {
			throw new Error(`Expected ${actual} to be a promise`)
		}

    return actual.catch((error) => {
      if (error.message !== expected.message) {
        throw new Error(`Expected error message to be ${error.message} but got ${expected}`)
      }
    })
  }
}
In the video, I made a mistake at 02:00! The handling of false-positive scenarios must be done via .then(onFulfilled, onRejected) callback, not via the .then().catch() chaining. See the correct implementation below, and feel free to skip to 02:28 to watch the rest of the solution.
To handle the unwanted cases when the actual promise resolves, I will replace the .catch() callback with a single .then() callback, providing it with two arguments:
return actual.catch(onRejected)
return actual.then(onFulfilled, onRejected)
The onFulfilled function will be executed when the actual promise resolves, and I will throw an error if that happens. The onRejected function will be the same we've provided to the .catch() method before.
		rejects: {
			toThrow(expected) {
				if (!(actual instanceof Promise)) {
					throw new Error(`Expected ${actual} to be a promise`)
				}

				return actual.then(
					() => {
						throw new Error(`Expected ${actual} to reject but it didn't`)
					},
					error => {
						if (error.message !== expected.message) {
							throw new Error(
								`Expected ${error.message} to equal to ${expected.message}`,
							)
						}
					},
				)
			},
Using a single .then() callback allow me to handle the promise fulfillment/rejection without introducing a chain. You can learn more about why this is important in this issue.
Finally, I change the test case to use the newly created .rejects.toThrow() assertion and provide the expected error:
test('throws on greeting user with undefined user response', async () => {
	await expect(greetByResponse(undefined)).rejects.toThrow(
		new Error('Failed to greet the user: no user response provided'),
	)
})
Notice that we have to await the expect() call because the .toThrow() function returns a promise.
And I verify that the newly introduced behavior of the greetByResponse() function behaves as intended by running the tests:
npx tsx --import ./setup.ts greet.test.ts
✓ returns a greeting message for the given name
✓ returns a congratulation message for the given name
✓ throws on greeting user with undefined user response
✓ returns a greeting message for the given user response

Access Denied

You must login or register for the workshop to view the diff.

Check out this video to see how the diff tab works.