diff --git a/.github/renovate.json b/.github/renovate.json index 776c66af68e..a1987ca038d 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -210,10 +210,10 @@ "eslint-plugin-storybook", "eslint-plugin-tailwindcss", "husky", - "jest-extended", "jest-junit", "jest-mock-extended", "jest-preset-angular", + "jest-diff", "lint-staged", "ts-jest" ], diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.spec.ts index 38ec6056d19..1f67dd51c21 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.spec.ts @@ -152,7 +152,7 @@ describe("VaultHeaderV2Component", () => { it("defaults the initial state to true", (done) => { // The initial value of the `state$` variable above is undefined component["initialDisclosureVisibility$"].subscribe((initialVisibility) => { - expect(initialVisibility).toBeTrue(); + expect(initialVisibility).toBe(true); done(); }); diff --git a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts index 7ee15aa833b..526ab2e2579 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts @@ -179,7 +179,7 @@ describe("ViewV2Component", () => { flush(); // Resolve all promises - expect(doAutofill).toHaveBeenCalledOnce(); + expect(doAutofill).toHaveBeenCalledTimes(1); })); it('invokes `copy` when action="copy-username"', fakeAsync(() => { @@ -187,7 +187,7 @@ describe("ViewV2Component", () => { flush(); // Resolve all promises - expect(copy).toHaveBeenCalledOnce(); + expect(copy).toHaveBeenCalledTimes(1); })); it('invokes `copy` when action="copy-password"', fakeAsync(() => { @@ -195,7 +195,7 @@ describe("ViewV2Component", () => { flush(); // Resolve all promises - expect(copy).toHaveBeenCalledOnce(); + expect(copy).toHaveBeenCalledTimes(1); })); it('invokes `copy` when action="copy-totp"', fakeAsync(() => { @@ -203,7 +203,7 @@ describe("ViewV2Component", () => { flush(); // Resolve all promises - expect(copy).toHaveBeenCalledOnce(); + expect(copy).toHaveBeenCalledTimes(1); })); it("closes the popout after a load action", fakeAsync(() => { @@ -218,9 +218,9 @@ describe("ViewV2Component", () => { flush(); // Resolve all promises - expect(doAutofill).toHaveBeenCalledOnce(); + expect(doAutofill).toHaveBeenCalledTimes(1); expect(focusSpy).toHaveBeenCalledWith(99); - expect(closeSpy).toHaveBeenCalledOnce(); + expect(closeSpy).toHaveBeenCalledTimes(1); })); }); }); diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts index 0eb91c6cbe2..e1236be08f9 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts @@ -488,7 +488,7 @@ describe("VaultPopupListFiltersService", () => { state$.next(true); service.filterVisibilityState$.subscribe((filterVisibility) => { - expect(filterVisibility).toBeTrue(); + expect(filterVisibility).toBe(true); done(); }); }); @@ -496,7 +496,7 @@ describe("VaultPopupListFiltersService", () => { it("updates stored filter state", async () => { await service.updateFilterVisibility(false); - expect(update).toHaveBeenCalledOnce(); + expect(update).toHaveBeenCalledTimes(1); // Get callback passed to `update` const updateCallback = update.mock.calls[0][0]; expect(updateCallback()).toBe(false); diff --git a/libs/admin-console/src/common/collections/services/default-vnext-collection.service.spec.ts b/libs/admin-console/src/common/collections/services/default-vnext-collection.service.spec.ts index 4aa54429aad..048a4733948 100644 --- a/libs/admin-console/src/common/collections/services/default-vnext-collection.service.spec.ts +++ b/libs/admin-console/src/common/collections/services/default-vnext-collection.service.spec.ts @@ -91,7 +91,7 @@ describe("DefaultvNextCollectionService", () => { // Assert emitted values expect(result.length).toBe(2); - expect(result).toIncludeAllPartialMembers([ + expect(result).toContainPartialObjects([ { id: collection1.id, name: "DEC_NAME_" + collection1.id, @@ -167,7 +167,7 @@ describe("DefaultvNextCollectionService", () => { const result = await firstValueFrom(collectionService.encryptedCollections$(userId)); expect(result.length).toBe(2); - expect(result).toIncludeAllPartialMembers([ + expect(result).toContainPartialObjects([ { id: collection1.id, name: makeEncString("ENC_NAME_" + collection1.id), @@ -205,7 +205,7 @@ describe("DefaultvNextCollectionService", () => { const result = await firstValueFrom(collectionService.encryptedCollections$(userId)); expect(result.length).toBe(3); - expect(result).toIncludeAllPartialMembers([ + expect(result).toContainPartialObjects([ { id: collection1.id, name: makeEncString("UPDATED_ENC_NAME_" + collection1.id), @@ -230,7 +230,7 @@ describe("DefaultvNextCollectionService", () => { const result = await firstValueFrom(collectionService.encryptedCollections$(userId)); expect(result.length).toBe(1); - expect(result).toIncludeAllPartialMembers([ + expect(result).toContainPartialObjects([ { id: collection1.id, name: makeEncString("ENC_NAME_" + collection1.id), @@ -253,7 +253,7 @@ describe("DefaultvNextCollectionService", () => { const result = await firstValueFrom(collectionService.encryptedCollections$(userId)); expect(result.length).toBe(1); - expect(result).toIncludeAllPartialMembers([ + expect(result).toContainPartialObjects([ { id: newCollection3.id, name: makeEncString("ENC_NAME_" + newCollection3.id), diff --git a/libs/admin-console/test.setup.ts b/libs/admin-console/test.setup.ts index 6be6e7b8dd1..8ab102f2cf4 100644 --- a/libs/admin-console/test.setup.ts +++ b/libs/admin-console/test.setup.ts @@ -1,6 +1,10 @@ import { webcrypto } from "crypto"; + +import { addCustomMatchers } from "@bitwarden/common/spec"; import "jest-preset-angular/setup-jest"; +addCustomMatchers(); + Object.defineProperty(window, "CSS", { value: null }); Object.defineProperty(window, "getComputedStyle", { value: () => { diff --git a/libs/admin-console/tsconfig.json b/libs/admin-console/tsconfig.json index 3d22cb2ec51..4f057fd6af0 100644 --- a/libs/admin-console/tsconfig.json +++ b/libs/admin-console/tsconfig.json @@ -8,6 +8,6 @@ "@bitwarden/key-management": ["../key-management/src"] } }, - "include": ["src", "spec"], + "include": ["src", "spec", "../../libs/common/custom-matchers.d.ts"], "exclude": ["node_modules", "dist"] } diff --git a/libs/common/spec/matchers/index.ts b/libs/common/spec/matchers/index.ts index 44440be5b54..b2e09cc8e92 100644 --- a/libs/common/spec/matchers/index.ts +++ b/libs/common/spec/matchers/index.ts @@ -1,16 +1,12 @@ -import * as matchers from "jest-extended"; - import { toBeFulfilled, toBeResolved, toBeRejected } from "./promise-fulfilled"; import { toAlmostEqual } from "./to-almost-equal"; +import { toContainPartialObjects } from "./to-contain-partial-objects"; import { toEqualBuffer } from "./to-equal-buffer"; export * from "./to-equal-buffer"; export * from "./to-almost-equal"; export * from "./promise-fulfilled"; -// add all jest-extended matchers -expect.extend(matchers); - export function addCustomMatchers() { expect.extend({ toEqualBuffer: toEqualBuffer, @@ -18,6 +14,7 @@ export function addCustomMatchers() { toBeFulfilled: toBeFulfilled, toBeResolved: toBeResolved, toBeRejected: toBeRejected, + toContainPartialObjects, }); } @@ -59,4 +56,9 @@ export interface CustomMatchers { * @returns CustomMatcherResult indicating whether or not the test passed */ toBeRejected(withinMs?: number): Promise; + /** + * Matches if the received array contains all the expected objects using partial matching (expect.objectContaining). + * @param expected An array of partial objects that should be contained in the received array. + */ + toContainPartialObjects(expected: Array): R; } diff --git a/libs/common/spec/matchers/to-contain-partial-objects.spec.ts b/libs/common/spec/matchers/to-contain-partial-objects.spec.ts new file mode 100644 index 00000000000..ab6f90adf17 --- /dev/null +++ b/libs/common/spec/matchers/to-contain-partial-objects.spec.ts @@ -0,0 +1,77 @@ +describe("toContainPartialObjects", () => { + describe("matches", () => { + it("if the array only contains the partial objects", () => { + const actual = [ + { + id: 1, + name: "foo", + }, + { + id: 2, + name: "bar", + }, + ]; + + const expected = [{ id: 1 }, { id: 2 }]; + + expect(actual).toContainPartialObjects(expected); + }); + + it("if the array contains the partial objects and other objects", () => { + const actual = [ + { + id: 1, + name: "foo", + }, + { + id: 2, + name: "bar", + }, + { + id: 3, + name: "baz", + }, + ]; + + const expected = [{ id: 1 }, { id: 2 }]; + + expect(actual).toContainPartialObjects(expected); + }); + }); + + describe("doesn't match", () => { + it("if the array does not contain any partial objects", () => { + const actual = [ + { + id: 1, + name: "foo", + }, + { + id: 2, + name: "bar", + }, + ]; + + const expected = [{ id: 1, name: "Foo" }]; + + expect(actual).not.toContainPartialObjects(expected); + }); + + it("if the array contains some but not all partial objects", () => { + const actual = [ + { + id: 1, + name: "foo", + }, + { + id: 2, + name: "bar", + }, + ]; + + const expected = [{ id: 2 }, { id: 3 }]; + + expect(actual).not.toContainPartialObjects(expected); + }); + }); +}); diff --git a/libs/common/spec/matchers/to-contain-partial-objects.ts b/libs/common/spec/matchers/to-contain-partial-objects.ts new file mode 100644 index 00000000000..f072ca6fba6 --- /dev/null +++ b/libs/common/spec/matchers/to-contain-partial-objects.ts @@ -0,0 +1,31 @@ +import { EOL } from "os"; + +import { diff } from "jest-diff"; + +export const toContainPartialObjects: jest.CustomMatcher = function ( + received: Array, + expected: Array, +) { + const matched = this.equals( + received, + expect.arrayContaining(expected.map((e) => expect.objectContaining(e))), + ); + + if (matched) { + return { + message: () => + "Expected the received array NOT to include partial matches for all expected objects." + + EOL + + diff(expected, received), + pass: true, + }; + } + + return { + message: () => + "Expected the received array to contain partial matches for all expected objects." + + EOL + + diff(expected, received), + pass: false, + }; +}; diff --git a/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.spec.ts b/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.spec.ts index 831cad74155..0b60aef4917 100644 --- a/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.spec.ts +++ b/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.spec.ts @@ -184,7 +184,7 @@ describe("KeyServiceLegacyEncryptorProvider", () => { singleUserId$.complete(); - expect(completed).toBeTrue(); + expect(completed).toBe(true); }); it("completes when `userKey$` emits a falsy value after emitting a truthy value", () => { @@ -199,7 +199,7 @@ describe("KeyServiceLegacyEncryptorProvider", () => { userKey$.next(null); - expect(completed).toBeTrue(); + expect(completed).toBe(true); }); it("completes once `dependencies.singleUserId$` emits and `userKey$` completes", () => { @@ -214,7 +214,7 @@ describe("KeyServiceLegacyEncryptorProvider", () => { userKey$.complete(); - expect(completed).toBeTrue(); + expect(completed).toBe(true); }); }); @@ -445,7 +445,7 @@ describe("KeyServiceLegacyEncryptorProvider", () => { singleOrganizationId$.complete(); - expect(completed).toBeTrue(); + expect(completed).toBe(true); }); it("completes when `orgKeys$` emits a falsy value after emitting a truthy value", () => { @@ -466,7 +466,7 @@ describe("KeyServiceLegacyEncryptorProvider", () => { orgKey$.next(OrgRecords); orgKey$.next(null); - expect(completed).toBeTrue(); + expect(completed).toBe(true); }); it("completes once `dependencies.singleOrganizationId$` emits and `userKey$` completes", () => { @@ -486,7 +486,7 @@ describe("KeyServiceLegacyEncryptorProvider", () => { orgKey$.complete(); - expect(completed).toBeTrue(); + expect(completed).toBe(true); }); }); }); diff --git a/libs/common/src/tools/rx.spec.ts b/libs/common/src/tools/rx.spec.ts index 9ce147a3ff4..2c433fef93b 100644 --- a/libs/common/src/tools/rx.spec.ts +++ b/libs/common/src/tools/rx.spec.ts @@ -56,7 +56,7 @@ describe("errorOnChange", () => { source$.complete(); - expect(complete).toBeTrue(); + expect(complete).toBe(true); }); it("errors when the input changes", async () => { diff --git a/libs/common/src/vault/services/folder/folder.service.spec.ts b/libs/common/src/vault/services/folder/folder.service.spec.ts index 9fdb4327b98..cc3aa1946ca 100644 --- a/libs/common/src/vault/services/folder/folder.service.spec.ts +++ b/libs/common/src/vault/services/folder/folder.service.spec.ts @@ -77,7 +77,7 @@ describe("Folder Service", () => { const result = await firstValueFrom(folderService.folders$(mockUserId)); expect(result.length).toBe(2); - expect(result).toIncludeAllPartialMembers([ + expect(result).toContainPartialObjects([ { id: "1", name: makeEncString("ENC_STRING_1") }, { id: "2", name: makeEncString("ENC_STRING_2") }, ]); @@ -98,7 +98,7 @@ describe("Folder Service", () => { const result = await firstValueFrom(folderService.folderViews$(mockUserId)); expect(result.length).toBe(3); - expect(result).toIncludeAllPartialMembers([ + expect(result).toContainPartialObjects([ { id: "1", name: "DEC" }, { id: "2", name: "DEC" }, { name: "No Folder" }, diff --git a/package-lock.json b/package-lock.json index 87e0354f7ae..a8f82952517 100644 --- a/package-lock.json +++ b/package-lock.json @@ -151,7 +151,7 @@ "html-webpack-injector": "1.1.4", "html-webpack-plugin": "5.6.3", "husky": "9.1.4", - "jest-extended": "4.0.2", + "jest-diff": "29.7.0", "jest-junit": "16.0.0", "jest-mock-extended": "3.0.7", "jest-preset-angular": "14.1.1", @@ -20729,28 +20729,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-extended": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/jest-extended/-/jest-extended-4.0.2.tgz", - "integrity": "sha512-FH7aaPgtGYHc9mRjriS0ZEHYM5/W69tLrFTIdzm+yJgeoCmmrSB/luSfMSqWP9O29QWHPEmJ4qmU6EwsZideog==", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-diff": "^29.0.0", - "jest-get-type": "^29.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "jest": ">=27.2.5" - }, - "peerDependenciesMeta": { - "jest": { - "optional": true - } - } - }, "node_modules/jest-get-type": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", diff --git a/package.json b/package.json index 641a49c4d04..0af1445a8ae 100644 --- a/package.json +++ b/package.json @@ -111,7 +111,7 @@ "html-webpack-injector": "1.1.4", "html-webpack-plugin": "5.6.3", "husky": "9.1.4", - "jest-extended": "4.0.2", + "jest-diff": "29.7.0", "jest-junit": "16.0.0", "jest-mock-extended": "3.0.7", "jest-preset-angular": "14.1.1",