Skip to content

Commit

Permalink
test: testing tour resizing
Browse files Browse the repository at this point in the history
  • Loading branch information
vinicius-guedes-brisa committed Dec 9, 2024
1 parent eec0076 commit 243db35
Show file tree
Hide file tree
Showing 7 changed files with 205 additions and 24 deletions.
80 changes: 80 additions & 0 deletions projects/ion/src/lib/tour/mocks/resizing-host-demo.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { Component, OnDestroy, OnInit } from '@angular/core';

import { IonTourService } from '../tour.service';

@Component({
selector: 'tour-resizing-host',
styles: [
`
main {
height: 800px;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
position: relative;
ion-button {
position: absolute;
}
}
`,
],
template: `
<main>
<ion-button
data-testid="demo-resizing-button"
[style.top.px]="top"
[style.left.px]="left"
[style.width.px]="buttonWidth"
label="Demo"
type="secondary"
[expand]="true"
ionTourStep
ionStepId="demo-resizing-step"
ionTourId="demo-resizing-tour"
ionStepTitle="Title Example"
[ionStepBody]="stepBody"
></ion-button>
</main>
<ng-template #stepBody>
<p>
The host is changing its size and position, but the tour should still
work
</p>
</ng-template>
`,
})
export class TourResizingHostComponent implements OnInit, OnDestroy {
public top = 100;
public left = 50;
public buttonWidth = 100;

private interval: ReturnType<typeof setInterval>;

constructor(private readonly ionTourService: IonTourService) {}

public ngOnInit(): void {
this.ionTourService.start();
this.animateButtonSize();
}

private animateButtonSize(): void {
this.interval = setInterval(() => {
this.top = this.getRandomNumber(0, 700);
this.left = this.getRandomNumber(0, 500);
this.buttonWidth = this.getRandomNumber(50, 300);
}, 700);
}

private getRandomNumber(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1)) + min;
}

