tests(Playwright): Full implementation of Page Object Models (POMs) to Playwright tests (#5608)

This commit is contained in:
Vinícius Alves de Faria Resende authored and GitHub committed 2025-12-12 11:46:45 -05:00
1 parent 7b12dcf048
commit d917e74e68
58 files changed
+1167 -496

No files matched your search

+134
View File
@@ -0,0 +1,134 @@
import { Locator, Page } from '@playwright/test';
export class DOMOverlayPageObject {
readonly page: Page;
constructor(page: Page) {
this.page = page;
}
get dialog() {
const page = this.page;
return {
get confirmation() {
const cancelButton = page.getByTestId('untracked-series-modal-cancel-button');
const confirmButton = page.getByTestId('untracked-series-modal-confirm-button');
return {
cancel: {
button: cancelButton,
click: async () => {
await cancelButton.click();
},
},
confirm: {
button: confirmButton,
click: async () => {
await confirmButton.click();
},
},
};
},
get input() {
const locator = page.getByTestId('dialog-input');
const saveButton = page.getByTestId('input-dialog-save-button');
return {
locator,
fill: async (text: string) => {
await locator.fill(text);
},
fillAndSave: async (text: string) => {
await locator.fill(text);
await saveButton.click();
},
save: async () => {
await saveButton.click();
},
};
},
title: page.locator('[role="dialog"] h2'),
};
}
get viewport() {
const page = this.page;
return {
get annotationContextMenu() {
return {
get addLabel() {
const button = page.getByTestId('context-menu-item').filter({ hasText: 'Add Label' });
return {
locator: button,
click: async () => {
await button.click();
},
};
},
get delete() {
const button = page.getByTestId('context-menu-item').filter({ hasText: 'Delete' });
return {
locator: button,
click: async () => {
await button.click();
},
};
},
open: async (annotation: Locator) => {
await annotation.click({ button: 'right', force: true });
},
};
},
get measurementTracking() {
const cancelButton = page.getByTestId('prompt-begin-tracking-cancel-btn');
const noAndNotAskAgainButton = page.getByTestId(
'prompt-begin-tracking-no-do-not-ask-again-btn'
);
const confirmButton = page.getByTestId('prompt-begin-tracking-yes-btn');
return {
locator: page.getByTestId('viewport-notification'),
cancel: {
button: cancelButton,
click: async () => {
await cancelButton.click();
},
},
noAndNotAskAgain: {
button: noAndNotAskAgainButton,
click: async () => {
await noAndNotAskAgainButton.click();
},
},
confirm: {
button: confirmButton,
click: async () => {
await confirmButton.click();
},
},
};
},
get segmentationHydration() {
const noButton = page.getByTestId('no-hydrate-btn');
const yesButton = page.getByTestId('yes-hydrate-btn');
return {
locator: page.getByTestId('viewport-notification'),
no: {
button: noButton,
click: async () => {
await noButton.click();
},
},
yes: {
button: yesButton,
click: async () => {
await yesButton.click();
},
},
};
},
async getModalityLoadBadgeCount() {
return await page.locator('css=div[data-cy^="ModalityLoadBadge-"]').count();
},
};
}
}
+29
View File
@@ -0,0 +1,29 @@
import { Page } from '@playwright/test';
export class DataOverlayPageObject {
readonly page: Page;
constructor(page: Page) {
this.page = page;
}
async toggle(viewportId: string = 'default') {
await this.page.getByTestId(`dataOverlayMenu-${viewportId}-btn`).click();
}
async addSegmentation(segmentation: string, viewportId: string = 'default') {
await this.page.getByTestId(`AddSegmentationDataOverlay-${viewportId}`).click();
await this.page.getByText('SELECT A SEGMENTATION').click();
await this.page.getByTestId(segmentation).click();
}
async changeSegmentation(from: string, to: string) {
await this.page.getByTestId(`overlay-ds-select-value-${from.toUpperCase()}`).click();
await this.page.getByTestId(`${to}-SEG`).click();
}
async remove(segmentation: string) {
await this.page.getByTestId(`overlay-ds-more-button-${segmentation}`).click();
await this.page.getByTestId(`overlay-ds-remove-button-${segmentation}`).click();
}
}
+35
View File
@@ -0,0 +1,35 @@
import { Page } from '@playwright/test';
export class LeftPanelPageObject {
readonly page: Page;
constructor(page: Page) {
this.page = page;
}
async toggle() {
await this.page.getByTestId('side-panel-header-left').click();
}
get thumbnails() {
return this.page.locator('[data-cy^="study-browser-thumbnail"]');
}
async loadSeriesByModality(modality: string, nth: number = 0) {
const matchingThumbnail = this.thumbnails
.filter({
has: this.page.locator('[data-cy="series-modality-label"]', { hasText: modality }),
})
.nth(nth);
await matchingThumbnail.dblclick();
}
async loadSeriesByDescription(description: string, nth: number = 0) {
const matchingThumbnail = this.thumbnails
.filter({
has: this.page.locator('[data-cy="series-description-label"]', { hasText: description }),
})
.nth(nth);
await matchingThumbnail.dblclick();
}
}
+18
View File
@@ -7,6 +7,15 @@ export class MainToolbarPageObject {
this.page = page;
}
get crosshairs() {
const button = this.page.getByTestId('Crosshairs');
return {
button,
async click() {
await button.click();
},
};
}
get layoutSelection() {
const page = this.page;
@@ -96,6 +105,15 @@ export class MainToolbarPageObject {
return {
...measurementTools,
get selectedTool() {
const button = page.getByTestId('MeasurementTools-split-button-primary');
return {
button,
async click() {
await button.click();
},
};
},
get arrowAnnotate() {
const button = page.getByTestId('ArrowAnnotate');
return {
+271
View File
@@ -0,0 +1,271 @@
import { Locator, Page } from '@playwright/test';
import { DOMOverlayPageObject } from './DOMOverlayPageObject';
export class RightPanelPageObject {
readonly page: Page;
private readonly DOMOverlayPageObject: DOMOverlayPageObject;
constructor(page: Page) {
this.page = page;
this.DOMOverlayPageObject = new DOMOverlayPageObject(page);
}
private getActionsMenu(row: Locator) {
const actionsButton = row.getByTestId('actionsMenuTrigger');
return {
button: actionsButton,
click: async () => {
await actionsButton.click();
},
delete: async () => {
await actionsButton.click();
await this.page.getByTestId('Delete').click();
},
toggleLock: async () => {
await actionsButton.click();
await this.page.getByTestId('LockToggle').click();
},
unlock: async () => {
await actionsButton.click();
await this.page.getByTestId('Unlock').click();
},
rename: async (text: string) => {
await actionsButton.click();
await this.page.getByTestId('Rename').click();
await this.DOMOverlayPageObject.dialog.input.fillAndSave(text);
},
};
}
private getPanelRowDataObject(row: Locator) {
const getActionsMenu = (row: Locator) => this.getActionsMenu.bind(this)(row);
return {
get actions() {
return getActionsMenu(row);
},
click: async () => {
await row.click();
},
locator: row,
toggleVisibility: async () => {
await row.getByTestId('data-row-visibility-toggle').click();
},
};
}
private getPanelRowByIdx(index: number) {
const row = this.page.getByTestId('data-row').nth(index);
return this.getPanelRowDataObject(row);
}
private getPanelRowByText(text: string) {
const row = this.page.getByTestId('data-row').filter({ hasText: text });
return this.getPanelRowDataObject(row);
}
async toggle() {
await this.page.getByTestId('side-panel-header-right').click();
}
get measurementsPanel() {
const page = this.page;
const getMeasurementByIdx = (index: number) => this.getPanelRowByIdx(index);
const getMeasurementByText = (text: string) => this.getPanelRowByText(text);
const menuButton = page.getByTestId('trackedMeasurements-btn');
return {
menuButton,
panel: {
deleteAll: async () => {
await page.getByRole('button', { name: 'Delete' }).click();
},
getMeasurementCount: async () => {
return await page.getByTestId('data-row').count();
},
locator: page.getByTestId('trackedMeasurements-panel').last(),
nthMeasurement(index: number) {
return getMeasurementByIdx(index);
},
measurementByText(text: string) {
return getMeasurementByText(text);
},
},
select: async () => {
await menuButton.click();
},
};
}
private get addSegmentationButton() {
const button = this.page.getByTestId('addSegmentation');
return {
button,
click: async () => {
await button.click();
},
};
}
private get segmentationPanel() {
const page = this.page;
const getSegmentationByIdx = (index: number) => this.getPanelRowByIdx(index);
const getSegmentationByText = (text: string) => this.getPanelRowByText(text);
return {
getSegmentationCount: async () => {
return await page.getByTestId('data-row').count();
},
// No data-cy exists in this panel, using Segmentation header button
locator: page.getByRole('button', { name: 'Segmentations' }),
nthSegmentation(index: number) {
return getSegmentationByIdx(index);
},
segmentationByText(text: string) {
return getSegmentationByText(text);
},
};
}
get contourSegmentationPanel() {
const page = this.page;
const addSegmentationButton = this.addSegmentationButton;
const panel = this.segmentationPanel;
const menuButton = page.getByTestId('panelSegmentationWithToolsContour-btn');
return {
addSegmentationButton,
menuButton,
panel,
select: async () => {
await menuButton.click();
},
};
}
get labelMapSegmentationPanel() {
const page = this.page;
const addSegmentationButton = this.addSegmentationButton;
const panel = this.segmentationPanel;
const menuButton = page.getByTestId('panelSegmentationWithToolsLabelMap-btn');
return {
addSegmentationButton,
menuButton,
panel,
select: async () => {
await menuButton.click();
},
tools: {
get brush() {
const button = page.getByTestId('Brush-btn');
const input = page.locator(`css=div[data-cy="brush-radius"] input`);
return {
button,
input,
click: async () => {
await button.click();
},
setRadius: async (radius: number) => {
await input.fill(radius.toString());
},
};
},
get eraser() {
const button = page.getByTestId('Eraser-btn');
const input = page.locator(`css=div[data-cy="eraser-radius"] input`);
return {
button,
input,
click: async () => {
await button.click();
},
setRadius: async (radius: number) => {
await input.fill(radius.toString());
},
};
},
get threshold() {
const button = page.getByTestId('Threshold-btn');
const input = page.locator(`css=div[data-cy="threshold-radius"] input`);
return {
button,
input,
click: async () => {
await button.click();
},
setRadius: async (radius: number) => {
await input.fill(radius.toString());
},
};
},
},
};
}
get noToolsSegmentationPanel() {
const page = this.page;
const panel = this.segmentationPanel;
const menuButton = page.getByTestId(/^panelSegmentation.*-btn$/).first();
return {
menuButton,
panel,
select: async () => {
await menuButton.click();
},
};
}
get tmtvPanel() {
const page = this.page;
return {
get addSegmentationButton() {
const button = page.getByTestId('addSegmentation');
return {
button,
click: async () => {
await button.click();
},
};
},
async exportTmtvCsvReport() {
await page.getByTestId('exportTmtvCsvReport').click();
},
tools: {
get brush() {
const button = page.getByTestId('Brush-btn');
const input = page.locator(`css=div[data-cy="brush-radius"] input`);
return {
button,
input,
click: async () => {
await button.click();
},
setRadius: async (radius: number) => {
await input.fill(radius.toString());
},
};
},
get rectangleROIThreshold() {
const button = page.getByTestId('RectangleROIStartEndThreshold-btn');
const input = page.getByTestId('percentage-of-max-suv-input');
return {
button,
click: async () => {
await button.click();
},
getPercentageOfMaxSUV: async () => {
return await input.inputValue();
},
setPercentageOfMaxSUV: async (percentage: string) => {
await input.fill(percentage);
},
};
},
},
};
}
}
+98 -14
View File
@@ -1,10 +1,13 @@
import { Locator, Page } from '@playwright/test';
import {
getMousePosition,
simulateClicksOnElement,
simulateDoubleClickOnElement,
simulateNormalizedClicksOnElement,
simulateNormalizedDragOnElement,
} from '../utils';
import { DataOverlayPageObject } from './DataOverlayPageObject';
import { DOMOverlayPageObject } from './DOMOverlayPageObject';
type SvgInnerElement = 'circle' | 'path' | 'd';
@@ -15,11 +18,18 @@ type NormalizedDragParams = {
};
export interface IViewportPageObject {
doubleClickAt: (point: { x: number; y: number }) => Promise<void>;
nthAnnotation(nth: number): {
locator: Locator;
click: () => Promise<void>;
contextMenu: {
open: () => Promise<void>;
};
};
clickAt: (
points: { x: number; y: number }[],
button?: 'left' | 'right' | 'middle'
) => Promise<void>;
doubleClickAt: (point: { x: number; y: number }) => Promise<void>;
normalizedClickAt: (
normalizedPoints: { x: number; y: number }[],
button?: 'left' | 'right' | 'middle'
@@ -38,10 +48,7 @@ export interface IViewportPageObject {
bottomRight: Locator;
};
overlayMenu: {
dataOverlay: {
button: Locator;
click: () => Promise<void>;
};
dataOverlay: DataOverlayPageObject;
orientation: {
button: Locator;
click: () => Promise<void>;
@@ -57,9 +64,29 @@ export interface IViewportPageObject {
export class ViewportPageObject {
readonly page: Page;
private readonly dataOverlayPageObject: DataOverlayPageObject;
constructor(page: Page) {
this.page = page;
this.dataOverlayPageObject = new DataOverlayPageObject(page);
}
private getAnnotation(viewport: Locator, nth: number) {
const page = this.page;
const domOverlayPageObject = new DOMOverlayPageObject(page);
const annotation = viewport.locator('g[data-annotation-uid]').nth(nth);
return {
locator: annotation,
click: async () => {
await annotation.click();
},
contextMenu: {
open: async () => {
await domOverlayPageObject.viewport.annotationContextMenu.open(annotation);
},
},
};
}
private getOrientationMarkers(viewport: Locator) {
@@ -82,15 +109,7 @@ export class ViewportPageObject {
private getOverlayMenu(viewport: Locator) {
return {
get dataOverlay() {
const button = viewport.locator('[data-cy^="dataOverlayMenu"]').first();
return {
button,
async click() {
await button.click();
},
};
},
dataOverlay: this.dataOverlayPageObject,
get orientation() {
const button = viewport.locator('[data-cy^="orientationMenu"]');
return {
@@ -118,6 +137,7 @@ export class ViewportPageObject {
private viewportPageObjectFactory(viewport: Locator): IViewportPageObject {
return {
nthAnnotation: (nth: number) => this.getAnnotation(viewport, nth),
doubleClickAt: async (point: { x: number; y: number }) => {
await simulateDoubleClickOnElement({
locator: viewport,
@@ -166,6 +186,70 @@ export class ViewportPageObject {
return this.viewportPageObjectFactory(viewport);
}
get crosshairs() {
const page = this.page;
async function increaseSlabThickness(locator: Locator, lineNumber: number, axis: string) {
const lineLocator = locator.locator('line').nth(lineNumber);
await lineLocator.click({ force: true });
await lineLocator.hover({ force: true });
const circleLocator = locator.locator('rect').first();
await circleLocator.hover({ force: true });
await page.mouse.down();
const position = await getMousePosition(page);
switch (axis) {
case 'x':
await page.mouse.move(position.x + 100, position.y);
break;
case 'y':
await page.mouse.move(position.x, position.y + 100);
break;
}
await page.mouse.up();
}
async function rotateCrosshairs(locator: Locator, lineNumber: number) {
const lineLocator = locator.locator('line').nth(lineNumber);
await lineLocator.click({ force: true });
await lineLocator.hover({ force: true });
const circleLocator = locator.locator('circle').nth(1);
await circleLocator.hover({ force: true });
await page.mouse.down();
const position = await getMousePosition(page);
await page.mouse.move(position.x, position.y + 100);
await page.mouse.up();
}
function crosshairsFactory(
locator: Locator,
increaseLineNumber: number,
increaseAxis: 'x' | 'y',
rotateLineNumber: number
) {
return {
increase: async () => {
await increaseSlabThickness(locator, increaseLineNumber, increaseAxis);
},
locator,
rotate: () => {
return rotateCrosshairs(locator, rotateLineNumber);
},
};
}
return {
axial: crosshairsFactory(page.locator('#svg-layer-mpr-axial'), 0, 'x', 3),
sagittal: crosshairsFactory(page.locator('#svg-layer-mpr-sagittal'), 2, 'x', 0),
coronal: crosshairsFactory(page.locator('#svg-layer-mpr-coronal'), 0, 'y', 0),
};
}
async getAll(): Promise<IViewportPageObject[]> {
const viewports = await this.page.getByTestId('viewport-pane').all();
return viewports.map(viewport => this.viewportPageObjectFactory(viewport));
+10 -1
View File
@@ -1,4 +1,13 @@
import { DOMOverlayPageObject } from './DOMOverlayPageObject';
import { MainToolbarPageObject } from './MainToolbarPageObject';
import { LeftPanelPageObject } from './LeftPanelPageObject';
import { RightPanelPageObject } from './RightPanelPageObject';
import { ViewportPageObject } from './ViewportPageObject';
export { MainToolbarPageObject, ViewportPageObject };
export {
DOMOverlayPageObject,
MainToolbarPageObject,
LeftPanelPageObject,
RightPanelPageObject,
ViewportPageObject,
};