Skip to content

Commit

Permalink
feat(testing): add a slider element (#2053)
Browse files Browse the repository at this point in the history
## Proposed change

Add a slider element

## Related issues

- 🚀 Feature #2045
  • Loading branch information
matthieu-crouzet authored Aug 14, 2024
2 parents 0321809 + d78410d commit f442d31
Show file tree
Hide file tree
Showing 14 changed files with 381 additions and 0 deletions.
5 changes: 5 additions & 0 deletions packages/@o3r/testing/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
"@angular/core": "~18.1.0",
"@angular/forms": "~18.1.0",
"@angular/platform-browser": "~18.1.0",
"@material/slider": "^14.0.0",
"@ngrx/store": "~18.0.0",
"@ngx-translate/core": "~15.0.0",
"@o3r/core": "workspace:^",
Expand All @@ -126,6 +127,9 @@
"@angular/cli": {
"optional": true
},
"@material/slider": {
"optional": true
},
"@ngx-translate/core": {
"optional": true
},
Expand Down Expand Up @@ -179,6 +183,7 @@
"@babel/core": "~7.25.0",
"@babel/preset-typescript": "~7.24.0",
"@compodoc/compodoc": "^1.1.19",
"@material/slider": "^14.0.0",
"@ngrx/store": "~18.0.0",
"@ngx-translate/core": "~15.0.0",
"@nx/eslint-plugin": "~19.5.0",
Expand Down
1 change: 1 addition & 0 deletions packages/@o3r/testing/src/core/angular-materials/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './autocomplete-material';
export * from './select-material';
export * from './slider-material';
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { O3rElement } from '../element';
import type { SliderElementProfile } from '../elements';

/**
* Interface to describe the material Slider elements that are used inside a fixture.
* As for ComponentFixtureProfile, this abstracts the testing framework that is used by choosing the right
* implementation at runtime.
*/
export interface MatSliderProfile extends SliderElementProfile {}

/**
* Mock for ElementProfile class.
* This class is used for fixture compilation purpose.
*/
export class MatSlider extends O3rElement implements MatSliderProfile {
constructor(sourceElement: any) {
super(sourceElement);
}
}
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './autocomplete-material';
export * from './select-material';
export * from './slider-material';
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { DebugElement } from '@angular/core';
import type { cssClasses } from '@material/slider';
import type { MatSliderProfile } from '../../angular-materials';
import { O3rElement } from '../element';
import { O3rSliderElement } from '../elements';

const TRACK_CLASS: typeof cssClasses.TRACK = 'mdc-slider__track';
const THUMB_CLASS: typeof cssClasses.THUMB = 'mdc-slider__thumb';

/**
* Implementation dedicated to angular / TestBed.
*/
export class MatSlider extends O3rSliderElement implements MatSliderProfile {
constructor(sourceElement: DebugElement | O3rElement) {
super(
sourceElement instanceof O3rElement ? sourceElement.sourceElement : sourceElement,
`.${TRACK_CLASS}`,
`.${THUMB_CLASS}`
);
}
}
1 change: 1 addition & 0 deletions packages/@o3r/testing/src/core/angular/elements/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './checkbox-element';
export * from './radio-element';
export * from './select-element';
export * from './slider-element';
83 changes: 83 additions & 0 deletions packages/@o3r/testing/src/core/angular/elements/slider-element.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';
import type { SliderElementProfile } from '../../elements';
import { O3rElement } from '../element';

/**
* Implementation dedicated to angular / TestBed.
*/
export class O3rSliderElement extends O3rElement implements SliderElementProfile {
constructor(
sourceElement: DebugElement,
private readonly trackSelector?: string,
private readonly thumbSelector?: string
) {
super(sourceElement);
}

private getInputElement() {
try {
const subElement = this.sourceElement.query(By.css('input[type="range"]'));
return subElement || this.sourceElement;
} catch {
return this.sourceElement;
}
}

private getTrackElement() {
if (!this.trackSelector) {
return this.sourceElement;
}
try {
const subElement = this.sourceElement.query(By.css(this.trackSelector));
return subElement || this.sourceElement;
} catch {
return this.sourceElement;
}
}

private getThumbElement() {
if (!this.thumbSelector) {
return this.sourceElement;
}
try {
const subElement = this.sourceElement.query(By.css(this.thumbSelector));
return subElement || this.sourceElement;
} catch {
return this.sourceElement;
}
}

/**
* @inheritdoc
* inspired from https://github.com/angular/components/blob/main/src/material/slider/slider.spec.ts#L1838
*/
public setValue(value: string): Promise<void> {
const trackNativeElement = this.getTrackElement().nativeElement;
const thumbNativeElement = this.getThumbElement().nativeElement;
const inputNativeElement = this.getInputElement().nativeElement;
const thumbBoundingBox: DOMRect = thumbNativeElement.getBoundingClientRect();
const startX = thumbBoundingBox.x + thumbBoundingBox.width / 2;
const startY = thumbBoundingBox.y + thumbBoundingBox.height / 2;
const max = +(inputNativeElement.max === '' ? '100' : inputNativeElement.max);
const min = +(inputNativeElement.min === '' ? '0' : inputNativeElement.min);
const sanitizeValue = Math.max(min, Math.min(+value, max));
const percent = (sanitizeValue - min) / (max - min);
const { top, left, width, height } = trackNativeElement.getBoundingClientRect() as DOMRect;
const endX = width * percent + left;
const endY = top + height / 2;
thumbNativeElement.dispatchEvent(new MouseEvent('mousedown', { clientX: startX, clientY: startY }));
trackNativeElement.focus();
trackNativeElement.dispatchEvent(new MouseEvent('mousemove', { clientX: endX, clientY: endY }));
inputNativeElement.value = `${sanitizeValue}`;
inputNativeElement.dispatchEvent(new Event('input'));
trackNativeElement.dispatchEvent(new MouseEvent('mouseup', { clientX: endX, clientY: endY }));
inputNativeElement.dispatchEvent(new Event('change'));
return Promise.resolve();
}

/** @inheritdoc */
public getValue() {
return (new O3rElement(this.getInputElement())).getValue();
}
}
1 change: 1 addition & 0 deletions packages/@o3r/testing/src/core/elements/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './checkbox-element';
export * from './radio-element';
export * from './select-element';
export * from './slider-element';
18 changes: 18 additions & 0 deletions packages/@o3r/testing/src/core/elements/slider-element.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ElementProfile, O3rElement } from '../element';

