From 300e8dbf9d8cd6473d0f76974d187a8fa5e932b4 Mon Sep 17 00:00:00 2001 From: Joshua <31038284+joshuajaco@users.noreply.github.com> Date: Sun, 10 Mar 2024 21:34:39 +0100 Subject: [PATCH] Add expectation message formatting --- .github/workflows/ci.yml | 2 + README.md | 75 ++++++++++++++++ package-lock.json | 86 ++++++++++++++++-- package.json | 3 +- src/ExpectationMessage.ts | 148 +++++++++++++++++++++++++++++++ src/MockServer.ts | 20 +++-- src/index.ts | 1 + src/matchRequest.ts | 13 +-- tests/ExpectationMessage.test.ts | 110 +++++++++++++++++++++++ 9 files changed, 438 insertions(+), 20 deletions(-) create mode 100644 src/ExpectationMessage.ts create mode 100644 tests/ExpectationMessage.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f259018..5bfc0b5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,8 @@ jobs: - run: npm run lint - run: npm run build - run: npm run test + env: + FORCE_COLOR: 2 - name: Coveralls uses: coverallsapp/github-action@v2 with: diff --git a/README.md b/README.md index 25eb814..fd2eb98 100644 --- a/README.md +++ b/README.md @@ -232,9 +232,13 @@ test("custom assertion", async () => { - [`calls()`](#calls-readonly-call) - [`hasBeenCalledWith()`](#hasbeencalledwithmatcher-boolean) - [`hasBeenCalledTimes()`](#hasbeencalledtimestimes-matcher-boolean) + - [`countCalls()`](#countcallsmatcher-number) - [`reset()`](#reset-void) - [`resetMocks()`](#resetmocks-void) - [`resetCalls()`](#resetcalls-void) +- [`ExpectationMessage`](#expectationmessage) + - [`hasBeenCalledWith()`](#hasbeencalledwithmockserver-matcher-string) + - [`hasBeenCalledTimes()`](#hasbeencalledtimesmockserver-times-matcher-string) - [`Options`](#options) - [`Request`](#request) - [`Matcher`](#matcher) @@ -575,6 +579,8 @@ If `matcher` is a `string` or `RegExp`, it will be used to match the request pat Returns `true` if the route has been called `times` times with the given `matcher`, `false` otherwise. +#### Example + ```ts mockServer.get("/test", { status: 200 }); @@ -589,6 +595,32 @@ console.log(mockServer.hasBeenCalledTimes(1, { path: "/test" })); // true --- +### `countCalls(matcher): number` + +Count the number of times the server was called with the given `matcher`. + +| Param | Type | Default | +| ------- | --------------------------------------------- | ------- | +| matcher | `string` \| `RegExp` \| [`Matcher`](#matcher) | - | + +If `matcher` is a `string` or `RegExp`, it will be used to match the request path. + +Returns the number of times the server has been called with the given `matcher`. + +#### Example + +```ts +mockServer.get("/test", { status: 200 }); + +console.log(mockServer.countCalls({ path: "/test" })); // 0 + +await fetch("http://localhost:3000/test"); + +console.log(mockServer.countCalls({ path: "/test" })); // 1 +``` + +--- + ### `reset(): void` Reset all mocks and calls. @@ -650,6 +682,49 @@ mockServer.resetCalls(); console.log(mockServer.calls()); // [] ``` +## `ExpectationMessage` + +### `hasBeenCalledWith(mockServer, matcher): string` + +Format an expectation message for [`hasBeenCalledWith()`](#hasbeencalledwithmatcher-boolean). + +| Param | Type | Default | +| ---------- | --------------------------- | ------- | +| mockServer | [`MockServer`](#mockserver) | - | +| matcher | [`Matcher`](#matcher) | - | + +Returns a string with the formatted expectation message. + +#### Example + +```ts +if (!mockServer.hasBeenCalledWith(matcher)) { + throw new Error(ExpectationMessage.hasBeenCalledWith(mockServer, matcher)); +} +``` + +### `hasBeenCalledTimes(mockServer, times, matcher): string` + +Format an expectation message for [`hasBeenCalledTimes()`](#hasbeencalledtimestimes-matcher-boolean). + +| Param | Type | Default | +| ---------- | --------------------------- | ------- | +| mockServer | [`MockServer`](#mockserver) | - | +| times | number | - | +| matcher | [`Matcher`](#matcher) | - | + +Returns a string with the formatted expectation message. + +#### Example + +```ts +if (!mockServer.hasBeenCalledTimes(mockServer, 2, matcher)) { + throw new Error( + ExpectationMessage.hasBeenCalledTimes(mockServer, 2, matcher), + ); +} +``` + ## `Options` Object with the following properties: diff --git a/package-lock.json b/package-lock.json index d370849..9e20e73 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,8 @@ "@types/express": "^4.17.21", "body-parser": "^1.20.2", "deep-equal": "^2.2.3", - "express": "^4.18.2" + "express": "^4.18.2", + "jest-diff": "^29.7.0" }, "devDependencies": { "@types/deep-equal": "^1.0.4", @@ -873,6 +874,17 @@ "node": ">=8" } }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", @@ -966,6 +978,11 @@ "node": ">=14" } }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==" + }, "node_modules/@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -1374,7 +1391,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -1633,7 +1649,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -1669,7 +1684,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -1680,8 +1694,7 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/commondir": { "version": "1.0.1", @@ -1889,6 +1902,14 @@ "node": ">=0.3.1" } }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -2733,7 +2754,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "engines": { "node": ">=8" } @@ -3354,6 +3374,28 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4129,6 +4171,30 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/process-on-spawn": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", @@ -4218,6 +4284,11 @@ "node": ">= 0.8" } }, + "node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, "node_modules/regexp.prototype.flags": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", @@ -4722,7 +4793,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "dependencies": { "has-flag": "^4.0.0" }, diff --git a/package.json b/package.json index 236745a..5f762d9 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,8 @@ "@types/express": "^4.17.21", "body-parser": "^1.20.2", "deep-equal": "^2.2.3", - "express": "^4.18.2" + "express": "^4.18.2", + "jest-diff": "^29.7.0" }, "devDependencies": { "@types/deep-equal": "^1.0.4", diff --git a/src/ExpectationMessage.ts b/src/ExpectationMessage.ts new file mode 100644 index 0000000..94a0397 --- /dev/null +++ b/src/ExpectationMessage.ts @@ -0,0 +1,148 @@ +import { diff } from "jest-diff"; +import { MockServer } from "./MockServer"; +import { + matchBody, + Matcher, + MatcherObj, + matchHeaders, + matchMethod, + matchPath, + matchQuery, + Request, +} from "./matchRequest"; + +/** + * @see [Documentation]{@link https://github.com/joshuajaco/mocaron#expectationmessage} + */ +export const ExpectationMessage = { + /** + * Format an expectation message for [`hasBeenCalledWith()`]{@link https://github.com/joshuajaco/mocaron#hasbeencalledwithmatcher-boolean}. + * @param {MockServer} mockServer The mock server instance + * @param {string | RegExp | Matcher} matcher If matcher is a `string` or `RegExp`, it will be used to match the request path + * @returns {string} the formatted expectation message + * @see [Documentation]{@link https://github.com/joshuajaco/mocaron#hasbeencalledwithmockserver-matcher-string} + * @example + * ExpectationMessage.hasBeenCalledWith(mockServer, matcher); + */ + hasBeenCalledWith(mockServer: MockServer, matcher: Matcher): string { + return `Expected 'mockServer' to have been called with matcher:\n${formatMatcher(matcher)}\n\n${formatDiffs(mockServer, matcher)}`; + }, + + /** + * Format an expectation message for [`hasBeenCalledTimes()`]{@link https://github.com/joshuajaco/mocaron#hasbeencalledtimestimes-matcher-boolean}. + * @param {MockServer} mockServer The mock server instance + * @param {number} times The number of times the mock server should have been called + * @param {string | RegExp | Matcher} matcher If matcher is a `string` or `RegExp`, it will be used to match the request path + * @returns {string} the formatted expectation message + * @see [Documentation]{@link https://github.com/joshuajaco/mocaron#hasbeencalledtimesmockserver-times-matcher-string} + * @example + * ExpectationMessage.hasBeenCalledTimes(mockServer, 1, matcher); + */ + hasBeenCalledTimes( + mockServer: MockServer, + times: number, + matcher: Matcher, + ): string { + const actualTimes = mockServer.countCalls(matcher); + + if (actualTimes > 0) { + return `Expected 'mockServer' to have been called ${times} times with matcher:\n${formatMatcher(matcher)}\n\nActual calls: ${actualTimes}`; + } + + return this.hasBeenCalledWith(mockServer, matcher); + }, +}; + +function formatMatcher(matcher: Matcher) { + if (typeof matcher === "function") return matcher.toString(); + return JSON.stringify(matcher, null, 2); +} + +function formatDiffs(mockServer: MockServer, matcher: Matcher) { + if (typeof matcher === "function") return "No diff (matcher is a function)"; + + return mockServer + .calls() + .map(({ request }) => [request, score(matcher, request)] as const) + .toSorted(([, scoreA], [, scoreB]) => scoreB - scoreA) + .map( + ([request]) => + `${request.method} ${request.path}:\n${formatDiff(matcher, request)}`, + ) + .join("\n\n"); +} + +function formatDiff(matcher: MatcherObj, request: Request) { + const req = sanitizeRequest(request); + + const actual: MatcherObj = filterKeys( + req, + Object.keys(matcher) as Array, + ); + + if (matcher.query) { + actual.query = filterKeys(req.query, Object.keys(matcher.query)); + } + + if (matcher.headers) { + actual.headers = filterKeys(req.headers, Object.keys(matcher.headers)); + } + + return diff(matcher, actual); +} + +function score(matcher: MatcherObj, request: Request): number { + let maxPoints = 0; + let points = 0; + + if (matcher.method) { + maxPoints += 1; + if (matchMethod(matcher, request)) points += 1; + } + + if (matcher.path) { + maxPoints += 3; + if (matchPath(matcher, request)) points += 3; + } + + if (matcher.query) { + maxPoints += 3; + if (matchQuery(matcher, request)) points += 3; + } + + if (matcher.headers) { + maxPoints += 2; + if (matchHeaders(matcher, request)) points += 2; + } + + if (matcher.body) { + maxPoints += 4; + if (matchBody(matcher, request)) points += 4; + } + + return (points / maxPoints) * 100; +} + +type SanitizedRequest = Required> & MatcherObj; + +function sanitizeRequest(request: Request): SanitizedRequest { + return { + method: request.method, + path: request.path, + query: request.query, + headers: request.headers, + body: request.body ? tryParse(request.body.toString()) : undefined, + }; +} + +function tryParse(body: string): string | Record { + try { + return JSON.parse(body); + } catch { + return body; + } +} + +function filterKeys(obj: T, keys: Array): Partial { + return Object.fromEntries(keys.map((key) => [key, obj[key]])) as Partial; +} diff --git a/src/MockServer.ts b/src/MockServer.ts index c34d223..1fd3a2a 100644 --- a/src/MockServer.ts +++ b/src/MockServer.ts @@ -368,6 +368,20 @@ export class MockServer { return this.#calls.some(({ request }) => matchRequest(resolved, request)); } + /** + * Count the number of times the server was called with the given `matcher`. + * @param {string | RegExp | Matcher} matcher - If matcher is a `string` or `RegExp`, it will be used to match the request path + * @returns {number} number of times the server has been called with the given `matcher` + * @see [Documentation]{@link https://github.com/joshuajaco/mocaron#countcallsmatcher-number} + * @example + * mockServer.countCalls({ path: "/test" }); + */ + public countCalls(matcher: string | RegExp | Matcher): number { + const resolved = this.#resolvePathMatcher(matcher); + return this.#calls.filter(({ request }) => matchRequest(resolved, request)) + .length; + } + /** * Check if the route has been called a certain number of times with the given `matcher`. * @param {number} times @@ -381,11 +395,7 @@ export class MockServer { times: number, matcher: string | RegExp | Matcher, ): boolean { - const resolved = this.#resolvePathMatcher(matcher); - return ( - this.#calls.filter(({ request }) => matchRequest(resolved, request)) - .length === times - ); + return this.countCalls(matcher) === times; } /** diff --git a/src/index.ts b/src/index.ts index 995d702..10f3fa9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,3 +9,4 @@ export type { Call, Options, } from "./MockServer"; +export { ExpectationMessage } from "./ExpectationMessage"; diff --git a/src/matchRequest.ts b/src/matchRequest.ts index 6fb55f8..779fd7c 100644 --- a/src/matchRequest.ts +++ b/src/matchRequest.ts @@ -1,5 +1,6 @@ import type express from "express"; import deepEqual from "deep-equal"; +import type http from "node:http"; /** * request the server was called with @@ -30,7 +31,7 @@ export type MatcherObj = { * Headers explicitly set to `undefined` will not match when provided * @see [Documentation]{@link https://github.com/joshuajaco/mocaron#matcherobj} */ - headers?: Record; + headers?: http.IncomingHttpHeaders; /** * body to match against - * If an `object` is given it will be compared to the request body parsed as JSON @@ -64,13 +65,13 @@ export function matchRequest(matcher: Matcher, req: Request): boolean { ); } -function matchMethod(matcher: MatcherObj, req: Request) { +export function matchMethod(matcher: MatcherObj, req: Request) { return ( !matcher.method || matcher.method.toLowerCase() === req.method.toLowerCase() ); } -function matchPath(matcher: MatcherObj, req: Request) { +export function matchPath(matcher: MatcherObj, req: Request) { return ( !matcher.path || (matcher.path instanceof RegExp @@ -79,21 +80,21 @@ function matchPath(matcher: MatcherObj, req: Request) { ); } -function matchQuery(matcher: MatcherObj, req: Request) { +export function matchQuery(matcher: MatcherObj, req: Request) { if (!matcher.query) return true; return Object.entries(matcher.query).every(([k, v]) => deepEqual(req.query[k], v, { strict: true }), ); } -function matchHeaders(matcher: MatcherObj, req: Request) { +export function matchHeaders(matcher: MatcherObj, req: Request) { if (!matcher.headers) return true; return Object.entries(matcher.headers).every( ([k, v]) => req.headers[k.toLowerCase()] === v, ); } -function matchBody(matcher: MatcherObj, req: Request) { +export function matchBody(matcher: MatcherObj, req: Request) { if (matcher.body == null) return true; if (!req.body) return false; diff --git a/tests/ExpectationMessage.test.ts b/tests/ExpectationMessage.test.ts new file mode 100644 index 0000000..f54f026 --- /dev/null +++ b/tests/ExpectationMessage.test.ts @@ -0,0 +1,110 @@ +import { after, afterEach, before, describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { getPort } from "get-port-please"; +import { MockServer, ExpectationMessage } from "../src"; + +describe("ExpectationMessage", () => { + let port: number; + let host: string; + let mockServer: MockServer; + + before(async () => { + port = await getPort(3000); + host = `http://localhost:${port}`; + mockServer = new MockServer({ port }); + await mockServer.start(); + }); + + afterEach(() => mockServer.reset()); + after(() => mockServer.stop()); + + describe(".hasBeenCalledWith", () => { + it("formats message", async () => { + mockServer + .get("/foo", 200) + .post("/bar", 200) + .get("/bar", 200) + .patch("/bar", 200) + .delete("/bar", 200); + + await fetch(`${host}/foo`); + + await fetch(`${host}/foo?foo=bar`); + + await fetch(`${host}/bar`, { + method: "POST", + body: JSON.stringify({ some: "content" }), + }); + + await fetch(`${host}/bar`); + + await fetch(`${host}/bar`, { + method: "PATCH", + body: "string", + headers: { Authorization: "1" }, + }); + + await fetch(`${host}/bar`, { method: "DELETE" }); + + const matcher = { + method: "POST", + path: "/foo", + query: { foo: "bar" }, + headers: { Authorization: "1" }, + body: { some: "content" }, + }; + + assert.equal( + ExpectationMessage.hasBeenCalledWith(mockServer, matcher), + `Expected 'mockServer' to have been called with matcher:\n{\n "method": "POST",\n "path": "/foo",\n "query": {\n "foo": "bar"\n },\n "headers": {\n "Authorization": "1"\n },\n "body": {\n "some": "content"\n }\n}\n\nGET /foo:\n\x1B[32m- Expected\x1B[39m\n\x1B[31m+ Received\x1B[39m\n\n\x1B[2m Object {\x1B[22m\n\x1B[32m- "body": Object {\x1B[39m\n\x1B[32m- "some": "content",\x1B[39m\n\x1B[32m- },\x1B[39m\n\x1B[31m+ "body": undefined,\x1B[39m\n\x1B[2m "headers": Object {\x1B[22m\n\x1B[32m- "Authorization": "1",\x1B[39m\n\x1B[31m+ "Authorization": undefined,\x1B[39m\n\x1B[2m },\x1B[22m\n\x1B[32m- "method": "POST",\x1B[39m\n\x1B[31m+ "method": "GET",\x1B[39m\n\x1B[2m "path": "/foo",\x1B[22m\n\x1B[2m "query": Object {\x1B[22m\n\x1B[2m "foo": "bar",\x1B[22m\n\x1B[2m },\x1B[22m\n\x1B[2m }\x1B[22m\n\nPOST /bar:\n\x1B[32m- Expected\x1B[39m\n\x1B[31m+ Received\x1B[39m\n\n\x1B[2m Object {\x1B[22m\n\x1B[2m "body": Object {\x1B[22m\n\x1B[2m "some": "content",\x1B[22m\n\x1B[2m },\x1B[22m\n\x1B[2m "headers": Object {\x1B[22m\n\x1B[32m- "Authorization": "1",\x1B[39m\n\x1B[31m+ "Authorization": undefined,\x1B[39m\n\x1B[2m },\x1B[22m\n\x1B[2m "method": "POST",\x1B[22m\n\x1B[32m- "path": "/foo",\x1B[39m\n\x1B[31m+ "path": "/bar",\x1B[39m\n\x1B[2m "query": Object {\x1B[22m\n\x1B[32m- "foo": "bar",\x1B[39m\n\x1B[31m+ "foo": undefined,\x1B[39m\n\x1B[2m },\x1B[22m\n\x1B[2m }\x1B[22m\n\nGET /foo:\n\x1B[32m- Expected\x1B[39m\n\x1B[31m+ Received\x1B[39m\n\n\x1B[2m Object {\x1B[22m\n\x1B[32m- "body": Object {\x1B[39m\n\x1B[32m- "some": "content",\x1B[39m\n\x1B[32m- },\x1B[39m\n\x1B[31m+ "body": undefined,\x1B[39m\n\x1B[2m "headers": Object {\x1B[22m\n\x1B[32m- "Authorization": "1",\x1B[39m\n\x1B[31m+ "Authorization": undefined,\x1B[39m\n\x1B[2m },\x1B[22m\n\x1B[32m- "method": "POST",\x1B[39m\n\x1B[31m+ "method": "GET",\x1B[39m\n\x1B[2m "path": "/foo",\x1B[22m\n\x1B[2m "query": Object {\x1B[22m\n\x1B[32m- "foo": "bar",\x1B[39m\n\x1B[31m+ "foo": undefined,\x1B[39m\n\x1B[2m },\x1B[22m\n\x1B[2m }\x1B[22m\n\nPATCH /bar:\n\x1B[32m- Expected\x1B[39m\n\x1B[31m+ Received\x1B[39m\n\n\x1B[2m Object {\x1B[22m\n\x1B[32m- "body": Object {\x1B[39m\n\x1B[32m- "some": "content",\x1B[39m\n\x1B[32m- },\x1B[39m\n\x1B[31m+ "body": "string",\x1B[39m\n\x1B[2m "headers": Object {\x1B[22m\n\x1B[32m- "Authorization": "1",\x1B[39m\n\x1B[31m+ "Authorization": undefined,\x1B[39m\n\x1B[2m },\x1B[22m\n\x1B[32m- "method": "POST",\x1B[39m\n\x1B[32m- "path": "/foo",\x1B[39m\n\x1B[31m+ "method": "PATCH",\x1B[39m\n\x1B[31m+ "path": "/bar",\x1B[39m\n\x1B[2m "query": Object {\x1B[22m\n\x1B[32m- "foo": "bar",\x1B[39m\n\x1B[31m+ "foo": undefined,\x1B[39m\n\x1B[2m },\x1B[22m\n\x1B[2m }\x1B[22m\n\nGET /bar:\n\x1B[32m- Expected\x1B[39m\n\x1B[31m+ Received\x1B[39m\n\n\x1B[2m Object {\x1B[22m\n\x1B[32m- "body": Object {\x1B[39m\n\x1B[32m- "some": "content",\x1B[39m\n\x1B[32m- },\x1B[39m\n\x1B[31m+ "body": undefined,\x1B[39m\n\x1B[2m "headers": Object {\x1B[22m\n\x1B[32m- "Authorization": "1",\x1B[39m\n\x1B[31m+ "Authorization": undefined,\x1B[39m\n\x1B[2m },\x1B[22m\n\x1B[32m- "method": "POST",\x1B[39m\n\x1B[32m- "path": "/foo",\x1B[39m\n\x1B[31m+ "method": "GET",\x1B[39m\n\x1B[31m+ "path": "/bar",\x1B[39m\n\x1B[2m "query": Object {\x1B[22m\n\x1B[32m- "foo": "bar",\x1B[39m\n\x1B[31m+ "foo": undefined,\x1B[39m\n\x1B[2m },\x1B[22m\n\x1B[2m }\x1B[22m\n\nDELETE /bar:\n\x1B[32m- Expected\x1B[39m\n\x1B[31m+ Received\x1B[39m\n\n\x1B[2m Object {\x1B[22m\n\x1B[32m- "body": Object {\x1B[39m\n\x1B[32m- "some": "content",\x1B[39m\n\x1B[32m- },\x1B[39m\n\x1B[31m+ "body": undefined,\x1B[39m\n\x1B[2m "headers": Object {\x1B[22m\n\x1B[32m- "Authorization": "1",\x1B[39m\n\x1B[31m+ "Authorization": undefined,\x1B[39m\n\x1B[2m },\x1B[22m\n\x1B[32m- "method": "POST",\x1B[39m\n\x1B[32m- "path": "/foo",\x1B[39m\n\x1B[31m+ "method": "DELETE",\x1B[39m\n\x1B[31m+ "path": "/bar",\x1B[39m\n\x1B[2m "query": Object {\x1B[22m\n\x1B[32m- "foo": "bar",\x1B[39m\n\x1B[31m+ "foo": undefined,\x1B[39m\n\x1B[2m },\x1B[22m\n\x1B[2m }\x1B[22m`, + ); + + assert.equal( + ExpectationMessage.hasBeenCalledWith(mockServer, { path: "/foobar" }), + `Expected 'mockServer' to have been called with matcher:\n{\n "path": "/foobar"\n}\n\nGET /foo:\n\x1B[32m- Expected\x1B[39m\n\x1B[31m+ Received\x1B[39m\n\n\x1B[2m Object {\x1B[22m\n\x1B[32m- "path": "/foobar",\x1B[39m\n\x1B[31m+ "path": "/foo",\x1B[39m\n\x1B[2m }\x1B[22m\n\nGET /foo:\n\x1B[32m- Expected\x1B[39m\n\x1B[31m+ Received\x1B[39m\n\n\x1B[2m Object {\x1B[22m\n\x1B[32m- "path": "/foobar",\x1B[39m\n\x1B[31m+ "path": "/foo",\x1B[39m\n\x1B[2m }\x1B[22m\n\nPOST /bar:\n\x1B[32m- Expected\x1B[39m\n\x1B[31m+ Received\x1B[39m\n\n\x1B[2m Object {\x1B[22m\n\x1B[32m- "path": "/foobar",\x1B[39m\n\x1B[31m+ "path": "/bar",\x1B[39m\n\x1B[2m }\x1B[22m\n\nGET /bar:\n\x1B[32m- Expected\x1B[39m\n\x1B[31m+ Received\x1B[39m\n\n\x1B[2m Object {\x1B[22m\n\x1B[32m- "path": "/foobar",\x1B[39m\n\x1B[31m+ "path": "/bar",\x1B[39m\n\x1B[2m }\x1B[22m\n\nPATCH /bar:\n\x1B[32m- Expected\x1B[39m\n\x1B[31m+ Received\x1B[39m\n\n\x1B[2m Object {\x1B[22m\n\x1B[32m- "path": "/foobar",\x1B[39m\n\x1B[31m+ "path": "/bar",\x1B[39m\n\x1B[2m }\x1B[22m\n\nDELETE /bar:\n\x1B[32m- Expected\x1B[39m\n\x1B[31m+ Received\x1B[39m\n\n\x1B[2m Object {\x1B[22m\n\x1B[32m- "path": "/foobar",\x1B[39m\n\x1B[31m+ "path": "/bar",\x1B[39m\n\x1B[2m }\x1B[22m`, + ); + + assert.equal( + ExpectationMessage.hasBeenCalledWith(mockServer, { method: "PUT" }), + `Expected 'mockServer' to have been called with matcher:\n{\n "method": "PUT"\n}\n\nGET /foo:\n\x1B[32m- Expected\x1B[39m\n\x1B[31m+ Received\x1B[39m\n\n\x1B[2m Object {\x1B[22m\n\x1B[32m- "method": "PUT",\x1B[39m\n\x1B[31m+ "method": "GET",\x1B[39m\n\x1B[2m }\x1B[22m\n\nGET /foo:\n\x1B[32m- Expected\x1B[39m\n\x1B[31m+ Received\x1B[39m\n\n\x1B[2m Object {\x1B[22m\n\x1B[32m- "method": "PUT",\x1B[39m\n\x1B[31m+ "method": "GET",\x1B[39m\n\x1B[2m }\x1B[22m\n\nPOST /bar:\n\x1B[32m- Expected\x1B[39m\n\x1B[31m+ Received\x1B[39m\n\n\x1B[2m Object {\x1B[22m\n\x1B[32m- "method": "PUT",\x1B[39m\n\x1B[31m+ "method": "POST",\x1B[39m\n\x1B[2m }\x1B[22m\n\nGET /bar:\n\x1B[32m- Expected\x1B[39m\n\x1B[31m+ Received\x1B[39m\n\n\x1B[2m Object {\x1B[22m\n\x1B[32m- "method": "PUT",\x1B[39m\n\x1B[31m+ "method": "GET",\x1B[39m\n\x1B[2m }\x1B[22m\n\nPATCH /bar:\n\x1B[32m- Expected\x1B[39m\n\x1B[31m+ Received\x1B[39m\n\n\x1B[2m Object {\x1B[22m\n\x1B[32m- "method": "PUT",\x1B[39m\n\x1B[31m+ "method": "PATCH",\x1B[39m\n\x1B[2m }\x1B[22m\n\nDELETE /bar:\n\x1B[32m- Expected\x1B[39m\n\x1B[31m+ Received\x1B[39m\n\n\x1B[2m Object {\x1B[22m\n\x1B[32m- "method": "PUT",\x1B[39m\n\x1B[31m+ "method": "DELETE",\x1B[39m\n\x1B[2m }\x1B[22m`, + ); + }); + + it("formats message with function matcher", async () => { + const matcher = () => true; + + assert.equal( + ExpectationMessage.hasBeenCalledWith(mockServer, matcher), + "Expected 'mockServer' to have been called with matcher:\n() => true\n\nNo diff (matcher is a function)", + ); + }); + }); + + describe(".hasBeenCalledTimes", () => { + it("formats message", async () => { + mockServer.get("/foo", 200); + + await fetch(`${host}/foo`); + + const matcher = { path: "/foo" }; + + assert.equal( + ExpectationMessage.hasBeenCalledTimes(mockServer, 2, matcher), + `Expected 'mockServer' to have been called 2 times with matcher:\n{\n "path": "/foo"\n}\n\nActual calls: 1`, + ); + }); + + it("formats message without calls", async () => { + mockServer.get("/foo", 200); + + await fetch(`${host}/foo`); + + const matcher = { path: "/bar" }; + + assert.equal( + ExpectationMessage.hasBeenCalledTimes(mockServer, 2, matcher), + `Expected 'mockServer' to have been called with matcher:\n{\n "path": "/bar"\n}\n\nGET /foo:\n\x1B[32m- Expected\x1B[39m\n\x1B[31m+ Received\x1B[39m\n\n\x1B[2m Object {\x1B[22m\n\x1B[32m- "path": "/bar",\x1B[39m\n\x1B[31m+ "path": "/foo",\x1B[39m\n\x1B[2m }\x1B[22m`, + ); + }); + }); +});