Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ export class Search {

/* XPaths */

private get openButtonXPath(): string {
public get openButtonXPath(): string {
return (
"//div[text()[normalize-space()='Search'] and button[.//*[local-name()='svg' and @data-icon='search']/*[local-name()='path']]]/button"
);
Expand Down
10 changes: 10 additions & 0 deletions playwright-e2e/dsm/component/modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Locator, Page } from '@playwright/test';
import Button from 'dss/component/button';
import Input from 'dss/component/input';
import Radiobutton from 'dss/component/radiobutton';
import { waitForNoSpinner } from 'utils/test-utils';

export default class Modal {
private readonly rootSelector: Locator;
Expand Down Expand Up @@ -31,6 +32,15 @@ export default class Modal {
return this.headerLocator().innerText();
}

async getBodyText(): Promise<string> {
return this.bodyLocator().innerText();
}

async close(): Promise<void> {
await this.getButton({ label: 'Close' }).click();
await waitForNoSpinner(this.page);
}

public getButton(opts: { label?: string | RegExp; ddpTestID?: string }): Button {
const { label, ddpTestID } = opts;
return new Button(this.page, { label, ddpTestID, root: this.toLocator() });
Expand Down
5 changes: 3 additions & 2 deletions playwright-e2e/dsm/component/tables/participant-list-table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import ParticipantPage from 'dsm/pages/participant-page/participant-page';
import {ParticipantsListPaginator} from 'lib/component/dsm/paginators/participantsListPaginator';
import {rows} from 'lib/component/dsm/paginators/types/rowsPerPage';
import { getDate, offsetDaysFromToday } from 'utils/date-utils';
import { AdditionalFilter } from '../filters/sections/search/search-enums';
import { waitForNoSpinner } from 'utils/test-utils';
import { AdditionalFilter } from 'dsm/component/filters/sections/search/search-enums';
import ParticipantListPage from 'dsm/pages/participant-list-page';
import addColumnsToParticipantList from 'dsm/pages/participant-list-page';

export class ParticipantListTable extends Table {
private readonly _participantPage: ParticipantPage = new ParticipantPage(this.page);
Expand Down Expand Up @@ -34,6 +34,7 @@ export class ParticipantListTable extends Table {

public async openParticipantPageAt(position: number): Promise<ParticipantPage> {
await this.getParticipantAt(position).click();
await waitForNoSpinner(this.page);
await this._participantPage.assertPageTitle();
return this._participantPage;
}
Expand Down
8 changes: 8 additions & 0 deletions playwright-e2e/dsm/component/tables/previous-surveys-table.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Page } from '@playwright/test';
import Table from 'dss/component/table';

export default class PreviousSurveysTable extends Table {
constructor(page: Page) {
super(page, { cssClassAttribute: '.table' });
}
}
17 changes: 17 additions & 0 deletions playwright-e2e/dsm/pages/dsm-page-base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Page } from '@playwright/test';
import { waitForNoSpinner } from 'utils/test-utils';

export default abstract class DsmPageBase {
protected readonly page: Page;
protected readonly baseUrl: string | undefined;

protected constructor(page: Page, baseURL?: string) {
this.page = page;
this.baseUrl = baseURL;
}

async reload(): Promise<void> {
await this.page.reload();
await waitForNoSpinner(this.page);
}
}
67 changes: 67 additions & 0 deletions playwright-e2e/dsm/pages/follow-up-survey-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { APIRequestContext, expect, Page } from '@playwright/test';
import { MiscellaneousEnum } from 'dsm/component/navigation/enums/miscellaneousNav-enum';
import { Navigation } from 'dsm/component/navigation/navigation';
import PreviousSurveysTable from 'dsm/component/tables/previous-surveys-table';
import { WelcomePage } from 'dsm/pages/welcome-page';
import Button from 'dss/component/button';
import Input from 'dss/component/input';
import Select from 'dss/component/select';
import { waitForNoSpinner } from 'utils/test-utils';

export default class FollowUpSurveyPage {
private readonly _table: PreviousSurveysTable = new PreviousSurveysTable(this.page);

static async goto(page: Page, study: string, request: APIRequestContext): Promise<FollowUpSurveyPage> {
const welcomePage = new WelcomePage(page);
await welcomePage.selectStudy(study);

const navigation = new Navigation(page, request);
await navigation.selectMiscellaneous(MiscellaneousEnum.FOLLOW_UP_SURVEY);
const followUpPage = new FollowUpSurveyPage(page);
await followUpPage.waitForReady();
return followUpPage;
}

constructor(private readonly page: Page) {}

public get previousSurveysTable(): PreviousSurveysTable {
return this._table;
}

public async waitForReady(): Promise<void> {
await expect(this.page.locator('h1')).toHaveText(/^\s*Follow-Up Survey\s*$/);
await expect(this.select().toLocator()).toBeVisible({ timeout: 30000 });
await waitForNoSpinner(this.page);
}

public async selectSurvey(survey: string): Promise<void> {
await this.select().selectOption(survey);
await waitForNoSpinner(this.page);
}

public async participantId(id: string): Promise<void> {
const input = new Input(this.page, { label: 'Participant ID' });
await input.fill(id);
}

public async reasonForFollowUpSurvey(reason: string): Promise<void> {
const input = new Input(this.page, { label: 'Reason for Follow-Up Survey' });
await input.fill(reason);
}

public async createSurvey(): Promise<void> {
const button = new Button(this.page, { label: 'Create Survey', root: 'app-survey' });
await button.click();
await waitForNoSpinner(this.page);
}

public select(): Select {
return new Select(this.page, { label: 'Survey', root: 'app-survey' });
}

public async reloadTable(): Promise<void> {
const button = new Button(this.page, { label: 'Reload', root: 'app-survey' });
await button.click();
await waitForNoSpinner(this.page);
}
}
6 changes: 4 additions & 2 deletions playwright-e2e/dsm/pages/mailing-list-page.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Download, expect, Locator, Page } from '@playwright/test';
import DsmPageBase from 'dsm/pages/dsm-page-base';
import Table from 'dss/component/table';

export const COLUMN = {
Expand All @@ -8,11 +9,12 @@ export const COLUMN = {
LAST_NAME: 'Last Name'
}

export default class MailingListPage {
export default class MailingListPage extends DsmPageBase {
private readonly title: string | RegExp;
readonly downloadButton: Locator;

constructor(private readonly page: Page, study: string|RegExp) {
constructor(page: Page, study: string|RegExp) {
super(page);
this.title = study;
this.downloadButton = this.page.getByRole('button', { name: 'Download mailing list' })
}
Expand Down
3 changes: 2 additions & 1 deletion playwright-e2e/dsm/pages/participant-list-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ export default class ParticipantListPage {
const navigation = new Navigation(page, request);
const participantListPage = await navigation.selectFromStudy<ParticipantListPage>(StudyNavEnum.PARTICIPANT_LIST);
await participantListPage.waitForReady();
await participantListPage.assertPageTitle();

const participantsTable = participantListPage.participantListTable;
const rowsTotal = await participantsTable.rowLocator().count();
Expand All @@ -32,7 +31,9 @@ export default class ParticipantListPage {
constructor(private readonly page: Page) {}

public async waitForReady(): Promise<void> {
await this.assertPageTitle();
await waitForNoSpinner(this.page);
await expect(this.page.locator(this.filters.searchPanel.openButtonXPath)).toBeVisible();
}

public async selectAll(): Promise<void> {
Expand Down
6 changes: 5 additions & 1 deletion playwright-e2e/dsm/pages/welcome-page.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Page } from '@playwright/test';
import { expect, Page } from '@playwright/test';
import Select from 'dss/component/select';
import { waitForNoSpinner } from 'utils/test-utils';

export class WelcomePage {
private readonly selectWidget: Select = new Select(this.page, { label: 'Select study' });
Expand All @@ -8,5 +9,8 @@ export class WelcomePage {

public async selectStudy(studyName: string): Promise<void> {
await this.selectWidget.selectOption(studyName);
await waitForNoSpinner(this.page);
await expect(this.page.locator('h1')).toHaveText('Welcome to the DDP Study Management System');
await expect(this.page.locator('h2')).toHaveText(`You have selected the ${studyName} study.`);
}
}
28 changes: 28 additions & 0 deletions playwright-e2e/dss/component/select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,32 @@ export default class Select extends WidgetBase {
break;
}
}

/**
* Finds all options in a Select or mat-select
* @returns {Promise<string[]>}
*/
async getAllOptions(): Promise<string[]> {
let options;
const tagName = await this.toLocator().evaluate((elem) => elem.tagName);
switch (tagName) {
case 'SELECT':
options = await this.toLocator().locator('option').allInnerTexts();
break;
default:
// Click first to open mat-select dropdown
await this.toLocator().click();
const ariaControlsId = await this.toLocator().getAttribute('aria-controls');
if (!ariaControlsId) {
throw Error('ERROR: Cannot find attribute "aria-controls"');
}
const dropdown = this.page.locator(`#${ariaControlsId}[role="listbox"]`);
options = await dropdown.locator('mat-option .mat-option-text').allInnerTexts();
break;
}
if (!options) {
throw new Error(`Failed to find all options in Select or mat-select`);
}
return options!;
}
}
37 changes: 31 additions & 6 deletions playwright-e2e/dss/component/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export default class Table {
private readonly rowCss: string;
private readonly cellCss: string;
private readonly footerCss: string;
private readonly headerRowCss: string;
private readonly nth: number;

constructor(protected readonly page: Page, opts: { cssClassAttribute?: string; ddpTestID?: string; nth?: number } = {}) {
Expand All @@ -23,14 +24,20 @@ export default class Table {
: cssClassAttribute
? `table${cssClassAttribute}, mat-table${cssClassAttribute}`
: 'table, mat-table, [role="table"]';
this.headerCss = 'th, [role="columnheader"]';
this.headerCss = 'thead th, [role="columnheader"]';
this.headerRowCss = 'thead tr';
this.rowCss = '[role="row"]:not([mat-header-row]):not(mat-header-row), tbody tr';
this.cellCss = '[role="cell"], td';
this.footerCss = 'tfoot tr';
}

async waitForReady() {
await expect(this.tableLocator().locator(this.headerCss).first()).toBeVisible();
async exists(): Promise<boolean> {
return await this.tableLocator().count() === 1;
}

async waitForReady(timeout?: number) {
await this.tableLocator().waitFor({ state: 'attached' });
await expect(this.tableLocator().locator(this.headerCss).first()).toBeVisible({ timeout });
expect(await this.rowLocator().count()).toBeGreaterThanOrEqual(1);
}

Expand Down Expand Up @@ -76,7 +83,6 @@ export default class Table {
const { exactMatch = true } = opts;

// Find the search column header index
const columnNames = await this.getHeaderNames();
const columnIndex = await this.getHeaderIndex(searchHeader, { exactMatch });
if (columnIndex === -1) {
return null; // Not found
Expand Down Expand Up @@ -126,6 +132,12 @@ export default class Table {
return rowText;
}

/**
* Finds text in row underneath specified column name
* @param {number} row It's zero based, nth(0) selects the first row.
* @param {string} column
* @returns {Promise<string | null>}
*/
async getRowText(row: number, column: string): Promise<string | null> {
// Find column index
const columns = await this.getHeaderNames();
Expand Down Expand Up @@ -187,14 +199,15 @@ export default class Table {

async sort(column: string, order: SortOrder): Promise<void> {
const header = this.getHeaderByName(RegExp(column));
await expect(header).toBeVisible();
const headerLink = header.locator('a');

if (await headerLink.count() > 0) {
await headerLink.click();
} else {
await header.click();
}

await waitForNoSpinner(this.page);
await expect(header).toBeVisible({ timeout: 30000 });
let ariaLabel = await header.locator('span').last().getAttribute('aria-label');
if (ariaLabel) {
if (ariaLabel !== order) {
Expand Down Expand Up @@ -249,6 +262,18 @@ export default class Table {
}
}

async searchByColumn(column1Name: string, value1: string, opts?: { column2Name: string, value2: string }): Promise<void> {
const column1Index = await this.getHeaderIndex(column1Name, { exactMatch: false });
const input1 = this.page.locator(this.headerRowCss).nth(1).locator('th').nth(column1Index).locator('input.form-control');
await input1.fill(value1);
if (opts) {
const column2Index = await this.getHeaderIndex(opts.column2Name, { exactMatch: false });
const input2 = this.page.locator(this.headerRowCss).nth(1).locator('th').nth(column2Index).locator('input.form-control');
await input2.fill(opts.value2);
}
await waitForNoSpinner(this.page);
}

private parseForNumber(text: string): number | null {
const numericalStr = text.match(/(\d|\.)+/g);
if (numericalStr) {
Expand Down
9 changes: 7 additions & 2 deletions playwright-e2e/dss/pages/atcp/atcp-dashboard-page.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { expect, Page } from '@playwright/test';
import Table from 'dss/component/table';
import { AtcpPageBase } from 'dss/pages/atcp/atcp-page-base';
import { waitForNoSpinner } from 'utils/test-utils';

export default class AtcpDashboardPage extends AtcpPageBase {
constructor(page: Page) {
Expand All @@ -9,8 +10,12 @@ export default class AtcpDashboardPage extends AtcpPageBase {

async waitForReady(): Promise<void> {
await super.waitForReady();
await expect(this.page).toHaveURL(/\/dashboard/);
await expect(this.page.locator('h1.title')).toHaveText('Thank you for joining the Global A-T Family Data Platform!');
await Promise.all([
expect(this.page).toHaveURL(/\/dashboard/),
expect(this.page.locator('h1.title')).toHaveText('Thank you for joining the Global A-T Family Data Platform!'),
])
await waitForNoSpinner(this.page);
await this.getTable().waitForReady();
}

getTable(): Table {
Expand Down
10 changes: 6 additions & 4 deletions playwright-e2e/dss/pages/lms/lms-home-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ export default class LmsHomePage extends LmsPageBase {
}

async waitForReady(): Promise<void> {
await expect(this.page).toHaveTitle('Leiomyosarcoma Project');
await expect(this.countMeInButton.first()).toBeVisible();
await expect(this.learnMoreButton).toBeVisible();
await expect(this.getLogInButton()).toBeVisible();
await waitForNoSpinner(this.page);
await Promise.all([
expect(this.page).toHaveTitle('Leiomyosarcoma Project'),
expect(this.countMeInButton.first()).toBeVisible(),
expect(this.learnMoreButton).toBeVisible(),
expect(this.getLogInButton()).toBeVisible()
])
}

async countMeIn(): Promise<void> {
Expand Down
2 changes: 1 addition & 1 deletion playwright-e2e/dss/pages/page-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ export default abstract class PageBase implements PageInterface {

async waitForReady(): Promise<void> {
await this.page.waitForLoadState('networkidle');
await waitForNoSpinner(this.page);
await expect(this.page).toHaveTitle(/\D+/);
await waitForNoSpinner(this.page);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,6 @@ test.describe('ATCP adult self-consent enrollment', () => {
await dashboardPage.waitForReady();

await expect(page.locator('h2.subtitle')).toHaveScreenshot('atcp-dashboard-thank-you-message.png');
await expect(page.locator('.registration-status')).toHaveScreenshot('atcp-dashboard-registration-status.png');

const expectedHeaders = ['Form', 'Summary', 'Created', 'Status', 'Actions'];
const table = dashboardPage.getTable();
Expand Down
Loading