/**
* Interface to describe the Slider elements that are used inside a fixture.
* As for ComponentFixtureProfile, this abstracts the testing framework that is used by choosing the right
* implementation at runtime.
*/
export interface SliderElementProfile extends ElementProfile {}

/**
* Mock for ElementProfile class.
* This class is used for fixture compilation purpose.
*/
export class O3rSliderElement extends O3rElement implements SliderElementProfile {
constructor(sourceElement: any, _trackSelector?: string, _thumbSelector?: string) {
super(sourceElement);
}
}
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './autocomplete-material';
export * from './select-material';
export * from './slider-material';
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { cssClasses } from '@material/slider';
import type { MatSliderProfile } from '../../angular-materials';
import { O3rElement, type PlaywrightSourceElement } from '../element';
import { O3rSliderElement } from '../elements';

const TRACK_CLASS: typeof cssClasses.TRACK = 'mdc-slider__track';
const THUMB_CLASS: typeof cssClasses.THUMB = 'mdc-slider__thumb';

/**
* Implementation dedicated to Playwright.
*/
export class MatSlider extends O3rSliderElement implements MatSliderProfile {
constructor(sourceElement: PlaywrightSourceElement | O3rElement) {
super(sourceElement, `.${TRACK_CLASS}`, `.${THUMB_CLASS}`);
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './select-element';
export * from './radio-element';
export * from './checkbox-element';
export * from './slider-element';
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import type { SliderElementProfile } from '../../elements';
import { O3rElement, type PlaywrightSourceElement } from '../element';

/**
* Implementation dedicated to Playwright.
*/
export class O3rSliderElement extends O3rElement implements SliderElementProfile {
constructor(
sourceElement: PlaywrightSourceElement | O3rElement,
private readonly trackSelector?: string,
private readonly thumbSelector?: string
) {
super(sourceElement);
}

private async getInputElement() {
try {
const subElement = this.sourceElement.element.locator('input[type="range"]');
if (await subElement.count()) {
return subElement.first();
}
return this.sourceElement.element;
} catch {
return this.sourceElement.element;
}
}

private async getTrackElement() {
if (!this.trackSelector) {
return this.sourceElement.element;
}
try {
const subElement = this.sourceElement.element.locator(this.trackSelector);
if (await subElement.count()) {
return subElement.first();
}
return this.sourceElement.element;
} catch {
return this.sourceElement.element;
}
}

private async getThumbElement() {
if (!this.thumbSelector) {
return this.sourceElement.element;
}
try {
const subElement = this.sourceElement.element.locator(this.thumbSelector);
if (await subElement.count()) {
return subElement.first();
}
return this.sourceElement.element;
} catch {
return this.sourceElement.element;
}
}

/** @inheritdoc */
public async setValue(value: string): Promise<void> {
const trackElement = await this.getTrackElement();
const trackBoundingBox = await trackElement.boundingBox();
const thumbElement = await this.getThumbElement();
const inputElement = await this.getInputElement();
const thumbBoundingBox = await thumbElement.boundingBox();
if (!trackBoundingBox || !thumbBoundingBox) {
return;
}
const startPosition = {
x: thumbBoundingBox.x + thumbBoundingBox.width / 2,
y: thumbBoundingBox.y + thumbBoundingBox.height / 2
};
await this.sourceElement.page.mouse.move(startPosition.x, startPosition.y);
await this.sourceElement.page.mouse.down();

const maxAttribute = await inputElement.getAttribute('max');
const max = maxAttribute ? +maxAttribute : 100;
const minAttribute = await inputElement.getAttribute('min');
const min = minAttribute ? +minAttribute : 0;
const percent = (Math.max(min, Math.min(+value, max)) - min) / (max - min);
await this.sourceElement.page.mouse.move(
trackBoundingBox.x + Math.round(trackBoundingBox.width * percent),
trackBoundingBox.y + trackBoundingBox.height / 2
);
await this.sourceElement.page.mouse.up();
}

/** @inheritdoc */
public async getValue() {
return (new O3rElement({
element: await this.getInputElement(),
page: this.sourceElement.page
})).getValue();
}
}
Loading

0 comments on commit f442d31

Please sign in to comment.