public ngOnDestroy(): void {

Check warning on line 75 in projects/ion/src/lib/tour/mocks/resizing-host-demo.component.ts

View workflow job for this annotation

GitHub Actions / test-prettier-lint (12.22.1)

Member ngOnDestroy should be declared before all private method definitions
if (this.interval) {
clearInterval(this.interval);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const sut = async (
};

const STEP_MOCK = {
target: {
getTarget: () => ({
x: 300,
y: 300,
width: 100,
Expand All @@ -24,7 +24,7 @@ const STEP_MOCK = {
right: 400,
left: 300,
top: 300,
} as DOMRect,
}),
} as IonTourStepProps;

describe('IonTourBackdropComponent', () => {
Expand Down
73 changes: 68 additions & 5 deletions projects/ion/src/lib/tour/tour-step.directive.spec.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
import { ChangeDetectorRef, ElementRef, ViewContainerRef } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';

Check warning on line 2 in projects/ion/src/lib/tour/tour-step.directive.spec.ts

View workflow job for this annotation

GitHub Actions / test-prettier-lint (12.22.1)

'ComponentFixture' is defined but never used
import {
fireEvent,
render,
RenderResult,
screen,
} from '@testing-library/angular';
import userEvent from '@testing-library/user-event';
import { cloneDeep } from 'lodash';
import { EMPTY, of } from 'rxjs';

import { IonButtonModule } from '../button/button.module';
import { IonTourStepProps } from '../core/types';
import { IonPopoverModule } from '../popover/popover.module';
import { TourResizingHostComponent } from './mocks/resizing-host-demo.component';
import { TourStepDemoComponent } from './mocks/tour-step-props.component';
import { IonTourModule } from './tour.module';
import { IonTourService } from './tour.service';
import { IonPopoverModule } from '../popover/popover.module';
import userEvent from '@testing-library/user-event';
import { IonTourStepDirective } from './tour-step.directive';
import { IonPositionService } from '../position/position.service';

const DEFAULT_PROPS: Partial<TourStepDemoComponent> = {
ionTourId: 'demo-tour',
Expand All @@ -23,7 +28,7 @@ const DEFAULT_PROPS: Partial<TourStepDemoComponent> = {
ionNextStepBtn: { label: 'Test Next' },
};

const tourServiceMock: Partial<IonTourService> = {
const tourServiceMock = {
saveStep: jest.fn(),
removeStep: jest.fn(),
start: jest.fn(),
Expand All @@ -34,6 +39,26 @@ const tourServiceMock: Partial<IonTourService> = {
currentStep$: EMPTY,
};

const generateRandomDOMRect = (): DOMRect => {
const data = {
x: Math.random() * 100,
y: Math.random() * 100,
width: Math.random() * 200 + 50,
height: Math.random() * 200 + 50,
bottom: Math.random() * 100 + 200,
right: Math.random() * 100 + 200,
left: Math.random() * 50,
top: Math.random() * 50,
};
return { ...data, toJSON: () => data } as DOMRect;
};

const elementRefMock = {
nativeElement: {
getBoundingClientRect: jest.fn().mockImplementation(generateRandomDOMRect),
},
};

function setActiveTour(tourId: string): void {
Object.defineProperty(tourServiceMock, 'activeTour$', { value: of(tourId) });
}
Expand All @@ -49,17 +74,43 @@ const sut = async (
): Promise<RenderResult<TourStepDemoComponent>> => {
const result = await render(TourStepDemoComponent, {
imports: [IonButtonModule, IonTourModule, IonPopoverModule],
providers: [{ provide: IonTourService, useValue: tourServiceMock }],
providers: [
{ provide: ElementRef, useValue: elementRefMock },
{ provide: IonTourService, useValue: tourServiceMock },
],
componentProperties: {
...DEFAULT_PROPS,
...props,
},
});
jest.runAllTimers();
jest.runOnlyPendingTimers();
result.fixture.detectChanges();
return result;
};

const sutResizingHostDemo = (): void => {
TestBed.configureTestingModule({
imports: [IonButtonModule, IonTourModule, IonPopoverModule],
declarations: [TourResizingHostComponent],
providers: [
IonTourService,
IonTourStepDirective,
IonPositionService,
ViewContainerRef,
ChangeDetectorRef,
{ provide: ElementRef, useValue: elementRefMock },
],
});

TestBed.get(IonTourStepDirective)['hostPositionChanged'] = jest
.fn()
.mockReturnValue(true);

TestBed.createComponent(TourResizingHostComponent).detectChanges();

jest.runOnlyPendingTimers();
};

describe('IonTourStepDirective', () => {
afterEach(() => {
jest.clearAllMocks();
Expand Down Expand Up @@ -103,6 +154,18 @@ describe('IonTourStepDirective', () => {
expect(screen.queryByText(newlabel)).toBeInTheDocument();
});

it('should reposition popover when host position changes', async () => {
sutResizingHostDemo();

const directive = TestBed.get(IonTourStepDirective);
const spy = jest.spyOn(directive, 'repositionPopover');
directive.observeHostPosition();

jest.runOnlyPendingTimers();

expect(spy).toHaveBeenCalled();
});

describe('popover actions', () => {
it('should render a default previous button', async () => {
const step = cloneDeep(DEFAULT_PROPS) as unknown as IonTourStepProps;
Expand Down
18 changes: 10 additions & 8 deletions projects/ion/src/lib/tour/tour-step.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { IonPositionService } from '../position/position.service';
import { SafeAny } from '../utils/safe-any';
import { generatePositionCallback } from './tour-position.calculator';
import { IonTourService } from './tour.service';
import { isEqual } from 'lodash';

@Directive({ selector: '[ionTourStep]' })
export class IonTourStepDirective implements OnInit, OnChanges, OnDestroy {
Expand Down Expand Up @@ -56,7 +57,7 @@ export class IonTourStepDirective implements OnInit, OnChanges, OnDestroy {
private isTourActive = false;
private destroy$ = new Subject<void>();

private observer: NodeJS.Timer;
private interval: ReturnType<typeof setInterval>;
private hostPosition: DOMRect;

constructor(
Expand Down Expand Up @@ -107,20 +108,21 @@ export class IonTourStepDirective implements OnInit, OnChanges, OnDestroy {
this.tourService.removeStep(this.ionStepId);
this.destroy$.next();
this.destroy$.complete();
if (this.observer) {
clearInterval(this.observer);
if (this.interval) {
clearInterval(this.interval);
}
}

private observeHostPosition(): void {
this.ngZone.runOutsideAngular(() => {
this.observer = setInterval(() => {
const interval30FPSinMs = 1000 / 30;
this.interval = setInterval(() => {
this.ngZone.run(() => {
if (this.hostPositionChanged()) {
this.repositionPopover();
}
});
}, 0);
}, interval30FPSinMs);
});
}

Expand Down Expand Up @@ -223,9 +225,9 @@ export class IonTourStepDirective implements OnInit, OnChanges, OnDestroy {
}

private hostPositionChanged(): boolean {
return (
this.elementRef.nativeElement.getBoundingClientRect().toJSON() !==
this.hostPosition.toJSON()
return !isEqual(
this.hostPosition,
this.elementRef.nativeElement.getBoundingClientRect()
);
}

Expand Down
11 changes: 7 additions & 4 deletions projects/ion/src/lib/tour/tour.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ describe('IonTourService', () => {

const updatedStep = {
...step1,
target: { toJSON: () => ({ ...TARGET_MOCK, x: 400 }) },
getTarget: () => ({ toJSON: () => ({ ...TARGET_MOCK, x: 400 }) }),

Check warning on line 128 in projects/ion/src/lib/tour/tour.service.spec.ts

View workflow job for this annotation

GitHub Actions / test-prettier-lint (12.22.1)

Missing return type on function
} as IonTourStepProps;

service.saveStep(step1);
Expand All @@ -134,6 +134,7 @@ describe('IonTourService', () => {

expect(service.currentStep.value).toEqual(step1);
service.saveStep(updatedStep);
jest.runAllTimers();
expect(service.currentStep.value).toEqual(updatedStep);
});
});
Expand Down Expand Up @@ -258,13 +259,14 @@ describe('IonTourService', () => {

const spy = jest.spyOn(step2.ionOnPrevStep, 'emit');
service.prevStep();
jest.runAllTimers();

expect(service.currentStep.value).toEqual(step1);
expect(spy).toHaveBeenCalledTimes(1);
});

it('should finish the tour if there is no next step and nextStep is called', () => {
const [step] = stepsMock;
const step = { ...stepsMock[0], ionNextStepId: undefined };

service.saveStep(step);
service.start({ tourId: step.ionTourId });
Expand All @@ -275,8 +277,8 @@ describe('IonTourService', () => {
service.nextStep();
jest.runAllTimers();

expect(service.currentStep.value).toBeUndefined();
expect(service.activeTour.value).toBeUndefined();
expect(service.currentStep.value).toBeNull();
expect(service.activeTour.value).toBeNull();
expect(spyNext).toHaveBeenCalledTimes(1);
expect(spyFinish).toHaveBeenCalledTimes(1);
});
Expand All @@ -291,6 +293,7 @@ describe('IonTourService', () => {
const spyPrev = jest.spyOn(step.ionOnPrevStep, 'emit');
const spyFinish = jest.spyOn(step.ionOnFinishTour, 'emit');
service.prevStep();
jest.runAllTimers();

expect(service.currentStep.value).toBeNull();
expect(service.activeTour.value).toBeNull();
Expand Down
10 changes: 5 additions & 5 deletions projects/ion/src/lib/tour/tour.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export class IonTourService {
if (
current &&
current.ionStepId === step.ionStepId &&
!isEqual(step.getTarget().toJSON(), current.getTarget().toJSON())
!isEqual(step.getTarget(), current.getTarget())
) {
this.navigateToStep(step);
}
Expand Down Expand Up @@ -105,13 +105,13 @@ export class IonTourService {

public prevStep(): void {
const currentStep = this.currentStep.getValue();
currentStep.ionOnPrevStep.emit();

if (!currentStep.ionPrevStepId) {
this.finish();
return;
}

currentStep.ionOnPrevStep.emit();

setTimeout(() => {
const prevStep = this._tours[this.activeTourId].get(
currentStep.ionPrevStepId
Expand All @@ -125,13 +125,13 @@ export class IonTourService {

public nextStep(): void {
const currentStep = this.currentStep.getValue();
currentStep.ionOnNextStep.emit();

if (!currentStep.ionNextStepId) {
this.finish();
return;
}

currentStep.ionOnNextStep.emit();

setTimeout(() => {
const nextStep = this._tours[this.activeTourId].get(
currentStep.ionNextStepId
Expand Down
Loading

0 comments on commit 243db35

Please sign in to comment.