From b9cea0380d9c150d0690fc99a43eb8f331c3763a Mon Sep 17 00:00:00 2001 From: Dan Schultz Date: Tue, 13 Jan 2026 10:47:09 -0500 Subject: [PATCH] Migrate to `ng-mocks We were using a library called `shallow-render` to help with mocking angular components for our tests. That library is not maintained by a large community, and was falling behind major angular updates. `ng-mocks` is a much more popular library for angular testing. It also provides more functionality beyond just shallow component mocking. Issue #879 Remove shallow-render dependency Co-Authored-By: Claude Opus 4.5 --- package-lock.json | 31 +- package.json | 2 +- .../android-app-notify.component.spec.ts | 59 +-- .../announcement.component.spec.ts | 140 +++---- .../manage-custom-metadata.component.spec.ts | 104 +++-- .../add-new-category.component.spec.ts | 86 ++-- .../category-edit.component.spec.ts | 130 +++--- .../form-create/form-create.component.spec.ts | 108 ++--- .../form-edit/form-edit.component.spec.ts | 172 ++++---- .../value-add/add-new-value.component.spec.ts | 73 ++-- .../value-edit/value-edit.component.spec.ts | 133 +++--- .../components/auth/auth.components.spec.ts | 16 +- .../components/login/login.component.spec.ts | 140 +++---- .../button/button.component.spec.ts | 58 ++- .../checkbox/checkbox.component.spec.ts | 81 ++-- .../form-input/form-input.component.spec.ts | 141 +++---- .../toggle/toggle.component.spec.ts | 39 +- .../account-settings.component.spec.ts | 98 +++-- .../advanced-settings.component.spec.ts | 57 ++- .../archive-payer.component.spec.ts | 4 +- .../archive-settings-dialog.component.spec.ts | 67 +-- .../billing-settings.component.spec.ts | 75 ++-- .../gift-storage.component.spec.ts | 154 ++++--- .../invitations-dialog.component.spec.ts | 112 ++++-- .../manage-tags/manage-tags.component.spec.ts | 261 ++++++------ .../public-settings.component.spec.ts | 83 ++-- .../redeem-gift/redeem-gift.component.spec.ts | 62 +-- .../storage-dialog.component.spec.ts | 52 ++- .../two-factor-auth.component.spec.ts | 116 +++--- .../upload-progress.component.spec.ts | 48 +-- .../directive-display.component.spec.ts | 221 ++++++---- .../directive-edit.component.spec.ts | 242 ++++++----- .../legacy-contact-display.component.spec.ts | 95 +++-- .../legacy-contact-edit.component.spec.ts | 162 ++++---- .../edit-tags/edit-tags.component.spec.ts | 218 +++++----- .../file-viewer/file-viewer.component.spec.ts | 380 +++++++++++------- .../publish/publish.component.spec.ts | 90 +++-- .../sharing-dialog.component.spec.ts | 98 +++-- .../sidebar/sidebar.component.spec.ts | 234 +++++++---- .../featured-archive.component.spec.ts | 74 ++-- .../gallery-header.component.spec.ts | 46 +-- .../gallery/gallery.component.spec.ts | 110 +++-- .../public-archives-list.component.spec.ts | 92 +++-- ...hive-creation-with-share.component.spec.ts | 154 ++++--- .../create-new-archive.component.spec.ts | 107 ++--- .../glam-pending-archives.component.spec.ts | 108 +++-- ...ve-creation-start-screen.component.spec.ts | 76 ++-- ...chive-type-select-dialog.component.spec.ts | 60 +-- .../archive-type-select.component.spec.ts | 103 ++--- ...te-archive-for-me-screen.component.spec.ts | 60 +-- ...-archive-creation-screen.component.spec.ts | 88 ++-- .../glam-goals-screen.component.spec.ts | 93 +++-- .../glam-header/glam-header.component.spec.ts | 54 ++- .../glam-reasons-screen.component.spec.ts | 93 +++-- .../glam-user-survey-square.component.spec.ts | 78 ++-- .../name-archive-screen.component.spec.ts | 115 +++--- .../pending-archive.component.spec.ts | 112 +++--- ...lect-archive-type-screen.component.spec.ts | 108 ++--- .../header/header.component.spec.ts | 55 +-- .../onboarding/onboarding.component.spec.ts | 203 +++++++--- .../welcome-screen.component.spec.ts | 44 +- .../new-pledge/new-pledge.component.spec.ts | 96 ++--- .../public-archive.component.spec.ts | 105 ++--- .../search-box/search-box.component.spec.ts | 42 +- .../mobile-banner.component.spec.ts | 44 +- .../new-archive-form.component.spec.ts | 160 +++++--- .../thumbnail/thumbnail.component.spec.ts | 216 ++++++---- .../zooming-image-viewer.component.spec.ts | 70 ++-- .../account/tests/account.service.spec.ts | 270 +++++++------ .../account/tests/refreshAccount.spec.ts | 175 +++----- .../services/device/device.service.spec.ts | 42 +- .../services/event/event.service.spec.ts | 33 +- .../mobile-banner.service.spec.ts | 34 +- .../services/profile/profile.service.spec.ts | 56 +-- .../task-icon/task-icon.component.spec.ts | 45 +-- .../user-checklist.component.spec.ts | 254 ++++++------ src/test.ts | 33 +- src/test/ng-mocks-matchers.ts | 84 ++++ 78 files changed, 4659 insertions(+), 3575 deletions(-) create mode 100644 src/test/ng-mocks-matchers.ts diff --git a/package-lock.json b/package-lock.json index bc813f882..95bd2b12b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -62,7 +62,6 @@ "progressbar.js": "1.1.1", "rxjs": "7.8.2", "sass": "^1.93.2", - "shallow-render": "^20.0.0", "time-ago-pipe": "^1.3.2", "ts-key-enum": "^3.0.13", "vis-data": "^8.0.3", @@ -111,6 +110,7 @@ "karma-junit-reporter": "^2.0.1", "karma-spec-reporter": "^0.0.36", "karma-verbose-reporter": "^0.0.8", + "ng-mocks": "^14.15.0", "patch-package": "^8.0.1", "prettier": "^3.6.2", "storybook": "10.1.11", @@ -20069,6 +20069,22 @@ "@angular/core": "^14.0.0 || ^15.0.0 || ^16.0.0 || >=17.0.0" } }, + "node_modules/ng-mocks": { + "version": "14.15.0", + "resolved": "https://registry.npmjs.org/ng-mocks/-/ng-mocks-14.15.0.tgz", + "integrity": "sha512-nKsudeVq5GUAonkQgjm3wnFRAb9v1U73W91qL5QVCFfDYSVCHfGLWZAuYmbjl6PJJlIxso7sNE7ym/YkVeQOKw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/help-me-mom" + }, + "peerDependencies": { + "@angular/common": "5.0.0-alpha - 5 || 6.0.0-alpha - 6 || 7.0.0-alpha - 7 || 8.0.0-alpha - 8 || 9.0.0-alpha - 9 || 10.0.0-alpha - 10 || 11.0.0-alpha - 11 || 12.0.0-alpha - 12 || 13.0.0-alpha - 13 || 14.0.0-alpha - 14 || 15.0.0-alpha - 15 || 16.0.0-alpha - 16 || 17.0.0-alpha - 17 || 18.0.0-alpha - 18 || 19.0.0-alpha - 19 || 20.0.0-alpha - 20", + "@angular/core": "5.0.0-alpha - 5 || 6.0.0-alpha - 6 || 7.0.0-alpha - 7 || 8.0.0-alpha - 8 || 9.0.0-alpha - 9 || 10.0.0-alpha - 10 || 11.0.0-alpha - 11 || 12.0.0-alpha - 12 || 13.0.0-alpha - 13 || 14.0.0-alpha - 14 || 15.0.0-alpha - 15 || 16.0.0-alpha - 16 || 17.0.0-alpha - 17 || 18.0.0-alpha - 18 || 19.0.0-alpha - 19 || 20.0.0-alpha - 20", + "@angular/forms": "5.0.0-alpha - 5 || 6.0.0-alpha - 6 || 7.0.0-alpha - 7 || 8.0.0-alpha - 8 || 9.0.0-alpha - 9 || 10.0.0-alpha - 10 || 11.0.0-alpha - 11 || 12.0.0-alpha - 12 || 13.0.0-alpha - 13 || 14.0.0-alpha - 14 || 15.0.0-alpha - 15 || 16.0.0-alpha - 16 || 17.0.0-alpha - 17 || 18.0.0-alpha - 18 || 19.0.0-alpha - 19 || 20.0.0-alpha - 20", + "@angular/platform-browser": "5.0.0-alpha - 5 || 6.0.0-alpha - 6 || 7.0.0-alpha - 7 || 8.0.0-alpha - 8 || 9.0.0-alpha - 9 || 10.0.0-alpha - 10 || 11.0.0-alpha - 11 || 12.0.0-alpha - 12 || 13.0.0-alpha - 13 || 14.0.0-alpha - 14 || 15.0.0-alpha - 15 || 16.0.0-alpha - 16 || 17.0.0-alpha - 17 || 18.0.0-alpha - 18 || 19.0.0-alpha - 19 || 20.0.0-alpha - 20" + } + }, "node_modules/ng-recaptcha-2": { "version": "16.0.1", "resolved": "https://registry.npmjs.org/ng-recaptcha-2/-/ng-recaptcha-2-16.0.1.tgz", @@ -23226,19 +23242,6 @@ "node": ">=8" } }, - "node_modules/shallow-render": { - "version": "20.0.0", - "resolved": "https://registry.npmjs.org/shallow-render/-/shallow-render-20.0.0.tgz", - "integrity": "sha512-7xptpNNu7JVmY//23NBXaXAcpAwCvLQUxUFe6WVCoJq1V6srfiPdEKDiHO5jWBCrYNwTRCAzwg0JX5B24riCgw==", - "license": "MIT", - "peerDependencies": { - "@angular/common": "20.x", - "@angular/compiler": "20.x", - "@angular/core": "20.x", - "@angular/forms": "20.x", - "@angular/platform-browser": "20.x" - } - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/package.json b/package.json index 9456d7e66..6d66245eb 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,6 @@ "progressbar.js": "1.1.1", "rxjs": "7.8.2", "sass": "^1.93.2", - "shallow-render": "^20.0.0", "time-ago-pipe": "^1.3.2", "ts-key-enum": "^3.0.13", "vis-data": "^8.0.3", @@ -131,6 +130,7 @@ "karma-junit-reporter": "^2.0.1", "karma-spec-reporter": "^0.0.36", "karma-verbose-reporter": "^0.0.8", + "ng-mocks": "^14.15.0", "patch-package": "^8.0.1", "prettier": "^3.6.2", "storybook": "10.1.11", diff --git a/src/app/announcement/components/android-app-notify/android-app-notify.component.spec.ts b/src/app/announcement/components/android-app-notify/android-app-notify.component.spec.ts index 6fcb3cb29..bdaa25826 100644 --- a/src/app/announcement/components/android-app-notify/android-app-notify.component.spec.ts +++ b/src/app/announcement/components/android-app-notify/android-app-notify.component.spec.ts @@ -1,4 +1,5 @@ -import { Shallow } from 'shallow-render'; +import { ComponentFixture } from '@angular/core/testing'; +import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; import { AnnouncementModule } from '@announcement/announcement.module'; import { @@ -26,68 +27,70 @@ class DummyInstallPromptEvent let prompted: boolean; describe('AndroidAppNotifyComponent', () => { - let shallow: Shallow; - const waitForPromptEvent = async (fixture) => { - const event = new DummyInstallPromptEvent(); - window.dispatchEvent(event); - fixture.detectChanges(); - await fixture.whenStable(); - }; - beforeEach(() => { + beforeEach(async () => { prompted = false; - shallow = new Shallow(AndroidAppNotifyComponent, AnnouncementModule); + await MockBuilder(AndroidAppNotifyComponent, AnnouncementModule); }); afterEach(() => { localStorage.clear(); }); - it('should exist', async () => { - const { element } = await shallow.render(); + const waitForPromptEvent = async ( + fixture: ComponentFixture, + ) => { + const event = new DummyInstallPromptEvent(); + window.dispatchEvent(event); + fixture.detectChanges(); + await fixture.whenStable(); + }; + + it('should exist', () => { + const fixture = MockRender(AndroidAppNotifyComponent); - expect(element).not.toBeNull(); + expect(fixture.debugElement).not.toBeNull(); }); - it('should be invisible before `beforeinstallprompt` event', async () => { - const { find } = await shallow.render(); + it('should be invisible before `beforeinstallprompt` event', () => { + MockRender(AndroidAppNotifyComponent); - expect(find('div').length).toBe(0); + expect(ngMocks.findAll('div').length).toBe(0); }); it('should appear when the `beforeinstallprompt` fires', async () => { - const { find, fixture } = await shallow.render(); + const fixture = MockRender(AndroidAppNotifyComponent); await waitForPromptEvent(fixture); - expect(find('div').length).toBeGreaterThan(0); + expect(ngMocks.findAll('div').length).toBeGreaterThan(0); }); it('has a clickable button that shows the prompt', async () => { - const { find, fixture } = await shallow.render(); + const fixture = MockRender(AndroidAppNotifyComponent); await waitForPromptEvent(fixture); - find('.prompt-button')[0].triggerEventHandler('click', {}); + ngMocks.findAll('.prompt-button')[0].triggerEventHandler('click', {}); expect(prompted).toBeTruthy(); }); it('should dismiss itself after the App Install Banner appears', async () => { - const { find, fixture } = await shallow.render(); + const fixture = MockRender(AndroidAppNotifyComponent); await waitForPromptEvent(fixture); - find('.prompt-button')[0].triggerEventHandler('click', {}); + ngMocks.findAll('.prompt-button')[0].triggerEventHandler('click', {}); await fixture.whenStable(); fixture.detectChanges(); await fixture.whenStable(); - expect(find('div').length).toBe(0); + expect(ngMocks.findAll('div').length).toBe(0); }); it('should be dismissable from a close button', async () => { - const { find, fixture } = await shallow.render(); + const fixture = MockRender(AndroidAppNotifyComponent); await waitForPromptEvent(fixture); - find('.dismiss-button')[0].triggerEventHandler('click', {}); + ngMocks.findAll('.dismiss-button')[0].triggerEventHandler('click', {}); fixture.detectChanges(); await fixture.whenStable(); - expect(find('div').length).toBe(0); + expect(ngMocks.findAll('div').length).toBe(0); const dismissed = localStorage.getItem( AndroidAppNotifyComponent.storageKey, ); @@ -97,9 +100,9 @@ describe('AndroidAppNotifyComponent', () => { it('should not show up if previously dismissed', async () => { localStorage.setItem(AndroidAppNotifyComponent.storageKey, 'true'); - const { find, fixture } = await shallow.render(); + const fixture = MockRender(AndroidAppNotifyComponent); await waitForPromptEvent(fixture); - expect(find('div').length).toBe(0); + expect(ngMocks.findAll('div').length).toBe(0); }); }); diff --git a/src/app/announcement/components/announcement/announcement.component.spec.ts b/src/app/announcement/components/announcement/announcement.component.spec.ts index 7dbb99dd6..e50e5d923 100644 --- a/src/app/announcement/components/announcement/announcement.component.spec.ts +++ b/src/app/announcement/components/announcement/announcement.component.spec.ts @@ -1,6 +1,4 @@ -import { DebugElement } from '@angular/core'; -import { Shallow } from 'shallow-render'; -import { QueryMatch } from 'shallow-render/dist/lib/models/query-match'; +import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; import { AnnouncementModule } from '@announcement/announcement.module'; import { AnnouncementEvent } from '@announcement/models/announcement-event'; @@ -29,80 +27,78 @@ Object.freeze(pastTestEvent); Object.freeze(futureTestEvent); describe('AnnouncementComponent', () => { - let shallow: Shallow; - async function defaultRender(events?: AnnouncementEvent[]) { - return await shallow.render( - ``, - { - bind: { - events, - }, - }, - ); - } - beforeEach(() => { - shallow = new Shallow(AnnouncementComponent, AnnouncementModule); + beforeEach(async () => { + await MockBuilder(AnnouncementComponent, AnnouncementModule); }); afterEach(() => { window.localStorage.clear(); }); - it('should exist', async () => { - expect(shallow).not.toBeNull(); + function defaultRender(events?: AnnouncementEvent[]) { + return MockRender( + ``, + { events }, + ); + } + + it('should exist', () => { + const fixture = defaultRender([currentTestEvent]); + + expect(fixture).not.toBeNull(); }); - it('should take in test data', async () => { - const { element } = await defaultRender([currentTestEvent]); + it('should take in test data', () => { + const fixture = defaultRender([currentTestEvent]); - expect(element).not.toBeNull(); + expect(fixture.debugElement).not.toBeNull(); }); - it('should display the message', async () => { - const { find, element } = await defaultRender([currentTestEvent]); + it('should display the message', () => { + const fixture = defaultRender([currentTestEvent]); - expect(find('.announcement').length).toBeGreaterThan(0); - expect(element.nativeElement.innerText).toContain('Test Event!!!'); + expect(ngMocks.findAll('.announcement').length).toBeGreaterThan(0); + expect(fixture.nativeElement.innerText).toContain('Test Event!!!'); }); - it('should hide itself if event is in the future', async () => { - const { find } = await defaultRender([futureTestEvent]); + it('should hide itself if event is in the future', () => { + defaultRender([futureTestEvent]); - expect(find('.announcement').length).toBe(0); + expect(ngMocks.findAll('.announcement').length).toBe(0); }); - it('should hide itself if the event is already over', async () => { - const { find } = await defaultRender([pastTestEvent]); + it('should hide itself if the event is already over', () => { + defaultRender([pastTestEvent]); - expect(find('.announcement').length).toBe(0); + expect(ngMocks.findAll('.announcement').length).toBe(0); }); - it('should support multiple event definitions', async () => { - const { find, element } = await defaultRender([ + it('should support multiple event definitions', () => { + const fixture = defaultRender([ pastTestEvent, currentTestEvent, futureTestEvent, ]); - expect(find('.announcement').length).toBe(1); - expect(element.nativeElement.innerText).toContain('Test Event!!!'); + expect(ngMocks.findAll('.announcement').length).toBe(1); + expect(fixture.nativeElement.innerText).toContain('Test Event!!!'); }); - it('should be dismissable', async () => { - const { find, fixture } = await defaultRender([currentTestEvent]); + it('should be dismissable', () => { + const fixture = defaultRender([currentTestEvent]); - expect(find('.dismiss-button').length).toBe(1); - find('.dismiss-button').nativeElement.click(); + expect(ngMocks.findAll('.dismiss-button').length).toBe(1); + ngMocks.find('.dismiss-button').nativeElement.click(); fixture.detectChanges(); - expect(find('.announcement').length).toBe(0); + expect(ngMocks.findAll('.announcement').length).toBe(0); }); - it('should set dismissed setting in localStorage', async () => { - const { find, fixture } = await defaultRender([currentTestEvent]); + it('should set dismissed setting in localStorage', () => { + const fixture = defaultRender([currentTestEvent]); - expect(find('.dismiss-button').length).toBe(1); - find('.dismiss-button').nativeElement.click(); + expect(ngMocks.findAll('.dismiss-button').length).toBe(1); + ngMocks.find('.dismiss-button').nativeElement.click(); fixture.detectChanges(); expect(window.localStorage.getItem('announcementDismissed')).toBe( @@ -110,67 +106,63 @@ describe('AnnouncementComponent', () => { ); }); - it('should recall dismissed setting from localStorage', async () => { + it('should recall dismissed setting from localStorage', () => { window.localStorage.setItem( 'announcementDismissed', currentTestEvent.start.toString(), ); - const { find } = await defaultRender([currentTestEvent]); + defaultRender([currentTestEvent]); - expect(find('.announcement').length).toBe(0); + expect(ngMocks.findAll('.announcement').length).toBe(0); }); - it('should still be able to show a new announcement after dismissing a previous one', async () => { + it('should still be able to show a new announcement after dismissing a previous one', () => { window.localStorage.setItem('announcementDismissed', 'pastevent'); - const { find } = await defaultRender([currentTestEvent]); + defaultRender([currentTestEvent]); - expect(find('.announcement').length).toBe(1); + expect(ngMocks.findAll('.announcement').length).toBe(1); }); - it('should be able to handle an empty data array', async () => { - const { find } = await defaultRender([]); + it('should be able to handle an empty data array', () => { + defaultRender([]); - expect(find('.announcement').length).toBe(0); + expect(ngMocks.findAll('.announcement').length).toBe(0); }); - it('should be able to handle the null case', async () => { - const { find } = await defaultRender(); + it('should be able to handle the null case', () => { + defaultRender(); - expect(find('.announcement').length).toBe(0); + expect(ngMocks.findAll('.announcement').length).toBe(0); }); describe('Layout Adjustment', () => { - async function renderWithAdjustables() { - return await shallow.render( + function renderWithAdjustables() { + return MockRender( `
`, - { - bind: { - events: [currentTestEvent], - }, - }, + { events: [currentTestEvent] }, ); } - function getAdjustedElements( - find: (s: string) => QueryMatch, - ) { - const adjustedElements = find('.adjust-for-announcement'); + + function getAdjustedElements() { + const adjustedElements = ngMocks.findAll('.adjust-for-announcement'); expect(adjustedElements.length).toBeGreaterThan(0); return adjustedElements; } - it('should adjust the page layout when it appears', async () => { - const { find } = await renderWithAdjustables(); - getAdjustedElements(find).forEach((element) => { + + it('should adjust the page layout when it appears', () => { + renderWithAdjustables(); + getAdjustedElements().forEach((element) => { expect(element.nativeElement.style.paddingTop).not.toBe('0px'); expect(element.nativeElement.style.paddingTop).not.toBeUndefined(); }); }); - it('should readjust the page layout when it disappears', async () => { - const { find, fixture } = await renderWithAdjustables(); - find('.dismiss-button').nativeElement.click(); + it('should readjust the page layout when it disappears', () => { + const fixture = renderWithAdjustables(); + ngMocks.find('.dismiss-button').nativeElement.click(); fixture.detectChanges(); - getAdjustedElements(find).forEach((element) => { + getAdjustedElements().forEach((element) => { expect(element.nativeElement.style.paddingTop).toBe('0px'); }); }); diff --git a/src/app/archive-settings/manage-metadata/manage-custom-metadata/manage-custom-metadata.component.spec.ts b/src/app/archive-settings/manage-metadata/manage-custom-metadata/manage-custom-metadata.component.spec.ts index 56dde3a23..702a3f724 100644 --- a/src/app/archive-settings/manage-metadata/manage-custom-metadata/manage-custom-metadata.component.spec.ts +++ b/src/app/archive-settings/manage-metadata/manage-custom-metadata/manage-custom-metadata.component.spec.ts @@ -1,4 +1,4 @@ -import { Shallow } from 'shallow-render'; +import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; import { ApiService } from '@shared/services/api/api.service'; import { Observable } from 'rxjs'; import { TagVO, TagVOData } from '@models/tag-vo'; @@ -8,7 +8,6 @@ import { ManageMetadataModule } from '../manage-metadata.module'; import { ManageCustomMetadataComponent } from './manage-custom-metadata.component'; describe('ManageCustomMetadataComponent #custom-metadata', () => { - let shallow: Shallow; let defaultTagList: TagVOData[] = []; beforeEach(async () => { @@ -45,74 +44,85 @@ describe('ManageCustomMetadataComponent #custom-metadata', () => { }, ]; - shallow = new Shallow(ManageCustomMetadataComponent, ManageMetadataModule) - .dontMock(MetadataValuePipe) - .mock(TagsService, { - getTags: () => [...defaultTagList], - getTags$: () => new Observable(), - refreshTags: async () => {}, - resetTags: async () => {}, + await MockBuilder(ManageCustomMetadataComponent, ManageMetadataModule) + .keep(MetadataValuePipe) + .provide({ + provide: TagsService, + useValue: { + getTags: () => [...defaultTagList], + getTags$: () => new Observable(), + refreshTags: async () => {}, + resetTags: async () => {}, + }, }) - .mock(ApiService, { - tag: { - update: async (tag: TagVO) => {}, - delete: async (tag: TagVO) => {}, + .provide({ + provide: ApiService, + useValue: { + tag: { + update: async (tag: TagVO) => {}, + delete: async (tag: TagVO) => {}, + }, }, }); }); - it('should create', async () => { - const { element } = await shallow.render(); + it('should create', () => { + const fixture = MockRender(ManageCustomMetadataComponent); - expect(element).not.toBeNull(); + expect(fixture.point.nativeElement).not.toBeNull(); }); - it('should load custom metadata categories', async () => { - const { find } = await shallow.render(); + it('should load custom metadata categories', () => { + MockRender(ManageCustomMetadataComponent); - expect(find('.category').length).toBe(3); + expect(ngMocks.findAll('.category').length).toBe(3); }); - it('should be able to select a category', async () => { - const { find, fixture } = await shallow.render(); + it('should be able to select a category', () => { + const fixture = MockRender(ManageCustomMetadataComponent); - expect(find('.category.selected').length).toBe(0); - find('.category')[0].triggerEventHandler('click', {}); + expect(ngMocks.findAll('.category.selected').length).toBe(0); + ngMocks.findAll('.category')[0].triggerEventHandler('click', {}); fixture.detectChanges(); - expect(find('.category.selected').length).toBe(1); + expect(ngMocks.findAll('.category.selected').length).toBe(1); }); - it('should be able to load tags by metadata category', async () => { - const { find, fixture } = await shallow.render(); + it('should be able to load tags by metadata category', () => { + const fixture = MockRender(ManageCustomMetadataComponent); - expect(find('.value').length).toBe(0); - find('.category')[0].triggerEventHandler('click', {}); + expect(ngMocks.findAll('.value').length).toBe(0); + ngMocks.findAll('.category')[0].triggerEventHandler('click', {}); fixture.detectChanges(); - expect(find('.value').length).toBe(3); + expect(ngMocks.findAll('.value').length).toBe(3); }); it('should be able to react to the add-new-value form', async () => { - const { find, fixture } = await shallow.render(); - find('.category')[0].triggerEventHandler('click', {}); + const fixture = MockRender(ManageCustomMetadataComponent); + + ngMocks.findAll('.category')[0].triggerEventHandler('click', {}); fixture.detectChanges(); - expect(find('.value').length).toBe(3); + expect(ngMocks.findAll('.value').length).toBe(3); defaultTagList.push({ tagId: 9, name: 'a:potato', type: 'type.tag.metadata.customField', }); - find('pr-metadata-add-new-value').triggerEventHandler('tagsUpdate', {}); + ngMocks + .find('pr-metadata-add-new-value') + .triggerEventHandler('tagsUpdate', {}); await fixture.whenStable(); fixture.detectChanges(); - expect(find('.value').length).toBe(4); + expect(ngMocks.findAll('.value').length).toBe(4); }); it('should be able to filter out deleted tags', async () => { - const { instance } = await shallow.render(); + const fixture = MockRender(ManageCustomMetadataComponent); + const instance = fixture.point.componentInstance; + instance.addDeletedTag(new TagVO(defaultTagList[1])); await instance.refreshTagsInPlace(); @@ -121,7 +131,9 @@ describe('ManageCustomMetadataComponent #custom-metadata', () => { }); it('should be able to filter out deleted categories', async () => { - const { instance } = await shallow.render(); + const fixture = MockRender(ManageCustomMetadataComponent); + const instance = fixture.point.componentInstance; + instance.addDeletedCategory('a'); await instance.refreshTagsInPlace(); @@ -132,7 +144,9 @@ describe('ManageCustomMetadataComponent #custom-metadata', () => { }); it('should be able to un-filter out deleted categories that are recreated', async () => { - const { instance } = await shallow.render(); + const fixture = MockRender(ManageCustomMetadataComponent); + const instance = fixture.point.componentInstance; + instance.addDeletedCategory('a'); await instance.refreshTagsInPlace(); defaultTagList.push({ @@ -145,8 +159,10 @@ describe('ManageCustomMetadataComponent #custom-metadata', () => { expect(instance.tagsList.length).toBe(3); }); - it('should unselect the current category if its been deleted', async () => { - const { instance } = await shallow.render(); + it('should unselect the current category if its been deleted', () => { + const fixture = MockRender(ManageCustomMetadataComponent); + const instance = fixture.point.componentInstance; + instance.activeCategory = 'potato'; instance.addDeletedCategory('potato'); @@ -154,7 +170,9 @@ describe('ManageCustomMetadataComponent #custom-metadata', () => { }); it('should unselect the current category if its last tag is deleted', async () => { - const { instance } = await shallow.render(); + const fixture = MockRender(ManageCustomMetadataComponent); + const instance = fixture.point.componentInstance; + instance.activeCategory = 'c'; defaultTagList.pop(); await instance.refreshTagsInPlace(); @@ -162,8 +180,10 @@ describe('ManageCustomMetadataComponent #custom-metadata', () => { expect(instance.activeCategory).toBeNull(); }); - it('should unselect the current category if its last tag is hidden by a deletion action', async () => { - const { instance } = await shallow.render(); + it('should unselect the current category if its last tag is hidden by a deletion action', () => { + const fixture = MockRender(ManageCustomMetadataComponent); + const instance = fixture.point.componentInstance; + instance.activeCategory = 'c'; instance.addDeletedTag(new TagVO(defaultTagList.slice(-1).pop())); diff --git a/src/app/archive-settings/manage-metadata/subcomponents/category-add/add-new-category.component.spec.ts b/src/app/archive-settings/manage-metadata/subcomponents/category-add/add-new-category.component.spec.ts index c01553702..4819209e8 100644 --- a/src/app/archive-settings/manage-metadata/subcomponents/category-add/add-new-category.component.spec.ts +++ b/src/app/archive-settings/manage-metadata/subcomponents/category-add/add-new-category.component.spec.ts @@ -1,4 +1,4 @@ -import { Shallow } from 'shallow-render'; +import { MockBuilder, MockRender } from 'ng-mocks'; import { FormsModule } from '@angular/forms'; import { MessageService } from '@shared/services/message/message.service'; import { ApiService } from '@shared/services/api/api.service'; @@ -9,7 +9,6 @@ import { ManageMetadataModule } from '../../manage-metadata.module'; import { AddNewCategoryComponent } from './add-new-category.component'; describe('AddNewCategoryComponent', () => { - let shallow: Shallow; let createdTag: TagVOData = null; let error = false; let messageShown = false; @@ -22,53 +21,68 @@ describe('AddNewCategoryComponent', () => { messageShown = false; acceptPrompt = true; firstValueName = null; - shallow = new Shallow(AddNewCategoryComponent, ManageMetadataModule) - .import(FormsModule) - .dontMock(FormCreateComponent) - .mock(PromptService, { - prompt: async (message: string) => { - if (acceptPrompt) { - return { valueName: firstValueName }; - } else { - throw new Error('Promise rejection from canceling out of Prompt'); - } + await MockBuilder(AddNewCategoryComponent, ManageMetadataModule) + .keep(FormsModule) + .keep(FormCreateComponent) + .provide({ + provide: PromptService, + useValue: { + prompt: async (message: string) => { + if (acceptPrompt) { + return { valueName: firstValueName }; + } else { + throw new Error('Promise rejection from canceling out of Prompt'); + } + }, }, }) - .mock(MessageService, { - showError: () => { - messageShown = true; + .provide({ + provide: MessageService, + useValue: { + showError: () => { + messageShown = true; + }, }, }) - .mock(ApiService, { - tag: { - create: async (tag: TagVOData) => { - if (error) { - throw new Error('Test Error'); - } - createdTag = tag; + .provide({ + provide: ApiService, + useValue: { + tag: { + create: async (tag: TagVOData) => { + if (error) { + throw new Error('Test Error'); + } + createdTag = tag; + }, }, }, }); }); - it('should create', async () => { - const { element } = await shallow.render(); + it('should create', () => { + const fixture = MockRender(AddNewCategoryComponent); - expect(element).not.toBeNull(); + expect(fixture.point.nativeElement).not.toBeNull(); }); it('should be able to create a new category', async () => { - const { instance, outputs } = await shallow.render(); + const fixture = MockRender(AddNewCategoryComponent); + const instance = fixture.point.componentInstance; + const tagsUpdateSpy = spyOn(instance.tagsUpdate, 'emit'); + const newCategorySpy = spyOn(instance.newCategory, 'emit'); + firstValueName = 'potato'; await instance.createNewCategory('vegetable'); expect(createdTag.name).toBe('vegetable:potato'); - expect(outputs.tagsUpdate.emit).toHaveBeenCalled(); - expect(outputs.newCategory.emit).toHaveBeenCalledWith('vegetable'); + expect(tagsUpdateSpy).toHaveBeenCalled(); + expect(newCategorySpy).toHaveBeenCalledWith('vegetable'); }); it('should reject category names containing a : character', async () => { - const { instance } = await shallow.render(); + const fixture = MockRender(AddNewCategoryComponent); + const instance = fixture.point.componentInstance; + firstValueName = 'test'; await expectAsync(instance.createNewCategory('a:b')).toBeRejected(); @@ -77,23 +91,29 @@ describe('AddNewCategoryComponent', () => { }); it('should be able to cancel out of creating a category', async () => { - const { instance, outputs } = await shallow.render(); + const fixture = MockRender(AddNewCategoryComponent); + const instance = fixture.point.componentInstance; + const tagsUpdateSpy = spyOn(instance.tagsUpdate, 'emit'); + acceptPrompt = false; firstValueName = 'test'; await instance.createNewCategory('test'); expect(createdTag).toBeNull(); - expect(outputs.tagsUpdate.emit).not.toHaveBeenCalled(); + expect(tagsUpdateSpy).not.toHaveBeenCalled(); }); it('should show an error message when it errors out', async () => { - const { instance, outputs } = await shallow.render(); + const fixture = MockRender(AddNewCategoryComponent); + const instance = fixture.point.componentInstance; + const tagsUpdateSpy = spyOn(instance.tagsUpdate, 'emit'); + error = true; firstValueName = 'potato'; await expectAsync(instance.createNewCategory('abc')).toBeRejected(); expect(createdTag).toBeNull(); expect(messageShown).toBeTrue(); - expect(outputs.tagsUpdate.emit).not.toHaveBeenCalled(); + expect(tagsUpdateSpy).not.toHaveBeenCalled(); }); }); diff --git a/src/app/archive-settings/manage-metadata/subcomponents/category-edit/category-edit.component.spec.ts b/src/app/archive-settings/manage-metadata/subcomponents/category-edit/category-edit.component.spec.ts index 64aa0bf2c..3c4ca690e 100644 --- a/src/app/archive-settings/manage-metadata/subcomponents/category-edit/category-edit.component.spec.ts +++ b/src/app/archive-settings/manage-metadata/subcomponents/category-edit/category-edit.component.spec.ts @@ -1,4 +1,4 @@ -import { Shallow } from 'shallow-render'; +import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; import { TagVO } from '@models/tag-vo'; import { ApiService } from '@shared/services/api/api.service'; import { @@ -11,7 +11,6 @@ import { ManageMetadataModule } from '../../manage-metadata.module'; import { CategoryEditComponent } from './category-edit.component'; describe('CategoryEditComponent', () => { - let shallow: Shallow; let category: string; let tags: TagVO[]; let deletedTags: TagVO[]; @@ -20,17 +19,7 @@ describe('CategoryEditComponent', () => { let messageShown: boolean; let rejectDelete: boolean; - const defaultRender = async () => - await shallow.render( - '', - { - bind: { - category, - tags, - }, - }, - ); - beforeEach(() => { + beforeEach(async () => { category = 'test'; tags = [ new TagVO({ @@ -64,68 +53,95 @@ describe('CategoryEditComponent', () => { error = false; messageShown = false; rejectDelete = false; - shallow = new Shallow(CategoryEditComponent, ManageMetadataModule) - .dontMock(FormEditComponent) - .mock(ApiService, { - tag: { - update: async (tag: TagVO[]) => { - if (error) { - throw new Error('Test Error'); - } - savedTags = savedTags.concat(tag); - }, - delete: async (tag: TagVO[]) => { - if (error) { - throw new Error('Test Error'); - } - deletedTags = deletedTags.concat(tag); + await MockBuilder(CategoryEditComponent, ManageMetadataModule) + .keep(FormEditComponent) + .provide({ + provide: ApiService, + useValue: { + tag: { + update: async (tag: TagVO[]) => { + if (error) { + throw new Error('Test Error'); + } + savedTags = savedTags.concat(tag); + }, + delete: async (tag: TagVO[]) => { + if (error) { + throw new Error('Test Error'); + } + deletedTags = deletedTags.concat(tag); + }, }, }, }) - .mock(MessageService, { - showError: async (msg: MessageDisplayOptions) => { - messageShown = true; + .provide({ + provide: MessageService, + useValue: { + showError: async (msg: MessageDisplayOptions) => { + messageShown = true; + }, }, }) - .mock(PromptService, { - confirm: async () => { - if (rejectDelete) { - throw new Error('Rejected delete'); - } - return true; + .provide({ + provide: PromptService, + useValue: { + confirm: async () => { + if (rejectDelete) { + throw new Error('Rejected delete'); + } + return true; + }, }, }); }); - it('should exist', async () => { - const { element } = await defaultRender(); + function defaultRender() { + return MockRender( + '', + { category, tags }, + ); + } - expect(element).not.toBeNull(); + it('should exist', () => { + const fixture = defaultRender(); + + expect(fixture.point.nativeElement).not.toBeNull(); }); it('should be able to delete a category', async () => { - const { instance, outputs } = await defaultRender(); + defaultRender(); + const instance = ngMocks.findInstance(CategoryEditComponent); + const refreshTagsSpy = spyOn(instance.refreshTags, 'emit'); + const deletedCategorySpy = spyOn(instance.deletedCategory, 'emit'); + await instance.delete(); expect(deletedTags.length).toBe(2); - await expect(deletedTags[0].name).toContain('test'); - await expect(deletedTags[1].name).toContain('test'); - expect(outputs.refreshTags.emit).toHaveBeenCalled(); - expect(outputs.deletedCategory.emit).toHaveBeenCalledWith('test'); + expect(deletedTags[0].name).toContain('test'); + expect(deletedTags[1].name).toContain('test'); + expect(refreshTagsSpy).toHaveBeenCalled(); + expect(deletedCategorySpy).toHaveBeenCalledWith('test'); }); it('should deal with errors while deleting', async () => { - const { instance, outputs } = await defaultRender(); + defaultRender(); + const instance = ngMocks.findInstance(CategoryEditComponent); + const refreshTagsSpy = spyOn(instance.refreshTags, 'emit'); + const deletedCategorySpy = spyOn(instance.deletedCategory, 'emit'); + error = true; await expectAsync(instance.delete()).toBeRejected(); expect(messageShown).toBeTrue(); - expect(outputs.refreshTags.emit).not.toHaveBeenCalled(); - expect(outputs.deletedCategory.emit).not.toHaveBeenCalled(); + expect(refreshTagsSpy).not.toHaveBeenCalled(); + expect(deletedCategorySpy).not.toHaveBeenCalled(); }); it('should be able to save a category', async () => { - const { instance, outputs } = await defaultRender(); + defaultRender(); + const instance = ngMocks.findInstance(CategoryEditComponent); + const refreshTagsSpy = spyOn(instance.refreshTags, 'emit'); + await instance.save('potato'); expect(savedTags.length).toBe(2); @@ -134,24 +150,30 @@ describe('CategoryEditComponent', () => { expect(tag.name.substring(6).includes('potato')).toBeFalse(); // Verify value name not changed }); - expect(outputs.refreshTags.emit).toHaveBeenCalled(); + expect(refreshTagsSpy).toHaveBeenCalled(); }); it('should deal with errors while saving', async () => { - const { instance, outputs } = await defaultRender(); + defaultRender(); + const instance = ngMocks.findInstance(CategoryEditComponent); + const refreshTagsSpy = spyOn(instance.refreshTags, 'emit'); + error = true; await expectAsync(instance.save('potato')).toBeRejected(); expect(messageShown).toBeTrue(); - expect(outputs.refreshTags.emit).not.toHaveBeenCalled(); + expect(refreshTagsSpy).not.toHaveBeenCalled(); }); it('should not do anything if they cancel out of the deletion confirmation prompt', async () => { rejectDelete = true; - const { instance, outputs } = await defaultRender(); + defaultRender(); + const instance = ngMocks.findInstance(CategoryEditComponent); + const deletedCategorySpy = spyOn(instance.deletedCategory, 'emit'); + await instance.delete(); expect(deletedTags.length).toBe(0); - expect(outputs.deletedCategory.emit).not.toHaveBeenCalled(); + expect(deletedCategorySpy).not.toHaveBeenCalled(); }); }); diff --git a/src/app/archive-settings/manage-metadata/subcomponents/form-create/form-create.component.spec.ts b/src/app/archive-settings/manage-metadata/subcomponents/form-create/form-create.component.spec.ts index 6d3f89daf..44108028c 100644 --- a/src/app/archive-settings/manage-metadata/subcomponents/form-create/form-create.component.spec.ts +++ b/src/app/archive-settings/manage-metadata/subcomponents/form-create/form-create.component.spec.ts @@ -1,76 +1,77 @@ -import { Shallow } from 'shallow-render'; +import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; import { FormsModule } from '@angular/forms'; import { A11yModule } from '@angular/cdk/a11y'; import { ManageMetadataModule } from '../../manage-metadata.module'; import { FormCreateComponent } from './form-create.component'; describe('FormCreateComponent', () => { - let shallow: Shallow; - - const defaultRender = async ( + beforeEach( + async () => + await MockBuilder(FormCreateComponent, ManageMetadataModule) + .keep(FormsModule) + .keep(A11yModule), + ); + + function defaultRender( c: (tagName: string) => Promise = async () => {}, - ) => - await shallow.render( + ) { + return MockRender( '', - { - bind: { - callback: c, - }, - }, + { callback: c }, ); + } - beforeEach(async () => { - shallow = new Shallow(FormCreateComponent, ManageMetadataModule) - .import(FormsModule) - .dontMock(A11yModule); - }); - - it('should create', async () => { - const { element } = await defaultRender(); + it('should create', () => { + const fixture = defaultRender(); - expect(element).not.toBeNull(); + expect(fixture.point.nativeElement).not.toBeNull(); }); - it('should be a text label at first', async () => { - const { find } = await defaultRender(); + it('should be a text label at first', () => { + defaultRender(); - expect(find('.placeholder-text').length).toBe(1); - expect(find('input').length).toBe(0); + expect(ngMocks.findAll('.placeholder-text').length).toBe(1); + expect(ngMocks.findAll('input').length).toBe(0); }); - it('should open up to a textbox', async () => { - const { find, fixture } = await defaultRender(); - find('.placeholder-text').triggerEventHandler('click', {}); + it('should open up to a textbox', () => { + const fixture = defaultRender(); + + ngMocks.find('.placeholder-text').triggerEventHandler('click', {}); fixture.detectChanges(); - expect(find('input').length).toBe(1); + expect(ngMocks.findAll('input').length).toBe(1); }); - it('should use specified placeholder text', async () => { - const { find, fixture } = await defaultRender(); + it('should use specified placeholder text', () => { + const fixture = defaultRender(); - expect(find('.placeholder-text').nativeElement.innerText).toBe( + expect(ngMocks.find('.placeholder-text').nativeElement.innerText).toBe( 'Add New Test', ); - find('.placeholder-text').triggerEventHandler('click', {}); + ngMocks.find('.placeholder-text').triggerEventHandler('click', {}); fixture.detectChanges(); - expect(find('input').nativeElement.placeholder).toBe('Add New Test'); + expect(ngMocks.find('input').nativeElement.placeholder).toBe( + 'Add New Test', + ); }); it('should take in a callback and execute it', async () => { let createdTag = ''; - const { instance, find, fixture } = await defaultRender(async (tagName) => { + const fixture = defaultRender(async (tagName) => { createdTag = tagName; }); - find('.placeholder-text').triggerEventHandler('click', {}); + const instance = ngMocks.findInstance(FormCreateComponent); + + ngMocks.find('.placeholder-text').triggerEventHandler('click', {}); fixture.detectChanges(); - const input = find('input'); + const input = ngMocks.find('input'); input.nativeElement.value = 'abc'; input.triggerEventHandler('input', { target: input.nativeElement }); fixture.detectChanges(); - find('form').triggerEventHandler('submit', { - target: find('form').nativeElement, + ngMocks.find('form').triggerEventHandler('submit', { + target: ngMocks.find('form').nativeElement, }); expect(instance.waiting).toBe(true); @@ -79,26 +80,28 @@ describe('FormCreateComponent', () => { expect(createdTag).toBe('abc'); expect(instance.waiting).toBe(false); - expect(find('.placeholder-text').length).toBe(1); + expect(ngMocks.findAll('.placeholder-text').length).toBe(1); }); it('should not close editor if callback promise is rejected', async () => { - const { instance, find, fixture } = await defaultRender(async (tagName) => { + const fixture = defaultRender(async (tagName) => { throw new Error(); }); - find('.placeholder-text').triggerEventHandler('click', {}); + const instance = ngMocks.findInstance(FormCreateComponent); + + ngMocks.find('.placeholder-text').triggerEventHandler('click', {}); fixture.detectChanges(); - const input = find('input'); + const input = ngMocks.find('input'); input.nativeElement.value = 'abc'; input.triggerEventHandler('input', { target: input.nativeElement }); fixture.detectChanges(); - find('form').triggerEventHandler('submit', { - target: find('form').nativeElement, + ngMocks.find('form').triggerEventHandler('submit', { + target: ngMocks.find('form').nativeElement, }); await fixture.whenStable(); fixture.detectChanges(); - expect(find('input').length).toBe(1); + expect(ngMocks.findAll('input').length).toBe(1); await new Promise((resolve) => { setTimeout(resolve, 1); }); @@ -107,7 +110,9 @@ describe('FormCreateComponent', () => { }); it('should blank out the form after submitting', async () => { - const { instance } = await defaultRender(); + defaultRender(); + const instance = ngMocks.findInstance(FormCreateComponent); + instance.newTagName = 'potato'; await instance.runSubmitCallback(); @@ -116,18 +121,19 @@ describe('FormCreateComponent', () => { it('should not send multiple create requests', async () => { let callbackCalls = 0; - const { find, fixture } = await defaultRender(async () => { + const fixture = defaultRender(async () => { callbackCalls += 1; }); - find('.placeholder-text').triggerEventHandler('click', {}); + + ngMocks.find('.placeholder-text').triggerEventHandler('click', {}); fixture.detectChanges(); - const input = find('input'); + const input = ngMocks.find('input'); input.nativeElement.value = 'abc'; input.triggerEventHandler('input', { target: input.nativeElement }); fixture.detectChanges(); for (let i = 0; i < 3; i += 1) { - find('form').triggerEventHandler('submit', { - target: find('form').nativeElement, + ngMocks.find('form').triggerEventHandler('submit', { + target: ngMocks.find('form').nativeElement, }); } await fixture.whenStable(); diff --git a/src/app/archive-settings/manage-metadata/subcomponents/form-edit/form-edit.component.spec.ts b/src/app/archive-settings/manage-metadata/subcomponents/form-edit/form-edit.component.spec.ts index 108282269..13c540694 100644 --- a/src/app/archive-settings/manage-metadata/subcomponents/form-edit/form-edit.component.spec.ts +++ b/src/app/archive-settings/manage-metadata/subcomponents/form-edit/form-edit.component.spec.ts @@ -1,4 +1,4 @@ -import { Shallow } from 'shallow-render'; +import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; import { FormsModule } from '@angular/forms'; import { Subject } from 'rxjs'; import { A11yModule } from '@angular/cdk/a11y'; @@ -7,100 +7,99 @@ import { MetadataValuePipe } from '../../pipes/metadata-value.pipe'; import { FormEditComponent } from './form-edit.component'; describe('FormEditComponent', () => { - let shallow: Shallow; let deleted = false; let updated = false; let newTagName: string; let callbackCalls: number; let subject: Subject; - const defaultRender = async (name: string = 'test') => - await shallow.render( - '', - { - bind: { - name, - delete: async () => { - callbackCalls += 1; - deleted = true; - }, - save: async (n: string) => { - callbackCalls += 1; - updated = true; - newTagName = n; - }, - subject, - }, - }, - ); - - beforeEach(() => { + beforeEach(async () => { deleted = false; updated = false; newTagName = null; callbackCalls = 0; subject = new Subject(); - shallow = new Shallow(FormEditComponent, ManageMetadataModule) - .dontMock(A11yModule) - .import(FormsModule) - .dontMock(MetadataValuePipe); + await MockBuilder(FormEditComponent, ManageMetadataModule) + .keep(A11yModule) + .keep(FormsModule) + .keep(MetadataValuePipe); }); - it('should exist', async () => { - const { element } = await shallow.render(); + function defaultRender(name: string = 'test') { + return MockRender( + '', + { + name, + delete: async () => { + callbackCalls += 1; + deleted = true; + }, + save: async (n: string) => { + callbackCalls += 1; + updated = true; + newTagName = n; + }, + subject, + }, + ); + } + + it('should exist', () => { + const fixture = MockRender(FormEditComponent); - expect(element).not.toBeNull(); + expect(fixture.point.nativeElement).not.toBeNull(); }); - it('should print tag value', async () => { - const { find } = await defaultRender(); + it('should print tag value', () => { + defaultRender(); - expect(find('.value-name').nativeElement.innerText).toBe('test'); + expect(ngMocks.find('.value-name').nativeElement.innerText).toBe('test'); }); - it('should have a dropdown edit/delete menu', async () => { - const { find, fixture } = await defaultRender(); + it('should have a dropdown edit/delete menu', () => { + const fixture = defaultRender(); - expect(find('.edit-delete-menu').length).toBe(0); - expect(find('.edit-delete-trigger').length).toBe(1); - find('.edit-delete-trigger')[0].triggerEventHandler('click', {}); + expect(ngMocks.findAll('.edit-delete-menu').length).toBe(0); + expect(ngMocks.findAll('.edit-delete-trigger').length).toBe(1); + ngMocks.findAll('.edit-delete-trigger')[0].triggerEventHandler('click', {}); fixture.detectChanges(); - expect(find('.edit-delete-menu').length).toBe(1); + expect(ngMocks.findAll('.edit-delete-menu').length).toBe(1); }); it('should be call the delete function', async () => { - const { find, fixture } = await defaultRender(); + const fixture = defaultRender(); - expect(find('.delete').length).toBe(0); - find('.edit-delete-trigger')[0].triggerEventHandler('click', {}); + expect(ngMocks.findAll('.delete').length).toBe(0); + ngMocks.findAll('.edit-delete-trigger')[0].triggerEventHandler('click', {}); fixture.detectChanges(); - expect(find('.delete').length).toBe(1); - find('.delete')[0].triggerEventHandler('click', {}); + expect(ngMocks.findAll('.delete').length).toBe(1); + ngMocks.findAll('.delete')[0].triggerEventHandler('click', {}); fixture.detectChanges(); await fixture.whenStable(); expect(deleted).toBeTrue(); }); - it('should be able to open the value editor', async () => { - const { instance, find, fixture } = await defaultRender('123'); + it('should be able to open the value editor', () => { + const fixture = defaultRender('123'); + const instance = ngMocks.findInstance(FormEditComponent); - expect(find('.value-editor').length).toBe(0); - expect(find('.edit').length).toBe(0); - find('.edit-delete-trigger')[0].triggerEventHandler('click', {}); + expect(ngMocks.findAll('.value-editor').length).toBe(0); + expect(ngMocks.findAll('.edit').length).toBe(0); + ngMocks.findAll('.edit-delete-trigger')[0].triggerEventHandler('click', {}); fixture.detectChanges(); - expect(find('.edit').length).toBe(1); - find('.edit')[0].triggerEventHandler('click', {}); + expect(ngMocks.findAll('.edit').length).toBe(1); + ngMocks.findAll('.edit')[0].triggerEventHandler('click', {}); fixture.detectChanges(); - expect(find('.value-line').length).toBe(0); - expect(find('.edit-delete-menu').length).toBe(0); - expect(find('.value-editor').length).toBe(1); - expect(find('.value-editor input').length).toBe(1); - const input = find('.value-editor input'); + expect(ngMocks.findAll('.value-line').length).toBe(0); + expect(ngMocks.findAll('.edit-delete-menu').length).toBe(0); + expect(ngMocks.findAll('.value-editor').length).toBe(1); + expect(ngMocks.findAll('.value-editor input').length).toBe(1); + const input = ngMocks.find('.value-editor input'); input.nativeElement.value = 'Test'; input.triggerEventHandler('input', { target: input.nativeElement }); @@ -109,32 +108,35 @@ describe('FormEditComponent', () => { }); it('should be able to edit a value', async () => { - const { find, fixture, instance } = await defaultRender('testValue'); - find('.edit-delete-trigger')[0].triggerEventHandler('click', {}); + const fixture = defaultRender('testValue'); + const instance = ngMocks.findInstance(FormEditComponent); + + ngMocks.findAll('.edit-delete-trigger')[0].triggerEventHandler('click', {}); fixture.detectChanges(); - find('.edit')[0].triggerEventHandler('click', {}); + ngMocks.findAll('.edit')[0].triggerEventHandler('click', {}); fixture.detectChanges(); - const input = find('.value-editor input'); + const input = ngMocks.find('.value-editor input'); expect(instance.newValueName).toBe('testValue'); input.nativeElement.value = 'potato'; input.triggerEventHandler('input', { target: input.nativeElement }); - find('form').triggerEventHandler('submit', {}); + ngMocks.find('form').triggerEventHandler('submit', {}); await fixture.whenStable(); fixture.detectChanges(); - expect(find('.value-editor').length).toBe(0); + expect(ngMocks.findAll('.value-editor').length).toBe(0); expect(updated).toBeTrue(); expect(newTagName).toBe('potato'); expect(instance.newValueName).toBe('potato'); }); it('should not send multiple delete calls', async () => { - const { find, fixture } = await defaultRender(); - find('.edit-delete-trigger')[0].triggerEventHandler('click', {}); + const fixture = defaultRender(); + + ngMocks.findAll('.edit-delete-trigger')[0].triggerEventHandler('click', {}); fixture.detectChanges(); - find('.delete')[0].triggerEventHandler('click', {}); - find('.delete')[0].triggerEventHandler('click', {}); + ngMocks.findAll('.delete')[0].triggerEventHandler('click', {}); + ngMocks.findAll('.delete')[0].triggerEventHandler('click', {}); fixture.detectChanges(); await fixture.whenStable(); @@ -142,48 +144,52 @@ describe('FormEditComponent', () => { }); it('should not send multiple save calls', async () => { - const { find, fixture } = await defaultRender(); - find('.edit-delete-trigger')[0].triggerEventHandler('click', {}); + const fixture = defaultRender(); + + ngMocks.findAll('.edit-delete-trigger')[0].triggerEventHandler('click', {}); fixture.detectChanges(); - find('.edit')[0].triggerEventHandler('click', {}); + ngMocks.findAll('.edit')[0].triggerEventHandler('click', {}); fixture.detectChanges(); - const input = find('.value-editor input'); + const input = ngMocks.find('.value-editor input'); input.nativeElement.value = 'potato'; input.triggerEventHandler('input', { target: input.nativeElement }); - find('form').triggerEventHandler('submit', {}); - find('form').triggerEventHandler('submit', {}); + ngMocks.find('form').triggerEventHandler('submit', {}); + ngMocks.find('form').triggerEventHandler('submit', {}); await fixture.whenStable(); fixture.detectChanges(); expect(callbackCalls).toBe(1); }); - it('should be able to subscribe to an event that closes the edit dialog', async () => { - const { find, fixture } = await defaultRender(); - find('.edit-delete-trigger')[0].triggerEventHandler('click', {}); + it('should be able to subscribe to an event that closes the edit dialog', () => { + const fixture = defaultRender(); + + ngMocks.findAll('.edit-delete-trigger')[0].triggerEventHandler('click', {}); fixture.detectChanges(); subject.next(Infinity); fixture.detectChanges(); - expect(find('.edit-delete-menu').length).toBe(0); + expect(ngMocks.findAll('.edit-delete-menu').length).toBe(0); }); - it('should emit an event that closes other edit dialogs when another is opened', async () => { + it('should emit an event that closes other edit dialogs when another is opened', () => { let emitted = false; subject.subscribe(() => { emitted = true; }); - const { find } = await defaultRender(); - find('.edit-delete-trigger')[0].triggerEventHandler('click', {}); + defaultRender(); + + ngMocks.findAll('.edit-delete-trigger')[0].triggerEventHandler('click', {}); expect(emitted).toBeTrue(); }); - it('should open the editor directly if double clicked', async () => { - const { find, fixture } = await defaultRender(); - find('.value-line').triggerEventHandler('dblclick', {}); + it('should open the editor directly if double clicked', () => { + const fixture = defaultRender(); + + ngMocks.find('.value-line').triggerEventHandler('dblclick', {}); fixture.detectChanges(); - expect(find('.value-editor input').length).toBe(1); + expect(ngMocks.findAll('.value-editor input').length).toBe(1); }); }); diff --git a/src/app/archive-settings/manage-metadata/subcomponents/value-add/add-new-value.component.spec.ts b/src/app/archive-settings/manage-metadata/subcomponents/value-add/add-new-value.component.spec.ts index d189e1aa6..235da2e84 100644 --- a/src/app/archive-settings/manage-metadata/subcomponents/value-add/add-new-value.component.spec.ts +++ b/src/app/archive-settings/manage-metadata/subcomponents/value-add/add-new-value.component.spec.ts @@ -1,4 +1,4 @@ -import { Shallow } from 'shallow-render'; +import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; import { FormsModule } from '@angular/forms'; import { MessageService } from '@shared/services/message/message.service'; import { ApiService } from '@shared/services/api/api.service'; @@ -8,70 +8,77 @@ import { FormCreateComponent } from '../form-create/form-create.component'; import { AddNewValueComponent } from './add-new-value.component'; describe('AddNewValueComponent', () => { - let shallow: Shallow; let category: string = 'test'; let createdTag: TagVOData = null; let error: boolean = false; let messageShown: boolean = false; - const defaultRender = async (categoryName: string = category) => - await shallow.render( - '', - { - bind: { - category: categoryName, - }, - }, - ); - beforeEach(async () => { category = 'test'; createdTag = null; error = false; messageShown = false; - shallow = new Shallow(AddNewValueComponent, ManageMetadataModule) - .import(FormsModule) - .dontMock(FormCreateComponent) - .mock(MessageService, { - showError: () => { - messageShown = true; + await MockBuilder(AddNewValueComponent, ManageMetadataModule) + .keep(FormsModule) + .keep(FormCreateComponent) + .provide({ + provide: MessageService, + useValue: { + showError: () => { + messageShown = true; + }, }, }) - .mock(ApiService, { - tag: { - create: async (tag: TagVOData) => { - if (error) { - throw new Error('Test Error'); - } - createdTag = tag; + .provide({ + provide: ApiService, + useValue: { + tag: { + create: async (tag: TagVOData) => { + if (error) { + throw new Error('Test Error'); + } + createdTag = tag; + }, }, }, }); }); - it('should create', async () => { - const { element } = await defaultRender(); + function defaultRender(categoryName: string = category) { + return MockRender( + '', + { category: categoryName }, + ); + } - expect(element).not.toBeNull(); + it('should create', () => { + const fixture = defaultRender(); + + expect(fixture.point.nativeElement).not.toBeNull(); }); it('should be able to create a new tag', async () => { - const { instance, outputs } = await defaultRender(); + defaultRender(); + const instance = ngMocks.findInstance(AddNewValueComponent); + const tagsUpdateSpy = spyOn(instance.tagsUpdate, 'emit'); - expect(outputs.tagsUpdate.emit).not.toHaveBeenCalled(); + expect(tagsUpdateSpy).not.toHaveBeenCalled(); await instance.createNewTag('abc'); expect(createdTag?.name).toBe('test:abc'); - expect(outputs.tagsUpdate.emit).toHaveBeenCalled(); + expect(tagsUpdateSpy).toHaveBeenCalled(); }); it('should show an error message when it errors out', async () => { - const { instance, outputs } = await defaultRender(); + defaultRender(); + const instance = ngMocks.findInstance(AddNewValueComponent); + const tagsUpdateSpy = spyOn(instance.tagsUpdate, 'emit'); + error = true; await expectAsync(instance.createNewTag('abc')).toBeRejected(); expect(createdTag).toBeNull(); expect(messageShown).toBeTrue(); - expect(outputs.tagsUpdate.emit).not.toHaveBeenCalled(); + expect(tagsUpdateSpy).not.toHaveBeenCalled(); }); }); diff --git a/src/app/archive-settings/manage-metadata/subcomponents/value-edit/value-edit.component.spec.ts b/src/app/archive-settings/manage-metadata/subcomponents/value-edit/value-edit.component.spec.ts index 968867a89..d72cdef4e 100644 --- a/src/app/archive-settings/manage-metadata/subcomponents/value-edit/value-edit.component.spec.ts +++ b/src/app/archive-settings/manage-metadata/subcomponents/value-edit/value-edit.component.spec.ts @@ -1,4 +1,4 @@ -import { Shallow } from 'shallow-render'; +import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; import { TagVO } from '@models/tag-vo'; import { ApiService } from '@shared/services/api/api.service'; import { FormsModule } from '@angular/forms'; @@ -13,7 +13,6 @@ import { ManageMetadataModule } from '../../manage-metadata.module'; import { EditValueComponent } from './value-edit.component'; describe('EditValueComponent', () => { - let shallow: Shallow; let deleted: boolean; let updated: boolean; let newTagName: string; @@ -21,19 +20,7 @@ describe('EditValueComponent', () => { let messageShown: boolean; let rejectDelete: boolean; - const defaultRender = async ( - tag: TagVO = new TagVO({ tagId: 1, name: 'abc:123' }), - ) => - await shallow.render( - '', - { - bind: { - tag, - }, - }, - ); - - beforeEach(() => { + beforeEach(async () => { updated = false; deleted = false; newTagName = ''; @@ -41,89 +28,123 @@ describe('EditValueComponent', () => { messageShown = false; rejectDelete = false; - shallow = new Shallow(EditValueComponent, ManageMetadataModule) - .import(FormsModule) - .dontMock(FormEditComponent) - .dontMock(MetadataValuePipe) - .mock(ApiService, { - tag: { - delete: async (tag: TagVO) => { - if (error) { - throw new Error('Test Error'); - } - deleted = true; - }, - update: async (tag: TagVO) => { - if (error) { - throw new Error('Test Error'); - } - updated = true; - newTagName = tag.name; + await MockBuilder(EditValueComponent, ManageMetadataModule) + .keep(FormsModule) + .keep(FormEditComponent) + .keep(MetadataValuePipe) + .provide({ + provide: ApiService, + useValue: { + tag: { + delete: async (tag: TagVO) => { + if (error) { + throw new Error('Test Error'); + } + deleted = true; + }, + update: async (tag: TagVO) => { + if (error) { + throw new Error('Test Error'); + } + updated = true; + newTagName = tag.name; + }, }, }, }) - .mock(MessageService, { - showError: (message: MessageDisplayOptions) => { - messageShown = true; + .provide({ + provide: MessageService, + useValue: { + showError: (message: MessageDisplayOptions) => { + messageShown = true; + }, }, }) - .mock(PromptService, { - confirm: async () => { - if (rejectDelete) { - throw new Error('promise rejection'); - } - return true; + .provide({ + provide: PromptService, + useValue: { + confirm: async () => { + if (rejectDelete) { + throw new Error('promise rejection'); + } + return true; + }, }, }); }); - it('should create', async () => { - const { element } = await defaultRender(); + function defaultRender( + tag: TagVO = new TagVO({ tagId: 1, name: 'abc:123' }), + ) { + return MockRender( + '', + { tag }, + ); + } + + it('should create', () => { + const fixture = defaultRender(); - expect(element).toBeTruthy(); + expect(fixture.point.nativeElement).toBeTruthy(); }); it('should be able to delete a tag', async () => { - const { instance, outputs } = await defaultRender(); + defaultRender(); + const instance = ngMocks.findInstance(EditValueComponent); + const refreshTagsSpy = spyOn(instance.refreshTags, 'emit'); + const deletedTagSpy = spyOn(instance.deletedTag, 'emit'); + await instance.delete(); expect(deleted).toBeTrue(); - expect(outputs.refreshTags.emit).toHaveBeenCalled(); - expect(outputs.deletedTag.emit).toHaveBeenCalled(); + expect(refreshTagsSpy).toHaveBeenCalled(); + expect(deletedTagSpy).toHaveBeenCalled(); }); it('should be able to edit a value', async () => { - const { instance, outputs } = await defaultRender(); + defaultRender(); + const instance = ngMocks.findInstance(EditValueComponent); + const refreshTagsSpy = spyOn(instance.refreshTags, 'emit'); + await instance.save('potato'); expect(updated).toBeTrue(); expect(newTagName).toBe('abc:potato'); - expect(outputs.refreshTags.emit).toHaveBeenCalled(); + expect(refreshTagsSpy).toHaveBeenCalled(); }); it('should deal with errors while deleting', async () => { - const { instance, outputs } = await defaultRender(); + defaultRender(); + const instance = ngMocks.findInstance(EditValueComponent); + const refreshTagsSpy = spyOn(instance.refreshTags, 'emit'); + error = true; await expectAsync(instance.delete()).toBeRejected(); expect(messageShown).toBeTrue(); - expect(outputs.refreshTags.emit).not.toHaveBeenCalled(); + expect(refreshTagsSpy).not.toHaveBeenCalled(); }); it('should deal with errors while editing', async () => { - const { instance, outputs } = await defaultRender(); + defaultRender(); + const instance = ngMocks.findInstance(EditValueComponent); + const refreshTagsSpy = spyOn(instance.refreshTags, 'emit'); + error = true; await expectAsync(instance.save('test')).toBeRejected(); expect(messageShown).toBeTrue(); - expect(outputs.refreshTags.emit).not.toHaveBeenCalled(); + expect(refreshTagsSpy).not.toHaveBeenCalled(); }); it('should not do anything if they cancel out of the deletion confirmation prompt', async () => { rejectDelete = true; - const { instance, outputs } = await defaultRender(); + defaultRender(); + const instance = ngMocks.findInstance(EditValueComponent); + const deletedTagSpy = spyOn(instance.deletedTag, 'emit'); + await instance.delete(); - expect(outputs.deletedTag.emit).not.toHaveBeenCalled(); + expect(deletedTagSpy).not.toHaveBeenCalled(); }); }); diff --git a/src/app/auth/components/auth/auth.components.spec.ts b/src/app/auth/components/auth/auth.components.spec.ts index 68f86080a..d30e409c6 100644 --- a/src/app/auth/components/auth/auth.components.spec.ts +++ b/src/app/auth/components/auth/auth.components.spec.ts @@ -1,17 +1,21 @@ -import { Shallow } from 'shallow-render'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MockBuilder } from 'ng-mocks'; import { AuthRoutingModule } from '@auth/auth.routes'; import { AuthComponent } from './auth.component'; describe('AuthComponent', () => { - let shallow: Shallow; + let fixture: ComponentFixture; + let instance: AuthComponent; beforeEach(async () => { - shallow = new Shallow(AuthComponent, AuthRoutingModule); - }); + await MockBuilder(AuthComponent, AuthRoutingModule); - it('should create', async () => { - const { instance } = await shallow.render(); + fixture = TestBed.createComponent(AuthComponent); + instance = fixture.componentInstance; + fixture.detectChanges(); + }); + it('should create', () => { expect(instance).toBeTruthy(); }); }); diff --git a/src/app/auth/components/login/login.component.spec.ts b/src/app/auth/components/login/login.component.spec.ts index caba36d4a..ab295d27f 100644 --- a/src/app/auth/components/login/login.component.spec.ts +++ b/src/app/auth/components/login/login.component.spec.ts @@ -1,21 +1,21 @@ -import { NgModule } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ngMocks } from 'ng-mocks'; import { ActivatedRoute, Router } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; +import { ReactiveFormsModule } from '@angular/forms'; import { CookieService } from 'ngx-cookie-service'; -import { Shallow } from 'shallow-render'; import { LoginComponent } from '@auth/components/login/login.component'; import { MessageService } from '@shared/services/message/message.service'; import { TEST_DATA } from '@core/core.module.spec'; import { AccountService } from '@shared/services/account/account.service'; import { AuthResponse } from '@shared/services/api/auth.repo'; -import { TestBed } from '@angular/core/testing'; import { DeviceService } from '@shared/services/device/device.service'; import { ArchiveVO } from '@models/index'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; const testEmail = 'unittest@example.com'; -@NgModule() -class DummyModule {} - class MockAccountService { public switchedToDefaultArchive: boolean = false; public archives: ArchiveVO[] = []; @@ -58,19 +58,13 @@ class MockActivatedRoute { }; } -class MockRouter { - public navigatedRoute: string[] = []; - public async navigate(path: string[]) { - this.navigatedRoute = path; - } -} - class MockMessageService { showMessage(_: string) {} } class LoginTestingHarness { private component: LoginComponent; + public navigateSpy: jasmine.Spy; constructor( private account: MockAccountService, private route: MockActivatedRoute, @@ -130,8 +124,8 @@ class LoginTestingHarness { }); } - public getMessageSpy(inject: typeof TestBed.inject) { - return spyOn(inject(MessageService), 'showMessage').and.callThrough(); + public getMessageSpy() { + return spyOn(ngMocks.get(MessageService), 'showMessage').and.callThrough(); } public hasPasswordBeenCleared(): boolean { @@ -140,54 +134,58 @@ class LoginTestingHarness { } describe('LoginComponent', () => { - let shallow: Shallow; + let fixture: ComponentFixture; + let instance: LoginComponent; let cookieService: Map; let accountService: MockAccountService; let activatedRoute: MockActivatedRoute; - let router: MockRouter; let harness: LoginTestingHarness; - beforeEach(() => { + beforeEach(async () => { accountService = new MockAccountService(); activatedRoute = new MockActivatedRoute(); - router = new MockRouter(); cookieService = new Map(); cookieService.set('rememberMe', testEmail); harness = new LoginTestingHarness(accountService, activatedRoute); - shallow = new Shallow(LoginComponent, DummyModule).provideMock( - { - provide: AccountService, - useValue: accountService, - }, - { provide: ActivatedRoute, useValue: activatedRoute }, - { provide: CookieService, useValue: cookieService }, - { provide: MessageService, useClass: MockMessageService }, - { provide: Router, useValue: router }, - { - provide: DeviceService, - useValue: { - isMobile() { - return true; + + await TestBed.configureTestingModule({ + imports: [ReactiveFormsModule, RouterTestingModule, NoopAnimationsModule], + declarations: [LoginComponent], + providers: [ + { provide: AccountService, useValue: accountService }, + { provide: ActivatedRoute, useValue: activatedRoute }, + { provide: CookieService, useValue: cookieService }, + { provide: MessageService, useClass: MockMessageService }, + { + provide: DeviceService, + useValue: { + isMobile() { + return true; + }, }, }, - }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(LoginComponent); + instance = fixture.componentInstance; + const router = TestBed.inject(Router); + harness.navigateSpy = spyOn(router, 'navigate').and.returnValue( + Promise.resolve(true), ); + fixture.detectChanges(); }); - it('should create', async () => { - const { instance } = await shallow.render(); - + it('should create', () => { expect(instance).toBeTruthy(); }); - it('should autofill with the email from cookies', async () => { - const { instance } = await shallow.render(); - + it('should autofill with the email from cookies', () => { expect(instance.loginForm.value.email).toEqual(testEmail); }); - it('should set error for missing email', async () => { - const { instance } = await shallow.render(); + it('should set error for missing email', () => { instance.loginForm.get('email').markAsTouched(); instance.loginForm.patchValue({ email: '', @@ -198,8 +196,7 @@ describe('LoginComponent', () => { expect(instance.loginForm.get('email').errors.required).toBeTruthy(); }); - it('should set error for invalid email', async () => { - const { instance } = await shallow.render(); + it('should set error for invalid email', () => { instance.loginForm.get('email').markAsTouched(); instance.loginForm.patchValue({ email: 'lasld;f;aslkj', @@ -210,8 +207,7 @@ describe('LoginComponent', () => { expect(instance.loginForm.get('email').errors.email).toBeTruthy(); }); - it('should set error for missing password', async () => { - const { instance } = await shallow.render(); + it('should set error for missing password', () => { instance.loginForm.get('password').markAsTouched(); instance.loginForm.patchValue({ email: TEST_DATA.user.email, @@ -222,8 +218,7 @@ describe('LoginComponent', () => { expect(instance.loginForm.get('password').errors.required).toBeTruthy(); }); - it('should set error for too short password', async () => { - const { instance } = await shallow.render(); + it('should set error for too short password', () => { instance.loginForm.get('password').markAsTouched(); instance.loginForm.patchValue({ email: TEST_DATA.user.email, @@ -234,8 +229,7 @@ describe('LoginComponent', () => { expect(instance.loginForm.get('password').errors.minlength).toBeTruthy(); }); - it('should have no errors when email and password valid', async () => { - const { instance } = await shallow.render(); + it('should have no errors when email and password valid', () => { instance.loginForm.markAsTouched(); instance.loginForm.patchValue({ email: TEST_DATA.user.email, @@ -246,73 +240,67 @@ describe('LoginComponent', () => { }); it('should log in the user if they have archives', async () => { - const { instance } = await shallow.render(); - harness.setComponent(instance); harness.setupNormalLogin(); await harness.testLogin(); - expect(router.navigatedRoute).toContain('/'); + expect(harness.navigateSpy).toHaveBeenCalledWith(['/'], jasmine.anything()); expect(accountService.switchedToDefaultArchive).toBeTrue(); }); it('should redirect to onboarding if the user has no archives', async () => { - const { instance } = await shallow.render(); - harness.setComponent(instance); harness.setupOnboarding(); await harness.testLogin(); - expect(router.navigatedRoute.join('/')).toContain('onboarding'); + expect(harness.navigateSpy).toHaveBeenCalledWith(['/app/onboarding']); }); it('should redirect to public if the user is coming from timeline view', async () => { - const { instance } = await shallow.render(); - harness.setComponent(instance); harness.setupTimelineCta(); await harness.testLogin(); - expect(router.navigatedRoute).toContain('/public'); + expect(harness.navigateSpy).toHaveBeenCalledWith( + ['/public'], + jasmine.objectContaining({ queryParams: { cta: 'timeline' } }), + ); }); it('should redirect to a sharebyurl if the param is set', async () => { - const { instance } = await shallow.render(); - harness.setComponent(instance); harness.setupShareByUrl('test-1234'); await harness.testLogin(); - expect(router.navigatedRoute).toContain('/share'); - expect(router.navigatedRoute).toContain('test-1234'); + expect(harness.navigateSpy).toHaveBeenCalledWith(['/share', 'test-1234']); }); it('should redirect to Verify page if user needs verification', async () => { - const { instance } = await shallow.render(); - harness.setComponent(instance); harness.setupVerify(); await harness.testLogin(); - expect(router.navigatedRoute).toContain('verify'); + expect(harness.navigateSpy).toHaveBeenCalledWith( + ['..', 'verify'], + jasmine.anything(), + ); }); it('should redirect to MFA page if user needs MFA', async () => { - const { instance } = await shallow.render(); - harness.setComponent(instance); harness.setupMfa(); await harness.testLogin(); - expect(router.navigatedRoute).toContain('mfa'); + expect(harness.navigateSpy).toHaveBeenCalledWith( + ['..', 'mfa'], + jasmine.anything(), + ); }); it('should show an error message in case of login failure', async () => { - const { inject, instance } = await shallow.render(); - harness.setComponent(instance); harness.setupLoginError(); - const messageSpy = harness.getMessageSpy(inject); + const messageSpy = harness.getMessageSpy(); await harness.testLogin(); expect(messageSpy).toHaveBeenCalled(); @@ -320,20 +308,16 @@ describe('LoginComponent', () => { }); it('should show an error message in case of wrong username/password', async () => { - const { inject, instance } = await shallow.render(); - harness.setComponent(instance); harness.setupIncorrectLogin(); - const messageSpy = harness.getMessageSpy(inject); + const messageSpy = harness.getMessageSpy(); await harness.testLogin(); expect(messageSpy).toHaveBeenCalled(); expect(harness.hasPasswordBeenCleared()).toBeTrue(); }); - it('should display the loading spinner', async () => { - const { instance, fixture } = await shallow.render(); - + it('should display the loading spinner', () => { instance.waiting = true; fixture.detectChanges(); const compiled = fixture.debugElement.nativeElement; diff --git a/src/app/component-library/components/button/button.component.spec.ts b/src/app/component-library/components/button/button.component.spec.ts index 2ebb8810c..5550da2af 100644 --- a/src/app/component-library/components/button/button.component.spec.ts +++ b/src/app/component-library/components/button/button.component.spec.ts @@ -1,23 +1,26 @@ -import { Shallow } from 'shallow-render'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MockBuilder, ngMocks } from 'ng-mocks'; import { ComponentsModule } from '../../components.module'; import { ButtonComponent } from './button.component'; describe('ButtonComponent', () => { - let shallow: Shallow; + let fixture: ComponentFixture; + let instance: ButtonComponent; beforeEach(async () => { - shallow = new Shallow(ButtonComponent, ComponentsModule); - }); + await MockBuilder(ButtonComponent, ComponentsModule); - it('should create', async () => { - const { instance } = await shallow.render(); + fixture = TestBed.createComponent(ButtonComponent); + instance = fixture.componentInstance; + fixture.detectChanges(); + }); + it('should create', () => { expect(instance).toBeTruthy(); }); - it('should have the correct class based on variant', async () => { - const { instance, fixture, find } = await shallow.render(); - const button = find('.button'); + it('should have the correct class based on variant', () => { + const button = ngMocks.find('.button'); expect(button.nativeElement.classList).toContain('button-primary'); @@ -32,9 +35,8 @@ describe('ButtonComponent', () => { expect(button.nativeElement.classList).toContain('button-tertiary'); }); - it('should have the correct class based on size', async () => { - const { instance, fixture, find } = await shallow.render(); - const button = find('.button'); + it('should have the correct class based on size', () => { + const button = ngMocks.find('.button'); expect(button.nativeElement.classList).toContain('button-hug'); @@ -44,9 +46,8 @@ describe('ButtonComponent', () => { expect(button.nativeElement.classList).toContain('button-fill'); }); - it('should disable the button', async () => { - const { instance, fixture, find } = await shallow.render(); - const button = find('.button'); + it('should disable the button', () => { + const button = ngMocks.find('.button'); instance.disabled = true; fixture.detectChanges(); @@ -54,18 +55,17 @@ describe('ButtonComponent', () => { expect(button.nativeElement.disabled).toBeTrue(); }); - it('should emit the @Output when clicking the button', async () => { - const { instance, find } = await shallow.render(); - const button = find('.button').nativeElement; + it('should emit the @Output when clicking the button', () => { + spyOn(instance.buttonClick, 'emit'); + const button = ngMocks.find('.button').nativeElement; button.click(); expect(instance.buttonClick.emit).toHaveBeenCalled(); }); - it('should have the correct class based on mode', async () => { - const { instance, fixture, find } = await shallow.render(); - const button = find('.button'); + it('should have the correct class based on mode', () => { + const button = ngMocks.find('.button'); expect(button.nativeElement.classList).toContain('button-light'); @@ -75,9 +75,8 @@ describe('ButtonComponent', () => { expect(button.nativeElement.classList).toContain('button-dark'); }); - it('should have the correct class based on orientation', async () => { - const { instance, fixture, find } = await shallow.render(); - const button = find('.button'); + it('should have the correct class based on orientation', () => { + const button = ngMocks.find('.button'); instance.orientation = 'right'; fixture.detectChanges(); @@ -85,9 +84,8 @@ describe('ButtonComponent', () => { expect(button.nativeElement.classList).toContain('button-reverse'); }); - it('should have the correct class based on height', async () => { - const { instance, fixture, find } = await shallow.render(); - const button = find('.button'); + it('should have the correct class based on height', () => { + const button = ngMocks.find('.button'); expect(button.nativeElement.classList).toContain('button-medium'); @@ -97,10 +95,8 @@ describe('ButtonComponent', () => { expect(button.nativeElement.classList).toContain('button-large'); }); - it('should have the correct type based on the type input', async () => { - const { instance, fixture, find } = await shallow.render(); - - const button = find('.button'); + it('should have the correct type based on the type input', () => { + const button = ngMocks.find('.button'); expect(button.nativeElement.type).toEqual('button'); diff --git a/src/app/component-library/components/checkbox/checkbox.component.spec.ts b/src/app/component-library/components/checkbox/checkbox.component.spec.ts index bf135d011..563d9c13a 100644 --- a/src/app/component-library/components/checkbox/checkbox.component.spec.ts +++ b/src/app/component-library/components/checkbox/checkbox.component.spec.ts @@ -1,91 +1,89 @@ -import { Shallow } from 'shallow-render'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MockBuilder, ngMocks } from 'ng-mocks'; import { ComponentsModule } from '../../components.module'; import { CheckboxComponent } from './checkbox.component'; describe('CheckboxCompoent', () => { - let shallow: Shallow; + let fixture: ComponentFixture; + let instance: CheckboxComponent; beforeEach(async () => { - shallow = new Shallow(CheckboxComponent, ComponentsModule); - }); + await MockBuilder(CheckboxComponent, ComponentsModule); - it('should create', async () => { - const { instance } = await shallow.render(); + fixture = TestBed.createComponent(CheckboxComponent); + instance = fixture.componentInstance; + fixture.detectChanges(); + }); + it('should create', () => { expect(instance).toBeTruthy(); }); - it('should have the disabled class if the checkbox is disabled', async () => { - const { instance, find, fixture } = await shallow.render(); + it('should have the disabled class if the checkbox is disabled', () => { instance.disabled = true; fixture.detectChanges(); - const checkbox = find('.checkbox-container').nativeElement; + const checkbox = ngMocks.find('.checkbox-container').nativeElement; expect(checkbox.classList).toContain('checkbox-container-disabled'); expect(checkbox.classList).not.toContain('checkbox-container-enabled'); }); - it('should have the enabled class if the checkbox is enabled', async () => { - const { find } = await shallow.render(); - const checkbox = find('.checkbox-container').nativeElement; + it('should have the enabled class if the checkbox is enabled', () => { + const checkbox = ngMocks.find('.checkbox-container').nativeElement; expect(checkbox.classList).toContain('checkbox-container-enabled'); expect(checkbox.classList).not.toContain('checkbox-container-disabled'); }); - it('should have the checked class if the checkbox is checked', async () => { - const { find, instance, fixture } = await shallow.render(); + it('should have the checked class if the checkbox is checked', () => { instance.isChecked = true; fixture.detectChanges(); - const checkbox = find('.checkbox').nativeElement; + const checkbox = ngMocks.find('.checkbox').nativeElement; expect(checkbox.classList).toContain('checked'); }); - it('should emit the correct value when the checkbox is clicked', async () => { - const { find, instance, fixture } = await shallow.render(); + it('should emit the correct value when the checkbox is clicked', () => { + spyOn(instance.isCheckedChange, 'emit'); instance.value = 'value'; fixture.detectChanges(); - const checkbox = find('.checkbox-container').nativeElement; + const checkbox = ngMocks.find('.checkbox-container').nativeElement; checkbox.click(); expect(instance.isCheckedChange.emit).toHaveBeenCalledWith(true); }); - it("should not emit any value when the checkbox is clicked and it's disabled ", async () => { - const { find, instance, fixture } = await shallow.render(); + it("should not emit any value when the checkbox is clicked and it's disabled ", () => { + spyOn(instance.isCheckedChange, 'emit'); instance.disabled = true; instance.value = 'value'; fixture.detectChanges(); - const checkbox = find('.checkbox-container').nativeElement; + const checkbox = ngMocks.find('.checkbox-container').nativeElement; checkbox.click(); expect(instance.isCheckedChange.emit).not.toHaveBeenCalled(); }); - it('should have the primary class if the variant is set to primary', async () => { - const { instance, find, fixture } = await shallow.render(); + it('should have the primary class if the variant is set to primary', () => { instance.variant = 'primary'; fixture.detectChanges(); - const checkbox = find('.checkbox-container').nativeElement; + const checkbox = ngMocks.find('.checkbox-container').nativeElement; expect(checkbox.classList).toContain('checkbox-container-primary'); }); - it('should have the secondary class if the variant is set to secondary', async () => { - const { instance, find, fixture } = await shallow.render(); + it('should have the secondary class if the variant is set to secondary', () => { instance.variant = 'secondary'; fixture.detectChanges(); - const checkbox = find('.checkbox-container').nativeElement; + const checkbox = ngMocks.find('.checkbox-container').nativeElement; expect(checkbox.classList).toContain('checkbox-container-secondary'); }); - it('should be focusable and have correct ARIA attributes', async () => { - const { find } = await shallow.render(); - const checkboxContainer = find('.checkbox').nativeElement; + it('should be focusable and have correct ARIA attributes', () => { + const checkboxContainer = ngMocks.find('.checkbox').nativeElement; expect(checkboxContainer.getAttribute('role')).toEqual('checkbox'); expect(checkboxContainer.getAttribute('tabindex')).toEqual('0'); @@ -93,36 +91,33 @@ describe('CheckboxCompoent', () => { expect(checkboxContainer.getAttribute('aria-disabled')).toEqual('false'); }); - it('should toggle checked state on Enter key press', async () => { - const { find, instance, fixture } = await shallow.render(); + it('should toggle checked state on Enter key press', () => { instance.isChecked = false; fixture.detectChanges(); - const checkboxContainer = find('.checkbox-container').nativeElement; + const checkboxContainer = ngMocks.find('.checkbox-container').nativeElement; const event = new KeyboardEvent('keydown', { key: 'Enter' }); checkboxContainer.dispatchEvent(event); expect(instance.isChecked).toBeTruthy(); }); - it('should toggle checked state on Space key press', async () => { - const { find, instance, fixture } = await shallow.render(); + it('should toggle checked state on Space key press', () => { instance.isChecked = false; fixture.detectChanges(); - const checkboxContainer = find('.checkbox-container').nativeElement; + const checkboxContainer = ngMocks.find('.checkbox-container').nativeElement; const event = new KeyboardEvent('keydown', { key: ' ' }); checkboxContainer.dispatchEvent(event); expect(instance.isChecked).toBeTruthy(); }); - it('should not toggle checked state when disabled and Enter key is pressed', async () => { - const { find, instance, fixture } = await shallow.render(); + it('should not toggle checked state when disabled and Enter key is pressed', () => { instance.disabled = true; fixture.detectChanges(); - const checkboxContainer = find('.checkbox-container').nativeElement; + const checkboxContainer = ngMocks.find('.checkbox-container').nativeElement; const event = new KeyboardEvent('keydown', { key: 'Enter' }); checkboxContainer.dispatchEvent(event); @@ -130,12 +125,10 @@ describe('CheckboxCompoent', () => { expect(instance.isChecked).toBeFalsy(); }); - it("should have aria-disabled set to 'true' when disabled", async () => { - const { find, fixture } = await shallow.render({ - bind: { disabled: true }, - }); + it("should have aria-disabled set to 'true' when disabled", () => { + instance.disabled = true; fixture.detectChanges(); - const checkboxContainer = find('.checkbox').nativeElement; + const checkboxContainer = ngMocks.find('.checkbox').nativeElement; expect(checkboxContainer.getAttribute('aria-disabled')).toEqual('true'); }); diff --git a/src/app/component-library/components/form-input/form-input.component.spec.ts b/src/app/component-library/components/form-input/form-input.component.spec.ts index 45341d868..7ea62a670 100644 --- a/src/app/component-library/components/form-input/form-input.component.spec.ts +++ b/src/app/component-library/components/form-input/form-input.component.spec.ts @@ -1,90 +1,83 @@ -import { Shallow } from 'shallow-render'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MockBuilder, ngMocks } from 'ng-mocks'; import { ReactiveFormsModule, UntypedFormControl } from '@angular/forms'; import { ComponentsModule } from '../../components.module'; import { FormInputComponent, FormInputConfig } from './form-input.component'; describe('FormInputComponent', () => { - let shallow: Shallow; + let fixture: ComponentFixture; + let instance: FormInputComponent; beforeEach(async () => { - shallow = new Shallow(FormInputComponent, ComponentsModule).import( + await MockBuilder(FormInputComponent, ComponentsModule).keep( ReactiveFormsModule, ); + + fixture = TestBed.createComponent(FormInputComponent); + instance = fixture.componentInstance; }); - it('should create with an empty control', async () => { - const mockControl = new UntypedFormControl(''); - const { instance } = await shallow.render({ - bind: { control: mockControl }, - }); + it('should create with an empty control', () => { + instance.control = new UntypedFormControl(''); + fixture.detectChanges(); expect(instance).toBeTruthy(); }); - it('should create with a control', async () => { - const mockControl = new UntypedFormControl('input'); - const { instance } = await shallow.render({ - bind: { control: mockControl }, - }); + it('should create with a control', () => { + instance.control = new UntypedFormControl('input'); + fixture.detectChanges(); expect(instance).toBeTruthy(); }); - it('should bind the input type with empty form control', async () => { - const mockControl = new UntypedFormControl(''); - const { find } = await shallow.render({ - bind: { type: 'password', control: mockControl }, - }); + it('should bind the input type with empty form control', () => { + instance.control = new UntypedFormControl(''); + instance.type = 'password'; + fixture.detectChanges(); - const inputElement = find('input').nativeElement; + const inputElement = ngMocks.find('input').nativeElement; expect(inputElement.type).toBe('password'); }); - it('should bind the input type with form control', async () => { - const control = new UntypedFormControl('input'); - const { find } = await shallow.render({ - bind: { control, type: 'password' }, - }); + it('should bind the input type with form control', () => { + instance.control = new UntypedFormControl('input'); + instance.type = 'password'; + fixture.detectChanges(); - const inputElement = find('input').nativeElement; + const inputElement = ngMocks.find('input').nativeElement; expect(inputElement.type).toBe('password'); }); - it('should hide label for empty number inputs', async () => { - const mockControl = new UntypedFormControl(''); - - const { instance } = await shallow.render({ - bind: { type: 'number', control: mockControl }, - }); + it('should hide label for empty number inputs', () => { + instance.control = new UntypedFormControl(''); + instance.type = 'number'; + fixture.detectChanges(); expect(instance.isLabelHidden()).toBeTrue(); }); - it('should show label for non-empty text inputs', async () => { - const mockControl = new UntypedFormControl('Some text'); - - const { instance } = await shallow.render({ - bind: { type: 'text', control: mockControl }, - }); + it('should show label for non-empty text inputs', () => { + instance.control = new UntypedFormControl('Some text'); + instance.type = 'text'; + fixture.detectChanges(); expect(instance.isLabelHidden()).toBeFalse(); }); - it('should set input attributes based on config', async () => { - const mockControl = new UntypedFormControl('Some text'); - + it('should set input attributes based on config', () => { + instance.control = new UntypedFormControl('Some text'); const config: FormInputConfig = { autocorrect: 'off', autocapitalize: 'off', spellcheck: 'off', }; - const { find } = await shallow.render({ - bind: { config, control: mockControl }, - }); + instance.config = config; + fixture.detectChanges(); - const inputElement = find('input').nativeElement; + const inputElement = ngMocks.find('input').nativeElement; expect(inputElement.getAttribute('autocorrect')).toBe(config.autocorrect); @@ -95,14 +88,11 @@ describe('FormInputComponent', () => { expect(inputElement.getAttribute('spellcheck')).toBe(config.spellcheck); }); - it('should emit valueChange event on input value change', async () => { - const mockControl = new UntypedFormControl(''); + it('should emit valueChange event on input value change', () => { + instance.control = new UntypedFormControl(''); + fixture.detectChanges(); const mockValue = 'test value'; - const { instance } = await shallow.render({ - bind: { control: mockControl }, - }); - const spy = spyOn(instance.valueChangeSubject, 'next'); instance.onInputChange(mockValue); @@ -110,47 +100,40 @@ describe('FormInputComponent', () => { expect(spy).toHaveBeenCalledWith(mockValue); }); - it('should apply right-align class based on config', async () => { - const mockControl = new UntypedFormControl(''); - const { instance, fixture } = await shallow.render({ - bind: { config: { textAlign: 'right' }, control: mockControl }, - }); + it('should apply right-align class based on config', () => { + instance.control = new UntypedFormControl(''); + instance.config = { textAlign: 'right' }; fixture.detectChanges(); expect(instance.rightAlign).toBeTrue(); }); - it('should bind the placeholder attribute to the input element', async () => { - const mockControl = new UntypedFormControl(''); + it('should bind the placeholder attribute to the input element', () => { + instance.control = new UntypedFormControl(''); const placeholderValue = 'Enter text here'; - const { find } = await shallow.render({ - bind: { placeholder: placeholderValue, control: mockControl }, - }); + instance.placeholder = placeholderValue; + fixture.detectChanges(); - const inputElement = find('input').nativeElement; + const inputElement = ngMocks.find('input').nativeElement; expect(inputElement.placeholder).toBe(placeholderValue); }); - it('should return the correct error from the validation array', async () => { - const mockControl = new UntypedFormControl(''); - const { instance } = await shallow.render({ - bind: { - validators: [ - { - validation: 'minLength', - message: 'Must be at least 3 characters', - value: 3, - }, - { - validation: 'maxLength', - message: 'Must be at most 10 characters', - value: 10, - }, - ], - control: mockControl, + it('should return the correct error from the validation array', () => { + instance.control = new UntypedFormControl(''); + instance.validators = [ + { + validation: 'minLength', + message: 'Must be at least 3 characters', + value: 3, }, - }); + { + validation: 'maxLength', + message: 'Must be at most 10 characters', + value: 10, + }, + ]; + fixture.detectChanges(); const errorMessage = instance.getInputErrorFromValue('aa'); diff --git a/src/app/component-library/components/toggle/toggle.component.spec.ts b/src/app/component-library/components/toggle/toggle.component.spec.ts index f80fcd013..2786e9a19 100644 --- a/src/app/component-library/components/toggle/toggle.component.spec.ts +++ b/src/app/component-library/components/toggle/toggle.component.spec.ts @@ -1,41 +1,44 @@ -import { Shallow } from 'shallow-render'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MockBuilder, ngMocks } from 'ng-mocks'; +import { ComponentsModule } from '../../components.module'; import { ToggleComponent } from './toggle.component'; describe('ToggleComponent', () => { - let shallow: Shallow; + let fixture: ComponentFixture; + let instance: ToggleComponent; beforeEach(async () => { - shallow = new Shallow(ToggleComponent, Shallow); - }); + await MockBuilder(ToggleComponent, ComponentsModule); - it('should create', async () => { - const { instance } = await shallow.render(); + fixture = TestBed.createComponent(ToggleComponent); + instance = fixture.componentInstance; + fixture.detectChanges(); + }); + it('should create', () => { expect(instance).toBeTruthy(); }); - it('should have the checked class when the toggle is checked', async () => { - const { instance, find, fixture } = await shallow.render(); + it('should have the checked class when the toggle is checked', () => { instance.isChecked = true; fixture.detectChanges(); - const toggle = find('.toggle-container').nativeElement; + const toggle = ngMocks.find('.toggle-container').nativeElement; expect(toggle.classList).toContain('checked'); }); - it('should have the disabled class when the toggle is disabled', async () => { - const { instance, find, fixture } = await shallow.render(); + it('should have the disabled class when the toggle is disabled', () => { instance.disabled = true; fixture.detectChanges(); - const toggle = find('.toggle-container').nativeElement; + const toggle = ngMocks.find('.toggle-container').nativeElement; expect(toggle.classList).toContain('disabled'); }); - it('should emit the correct value when the toggle is clicked', async () => { - const { instance, find, fixture } = await shallow.render(); + it('should emit the correct value when the toggle is clicked', () => { + spyOn(instance.isCheckedChange, 'emit'); fixture.detectChanges(); - const toggle = find('.toggle-container').nativeElement; + const toggle = ngMocks.find('.toggle-container').nativeElement; toggle.click(); expect(instance.isCheckedChange.emit).toHaveBeenCalledWith(true); @@ -47,11 +50,11 @@ describe('ToggleComponent', () => { expect(instance.isCheckedChange.emit).toHaveBeenCalledWith(false); }); - it('should not emit when the toggle is disabled', async () => { - const { instance, find, fixture } = await shallow.render(); + it('should not emit when the toggle is disabled', () => { + spyOn(instance.isCheckedChange, 'emit'); instance.disabled = true; fixture.detectChanges(); - const toggle = find('.toggle-container').nativeElement; + const toggle = ngMocks.find('.toggle-container').nativeElement; toggle.click(); expect(instance.isCheckedChange.emit).not.toHaveBeenCalled(); diff --git a/src/app/core/components/account-settings/account-settings.component.spec.ts b/src/app/core/components/account-settings/account-settings.component.spec.ts index 91e395f98..494039f6d 100644 --- a/src/app/core/components/account-settings/account-settings.component.spec.ts +++ b/src/app/core/components/account-settings/account-settings.component.spec.ts @@ -1,10 +1,13 @@ -import { Shallow } from 'shallow-render'; import { NgModule } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { AccountService } from '@shared/services/account/account.service'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { ApiService } from '@shared/services/api/api.service'; import { MessageService } from '@shared/services/message/message.service'; +import { PrConstantsService } from '@shared/services/pr-constants/pr-constants.service'; +import { EventService } from '@shared/services/event/event.service'; import { AccountVO } from '@models/account-vo'; import { AccountSettingsComponent } from './account-settings.component'; @@ -24,39 +27,54 @@ class MockAccountService { } describe('AccountSettingsComponent', () => { - let shallow: Shallow; - - beforeEach(() => { - shallow = new Shallow(AccountSettingsComponent, DummyModule) - .provide(HttpClientTestingModule) - .provideMock( - { provide: AccountService, useClass: MockAccountService }, - { provide: ActivatedRoute, useValue: {} }, - { - provide: ApiService, - useValue: { account: { update: async () => new AccountVO({}) } }, + beforeEach(async () => { + await MockBuilder(AccountSettingsComponent, DummyModule) + .keep(HttpClientTestingModule, { export: true }) + .provide({ provide: AccountService, useClass: MockAccountService }) + .provide({ provide: ActivatedRoute, useValue: {} }) + .provide({ provide: Router, useValue: { navigate: async () => {} } }) + .provide({ + provide: PrConstantsService, + useValue: { + getCountries: () => [], + getStates: () => ({}), }, - { - provide: MessageService, - useValue: { showMessage(_: any) {}, showError(_: any) {} }, - }, - ); + }) + .provide({ + provide: EventService, + useValue: { dispatch: () => {} }, + }) + .provide({ + provide: ApiService, + useValue: { account: { update: async () => new AccountVO({}) } }, + }) + .provide({ + provide: MessageService, + useValue: { showMessage(_: any) {}, showError(_: any) {} }, + }); }); - it('exists', async () => { - const { instance } = await shallow.render(); + it('exists', () => { + const fixture = MockRender(AccountSettingsComponent); - expect(instance).toBeTruthy(); + expect(fixture.point.componentInstance).toBeTruthy(); }); it('can save an account property', async () => { - const { instance, inject } = await shallow.render(); + const fixture = MockRender(AccountSettingsComponent); + const instance = fixture.point.componentInstance; - const accountService = inject(AccountService); + const accountService = TestBed.inject(AccountService); const setAccountSpy = spyOn(accountService, 'setAccount').and.callThrough(); - const accountUpdateSpy = spyOn(inject(ApiService).account, 'update'); - const successfulMessageSpy = spyOn(inject(MessageService), 'showMessage'); - const errorMessageSpy = spyOn(inject(MessageService), 'showError'); + const accountUpdateSpy = spyOn( + TestBed.inject(ApiService).account, + 'update', + ); + const successfulMessageSpy = spyOn( + TestBed.inject(MessageService), + 'showMessage', + ); + const errorMessageSpy = spyOn(TestBed.inject(MessageService), 'showError'); try { await instance.onSaveProfileInfo('fullName', 'New Name'); @@ -73,16 +91,20 @@ describe('AccountSettingsComponent', () => { }); it('should reset an account property if an error occurs', async () => { - const { instance, inject } = await shallow.render(); + const fixture = MockRender(AccountSettingsComponent); + const instance = fixture.point.componentInstance; - const accountService = inject(AccountService); + const accountService = TestBed.inject(AccountService); const setAccountSpy = spyOn(accountService, 'setAccount').and.callThrough(); const accountUpdateSpy = spyOn( - inject(ApiService).account, + TestBed.inject(ApiService).account, 'update', ).and.rejectWith({}); - const successfulMessageSpy = spyOn(inject(MessageService), 'showMessage'); - const errorMessageSpy = spyOn(inject(MessageService), 'showError'); + const successfulMessageSpy = spyOn( + TestBed.inject(MessageService), + 'showMessage', + ); + const errorMessageSpy = spyOn(TestBed.inject(MessageService), 'showError'); try { await instance.onSaveProfileInfo('fullName', 'New Name'); @@ -98,26 +120,28 @@ describe('AccountSettingsComponent', () => { } }); - it('should disable "Verify Phone Number" button if primaryPhone is empty', async () => { - const { find, instance, fixture } = await shallow.render(); + it('should disable "Verify Phone Number" button if primaryPhone is empty', () => { + const fixture = MockRender(AccountSettingsComponent); + const instance = fixture.point.componentInstance; instance.account.primaryPhone = ''; instance.account.phoneStatus = ''; fixture.detectChanges(); - const button = find('.verify-phone-button'); + const button = ngMocks.find('.verify-phone-button'); expect(button.properties.disabled).toBeTrue(); }); - it('should enable "Verify Phone Number" button if primaryPhone exists', async () => { - const { find, instance, fixture } = await shallow.render(); + it('should enable "Verify Phone Number" button if primaryPhone exists', () => { + const fixture = MockRender(AccountSettingsComponent); + const instance = fixture.point.componentInstance; instance.account.primaryPhone = '1234567890'; instance.account.phoneStatus = ''; fixture.detectChanges(); - const button = find('.verify-phone-button'); + const button = ngMocks.find('.verify-phone-button'); expect(button.properties.disabled).toBeFalse(); }); diff --git a/src/app/core/components/advanced-settings/advanced-settings.component.spec.ts b/src/app/core/components/advanced-settings/advanced-settings.component.spec.ts index 03e4375af..70f0522d4 100644 --- a/src/app/core/components/advanced-settings/advanced-settings.component.spec.ts +++ b/src/app/core/components/advanced-settings/advanced-settings.component.spec.ts @@ -1,11 +1,16 @@ +import { NgModule } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { MockBuilder, MockRender } from 'ng-mocks'; + import { AccountService } from '@shared/services/account/account.service'; import { AccountVO } from '@root/app/models'; -import { CoreModule } from '@core/core.module'; import { MessageService } from '@shared/services/message/message.service'; -import { Shallow } from 'shallow-render'; import { ApiService } from '../../../shared/services/api/api.service'; import { AdvancedSettingsComponent } from './advanced-settings.component'; +@NgModule() +class DummyModule {} + const mockAccountService = { getAccount: () => new AccountVO({ accountId: 1, allowSftpDeletion: true }), setAccount: (account: AccountVO) => {}, @@ -18,50 +23,60 @@ const mockApiService = { }; describe('AdvancedSettingsComponent', () => { - let shallow: Shallow; let messageShown = false; beforeEach(async () => { - shallow = new Shallow(AdvancedSettingsComponent, CoreModule) - .mock(MessageService, { - showError: () => { - messageShown = true; + messageShown = false; + await MockBuilder(AdvancedSettingsComponent, DummyModule) + .provide({ + provide: MessageService, + useValue: { + showError: () => { + messageShown = true; + }, }, }) - .mock(ApiService, mockApiService) - .mock(AccountService, mockAccountService); + .provide({ + provide: ApiService, + useValue: mockApiService, + }) + .provide({ + provide: AccountService, + useValue: mockAccountService, + }); }); - it('should create', async () => { - const { instance } = await shallow.render(); + it('should create', () => { + const fixture = MockRender(AdvancedSettingsComponent); - expect(instance).not.toBeNull(); + expect(fixture.point.componentInstance).not.toBeNull(); }); - it('initializes allowSFTPDeletion from the account service', async () => { - const { instance } = await shallow.render(); + it('initializes allowSFTPDeletion from the account service', () => { + const fixture = MockRender(AdvancedSettingsComponent); - expect(instance.allowSFTPDeletion).toEqual(1); + expect(fixture.point.componentInstance.allowSFTPDeletion).toEqual(1); }); it('updates account on calling onAllowSFTPDeletion', async () => { - const { instance, inject } = await shallow.render(); - const apiService = inject(ApiService); + const fixture = MockRender(AdvancedSettingsComponent); + const apiService = TestBed.inject(ApiService); const spy = spyOn(apiService.account, 'update').and.resolveTo( new AccountVO({}), ); - await instance.onAllowSFTPDeletion(); + await fixture.point.componentInstance.onAllowSFTPDeletion(); expect(spy).toHaveBeenCalled(); }); it('handles errors in onAllowSFTPDeletion', async () => { - const { instance, inject } = await shallow.render(); + const fixture = MockRender(AdvancedSettingsComponent); + const apiService = TestBed.inject(ApiService); - spyOn(inject(ApiService).account, 'update').and.throwError('test error'); + spyOn(apiService.account, 'update').and.throwError('test error'); - await instance.onAllowSFTPDeletion(); + await fixture.point.componentInstance.onAllowSFTPDeletion(); expect(messageShown).toBe(true); }); diff --git a/src/app/core/components/archive-payer/archive-payer.component.spec.ts b/src/app/core/components/archive-payer/archive-payer.component.spec.ts index eecc1f6d3..0f629ed99 100644 --- a/src/app/core/components/archive-payer/archive-payer.component.spec.ts +++ b/src/app/core/components/archive-payer/archive-payer.component.spec.ts @@ -10,7 +10,7 @@ describe('ArchivePayerComponent', () => { let fixture: ComponentFixture; let mockAccountService; - beforeEach(() => { + beforeEach(async () => { mockAccountService = { getAccount: jasmine .createSpy('getAccount') @@ -19,9 +19,7 @@ describe('ArchivePayerComponent', () => { .createSpy('getArchive') .and.returnValue({ accessRole: 'access.role.manager' }), }; - }); - beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [ArchivePayerComponent], providers: [ diff --git a/src/app/core/components/archive-settings-dialog/archive-settings-dialog.component.spec.ts b/src/app/core/components/archive-settings-dialog/archive-settings-dialog.component.spec.ts index d6a06bb5c..bfc20acd2 100644 --- a/src/app/core/components/archive-settings-dialog/archive-settings-dialog.component.spec.ts +++ b/src/app/core/components/archive-settings-dialog/archive-settings-dialog.component.spec.ts @@ -1,6 +1,7 @@ -import { Shallow } from 'shallow-render'; -import { CoreModule } from '@core/core.module'; +import { NgModule } from '@angular/core'; +import { MockBuilder, MockRender } from 'ng-mocks'; import { DialogRef } from '@angular/cdk/dialog'; +import { ActivatedRoute } from '@angular/router'; import { ApiService } from '@shared/services/api/api.service'; import { ArchiveVO } from '@models/index'; import { AccountService } from '@shared/services/account/account.service'; @@ -8,6 +9,9 @@ import { TagsService } from '@core/services/tags/tags.service'; import { ArchiveResponse } from '@shared/services/api/archive.repo'; import { ArchiveSettingsDialogComponent } from './archive-settings-dialog.component'; +@NgModule() +class DummyModule {} + class MockDialogRef { close(value?: any): void {} } @@ -15,7 +19,7 @@ class MockDialogRef { const mockApiService = { tag: { async getTagsByArchive(archive) { - return await Promise.resolve([]); + return await Promise.resolve({ getTagVOs: () => [] }); }, }, archive: { @@ -40,37 +44,40 @@ const mockAccountService = { }; describe('ArchiveSettingsDialogComponent', () => { - let shallow: Shallow; - - beforeEach(() => { - shallow = new Shallow(ArchiveSettingsDialogComponent, CoreModule) - .provide({ - provide: DialogRef, - useClass: MockDialogRef, - }) - .provideMock({ - provide: ApiService, - useValue: mockApiService, - }) - .provideMock({ - provide: AccountService, - useValue: mockAccountService, - }) - .provide({ - provide: TagsService, - useValue: mockTagsService, - }); - }); + beforeEach( + async () => + await MockBuilder(ArchiveSettingsDialogComponent, DummyModule) + .provide({ + provide: DialogRef, + useClass: MockDialogRef, + }) + .provide({ + provide: ActivatedRoute, + useValue: { snapshot: { fragment: null } }, + }) + .provide({ + provide: ApiService, + useValue: mockApiService, + }) + .provide({ + provide: AccountService, + useValue: mockAccountService, + }) + .provide({ + provide: TagsService, + useValue: mockTagsService, + }), + ); - it('should create', async () => { - const { instance } = await shallow.render(); + it('should create', () => { + const fixture = MockRender(ArchiveSettingsDialogComponent); - expect(instance).toBeTruthy(); + expect(fixture.point.componentInstance).toBeTruthy(); }); - it('should initialize with public-settings tab', async () => { - const { instance } = await shallow.render(); + it('should initialize with public-settings tab', () => { + const fixture = MockRender(ArchiveSettingsDialogComponent); - expect(instance.activeTab).toBe('public-settings'); + expect(fixture.point.componentInstance.activeTab).toBe('public-settings'); }); }); diff --git a/src/app/core/components/billing-settings/billing-settings.component.spec.ts b/src/app/core/components/billing-settings/billing-settings.component.spec.ts index 9d3b7f56f..f8aa64224 100644 --- a/src/app/core/components/billing-settings/billing-settings.component.spec.ts +++ b/src/app/core/components/billing-settings/billing-settings.component.spec.ts @@ -1,10 +1,13 @@ -import { Shallow } from 'shallow-render'; import { NgModule } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { MockBuilder, MockRender } from 'ng-mocks'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { AccountVO } from '@models/account-vo'; import { AccountService } from '@shared/services/account/account.service'; import { ApiService } from '@shared/services/api/api.service'; import { MessageService } from '@shared/services/message/message.service'; +import { PrConstantsService } from '@shared/services/pr-constants/pr-constants.service'; +import { EventService } from '@shared/services/event/event.service'; import { BillingSettingsComponent } from './billing-settings.component'; @NgModule() @@ -27,41 +30,49 @@ class MockAccountService { } describe('BillingSettingsComponent', () => { - let shallow: Shallow; - - beforeEach(() => { - shallow = new Shallow(BillingSettingsComponent, DummyModule) - .provide(HttpClientTestingModule) - .provideMock( - { provide: AccountService, useClass: MockAccountService }, - { - provide: ApiService, - useValue: { account: { update: async () => new AccountVO({}) } }, - }, - { - provide: MessageService, - useValue: { showMessage(_: any) {}, showError(_: any) {} }, - }, - ); + beforeEach(async () => { + await MockBuilder(BillingSettingsComponent, DummyModule) + .keep(HttpClientTestingModule, { export: true }) + .provide({ provide: AccountService, useClass: MockAccountService }) + .provide({ + provide: ApiService, + useValue: { account: { update: async () => new AccountVO({}) } }, + }) + .provide({ + provide: MessageService, + useValue: { showMessage(_: any) {}, showError(_: any) {} }, + }) + .provide({ + provide: PrConstantsService, + useValue: { getCountries: () => [], getStates: () => ({}) }, + }) + .provide({ + provide: EventService, + useValue: { dispatch: () => {} }, + }); }); - it('exists', async () => { - const { instance } = await shallow.render(); + it('exists', () => { + const fixture = MockRender(BillingSettingsComponent); - expect(instance).toBeTruthy(); + expect(fixture.point.componentInstance).toBeTruthy(); }); it('can save an account property', async () => { - const { instance, inject } = await shallow.render(); + const fixture = MockRender(BillingSettingsComponent); + const instance = fixture.point.componentInstance; - const accountService = inject(AccountService); + const accountService = TestBed.inject(AccountService); const setAccountSpy = spyOn(accountService, 'setAccount').and.callThrough(); const accountUpdateSpy = spyOn( - inject(ApiService).account, + TestBed.inject(ApiService).account, 'update', ).and.resolveTo(new AccountVO({ zip: null })); - const successfulMessageSpy = spyOn(inject(MessageService), 'showMessage'); - const errorMessageSpy = spyOn(inject(MessageService), 'showError'); + const successfulMessageSpy = spyOn( + TestBed.inject(MessageService), + 'showMessage', + ); + const errorMessageSpy = spyOn(TestBed.inject(MessageService), 'showError'); try { await instance.onSaveInfo('fullName', 'New Name'); @@ -79,16 +90,20 @@ describe('BillingSettingsComponent', () => { }); it('should reset an account property if an error occurs', async () => { - const { instance, inject } = await shallow.render(); + const fixture = MockRender(BillingSettingsComponent); + const instance = fixture.point.componentInstance; - const accountService = inject(AccountService); + const accountService = TestBed.inject(AccountService); const setAccountSpy = spyOn(accountService, 'setAccount').and.callThrough(); const accountUpdateSpy = spyOn( - inject(ApiService).account, + TestBed.inject(ApiService).account, 'update', ).and.rejectWith({}); - const successfulMessageSpy = spyOn(inject(MessageService), 'showMessage'); - const errorMessageSpy = spyOn(inject(MessageService), 'showError'); + const successfulMessageSpy = spyOn( + TestBed.inject(MessageService), + 'showMessage', + ); + const errorMessageSpy = spyOn(TestBed.inject(MessageService), 'showError'); try { await instance.onSaveInfo('fullName', 'New Name'); diff --git a/src/app/core/components/gift-storage/gift-storage.component.spec.ts b/src/app/core/components/gift-storage/gift-storage.component.spec.ts index 78f2889be..1d88af4c5 100644 --- a/src/app/core/components/gift-storage/gift-storage.component.spec.ts +++ b/src/app/core/components/gift-storage/gift-storage.component.spec.ts @@ -1,6 +1,6 @@ -import { Shallow } from 'shallow-render'; -import { HttpClient, HttpHandler } from '@angular/common/http'; -import { CoreModule } from '@core/core.module'; +import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ReactiveFormsModule, UntypedFormBuilder } from '@angular/forms'; import { AccountService } from '@shared/services/account/account.service'; import { MessageService } from '@shared/services/message/message.service'; import { DialogCdkService } from '@root/app/dialog-cdk/dialog-cdk.service'; @@ -12,56 +12,71 @@ import { AccountVO } from '../../../models/account-vo'; import { GiftStorageComponent } from './gift-storage.component'; describe('GiftStorageComponent', () => { - let shallow: Shallow; - const mockAccount = new AccountVO({ accountId: 1 }); - const mockAccountService = { - getAccount: jasmine.createSpy('getAccount').and.returnValue({ - mockAccount, - }), - setAccount: jasmine.createSpy('setAccount'), - }; + let mockAccountService: any; + let mockDialog: any; + let mockApiService: any; - const mockDialog = jasmine.createSpyObj('DialogCdkService', ['open']); - mockDialog.open.and.returnValue({ - closed: of(true), - }); + beforeEach(async () => { + mockAccountService = { + getAccount: jasmine.createSpy('getAccount').and.returnValue(mockAccount), + setAccount: jasmine.createSpy('setAccount'), + }; + + mockDialog = jasmine.createSpyObj('DialogCdkService', ['open']); + mockDialog.open.and.returnValue({ + closed: of(true), + }); - const mockApiService = { - billing: { - giftStorage: jasmine.createSpy('giftStorage').and.returnValue( - Promise.resolve( - new GiftingResponse({ - storageGifted: 50, - giftDelivered: ['test@example.com', 'test1@example.com'], - invitationSent: ['test@example.com', 'test2@example.com'], - alreadyInvited: [], - }), + mockApiService = { + billing: { + giftStorage: jasmine.createSpy('giftStorage').and.returnValue( + Promise.resolve( + new GiftingResponse({ + storageGifted: 50, + giftDelivered: ['test@example.com', 'test1@example.com'], + invitationSent: ['test@example.com', 'test2@example.com'], + alreadyInvited: [], + }), + ), ), - ), - }, - }; - - beforeEach(() => { - shallow = new Shallow(GiftStorageComponent, CoreModule) - .provide([HttpClient, HttpHandler]) - .mock(AccountService, mockAccountService) - .mock(MessageService, { - showError: () => {}, + }, + }; + + await MockBuilder(GiftStorageComponent) + .keep(HttpClientTestingModule, { export: true }) + .keep(ReactiveFormsModule) + .keep(UntypedFormBuilder) + .provide({ + provide: AccountService, + useValue: mockAccountService, + }) + .provide({ + provide: MessageService, + useValue: { + showError: () => {}, + }, + }) + .provide({ + provide: DialogCdkService, + useValue: mockDialog, }) - .mock(DialogCdkService, mockDialog) - .mock(ApiService, mockApiService); + .provide({ + provide: ApiService, + useValue: mockApiService, + }); }); - it('should create', async () => { - const { instance } = await shallow.render(); + it('should create', () => { + const fixture = MockRender(GiftStorageComponent); - expect(instance).toBeTruthy(); + expect(fixture.point.componentInstance).toBeTruthy(); }); it('enables the "Send Gift Storage" button when the form is valid', async () => { - const { find, instance, fixture } = await shallow.render(); + const fixture = MockRender(GiftStorageComponent); + const instance = fixture.point.componentInstance; instance.availableSpace = '10'; @@ -74,13 +89,15 @@ describe('GiftStorageComponent', () => { fixture.detectChanges(); await fixture.whenStable(); - const button: HTMLButtonElement = find('.btn-primary').nativeElement; + const button: HTMLButtonElement = + ngMocks.find('.btn-primary').nativeElement; expect(button.disabled).toBe(false); }); - it('disables the submit button if at least one email is not valid', async () => { - const { find, instance, fixture } = await shallow.render(); + it('disables the submit button if at least one email is not valid', () => { + const fixture = MockRender(GiftStorageComponent); + const instance = fixture.point.componentInstance; instance.availableSpace = '5'; @@ -90,13 +107,15 @@ describe('GiftStorageComponent', () => { instance.giftForm.updateValueAndValidity(); fixture.detectChanges(); - const button: HTMLButtonElement = find('.btn-primary').nativeElement; + const button: HTMLButtonElement = + ngMocks.find('.btn-primary').nativeElement; expect(button.disabled).toBe(true); }); - it('disables the submit button if the there is a duplicate email', async () => { - const { find, instance, fixture } = await shallow.render(); + it('disables the submit button if the there is a duplicate email', () => { + const fixture = MockRender(GiftStorageComponent); + const instance = fixture.point.componentInstance; instance.availableSpace = '5'; @@ -108,13 +127,15 @@ describe('GiftStorageComponent', () => { instance.giftForm.updateValueAndValidity(); fixture.detectChanges(); - const button: HTMLButtonElement = find('.btn-primary').nativeElement; + const button: HTMLButtonElement = + ngMocks.find('.btn-primary').nativeElement; expect(button.disabled).toBe(true); }); it('disables the submit button if the amount entered exceeds the available amount', async () => { - const { find, instance, fixture } = await shallow.render(); + const fixture = MockRender(GiftStorageComponent); + const instance = fixture.point.componentInstance; instance.availableSpace = '5'; @@ -126,13 +147,15 @@ describe('GiftStorageComponent', () => { fixture.detectChanges(); await fixture.whenStable(); - const button: HTMLButtonElement = find('.btn-primary').nativeElement; + const button: HTMLButtonElement = + ngMocks.find('.btn-primary').nativeElement; expect(button.disabled).toBe(true); }); it('displays the total amount gifted based on the number of emails', async () => { - const { instance, fixture } = await shallow.render(); + const fixture = MockRender(GiftStorageComponent); + const instance = fixture.point.componentInstance; instance.availableSpace = '5'; @@ -150,7 +173,8 @@ describe('GiftStorageComponent', () => { }); it('disables the submit button if the amount multiplied by the number of emails exceeds the available amount', async () => { - const { find, instance, fixture } = await shallow.render(); + const fixture = MockRender(GiftStorageComponent); + const instance = fixture.point.componentInstance; instance.availableSpace = '5'; @@ -164,13 +188,15 @@ describe('GiftStorageComponent', () => { fixture.detectChanges(); await fixture.whenStable(); - const button: HTMLButtonElement = find('.btn-primary').nativeElement; + const button: HTMLButtonElement = + ngMocks.find('.btn-primary').nativeElement; expect(button.disabled).toBe(true); }); - it('parses the email string correctly', async () => { - const { instance } = await shallow.render(); + it('parses the email string correctly', () => { + const fixture = MockRender(GiftStorageComponent); + const instance = fixture.point.componentInstance; const result = instance.parseEmailString( 'test@example.com, test1@example.com', @@ -180,7 +206,8 @@ describe('GiftStorageComponent', () => { }); it('returns all the duplicate emails', async () => { - const { instance } = await shallow.render(); + const fixture = MockRender(GiftStorageComponent); + const instance = fixture.point.componentInstance; const testEmailString = 'test@example.com,test@example.com,test2@example.com'; @@ -192,8 +219,9 @@ describe('GiftStorageComponent', () => { expect(duplicates).toEqual(expectedDuplicates); }); - it('calls submitStorageGiftForm when the form is valid', async () => { - const { instance } = await shallow.render(); + it('calls submitStorageGiftForm when the form is valid', () => { + const fixture = MockRender(GiftStorageComponent); + const instance = fixture.point.componentInstance; instance.giftForm.controls.email.setValue('test@example.com'); instance.giftForm.controls.amount.setValue(5); @@ -211,9 +239,9 @@ describe('GiftStorageComponent', () => { }); it('filters out the duplicates from the giftDelivered and invitationSent of the response', async () => { - const { instance, fixture } = await shallow.render(); + const fixture = MockRender(GiftStorageComponent); + const instance = fixture.point.componentInstance; - // Simulate setting form values and submission instance.giftForm.controls.email.setValue('test@example.com'); instance.giftForm.controls.amount.setValue(5); instance.giftForm.updateValueAndValidity(); @@ -230,9 +258,10 @@ describe('GiftStorageComponent', () => { }); it('updates account details upon successful gift operation', async () => { - const { instance, fixture } = await shallow.render(); + const fixture = MockRender(GiftStorageComponent); + const instance = fixture.point.componentInstance; - instance.availableSpace = '100'; // 100 GB + instance.availableSpace = '100'; instance.giftForm.controls.email.setValue('test@example.com'); instance.giftForm.controls.amount.setValue('50'); @@ -242,7 +271,6 @@ describe('GiftStorageComponent', () => { await fixture.whenStable(); - // Expect that setAccount was called on the AccountService expect(mockAccountService.setAccount).toHaveBeenCalled(); expect(instance.availableSpace).toBe('50.00'); }); diff --git a/src/app/core/components/invitations-dialog/invitations-dialog.component.spec.ts b/src/app/core/components/invitations-dialog/invitations-dialog.component.spec.ts index 07b969d37..f0829c6c6 100644 --- a/src/app/core/components/invitations-dialog/invitations-dialog.component.spec.ts +++ b/src/app/core/components/invitations-dialog/invitations-dialog.component.spec.ts @@ -1,12 +1,16 @@ +import { NgModule } from '@angular/core'; +import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; +import { ReactiveFormsModule, UntypedFormBuilder } from '@angular/forms'; import { ApiService } from '@shared/services/api/api.service'; import { DialogRef, DIALOG_DATA } from '@angular/cdk/dialog'; import { AccountVO, InviteVO } from '@root/app/models'; -import { CoreModule } from '@core/core.module'; -import { Shallow } from 'shallow-render'; import { MessageService } from '@shared/services/message/message.service'; import { AccountService } from '@shared/services/account/account.service'; import { InvitationsDialogComponent } from './invitations-dialog.component'; +@NgModule() +class DummyModule {} + const mockAccountService = { getAccount: () => new AccountVO({ @@ -29,78 +33,96 @@ const mockApiService = { }; describe('InvitationsDialog', () => { - let shallow: Shallow; - - beforeEach(() => { - shallow = new Shallow(InvitationsDialogComponent, CoreModule) - .mock(DIALOG_DATA, { useValue: {} }) - .provide({ - provide: DialogRef, - useClass: DialogRefMock, - }) - .mock(AccountService, mockAccountService) - .mock(ApiService, mockApiService) - .mock(MessageService, { - showError: () => {}, - }); - }); - - it('exists', async () => { - const { element } = await shallow.render(); - - expect(element).not.toBeNull(); + beforeEach( + async () => + await MockBuilder(InvitationsDialogComponent, DummyModule) + .keep(ReactiveFormsModule, { export: true }) + .keep(UntypedFormBuilder) + .provide({ + provide: DIALOG_DATA, + useValue: {}, + }) + .provide({ + provide: DialogRef, + useClass: DialogRefMock, + }) + .provide({ + provide: AccountService, + useValue: mockAccountService, + }) + .provide({ + provide: ApiService, + useValue: mockApiService, + }) + .provide({ + provide: MessageService, + useValue: { + showError: () => {}, + }, + }), + ); + + it('exists', () => { + const fixture = MockRender(InvitationsDialogComponent); + + expect(fixture.point.nativeElement).not.toBeNull(); }); - it('displays the pending invitations table if there are any invitations pending', async () => { - const { fixture, find, instance } = await shallow.render(); + it('displays the pending invitations table if there are any invitations pending', () => { + const fixture = MockRender(InvitationsDialogComponent); + const instance = fixture.point.componentInstance; instance.activeTab = 'pending'; instance.pendingInvites = [new InviteVO({ email: 'testEmail1@test.com' })]; fixture.detectChanges(); - const table = find('.invitation'); + const table = ngMocks.findAll('.invitation'); expect(table.length).toBe(instance.pendingInvites.length); }); - it('displays the "no pending invites" message if there are no invites', async () => { - const { fixture, find, instance } = await shallow.render(); + it('displays the "no pending invites" message if there are no invites', () => { + const fixture = MockRender(InvitationsDialogComponent); + const instance = fixture.point.componentInstance; instance.activeTab = 'pending'; instance.pendingInvites = []; fixture.detectChanges(); - const message = find('.text-muted'); + const message = ngMocks.find('.text-muted'); expect(message).not.toBeNull(); }); - it('displays the accepted invitations table if there are any accepted pending', async () => { - const { fixture, find, instance } = await shallow.render(); + it('displays the accepted invitations table if there are any accepted pending', () => { + const fixture = MockRender(InvitationsDialogComponent); + const instance = fixture.point.componentInstance; instance.activeTab = 'accepted'; instance.acceptedInvites = [new InviteVO({ email: 'testEmail1@test.com' })]; fixture.detectChanges(); - const table = find('.invitation'); + const table = ngMocks.findAll('.invitation'); expect(table.length).toBe(instance.acceptedInvites.length); }); - it('displays the "no accepted invites" message if there are no invites', async () => { - const { fixture, find, instance } = await shallow.render(); + it('displays the "no accepted invites" message if there are no invites', () => { + const fixture = MockRender(InvitationsDialogComponent); + const instance = fixture.point.componentInstance; instance.activeTab = 'accepted'; instance.acceptedInvites = []; fixture.detectChanges(); - const message = find('.text-muted'); + const message = ngMocks.find('.text-muted'); expect(message).not.toBeNull(); }); - it('displays the gifted amount in the table if there is any, otherwise display the "None Given" text', async () => { - const { fixture, find, instance } = await shallow.render(); + it('displays the gifted amount in the table if there is any, otherwise display the "None Given" text', () => { + const fixture = MockRender(InvitationsDialogComponent); + const instance = fixture.point.componentInstance; instance.activeTab = 'pending'; instance.pendingInvites = [ new InviteVO({ email: 'test1@example.com', giftSizeInMB: 1024 }), @@ -112,15 +134,16 @@ describe('InvitationsDialog', () => { fixture.detectChanges(); - const invitesWithGift = find('.has-amount'); - const invitesWithoutGift = find('.none'); + const invitesWithGift = ngMocks.findAll('.has-amount'); + const invitesWithoutGift = ngMocks.findAll('.none'); expect(invitesWithGift.length).toBe(3); expect(invitesWithoutGift.length).toBe(2); }); - it('displays the amount sent in the invite', async () => { - const { fixture, find, instance } = await shallow.render(); + it('displays the amount sent in the invite', () => { + const fixture = MockRender(InvitationsDialogComponent); + const instance = fixture.point.componentInstance; instance.activeTab = 'pending'; instance.pendingInvites = [ new InviteVO({ email: 'test1@example.com', giftSizeInMB: 1024 }), @@ -130,7 +153,7 @@ describe('InvitationsDialog', () => { fixture.detectChanges(); - const invitesWithGift = find('.has-amount'); + const invitesWithGift = ngMocks.findAll('.has-amount'); const giftedAmount = invitesWithGift[1].nativeElement.textContent.trim(); @@ -139,8 +162,9 @@ describe('InvitationsDialog', () => { expect(giftedAmount).toBe(expectedText); }); - it('displays the "None given" text if no amount was sent in the invite', async () => { - const { fixture, find, instance } = await shallow.render(); + it('displays the "None given" text if no amount was sent in the invite', () => { + const fixture = MockRender(InvitationsDialogComponent); + const instance = fixture.point.componentInstance; instance.activeTab = 'pending'; instance.pendingInvites = [ new InviteVO({ email: 'test1@example.com', giftSizeInMB: 1024 }), @@ -150,7 +174,7 @@ describe('InvitationsDialog', () => { fixture.detectChanges(); - const invitesWithGift = find('.invitation .amount'); + const invitesWithGift = ngMocks.findAll('.invitation .amount'); const giftedAmount = invitesWithGift[1].nativeElement.textContent.trim(); diff --git a/src/app/core/components/manage-tags/manage-tags.component.spec.ts b/src/app/core/components/manage-tags/manage-tags.component.spec.ts index 40bf3d72e..07be62c0e 100644 --- a/src/app/core/components/manage-tags/manage-tags.component.spec.ts +++ b/src/app/core/components/manage-tags/manage-tags.component.spec.ts @@ -1,5 +1,5 @@ import { NgModule } from '@angular/core'; -import { Shallow } from 'shallow-render'; +import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; import { TagVO } from '@models'; import { ApiService } from '@shared/services/api/api.service'; @@ -14,153 +14,180 @@ import { ManageTagsComponent } from './manage-tags.component'; }) class DummyModule {} -/** - * Build fresh Shallow + mock state for each test. - * Nothing persists across tests. - */ -function buildHarness(initialTags?: TagVO[]) { - // Per-test mutable state lives in this closure: - const state = { - throwError: false, - confirmResult: true, - deleted: false as boolean, - deletedTag: null as TagVO | null, - renamed: false as boolean, - renamedTag: null as TagVO | null, - tags: initialTags ?? [ - new TagVO({ name: 'Tomato', tagId: 2 }), - new TagVO({ name: 'Potato', tagId: 1 }), - new TagVO({ - name: 'vegetable:potato', - tagId: 3, - type: 'type.tag.metadata.customField', - }), - ], +describe('ManageTagsComponent #manage-tags (ng-mocks)', () => { + // Per-test mutable state + let state: { + throwError: boolean; + confirmResult: boolean; + deleted: boolean; + deletedTag: TagVO | null; + renamed: boolean; + renamedTag: TagVO | null; + tags: TagVO[]; }; - const mockApiService = { - tag: { - delete: async (data: any) => { - if (state.throwError) throw 'Test Error'; - state.deleted = true; - state.deletedTag = data as TagVO; - return { getTagVOData: () => data }; + let mockApiService: any; + let mockPromptService: any; + + function initState(initialTags?: TagVO[]) { + state = { + throwError: false, + confirmResult: true, + deleted: false, + deletedTag: null, + renamed: false, + renamedTag: null, + tags: initialTags ?? [ + new TagVO({ name: 'Tomato', tagId: 2 }), + new TagVO({ name: 'Potato', tagId: 1 }), + new TagVO({ + name: 'vegetable:potato', + tagId: 3, + type: 'type.tag.metadata.customField', + }), + ], + }; + + mockApiService = { + tag: { + delete: async (data: any) => { + if (state.throwError) throw 'Test Error'; + state.deleted = true; + state.deletedTag = data as TagVO; + return { getTagVOData: () => data }; + }, + update: async (data: any) => { + if (state.throwError) throw 'Test Error'; + state.renamed = true; + state.renamedTag = data as TagVO; + return { getTagVOData: () => data }; + }, }, - update: async (data: any) => { - if (state.throwError) throw 'Test Error'; - state.renamed = true; - state.renamedTag = data as TagVO; - return { getTagVOData: () => data }; - }, - }, - }; - - const mockPromptService = { - async confirm(): Promise { - return state.confirmResult - ? await Promise.resolve(true) - : await Promise.reject(); - }, - }; + }; - const shallow = new Shallow(ManageTagsComponent, DummyModule) - .mock(ApiService, mockApiService) - .mock(PromptService, mockPromptService); - - /** - * Convenience renderers that bind tags - */ - async function render(tags = state.tags) { - return await shallow.render( - ``, - { - bind: { tags }, + mockPromptService = { + async confirm(): Promise { + return state.confirmResult + ? await Promise.resolve(true) + : await Promise.reject(); }, - ); + }; } - return { state, render }; -} + async function setupMockBuilder() { + await MockBuilder(ManageTagsComponent, DummyModule) + .provide({ + provide: ApiService, + useValue: mockApiService, + }) + .provide({ + provide: PromptService, + useValue: mockPromptService, + }); + } + + function render(tags = state.tags) { + return MockRender(``, { + tags, + }); + } -describe('ManageTagsComponent #manage-tags (shallow-safe)', () => { it('should exist', async () => { - const { render } = buildHarness(); - const { element } = await render(); + initState(); + await setupMockBuilder(); + const fixture = render(); - expect(element).not.toBeNull(); + expect(fixture.point.nativeElement).not.toBeNull(); }); it('should have a sorted list of tags', async () => { - const { render } = buildHarness(); - const { find } = await render(); + initState(); + await setupMockBuilder(); + render(); - expect(find('.tag').length).toBe(2); - expect(find('.tag')[0].nativeElement.textContent).toContain('Potato'); + expect(ngMocks.findAll('.tag').length).toBe(2); + expect(ngMocks.findAll('.tag')[0].nativeElement.textContent).toContain( + 'Potato', + ); }); it('should have a delete button for each keyword', async () => { - const { render } = buildHarness(); - const { find, outputs } = await render(); + initState(); + await setupMockBuilder(); + render(); + const instance = ngMocks.findInstance(ManageTagsComponent); - expect(find('.delete').length).toBeGreaterThan(0); - expect(outputs.refreshTags.emit).not.toHaveBeenCalled(); + expect(ngMocks.findAll('.delete').length).toBeGreaterThan(0); + const refreshTagsSpy = spyOn(instance.refreshTags, 'emit'); + + expect(refreshTagsSpy).not.toHaveBeenCalled(); }); it('should be able to delete a keyword', async () => { - const { state, render } = buildHarness(); - const { find, fixture, outputs } = await render(); + initState(); + await setupMockBuilder(); + const fixture = render(); + const instance = ngMocks.findInstance(ManageTagsComponent); + const refreshTagsSpy = spyOn(instance.refreshTags, 'emit'); - find('.delete')[0].nativeElement.click(); + ngMocks.findAll('.delete')[0].nativeElement.click(); await fixture.whenStable(); fixture.detectChanges(); expect(state.deleted).toBeTrue(); expect(state.deletedTag!.name).toBe('Potato'); - expect(outputs.refreshTags.emit).toHaveBeenCalled(); - expect(find('.tag').length).toBe(1); + expect(refreshTagsSpy).toHaveBeenCalled(); + expect(ngMocks.findAll('.tag').length).toBe(1); }); it('should not delete a keyword if an error happens', async () => { - const { state, render } = buildHarness(); - const { element } = await render(); + initState(); + await setupMockBuilder(); + render(); + const instance = ngMocks.findInstance(ManageTagsComponent); state.throwError = true; try { - await element.componentInstance.deleteTag(state.tags[0]); + await instance.deleteTag(state.tags[0]); fail('expected deleteTag to throw'); } catch { // expected } finally { - expect(element.componentInstance.getFilteredTags().length).toBe(2); + expect(instance.getFilteredTags().length).toBe(2); } }); it('should have edit buttons for each keyword', async () => { - const { render } = buildHarness(); - const { find } = await render(); + initState(); + await setupMockBuilder(); + render(); - expect(find('.edit').length).toBeGreaterThan(0); + expect(ngMocks.findAll('.edit').length).toBeGreaterThan(0); }); it('should be able to enter edit mode for a keyword', async () => { - const { render } = buildHarness(); - const { find, fixture } = await render(); + initState(); + await setupMockBuilder(); + const fixture = render(); - find('.edit')[0].nativeElement.click(); + ngMocks.findAll('.edit')[0].nativeElement.click(); fixture.detectChanges(); - expect(find('.tag input').length).toBe(1); - expect(find('.tag input').nativeElement.value).toBe('Potato'); + expect(ngMocks.findAll('.tag input').length).toBe(1); + expect(ngMocks.find('.tag input').nativeElement.value).toBe('Potato'); }); it('should be able to rename tags', async () => { - const { state, render } = buildHarness(); - const { find, fixture, outputs } = await render(); + initState(); + await setupMockBuilder(); + const fixture = render(); + const instance = ngMocks.findInstance(ManageTagsComponent); + const refreshTagsSpy = spyOn(instance.refreshTags, 'emit'); - find('.edit')[0].nativeElement.click(); + ngMocks.findAll('.edit')[0].nativeElement.click(); fixture.detectChanges(); - const input = find('.tag input').nativeElement; + const input = ngMocks.find('.tag input').nativeElement; input.focus(); input.value = 'Starchy Tuber'; input.dispatchEvent(new Event('change')); @@ -168,50 +195,53 @@ describe('ManageTagsComponent #manage-tags (shallow-safe)', () => { await fixture.whenStable(); fixture.detectChanges(); - expect(find('.tag input').length).toBe(0); + expect(ngMocks.findAll('.tag input').length).toBe(0); expect(state.renamed).toBeTrue(); expect(state.renamedTag!.name).toBe('Starchy Tuber'); - expect(outputs.refreshTags.emit).toHaveBeenCalled(); + expect(refreshTagsSpy).toHaveBeenCalled(); }); it('can cancel out of renaming a keyword', async () => { - const { render } = buildHarness(); - const { find, fixture } = await render(); + initState(); + await setupMockBuilder(); + const fixture = render(); - find('.edit')[0].nativeElement.click(); + ngMocks.findAll('.edit')[0].nativeElement.click(); fixture.detectChanges(); - const input = find('.tag input').nativeElement; + const input = ngMocks.find('.tag input').nativeElement; input.value = 'Do Not Show Value'; input.dispatchEvent(new Event('change')); - find('.cancel').nativeElement.click(); + ngMocks.find('.cancel').nativeElement.click(); fixture.detectChanges(); - expect(find('.cancel').length).toBe(0); - expect(find('.tag')[0].nativeElement.textContent).not.toContain( + expect(ngMocks.findAll('.cancel').length).toBe(0); + expect(ngMocks.findAll('.tag')[0].nativeElement.textContent).not.toContain( 'Do Not Show Value', ); }); it('should have a null state', async () => { - const { render } = buildHarness([]); - const { find } = await render([]); + initState([]); + await setupMockBuilder(); + render([]); - expect(find('.tag').length).toBe(0); - expect(find('.tagList').length).toBe(0); + expect(ngMocks.findAll('.tag').length).toBe(0); + expect(ngMocks.findAll('.tagList').length).toBe(0); }); describe('Keywords filtering', () => { async function testValue(val: string, expectedCount: number) { - const { render } = buildHarness(); - const { find, fixture } = await render(); - const input = find('input.filter').nativeElement; + initState(); + await setupMockBuilder(); + const fixture = render(); + const input = ngMocks.find('input.filter').nativeElement; input.value = val; input.dispatchEvent(new Event('change')); fixture.detectChanges(); - expect(find('.tag').length).toBe(expectedCount); + expect(ngMocks.findAll('.tag').length).toBe(expectedCount); } it('Trimming input', async () => { @@ -237,11 +267,12 @@ describe('ManageTagsComponent #manage-tags (shallow-safe)', () => { describe('Prompting for deletion', () => { async function testConfirm(clickConfirm: boolean) { - const { state, render } = buildHarness(); - const { find, fixture } = await render(); - + initState(); state.confirmResult = clickConfirm; - find('.delete')[0].nativeElement.click(); + await setupMockBuilder(); + const fixture = render(); + + ngMocks.findAll('.delete')[0].nativeElement.click(); await fixture.whenStable(); expect(state.deleted).toBe(clickConfirm); diff --git a/src/app/core/components/public-settings/public-settings.component.spec.ts b/src/app/core/components/public-settings/public-settings.component.spec.ts index 785ccee3e..9f7be5afc 100644 --- a/src/app/core/components/public-settings/public-settings.component.spec.ts +++ b/src/app/core/components/public-settings/public-settings.component.spec.ts @@ -1,5 +1,5 @@ import { NgModule } from '@angular/core'; -import { Shallow } from 'shallow-render'; +import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; import { ArchiveVO } from '@models'; import { ApiService } from '@shared/services/api/api.service'; @@ -7,10 +7,10 @@ import { MessageService } from '@shared/services/message/message.service'; import { PublicSettingsComponent } from './public-settings.component'; @NgModule({ - declarations: [], // components your module owns. - imports: [], // other modules your module needs. - providers: [ApiService], // providers available to your module. - bootstrap: [], // bootstrap this root component. + declarations: [], + imports: [], + providers: [ApiService], + bootstrap: [], }) class DummyModule {} @@ -31,73 +31,80 @@ const mockApiService = { }; describe('PublicSettingsComponent', () => { - let shallow: Shallow; - async function defaultRender(a: ArchiveVO = archive) { - return await shallow.render( - ``, - { - bind: { - archive: a, - }, - }, - ); - } - beforeEach(() => { + beforeEach(async () => { throwError = false; updatedDownload = null; updated = false; archive = { allowPublicDownload: true, } as ArchiveVO; - shallow = new Shallow(PublicSettingsComponent, DummyModule) - .mock(ApiService, mockApiService) - .mock(MessageService, { - showError: () => {}, + await MockBuilder(PublicSettingsComponent, DummyModule) + .provide({ + provide: ApiService, + useValue: mockApiService, + }) + .provide({ + provide: MessageService, + useValue: { + showError: () => {}, + }, }); }); - it('should exist', async () => { - const { element } = await defaultRender(); + function defaultRender(a: ArchiveVO = archive) { + return MockRender( + ``, + { archive: a }, + ); + } - expect(element).not.toBeNull(); + it('should exist', () => { + const fixture = defaultRender(); + + expect(fixture.point.nativeElement).not.toBeNull(); }); describe('it should have the proper option checked by default', () => { - it('on', async () => { - const { element } = await defaultRender(); + it('on', () => { + defaultRender(); + const instance = ngMocks.findInstance(PublicSettingsComponent); - expect(element.componentInstance.allowDownloadsToggle).toBeTruthy(); + expect(instance.allowDownloadsToggle).toBeTruthy(); }); - it('off', async () => { - const { element } = await defaultRender({ + it('off', () => { + defaultRender({ allowPublicDownload: false, } as ArchiveVO); + const instance = ngMocks.findInstance(PublicSettingsComponent); - expect(element.componentInstance.allowDownloadsToggle).toBeFalsy(); + expect(instance.allowDownloadsToggle).toBeFalsy(); }); }); it('should save the archive setting when changed', async () => { - const { element } = await defaultRender(); + defaultRender(); + const instance = ngMocks.findInstance(PublicSettingsComponent); expect(updated).toBeFalse(); - element.componentInstance.allowDownloadsToggle = 0; - await element.componentInstance.onAllowDownloadsChange(); + instance.allowDownloadsToggle = 0; + await instance.onAllowDownloadsChange(); expect(updated).toBeTrue(); expect(updatedDownload).toBeFalse(); - element.componentInstance.allowDownloadsToggle = 1; - await element.componentInstance.onAllowDownloadsChange(); + instance.allowDownloadsToggle = 1; + await instance.onAllowDownloadsChange(); expect(updatedDownload).toBeTrue(); }); it('should fail silently', async () => { - const { element } = await defaultRender(); + defaultRender(); + const instance = ngMocks.findInstance(PublicSettingsComponent); + throwError = true; - element.componentInstance.allowDownloadsToggle = 0; - await element.componentInstance.onAllowDownloadsChange(); + instance.allowDownloadsToggle = 0; + await instance.onAllowDownloadsChange(); expect(updated).toBeFalse(); }); diff --git a/src/app/core/components/redeem-gift/redeem-gift.component.spec.ts b/src/app/core/components/redeem-gift/redeem-gift.component.spec.ts index fdba12b13..6d5afa47d 100644 --- a/src/app/core/components/redeem-gift/redeem-gift.component.spec.ts +++ b/src/app/core/components/redeem-gift/redeem-gift.component.spec.ts @@ -1,6 +1,7 @@ -import { Shallow } from 'shallow-render'; +import { NgModule } from '@angular/core'; +import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; +import { ReactiveFormsModule, UntypedFormBuilder } from '@angular/forms'; import { AccountService } from '@shared/services/account/account.service'; -import { CoreModule } from '@core/core.module'; import { ActivatedRoute, convertToParamMap } from '@angular/router'; import { BehaviorSubject } from 'rxjs'; import { PromoVOData } from '../../../models/promo-vo'; @@ -13,15 +14,17 @@ import { MockBillingRepo, } from './shared-mocks'; +@NgModule() +class DummyModule {} + describe('StorageDialogComponent', () => { - let shallow: Shallow; let mockAccountService: MockAccountService; let mockApiService: MockApiService; let mockActivatedRoute; const paramMap = new BehaviorSubject(convertToParamMap({})); const queryParamMap = new BehaviorSubject(convertToParamMap({})); - beforeEach(() => { + beforeEach(async () => { mockActivatedRoute = { paramMap: paramMap.asObservable(), queryParamMap: queryParamMap.asObservable(), @@ -30,35 +33,33 @@ describe('StorageDialogComponent', () => { mockApiService = { billing: new MockBillingRepo(), }; - shallow = new Shallow(RedeemGiftComponent, CoreModule) - .dontMock(AccountService) - .dontMock(ApiService) - .mock(MessageService, { - showError: () => {}, + await MockBuilder(RedeemGiftComponent, DummyModule) + .keep(ReactiveFormsModule, { export: true }) + .keep(UntypedFormBuilder) + .provide({ + provide: MessageService, + useValue: { showError: () => {} }, }) .provide({ provide: AccountService, useValue: mockAccountService }) .provide({ provide: ApiService, useValue: mockApiService }) - .provideMock([{ provide: ActivatedRoute, useValue: mockActivatedRoute }]); + .provide({ provide: ActivatedRoute, useValue: mockActivatedRoute }); }); - it('should exist', async () => { - const { element } = await shallow.render(); + it('should exist', () => { + const fixture = MockRender(RedeemGiftComponent); - expect(element).not.toBeNull(); + expect(fixture.point.nativeElement).not.toBeNull(); }); - it('has an input for a prefilled promo code', async () => { - const { find } = await shallow.render({ - bind: { - promoCode: 'potato', - }, - }); + it('has an input for a prefilled promo code', () => { + MockRender(RedeemGiftComponent, { promoCode: 'potato' }); - expect(find('input').nativeElement.value).toBe('potato'); + expect(ngMocks.find('input').nativeElement.value).toBe('potato'); }); it('should send an API request when submitting a promo code', async () => { - const { instance } = await shallow.render(); + const fixture = MockRender(RedeemGiftComponent); + const instance = fixture.point.componentInstance; const promoData: PromoVOData = { code: 'promo' }; await instance.onPromoFormSubmit(promoData); @@ -67,7 +68,8 @@ describe('StorageDialogComponent', () => { }); it('should update the account after redeeming a promo code', async () => { - const { instance } = await shallow.render(); + const fixture = MockRender(RedeemGiftComponent); + const instance = fixture.point.componentInstance; const promoData: PromoVOData = { code: 'promo' }; await instance.onPromoFormSubmit(promoData); @@ -75,20 +77,22 @@ describe('StorageDialogComponent', () => { expect(instance.resultMessage.successful).toBeTrue(); }); - it('should enable the submit button after adding a promo code', async () => { - const { find, instance, fixture } = await shallow.render(); + it('should enable the submit button after adding a promo code', () => { + const fixture = MockRender(RedeemGiftComponent); + const instance = fixture.point.componentInstance; instance.promoForm.patchValue({ code: 'promo1', }); instance.promoForm.updateValueAndValidity(); fixture.detectChanges(); - const button = find('.btn-primary'); + const button = ngMocks.find('.btn-primary'); expect(button.nativeElement.disabled).toBeFalsy(); }); it('should handle an invalid promo code', async () => { - const { instance } = await shallow.render(); + const fixture = MockRender(RedeemGiftComponent); + const instance = fixture.point.componentInstance; mockApiService.billing.isSuccessful = false; await instance.onPromoFormSubmit({ code: 'potato' }); @@ -96,7 +100,8 @@ describe('StorageDialogComponent', () => { }); it('should handle any other unexpected errors when redeeming promo code', async () => { - const { instance } = await shallow.render(); + const fixture = MockRender(RedeemGiftComponent); + const instance = fixture.point.componentInstance; mockAccountService.failRefresh = true; await instance.onPromoFormSubmit({ code: 'potato' }); @@ -104,7 +109,8 @@ describe('StorageDialogComponent', () => { }); it('should not bump up account storage if it has already been done on the server side', async () => { - const { instance } = await shallow.render(); + const fixture = MockRender(RedeemGiftComponent); + const instance = fixture.point.componentInstance; mockAccountService.addMoreSpaceAfterRefresh = true; await instance.onPromoFormSubmit({ code: 'potato' }); diff --git a/src/app/core/components/storage-dialog/storage-dialog.component.spec.ts b/src/app/core/components/storage-dialog/storage-dialog.component.spec.ts index de2f7c63e..5f106d962 100644 --- a/src/app/core/components/storage-dialog/storage-dialog.component.spec.ts +++ b/src/app/core/components/storage-dialog/storage-dialog.component.spec.ts @@ -1,11 +1,15 @@ -import { Shallow } from 'shallow-render'; -import { CoreModule } from '@core/core.module'; +import { NgModule } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { MockBuilder, MockRender } from 'ng-mocks'; import { ActivatedRoute, convertToParamMap } from '@angular/router'; import { BehaviorSubject } from 'rxjs'; import { DialogRef } from '@angular/cdk/dialog'; import { EventService } from '@shared/services/event/event.service'; import { StorageDialogComponent } from './storage-dialog.component'; +@NgModule() +class DummyModule {} + class MockDialogRef { close(_?: any): void { // Mock close method @@ -13,54 +17,60 @@ class MockDialogRef { } describe('StorageDialogComponent', () => { - let shallow: Shallow; let mockActivatedRoute; const paramMap = new BehaviorSubject(convertToParamMap({})); const queryParamMap = new BehaviorSubject(convertToParamMap({})); - beforeEach(() => { + beforeEach(async () => { mockActivatedRoute = { paramMap: paramMap.asObservable(), queryParamMap: queryParamMap.asObservable(), snapshot: { fragment: null }, }; - shallow = new Shallow(StorageDialogComponent, CoreModule) - .provideMock({ provide: DialogRef, useClass: MockDialogRef }) - .provideMock([{ provide: ActivatedRoute, useValue: mockActivatedRoute }]); + await MockBuilder(StorageDialogComponent, DummyModule) + .provide({ provide: DialogRef, useClass: MockDialogRef }) + .provide({ provide: ActivatedRoute, useValue: mockActivatedRoute }) + .keep(EventService); }); - it('should exist', async () => { - const { element } = await shallow.render(); + it('should exist', () => { + const fixture = MockRender(StorageDialogComponent); - expect(element).not.toBeNull(); + expect(fixture.point.nativeElement).not.toBeNull(); }); - it('should set the tab if the URL fragment matches a tab', async () => { + it('should set the tab if the URL fragment matches a tab', () => { mockActivatedRoute.snapshot.fragment = 'promo'; - const { instance } = await shallow.render(); + const fixture = MockRender(StorageDialogComponent); - expect(instance.activeTab).toBe('promo'); + expect(fixture.point.componentInstance.activeTab).toBe('promo'); }); - it('should not set the tab if the URL fragment is invalid', async () => { + it('should not set the tab if the URL fragment is invalid', () => { mockActivatedRoute.snapshot.fragment = 'not-a-real-tab'; - const { instance } = await shallow.render(); + const fixture = MockRender(StorageDialogComponent); - expect(instance.activeTab).not.toBe(mockActivatedRoute.snapshot.fragment); + expect(fixture.point.componentInstance.activeTab).not.toBe( + mockActivatedRoute.snapshot.fragment, + ); }); - it('can close the dialog', async () => { - const { instance, inject } = await shallow.render(); - const spy = spyOn(inject(DialogRef), 'close'); + it('can close the dialog', () => { + const fixture = MockRender(StorageDialogComponent); + const instance = fixture.point.componentInstance; + const dialogRef = TestBed.inject(DialogRef); + const spy = spyOn(dialogRef, 'close'); instance.onDoneClick(); expect(spy).toHaveBeenCalled(); }); it('should emit an event when the promo tab is selected', async () => { - const { fixture, instance, inject } = await shallow.render(); + const fixture = MockRender(StorageDialogComponent); + const instance = fixture.point.componentInstance; + const eventService = TestBed.inject(EventService); let eventCalled = false; - inject(EventService).addObserver({ + eventService.addObserver({ async update() { eventCalled = true; }, diff --git a/src/app/core/components/two-factor-auth/two-factor-auth.component.spec.ts b/src/app/core/components/two-factor-auth/two-factor-auth.component.spec.ts index 1fed46bba..787c56dbc 100644 --- a/src/app/core/components/two-factor-auth/two-factor-auth.component.spec.ts +++ b/src/app/core/components/two-factor-auth/two-factor-auth.component.spec.ts @@ -1,11 +1,19 @@ -import { Shallow } from 'shallow-render'; +import { NgModule } from '@angular/core'; +import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; +import { + ReactiveFormsModule, + FormsModule, + UntypedFormBuilder, +} from '@angular/forms'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { CoreModule } from '@core/core.module'; import { MessageService } from '@shared/services/message/message.service'; import { ApiService } from '@shared/services/api/api.service'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; import { TwoFactorAuthComponent } from './two-factor-auth.component'; +@NgModule() +class DummyModule {} + const mockApiService = { idpuser: { getTwoFactorMethods: async () => @@ -20,25 +28,32 @@ const mockApiService = { }; describe('TwoFactorAuthComponent', () => { - let shallow: Shallow; - - beforeEach(async () => { - shallow = new Shallow(TwoFactorAuthComponent, CoreModule) - .mock(MessageService, { - showError: () => {}, - }) - .mock(ApiService, mockApiService) - .import(HttpClientTestingModule); - }); - - it('should create', async () => { - const { instance } = await shallow.render(); - - expect(instance).toBeTruthy(); + beforeEach( + async () => + await MockBuilder(TwoFactorAuthComponent, DummyModule) + .keep(HttpClientTestingModule, { export: true }) + .keep(ReactiveFormsModule, { export: true }) + .keep(FormsModule, { export: true }) + .keep(UntypedFormBuilder) + .provide({ + provide: MessageService, + useValue: { showError: () => {} }, + }) + .provide({ + provide: ApiService, + useValue: mockApiService, + }), + ); + + it('should create', () => { + const fixture = MockRender(TwoFactorAuthComponent); + + expect(fixture.point.componentInstance).toBeTruthy(); }); - it('should remove method and update form state', async () => { - const { instance } = await shallow.render(); + it('should remove method and update form state', () => { + const fixture = MockRender(TwoFactorAuthComponent); + const instance = fixture.point.componentInstance; const method = { methodId: 'email', method: 'email', @@ -51,32 +66,36 @@ describe('TwoFactorAuthComponent', () => { expect(instance.form.get('contactInfo').value).toBe('user@example.com'); }); - it('should format phone number correctly', async () => { - const { instance } = await shallow.render(); + it('should format phone number correctly', () => { + const fixture = MockRender(TwoFactorAuthComponent); + const instance = fixture.point.componentInstance; instance.method = 'sms'; instance.formatPhoneNumber('1234567890'); expect(instance.form.get('contactInfo').value).toBe('(123) 456-7890'); }); - it('should format phone number with country code correctly', async () => { - const { instance } = await shallow.render(); + it('should format phone number with country code correctly', () => { + const fixture = MockRender(TwoFactorAuthComponent); + const instance = fixture.point.componentInstance; instance.method = 'sms'; instance.formatPhoneNumber('+12345678900'); expect(instance.form.get('contactInfo').value).toBe('+1 (234) 567-8900'); }); - it('should format international phone numbers correctly', async () => { - const { instance } = await shallow.render(); + it('should format international phone numbers correctly', () => { + const fixture = MockRender(TwoFactorAuthComponent); + const instance = fixture.point.componentInstance; instance.method = 'sms'; instance.formatPhoneNumber('0040123456789'); expect(instance.form.get('contactInfo').value).toBe('+401 (234) 567-89'); }); - it('should handle international phone numbers with country code correctly', async () => { - const { instance } = await shallow.render(); + it('should handle international phone numbers with country code correctly', () => { + const fixture = MockRender(TwoFactorAuthComponent); + const instance = fixture.point.componentInstance; instance.method = 'sms'; instance.formatPhoneNumber('+40123456789'); @@ -84,7 +103,8 @@ describe('TwoFactorAuthComponent', () => { }); it('should set codeSent to true when sendCode is called', async () => { - const { instance } = await shallow.render(); + const fixture = MockRender(TwoFactorAuthComponent); + const instance = fixture.point.componentInstance; const event = { preventDefault: () => {}, }; @@ -93,8 +113,9 @@ describe('TwoFactorAuthComponent', () => { expect(instance.codeSent).toBe(true); }); - it('should call submitData with the form value', async () => { - const { instance } = await shallow.render(); + it('should call submitData with the form value', () => { + const fixture = MockRender(TwoFactorAuthComponent); + const instance = fixture.point.componentInstance; instance.form.setValue({ code: '1234', contactInfo: 'user@example.com' }); const submitDataSpy = spyOn(instance, 'submitData').and.callThrough(); @@ -106,8 +127,9 @@ describe('TwoFactorAuthComponent', () => { }); }); - it('should reset component state when cancel is called', async () => { - const { instance } = await shallow.render(); + it('should reset component state when cancel is called', () => { + const fixture = MockRender(TwoFactorAuthComponent); + const instance = fixture.point.componentInstance; instance.cancel(); expect(instance.method).toBe(''); @@ -117,19 +139,20 @@ describe('TwoFactorAuthComponent', () => { expect(instance.form.get('code').value).toBe(''); }); - it('should display methods correctly in the table', async () => { + it('should display methods correctly in the table', () => { const methods = [ { methodId: 'email', method: 'email', value: 'janedoe@example.com' }, { methodId: 'sms', method: 'sms', value: '(123) 456-7890' }, ]; - const { instance, find, fixture } = await shallow.render(); + const fixture = MockRender(TwoFactorAuthComponent); + const instance = fixture.point.componentInstance; - instance.methods = methods; // Set the methods directly on the component instance + instance.methods = methods; fixture.detectChanges(); - const methodRows = find('.method'); + const methodRows = ngMocks.findAll('.method'); expect(methodRows.length).toBe(methods.length); expect(methodRows[0].nativeElement.textContent).toContain('Email'); @@ -141,33 +164,36 @@ describe('TwoFactorAuthComponent', () => { expect(methodRows[1].nativeElement.textContent).toContain('(123) 456-7890'); }); - it('should display the code input after the code was sent', async () => { - const { instance, find, fixture } = await shallow.render(); + it('should display the code input after the code was sent', () => { + const fixture = MockRender(TwoFactorAuthComponent); + const instance = fixture.point.componentInstance; instance.codeSent = true; instance.turnOn = true; instance.method = 'sms'; fixture.detectChanges(); - const codeContaier = find('.code-container'); + const codeContainer = ngMocks.findAll('.code-container'); - expect(codeContaier.length).toBe(1); + expect(codeContainer.length).toBe(1); }); - it('should not display the code input if the code was not sent', async () => { - const { find, instance, fixture } = await shallow.render(); + it('should not display the code input if the code was not sent', () => { + const fixture = MockRender(TwoFactorAuthComponent); + const instance = fixture.point.componentInstance; instance.turnOn = true; instance.method = 'sms'; fixture.detectChanges(); - const codeContainer = find('.code-container'); + const codeContainer = ngMocks.findAll('.code-container'); expect(codeContainer.length).toBe(0); }); it('should retrieve all the methods after a method has been deleted', async () => { - const { instance } = await shallow.render(); + const fixture = MockRender(TwoFactorAuthComponent); + const instance = fixture.point.componentInstance; instance.methods = [ { methodId: 'email', method: 'email', value: 'janedoe@example.com' }, diff --git a/src/app/core/components/upload-progress/upload-progress.component.spec.ts b/src/app/core/components/upload-progress/upload-progress.component.spec.ts index 81b294bd1..d89483235 100644 --- a/src/app/core/components/upload-progress/upload-progress.component.spec.ts +++ b/src/app/core/components/upload-progress/upload-progress.component.spec.ts @@ -1,7 +1,7 @@ +import { NgModule, EventEmitter } from '@angular/core'; +import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; + import { UploadService } from '@core/services/upload/upload.service'; -import { CoreModule } from '@core/core.module'; -import { EventEmitter } from '@angular/core'; -import { Shallow } from 'shallow-render'; import { UploadProgressEvent, UploadSessionStatus, @@ -10,6 +10,9 @@ import { UploadItem } from '@core/services/upload/uploadItem'; import { FolderVO } from '@models/index'; import { UploadProgressComponent } from './upload-progress.component'; +@NgModule() +class DummyModule {} + class MockUploadSession { public progress = new EventEmitter(); } @@ -22,30 +25,29 @@ const mockUploadService = { }; describe('UploadProgressComponent', () => { - let shallow: Shallow; - - beforeEach(() => { - shallow = new Shallow(UploadProgressComponent, CoreModule).mock( - UploadService, - mockUploadService, - ); + beforeEach(async () => { + await MockBuilder(UploadProgressComponent, DummyModule).provide({ + provide: UploadService, + useValue: mockUploadService, + }); }); - it('should create', async () => { - const { instance } = await shallow.render(); + it('should create', () => { + const fixture = MockRender(UploadProgressComponent); - expect(instance).toBeTruthy(); + expect(fixture.point.componentInstance).toBeTruthy(); }); - it('should become visible when show() is called', async () => { - const { instance } = await shallow.render(); + it('should become visible when show() is called', () => { + const fixture = MockRender(UploadProgressComponent); + const instance = fixture.point.componentInstance; instance.show(); expect(instance.visible).toBe(true); }); - it('should display the correct file name and the folder when dragging the file into a folder', async () => { - const { find, fixture } = await shallow.render(); + it('should display the correct file name and the folder when dragging the file into a folder', () => { + const fixture = MockRender(UploadProgressComponent); const mockContent = new Uint8Array(10000); const progressEvent = { @@ -65,19 +67,19 @@ describe('UploadProgressComponent', () => { fixture.detectChanges(); - expect(find('.current-file').nativeElement.textContent.trim()).toBe( + expect(ngMocks.find('.current-file').nativeElement.textContent.trim()).toBe( `Uploading ${progressEvent.item.file.name} to ${progressEvent.item.parentFolder.displayName}`, ); - const fileCountElements = find('.file-count strong'); + const fileCountElements = ngMocks.findAll('.file-count strong'); expect(fileCountElements[0].nativeElement.textContent).toEqual('1'); expect(fileCountElements[1].nativeElement.textContent).toEqual('5'); }); - it('should display the correct file name, the target folder and the current folder when dragging a file nested into a folder over a folder', async () => { - const { find, fixture } = await shallow.render(); + it('should display the correct file name, the target folder and the current folder when dragging a file nested into a folder over a folder', () => { + const fixture = MockRender(UploadProgressComponent); const mockContent = new Uint8Array(10000); const progressEvent = { @@ -97,11 +99,11 @@ describe('UploadProgressComponent', () => { fixture.detectChanges(); - expect(find('.current-file').nativeElement.textContent.trim()).toBe( + expect(ngMocks.find('.current-file').nativeElement.textContent.trim()).toBe( `Uploading ${progressEvent.item.file.name} to testfolder/${progressEvent.item.parentFolder.displayName}`, ); - const fileCountElements = find('.file-count strong'); + const fileCountElements = ngMocks.findAll('.file-count strong'); expect(fileCountElements[0].nativeElement.textContent).toEqual('1'); diff --git a/src/app/directive/components/directive-display/directive-display.component.spec.ts b/src/app/directive/components/directive-display/directive-display.component.spec.ts index 6143b875e..e22052dfb 100644 --- a/src/app/directive/components/directive-display/directive-display.component.spec.ts +++ b/src/app/directive/components/directive-display/directive-display.component.spec.ts @@ -1,9 +1,10 @@ -import { Shallow } from 'shallow-render'; +import { NgModule } from '@angular/core'; +import { fakeAsync, flush, TestBed } from '@angular/core/testing'; +import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; import { ArchiveVO } from '@models/index'; import { AccountService } from '@shared/services/account/account.service'; import { ApiService } from '@shared/services/api/api.service'; -import { DirectiveModule } from '../../directive.module'; import { DirectiveDisplayComponent } from './directive-display.component'; import { @@ -12,19 +13,13 @@ import { MockDirectiveRepo, } from './test-utils'; +@NgModule() +class DummyModule {} + describe('DirectiveDisplayComponent', () => { - let shallow: Shallow; + let mockApiService: MockApiService; - beforeEach(() => { - shallow = new Shallow(DirectiveDisplayComponent, DirectiveModule) - .provideMock({ - provide: AccountService, - useClass: MockAccountService, - }) - .provideMock({ - provide: ApiService, - useClass: MockApiService, - }); + beforeEach(async () => { MockAccountService.mockArchive = new ArchiveVO({ archiveId: 1, fullName: 'Test', @@ -34,106 +29,192 @@ describe('DirectiveDisplayComponent', () => { MockDirectiveRepo.mockNote = 'Unit Testing!'; MockDirectiveRepo.legacyContactName = 'Test User'; MockDirectiveRepo.legacyContactEmail = 'test@example.com'; + mockApiService = new MockApiService(); + await MockBuilder(DirectiveDisplayComponent, DummyModule) + .keep(AccountService) + .keep(ApiService) + .provide({ + provide: AccountService, + useValue: new MockAccountService(), + }) + .provide({ + provide: ApiService, + useValue: mockApiService, + }); }); - it('should create', async () => { - const { find, instance } = await shallow.render(); + it('should create', fakeAsync(() => { + const fixture = MockRender(DirectiveDisplayComponent); + flush(); + fixture.detectChanges(); + const instance = fixture.point.componentInstance; expect(instance).toBeTruthy(); - expect(find('.error').length).toBe(0); - }); + expect(ngMocks.findAll('.error').length).toBe(0); + })); - it('should fill in current archive info', async () => { - const { find } = await shallow.render(); + it('should fill in current archive info', fakeAsync(() => { + const fixture = MockRender(DirectiveDisplayComponent); + flush(); + fixture.detectChanges(); expect( - find('.archive-steward-header')[0].nativeElement.innerText, + ngMocks.findAll('.archive-steward-header')[0].nativeElement.innerText, ).toContain('The Test Archive'); - }); + })); - it('should fetch directive info from API', async () => { - const { find, instance } = await shallow.render(); + it('should fetch directive info from API', fakeAsync(() => { + const fixture = MockRender(DirectiveDisplayComponent); + flush(); + fixture.detectChanges(); + const instance = fixture.point.componentInstance; expect(instance.directive).not.toBeUndefined(); - expect(find('.archive-steward-note')[0].nativeElement.innerText).toContain( - 'Unit Testing!', - ); + expect( + ngMocks.findAll('.archive-steward-note')[0].nativeElement.innerText, + ).toContain('Unit Testing!'); - expect(find('.archive-steward-email')[0].nativeElement.innerText).toContain( - 'test@example.com', - ); - }); + expect( + ngMocks.findAll('.archive-steward-email')[0].nativeElement.innerText, + ).toContain('test@example.com'); + })); - it('should format null fields properly', async () => { + it('should format null fields properly', fakeAsync(() => { MockDirectiveRepo.reset(); - const { find } = await shallow.render(); + const fixture = MockRender(DirectiveDisplayComponent); + flush(); + fixture.detectChanges(); - expect(find('.not-assigned').length).toBeGreaterThan(0); - }); + expect(ngMocks.findAll('.not-assigned').length).toBeGreaterThan(0); + })); - it('should format filled out fields properly', async () => { - const { find } = await shallow.render(); + it('should format filled out fields properly', fakeAsync(() => { + const fixture = MockRender(DirectiveDisplayComponent); + flush(); + fixture.detectChanges(); - expect(find('.not-assigned').length).toBe(0); - }); + expect(ngMocks.findAll('.not-assigned').length).toBe(0); + })); - it('should be able to handle API errors when fetching Directive', async () => { + it('should be able to handle API errors when fetching Directive', fakeAsync(() => { MockDirectiveRepo.failRequest = true; - const { find } = await shallow.render(); + const fixture = MockRender(DirectiveDisplayComponent); + flush(); + fixture.detectChanges(); - expect(find('.error').length).toBe(1); - expect(find('.archive-steward-table').length).toBe(0); - expect(find('button').nativeElement.disabled).toBeTruthy(); - }); + expect(ngMocks.findAll('.error').length).toBe(1); + expect(ngMocks.findAll('.archive-steward-table').length).toBe(0); + expect(ngMocks.find('button').nativeElement.disabled).toBeTruthy(); + })); it('should show the "No Plan" warning if the user does not have a legacy contact', async () => { + // Reset TestBed and reconfigure for this test + TestBed.resetTestingModule(); + MockDirectiveRepo.reset(); MockDirectiveRepo.legacyContactName = null; MockDirectiveRepo.legacyContactEmail = null; - const { find } = await shallow.render(); + MockDirectiveRepo.mockStewardEmail = 'test@example.com'; + MockDirectiveRepo.mockNote = 'Unit Testing!'; - expect(find('.no-plan-warning').length).toBe(1); - expect(find('button').nativeElement.disabled).toBeTruthy(); - }); + await TestBed.configureTestingModule({ + declarations: [DirectiveDisplayComponent], + providers: [ + { provide: AccountService, useValue: new MockAccountService() }, + { provide: ApiService, useValue: new MockApiService() }, + ], + }).compileComponents(); + + const fixture = TestBed.createComponent(DirectiveDisplayComponent); + const instance = fixture.componentInstance; + fixture.detectChanges(); - it('should not show the "No Plan" warning if the user does have a legacy contact', async () => { - const { find } = await shallow.render(); + // Wait for ngOnInit to complete + await instance.ngOnInit(); + fixture.detectChanges(); - expect(find('.no-plan-warning').length).toBe(0); - expect(find('button').nativeElement.disabled).toBeFalsy(); + expect(instance.noPlan).toBeTrue(); + expect( + fixture.nativeElement.querySelectorAll('.no-plan-warning').length, + ).toBe(1); + + expect(fixture.nativeElement.querySelector('button').disabled).toBeTruthy(); }); + it('should not show the "No Plan" warning if the user does have a legacy contact', fakeAsync(() => { + const fixture = MockRender(DirectiveDisplayComponent); + flush(); + fixture.detectChanges(); + + expect(ngMocks.findAll('.no-plan-warning').length).toBe(0); + expect(ngMocks.find('button').nativeElement.disabled).toBeFalsy(); + })); + it('should be able to handle API errors when fetching Legacy Contact', async () => { + // Reset TestBed and reconfigure for this test + TestBed.resetTestingModule(); + MockDirectiveRepo.reset(); MockDirectiveRepo.failLegacyRequest = true; - const { find } = await shallow.render(); + MockDirectiveRepo.mockStewardEmail = 'test@example.com'; + MockDirectiveRepo.mockNote = 'Unit Testing!'; + MockDirectiveRepo.legacyContactName = 'Test User'; + MockDirectiveRepo.legacyContactEmail = 'test@example.com'; + + await TestBed.configureTestingModule({ + declarations: [DirectiveDisplayComponent], + providers: [ + { provide: AccountService, useValue: new MockAccountService() }, + { provide: ApiService, useValue: new MockApiService() }, + ], + }).compileComponents(); + + const fixture = TestBed.createComponent(DirectiveDisplayComponent); + const instance = fixture.componentInstance; + fixture.detectChanges(); + + // Wait for ngOnInit to complete + await instance.ngOnInit(); + fixture.detectChanges(); + + expect(instance.error).toBeTrue(); + expect(fixture.nativeElement.querySelectorAll('.error').length).toBe(1); + expect( + fixture.nativeElement.querySelectorAll('.archive-steward-table').length, + ).toBe(0); - expect(find('.error').length).toBe(1); - expect(find('.archive-steward-table').length).toBe(0); - expect(find('button').nativeElement.disabled).toBeTruthy(); + expect(fixture.nativeElement.querySelector('button').disabled).toBeTruthy(); }); - it('should be able to disable the Legacy Contact check through an input', async () => { + it('should be able to disable the Legacy Contact check through an input', fakeAsync(() => { MockDirectiveRepo.legacyContactName = null; MockDirectiveRepo.legacyContactEmail = null; - const { find } = await shallow.render( + const fixture = MockRender( '', ); + flush(); + fixture.detectChanges(); - expect(find('.no-plan-warning').length).toBe(0); - expect(find('button').nativeElement.disabled).toBeFalsy(); - }); + expect(ngMocks.findAll('.no-plan-warning').length).toBe(0); + expect(ngMocks.find('button').nativeElement.disabled).toBeFalsy(); + })); - it('should say "Assign" for new directive', async () => { + it('should say "Assign" for new directive', fakeAsync(() => { MockDirectiveRepo.mockStewardEmail = null; MockDirectiveRepo.mockNote = null; - const { find } = await shallow.render(); + const fixture = MockRender(DirectiveDisplayComponent); + flush(); + fixture.detectChanges(); - expect(find('button').nativeElement.innerText).toContain('Assign'); - expect(find('button').nativeElement.innerText).not.toContain('Edit'); - }); + expect(ngMocks.find('button').nativeElement.innerText).toContain('Assign'); + expect(ngMocks.find('button').nativeElement.innerText).not.toContain( + 'Edit', + ); + })); - it('should say "Edit" for existing directive', async () => { - const { find } = await shallow.render(); + it('should say "Edit" for existing directive', fakeAsync(() => { + const fixture = MockRender(DirectiveDisplayComponent); + flush(); + fixture.detectChanges(); - expect(find('button').nativeElement.innerText).toContain('Edit'); - }); + expect(ngMocks.find('button').nativeElement.innerText).toContain('Edit'); + })); }); diff --git a/src/app/directive/components/directive-edit/directive-edit.component.spec.ts b/src/app/directive/components/directive-edit/directive-edit.component.spec.ts index 46e00c961..47c4cd9c3 100644 --- a/src/app/directive/components/directive-edit/directive-edit.component.spec.ts +++ b/src/app/directive/components/directive-edit/directive-edit.component.spec.ts @@ -1,12 +1,12 @@ -import { DebugElement, Type } from '@angular/core'; +import { NgModule } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; +import { FormsModule } from '@angular/forms'; import { DirectiveData } from '@models/directive'; import { AccountService } from '@shared/services/account/account.service'; import { ApiService } from '@shared/services/api/api.service'; -import { Shallow } from 'shallow-render'; -import { QueryMatch } from 'shallow-render/dist/lib/models/query-match'; import { EventService } from '@shared/services/event/event.service'; import { MessageService } from '@shared/services/message/message.service'; -import { DirectiveModule } from '../../directive.module'; import { MockAccountService } from '../directive-display/test-utils'; import { DirectiveEditComponent } from './directive-edit.component'; import { @@ -15,24 +15,18 @@ import { createDirective, } from './test-utils'; +@NgModule() +class DummyModule {} + class MockApiService { public directive = new MockDirectiveRepo(); } -type Find = ( - cssOrDirective: string | Type, - options?: { - query?: string; - }, -) => QueryMatch; - describe('DirectiveEditComponent', () => { - let shallow: Shallow; - - const fillOutForm = (find: Find, email: string, note: string) => { - const emailInput = find('.archive-steward-email')[0] + const fillOutForm = (email: string, note: string) => { + const emailInput = ngMocks.findAll('.archive-steward-email')[0] .nativeElement as HTMLInputElement; - const noteInput = find('.archive-steward-note')[0] + const noteInput = ngMocks.findAll('.archive-steward-note')[0] .nativeElement as HTMLTextAreaElement; emailInput.value = email; @@ -41,82 +35,86 @@ describe('DirectiveEditComponent', () => { noteInput.dispatchEvent(new Event('input')); }; - beforeEach(() => { + beforeEach(async () => { MockDirectiveRepo.reset(); MockMessageService.reset(); - shallow = new Shallow( - DirectiveEditComponent, - DirectiveModule, - ) - .provideMock([ - { - provide: ApiService, - useClass: MockApiService, - }, - ]) - .provideMock([ - { - provide: AccountService, - useClass: MockAccountService, - }, - ]) - .provideMock([ - { - provide: MessageService, - useClass: MockMessageService, - }, - ]) - .provide(EventService) - .dontMock(EventService); + await MockBuilder(DirectiveEditComponent, DummyModule) + .keep(FormsModule, { export: true }) + .provide({ + provide: ApiService, + useClass: MockApiService, + }) + .provide({ + provide: AccountService, + useClass: MockAccountService, + }) + .provide({ + provide: MessageService, + useClass: MockMessageService, + }) + .keep(EventService); }); - it('should create', async () => { - const { instance } = await shallow.render(); + it('should create', () => { + const fixture = MockRender(DirectiveEditComponent); - expect(instance).not.toBeNull(); + expect(fixture.point.componentInstance).not.toBeNull(); }); - it('should be able to fill out the directive form', async () => { - const { instance, find } = await shallow.render(); + it('should be able to fill out the directive form', () => { + const fixture = MockRender(DirectiveEditComponent); + const instance = fixture.point.componentInstance; - expect(find('.archive-steward-email').length).toBe(1); - expect(find('.archive-steward-note').length).toBe(1); + expect(ngMocks.findAll('.archive-steward-email').length).toBe(1); + expect(ngMocks.findAll('.archive-steward-note').length).toBe(1); - fillOutForm(find, 'test@example.com', 'Unit Testing!'); + fillOutForm('test@example.com', 'Unit Testing!'); expect(instance.email).toBe('test@example.com'); expect(instance.note).toBe('Unit Testing!'); }); it('should be able to save a new directive', async () => { - const { find, fixture } = await shallow.render(); - - fillOutForm(find, 'test@example.com', 'Test Memo'); + // Reset TestBed and reconfigure for this test + TestBed.resetTestingModule(); + MockDirectiveRepo.reset(); - expect(find('.save-btn').length).toBe(1); - find('.save-btn').nativeElement.dispatchEvent(new Event('click')); + await TestBed.configureTestingModule({ + imports: [FormsModule], + declarations: [DirectiveEditComponent], + providers: [ + { provide: ApiService, useValue: new MockApiService() }, + { provide: AccountService, useValue: new MockAccountService() }, + { provide: MessageService, useValue: new MockMessageService() }, + EventService, + ], + }).compileComponents(); + + const fixture = TestBed.createComponent(DirectiveEditComponent); + const instance = fixture.componentInstance; fixture.detectChanges(); - expect(find('*[disabled], *[readonly]').length).toBe(3); - await fixture.whenStable(); + // Fill out form manually + instance.email = 'test@example.com'; + instance.note = 'Test Memo'; fixture.detectChanges(); - expect(find('*[disabled], *[readonly]').length).toBe(0); + // Call submitForm directly and await it + await instance.submitForm(); + fixture.detectChanges(); - expect(find('.account-not-found').length).toBe(0); + // Check that form is enabled after save + expect(instance.waiting).toBeFalse(); expect(MockDirectiveRepo.createdDirective).not.toBeNull(); }); - it('should be able to have existing directive data passed in', async () => { + it('should be able to have existing directive data passed in', () => { const directive = createDirective( 'existing@example.com', 'already existing directive', ); - const { instance } = await shallow.render({ - bind: { - directive, - }, - }); + const fixture = MockRender(DirectiveEditComponent, { directive }); + const instance = fixture.point.componentInstance; expect(instance.email).toBe('existing@example.com'); expect(instance.note).toBe('already existing directive'); @@ -128,37 +126,37 @@ describe('DirectiveEditComponent', () => { 'already existing directive', ); - const { find, fixture } = await shallow.render({ - bind: { - directive, - }, - }); + const fixture = MockRender(DirectiveEditComponent, { directive }); + const instance = fixture.point.componentInstance; - fillOutForm(find, 'test@example.com', 'Test Memo'); + fillOutForm('test@example.com', 'Test Memo'); - find('.save-btn').nativeElement.dispatchEvent(new Event('click')); + ngMocks.find('.save-btn').nativeElement.dispatchEvent(new Event('click')); fixture.detectChanges(); - expect(find('*[disabled], *[readonly]').length).toBe(3); + // Check that form is disabled during save + expect(instance.waiting).toBeTrue(); await fixture.whenStable(); fixture.detectChanges(); - expect(find('*[disabled], *[readonly]').length).toBe(0); + // Check that form is enabled after save + expect(instance.waiting).toBeFalse(); + expect(ngMocks.find('.save-btn').nativeElement.disabled).toBeFalse(); expect(MockDirectiveRepo.createdDirective).toBeNull(); expect(MockDirectiveRepo.editedDirective).not.toBeNull(); }); it('should handle API errors on creation', async () => { MockDirectiveRepo.failRequest = true; - const { find, fixture } = await shallow.render(); + const fixture = MockRender(DirectiveEditComponent); - fillOutForm(find, 'test@example.com', 'Test Memo'); - find('.save-btn').nativeElement.dispatchEvent(new Event('click')); + fillOutForm('test@example.com', 'Test Memo'); + ngMocks.find('.save-btn').nativeElement.dispatchEvent(new Event('click')); fixture.detectChanges(); await fixture.whenStable(); expect(MockDirectiveRepo.createdDirective).toBeNull(); - expect(find('.account-not-found').length).toBe(0); + expect(ngMocks.findAll('.account-not-found').length).toBe(0); expect(MockMessageService.errorShown).toBeTrue(); }); @@ -168,32 +166,29 @@ describe('DirectiveEditComponent', () => { 'existing@example.com', 'already existing directive', ); - const { find, fixture } = await shallow.render({ - bind: { - directive, - }, - }); - - fillOutForm(find, 'test@example.com', 'Test Memo'); - find('.save-btn').nativeElement.dispatchEvent(new Event('click')); + const fixture = MockRender(DirectiveEditComponent, { directive }); + + fillOutForm('test@example.com', 'Test Memo'); + ngMocks.find('.save-btn').nativeElement.dispatchEvent(new Event('click')); fixture.detectChanges(); await fixture.whenStable(); expect(MockDirectiveRepo.editedDirective).toBeNull(); - expect(find('.account-not-found').length).toBe(0); + expect(ngMocks.findAll('.account-not-found').length).toBe(0); expect(MockMessageService.errorShown).toBeTrue(); }); it('should emit an output when a directive is created', async () => { - const { find, fixture, instance } = await shallow.render(); + const fixture = MockRender(DirectiveEditComponent); + const instance = fixture.point.componentInstance; let savedDirective: DirectiveData; instance.savedDirective.emit = jasmine .createSpy() .and.callFake((dir: DirectiveData) => { savedDirective = dir; }); - fillOutForm(find, 'test@example.com', 'Test Memo'); - find('.save-btn').nativeElement.dispatchEvent(new Event('click')); + fillOutForm('test@example.com', 'Test Memo'); + ngMocks.find('.save-btn').nativeElement.dispatchEvent(new Event('click')); fixture.detectChanges(); await fixture.whenStable(); @@ -206,19 +201,16 @@ describe('DirectiveEditComponent', () => { 'existing@example.com', 'already existing directive', ); - const { instance, find, fixture } = await shallow.render({ - bind: { - directive, - }, - }); + const fixture = MockRender(DirectiveEditComponent, { directive }); + const instance = fixture.point.componentInstance; let savedDirective: DirectiveData; instance.savedDirective.emit = jasmine .createSpy() .and.callFake((dir: DirectiveData) => { savedDirective = dir; }); - fillOutForm(find, 'test@example.com', 'Test Memo'); - find('.save-btn').nativeElement.dispatchEvent(new Event('click')); + fillOutForm('test@example.com', 'Test Memo'); + ngMocks.find('.save-btn').nativeElement.dispatchEvent(new Event('click')); fixture.detectChanges(); await fixture.whenStable(); @@ -226,43 +218,45 @@ describe('DirectiveEditComponent', () => { expect(savedDirective).not.toBeUndefined(); }); - it('should not allow submitting a form until email is filled out', async () => { - const { find, fixture } = await shallow.render(); - fillOutForm(find, ' ', 'Test Memo'); + it('should not allow submitting a form until email is filled out', () => { + const fixture = MockRender(DirectiveEditComponent); + fillOutForm(' ', 'Test Memo'); fixture.detectChanges(); - expect(find('.save-btn').nativeElement.disabled).toBeTruthy(); - fillOutForm(find, 'email@example.com', ''); + expect(ngMocks.find('.save-btn').nativeElement.disabled).toBeTruthy(); + fillOutForm('email@example.com', ''); fixture.detectChanges(); - expect(find('.save-btn').nativeElement.disabled).toBeFalsy(); - fillOutForm(find, 'email@example.com', 'memo'); + expect(ngMocks.find('.save-btn').nativeElement.disabled).toBeFalsy(); + fillOutForm('email@example.com', 'memo'); fixture.detectChanges(); - expect(find('.save-btn').nativeElement.disabled).toBeFalsy(); + expect(ngMocks.find('.save-btn').nativeElement.disabled).toBeFalsy(); }); it('should show an error message if a user with the given email does not exist when creating a directive', async () => { MockDirectiveRepo.accountExists = false; - const { find, fixture, outputs } = await shallow.render(); - fillOutForm(find, 'notfound@example.com', 'Test Memo'); + const fixture = MockRender(DirectiveEditComponent); + const instance = fixture.point.componentInstance; + const savedDirectiveSpy = spyOn(instance.savedDirective, 'emit'); + fillOutForm('notfound@example.com', 'Test Memo'); - expect(find('.account-not-found').length).toBe(0); - find('.save-btn').nativeElement.dispatchEvent(new Event('click')); + expect(ngMocks.findAll('.account-not-found').length).toBe(0); + ngMocks.find('.save-btn').nativeElement.dispatchEvent(new Event('click')); await fixture.whenStable(); fixture.detectChanges(); expect(MockDirectiveRepo.createdDirective).toBeNull(); - expect(outputs.savedDirective.emit).not.toHaveBeenCalled(); - expect(find('.account-not-found').length).toBe(1); + expect(savedDirectiveSpy).not.toHaveBeenCalled(); + expect(ngMocks.findAll('.account-not-found').length).toBe(1); MockDirectiveRepo.accountExists = true; - find('.save-btn').nativeElement.dispatchEvent(new Event('click')); + ngMocks.find('.save-btn').nativeElement.dispatchEvent(new Event('click')); await fixture.whenStable(); fixture.detectChanges(); expect(MockDirectiveRepo.createdDirective).not.toBeNull(); - expect(outputs.savedDirective.emit).toHaveBeenCalled(); - expect(find('.account-not-found').length).toBe(0); + expect(savedDirectiveSpy).toHaveBeenCalled(); + expect(ngMocks.findAll('.account-not-found').length).toBe(0); }); it('should show an error message if a user with the given email does not exist when editing', async () => { @@ -271,26 +265,26 @@ describe('DirectiveEditComponent', () => { 'existing@example.com', 'already existing directive', ); - const { find, fixture, outputs } = await shallow.render({ - bind: { directive }, - }); - fillOutForm(find, 'notfound@example.com', 'Test Memo'); + const fixture = MockRender(DirectiveEditComponent, { directive }); + const instance = fixture.point.componentInstance; + const savedDirectiveSpy = spyOn(instance.savedDirective, 'emit'); + fillOutForm('notfound@example.com', 'Test Memo'); - expect(find('.account-not-found').length).toBe(0); - find('.save-btn').nativeElement.dispatchEvent(new Event('click')); + expect(ngMocks.findAll('.account-not-found').length).toBe(0); + ngMocks.find('.save-btn').nativeElement.dispatchEvent(new Event('click')); await fixture.whenStable(); fixture.detectChanges(); expect(MockDirectiveRepo.editedDirective).toBeNull(); - expect(outputs.savedDirective.emit).not.toHaveBeenCalled(); - expect(find('.account-not-found').length).toBe(1); + expect(savedDirectiveSpy).not.toHaveBeenCalled(); + expect(ngMocks.findAll('.account-not-found').length).toBe(1); MockDirectiveRepo.accountExists = true; - find('.save-btn').nativeElement.dispatchEvent(new Event('click')); + ngMocks.find('.save-btn').nativeElement.dispatchEvent(new Event('click')); await fixture.whenStable(); fixture.detectChanges(); expect(MockDirectiveRepo.editedDirective).not.toBeNull(); - expect(outputs.savedDirective.emit).toHaveBeenCalled(); - expect(find('.account-not-found').length).toBe(0); + expect(savedDirectiveSpy).toHaveBeenCalled(); + expect(ngMocks.findAll('.account-not-found').length).toBe(0); }); }); diff --git a/src/app/directive/components/legacy-contact-display/legacy-contact-display.component.spec.ts b/src/app/directive/components/legacy-contact-display/legacy-contact-display.component.spec.ts index 0eaecea10..940d0367e 100644 --- a/src/app/directive/components/legacy-contact-display/legacy-contact-display.component.spec.ts +++ b/src/app/directive/components/legacy-contact-display/legacy-contact-display.component.spec.ts @@ -1,92 +1,103 @@ +import { NgModule } from '@angular/core'; +import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; import { ApiService } from '@shared/services/api/api.service'; -import { Shallow } from 'shallow-render'; import { AccountService } from '@shared/services/account/account.service'; import { MessageService } from '@shared/services/message/message.service'; -import { DirectiveModule } from '../../directive.module'; import { MockAccountService } from '../directive-display/test-utils'; import { MockMessageService } from '../directive-edit/test-utils'; import { LegacyContactDisplayComponent } from './legacy-contact-display.component'; import { MockApiService, MockDirectiveRepo } from './test-utils'; -describe('LegacyContactDisplayComponent', () => { - let shallow: Shallow; +@NgModule() +class DummyModule {} - beforeEach(() => { - shallow = new Shallow(LegacyContactDisplayComponent, DirectiveModule) - .provideMock({ +describe('LegacyContactDisplayComponent', () => { + beforeEach(async () => { + MockDirectiveRepo.reset(); + MockMessageService.reset(); + await MockBuilder(LegacyContactDisplayComponent, DummyModule) + .provide({ provide: ApiService, useClass: MockApiService, }) - .provideMock({ + .provide({ provide: AccountService, useClass: MockAccountService, }) - .provideMock({ + .provide({ provide: MessageService, useClass: MockMessageService, }); - MockDirectiveRepo.reset(); - MockMessageService.reset(); }); - it('should create', async () => { - const { instance } = await shallow.render(); + it('should create', () => { + const fixture = MockRender(LegacyContactDisplayComponent); - expect(instance).toBeTruthy(); + expect(fixture.point.componentInstance).toBeTruthy(); }); it('should list legacy contact information', async () => { MockDirectiveRepo.legacyContactName = 'Test User'; MockDirectiveRepo.legacyContactEmail = 'test@example.com'; - const { find } = await shallow.render(); + const fixture = MockRender(LegacyContactDisplayComponent); + await fixture.whenStable(); + fixture.detectChanges(); - expect(find('.legacy-contact-name').length).toBe(1); - expect(find('.legacy-contact-name')[0].nativeElement.innerText).toContain( - 'Test User', - ); + expect(ngMocks.findAll('.legacy-contact-name').length).toBe(1); + expect( + ngMocks.findAll('.legacy-contact-name')[0].nativeElement.innerText, + ).toContain('Test User'); - expect(find('.legacy-contact-email').length).toBe(1); - expect(find('.legacy-contact-email')[0].nativeElement.innerText).toContain( - 'test@example.com', - ); + expect(ngMocks.findAll('.legacy-contact-email').length).toBe(1); + expect( + ngMocks.findAll('.legacy-contact-email')[0].nativeElement.innerText, + ).toContain('test@example.com'); - expect(find('.not-assigned').length).toBe(0); + expect(ngMocks.findAll('.not-assigned').length).toBe(0); }); it('should show "not assigned" if no legacy contact', async () => { - const { find } = await shallow.render(); + const fixture = MockRender(LegacyContactDisplayComponent); + await fixture.whenStable(); + fixture.detectChanges(); - expect(find('.legacy-contact-name')[0].nativeElement.innerText).toContain( - 'not assigned', - ); + expect( + ngMocks.findAll('.legacy-contact-name')[0].nativeElement.innerText, + ).toContain('not assigned'); - expect(find('.legacy-contact-email')[0].nativeElement.innerText).toContain( - 'not assigned', - ); + expect( + ngMocks.findAll('.legacy-contact-email')[0].nativeElement.innerText, + ).toContain('not assigned'); - expect(find('.not-assigned').length).toBe(2); + expect(ngMocks.findAll('.not-assigned').length).toBe(2); }); it('should show an error message if there was an error fetching legacy contact', async () => { MockDirectiveRepo.throwError = true; - const { find } = await shallow.render(); + const fixture = MockRender(LegacyContactDisplayComponent); + await fixture.whenStable(); + fixture.detectChanges(); - expect(find('.legacy-contact-name').length).toBe(0); - expect(find('.legacy-contact-email').length).toBe(0); - expect(find('.error').length).toBe(1); + expect(ngMocks.findAll('.legacy-contact-name').length).toBe(0); + expect(ngMocks.findAll('.legacy-contact-email').length).toBe(0); + expect(ngMocks.findAll('.error').length).toBe(1); }); - it('should emit an event when the edit button is pressed', async () => { - const { find, outputs } = await shallow.render(); - find('button').nativeElement.dispatchEvent(new Event('click')); + it('should emit an event when the edit button is pressed', () => { + const fixture = MockRender(LegacyContactDisplayComponent); + const instance = fixture.point.componentInstance; + const beginEditSpy = spyOn(instance.beginEdit, 'emit'); + ngMocks.find('button').nativeElement.dispatchEvent(new Event('click')); - expect(outputs.beginEdit.emit).toHaveBeenCalled(); + expect(beginEditSpy).toHaveBeenCalled(); }); it('should emit an event when the legacy contact is fetched', async () => { - const { fixture, outputs } = await shallow.render(); + const fixture = MockRender(LegacyContactDisplayComponent); + const instance = fixture.point.componentInstance; + const loadedLegacyContactSpy = spyOn(instance.loadedLegacyContact, 'emit'); await fixture.whenStable(); - expect(outputs.loadedLegacyContact.emit).toHaveBeenCalled(); + expect(loadedLegacyContactSpy).toHaveBeenCalled(); }); }); diff --git a/src/app/directive/components/legacy-contact-edit/legacy-contact-edit.component.spec.ts b/src/app/directive/components/legacy-contact-edit/legacy-contact-edit.component.spec.ts index c9d2d112f..4213c77ce 100644 --- a/src/app/directive/components/legacy-contact-edit/legacy-contact-edit.component.spec.ts +++ b/src/app/directive/components/legacy-contact-edit/legacy-contact-edit.component.spec.ts @@ -1,33 +1,26 @@ -import { Type, DebugElement } from '@angular/core'; -import { Shallow } from 'shallow-render'; -import { QueryMatch } from 'shallow-render/dist/lib/models/query-match'; +import { NgModule } from '@angular/core'; +import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; +import { FormsModule } from '@angular/forms'; import { LegacyContact } from '@models/directive'; import { ApiService } from '@shared/services/api/api.service'; import { MessageService } from '@shared/services/message/message.service'; import { EventService } from '@shared/services/event/event.service'; -import { DirectiveModule } from '../../directive.module'; import { MockDirectiveRepo } from '../legacy-contact-display/test-utils'; import { MockMessageService } from '../directive-edit/test-utils'; import { LegacyContactEditComponent } from './legacy-contact-edit.component'; -type Find = ( - cssOrDirective: string | Type, - options?: { - query?: string; - }, -) => QueryMatch; +@NgModule() +class DummyModule {} class MockApiService { public directive = new MockDirectiveRepo(); } describe('LegacyContactEditComponent', () => { - let shallow: Shallow; - - const fillOutForm = (find: Find, email: string, name: string) => { - const emailInput = find('.legacy-contact-email')[0] + const fillOutForm = (email: string, name: string) => { + const emailInput = ngMocks.findAll('.legacy-contact-email')[0] .nativeElement as HTMLInputElement; - const nameInput = find('.legacy-contact-name')[0] + const nameInput = ngMocks.findAll('.legacy-contact-name')[0] .nativeElement as HTMLTextAreaElement; emailInput.value = email; @@ -36,71 +29,68 @@ describe('LegacyContactEditComponent', () => { nameInput.dispatchEvent(new Event('input')); }; - beforeEach(() => { - shallow = new Shallow(LegacyContactEditComponent, DirectiveModule) - .provideMock( - { - provide: ApiService, - useClass: MockApiService, - }, - { - provide: MessageService, - useClass: MockMessageService, - }, - ) - .provide(EventService) - .dontMock(EventService); + beforeEach(async () => { MockDirectiveRepo.reset(); + MockMessageService.reset(); + await MockBuilder(LegacyContactEditComponent, DummyModule) + .keep(FormsModule, { export: true }) + .provide({ + provide: ApiService, + useClass: MockApiService, + }) + .provide({ + provide: MessageService, + useClass: MockMessageService, + }) + .keep(EventService); }); - it('should create', async () => { - const { instance } = await shallow.render(); + it('should create', () => { + const fixture = MockRender(LegacyContactEditComponent); - expect(instance).toBeTruthy(); + expect(fixture.point.componentInstance).toBeTruthy(); }); - it('should be able to fill out legacy contact form', async () => { - const { instance, find } = await shallow.render(); + it('should be able to fill out legacy contact form', () => { + const fixture = MockRender(LegacyContactEditComponent); + const instance = fixture.point.componentInstance; - expect(find('.legacy-contact-name').length).toBe(1); - expect(find('.legacy-contact-email').length).toBe(1); + expect(ngMocks.findAll('.legacy-contact-name').length).toBe(1); + expect(ngMocks.findAll('.legacy-contact-email').length).toBe(1); - fillOutForm(find, 'test@example.com', 'Unit Testing'); + fillOutForm('test@example.com', 'Unit Testing'); expect(instance.name).toBe('Unit Testing'); expect(instance.email).toBe('test@example.com'); }); it('should be able to save a legacy contact', async () => { - const { find, fixture } = await shallow.render(); + const fixture = MockRender(LegacyContactEditComponent); - fillOutForm(find, 'save@example.com', 'Save Test'); + fillOutForm('save@example.com', 'Save Test'); - expect(find('.save-btn').length).toBe(1); - find('.save-btn').nativeElement.dispatchEvent(new Event('click')); + expect(ngMocks.findAll('.save-btn').length).toBe(1); + ngMocks.find('.save-btn').nativeElement.dispatchEvent(new Event('click')); fixture.detectChanges(); - expect(find('*[disabled]').length).toBe(3); + expect(ngMocks.findAll('*[disabled]').length).toBe(3); await fixture.whenStable(); fixture.detectChanges(); - expect(find('*[disabled]').length).toBe(0); + expect(ngMocks.findAll('*[disabled]').length).toBe(0); expect(MockDirectiveRepo.savedLegacyContact.email).toBe('save@example.com'); expect(MockDirectiveRepo.savedLegacyContact.name).toBe('Save Test'); expect(MockDirectiveRepo.createdLegacyContact).toBeTrue(); }); - it('should be able to have existing legacy contact data passed in', async () => { + it('should be able to have existing legacy contact data passed in', () => { const legacyContact: LegacyContact = { name: 'Existing Contact', email: 'existing@example.com', }; - const { instance } = await shallow.render({ - bind: { - legacyContact, - }, - }); + const fixture = MockRender(LegacyContactEditComponent, { legacyContact }); + const instance = fixture.point.componentInstance; expect(instance.email).toBe('existing@example.com'); expect(instance.name).toBe('Existing Contact'); @@ -111,15 +101,11 @@ describe('LegacyContactEditComponent', () => { name: 'Existing Contact', email: 'existing@example.com', }; - const { find, fixture } = await shallow.render({ - bind: { - legacyContact, - }, - }); + const fixture = MockRender(LegacyContactEditComponent, { legacyContact }); - fillOutForm(find, 'existing@example.com', 'Existing Updated Contact'); + fillOutForm('existing@example.com', 'Existing Updated Contact'); - find('.save-btn').nativeElement.dispatchEvent(new Event('click')); + ngMocks.find('.save-btn').nativeElement.dispatchEvent(new Event('click')); await fixture.whenStable(); fixture.detectChanges(); @@ -133,11 +119,11 @@ describe('LegacyContactEditComponent', () => { it('should handle API errors on creation', async () => { MockDirectiveRepo.throwError = true; - const { find, fixture } = await shallow.render(); + const fixture = MockRender(LegacyContactEditComponent); - fillOutForm(find, 'error@example.com', 'Throw Error'); + fillOutForm('error@example.com', 'Throw Error'); - find('.save-btn').nativeElement.dispatchEvent(new Event('click')); + ngMocks.find('.save-btn').nativeElement.dispatchEvent(new Event('click')); await fixture.whenStable(); fixture.detectChanges(); @@ -151,15 +137,11 @@ describe('LegacyContactEditComponent', () => { name: 'Test', email: 'test@example.com', }; - const { find, fixture } = await shallow.render({ - bind: { - legacyContact, - }, - }); + const fixture = MockRender(LegacyContactEditComponent, { legacyContact }); - fillOutForm(find, 'error@example.com', 'Throw Error'); + fillOutForm('error@example.com', 'Throw Error'); - find('.save-btn').nativeElement.dispatchEvent(new Event('click')); + ngMocks.find('.save-btn').nativeElement.dispatchEvent(new Event('click')); await fixture.whenStable(); fixture.detectChanges(); @@ -168,55 +150,57 @@ describe('LegacyContactEditComponent', () => { }); it('should emit an output after saving (creation)', async () => { - const { find, fixture, outputs } = await shallow.render(); + const fixture = MockRender(LegacyContactEditComponent); + const instance = fixture.point.componentInstance; + const savedLegacyContactSpy = spyOn(instance.savedLegacyContact, 'emit'); - fillOutForm(find, 'output@example.com', 'Test Output'); + fillOutForm('output@example.com', 'Test Output'); - find('.save-btn').nativeElement.dispatchEvent(new Event('click')); + ngMocks.find('.save-btn').nativeElement.dispatchEvent(new Event('click')); await fixture.whenStable(); fixture.detectChanges(); - expect(outputs.savedLegacyContact.emit).toHaveBeenCalled(); + expect(savedLegacyContactSpy).toHaveBeenCalled(); }); it('should emit an output after saving (update)', async () => { - const { find, fixture, outputs } = await shallow.render({ - bind: { - legacyContact: { - id: '1', - name: 'Test Output', - email: 'output@example.com', - }, + const fixture = MockRender(LegacyContactEditComponent, { + legacyContact: { + id: '1', + name: 'Test Output', + email: 'output@example.com', }, }); + const instance = fixture.point.componentInstance; + const savedLegacyContactSpy = spyOn(instance.savedLegacyContact, 'emit'); - fillOutForm(find, 'output@example.com', 'Test Update Output'); + fillOutForm('output@example.com', 'Test Update Output'); - find('.save-btn').nativeElement.dispatchEvent(new Event('click')); + ngMocks.find('.save-btn').nativeElement.dispatchEvent(new Event('click')); await fixture.whenStable(); fixture.detectChanges(); - expect(outputs.savedLegacyContact.emit).toHaveBeenCalled(); + expect(savedLegacyContactSpy).toHaveBeenCalled(); }); - it('should not allow the form to submit until all fields are filled out', async () => { - const { find, fixture } = await shallow.render(); + it('should not allow the form to submit until all fields are filled out', () => { + const fixture = MockRender(LegacyContactEditComponent); - expect(find('.save-btn[disabled]').length).toBe(1); + expect(ngMocks.findAll('.save-btn[disabled]').length).toBe(1); - fillOutForm(find, '', 'Test No Submit'); + fillOutForm('', 'Test No Submit'); fixture.detectChanges(); - expect(find('.save-btn[disabled]').length).toBe(1); + expect(ngMocks.findAll('.save-btn[disabled]').length).toBe(1); - fillOutForm(find, 'no-submit@example.com', ''); + fillOutForm('no-submit@example.com', ''); fixture.detectChanges(); - expect(find('.save-btn[disabled]').length).toBe(1); + expect(ngMocks.findAll('.save-btn[disabled]').length).toBe(1); - fillOutForm(find, 'submit@example.com', 'Submit Now Works'); + fillOutForm('submit@example.com', 'Submit Now Works'); fixture.detectChanges(); - expect(find('.save-btn[disabled]').length).toBe(0); + expect(ngMocks.findAll('.save-btn[disabled]').length).toBe(0); }); }); diff --git a/src/app/file-browser/components/edit-tags/edit-tags.component.spec.ts b/src/app/file-browser/components/edit-tags/edit-tags.component.spec.ts index 5e21888af..2fcec68cf 100644 --- a/src/app/file-browser/components/edit-tags/edit-tags.component.spec.ts +++ b/src/app/file-browser/components/edit-tags/edit-tags.component.spec.ts @@ -1,6 +1,9 @@ -import { waitForAsync } from '@angular/core/testing'; -import { Shallow } from 'shallow-render'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { of } from 'rxjs'; +import { By } from '@angular/platform-browser'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { FormsModule } from '@angular/forms'; import { ItemVO, TagVOData, RecordVO } from '@models'; import { ApiService } from '@shared/services/api/api.service'; @@ -9,10 +12,7 @@ import { TagsService } from '@core/services/tags/tags.service'; import { MessageService } from '@shared/services/message/message.service'; import { SearchService } from '@search/services/search.service'; import { TagResponse } from '@shared/services/api/tag.repo'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { By } from '@angular/platform-browser'; import { DialogCdkService } from '@root/app/dialog-cdk/dialog-cdk.service'; -import { FileBrowserComponentsModule } from '../../file-browser-components.module'; import { EditTagsComponent, TagType } from './edit-tags.component'; const defaultTagList: TagVOData[] = [ @@ -40,174 +40,191 @@ const defaultTagList: TagVOData[] = [ const defaultItem: ItemVO = new RecordVO({ TagVOs: defaultTagList }); describe('EditTagsComponent', () => { - let shallow: Shallow; - async function defaultRender( + let component: EditTagsComponent; + let fixture: ComponentFixture; + let dialogCdkServiceSpy: jasmine.SpyObj; + + beforeEach(async () => { + dialogCdkServiceSpy = jasmine.createSpyObj('DialogCdkService', ['open']); + + await TestBed.configureTestingModule({ + declarations: [EditTagsComponent], + imports: [NoopAnimationsModule, FormsModule], + providers: [ + { + provide: SearchService, + useValue: { getTagResults: (tag: string) => defaultTagList }, + }, + { + provide: TagsService, + useValue: { + getTags: () => defaultTagList, + getTags$: () => of(defaultTagList), + setItemTags: () => {}, + getItemTags$: () => of([]), + }, + }, + { + provide: MessageService, + useValue: { showError: () => {} }, + }, + { + provide: ApiService, + useValue: { + tag: { + deleteTagLink: async (tag: TagVOData, tagLink: any) => + await Promise.resolve(new TagResponse()), + create: async (tag: TagVOData, tagLink: any) => + await Promise.resolve(new TagResponse()), + }, + }, + }, + { + provide: DataService, + useValue: { + fetchFullItems: async (items: ItemVO[]) => + await Promise.resolve([]), + }, + }, + { + provide: DialogCdkService, + useValue: dialogCdkServiceSpy, + }, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(EditTagsComponent); + component = fixture.componentInstance; + }); + + function setupComponent( item: ItemVO = defaultItem, tagType: TagType = 'keyword', ) { - return await shallow.render( - ``, - { - bind: { - item, - tagType, - }, - }, - ); + component.item = item; + component.tagType = tagType; + fixture.detectChanges(); } - beforeEach(waitForAsync(() => { - shallow = new Shallow(EditTagsComponent, FileBrowserComponentsModule) - .dontMock(NoopAnimationsModule) - .import(NoopAnimationsModule) - .mock(SearchService, { getTagResults: (tag) => defaultTagList }) - .mock(TagsService, { - getTags: () => defaultTagList, - getTags$: () => of(defaultTagList), - setItemTags: () => {}, - }) - .mock(MessageService, { showError: () => {} }) - .mock(ApiService, { - tag: { - deleteTagLink: async (tag, tagLink) => - await Promise.resolve(new TagResponse()), - create: async (tag, tagLink) => - await Promise.resolve(new TagResponse()), - }, - }) - .mock(DataService, { - fetchFullItems: async (items) => await Promise.resolve([]), - }) - .mock( - DialogCdkService, - jasmine.createSpyObj('DialogCdkService', ['open']), - ); - })); - - it('should create', async () => { - const { element } = await defaultRender(); - - expect(element).not.toBeNull(); + it('should create', () => { + setupComponent(); + + expect(component).not.toBeNull(); }); - it('should only show keywords in keyword mode', async () => { - const { element } = await defaultRender(); + it('should only show keywords in keyword mode', () => { + setupComponent(); expect( - element.componentInstance.itemTags.find((tag) => tag.name === 'tagOne'), + component.itemTags.find((tag) => tag.name === 'tagOne'), ).toBeTruthy(); expect( - element.componentInstance.itemTags.find((tag) => tag.name === 'tagTwo'), + component.itemTags.find((tag) => tag.name === 'tagTwo'), ).toBeTruthy(); expect( - element.componentInstance.itemTags.find( + component.itemTags.find( (tag) => tag.name === 'customField:customValueOne', ), ).not.toBeTruthy(); expect( - element.componentInstance.itemTags.find( + component.itemTags.find( (tag) => tag.name === 'customField:customValueTwo', ), ).not.toBeTruthy(); expect( - element.componentInstance.matchingTags.find( - (tag) => tag.name === 'tagOne', - ), + component.matchingTags.find((tag) => tag.name === 'tagOne'), ).toBeTruthy(); expect( - element.componentInstance.matchingTags.find( - (tag) => tag.name === 'tagTwo', - ), + component.matchingTags.find((tag) => tag.name === 'tagTwo'), ).toBeTruthy(); expect( - element.componentInstance.matchingTags.find( + component.matchingTags.find( (tag) => tag.name === 'customField:customValueOne', ), ).not.toBeTruthy(); expect( - element.componentInstance.matchingTags.find( + component.matchingTags.find( (tag) => tag.name === 'customField:customValueTwo', ), ).not.toBeTruthy(); }); - it('should only show custom metadata in custom metadata mode', async () => { - const { element } = await defaultRender(defaultItem, 'customMetadata'); + it('should only show custom metadata in custom metadata mode', () => { + setupComponent(defaultItem, 'customMetadata'); expect( - element.componentInstance.itemTags.find((tag) => tag.name === 'tagOne'), + component.itemTags.find((tag) => tag.name === 'tagOne'), ).not.toBeTruthy(); expect( - element.componentInstance.itemTags.find((tag) => tag.name === 'tagTwo'), + component.itemTags.find((tag) => tag.name === 'tagTwo'), ).not.toBeTruthy(); expect( - element.componentInstance.itemTags.find( + component.itemTags.find( (tag) => tag.name === 'customField:customValueOne', ), ).toBeTruthy(); expect( - element.componentInstance.itemTags.find( + component.itemTags.find( (tag) => tag.name === 'customField:customValueTwo', ), ).toBeTruthy(); expect( - element.componentInstance.matchingTags.find( - (tag) => tag.name === 'tagOne', - ), + component.matchingTags.find((tag) => tag.name === 'tagOne'), ).not.toBeTruthy(); expect( - element.componentInstance.matchingTags.find( - (tag) => tag.name === 'tagTwo', - ), + component.matchingTags.find((tag) => tag.name === 'tagTwo'), ).not.toBeTruthy(); expect( - element.componentInstance.matchingTags.find( + component.matchingTags.find( (tag) => tag.name === 'customField:customValueOne', ), ).toBeTruthy(); expect( - element.componentInstance.matchingTags.find( + component.matchingTags.find( (tag) => tag.name === 'customField:customValueTwo', ), ).toBeTruthy(); }); it('should not create custom metadata in keyword mode', async () => { - const { element } = await defaultRender(); - const tagCreateSpy = spyOn(element.componentInstance.api.tag, 'create'); - await element.componentInstance.onInputEnter('key:value'); + setupComponent(); + const apiService = TestBed.inject(ApiService); + const tagCreateSpy = spyOn(apiService.tag, 'create'); + await component.onInputEnter('key:value'); - expect(element.componentInstance.newTagInputError).toBeTruthy(); + expect(component.newTagInputError).toBeTruthy(); expect(tagCreateSpy).not.toHaveBeenCalled(); }); it('should not create keyword in custom metadata mode', async () => { - const { element } = await defaultRender(defaultItem, 'customMetadata'); - const tagCreateSpy = spyOn(element.componentInstance.api.tag, 'create'); - await element.componentInstance.onInputEnter('keyword'); + setupComponent(defaultItem, 'customMetadata'); + const apiService = TestBed.inject(ApiService); + const tagCreateSpy = spyOn(apiService.tag, 'create'); + await component.onInputEnter('keyword'); - expect(element.componentInstance.newTagInputError).toBeTruthy(); + expect(component.newTagInputError).toBeTruthy(); expect(tagCreateSpy).not.toHaveBeenCalled(); }); - it('should highlight the correct tag on key down', async () => { - const { fixture, element } = await defaultRender(); + it('should highlight the correct tag on key down', () => { + setupComponent(); - element.componentInstance.isEditing = true; + component.isEditing = true; fixture.detectChanges(); const tags = fixture.debugElement.queryAll(By.css('.edit-tag')); @@ -222,10 +239,10 @@ describe('EditTagsComponent', () => { expect(focusedElement).toBe(tags[1].nativeElement); }); - it('should highlight the correct tag on key up', async () => { - const { fixture, element } = await defaultRender(); + it('should highlight the correct tag on key up', () => { + setupComponent(); - element.componentInstance.isEditing = true; + component.isEditing = true; fixture.detectChanges(); const tags = fixture.debugElement.queryAll(By.css('.edit-tag')); @@ -240,10 +257,10 @@ describe('EditTagsComponent', () => { expect(focusedElement).toBe(tags[0].nativeElement); }); - it('should highlight the input on key up', async () => { - const { fixture, element } = await defaultRender(); + it('should highlight the input on key up', () => { + setupComponent(); - element.componentInstance.isEditing = true; + component.isEditing = true; fixture.detectChanges(); const tag = fixture.debugElement.query(By.css('.edit-tag')); @@ -260,18 +277,17 @@ describe('EditTagsComponent', () => { expect(focusedElement).toEqual(input.nativeElement); }); - it('should open dialog when manage link is clicked', async () => { - const { element, find, inject, fixture } = await defaultRender(); - const dialogOpenSpy = inject(DialogCdkService); + it('should open dialog when manage link is clicked', () => { + setupComponent(); - element.componentInstance.isEditing = true; + component.isEditing = true; fixture.detectChanges(); - find('.manage-tags-message .manage-tags-link').triggerEventHandler( - 'click', - {}, + const manageTagsLink = fixture.debugElement.query( + By.css('.manage-tags-message .manage-tags-link'), ); + manageTagsLink.triggerEventHandler('click', {}); - expect(dialogOpenSpy.open).toHaveBeenCalled(); + expect(dialogCdkServiceSpy.open).toHaveBeenCalled(); }); }); diff --git a/src/app/file-browser/components/file-viewer/file-viewer.component.spec.ts b/src/app/file-browser/components/file-viewer/file-viewer.component.spec.ts index eeb1f59b7..37f3eda56 100644 --- a/src/app/file-browser/components/file-viewer/file-viewer.component.spec.ts +++ b/src/app/file-browser/components/file-viewer/file-viewer.component.spec.ts @@ -1,7 +1,8 @@ +import { CUSTOM_ELEMENTS_SCHEMA, Pipe, PipeTransform } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { Router, ActivatedRoute } from '@angular/router'; -import { SecurityContext } from '@angular/core'; -import { Shallow } from 'shallow-render'; import { Subject } from 'rxjs'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; import { RecordVO, ItemVO, TagVOData, ArchiveVO } from '@root/app/models'; import { AccountService } from '@shared/services/account/account.service'; @@ -9,11 +10,33 @@ import { DataService } from '@shared/services/data/data.service'; import { EditService } from '@core/services/edit/edit.service'; import { TagsService } from '@core/services/tags/tags.service'; import { PublicProfileService } from '@public/services/public-profile/public-profile.service'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { FileBrowserComponentsModule } from '../../file-browser-components.module'; +import { ShareLinksService } from '@root/app/share-links/services/share-links.service'; +import { ApiService } from '@shared/services/api/api.service'; +import { MockComponent } from 'ng-mocks'; import { TagsComponent } from '../../../shared/components/tags/tags.component'; import { FileViewerComponent } from './file-viewer.component'; +@Pipe({ name: 'dsFileSize', standalone: false }) +class MockFileSizePipe implements PipeTransform { + transform(value: number): string { + return value?.toString() || ''; + } +} + +@Pipe({ name: 'getAltText', standalone: false }) +class MockGetAltTextPipe implements PipeTransform { + transform(value: any): string { + return value?.displayName || ''; + } +} + +@Pipe({ name: 'prConstants', standalone: false }) +class MockPrConstantsPipe implements PipeTransform { + transform(value: any): any { + return value; + } +} + const defaultTagList: TagVOData[] = [ { tagId: 1, @@ -63,7 +86,8 @@ class MockTagsService { } describe('FileViewerComponent', () => { - let shallow: Shallow; + let component: FileViewerComponent; + let fixture: ComponentFixture; let activatedRouteData: ActivatedRouteSnapshotData; let folderChildren: ItemVO[]; let tagsService: MockTagsService; @@ -72,9 +96,7 @@ describe('FileViewerComponent', () => { let hasAccess: boolean; let openedDialogs: string[]; let downloaded: boolean; - async function defaultRender() { - return await shallow.render(``); - } + let publicProfileService: PublicProfileService; function setUpMultipleRecords(...items: ItemVO[]) { folderChildren.push(...items); @@ -94,112 +116,147 @@ describe('FileViewerComponent', () => { hasAccess = true; openedDialogs = []; downloaded = false; - shallow = new Shallow(FileViewerComponent, FileBrowserComponentsModule) - .import(HttpClientTestingModule) - .dontMock(TagsService) - .dontMock(PublicProfileService) - .mock(Router, { - navigate: async (route: string[]) => { - navigatedUrl = route; - return await Promise.resolve(true); - }, - routerState: { - snapshot: { - url: 'exampleUrl.com', + publicProfileService = new PublicProfileService(); + + await TestBed.configureTestingModule({ + declarations: [ + FileViewerComponent, + MockComponent(TagsComponent), + MockFileSizePipe, + MockGetAltTextPipe, + MockPrConstantsPipe, + ], + imports: [HttpClientTestingModule], + providers: [ + { + provide: Router, + useValue: { + navigate: async (route: string[]) => { + navigatedUrl = route; + return await Promise.resolve(true); + }, + routerState: { + snapshot: { + url: 'exampleUrl.com', + }, + }, }, }, - }) - .mock(ActivatedRoute, { - snapshot: { - data: activatedRouteData, + { + provide: ActivatedRoute, + useValue: { + snapshot: { + data: activatedRouteData, + }, + }, }, - }) - .mock(DataService, { - currentFolder: { - ChildItemVOs: folderChildren, + { + provide: DataService, + useValue: { + currentFolder: { + ChildItemVOs: folderChildren, + }, + fetchFullItems: async () => {}, + fetchLeanItems: async () => {}, + async downloadFile(_item: RecordVO, _type: string) { + downloaded = true; + }, + }, }, - fetchFullItems: async () => {}, - fetchLeanItems: async () => {}, - async downloadFile(_item: RecordVO, _type: string) { - downloaded = true; + { + provide: AccountService, + useValue: { + checkMinimumAccess: (_itemAccessRole: any, _minimumAccess: any) => + hasAccess, + }, }, - }) - .mock(AccountService, { - checkMinimumAccess: (_itemAccessRole, _minimumAccess) => hasAccess, - }) - .mock(EditService, { - async saveItemVoProperty(_record, name, value) { - savedProperty = { name: name as string, value }; + { + provide: EditService, + useValue: { + async saveItemVoProperty(_record: any, name: string, value: any) { + savedProperty = { name: name as string, value }; + }, + async openLocationDialog(_item: any) { + openedDialogs.push('location'); + }, + async openTagsDialog(_record: any, _type: any) { + openedDialogs.push('tags'); + }, + }, }, - async openLocationDialog(_item) { - openedDialogs.push('location'); + { provide: TagsService, useValue: tagsService }, + { provide: PublicProfileService, useValue: publicProfileService }, + { + provide: ShareLinksService, + useValue: { + isUnlistedShare: async () => false, + currentShareToken: null, + }, }, - async openTagsDialog(_record, _type) { - openedDialogs.push('tags'); + { + provide: ApiService, + useValue: { + record: { + get: async () => ({ getRecordVO: () => defaultItem }), + }, + }, }, - }) - .provide({ provide: TagsService, useValue: tagsService }) - .provide({ - provide: PublicProfileService, - useValue: new PublicProfileService(), - }); - }); + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); - it('should create', async () => { - const { element } = await defaultRender(); + fixture = TestBed.createComponent(FileViewerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); - expect(element).not.toBeNull(); + it('should create', () => { + expect(component).not.toBeNull(); }); - it('should have two tags components', async () => { - const { findComponent } = await defaultRender(); + it('should have two tags components', () => { + const tagsComponents = fixture.nativeElement.querySelectorAll('pr-tags'); - expect(findComponent(TagsComponent)).toHaveFound(2); + expect(tagsComponents.length).toBe(2); }); - it('should correctly distinguish between keywords and custom metadata', async () => { - const { element } = await defaultRender(); - + it('should correctly distinguish between keywords and custom metadata', () => { expect( - element.componentInstance.keywords.find((tag) => tag.name === 'tagOne'), + component.keywords.find((tag) => tag.name === 'tagOne'), ).toBeTruthy(); expect( - element.componentInstance.keywords.find((tag) => tag.name === 'tagTwo'), + component.keywords.find((tag) => tag.name === 'tagTwo'), ).toBeTruthy(); expect( - element.componentInstance.keywords.find( + component.keywords.find( (tag) => tag.name === 'customField:customValueOne', ), ).not.toBeTruthy(); expect( - element.componentInstance.keywords.find( + component.keywords.find( (tag) => tag.name === 'customField:customValueTwo', ), ).not.toBeTruthy(); expect( - element.componentInstance.customMetadata.find( - (tag) => tag.name === 'tagOne', - ), + component.customMetadata.find((tag) => tag.name === 'tagOne'), ).not.toBeTruthy(); expect( - element.componentInstance.customMetadata.find( - (tag) => tag.name === 'tagTwo', - ), + component.customMetadata.find((tag) => tag.name === 'tagTwo'), ).not.toBeTruthy(); expect( - element.componentInstance.customMetadata.find( + component.customMetadata.find( (tag) => tag.name === 'customField:customValueOne', ), ).toBeTruthy(); expect( - element.componentInstance.customMetadata.find( + component.customMetadata.find( (tag) => tag.name === 'customField:customValueTwo', ), ).toBeTruthy(); @@ -207,37 +264,39 @@ describe('FileViewerComponent', () => { it('should be able to load multiple record in a folder', async () => { setUpMultipleRecords(defaultItem, secondItem); - const { element } = await defaultRender(); + // Need to recreate the component with the new data + fixture = TestBed.createComponent(FileViewerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); - expect(element).not.toBeNull(); + expect(component).not.toBeNull(); }); - it('should listen to tag updates from the tag service', async () => { - const { instance } = await defaultRender(); + it('should listen to tag updates from the tag service', () => { tagsService.itemTagsObservable.next([ { type: 'type.tag.metadata.customField', name: 'test:metadta' }, { type: 'type.generic.placeholder', name: 'test' }, ]); - expect(instance.keywords.length).toBe(1); - expect(instance.customMetadata.length).toBe(1); + expect(component.keywords.length).toBe(1); + expect(component.customMetadata.length).toBe(1); }); - it('should listen to public profile archive updates', async () => { - const { inject, instance } = await defaultRender(); - const publicProfile = inject(PublicProfileService); - publicProfile.archiveBs.next(new ArchiveVO({ allowPublicDownload: true })); + it('should listen to public profile archive updates', () => { + publicProfileService.archiveBs.next( + new ArchiveVO({ allowPublicDownload: true }), + ); - expect(instance.allowDownloads).toBeTruthy(); + expect(component.allowDownloads).toBeTruthy(); }); - it('should handle null public profile archive updates', async () => { - const { inject, instance } = await defaultRender(); - const publicProfile = inject(PublicProfileService); - publicProfile.archiveBs.next(new ArchiveVO({ allowPublicDownload: true })); - publicProfile.archiveBs.next(null); + it('should handle null public profile archive updates', () => { + publicProfileService.archiveBs.next( + new ArchiveVO({ allowPublicDownload: true }), + ); + publicProfileService.archiveBs.next(null); - expect(instance.allowDownloads).toBeFalsy(); + expect(component.allowDownloads).toBeFalsy(); }); describe('Keyboard Input', () => { @@ -247,36 +306,46 @@ describe('FileViewerComponent', () => { ) { instance.onKeyDown(new KeyboardEvent('keydown', { key })); } + it('should handle right arrow key input', async () => { setUpMultipleRecords(defaultItem, secondItem); - const { instance } = await defaultRender(); - keyDown(instance, 'ArrowRight'); + // Recreate component with new data + fixture = TestBed.createComponent(FileViewerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + keyDown(component, 'ArrowRight'); - expect(instance.currentIndex).toBe(1); + expect(component.currentIndex).toBe(1); }); it('should handle left arrow key input', async () => { setUpMultipleRecords(secondItem, defaultItem); - const { instance } = await defaultRender(); - keyDown(instance, 'ArrowLeft'); + fixture = TestBed.createComponent(FileViewerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + keyDown(component, 'ArrowLeft'); - expect(instance.currentIndex).toBe(0); + expect(component.currentIndex).toBe(0); }); it('does not wrap around on right arrow', async () => { setUpMultipleRecords(secondItem, defaultItem); - const { instance } = await defaultRender(); - keyDown(instance, 'ArrowRight'); + fixture = TestBed.createComponent(FileViewerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + keyDown(component, 'ArrowRight'); - expect(instance.currentIndex).toBe(1); + expect(component.currentIndex).toBe(1); }); it('does not wrap around on left arrow', async () => { setUpMultipleRecords(defaultItem, secondItem); - const { instance } = await defaultRender(); - keyDown(instance, 'ArrowLeft'); + fixture = TestBed.createComponent(FileViewerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + keyDown(component, 'ArrowLeft'); - expect(instance.currentIndex).toBe(0); + expect(component.currentIndex).toBe(0); }); it('does not increment if the current record is still loading', async () => { @@ -285,11 +354,13 @@ describe('FileViewerComponent', () => { secondItem, new RecordVO({ folder_linkId: 2 }), ); - const { instance } = await defaultRender(); - keyDown(instance, 'ArrowRight'); - keyDown(instance, 'ArrowRight'); + fixture = TestBed.createComponent(FileViewerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + keyDown(component, 'ArrowRight'); + keyDown(component, 'ArrowRight'); - expect(instance.currentIndex).toBe(1); + expect(component.currentIndex).toBe(1); }); describe('Navigation after keyboard input', () => { @@ -298,8 +369,10 @@ describe('FileViewerComponent', () => { archiveNbr: '1234-1234', }); setUpMultipleRecords(defaultItem, secondItemWithArchiveNbr); - const { fixture, instance } = await defaultRender(); - keyDown(instance, 'ArrowRight'); + fixture = TestBed.createComponent(FileViewerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + keyDown(component, 'ArrowRight'); await fixture.whenStable(); expect(navigatedUrl).toContain('1234-1234'); @@ -313,8 +386,10 @@ describe('FileViewerComponent', () => { }), }); setUpMultipleRecords(defaultItem, secondItemFetching); - const { fixture, instance } = await defaultRender(); - keyDown(instance, 'ArrowRight'); + fixture = TestBed.createComponent(FileViewerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + keyDown(component, 'ArrowRight'); secondItemFetching.archiveNbr = '1234-1234'; await fixture.whenStable(); @@ -358,39 +433,50 @@ describe('FileViewerComponent', () => { const url = instance.getDocumentUrl(); expect(url).toBeTruthy(); - expect( - instance.sanitizer.sanitize(SecurityContext.RESOURCE_URL, url), - ).toContain(phrase); + // Access the internal URL from SafeResourceUrl + const internalUrl = (url as any).changingThisBreaksApplicationSecurity; + + expect(internalUrl).toContain(phrase); } it('can get the URL of a document', async () => { setUpCurrentRecord('doc'); - const { instance } = await defaultRender(); - expectSantizedUrlToContain(instance, 'used'); + fixture = TestBed.createComponent(FileViewerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + await fixture.whenStable(); + expectSantizedUrlToContain(component, 'used'); }); it('will prefer the URL of the original if it is a PDF', async () => { setUpCurrentRecord('pdf'); - const { instance } = await defaultRender(); - expectSantizedUrlToContain(instance, 'original'); + fixture = TestBed.createComponent(FileViewerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + await fixture.whenStable(); + expectSantizedUrlToContain(component, 'original'); }); it('will prefer the URL of the original if it is a TXT file', async () => { setUpCurrentRecord('txt'); - const { instance } = await defaultRender(); - expectSantizedUrlToContain(instance, 'original'); + fixture = TestBed.createComponent(FileViewerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + await fixture.whenStable(); + expectSantizedUrlToContain(component, 'original'); }); it('will have a falsy document URL if it is not a document', async () => { - const { instance } = await defaultRender(); - - expect(instance.getDocumentUrl()).toBeFalsy(); + expect(component.getDocumentUrl()).toBeFalsy(); }); it('will have a falsy document URL if the URL is falsy', async () => { setUpCurrentRecord('pdf', false); - const { instance } = await defaultRender(); + fixture = TestBed.createComponent(FileViewerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + await fixture.whenStable(); - expect(instance.getDocumentUrl()).toBeFalsy(); + expect(component.getDocumentUrl()).toBeFalsy(); }); }); @@ -399,16 +485,14 @@ describe('FileViewerComponent', () => { hasAccess = access; activatedRouteData.isPublicArchive = false; } - it('can close the file viewer', async () => { - const { instance } = await defaultRender(); - instance.close(); + it('can close the file viewer', () => { + component.close(); expect(navigatedUrl).toContain('.'); }); it('can finish editing', async () => { - const { instance } = await defaultRender(); - await instance.onFinishEditing('displayName', 'Test'); + await component.onFinishEditing('displayName', 'Test'); expect(savedProperty.name).toBe('displayName'); expect(savedProperty.value).toBe('Test'); @@ -416,9 +500,11 @@ describe('FileViewerComponent', () => { it('can open the location dialog with edit permissions', async () => { setAccess(true); - const { fixture, instance } = await defaultRender(); - instance.canEdit = true; - instance.onLocationClick(); + fixture = TestBed.createComponent(FileViewerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + component.canEdit = true; + component.onLocationClick(); await fixture.whenStable(); expect(openedDialogs).toContain('location'); @@ -426,8 +512,10 @@ describe('FileViewerComponent', () => { it('cannot open the location dialog without edit permissions', async () => { setAccess(false); - const { fixture, instance } = await defaultRender(); - instance.onLocationClick(); + fixture = TestBed.createComponent(FileViewerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + component.onLocationClick(); await fixture.whenStable(); expect(openedDialogs).not.toContain('location'); @@ -435,9 +523,11 @@ describe('FileViewerComponent', () => { it('can open the tags dialog with edit permissions', async () => { setAccess(true); - const { fixture, instance } = await defaultRender(); - instance.canEdit = true; - instance.onTagsClick('keyword'); + fixture = TestBed.createComponent(FileViewerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + component.canEdit = true; + component.onTagsClick('keyword'); await fixture.whenStable(); expect(openedDialogs).toContain('tags'); @@ -445,30 +535,28 @@ describe('FileViewerComponent', () => { it('cannot open the tags dialog with edit permissions', async () => { setAccess(false); - const { fixture, instance } = await defaultRender(); - instance.onTagsClick('keyword'); + fixture = TestBed.createComponent(FileViewerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + component.onTagsClick('keyword'); await fixture.whenStable(); expect(openedDialogs).not.toContain('tags'); }); it('can download items', async () => { - const { fixture, instance } = await defaultRender(); - instance.onDownloadClick(); + component.onDownloadClick(); await fixture.whenStable(); expect(downloaded).toBeTrue(); }); - it('should display "Click to add location" on fullscreen view', async () => { - const { fixture, instance, find } = await defaultRender(); - instance.canEdit = true; + it('should display "Click to add location" on fullscreen view', () => { + component.canEdit = true; fixture.detectChanges(); - const locationSpan = find('.add-location'); + const locationSpan = fixture.nativeElement.querySelector('.add-location'); - expect(locationSpan.nativeElement.textContent.trim()).toBe( - 'Click to add location', - ); + expect(locationSpan.textContent.trim()).toBe('Click to add location'); }); }); }); diff --git a/src/app/file-browser/components/publish/publish.component.spec.ts b/src/app/file-browser/components/publish/publish.component.spec.ts index b76850506..8319e2f87 100644 --- a/src/app/file-browser/components/publish/publish.component.spec.ts +++ b/src/app/file-browser/components/publish/publish.component.spec.ts @@ -1,4 +1,5 @@ -import { Shallow } from 'shallow-render'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { AccountService } from '@shared/services/account/account.service'; import { FolderVO, RecordVO } from '@models/index'; import { FolderResponse } from '@shared/services/api/folder.repo'; @@ -6,8 +7,8 @@ import { Observable } from 'rxjs'; import { MessageService } from '@shared/services/message/message.service'; import { EventService } from '@shared/services/event/event.service'; import { DIALOG_DATA, DialogRef } from '@angular/cdk/dialog'; +import { Router } from '@angular/router'; import { ArchiveVO } from '../../../models/archive-vo'; -import { FileBrowserComponentsModule } from '../../file-browser-components.module'; import { ApiService } from '../../../shared/services/api/api.service'; import { PublishComponent } from './publish.component'; @@ -16,6 +17,10 @@ const mockAccountService = { const archive = new ArchiveVO({ accessRole: 'access.role.viewer' }); return archive; }, + getRootFolder: () => ({ + ChildItemVOs: [], + }), + refreshAccountDebounced: () => {}, }; class MockDialogRef { @@ -31,42 +36,73 @@ const mockApiService = { navigateLean: (folder: FolderVO): Observable => new Observable(), }, + publish: { + getInternetArchiveLink: async () => ({ + getPublishIaVO: () => null, + }), + publishToInternetArchive: async () => ({ + getPublishIaVO: () => null, + }), + }, + record: { + copy: async () => ({ + getRecordVO: () => new RecordVO({}), + }), + }, }; describe('PublishComponent', () => { - let shallow: Shallow; + let component: PublishComponent; + let fixture: ComponentFixture; - beforeEach(() => { - shallow = new Shallow(PublishComponent, FileBrowserComponentsModule) - .mock(AccountService, mockAccountService) - .mock(ApiService, mockApiService) - .mock(DIALOG_DATA, { - item: { folder_linkType: 'linkType' }, - }) - .provide({ provide: DialogRef, useClass: MockDialogRef }) - .provide(EventService) - .dontMock(EventService) - .mock(MessageService, { - showError: () => {}, - }); - }); + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [PublishComponent], + providers: [ + { provide: AccountService, useValue: mockAccountService }, + { provide: ApiService, useValue: mockApiService }, + { + provide: DIALOG_DATA, + useValue: { + item: { folder_linkType: 'linkType' }, + }, + }, + { provide: DialogRef, useClass: MockDialogRef }, + EventService, + { + provide: MessageService, + useValue: { + showError: () => {}, + }, + }, + { + provide: Router, + useValue: { + navigate: () => {}, + }, + }, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); - it('should create', async () => { - const { instance } = await shallow.render(); + fixture = TestBed.createComponent(PublishComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); - expect(instance).toBeTruthy(); + it('should create', () => { + expect(component).toBeTruthy(); }); - it('should disaple the public to internet archive button if the user does not have the correct access role', async () => { - const { instance, find, fixture } = await shallow.render(); - instance.publicItem = new RecordVO({ recordId: 1 }); - instance.publishIa = null; - instance.publicLink = null; + it('should disaple the public to internet archive button if the user does not have the correct access role', () => { + component.publicItem = new RecordVO({ recordId: 1 }); + component.publishIa = null; + component.publicLink = null; fixture.detectChanges(); - const button = find('.publish-to-archive'); + const button = fixture.nativeElement.querySelector('.publish-to-archive'); - expect(button.nativeElement.disabled).toBeTruthy(); + expect(button.disabled).toBeTruthy(); }); }); diff --git a/src/app/file-browser/components/sharing-dialog/sharing-dialog.component.spec.ts b/src/app/file-browser/components/sharing-dialog/sharing-dialog.component.spec.ts index 44f6b8ea8..846b363af 100644 --- a/src/app/file-browser/components/sharing-dialog/sharing-dialog.component.spec.ts +++ b/src/app/file-browser/components/sharing-dialog/sharing-dialog.component.spec.ts @@ -5,6 +5,7 @@ import { TestModuleMetadata, tick, } from '@angular/core/testing'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { SharedModule } from '@shared/shared.module'; import { cloneDeep } from 'lodash'; import * as Testing from '@root/test/testbedConfig'; @@ -15,8 +16,6 @@ import { ApiService } from '@shared/services/api/api.service'; import { ShareResponse } from '@shared/services/api/share.repo'; import { AccessRoleType, PermissionsLevel } from '@models/access-role'; import { MessageService } from '@shared/services/message/message.service'; -import { Shallow } from 'shallow-render'; -import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; import { ActivatedRoute, Router } from '@angular/router'; @@ -336,52 +335,65 @@ describe('SharingDialogComponent', () => { })); }); -describe('SharingDialogComponent - Shallow Rendering', () => { - it('should be able to save default access role on a share link', async () => { +describe('SharingDialogComponent - Share Link Test', () => { + let component: SharingDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { MockShareLinksApiService.reset(); - // We have to use another describe() here since we're creating a component with a - // different set up, and these unit tests (and Angular's testing utilities in general) - // only expect there to be one TestBed that you use per suite of unit tests. - @NgModule({ + + await TestBed.configureTestingModule({ + declarations: [SharingDialogComponent], imports: [FormsModule, CommonModule, ReactiveFormsModule], - }) - class ShallowTestingModule {} - - const shallow = new Shallow( - SharingDialogComponent, - ShallowTestingModule, - ) - .mock(AccountService, new MockAccountService()) - .mock(ApiService, new MockShareLinksApiService()) - .mock(ShareLinksApiService, new MockShareLinksApiService()) - .mock(ShareLinkMappingService, new MockShareLinkMappingService()) - .mock(RelationshipService, new MockRelationshipService()) - .mock(GoogleAnalyticsService, new MockGoogleAnalyticsService()) - .mock(PromptService, new NullDependency()) - .mock(FeatureFlagService, new MockFeatureFlagService()) - .mock(Router, new NullDependency()) - .mock(DialogRef, new NullDependency()) - .mock(MessageService, new NullDependency()) - .mock(ActivatedRoute, new NullDependency()) - .mock(DIALOG_DATA, { - item: new RecordVO({ - recordId: '123', - displayName: 'Test File', - accessRole: 'access.role.owner', - }), - }); - const { instance } = await shallow.render(); - - await instance.generateShareLink(); - - expect(instance.newShareLink).toBeDefined(); - - instance.linkDefaultAccessRole = 'access.role.owner'; - await instance.onShareLinkPropChange( + providers: [ + { provide: AccountService, useClass: MockAccountService }, + { provide: ApiService, useClass: MockShareLinksApiService }, + { provide: ShareLinksApiService, useClass: MockShareLinksApiService }, + { + provide: ShareLinkMappingService, + useClass: MockShareLinkMappingService, + }, + { provide: RelationshipService, useClass: MockRelationshipService }, + { + provide: GoogleAnalyticsService, + useClass: MockGoogleAnalyticsService, + }, + { provide: PromptService, useClass: NullDependency }, + { provide: FeatureFlagService, useClass: MockFeatureFlagService }, + { provide: Router, useClass: NullDependency }, + { provide: DialogRef, useClass: NullDependency }, + { provide: MessageService, useClass: NullDependency }, + { provide: ActivatedRoute, useClass: NullDependency }, + { + provide: DIALOG_DATA, + useValue: { + item: new RecordVO({ + recordId: '123', + displayName: 'Test File', + accessRole: 'access.role.owner', + }), + }, + }, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(SharingDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should be able to save default access role on a share link', async () => { + await component.generateShareLink(); + + expect(component.newShareLink).toBeDefined(); + + component.linkDefaultAccessRole = 'access.role.owner'; + await component.onShareLinkPropChange( 'defaultAccessRole', 'access.role.owner', ); - expect(instance.newShareLink.permissionsLevel).toBe('owner'); + expect(component.newShareLink.permissionsLevel).toBe('owner'); }); }); diff --git a/src/app/file-browser/components/sidebar/sidebar.component.spec.ts b/src/app/file-browser/components/sidebar/sidebar.component.spec.ts index 9c94b3609..1aed9d24a 100644 --- a/src/app/file-browser/components/sidebar/sidebar.component.spec.ts +++ b/src/app/file-browser/components/sidebar/sidebar.component.spec.ts @@ -1,13 +1,96 @@ -import { Shallow } from 'shallow-render'; -import { FileBrowserComponentsModule } from '@fileBrowser/file-browser-components.module'; +import { CUSTOM_ELEMENTS_SCHEMA, Pipe, PipeTransform } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { DataService } from '@shared/services/data/data.service'; import { EditService } from '@core/services/edit/edit.service'; import { AccountService } from '@shared/services/account/account.service'; import { ArchiveVO, RecordVO } from '@models/index'; import { of } from 'rxjs'; -import { RecordCastPipe } from '@shared/pipes/cast.pipe'; import { SidebarComponent } from './sidebar.component'; +@Pipe({ name: 'prTooltip', standalone: false }) +class MockPrTooltipPipe implements PipeTransform { + transform(value: any): any { + return value; + } +} + +@Pipe({ name: 'prConstants', standalone: false }) +class MockPrConstantsPipe implements PipeTransform { + transform(value: any): any { + return value; + } +} + +@Pipe({ name: 'getAltText', standalone: false }) +class MockGetAltTextPipe implements PipeTransform { + transform(value: any): string { + return value?.displayName || ''; + } +} + +@Pipe({ name: 'prDate', standalone: false }) +class MockPrDatePipe implements PipeTransform { + transform(value: any): string { + return value?.toString() || ''; + } +} + +@Pipe({ name: 'prLocation', standalone: false }) +class MockPrLocationPipe implements PipeTransform { + transform(value: any): string { + return value || ''; + } +} + +@Pipe({ name: 'asRecord', standalone: false }) +class MockAsRecordPipe implements PipeTransform { + transform(value: any): any { + return value; + } +} + +@Pipe({ name: 'asFolder', standalone: false }) +class MockAsFolderPipe implements PipeTransform { + transform(value: any): any { + return value; + } +} + +@Pipe({ name: 'dsFileSize', standalone: false }) +class MockDsFileSizePipe implements PipeTransform { + transform(value: any): string { + return value?.toString() || ''; + } +} + +@Pipe({ name: 'folderContents', standalone: false }) +class MockFolderContentsPipe implements PipeTransform { + transform(value: any): string { + return ''; + } +} + +@Pipe({ name: 'isPublicItem', standalone: false }) +class MockIsPublicItemPipe implements PipeTransform { + transform(value: any): boolean { + return false; + } +} + +@Pipe({ name: 'originalFileExtension', standalone: false }) +class MockOriginalFileExtensionPipe implements PipeTransform { + transform(value: any): string { + return ''; + } +} + +@Pipe({ name: 'selectedItem', standalone: false }) +class MockSelectedItemPipe implements PipeTransform { + transform(value: any): any { + return value; + } +} + const mockDataService = { selectedItems$: () => of( @@ -17,11 +100,14 @@ const mockDataService = { }), ]), ), - fetchFullItems: (_) => {}, + fetchFullItems: (_: any) => {}, + currentFolder: { + type: 'folder', + }, }; const mockEditService = { - openLocationDialog: (_) => {}, + openLocationDialog: (_: any) => {}, }; class MockAccountService { @@ -37,109 +123,121 @@ class MockAccountService { } describe('SidebarComponent', () => { - let shallow: Shallow; - - beforeEach(() => { - shallow = new Shallow(SidebarComponent, FileBrowserComponentsModule) - .provideMock({ - provide: DataService, - useValue: mockDataService, - }) - .provideMock({ - provide: EditService, - useValue: mockEditService, - }) - .provideMock({ - provide: AccountService, - useClass: MockAccountService, - }) - .dontMock(RecordCastPipe); + let component: SidebarComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ + SidebarComponent, + MockPrTooltipPipe, + MockPrConstantsPipe, + MockGetAltTextPipe, + MockPrDatePipe, + MockPrLocationPipe, + MockAsRecordPipe, + MockAsFolderPipe, + MockDsFileSizePipe, + MockFolderContentsPipe, + MockIsPublicItemPipe, + MockOriginalFileExtensionPipe, + MockSelectedItemPipe, + ], + providers: [ + { + provide: DataService, + useValue: mockDataService, + }, + { + provide: EditService, + useValue: mockEditService, + }, + { + provide: AccountService, + useClass: MockAccountService, + }, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(SidebarComponent); + component = fixture.componentInstance; + fixture.detectChanges(); }); - it('should create', async () => { - const { instance } = await shallow.render(); - - expect(instance).toBeTruthy(); + it('should create', () => { + expect(component).toBeTruthy(); }); - it('should open location dialog on Enter key press if editable', async () => { - const { instance } = await shallow.render(); - + it('should open location dialog on Enter key press if editable', () => { const locationDialogSpy = spyOn( mockEditService, 'openLocationDialog', ).and.callThrough(); - instance.onLocationEnterPress( + component.onLocationEnterPress( new KeyboardEvent('keydown', { key: 'Enter' }), ); - expect(locationDialogSpy).toHaveBeenCalledWith(instance.selectedItem); + expect(locationDialogSpy).toHaveBeenCalledWith(component.selectedItem); }); - it('should set currentTab correctly when setCurrentTab is called', async () => { - const { instance, fixture } = await shallow.render(); - - instance.setCurrentTab('info'); + it('should set currentTab correctly when setCurrentTab is called', () => { + component.setCurrentTab('info'); fixture.detectChanges(); - expect(instance.currentTab).toBe('info'); + expect(component.currentTab).toBe('info'); - instance.isRootFolder = false; - instance.isPublicItem = false; - instance.setCurrentTab('sharing'); + component.isRootFolder = false; + component.isPublicItem = false; + component.setCurrentTab('sharing'); fixture.detectChanges(); - expect(instance.currentTab).toBe('sharing'); + expect(component.currentTab).toBe('sharing'); }); - it('should call editService.openLocationDialog when onLocationClick is called if editable', async () => { - const { instance, inject } = await shallow.render(); - const editService = inject(EditService); + it('should call editService.openLocationDialog when onLocationClick is called if editable', () => { + const editService = TestBed.inject(EditService); spyOn(editService, 'openLocationDialog'); - instance.canEdit = true; - instance.selectedItem = new RecordVO({}); + component.canEdit = true; + component.selectedItem = new RecordVO({}); - instance.onLocationClick(); + component.onLocationClick(); expect(editService.openLocationDialog).toHaveBeenCalledWith( - instance.selectedItem, + component.selectedItem, ); }); - it('should correctly update canEdit and canShare when checkPermissions is called', async () => { - const { instance } = await shallow.render(); - - instance.selectedItem = new RecordVO({ + it('should correctly update canEdit and canShare when checkPermissions is called', () => { + component.selectedItem = new RecordVO({ accessRole: 'access.role.editor', }); - instance.selectedItems = [instance.selectedItem]; - instance.isRootFolder = false; - instance.isPublicItem = false; + component.selectedItems = [component.selectedItem]; + component.isRootFolder = false; + component.isPublicItem = false; - instance.checkPermissions(); + component.checkPermissions(); - expect(instance.canEdit).toBe(true); - expect(instance.canShare).toBe(true); + expect(component.canEdit).toBe(true); + expect(component.canShare).toBe(true); - instance.selectedItem = new RecordVO({ + component.selectedItem = new RecordVO({ accessRole: 'access.role.viewer', }); - instance.selectedItems = [instance.selectedItem]; - instance.isRootFolder = false; - instance.isPublicItem = false; + component.selectedItems = [component.selectedItem]; + component.isRootFolder = false; + component.isPublicItem = false; - instance.checkPermissions(); + component.checkPermissions(); - expect(instance.canEdit).toBe(false); - expect(instance.canShare).toBe(true); + expect(component.canEdit).toBe(false); + expect(component.canShare).toBe(true); }); - it('should hide the original format for folders', async () => { - const { instance, fixture } = await shallow.render(); - - instance.isRecord = false; + it('should hide the original format for folders', () => { + component.isRecord = false; fixture.detectChanges(); diff --git a/src/app/gallery/components/featured-archive/featured-archive.component.spec.ts b/src/app/gallery/components/featured-archive/featured-archive.component.spec.ts index 8d03c175b..9648fe804 100644 --- a/src/app/gallery/components/featured-archive/featured-archive.component.spec.ts +++ b/src/app/gallery/components/featured-archive/featured-archive.component.spec.ts @@ -1,9 +1,16 @@ -import { Shallow } from 'shallow-render'; +import { CUSTOM_ELEMENTS_SCHEMA, Pipe, PipeTransform } from '@angular/core'; +import { TestBed, ComponentFixture } from '@angular/core/testing'; import { ArchiveType } from '@models/archive-vo'; import { FeaturedArchive } from '../../types/featured-archive'; -import { GalleryModule } from '../../gallery.module'; import { FeaturedArchiveComponent } from './featured-archive.component'; +@Pipe({ name: 'archiveTypeName', standalone: false }) +class MockArchiveTypeNamePipe implements PipeTransform { + transform(value: string): string { + return value?.replace('type.archive.', '') || ''; + } +} + const testArchive: FeaturedArchive = { archiveNbr: '0000-0000', name: 'Unit Testing', @@ -13,47 +20,44 @@ const testArchive: FeaturedArchive = { }; describe('FeaturedArchiveComponent', () => { - let shallow: Shallow; + let fixture: ComponentFixture; + let component: FeaturedArchiveComponent; - const defaultRender = async () => - await shallow.render( - '', - { - bind: { - archive: testArchive, - }, - }, - ); + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [FeaturedArchiveComponent, MockArchiveTypeNamePipe], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); - beforeEach(() => { - shallow = new Shallow(FeaturedArchiveComponent, GalleryModule); + fixture = TestBed.createComponent(FeaturedArchiveComponent); + component = fixture.componentInstance; + component.archive = testArchive; + fixture.detectChanges(); }); - it('should exist', async () => { - const { instance } = await defaultRender(); - - expect(instance).toBeTruthy(); + it('should exist', () => { + expect(component).toBeTruthy(); }); - it('should include all archive information', async () => { - const { find, element } = await defaultRender(); + it('should include all archive information', () => { + const profilePicImg = + fixture.nativeElement.querySelector('.profile-pic img'); - expect(find('.profile-pic img').attributes.src).toBe('thumbUrl'); - expect(element.nativeElement.innerText).toContain( + expect(profilePicImg.src).toContain('thumbUrl'); + expect(fixture.nativeElement.innerText).toContain( 'The Unit Testing Archive', ); }); - it('should be able to get proper classnames', async () => { - const { instance } = await defaultRender(); + it('should be able to get proper classnames', () => { function expectClassnameForArchiveType( archiveType: ArchiveType, expectedClassname: string, ) { - instance.archive.type = archiveType; - instance.ngOnInit(); + component.archive.type = archiveType; + component.ngOnInit(); - expect(instance.classNames).toContain(expectedClassname); + expect(component.classNames).toContain(expectedClassname); } expectClassnameForArchiveType('type.archive.person', 'personal'); @@ -64,19 +68,17 @@ describe('FeaturedArchiveComponent', () => { }); describe('Accessibility', () => { - it('has alt text for all img tags', async () => { - const { find } = await defaultRender(); - const images = find('img'); - images.forEach(() => { - expect(images.attributes.alt).not.toBeUndefined(); + it('has alt text for all img tags', () => { + const images = fixture.nativeElement.querySelectorAll('img'); + images.forEach((img: HTMLImageElement) => { + expect(img.alt).toBeDefined(); }); }); - it('has specific label text for each link', async () => { - const { find } = await defaultRender(); - const link = find('a'); + it('has specific label text for each link', () => { + const link = fixture.nativeElement.querySelector('a'); - expect(link.attributes['aria-label']).toContain('Unit Testing'); + expect(link.getAttribute('aria-label')).toContain('Unit Testing'); }); }); }); diff --git a/src/app/gallery/components/gallery-header/gallery-header.component.spec.ts b/src/app/gallery/components/gallery-header/gallery-header.component.spec.ts index 778e46384..13615ed43 100644 --- a/src/app/gallery/components/gallery-header/gallery-header.component.spec.ts +++ b/src/app/gallery/components/gallery-header/gallery-header.component.spec.ts @@ -1,15 +1,15 @@ -import { Shallow } from 'shallow-render'; import { NgModule } from '@angular/core'; +import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; import { AccountService } from '@shared/services/account/account.service'; import { AccountDropdownComponent } from '@shared/components/account-dropdown/account-dropdown.component'; import { GalleryHeaderComponent } from './gallery-header.component'; @NgModule({ - declarations: [GalleryHeaderComponent, AccountDropdownComponent], // components your module owns. - imports: [], // other modules your module needs. - providers: [AccountService], // providers available to your module. - bootstrap: [], // bootstrap this root component. + declarations: [GalleryHeaderComponent, AccountDropdownComponent], + imports: [], + providers: [AccountService], + bootstrap: [], }) class DummyModule {} @@ -19,31 +19,29 @@ const accountMock = { }; describe('GalleryHeaderComponent', () => { - let shallow: Shallow; - - const defaultRender = async () => - await shallow.render('', { - bind: { - isLoggedIn, - }, - }); - beforeEach(() => { + beforeEach(async () => { isLoggedIn = true; - shallow = new Shallow(GalleryHeaderComponent, DummyModule).mock( - AccountService, - accountMock, - ); + await MockBuilder(GalleryHeaderComponent, DummyModule).provide({ + provide: AccountService, + useValue: accountMock, + }); }); - it('should render', async () => { - const { element } = await defaultRender(); + function defaultRender() { + return MockRender('', { + isLoggedIn, + }); + } + + it('should render', () => { + const fixture = defaultRender(); - expect(element).not.toBeNull(); + expect(fixture.point.nativeElement).not.toBeNull(); }); - it('should render back button when logged in', async () => { - const { find } = await defaultRender(); + it('should render back button when logged in', () => { + defaultRender(); - expect(find('.return-btn').length).toBeGreaterThan(0); + expect(ngMocks.findAll('.return-btn').length).toBeGreaterThan(0); }); }); diff --git a/src/app/gallery/components/gallery/gallery.component.spec.ts b/src/app/gallery/components/gallery/gallery.component.spec.ts index a10a576f5..440efac87 100644 --- a/src/app/gallery/components/gallery/gallery.component.spec.ts +++ b/src/app/gallery/components/gallery/gallery.component.spec.ts @@ -1,11 +1,12 @@ -import { Shallow } from 'shallow-render'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { TestBed, ComponentFixture } from '@angular/core/testing'; import { AccountService } from '@shared/services/account/account.service'; +import { EventService } from '@shared/services/event/event.service'; import { FeaturedArchive } from '../../types/featured-archive'; import { FEATURED_ARCHIVE_API, FeaturedArchiveApi, } from '../../types/featured-archive-api'; -import { GalleryModule } from '../../gallery.module'; import { GalleryComponent } from './gallery.component'; class DummyFeaturedArchiveAPI implements FeaturedArchiveApi { @@ -44,75 +45,120 @@ const testArchive: FeaturedArchive = { } as const; describe('GalleryComponent', () => { - let shallow: Shallow; + let fixture: ComponentFixture; + let component: GalleryComponent; let dummyApi: DummyFeaturedArchiveAPI; - let dummyAccount: DummyAccountService; beforeEach(async () => { DummyFeaturedArchiveAPI.reset(); DummyAccountService.loggedIn = false; dummyApi = new DummyFeaturedArchiveAPI(); - dummyAccount = new DummyAccountService(); - shallow = new Shallow(GalleryComponent, GalleryModule); - shallow - .provide({ - provide: FEATURED_ARCHIVE_API, - useValue: dummyApi, - }) - .provide({ - provide: AccountService, - useValue: dummyAccount, - }); - shallow.dontMock(FEATURED_ARCHIVE_API, AccountService); + + await TestBed.configureTestingModule({ + declarations: [GalleryComponent], + providers: [ + { + provide: FEATURED_ARCHIVE_API, + useValue: dummyApi, + }, + { + provide: AccountService, + useClass: DummyAccountService, + }, + { + provide: EventService, + useValue: { dispatch: () => {} }, + }, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(GalleryComponent); + component = fixture.componentInstance; }); it('should fetch featured archives from the API', async () => { - await shallow.render(); + fixture.detectChanges(); + await fixture.whenStable(); expect(dummyApi.fetchedFromApi).toBeTrue(); }); it('displays the list of featured archives', async () => { DummyFeaturedArchiveAPI.FeaturedArchives = [testArchive]; - const { fixture, find } = await shallow.render(); + fixture.detectChanges(); await fixture.whenStable(); + fixture.detectChanges(); + + const featuredArchives = fixture.nativeElement.querySelectorAll( + 'pr-featured-archive', + ); - expect(find('pr-featured-archive').length).toBe(1); + expect(featuredArchives.length).toBe(1); }); - it('does not display the error message while loading the archives', async () => { + it('does not display the error message while loading the archives', () => { DummyFeaturedArchiveAPI.FeaturedArchives = [testArchive]; - const { find, instance } = await shallow.render(); - instance.loading = true; + fixture.detectChanges(); + component.loading = true; + fixture.detectChanges(); - expect(find('.null-message').length).toBe(0); + const nullMessage = fixture.nativeElement.querySelector('.null-message'); + + expect(nullMessage).toBeFalsy(); }); it('displays an error message if no featured archives exist', async () => { - const { find } = await shallow.render(); + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); + + const featuredArchives = fixture.nativeElement.querySelectorAll( + 'pr-featured-archive', + ); + const nullMessage = fixture.nativeElement.querySelector('.null-message'); - expect(find('pr-featured-archive').length).toBe(0); - expect(find('.null-message').length).toBe(1); + expect(featuredArchives.length).toBe(0); + expect(nullMessage).toBeTruthy(); }); it('displays an error message if the fetch failed', async () => { DummyFeaturedArchiveAPI.FeaturedArchives = [testArchive]; DummyFeaturedArchiveAPI.failRequest = true; - const { find } = await shallow.render(); + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); + + const nullMessage = fixture.nativeElement.querySelector('.null-message'); - expect(find('.null-message').length).toBe(1); + expect(nullMessage).toBeTruthy(); }); it("does not display the user's public archives list if logged out", async () => { - const { find } = await shallow.render(); + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); - expect(find('pr-public-archives-list').length).toBe(0); + const publicArchivesList = fixture.nativeElement.querySelector( + 'pr-public-archives-list', + ); + + expect(publicArchivesList).toBeFalsy(); }); it("displays the user's public archives list if logged in", async () => { DummyAccountService.loggedIn = true; - const { find } = await shallow.render(); + // Need to recreate the component since isLoggedIn is called in constructor + fixture = TestBed.createComponent(GalleryComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); + + const publicArchivesList = fixture.nativeElement.querySelector( + 'pr-public-archives-list', + ); - expect(find('pr-public-archives-list').length).toBe(1); + expect(publicArchivesList).toBeTruthy(); }); }); diff --git a/src/app/gallery/components/public-archives-list/public-archives-list.component.spec.ts b/src/app/gallery/components/public-archives-list/public-archives-list.component.spec.ts index be489b231..d943b3601 100644 --- a/src/app/gallery/components/public-archives-list/public-archives-list.component.spec.ts +++ b/src/app/gallery/components/public-archives-list/public-archives-list.component.spec.ts @@ -1,87 +1,107 @@ -import { Shallow } from 'shallow-render'; +import { CUSTOM_ELEMENTS_SCHEMA, Pipe, PipeTransform } from '@angular/core'; +import { TestBed, ComponentFixture } from '@angular/core/testing'; import { AccountService } from '@shared/services/account/account.service'; import { MessageService } from '@shared/services/message/message.service'; import { Router } from '@angular/router'; import { ArchiveVO } from '../../../models/archive-vo'; -import { GalleryModule } from '../../gallery.module'; import { PublicArchivesListComponent } from './public-archives-list.component'; +@Pipe({ name: 'accessRole', standalone: false }) +class MockAccessRolePipe implements PipeTransform { + transform(value: string): string { + return value || ''; + } +} + const mockAccountService = { getAllPublicArchives: async () => await Promise.resolve([]), }; +const mockMessageService = { + showError: () => {}, +}; + +const mockRouter = { + navigate: jasmine + .createSpy('navigate') + .and.returnValue(Promise.resolve(true)), +}; + describe('PublicArchivesComponent', () => { - let shallow: Shallow; + let fixture: ComponentFixture; + let component: PublicArchivesListComponent; beforeEach(async () => { - shallow = new Shallow(PublicArchivesListComponent, GalleryModule) - .mock(AccountService, mockAccountService) - .mock(MessageService, { - showError: () => {}, - }) - .mock(Router, { - navigate: async () => await Promise.resolve(true), - }); + mockRouter.navigate.calls.reset(); + + await TestBed.configureTestingModule({ + declarations: [PublicArchivesListComponent, MockAccessRolePipe], + providers: [ + { provide: AccountService, useValue: mockAccountService }, + { provide: MessageService, useValue: mockMessageService }, + { provide: Router, useValue: mockRouter }, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(PublicArchivesListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); }); - it('should create', async () => { - const { instance } = await shallow.render(); - - expect(instance).toBeTruthy(); + it('should create', () => { + expect(component).toBeTruthy(); }); it('should show all the public archives of the user', async () => { - const { instance, fixture, find } = await shallow.render(); - instance.publicArchives = [ + await fixture.whenStable(); + component.publicArchives = [ new ArchiveVO({ archiveNbr: 1, name: 'test', public: 1 }), new ArchiveVO({ archiveNbr: 2, name: 'test2', public: 1 }), ]; fixture.detectChanges(); - const archives = find('.public-archive'); + const archives = fixture.nativeElement.querySelectorAll('.public-archive'); expect(archives.length).toEqual(2); }); it('should display the "no archives" element', async () => { - const { instance, fixture, find } = await shallow.render(); - instance.publicArchives = []; + await fixture.whenStable(); + component.publicArchives = []; fixture.detectChanges(); - const element = find('.no-archives'); + const element = fixture.nativeElement.querySelector('.no-archives'); - expect(element.nativeElement).toBeTruthy(); + expect(element).toBeTruthy(); }); - it('should redirect the user to the archive when clicking on it', async () => { - const { instance, fixture, inject } = await shallow.render(); - - const router = inject(Router); + it('should redirect the user to the archive when clicking on it', () => { const archive = new ArchiveVO({ archiveNbr: 1, name: 'test', public: 1 }); - instance.goToArchive(archive); + component.goToArchive(archive); fixture.detectChanges(); - expect(router.navigate).toHaveBeenCalledWith([ + expect(mockRouter.navigate).toHaveBeenCalledWith([ '/p/archive', archive.archiveNbr, ]); }); it('should expand the archives list when clicking "See more" on mobile', async () => { - const { instance, fixture, find } = await shallow.render(); - instance.publicArchives = [ + await fixture.whenStable(); + component.publicArchives = [ new ArchiveVO({ archiveNbr: 1, name: 'test', public: 1 }), new ArchiveVO({ archiveNbr: 2, name: 'test2', public: 1 }), ]; - instance.toggleArchives(); + component.toggleArchives(); fixture.detectChanges(); - expect(instance.expanded).toBeTrue(); + expect(component.expanded).toBeTrue(); - const archiveList = find('.public-archives-list'); - - expect(archiveList.nativeElement).toHaveClass( - 'public-archives-list-expanded', + const archiveList = fixture.nativeElement.querySelector( + '.public-archives-list', ); + + expect(archiveList.classList).toContain('public-archives-list-expanded'); }); }); diff --git a/src/app/onboarding/components/archive-creation-with-share/archive-creation-with-share.component.spec.ts b/src/app/onboarding/components/archive-creation-with-share/archive-creation-with-share.component.spec.ts index 5c98fd230..381a49ae5 100644 --- a/src/app/onboarding/components/archive-creation-with-share/archive-creation-with-share.component.spec.ts +++ b/src/app/onboarding/components/archive-creation-with-share/archive-creation-with-share.component.spec.ts @@ -1,9 +1,8 @@ -import { Shallow } from 'shallow-render'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { TestBed, ComponentFixture } from '@angular/core/testing'; import { ApiService } from '@shared/services/api/api.service'; import { InviteVO, InviteVOData } from '@models/invite-vo'; import { InviteResponse } from '@shared/services/api/invite.repo'; -import { OnboardingModule } from '../../onboarding.module'; import { ArchiveCreationWithShareComponent } from './archive-creation-with-share.component'; class MockInviteApiResponse extends InviteResponse { @@ -38,27 +37,34 @@ class MockInviteRepo { } describe('ArchiveCreationWithShareToken', () => { - let shallow: Shallow; + let fixture: ComponentFixture; + let instance: ArchiveCreationWithShareComponent; let mockInvite: MockInviteRepo; function setLocalStorage(token: string) { spyOn(localStorage, 'getItem').and.returnValue(token); } - beforeEach(() => { + beforeEach(async () => { mockInvite = new MockInviteRepo(); - shallow = new Shallow(ArchiveCreationWithShareComponent, OnboardingModule) - .provideMock({ - provide: ApiService, - useValue: { invite: mockInvite }, - }) - .import(HttpClientTestingModule); + await TestBed.configureTestingModule({ + declarations: [ArchiveCreationWithShareComponent], + providers: [ + { + provide: ApiService, + useValue: { invite: mockInvite }, + }, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); }); it('should create', async () => { setLocalStorage(null); - const { instance } = await shallow.render(); + fixture = TestBed.createComponent(ArchiveCreationWithShareComponent); + instance = fixture.componentInstance; + fixture.detectChanges(); expect(instance).toBeTruthy(); }); @@ -70,7 +76,9 @@ describe('ArchiveCreationWithShareToken', () => { }; setLocalStorage('shareToken'); - const { instance, fixture } = await shallow.render(); + fixture = TestBed.createComponent(ArchiveCreationWithShareComponent); + instance = fixture.componentInstance; + fixture.detectChanges(); instance.ngOnInit(); await fixture.whenStable(); @@ -82,7 +90,9 @@ describe('ArchiveCreationWithShareToken', () => { it('should not fetch invite data if no shareToken is present', async () => { setLocalStorage(null); - const { instance, fixture } = await shallow.render(); + fixture = TestBed.createComponent(ArchiveCreationWithShareComponent); + instance = fixture.componentInstance; + fixture.detectChanges(); instance.ngOnInit(); await fixture.whenStable(); @@ -99,7 +109,9 @@ describe('ArchiveCreationWithShareToken', () => { }; setLocalStorage('shareToken'); - const { instance, fixture } = await shallow.render(); + fixture = TestBed.createComponent(ArchiveCreationWithShareComponent); + instance = fixture.componentInstance; + fixture.detectChanges(); instance.ngOnInit(); await fixture.whenStable(); @@ -114,7 +126,9 @@ describe('ArchiveCreationWithShareToken', () => { }; setLocalStorage('shareToken'); - const { instance, fixture } = await shallow.render(); + fixture = TestBed.createComponent(ArchiveCreationWithShareComponent); + instance = fixture.componentInstance; + fixture.detectChanges(); instance.ngOnInit(); await fixture.whenStable(); @@ -123,80 +137,100 @@ describe('ArchiveCreationWithShareToken', () => { }); it('should fetch invite data and set sharer and shared item names when copyToken is present', async () => { - const mockApi = { - share: { - checkShareLink: jasmine.createSpy().and.returnValue( - Promise.resolve({ - Results: [ - { - data: [ - { - Shareby_urlVO: { - AccountVO: { fullName: 'Sharer Name' }, - RecordVO: { displayName: 'Shared Item Name' }, - }, + const mockShareApi = { + checkShareLink: jasmine.createSpy().and.returnValue( + Promise.resolve({ + Results: [ + { + data: [ + { + Shareby_urlVO: { + AccountVO: { fullName: 'Sharer Name' }, + RecordVO: { displayName: 'Shared Item Name' }, }, - ], - }, - ], - }), - ), - }, + }, + ], + }, + ], + }), + ), }; - const { instance, fixture } = await shallow - .mock(ApiService, mockApi) - .render(); + TestBed.resetTestingModule(); + await TestBed.configureTestingModule({ + declarations: [ArchiveCreationWithShareComponent], + providers: [ + { + provide: ApiService, + useValue: { invite: mockInvite, share: mockShareApi }, + }, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(ArchiveCreationWithShareComponent); + instance = fixture.componentInstance; spyOn(localStorage, 'getItem').and.callFake((key) => { if (key === 'shareTokenFromCopy') return 'copyToken'; return null; }); + fixture.detectChanges(); instance.ngOnInit(); await fixture.whenStable(); - expect(mockApi.share.checkShareLink).toHaveBeenCalledWith('copyToken'); + expect(mockShareApi.checkShareLink).toHaveBeenCalledWith('copyToken'); expect(instance.sharerName).toBe('Sharer Name'); expect(instance.sharedItemName).toBe('Shared Item Name'); }); it('should set sharedItemName using FolderVO if RecordVO is missing', async () => { - const mockApi = { - share: { - checkShareLink: jasmine.createSpy().and.returnValue( - Promise.resolve({ - Results: [ - { - data: [ - { - Shareby_urlVO: { - AccountVO: { fullName: 'Sharer Name' }, - FolderVO: { displayName: 'Shared Folder Name' }, - }, + const mockShareApi = { + checkShareLink: jasmine.createSpy().and.returnValue( + Promise.resolve({ + Results: [ + { + data: [ + { + Shareby_urlVO: { + AccountVO: { fullName: 'Sharer Name' }, + FolderVO: { displayName: 'Shared Folder Name' }, }, - ], - }, - ], - }), - ), - }, + }, + ], + }, + ], + }), + ), }; - const { instance, fixture } = await shallow - .mock(ApiService, mockApi) - .render(); + TestBed.resetTestingModule(); + await TestBed.configureTestingModule({ + declarations: [ArchiveCreationWithShareComponent], + providers: [ + { + provide: ApiService, + useValue: { invite: mockInvite, share: mockShareApi }, + }, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(ArchiveCreationWithShareComponent); + instance = fixture.componentInstance; spyOn(localStorage, 'getItem').and.callFake((key) => { if (key === 'shareTokenFromCopy') return 'copyToken'; return null; }); + fixture.detectChanges(); instance.ngOnInit(); await fixture.whenStable(); - expect(mockApi.share.checkShareLink).toHaveBeenCalledWith('copyToken'); + expect(mockShareApi.checkShareLink).toHaveBeenCalledWith('copyToken'); expect(instance.sharerName).toBe('Sharer Name'); expect(instance.sharedItemName).toBe('Shared Folder Name'); // Folder name should be set }); diff --git a/src/app/onboarding/components/create-new-archive/create-new-archive.component.spec.ts b/src/app/onboarding/components/create-new-archive/create-new-archive.component.spec.ts index 466d45ee2..dc954ddcd 100644 --- a/src/app/onboarding/components/create-new-archive/create-new-archive.component.spec.ts +++ b/src/app/onboarding/components/create-new-archive/create-new-archive.component.spec.ts @@ -1,6 +1,6 @@ +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { BehaviorSubject } from 'rxjs'; -import { Shallow } from 'shallow-render'; -import { OnboardingModule } from '@onboarding/onboarding.module'; import { ArchiveVO } from '@models/archive-vo'; import { ApiService } from '@shared/services/api/api.service'; import { AccountService } from '@shared/services/account/account.service'; @@ -36,83 +36,93 @@ const mockAccountService = { }), }; -let shallow: Shallow; - describe('CreateNewArchiveComponent #onboarding', () => { + let component: CreateNewArchiveComponent; + let fixture: ComponentFixture; let feature: FeatureFlagService; - beforeEach(() => { + + beforeEach(async () => { feature = new FeatureFlagService(undefined, new SecretsService()); calledAccept = false; acceptedArchive = null; - shallow = new Shallow(CreateNewArchiveComponent, OnboardingModule) - .mock(ApiService, mockApiService) - .mock(AccountService, mockAccountService) - .provide(EventService) - .provide({ provide: FeatureFlagService, useValue: feature }) - .dontMock(EventService) - .dontMock(OnboardingService) - .dontMock(FeatureFlagService); - }); - it('should exist', async () => { - const { element } = await shallow.render(); + await TestBed.configureTestingModule({ + declarations: [CreateNewArchiveComponent], + providers: [ + { provide: ApiService, useValue: mockApiService }, + { provide: AccountService, useValue: mockAccountService }, + EventService, + OnboardingService, + { provide: FeatureFlagService, useValue: feature }, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(CreateNewArchiveComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); - expect(element).not.toBeNull(); + it('should exist', () => { + expect(fixture.nativeElement).not.toBeNull(); }); - it('should emit a progress bar change event on mount', async () => { - const { outputs } = await shallow.render(); + it('should emit a progress bar change event on mount', () => { + spyOn(component.progressUpdated, 'emit'); + + // Recreate fixture to capture mount event + const testFixture = TestBed.createComponent(CreateNewArchiveComponent); + const testComponent = testFixture.componentInstance; + spyOn(testComponent.progressUpdated, 'emit'); + testFixture.detectChanges(); - expect(outputs.progressUpdated.emit).toHaveBeenCalledWith(0); + expect(testComponent.progressUpdated.emit).toHaveBeenCalledWith(0); }); - it('the next button should be disabled if no goals have been selected', async () => { - const { find, instance, fixture } = await shallow.render(); - instance.screen = 'goals'; - instance.selectedGoals = []; + it('the next button should be disabled if no goals have been selected', () => { + component.screen = 'goals'; + component.selectedGoals = []; fixture.detectChanges(); - const button = find('.goals-next').nativeElement; + const button = fixture.nativeElement.querySelector('.goals-next'); expect(button.disabled).toBe(true); }); - it('should show the reasons screen after selecting goals and then clicking next', async () => { - const { find, instance, fixture } = await shallow.render(); - instance.screen = 'goals'; - instance.selectedGoals = ['goal 1', 'goal 2']; + it('should show the reasons screen after selecting goals and then clicking next', () => { + component.screen = 'goals'; + component.selectedGoals = ['goal 1', 'goal 2']; fixture.detectChanges(); - find('.goals-next').triggerEventHandler('click', null); + const goalsNextButton = fixture.nativeElement.querySelector('.goals-next'); + goalsNextButton.click(); fixture.detectChanges(); - expect(instance.screen).toBe('reasons'); // Expecting the overlay to be present + expect(component.screen).toBe('reasons'); // Expecting the overlay to be present }); - it('the create archive button should not work without any reasons selected', async () => { - const { find, instance, fixture } = await shallow.render(); - instance.screen = 'reasons'; - instance.selectedReasons = []; + it('the create archive button should not work without any reasons selected', () => { + component.screen = 'reasons'; + component.selectedReasons = []; fixture.detectChanges(); - const button = find('.create-archive').nativeElement; + const button = fixture.nativeElement.querySelector('.create-archive'); expect(button.disabled).toBe(true); }); - it('should disable the Skip This Step and submit buttons when the archive has been submitted', async () => { - const { find, instance, fixture } = await shallow.render(); - instance.screen = 'reasons'; - instance.isArchiveSubmitted = true; + it('should disable the Skip This Step and submit buttons when the archive has been submitted', () => { + component.screen = 'reasons'; + component.isArchiveSubmitted = true; fixture.detectChanges(); - const submitButton = find('.create-archive').nativeElement; + const submitButton = fixture.nativeElement.querySelector('.create-archive'); - const skipStepButton = find('.skip-step').nativeElement; + const skipStepButton = fixture.nativeElement.querySelector('.skip-step'); expect(submitButton.disabled).toBe(true); expect(skipStepButton.disabled).toBe(true); @@ -120,19 +130,18 @@ describe('CreateNewArchiveComponent #onboarding', () => { it('should accept pending archives in the old flow', async () => { feature.set('glam-onboarding', false); - const { instance } = await shallow.render(); - instance.pendingArchive = new ArchiveVO({ archiveId: 1234 }); - await instance.onSubmit(); + component.pendingArchive = new ArchiveVO({ archiveId: 1234 }); + await component.onSubmit(); expect(calledAccept).toBeTrue(); expect(acceptedArchive.archiveId).toBe(1234); }); it('should not accept pending archives in the glam flow (they are already accepted in an earlier step)', async () => { - feature.set('glam-onboarding', true); - const { instance } = await shallow.render(); - instance.pendingArchive = new ArchiveVO({ archiveId: 1234 }); - await instance.onSubmit(); + // Feature flag is read in constructor, so we must set isGlam directly + component.isGlam = true; + component.pendingArchive = new ArchiveVO({ archiveId: 1234 }); + await component.onSubmit(); expect(calledAccept).toBeFalse(); expect(acceptedArchive).toBeNull(); diff --git a/src/app/onboarding/components/glam-pending-archives/glam-pending-archives.component.spec.ts b/src/app/onboarding/components/glam-pending-archives/glam-pending-archives.component.spec.ts index 1960aabe1..e32de28fd 100644 --- a/src/app/onboarding/components/glam-pending-archives/glam-pending-archives.component.spec.ts +++ b/src/app/onboarding/components/glam-pending-archives/glam-pending-archives.component.spec.ts @@ -1,8 +1,8 @@ -import { Shallow } from 'shallow-render'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { TestBed, ComponentFixture } from '@angular/core/testing'; import { AccountService } from '@shared/services/account/account.service'; import { ArchiveVO } from '@models/index'; import { ApiService } from '@shared/services/api/api.service'; -import { OnboardingModule } from '../../onboarding.module'; import { OnboardingService } from '../../services/onboarding.service'; import { GlamPendingArchivesComponent } from './glam-pending-archives.component'; @@ -13,30 +13,46 @@ const mockAccountService = { }; describe('GlamPendingArchivesComponent', () => { - let shallow: Shallow; + let fixture: ComponentFixture; + let instance: GlamPendingArchivesComponent; let onboardingService: OnboardingService; + let apiService: ApiService; beforeEach(async () => { onboardingService = new OnboardingService(); - shallow = new Shallow(GlamPendingArchivesComponent, OnboardingModule) - .mock(AccountService, mockAccountService) - .mock(ApiService, { - archive: { - accept: async (archive: ArchiveVO) => await Promise.resolve(), + + await TestBed.configureTestingModule({ + declarations: [GlamPendingArchivesComponent], + providers: [ + { provide: AccountService, useValue: mockAccountService }, + { + provide: ApiService, + useValue: { + archive: { + accept: async (archive: ArchiveVO) => await Promise.resolve(), + }, + }, }, - }) - .provide({ provide: OnboardingService, useValue: onboardingService }) - .dontMock(OnboardingService); + { provide: OnboardingService, useValue: onboardingService }, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + apiService = TestBed.inject(ApiService); }); it('should create the component', async () => { - const { instance } = await shallow.render(); + fixture = TestBed.createComponent(GlamPendingArchivesComponent); + instance = fixture.componentInstance; + fixture.detectChanges(); expect(instance).toBeTruthy(); }); it('should initialize accountName from AccountService', async () => { - const { instance } = await shallow.render(); + fixture = TestBed.createComponent(GlamPendingArchivesComponent); + instance = fixture.componentInstance; + fixture.detectChanges(); expect(instance.accountName).toBe('John Doe'); }); @@ -47,24 +63,34 @@ describe('GlamPendingArchivesComponent', () => { new ArchiveVO({ archiveId: 2, fullName: 'Archive 2' }), ]; - const { find } = await shallow.render({ - bind: { pendingArchives }, - }); + fixture = TestBed.createComponent(GlamPendingArchivesComponent); + instance = fixture.componentInstance; + instance.pendingArchives = pendingArchives; + fixture.detectChanges(); - const archiveElements = find('pr-pending-archive'); + const archiveElements = + fixture.nativeElement.querySelectorAll('pr-pending-archive'); expect(archiveElements.length).toBe(2); }); it('should emit createNewArchiveOutput when createNewArchive is called', async () => { - const { instance, outputs } = await shallow.render(); + fixture = TestBed.createComponent(GlamPendingArchivesComponent); + instance = fixture.componentInstance; + fixture.detectChanges(); + + spyOn(instance.createNewArchiveOutput, 'emit'); instance.createNewArchive(); - expect(outputs.createNewArchiveOutput.emit).toHaveBeenCalled(); + expect(instance.createNewArchiveOutput.emit).toHaveBeenCalled(); }); it('should emit nextOutput with selected archive when next is called', async () => { - const { instance, outputs } = await shallow.render(); + fixture = TestBed.createComponent(GlamPendingArchivesComponent); + instance = fixture.componentInstance; + fixture.detectChanges(); + + spyOn(instance.nextOutput, 'emit'); const selectedArchive: ArchiveVO = new ArchiveVO({ archiveId: 1, fullName: 'Test Archive', @@ -73,12 +99,14 @@ describe('GlamPendingArchivesComponent', () => { instance.selectedArchive = selectedArchive; instance.next(); - expect(outputs.nextOutput.emit).toHaveBeenCalledWith(selectedArchive); + expect(instance.nextOutput.emit).toHaveBeenCalledWith(selectedArchive); }); it('should call api.archive.accept when selectArchive is called', async () => { - const { instance, inject } = await shallow.render(); - const apiService = inject(ApiService); + fixture = TestBed.createComponent(GlamPendingArchivesComponent); + instance = fixture.componentInstance; + fixture.detectChanges(); + spyOn(apiService.archive, 'accept').and.callThrough(); const archive: ArchiveVO = new ArchiveVO({ @@ -92,7 +120,10 @@ describe('GlamPendingArchivesComponent', () => { }); it('should add archive to acceptedArchives when selectArchive is called', async () => { - const { instance } = await shallow.render(); + fixture = TestBed.createComponent(GlamPendingArchivesComponent); + instance = fixture.componentInstance; + fixture.detectChanges(); + const archive: ArchiveVO = new ArchiveVO({ archiveId: 1, fullName: 'Test Archive', @@ -105,7 +136,10 @@ describe('GlamPendingArchivesComponent', () => { }); it('should set selectedArchive if no archive was previously selected', async () => { - const { instance } = await shallow.render(); + fixture = TestBed.createComponent(GlamPendingArchivesComponent); + instance = fixture.componentInstance; + fixture.detectChanges(); + const archive: ArchiveVO = new ArchiveVO({ archiveId: 1, fullName: 'Test Archive', @@ -118,7 +152,10 @@ describe('GlamPendingArchivesComponent', () => { }); it('should return true when isSelected is called for an accepted archive', async () => { - const { instance } = await shallow.render(); + fixture = TestBed.createComponent(GlamPendingArchivesComponent); + instance = fixture.componentInstance; + fixture.detectChanges(); + const archive: ArchiveVO = new ArchiveVO({ archiveId: 1, fullName: 'Test Archive', @@ -130,13 +167,18 @@ describe('GlamPendingArchivesComponent', () => { }); it('should return false when isSelected is called for a non-accepted archive', async () => { - const { instance } = await shallow.render(); + fixture = TestBed.createComponent(GlamPendingArchivesComponent); + instance = fixture.componentInstance; + fixture.detectChanges(); expect(instance.isSelected(1)).toBeFalse(); }); it('should register accepted archives with the onboardingservice', async () => { - const { instance, inject } = await shallow.render(); + fixture = TestBed.createComponent(GlamPendingArchivesComponent); + instance = fixture.componentInstance; + fixture.detectChanges(); + const archives: ArchiveVO[] = [ new ArchiveVO({ archiveId: 1, @@ -155,7 +197,6 @@ describe('GlamPendingArchivesComponent', () => { await instance.selectArchive(archive); } instance.next(); - const onboardingService = inject(OnboardingService); expect(onboardingService.getFinalArchives().length).toBe(3); }); @@ -164,11 +205,10 @@ describe('GlamPendingArchivesComponent', () => { const archive = new ArchiveVO({ archiveId: 1, fullName: 'Archive 1' }); onboardingService.registerArchive(archive); - const { instance } = await shallow.render({ - bind: { - pendingArchives: [archive], - }, - }); + fixture = TestBed.createComponent(GlamPendingArchivesComponent); + instance = fixture.componentInstance; + instance.pendingArchives = [archive]; + fixture.detectChanges(); expect(instance.acceptedArchives.length).toBe(1); expect(instance.acceptedArchives[0].archiveId).toBe(archive.archiveId); diff --git a/src/app/onboarding/components/glam/archive-creation-start-screen/archive-creation-start-screen.component.spec.ts b/src/app/onboarding/components/glam/archive-creation-start-screen/archive-creation-start-screen.component.spec.ts index c5d7ba0f8..9331c10c3 100644 --- a/src/app/onboarding/components/glam/archive-creation-start-screen/archive-creation-start-screen.component.spec.ts +++ b/src/app/onboarding/components/glam/archive-creation-start-screen/archive-creation-start-screen.component.spec.ts @@ -1,8 +1,7 @@ -import { Shallow } from 'shallow-render'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { AccountService } from '@shared/services/account/account.service'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; -import { OnboardingModule } from '../../../onboarding.module'; +import { AccountService } from '@shared/services/account/account.service'; import { ArchiveCreationStartScreenComponent } from './archive-creation-start-screen.component'; const mockAccountService = { @@ -10,28 +9,30 @@ const mockAccountService = { }; describe('ArchiveCreationStartScreenComponent', () => { - let shallow: Shallow; - - beforeEach(() => { - shallow = new Shallow(ArchiveCreationStartScreenComponent, OnboardingModule) - .mock(AccountService, mockAccountService) - .import(HttpClientTestingModule); + let component: ArchiveCreationStartScreenComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ArchiveCreationStartScreenComponent], + providers: [{ provide: AccountService, useValue: mockAccountService }], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(ArchiveCreationStartScreenComponent); + component = fixture.componentInstance; + fixture.detectChanges(); }); - it('should create', async () => { - const { instance } = await shallow.render(); - - expect(instance).toBeTruthy(); + it('should create', () => { + expect(component).toBeTruthy(); }); - it('should initialize with the account name', async () => { - const { instance } = await shallow.render(); - - expect(instance.name).toBe('John Doe'); + it('should initialize with the account name', () => { + expect(component.name).toBe('John Doe'); }); - it('should render the account name in the greeting', async () => { - const { fixture } = await shallow.render(); + it('should render the account name in the greeting', () => { const greetingElement = fixture.debugElement.query( By.css('.greetings-container b'), ).nativeElement; @@ -39,49 +40,46 @@ describe('ArchiveCreationStartScreenComponent', () => { expect(greetingElement.textContent).toContain('John Doe'); }); - it('should emit getStartedOutput event when Get Started button is clicked', async () => { - const { fixture, instance, outputs } = await shallow.render(); - spyOn(instance, 'getStarted').and.callThrough(); + it('should emit getStartedOutput event when Get Started button is clicked', () => { + spyOn(component, 'getStarted').and.callThrough(); + spyOn(component.getStartedOutput, 'emit'); const getStartedButton = fixture.debugElement.query(By.css('.get-started')); getStartedButton.triggerEventHandler('buttonClick', null); - expect(instance.getStarted).toHaveBeenCalled(); - expect(outputs.getStartedOutput.emit).toHaveBeenCalled(); + expect(component.getStarted).toHaveBeenCalled(); + expect(component.getStartedOutput.emit).toHaveBeenCalled(); }); - it('should emit createArchiveForMeOutput event when Create Archive for Me button is clicked', async () => { - const { fixture, instance, outputs } = await shallow.render(); - spyOn(instance, 'createArchiveForMe').and.callThrough(); + it('should emit createArchiveForMeOutput event when Create Archive for Me button is clicked', () => { + spyOn(component, 'createArchiveForMe').and.callThrough(); + spyOn(component.createArchiveForMeOutput, 'emit'); const createArchiveButton = fixture.debugElement.query( By.css('.create-archive-for-me'), ); createArchiveButton.triggerEventHandler('buttonClick', null); - expect(instance.createArchiveForMe).toHaveBeenCalled(); - expect(outputs.createArchiveForMeOutput.emit).toHaveBeenCalled(); + expect(component.createArchiveForMe).toHaveBeenCalled(); + expect(component.createArchiveForMeOutput.emit).toHaveBeenCalled(); }); - it('should set hasToken to true if there is a token in the local storage', async () => { - const { instance, fixture } = await shallow.render(); - + it('should set hasToken to true if there is a token in the local storage', () => { spyOn(localStorage, 'getItem').and.returnValue('someToken'); - instance.ngOnInit(); + component.ngOnInit(); fixture.detectChanges(); - expect(instance.hasShareToken).toBeTrue(); + expect(component.hasShareToken).toBeTrue(); }); - it('should not set hasShareToken if shareToken does not exist in localStorage', async () => { + it('should not set hasShareToken if shareToken does not exist in localStorage', () => { spyOn(localStorage, 'getItem').and.returnValue(null); - const { instance, fixture } = await shallow.render(); - instance.ngOnInit(); + component.ngOnInit(); fixture.detectChanges(); - expect(instance.hasShareToken).toBeFalse(); + expect(component.hasShareToken).toBeFalse(); }); }); diff --git a/src/app/onboarding/components/glam/archive-type-select-dialog/archive-type-select-dialog.component.spec.ts b/src/app/onboarding/components/glam/archive-type-select-dialog/archive-type-select-dialog.component.spec.ts index 98ee0469f..b85935171 100644 --- a/src/app/onboarding/components/glam/archive-type-select-dialog/archive-type-select-dialog.component.spec.ts +++ b/src/app/onboarding/components/glam/archive-type-select-dialog/archive-type-select-dialog.component.spec.ts @@ -1,42 +1,48 @@ +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { DialogRef } from '@angular/cdk/dialog'; -import { Shallow } from 'shallow-render'; import { archiveOptions } from '../types/archive-types'; import { ArchiveTypeSelectDialogComponent } from './archive-type-select-dialog.component'; describe('ArchiveTypeSelectDialogComponent', () => { - let shallow: Shallow; - - beforeEach(() => { - shallow = new Shallow(ArchiveTypeSelectDialogComponent) - .provide({ - provide: DialogRef, - useValue: { - close() {}, - }, - }) - .dontMock(DialogRef); + let component: ArchiveTypeSelectDialogComponent; + let fixture: ComponentFixture; + let dialogRefSpy: jasmine.SpyObj; + + beforeEach(async () => { + dialogRefSpy = jasmine.createSpyObj('DialogRef', ['close']); + + await TestBed.configureTestingModule({ + imports: [ArchiveTypeSelectDialogComponent], + providers: [{ provide: DialogRef, useValue: dialogRefSpy }], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(ArchiveTypeSelectDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); }); - it('should create', async () => { - const { instance } = await shallow.render(); - - expect(instance).toBeTruthy(); + it('should create', () => { + expect(component).toBeTruthy(); }); - it('should have multiple options for archive types', async () => { - const { find } = await shallow.render(); + it('should have multiple options for archive types', () => { + const archiveTypeElements = + fixture.nativeElement.querySelectorAll('.archive-type'); - expect(find('.archive-type').length).toEqual(archiveOptions.length); + expect(archiveTypeElements.length).toEqual(archiveOptions.length); }); - it('should close the dialog and return type when clicking a type', async () => { - const { find, inject } = await shallow.render(); - const dialogRef = inject(DialogRef); - const close = spyOn(dialogRef, 'close'); - find('#type-myself').nativeElement.click(); - find('#type-org').nativeElement.click(); + it('should close the dialog and return type when clicking a type', () => { + const typeMyselfElement = + fixture.nativeElement.querySelector('#type-myself'); + const typeOrgElement = fixture.nativeElement.querySelector('#type-org'); + + typeMyselfElement.click(); + typeOrgElement.click(); - expect(close).toHaveBeenCalledWith('type:myself'); - expect(close).toHaveBeenCalledWith('type:org'); + expect(dialogRefSpy.close).toHaveBeenCalledWith('type:myself'); + expect(dialogRefSpy.close).toHaveBeenCalledWith('type:org'); }); }); diff --git a/src/app/onboarding/components/glam/archive-type-select/archive-type-select.component.spec.ts b/src/app/onboarding/components/glam/archive-type-select/archive-type-select.component.spec.ts index 668fd223a..5c3a9547f 100644 --- a/src/app/onboarding/components/glam/archive-type-select/archive-type-select.component.spec.ts +++ b/src/app/onboarding/components/glam/archive-type-select/archive-type-select.component.spec.ts @@ -1,4 +1,5 @@ -import { Shallow } from 'shallow-render'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { Subject } from 'rxjs'; import { DialogCdkService } from '@root/app/dialog-cdk/dialog-cdk.service'; import { OnboardingTypes } from '@root/app/onboarding/shared/onboarding-screen'; @@ -6,100 +7,100 @@ import { archiveDescriptions } from '../types/archive-types'; import { GlamArchiveTypeSelectComponent } from './archive-type-select.component'; describe('ArchiveTypeSelectComponent', () => { - let shallow: Shallow; + let component: GlamArchiveTypeSelectComponent; + let fixture: ComponentFixture; let dialogRef: Subject; + let mockDialogService: { open: jasmine.Spy }; - function expectCommunityDisplayed(find) { - expect(find('.type-name').nativeElement.innerText).toContain('Community'); - expect(find('.type-description').nativeElement.innerText).toContain( + function expectCommunityDisplayed() { + const typeNameElement = fixture.nativeElement.querySelector('.type-name'); + const typeDescriptionElement = + fixture.nativeElement.querySelector('.type-description'); + + expect(typeNameElement.innerText).toContain('Community'); + expect(typeDescriptionElement.innerText).toContain( archiveDescriptions['type:community'], ); } - beforeEach(() => { + beforeEach(async () => { if (dialogRef) { dialogRef.complete(); } dialogRef = new Subject(); - shallow = new Shallow(GlamArchiveTypeSelectComponent) - .provide({ - provide: DialogCdkService, - useValue: { - open() { - return { - closed: dialogRef, - }; - }, - }, - }) - .dontMock(DialogCdkService); + mockDialogService = { + open: jasmine.createSpy('open').and.returnValue({ + closed: dialogRef, + }), + }; + + await TestBed.configureTestingModule({ + imports: [GlamArchiveTypeSelectComponent], + providers: [{ provide: DialogCdkService, useValue: mockDialogService }], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(GlamArchiveTypeSelectComponent); + component = fixture.componentInstance; + fixture.detectChanges(); }); - it('should create', async () => { - const { instance } = await shallow.render(); - - expect(instance).toBeTruthy(); + it('should create', () => { + expect(component).toBeTruthy(); }); - it('should open a selection dialog on click', async () => { - const { instance, inject } = await shallow.render(); - const dialogService = inject(DialogCdkService); - const open = spyOn(dialogService, 'open').and.callThrough(); - instance.onClick(); + it('should open a selection dialog on click', () => { + component.onClick(); - expect(open).toHaveBeenCalled(); + expect(mockDialogService.open).toHaveBeenCalled(); }); - it('should change displayed archive type when the dialog returns with a type', async () => { - const { find, instance, fixture } = await shallow.render(); - instance.onClick(); + it('should change displayed archive type when the dialog returns with a type', () => { + component.onClick(); dialogRef.next(OnboardingTypes.community); fixture.detectChanges(); - expectCommunityDisplayed(find); + expectCommunityDisplayed(); }); - it('can change type multiple times', async () => { - const { find, instance, fixture } = await shallow.render(); - instance.onClick(); + it('can change type multiple times', () => { + component.onClick(); dialogRef.next(OnboardingTypes.famhist); - instance.onClick(); + component.onClick(); dialogRef.next(OnboardingTypes.community); fixture.detectChanges(); - expectCommunityDisplayed(find); + expectCommunityDisplayed(); }); - it('should not change the displayed archive type if the dialog is closed without any selection', async () => { - const { find, fixture, instance } = await shallow.render(); - instance.onClick(); + it('should not change the displayed archive type if the dialog is closed without any selection', () => { + component.onClick(); dialogRef.next(OnboardingTypes.community); - instance.onClick(); + component.onClick(); dialogRef.next(undefined); fixture.detectChanges(); - expectCommunityDisplayed(find); + expectCommunityDisplayed(); }); - it('handles an invalid onboardingtype', async () => { - const { find, fixture, instance } = await shallow.render(); - instance.onClick(); + it('handles an invalid onboardingtype', () => { + component.onClick(); dialogRef.next(OnboardingTypes.community); - instance.onClick(); + component.onClick(); dialogRef.next('not-valid-type' as OnboardingTypes); fixture.detectChanges(); - expectCommunityDisplayed(find); + expectCommunityDisplayed(); }); - it('emits the selected archive type', async () => { - const { outputs, instance } = await shallow.render(); - instance.onClick(); + it('emits the selected archive type', () => { + spyOn(component.typeSelected, 'emit'); + component.onClick(); dialogRef.next(OnboardingTypes.famhist); - expect(outputs.typeSelected.emit).toHaveBeenCalledWith( + expect(component.typeSelected.emit).toHaveBeenCalledWith( OnboardingTypes.famhist, ); }); diff --git a/src/app/onboarding/components/glam/create-archive-for-me-screen/create-archive-for-me-screen.component.spec.ts b/src/app/onboarding/components/glam/create-archive-for-me-screen/create-archive-for-me-screen.component.spec.ts index e191f88c6..ee3b01c83 100644 --- a/src/app/onboarding/components/glam/create-archive-for-me-screen/create-archive-for-me-screen.component.spec.ts +++ b/src/app/onboarding/components/glam/create-archive-for-me-screen/create-archive-for-me-screen.component.spec.ts @@ -1,7 +1,8 @@ -import { Shallow } from 'shallow-render'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; import { AccountService } from '@shared/services/account/account.service'; import { OnboardingService } from '@root/app/onboarding/services/onboarding.service'; -import { OnboardingModule } from '../../../onboarding.module'; import { CreateArchiveForMeScreenComponent } from './create-archive-for-me-screen.component'; const mockAccountService = { @@ -9,50 +10,55 @@ const mockAccountService = { }; describe('CreateArchiveForMeScreenComponent', () => { - let shallow: Shallow; + let component: CreateArchiveForMeScreenComponent; + let fixture: ComponentFixture; beforeEach(async () => { - shallow = new Shallow(CreateArchiveForMeScreenComponent, OnboardingModule) - .mock(AccountService, mockAccountService) - .provide(OnboardingService) - .dontMock(OnboardingService); - }); - - it('should create', async () => { - const { instance } = await shallow.render(); + await TestBed.configureTestingModule({ + declarations: [CreateArchiveForMeScreenComponent], + providers: [ + { provide: AccountService, useValue: mockAccountService }, + OnboardingService, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); - expect(instance).toBeTruthy(); + fixture = TestBed.createComponent(CreateArchiveForMeScreenComponent); + component = fixture.componentInstance; + fixture.detectChanges(); }); - it('should initialize name with the account full name', async () => { - const { instance } = await shallow.render(); + it('should create', () => { + expect(component).toBeTruthy(); + }); - expect(instance.name).toBe('John Doe'); + it('should initialize name with the account full name', () => { + expect(component.name).toBe('John Doe'); }); - it('should render the archive name in the template', async () => { - const { find } = await shallow.render(); - const archiveNameElement = find('.archive-name'); + it('should render the archive name in the template', () => { + const archiveNameElement = + fixture.nativeElement.querySelector('.archive-name'); - expect(archiveNameElement.nativeElement.textContent).toContain('John Doe'); + expect(archiveNameElement.textContent).toContain('John Doe'); }); - it('should emit goBackOutput when the Back button is clicked', async () => { - const { outputs, find } = await shallow.render(); - const backButton = find('.back'); + it('should emit goBackOutput when the Back button is clicked', () => { + spyOn(component.goBackOutput, 'emit'); + const backButton = fixture.debugElement.query(By.css('.back')); backButton.triggerEventHandler('buttonClick', null); - expect(outputs.goBackOutput.emit).toHaveBeenCalledWith('start'); + expect(component.goBackOutput.emit).toHaveBeenCalledWith('start'); }); - it('should emit continueOutput with correct payload when the Yes, create archive button is clicked', async () => { - const { outputs, find } = await shallow.render(); - const continueButton = find('.continue'); + it('should emit continueOutput with correct payload when the Yes, create archive button is clicked', () => { + spyOn(component.continueOutput, 'emit'); + const continueButton = fixture.debugElement.query(By.css('.continue')); continueButton.triggerEventHandler('buttonClick', null); - expect(outputs.continueOutput.emit).toHaveBeenCalledWith({ + expect(component.continueOutput.emit).toHaveBeenCalledWith({ screen: 'goals', type: 'type.archive.person', name: 'John Doe', diff --git a/src/app/onboarding/components/glam/finalize-archive-creation-screen/finalize-archive-creation-screen.component.spec.ts b/src/app/onboarding/components/glam/finalize-archive-creation-screen/finalize-archive-creation-screen.component.spec.ts index 4f0ab587e..9279f82fb 100644 --- a/src/app/onboarding/components/glam/finalize-archive-creation-screen/finalize-archive-creation-screen.component.spec.ts +++ b/src/app/onboarding/components/glam/finalize-archive-creation-screen/finalize-archive-creation-screen.component.spec.ts @@ -1,43 +1,50 @@ -import { Shallow } from 'shallow-render'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; import { OnboardingService } from '@root/app/onboarding/services/onboarding.service'; import { ArchiveVO } from '@models/index'; import { AccessRolePipe } from '@shared/pipes/access-role.pipe'; -import { OnboardingModule } from '../../../onboarding.module'; import { FinalizeArchiveCreationScreenComponent } from './finalize-archive-creation-screen.component'; describe('FinalizeArchiveCreationScreenComponent', () => { - let shallow: Shallow; + let component: FinalizeArchiveCreationScreenComponent; + let fixture: ComponentFixture; let onboardingService: OnboardingService; beforeEach(async () => { onboardingService = new OnboardingService(); - shallow = new Shallow( - FinalizeArchiveCreationScreenComponent, - OnboardingModule, - ) - .provide({ provide: OnboardingService, useValue: onboardingService }) - .dontMock(OnboardingService) - .dontMock(AccessRolePipe); - }); - it('should create', async () => { - const { instance } = await shallow.render(); + await TestBed.configureTestingModule({ + declarations: [FinalizeArchiveCreationScreenComponent, AccessRolePipe], + providers: [{ provide: OnboardingService, useValue: onboardingService }], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); - expect(instance).toBeTruthy(); + fixture = TestBed.createComponent(FinalizeArchiveCreationScreenComponent); + component = fixture.componentInstance; + fixture.detectChanges(); }); - it('should display the archive name correctly', async () => { + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should display the archive name correctly', () => { const name = 'John Doe'; onboardingService.registerArchive(new ArchiveVO({ fullName: name })); - const { find } = await shallow.render(); - const archiveNameElement = find('.archive-info p'); - expect(archiveNameElement.nativeElement.textContent).toContain( - `The ${name} Archive`, - ); + // Recreate the fixture to pick up the registered archive + fixture = TestBed.createComponent(FinalizeArchiveCreationScreenComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + const archiveNameElement = + fixture.nativeElement.querySelector('.archive-info p'); + + expect(archiveNameElement.textContent).toContain(`The ${name} Archive`); }); - it('it should display multiple archives with access roles correctly', async () => { + it('it should display multiple archives with access roles correctly', () => { onboardingService.registerArchive( new ArchiveVO({ fullName: 'Unit Test', accessRole: 'access.role.owner' }), ); @@ -54,40 +61,41 @@ describe('FinalizeArchiveCreationScreenComponent', () => { }), ); - const { find } = await shallow.render(); - const archiveNameElement = find('.archive-info .single-archive'); + // Recreate the fixture to pick up the registered archives + fixture = TestBed.createComponent(FinalizeArchiveCreationScreenComponent); + component = fixture.componentInstance; + fixture.detectChanges(); - expect(archiveNameElement.length).toBe(3); - expect(archiveNameElement[0].nativeElement.textContent).toContain( - 'Unit Test Archive', + const archiveNameElements = fixture.nativeElement.querySelectorAll( + '.archive-info .single-archive', ); - expect(archiveNameElement[0].nativeElement.textContent).toContain('Owner'); + expect(archiveNameElements.length).toBe(3); + expect(archiveNameElements[0].textContent).toContain('Unit Test Archive'); + expect(archiveNameElements[0].textContent).toContain('Owner'); }); - it('should emit finalizeArchiveOutput when finalizeArchive is called', async () => { - const { instance, outputs } = await shallow.render(); - instance.finalizeArchive(); + it('should emit finalizeArchiveOutput when finalizeArchive is called', () => { + spyOn(component.finalizeArchiveOutput, 'emit'); + component.finalizeArchive(); - expect(outputs.finalizeArchiveOutput.emit).toHaveBeenCalled(); + expect(component.finalizeArchiveOutput.emit).toHaveBeenCalled(); }); - it('should call finalizeArchive when the Done button is clicked', async () => { - const { instance, find } = await shallow.render(); - const doneButton = find('pr-button'); - spyOn(instance, 'finalizeArchive'); + it('should call finalizeArchive when the Done button is clicked', () => { + const doneButton = fixture.debugElement.query(By.css('pr-button')); + spyOn(component, 'finalizeArchive'); doneButton.triggerEventHandler('buttonClick', null); - expect(instance.finalizeArchive).toHaveBeenCalled(); + expect(component.finalizeArchive).toHaveBeenCalled(); }); - it('should disable the done button when it is clicked', async () => { - const { instance, fixture, find } = await shallow.render(); - const doneButton = find('pr-button'); + it('should disable the done button when it is clicked', () => { + const doneButton = fixture.debugElement.query(By.css('pr-button')); doneButton.triggerEventHandler('buttonClick', null); fixture.detectChanges(); - expect(instance.isArchiveSubmitted).toBe(true); + expect(component.isArchiveSubmitted).toBe(true); }); }); diff --git a/src/app/onboarding/components/glam/glam-goals-screen/glam-goals-screen.component.spec.ts b/src/app/onboarding/components/glam/glam-goals-screen/glam-goals-screen.component.spec.ts index 02a7e5f43..b65553d2f 100644 --- a/src/app/onboarding/components/glam/glam-goals-screen/glam-goals-screen.component.spec.ts +++ b/src/app/onboarding/components/glam/glam-goals-screen/glam-goals-screen.component.spec.ts @@ -1,48 +1,49 @@ -import { Shallow } from 'shallow-render'; -import { OnboardingModule } from '../../../onboarding.module'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { goals } from '../../../shared/onboarding-screen'; import { GlamGoalsScreenComponent } from './glam-goals-screen.component'; describe('GlamGoalsScreenComponent', () => { - let shallow: Shallow; + let component: GlamGoalsScreenComponent; + let fixture: ComponentFixture; beforeEach(async () => { - shallow = new Shallow(GlamGoalsScreenComponent, OnboardingModule); + await TestBed.configureTestingModule({ + declarations: [GlamGoalsScreenComponent], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); spyOn(sessionStorage, 'getItem').and.callFake((key) => { - const store = { + const store: { [key: string]: string } = { goals: JSON.stringify(['Mock Goal']), }; return store[key] || null; }); spyOn(sessionStorage, 'setItem').and.callFake((key, value) => {}); - }); - - it('should create', async () => { - const { instance } = await shallow.render(); - expect(instance).toBeTruthy(); + fixture = TestBed.createComponent(GlamGoalsScreenComponent); + component = fixture.componentInstance; + fixture.detectChanges(); }); - it('should initialize goals from shared/onboarding-screen', async () => { - const { instance } = await shallow.render(); - - expect(instance.goals).toEqual(goals); + it('should create', () => { + expect(component).toBeTruthy(); }); - it('should initialize selectedGoals from sessionStorage', async () => { - const { instance } = await shallow.render(); + it('should initialize goals from shared/onboarding-screen', () => { + expect(component.goals).toEqual(goals); + }); + it('should initialize selectedGoals from sessionStorage', () => { expect(sessionStorage.getItem).toHaveBeenCalledWith('goals'); - expect(instance.selectedGoals).toEqual(['Mock Goal']); + expect(component.selectedGoals).toEqual(['Mock Goal']); }); - it('should update sessionStorage when addGoal is called', async () => { - const { instance } = await shallow.render(); + it('should update sessionStorage when addGoal is called', () => { const goal = 'Test Goal'; - instance.addGoal(goal); + component.addGoal(goal); expect(sessionStorage.setItem).toHaveBeenCalledWith( 'goals', @@ -50,65 +51,61 @@ describe('GlamGoalsScreenComponent', () => { ); }); - it('should add goal to selectedGoals when addGoal is called', async () => { - const { instance } = await shallow.render(); + it('should add goal to selectedGoals when addGoal is called', () => { const goal = 'Test Goal'; - instance.addGoal(goal); + component.addGoal(goal); - expect(instance.selectedGoals).toContain(goal); + expect(component.selectedGoals).toContain(goal); }); - it('should remove goal from selectedGoals when addGoal is called twice', async () => { - const { instance } = await shallow.render(); + it('should remove goal from selectedGoals when addGoal is called twice', () => { const goal = 'Test Goal'; - instance.addGoal(goal); - instance.addGoal(goal); + component.addGoal(goal); + component.addGoal(goal); - expect(instance.selectedGoals).not.toContain(goal); + expect(component.selectedGoals).not.toContain(goal); }); - it('should emit backToNameArchiveOutput when backToNameArchive is called', async () => { - const { instance, outputs } = await shallow.render(); - instance.backToNameArchive(); + it('should emit backToNameArchiveOutput when backToNameArchive is called', () => { + spyOn(component.goalsOutput, 'emit'); + component.backToNameArchive(); - expect(outputs.goalsOutput.emit).toHaveBeenCalledWith({ + expect(component.goalsOutput.emit).toHaveBeenCalledWith({ screen: 'name-archive', goals: ['Mock Goal'], }); }); - it('should emit goToNextReasonsOutput with selectedGoals when goToNextReasons is called', async () => { - const { instance, outputs } = await shallow.render(); + it('should emit goToNextReasonsOutput with selectedGoals when goToNextReasons is called', () => { + spyOn(component.goalsOutput, 'emit'); const goal = 'Test Goal'; - instance.addGoal(goal); - instance.goToNextReasons(); + component.addGoal(goal); + component.goToNextReasons(); - expect(outputs.goalsOutput.emit).toHaveBeenCalledWith({ + expect(component.goalsOutput.emit).toHaveBeenCalledWith({ screen: 'reasons', goals: ['Mock Goal', goal], }); }); - it('should clear selectedGoals and update sessionStorage when skipStep is called', async () => { - const { instance } = await shallow.render(); - - expect(instance.selectedGoals).toEqual(['Mock Goal']); + it('should clear selectedGoals and update sessionStorage when skipStep is called', () => { + expect(component.selectedGoals).toEqual(['Mock Goal']); - instance.skipStep(); + component.skipStep(); - expect(instance.selectedGoals).toEqual([]); + expect(component.selectedGoals).toEqual([]); expect(sessionStorage.setItem).toHaveBeenCalledWith( 'goals', JSON.stringify([]), ); }); - it('should emit goalsOutput with empty goals when skipStep is called', async () => { - const { instance, outputs } = await shallow.render(); + it('should emit goalsOutput with empty goals when skipStep is called', () => { + spyOn(component.goalsOutput, 'emit'); - instance.skipStep(); + component.skipStep(); - expect(outputs.goalsOutput.emit).toHaveBeenCalledWith({ + expect(component.goalsOutput.emit).toHaveBeenCalledWith({ screen: 'reasons', goals: [], }); diff --git a/src/app/onboarding/components/glam/glam-header/glam-header.component.spec.ts b/src/app/onboarding/components/glam/glam-header/glam-header.component.spec.ts index 37de84482..ee1ad6ba6 100644 --- a/src/app/onboarding/components/glam/glam-header/glam-header.component.spec.ts +++ b/src/app/onboarding/components/glam/glam-header/glam-header.component.spec.ts @@ -1,35 +1,49 @@ -import { Shallow } from 'shallow-render'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { AccountService } from '@shared/services/account/account.service'; import { Router } from '@angular/router'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { OnboardingModule } from '../../../onboarding.module'; import { GlamOnboardingHeaderComponent } from './glam-header.component'; +const mockAccountService = { + clear: jasmine.createSpy('clear'), +}; + +const mockRouter = { + navigate: jasmine.createSpy('navigate').and.resolveTo(true), +}; + describe('GlamHeaderComponent', () => { - let shallow: Shallow; + let component: GlamOnboardingHeaderComponent; + let fixture: ComponentFixture; beforeEach(async () => { - shallow = new Shallow(GlamOnboardingHeaderComponent, OnboardingModule) - .import(HttpClientTestingModule) - .provideMock({ provide: AccountService, useValue: { clear: () => {} } }); - }); + mockAccountService.clear.calls.reset(); + mockRouter.navigate.calls.reset(); - it('should create', async () => { - const { instance } = await shallow.render(); + await TestBed.configureTestingModule({ + declarations: [GlamOnboardingHeaderComponent], + providers: [ + { provide: AccountService, useValue: mockAccountService }, + { provide: Router, useValue: mockRouter }, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); - expect(instance).toBeTruthy(); + fixture = TestBed.createComponent(GlamOnboardingHeaderComponent); + component = fixture.componentInstance; + fixture.detectChanges(); }); - it('can log out the user', async () => { - const { find, inject } = await shallow.render(); + it('should create', () => { + expect(component).toBeTruthy(); + }); - const accountClearSpy = spyOn(inject(AccountService), 'clear').and.callFake( - () => {}, - ); - const navigateSpy = spyOn(inject(Router), 'navigate').and.resolveTo(true); - find('.actions .log-out').triggerEventHandler('click'); + it('can log out the user', () => { + const logoutButton = + fixture.nativeElement.querySelector('.actions .log-out'); + logoutButton.click(); - expect(accountClearSpy).toHaveBeenCalled(); - expect(navigateSpy).toHaveBeenCalled(); + expect(mockAccountService.clear).toHaveBeenCalled(); + expect(mockRouter.navigate).toHaveBeenCalled(); }); }); diff --git a/src/app/onboarding/components/glam/glam-reasons-screen/glam-reasons-screen.component.spec.ts b/src/app/onboarding/components/glam/glam-reasons-screen/glam-reasons-screen.component.spec.ts index b05b3183a..17aefe38b 100644 --- a/src/app/onboarding/components/glam/glam-reasons-screen/glam-reasons-screen.component.spec.ts +++ b/src/app/onboarding/components/glam/glam-reasons-screen/glam-reasons-screen.component.spec.ts @@ -1,13 +1,17 @@ -import { Shallow } from 'shallow-render'; -import { OnboardingModule } from '../../../onboarding.module'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { reasons } from '../../../shared/onboarding-screen'; import { GlamReasonsScreenComponent } from './glam-reasons-screen.component'; describe('GlamReasonsScreenComponent', () => { - let shallow: Shallow; + let component: GlamReasonsScreenComponent; + let fixture: ComponentFixture; beforeEach(async () => { - shallow = new Shallow(GlamReasonsScreenComponent, OnboardingModule); + await TestBed.configureTestingModule({ + declarations: [GlamReasonsScreenComponent], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); spyOn(sessionStorage, 'getItem').and.callFake((key) => { const store = { @@ -17,32 +21,29 @@ describe('GlamReasonsScreenComponent', () => { }); spyOn(sessionStorage, 'setItem').and.callFake((key, value) => {}); - }); - - it('should create', async () => { - const { instance } = await shallow.render(); - expect(instance).toBeTruthy(); + fixture = TestBed.createComponent(GlamReasonsScreenComponent); + component = fixture.componentInstance; + fixture.detectChanges(); }); - it('should initialize reasons from shared/onboarding-screen', async () => { - const { instance } = await shallow.render(); - - expect(instance.reasons).toEqual(reasons); + it('should create', () => { + expect(component).toBeTruthy(); }); - it('should initialize selectedReasons from sessionStorage', async () => { - const { instance } = await shallow.render(); + it('should initialize reasons from shared/onboarding-screen', () => { + expect(component.reasons).toEqual(reasons); + }); + it('should initialize selectedReasons from sessionStorage', () => { expect(sessionStorage.getItem).toHaveBeenCalledWith('reasons'); - expect(instance.selectedReasons).toEqual(['Mock Reason']); + expect(component.selectedReasons).toEqual(['Mock Reason']); }); - it('should update sessionStorage when addReason is called', async () => { - const { instance } = await shallow.render(); + it('should update sessionStorage when addReason is called', () => { const reason = 'Test Reason'; - instance.addReason(reason); + component.addReason(reason); expect(sessionStorage.setItem).toHaveBeenCalledWith( 'reasons', @@ -50,67 +51,63 @@ describe('GlamReasonsScreenComponent', () => { ); }); - it('should add reason to selectedReasons when addReason is called', async () => { - const { instance } = await shallow.render(); + it('should add reason to selectedReasons when addReason is called', () => { const reason = 'Test Reason'; - instance.addReason(reason); + component.addReason(reason); - expect(instance.selectedReasons).toContain(reason); + expect(component.selectedReasons).toContain(reason); }); - it('should remove reason from selectedReasons when addReason is called twice', async () => { - const { instance } = await shallow.render(); + it('should remove reason from selectedReasons when addReason is called twice', () => { const reason = 'Test Reason'; - instance.addReason(reason); - instance.addReason(reason); + component.addReason(reason); + component.addReason(reason); - expect(instance.selectedReasons).not.toContain(reason); + expect(component.selectedReasons).not.toContain(reason); }); - it('should emit reasonsEmit with selectedReasons when finalizeArchive is called', async () => { - const { instance, outputs } = await shallow.render(); + it('should emit reasonsEmit with selectedReasons when finalizeArchive is called', () => { + spyOn(component.reasonsEmit, 'emit'); const reason = 'Test Reason'; - instance.addReason(reason); - instance.finalizeArchive(); + component.addReason(reason); + component.finalizeArchive(); - expect(outputs.reasonsEmit.emit).toHaveBeenCalledWith({ + expect(component.reasonsEmit.emit).toHaveBeenCalledWith({ screen: 'finalize', reasons: ['Mock Reason', reason], }); }); - it('should emit reasonsEmit with selectedReasons when backToGoals is called', async () => { - const { instance, outputs } = await shallow.render(); + it('should emit reasonsEmit with selectedReasons when backToGoals is called', () => { + spyOn(component.reasonsEmit, 'emit'); const reason = 'Test Reason'; - instance.addReason(reason); - instance.backToGoals(); + component.addReason(reason); + component.backToGoals(); - expect(outputs.reasonsEmit.emit).toHaveBeenCalledWith({ + expect(component.reasonsEmit.emit).toHaveBeenCalledWith({ screen: 'goals', reasons: ['Mock Reason', reason], }); }); - it('should clear selectedReasons and update sessionStorage when skipStep is called', async () => { - const { instance } = await shallow.render(); - - expect(instance.selectedReasons).toEqual(['Mock Reason']); + it('should clear selectedReasons and update sessionStorage when skipStep is called', () => { + expect(component.selectedReasons).toEqual(['Mock Reason']); - instance.skipStep(); + component.skipStep(); - expect(instance.selectedReasons).toEqual([]); + expect(component.selectedReasons).toEqual([]); expect(sessionStorage.setItem).toHaveBeenCalledWith( 'reasons', JSON.stringify([]), ); }); - it('should emit reasonOutput with empty reasons when skipStep is called', async () => { - const { instance, outputs } = await shallow.render(); + it('should emit reasonOutput with empty reasons when skipStep is called', () => { + spyOn(component.reasonsEmit, 'emit'); - instance.skipStep(); + component.skipStep(); - expect(outputs.reasonsEmit.emit).toHaveBeenCalledWith({ + expect(component.reasonsEmit.emit).toHaveBeenCalledWith({ screen: 'finalize', reasons: [], }); diff --git a/src/app/onboarding/components/glam/glam-user-survey-square/glam-user-survey-square.component.spec.ts b/src/app/onboarding/components/glam/glam-user-survey-square/glam-user-survey-square.component.spec.ts index ea936e4de..ab46d8468 100644 --- a/src/app/onboarding/components/glam/glam-user-survey-square/glam-user-survey-square.component.spec.ts +++ b/src/app/onboarding/components/glam/glam-user-survey-square/glam-user-survey-square.component.spec.ts @@ -1,51 +1,71 @@ -import { Shallow } from 'shallow-render'; -import { OnboardingModule } from '@root/app/onboarding/onboarding.module'; +import { CUSTOM_ELEMENTS_SCHEMA, Component, Input } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { GlamUserSurveySquareComponent } from './glam-user-survey-square.component'; +@Component({ + selector: 'pr-checkbox', + template: '', + standalone: false, +}) +class MockCheckboxComponent { + @Input() isChecked: boolean; + @Input() variant: string; + @Input() onboarding: boolean; +} + describe('GlamUserSurveySquareComponent', () => { - let shallow: Shallow; + let component: GlamUserSurveySquareComponent; + let fixture: ComponentFixture; beforeEach(async () => { - shallow = new Shallow(GlamUserSurveySquareComponent, OnboardingModule); - }); + await TestBed.configureTestingModule({ + declarations: [GlamUserSurveySquareComponent, MockCheckboxComponent], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); - it('should create', async () => { - const { instance } = await shallow.render(); + fixture = TestBed.createComponent(GlamUserSurveySquareComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); - expect(instance).toBeTruthy(); + it('should create', () => { + expect(component).toBeTruthy(); }); - it('should display the text correctly', async () => { + it('should display the text correctly', () => { const text = 'Test Text'; - const { find } = await shallow.render({ bind: { text } }); - const textElement = find('.text'); + component.text = text; + fixture.detectChanges(); + const textElement = fixture.nativeElement.querySelector('.text'); - expect(textElement.nativeElement.textContent).toContain(text); + expect(textElement.textContent).toContain(text); }); - it('should toggle selected state and emit selectedChange when clicked', async () => { + it('should toggle selected state and emit selectedChange when clicked', () => { const tag = 'test-tag'; - const { instance, outputs, find } = await shallow.render({ - bind: { tag }, - }); - const squareElement = find('.square'); - squareElement.triggerEventHandler('click', null); + component.tag = tag; + fixture.detectChanges(); - expect(instance.selected).toBeTrue(); - expect(outputs.selectedChange.emit).toHaveBeenCalledWith(tag); + spyOn(component.selectedChange, 'emit'); - squareElement.triggerEventHandler('click', null); + const squareElement = fixture.nativeElement.querySelector('.square'); + squareElement.click(); - expect(instance.selected).toBeFalse(); - expect(outputs.selectedChange.emit).toHaveBeenCalledWith(tag); + expect(component.selected).toBeTrue(); + expect(component.selectedChange.emit).toHaveBeenCalledWith(tag); + + squareElement.click(); + + expect(component.selected).toBeFalse(); + expect(component.selectedChange.emit).toHaveBeenCalledWith(tag); }); - it('should add selected class when selected is true', async () => { - const { find } = await shallow.render({ - bind: { selected: true }, - }); - const squareElement = find('.square'); + it('should add selected class when selected is true', () => { + component.selected = true; + fixture.detectChanges(); + + const squareElement = fixture.nativeElement.querySelector('.square'); - expect(squareElement.classes.selected).toBeTrue(); + expect(squareElement.classList.contains('selected')).toBeTrue(); }); }); diff --git a/src/app/onboarding/components/glam/name-archive-screen/name-archive-screen.component.spec.ts b/src/app/onboarding/components/glam/name-archive-screen/name-archive-screen.component.spec.ts index 7365be11e..21cbb67fe 100644 --- a/src/app/onboarding/components/glam/name-archive-screen/name-archive-screen.component.spec.ts +++ b/src/app/onboarding/components/glam/name-archive-screen/name-archive-screen.component.spec.ts @@ -1,19 +1,22 @@ -import { Shallow } from 'shallow-render'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { ReactiveFormsModule } from '@angular/forms'; import { OnboardingService } from '@root/app/onboarding/services/onboarding.service'; -import { OnboardingModule } from '../../../onboarding.module'; import { NameArchiveScreenComponent } from './name-archive-screen.component'; describe('NameArchiveScreenComponent', () => { - let shallow: Shallow; + let component: NameArchiveScreenComponent; + let fixture: ComponentFixture; const mockSessionStorage: { [key: string]: string } = {}; beforeEach(async () => { - shallow = new Shallow(NameArchiveScreenComponent, OnboardingModule) - .import(ReactiveFormsModule) - .provide(OnboardingService) - .dontMock(OnboardingService); + await TestBed.configureTestingModule({ + declarations: [NameArchiveScreenComponent], + imports: [ReactiveFormsModule], + providers: [OnboardingService], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); spyOn(sessionStorage, 'getItem').and.callFake( (key: string) => mockSessionStorage[key] || null, @@ -31,69 +34,71 @@ describe('NameArchiveScreenComponent', () => { Object.keys(mockSessionStorage).forEach( (key) => delete mockSessionStorage[key], ); - }); - - it('should create', async () => { - const { instance } = await shallow.render(); - expect(instance).toBeTruthy(); + fixture = TestBed.createComponent(NameArchiveScreenComponent); + component = fixture.componentInstance; + fixture.detectChanges(); }); - it('should initialize with default values', async () => { - const { instance } = await shallow.render(); + it('should create', () => { + expect(component).toBeTruthy(); + }); - expect(instance.nameForm).toBeTruthy(); - expect(instance.nameForm.controls.archiveName.value).toBe(''); + it('should initialize with default values', () => { + expect(component.nameForm).toBeTruthy(); + expect(component.nameForm.controls.archiveName.value).toBe(''); }); it('should patch the form value with input name on init', async () => { - const { instance } = await shallow.render({ - bind: { name: 'Test Archive' }, - }); - - expect(instance.nameForm.controls.archiveName.value).toBe('Test Archive'); + // Create a new fixture to test with different input + const testFixture = TestBed.createComponent(NameArchiveScreenComponent); + const testComponent = testFixture.componentInstance; + testComponent.name = 'Test Archive'; + testFixture.detectChanges(); + + expect(testComponent.nameForm.controls.archiveName.value).toBe( + 'Test Archive', + ); }); - it('should emit backToCreate event when backToCreate is called', async () => { - const { instance, outputs } = await shallow.render(); - instance.backToCreate(); + it('should emit backToCreate event when backToCreate is called', () => { + spyOn(component.backToCreateEmitter, 'emit'); + component.backToCreate(); - expect(outputs.backToCreateEmitter.emit).toHaveBeenCalledWith('create'); + expect(component.backToCreateEmitter.emit).toHaveBeenCalledWith('create'); }); - it('should emit archiveCreated event with form value when createArchive is called and form is valid', async () => { - const { instance, outputs } = await shallow.render(); - instance.nameForm.controls.archiveName.setValue('Valid Archive Name'); - instance.createArchive(); + it('should emit archiveCreated event with form value when createArchive is called and form is valid', () => { + spyOn(component.archiveCreatedEmitter, 'emit'); + component.nameForm.controls.archiveName.setValue('Valid Archive Name'); + component.createArchive(); - expect(outputs.archiveCreatedEmitter.emit).toHaveBeenCalledWith( + expect(component.archiveCreatedEmitter.emit).toHaveBeenCalledWith( 'Valid Archive Name', ); }); - it('should not emit archiveCreated event when createArchive is called and form is invalid', async () => { - const { instance, outputs } = await shallow.render(); - instance.nameForm.controls.archiveName.setValue(''); - instance.createArchive(); + it('should not emit archiveCreated event when createArchive is called and form is invalid', () => { + spyOn(component.archiveCreatedEmitter, 'emit'); + component.nameForm.controls.archiveName.setValue(''); + component.createArchive(); - expect(outputs.archiveCreatedEmitter.emit).not.toHaveBeenCalled(); + expect(component.archiveCreatedEmitter.emit).not.toHaveBeenCalled(); }); - it('should call backToCreate when Back button is clicked', async () => { - const { fixture, instance } = await shallow.render(); - spyOn(instance, 'backToCreate'); + it('should call backToCreate when Back button is clicked', () => { + spyOn(component, 'backToCreate'); const backButton = fixture.debugElement.query( By.css('.back-button-component'), ); backButton.triggerEventHandler('buttonClick', null); - expect(instance.backToCreate).toHaveBeenCalled(); + expect(component.backToCreate).toHaveBeenCalled(); }); - it('should call createArchive when create archive button is clicked', async () => { - const { fixture, instance } = await shallow.render(); - spyOn(instance, 'createArchive'); - instance.nameForm.controls.archiveName.setValue('Valid Archive Name'); + it('should call createArchive when create archive button is clicked', () => { + spyOn(component, 'createArchive'); + component.nameForm.controls.archiveName.setValue('Valid Archive Name'); fixture.detectChanges(); const createButton = fixture.debugElement.query( @@ -101,15 +106,14 @@ describe('NameArchiveScreenComponent', () => { ); createButton.triggerEventHandler('buttonClick', null); - expect(instance.createArchive).toHaveBeenCalled(); + expect(component.createArchive).toHaveBeenCalled(); }); - it('should call createArchive when create archive button is clicked and form is valid', async () => { - const { fixture, instance } = await shallow.render(); - instance.nameForm.controls.archiveName.setValue('Valid Archive Name'); + it('should call createArchive when create archive button is clicked and form is valid', () => { + component.nameForm.controls.archiveName.setValue('Valid Archive Name'); fixture.detectChanges(); - spyOn(instance, 'createArchive'); + spyOn(component, 'createArchive'); const createButton = fixture.debugElement.query( By.css('.create-archive-button'), @@ -117,21 +121,24 @@ describe('NameArchiveScreenComponent', () => { createButton.triggerEventHandler('buttonClick', null); - expect(instance.createArchive).toHaveBeenCalled(); + expect(component.createArchive).toHaveBeenCalled(); }); it('should initialize archiveName from sessionStorage if available', async () => { mockSessionStorage.archiveName = 'Stored Archive Name'; - const { instance } = await shallow.render(); - expect(instance.nameForm.controls.archiveName.value).toBe( + // Create a new fixture to test with session storage value + const testFixture = TestBed.createComponent(NameArchiveScreenComponent); + const testComponent = testFixture.componentInstance; + testFixture.detectChanges(); + + expect(testComponent.nameForm.controls.archiveName.value).toBe( 'Stored Archive Name', ); }); - it('should update sessionStorage when archiveName value changes', async () => { - const { instance } = await shallow.render(); - instance.nameForm.controls.archiveName.setValue('Updated Archive Name'); + it('should update sessionStorage when archiveName value changes', () => { + component.nameForm.controls.archiveName.setValue('Updated Archive Name'); expect(sessionStorage.setItem).toHaveBeenCalledWith( 'archiveName', diff --git a/src/app/onboarding/components/glam/pending-archive/pending-archive.component.spec.ts b/src/app/onboarding/components/glam/pending-archive/pending-archive.component.spec.ts index e84ba42e2..4ca39449e 100644 --- a/src/app/onboarding/components/glam/pending-archive/pending-archive.component.spec.ts +++ b/src/app/onboarding/components/glam/pending-archive/pending-archive.component.spec.ts @@ -1,97 +1,91 @@ -import { Shallow } from 'shallow-render'; -import { OnboardingModule } from '@root/app/onboarding/onboarding.module'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ArchiveVO } from '@models/index'; import { PendingArchiveComponent } from './pending-archive.component'; describe('PendingArchiveComponent', () => { - let shallow: Shallow; + let component: PendingArchiveComponent; + let fixture: ComponentFixture; beforeEach(async () => { - shallow = new Shallow(PendingArchiveComponent, OnboardingModule); + await TestBed.configureTestingModule({ + declarations: [PendingArchiveComponent], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(PendingArchiveComponent); + component = fixture.componentInstance; }); - it('should create', async () => { - const { instance } = await shallow.render({ - bind: { - archive: { fullName: 'John Doe' }, - }, - }); + it('should create', () => { + component.archive = { fullName: 'John Doe' } as ArchiveVO; + fixture.detectChanges(); - expect(instance).toBeTruthy(); + expect(component).toBeTruthy(); }); - it('should display the archive fullName', async () => { - const { find } = await shallow.render({ - bind: { - archive: { - fullName: 'Test Archive', - id: 1, - role: 'access.role.viewer', - }, - }, + it('should display the archive fullName', () => { + component.archive = new ArchiveVO({ + fullName: 'Test Archive', + archiveId: 1, + accessRole: 'access.role.viewer', }); + fixture.detectChanges(); - const fullNameElement = find('.name b'); + const fullNameElement = fixture.nativeElement.querySelector('.name b'); - expect(fullNameElement.nativeElement.textContent).toContain('Test Archive'); + expect(fullNameElement.textContent).toContain('Test Archive'); }); - it('should handle undefined archive input gracefully', async () => { - const { find } = await shallow.render({ - bind: { - archive: {}, - }, - }); + it('should handle undefined archive input gracefully', () => { + component.archive = {} as ArchiveVO; + fixture.detectChanges(); - const fullNameElement = find('.name b'); + const fullNameElement = fixture.nativeElement.querySelector('.name b'); - expect(fullNameElement.nativeElement.textContent).toBe(''); + expect(fullNameElement.textContent).toBe(''); }); - it('should emit acceptArchiveOutput event with the archive when acceptArchive is called', async () => { + it('should emit acceptArchiveOutput event with the archive when acceptArchive is called', () => { const archiveData = new ArchiveVO({ fullName: 'Test Archive', id: 1, role: 'access.role.viewer', }); - const { instance, outputs } = await shallow.render({ - bind: { - archive: archiveData, - }, - }); + component.archive = archiveData; + fixture.detectChanges(); - instance.acceptArchive(archiveData); + spyOn(component.acceptArchiveOutput, 'emit'); - expect(outputs.acceptArchiveOutput.emit).toHaveBeenCalledWith(archiveData); + component.acceptArchive(archiveData); + + expect(component.acceptArchiveOutput.emit).toHaveBeenCalledWith( + archiveData, + ); }); - it('should display the correct role name based on the role input', async () => { - const { find } = await shallow.render({ - bind: { - archive: { - fullName: 'Test Archive', - id: 1, - accessRole: 'access.role.editor', - }, - }, + it('should display the correct role name based on the role input', () => { + component.archive = new ArchiveVO({ + fullName: 'Test Archive', + archiveId: 1, + accessRole: 'access.role.editor', }); + fixture.detectChanges(); - const roleElement = find('.role'); + const roleElement = fixture.nativeElement.querySelector('.role'); - expect(roleElement.nativeElement.textContent).toContain('Editor'); + expect(roleElement.textContent).toContain('Editor'); }); - it('should map role key to role name correctly', async () => { - const { instance } = await shallow.render({ - bind: { - archive: { - fullName: 'Test Archive', - id: 1, - role: 'access.role.editor', - }, - }, + it('should map role key to role name correctly', () => { + component.archive = new ArchiveVO({ + fullName: 'Test Archive', + archiveId: 1, + accessRole: 'access.role.editor', }); - const roleName = instance.roles['access.role.contributor']; + fixture.detectChanges(); + + const roleName = component.roles['access.role.contributor']; expect(roleName).toBe('Contributor'); }); diff --git a/src/app/onboarding/components/glam/select-archive-type-screen/select-archive-type-screen.component.spec.ts b/src/app/onboarding/components/glam/select-archive-type-screen/select-archive-type-screen.component.spec.ts index 986b6f8d1..d6b3f01ac 100644 --- a/src/app/onboarding/components/glam/select-archive-type-screen/select-archive-type-screen.component.spec.ts +++ b/src/app/onboarding/components/glam/select-archive-type-screen/select-archive-type-screen.component.spec.ts @@ -1,39 +1,43 @@ -import { Shallow } from 'shallow-render'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; -import { OnboardingModule } from '../../../onboarding.module'; import { SelectArchiveTypeScreenComponent } from './select-archive-type-screen.component'; describe('SelectArchiveTypeScreenComponent', () => { - let shallow: Shallow; + let component: SelectArchiveTypeScreenComponent; + let fixture: ComponentFixture; beforeEach(async () => { - shallow = new Shallow(SelectArchiveTypeScreenComponent, OnboardingModule); - }); - - it('should create', async () => { - const { instance } = await shallow.render(); + await TestBed.configureTestingModule({ + declarations: [SelectArchiveTypeScreenComponent], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); - expect(instance).toBeTruthy(); + fixture = TestBed.createComponent(SelectArchiveTypeScreenComponent); + component = fixture.componentInstance; + fixture.detectChanges(); }); - it('should initialize with default values', async () => { - const { instance } = await shallow.render(); + it('should create', () => { + expect(component).toBeTruthy(); + }); - expect(instance.selectedValue).toBe(''); - expect(instance.buttonText).toBe('a Personal'); - expect(instance.headerText).toBe(''); - expect(instance.tag).toBe(''); - expect(instance.type).toBe(''); + it('should initialize with default values', () => { + expect(component.selectedValue).toBe(''); + expect(component.buttonText).toBe('a Personal'); + expect(component.headerText).toBe(''); + expect(component.tag).toBe(''); + expect(component.type).toBe(''); }); - it('should emit navigation event when navigate is called with start', async () => { - const { instance, outputs } = await shallow.render(); - instance.type = 'mockType'; - instance.tag = 'mockTag'; - instance.headerText = 'mockHeaderText'; - instance.navigate('start'); + it('should emit navigation event when navigate is called with start', () => { + spyOn(component.submitEmitter, 'emit'); + component.type = 'mockType'; + component.tag = 'mockTag'; + component.headerText = 'mockHeaderText'; + component.navigate('start'); - expect(outputs.submitEmitter.emit).toHaveBeenCalledWith({ + expect(component.submitEmitter.emit).toHaveBeenCalledWith({ screen: 'start', type: 'mockType', tag: 'mockTag', @@ -41,15 +45,15 @@ describe('SelectArchiveTypeScreenComponent', () => { }); }); - it('should emit submit event when navigate is called with other screen', async () => { - const { instance, outputs } = await shallow.render(); - instance.type = 'mockType'; - instance.tag = 'mockTag'; - instance.headerText = 'mockHeaderText'; + it('should emit submit event when navigate is called with other screen', () => { + spyOn(component.submitEmitter, 'emit'); + component.type = 'mockType'; + component.tag = 'mockTag'; + component.headerText = 'mockHeaderText'; - instance.navigate('name-archive'); + component.navigate('name-archive'); - expect(outputs.submitEmitter.emit).toHaveBeenCalledWith({ + expect(component.submitEmitter.emit).toHaveBeenCalledWith({ screen: 'name-archive', type: 'mockType', tag: 'mockTag', @@ -57,19 +61,17 @@ describe('SelectArchiveTypeScreenComponent', () => { }); }); - it('should call navigate with start when Back button is clicked', async () => { - const { fixture, instance } = await shallow.render(); - spyOn(instance, 'navigate'); + it('should call navigate with start when Back button is clicked', () => { + spyOn(component, 'navigate'); const backButton = fixture.debugElement.query(By.css('.back-button')); backButton.triggerEventHandler('buttonClick', null); - expect(instance.navigate).toHaveBeenCalledWith('start'); + expect(component.navigate).toHaveBeenCalledWith('start'); }); - it('should call navigate with name-archive when create archive button is clicked', async () => { - const { fixture, instance } = await shallow.render(); - spyOn(instance, 'navigate'); - instance.selectedValue = 'someValue'; + it('should call navigate with name-archive when create archive button is clicked', () => { + spyOn(component, 'navigate'); + component.selectedValue = 'someValue'; fixture.detectChanges(); const createButton = fixture.debugElement.query( @@ -77,30 +79,30 @@ describe('SelectArchiveTypeScreenComponent', () => { ); createButton.triggerEventHandler('buttonClick', null); - expect(instance.navigate).toHaveBeenCalledWith('name-archive'); + expect(component.navigate).toHaveBeenCalledWith('name-archive'); }); - it('should set buttonText correctly in ngOnInit if tag is defined', async () => { - const { instance } = await shallow.render({ - bind: { - tag: 'type:community', - }, - }); + it('should set buttonText correctly in ngOnInit if tag is defined', () => { + // Create a new fixture to test with tag input + const testFixture = TestBed.createComponent( + SelectArchiveTypeScreenComponent, + ); + const testComponent = testFixture.componentInstance; + testComponent.tag = 'type:community'; + testFixture.detectChanges(); - instance.ngOnInit(); + testComponent.ngOnInit(); - expect(instance.buttonText).toBe('a Community'); + expect(testComponent.buttonText).toBe('a Community'); }); - it('should not call generateElementText if tag is not defined', async () => { - const { instance, fixture } = await shallow.render(); - - instance.tag = ''; + it('should not call generateElementText if tag is not defined', () => { + component.tag = ''; fixture.detectChanges(); - instance.ngOnInit(); + component.ngOnInit(); - expect(instance.buttonText).toBe('a Personal'); // Default value + expect(component.buttonText).toBe('a Personal'); // Default value }); }); diff --git a/src/app/onboarding/components/header/header.component.spec.ts b/src/app/onboarding/components/header/header.component.spec.ts index 6b4e982f4..9ce47a0d5 100644 --- a/src/app/onboarding/components/header/header.component.spec.ts +++ b/src/app/onboarding/components/header/header.component.spec.ts @@ -1,49 +1,50 @@ -import { Shallow } from 'shallow-render'; +import { NgModule } from '@angular/core'; +import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; import { AccountService } from '@shared/services/account/account.service'; import { Router } from '@angular/router'; -import { OnboardingModule } from '../../onboarding.module'; import { OnboardingHeaderComponent } from './header.component'; +@NgModule() +class DummyModule {} + class AccountClearStub { public clear() {} } describe('OnboardingHeaderComponent', () => { - let shallow: Shallow; - - async function defaultRender(accountName: string = 'Unit Test') { - return await shallow.render({ - bind: { - accountName, - }, - }); - } - beforeEach(async () => { - shallow = new Shallow( - OnboardingHeaderComponent, - OnboardingModule, - ).provideMock({ provide: AccountService, useClass: AccountClearStub }); + await MockBuilder(OnboardingHeaderComponent, DummyModule).provide({ + provide: AccountService, + useClass: AccountClearStub, + }); }); - it('should create', async () => { - const { instance } = await defaultRender(); + it('should create', () => { + const fixture = MockRender(OnboardingHeaderComponent, { + accountName: 'Unit Test', + }); - expect(instance).toBeTruthy(); + expect(fixture.point.componentInstance).toBeTruthy(); }); - it("should display the user's name", async () => { - const { element } = await defaultRender(); + it("should display the user's name", () => { + const fixture = MockRender(OnboardingHeaderComponent, { + accountName: 'Unit Test', + }); - expect(element.nativeElement.innerText).toContain('Unit Test'); + expect(fixture.nativeElement.innerText).toContain('Unit Test'); }); - it('can log out the user', async () => { - const { find, inject } = await defaultRender(); + it('can log out the user', () => { + MockRender(OnboardingHeaderComponent, { + accountName: 'Unit Test', + }); - const accountClearSpy = spyOn(inject(AccountService), 'clear'); - const navigateSpy = spyOn(inject(Router), 'navigate'); - find('.banner-logout button').triggerEventHandler('click'); + const accountService = ngMocks.get(AccountService); + const router = ngMocks.get(Router); + const accountClearSpy = spyOn(accountService, 'clear'); + const navigateSpy = spyOn(router, 'navigate'); + ngMocks.find('.banner-logout button').triggerEventHandler('click', null); expect(accountClearSpy).toHaveBeenCalled(); expect(navigateSpy).toHaveBeenCalled(); diff --git a/src/app/onboarding/components/onboarding/onboarding.component.spec.ts b/src/app/onboarding/components/onboarding/onboarding.component.spec.ts index c92540e12..78bce13ae 100644 --- a/src/app/onboarding/components/onboarding/onboarding.component.spec.ts +++ b/src/app/onboarding/components/onboarding/onboarding.component.spec.ts @@ -1,16 +1,22 @@ -import { Shallow } from 'shallow-render'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { ngMocks, MockComponent } from 'ng-mocks'; import { Location } from '@angular/common'; -import { ActivatedRoute, RouterModule, Router } from '@angular/router'; -import { RouterTestingModule } from '@angular/router/testing'; +import { ActivatedRoute, Router } from '@angular/router'; import { ArchiveVO } from '@models/archive-vo'; import { AccountVO } from '@models/account-vo'; import { OnboardingScreen } from '@onboarding/shared/onboarding-screen'; import { AccountService } from '@shared/services/account/account.service'; import { ApiService } from '@shared/services/api/api.service'; import { MessageService } from '@shared/services/message/message.service'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; import { EventService } from '@shared/services/event/event.service'; -import { OnboardingModule } from '../../onboarding.module'; +import { MobileBannerComponent } from '@shared/components/mobile-banner/mobile-banner.component'; +import { PromptComponent } from '@shared/components/prompt/prompt.component'; +import { OnboardingHeaderComponent } from '../header/header.component'; +import { GlamOnboardingHeaderComponent } from '../glam/glam-header/glam-header.component'; +import { WelcomeScreenComponent } from '../welcome-screen/welcome-screen.component'; +import { GlamPendingArchivesComponent } from '../glam-pending-archives/glam-pending-archives.component'; +import { CreateNewArchiveComponent } from '../create-new-archive/create-new-archive.component'; import { OnboardingComponent } from './onboarding.component'; class NullRoute { @@ -55,60 +61,86 @@ const mockRouter = { }; describe('OnboardingComponent #onboarding', () => { - let shallow: Shallow; - beforeEach(() => { - shallow = new Shallow(OnboardingComponent, OnboardingModule) - .mock(ActivatedRoute, new NullRoute()) - .mock(Location, { go: (path: string) => {} }) - .mock(ApiService, mockApiService) - .mock(AccountService, mockAccountService) - .mock(Router, mockRouter) - .mock(MessageService, mockMessageService) - .provide(EventService) - .dontMock(EventService) - .replaceModule(RouterModule, RouterTestingModule); + beforeEach(async () => { + mockRouter.navigate.calls.reset(); + await TestBed.configureTestingModule({ + declarations: [ + OnboardingComponent, + MockComponent(OnboardingHeaderComponent), + MockComponent(GlamOnboardingHeaderComponent), + MockComponent(WelcomeScreenComponent), + MockComponent(GlamPendingArchivesComponent), + MockComponent(CreateNewArchiveComponent), + MockComponent(MobileBannerComponent), + MockComponent(PromptComponent), + ], + providers: [ + { provide: ActivatedRoute, useValue: new NullRoute() }, + { provide: Location, useValue: { go: (path: string) => {} } }, + { provide: ApiService, useValue: mockApiService }, + { provide: AccountService, useValue: mockAccountService }, + { provide: Router, useValue: mockRouter }, + { provide: MessageService, useValue: mockMessageService }, + EventService, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); }); it('should exist', async () => { - const { element } = await shallow.render(); + const fixture = TestBed.createComponent(OnboardingComponent); + fixture.detectChanges(); - expect(element).not.toBeNull(); + expect(fixture.nativeElement).not.toBeNull(); }); it('should load the create new archive screen as default', async () => { - const { find, fixture } = await shallow.render(); + const fixture = TestBed.createComponent(OnboardingComponent); + fixture.detectChanges(); + await fixture.whenStable(); fixture.detectChanges(); - expect(find('pr-create-new-archive')).toHaveFoundOne(); + expect( + fixture.nativeElement.querySelectorAll('pr-create-new-archive').length, + ).toBe(1); }); it('can change screens', async () => { - const { find, fixture } = await shallow.render(); - - expect(find('pr-create-new-archive')).toHaveFoundOne(); + const fixture = TestBed.createComponent(OnboardingComponent); fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); + + expect( + fixture.nativeElement.querySelectorAll('pr-create-new-archive').length, + ).toBe(1); - expect(find('pr-welcome-screen')).toHaveFound(0); + expect( + fixture.nativeElement.querySelectorAll('pr-welcome-screen').length, + ).toBe(0); }); it('stores the newly created archive', async () => { - const { element, find, fixture } = await shallow.render(); + const fixture = TestBed.createComponent(OnboardingComponent); + const instance = fixture.componentInstance; - expect(element.componentInstance.currentArchive).toBeUndefined(); + expect(instance.currentArchive).toBeUndefined(); + fixture.detectChanges(); + await fixture.whenStable(); fixture.detectChanges(); - const child = find('pr-create-new-archive'); + const child = ngMocks.find('pr-create-new-archive'); - expect(child).toHaveFoundOne(); + expect(child).toBeTruthy(); child.triggerEventHandler('createdArchive', new ArchiveVO({})); - expect(element.componentInstance.currentArchive).not.toBeUndefined(); + expect(instance.currentArchive).not.toBeUndefined(); }); it('stores an accepted archive invitation', async () => { const mockPendingArchive = new ArchiveVO({ status: 'someStatus-pending' }); - const mockAccountService = { + const localMockAccountService = { refreshArchives: jasmine .createSpy('refreshArchives') .and.returnValue(Promise.resolve([mockPendingArchive])), @@ -119,47 +151,73 @@ describe('OnboardingComponent #onboarding', () => { ), }; - const shallow = new Shallow(OnboardingComponent, OnboardingModule) - .mock(AccountService, mockAccountService) - .mock(ApiService, mockApiService) - .import(HttpClientTestingModule); - - const { instance, find, fixture, element } = await shallow.render(); + TestBed.resetTestingModule(); + await TestBed.configureTestingModule({ + declarations: [ + OnboardingComponent, + MockComponent(OnboardingHeaderComponent), + MockComponent(GlamOnboardingHeaderComponent), + MockComponent(WelcomeScreenComponent), + MockComponent(GlamPendingArchivesComponent), + MockComponent(CreateNewArchiveComponent), + MockComponent(MobileBannerComponent), + MockComponent(PromptComponent), + ], + providers: [ + { provide: AccountService, useValue: localMockAccountService }, + { provide: ApiService, useValue: mockApiService }, + { provide: ActivatedRoute, useValue: new NullRoute() }, + { provide: Location, useValue: { go: (path: string) => {} } }, + { provide: Router, useValue: mockRouter }, + { provide: MessageService, useValue: mockMessageService }, + EventService, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + const fixture = TestBed.createComponent(OnboardingComponent); + const instance = fixture.componentInstance; instance.ngOnInit(); + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); if (instance.pendingArchives.length > 0) { expect(instance.screen).toBe(OnboardingScreen.pendingArchives); } - expect(find('pr-welcome-screen')).toHaveFoundOne(); - find('pr-welcome-screen').triggerEventHandler( - 'selectInvitation', - new ArchiveVO({ fullName: 'Pending Test' }), - ); + expect( + fixture.nativeElement.querySelectorAll('pr-welcome-screen').length, + ).toBe(1); + ngMocks + .find('pr-welcome-screen') + .triggerEventHandler( + 'selectInvitation', + new ArchiveVO({ fullName: 'Pending Test' }), + ); fixture.detectChanges(); await fixture.whenStable(); - expect( - element.componentInstance.selectedPendingArchive, - ).not.toBeUndefined(); + expect(instance.selectedPendingArchive).not.toBeUndefined(); }); it('can be tested with debugging component', async () => { - const { element } = await shallow.render(); + const fixture = TestBed.createComponent(OnboardingComponent); + const instance = fixture.componentInstance; - expect(element.componentInstance.pendingArchives.length).toBe(0); - element.componentInstance.setState({ + expect(instance.pendingArchives.length).toBe(0); + instance.setState({ pendingArchives: [new ArchiveVO({})], }); - expect(element.componentInstance.pendingArchives.length).toBe(1); + expect(instance.pendingArchives.length).toBe(1); }); it('displays the pending archives screen when there are pending archives', async () => { const mockPendingArchive = new ArchiveVO({ status: 'someStatus-pending' }); - const mockAccountService = { + const localMockAccountService = { refreshArchives: jasmine .createSpy('refreshArchives') .and.returnValue(Promise.resolve([mockPendingArchive])), @@ -170,14 +228,35 @@ describe('OnboardingComponent #onboarding', () => { ), }; - const shallow = new Shallow(OnboardingComponent, OnboardingModule) - .mock(AccountService, mockAccountService) - .mock(ApiService, mockApiService) - .import(HttpClientTestingModule); - - const { instance } = await shallow.render(); + TestBed.resetTestingModule(); + await TestBed.configureTestingModule({ + declarations: [ + OnboardingComponent, + MockComponent(OnboardingHeaderComponent), + MockComponent(GlamOnboardingHeaderComponent), + MockComponent(WelcomeScreenComponent), + MockComponent(GlamPendingArchivesComponent), + MockComponent(CreateNewArchiveComponent), + MockComponent(MobileBannerComponent), + MockComponent(PromptComponent), + ], + providers: [ + { provide: AccountService, useValue: localMockAccountService }, + { provide: ApiService, useValue: mockApiService }, + { provide: ActivatedRoute, useValue: new NullRoute() }, + { provide: Location, useValue: { go: (path: string) => {} } }, + { provide: Router, useValue: mockRouter }, + { provide: MessageService, useValue: mockMessageService }, + EventService, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + const fixture = TestBed.createComponent(OnboardingComponent); + const instance = fixture.componentInstance; instance.ngOnInit(); + fixture.detectChanges(); if (instance.pendingArchives.length > 0) { expect(instance.screen).toBe(OnboardingScreen.pendingArchives); @@ -185,7 +264,8 @@ describe('OnboardingComponent #onboarding', () => { }); it('should remove shareToken from localStorage', async () => { - const { instance, fixture } = await shallow.render(); + const fixture = TestBed.createComponent(OnboardingComponent); + const instance = fixture.componentInstance; const getItemSpy = spyOn(localStorage, 'getItem').and.returnValue( 'someToken', @@ -204,7 +284,8 @@ describe('OnboardingComponent #onboarding', () => { }); it('should navigate to /app/welcome if shareToken is not in localStorage and isGlam is false', async () => { - const { instance, fixture } = await shallow.render(); + const fixture = TestBed.createComponent(OnboardingComponent); + const instance = fixture.componentInstance; spyOn(localStorage, 'getItem').and.returnValue(null); instance.isGlam = false; @@ -218,7 +299,8 @@ describe('OnboardingComponent #onboarding', () => { }); it('should navigate to /app if shareToken is not in localStorage and isGlam is true', async () => { - const { instance, fixture } = await shallow.render(); + const fixture = TestBed.createComponent(OnboardingComponent); + const instance = fixture.componentInstance; spyOn(localStorage, 'getItem').and.returnValue(null); instance.isGlam = true; @@ -232,7 +314,8 @@ describe('OnboardingComponent #onboarding', () => { }); it('should navigate to /app/welcome-invite if shareToken is not in localStorage and isGlam is true', async () => { - const { instance, fixture } = await shallow.render(); + const fixture = TestBed.createComponent(OnboardingComponent); + const instance = fixture.componentInstance; spyOn(localStorage, 'getItem').and.returnValue(null); instance.isGlam = false; diff --git a/src/app/onboarding/components/welcome-screen/welcome-screen.component.spec.ts b/src/app/onboarding/components/welcome-screen/welcome-screen.component.spec.ts index d25f0ce5d..8c98ad196 100644 --- a/src/app/onboarding/components/welcome-screen/welcome-screen.component.spec.ts +++ b/src/app/onboarding/components/welcome-screen/welcome-screen.component.spec.ts @@ -1,28 +1,25 @@ -import { Shallow } from 'shallow-render'; +import { NgModule } from '@angular/core'; +import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; import { ArchiveVO } from '@models/archive-vo'; -import { OnboardingModule } from '../../onboarding.module'; import { WelcomeScreenComponent } from './welcome-screen.component'; +@NgModule() +class DummyModule {} + describe('WelcomeScreenComponent #onboarding', () => { - let shallow: Shallow; async function defaultRender(pendingArchives: ArchiveVO[] = []) { - return await shallow.render( - ``, - { - bind: { - pendingArchives, - }, - }, - ); + return MockRender(WelcomeScreenComponent, { + pendingArchives, + }); } - beforeEach(() => { - shallow = new Shallow(WelcomeScreenComponent, OnboardingModule); + beforeEach(async () => { + await MockBuilder(WelcomeScreenComponent, DummyModule); }); it('should exist', async () => { - const { find } = await defaultRender(); + await defaultRender(); - expect(find('.welcome-screen')).toHaveFoundOne(); + expect(ngMocks.find('.welcome-screen')).toBeTruthy(); }); it('should display a list of pending archives if they are available', async () => { @@ -31,9 +28,9 @@ describe('WelcomeScreenComponent #onboarding', () => { fullName: 'Pending Test', }), ]; - const { find } = await defaultRender(pendingArchives); + await defaultRender(pendingArchives); - expect(find('pr-archive-small')).toHaveFoundOne(); + expect(ngMocks.findAll('pr-archive-small').length).toBe(1); }); it('should pass up a selected archive', async () => { @@ -42,11 +39,14 @@ describe('WelcomeScreenComponent #onboarding', () => { fullName: 'Pending Test', }), ]; - const { element, outputs } = await defaultRender(pendingArchives); - element.componentInstance.selectPendingArchive(pendingArchives[0]); - - expect(outputs.selectInvitation.emit).toHaveBeenCalledWith( - pendingArchives[0], + const fixture = await defaultRender(pendingArchives); + const instance = fixture.point.componentInstance; + const selectInvitationSpy = spyOn( + fixture.point.componentInstance.selectInvitation, + 'emit', ); + instance.selectPendingArchive(pendingArchives[0]); + + expect(selectInvitationSpy).toHaveBeenCalledWith(pendingArchives[0]); }); }); diff --git a/src/app/pledge/components/new-pledge/new-pledge.component.spec.ts b/src/app/pledge/components/new-pledge/new-pledge.component.spec.ts index 32dc8f2fb..945030f12 100644 --- a/src/app/pledge/components/new-pledge/new-pledge.component.spec.ts +++ b/src/app/pledge/components/new-pledge/new-pledge.component.spec.ts @@ -1,15 +1,16 @@ -import { HttpClient } from '@angular/common/http'; -import { Shallow } from 'shallow-render'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { RouterTestingModule } from '@angular/router/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; import { BillingPaymentVO } from '@models/billing-payment-vo'; import { BillingResponse } from '@shared/services/api/billing.repo'; import { AccountService } from '@shared/services/account/account.service'; import { AccountVO } from '@models/account-vo'; import { MessageService } from '@shared/services/message/message.service'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; import { EventService } from '@shared/services/event/event.service'; import { PledgeService } from '../../services/pledge.service'; import { ApiService } from '../../../shared/services/api/api.service'; -import { PledgeModule } from '../../pledge.module'; import { NewPledgeComponent } from './new-pledge.component'; const mockPromoData = { @@ -56,32 +57,38 @@ const mockApiService = { }; describe('NewPledgeComponent', () => { - let shallow: Shallow; - - beforeEach(() => { - shallow = new Shallow(NewPledgeComponent, PledgeModule) - .provide(HttpClient) - .replaceModule(HttpClient, HttpClientTestingModule) - .dontMock(HttpClientTestingModule) - .mock(ApiService, mockApiService) - .mock(AccountService, mockAccountService) - .mock(PledgeService, mockPledgeService) - .mock(MessageService, { - showError: () => {}, - }) - .provide(EventService) - .dontMock(EventService); + let fixture: ComponentFixture; + let instance: NewPledgeComponent; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + HttpClientTestingModule, + ReactiveFormsModule, + RouterTestingModule, + ], + declarations: [NewPledgeComponent], + providers: [ + { provide: ApiService, useValue: mockApiService }, + { provide: AccountService, useValue: mockAccountService }, + { provide: PledgeService, useValue: mockPledgeService }, + { provide: MessageService, useValue: { showError: () => {} } }, + EventService, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(NewPledgeComponent); + instance = fixture.componentInstance; + fixture.detectChanges(); }); it('should exist', async () => { - const { element } = await shallow.render(); - - expect(element).not.toBeNull(); + expect(fixture.nativeElement).not.toBeNull(); }); it('should enable the button if the data is correct', async () => { - const { find, instance, fixture } = await shallow.render(); - const button = find('.btn-primary'); + const button = fixture.nativeElement.querySelector('.btn-primary'); instance.pledgeForm.patchValue({ email: 'test@example.com', @@ -95,12 +102,11 @@ describe('NewPledgeComponent', () => { fixture.detectChanges(); - expect(button.nativeElement.disabled).toBeFalsy(); + expect(button.disabled).toBeFalsy(); }); it('should disabled the button if there is card form is not complete', async () => { - const { find, instance, fixture } = await shallow.render(); - const button = find('.btn-primary'); + const button = fixture.nativeElement.querySelector('.btn-primary'); instance.pledgeForm.patchValue({ email: 'test@example.com', @@ -114,12 +120,11 @@ describe('NewPledgeComponent', () => { fixture.detectChanges(); - expect(button.nativeElement.disabled).toBeTruthy(); + expect(button.disabled).toBeTruthy(); }); it('should disabled the button if the email is invalid', async () => { - const { find, instance, fixture } = await shallow.render(); - const button = find('.btn-primary'); + const button = fixture.nativeElement.querySelector('.btn-primary'); instance.pledgeForm.patchValue({ email: 'test', @@ -133,12 +138,11 @@ describe('NewPledgeComponent', () => { fixture.detectChanges(); - expect(button.nativeElement.disabled).toBeTruthy(); + expect(button.disabled).toBeTruthy(); }); it('should disabled the button if no name was provided', async () => { - const { find, instance, fixture } = await shallow.render(); - const button = find('.btn-primary'); + const button = fixture.nativeElement.querySelector('.btn-primary'); instance.pledgeForm.patchValue({ email: 'test@mail.com', @@ -151,50 +155,46 @@ describe('NewPledgeComponent', () => { fixture.detectChanges(); - expect(button.nativeElement.disabled).toBeTruthy(); + expect(button.disabled).toBeTruthy(); }); it('should set the correct amount when clicking on a button', async () => { - const { find, instance } = await shallow.render(); - - const buttons = find('.pledge-button'); + const buttons = fixture.nativeElement.querySelectorAll('.pledge-button'); expect(buttons.length).toBe(4); - buttons[1].triggerEventHandler('click', null); + buttons[1].click(); expect(instance.donationAmount).toBe(20); }); it('should select the custom value for the last input when clicked on it', async () => { - const { find, instance } = await shallow.render(); - - const buttons = find('.pledge-button'); + const buttons = fixture.nativeElement.querySelectorAll('.pledge-button'); expect(buttons.length).toBe(4); - buttons[3].triggerEventHandler('click', null); + buttons[3].click(); expect(instance.donationSelection).toBe('custom'); }); it('should display the loading spinner', async () => { - const { find, instance } = await shallow.render(); - instance.waiting = true; + fixture.detectChanges(); - expect(find('pr-loading-spinner')).toBeTruthy(); + expect( + fixture.nativeElement.querySelector('pr-loading-spinner'), + ).toBeTruthy(); }); it('should display the succes message if the transaction is succesful', async () => { - const { find, instance, fixture } = await shallow.render(); - instance.isSuccessful = true; instance.amountInGb = 5; fixture.detectChanges(); - const displayedMessage = find('.success-message'); + const displayedMessage = + fixture.nativeElement.querySelector('.success-message'); expect(displayedMessage).toBeTruthy(); }); diff --git a/src/app/public/components/public-archive/public-archive.component.spec.ts b/src/app/public/components/public-archive/public-archive.component.spec.ts index 1f91e993f..63328f3be 100644 --- a/src/app/public/components/public-archive/public-archive.component.spec.ts +++ b/src/app/public/components/public-archive/public-archive.component.spec.ts @@ -1,10 +1,10 @@ -import { waitForAsync } from '@angular/core/testing'; -import { PublicModule } from '@public/public.module'; -import { Shallow } from 'shallow-render'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { TestBed, ComponentFixture } from '@angular/core/testing'; import { of } from 'rxjs'; import { PublicProfileService } from '@public/services/public-profile/public-profile.service'; -import { Router } from '@angular/router'; +import { Router, ActivatedRoute } from '@angular/router'; import { ArchiveVO } from '@models/index'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { PublicArchiveComponent } from './public-archive.component'; const publicProfileServiceMock = { @@ -13,74 +13,83 @@ const publicProfileServiceMock = { profileItemsDictionary$: () => of({}), }; -describe('PublicArchiveComponent', () => { - let shallow: Shallow; - - beforeEach(waitForAsync(() => { - shallow = new Shallow(PublicArchiveComponent, PublicModule).provideMock({ - provide: PublicProfileService, - useValue: publicProfileServiceMock, - }); - })); +const mockRouter = { + events: of(), + url: '/test', + navigate: jasmine.createSpy('navigate'), +}; - it('should create', async () => { - const { instance } = await shallow.render(); +const mockActivatedRoute = {}; - expect(instance).toBeTruthy(); +describe('PublicArchiveComponent', () => { + let fixture: ComponentFixture; + let component: PublicArchiveComponent; + + beforeEach(async () => { + mockRouter.navigate.calls.reset(); + + await TestBed.configureTestingModule({ + imports: [BrowserAnimationsModule], + declarations: [PublicArchiveComponent], + providers: [ + { provide: PublicProfileService, useValue: publicProfileServiceMock }, + { provide: Router, useValue: mockRouter }, + { provide: ActivatedRoute, useValue: mockActivatedRoute }, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(PublicArchiveComponent); + component = fixture.componentInstance; + fixture.detectChanges(); }); - it('should have the information hidden as default', async () => { - const { instance } = await shallow.render(); + it('should create', () => { + expect(component).toBeTruthy(); + }); - expect(instance.showProfileInformation).toBe(false); + it('should have the information hidden as default', () => { + expect(component.showProfileInformation).toBe(false); }); - it('should have the information shown when the button is clicked', async () => { - const { instance, find } = await shallow.render(); - const icon = find('.icon-expand'); - icon.nativeElement.click(); + it('should have the information shown when the button is clicked', () => { + const icon = fixture.nativeElement.querySelector('.icon-expand'); + icon.click(); - expect(instance.showProfileInformation).toBe(true); + expect(component.showProfileInformation).toBe(true); }); - it('should give the correct classes when expanding the information', async () => { - const { find, fixture } = await shallow.render(); - const icon = find('.icon-expand'); - icon.nativeElement.click(); + it('should give the correct classes when expanding the information', () => { + const icon = fixture.nativeElement.querySelector('.icon-expand'); + icon.click(); fixture.detectChanges(); - expect(find('.archive-description').classes).toEqual({ - 'archive-description': true, - 'archive-description-show': true, - }); - }); + const archiveDescription = fixture.nativeElement.querySelector( + '.archive-description', + ); - it('should navigate to the correct search URL on handleSearch', async () => { - const { instance, inject } = await shallow.render(); - const router = inject(Router); - spyOn(router, 'navigate'); + expect(archiveDescription.classList).toContain('archive-description'); + expect(archiveDescription.classList).toContain('archive-description-show'); + }); - instance.archive = { archiveId: '123' } as any; + it('should navigate to the correct search URL on handleSearch', () => { + component.archive = { archiveId: '123' } as any; - instance.onHandleSearch('test-query'); + component.onHandleSearch('test-query'); - expect(router.navigate).toHaveBeenCalledWith( + expect(mockRouter.navigate).toHaveBeenCalledWith( ['search', '123', 'test-query'], jasmine.any(Object), ); }); - it('should navigate to the correct search-tag URL on tag click', async () => { - const { instance, inject } = await shallow.render(); - const router = inject(Router); - spyOn(router, 'navigate'); - - instance.archive = new ArchiveVO({ archiveId: '123' }); + it('should navigate to the correct search-tag URL on tag click', () => { + component.archive = new ArchiveVO({ archiveId: '123' }); - instance.onTagClick({ tagId: 'example-tag-id', tagName: 'tag-name' }); + component.onTagClick({ tagId: 'example-tag-id', tagName: 'tag-name' }); - expect(router.navigate).toHaveBeenCalledWith( + expect(mockRouter.navigate).toHaveBeenCalledWith( ['search-tag', '123', 'example-tag-id', 'tag-name'], jasmine.any(Object), ); diff --git a/src/app/public/components/search-box/search-box.component.spec.ts b/src/app/public/components/search-box/search-box.component.spec.ts index d038ae77a..6a73728d6 100644 --- a/src/app/public/components/search-box/search-box.component.spec.ts +++ b/src/app/public/components/search-box/search-box.component.spec.ts @@ -1,19 +1,41 @@ -import { Shallow } from 'shallow-render'; -import { PublicModule } from '@public/public.module'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { Router } from '@angular/router'; import { ApiService } from '@shared/services/api/api.service'; import { SearchBoxComponent } from './search-box.component'; +const mockApiService = { + search: { + archiveByNameObservable: () => {}, + }, +}; + +const mockRouter = { + navigate: jasmine.createSpy('navigate'), +}; + describe('SearchBoxComponent', () => { - let shallow: Shallow; + let fixture: ComponentFixture; + let component: SearchBoxComponent; - beforeEach(() => { - shallow = new Shallow(SearchBoxComponent, PublicModule); - shallow.mock(ApiService, {}); - }); + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ReactiveFormsModule], + declarations: [SearchBoxComponent], + providers: [ + { provide: ApiService, useValue: mockApiService }, + { provide: Router, useValue: mockRouter }, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); - it('should exist', async () => { - const { instance } = await shallow.render(); + fixture = TestBed.createComponent(SearchBoxComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); - expect(instance).toBeTruthy(); + it('should exist', () => { + expect(component).toBeTruthy(); }); }); diff --git a/src/app/shared/components/mobile-banner/mobile-banner.component.spec.ts b/src/app/shared/components/mobile-banner/mobile-banner.component.spec.ts index c6b72722e..9ae0cc80d 100644 --- a/src/app/shared/components/mobile-banner/mobile-banner.component.spec.ts +++ b/src/app/shared/components/mobile-banner/mobile-banner.component.spec.ts @@ -1,6 +1,7 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MockBuilder, ngMocks } from 'ng-mocks'; import { SharedModule } from '@shared/shared.module'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { Shallow } from 'shallow-render'; import { MobileBannerService } from '@shared/services/mobile-banner/mobile-banner.service'; import { MobileBannerComponent } from './mobile-banner.component'; @@ -21,24 +22,24 @@ const mockBannerService = { }; describe('MobileBannerComponent', () => { - let shallow: Shallow; + let fixture: ComponentFixture; + let instance: MobileBannerComponent; - beforeEach(() => { - shallow = new Shallow(MobileBannerComponent, SharedModule) + beforeEach(async () => { + await MockBuilder(MobileBannerComponent, SharedModule) .mock(MobileBannerService, mockBannerService) - .dontMock(NoopAnimationsModule) - .import(NoopAnimationsModule); - }); + .keep(NoopAnimationsModule, { export: true }); - it('should exist', async () => { - const { instance } = await shallow.render(); + fixture = TestBed.createComponent(MobileBannerComponent); + instance = fixture.componentInstance; + fixture.detectChanges(); + }); + it('should exist', () => { expect(instance).toBeTruthy(); }); - it('should initialize with correct visibility and URL', async () => { - const { instance } = await shallow.render(); - + it('should initialize with correct visibility and URL', () => { expect(instance.bannerService.isVisible).toBe(mockBannerService.isVisible); expect(instance.url).toBe( @@ -48,14 +49,13 @@ describe('MobileBannerComponent', () => { ); }); - it('should display the banner if visible', async () => { - const { find } = await shallow.render(); + it('should display the banner if visible', () => { + const banners = ngMocks.findAll('.banner'); - expect(find('.banner')).toHaveFoundOne(); + expect(banners).toHaveFoundOne(); }); - it('should use the correct URL based on platform', async () => { - const { instance } = await shallow.render(); + it('should use the correct URL based on platform', () => { if (instance.bannerService.isIos) { expect(instance.url).toBe(mockBannerService.appStoreUrl); } else { @@ -63,12 +63,12 @@ describe('MobileBannerComponent', () => { } }); - it('should close the banner when close icon is clicked', async () => { - spyOn(mockBannerService, 'hideBanner'); + it('should close the banner when close icon is clicked', () => { + spyOn(instance.bannerService, 'hideBanner'); const mockEvent = { stopPropagation: () => {} }; - const { find } = await shallow.render(); - find('.material-icons').triggerEventHandler('click', mockEvent); + const closeIcon = ngMocks.find('.material-icons'); + closeIcon.triggerEventHandler('click', mockEvent); - expect(mockBannerService.hideBanner).toHaveBeenCalled(); + expect(instance.bannerService.hideBanner).toHaveBeenCalled(); }); }); diff --git a/src/app/shared/components/new-archive-form/new-archive-form.component.spec.ts b/src/app/shared/components/new-archive-form/new-archive-form.component.spec.ts index 00b36a73f..d804f95e9 100644 --- a/src/app/shared/components/new-archive-form/new-archive-form.component.spec.ts +++ b/src/app/shared/components/new-archive-form/new-archive-form.component.spec.ts @@ -1,4 +1,6 @@ -import { Shallow } from 'shallow-render'; +import { TestBed } from '@angular/core/testing'; +import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; +import { FormsModule } from '@angular/forms'; import { SharedModule } from '@shared/shared.module'; import { ApiService } from '@shared/services/api/api.service'; import { @@ -26,112 +28,140 @@ const mockApiService = { }; describe('NewArchiveFormComponent #onboarding', () => { - let shallow: Shallow; - function fillOutForm(find: (a: string) => any) { - const input = find('#newArchiveName'); - input.nativeElement.value = 'Test User'; - input.triggerEventHandler('input', { target: input.nativeElement }); - const radio = find('input[type="radio"][required]'); - radio.nativeElement.click(); - } - beforeEach(() => { + let fixture; + let instance: NewArchiveFormComponent; + + beforeEach(async () => { created = false; createdArchive = null; throwError = false; - //I hate to do this but I don't have time to mock out the entire API service in a type-safe way. - //@ts-ignore - shallow = new Shallow(NewArchiveFormComponent, SharedModule).mock( - ApiService, - mockApiService, - ); - }); - it('should create', async () => { - const { element } = await shallow.render(); + await MockBuilder(NewArchiveFormComponent, SharedModule) + .keep(FormsModule) + .mock(ApiService, mockApiService as any); - expect(element).not.toBeNull(); + fixture = TestBed.createComponent(NewArchiveFormComponent); + instance = fixture.componentInstance; + fixture.detectChanges(); }); - it('should not submit when form is invalid', async () => { - const { find, outputs } = await shallow.render(); + it('should create', () => { + expect(fixture.debugElement).not.toBeNull(); + }); + + it('should not submit when form is invalid', () => { + spyOn(instance.success, 'emit'); + spyOn(instance.errorOccurred, 'emit'); - expect(find('button').nativeElement.disabled).toBeFalsy(); - find('button').nativeElement.click(); + expect(ngMocks.find('button').nativeElement.disabled).toBeFalsy(); + ngMocks.find('button').nativeElement.click(); - expect(outputs.success.emit).not.toHaveBeenCalled(); - expect(outputs.errorOccurred.emit).not.toHaveBeenCalled(); + expect(instance.success.emit).not.toHaveBeenCalled(); + expect(instance.errorOccurred.emit).not.toHaveBeenCalled(); }); - it('should disable button when form is waiting', async () => { - const { find, fixture } = await shallow.render(); - fillOutForm(find); - fixture.detectChanges(); - find('button').nativeElement.click(); + it('should disable button when form is waiting', () => { + instance.waiting = true; fixture.detectChanges(); - expect(find('button').nativeElement.disabled).toBeTruthy(); + expect(ngMocks.find('button').nativeElement.disabled).toBeTruthy(); }); it('should create a new archive on submit', async () => { - const { find, fixture } = await shallow.render(); - fillOutForm(find); - fixture.detectChanges(); - find('button').nativeElement.click(); + // Set form data directly + instance.formData = { + fullName: 'Test User', + type: 'type.archive.person', + }; + // Mock isFormValid to bypass native validation check + spyOn(instance, 'isFormValid').and.returnValue(true); fixture.detectChanges(); + // Call onSubmit directly + await instance.onSubmit(); expect(created).toBeTrue(); }); it('should output new archiveVO when submitted', async () => { - const { find, fixture, outputs } = await shallow.render(); - fillOutForm(find); + spyOn(instance.success, 'emit'); + spyOn(instance.errorOccurred, 'emit'); + instance.formData = { + fullName: 'Test User', + type: 'type.archive.person', + relationType: null, + }; + spyOn(instance, 'isFormValid').and.returnValue(true); fixture.detectChanges(); - find('button').nativeElement.click(); - await fixture.whenStable(); + await instance.onSubmit(); - expect(outputs.success.emit).toHaveBeenCalled(); + expect(instance.success.emit).toHaveBeenCalled(); expect(createdArchive.fullName).toBe('Test User'); expect(createdArchive.type).toBe('type.archive.person'); expect(createdArchive.relationType).toBeNull(); - expect(outputs.errorOccurred.emit).not.toHaveBeenCalled(); + expect(instance.errorOccurred.emit).not.toHaveBeenCalled(); }); it('should output errors if they occur', async () => { throwError = true; - const { find, fixture, outputs } = await shallow.render(); - fillOutForm(find); + spyOn(instance.success, 'emit'); + spyOn(instance.errorOccurred, 'emit'); + instance.formData = { + fullName: 'Test User', + type: 'type.archive.person', + }; + spyOn(instance, 'isFormValid').and.returnValue(true); fixture.detectChanges(); - find('button').nativeElement.click(); - await fixture.whenStable(); + await instance.onSubmit(); - expect(outputs.errorOccurred.emit).toHaveBeenCalled(); - expect(outputs.success.emit).not.toHaveBeenCalled(); + expect(instance.errorOccurred.emit).toHaveBeenCalled(); + expect(instance.success.emit).not.toHaveBeenCalled(); }); - it('should have an input that enables relations', async () => { - const { find, fixture } = await shallow.render( - '', + it('should have an input that enables relations', () => { + // Use MockRender for template-based tests + const renderFixture = MockRender( + ``, + { showRelations: true }, ); - fillOutForm(find); - fixture.detectChanges(); + const componentInstance = ngMocks.find(NewArchiveFormComponent) + .componentInstance as NewArchiveFormComponent; + renderFixture.detectChanges(); - expect(find('select[name="relation"]')).toHaveFoundOne(); - find('input[type="radio"]')[1].nativeElement.click(); - fixture.detectChanges(); + // Set form data to person type (shows relations) + componentInstance.formData = { + fullName: 'Test User', + type: 'type.archive.person', + }; + renderFixture.detectChanges(); - expect(find('select[name="relation"]')).not.toHaveFoundOne(); + expect(ngMocks.findAll('select[name="relation"]')).toHaveFoundOne(); + + // Change to group type (hides relations) + componentInstance.formData.type = 'type.archive.group'; + renderFixture.detectChanges(); + + expect(ngMocks.findAll('select[name="relation"]')).not.toHaveFoundOne(); }); it('should submit relationType to API if it is enabled', async () => { - const { element, find, fixture } = await shallow.render( - '', + // Use MockRender for template-based tests + const renderFixture = MockRender( + ``, + { showRelations: true }, ); - fillOutForm(find); - fixture.detectChanges(); - find('select').nativeElement.value = 'relation.other'; - element.componentInstance.formData.relationType = 'relation.other'; - find('button').nativeElement.click(); - await fixture.whenStable(); + const componentInstance = ngMocks.find(NewArchiveFormComponent) + .componentInstance as NewArchiveFormComponent; + renderFixture.detectChanges(); + + // Set form data directly + componentInstance.formData = { + fullName: 'Test User', + type: 'type.archive.person', + relationType: 'relation.other', + }; + spyOn(componentInstance, 'isFormValid').and.returnValue(true); + renderFixture.detectChanges(); + await componentInstance.onSubmit(); expect(createdArchive.relationType).toBe('relation.other'); }); diff --git a/src/app/shared/components/thumbnail/thumbnail.component.spec.ts b/src/app/shared/components/thumbnail/thumbnail.component.spec.ts index 1b118c865..c77755569 100644 --- a/src/app/shared/components/thumbnail/thumbnail.component.spec.ts +++ b/src/app/shared/components/thumbnail/thumbnail.component.spec.ts @@ -1,9 +1,18 @@ -import { Shallow } from 'shallow-render'; +import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { + ComponentFixture, + TestBed, + fakeAsync, + tick, +} from '@angular/core/testing'; import { ThumbnailComponent } from '@shared/components/thumbnail/thumbnail.component'; import { FolderVO, ItemVO, RecordVO } from '@models'; import { DataStatus } from '@models/data-status.enum'; -import { NgModule } from '@angular/core'; -import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; +import { + FontAwesomeModule, + FaIconLibrary, +} from '@fortawesome/angular-fontawesome'; +import { faFileArchive } from '@fortawesome/free-solid-svg-icons'; import { GetAltTextPipe } from '../../pipes/get-alt-text.pipe'; class TestImage { @@ -74,154 +83,207 @@ const fullItem2 = new RecordVO( { dataStatus: DataStatus.Full }, ); -@NgModule({}) -class DummyModule {} +@Component({ + selector: 'pr-thumbnail-test-host', + template: `
+ +
`, + standalone: false, +}) +class ThumbnailTestHostComponent { + item: ItemVO = minItem; + maxWidth: number | undefined; + width: string = '200px'; + height: string = '200px'; +} describe('ThumbnailComponent', () => { - let shallow: Shallow; + let fixture: ComponentFixture; + let hostComponent: ThumbnailTestHostComponent; + let thumbnailComponent: ThumbnailComponent; - beforeEach(() => { + beforeEach(async () => { TestImage.testError = false; window.devicePixelRatio = 1; - shallow = new Shallow(ThumbnailComponent, DummyModule) - .declare(GetAltTextPipe) - .provideMock({ - provide: 'Image', - useValue: TestImage, - }) - .import(FontAwesomeModule); + + await TestBed.configureTestingModule({ + imports: [FontAwesomeModule], + declarations: [ + ThumbnailComponent, + ThumbnailTestHostComponent, + GetAltTextPipe, + ], + providers: [{ provide: 'Image', useValue: TestImage }], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + const library = TestBed.inject(FaIconLibrary); + library.addIcons(faFileArchive); + + fixture = TestBed.createComponent(ThumbnailTestHostComponent); + hostComponent = fixture.componentInstance; + fixture.detectChanges(); + thumbnailComponent = + fixture.debugElement.children[0].children[0].componentInstance; }); - async function renderWithItem( - item: ItemVO = minItem, - size: number = 200, - maxWidth?: number, - ) { - return await shallow.render( - `
`, - { - bind: { - item: item.isFolder ? new FolderVO(item) : new RecordVO(item), - maxWidth, - }, - }, - ); + function setItem(item: ItemVO, size: number = 200, maxWidth?: number) { + hostComponent.item = item.isFolder + ? new FolderVO(item) + : new RecordVO(item); + hostComponent.width = `${size}px`; + hostComponent.height = `${size}px`; + hostComponent.maxWidth = maxWidth; + + // Mock the clientWidth since it's 0 in headless test environments + const thumbnailElement = + fixture.debugElement.children[0].children[0].nativeElement; + Object.defineProperty(thumbnailElement, 'clientWidth', { + get: () => size, + configurable: true, + }); + + // Update dpiScale based on current devicePixelRatio (it's cached in constructor) + (thumbnailComponent as any).dpiScale = + window?.devicePixelRatio > 1.75 ? 2 : 1; + + fixture.detectChanges(); + + // Force recalculation after mocking clientWidth + thumbnailComponent.resetImage(); + + // Update the view with the new state (isZip, etc.) + fixture.detectChanges(); } it('should exist', async () => { - const { instance } = await renderWithItem(); - - expect(instance).toBeTruthy(); + expect(thumbnailComponent).toBeTruthy(); }); it('should use image 200 if item is lean at any DPI and width', async () => { - const { instance } = await renderWithItem(leanItem); + setItem(leanItem); - expect(instance.getCurrentThumbUrl()).toEqual(image200); + expect(thumbnailComponent.getCurrentThumbUrl()).toEqual(image200); }); it('should use image 200 for low DPI at width 100', async () => { - const { instance } = await renderWithItem(fullItem, 100); + setItem(fullItem, 100); - expect(instance.getTargetThumbWidth()).toEqual(200); - expect(instance.getCurrentThumbUrl()).toEqual(image200); + expect(thumbnailComponent.getTargetThumbWidth()).toEqual(200); + expect(thumbnailComponent.getCurrentThumbUrl()).toEqual(image200); }); it('should use image 200 for high DPI at width 100', async () => { window.devicePixelRatio = 2; - const { instance } = await renderWithItem(fullItem, 100); + setItem(fullItem, 100); - expect(instance.getTargetThumbWidth()).toEqual(200); - expect(instance.getCurrentThumbUrl()).toEqual(image200); + expect(thumbnailComponent.getTargetThumbWidth()).toEqual(200); + expect(thumbnailComponent.getCurrentThumbUrl()).toEqual(image200); }); it('should use image 200 for low DPI at width 200', async () => { - const { instance } = await renderWithItem(fullItem, 200); + setItem(fullItem, 200); - expect(instance.getTargetThumbWidth()).toEqual(200); - expect(instance.getCurrentThumbUrl()).toEqual(image200); + expect(thumbnailComponent.getTargetThumbWidth()).toEqual(200); + expect(thumbnailComponent.getCurrentThumbUrl()).toEqual(image200); }); it('should use image 500 for high DPI at width 200', async () => { window.devicePixelRatio = 2; - const { instance } = await renderWithItem(fullItem, 200); + setItem(fullItem, 200); - expect(instance.getTargetThumbWidth()).toEqual(500); - expect(instance.getCurrentThumbUrl()).toEqual(image500); + expect(thumbnailComponent.getTargetThumbWidth()).toEqual(500); + expect(thumbnailComponent.getCurrentThumbUrl()).toEqual(image500); }); it('should use the maximum image size if there is no bigger thumbnail', async () => { window.devicePixelRatio = 10000; - const { instance } = await renderWithItem(fullItem, 10000); + setItem(fullItem, 10000); - expect(instance.getTargetThumbWidth()).toEqual(2000); - expect(instance.getCurrentThumbUrl()).toEqual(image2000); + expect(thumbnailComponent.getTargetThumbWidth()).toEqual(2000); + expect(thumbnailComponent.getCurrentThumbUrl()).toEqual(image2000); }); it('should use reset when changing records', async () => { window.devicePixelRatio = 2; - const { instance, fixture } = await renderWithItem(fullItem, 200); + setItem(fullItem, 200); - expect(instance.getTargetThumbWidth()).toEqual(500); - expect(instance.getCurrentThumbUrl()).toEqual(image500); + expect(thumbnailComponent.getTargetThumbWidth()).toEqual(500); + expect(thumbnailComponent.getCurrentThumbUrl()).toEqual(image500); - instance.item = fullItem2; + thumbnailComponent.item = fullItem2; fixture.detectChanges(); - expect(instance.getTargetThumbWidth()).toEqual(500); - expect(instance.getCurrentThumbUrl()).toEqual(fullItem2.thumbURL500); + expect(thumbnailComponent.getTargetThumbWidth()).toEqual(500); + expect(thumbnailComponent.getCurrentThumbUrl()).toEqual( + fullItem2.thumbURL500, + ); }); it('should show a zip icon if the item is a .zip archive', async () => { - const { find } = await renderWithItem( - new RecordVO({ ...fullItem, type: 'type.record.archive' }), - 200, + setItem(new RecordVO({ ...fullItem, type: 'type.record.archive' }), 200); + + const faIcons = fixture.nativeElement.querySelectorAll('fa-icon'); + const visibleImages = fixture.nativeElement.querySelectorAll( + '.pr-thumbnail-image:not([hidden])', ); - expect(find('fa-icon').length).toBeGreaterThan(0); - expect(find('.pr-thumbnail-image:not([hidden])').length).toBe(0); + expect(faIcons.length).toBeGreaterThan(0); + expect(visibleImages.length).toBe(0); }); it('should show a folder icon if the item is a folder', async () => { - const { find } = await renderWithItem( + setItem( new FolderVO({ thumbURL200: 'https://do-not-use', }), ); - expect(find('.pr-thumbnail-image[hidden]').length).toBe(1); - expect(find('i.ion-md-folder[hidden]').length).toBe(0); + const hiddenImages = fixture.nativeElement.querySelectorAll( + '.pr-thumbnail-image[hidden]', + ); + const folderIcons = fixture.nativeElement.querySelectorAll( + 'i.ion-md-folder:not([hidden])', + ); + + expect(hiddenImages.length).toBe(1); + expect(folderIcons.length).toBeGreaterThan(0); }); it('can have a maximum width set', async () => { window.devicePixelRatio = 2; - const { instance } = await renderWithItem(fullItem, 10000, 200); + setItem(fullItem, 10000, 200); - expect(instance.getTargetThumbWidth()).toEqual(200); - expect(instance.getCurrentThumbUrl()).toEqual(image200); + expect(thumbnailComponent.getTargetThumbWidth()).toEqual(200); + expect(thumbnailComponent.getCurrentThumbUrl()).toEqual(image200); }); - it('should show set the background image after it loads', async () => { - const { find, fixture } = await renderWithItem(fullItem); + it('should show set the background image after it loads', fakeAsync(() => { + setItem(fullItem); - await fixture.whenStable(); + // Wait for the TestImage setTimeout to fire + tick(100); fixture.detectChanges(); - expect( - find('.pr-thumbnail-image').nativeElement.style.backgroundImage, - ).toContain(image200); - }); + const thumbnailImage = fixture.nativeElement.querySelector( + '.pr-thumbnail-image', + ); + + expect(thumbnailImage.style.backgroundImage).toContain(image200); + })); it('should be able to handle an image erroring out', async () => { TestImage.testError = true; - const { instance, find, fixture } = await renderWithItem(leanItem); + setItem(leanItem); - instance.item.update(fullItem); + thumbnailComponent.item.update(fullItem); await fixture.whenStable(); fixture.detectChanges(); - expect( - find('.pr-thumbnail-image').nativeElement.style.backgroundImage, - ).toBe(''); + const thumbnailImage = fixture.nativeElement.querySelector( + '.pr-thumbnail-image', + ); + + expect(thumbnailImage.style.backgroundImage).toBe(''); }); }); diff --git a/src/app/shared/components/zooming-image-viewer/zooming-image-viewer.component.spec.ts b/src/app/shared/components/zooming-image-viewer/zooming-image-viewer.component.spec.ts index 3f6e289c6..e185a6d41 100644 --- a/src/app/shared/components/zooming-image-viewer/zooming-image-viewer.component.spec.ts +++ b/src/app/shared/components/zooming-image-viewer/zooming-image-viewer.component.spec.ts @@ -1,13 +1,10 @@ -import { Shallow } from 'shallow-render'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import OpenSeadragon from 'openseadragon'; -import { NgModule } from '@angular/core'; import { GetAltTextPipe } from '@shared/pipes/get-alt-text.pipe'; import { RecordVO } from '@models/index'; import { ZoomingImageViewerComponent } from './zooming-image-viewer.component'; -@NgModule() -class DummyModule {} - class MockOpenSeaDragon { private handlers = new Map void>(); @@ -31,19 +28,25 @@ function createMockOpenSeaDragon(options: OpenSeadragon.Options) { } describe('ZoomingImageViewerComponent', () => { - let shallow: Shallow; - - beforeEach(() => { - shallow = new Shallow(ZoomingImageViewerComponent, DummyModule) - .declare(GetAltTextPipe) - .provideMock({ - provide: 'openseadragon', - useValue: createMockOpenSeaDragon, - }); + let fixture: ComponentFixture; + let instance: ZoomingImageViewerComponent; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ZoomingImageViewerComponent, GetAltTextPipe], + providers: [ + { provide: 'openseadragon', useValue: createMockOpenSeaDragon }, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(ZoomingImageViewerComponent); + instance = fixture.componentInstance; }); - async function renderWithRecord(record: RecordVO) { - return await shallow.render({ bind: { item: record } }); + function renderWithRecord(record: RecordVO) { + instance.item = record; + fixture.detectChanges(); } function getValidTestRecord(): RecordVO { @@ -54,7 +57,7 @@ describe('ZoomingImageViewerComponent', () => { } it('should create', async () => { - const { instance } = await shallow.render(); + renderWithRecord(getValidTestRecord()); expect(instance).toBeTruthy(); }); @@ -101,30 +104,28 @@ describe('ZoomingImageViewerComponent', () => { }); describe('Record formats that prevent openseadragon setup', () => { - async function expectNoViewerWithRecord(record: RecordVO) { - const { instance } = await renderWithRecord(record); + function expectNoViewerWithRecord(record: RecordVO) { + renderWithRecord(record); expect(instance.viewer).toBeUndefined(); } it('needs an instance of RecordVO', async () => { - await expectNoViewerWithRecord({} as RecordVO); + expectNoViewerWithRecord({} as RecordVO); }); it('needs FileVOs', async () => { - await expectNoViewerWithRecord( - new RecordVO({ type: 'type.record.image' }), - ); + expectNoViewerWithRecord(new RecordVO({ type: 'type.record.image' })); }); it('needs to be an image', async () => { - await expectNoViewerWithRecord( + expectNoViewerWithRecord( new RecordVO({ FileVOs: [{ fileURL: 'test' }] }), ); }); it('needs a valid image url', async () => { - await expectNoViewerWithRecord( + expectNoViewerWithRecord( new RecordVO({ FileVOs: [{}], type: 'type.record.image', @@ -134,40 +135,43 @@ describe('ZoomingImageViewerComponent', () => { }); it('sets up openseadragon with a record that is an image', async () => { - const { instance } = await renderWithRecord(getValidTestRecord()); + renderWithRecord(getValidTestRecord()); expect(instance.viewer).toBeTruthy(); }); it('should output an event when going fullscreen', async () => { - const { instance, outputs } = await renderWithRecord(getValidTestRecord()); + renderWithRecord(getValidTestRecord()); + const isFullScreenSpy = spyOn(instance.isFullScreen, 'emit'); + const viewer = instance.viewer; viewer.raiseEvent('full-screen', { fullScreen: true }); - expect(outputs.isFullScreen.emit).toHaveBeenCalledWith(true); + expect(isFullScreenSpy).toHaveBeenCalledWith(true); viewer.raiseEvent('full-screen', { fullScreen: false }); - expect(outputs.isFullScreen.emit).toHaveBeenCalledWith(false); + expect(isFullScreenSpy).toHaveBeenCalledWith(false); }); it('should output an event if the user zooms in', async () => { - const { instance, outputs } = await renderWithRecord(getValidTestRecord()); + renderWithRecord(getValidTestRecord()); + const disableSwipeSpy = spyOn(instance.disableSwipe, 'emit'); const viewer = instance.viewer; viewer.raiseEvent('zoom', { zoom: 2 }); viewer.raiseEvent('zoom', { zoom: 3 }); - expect(outputs.disableSwipe.emit).toHaveBeenCalledWith(true); + expect(disableSwipeSpy).toHaveBeenCalledWith(true); viewer.raiseEvent('zoom', { zoom: 2 }); - expect(outputs.disableSwipe.emit).toHaveBeenCalledWith(false); + expect(disableSwipeSpy).toHaveBeenCalledWith(false); }); it('should disable panning if the zoom level is lesss than 1', async () => { - const { instance } = await renderWithRecord(getValidTestRecord()); + renderWithRecord(getValidTestRecord()); const viewer = instance.viewer; const expectPanning = (enabled: boolean) => { diff --git a/src/app/shared/services/account/tests/account.service.spec.ts b/src/app/shared/services/account/tests/account.service.spec.ts index 4cd0e34e7..01996f585 100644 --- a/src/app/shared/services/account/tests/account.service.spec.ts +++ b/src/app/shared/services/account/tests/account.service.spec.ts @@ -1,6 +1,6 @@ +import { TestBed } from '@angular/core/testing'; import { CookieService } from 'ngx-cookie-service'; import { Router } from '@angular/router'; -import { Shallow } from 'shallow-render'; import { Observable } from 'rxjs'; import { UploadService } from '@core/services/upload/upload.service'; import { AccountService } from '@shared/services/account/account.service'; @@ -9,123 +9,173 @@ import { AuthResponse } from '@shared/services/api/index.repo'; import { AccountVO, FolderVO, RecordVO } from '@root/app/models'; import { HttpV2Service } from '@shared/services/http-v2/http-v2.service'; import { HttpService } from '@shared/services/http/http.service'; -import { AppModule } from '../../../../app.module'; +import { LocationStrategy } from '@angular/common'; +import { DialogCdkService } from '@root/app/dialog-cdk/dialog-cdk.service'; +import { Subject } from 'rxjs'; import { StorageService } from '../../storage/storage.service'; import { EditService } from '../../../../core/services/edit/edit.service'; +import { EventService } from '../../event/event.service'; -describe('AccountService', () => { - let shallow: Shallow; - - beforeEach(() => { - shallow = new Shallow(AccountService, AppModule) - .mock(ApiService, { - account: { - signUp: ( - email: string, - fullName: string, - password: string, - passwordConfirm: string, - agreed: boolean, - optIn: boolean, - createDefaultArchive: boolean, - phone?: string, - inviteCode?: string, - ) => - new Observable((observer) => { - observer.next( - new AccountVO({ - primaryEmail: 'test@permanent.org', - fullName: 'Test User', - }), - ); - observer.complete(); - }), - get: async (account: AccountVO) => await Promise.reject({}), - }, - auth: { - verify: (account, token, type) => - new Observable((observer) => { - observer.next( - new AuthResponse({ - isSuccessful: true, - Results: [ - { - data: [ - { - AccountVO: { - primaryEmail: 'test@permanent.org', - fullName: 'Test User', - emailStatus: 'status.auth.verified', - phoneStatus: 'status.auth.verified', - }, - }, - ], +const mockApiService = { + account: { + signUp: ( + email: string, + fullName: string, + password: string, + passwordConfirm: string, + agreed: boolean, + optIn: boolean, + createDefaultArchive: boolean, + phone?: string, + inviteCode?: string, + ) => + new Observable((observer) => { + observer.next( + new AccountVO({ + primaryEmail: 'test@permanent.org', + fullName: 'Test User', + }), + ); + observer.complete(); + }), + get: async (account: AccountVO) => await Promise.reject({}), + }, + auth: { + verify: (account, token, type) => + new Observable((observer) => { + observer.next( + new AuthResponse({ + isSuccessful: true, + Results: [ + { + data: [ + { + AccountVO: { + primaryEmail: 'test@permanent.org', + fullName: 'Test User', + emailStatus: 'status.auth.verified', + phoneStatus: 'status.auth.verified', }, - ], - }), - ); - observer.complete(); - }), - logIn: ( - email: string, - password: string, - rememberMe: boolean, - keepLoggedIn: boolean, - ) => - new Observable((observer) => { - observer.next( - new AuthResponse({ - isSuccessful: true, - Results: [ - { - data: [ - { - AccountVO: { - primaryEmail: 'test@permanent.org', - fullName: 'Test User', - }, - }, - ], + }, + ], + }, + ], + }), + ); + observer.complete(); + }), + logIn: ( + email: string, + password: string, + rememberMe: boolean, + keepLoggedIn: boolean, + ) => + new Observable((observer) => { + observer.next( + new AuthResponse({ + isSuccessful: true, + Results: [ + { + data: [ + { + AccountVO: { + primaryEmail: 'test@permanent.org', + fullName: 'Test User', }, - ], - }), - ); - observer.complete(); - }), - }, - }) - .mock(Router, { - navigate: async (route: string[]) => await Promise.resolve(true), - }) - .mock(StorageService, { - local: { - get: () => {}, - set: () => {}, + }, + ], + }, + ], + }), + ); + observer.complete(); + }), + }, +}; + +const mockRouter = { + navigate: async (route: string[]) => await Promise.resolve(true), +}; + +const mockStorageService = { + local: { + get: () => {}, + set: () => {}, + }, + session: { + get: () => {}, + set: () => {}, + }, +}; + +const mockUploadService = { + uploadFiles: async (parentFolder: FolderVO, files: File[]) => + await Promise.resolve(true), +}; + +const mockEditService = { + deleteItems: async (items: any[]) => await Promise.resolve(true), +}; + +const mockCookieService = { + set: (key: string, value: string) => {}, +}; + +const mockDialogCdkService = { + open: () => {}, +}; + +const mockEventService = { + dispatch: () => {}, +}; + +const mockLocationStrategy = { + path: () => '/app/private', +}; + +describe('AccountService', () => { + let instance: AccountService; + let uploadService: UploadService; + let editService: EditService; + let httpV2Service: HttpV2Service; + let httpService: HttpService; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + providers: [ + AccountService, + { provide: ApiService, useValue: mockApiService }, + { provide: Router, useValue: mockRouter }, + { provide: StorageService, useValue: mockStorageService }, + { provide: UploadService, useValue: mockUploadService }, + { provide: EditService, useValue: mockEditService }, + { provide: CookieService, useValue: mockCookieService }, + { provide: DialogCdkService, useValue: mockDialogCdkService }, + { provide: EventService, useValue: mockEventService }, + { provide: LocationStrategy, useValue: mockLocationStrategy }, + { + provide: HttpV2Service, + useValue: { tokenExpired: new Subject() }, }, - session: { - get: () => {}, - set: () => {}, + { + provide: HttpService, + useValue: { tokenExpired: new Subject() }, }, - }) - .mock(UploadService, { - uploadFiles: async (parentFolder: FolderVO, files: File[]) => - await Promise.resolve(true), - }) - .mock(EditService, { - deleteItems: async (items: any[]) => await Promise.resolve(true), - }) - .mock(CookieService, { set: (key: string, value: string) => {} }); + ], + }).compileComponents(); + + instance = TestBed.inject(AccountService); + uploadService = TestBed.inject(UploadService); + editService = TestBed.inject(EditService); + httpV2Service = TestBed.inject(HttpV2Service); + httpService = TestBed.inject(HttpService); }); it('should be created', () => { - const { instance } = shallow.createService(); - expect(instance).toBeTruthy(); }); it('should make the correct API calls during signUp', async () => { - const { instance, inject } = shallow.createService(); - inject(ApiService); const account = await instance.signUp( 'test@permanent.org', 'Test User', @@ -142,8 +192,6 @@ describe('AccountService', () => { }); it('should pass along errors encountered during signUp', async () => { - const { instance, inject } = shallow.createService(); - inject(ApiService); const expectedError = 'Out of cheese error. Redo from start'; try { await instance.signUp( @@ -163,8 +211,6 @@ describe('AccountService', () => { }); it('should update the account storage when a file is uploaded successfully', async () => { - const { instance, inject } = shallow.createService(); - const uploadService = inject(UploadService); const account = new AccountVO({ spaceLeft: 100000, }); @@ -177,8 +223,6 @@ describe('AccountService', () => { }); it('should add storage back after deleting an item', async () => { - const { instance, inject } = shallow.createService(); - const editService = inject(EditService); const account = new AccountVO({ spaceLeft: 100000, }); @@ -203,17 +247,15 @@ describe('AccountService', () => { describe('Log out on token expiration', () => { it('HttpV2Service', async () => { - const { instance, inject } = shallow.createService(); const logOut = spyOn(instance, 'logOut').and.rejectWith(false); - inject(HttpV2Service).tokenExpired.next(); + httpV2Service.tokenExpired.next(); expect(logOut).toHaveBeenCalled(); }); it('HttpService', async () => { - const { instance, inject } = shallow.createService(); const spy = spyOn(instance, 'logOut').and.rejectWith(false); - inject(HttpService).tokenExpired.next(); + httpService.tokenExpired.next(); expect(spy).toHaveBeenCalled(); }); diff --git a/src/app/shared/services/account/tests/refreshAccount.spec.ts b/src/app/shared/services/account/tests/refreshAccount.spec.ts index 1246b7125..f8034094a 100644 --- a/src/app/shared/services/account/tests/refreshAccount.spec.ts +++ b/src/app/shared/services/account/tests/refreshAccount.spec.ts @@ -1,14 +1,18 @@ -import { Shallow } from 'shallow-render'; +import { TestBed } from '@angular/core/testing'; import { AccountVO } from '@models/account-vo'; -import { Observable } from 'rxjs'; +import { Observable, Subject } from 'rxjs'; import { AuthResponse } from '@shared/services/api/auth.repo'; -import { AppModule } from '@root/app/app.module'; import { ApiService } from '@shared/services/api/api.service'; import { StorageService } from '@shared/services/storage/storage.service'; import { Router } from '@angular/router'; import { ArchiveVO } from '@models/index'; import { AccountResponse } from '@shared/services/api/account.repo'; import { LocationStrategy } from '@angular/common'; +import { CookieService } from 'ngx-cookie-service'; +import { DialogCdkService } from '@root/app/dialog-cdk/dialog-cdk.service'; +import { EventService } from '@shared/services/event/event.service'; +import { HttpV2Service } from '@shared/services/http-v2/http-v2.service'; +import { HttpService } from '@shared/services/http/http.service'; import { AccountService } from '../account.service'; class AccountRepoStub { @@ -81,69 +85,80 @@ const dummyStorageService = { }; describe('AccountService: refreshAccount', () => { - let shallow: Shallow; + let instance: AccountService; + let apiService: ApiService; + let router: Router; + let location: LocationStrategy; + let storageService: StorageService; let accountRepo: AccountRepoStub; let authRepo: AuthRepoStub; - beforeEach(() => { + beforeEach(async () => { AuthRepoStub.loggedIn = true; AccountRepoStub.failRequest = false; accountRepo = new AccountRepoStub(); authRepo = new AuthRepoStub(); - shallow = new Shallow(AccountService, AppModule) - .dontMock(ApiService) - .provide({ - provide: ApiService, - useValue: { auth: authRepo, account: accountRepo }, - }) - .dontMock(StorageService) - .provide({ provide: StorageService, useValue: dummyStorageService }); + + await TestBed.configureTestingModule({ + providers: [ + AccountService, + { + provide: ApiService, + useValue: { auth: authRepo, account: accountRepo }, + }, + { provide: StorageService, useValue: dummyStorageService }, + { + provide: Router, + useValue: { navigate: jasmine.createSpy('router.navigate') }, + }, + { + provide: LocationStrategy, + useValue: { path: () => '/app/private' }, + }, + { provide: CookieService, useValue: {} }, + { provide: DialogCdkService, useValue: {} }, + { provide: EventService, useValue: { dispatch: () => {} } }, + { + provide: HttpV2Service, + useValue: { tokenExpired: new Subject() }, + }, + { + provide: HttpService, + useValue: { tokenExpired: new Subject() }, + }, + ], + }).compileComponents(); + + instance = TestBed.inject(AccountService); + apiService = TestBed.inject(ApiService); + router = TestBed.inject(Router); + location = TestBed.inject(LocationStrategy); + storageService = TestBed.inject(StorageService); }); function setUpSpies( - services: { - apiService: ApiService; - router: Router; - location: LocationStrategy; - instance: AccountService; - storage?: StorageService; - }, url: string = '/app/private', + withStorage: boolean = false, ) { - services.router.navigate = jasmine - .createSpy('router.navigate') - .and.callFake(() => {}); - - const logOutSpy = spyOn( - services.apiService.auth, - 'logOut', - ).and.callThrough(); - spyOn(services.location, 'path').and.returnValue(url); + const logOutSpy = spyOn(apiService.auth, 'logOut').and.callThrough(); + spyOn(location, 'path').and.returnValue(url); - services.instance.setArchive(new ArchiveVO({})); - services.instance.setAccount(new AccountVO({})); + instance.setArchive(new ArchiveVO({})); + instance.setAccount(new AccountVO({})); let localStorageSpy; - if (services.storage) { - localStorageSpy = spyOn(services.storage.local, 'set').and.callThrough(); - spyOn(services.instance, 'getStorage').and.returnValue( + if (withStorage) { + localStorageSpy = spyOn(storageService.local, 'set').and.callThrough(); + spyOn(instance, 'getStorage').and.returnValue( new AccountVO({ keepLoggedIn: true }), ); } return { logOutSpy, localStorageSpy }; } + it('should be able to check if the user is logged in', async () => { - const { instance, inject } = shallow.createService(); - const router = inject(Router); - - const { logOutSpy, localStorageSpy } = setUpSpies({ - apiService: inject(ApiService), - router, - location: inject(LocationStrategy), - instance, - storage: inject(StorageService), - }); + const { logOutSpy, localStorageSpy } = setUpSpies('/app/private', true); await instance.refreshAccount(); @@ -153,15 +168,7 @@ describe('AccountService: refreshAccount', () => { }); it('should redirect the user to the login page if their session expires', async () => { - const { instance, inject } = shallow.createService(); - const router = inject(Router); - - const { logOutSpy } = setUpSpies({ - apiService: inject(ApiService), - router, - location: inject(LocationStrategy), - instance, - }); + const { logOutSpy } = setUpSpies('/app/private'); AuthRepoStub.loggedIn = false; await instance.refreshAccount(); @@ -171,18 +178,7 @@ describe('AccountService: refreshAccount', () => { }); it('should not redirect the user to login page if their session expires on a public archive', async () => { - const { instance, inject } = shallow.createService(); - const router = inject(Router); - - const { logOutSpy } = setUpSpies( - { - apiService: inject(ApiService), - router, - location: inject(LocationStrategy), - instance, - }, - '///p/0001-0000/?ksljflkasjlf', - ); + const { logOutSpy } = setUpSpies('///p/0001-0000/?ksljflkasjlf'); AuthRepoStub.loggedIn = false; await instance.refreshAccount(); @@ -192,18 +188,7 @@ describe('AccountService: refreshAccount', () => { }); it('should not redirect the user to login page if their session expires in the public gallery', async () => { - const { instance, inject } = shallow.createService(); - const router = inject(Router); - - const { logOutSpy } = setUpSpies( - { - apiService: inject(ApiService), - router, - location: inject(LocationStrategy), - instance, - }, - '///gallery/////', - ); + const { logOutSpy } = setUpSpies('///gallery/////'); AuthRepoStub.loggedIn = false; await instance.refreshAccount(); @@ -213,15 +198,7 @@ describe('AccountService: refreshAccount', () => { }); it('should redirect the user if the account/get call fails', async () => { - const { instance, inject } = shallow.createService(); - const router = inject(Router); - - const { logOutSpy } = setUpSpies({ - apiService: inject(ApiService), - router, - location: inject(LocationStrategy), - instance, - }); + const { logOutSpy } = setUpSpies('/app/private'); AccountRepoStub.failRequest = true; await instance.refreshAccount(); @@ -231,18 +208,7 @@ describe('AccountService: refreshAccount', () => { }); it('should not redirect the user if the account/get call fails on the public archive', async () => { - const { instance, inject } = shallow.createService(); - const router = inject(Router); - - const { logOutSpy } = setUpSpies( - { - apiService: inject(ApiService), - router, - location: inject(LocationStrategy), - instance, - }, - '/p/0001-0000/', - ); + const { logOutSpy } = setUpSpies('/p/0001-0000/'); AccountRepoStub.failRequest = true; await instance.refreshAccount(); @@ -252,18 +218,7 @@ describe('AccountService: refreshAccount', () => { }); it('should not redirect the user if the account/get call fails on the public gallery', async () => { - const { instance, inject } = shallow.createService(); - const router = inject(Router); - - const { logOutSpy } = setUpSpies( - { - apiService: inject(ApiService), - router, - location: inject(LocationStrategy), - instance, - }, - '/gallery/', - ); + const { logOutSpy } = setUpSpies('/gallery/'); AccountRepoStub.failRequest = true; await instance.refreshAccount(); diff --git a/src/app/shared/services/device/device.service.spec.ts b/src/app/shared/services/device/device.service.spec.ts index c68a5669a..4a1e42ea4 100644 --- a/src/app/shared/services/device/device.service.spec.ts +++ b/src/app/shared/services/device/device.service.spec.ts @@ -1,24 +1,29 @@ -import { SharedModule } from '@shared/shared.module'; +import { TestBed } from '@angular/core/testing'; +import { MockBuilder } from 'ng-mocks'; import { CookieService } from 'ngx-cookie-service'; -import { Shallow } from 'shallow-render'; import { DeviceService } from './device.service'; describe('DeviceService', () => { - let shallow: Shallow; - beforeEach(() => { - shallow = new Shallow(DeviceService, SharedModule).mock(CookieService, { - check: () => false, + let cookieServiceMock: jasmine.SpyObj; + + beforeEach(async () => { + cookieServiceMock = jasmine.createSpyObj('CookieService', ['check']); + cookieServiceMock.check.and.returnValue(false); + + await MockBuilder(DeviceService).provide({ + provide: CookieService, + useValue: cookieServiceMock, }); }); - it('should be created', async () => { - const { instance } = shallow.createService(); + it('should be created', () => { + const instance = TestBed.inject(DeviceService); expect(instance).toBeTruthy(); }); - it('should detect mobile width correctly', async () => { + it('should detect mobile width correctly', () => { spyOn(window, 'matchMedia').and.callFake( (query: string) => ({ @@ -26,40 +31,37 @@ describe('DeviceService', () => { }) as MediaQueryList, ); - const { instance } = shallow.createService(); + const instance = TestBed.inject(DeviceService); expect(instance.isMobileWidth()).toBeTrue(); }); - it('should detect mobile device correctly', async () => { + it('should detect mobile device correctly', () => { Object.defineProperty(window.navigator, 'userAgent', { value: 'iphone', configurable: true, }); - const { instance } = shallow.createService(); + const instance = TestBed.inject(DeviceService); expect(instance.isMobile()).toBeTrue(); }); - it('should detect iOS device correctly', async () => { + it('should detect iOS device correctly', () => { Object.defineProperty(window.navigator, 'userAgent', { value: 'ipad', configurable: true, }); - const { instance } = shallow.createService(); + const instance = TestBed.inject(DeviceService); expect(instance.isIos()).toBeTrue(); }); - it('should handle cookie check for opt-out correctly', async () => { - const cookieService = jasmine.createSpyObj('CookieService', ['check']); - cookieService.check.and.returnValue(true); - - shallow = shallow.mock(CookieService, cookieService); + it('should handle cookie check for opt-out correctly', () => { + cookieServiceMock.check.and.returnValue(true); - const { instance } = shallow.createService(); + const instance = TestBed.inject(DeviceService); expect(instance.didOptOut()).toBeTrue(); }); diff --git a/src/app/shared/services/event/event.service.spec.ts b/src/app/shared/services/event/event.service.spec.ts index c402bb45a..b4793dfd3 100644 --- a/src/app/shared/services/event/event.service.spec.ts +++ b/src/app/shared/services/event/event.service.spec.ts @@ -1,25 +1,24 @@ -import { SharedModule } from '@shared/shared.module'; -import { Shallow } from 'shallow-render'; +import { TestBed } from '@angular/core/testing'; +import { MockBuilder } from 'ng-mocks'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { EventObserver, EventService } from './event.service'; import { PermanentEvent } from './event-types'; describe('EventService', () => { - let shallow: Shallow; - beforeEach(() => { - shallow = new Shallow(EventService, SharedModule).import( - HttpClientTestingModule, - ); + beforeEach(async () => { + await MockBuilder(EventService).keep(HttpClientTestingModule, { + export: true, + }); }); - it('should be created', async () => { - const { instance } = shallow.createService(); + it('should be created', () => { + const instance = TestBed.inject(EventService); expect(instance).toBeTruthy(); }); - it('should add an observer', async () => { - const { instance } = shallow.createService(); + it('should add an observer', () => { + const instance = TestBed.inject(EventService); const mockObserver: EventObserver = { update: async (_: PermanentEvent) => {}, }; @@ -29,8 +28,8 @@ describe('EventService', () => { expect(instance.getObservers()).toContain(mockObserver); }); - it('should notify all observers', async () => { - const { instance } = shallow.createService(); + it('should notify all observers', () => { + const instance = TestBed.inject(EventService); const mockObserver1: EventObserver = { update: jasmine.createSpy('update'), }; @@ -51,8 +50,8 @@ describe('EventService', () => { expect(mockObserver2.update).toHaveBeenCalledWith(eventData); }); - it('should remove an observer', async () => { - const { instance } = shallow.createService(); + it('should remove an observer', () => { + const instance = TestBed.inject(EventService); const mockObserver: EventObserver = { update: jasmine.createSpy('update'), }; @@ -67,8 +66,8 @@ describe('EventService', () => { expect(mockObserver.update).not.toHaveBeenCalled(); }); - it('should not notify removed observers', async () => { - const { instance } = shallow.createService(); + it('should not notify removed observers', () => { + const instance = TestBed.inject(EventService); const mockObserver: EventObserver = { update: jasmine.createSpy('update'), }; diff --git a/src/app/shared/services/mobile-banner/mobile-banner.service.spec.ts b/src/app/shared/services/mobile-banner/mobile-banner.service.spec.ts index bb9b2e46d..a48bec43f 100644 --- a/src/app/shared/services/mobile-banner/mobile-banner.service.spec.ts +++ b/src/app/shared/services/mobile-banner/mobile-banner.service.spec.ts @@ -1,6 +1,6 @@ +import { TestBed } from '@angular/core/testing'; +import { MockBuilder } from 'ng-mocks'; import { DeviceService } from '@shared/services/device/device.service'; -import { CoreModule } from '@core/core.module'; -import { Shallow } from 'shallow-render'; import { MobileBannerService } from './mobile-banner.service'; const mockAndroidDeviceService = { @@ -13,18 +13,16 @@ const mockIosDeviceService = { }; describe('BannerService', () => { - let shallow: Shallow; - describe('when on an Android device', () => { - beforeEach(() => { - shallow = new Shallow(MobileBannerService, CoreModule).mock( - DeviceService, - mockAndroidDeviceService, - ); + beforeEach(async () => { + await MockBuilder(MobileBannerService).provide({ + provide: DeviceService, + useValue: mockAndroidDeviceService, + }); }); - it('should be visible on Android', async () => { - const { instance } = shallow.createService(); + it('should be visible on Android', () => { + const instance = TestBed.inject(MobileBannerService); expect(instance.isVisible).toBeTrue(); @@ -33,15 +31,15 @@ describe('BannerService', () => { }); describe('when on an iOS device', () => { - beforeEach(() => { - shallow = new Shallow(MobileBannerService, CoreModule).mock( - DeviceService, - mockIosDeviceService, - ); + beforeEach(async () => { + await MockBuilder(MobileBannerService).provide({ + provide: DeviceService, + useValue: mockIosDeviceService, + }); }); - it('should be visible on iOS', async () => { - const { instance } = shallow.createService(); + it('should be visible on iOS', () => { + const instance = TestBed.inject(MobileBannerService); expect(instance.isVisible).toBeTrue(); diff --git a/src/app/shared/services/profile/profile.service.spec.ts b/src/app/shared/services/profile/profile.service.spec.ts index 253d4ba6f..5b3f4f0eb 100644 --- a/src/app/shared/services/profile/profile.service.spec.ts +++ b/src/app/shared/services/profile/profile.service.spec.ts @@ -1,8 +1,7 @@ -import { NgModule } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { MockBuilder } from 'ng-mocks'; import { FolderPickerService } from '@core/services/folder-picker/folder-picker.service'; import { ArchiveVO, FolderVO } from '@models/index'; -import { Shallow } from 'shallow-render'; -import { ProfileItemVOData } from '@models/profile-item-vo'; import { RecordVO } from '../../../models/record-vo'; import { ArchiveResponse } from '../api/archive.repo'; import { AccountService } from '../account/account.service'; @@ -19,9 +18,9 @@ const mockApiService = { await Promise.resolve(new ArchiveResponse({})), getAllProfileItems: async (archive: ArchiveVO) => await Promise.resolve(new ArchiveResponse({})), - addUpdateProfileItems: async (profileItems: ProfileItemVOData[]) => + addUpdateProfileItems: async (profileItems: any[]) => await Promise.resolve(new ArchiveResponse({})), - deleteProfileItem: async (profileItem: ProfileItemVOData) => + deleteProfileItem: async (profileItem: any) => await Promise.resolve(new ArchiveResponse({})), }, }; @@ -33,46 +32,33 @@ const mockAccountService = { new ArchiveVO({ archiveId: 1, type: 'archive.type.organization' }), }; const mockFolderPickerService = { - hooseRecord: async (startingFolder: FolderVO) => + chooseRecord: async (startingFolder: FolderVO) => await Promise.resolve(new RecordVO({})), }; -@NgModule({ - declarations: [ProfileService], // components your module owns. - imports: [], // other modules your module needs. - providers: [ProfileService], // providers available to your module. - bootstrap: [], // bootstrap this root component. -}) -class DummyModule {} - describe('ProfileService', () => { - let shallow: Shallow; - - beforeEach(() => { - shallow = new Shallow(ProfileService, DummyModule) - .mock(MessageService, { - showError: () => {}, - }) - .provideMock({ provide: ApiService, useValue: mockApiService }) - .provideMock({ + beforeEach(async () => { + await MockBuilder(ProfileService) + .mock(MessageService, { showError: () => {} }) + .provide({ provide: ApiService, useValue: mockApiService }) + .provide({ provide: PrConstantsService, useValue: mockConstantsService, }) - .provideMock({ provide: AccountService, useValue: mockAccountService }) - .provideMock({ + .provide({ provide: AccountService, useValue: mockAccountService }) + .provide({ provide: FolderPickerService, useValue: mockFolderPickerService, - }) - .dontMock(ProfileService); + }); }); - it('should exist', async () => { - const { instance } = shallow.createService(); + it('should exist', () => { + const instance = TestBed.inject(ProfileService); expect(instance).toBeTruthy(); }); - it('should return the correct completion value when not all the fields have a value', async () => { + it('should return the correct completion value when not all the fields have a value', () => { const mockDictionary = { birth_info: [ { @@ -118,7 +104,7 @@ describe('ProfileService', () => { }, ], }; - const { instance } = shallow.createService(); + const instance = TestBed.inject(ProfileService); (instance as any).profileItemDictionary = mockDictionary; const progress = instance.calculateProfileProgress(); @@ -126,7 +112,7 @@ describe('ProfileService', () => { expect(progress).toBe(0.8); }); - it('should 1 when at least one of each profile category has a value', async () => { + it('should 1 when at least one of each profile category has a value', () => { const mockDictionary = { birth_info: [ { @@ -173,7 +159,7 @@ describe('ProfileService', () => { }, ], }; - const { instance } = shallow.createService(); + const instance = TestBed.inject(ProfileService); (instance as any).profileItemDictionary = mockDictionary; const progress = instance.calculateProfileProgress(); @@ -181,9 +167,9 @@ describe('ProfileService', () => { expect(progress).toBe(1); }); - it('should return 0 when no fields have any values', async () => { + it('should return 0 when no fields have any values', () => { const mockDictionary = {}; - const { instance } = shallow.createService(); + const instance = TestBed.inject(ProfileService); (instance as any).profileItemDictionary = mockDictionary; const progress = instance.calculateProfileProgress(); diff --git a/src/app/user-checklist/components/task-icon/task-icon.component.spec.ts b/src/app/user-checklist/components/task-icon/task-icon.component.spec.ts index 160f91287..336aeef42 100644 --- a/src/app/user-checklist/components/task-icon/task-icon.component.spec.ts +++ b/src/app/user-checklist/components/task-icon/task-icon.component.spec.ts @@ -1,47 +1,38 @@ -import { Shallow } from 'shallow-render'; +import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; import { UserChecklistModule } from '../../user-checklist.module'; import { TaskIconComponent } from './task-icon.component'; describe('TaskIconComponent', () => { - let shallow: Shallow; - beforeEach(async () => { - shallow = new Shallow(TaskIconComponent, UserChecklistModule); + await MockBuilder(TaskIconComponent, UserChecklistModule); }); - it('does nothing with no icon input', async () => { - const { find } = await shallow.render(); + it('does nothing with no icon input', () => { + MockRender(TaskIconComponent); - expect(find('.completed').length).toBe(0); - expect(find('svg').length).toBe(0); + expect(ngMocks.findAll('.completed').length).toBe(0); + expect(ngMocks.findAll('svg').length).toBe(0); }); - it('can handle undefined icons', async () => { - const { find } = await shallow.render({ - bind: { icon: 'undefinedIconForUnitTest' }, - }); + it('can handle undefined icons', () => { + MockRender(TaskIconComponent, { icon: 'undefinedIconForUnitTest' }); - expect(find('svg').length).toBe(0); + expect(ngMocks.findAll('svg').length).toBe(0); }); - it('should mark the element as completed if specified in the input', async () => { - const { find } = await shallow.render({ - bind: { completed: true }, - }); + it('should mark the element as completed if specified in the input', () => { + MockRender(TaskIconComponent, { completed: true }); - expect(find('.completed').length).toBe(1); + expect(ngMocks.findAll('.completed').length).toBe(1); }); describe('defined icons', () => { - async function expectIconToHaveDefinedSvg(icon: string) { - const { find } = await shallow.render({ - bind: { - icon, - }, - }); + function expectIconToHaveDefinedSvg(icon: string) { + MockRender(TaskIconComponent, { icon }); - expect(find('svg').length).toBe(1); + expect(ngMocks.findAll('svg').length).toBe(1); } + const icons = [ 'archiveCreated', 'storageRedeemed', @@ -52,8 +43,8 @@ describe('TaskIconComponent', () => { 'publishContent', ]; icons.forEach((icon) => { - it(`has an icon for the "${icon}" item`, async () => { - await expectIconToHaveDefinedSvg(icon); + it(`has an icon for the "${icon}" item`, () => { + expectIconToHaveDefinedSvg(icon); }); }); }); diff --git a/src/app/user-checklist/components/user-checklist/user-checklist.component.spec.ts b/src/app/user-checklist/components/user-checklist/user-checklist.component.spec.ts index 8ca322472..2486cdf2a 100644 --- a/src/app/user-checklist/components/user-checklist/user-checklist.component.spec.ts +++ b/src/app/user-checklist/components/user-checklist/user-checklist.component.spec.ts @@ -1,4 +1,5 @@ -import { Shallow } from 'shallow-render'; +import { TestBed, fakeAsync, flush } from '@angular/core/testing'; +import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; import { CHECKLIST_API } from '../../types/checklist-api'; import { ChecklistItem } from '../../types/checklist-item'; import { UserChecklistModule } from '../../user-checklist.module'; @@ -6,8 +7,6 @@ import { UserChecklistComponent } from './user-checklist.component'; import { DummyChecklistApi } from './shared-mocks'; describe('UserChecklistComponent', () => { - let shallow: Shallow; - function createTestTask(data?: Partial): ChecklistItem { return Object.assign( { @@ -19,215 +18,243 @@ describe('UserChecklistComponent', () => { ); } - function expectComponentToBeInvisible(find) { - expect(find('.user-checklist').length).toBe(0); - expect(find('.user-checklist-minimized').length).toBe(0); + function expectComponentToBeInvisible() { + expect(ngMocks.findAll('.user-checklist').length).toBe(0); + expect(ngMocks.findAll('.user-checklist-minimized').length).toBe(0); } beforeEach(async () => { DummyChecklistApi.reset(); DummyChecklistApi.items = [createTestTask()]; - shallow = new Shallow( - UserChecklistComponent, - UserChecklistModule, - ).provideMock({ + await MockBuilder(UserChecklistComponent, UserChecklistModule).provide({ provide: CHECKLIST_API, useClass: DummyChecklistApi, }); }); - it('should create', async () => { - const { find, instance } = await shallow.render(); + it('should create', () => { + const fixture = MockRender(UserChecklistComponent); - expect(instance).toBeTruthy(); - expect(find('.user-checklist').length).toBeGreaterThan(0); + expect(fixture.point.componentInstance).toBeTruthy(); + expect(ngMocks.findAll('.user-checklist').length).toBeGreaterThan(0); }); - it('should list all tasks received from the API', async () => { + it('should list all tasks received from the API', fakeAsync(() => { DummyChecklistApi.items = [ createTestTask({ title: 'Write a unit test' }), createTestTask({ title: 'Write production code' }), ]; - const { element } = await shallow.render(); + const fixture = MockRender(UserChecklistComponent); + flush(); + fixture.detectChanges(); - expect(element.nativeElement.innerText).toContain('Write a unit test'); - expect(element.nativeElement.innerText).toContain('Write production code'); - }); + expect(fixture.point.nativeElement.innerText).toContain( + 'Write a unit test', + ); - it('should mark completed status on completed tasks', async () => { + expect(fixture.point.nativeElement.innerText).toContain( + 'Write production code', + ); + })); + + it('should mark completed status on completed tasks', fakeAsync(() => { DummyChecklistApi.items = [createTestTask({ completed: true })]; - const { find } = await shallow.render(); + const fixture = MockRender(UserChecklistComponent); + flush(); + fixture.detectChanges(); - expect(find('.task-name.completed').length).toBe(1); - expect(find('.task .fake-checkbox.checked').length).toBeGreaterThan(0); - }); + expect(ngMocks.findAll('.task-name.completed').length).toBe(1); + expect( + ngMocks.findAll('.task .fake-checkbox.checked').length, + ).toBeGreaterThan(0); + })); - it('should not mark completed status on incomplete tasks', async () => { + it('should not mark completed status on incomplete tasks', fakeAsync(() => { DummyChecklistApi.items = [createTestTask({ completed: false })]; - const { find } = await shallow.render(); + const fixture = MockRender(UserChecklistComponent); + flush(); + fixture.detectChanges(); - expect(find('.task-name.completed').length).toBe(0); - expect(find('.task .fake-checkbox.checked').length).toBe(0); - }); + expect(ngMocks.findAll('.task-name.completed').length).toBe(0); + expect(ngMocks.findAll('.task .fake-checkbox.checked').length).toBe(0); + })); - it('should be able to handle an API error', async () => { + it('should be able to handle an API error', fakeAsync(() => { DummyChecklistApi.error = true; - const { fixture, find } = await shallow.render(); - await fixture.whenStable(); + const fixture = MockRender(UserChecklistComponent); + flush(); + fixture.detectChanges(); - expectComponentToBeInvisible(find); - }); + expectComponentToBeInvisible(); + })); - it('can be minimized', async () => { - const { find, fixture } = await shallow.render(); + it('can be minimized', fakeAsync(() => { + const fixture = MockRender(UserChecklistComponent); + flush(); + fixture.detectChanges(); - find('.minimize-button').triggerEventHandler('click'); + ngMocks.find('.minimize-button').triggerEventHandler('click', {}); fixture.detectChanges(); - expect(find('.user-checklist').length).toBe(0); - }); + expect(ngMocks.findAll('.user-checklist').length).toBe(0); + })); - it('can be opened again after being minimized', async () => { - const { find, fixture } = await shallow.render(); + it('can be opened again after being minimized', fakeAsync(() => { + const fixture = MockRender(UserChecklistComponent); + flush(); + fixture.detectChanges(); - find('.minimize-button').triggerEventHandler('click'); + ngMocks.find('.minimize-button').triggerEventHandler('click', {}); fixture.detectChanges(); - find('.open-button').triggerEventHandler('click'); + ngMocks.find('.open-button').triggerEventHandler('click', {}); fixture.detectChanges(); - expect(find('.user-checklist').length).toBeGreaterThan(0); - }); + expect(ngMocks.findAll('.user-checklist').length).toBeGreaterThan(0); + })); - it('is hidden completely if no tasks come back from the API', async () => { + it('is hidden completely if no tasks come back from the API', fakeAsync(() => { DummyChecklistApi.items = []; - const { find } = await shallow.render(); + const fixture = MockRender(UserChecklistComponent); + flush(); + fixture.detectChanges(); - expectComponentToBeInvisible(find); - }); + expectComponentToBeInvisible(); + })); - it('is hidden if the account has the hideChecklist setting enabled', async () => { + it('is hidden if the account has the hideChecklist setting enabled', () => { DummyChecklistApi.accountHidden = true; - const { find } = await shallow.render(); + MockRender(UserChecklistComponent); - expectComponentToBeInvisible(find); + expectComponentToBeInvisible(); }); - it('is hidden if the account does not own the archive', async () => { + it('is hidden if the account does not own the archive', () => { DummyChecklistApi.archiveAccess = 'access.role.viewer'; - const { find } = await shallow.render(); + MockRender(UserChecklistComponent); - expectComponentToBeInvisible(find); + expectComponentToBeInvisible(); }); - it('should hide itself and save account property when clicking the dismiss button', async () => { - const { find, fixture } = await shallow.render(); + it('should hide itself and save account property when clicking the dismiss button', fakeAsync(() => { + const fixture = MockRender(UserChecklistComponent); + flush(); + fixture.detectChanges(); - find('.dont-show-again').triggerEventHandler('click'); - await fixture.whenStable(); + ngMocks.find('.dont-show-again').triggerEventHandler('click', {}); + flush(); fixture.detectChanges(); - expectComponentToBeInvisible(find); + expectComponentToBeInvisible(); expect(DummyChecklistApi.savedAccount).toBeTrue(); - }); + })); it('should fail silently if the account save fails', async () => { - const { instance, inject } = await shallow.render(); + const fixture = MockRender(UserChecklistComponent); + const instance = fixture.point.componentInstance; DummyChecklistApi.error = true; - await expectAsync( - inject(CHECKLIST_API).setChecklistHidden(), - ).toBeRejected(); + const api = TestBed.inject(CHECKLIST_API); + await expectAsync(api.setChecklistHidden()).toBeRejected(); await expectAsync(instance.hideChecklistForever()).not.toBeRejected(); }); - it('should be able to watch for archive info changes and hide when archive is not owned', async () => { - const { find, fixture, inject } = await shallow.render(); + it('should be able to watch for archive info changes and hide when archive is not owned', fakeAsync(() => { + const fixture = MockRender(UserChecklistComponent); + flush(); + fixture.detectChanges(); - await fixture.whenStable(); DummyChecklistApi.archiveAccess = 'access.role.viewer'; - expect(find('.user-checklist').length).toBe(1); + expect(ngMocks.findAll('.user-checklist').length).toBe(1); - const api = inject(CHECKLIST_API) as DummyChecklistApi; + const api = TestBed.inject(CHECKLIST_API) as DummyChecklistApi; api.sendArchiveChangeEvent(); fixture.detectChanges(); - expectComponentToBeInvisible(find); - }); + expectComponentToBeInvisible(); + })); - it('should be able to watch for archive info changes and show when archive is owned', async () => { + it('should be able to watch for archive info changes and show when archive is owned', fakeAsync(() => { DummyChecklistApi.archiveAccess = 'access.role.viewer'; - const { find, fixture, inject } = await shallow.render(); - - await fixture.whenStable(); + const fixture = MockRender(UserChecklistComponent); + flush(); + fixture.detectChanges(); DummyChecklistApi.archiveAccess = 'access.role.owner'; - expectComponentToBeInvisible(find); + expectComponentToBeInvisible(); - const api = inject(CHECKLIST_API) as DummyChecklistApi; + const api = TestBed.inject(CHECKLIST_API) as DummyChecklistApi; api.sendArchiveChangeEvent(); + flush(); fixture.detectChanges(); - expect(find('.user-checklist').length).toBe(1); - }); + expect(ngMocks.findAll('.user-checklist').length).toBe(1); + })); - it('unsubscribes from archive change subscriptions on destroy', async () => { - const { instance, inject } = await shallow.render(); + it('unsubscribes from archive change subscriptions on destroy', fakeAsync(() => { + const fixture = MockRender(UserChecklistComponent); + const instance = fixture.point.componentInstance; + flush(); instance.ngOnDestroy(); DummyChecklistApi.archiveAccess = 'access.role.viewer'; - const api = inject(CHECKLIST_API) as DummyChecklistApi; + const api = TestBed.inject(CHECKLIST_API) as DummyChecklistApi; api.sendArchiveChangeEvent(); expect(instance.isDisplayed).toBeTruthy(); - }); + })); - it('should refresh the checklist when a refresh event fires', async () => { - const { fixture, instance, inject } = await shallow.render(); + it('should refresh the checklist when a refresh event fires', fakeAsync(() => { + const fixture = MockRender(UserChecklistComponent); + const instance = fixture.point.componentInstance; + flush(); DummyChecklistApi.items = [ createTestTask({ id: 'refresh', title: 'Refresh the checklist' }), ]; - const api = inject(CHECKLIST_API) as DummyChecklistApi; + const api = TestBed.inject(CHECKLIST_API) as DummyChecklistApi; api.sendRefreshEvent(); - await fixture.whenStable(); + flush(); expect(instance.items[0].id).toBe('refresh'); - }); + })); - it('unsubscribes from refresh subscriptions on destroy', async () => { - const { fixture, instance, inject } = await shallow.render(); + it('unsubscribes from refresh subscriptions on destroy', fakeAsync(() => { + const fixture = MockRender(UserChecklistComponent); + const instance = fixture.point.componentInstance; + flush(); instance.ngOnDestroy(); DummyChecklistApi.items = [ createTestTask({ id: 'refresh', title: 'Refresh the checklist' }), ]; - const api = inject(CHECKLIST_API) as DummyChecklistApi; + const api = TestBed.inject(CHECKLIST_API) as DummyChecklistApi; api.sendRefreshEvent(); - await fixture.whenStable(); + flush(); expect(instance.items[0].id).not.toBe('refresh'); - }); + })); - it('should not throw errors if subscriptions are undefined', async () => { + it('should not throw errors if subscriptions are undefined', () => { DummyChecklistApi.accountHidden = true; - const { instance } = await shallow.render(); + const fixture = MockRender(UserChecklistComponent); + const instance = fixture.point.componentInstance; expect(() => instance.ngOnDestroy()).not.toThrow(); }); describe('Percentage completion', () => { - async function expectPercentage( + function expectPercentage( completed: number, incomplete: number, percentage: number, @@ -239,31 +266,34 @@ describe('UserChecklistComponent', () => { for (let i = 0; i < incomplete; i += 1) { DummyChecklistApi.items.push(createTestTask({ completed: false })); } - const { find } = await shallow.render(); + const fixture = MockRender(UserChecklistComponent); + flush(); + fixture.detectChanges(); + const meterValue = parseFloat( - find('.meter-value').nativeElement.style.width, + ngMocks.find('.meter-value').nativeElement.style.width, ); expect(Math.round(meterValue)).toBe(percentage); - expect(find('.percent-value').nativeElement.innerText).toContain( + expect(ngMocks.find('.percent-value').nativeElement.innerText).toContain( `${percentage}%`, ); } - it('should list 0% for no tasks done', async () => { - await expectPercentage(0, 1, 0); - }); + it('should list 0% for no tasks done', fakeAsync(() => { + expectPercentage(0, 1, 0); + })); - it('should list 100% for all tasks done', async () => { - await expectPercentage(1, 0, 100); - }); + it('should list 100% for all tasks done', fakeAsync(() => { + expectPercentage(1, 0, 100); + })); - it('should round down to whole numbers', async () => { - await expectPercentage(1, 6, 14); - }); + it('should round down to whole numbers', fakeAsync(() => { + expectPercentage(1, 6, 14); + })); - it('should round up to whole numbers if nearest', async () => { - await expectPercentage(6, 1, 86); - }); + it('should round up to whole numbers if nearest', fakeAsync(() => { + expectPercentage(6, 1, 86); + })); }); }); diff --git a/src/test.ts b/src/test.ts index 985b77b45..084be7ef8 100644 --- a/src/test.ts +++ b/src/test.ts @@ -6,7 +6,7 @@ import { BrowserDynamicTestingModule, platformBrowserDynamicTesting, } from '@angular/platform-browser-dynamic/testing'; -import { Shallow } from 'shallow-render'; +import { ngMocks } from 'ng-mocks'; import { RouterModule } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { @@ -16,7 +16,8 @@ import { NgbTimepickerModule, NgbTooltipModule, } from '@ng-bootstrap/ng-bootstrap'; -import { CountUpDirective } from 'ngx-countup'; +import { CountUpDirective, CountUpModule } from 'ngx-countup'; +import { installNgMocksMatchers } from './test/ng-mocks-matchers'; window.Stripe = () => ({ elements: () => ({ @@ -36,16 +37,26 @@ window.doNotLoadGoogleMapsAPI = true; // Disable loading of MixPanel +// Configure ng-mocks auto spy for Jasmine +ngMocks.autoSpy('jasmine'); + +// Allow respy for Jasmine (needed for some test patterns) +jasmine.getEnv().allowRespy(true); + // Always Replace RouterModule with RouterTestingModule to avoid errors. -Shallow.alwaysReplaceModule(RouterModule, RouterTestingModule); -Shallow.neverMock( - NgbDatepickerModule, - NgbTimepickerModule, - NgbTooltipModule, - NgbDropdownModule, - NgbPaginationModule, - CountUpDirective, -); +ngMocks.globalReplace(RouterModule, RouterTestingModule); + +// Never mock these modules/directives (keep them real) +ngMocks.globalKeep(NgbDatepickerModule); +ngMocks.globalKeep(NgbTimepickerModule); +ngMocks.globalKeep(NgbTooltipModule); +ngMocks.globalKeep(NgbDropdownModule); +ngMocks.globalKeep(NgbPaginationModule); +ngMocks.globalKeep(CountUpDirective); +ngMocks.globalKeep(CountUpModule); + +// Install custom Jasmine matchers for ng-mocks compatibility +installNgMocksMatchers(); // First, initialize the Angular testing environment. getTestBed().initTestEnvironment( diff --git a/src/test/ng-mocks-matchers.ts b/src/test/ng-mocks-matchers.ts new file mode 100644 index 000000000..2510816dc --- /dev/null +++ b/src/test/ng-mocks-matchers.ts @@ -0,0 +1,84 @@ +/** + * Custom Jasmine matchers for ng-mocks migration + * These provide compatibility with shallow-render's toHaveFound/toHaveFoundOne matchers + */ + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace -- TypeScript requires namespace syntax for augmenting global types like jasmine.Matchers + namespace jasmine { + // eslint-disable-next-line @typescript-eslint/no-unused-vars -- Generic type T is required to match Jasmine's Matchers interface signature + interface Matchers { + toHaveFoundOne(): boolean; + toHaveFound(expected: number): boolean; + } + } +} + +export const ngMocksCustomMatchers: jasmine.CustomMatcherFactories = { + toHaveFoundOne(): jasmine.CustomMatcher { + return { + compare(actual: unknown): jasmine.CustomMatcherResult { + let count: number; + if (Array.isArray(actual)) { + count = actual.length; + } else if (actual && typeof actual === 'object' && 'length' in actual) { + count = (actual as { length: number }).length; + } else { + count = actual ? 1 : 0; + } + const pass = count === 1; + return { + pass, + message: pass + ? `Expected not to find exactly one element, but found ${count}` + : `Expected to find exactly one element, but found ${count}`, + }; + }, + negativeCompare(actual: unknown): jasmine.CustomMatcherResult { + let count: number; + if (Array.isArray(actual)) { + count = actual.length; + } else if (actual && typeof actual === 'object' && 'length' in actual) { + count = (actual as { length: number }).length; + } else { + count = actual ? 1 : 0; + } + const pass = count !== 1; + return { + pass, + message: pass + ? `Expected to find exactly one element, but found ${count}` + : `Expected not to find exactly one element`, + }; + }, + }; + }, + + toHaveFound(): jasmine.CustomMatcher { + return { + compare(actual: unknown, expected: number): jasmine.CustomMatcherResult { + let count: number; + if (Array.isArray(actual)) { + count = actual.length; + } else if (actual && typeof actual === 'object' && 'length' in actual) { + count = (actual as { length: number }).length; + } else { + count = actual ? 1 : 0; + } + const pass = count === expected; + return { + pass, + message: pass + ? `Expected not to find ${expected} elements, but found ${count}` + : `Expected to find ${expected} elements, but found ${count}`, + }; + }, + }; + }, +}; + +export function installNgMocksMatchers(): void { + beforeEach(() => { + jasmine.addMatchers(ngMocksCustomMatchers); + }); +}