diff --git a/projects/element-ng/autocomplete/si-autocomplete.directive.spec.ts b/projects/element-ng/autocomplete/si-autocomplete.directive.spec.ts index 073337caa..d9b6fd918 100644 --- a/projects/element-ng/autocomplete/si-autocomplete.directive.spec.ts +++ b/projects/element-ng/autocomplete/si-autocomplete.directive.spec.ts @@ -57,6 +57,7 @@ describe('SiAutocompleteDirective', () => { afterEach(() => { vi.useRealTimers(); + vi.restoreAllMocks(); }); it('should be navigable', async () => { diff --git a/projects/element-ng/circle-status/si-circle-status.component.spec.ts b/projects/element-ng/circle-status/si-circle-status.component.spec.ts index d958c488a..5d51ee1df 100644 --- a/projects/element-ng/circle-status/si-circle-status.component.spec.ts +++ b/projects/element-ng/circle-status/si-circle-status.component.spec.ts @@ -40,6 +40,10 @@ describe('SiCircleStatusComponent', () => { element = fixture.nativeElement; }); + afterEach(() => { + vi.useRealTimers(); + }); + it('should not set icon class, if no icon is configured', async () => { icon.set(undefined); await fixture.whenStable(); diff --git a/projects/element-ng/column-selection-dialog/si-column-selection-dialog.component.spec.ts b/projects/element-ng/column-selection-dialog/si-column-selection-dialog.component.spec.ts index d82dea3a9..8cff9e789 100644 --- a/projects/element-ng/column-selection-dialog/si-column-selection-dialog.component.spec.ts +++ b/projects/element-ng/column-selection-dialog/si-column-selection-dialog.component.spec.ts @@ -5,6 +5,7 @@ import { inputBinding, signal, twoWayBinding, WritableSignal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ModalRef } from '@siemens/element-ng/modal'; +import { afterEach } from 'vitest'; import { SiColumnSelectionDialogComponent } from './si-column-selection-dialog.component'; import { Column } from './si-column-selection-dialog.types'; @@ -86,6 +87,8 @@ describe('ColumnDialogComponent', () => { element = fixture.nativeElement; }); + afterEach(() => vi.useRealTimers()); + it('should create', async () => { columns.set(cloneData()); await fixture.whenStable(); @@ -257,20 +260,21 @@ describe('ColumnDialogComponent', () => { it('should toggle edit mode with keyboard', async () => { const spy = vi.spyOn(modalRef.hidden, 'next'); + vi.useFakeTimers(); columns.set(cloneData()); - fixture.autoDetectChanges(); + await fixture.whenStable(); document .querySelector('si-column-selection-editor')! .dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' })); + vi.advanceTimersToNextFrame(); await fixture.whenStable(); expect(spy).not.toHaveBeenCalled(); const inputField = document.querySelector( 'si-column-selection-editor input.form-control' )!; expect(inputField).toBeInTheDocument(); - // Wait for setTimeout in startEdit() to complete - await new Promise(resolve => setTimeout(resolve, 100)); inputField.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })); + vi.advanceTimersToNextFrame(); await fixture.whenStable(); expect( document.querySelector('si-column-selection-editor input.form-control') diff --git a/projects/element-ng/content-action-bar/si-content-action-bar.component.spec.ts b/projects/element-ng/content-action-bar/si-content-action-bar.component.spec.ts index 89dad3297..e203c1287 100644 --- a/projects/element-ng/content-action-bar/si-content-action-bar.component.spec.ts +++ b/projects/element-ng/content-action-bar/si-content-action-bar.component.spec.ts @@ -116,9 +116,21 @@ describe('SiContentActionBarComponent', () => { // cannot use jasmine.clock here await new Promise(resolve => setTimeout(resolve, 50)); expect(await harness.isPrimaryExpanded()).toBe(false); - await harness.togglePrimary(); - await fixture.whenStable(); - expect(await harness.isPrimaryExpanded()).toBe(true); + + // Click the primary toggle button directly instead of going through harness.togglePrimary() + // to avoid CDK harness stability checks that can hang due to ResizeObserver + auditTime(0) feedback loop. + const toggleButton = fixture.nativeElement.querySelector( + 'button[si-content-action-bar-toggle]:not(.cdk-menu-trigger)' + ) as HTMLElement; + toggleButton.click(); + fixture.detectChanges(); + // Wait for the setTimeout in expand() and ResizeObserver cycle to settle + await new Promise(resolve => setTimeout(resolve, 100)); + fixture.detectChanges(); + const listItem = fixture.nativeElement.querySelector( + '[siAutoCollapsableListItem]' + ) as HTMLElement; + expect(getComputedStyle(listItem).visibility).toBe('visible'); }); it('should disable menu item by disabled attribute', async () => { diff --git a/projects/element-ng/datatable/si-datatable-interaction.directive.spec.ts b/projects/element-ng/datatable/si-datatable-interaction.directive.spec.ts index 9c789a858..3972a123f 100644 --- a/projects/element-ng/datatable/si-datatable-interaction.directive.spec.ts +++ b/projects/element-ng/datatable/si-datatable-interaction.directive.spec.ts @@ -276,12 +276,14 @@ describe('SiDatatableInteractionDirective', () => { await refresh(); - expect(wrapperComponent.selected).toContain({ - id: 1, - firstname: 'First 1', - lastname: 'Last 1', - age: 50 - }); + expect(wrapperComponent.selected).toContainEqual( + expect.objectContaining({ + id: 1, + firstname: 'First 1', + lastname: 'Last 1', + age: 50 + }) + ); } }); @@ -317,12 +319,14 @@ describe('SiDatatableInteractionDirective', () => { await refresh(); - expect(wrapperComponent.selected).toContain({ - id: 1, - firstname: 'First 1', - lastname: 'Last 1', - age: 50 - }); + expect(wrapperComponent.selected).toContainEqual( + expect.objectContaining({ + id: 1, + firstname: 'First 1', + lastname: 'Last 1', + age: 50 + }) + ); } }); }); diff --git a/projects/element-ng/date-range-filter/__screenshots__/si-date-range-calculation.service.spec.ts/SiDateRangeCalculationService-resolves-with-before-and-now-as-reference-1.png b/projects/element-ng/date-range-filter/__screenshots__/si-date-range-calculation.service.spec.ts/SiDateRangeCalculationService-resolves-with-before-and-now-as-reference-1.png new file mode 100644 index 000000000..8d0764546 Binary files /dev/null and b/projects/element-ng/date-range-filter/__screenshots__/si-date-range-calculation.service.spec.ts/SiDateRangeCalculationService-resolves-with-before-and-now-as-reference-1.png differ diff --git a/projects/element-ng/date-range-filter/si-date-range-calculation.service.spec.ts b/projects/element-ng/date-range-filter/si-date-range-calculation.service.spec.ts index b3bbcb08e..a38de14ac 100644 --- a/projects/element-ng/date-range-filter/si-date-range-calculation.service.spec.ts +++ b/projects/element-ng/date-range-filter/si-date-range-calculation.service.spec.ts @@ -18,12 +18,17 @@ describe('SiDateRangeCalculationService', () => { expect(formatDate(d1, 'dateShort', 'en')).toEqual(formatDate(d2, 'dateShort', 'en')); beforeEach(() => { + vi.useFakeTimers(); TestBed.configureTestingModule({ providers: [SiDateRangeCalculationService] }); service = TestBed.inject(SiDateRangeCalculationService); }); + afterEach(() => { + vi.useRealTimers(); + }); + it('resolves with before', () => { const filter: DateRangeFilter = { point1: new Date('2023-07-15'), diff --git a/projects/element-ng/datepicker/components/si-day-selection.component.spec.ts b/projects/element-ng/datepicker/components/si-day-selection.component.spec.ts index f92a65406..3b39e6077 100644 --- a/projects/element-ng/datepicker/components/si-day-selection.component.spec.ts +++ b/projects/element-ng/datepicker/components/si-day-selection.component.spec.ts @@ -25,6 +25,10 @@ describe('SiDaySelectionComponent', () => { let fixture: ComponentFixture; let helper: CalendarTestHelper; + afterEach(() => { + vi.restoreAllMocks(); + }); + const selectDate = (date: number): void => { helper.clickEnabledCell(date.toString()); fixture.detectChanges(); diff --git a/projects/element-ng/datepicker/si-calendar-button.component.spec.ts b/projects/element-ng/datepicker/si-calendar-button.component.spec.ts index 3bcf772e6..7d0439c19 100644 --- a/projects/element-ng/datepicker/si-calendar-button.component.spec.ts +++ b/projects/element-ng/datepicker/si-calendar-button.component.spec.ts @@ -52,6 +52,10 @@ describe('SiCalendarButtonComponent', () => { fixture.detectChanges(); }); + afterEach(() => { + vi.useRealTimers(); + }); + it('should show datepicker overlay', async () => { calendarToggleButton().click(); fixture.detectChanges(); @@ -72,11 +76,12 @@ describe('SiCalendarButtonComponent', () => { expect(spy).toHaveBeenCalled(); }); - it('should mark as touched if button is blurred', () => { + it('should mark as touched if button is blurred', async () => { const touchSpy = vi.spyOn(SiDatepickerDirective.prototype, 'touch'); const button = calendarToggleButton(); button.focus(); button.blur(); + await fixture.whenStable(); expect(touchSpy).toHaveBeenCalled(); }); diff --git a/projects/element-ng/datepicker/si-datepicker.component.spec.ts b/projects/element-ng/datepicker/si-datepicker.component.spec.ts index a825fd620..c1287e7b7 100644 --- a/projects/element-ng/datepicker/si-datepicker.component.spec.ts +++ b/projects/element-ng/datepicker/si-datepicker.component.spec.ts @@ -64,6 +64,10 @@ describe('SiDatepickerComponent', () => { picker = await loader.getHarness(SiDatepickerComponentHarness); }); + afterEach(() => { + vi.useRealTimers(); + }); + it('should includeTimeLabel matches enabledTimeText', async () => { const enabledTimeText = 'Consider Time'; await updateConfig({ ...component.config(), enabledTimeText, showTime: true }); diff --git a/projects/element-ng/file-uploader/si-file-uploader.component.spec.ts b/projects/element-ng/file-uploader/si-file-uploader.component.spec.ts index 2e187b750..de5872d27 100644 --- a/projects/element-ng/file-uploader/si-file-uploader.component.spec.ts +++ b/projects/element-ng/file-uploader/si-file-uploader.component.spec.ts @@ -108,6 +108,10 @@ describe('SiFileUploaderComponent', () => { element = fixture.nativeElement; }); + afterEach(() => { + vi.useRealTimers(); + }); + const createFileList = (files: string[], type?: string[]): DataTransfer => { const dt = new DataTransfer(); files.forEach((f, i) => dt.items.add(new File(['blub'], f, { type: type?.[i] }))); diff --git a/projects/element-ng/filtered-search/si-filtered-search.component.spec.ts b/projects/element-ng/filtered-search/si-filtered-search.component.spec.ts index 913a70471..011a7e335 100644 --- a/projects/element-ng/filtered-search/si-filtered-search.component.spec.ts +++ b/projects/element-ng/filtered-search/si-filtered-search.component.spec.ts @@ -597,14 +597,13 @@ describe('SiFilteredSearchComponent', () => { await criterionValue?.sendKeys(TestKey.BACKSPACE); vi.useRealTimers(); - // needed to avoid flaky test await new Promise(resolve => setTimeout(resolve, 0)); - vi.useFakeTimers(); await filteredSearch.freeTextSearch().then(async freeTextSearch => { await freeTextSearch.focus(); }); - await tick(); + await new Promise(resolve => setTimeout(resolve, 100)); + await fixture.whenStable(); expect(await criterionValue?.isEditable()).toBeFalsy(); }); @@ -2484,12 +2483,11 @@ describe('SiFilteredSearchComponent - With translation', () => { await tick(); await value!.sendKeys('broken-format'); vi.useRealTimers(); - // needed to avoid flaky test await new Promise(resolve => setTimeout(resolve, 0)); - vi.useFakeTimers(); await value!.blur(); await filteredSearch.clickSearchButton(); - await tick(); + await new Promise(resolve => setTimeout(resolve, 100)); + await fixture.whenStable(); expect(await value!.text()).toBe('Invalid Date'); expect(spy).toHaveBeenCalledWith({ criteria: [ diff --git a/projects/element-ng/form/si-form-container/si-form-container.component.spec.ts b/projects/element-ng/form/si-form-container/si-form-container.component.spec.ts index 64d812e78..52b23f6ec 100644 --- a/projects/element-ng/form/si-form-container/si-form-container.component.spec.ts +++ b/projects/element-ng/form/si-form-container/si-form-container.component.spec.ts @@ -167,6 +167,7 @@ describe('SiFormContainerComponent', () => { afterEach(() => { vi.useRealTimers(); + vi.restoreAllMocks(); }); it('should create', async () => { diff --git a/projects/element-ng/formly/fields/number/si-formly-number.component.spec.ts b/projects/element-ng/formly/fields/number/si-formly-number.component.spec.ts index 96e64ad8c..348d0f6ce 100644 --- a/projects/element-ng/formly/fields/number/si-formly-number.component.spec.ts +++ b/projects/element-ng/formly/fields/number/si-formly-number.component.spec.ts @@ -57,6 +57,10 @@ describe('formly number type', () => { fixture = TestBed.createComponent(FormlyTestComponent); }); + afterEach(() => { + vi.useRealTimers(); + }); + it('should display the number input based on props provided', async () => { vi.useFakeTimers(); const componentInstance = fixture.componentInstance; diff --git a/projects/element-ng/landing-page/change-password/si-change-password.component.spec.ts b/projects/element-ng/landing-page/change-password/si-change-password.component.spec.ts index ef80725b2..b7122ea70 100644 --- a/projects/element-ng/landing-page/change-password/si-change-password.component.spec.ts +++ b/projects/element-ng/landing-page/change-password/si-change-password.component.spec.ts @@ -21,16 +21,16 @@ describe('SiChangePasswordComponent', () => { let fixture: ComponentFixture; let element: HTMLElement; - const passwordPolicyContent = signal('Policy content'); - const passwordStrength = signal(passwordStrengthValue); - const newPasswordLabel = signal(''); - const confirmPasswordLabel = signal(''); - const changeButtonLabel = signal(''); - const backButtonLabel = signal(''); - const disableChange = signal(false); - const passwordPolicyTitle = signal(''); - const changePasswordRequested = vi.fn<(value: ChangePassword) => void>(); - const back = vi.fn(); + let passwordPolicyContent = signal('Policy content'); + let passwordStrength = signal(passwordStrengthValue); + let newPasswordLabel = signal(''); + let confirmPasswordLabel = signal(''); + let changeButtonLabel = signal(''); + let backButtonLabel = signal(''); + let disableChange = signal(false); + let passwordPolicyTitle = signal(''); + let changePasswordRequested = vi.fn<(value: ChangePassword) => void>(); + let back = vi.fn(); const enterValue = (input: HTMLInputElement, value: string): void => { input.value = value; @@ -38,6 +38,16 @@ describe('SiChangePasswordComponent', () => { }; beforeEach(async () => { + passwordPolicyContent = signal('Policy content'); + passwordStrength = signal(passwordStrengthValue); + newPasswordLabel = signal(''); + confirmPasswordLabel = signal(''); + changeButtonLabel = signal(''); + backButtonLabel = signal(''); + disableChange = signal(false); + passwordPolicyTitle = signal(''); + changePasswordRequested = vi.fn<(value: ChangePassword) => void>(); + back = vi.fn(); fixture = TestBed.createComponent(TestComponent, { bindings: [ inputBinding('passwordPolicyContent', passwordPolicyContent), diff --git a/projects/element-ng/landing-page/login-single-sign-on/si-login-single-sign-on.component.spec.ts b/projects/element-ng/landing-page/login-single-sign-on/si-login-single-sign-on.component.spec.ts index 9075cbec6..555938ad6 100644 --- a/projects/element-ng/landing-page/login-single-sign-on/si-login-single-sign-on.component.spec.ts +++ b/projects/element-ng/landing-page/login-single-sign-on/si-login-single-sign-on.component.spec.ts @@ -10,11 +10,12 @@ import { SiLoginSingleSignOnComponent as TestComponent } from './si-login-single describe('SiLoginSingleSignOnComponent', () => { let fixture: ComponentFixture; let element: HTMLElement; - - const disableSso = signal(false); - const ssoEvent = vi.fn(); + let disableSso = signal(false); + let ssoEvent = vi.fn(); beforeEach(async () => { + disableSso = signal(false); + ssoEvent = vi.fn(); fixture = TestBed.createComponent(TestComponent, { bindings: [inputBinding('disableSso', disableSso), outputBinding('ssoEvent', ssoEvent)] }); diff --git a/projects/element-ng/list-details/si-list-details.component.spec.ts b/projects/element-ng/list-details/si-list-details.component.spec.ts index d8b146b0b..d77ec3a4f 100644 --- a/projects/element-ng/list-details/si-list-details.component.spec.ts +++ b/projects/element-ng/list-details/si-list-details.component.spec.ts @@ -85,10 +85,10 @@ describe('ListDetailsComponent', () => { const viewportHeight = window.innerHeight || document.documentElement.clientHeight; const bounding = elem.getBoundingClientRect(); return ( - bounding.top >= 0 && - bounding.left >= 0 && - bounding.bottom <= viewportHeight && - bounding.right <= viewportWidth + Math.round(bounding.top) >= 0 && + Math.round(bounding.left) >= 0 && + Math.round(bounding.bottom) <= viewportHeight && + Math.round(bounding.right) <= viewportWidth ); }; const drag = ( @@ -419,6 +419,10 @@ describe('ListDetailsComponent', () => { }); }); + afterEach(() => { + vi.restoreAllMocks(); + }); + it('should navigate back and forward in mobile mode by clicking', async () => { vi.spyOn(ResizeObserverService.prototype, 'observe').mockReturnValue( of({ width: 100, height: 100 }) diff --git a/projects/element-ng/loading-spinner/si-loading-spinner.directive.spec.ts b/projects/element-ng/loading-spinner/si-loading-spinner.directive.spec.ts index ae6c974c8..0b9fec5cf 100644 --- a/projects/element-ng/loading-spinner/si-loading-spinner.directive.spec.ts +++ b/projects/element-ng/loading-spinner/si-loading-spinner.directive.spec.ts @@ -69,6 +69,7 @@ describe('SiLoadingSpinnerDirective', () => { }); it('should show and hide spinner', async () => { + await fixture.whenStable(); await vi.advanceTimersByTimeAsync(initialDelay); await vi.advanceTimersByTimeAsync(initialDelay); expect(isLoading()).toBe(true); diff --git a/projects/element-ng/number-input/si-number-input.component.spec.ts b/projects/element-ng/number-input/si-number-input.component.spec.ts index da848b0bc..239ef2c4f 100644 --- a/projects/element-ng/number-input/si-number-input.component.spec.ts +++ b/projects/element-ng/number-input/si-number-input.component.spec.ts @@ -93,6 +93,10 @@ describe('SiNumberInputComponent', () => { element = fixture.nativeElement; }); + afterEach(() => { + vi.useRealTimers(); + }); + it('should support short press increments', async () => { value.set(50); await fixture.whenStable(); diff --git a/projects/element-ng/photo-upload/si-photo-upload.component.spec.ts b/projects/element-ng/photo-upload/si-photo-upload.component.spec.ts index 7cd33b418..46f530479 100644 --- a/projects/element-ng/photo-upload/si-photo-upload.component.spec.ts +++ b/projects/element-ng/photo-upload/si-photo-upload.component.spec.ts @@ -16,22 +16,19 @@ describe(`SiPhotoUploadComponent`, () => { let componentRef: ComponentRef; let fixture: ComponentFixture; let callback: (e: any) => void; - let originalFileReader: typeof FileReader; const mockFileReader = (data: string): any => { - const readerSpy = { - addEventListener: vi.fn((event: string, cb: (e: any) => void) => (callback = cb)), - readAsDataURL: vi.fn(() => callback(data)), - result: data - }; - - originalFileReader = window.FileReader; - window.FileReader = class { - addEventListener = readerSpy.addEventListener; - readAsDataURL = readerSpy.readAsDataURL; - result = readerSpy.result; - } as any; - return readerSpy; + vi.spyOn(window, 'FileReader').mockImplementation( + class MockFileReader implements Partial { + addEventListener = (event: unknown, cb: (e: Event) => void): void => { + callback = cb; + }; + readAsDataURL = (blob: Blob): void => { + callback(blob); + }; + result = data; + } as typeof FileReader + ); }; const generateImage = (): File => { @@ -60,11 +57,7 @@ describe(`SiPhotoUploadComponent`, () => { componentRef = fixture.componentRef; }); - afterEach(() => { - if (originalFileReader) { - window.FileReader = originalFileReader; - } - }); + afterEach(() => vi.resetAllMocks()); it('should display placeholder', () => { componentRef.setInput('placeholderAltText', 'MX'); @@ -143,13 +136,12 @@ describe(`SiPhotoUploadComponent`, () => { await fixture.whenStable(); const modal = document.querySelector('si-modal'); expect(modal).toBeInTheDocument(); - // cannot use vi.useFakeTimers here - await new Promise(resolve => setTimeout(resolve, 100)); // Allow cropping lib to process - expect(modal?.querySelector('img')).toBeInTheDocument(); + // Wait for the cropping lib to render the image + await expect.poll(() => modal?.querySelector('img')).toBeTruthy(); }); it('should apply photo', async () => { - fixture.detectChanges(); + await fixture.whenStable(); const input = fixture.debugElement.query(By.css('input[type="file"]')); vi.spyOn(input.nativeElement, 'click').mockImplementation(() => { input.triggerEventHandler('change', { @@ -162,17 +154,16 @@ describe(`SiPhotoUploadComponent`, () => { fixture.debugElement.query(By.css('button'))!.nativeElement.click(); - fixture.detectChanges(); await fixture.whenStable(); - // cannot use vi.useFakeTimers here - await new Promise(resolve => setTimeout(resolve, 100)); // Allow cropping lib to process + // Wait for the cropping lib to render the image + await expect.poll(() => document.querySelector('si-modal img')).toBeTruthy(); getButton(document.querySelector('si-modal')!, 'Apply').click(); - fixture.detectChanges(); await fixture.whenStable(); - expect(fixture.debugElement.query(By.css('img'))!.attributes.src).toBeTruthy(); + // Wait for the cropped image to be processed and rendered + await expect.poll(() => fixture.nativeElement.querySelector('img')?.src).toBeTruthy(); }); it('should apply photo without modal when disabledCropping = true', async () => { diff --git a/projects/element-ng/pills-input/si-pills-input.component.spec.ts b/projects/element-ng/pills-input/si-pills-input.component.spec.ts index 677de6bed..b4174d8f6 100644 --- a/projects/element-ng/pills-input/si-pills-input.component.spec.ts +++ b/projects/element-ng/pills-input/si-pills-input.component.spec.ts @@ -125,6 +125,10 @@ describe('SiPillsInputComponent', () => { }); describe('with csv input handler', () => { + afterEach(() => { + vi.useRealTimers(); + }); + it('should update on input with separator', async () => { csvInputElement.value = 'a'; csvInputElement.dispatchEvent(new InputEvent('input')); diff --git a/projects/element-ng/popover/si-popover.directive.spec.ts b/projects/element-ng/popover/si-popover.directive.spec.ts index f79926817..4151bbc5c 100644 --- a/projects/element-ng/popover/si-popover.directive.spec.ts +++ b/projects/element-ng/popover/si-popover.directive.spec.ts @@ -50,6 +50,10 @@ describe('SiPopoverNextDirective', () => { wrapperComponent = fixture.componentInstance; }); + afterEach(() => { + vi.useRealTimers(); + }); + it('should open/close on click', async () => { vi.useFakeTimers({ shouldAdvanceTime: true }); fixture.detectChanges(); @@ -177,6 +181,10 @@ describe('with custom template', () => { fixture = TestBed.createComponent(CustomTemplateHostComponent); }); + afterEach(() => { + vi.useRealTimers(); + }); + it('should focus on the first interactive element', async () => { vi.useFakeTimers({ shouldAdvanceTime: true }); fixture.detectChanges(); diff --git a/projects/element-ng/resize-observer/resize-observer.service.spec.ts b/projects/element-ng/resize-observer/resize-observer.service.spec.ts index 38adea52e..ba704de5a 100644 --- a/projects/element-ng/resize-observer/resize-observer.service.spec.ts +++ b/projects/element-ng/resize-observer/resize-observer.service.spec.ts @@ -23,12 +23,6 @@ class TestHostComponent { readonly height = signal(100); } -// A timeout that works with `await`. We have to use `waitForAsync()`` -// in the tests below because `tick()` doesn't work because `ResizeObserver` -// operates outside of the zone -const timeout = async (ms: number): Promise => - new Promise(resolve => setTimeout(resolve, ms)); - describe('ResizeObserverService', () => { let fixture: ComponentFixture; let component: TestHostComponent; @@ -51,6 +45,7 @@ describe('ResizeObserverService', () => { }; beforeEach(() => { + vi.useFakeTimers(); mockResizeObserver(); service = TestBed.inject(ResizeObserverService); fixture = TestBed.createComponent(TestHostComponent); @@ -63,71 +58,63 @@ describe('ResizeObserverService', () => { afterEach(() => { subscription?.unsubscribe(); restoreResizeObserver(); + vi.useRealTimers(); }); - it('emits initial size event when asked', async () => { + it('emits initial size event when asked', () => { subscribe(true); - await timeout(10); + vi.advanceTimersByTime(10); expect(spy).toHaveBeenCalledWith(expect.objectContaining({ width: 100, height: 100 })); }); - it('emits no initial size event when not asked', async () => { + it('emits no initial size event when not asked', () => { subscribe(false); - await timeout(10); + vi.advanceTimersByTime(10); expect(spy).not.toHaveBeenCalled(); }); - it('emits on width change', async () => { + it('emits on width change', () => { subscribe(false); detectSizeChange(200, 100); - // Skip test when browser is not focussed to prevent failures. - if (document.hasFocus()) { - // with throttling, this shouldn't fire just yet - await timeout(20); - expect(spy).not.toHaveBeenCalled(); + // with throttling, this shouldn't fire just yet + vi.advanceTimersByTime(20); + expect(spy).not.toHaveBeenCalled(); - await timeout(150); - expect(spy).toHaveBeenCalledWith(expect.objectContaining({ width: 200, height: 100 })); - } + vi.advanceTimersByTime(50); + expect(spy).toHaveBeenCalledWith(expect.objectContaining({ width: 200, height: 100 })); }); - it('emits on height change', async () => { + it('emits on height change', () => { subscribe(false); detectSizeChange(100, 200); - // Skip test when browser is not focussed to prevent failures. - if (document.hasFocus()) { - // with throttling, this shouldn't fire just yet - await timeout(20); - expect(spy).not.toHaveBeenCalled(); + // with throttling, this shouldn't fire just yet + vi.advanceTimersByTime(20); + expect(spy).not.toHaveBeenCalled(); - await timeout(150); - expect(spy).toHaveBeenCalledWith(expect.objectContaining({ width: 100, height: 200 })); - } + vi.advanceTimersByTime(50); + expect(spy).toHaveBeenCalledWith(expect.objectContaining({ width: 100, height: 200 })); }); - it('can handle multiple subscriptions on same element', async () => { + it('can handle multiple subscriptions on same element', () => { subscribe(true); - // Skip test when browser is not focussed to prevent failures. - if (document.hasFocus()) { - const spy2: Mock<(dim: ElementDimensions) => void> = vi.fn(); - const subs2 = service - .observe(component.theDiv().nativeElement, 50, true) - .subscribe(dim => spy2(dim)); + const spy2: Mock<(dim: ElementDimensions) => void> = vi.fn(); + const subs2 = service + .observe(component.theDiv().nativeElement, 50, true) + .subscribe(dim => spy2(dim)); - await timeout(20); - expect(spy).toHaveBeenCalledWith(expect.objectContaining({ width: 100, height: 100 })); - expect(spy2).toHaveBeenCalledWith(expect.objectContaining({ width: 100, height: 100 })); + vi.advanceTimersByTime(10); + expect(spy).toHaveBeenCalledWith(expect.objectContaining({ width: 100, height: 100 })); + expect(spy2).toHaveBeenCalledWith(expect.objectContaining({ width: 100, height: 100 })); - detectSizeChange(200, 100); + detectSizeChange(200, 100); - await timeout(150); - expect(spy).toHaveBeenCalledWith(expect.objectContaining({ width: 200, height: 100 })); - expect(spy2).toHaveBeenCalledWith(expect.objectContaining({ width: 200, height: 100 })); + vi.advanceTimersByTime(100); + expect(spy).toHaveBeenCalledWith(expect.objectContaining({ width: 200, height: 100 })); + expect(spy2).toHaveBeenCalledWith(expect.objectContaining({ width: 200, height: 100 })); - subs2.unsubscribe(); - } + subs2.unsubscribe(); }); }); diff --git a/projects/element-ng/resize-observer/si-responsive-container.directive.spec.ts b/projects/element-ng/resize-observer/si-responsive-container.directive.spec.ts index f65112539..f893a43c0 100644 --- a/projects/element-ng/resize-observer/si-responsive-container.directive.spec.ts +++ b/projects/element-ng/resize-observer/si-responsive-container.directive.spec.ts @@ -41,7 +41,10 @@ describe('SiResponsiveContainerDirective', () => { element = fixture.nativeElement; }); - afterEach(() => restoreResizeObserver()); + afterEach(() => { + restoreResizeObserver(); + vi.useRealTimers(); + }); const testSize = async (size: number, clazz: string): Promise => { vi.useFakeTimers(); diff --git a/projects/element-ng/search-bar/si-search-bar.component.spec.ts b/projects/element-ng/search-bar/si-search-bar.component.spec.ts index 85b06da85..de6f5f41e 100644 --- a/projects/element-ng/search-bar/si-search-bar.component.spec.ts +++ b/projects/element-ng/search-bar/si-search-bar.component.spec.ts @@ -175,6 +175,10 @@ describe('SiSearchBarComponent', () => { }); describe('debounceTime', () => { + afterEach(() => { + vi.useRealTimers(); + }); + const fakeInput = (text: string, element: HTMLElement): void => { const input = getInput(element); input.value = text; diff --git a/projects/element-ng/side-panel/si-side-panel.component.spec.ts b/projects/element-ng/side-panel/si-side-panel.component.spec.ts index ccafb7fb7..8d83737f3 100644 --- a/projects/element-ng/side-panel/si-side-panel.component.spec.ts +++ b/projects/element-ng/side-panel/si-side-panel.component.spec.ts @@ -68,6 +68,10 @@ describe('SiSidePanelComponent', () => { service = TestBed.inject(SiSidePanelService); }); + afterEach(() => { + vi.useRealTimers(); + }); + it('should create', () => { expect(component).toBeTruthy(); }); diff --git a/projects/element-ng/split/si-split.component.spec.ts b/projects/element-ng/split/si-split.component.spec.ts index 5dd872131..037be96f4 100644 --- a/projects/element-ng/split/si-split.component.spec.ts +++ b/projects/element-ng/split/si-split.component.spec.ts @@ -410,7 +410,8 @@ describe('SiSplitComponent', () => { }); it('should save ui state ', async () => { - // cannot use jasmine.clock here + // Drain the async chain: asapScheduler -> parts.changes -> setTimeout(refreshAllPartSizes) + await new Promise(resolve => setTimeout(resolve)); await new Promise(resolve => setTimeout(resolve)); wrapperComponent.splitPart().toggleCollapse(); const uiStateMock = @@ -435,15 +436,17 @@ describe('SiSplitComponent', () => { fixture = TestBed.createComponent(WrapperComponent); wrapperComponent = fixture.componentInstance; wrapperComponent.sizes = [20, 60, 20]; - // We need this here to run checks after async tasks completed. - fixture.autoDetectChanges(true); element = fixture.nativeElement; }); it('should load and configure split parts', async () => { - // cannot use jasmine.clock here + fixture.detectChanges(); + // Drain the async chain: asapScheduler -> parts.changes handler -> + // restoreFormUIState() -> uiStateService.load().then() -> setTimeout(refreshAllPartSizes) + // Multiple setTimeout rounds are needed to flush the nested async operations. + await new Promise(resolve => setTimeout(resolve)); await new Promise(resolve => setTimeout(resolve)); - await fixture.whenStable(); + fixture.detectChanges(); expect(wrapperComponent.measureSize1()).toBeCloseTo(200, 0); expect(wrapperComponent.measureSize2()).toBeCloseTo(200, 0); expect(wrapperComponent.measureSize3()).toBeCloseTo(100, 0); @@ -510,6 +513,8 @@ describe('SiSplitComponent', () => { wrapperComponent.scale3 = 'none'; await runOnPushChangeDetection(fixture); fixture.detectChanges(); + // Drain the async chain: asapScheduler -> parts.changes -> setTimeout(refreshAllPartSizes) + await new Promise(resolve => setTimeout(resolve)); await new Promise(resolve => setTimeout(resolve)); expect(wrapperComponent.measureSize1()).toBeCloseTo(200, 0); diff --git a/projects/element-ng/tabs-legacy/si-tabset/si-tabset-legacy.component.spec.ts b/projects/element-ng/tabs-legacy/si-tabset/si-tabset-legacy.component.spec.ts index 5f3d8bb73..5a71ab128 100644 --- a/projects/element-ng/tabs-legacy/si-tabset/si-tabset-legacy.component.spec.ts +++ b/projects/element-ng/tabs-legacy/si-tabset/si-tabset-legacy.component.spec.ts @@ -199,9 +199,11 @@ describe('SiTabset', () => { it('should handle focus correctly', async () => { testComponent.tabs = ['1', '2', '3']; fixture.detectChanges(); + vi.advanceTimersByTime(1000); await fixture.whenStable(); if (document.hasFocus()) { getElement(0).focus(); + vi.advanceTimersByTime(1000); await fixture.whenStable(); expect(getElement(0).getAttribute('tabindex')).toEqual('-1'); focusNext(); diff --git a/projects/element-ng/theme/si-theme.service.spec.ts b/projects/element-ng/theme/si-theme.service.spec.ts index b127ab1a7..03159e851 100644 --- a/projects/element-ng/theme/si-theme.service.spec.ts +++ b/projects/element-ng/theme/si-theme.service.spec.ts @@ -30,7 +30,10 @@ describe('SiThemeService', () => { }; beforeEach(() => (themeSwitchSpy = vi.fn())); - afterEach(() => localStorage.removeItem(SI_THEME_LOCAL_STORAGE_KEY)); + afterEach(() => { + localStorage.removeItem(SI_THEME_LOCAL_STORAGE_KEY); + vi.restoreAllMocks(); + }); describe('with theme type auto', () => { it('should set theme to `light` if preferred', () => { diff --git a/projects/element-ng/toast-notification/si-toast-notification.service.spec.ts b/projects/element-ng/toast-notification/si-toast-notification.service.spec.ts index dcb01954a..10a3fd3fd 100644 --- a/projects/element-ng/toast-notification/si-toast-notification.service.spec.ts +++ b/projects/element-ng/toast-notification/si-toast-notification.service.spec.ts @@ -17,6 +17,10 @@ describe('SiToastNotificationService', () => { beforeEach(() => (service = TestBed.inject(SiToastNotificationService))); + afterEach(() => { + vi.useRealTimers(); + }); + it('should be created', () => { expect(service).toBeTruthy(); }); diff --git a/projects/element-ng/vitest-base.config.ts b/projects/element-ng/vitest-base.config.ts index 88cceb104..efd1bf92b 100644 --- a/projects/element-ng/vitest-base.config.ts +++ b/projects/element-ng/vitest-base.config.ts @@ -13,8 +13,7 @@ export default defineConfig({ test: { env: { TZ: 'UTC' - }, - isolate: true + } }, resolve: { dedupe: ['@angular/core', '@angular/common', '@angular/platform-browser'] diff --git a/projects/element-ng/wizard/si-wizard.component.spec.ts b/projects/element-ng/wizard/si-wizard.component.spec.ts index 8cc1966db..2bb820b9b 100644 --- a/projects/element-ng/wizard/si-wizard.component.spec.ts +++ b/projects/element-ng/wizard/si-wizard.component.spec.ts @@ -364,6 +364,10 @@ describe('SiWizardComponent', () => { }); describe('steps with lazy loading', () => { + afterEach(() => { + vi.useRealTimers(); + }); + it('should render steps if they are loaded lazily', async () => { hostComponent.steps.set([]); fixture.changeDetectorRef.markForCheck();