From 1d276119c7d3e72e327aaf7d322fa01c0f2cfcd7 Mon Sep 17 00:00:00 2001 From: david oluwasusi Date: Tue, 16 Dec 2025 13:25:46 +0000 Subject: [PATCH 01/34] upload artificats if test fails in CI --- .github/workflows/e2e-ci.yml | 6 ++++++ packages/platform-test/playwright-report/index.html | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/e2e-ci.yml b/.github/workflows/e2e-ci.yml index 4e0553992..143a4360a 100644 --- a/.github/workflows/e2e-ci.yml +++ b/.github/workflows/e2e-ci.yml @@ -52,6 +52,12 @@ jobs: yarn playwright install --with-deps chromium webkit - name: yarn dev:test:platform:e2e run: yarn dev:test:platform:e2e + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: playwright-report/ + retention-days: 7 env: PLAYWRIGHT_TEST_BASE_URL: "https://${{ needs.get_branch_name.outputs.branch }}--ot-platform.netlify.app" DEBUG: pw:api \ No newline at end of file diff --git a/packages/platform-test/playwright-report/index.html b/packages/platform-test/playwright-report/index.html index dc7801c9d..9cff85cfa 100644 --- a/packages/platform-test/playwright-report/index.html +++ b/packages/platform-test/playwright-report/index.html @@ -73,4 +73,4 @@
- \ No newline at end of file + From 0689684b0f5c49a7a2295b973eba3ad9ea5b54d6 Mon Sep 17 00:00:00 2001 From: david oluwasusi Date: Tue, 16 Dec 2025 13:44:20 +0000 Subject: [PATCH 02/34] bug(e2e-artifact):adjust path relative to actions context --- .github/workflows/e2e-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e-ci.yml b/.github/workflows/e2e-ci.yml index 143a4360a..e0a2f7790 100644 --- a/.github/workflows/e2e-ci.yml +++ b/.github/workflows/e2e-ci.yml @@ -56,7 +56,7 @@ jobs: if: ${{ !cancelled() }} with: name: playwright-report - path: playwright-report/ + path: ${{ github.workspace }}/packages/platform-test/playwright-report retention-days: 7 env: PLAYWRIGHT_TEST_BASE_URL: "https://${{ needs.get_branch_name.outputs.branch }}--ot-platform.netlify.app" From 308b985b72a8a68feca2e018dfaf9d8170cb612b Mon Sep 17 00:00:00 2001 From: david oluwasusi Date: Tue, 16 Dec 2025 13:57:06 +0000 Subject: [PATCH 03/34] chore(artifact-e2e): increase timeout to test artifact upload --- .github/workflows/e2e-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e-ci.yml b/.github/workflows/e2e-ci.yml index e0a2f7790..45be472fc 100644 --- a/.github/workflows/e2e-ci.yml +++ b/.github/workflows/e2e-ci.yml @@ -38,7 +38,7 @@ jobs: needs: [tests_e2e_netlify_prepare, get_branch_name] name: Run end-to-end tests on Netlify PR preview runs-on: ubuntu-latest - timeout-minutes: 5 + timeout-minutes: 20 steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 From d7476691081417a3f69167ebc6328e3aa2247c50 Mon Sep 17 00:00:00 2001 From: david oluwasusi Date: Thu, 18 Dec 2025 16:10:34 +0000 Subject: [PATCH 04/34] feat(e2e-study): add interactors and tests for study pages --- apps/platform/src/pages/StudyPage/Header.tsx | 2 + .../pages/StudyPage/StudyProfileHeader.tsx | 33 +- .../StudyProfileHeader/studyProfileHeader.ts | 365 ++++++++++++++ .../widgets/GWAS/gwasStudiesSection.ts | 4 +- .../widgets/Study/gwasCredibleSetsSection.ts | 123 +++++ .../widgets/Study/qtlCredibleSetsSection.ts | 110 ++++ .../Study/sharedTraitStudiesSection.ts | 123 +++++ .../platform-test/POM/page/disease/disease.ts | 24 +- .../platform-test/POM/page/study/study.ts | 83 +++ .../e2e/pages/study/studyPageGWAS.spec.ts | 475 ++++++++++++++++++ .../e2e/pages/study/studyPageQTL.spec.ts | 305 +++++++++++ .../playwright-report/index.html | 2 +- packages/ui/src/components/DetailPopover.tsx | 4 +- packages/ui/src/components/Header.tsx | 2 +- .../ui/src/components/OtTable/OtTable.tsx | 4 + .../ui/src/components/ProfileHeader/Field.tsx | 5 +- .../ui/src/components/Summary/SummaryItem.jsx | 1 + .../Table/TablePaginationActions.jsx | 4 + 18 files changed, 1644 insertions(+), 25 deletions(-) create mode 100644 packages/platform-test/POM/objects/components/StudyProfileHeader/studyProfileHeader.ts create mode 100644 packages/platform-test/POM/objects/widgets/Study/gwasCredibleSetsSection.ts create mode 100644 packages/platform-test/POM/objects/widgets/Study/qtlCredibleSetsSection.ts create mode 100644 packages/platform-test/POM/objects/widgets/Study/sharedTraitStudiesSection.ts create mode 100644 packages/platform-test/POM/page/study/study.ts create mode 100644 packages/platform-test/e2e/pages/study/studyPageGWAS.spec.ts create mode 100644 packages/platform-test/e2e/pages/study/studyPageQTL.spec.ts diff --git a/apps/platform/src/pages/StudyPage/Header.tsx b/apps/platform/src/pages/StudyPage/Header.tsx index 08666f5ef..aff96a88b 100644 --- a/apps/platform/src/pages/StudyPage/Header.tsx +++ b/apps/platform/src/pages/StudyPage/Header.tsx @@ -14,6 +14,7 @@ type HeaderProps = { name: string; }[]; studyCategory: string; + testId?: string; }; function Header({ @@ -23,6 +24,7 @@ function Header({ targetId, diseases, projectId, + testId, }: HeaderProps) { let traitLinks, sourceLink; if (projectId === "GCST") { diff --git a/apps/platform/src/pages/StudyPage/StudyProfileHeader.tsx b/apps/platform/src/pages/StudyPage/StudyProfileHeader.tsx index f621a37ae..68b193af0 100644 --- a/apps/platform/src/pages/StudyPage/StudyProfileHeader.tsx +++ b/apps/platform/src/pages/StudyPage/StudyProfileHeader.tsx @@ -51,16 +51,16 @@ function ProfileHeader() { return ( - + {getStudyTypeDisplay(studyType)} {studyType === "gwas" && ( <> - + {traitFromSource} {diseases?.length > 0 && ( - + {diseases.map(({ id, name }, index) => ( {index > 0 ? ", " : null} @@ -70,7 +70,7 @@ function ProfileHeader() { )} {backgroundTraits?.length > 0 && ( - + {backgroundTraits.map(({ id, name }, index) => ( {index > 0 ? ", " : null} @@ -84,29 +84,29 @@ function ProfileHeader() { {studyType !== "gwas" && ( // QTL <> {projectId && ( - + {projectId?.replace(/_/gi, " ")} )} {target?.id && ( - + {target.approvedSymbol} )} {biosample?.biosampleId && ( - + {biosample.biosampleName} )} - + {condition} )} {publicationFirstAuthor && ( - + )} {pubmedId && ( - + )} - + {!hasSumstats ? ( "Not Available" ) : sumstatQCValues ? ( @@ -134,7 +134,7 @@ function ProfileHeader() { )} {qualityControls?.length > 0 && ( - +
    )} {nSamples && ( - + )} - + {/* do not show anything when value 0 */} {nCases ? nCases?.toLocaleString() : null} - + {/* do not show anything when value 0 */} {nCases ? nControls?.toLocaleString() : null} - + {analysisFlags?.join(", ")} @@ -178,6 +178,7 @@ function ProfileHeader() { ({ ldPopulation, relativeSampleSize }) => ( { + return await this.getProfileHeader() + .isVisible() + .catch(() => false); + } + + // Study type field + async getStudyTypeField(): Promise { + const studyText = await this.page.locator("[data-testid='field-study-type']"); + return studyText; + } + + async getStudyType(): Promise { + const studyTypeField = await this.getStudyTypeField(); + return await studyTypeField.textContent(); + } + + async isStudyTypeVisible(): Promise { + const studyTypeField = await this.getStudyTypeField(); + return await studyTypeField.isVisible().catch(() => false); + } + + // Reported trait (GWAS only) + getReportedTraitField(): Locator { + return this.page.locator("[data-testid='field-reported-trait']"); + } + + async getReportedTrait(): Promise { + const reportedTraitField = await this.getReportedTraitField(); + return await reportedTraitField.textContent(); + } + + async isReportedTraitVisible(): Promise { + return await this.getReportedTraitField() + .isVisible() + .catch(() => false); + } + + // Disease or phenotype field (GWAS only) + getDiseaseField(): Locator { + return this.page.locator("[data-testid='field-disease-or-phenotype']"); + } + + async getDiseases(): Promise { + const diseaseLinks = this.getDiseaseField().locator("a"); + const count = await diseaseLinks.count(); + const diseases: string[] = []; + + for (let i = 0; i < count; i++) { + const text = await diseaseLinks.nth(i).textContent(); + if (text) diseases.push(text); + } + + return diseases; + } + + async isDiseaseFieldVisible(): Promise { + return await this.getDiseaseField() + .isVisible() + .catch(() => false); + } + + async clickDiseaseLink(index: number = 0): Promise { + await this.getDiseaseField().locator("a").nth(index).click(); + } + + // Background trait (GWAS only) + getBackgroundTraitField(): Locator { + return this.page.locator("[data-testid='field-background-trait']"); + } + + async getBackgroundTraits(): Promise { + const traitLinks = this.getBackgroundTraitField().locator("a"); + const count = await traitLinks.count(); + const traits: string[] = []; + + for (let i = 0; i < count; i++) { + const text = await traitLinks.nth(i).textContent(); + if (text) traits.push(text); + } + + return traits; + } + + async isBackgroundTraitVisible(): Promise { + return await this.getBackgroundTraitField() + .isVisible() + .catch(() => false); + } + + // Project field (QTL only) + getProjectField(): Locator { + return this.page.locator("[data-testid='field-project']"); + } + + async getProject(): Promise { + const projectField = await this.getProjectField(); + return await projectField.textContent(); + } + + async isProjectFieldVisible(): Promise { + return await this.getProjectField() + .isVisible() + .catch(() => false); + } + + // Affected gene field (QTL only) + getAffectedGeneField(): Locator { + return this.page.locator("[data-testid='field-affected-gene']"); + } + + async getAffectedGene(): Promise { + const link = this.getAffectedGeneField().locator("a"); + return await link.textContent(); + } + + async isAffectedGeneVisible(): Promise { + return await this.getAffectedGeneField() + .isVisible() + .catch(() => false); + } + + async clickAffectedGeneLink(): Promise { + await this.getAffectedGeneField().locator("a").click(); + } + + // Affected cell/tissue field (QTL only) + getAffectedCellTissueField(): Locator { + return this.page.locator("[data-testid='field-affected-cell-tissue']"); + } + + async getAffectedCellTissue(): Promise { + const link = this.getAffectedCellTissueField().locator("a"); + return await link.textContent(); + } + + async isAffectedCellTissueVisible(): Promise { + return await this.getAffectedCellTissueField() + .isVisible() + .catch(() => false); + } + + // Condition field (QTL only) + getConditionField(): Locator { + return this.page.locator("[data-testid='field-condition']"); + } + + async getCondition(): Promise { + const conditionField = await this.getConditionField(); + return await conditionField.textContent(); + } + + async isConditionVisible(): Promise { + return await this.getConditionField() + .isVisible() + .catch(() => false); + } + + // Publication field + getPublicationField(): Locator { + return this.page.locator("[data-testid='field-publication']"); + } + + async getPublication(): Promise { + const publicationField = await this.getPublicationField(); + return await publicationField.textContent(); + } + + async isPublicationVisible(): Promise { + return await this.getPublicationField() + .isVisible() + .catch(() => false); + } + + // PubMed field + getPubMedField(): Locator { + return this.page.locator("[data-testid='field-pubmed']"); + } + + async getPubMedId(): Promise { + return await this.getPubMedField().textContent(); + } + + async isPubMedVisible(): Promise { + return await this.getPubMedField() + .isVisible() + .catch(() => false); + } + + async clickPubMedLink(): Promise { + await this.getPubMedField().locator("button").click(); + } + + // Summary statistics field + getSummaryStatsField(): Locator { + return this.page.locator("[data-testid='field-summary-statistics']"); + } + + async getSummaryStatsText(): Promise { + // Check if there's a DetailPopover button (when sumstatQCValues exist) + const popoverButton = this.getSummaryStatsField().locator( + "[data-testid='detail-popover-trigger']" + ); + const hasPopover = await popoverButton.isVisible().catch(() => false); + + if (hasPopover) { + // Get the button text which will be "Available" + return await popoverButton.textContent(); + } + + // Otherwise get the direct text content (either "Not Available" or "Available") + const fieldContent = this.getSummaryStatsField(); + return await fieldContent.textContent(); + } + + async isSummaryStatsAvailable(): Promise { + const text = await this.getSummaryStatsText(); + return text?.includes("Available") && !text?.includes("Not Available"); + } + + async hasSummaryStatsPopover(): Promise { + const button = this.getSummaryStatsField().locator("[data-testid='detail-popover-trigger']"); + return await button.isVisible().catch(() => false); + } + + async clickSummaryStatsPopover(): Promise { + const button = this.getSummaryStatsField().locator("[data-testid='detail-popover-trigger']"); + const hasPopover = await button.isVisible().catch(() => false); + + if (hasPopover) { + await button.click(); + } else { + throw new Error("Summary statistics popover button not found"); + } + } + + // QC warnings + getQCWarningsField(): Locator { + return this.page.locator("[data-testid='field-qc-warnings']"); + } + + async hasQCWarnings(): Promise { + return await this.getQCWarningsField() + .isVisible() + .catch(() => false); + } + + async clickQCWarnings(): Promise { + await this.getQCWarningsField().locator("[data-testid='detail-popover-trigger']").click(); + } + + // Sample size field + getSampleSizeField(): Locator { + return this.page.locator("[data-testid='field-sample-size']"); + } + + async getSampleSize(): Promise { + const sampleSizeField = await this.getSampleSizeField(); + return await sampleSizeField.textContent(); + } + + async isSampleSizeVisible(): Promise { + const sampleSizeField = await this.getSampleSizeField(); + return await sampleSizeField.isVisible().catch(() => false); + } + + // N cases field + getNCasesField(): Locator { + return this.page.locator("[data-testid='field-n-cases']"); + } + + async getNCases(): Promise { + const nCasesField = await this.getNCasesField(); + return await nCasesField.textContent(); + } + + async isNCasesVisible(): Promise { + return await this.getNCasesField() + .isVisible() + .catch(() => false); + } + + // N controls field + getNControlsField(): Locator { + return this.page.locator("[data-testid='field-n-controls']"); + } + + async getNControls(): Promise { + const nControlsField = await this.getNControlsField(); + return await nControlsField.textContent(); + } + + async isNControlsVisible(): Promise { + return await this.getNControlsField() + .isVisible() + .catch(() => false); + } + + // Analysis field + getAnalysisField(): Locator { + return this.page.locator("[data-testid='field-analysis']"); + } + + async getAnalysis(): Promise { + const analysisField = await this.getAnalysisField(); + return await analysisField.textContent(); + } + + async isAnalysisVisible(): Promise { + return await this.getAnalysisField() + .isVisible() + .catch(() => false); + } + + // Population chips (LD reference population) + getPopulationChips(): Locator { + return this.page.locator("[data-testid^='chip-ld-population-']"); + } + + async getPopulationChipsCount(): Promise { + return await this.getPopulationChips().count(); + } + + async getPopulationChipLabel(index: number): Promise { + return await this.getPopulationChips() + .nth(index) + .locator("[data-testid^='chip-ld-population-']") + .first() + .textContent(); + } + + async getPopulationChipValue(index: number): Promise { + return await this.getPopulationChips() + .nth(index) + .locator("[data-testid^='chip-ld-population-']") + .nth(1) + .textContent(); + } + + async hoverPopulationChip(index: number): Promise { + await this.getPopulationChips().nth(index).hover(); + } + + // Wait for profile header to load + async waitForProfileHeaderLoad(): Promise { + await this.page.waitForSelector("[data-testid='profile-page-header-block']", { + state: "visible", + }); + await this.page.waitForTimeout(500); + } +} diff --git a/packages/platform-test/POM/objects/widgets/GWAS/gwasStudiesSection.ts b/packages/platform-test/POM/objects/widgets/GWAS/gwasStudiesSection.ts index 96e07fa3d..4a35da2cf 100644 --- a/packages/platform-test/POM/objects/widgets/GWAS/gwasStudiesSection.ts +++ b/packages/platform-test/POM/objects/widgets/GWAS/gwasStudiesSection.ts @@ -50,12 +50,12 @@ export class GWASStudiesSection { } async getStudyId(rowIndex: number): Promise { - return await this.getStudyCell(rowIndex, 0).textContent(); + return await this.getStudyLink(rowIndex).textContent(); } // Study links getStudyLink(rowIndex: number): Locator { - return this.getTableRows().nth(rowIndex).locator("a").first(); + return this.getTableRows().nth(rowIndex).locator('a[href*="/study/"]'); } async clickStudy(rowIndex: number): Promise { diff --git a/packages/platform-test/POM/objects/widgets/Study/gwasCredibleSetsSection.ts b/packages/platform-test/POM/objects/widgets/Study/gwasCredibleSetsSection.ts new file mode 100644 index 000000000..585df30b9 --- /dev/null +++ b/packages/platform-test/POM/objects/widgets/Study/gwasCredibleSetsSection.ts @@ -0,0 +1,123 @@ +import type { Locator, Page } from "@playwright/test"; + +export class GWASCredibleSetsSection { + page: Page; + + constructor(page: Page) { + this.page = page; + } + + // Section container + getSection(): Locator { + return this.page.locator("[data-testid='section-gwas-credible-sets']"); + } + + async isSectionVisible(): Promise { + return await this.getSection() + .isVisible() + .catch(() => false); + } + + // Section header + getSectionHeader(): Locator { + return this.page.locator("[data-testid='section-gwas-credible-sets-header']"); + } + + async getSectionTitle(): Promise { + return await this.getSectionHeader().textContent(); + } + + // Table + getTable(): Locator { + return this.getSection().locator("table"); + } + + async isTableVisible(): Promise { + return await this.getTable() + .isVisible() + .catch(() => false); + } + + getTableRows(): Locator { + return this.getTable().locator("tbody tr"); + } + + async getRowCount(): Promise { + return await this.getTableRows().count(); + } + + // Get cell data + getCell(rowIndex: number, columnIndex: number): Locator { + return this.getTableRows().nth(rowIndex).locator("td").nth(columnIndex); + } + + async getCellText(rowIndex: number, columnIndex: number): Promise { + return await this.getCell(rowIndex, columnIndex).textContent(); + } + + // Get variant link + getVariantLink(rowIndex: number): Locator { + return this.getTableRows().nth(rowIndex).locator("a[href*='/variant/']"); + } + + async clickVariantLink(rowIndex: number): Promise { + await this.getVariantLink(rowIndex).click(); + } + + async getVariantId(rowIndex: number): Promise { + return await this.getVariantLink(rowIndex).textContent(); + } + + // Get gene link + getGeneLink(rowIndex: number): Locator { + return this.getTableRows().nth(rowIndex).locator("a[href*='/target/']"); + } + + async clickGeneLink(rowIndex: number): Promise { + await this.getGeneLink(rowIndex).click(); + } + + async getGeneName(rowIndex: number): Promise { + return await this.getGeneLink(rowIndex).textContent(); + } + + // Search/Filter + getSearchInput(): Locator { + return this.getSection().locator("input[type='text']"); + } + + async searchCredibleSet(searchTerm: string): Promise { + await this.getSearchInput().fill(searchTerm); + } + + // Pagination + getNextPageButton(): Locator { + return this.getSection().locator("[data-testid='pagination-next-button']"); + } + + getPreviousPageButton(): Locator { + return this.getSection().locator("[data-testid='pagination-previous-button']"); + } + + async clickNextPage(): Promise { + await this.getNextPageButton().click(); + } + + async clickPreviousPage(): Promise { + await this.getPreviousPageButton().click(); + } + + async isNextPageEnabled(): Promise { + return await this.getNextPageButton().isEnabled(); + } + + async isPreviousPageEnabled(): Promise { + return await this.getPreviousPageButton().isEnabled(); + } + + // Wait for section to load + async waitForSectionLoad(): Promise { + await this.getSection().waitFor({ state: "visible", timeout: 10000 }); + await this.page.waitForTimeout(500); + } +} diff --git a/packages/platform-test/POM/objects/widgets/Study/qtlCredibleSetsSection.ts b/packages/platform-test/POM/objects/widgets/Study/qtlCredibleSetsSection.ts new file mode 100644 index 000000000..9fbd2a2aa --- /dev/null +++ b/packages/platform-test/POM/objects/widgets/Study/qtlCredibleSetsSection.ts @@ -0,0 +1,110 @@ +import type { Locator, Page } from "@playwright/test"; + +export class QTLCredibleSetsSection { + page: Page; + + constructor(page: Page) { + this.page = page; + } + + // Section container + getSection(): Locator { + return this.page.locator("[data-testid='section-qtl-credible-sets']"); + } + + async isSectionVisible(): Promise { + return await this.getSection() + .isVisible() + .catch(() => false); + } + + // Section header + getSectionHeader(): Locator { + return this.page.locator("[data-testid='section-qtl-credible-sets-header']"); + } + + async getSectionTitle(): Promise { + return await this.getSectionHeader().textContent(); + } + + // Table + getTable(): Locator { + return this.getSection().locator("table"); + } + + async isTableVisible(): Promise { + return await this.getTable() + .isVisible() + .catch(() => false); + } + + getTableRows(): Locator { + return this.getTable().locator("tbody tr"); + } + + async getRowCount(): Promise { + return await this.getTableRows().count(); + } + + // Get cell data + getCell(rowIndex: number, columnIndex: number): Locator { + return this.getTableRows().nth(rowIndex).locator("td").nth(columnIndex); + } + + async getCellText(rowIndex: number, columnIndex: number): Promise { + return await this.getCell(rowIndex, columnIndex).textContent(); + } + + // Get variant link + getVariantLink(rowIndex: number): Locator { + return this.getTableRows().nth(rowIndex).locator("a[href*='/variant/']"); + } + + async clickVariantLink(rowIndex: number): Promise { + await this.getVariantLink(rowIndex).click(); + } + + async getVariantId(rowIndex: number): Promise { + return await this.getVariantLink(rowIndex).textContent(); + } + + // Search/Filter + getSearchInput(): Locator { + return this.getSection().locator("input[type='text']"); + } + + async searchCredibleSet(searchTerm: string): Promise { + await this.getSearchInput().fill(searchTerm); + } + + // Pagination + getNextPageButton(): Locator { + return this.getSection().locator("[data-testid='pagination-next-button']"); + } + + getPreviousPageButton(): Locator { + return this.getSection().locator("[data-testid='pagination-previous-button']"); + } + + async clickNextPage(): Promise { + await this.getNextPageButton().click(); + } + + async clickPreviousPage(): Promise { + await this.getPreviousPageButton().click(); + } + + async isNextPageEnabled(): Promise { + return await this.getNextPageButton().isEnabled(); + } + + async isPreviousPageEnabled(): Promise { + return await this.getPreviousPageButton().isEnabled(); + } + + // Wait for section to load + async waitForSectionLoad(): Promise { + await this.getSection().waitFor({ state: "visible", timeout: 10000 }); + await this.page.waitForTimeout(500); + } +} diff --git a/packages/platform-test/POM/objects/widgets/Study/sharedTraitStudiesSection.ts b/packages/platform-test/POM/objects/widgets/Study/sharedTraitStudiesSection.ts new file mode 100644 index 000000000..ddec6d2b0 --- /dev/null +++ b/packages/platform-test/POM/objects/widgets/Study/sharedTraitStudiesSection.ts @@ -0,0 +1,123 @@ +import type { Locator, Page } from "@playwright/test"; + +export class SharedTraitStudiesSection { + page: Page; + + constructor(page: Page) { + this.page = page; + } + + // Section container + getSection(): Locator { + return this.page.locator("[data-testid='section-shared-trait-studies']"); + } + + async isSectionVisible(): Promise { + return await this.getSection() + .isVisible() + .catch(() => false); + } + + // Section header + getSectionHeader(): Locator { + return this.page.locator("[data-testid='section-shared-trait-studies-header']"); + } + + async getSectionTitle(): Promise { + return await this.getSectionHeader().textContent(); + } + + // Table + getTable(): Locator { + return this.getSection().locator("table"); + } + + async isTableVisible(): Promise { + return await this.getTable() + .isVisible() + .catch(() => false); + } + + getTableRows(): Locator { + return this.getTable().locator("tbody tr"); + } + + async getRowCount(): Promise { + return await this.getTableRows().count(); + } + + // Get cell data + getCell(rowIndex: number, columnIndex: number): Locator { + return this.getTableRows().nth(rowIndex).locator("td").nth(columnIndex); + } + + async getCellText(rowIndex: number, columnIndex: number): Promise { + return await this.getCell(rowIndex, columnIndex).textContent(); + } + + // Get study link + getStudyLink(rowIndex: number): Locator { + return this.getTableRows().nth(rowIndex).locator("a[href*='/study/']"); + } + + async clickStudyLink(rowIndex: number): Promise { + await this.getStudyLink(rowIndex).click(); + } + + async getStudyId(rowIndex: number): Promise { + return await this.getStudyLink(rowIndex).textContent(); + } + + // Get disease link + getDiseaseLink(rowIndex: number): Locator { + return this.getTableRows().nth(rowIndex).locator("a[href*='/disease/']"); + } + + async clickDiseaseLink(rowIndex: number): Promise { + await this.getDiseaseLink(rowIndex).click(); + } + + async getDiseaseName(rowIndex: number): Promise { + return await this.getDiseaseLink(rowIndex).textContent(); + } + + // Search/Filter + getSearchInput(): Locator { + return this.getSection().locator("input[type='text']"); + } + + async searchStudy(searchTerm: string): Promise { + await this.getSearchInput().fill(searchTerm); + } + + // Pagination + getNextPageButton(): Locator { + return this.getSection().locator("[data-testid='pagination-next-button']"); + } + + getPreviousPageButton(): Locator { + return this.getSection().locator("[data-testid='pagination-previous-button']"); + } + + async clickNextPage(): Promise { + await this.getNextPageButton().click(); + } + + async clickPreviousPage(): Promise { + await this.getPreviousPageButton().click(); + } + + async isNextPageEnabled(): Promise { + return await this.getNextPageButton().isEnabled(); + } + + async isPreviousPageEnabled(): Promise { + return await this.getPreviousPageButton().isEnabled(); + } + + // Wait for section to load + async waitForSectionLoad(): Promise { + await this.getSection().waitFor({ state: "visible", timeout: 10000 }); + await this.page.waitForTimeout(500); + } +} diff --git a/packages/platform-test/POM/page/disease/disease.ts b/packages/platform-test/POM/page/disease/disease.ts index 661d19dc4..8341e4ff9 100644 --- a/packages/platform-test/POM/page/disease/disease.ts +++ b/packages/platform-test/POM/page/disease/disease.ts @@ -6,7 +6,7 @@ export class DiseasePage { constructor(page: Page) { this.page = page; - this.originalURL = page.url(); + this.originalURL = page.url(); } getProfilePage() { @@ -16,7 +16,7 @@ export class DiseasePage { async goToProfilePage() { await this.page.goto(this.getProfilePage()); } - + async goToAssociationsPage() { await this.page.goto(`${this.originalURL}`); } @@ -41,4 +41,24 @@ export class DiseasePage { async getFirstXrefLinkHref(): Promise { return await this.getXrefLinks().first().getAttribute("href"); } + + // Navigate to study page from evidence table + getStudyLinkInEvidence(studyId: string): Locator { + return this.page.locator(`a[href*="/study/${studyId}"]`).first(); + } + + async goToStudyPageFromEvidence(studyId: string): Promise { + await this.getStudyLinkInEvidence(studyId).click(); + await this.page.waitForURL(`**/study/${studyId}**`); + } + + async getFirstStudyLinkInEvidence(): Promise { + return this.page.locator('a[href*="/study/"]').first(); + } + + async clickFirstStudyInEvidence(): Promise { + const firstStudyLink = await this.getFirstStudyLinkInEvidence(); + await firstStudyLink.click(); + await this.page.waitForLoadState("networkidle"); + } } diff --git a/packages/platform-test/POM/page/study/study.ts b/packages/platform-test/POM/page/study/study.ts new file mode 100644 index 000000000..21f21d493 --- /dev/null +++ b/packages/platform-test/POM/page/study/study.ts @@ -0,0 +1,83 @@ +import type { Locator, Page } from "@playwright/test"; +import { GWASStudiesSection } from "../../objects/widgets/GWAS/gwasStudiesSection" + +export class StudyPage { + page: Page; + originalURL: string; + STUDY_BASE_URL = "/study/"; + CHOSEN_STUDY_ID = "" + + constructor(page: Page) { + this.page = page; + this.originalURL = page.url(); + } + + /** + * Navigate to a study page from disease page evidence table + * @param studyId - The study ID to navigate to + */ + async goToStudyPageFromEvidence(studyId: string) { + // Find the study link in evidence tables and click it + const studyLink = this.page.locator(`a[href*="${this.STUDY_BASE_URL}${studyId}"]`).first(); + await studyLink.click(); + await this.page.waitForURL(`**${this.STUDY_BASE_URL}${studyId}**`); + } + + /** + * Navigate directly to a study page + * @param baseURL - The base URL of the application + * @param studyId - The study ID to navigate to + */ + async goToStudyPage(baseURL: string, studyId: string) { + await this.page.goto(`${baseURL}${this.STUDY_BASE_URL}${studyId}`); + } + + async goToStudyPageFromGWASWidgetOnDiseasePage(DISEASE_EFO_ID: string) { + await this.page.goto(`/disease/${DISEASE_EFO_ID}/`); + const gwasStudiesSection = new GWASStudiesSection(this.page); + await gwasStudiesSection.getStudyId(0).then(async (studyId) => { + if (studyId) { + const trimmedStudyId = studyId.trim(); + this.CHOSEN_STUDY_ID = trimmedStudyId; + await this.goToStudyPageFromEvidence(trimmedStudyId); + } + }); + + } + + // Tab navigation + getProfileTab(): Locator { + return this.page.locator(`a[href*="${this.STUDY_BASE_URL}"][role="tab"]`).first(); + } + + async isProfileTabActive(): Promise { + const tab = this.getProfileTab(); + const ariaSelected = await tab.getAttribute("aria-selected"); + return ariaSelected === "true"; + } + + async clickProfileTab(): Promise { + await this.getProfileTab().click(); + } + + // Study page header + getStudyHeader(): Locator { + return this.page.locator("[data-testid='profile-page-header']"); + } + + async isStudyHeaderVisible(): Promise { + return await this.getStudyHeader() + .isVisible() + .catch(() => false); + } + + async getStudyIdFromHeader(): Promise { + return await this.page.locator("[data-testid='profile-page-header-text']").textContent(); + } + + // Wait for page load + async waitForStudyPageLoad(): Promise { + await this.page.waitForLoadState("networkidle"); + await this.page.waitForTimeout(500); + } +} diff --git a/packages/platform-test/e2e/pages/study/studyPageGWAS.spec.ts b/packages/platform-test/e2e/pages/study/studyPageGWAS.spec.ts new file mode 100644 index 000000000..8d27588f3 --- /dev/null +++ b/packages/platform-test/e2e/pages/study/studyPageGWAS.spec.ts @@ -0,0 +1,475 @@ +import { test } from "@playwright/test"; +import { StudyProfileHeader } from "../../../POM/objects/components/StudyProfileHeader/studyProfileHeader"; +import { GWASCredibleSetsSection } from "../../../POM/objects/widgets/Study/gwasCredibleSetsSection"; +import { SharedTraitStudiesSection } from "../../../POM/objects/widgets/Study/sharedTraitStudiesSection"; +import { StudyPage } from "../../../POM/page/study/study"; + +// Test Disease study +const DISEASE_EFO_ID = "EFO_0000612"; + +test.describe("Study Page - GWAS Study", () => { + test.beforeEach(async ({ page, baseURL }) => { + const studyPage = new StudyPage(page); + // await studyPage.goToStudyPageFromGWASWidgetOnDiseasePage(DISEASE_EFO_ID); + await studyPage.goToStudyPage(baseURL!, "GCST90475211"); + await studyPage.waitForStudyPageLoad(); + }); + + test.describe("GWAS Study Profile Header", () => { + test("Profile header is visible and displays study information", async ({ page }) => { + const profileHeader = new StudyProfileHeader(page); + await profileHeader.waitForProfileHeaderLoad(); + + const isVisible = await profileHeader.isProfileHeaderVisible(); + test.expect(isVisible).toBe(true); + }); + + test("Study type is displayed correctly as GWAS", async ({ page }) => { + const profileHeader = new StudyProfileHeader(page); + + const studyType = await profileHeader.getStudyType(); + test.expect(studyType).toContain("GWAS"); + }); + + test("Reported trait is visible and has content", async ({ page }) => { + const profileHeader = new StudyProfileHeader(page); + + const isVisible = await profileHeader.isReportedTraitVisible(); + test.expect(isVisible).toBe(true); + + const reportedTrait = await profileHeader.getReportedTrait(); + test.expect(reportedTrait).toBeTruthy(); + test.expect(reportedTrait?.length).toBeGreaterThan(0); + }); + + test("Disease or phenotype field displays when disease data exists", async ({ page }) => { + const profileHeader = new StudyProfileHeader(page); + await profileHeader.waitForProfileHeaderLoad(); + + const isVisible = await profileHeader.isDiseaseFieldVisible(); + + if (isVisible) { + const diseases = await profileHeader.getDiseases(); + test.expect(diseases.length).toBeGreaterThan(0); + + // Verify first disease has text + test.expect(diseases[0]).toBeTruthy(); + } + }); + + test("Can click disease link and navigate to disease page", async ({ page }) => { + const profileHeader = new StudyProfileHeader(page); + await profileHeader.waitForProfileHeaderLoad(); + + const isVisible = await profileHeader.isDiseaseFieldVisible(); + + if (isVisible) { + const diseases = await profileHeader.getDiseases(); + + if (diseases.length > 0) { + // Click on the disease link + await profileHeader.clickDiseaseLink(0); + + // Wait for navigation to disease page + await page.waitForURL("**/disease/**"); + + // Verify we're on a disease page + test.expect(page.url()).toContain("/disease/"); + } + } + }); + + test("Background trait is displayed when available", async ({ page }) => { + const profileHeader = new StudyProfileHeader(page); + await profileHeader.waitForProfileHeaderLoad(); + + const isVisible = await profileHeader.isBackgroundTraitVisible(); + + if (isVisible) { + const backgroundTraits = await profileHeader.getBackgroundTraits(); + test.expect(backgroundTraits.length).toBeGreaterThan(0); + } else { + test.skip(true, "No background traits available for this study"); + } + }); + + test("Publication information is displayed", async ({ page }) => { + const profileHeader = new StudyProfileHeader(page); + await profileHeader.waitForProfileHeaderLoad(); + + const isVisible = await profileHeader.isPublicationVisible(); + console.log("isVisible:", isVisible); + if (isVisible) { + const publication = await profileHeader.getPublication(); + test.expect(publication).toBeTruthy(); + } + }); + + test("PubMed ID is displayed when available", async ({ page }) => { + const profileHeader = new StudyProfileHeader(page); + await profileHeader.waitForProfileHeaderLoad(); + + const isVisible = await profileHeader.isPubMedVisible(); + + if (isVisible) { + const pubmedId = await profileHeader.getPubMedId(); + test.expect(pubmedId).toBeTruthy(); + test.expect(pubmedId).toMatch(/\d+/); // Should contain numbers + } + }); + + test("Summary statistics availability is indicated", async ({ page }) => { + const profileHeader = new StudyProfileHeader(page); + await profileHeader.waitForProfileHeaderLoad(); + + const summaryStatsText = await profileHeader.getSummaryStatsText(); + test.expect(summaryStatsText).toBeTruthy(); + + // Check if summary stats are available or not + const isAvailable = await profileHeader.isSummaryStatsAvailable(); + if (isAvailable) { + test.expect(summaryStatsText).not.toContain("Not Available"); + + // Check if there's a popover for detailed stats + const hasPopover = await profileHeader.hasSummaryStatsPopover(); + if (hasPopover) { + await profileHeader.clickSummaryStatsPopover(); + await page.waitForTimeout(300); + + // Verify popover appeared + const popover = page.locator("[data-testid='detail-popover-content']"); + const isPopoverVisible = await popover.isVisible().catch(() => false); + test.expect(isPopoverVisible).toBe(true); + } + } else { + test.expect(summaryStatsText).toContain("Not Available"); + } + }); + + test("QC warnings are displayed when present", async ({ page }) => { + const profileHeader = new StudyProfileHeader(page); + + const hasWarnings = await profileHeader.hasQCWarnings(); + + if (hasWarnings) { + await profileHeader.clickQCWarnings(); + await page.waitForTimeout(300); + + // Verify warnings popover appeared + const popover = page.locator("[data-testid='detail-popover-content']"); + const isPopoverVisible = await popover.isVisible().catch(() => false); + test.expect(isPopoverVisible).toBe(true); + } else { + test.skip(true, "No QC warnings available for this study"); + } + }); + + test("Sample size is displayed", async ({ page }) => { + const profileHeader = new StudyProfileHeader(page); + await profileHeader.waitForProfileHeaderLoad(); + + const isVisible = await profileHeader.isSampleSizeVisible(); + + if (isVisible) { + const sampleSize = await profileHeader.getSampleSize(); + test.expect(sampleSize).toBeTruthy(); + } + }); + + test("N cases is displayed when available", async ({ page }) => { + const profileHeader = new StudyProfileHeader(page); + await profileHeader.waitForProfileHeaderLoad(); + + const isVisible = await profileHeader.isNCasesVisible(); + + if (isVisible) { + const nCases = await profileHeader.getNCases(); + test.expect(nCases).toBeTruthy(); + test.expect(nCases).toMatch(/\d/); // Should contain at least one digit + } + }); + + test("N controls is displayed when available", async ({ page }) => { + const profileHeader = new StudyProfileHeader(page); + await profileHeader.waitForProfileHeaderLoad(); + + const isVisible = await profileHeader.isNControlsVisible(); + + if (isVisible) { + const nControls = await profileHeader.getNControls(); + test.expect(nControls).toBeTruthy(); + test.expect(nControls).toMatch(/\d/); // Should contain at least one digit + } + }); + + test("Analysis flags are displayed when available", async ({ page }) => { + const profileHeader = new StudyProfileHeader(page); + await profileHeader.waitForProfileHeaderLoad(); + + const isVisible = await profileHeader.isAnalysisVisible(); + + if (isVisible) { + const analysis = await profileHeader.getAnalysis(); + test.expect(analysis).toBeTruthy(); + } + }); + + test("Population chips display LD reference populations", async ({ page }) => { + const profileHeader = new StudyProfileHeader(page); + await profileHeader.waitForProfileHeaderLoad(); + + const chipCount = await profileHeader.getPopulationChipsCount(); + + if (chipCount > 0) { + // Check first population chip + const label = await profileHeader.getPopulationChipLabel(0); + const value = await profileHeader.getPopulationChipValue(0); + + test.expect(label).toBeTruthy(); + test.expect(value).toMatch(/\d+%/); // Should be a percentage + + // Hover to see tooltip + await profileHeader.hoverPopulationChip(0); + await page.waitForTimeout(300); + } + }); + + test("QTL-specific fields are not visible for GWAS studies", async ({ page }) => { + const profileHeader = new StudyProfileHeader(page); + await profileHeader.waitForProfileHeaderLoad(); + + // QTL-specific fields should not be visible + const projectVisible = await profileHeader.isProjectFieldVisible(); + const affectedGeneVisible = await profileHeader.isAffectedGeneVisible(); + const affectedCellVisible = await profileHeader.isAffectedCellTissueVisible(); + const conditionVisible = await profileHeader.isConditionVisible(); + + test.expect(projectVisible).toBe(false); + test.expect(affectedGeneVisible).toBe(false); + test.expect(affectedCellVisible).toBe(false); + test.expect(conditionVisible).toBe(false); + }); + }); + + test.describe("GWAS Credible Sets Section", () => { + test("GWAS Credible Sets section is visible", async ({ page }) => { + const gwasCredibleSets = new GWASCredibleSetsSection(page); + + const isVisible = await gwasCredibleSets.isSectionVisible(); + test.expect(isVisible).toBe(true); + + if (isVisible) { + await gwasCredibleSets.waitForSectionLoad(); + + const title = await gwasCredibleSets.getSectionTitle(); + test.expect(title).toBeTruthy(); + } + }); + + test("GWAS Credible Sets table displays data", async ({ page }) => { + const gwasCredibleSets = new GWASCredibleSetsSection(page); + await gwasCredibleSets.waitForSectionLoad(); + + const isTableVisible = await gwasCredibleSets.isTableVisible(); + + if (isTableVisible) { + const rowCount = await gwasCredibleSets.getRowCount(); + test.expect(rowCount).toBeGreaterThan(0); + + // Check first row has data + const firstCellText = await gwasCredibleSets.getCellText(0, 0); + test.expect(firstCellText).toBeTruthy(); + } + }); + + test("Can click variant link in GWAS Credible Sets table", async ({ page }) => { + const gwasCredibleSets = new GWASCredibleSetsSection(page); + await gwasCredibleSets.waitForSectionLoad(); + + const isTableVisible = await gwasCredibleSets.isTableVisible(); + + if (isTableVisible) { + const rowCount = await gwasCredibleSets.getRowCount(); + + if (rowCount > 0) { + const variantId = await gwasCredibleSets.getVariantId(0); + test.expect(variantId).toBeTruthy(); + + // Click variant link + await gwasCredibleSets.clickVariantLink(0); + await page.waitForURL("**/variant/**"); + + // Verify we're on variant page + test.expect(page.url()).toContain("/variant/"); + } + } + }); + + test("Can paginate through GWAS Credible Sets table", async ({ page }) => { + const gwasCredibleSets = new GWASCredibleSetsSection(page); + await gwasCredibleSets.waitForSectionLoad(); + + const isTableVisible = await gwasCredibleSets.isTableVisible(); + + if (isTableVisible) { + const isNextEnabled = await gwasCredibleSets.isNextPageEnabled(); + + if (isNextEnabled) { + // Get first page data + const firstPageData = await gwasCredibleSets.getCellText(0, 1); + + // Go to next page + await gwasCredibleSets.clickNextPage(); + await page.waitForTimeout(1000); + + // Get second page data + const secondPageData = await gwasCredibleSets.getCellText(0, 1); + + // Data should be different + test.expect(firstPageData).not.toBe(secondPageData); + + // Go back + const isPrevEnabled = await gwasCredibleSets.isPreviousPageEnabled(); + if (isPrevEnabled) { + await gwasCredibleSets.clickPreviousPage(); + await page.waitForTimeout(1000); + } + } + } + }); + + test("Can search in GWAS Credible Sets table", async ({ page }) => { + const gwasCredibleSets = new GWASCredibleSetsSection(page); + await gwasCredibleSets.waitForSectionLoad(); + + const isTableVisible = await gwasCredibleSets.isTableVisible(); + + if (isTableVisible) { + const rowCount = await gwasCredibleSets.getRowCount(); + + if (rowCount > 0) { + // Get a variant ID to search for + const variantId = await gwasCredibleSets.getVariantId(0); + + if (variantId) { + // Search for it + await gwasCredibleSets.searchCredibleSet(variantId); + await page.waitForTimeout(1000); + + // Verify filtered results + const filteredRowCount = await gwasCredibleSets.getRowCount(); + test.expect(filteredRowCount).toBeGreaterThan(0); + } + } + } + }); + }); + + test.describe("Shared Trait Studies Section", () => { + test("Shared Trait Studies section is visible for GWAS studies", async ({ page }) => { + const sharedTraitStudies = new SharedTraitStudiesSection(page); + + const isVisible = await sharedTraitStudies.isSectionVisible(); + + if (isVisible) { + await sharedTraitStudies.waitForSectionLoad(); + + const title = await sharedTraitStudies.getSectionTitle(); + test.expect(title).toBeTruthy(); + } + }); + + test("Shared Trait Studies table displays related studies", async ({ page }) => { + const sharedTraitStudies = new SharedTraitStudiesSection(page); + + const isVisible = await sharedTraitStudies.isSectionVisible(); + + if (isVisible) { + await sharedTraitStudies.waitForSectionLoad(); + + const isTableVisible = await sharedTraitStudies.isTableVisible(); + + if (isTableVisible) { + const rowCount = await sharedTraitStudies.getRowCount(); + test.expect(rowCount).toBeGreaterThan(0); + + // Check first row has study ID + const studyId = await sharedTraitStudies.getStudyId(0); + test.expect(studyId).toBeTruthy(); + } + } + }); + + test("Can navigate to another study from Shared Trait Studies table", async ({ page }) => { + const sharedTraitStudies = new SharedTraitStudiesSection(page); + + const isVisible = await sharedTraitStudies.isSectionVisible(); + + if (isVisible) { + await sharedTraitStudies.waitForSectionLoad(); + + const isTableVisible = await sharedTraitStudies.isTableVisible(); + + if (isTableVisible) { + const rowCount = await sharedTraitStudies.getRowCount(); + + if (rowCount > 0) { + const studyId = await sharedTraitStudies.getStudyId(0); + + // Click study link + await sharedTraitStudies.clickStudyLink(0); + await page.waitForURL("**/study/**"); + + // Verify we're on a different study page + test.expect(page.url()).toContain("/study/"); + } + } + } + }); + + test("Can navigate to disease from Shared Trait Studies table", async ({ page }) => { + const sharedTraitStudies = new SharedTraitStudiesSection(page); + + const isVisible = await sharedTraitStudies.isSectionVisible(); + + if (isVisible) { + await sharedTraitStudies.waitForSectionLoad(); + + const isTableVisible = await sharedTraitStudies.isTableVisible(); + + if (isTableVisible) { + const rowCount = await sharedTraitStudies.getRowCount(); + + if (rowCount > 0) { + const diseaseName = await sharedTraitStudies.getDiseaseName(0); + + if (diseaseName) { + // Click disease link + await sharedTraitStudies.clickDiseaseLink(0); + await page.waitForURL("**/disease/**"); + + // Verify we're on disease page + test.expect(page.url()).toContain("/disease/"); + } + } + } + } + }); + }); + + // test.describe("Study Page Navigation", () => { + // test("Profile tab is active by default", async ({ page }) => { + // const studyPage = new StudyPage(page); + + // const isActive = await studyPage.isProfileTabActive(); + // test.expect(isActive).toBe(true); + // }); + + // // test("Study page displays correct study ID in header", async ({ page }) => { + // // const studyPage = new StudyPage(page); + + // // const studyId = await studyPage.getStudyIdFromHeader(); + // // test.expect(studyId).toContain(GWAS_STUDY_ID); + // // }); + // }); +}); diff --git a/packages/platform-test/e2e/pages/study/studyPageQTL.spec.ts b/packages/platform-test/e2e/pages/study/studyPageQTL.spec.ts new file mode 100644 index 000000000..f307cdc40 --- /dev/null +++ b/packages/platform-test/e2e/pages/study/studyPageQTL.spec.ts @@ -0,0 +1,305 @@ +import { test } from "@playwright/test"; +import { StudyProfileHeader } from "../../../POM/objects/components/StudyProfileHeader/studyProfileHeader"; +import { QTLCredibleSetsSection } from "../../../POM/objects/widgets/Study/qtlCredibleSetsSection"; +import { StudyPage } from "../../../POM/page/study/study"; + +// Test QTL study - eQTL +const QTL_STUDY_ID = "UKB_PPP_EUR_LPA_P08519_OID30747_v1"; + +test.describe("Study Page - QTL Study", () => { + test.beforeEach(async ({ page, baseURL }) => { + const studyPage = new StudyPage(page); + await studyPage.goToStudyPage(baseURL as string, QTL_STUDY_ID); + await studyPage.waitForStudyPageLoad(); + }); + + test.describe("QTL Study Profile Header", () => { + test("Profile header is visible and displays study information", async ({ page }) => { + const profileHeader = new StudyProfileHeader(page); + await profileHeader.waitForProfileHeaderLoad(); + + const isVisible = await profileHeader.isProfileHeaderVisible(); + test.expect(isVisible).toBe(true); + }); + + test("Study type is displayed correctly as QTL", async ({ page }) => { + const profileHeader = new StudyProfileHeader(page); + await profileHeader.waitForProfileHeaderLoad(); + + const studyType = await profileHeader.getStudyType(); + test.expect(studyType).toContain("QTL"); + }); + + test("Project field is displayed for QTL studies", async ({ page }) => { + const profileHeader = new StudyProfileHeader(page); + await profileHeader.waitForProfileHeaderLoad(); + + const isVisible = await profileHeader.isProjectFieldVisible(); + + if (isVisible) { + const project = await profileHeader.getProject(); + test.expect(project).toBeTruthy(); + test.expect(project?.length).toBeGreaterThan(0); + } + }); + + test("Affected gene field is displayed and clickable", async ({ page }) => { + const profileHeader = new StudyProfileHeader(page); + await profileHeader.waitForProfileHeaderLoad(); + + const isVisible = await profileHeader.isAffectedGeneVisible(); + + if (isVisible) { + const geneName = await profileHeader.getAffectedGene(); + test.expect(geneName).toBeTruthy(); + + // Click gene link to verify navigation + await profileHeader.clickAffectedGeneLink(); + await page.waitForURL("**/target/**"); + + // Verify we're on target page + test.expect(page.url()).toContain("/target/"); + } + }); + + test("Affected cell/tissue field is displayed with external link", async ({ page }) => { + const profileHeader = new StudyProfileHeader(page); + await profileHeader.waitForProfileHeaderLoad(); + + const isVisible = await profileHeader.isAffectedCellTissueVisible(); + + if (isVisible) { + const cellTissue = await profileHeader.getAffectedCellTissue(); + test.expect(cellTissue).toBeTruthy(); + } + }); + + test("Condition field is displayed for QTL studies", async ({ page }) => { + const profileHeader = new StudyProfileHeader(page); + await profileHeader.waitForProfileHeaderLoad(); + + const isVisible = await profileHeader.isConditionVisible(); + + if (isVisible) { + const condition = await profileHeader.getCondition(); + test.expect(condition).toBeTruthy(); + } + }); + + test("Publication information is displayed when available", async ({ page }) => { + const profileHeader = new StudyProfileHeader(page); + await profileHeader.waitForProfileHeaderLoad(); + + const isVisible = await profileHeader.isPublicationVisible(); + + if (isVisible) { + const publication = await profileHeader.getPublication(); + test.expect(publication).toBeTruthy(); + } + }); + + test("PubMed ID is displayed when available", async ({ page }) => { + const profileHeader = new StudyProfileHeader(page); + await profileHeader.waitForProfileHeaderLoad(); + + const isVisible = await profileHeader.isPubMedVisible(); + + if (isVisible) { + const pubmedId = await profileHeader.getPubMedId(); + test.expect(pubmedId).toBeTruthy(); + } + }); + + test("Summary statistics availability is indicated", async ({ page }) => { + const profileHeader = new StudyProfileHeader(page); + await profileHeader.waitForProfileHeaderLoad(); + + const summaryStatsText = await profileHeader.getSummaryStatsText(); + test.expect(summaryStatsText).toBeTruthy(); + }); + + test("Sample size is displayed when available", async ({ page }) => { + const profileHeader = new StudyProfileHeader(page); + await profileHeader.waitForProfileHeaderLoad(); + + const isVisible = await profileHeader.isSampleSizeVisible(); + + if (isVisible) { + const sampleSize = await profileHeader.getSampleSize(); + test.expect(sampleSize).toBeTruthy(); + } + }); + + test("Analysis flags are displayed when available", async ({ page }) => { + const profileHeader = new StudyProfileHeader(page); + await profileHeader.waitForProfileHeaderLoad(); + + const isVisible = await profileHeader.isAnalysisVisible(); + + if (isVisible) { + const analysis = await profileHeader.getAnalysis(); + test.expect(analysis).toBeTruthy(); + } + }); + + test("Population chips display LD reference populations", async ({ page }) => { + const profileHeader = new StudyProfileHeader(page); + await profileHeader.waitForProfileHeaderLoad(); + + const chipCount = await profileHeader.getPopulationChipsCount(); + + if (chipCount > 0) { + // Check first population chip + const label = await profileHeader.getPopulationChipLabel(0); + const value = await profileHeader.getPopulationChipValue(0); + + test.expect(label).toBeTruthy(); + test.expect(value).toMatch(/\d+%/); // Should be a percentage + } + }); + + test("GWAS-specific fields are not visible for QTL studies", async ({ page }) => { + const profileHeader = new StudyProfileHeader(page); + await profileHeader.waitForProfileHeaderLoad(); + + // GWAS-specific fields should not be visible + const reportedTraitVisible = await profileHeader.isReportedTraitVisible(); + const diseaseVisible = await profileHeader.isDiseaseFieldVisible(); + const backgroundTraitVisible = await profileHeader.isBackgroundTraitVisible(); + + test.expect(reportedTraitVisible).toBe(false); + test.expect(diseaseVisible).toBe(false); + test.expect(backgroundTraitVisible).toBe(false); + }); + }); + + test.describe("QTL Credible Sets Section", () => { + test("QTL Credible Sets section is visible", async ({ page }) => { + const qtlCredibleSets = new QTLCredibleSetsSection(page); + + const isVisible = await qtlCredibleSets.isSectionVisible(); + test.expect(isVisible).toBe(true); + + if (isVisible) { + await qtlCredibleSets.waitForSectionLoad(); + + const title = await qtlCredibleSets.getSectionTitle(); + test.expect(title).toBeTruthy(); + } + }); + + test("QTL Credible Sets table displays data", async ({ page }) => { + const qtlCredibleSets = new QTLCredibleSetsSection(page); + await qtlCredibleSets.waitForSectionLoad(); + + const isTableVisible = await qtlCredibleSets.isTableVisible(); + + if (isTableVisible) { + const rowCount = await qtlCredibleSets.getRowCount(); + test.expect(rowCount).toBeGreaterThan(0); + + // Check first row has data + const firstCellText = await qtlCredibleSets.getCellText(0, 0); + test.expect(firstCellText).toBeTruthy(); + } + }); + + test("Can click variant link in QTL Credible Sets table", async ({ page }) => { + const qtlCredibleSets = new QTLCredibleSetsSection(page); + await qtlCredibleSets.waitForSectionLoad(); + + const isTableVisible = await qtlCredibleSets.isTableVisible(); + + if (isTableVisible) { + const rowCount = await qtlCredibleSets.getRowCount(); + + if (rowCount > 0) { + const variantId = await qtlCredibleSets.getVariantId(0); + test.expect(variantId).toBeTruthy(); + + // Click variant link + await qtlCredibleSets.clickVariantLink(0); + await page.waitForURL("**/variant/**"); + + // Verify we're on variant page + test.expect(page.url()).toContain("/variant/"); + } + } + }); + + test("Can paginate through QTL Credible Sets table", async ({ page }) => { + const qtlCredibleSets = new QTLCredibleSetsSection(page); + await qtlCredibleSets.waitForSectionLoad(); + + const isTableVisible = await qtlCredibleSets.isTableVisible(); + + if (isTableVisible) { + const isNextEnabled = await qtlCredibleSets.isNextPageEnabled(); + + if (isNextEnabled) { + // Get first page data + const firstPageData = await qtlCredibleSets.getCellText(1, 1); + + // Go to next page + await qtlCredibleSets.clickNextPage(); + await page.waitForTimeout(1000); + + // Get second page data + const secondPageData = await qtlCredibleSets.getCellText(1, 1); + + // Data should be different + test.expect(firstPageData).not.toBe(secondPageData); + + // Go back + const isPrevEnabled = await qtlCredibleSets.isPreviousPageEnabled(); + if (isPrevEnabled) { + await qtlCredibleSets.clickPreviousPage(); + await page.waitForTimeout(1000); + } + } + } + }); + + test("Can search in QTL Credible Sets table", async ({ page }) => { + const qtlCredibleSets = new QTLCredibleSetsSection(page); + await qtlCredibleSets.waitForSectionLoad(); + + const isTableVisible = await qtlCredibleSets.isTableVisible(); + + if (isTableVisible) { + const rowCount = await qtlCredibleSets.getRowCount(); + + if (rowCount > 0) { + // Get a variant ID to search for + const variantId = await qtlCredibleSets.getVariantId(0); + + if (variantId) { + // Search for it + await qtlCredibleSets.searchCredibleSet(variantId); + await page.waitForTimeout(1000); + + // Verify filtered results + const filteredRowCount = await qtlCredibleSets.getRowCount(); + test.expect(filteredRowCount).toBeGreaterThan(0); + } + } + } + }); + }); + + test.describe("Study Page Navigation", () => { + test("Profile tab is active by default", async ({ page }) => { + const studyPage = new StudyPage(page); + + const isActive = await studyPage.isProfileTabActive(); + test.expect(isActive).toBe(true); + }); + + test("Study page displays correct study ID in header", async ({ page }) => { + const studyPage = new StudyPage(page); + + const studyId = await studyPage.getStudyIdFromHeader(); + test.expect(studyId).toContain(QTL_STUDY_ID); + }); + }); +}); diff --git a/packages/platform-test/playwright-report/index.html b/packages/platform-test/playwright-report/index.html index fcf46348a..2a4208643 100644 --- a/packages/platform-test/playwright-report/index.html +++ b/packages/platform-test/playwright-report/index.html @@ -73,4 +73,4 @@
    - + \ No newline at end of file diff --git a/packages/ui/src/components/DetailPopover.tsx b/packages/ui/src/components/DetailPopover.tsx index 74de2dc53..e71b56557 100644 --- a/packages/ui/src/components/DetailPopover.tsx +++ b/packages/ui/src/components/DetailPopover.tsx @@ -39,6 +39,8 @@ export default function DetailPopover({ fontWeight: 600, color: "secondary.main", }} + data-testid="detail-popover-trigger" + aria-roledescription="button" aria-describedby={id} onClick={handleClick} > @@ -57,7 +59,7 @@ export default function DetailPopover({ horizontal: "left", }} > - {children} + {children} ); diff --git a/packages/ui/src/components/Header.tsx b/packages/ui/src/components/Header.tsx index 3881bc542..b27933541 100644 --- a/packages/ui/src/components/Header.tsx +++ b/packages/ui/src/components/Header.tsx @@ -76,7 +76,7 @@ function Header({ const classes = useStyles(); return ( - + diff --git a/packages/ui/src/components/OtTable/OtTable.tsx b/packages/ui/src/components/OtTable/OtTable.tsx index fd05cbdb3..345510ea9 100644 --- a/packages/ui/src/components/OtTable/OtTable.tsx +++ b/packages/ui/src/components/OtTable/OtTable.tsx @@ -340,6 +340,7 @@ function OtTable({ onClick={() => table.setPageIndex(0)} disabled={!table.getCanPreviousPage()} aria-label="First Page" + data-testid="pagination-first-button" > @@ -347,12 +348,14 @@ function OtTable({ onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()} aria-label="Previous Page" + data-testid="pagination-previous-button" > table.nextPage()} + data-testid="pagination-next-button" disabled={!table.getCanNextPage()} aria-label="Next Page" > @@ -361,6 +364,7 @@ function OtTable({ table.setPageIndex(table.getPageCount() - 1)} disabled={!table.getCanNextPage()} + data-testid="pagination-last-button" aria-label="last page" > diff --git a/packages/ui/src/components/ProfileHeader/Field.tsx b/packages/ui/src/components/ProfileHeader/Field.tsx index a08b847bf..e585bfcb1 100644 --- a/packages/ui/src/components/ProfileHeader/Field.tsx +++ b/packages/ui/src/components/ProfileHeader/Field.tsx @@ -5,9 +5,10 @@ type FieldProps = { children?: ReactNode; loading: boolean; title: string; + testId?: string; }; -function Field({ title, loading, children }: FieldProps): ReactNode { +function Field({ title, loading, children, testId }: FieldProps): ReactNode { if (loading) return ; if (!children || (Array.isArray(children) && children.length === 0)) { @@ -15,7 +16,7 @@ function Field({ title, loading, children }: FieldProps): ReactNode { } return ( - + {title}:{" "} {children} diff --git a/packages/ui/src/components/Summary/SummaryItem.jsx b/packages/ui/src/components/Summary/SummaryItem.jsx index db7541a78..cc5f67a97 100644 --- a/packages/ui/src/components/Summary/SummaryItem.jsx +++ b/packages/ui/src/components/Summary/SummaryItem.jsx @@ -31,6 +31,7 @@ function SummaryItem({ definition, request, subText }) { return ( = Math.ceil(count / rowsPerPage) - 1} @@ -56,6 +59,7 @@ export function PaginationActionsComplete({ count, page, rowsPerPage, onPageChan = Math.ceil(count / rowsPerPage) - 1} From d10217e26268692a1b8b245bf55797698dcca859 Mon Sep 17 00:00:00 2001 From: david oluwasusi Date: Thu, 18 Dec 2025 16:42:52 +0000 Subject: [PATCH 05/34] chore(e2e-study): refactor profile header interactors --- .../StudyProfileHeader/GWASFields.ts | 75 ++++ .../StudyProfileHeader/PublicationFields.ts | 42 ++ .../StudyProfileHeader/QTLFields.ts | 74 ++++ .../StudyProfileHeader/SampleFields.ts | 97 +++++ .../StudyProfileHeader/StatisticsFields.ts | 62 +++ .../StudyProfileHeader/studyProfileHeader.ts | 408 ++++-------------- .../playwright-report/index.html | 2 +- 7 files changed, 436 insertions(+), 324 deletions(-) create mode 100644 packages/platform-test/POM/objects/components/StudyProfileHeader/GWASFields.ts create mode 100644 packages/platform-test/POM/objects/components/StudyProfileHeader/PublicationFields.ts create mode 100644 packages/platform-test/POM/objects/components/StudyProfileHeader/QTLFields.ts create mode 100644 packages/platform-test/POM/objects/components/StudyProfileHeader/SampleFields.ts create mode 100644 packages/platform-test/POM/objects/components/StudyProfileHeader/StatisticsFields.ts diff --git a/packages/platform-test/POM/objects/components/StudyProfileHeader/GWASFields.ts b/packages/platform-test/POM/objects/components/StudyProfileHeader/GWASFields.ts new file mode 100644 index 000000000..6e0ab3376 --- /dev/null +++ b/packages/platform-test/POM/objects/components/StudyProfileHeader/GWASFields.ts @@ -0,0 +1,75 @@ +import type { Locator, Page } from "@playwright/test"; + +/** + * Helper class for GWAS-specific profile fields + */ +export class GWASFields { + constructor(private page: Page) {} + + // Reported trait + getReportedTraitField(): Locator { + return this.page.locator("[data-testid='field-reported-trait']"); + } + + async getReportedTrait(): Promise { + return await this.getReportedTraitField().textContent(); + } + + async isReportedTraitVisible(): Promise { + return await this.getReportedTraitField() + .isVisible() + .catch(() => false); + } + + // Disease or phenotype + getDiseaseField(): Locator { + return this.page.locator("[data-testid='field-disease-or-phenotype']"); + } + + async getDiseases(): Promise { + const diseaseLinks = this.getDiseaseField().locator("a"); + const count = await diseaseLinks.count(); + const diseases: string[] = []; + + for (let i = 0; i < count; i++) { + const text = await diseaseLinks.nth(i).textContent(); + if (text) diseases.push(text); + } + + return diseases; + } + + async isDiseaseFieldVisible(): Promise { + return await this.getDiseaseField() + .isVisible() + .catch(() => false); + } + + async clickDiseaseLink(index: number = 0): Promise { + await this.getDiseaseField().locator("a").nth(index).click(); + } + + // Background trait + getBackgroundTraitField(): Locator { + return this.page.locator("[data-testid='field-background-trait']"); + } + + async getBackgroundTraits(): Promise { + const traitLinks = this.getBackgroundTraitField().locator("a"); + const count = await traitLinks.count(); + const traits: string[] = []; + + for (let i = 0; i < count; i++) { + const text = await traitLinks.nth(i).textContent(); + if (text) traits.push(text); + } + + return traits; + } + + async isBackgroundTraitVisible(): Promise { + return await this.getBackgroundTraitField() + .isVisible() + .catch(() => false); + } +} diff --git a/packages/platform-test/POM/objects/components/StudyProfileHeader/PublicationFields.ts b/packages/platform-test/POM/objects/components/StudyProfileHeader/PublicationFields.ts new file mode 100644 index 000000000..c296d6e2a --- /dev/null +++ b/packages/platform-test/POM/objects/components/StudyProfileHeader/PublicationFields.ts @@ -0,0 +1,42 @@ +import type { Locator, Page } from "@playwright/test"; + +/** + * Helper class for publication and metadata fields + */ +export class PublicationFields { + constructor(private page: Page) {} + + // Publication + getPublicationField(): Locator { + return this.page.locator("[data-testid='field-publication']"); + } + + async getPublication(): Promise { + return await this.getPublicationField().textContent(); + } + + async isPublicationVisible(): Promise { + return await this.getPublicationField() + .isVisible() + .catch(() => false); + } + + // PubMed + getPubMedField(): Locator { + return this.page.locator("[data-testid='field-pubmed']"); + } + + async getPubMedId(): Promise { + return await this.getPubMedField().textContent(); + } + + async isPubMedVisible(): Promise { + return await this.getPubMedField() + .isVisible() + .catch(() => false); + } + + async clickPubMedLink(): Promise { + await this.getPubMedField().locator("button").click(); + } +} diff --git a/packages/platform-test/POM/objects/components/StudyProfileHeader/QTLFields.ts b/packages/platform-test/POM/objects/components/StudyProfileHeader/QTLFields.ts new file mode 100644 index 000000000..97dac1d07 --- /dev/null +++ b/packages/platform-test/POM/objects/components/StudyProfileHeader/QTLFields.ts @@ -0,0 +1,74 @@ +import type { Locator, Page } from "@playwright/test"; + +/** + * Helper class for QTL-specific profile fields + */ +export class QTLFields { + constructor(private page: Page) {} + + // Project + getProjectField(): Locator { + return this.page.locator("[data-testid='field-project']"); + } + + async getProject(): Promise { + return await this.getProjectField().textContent(); + } + + async isProjectFieldVisible(): Promise { + return await this.getProjectField() + .isVisible() + .catch(() => false); + } + + // Affected gene + getAffectedGeneField(): Locator { + return this.page.locator("[data-testid='field-affected-gene']"); + } + + async getAffectedGene(): Promise { + const link = this.getAffectedGeneField().locator("a"); + return await link.textContent(); + } + + async isAffectedGeneVisible(): Promise { + return await this.getAffectedGeneField() + .isVisible() + .catch(() => false); + } + + async clickAffectedGeneLink(): Promise { + await this.getAffectedGeneField().locator("a").click(); + } + + // Affected cell/tissue + getAffectedCellTissueField(): Locator { + return this.page.locator("[data-testid='field-affected-cell-tissue']"); + } + + async getAffectedCellTissue(): Promise { + const link = this.getAffectedCellTissueField().locator("a"); + return await link.textContent(); + } + + async isAffectedCellTissueVisible(): Promise { + return await this.getAffectedCellTissueField() + .isVisible() + .catch(() => false); + } + + // Condition + getConditionField(): Locator { + return this.page.locator("[data-testid='field-condition']"); + } + + async getCondition(): Promise { + return await this.getConditionField().textContent(); + } + + async isConditionVisible(): Promise { + return await this.getConditionField() + .isVisible() + .catch(() => false); + } +} diff --git a/packages/platform-test/POM/objects/components/StudyProfileHeader/SampleFields.ts b/packages/platform-test/POM/objects/components/StudyProfileHeader/SampleFields.ts new file mode 100644 index 000000000..d0324bab5 --- /dev/null +++ b/packages/platform-test/POM/objects/components/StudyProfileHeader/SampleFields.ts @@ -0,0 +1,97 @@ +import type { Locator, Page } from "@playwright/test"; + +/** + * Helper class for sample size and population data + */ +export class SampleFields { + constructor(private page: Page) {} + + // Sample size + getSampleSizeField(): Locator { + return this.page.locator("[data-testid='field-sample-size']"); + } + + async getSampleSize(): Promise { + return await this.getSampleSizeField().textContent(); + } + + async isSampleSizeVisible(): Promise { + return await this.getSampleSizeField() + .isVisible() + .catch(() => false); + } + + // N cases + getNCasesField(): Locator { + return this.page.locator("[data-testid='field-n-cases']"); + } + + async getNCases(): Promise { + return await this.getNCasesField().textContent(); + } + + async isNCasesVisible(): Promise { + return await this.getNCasesField() + .isVisible() + .catch(() => false); + } + + // N controls + getNControlsField(): Locator { + return this.page.locator("[data-testid='field-n-controls']"); + } + + async getNControls(): Promise { + return await this.getNControlsField().textContent(); + } + + async isNControlsVisible(): Promise { + return await this.getNControlsField() + .isVisible() + .catch(() => false); + } + + // Analysis + getAnalysisField(): Locator { + return this.page.locator("[data-testid='field-analysis']"); + } + + async getAnalysis(): Promise { + return await this.getAnalysisField().textContent(); + } + + async isAnalysisVisible(): Promise { + return await this.getAnalysisField() + .isVisible() + .catch(() => false); + } + + // Population chips + getPopulationChips(): Locator { + return this.page.locator("[data-testid^='chip-ld-population-']"); + } + + async getPopulationChipsCount(): Promise { + return await this.getPopulationChips().count(); + } + + async getPopulationChipLabel(index: number): Promise { + return await this.getPopulationChips() + .nth(index) + .locator("[data-testid^='chip-ld-population-']") + .first() + .textContent(); + } + + async getPopulationChipValue(index: number): Promise { + return await this.getPopulationChips() + .nth(index) + .locator("[data-testid^='chip-ld-population-']") + .nth(1) + .textContent(); + } + + async hoverPopulationChip(index: number): Promise { + await this.getPopulationChips().nth(index).hover(); + } +} diff --git a/packages/platform-test/POM/objects/components/StudyProfileHeader/StatisticsFields.ts b/packages/platform-test/POM/objects/components/StudyProfileHeader/StatisticsFields.ts new file mode 100644 index 000000000..139c00090 --- /dev/null +++ b/packages/platform-test/POM/objects/components/StudyProfileHeader/StatisticsFields.ts @@ -0,0 +1,62 @@ +import type { Locator, Page } from "@playwright/test"; + +/** + * Helper class for summary statistics and QC warnings + */ +export class StatisticsFields { + constructor(private page: Page) {} + + // Summary statistics + getSummaryStatsField(): Locator { + return this.page.locator("[data-testid='field-summary-statistics']"); + } + + async getSummaryStatsText(): Promise { + const popoverButton = this.getSummaryStatsField().locator( + "[data-testid='detail-popover-trigger']" + ); + const hasPopover = await popoverButton.isVisible().catch(() => false); + + if (hasPopover) { + return await popoverButton.textContent(); + } + + return await this.getSummaryStatsField().textContent(); + } + + async isSummaryStatsAvailable(): Promise { + const text = await this.getSummaryStatsText(); + return !!text && text.includes("Available") && !text.includes("Not Available"); + } + + async hasSummaryStatsPopover(): Promise { + const button = this.getSummaryStatsField().locator("[data-testid='detail-popover-trigger']"); + return await button.isVisible().catch(() => false); + } + + async clickSummaryStatsPopover(): Promise { + const button = this.getSummaryStatsField().locator("[data-testid='detail-popover-trigger']"); + const hasPopover = await button.isVisible().catch(() => false); + + if (hasPopover) { + await button.click(); + } else { + throw new Error("Summary statistics popover button not found"); + } + } + + // QC warnings + getQCWarningsField(): Locator { + return this.page.locator("[data-testid='field-qc-warnings']"); + } + + async hasQCWarnings(): Promise { + return await this.getQCWarningsField() + .isVisible() + .catch(() => false); + } + + async clickQCWarnings(): Promise { + await this.getQCWarningsField().locator("[data-testid='detail-popover-trigger']").click(); + } +} diff --git a/packages/platform-test/POM/objects/components/StudyProfileHeader/studyProfileHeader.ts b/packages/platform-test/POM/objects/components/StudyProfileHeader/studyProfileHeader.ts index b68cc5ce9..6ad0f4d8c 100644 --- a/packages/platform-test/POM/objects/components/StudyProfileHeader/studyProfileHeader.ts +++ b/packages/platform-test/POM/objects/components/StudyProfileHeader/studyProfileHeader.ts @@ -1,10 +1,29 @@ import type { Locator, Page } from "@playwright/test"; - +import { GWASFields } from "./GWASFields"; +import { PublicationFields } from "./PublicationFields"; +import { QTLFields } from "./QTLFields"; +import { SampleFields } from "./SampleFields"; +import { StatisticsFields } from "./StatisticsFields"; + +/** + * Main class for Study Profile Header interactions + * Composed of specialized field classes for better organization + */ export class StudyProfileHeader { page: Page; + gwas: GWASFields; + qtl: QTLFields; + publication: PublicationFields; + statistics: StatisticsFields; + sample: SampleFields; constructor(page: Page) { this.page = page; + this.gwas = new GWASFields(page); + this.qtl = new QTLFields(page); + this.publication = new PublicationFields(page); + this.statistics = new StatisticsFields(page); + this.sample = new SampleFields(page); } // Profile header container @@ -20,8 +39,7 @@ export class StudyProfileHeader { // Study type field async getStudyTypeField(): Promise { - const studyText = await this.page.locator("[data-testid='field-study-type']"); - return studyText; + return this.page.locator("[data-testid='field-study-type']"); } async getStudyType(): Promise { @@ -34,326 +52,70 @@ export class StudyProfileHeader { return await studyTypeField.isVisible().catch(() => false); } - // Reported trait (GWAS only) - getReportedTraitField(): Locator { - return this.page.locator("[data-testid='field-reported-trait']"); - } - - async getReportedTrait(): Promise { - const reportedTraitField = await this.getReportedTraitField(); - return await reportedTraitField.textContent(); - } - - async isReportedTraitVisible(): Promise { - return await this.getReportedTraitField() - .isVisible() - .catch(() => false); - } - - // Disease or phenotype field (GWAS only) - getDiseaseField(): Locator { - return this.page.locator("[data-testid='field-disease-or-phenotype']"); - } - - async getDiseases(): Promise { - const diseaseLinks = this.getDiseaseField().locator("a"); - const count = await diseaseLinks.count(); - const diseases: string[] = []; - - for (let i = 0; i < count; i++) { - const text = await diseaseLinks.nth(i).textContent(); - if (text) diseases.push(text); - } - - return diseases; - } - - async isDiseaseFieldVisible(): Promise { - return await this.getDiseaseField() - .isVisible() - .catch(() => false); - } - - async clickDiseaseLink(index: number = 0): Promise { - await this.getDiseaseField().locator("a").nth(index).click(); - } - - // Background trait (GWAS only) - getBackgroundTraitField(): Locator { - return this.page.locator("[data-testid='field-background-trait']"); - } - - async getBackgroundTraits(): Promise { - const traitLinks = this.getBackgroundTraitField().locator("a"); - const count = await traitLinks.count(); - const traits: string[] = []; - - for (let i = 0; i < count; i++) { - const text = await traitLinks.nth(i).textContent(); - if (text) traits.push(text); - } - - return traits; - } - - async isBackgroundTraitVisible(): Promise { - return await this.getBackgroundTraitField() - .isVisible() - .catch(() => false); - } - - // Project field (QTL only) - getProjectField(): Locator { - return this.page.locator("[data-testid='field-project']"); - } - - async getProject(): Promise { - const projectField = await this.getProjectField(); - return await projectField.textContent(); - } - - async isProjectFieldVisible(): Promise { - return await this.getProjectField() - .isVisible() - .catch(() => false); - } - - // Affected gene field (QTL only) - getAffectedGeneField(): Locator { - return this.page.locator("[data-testid='field-affected-gene']"); - } - - async getAffectedGene(): Promise { - const link = this.getAffectedGeneField().locator("a"); - return await link.textContent(); - } - - async isAffectedGeneVisible(): Promise { - return await this.getAffectedGeneField() - .isVisible() - .catch(() => false); - } - - async clickAffectedGeneLink(): Promise { - await this.getAffectedGeneField().locator("a").click(); - } - - // Affected cell/tissue field (QTL only) - getAffectedCellTissueField(): Locator { - return this.page.locator("[data-testid='field-affected-cell-tissue']"); - } - - async getAffectedCellTissue(): Promise { - const link = this.getAffectedCellTissueField().locator("a"); - return await link.textContent(); - } - - async isAffectedCellTissueVisible(): Promise { - return await this.getAffectedCellTissueField() - .isVisible() - .catch(() => false); - } - - // Condition field (QTL only) - getConditionField(): Locator { - return this.page.locator("[data-testid='field-condition']"); - } - - async getCondition(): Promise { - const conditionField = await this.getConditionField(); - return await conditionField.textContent(); - } - - async isConditionVisible(): Promise { - return await this.getConditionField() - .isVisible() - .catch(() => false); - } - - // Publication field - getPublicationField(): Locator { - return this.page.locator("[data-testid='field-publication']"); - } - - async getPublication(): Promise { - const publicationField = await this.getPublicationField(); - return await publicationField.textContent(); - } - - async isPublicationVisible(): Promise { - return await this.getPublicationField() - .isVisible() - .catch(() => false); - } - - // PubMed field - getPubMedField(): Locator { - return this.page.locator("[data-testid='field-pubmed']"); - } - - async getPubMedId(): Promise { - return await this.getPubMedField().textContent(); - } - - async isPubMedVisible(): Promise { - return await this.getPubMedField() - .isVisible() - .catch(() => false); - } - - async clickPubMedLink(): Promise { - await this.getPubMedField().locator("button").click(); - } - - // Summary statistics field - getSummaryStatsField(): Locator { - return this.page.locator("[data-testid='field-summary-statistics']"); - } - - async getSummaryStatsText(): Promise { - // Check if there's a DetailPopover button (when sumstatQCValues exist) - const popoverButton = this.getSummaryStatsField().locator( - "[data-testid='detail-popover-trigger']" - ); - const hasPopover = await popoverButton.isVisible().catch(() => false); - - if (hasPopover) { - // Get the button text which will be "Available" - return await popoverButton.textContent(); - } - - // Otherwise get the direct text content (either "Not Available" or "Available") - const fieldContent = this.getSummaryStatsField(); - return await fieldContent.textContent(); - } - - async isSummaryStatsAvailable(): Promise { - const text = await this.getSummaryStatsText(); - return text?.includes("Available") && !text?.includes("Not Available"); - } - - async hasSummaryStatsPopover(): Promise { - const button = this.getSummaryStatsField().locator("[data-testid='detail-popover-trigger']"); - return await button.isVisible().catch(() => false); - } - - async clickSummaryStatsPopover(): Promise { - const button = this.getSummaryStatsField().locator("[data-testid='detail-popover-trigger']"); - const hasPopover = await button.isVisible().catch(() => false); - - if (hasPopover) { - await button.click(); - } else { - throw new Error("Summary statistics popover button not found"); - } - } - - // QC warnings - getQCWarningsField(): Locator { - return this.page.locator("[data-testid='field-qc-warnings']"); - } - - async hasQCWarnings(): Promise { - return await this.getQCWarningsField() - .isVisible() - .catch(() => false); - } - - async clickQCWarnings(): Promise { - await this.getQCWarningsField().locator("[data-testid='detail-popover-trigger']").click(); - } - - // Sample size field - getSampleSizeField(): Locator { - return this.page.locator("[data-testid='field-sample-size']"); - } - - async getSampleSize(): Promise { - const sampleSizeField = await this.getSampleSizeField(); - return await sampleSizeField.textContent(); - } - - async isSampleSizeVisible(): Promise { - const sampleSizeField = await this.getSampleSizeField(); - return await sampleSizeField.isVisible().catch(() => false); - } - - // N cases field - getNCasesField(): Locator { - return this.page.locator("[data-testid='field-n-cases']"); - } - - async getNCases(): Promise { - const nCasesField = await this.getNCasesField(); - return await nCasesField.textContent(); - } - - async isNCasesVisible(): Promise { - return await this.getNCasesField() - .isVisible() - .catch(() => false); - } - - // N controls field - getNControlsField(): Locator { - return this.page.locator("[data-testid='field-n-controls']"); - } - - async getNControls(): Promise { - const nControlsField = await this.getNControlsField(); - return await nControlsField.textContent(); - } - - async isNControlsVisible(): Promise { - return await this.getNControlsField() - .isVisible() - .catch(() => false); - } - - // Analysis field - getAnalysisField(): Locator { - return this.page.locator("[data-testid='field-analysis']"); - } - - async getAnalysis(): Promise { - const analysisField = await this.getAnalysisField(); - return await analysisField.textContent(); - } - - async isAnalysisVisible(): Promise { - return await this.getAnalysisField() - .isVisible() - .catch(() => false); - } - - // Population chips (LD reference population) - getPopulationChips(): Locator { - return this.page.locator("[data-testid^='chip-ld-population-']"); - } - - async getPopulationChipsCount(): Promise { - return await this.getPopulationChips().count(); - } - - async getPopulationChipLabel(index: number): Promise { - return await this.getPopulationChips() - .nth(index) - .locator("[data-testid^='chip-ld-population-']") - .first() - .textContent(); - } - - async getPopulationChipValue(index: number): Promise { - return await this.getPopulationChips() - .nth(index) - .locator("[data-testid^='chip-ld-population-']") - .nth(1) - .textContent(); - } - - async hoverPopulationChip(index: number): Promise { - await this.getPopulationChips().nth(index).hover(); - } + // Delegate GWAS-specific methods + getReportedTraitField = () => this.gwas.getReportedTraitField(); + getReportedTrait = () => this.gwas.getReportedTrait(); + isReportedTraitVisible = () => this.gwas.isReportedTraitVisible(); + getDiseaseField = () => this.gwas.getDiseaseField(); + getDiseases = () => this.gwas.getDiseases(); + isDiseaseFieldVisible = () => this.gwas.isDiseaseFieldVisible(); + clickDiseaseLink = (index?: number) => this.gwas.clickDiseaseLink(index); + getBackgroundTraitField = () => this.gwas.getBackgroundTraitField(); + getBackgroundTraits = () => this.gwas.getBackgroundTraits(); + isBackgroundTraitVisible = () => this.gwas.isBackgroundTraitVisible(); + + // Delegate QTL-specific methods + getProjectField = () => this.qtl.getProjectField(); + getProject = () => this.qtl.getProject(); + isProjectFieldVisible = () => this.qtl.isProjectFieldVisible(); + getAffectedGeneField = () => this.qtl.getAffectedGeneField(); + getAffectedGene = () => this.qtl.getAffectedGene(); + isAffectedGeneVisible = () => this.qtl.isAffectedGeneVisible(); + clickAffectedGeneLink = () => this.qtl.clickAffectedGeneLink(); + getAffectedCellTissueField = () => this.qtl.getAffectedCellTissueField(); + getAffectedCellTissue = () => this.qtl.getAffectedCellTissue(); + isAffectedCellTissueVisible = () => this.qtl.isAffectedCellTissueVisible(); + getConditionField = () => this.qtl.getConditionField(); + getCondition = () => this.qtl.getCondition(); + isConditionVisible = () => this.qtl.isConditionVisible(); + + // Delegate publication methods + getPublicationField = () => this.publication.getPublicationField(); + getPublication = () => this.publication.getPublication(); + isPublicationVisible = () => this.publication.isPublicationVisible(); + getPubMedField = () => this.publication.getPubMedField(); + getPubMedId = () => this.publication.getPubMedId(); + isPubMedVisible = () => this.publication.isPubMedVisible(); + clickPubMedLink = () => this.publication.clickPubMedLink(); + + // Delegate statistics methods + getSummaryStatsField = () => this.statistics.getSummaryStatsField(); + getSummaryStatsText = () => this.statistics.getSummaryStatsText(); + isSummaryStatsAvailable = () => this.statistics.isSummaryStatsAvailable(); + hasSummaryStatsPopover = () => this.statistics.hasSummaryStatsPopover(); + clickSummaryStatsPopover = () => this.statistics.clickSummaryStatsPopover(); + getQCWarningsField = () => this.statistics.getQCWarningsField(); + hasQCWarnings = () => this.statistics.hasQCWarnings(); + clickQCWarnings = () => this.statistics.clickQCWarnings(); + + // Delegate sample methods + getSampleSizeField = () => this.sample.getSampleSizeField(); + getSampleSize = () => this.sample.getSampleSize(); + isSampleSizeVisible = () => this.sample.isSampleSizeVisible(); + getNCasesField = () => this.sample.getNCasesField(); + getNCases = () => this.sample.getNCases(); + isNCasesVisible = () => this.sample.isNCasesVisible(); + getNControlsField = () => this.sample.getNControlsField(); + getNControls = () => this.sample.getNControls(); + isNControlsVisible = () => this.sample.isNControlsVisible(); + getAnalysisField = () => this.sample.getAnalysisField(); + getAnalysis = () => this.sample.getAnalysis(); + isAnalysisVisible = () => this.sample.isAnalysisVisible(); + getPopulationChips = () => this.sample.getPopulationChips(); + getPopulationChipsCount = () => this.sample.getPopulationChipsCount(); + getPopulationChipLabel = (index: number) => this.sample.getPopulationChipLabel(index); + getPopulationChipValue = (index: number) => this.sample.getPopulationChipValue(index); + hoverPopulationChip = (index: number) => this.sample.hoverPopulationChip(index); // Wait for profile header to load async waitForProfileHeaderLoad(): Promise { diff --git a/packages/platform-test/playwright-report/index.html b/packages/platform-test/playwright-report/index.html index 2a4208643..cdafa7ed8 100644 --- a/packages/platform-test/playwright-report/index.html +++ b/packages/platform-test/playwright-report/index.html @@ -73,4 +73,4 @@
    - \ No newline at end of file + \ No newline at end of file From 7dede276d347d6e1989fe319fd6a0957222aa81e Mon Sep 17 00:00:00 2001 From: david oluwasusi Date: Thu, 1 Jan 2026 19:32:59 +0000 Subject: [PATCH 06/34] feat(e2e-variant-page): variant page interactors and base tests for the variant page --- .../src/pages/VariantPage/ProfileHeader.tsx | 20 +- .../variantProfileHeader.ts | 124 ++++++++++ .../widgets/Study/gwasCredibleSetsSection.ts | 125 +--------- .../widgets/Variant/variantEffectSection.ts | 130 ++++++++++ .../Variant/variantGWASCredibleSetsSection.ts | 2 + .../widgets/shared/GWASCredibleSetsSection.ts | 228 ++++++++++++++++++ .../enhancerToGenePredictionsSection.ts | 92 +++++++ .../POM/objects/widgets/shared/evaSection.ts | 86 +++++++ .../shared/molecularStructureSection.ts | 52 ++++ .../widgets/shared/pharmacogenomicsSection.ts | 175 ++++++++++++++ .../widgets/shared/qtlCredibleSetsSection.ts | 101 ++++++++ .../widgets/shared/uniprotVariantsSection.ts | 95 ++++++++ .../shared/variantEffectPredictorSection.ts | 98 ++++++++ .../platform-test/POM/page/variant/variant.ts | 105 ++++++++ .../platform-test/e2e/pages/homepage.spec.ts | 12 +- .../e2e/pages/study/studyPageGWAS.spec.ts | 2 +- .../variant/variantGWASCredibleSets.spec.ts | 169 +++++++++++++ .../variant/variantPharmacogenomics.spec.ts | 165 +++++++++++++ .../variant/variantQTLCredibleSets.spec.ts | 90 +++++++ .../variant/variantUniProtVariants.spec.ts | 84 +++++++ .../playwright-report/index.html | 2 +- packages/platform-test/utils/fillPolling.ts | 12 + .../MolecularStructure/StructureViewer.tsx | 2 +- .../components/Section/SectionViewToggle.tsx | 12 +- 24 files changed, 1835 insertions(+), 148 deletions(-) create mode 100644 packages/platform-test/POM/objects/components/VariantProfileHeader/variantProfileHeader.ts create mode 100644 packages/platform-test/POM/objects/widgets/Variant/variantEffectSection.ts create mode 100644 packages/platform-test/POM/objects/widgets/Variant/variantGWASCredibleSetsSection.ts create mode 100644 packages/platform-test/POM/objects/widgets/shared/GWASCredibleSetsSection.ts create mode 100644 packages/platform-test/POM/objects/widgets/shared/enhancerToGenePredictionsSection.ts create mode 100644 packages/platform-test/POM/objects/widgets/shared/evaSection.ts create mode 100644 packages/platform-test/POM/objects/widgets/shared/molecularStructureSection.ts create mode 100644 packages/platform-test/POM/objects/widgets/shared/pharmacogenomicsSection.ts create mode 100644 packages/platform-test/POM/objects/widgets/shared/qtlCredibleSetsSection.ts create mode 100644 packages/platform-test/POM/objects/widgets/shared/uniprotVariantsSection.ts create mode 100644 packages/platform-test/POM/objects/widgets/shared/variantEffectPredictorSection.ts create mode 100644 packages/platform-test/POM/page/variant/variant.ts create mode 100644 packages/platform-test/e2e/pages/variant/variantGWASCredibleSets.spec.ts create mode 100644 packages/platform-test/e2e/pages/variant/variantPharmacogenomics.spec.ts create mode 100644 packages/platform-test/e2e/pages/variant/variantQTLCredibleSets.spec.ts create mode 100644 packages/platform-test/e2e/pages/variant/variantUniProtVariants.spec.ts create mode 100644 packages/platform-test/utils/fillPolling.ts diff --git a/apps/platform/src/pages/VariantPage/ProfileHeader.tsx b/apps/platform/src/pages/VariantPage/ProfileHeader.tsx index da8c2d004..5b794fc64 100644 --- a/apps/platform/src/pages/VariantPage/ProfileHeader.tsx +++ b/apps/platform/src/pages/VariantPage/ProfileHeader.tsx @@ -27,19 +27,20 @@ function ProfileHeader() { Location - + {data?.variant.chromosome}:{data?.variant.position} - + Ensembl Variant Effect Predictor (Ensembl VEP) - + {data?.variant.alleleFrequencies.length > 0 && ( - + = 15 ? ( - <> + {label} {value} - + ) : ( - + {value} ); @@ -160,7 +162,7 @@ function AlleleFrequencyPlot({ data }) { function BarGroup({ dataRow: { label, alleleFrequency }, dps }) { return ( - + {label} diff --git a/packages/platform-test/POM/objects/components/VariantProfileHeader/variantProfileHeader.ts b/packages/platform-test/POM/objects/components/VariantProfileHeader/variantProfileHeader.ts new file mode 100644 index 000000000..8a5f494a3 --- /dev/null +++ b/packages/platform-test/POM/objects/components/VariantProfileHeader/variantProfileHeader.ts @@ -0,0 +1,124 @@ +import type { Locator, Page } from "@playwright/test"; + +/** + * Interactor for Variant Profile Header component + */ +export class VariantProfileHeader { + constructor(private page: Page) {} + + // Profile header container + getProfileHeader(): Locator { + return this.page.locator("[data-testid='profile-header']"); + } + + async isProfileHeaderVisible(): Promise { + return await this.getProfileHeader() + .isVisible() + .catch(() => false); + } + + // Variant description + getVariantDescriptionField(): Locator { + return this.page.locator("[data-testid='profile-description']"); + } + + async getVariantDescription(): Promise { + return await this.getVariantDescriptionField().textContent(); + } + + async isVariantDescriptionVisible(): Promise { + return await this.getVariantDescriptionField() + .isVisible() + .catch(() => false); + } + + // GRCh38 location + getGRCh38LocationField(): Locator { + return this.page.locator("[data-testid='field-grch38-location']"); + } + + async getGRCh38Location(): Promise { + return await this.getGRCh38LocationField().textContent(); + } + + async isGRCh38LocationVisible(): Promise { + return await this.getGRCh38LocationField() + .isVisible() + .catch(() => false); + } + + // Reference allele + getReferenceAlleleField(): Locator { + return this.page.locator("[data-testid='field-reference-allele']"); + } + + async getReferenceAllele(): Promise { + return await this.getReferenceAlleleField().textContent(); + } + + async isReferenceAlleleVisible(): Promise { + return await this.getReferenceAlleleField() + .isVisible() + .catch(() => false); + } + + // Alternative allele + getAlternativeAlleleField(): Locator { + return this.page.locator("[data-testid='field-alternative-allele']"); + } + + async getAlternativeAllele(): Promise { + return await this.getAlternativeAlleleField().textContent(); + } + + async isAlternativeAlleleVisible(): Promise { + return await this.getAlternativeAlleleField() + .isVisible() + .catch(() => false); + } + + // Most severe consequence + getMostSevereConsequenceField(): Locator { + return this.page.locator("[data-testid='field-most-severe-consequence']"); + } + + async getMostSevereConsequence(): Promise { + const link = this.getMostSevereConsequenceField().locator("a"); + return await link.textContent(); + } + + async isMostSevereConsequenceVisible(): Promise { + return await this.getMostSevereConsequenceField() + .isVisible() + .catch(() => false); + } + + async clickMostSevereConsequenceLink(): Promise { + await this.getMostSevereConsequenceField().locator("a").click(); + } + + // Allele frequencies section + getAlleleFrequenciesSection(): Locator { + return this.page.locator("[data-testid='allele-frequencies-section']"); + } + + async hasAlleleFrequencies(): Promise { + return await this.getAlleleFrequenciesSection() + .isVisible() + .catch(() => false); + } + + async getAlleleFrequencyBars(): Promise { + const section = this.getAlleleFrequenciesSection(); + const bars = section.locator("[data-testid='allele-frequency-bar']"); + return await bars.count(); + } + + // Wait for profile header to load + async waitForProfileHeaderLoad(): Promise { + await this.page.waitForSelector("[data-testid='profile-page-header-block']", { + state: "visible", + }); + await this.page.waitForTimeout(500); + } +} diff --git a/packages/platform-test/POM/objects/widgets/Study/gwasCredibleSetsSection.ts b/packages/platform-test/POM/objects/widgets/Study/gwasCredibleSetsSection.ts index 585df30b9..506ad335b 100644 --- a/packages/platform-test/POM/objects/widgets/Study/gwasCredibleSetsSection.ts +++ b/packages/platform-test/POM/objects/widgets/Study/gwasCredibleSetsSection.ts @@ -1,123 +1,2 @@ -import type { Locator, Page } from "@playwright/test"; - -export class GWASCredibleSetsSection { - page: Page; - - constructor(page: Page) { - this.page = page; - } - - // Section container - getSection(): Locator { - return this.page.locator("[data-testid='section-gwas-credible-sets']"); - } - - async isSectionVisible(): Promise { - return await this.getSection() - .isVisible() - .catch(() => false); - } - - // Section header - getSectionHeader(): Locator { - return this.page.locator("[data-testid='section-gwas-credible-sets-header']"); - } - - async getSectionTitle(): Promise { - return await this.getSectionHeader().textContent(); - } - - // Table - getTable(): Locator { - return this.getSection().locator("table"); - } - - async isTableVisible(): Promise { - return await this.getTable() - .isVisible() - .catch(() => false); - } - - getTableRows(): Locator { - return this.getTable().locator("tbody tr"); - } - - async getRowCount(): Promise { - return await this.getTableRows().count(); - } - - // Get cell data - getCell(rowIndex: number, columnIndex: number): Locator { - return this.getTableRows().nth(rowIndex).locator("td").nth(columnIndex); - } - - async getCellText(rowIndex: number, columnIndex: number): Promise { - return await this.getCell(rowIndex, columnIndex).textContent(); - } - - // Get variant link - getVariantLink(rowIndex: number): Locator { - return this.getTableRows().nth(rowIndex).locator("a[href*='/variant/']"); - } - - async clickVariantLink(rowIndex: number): Promise { - await this.getVariantLink(rowIndex).click(); - } - - async getVariantId(rowIndex: number): Promise { - return await this.getVariantLink(rowIndex).textContent(); - } - - // Get gene link - getGeneLink(rowIndex: number): Locator { - return this.getTableRows().nth(rowIndex).locator("a[href*='/target/']"); - } - - async clickGeneLink(rowIndex: number): Promise { - await this.getGeneLink(rowIndex).click(); - } - - async getGeneName(rowIndex: number): Promise { - return await this.getGeneLink(rowIndex).textContent(); - } - - // Search/Filter - getSearchInput(): Locator { - return this.getSection().locator("input[type='text']"); - } - - async searchCredibleSet(searchTerm: string): Promise { - await this.getSearchInput().fill(searchTerm); - } - - // Pagination - getNextPageButton(): Locator { - return this.getSection().locator("[data-testid='pagination-next-button']"); - } - - getPreviousPageButton(): Locator { - return this.getSection().locator("[data-testid='pagination-previous-button']"); - } - - async clickNextPage(): Promise { - await this.getNextPageButton().click(); - } - - async clickPreviousPage(): Promise { - await this.getPreviousPageButton().click(); - } - - async isNextPageEnabled(): Promise { - return await this.getNextPageButton().isEnabled(); - } - - async isPreviousPageEnabled(): Promise { - return await this.getPreviousPageButton().isEnabled(); - } - - // Wait for section to load - async waitForSectionLoad(): Promise { - await this.getSection().waitFor({ state: "visible", timeout: 10000 }); - await this.page.waitForTimeout(500); - } -} +// Re-export the shared GWAS Credible Sets section +export { GWASCredibleSetsSection } from "../shared/GWASCredibleSetsSection"; diff --git a/packages/platform-test/POM/objects/widgets/Variant/variantEffectSection.ts b/packages/platform-test/POM/objects/widgets/Variant/variantEffectSection.ts new file mode 100644 index 000000000..9e6a5cecd --- /dev/null +++ b/packages/platform-test/POM/objects/widgets/Variant/variantEffectSection.ts @@ -0,0 +1,130 @@ +import type { Locator, Page } from "@playwright/test"; + +/** + * Interactor for Variant Effect section + */ +export class VariantEffectSection { + constructor(private page: Page) {} + + // Section container + getSection(): Locator { + return this.page.locator("[data-testid='section-in-silico-predictors']"); + } + + async isSectionVisible(): Promise { + return await this.getSection() + .isVisible() + .catch(() => false); + } + + /** + * Wait for the section to finish loading (no skeleton loaders) + */ + async waitForLoad(): Promise { + const section = this.getSection(); + await section.waitFor({ state: "visible", timeout: 10000 }); + + // Wait for skeleton loaders to disappear + await this.page + .waitForFunction( + () => { + const sect = document.querySelector("[data-testid='section-in-silico-predictors']"); + if (!sect) return false; + const skeletons = sect.querySelectorAll(".MuiSkeleton-root"); + return skeletons.length === 0; + }, + { timeout: 15000 } + ) + .catch(() => { + // If no skeletons found, section already loaded + }); + } + + // Table + getTable(): Locator { + return this.getSection().locator("table"); + } + + async getTableRows(): Promise { + const tbody = this.getTable().locator("tbody"); + const rows = tbody.locator("tr"); + return await rows.count(); + } + + async getTableRow(index: number): Promise { + const tbody = this.getTable().locator("tbody"); + return tbody.locator("tr").nth(index); + } + + // Get method name from row + async getMethodName(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + const methodCell = row.locator("td").first(); + return await methodCell.textContent(); + } + + // Get prediction/assessment from row + async getPrediction(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + const predictionCell = row.locator("td").nth(1); + return await predictionCell.textContent(); + } + + // Get score from row + async getScore(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + const scoreCell = row.locator("td").nth(2); + return await scoreCell.textContent(); + } + + // Get normalised score from row + async getNormalisedScore(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + const normScoreCell = row.locator("td").nth(3); + return await normScoreCell.textContent(); + } + + // View switcher (table/chart) + getChartViewButton(): Locator { + return this.getSection().locator("[data-testid='view-toggle-chart']"); + } + + getTableViewButton(): Locator { + return this.getSection().locator("[data-testid='view-toggle-table']"); + } + + async switchToChartView(): Promise { + await this.getChartViewButton().click(); + await this.page.waitForTimeout(300); // Brief wait for view transition animation + } + + async switchToTableView(): Promise { + await this.getTableViewButton().click(); + await this.page.waitForTimeout(500); // Wait for view transition + } + + async isChartViewActive(): Promise { + // Check the chart button's aria-pressed state + const button = this.getChartViewButton(); + const ariaPressed = await button.getAttribute("aria-pressed"); + return ariaPressed === "true"; + } + + async isTableViewActive(): Promise { + // Check the table button's aria-pressed state + const button = this.getTableViewButton(); + const ariaPressed = await button.getAttribute("aria-pressed"); + return ariaPressed === "true"; + } + + // Chart + getChart(): Locator { + return this.getSection().locator("svg[class^='plot-']"); + } + + async isChartVisible(): Promise { + return await this.getChart() + .isVisible() + .catch(() => false); + } +} diff --git a/packages/platform-test/POM/objects/widgets/Variant/variantGWASCredibleSetsSection.ts b/packages/platform-test/POM/objects/widgets/Variant/variantGWASCredibleSetsSection.ts new file mode 100644 index 000000000..293eaa6a7 --- /dev/null +++ b/packages/platform-test/POM/objects/widgets/Variant/variantGWASCredibleSetsSection.ts @@ -0,0 +1,2 @@ +// Re-export the shared GWAS Credible Sets section +export { GWASCredibleSetsSection as VariantGWASCredibleSetsSection } from "../shared/GWASCredibleSetsSection"; diff --git a/packages/platform-test/POM/objects/widgets/shared/GWASCredibleSetsSection.ts b/packages/platform-test/POM/objects/widgets/shared/GWASCredibleSetsSection.ts new file mode 100644 index 000000000..71dd8a53a --- /dev/null +++ b/packages/platform-test/POM/objects/widgets/shared/GWASCredibleSetsSection.ts @@ -0,0 +1,228 @@ +import type { Locator, Page } from "@playwright/test"; + +/** + * Shared interactor for GWAS Credible Sets section + * Used in both Variant and Study pages + */ +export class GWASCredibleSetsSection { + constructor(private page: Page) {} + + // Section container + getSection(): Locator { + return this.page.locator("[data-testid='section-gwas-credible-sets']"); + } + + async isSectionVisible(): Promise { + return await this.getSection() + .isVisible() + .catch(() => false); + } + + /** + * Wait for the section to finish loading (no skeleton loaders) + */ + async waitForLoad(): Promise { + const section = this.getSection(); + await section.waitFor({ state: "visible", timeout: 10000 }); + + // Wait for skeleton loaders to disappear + await this.page + .waitForFunction( + () => { + const sect = document.querySelector("[data-testid='section-gwas-credible-sets']"); + if (!sect) return false; + const skeletons = sect.querySelectorAll(".MuiSkeleton-root"); + return skeletons.length === 0; + }, + { timeout: 15000 } + ) + .catch(() => { + // If no skeletons found, section already loaded + }); + } + + // Table + getTable(): Locator { + return this.getSection().locator("table"); + } + + async getTableRows(): Promise { + const tbody = this.getTable().locator("tbody"); + const rows = tbody.locator("tr"); + return await rows.count(); + } + + async getTableRow(index: number): Promise { + const tbody = this.getTable().locator("tbody"); + return tbody.locator("tr").nth(index); + } + + // Credible set link + async getCredibleSetLink(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + return row.locator("a[href*='/credible-set/']"); + } + + async clickCredibleSetLink(rowIndex: number): Promise { + const link = await this.getCredibleSetLink(rowIndex); + await link.click(); + } + + async getCredibleSetId(rowIndex: number): Promise { + const link = await this.getCredibleSetLink(rowIndex); + return await link.textContent(); + } + + // Lead variant + async getLeadVariantLink(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + return row.locator("td").nth(1).locator("a[href*='/variant/']"); + } + + async hasLeadVariantLink(rowIndex: number): Promise { + const link = await this.getLeadVariantLink(rowIndex); + return await link.isVisible().catch(() => false); + } + + async clickLeadVariantLink(rowIndex: number): Promise { + const link = await this.getLeadVariantLink(rowIndex); + await link.click(); + } + + // Disease links + async getDiseaseLinks(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + return row.locator("td").nth(3).locator("a[href*='/disease/']"); + } + + async getDiseaseLinksCount(rowIndex: number): Promise { + const links = await this.getDiseaseLinks(rowIndex); + return await links.count(); + } + + async clickDiseaseLink(rowIndex: number, linkIndex: number = 0): Promise { + const links = await this.getDiseaseLinks(rowIndex); + await links.nth(linkIndex).click(); + } + + // Study link + async getStudyLink(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + return row.locator("td").nth(4).locator("a[href*='/study/']"); + } + + async clickStudyLink(rowIndex: number): Promise { + const link = await this.getStudyLink(rowIndex); + await link.click(); + } + + async getStudyId(rowIndex: number): Promise { + const link = await this.getStudyLink(rowIndex); + return await link.textContent(); + } + + // L2G gene link + async getL2GGeneLink(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + return row.locator("a[href*='/target/']"); + } + + async hasL2GGeneLink(rowIndex: number): Promise { + const link = await this.getL2GGeneLink(rowIndex); + return await link.isVisible().catch(() => false); + } + + async clickL2GGeneLink(rowIndex: number): Promise { + const link = await this.getL2GGeneLink(rowIndex); + await link.click(); + } + + async getL2GGeneName(rowIndex: number): Promise { + const link = await this.getL2GGeneLink(rowIndex); + return await link.textContent(); + } + + // Pagination + getNextPageButton(): Locator { + return this.getSection().locator("button[aria-label='Next Page']"); + } + + getPreviousPageButton(): Locator { + return this.getSection().locator("button[aria-label='Previous Page']"); + } + + async clickNextPage(): Promise { + await this.getNextPageButton().click(); + await this.waitForLoad(); + } + + async clickPreviousPage(): Promise { + await this.getPreviousPageButton().click(); + await this.waitForLoad(); + } + + async isNextPageEnabled(): Promise { + return await this.getNextPageButton().isEnabled(); + } + + async isPreviousPageEnabled(): Promise { + return await this.getPreviousPageButton().isEnabled(); + } + + // Global filter/search + getSearchInput(): Locator { + return this.getSection().locator("input[placeholder*='Search']"); + } + + async search(searchTerm: string): Promise { + await this.getSearchInput().fill(searchTerm); + await this.waitForLoad(); + } + + async clearSearch(): Promise { + await this.getSearchInput().clear(); + await this.waitForLoad(); + } + + // Additional methods for Study page compatibility + getSectionHeader(): Locator { + return this.page.locator("[data-testid='section-gwas-credible-sets-header']"); + } + + async getSectionTitle(): Promise { + return await this.getSectionHeader().textContent(); + } + + async isTableVisible(): Promise { + return await this.getTable() + .isVisible() + .catch(() => false); + } + + async getRowCount(): Promise { + return await this.getTableRows(); + } + + async getCellText(rowIndex: number, columnIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + const cell = row.locator("td").nth(columnIndex); + return await cell.textContent(); + } + + async getVariantId(rowIndex: number): Promise { + const link = await this.getLeadVariantLink(rowIndex); + return await link.textContent(); + } + + async clickVariantLink(rowIndex: number): Promise { + await this.clickLeadVariantLink(rowIndex); + } + + async searchCredibleSet(searchTerm: string): Promise { + await this.search(searchTerm); + } + + async waitForSectionLoad(): Promise { + await this.waitForLoad(); + } +} diff --git a/packages/platform-test/POM/objects/widgets/shared/enhancerToGenePredictionsSection.ts b/packages/platform-test/POM/objects/widgets/shared/enhancerToGenePredictionsSection.ts new file mode 100644 index 000000000..888319116 --- /dev/null +++ b/packages/platform-test/POM/objects/widgets/shared/enhancerToGenePredictionsSection.ts @@ -0,0 +1,92 @@ +import type { Locator, Page } from "@playwright/test"; + +/** + * Interactor for Enhancer-to-Gene Predictions section on Variant page + */ +export class EnhancerToGenePredictionsSection { + constructor(private page: Page) {} + + // Section container + getSection(): Locator { + return this.page.locator("[data-testid='section-enhancer-to-gene-predictions']"); + } + + async isSectionVisible(): Promise { + return await this.getSection() + .isVisible() + .catch(() => false); + } + + /** + * Wait for the section to finish loading (no skeleton loaders) + */ + async waitForLoad(): Promise { + const section = this.getSection(); + await section.waitFor({ state: "visible", timeout: 10000 }); + + // Wait for skeleton loaders to disappear + await this.page + .waitForFunction( + () => { + const sect = document.querySelector("[data-testid='section-enhancer-to-gene-predictions']"); + if (!sect) return false; + const skeletons = sect.querySelectorAll(".MuiSkeleton-root"); + return skeletons.length === 0; + }, + { timeout: 15000 } + ) + .catch(() => { + // If no skeletons found, section already loaded + }); + } + + // Table + getTable(): Locator { + return this.getSection().locator("table"); + } + + async getTableRows(): Promise { + const tbody = this.getTable().locator("tbody"); + const rows = tbody.locator("tr"); + return await rows.count(); + } + + async getTableRow(index: number): Promise { + const tbody = this.getTable().locator("tbody"); + return tbody.locator("tr").nth(index); + } + + // Get target gene link + async getTargetGeneLink(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + return row.locator("a[href*='/target/']"); + } + + async clickTargetGeneLink(rowIndex: number): Promise { + const link = await this.getTargetGeneLink(rowIndex); + await link.click(); + } + + async getTargetGeneName(rowIndex: number): Promise { + const link = await this.getTargetGeneLink(rowIndex); + return await link.textContent(); + } + + // Get E2G score + async getE2GScore(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + // Score is typically in a specific column, adjust index as needed + const cell = row.locator("td").nth(2); + return await cell.textContent(); + } + + // Global filter/search + getSearchInput(): Locator { + return this.getSection().locator("input[placeholder*='Search']"); + } + + async search(searchTerm: string): Promise { + await this.getSearchInput().fill(searchTerm); + await this.waitForLoad(); + } +} diff --git a/packages/platform-test/POM/objects/widgets/shared/evaSection.ts b/packages/platform-test/POM/objects/widgets/shared/evaSection.ts new file mode 100644 index 000000000..9182a9e1c --- /dev/null +++ b/packages/platform-test/POM/objects/widgets/shared/evaSection.ts @@ -0,0 +1,86 @@ +import type { Locator, Page } from "@playwright/test"; + +/** + * Interactor for EVA/ClinVar section on Variant page + */ +export class EVASection { + constructor(private page: Page) {} + + // Section container + getSection(): Locator { + return this.page.locator("[data-testid='section-eva']"); + } + + async isSectionVisible(): Promise { + return await this.getSection() + .isVisible() + .catch(() => false); + } + + /** + * Wait for the section to finish loading (no skeleton loaders) + */ + async waitForLoad(): Promise { + const section = this.getSection(); + await section.waitFor({ state: "visible", timeout: 10000 }); + + // Wait for skeleton loaders to disappear + await this.page + .waitForFunction( + () => { + const sect = document.querySelector("[data-testid='section-eva']"); + if (!sect) return false; + const skeletons = sect.querySelectorAll(".MuiSkeleton-root"); + return skeletons.length === 0; + }, + { timeout: 15000 } + ) + .catch(() => { + // If no skeletons found, section already loaded + }); + } + + // Table + getTable(): Locator { + return this.getSection().locator("table"); + } + + async getTableRows(): Promise { + const tbody = this.getTable().locator("tbody"); + const rows = tbody.locator("tr"); + return await rows.count(); + } + + async getTableRow(index: number): Promise { + const tbody = this.getTable().locator("tbody"); + return tbody.locator("tr").nth(index); + } + + // Get disease link + async getDiseaseLink(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + return row.locator("a[href*='/disease/']"); + } + + async clickDiseaseLink(rowIndex: number): Promise { + const link = await this.getDiseaseLink(rowIndex); + await link.click(); + } + + // Get clinical significance + async getClinicalSignificance(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + const cell = row.locator("td").nth(1); + return await cell.textContent(); + } + + // Global filter/search + getSearchInput(): Locator { + return this.getSection().locator("input[placeholder*='Search']"); + } + + async search(searchTerm: string): Promise { + await this.getSearchInput().fill(searchTerm); + await this.waitForLoad(); + } +} diff --git a/packages/platform-test/POM/objects/widgets/shared/molecularStructureSection.ts b/packages/platform-test/POM/objects/widgets/shared/molecularStructureSection.ts new file mode 100644 index 000000000..f2f309595 --- /dev/null +++ b/packages/platform-test/POM/objects/widgets/shared/molecularStructureSection.ts @@ -0,0 +1,52 @@ +import type { Locator, Page } from "@playwright/test"; + +/** + * Interactor for Molecular Structure section on Variant page + */ +export class MolecularStructureSection { + constructor(private page: Page) {} + + // Section container + getSection(): Locator { + return this.page.locator("[data-testid='section-molecular-structure']"); + } + + async isSectionVisible(): Promise { + return await this.getSection() + .isVisible() + .catch(() => false); + } + + /** + * Wait for the section to finish loading + */ + async waitForLoad(): Promise { + const section = this.getSection(); + await section.waitFor({ state: "visible", timeout: 10000 }); + await this.page.waitForTimeout(500); + } + + // AlphaFold viewer + getAlphaFoldViewer(): Locator { + return this.getSection().locator("[data-testid='alphafold-viewer']"); + } + + async isAlphaFoldViewerVisible(): Promise { + return await this.getAlphaFoldViewer() + .isVisible() + .catch(() => false); + } + + // Check if section has structure viewer content + async hasStructureViewer(): Promise { + return await this.getAlphaFoldViewer() + .isVisible() + .catch(() => false); + } + + // Structure information + async hasStructureInfo(): Promise { + const info = this.getSection().locator("text=AlphaFold"); + return await info.isVisible().catch(() => false); + } +} diff --git a/packages/platform-test/POM/objects/widgets/shared/pharmacogenomicsSection.ts b/packages/platform-test/POM/objects/widgets/shared/pharmacogenomicsSection.ts new file mode 100644 index 000000000..ac4272245 --- /dev/null +++ b/packages/platform-test/POM/objects/widgets/shared/pharmacogenomicsSection.ts @@ -0,0 +1,175 @@ +import type { Locator, Page } from "@playwright/test"; +import { fillPolling } from "../../../../utils/fillPolling"; + +/** + * Interactor for Pharmacogenomics section + */ +export class PharmacogenomicsSection { + constructor(private page: Page) {} + + // Section container + getSection(): Locator { + return this.page.locator("[data-testid='section-pharmacogenetics']"); + } + + async isSectionVisible(): Promise { + return await this.getSection() + .isVisible() + .catch(() => false); + } + + /** + * Wait for the section to finish loading (no skeleton loaders) + */ + async waitForLoad(): Promise { + const section = this.getSection(); + await section.waitFor({ state: "visible", timeout: 10000 }); + + // Wait for skeleton loaders to disappear + await this.page + .waitForFunction( + () => { + const sect = document.querySelector("[data-testid='section-pharmacogenetics']"); + if (!sect) return false; + const skeletons = sect.querySelectorAll(".MuiSkeleton-root"); + return skeletons.length === 0; + }, + { timeout: 15000 } + ) + .catch(() => { + // If no skeletons found, section already loaded + }); + } + + // Table + getTable(): Locator { + return this.getSection().locator("table"); + } + + async getTableRows(): Promise { + const tbody = this.getTable().locator("tbody"); + const rows = tbody.locator("tr"); + return await rows.count(); + } + + async getTableRow(index: number): Promise { + const tbody = this.getTable().locator("tbody"); + return tbody.locator("tr").nth(index); + } + + // Get genotype ID from row + async getGenotypeId(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + const genotypeCell = row.locator("td").first(); + return await genotypeCell.textContent(); + } + + // Drug links + async getDrugLinks(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + return row.locator("td").nth(1).locator("a[href*='/drug/']"); + } + + async getDrugLinksCount(rowIndex: number): Promise { + const links = await this.getDrugLinks(rowIndex); + return await links.count(); + } + + async clickDrugLink(rowIndex: number, linkIndex: number = 0): Promise { + const links = await this.getDrugLinks(rowIndex); + await links.nth(linkIndex).click(); + } + + async getDrugName(rowIndex: number, linkIndex: number = 0): Promise { + const links = await this.getDrugLinks(rowIndex); + return await links.nth(linkIndex).textContent(); + } + + // Gene/Target link + async getGeneLink(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + return row.locator("a[href*='/target/']"); + } + + async hasGeneLink(rowIndex: number): Promise { + const link = await this.getGeneLink(rowIndex); + return await link.isVisible().catch(() => false); + } + + async clickGeneLink(rowIndex: number): Promise { + const link = await this.getGeneLink(rowIndex); + await link.click(); + } + + async getGeneName(rowIndex: number): Promise { + const link = await this.getGeneLink(rowIndex); + return await link.textContent(); + } + + // Phenotype link (disease) + async getPhenotypeLink(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + return row.locator("td").nth(2).locator("a[href*='/disease/']"); + } + + async hasPhenotypeLink(rowIndex: number): Promise { + const link = await this.getPhenotypeLink(rowIndex); + return await link.isVisible().catch(() => false); + } + + async clickPhenotypeLink(rowIndex: number): Promise { + const link = await this.getPhenotypeLink(rowIndex); + await link.click(); + } + + // Get confidence level from row + async getConfidenceLevel(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + // Confidence level column has a colored badge + const confidenceCell = row.locator("td").nth(7); + return await confidenceCell.textContent(); + } + + // Pagination + getNextPageButton(): Locator { + return this.getSection().locator("[data-testid='pagination-next-button']"); + } + + getPreviousPageButton(): Locator { + return this.getSection().locator("[data-testid='pagination-previous-button']"); + } + + async clickNextPage(): Promise { + await this.getNextPageButton().click(); + await this.waitForLoad(); + } + + async clickPreviousPage(): Promise { + await this.getPreviousPageButton().click(); + await this.waitForLoad(); + } + + async isNextPageEnabled(): Promise { + return await this.getNextPageButton().isEnabled(); + } + + async isPreviousPageEnabled(): Promise { + return await this.getPreviousPageButton().isEnabled(); + } + + // Global filter/search + getSearchInput(): Locator { + return this.getSection().locator("input[placeholder*='Search']"); + } + + async search(searchTerm: string): Promise { + await fillPolling(this.getSearchInput(), searchTerm); + await this.getSearchInput().press("Enter"); + await this.waitForLoad(); + } + + async clearSearch(): Promise { + await this.getSearchInput().clear(); + await this.waitForLoad(); + } +} diff --git a/packages/platform-test/POM/objects/widgets/shared/qtlCredibleSetsSection.ts b/packages/platform-test/POM/objects/widgets/shared/qtlCredibleSetsSection.ts new file mode 100644 index 000000000..917c75a1b --- /dev/null +++ b/packages/platform-test/POM/objects/widgets/shared/qtlCredibleSetsSection.ts @@ -0,0 +1,101 @@ +import type { Locator, Page } from "@playwright/test"; + +/** + * Interactor for QTL Credible Sets section on Variant page + */ +export class QTLCredibleSetsSection { + constructor(private page: Page) {} + + // Section container + getSection(): Locator { + return this.page.locator("[data-testid='section-qtl-credible-sets']"); + } + + async isSectionVisible(): Promise { + return await this.getSection() + .isVisible() + .catch(() => false); + } + + /** + * Wait for the section to finish loading (no skeleton loaders) + */ + async waitForLoad(): Promise { + const section = this.getSection(); + await section.waitFor({ state: "visible", timeout: 10000 }); + + // Wait for skeleton loaders to disappear + await this.page + .waitForFunction( + () => { + const sect = document.querySelector("[data-testid='section-qtl-credible-sets']"); + if (!sect) return false; + const skeletons = sect.querySelectorAll(".MuiSkeleton-root"); + return skeletons.length === 0; + }, + { timeout: 15000 } + ) + .catch(() => { + // If no skeletons found, section already loaded + }); + } + + // Table + getTable(): Locator { + return this.getSection().locator("table"); + } + + async getTableRows(): Promise { + const tbody = this.getTable().locator("tbody"); + const rows = tbody.locator("tr"); + return await rows.count(); + } + + async getTableRow(index: number): Promise { + const tbody = this.getTable().locator("tbody"); + return tbody.locator("tr").nth(index); + } + + // Credible set link + async getCredibleSetLink(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + return row.locator("a[href*='/credible-set/']"); + } + + async clickCredibleSetLink(rowIndex: number): Promise { + const link = await this.getCredibleSetLink(rowIndex); + await link.click(); + } + + // Study link + async getStudyLink(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + return row.locator("a[href*='/study/']"); + } + + async clickStudyLink(rowIndex: number): Promise { + const link = await this.getStudyLink(rowIndex); + await link.click(); + } + + // Affected gene link + async getAffectedGeneLink(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + return row.locator("a[href*='/target/']"); + } + + async clickAffectedGeneLink(rowIndex: number): Promise { + const link = await this.getAffectedGeneLink(rowIndex); + await link.click(); + } + + // Global filter/search + getSearchInput(): Locator { + return this.getSection().locator("input[placeholder*='Search']"); + } + + async search(searchTerm: string): Promise { + await this.getSearchInput().fill(searchTerm); + await this.waitForLoad(); + } +} diff --git a/packages/platform-test/POM/objects/widgets/shared/uniprotVariantsSection.ts b/packages/platform-test/POM/objects/widgets/shared/uniprotVariantsSection.ts new file mode 100644 index 000000000..6faf90c5d --- /dev/null +++ b/packages/platform-test/POM/objects/widgets/shared/uniprotVariantsSection.ts @@ -0,0 +1,95 @@ +import type { Locator, Page } from "@playwright/test"; + +/** + * Interactor for UniProt Variants section on Variant page + */ +export class UniProtVariantsSection { + constructor(private page: Page) {} + + // Section container + getSection(): Locator { + return this.page.locator("[data-testid='section-uniprot-variants']"); + } + + async isSectionVisible(): Promise { + return await this.getSection() + .isVisible() + .catch(() => false); + } + + /** + * Wait for the section to finish loading (no skeleton loaders) + */ + async waitForLoad(): Promise { + const section = this.getSection(); + await section.waitFor({ state: "visible", timeout: 10000 }); + + // Wait for skeleton loaders to disappear + await this.page + .waitForFunction( + () => { + const sect = document.querySelector("[data-testid='section-uniprot-variants']"); + if (!sect) return false; + const skeletons = sect.querySelectorAll(".MuiSkeleton-root"); + return skeletons.length === 0; + }, + { timeout: 15000 } + ) + .catch(() => { + // If no skeletons found, section already loaded + }); + } + + // Table + getTable(): Locator { + return this.getSection().locator("table"); + } + + async getTableRows(): Promise { + const tbody = this.getTable().locator("tbody"); + const rows = tbody.locator("tr"); + return await rows.count(); + } + + async getTableRow(index: number): Promise { + const tbody = this.getTable().locator("tbody"); + return tbody.locator("tr").nth(index); + } + + // Get target gene link + async getTargetGeneLink(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + return row.locator("a[href*='/target/']"); + } + + async clickTargetGeneLink(rowIndex: number): Promise { + const link = await this.getTargetGeneLink(rowIndex); + await link.click(); + } + + // Get disease links + async getDiseaseLinks(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + return row.locator("a[href*='/disease/']"); + } + + async getDiseaseLinksCount(rowIndex: number): Promise { + const links = await this.getDiseaseLinks(rowIndex); + return await links.count(); + } + + async clickDiseaseLink(rowIndex: number, linkIndex: number = 0): Promise { + const links = await this.getDiseaseLinks(rowIndex); + await links.nth(linkIndex).click(); + } + + // Global filter/search + getSearchInput(): Locator { + return this.getSection().locator("input[placeholder*='Search']"); + } + + async search(searchTerm: string): Promise { + await this.getSearchInput().fill(searchTerm); + await this.waitForLoad(); + } +} diff --git a/packages/platform-test/POM/objects/widgets/shared/variantEffectPredictorSection.ts b/packages/platform-test/POM/objects/widgets/shared/variantEffectPredictorSection.ts new file mode 100644 index 000000000..db9477de1 --- /dev/null +++ b/packages/platform-test/POM/objects/widgets/shared/variantEffectPredictorSection.ts @@ -0,0 +1,98 @@ +import type { Locator, Page } from "@playwright/test"; + +/** + * Interactor for Variant Effect Predictor/Transcript Consequences section + */ +export class VariantEffectPredictorSection { + constructor(private page: Page) {} + + // Section container + getSection(): Locator { + return this.page.locator("[data-testid='section-variant-effect-predictor']"); + } + + async isSectionVisible(): Promise { + return await this.getSection() + .isVisible() + .catch(() => false); + } + + /** + * Wait for the section to finish loading (no skeleton loaders) + */ + async waitForLoad(): Promise { + const section = this.getSection(); + await section.waitFor({ state: "visible", timeout: 10000 }); + + // Wait for skeleton loaders to disappear + await this.page + .waitForFunction( + () => { + const sect = document.querySelector("[data-testid='section-variant-effect-predictor']"); + if (!sect) return false; + const skeletons = sect.querySelectorAll(".MuiSkeleton-root"); + return skeletons.length === 0; + }, + { timeout: 15000 } + ) + .catch(() => { + // If no skeletons found, section already loaded + }); + } + + // Table + getTable(): Locator { + return this.getSection().locator("table"); + } + + async getTableRows(): Promise { + const tbody = this.getTable().locator("tbody"); + const rows = tbody.locator("tr"); + return await rows.count(); + } + + async getTableRow(index: number): Promise { + const tbody = this.getTable().locator("tbody"); + return tbody.locator("tr").nth(index); + } + + // Get gene link + async getGeneLink(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + return row.locator("a[href*='/target/']"); + } + + async clickGeneLink(rowIndex: number): Promise { + const link = await this.getGeneLink(rowIndex); + await link.click(); + } + + async getGeneName(rowIndex: number): Promise { + const link = await this.getGeneLink(rowIndex); + return await link.textContent(); + } + + // Get predicted consequence + async getPredictedConsequence(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + const cell = row.locator("td").nth(1); + return await cell.textContent(); + } + + // Get impact + async getImpact(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + const cell = row.locator("td").nth(2); + return await cell.textContent(); + } + + // Global filter/search + getSearchInput(): Locator { + return this.getSection().locator("input[placeholder*='Search']"); + } + + async search(searchTerm: string): Promise { + await this.getSearchInput().fill(searchTerm); + await this.waitForLoad(); + } +} diff --git a/packages/platform-test/POM/page/variant/variant.ts b/packages/platform-test/POM/page/variant/variant.ts new file mode 100644 index 000000000..39c1f7b4b --- /dev/null +++ b/packages/platform-test/POM/page/variant/variant.ts @@ -0,0 +1,105 @@ +import type { Page } from "@playwright/test"; + +export class VariantPage { + page: Page; + originalURL: string; + + constructor(page: Page) { + this.page = page; + this.originalURL = page.url(); + } + + /** + * Navigate to a variant page by variant ID + * @param variantId - Variant ID in format: chromosome_position_ref_alt (e.g., "1_154453788_C_T") + */ + async goToVariantPage(variantId: string): Promise { + await this.page.goto(`/variant/${variantId}`); + await this.waitForVariantPageLoad(); + } + + /** + * Wait for the variant page to load + */ + async waitForVariantPageLoad(): Promise { + await this.page.waitForSelector("[data-testid='profile-page-header-block']", { + state: "visible", + }); + // Wait for any section loaders to disappear + await this.page + .waitForSelector("[data-testid='section-loader']", { + state: "hidden", + timeout: 10000, + }) + .catch(() => { + // If no loader found, that's fine - means sections loaded quickly + }); + } + + /** + * Get variant ID from the page header + */ + async getVariantIdFromHeader(): Promise { + const headerTitle = this.page.locator("[data-testid='profile-page-header-text']"); + return await headerTitle.textContent(); + } + + /** + * Navigate to variant page from a credible set link + * @param variantId - Variant ID to navigate to + */ + async goToVariantPageFromCredibleSet(variantId: string): Promise { + const variantLink = this.page.locator(`a[href*="/variant/${variantId}"]`).first(); + await variantLink.click(); + await this.waitForVariantPageLoad(); + } + + /** + * Navigate to variant page from any table that has variant links + * @param variantId - Variant ID to navigate to + */ + async goToVariantPageFromTable(variantId: string): Promise { + const variantLink = this.page.locator(`a[href*="/variant/${variantId}"]`).first(); + await variantLink.click(); + await this.waitForVariantPageLoad(); + } + + /** + * Check if the variant page is currently displayed + */ + async isVariantPage(): Promise { + const url = this.page.url(); + return url.includes("/variant/"); + } + + /** + * Wait for a specific section to finish loading + * @param sectionTestId - The test-id of the section (e.g., 'section-gwas-credible-sets') + */ + async waitForSectionToLoad(sectionTestId: string): Promise { + const section = this.page.locator(`[data-testid='${sectionTestId}']`); + + // Wait for section to be visible + await section.waitFor({ state: "visible", timeout: 10000 }); + + // Wait for any skeleton loaders within the section to disappear + const skeletons = section.locator(".MuiSkeleton-root"); + const hasSkeletons = await skeletons + .count() + .then((count) => count > 0) + .catch(() => false); + + if (hasSkeletons) { + await this.page.waitForFunction( + (testId) => { + const sect = document.querySelector(`[data-testid='${testId}']`); + if (!sect) return false; + const skels = sect.querySelectorAll(".MuiSkeleton-root"); + return skels.length === 0; + }, + sectionTestId, + { timeout: 15000 } + ); + } + } +} diff --git a/packages/platform-test/e2e/pages/homepage.spec.ts b/packages/platform-test/e2e/pages/homepage.spec.ts index 053345e6b..23b1bb2ec 100644 --- a/packages/platform-test/e2e/pages/homepage.spec.ts +++ b/packages/platform-test/e2e/pages/homepage.spec.ts @@ -1,15 +1,5 @@ import { expect, type Locator, test } from "@playwright/test"; - -// https://github.com/microsoft/playwright/issues/36395 -// Helper function to fill input with polling to avoid flaky tests -async function fillPolling(locator: Locator, value: string) { - await expect - .poll(async () => { - await locator.fill(value); - return await locator.inputValue(); - }) - .toEqual(value); -} +import { fillPolling } from "../../utils/fillPolling"; test.describe("Home page actions", () => { test("Validate page title", async ({ page, baseURL }) => { diff --git a/packages/platform-test/e2e/pages/study/studyPageGWAS.spec.ts b/packages/platform-test/e2e/pages/study/studyPageGWAS.spec.ts index 8d27588f3..27676607d 100644 --- a/packages/platform-test/e2e/pages/study/studyPageGWAS.spec.ts +++ b/packages/platform-test/e2e/pages/study/studyPageGWAS.spec.ts @@ -1,7 +1,7 @@ import { test } from "@playwright/test"; import { StudyProfileHeader } from "../../../POM/objects/components/StudyProfileHeader/studyProfileHeader"; -import { GWASCredibleSetsSection } from "../../../POM/objects/widgets/Study/gwasCredibleSetsSection"; import { SharedTraitStudiesSection } from "../../../POM/objects/widgets/Study/sharedTraitStudiesSection"; +import { GWASCredibleSetsSection } from "../../../POM/objects/widgets/shared/GWASCredibleSetsSection"; import { StudyPage } from "../../../POM/page/study/study"; // Test Disease study diff --git a/packages/platform-test/e2e/pages/variant/variantGWASCredibleSets.spec.ts b/packages/platform-test/e2e/pages/variant/variantGWASCredibleSets.spec.ts new file mode 100644 index 000000000..0955a2782 --- /dev/null +++ b/packages/platform-test/e2e/pages/variant/variantGWASCredibleSets.spec.ts @@ -0,0 +1,169 @@ +import { expect, test } from "@playwright/test"; +import { GWASCredibleSetsSection as VariantGWASCredibleSetsSection } from "../../../POM/objects/widgets/shared/GWASCredibleSetsSection"; +import { VariantPage } from "../../../POM/page/variant/variant"; + +test.describe("Variant GWAS Credible Sets Section", () => { + let variantPage: VariantPage; + let gwasSection: VariantGWASCredibleSetsSection; + + test.beforeEach(async ({ page }) => { + variantPage = new VariantPage(page); + gwasSection = new VariantGWASCredibleSetsSection(page); + + // Navigate to a variant with GWAS credible sets data + await variantPage.goToVariantPage("1_154453788_C_T"); + + // Wait for the section to fully load + await gwasSection.waitForLoad(); + }); + + test("GWAS Credible Sets section is visible", async () => { + expect(await gwasSection.isSectionVisible()).toBe(true); + }); + + test("GWAS Credible Sets table displays data", async () => { + const rowCount = await gwasSection.getTableRows(); + + expect(rowCount).toBeGreaterThan(0); + }); + + test("Can get credible set ID from table", async () => { + const credibleSetId = await gwasSection.getCredibleSetId(0); + + expect(credibleSetId).not.toBeNull(); + expect(credibleSetId).not.toBe(""); + }); + + test("Can click credible set link in table", async ({ page }) => { + await gwasSection.clickCredibleSetLink(0); + + // Wait for navigation to credible set page + await page.waitForURL((url) => url.toString().includes("/credible-set/"), { timeout: 5000 }); + + // Should navigate to credible set page + expect(page.url()).toContain("/credible-set/"); + }); + + test("Lead variant column displays correctly", async () => { + const hasLeadVariant = await gwasSection.hasLeadVariantLink(0); + + // Lead variant might be the current variant (no link) or a different variant (with link) + // Either way, the cell should have content + const row = await gwasSection.getTableRow(0); + const leadVariantCell = row.locator("td").nth(1); + const content = await leadVariantCell.textContent(); + + expect(content).not.toBeNull(); + }); + + test("Can click lead variant link when available", async ({ page }) => { + const hasLink = await gwasSection.hasLeadVariantLink(0); + + if (hasLink) { + await gwasSection.clickLeadVariantLink(0); + + // Wait for navigation to variant page + await page.waitForURL((url) => url.toString().includes("/variant/"), { timeout: 5000 }); + + // Should navigate to a variant page + expect(page.url()).toContain("/variant/"); + } + }); + + test("Disease links are displayed in table", async () => { + const diseaseCount = await gwasSection.getDiseaseLinksCount(0); + + expect(diseaseCount).toBeGreaterThan(0); + }); + + test("Can click disease link in table", async ({ page }) => { + await gwasSection.clickDiseaseLink(0); + + // Wait for navigation to disease page + await page.waitForURL((url) => url.toString().includes("/disease/"), { timeout: 5000 }); + + // Should navigate to disease page + expect(page.url()).toContain("/disease/"); + }); + + test("Study ID is displayed in table", async () => { + const studyId = await gwasSection.getStudyId(0); + + expect(studyId).not.toBeNull(); + expect(studyId).not.toBe(""); + }); + + test("Can click study link in table", async ({ page }) => { + await gwasSection.clickStudyLink(0); + + // Wait for navigation to study page + await page.waitForURL((url) => url.toString().includes("/study/"), { timeout: 5000 }); + + // Should navigate to study page + expect(page.url()).toContain("/study/"); + }); + + test("L2G gene link is displayed when available", async () => { + const hasL2GLink = await gwasSection.hasL2GGeneLink(0); + + if (hasL2GLink) { + const geneName = await gwasSection.getL2GGeneName(0); + expect(geneName).not.toBeNull(); + expect(geneName).not.toBe(""); + } + }); + + test("Can click L2G gene link", async ({ page }) => { + const hasL2GLink = await gwasSection.hasL2GGeneLink(0); + + if (hasL2GLink) { + await gwasSection.clickL2GGeneLink(0); + + // Wait for navigation to target page + await page.waitForURL((url) => url.toString().includes("/target/"), { timeout: 5000 }); + + // Should navigate to target page + expect(page.url()).toContain("/target/"); + } + }); + + test("Pagination buttons work correctly", async () => { + const rowCount = await gwasSection.getTableRows(); + + // If we have enough rows for pagination + if (rowCount >= 10) { + const nextEnabled = await gwasSection.isNextPageEnabled(); + + if (nextEnabled) { + await gwasSection.clickNextPage(); + + // Should still be on the same section + expect(await gwasSection.isSectionVisible()).toBe(true); + + // Previous button should now be enabled + expect(await gwasSection.isPreviousPageEnabled()).toBe(true); + } + } + }); + + test("Search functionality works", async () => { + const initialRows = await gwasSection.getTableRows(); + + // Search for a specific study or trait + await gwasSection.search("PCSK9"); + + const filteredRows = await gwasSection.getTableRows(); + + // Filtered results should be less than or equal to initial rows + expect(filteredRows).toBeLessThanOrEqual(initialRows); + }); + + test("Can clear search", async () => { + await gwasSection.search("PCSK9"); + await gwasSection.clearSearch(); + + // Should show all rows again + const rowCount = await gwasSection.getTableRows(); + expect(rowCount).toBeGreaterThan(0); + }); +}); diff --git a/packages/platform-test/e2e/pages/variant/variantPharmacogenomics.spec.ts b/packages/platform-test/e2e/pages/variant/variantPharmacogenomics.spec.ts new file mode 100644 index 000000000..09742fbf0 --- /dev/null +++ b/packages/platform-test/e2e/pages/variant/variantPharmacogenomics.spec.ts @@ -0,0 +1,165 @@ +import { expect, test } from "@playwright/test"; +import { PharmacogenomicsSection } from "../../../POM/objects/widgets/shared/pharmacogenomicsSection"; +import { VariantPage } from "../../../POM/page/variant/variant"; + +test.describe("Pharmacogenomics Section", () => { + let variantPage: VariantPage; + let pharmacoSection: PharmacogenomicsSection; + + test.beforeEach(async ({ page }) => { + variantPage = new VariantPage(page); + pharmacoSection = new PharmacogenomicsSection(page); + + // Navigate to a variant with pharmacogenomics data + // Using rs662 (PON1 gene) which should have pharmaco data + await variantPage.goToVariantPage("7_95308134_T_C"); + + // Wait for the section to load if it's visible + const isVisible = await pharmacoSection.isSectionVisible(); + if (isVisible) { + await pharmacoSection.waitForLoad(); + } + if (isVisible) { + expect(isVisible).toBe(true); + } else { + test.skip(); + } + }); + + test("Pharmacogenomics table displays data when available", async () => { + const isVisible = await pharmacoSection.isSectionVisible(); + + if (isVisible) { + const rowCount = await pharmacoSection.getTableRows(); + expect(rowCount).toBeGreaterThan(0); + } else { + test.skip(); + } + }); + + test("Genotype ID is displayed in table", async () => { + const isVisible = await pharmacoSection.isSectionVisible(); + + if (isVisible) { + const genotypeId = await pharmacoSection.getGenotypeId(0); + + expect(genotypeId).not.toBeNull(); + expect(genotypeId).not.toBe(""); + } else { + test.skip(); + } + }); + + test("Drug links are displayed in table", async () => { + const isVisible = await pharmacoSection.isSectionVisible(); + + if (isVisible) { + const drugCount = await pharmacoSection.getDrugLinksCount(0); + + expect(drugCount).toBeGreaterThan(0); + } else { + test.skip(); + } + }); + + test("Can click drug link in table", async ({ page }) => { + const isVisible = await pharmacoSection.isSectionVisible(); + + if (isVisible) { + await pharmacoSection.clickDrugLink(0); + + // Wait for navigation to drug page + await page.waitForURL(url => url.toString().includes("/drug/"), { timeout: 5000 }); + } +}); + + test("Gene/Target link is displayed in table", async () => { + const isVisible = await pharmacoSection.isSectionVisible(); + + if (isVisible) { + const hasGeneLink = await pharmacoSection.hasGeneLink(0); + + if (hasGeneLink) { + const geneName = await pharmacoSection.getGeneName(0); + expect(geneName).not.toBeNull(); + } + } else { + test.skip(); + } + }); + + test("Can click gene link in table", async ({ page }) => { + const isVisible = await pharmacoSection.isSectionVisible(); + + if (isVisible) { + const hasGeneLink = await pharmacoSection.hasGeneLink(0); + + if (hasGeneLink) { + await pharmacoSection.clickGeneLink(0); + + // Wait for navigation to target page + await page.waitForURL(url => url.toString().includes("/target/"), { timeout: 5000 }); + + } + } + }); + test("Confidence level is displayed in table", async () => { + const isVisible = await pharmacoSection.isSectionVisible(); + + if (isVisible) { + const confidenceLevel = await pharmacoSection.getConfidenceLevel(0); + + expect(confidenceLevel).not.toBeNull(); + expect(confidenceLevel).toContain("Level"); + } else { + test.skip(); + } + }); + + test("Search functionality works", async () => { + const isVisible = await pharmacoSection.isSectionVisible(); + + if (isVisible) { + const initialRows = await pharmacoSection.getTableRows(); + + // Search for something + await pharmacoSection.search("simvastatin"); + + const filteredRows = await pharmacoSection.getTableRows(); + + // Filtered results should be less than or equal to initial rows + expect(filteredRows).toBeLessThanOrEqual(initialRows); + + // Clear search + await pharmacoSection.clearSearch(); + } else { + test.skip(); + } + }); + + test("Pagination works when data is sufficient", async () => { + const isVisible = await pharmacoSection.isSectionVisible(); + + if (isVisible) { + const rowCount = await pharmacoSection.getTableRows(); + + // Only test pagination if we have enough rows + if (rowCount >= 10) { + const nextEnabled = await pharmacoSection.isNextPageEnabled(); + + if (nextEnabled) { + await pharmacoSection.clickNextPage(); + + // Should still be on the same section + expect(await pharmacoSection.isSectionVisible()).toBe(true); + + // Previous button should now be enabled + expect(await pharmacoSection.isPreviousPageEnabled()).toBe(true); + } + } + } else { + test.skip(); + } + }); + +}); \ No newline at end of file diff --git a/packages/platform-test/e2e/pages/variant/variantQTLCredibleSets.spec.ts b/packages/platform-test/e2e/pages/variant/variantQTLCredibleSets.spec.ts new file mode 100644 index 000000000..c271d6f57 --- /dev/null +++ b/packages/platform-test/e2e/pages/variant/variantQTLCredibleSets.spec.ts @@ -0,0 +1,90 @@ +import { expect, test } from "@playwright/test"; +import { QTLCredibleSetsSection } from "../../../POM/objects/widgets/shared/qtlCredibleSetsSection"; +import { VariantPage } from "../../../POM/page/variant/variant"; + +test.describe("Variant QTL Credible Sets Section", () => { + let variantPage: VariantPage; + let qtlSection: QTLCredibleSetsSection; + + test.beforeEach(async ({ page }) => { + variantPage = new VariantPage(page); + qtlSection = new QTLCredibleSetsSection(page); + + // Navigate to a variant with QTL credible sets data + await variantPage.goToVariantPage("1_154453788_C_T"); + + // Check if section is visible + const isVisible = await qtlSection.isSectionVisible(); + if (isVisible) { + await qtlSection.waitForLoad(); + } else { + test.skip(); + } + }); + + test("QTL Credible Sets section is visible when data available", async () => { + const isVisible = await qtlSection.isSectionVisible(); + expect(isVisible).toBe(true); + }); + + test("QTL Credible Sets table displays data", async () => { + const rowCount = await qtlSection.getTableRows(); + + expect(rowCount).toBeGreaterThan(0); + }); + + test("Can click credible set link in table", async ({ page }) => { + await qtlSection.clickCredibleSetLink(0); + + // Wait for navigation to credible set page + await page.waitForURL((url) => url.toString().includes("/credible-set/"), { timeout: 5000 }); + + // Should navigate to credible set page + expect(page.url()).toContain("/credible-set/"); + }); + + test("Study link is displayed in table", async () => { + const studyLink = await qtlSection.getStudyLink(0); + + expect(await studyLink.isVisible()).toBe(true); + }); + + test("Can click study link in table", async ({ page }) => { + await qtlSection.clickStudyLink(0); + + // Wait for navigation to study page + await page.waitForURL((url) => url.toString().includes("/study/"), { timeout: 5000 }); + + // Should navigate to study page + expect(page.url()).toContain("/study/"); + }); + + test("Affected gene link is displayed in table", async () => { + const geneLink = await qtlSection.getAffectedGeneLink(0); + + expect(await geneLink.isVisible()).toBe(true); + }); + + test("Can click affected gene link in table", async ({ page }) => { + await qtlSection.clickAffectedGeneLink(0); + + // Wait for navigation to target page + await page.waitForURL((url) => url.toString().includes("/target/"), { timeout: 5000 }); + + // Should navigate to target/gene page + expect(page.url()).toContain("/target/"); + }); + + test("Can search/filter QTL credible sets", async () => { + const initialRowCount = await qtlSection.getTableRows(); + + // Search for a specific term + await qtlSection.search("ENSG"); + + // Row count should change + const filteredRowCount = await qtlSection.getTableRows(); + + // At least the search should execute without error + expect(filteredRowCount).toBeGreaterThanOrEqual(0); + }); +}); diff --git a/packages/platform-test/e2e/pages/variant/variantUniProtVariants.spec.ts b/packages/platform-test/e2e/pages/variant/variantUniProtVariants.spec.ts new file mode 100644 index 000000000..caa6cc2ce --- /dev/null +++ b/packages/platform-test/e2e/pages/variant/variantUniProtVariants.spec.ts @@ -0,0 +1,84 @@ +import { expect, test } from "@playwright/test"; +import { UniProtVariantsSection } from "../../../POM/objects/widgets/shared/uniprotVariantsSection"; +import { VariantPage } from "../../../POM/page/variant/variant"; + +test.describe("UniProt Variants Section", () => { + let variantPage: VariantPage; + let uniprotSection: UniProtVariantsSection; + + test.beforeEach(async ({ page }) => { + variantPage = new VariantPage(page); + uniprotSection = new UniProtVariantsSection(page); + + // Navigate to a variant with UniProt data + await variantPage.goToVariantPage("19_44908822_C_T"); + + // Check if section is visible + const isVisible = await uniprotSection.isSectionVisible(); + if (isVisible) { + await uniprotSection.waitForLoad(); + } else { + test.skip(); + } + }); + + test("UniProt Variants section is visible when data available", async () => { + const isVisible = await uniprotSection.isSectionVisible(); + expect(isVisible).toBe(true); + }); + + test("UniProt table displays data", async () => { + const rowCount = await uniprotSection.getTableRows(); + + expect(rowCount).toBeGreaterThan(0); + }); + + test("Target gene link is displayed in table", async () => { + const geneLink = await uniprotSection.getTargetGeneLink(0); + + expect(await geneLink.isVisible()).toBe(true); + }); + + test("Can click target gene link in table", async ({ page }) => { + await uniprotSection.clickTargetGeneLink(0); + + // Wait for navigation to target page + await page.waitForURL((url) => url.toString().includes("/target/"), { timeout: 5000 }); + + // Should navigate to target/gene page + expect(page.url()).toContain("/target/"); + }); + + test("Disease links are displayed in table", async () => { + const diseaseCount = await uniprotSection.getDiseaseLinksCount(0); + + expect(diseaseCount).toBeGreaterThan(0); + }); + + test("Can click disease link in table", async ({ page }) => { + const hasLinks = await uniprotSection.getDiseaseLinksCount(0); + + if (hasLinks > 0) { + await uniprotSection.clickDiseaseLink(0); + + // Wait for navigation to disease page + await page.waitForURL((url) => url.toString().includes("/disease/"), { timeout: 5000 }); + + // Should navigate to disease page + expect(page.url()).toContain("/disease/"); + } + }); + + test("Can search/filter UniProt variants", async () => { + const initialRowCount = await uniprotSection.getTableRows(); + + // Search for a specific term + await uniprotSection.search("disease"); + + // Row count should change + const filteredRowCount = await uniprotSection.getTableRows(); + + // At least the search should execute without error + expect(filteredRowCount).toBeGreaterThanOrEqual(0); + }); +}); diff --git a/packages/platform-test/playwright-report/index.html b/packages/platform-test/playwright-report/index.html index cdafa7ed8..9672e41a5 100644 --- a/packages/platform-test/playwright-report/index.html +++ b/packages/platform-test/playwright-report/index.html @@ -73,4 +73,4 @@
    - \ No newline at end of file + \ No newline at end of file diff --git a/packages/platform-test/utils/fillPolling.ts b/packages/platform-test/utils/fillPolling.ts new file mode 100644 index 000000000..13991f2cd --- /dev/null +++ b/packages/platform-test/utils/fillPolling.ts @@ -0,0 +1,12 @@ +import { expect, type Locator } from "@playwright/test"; + +// https://github.com/microsoft/playwright/issues/36395 +// Helper function to fill input with polling to avoid flaky tests +export async function fillPolling(locator: Locator, value: string) { + await expect + .poll(async () => { + await locator.fill(value); + return await locator.inputValue(); + }) + .toEqual(value); +} diff --git a/packages/sections/src/variant/MolecularStructure/StructureViewer.tsx b/packages/sections/src/variant/MolecularStructure/StructureViewer.tsx index 209248580..741a82a8f 100644 --- a/packages/sections/src/variant/MolecularStructure/StructureViewer.tsx +++ b/packages/sections/src/variant/MolecularStructure/StructureViewer.tsx @@ -140,7 +140,7 @@ function StructureViewer({ row }) { } return ( - + {structureData ? ( <> diff --git a/packages/ui/src/components/Section/SectionViewToggle.tsx b/packages/ui/src/components/Section/SectionViewToggle.tsx index 99c008ebd..968b67813 100644 --- a/packages/ui/src/components/Section/SectionViewToggle.tsx +++ b/packages/ui/src/components/Section/SectionViewToggle.tsx @@ -41,11 +41,19 @@ function SectionViewToggle({ value={sectionView} onChange={handleViewChange} > - + {VIEW.table} view - + {VIEW.chart} From 71ed64f9876b1186b3084cbd267d2f152fab3816 Mon Sep 17 00:00:00 2001 From: david oluwasusi Date: Thu, 1 Jan 2026 19:34:42 +0000 Subject: [PATCH 07/34] feat(e2e-variant-page): add tests for other variant widget --- .../e2e/pages/variant/variantEVA.spec.ts | 71 +++++++++++ .../e2e/pages/variant/variantEffect.spec.ts | 117 ++++++++++++++++++ .../variant/variantEffectPredictor.spec.ts | 72 +++++++++++ .../variantEnhancerToGenePredictions.spec.ts | 72 +++++++++++ .../variant/variantMolecularStructure.spec.ts | 42 +++++++ .../variant/variantPageNavigation.spec.ts | 48 +++++++ .../variant/variantProfileHeader.spec.ts | 87 +++++++++++++ .../playwright-report/index.html | 2 +- 8 files changed, 510 insertions(+), 1 deletion(-) create mode 100644 packages/platform-test/e2e/pages/variant/variantEVA.spec.ts create mode 100644 packages/platform-test/e2e/pages/variant/variantEffect.spec.ts create mode 100644 packages/platform-test/e2e/pages/variant/variantEffectPredictor.spec.ts create mode 100644 packages/platform-test/e2e/pages/variant/variantEnhancerToGenePredictions.spec.ts create mode 100644 packages/platform-test/e2e/pages/variant/variantMolecularStructure.spec.ts create mode 100644 packages/platform-test/e2e/pages/variant/variantPageNavigation.spec.ts create mode 100644 packages/platform-test/e2e/pages/variant/variantProfileHeader.spec.ts diff --git a/packages/platform-test/e2e/pages/variant/variantEVA.spec.ts b/packages/platform-test/e2e/pages/variant/variantEVA.spec.ts new file mode 100644 index 000000000..777bb654c --- /dev/null +++ b/packages/platform-test/e2e/pages/variant/variantEVA.spec.ts @@ -0,0 +1,71 @@ +import { expect, test } from "@playwright/test"; +import { EVASection } from "../../../POM/objects/widgets/shared/evaSection"; +import { VariantPage } from "../../../POM/page/variant/variant"; + +test.describe("EVA / ClinVar Section", () => { + let variantPage: VariantPage; + let evaSection: EVASection; + + test.beforeEach(async ({ page }) => { + variantPage = new VariantPage(page); + evaSection = new EVASection(page); + + // Navigate to a variant with ClinVar data + await variantPage.goToVariantPage("19_44908822_C_T"); + + // Check if section is visible + const isVisible = await evaSection.isSectionVisible(); + if (isVisible) { + await evaSection.waitForLoad(); + } else { + test.skip(); + } + }); + + test("EVA/ClinVar section is visible when data available", async () => { + const isVisible = await evaSection.isSectionVisible(); + expect(isVisible).toBe(true); + }); + + test("ClinVar table displays data", async () => { + const rowCount = await evaSection.getTableRows(); + + expect(rowCount).toBeGreaterThan(0); + }); + + test("Disease links are displayed in table", async () => { + const diseaseLink = await evaSection.getDiseaseLink(0); + + expect(await diseaseLink.isVisible()).toBe(true); + }); + + test("Can click disease link in table", async ({ page }) => { + await evaSection.clickDiseaseLink(0); + + // Wait for navigation to disease page + await page.waitForURL((url) => url.toString().includes("/disease/"), { timeout: 5000 }); + + // Should navigate to disease page + expect(page.url()).toContain("/disease/"); + }); + + test("Clinical significance is displayed", async () => { + const significance = await evaSection.getClinicalSignificance(0); + + expect(significance).not.toBeNull(); + expect(significance).not.toBe(""); + }); + + test("Can search/filter ClinVar entries", async () => { + const initialRowCount = await evaSection.getTableRows(); + + // Search for a specific term + await evaSection.search("pathogenic"); + + // Row count should change + const filteredRowCount = await evaSection.getTableRows(); + + // At least the search should execute without error + expect(filteredRowCount).toBeGreaterThanOrEqual(0); + }); +}); diff --git a/packages/platform-test/e2e/pages/variant/variantEffect.spec.ts b/packages/platform-test/e2e/pages/variant/variantEffect.spec.ts new file mode 100644 index 000000000..22f0bd7eb --- /dev/null +++ b/packages/platform-test/e2e/pages/variant/variantEffect.spec.ts @@ -0,0 +1,117 @@ +import { expect, test } from "@playwright/test"; +import { VariantEffectSection } from "../../../POM/objects/widgets/Variant/variantEffectSection"; +import { VariantPage } from "../../../POM/page/variant/variant"; + +test.describe("Variant Effect Section", () => { + let variantPage: VariantPage; + let variantEffectSection: VariantEffectSection; + + test.beforeEach(async ({ page }) => { + variantPage = new VariantPage(page); + variantEffectSection = new VariantEffectSection(page); + + // Navigate to a variant with variant effect data + await variantPage.goToVariantPage("1_154453788_C_T"); + + // Wait for the section to fully load + await variantEffectSection.waitForLoad(); + }); + + test("Variant Effect section is visible", async () => { + expect(await variantEffectSection.isSectionVisible()).toBe(true); + }); + + test("Variant Effect table displays data", async () => { + // Switch to table view first (chart is default) + await variantEffectSection.switchToTableView(); + + const rowCount = await variantEffectSection.getTableRows(); + + expect(rowCount).toBeGreaterThan(0); + }); + + test("Method names are displayed in table", async () => { + // Switch to table view first (chart is default) + await variantEffectSection.switchToTableView(); + + const methodName = await variantEffectSection.getMethodName(0); + + expect(methodName).not.toBeNull(); + expect(methodName).not.toBe(""); + }); + + test("Predictions are displayed in table", async () => { + // Switch to table view first (chart is default) + await variantEffectSection.switchToTableView(); + + const prediction = await variantEffectSection.getPrediction(0); + + expect(prediction).not.toBeNull(); + }); + + test("Scores are displayed in table", async () => { + // Switch to table view first (chart is default) + await variantEffectSection.switchToTableView(); + + const score = await variantEffectSection.getScore(0); + + expect(score).not.toBeNull(); + }); + + test("Normalised scores are displayed in table", async () => { + // Switch to table view first (chart is default) + await variantEffectSection.switchToTableView(); + + const normScore = await variantEffectSection.getNormalisedScore(0); + + expect(normScore).not.toBeNull(); + }); + + test("Can switch to chart view", async () => { + // Chart view is already default, but explicitly switch for clarity + await variantEffectSection.switchToChartView(); + + // Verify chart is active + expect(await variantEffectSection.isChartViewActive()).toBe(true); + + // Verify chart is visible + expect(await variantEffectSection.isChartVisible()).toBe(true); + }); + + test("Can switch back to table view from chart view", async () => { + // Chart is default, so just switch to table + await variantEffectSection.switchToTableView(); + + // Verify table view is active + expect(await variantEffectSection.isTableViewActive()).toBe(true); + + // Verify table is visible + const rowCount = await variantEffectSection.getTableRows(); + expect(rowCount).toBeGreaterThan(0); + }); + + test("Chart view displays visualization", async () => { + // Chart is default view + const isChartVisible = await variantEffectSection.isChartVisible(); + + expect(isChartVisible).toBe(true); + }); + + test("All rows have complete data", async () => { + // Switch to table view first (chart is default) + await variantEffectSection.switchToTableView(); + + const rowCount = await variantEffectSection.getTableRows(); + + // Check first 3 rows for complete data + const rowsToCheck = Math.min(3, rowCount); + + for (let i = 0; i < rowsToCheck; i++) { + const methodName = await variantEffectSection.getMethodName(i); + const prediction = await variantEffectSection.getPrediction(i); + + expect(methodName).not.toBeNull(); + expect(prediction).not.toBeNull(); + } + }); +}); diff --git a/packages/platform-test/e2e/pages/variant/variantEffectPredictor.spec.ts b/packages/platform-test/e2e/pages/variant/variantEffectPredictor.spec.ts new file mode 100644 index 000000000..13e880921 --- /dev/null +++ b/packages/platform-test/e2e/pages/variant/variantEffectPredictor.spec.ts @@ -0,0 +1,72 @@ +import { expect, test } from "@playwright/test"; +import { VariantEffectPredictorSection } from "../../../POM/objects/widgets/shared/variantEffectPredictorSection"; +import { VariantPage } from "../../../POM/page/variant/variant"; + +test.describe("Variant Effect Predictor / Transcript Consequences Section", () => { + let variantPage: VariantPage; + let vepSection: VariantEffectPredictorSection; + + test.beforeEach(async ({ page }) => { + variantPage = new VariantPage(page); + vepSection = new VariantEffectPredictorSection(page); + + // Navigate to a variant with transcript consequence data + await variantPage.goToVariantPage("1_154453788_C_T"); + + // Wait for the section to fully load + await vepSection.waitForLoad(); + }); + + test("Variant Effect Predictor section is visible", async () => { + expect(await vepSection.isSectionVisible()).toBe(true); + }); + + test("Transcript consequences table displays data", async () => { + const rowCount = await vepSection.getTableRows(); + + expect(rowCount).toBeGreaterThan(0); + }); + + test("Gene links are displayed in table", async () => { + const geneName = await vepSection.getGeneName(0); + + expect(geneName).not.toBeNull(); + expect(geneName).not.toBe(""); + }); + + test("Can click gene link in table", async ({ page }) => { + await vepSection.clickGeneLink(0); + + // Wait for navigation to target page + await page.waitForURL((url) => url.toString().includes("/target/"), { timeout: 5000 }); + + // Should navigate to target/gene page + expect(page.url()).toContain("/target/"); + }); + + test("Predicted consequence is displayed", async () => { + const consequence = await vepSection.getPredictedConsequence(0); + + expect(consequence).not.toBeNull(); + expect(consequence).not.toBe(""); + }); + + test("Impact value is displayed", async () => { + const impact = await vepSection.getImpact(0); + + expect(impact).not.toBeNull(); + }); + + test("Can search/filter transcript consequences", async () => { + const initialRowCount = await vepSection.getTableRows(); + + // Search for a specific term + await vepSection.search("missense"); + + // Row count should change (could be more or less depending on data) + const filteredRowCount = await vepSection.getTableRows(); + + // At least the search should execute without error + expect(filteredRowCount).toBeGreaterThanOrEqual(0); + }); +}); diff --git a/packages/platform-test/e2e/pages/variant/variantEnhancerToGenePredictions.spec.ts b/packages/platform-test/e2e/pages/variant/variantEnhancerToGenePredictions.spec.ts new file mode 100644 index 000000000..f0f71bca0 --- /dev/null +++ b/packages/platform-test/e2e/pages/variant/variantEnhancerToGenePredictions.spec.ts @@ -0,0 +1,72 @@ +import { expect, test } from "@playwright/test"; +import { EnhancerToGenePredictionsSection } from "../../../POM/objects/widgets/shared/enhancerToGenePredictionsSection"; +import { VariantPage } from "../../../POM/page/variant/variant"; + +test.describe("Enhancer-to-Gene Predictions Section", () => { + let variantPage: VariantPage; + let e2gSection: EnhancerToGenePredictionsSection; + + test.beforeEach(async ({ page }) => { + variantPage = new VariantPage(page); + e2gSection = new EnhancerToGenePredictionsSection(page); + + // Navigate to a variant with E2G predictions + await variantPage.goToVariantPage("19_44908822_C_T"); + + // Check if section is visible + const isVisible = await e2gSection.isSectionVisible(); + if (isVisible) { + await e2gSection.waitForLoad(); + } else { + test.skip(); + } + }); + + test("Enhancer-to-Gene Predictions section is visible when data available", async () => { + const isVisible = await e2gSection.isSectionVisible(); + expect(isVisible).toBe(true); + }); + + test("E2G predictions table displays data", async () => { + const rowCount = await e2gSection.getTableRows(); + + expect(rowCount).toBeGreaterThan(0); + }); + + test("Target gene name is displayed", async () => { + const geneName = await e2gSection.getTargetGeneName(0); + + expect(geneName).not.toBeNull(); + expect(geneName).not.toBe(""); + }); + + test("Can click target gene link in table", async ({ page }) => { + await e2gSection.clickTargetGeneLink(0); + + // Wait for navigation to target page + await page.waitForURL((url) => url.toString().includes("/target/"), { timeout: 5000 }); + + // Should navigate to target/gene page + expect(page.url()).toContain("/target/"); + }); + + test("E2G score is displayed", async () => { + const score = await e2gSection.getE2GScore(0); + + expect(score).not.toBeNull(); + expect(score).not.toBe(""); + }); + + test("Can search/filter E2G predictions", async () => { + const initialRowCount = await e2gSection.getTableRows(); + + // Search for a specific term + await e2gSection.search("ENSG"); + + // Row count should change + const filteredRowCount = await e2gSection.getTableRows(); + + // At least the search should execute without error + expect(filteredRowCount).toBeGreaterThanOrEqual(0); + }); +}); diff --git a/packages/platform-test/e2e/pages/variant/variantMolecularStructure.spec.ts b/packages/platform-test/e2e/pages/variant/variantMolecularStructure.spec.ts new file mode 100644 index 000000000..7bf28af06 --- /dev/null +++ b/packages/platform-test/e2e/pages/variant/variantMolecularStructure.spec.ts @@ -0,0 +1,42 @@ +import { expect, test } from "@playwright/test"; + +import { MolecularStructureSection } from "../../../POM/objects/widgets/shared/molecularStructureSection"; +import { VariantPage } from "../../../POM/page/variant/variant"; + +test.describe("Molecular Structure Section", () => { + let variantPage: VariantPage; + let structureSection: MolecularStructureSection; + + test.beforeEach(async ({ page }) => { + variantPage = new VariantPage(page); + structureSection = new MolecularStructureSection(page); + + // Navigate to a variant with molecular structure data + await variantPage.goToVariantPage("19_44908822_C_T"); + + // Check if section is visible + const isVisible = await structureSection.isSectionVisible(); + if (isVisible) { + await structureSection.waitForLoad(); + } else { + test.skip(); + } + }); + + test("Molecular Structure section is visible when data available", async () => { + const isVisible = await structureSection.isSectionVisible(); + expect(isVisible).toBe(true); + }); + + test("AlphaFold viewer is displayed", async () => { + const isViewerVisible = await structureSection.isAlphaFoldViewerVisible(); + + expect(isViewerVisible).toBe(true); + }); + + test("Structure information is displayed", async () => { + const hasInfo = await structureSection.hasStructureInfo(); + + expect(hasInfo).toBe(true); + }); +}); diff --git a/packages/platform-test/e2e/pages/variant/variantPageNavigation.spec.ts b/packages/platform-test/e2e/pages/variant/variantPageNavigation.spec.ts new file mode 100644 index 000000000..d52ae4205 --- /dev/null +++ b/packages/platform-test/e2e/pages/variant/variantPageNavigation.spec.ts @@ -0,0 +1,48 @@ +import { expect, test } from "@playwright/test"; +import { VariantPage } from "../../../POM/page/variant/variant"; + +test.describe("Variant Page Navigation", () => { + let variantPage: VariantPage; + + test.beforeEach(async ({ page }) => { + variantPage = new VariantPage(page); + }); + + test("Can navigate to variant page directly by variant ID", async ({ page }) => { + const variantId = "1_154453788_C_T"; + + await variantPage.goToVariantPage(variantId); + + // Verify we're on the variant page + expect(await variantPage.isVariantPage()).toBe(true); + + // Verify the URL contains the variant ID + expect(page.url()).toContain(`/variant/${variantId}`); + }); + + test("Can get variant ID from page header", async () => { + const variantId = "1_154453788_C_T"; + + await variantPage.goToVariantPage(variantId); + + const headerVariantId = await variantPage.getVariantIdFromHeader(); + + // The header displays the variant ID (may have formatting) + expect(headerVariantId).toContain("1"); + expect(headerVariantId).toContain("154453788"); + }); + + test("Variant page loads successfully with profile header", async ({ page }) => { + const variantId = "1_154453788_C_T"; + + await variantPage.goToVariantPage(variantId); + + // Wait for page to load + await variantPage.waitForVariantPageLoad(); + + // Verify profile header block is visible + const profileHeaderBlock = page.locator("[data-testid='profile-page-header-block']"); + await expect(profileHeaderBlock).toBeVisible(); + }); + +}); diff --git a/packages/platform-test/e2e/pages/variant/variantProfileHeader.spec.ts b/packages/platform-test/e2e/pages/variant/variantProfileHeader.spec.ts new file mode 100644 index 000000000..0817c5e5a --- /dev/null +++ b/packages/platform-test/e2e/pages/variant/variantProfileHeader.spec.ts @@ -0,0 +1,87 @@ +import { expect, test } from "@playwright/test"; +import { VariantProfileHeader } from "../../../POM/objects/components/VariantProfileHeader/variantProfileHeader"; +import { VariantPage } from "../../../POM/page/variant/variant"; + +test.describe("Variant Profile Header", () => { + let variantPage: VariantPage; + let variantProfileHeader: VariantProfileHeader; + + test.beforeEach(async ({ page }) => { + variantPage = new VariantPage(page); + variantProfileHeader = new VariantProfileHeader(page); + + // Navigate to a variant page with known data + await variantPage.goToVariantPage("1_154453788_C_T"); + }); + + test("Profile header is visible", async () => { + expect(await variantProfileHeader.isProfileHeaderVisible()).toBe(true); + }); + + test("Variant description is displayed", async () => { + const description = await variantProfileHeader.getVariantDescription(); + + expect(description).not.toBeNull(); + expect(description).not.toBe(""); + }); + + test("GRCh38 location is displayed", async () => { + expect(await variantProfileHeader.isGRCh38LocationVisible()).toBe(true); + + const location = await variantProfileHeader.getGRCh38Location(); + + // Should contain chromosome and position + expect(location).toContain("1"); + expect(location).toContain("154453788"); + }); + + test("Reference allele is displayed", async () => { + expect(await variantProfileHeader.isReferenceAlleleVisible()).toBe(true); + + const refAllele = await variantProfileHeader.getReferenceAllele(); + + expect(refAllele).not.toBeNull(); + expect(refAllele).toContain("C"); + }); + + test("Alternative allele is displayed", async () => { + expect(await variantProfileHeader.isAlternativeAlleleVisible()).toBe(true); + + const altAllele = await variantProfileHeader.getAlternativeAllele(); + + expect(altAllele).not.toBeNull(); + expect(altAllele).toContain("T"); + }); + + test("Most severe consequence is displayed", async () => { + expect(await variantProfileHeader.isMostSevereConsequenceVisible()).toBe(true); + + const consequence = await variantProfileHeader.getMostSevereConsequence(); + + expect(consequence).not.toBeNull(); + expect(consequence).not.toBe(""); + }); + + test("Can click most severe consequence link", async ({ page }) => { + const initialUrl = page.url(); + + await variantProfileHeader.clickMostSevereConsequenceLink(); + + // Wait for navigation + await page.waitForURL((url) => url.toString() !== initialUrl, { timeout: 5000 }); + + // Check that navigation occurred and URL is valid + const url = page.url(); + expect(url).not.toBe(initialUrl); + expect(url).toMatch(/^https?:\/\/.+/); // Valid HTTP/HTTPS URL + }); + + test("Allele frequencies section displays when available", async () => { + const hasFrequencies = await variantProfileHeader.hasAlleleFrequencies(); + + if (hasFrequencies) { + const barCount = await variantProfileHeader.getAlleleFrequencyBars(); + expect(barCount).toBeGreaterThan(0); + } + }); +}); diff --git a/packages/platform-test/playwright-report/index.html b/packages/platform-test/playwright-report/index.html index 9672e41a5..cd71d4641 100644 --- a/packages/platform-test/playwright-report/index.html +++ b/packages/platform-test/playwright-report/index.html @@ -73,4 +73,4 @@
    - \ No newline at end of file + \ No newline at end of file From cea91a26de7bbccbf08501230f6d495dd4a06a5c Mon Sep 17 00:00:00 2001 From: david oluwasusi Date: Fri, 2 Jan 2026 18:43:07 +0000 Subject: [PATCH 08/34] feat(e2e-drug-page): create interactors and tests for drug page --- .../components/DrugHeader/drugHeader.ts | 133 ++++++++++++++++++ .../Bibliography/bibliographySection.ts | 6 +- .../widgets/KnownDrugs/knownDrugsSection.ts | 2 +- .../widgets/shared/adverseEventsSection.ts | 95 +++++++++++++ .../widgets/shared/drugWarningsSection.ts | 91 ++++++++++++ .../widgets/shared/indicationsSection.ts | 92 ++++++++++++ .../shared/mechanismsOfActionSection.ts | 98 +++++++++++++ .../widgets/shared/pharmacogenomicsSection.ts | 6 +- packages/platform-test/POM/page/drug/drug.ts | 33 +++++ .../e2e/pages/disease/diseaseProfile.spec.ts | 6 +- .../e2e/pages/drug/drugBibliography.spec.ts | 61 ++++++++ .../pages/drug/drugClinicalPrecedence.spec.ts | 47 +++++++ .../e2e/pages/drug/drugHeader.spec.ts | 90 ++++++++++++ .../e2e/pages/drug/drugIndications.spec.ts | 66 +++++++++ .../pages/drug/drugMechanismsOfAction.spec.ts | 69 +++++++++ .../pages/drug/drugPharmacogenomics.spec.ts | 83 +++++++++++ .../pages/drug/drugPharmacovigilance.spec.ts | 67 +++++++++ .../e2e/pages/drug/drugWarnings.spec.ts | 69 +++++++++ .../playwright-report/index.html | 2 +- packages/ui/src/components/Header.tsx | 2 +- .../PublicationsDrawer/PublicationWrapper.jsx | 2 +- 21 files changed, 1107 insertions(+), 13 deletions(-) create mode 100644 packages/platform-test/POM/objects/components/DrugHeader/drugHeader.ts create mode 100644 packages/platform-test/POM/objects/widgets/shared/adverseEventsSection.ts create mode 100644 packages/platform-test/POM/objects/widgets/shared/drugWarningsSection.ts create mode 100644 packages/platform-test/POM/objects/widgets/shared/indicationsSection.ts create mode 100644 packages/platform-test/POM/objects/widgets/shared/mechanismsOfActionSection.ts create mode 100644 packages/platform-test/POM/page/drug/drug.ts create mode 100644 packages/platform-test/e2e/pages/drug/drugBibliography.spec.ts create mode 100644 packages/platform-test/e2e/pages/drug/drugClinicalPrecedence.spec.ts create mode 100644 packages/platform-test/e2e/pages/drug/drugHeader.spec.ts create mode 100644 packages/platform-test/e2e/pages/drug/drugIndications.spec.ts create mode 100644 packages/platform-test/e2e/pages/drug/drugMechanismsOfAction.spec.ts create mode 100644 packages/platform-test/e2e/pages/drug/drugPharmacogenomics.spec.ts create mode 100644 packages/platform-test/e2e/pages/drug/drugPharmacovigilance.spec.ts create mode 100644 packages/platform-test/e2e/pages/drug/drugWarnings.spec.ts diff --git a/packages/platform-test/POM/objects/components/DrugHeader/drugHeader.ts b/packages/platform-test/POM/objects/components/DrugHeader/drugHeader.ts new file mode 100644 index 000000000..1a6ae4450 --- /dev/null +++ b/packages/platform-test/POM/objects/components/DrugHeader/drugHeader.ts @@ -0,0 +1,133 @@ +import type { Locator, Page } from "@playwright/test"; + +/** + * Interactor for Drug Header component + */ +export class DrugHeader { + constructor(private page: Page) {} + + // Header container + getHeader(): Locator { + return this.page.locator("[data-testid='profile-page-header-block']"); + } + + async isHeaderVisible(): Promise { + return await this.getHeader() + .isVisible() + .catch(() => false); + } + + // Drug name/title + getDrugTitle(): Locator { + return this.getHeader().locator("[data-testid='profile-page-header-text']"); + } + + async getDrugName(): Promise { + return await this.getDrugTitle().textContent(); + } + + async isDrugTitleVisible(): Promise { + return await this.getDrugTitle() + .isVisible() + .catch(() => false); + } + + // External links section + getExternalLinksSection(): Locator { + return this.getHeader().locator("[data-testid='external-links']"); + } + + async hasExternalLinks(): Promise { + return await this.getExternalLinksSection() + .isVisible() + .catch(() => false); + } + + // ChEMBL link + getChemblLink(): Locator { + return this.getExternalLinksSection().locator("a[href*='ebi.ac.uk/chembl']"); + } + + async hasChemblLink(): Promise { + return await this.getChemblLink() + .isVisible() + .catch(() => false); + } + + async clickChemblLink(): Promise { + await this.getChemblLink().click(); + } + + // DrugBank link + getDrugBankLink(): Locator { + return this.getExternalLinksSection().locator("a[href*='drugbank']"); + } + + async hasDrugBankLink(): Promise { + return await this.getDrugBankLink() + .isVisible() + .catch(() => false); + } + + async clickDrugBankLink(): Promise { + await this.getDrugBankLink().click(); + } + + // ChEBI link + getChebiLink(): Locator { + return this.getExternalLinksSection().locator("a[href*='CHEBI']"); + } + + async hasChebiLink(): Promise { + return await this.getChebiLink() + .isVisible() + .catch(() => false); + } + + // DailyMed link + getDailyMedLink(): Locator { + return this.getExternalLinksSection().locator("a[href*='dailymed']"); + } + + async hasDailyMedLink(): Promise { + return await this.getDailyMedLink() + .isVisible() + .catch(() => false); + } + + // DrugCentral link + getDrugCentralLink(): Locator { + return this.getExternalLinksSection().locator("a[href*='drugcentral']"); + } + + async hasDrugCentralLink(): Promise { + return await this.getDrugCentralLink() + .isVisible() + .catch(() => false); + } + + // Wikipedia link + getWikipediaLink(): Locator { + return this.getExternalLinksSection().locator("a[href*='wikipedia']"); + } + + async hasWikipediaLink(): Promise { + return await this.getWikipediaLink() + .isVisible() + .catch(() => false); + } + + // Get all external links + getAllExternalLinks(): Locator { + return this.getExternalLinksSection().locator("a"); + } + + async getExternalLinksCount(): Promise { + return await this.getAllExternalLinks().count(); + } + + // Wait for header to load + async waitForHeaderLoad(): Promise { + await this.getHeader().waitFor({ state: "visible", timeout: 10000 }); + } +} diff --git a/packages/platform-test/POM/objects/widgets/Bibliography/bibliographySection.ts b/packages/platform-test/POM/objects/widgets/Bibliography/bibliographySection.ts index 53d5b6633..afbc92d63 100644 --- a/packages/platform-test/POM/objects/widgets/Bibliography/bibliographySection.ts +++ b/packages/platform-test/POM/objects/widgets/Bibliography/bibliographySection.ts @@ -39,9 +39,9 @@ export class BibliographySection { } async getLiteratureTitle(index: number): Promise { - return await this.getLiteratureEntry(index) - .locator("[data-testid='literature-title']") - .textContent(); + const entry = this.getLiteratureEntry(index); + const title = entry.locator("a").first(); + return await title.textContent(); } // PubMed links diff --git a/packages/platform-test/POM/objects/widgets/KnownDrugs/knownDrugsSection.ts b/packages/platform-test/POM/objects/widgets/KnownDrugs/knownDrugsSection.ts index cb35cd290..7e5aeb373 100644 --- a/packages/platform-test/POM/objects/widgets/KnownDrugs/knownDrugsSection.ts +++ b/packages/platform-test/POM/objects/widgets/KnownDrugs/knownDrugsSection.ts @@ -1,6 +1,6 @@ import type { Locator, Page } from "@playwright/test"; -export class KnownDrugsSection { +export class ClinicalPrecedenceSection { page: Page; constructor(page: Page) { diff --git a/packages/platform-test/POM/objects/widgets/shared/adverseEventsSection.ts b/packages/platform-test/POM/objects/widgets/shared/adverseEventsSection.ts new file mode 100644 index 000000000..39f557d20 --- /dev/null +++ b/packages/platform-test/POM/objects/widgets/shared/adverseEventsSection.ts @@ -0,0 +1,95 @@ +import type { Locator, Page } from "@playwright/test"; + +/** + * Interactor for Adverse Events section + */ +export class PharmacovigilanceSection { + constructor(private page: Page) {} + + // Section container + getSection(): Locator { + return this.page.locator("[data-testid='section-adverseevents']"); + } + + async isSectionVisible(): Promise { + return await this.getSection() + .isVisible() + .catch(() => false); + } + + /** + * Wait for the section to finish loading (no skeleton loaders) + */ + async waitForLoad(): Promise { + const section = this.getSection(); + await section.waitFor({ state: "visible", timeout: 10000 }); + + // Wait for skeleton loaders to disappear + await this.page + .waitForFunction( + () => { + const sect = document.querySelector("[data-testid='section-adverseevents']"); + if (!sect) return false; + const skeletons = sect.querySelectorAll(".MuiSkeleton-root"); + return skeletons.length === 0; + }, + { timeout: 15000 } + ) + .catch(() => { + // If no skeletons found, section already loaded + }); + } + + // Table + getTable(): Locator { + return this.getSection().locator("table"); + } + + async getTableRows(): Promise { + const tbody = this.getTable().locator("tbody"); + const rows = tbody.locator("tr"); + return await rows.count(); + } + + async getTableRow(index: number): Promise { + const tbody = this.getTable().locator("tbody"); + return tbody.locator("tr").nth(index); + } + + // Get adverse event name + async getAdverseEventName(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + const cell = row.locator("td").nth(0); + return await cell.textContent(); + } + + // Get adverse event link (MedDRA) + async getAdverseEventLink(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + return row.locator("a[href*='identifiers.org/meddra']"); + } + + async hasAdverseEventLink(rowIndex: number): Promise { + const link = await this.getAdverseEventLink(rowIndex); + return await link.isVisible().catch(() => false); + } + + async clickAdverseEventLink(rowIndex: number): Promise { + const link = await this.getAdverseEventLink(rowIndex); + await link.click(); + } + + // Get count of reported events + async getReportedEventsCount(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + const cell = row.locator("td").nth(1); + return await cell.textContent(); + } + + // Get log likelihood ratio + async getLogLikelihoodRatio(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + const cell = row.locator("td").nth(2); + return await cell.textContent(); + } +} diff --git a/packages/platform-test/POM/objects/widgets/shared/drugWarningsSection.ts b/packages/platform-test/POM/objects/widgets/shared/drugWarningsSection.ts new file mode 100644 index 000000000..067e36ee1 --- /dev/null +++ b/packages/platform-test/POM/objects/widgets/shared/drugWarningsSection.ts @@ -0,0 +1,91 @@ +import type { Locator, Page } from "@playwright/test"; + +/** + * Interactor for Drug Warnings section + */ +export class DrugWarningsSection { + constructor(private page: Page) {} + + // Section container + getSection(): Locator { + return this.page.locator("[data-testid='section-drugwarnings']"); + } + + async isSectionVisible(): Promise { + return await this.getSection() + .isVisible() + .catch(() => false); + } + + /** + * Wait for the section to finish loading (no skeleton loaders) + */ + async waitForLoad(): Promise { + const section = this.getSection(); + await section.waitFor({ state: "visible", timeout: 10000 }); + + // Wait for skeleton loaders to disappear + await this.page + .waitForFunction( + () => { + const sect = document.querySelector("[data-testid='section-drugwarnings']"); + if (!sect) return false; + const skeletons = sect.querySelectorAll(".MuiSkeleton-root"); + return skeletons.length === 0; + }, + { timeout: 15000 } + ) + .catch(() => { + // If no skeletons found, section already loaded + }); + } + + // Table + getTable(): Locator { + return this.getSection().locator("table"); + } + + async getTableRows(): Promise { + const tbody = this.getTable().locator("tbody"); + const rows = tbody.locator("tr"); + return await rows.count(); + } + + async getTableRow(index: number): Promise { + const tbody = this.getTable().locator("tbody"); + return tbody.locator("tr").nth(index); + } + + // Get warning type + async getWarningType(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + const cell = row.locator("td").nth(0); + return await cell.textContent(); + } + + // Get adverse event link + async getAdverseEventLink(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + return row.locator("a[href*='/disease/']").first(); + } + + async hasAdverseEventLink(rowIndex: number): Promise { + const link = await this.getAdverseEventLink(rowIndex); + return await link.isVisible().catch(() => false); + } + + async clickAdverseEventLink(rowIndex: number): Promise { + const link = await this.getAdverseEventLink(rowIndex); + await link.click(); + } + + // Global filter/search + getSearchInput(): Locator { + return this.getSection().locator("input[placeholder*='Search']"); + } + + async search(searchTerm: string): Promise { + await this.getSearchInput().fill(searchTerm); + await this.waitForLoad(); + } +} diff --git a/packages/platform-test/POM/objects/widgets/shared/indicationsSection.ts b/packages/platform-test/POM/objects/widgets/shared/indicationsSection.ts new file mode 100644 index 000000000..1fe765009 --- /dev/null +++ b/packages/platform-test/POM/objects/widgets/shared/indicationsSection.ts @@ -0,0 +1,92 @@ +import type { Locator, Page } from "@playwright/test"; + +/** + * Interactor for Indications section + */ +export class IndicationsSection { + constructor(private page: Page) {} + + // Section container + getSection(): Locator { + return this.page.locator("[data-testid='section-indications']"); + } + + async isSectionVisible(): Promise { + return await this.getSection() + .isVisible() + .catch(() => false); + } + + /** + * Wait for the section to finish loading (no skeleton loaders) + */ + async waitForLoad(): Promise { + const section = this.getSection(); + await section.waitFor({ state: "visible", timeout: 10000 }); + + // Wait for skeleton loaders to disappear + await this.page + .waitForFunction( + () => { + const sect = document.querySelector("[data-testid='section-indications']"); + if (!sect) return false; + const skeletons = sect.querySelectorAll(".MuiSkeleton-root"); + return skeletons.length === 0; + }, + { timeout: 15000 } + ) + .catch(() => { + // If no skeletons found, section already loaded + }); + } + + // Table + getTable(): Locator { + return this.getSection().locator("table"); + } + + async getTableRows(): Promise { + const tbody = this.getTable().locator("tbody"); + const rows = tbody.locator("tr"); + return await rows.count(); + } + + async getTableRow(index: number): Promise { + const tbody = this.getTable().locator("tbody"); + return tbody.locator("tr").nth(index); + } + + // Get indication (disease) link + async getIndicationLink(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + // Get link from first column (indication column) specifically + return row.locator("td").first().locator("a[href*='/disease/']"); + } + + async clickIndicationLink(rowIndex: number): Promise { + const link = await this.getIndicationLink(rowIndex); + await link.click(); + } + + async getIndicationName(rowIndex: number): Promise { + const link = await this.getIndicationLink(rowIndex); + return await link.textContent(); + } + + // Get max phase + async getMaxPhase(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + const cell = row.locator("td").nth(2); + return await cell.textContent(); + } + + // Global filter/search + getSearchInput(): Locator { + return this.getSection().locator("input[placeholder*='Search']"); + } + + async search(searchTerm: string): Promise { + await this.getSearchInput().fill(searchTerm); + await this.waitForLoad(); + } +} diff --git a/packages/platform-test/POM/objects/widgets/shared/mechanismsOfActionSection.ts b/packages/platform-test/POM/objects/widgets/shared/mechanismsOfActionSection.ts new file mode 100644 index 000000000..9dd7c29f0 --- /dev/null +++ b/packages/platform-test/POM/objects/widgets/shared/mechanismsOfActionSection.ts @@ -0,0 +1,98 @@ +import type { Locator, Page } from "@playwright/test"; + +/** + * Interactor for Mechanisms of Action section + */ +export class MechanismsOfActionSection { + constructor(private page: Page) {} + + // Section container + getSection(): Locator { + return this.page.locator("[data-testid='section-mechanismsofaction']"); + } + + async isSectionVisible(): Promise { + return await this.getSection() + .isVisible() + .catch(() => false); + } + + /** + * Wait for the section to finish loading (no skeleton loaders) + */ + async waitForLoad(): Promise { + const section = this.getSection(); + await section.waitFor({ state: "visible", timeout: 10000 }); + + // Wait for skeleton loaders to disappear + await this.page + .waitForFunction( + () => { + const sect = document.querySelector("[data-testid='section-mechanismsofaction']"); + if (!sect) return false; + const skeletons = sect.querySelectorAll(".MuiSkeleton-root"); + return skeletons.length === 0; + }, + { timeout: 15000 } + ) + .catch(() => { + // If no skeletons found, section already loaded + }); + } + + // Table + getTable(): Locator { + return this.getSection().locator("table"); + } + + async getTableRows(): Promise { + const tbody = this.getTable().locator("tbody"); + const rows = tbody.locator("tr"); + return await rows.count(); + } + + async getTableRow(index: number): Promise { + const tbody = this.getTable().locator("tbody"); + return tbody.locator("tr").nth(index); + } + + // Get mechanism of action + async getMechanismOfAction(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + const cell = row.locator("td").nth(0); + return await cell.textContent(); + } + + // Get target name + async getTargetName(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + const cell = row.locator("td").nth(1); + return await cell.textContent(); + } + + // Get target links + async getTargetLinks(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + return row.locator("a[href*='/target/']"); + } + + async getTargetLinksCount(rowIndex: number): Promise { + const links = await this.getTargetLinks(rowIndex); + return await links.count(); + } + + async clickTargetLink(rowIndex: number, linkIndex: number = 0): Promise { + const links = await this.getTargetLinks(rowIndex); + await links.nth(linkIndex).click(); + } + + // Global filter/search + getSearchInput(): Locator { + return this.getSection().locator("input[placeholder*='Search']"); + } + + async search(searchTerm: string): Promise { + await this.getSearchInput().fill(searchTerm); + await this.waitForLoad(); + } +} diff --git a/packages/platform-test/POM/objects/widgets/shared/pharmacogenomicsSection.ts b/packages/platform-test/POM/objects/widgets/shared/pharmacogenomicsSection.ts index ac4272245..280922be4 100644 --- a/packages/platform-test/POM/objects/widgets/shared/pharmacogenomicsSection.ts +++ b/packages/platform-test/POM/objects/widgets/shared/pharmacogenomicsSection.ts @@ -64,10 +64,10 @@ export class PharmacogenomicsSection { return await genotypeCell.textContent(); } - // Drug links + // Drug links - Not applicable for Drug page, only for Variant page async getDrugLinks(rowIndex: number): Promise { const row = await this.getTableRow(rowIndex); - return row.locator("td").nth(1).locator("a[href*='/drug/']"); + return row.locator("a[href*='/drug/']"); } async getDrugLinksCount(rowIndex: number): Promise { @@ -88,7 +88,7 @@ export class PharmacogenomicsSection { // Gene/Target link async getGeneLink(rowIndex: number): Promise { const row = await this.getTableRow(rowIndex); - return row.locator("a[href*='/target/']"); + return row.locator("td").first().locator("a[href*='/target/']"); } async hasGeneLink(rowIndex: number): Promise { diff --git a/packages/platform-test/POM/page/drug/drug.ts b/packages/platform-test/POM/page/drug/drug.ts new file mode 100644 index 000000000..761a61534 --- /dev/null +++ b/packages/platform-test/POM/page/drug/drug.ts @@ -0,0 +1,33 @@ +import type { Page } from "@playwright/test"; + +/** + * Drug Page Object Model + */ +export class DrugPage { + constructor(private page: Page) {} + + /** + * Navigate to a drug page + * @param chemblId - ChEMBL ID of the drug (e.g., "CHEMBL1201585") + */ + async goToDrugPage(chemblId: string): Promise { + await this.page.goto(`/drug/${chemblId}`); + await this.page.waitForLoadState("networkidle"); + } + + /** + * Get the drug page header + */ + getDrugHeader(): any { + return this.page.locator("[data-testid='profile-header']"); + } + + /** + * Check if drug page is loaded + */ + async isDrugPageLoaded(): Promise { + return await this.getDrugHeader() + .isVisible() + .catch(() => false); + } +} diff --git a/packages/platform-test/e2e/pages/disease/diseaseProfile.spec.ts b/packages/platform-test/e2e/pages/disease/diseaseProfile.spec.ts index a75dcf9c0..71624b3c7 100644 --- a/packages/platform-test/e2e/pages/disease/diseaseProfile.spec.ts +++ b/packages/platform-test/e2e/pages/disease/diseaseProfile.spec.ts @@ -2,7 +2,7 @@ import { test } from "@playwright/test"; import { ProfileHeader } from "../../../POM/objects/components/ProfileHeader/profileHeader"; import { BibliographySection } from "../../../POM/objects/widgets/Bibliography/bibliographySection"; import { GWASStudiesSection } from "../../../POM/objects/widgets/GWAS/gwasStudiesSection"; -import { KnownDrugsSection } from "../../../POM/objects/widgets/KnownDrugs/knownDrugsSection"; +import { ClinicalPrecedenceSection } from "../../../POM/objects/widgets/KnownDrugs/knownDrugsSection"; import { OntologySection } from "../../../POM/objects/widgets/Ontology/ontologySection"; import { OTProjectsSection } from "../../../POM/objects/widgets/OTProjects/otProjectsSection"; import { PhenotypesSection } from "../../../POM/objects/widgets/Phenotypes/phenotypesSection"; @@ -101,7 +101,7 @@ test.describe("Disease Profile Page", () => { }); test("Known Drugs section is visible when disease has drug data", async ({ page }) => { - const knownDrugsSection = new KnownDrugsSection(page); + const knownDrugsSection = new ClinicalPrecedenceSection(page); // Wait for section to potentially load const isVisible = await knownDrugsSection.isSectionVisible(); @@ -195,7 +195,7 @@ test.describe("Disease Profile Page", () => { // Create instances of all section interactors const sections = [ { name: "Ontology", interactor: new OntologySection(page) }, - { name: "Known Drugs", interactor: new KnownDrugsSection(page) }, + { name: "Known Drugs", interactor: new ClinicalPrecedenceSection(page) }, { name: "Phenotypes", interactor: new PhenotypesSection(page) }, { name: "OT Projects", interactor: new OTProjectsSection(page) }, { name: "GWAS Studies", interactor: new GWASStudiesSection(page) }, diff --git a/packages/platform-test/e2e/pages/drug/drugBibliography.spec.ts b/packages/platform-test/e2e/pages/drug/drugBibliography.spec.ts new file mode 100644 index 000000000..da60db8e6 --- /dev/null +++ b/packages/platform-test/e2e/pages/drug/drugBibliography.spec.ts @@ -0,0 +1,61 @@ +import { expect, test } from "@playwright/test"; +import { BibliographySection } from "../../../POM/objects/widgets/Bibliography/bibliographySection"; +import { DrugPage } from "../../../POM/page/drug/drug"; + +test.describe("Drug Bibliography Section", () => { + let drugPage: DrugPage; + let bibliographySection: BibliographySection; + + test.beforeEach(async ({ page }) => { + drugPage = new DrugPage(page); + bibliographySection = new BibliographySection(page); + + // Navigate to a drug with bibliography data + await drugPage.goToDrugPage("CHEMBL1201585"); + + // Check if section is visible + const isVisible = await bibliographySection.isSectionVisible(); + if (isVisible) { + // Wait for literature entries + await page.waitForTimeout(1000); + } else { + test.skip(); + } + }); + + test("Bibliography section is visible when data available", async () => { + const isVisible = await bibliographySection.isSectionVisible(); + expect(isVisible).toBe(true); + }); + + test("Literature entries are displayed", async () => { + const count = await bibliographySection.getLiteratureCount(); + + expect(count).toBeGreaterThan(0); + }); + + test("Literature title is displayed", async () => { + const title = await bibliographySection.getLiteratureTitle(0); + + expect(title).not.toBeNull(); + expect(title).not.toBe(""); + }); + + test("Can click PubMed link", async ({ page }) => { + const pubmedLink = bibliographySection.getPubMedLink(0); + const hasLink = await pubmedLink.isVisible().catch(() => false); + + if (hasLink) { + const initialUrl = page.url(); + await pubmedLink.click(); + + // Wait for navigation + await page.waitForURL((url) => url.toString() !== initialUrl, { timeout: 5000 }); + + // Check that navigation occurred + const url = page.url(); + expect(url).not.toBe(initialUrl); + expect(url).toMatch(/^https?:\/\/.+/); + } + }); +}); diff --git a/packages/platform-test/e2e/pages/drug/drugClinicalPrecedence.spec.ts b/packages/platform-test/e2e/pages/drug/drugClinicalPrecedence.spec.ts new file mode 100644 index 000000000..e807819a1 --- /dev/null +++ b/packages/platform-test/e2e/pages/drug/drugClinicalPrecedence.spec.ts @@ -0,0 +1,47 @@ +import { expect, test } from "@playwright/test"; +import { ClinicalPrecedenceSection } from "../../../POM/objects/widgets/KnownDrugs/knownDrugsSection"; +import { DrugPage } from "../../../POM/page/drug/drug"; + +test.describe("Clinical Precedence Section", () => { + let drugPage: DrugPage; + let clinicalPrecedence: ClinicalPrecedenceSection; + + test.beforeEach(async ({ page }) => { + drugPage = new DrugPage(page); + clinicalPrecedence = new ClinicalPrecedenceSection(page); + + // Navigate to a drug with Clinical precedence data + await drugPage.goToDrugPage("CHEMBL1201585"); + + // Check if section is visible + const isVisible = await clinicalPrecedence.isSectionVisible(); + if (isVisible) { + // Wait for table to be visible + await page.waitForSelector("[data-testid='section-knowndrugs'] table", { + state: "visible", + timeout: 10000, + }); + } else { + test.skip(); + } + }); + + test("Clinical precedence section is visible when data available", async () => { + const isVisible = await clinicalPrecedence.isSectionVisible(); + expect(isVisible).toBe(true); + }); + + test("Clinical precedence table displays data", async () => { + const rowCount = await clinicalPrecedence.getRowCount(); + + expect(rowCount).toBeGreaterThan(0); + }); + + test("Drug name is displayed", async () => { + const drugCell = clinicalPrecedence.getDrugCell(0); + const drugName = await drugCell.textContent(); + + expect(drugName).not.toBeNull(); + expect(drugName).not.toBe(""); + }); +}); diff --git a/packages/platform-test/e2e/pages/drug/drugHeader.spec.ts b/packages/platform-test/e2e/pages/drug/drugHeader.spec.ts new file mode 100644 index 000000000..beb86bf29 --- /dev/null +++ b/packages/platform-test/e2e/pages/drug/drugHeader.spec.ts @@ -0,0 +1,90 @@ +import { expect, test } from "@playwright/test"; +import { DrugHeader } from "../../../POM/objects/components/DrugHeader/drugHeader"; +import { DrugPage } from "../../../POM/page/drug/drug"; + +test.describe("Drug Header", () => { + let drugPage: DrugPage; + let drugHeader: DrugHeader; + + test.beforeEach(async ({ page }) => { + drugPage = new DrugPage(page); + drugHeader = new DrugHeader(page); + + // Navigate to a drug page + await drugPage.goToDrugPage("CHEMBL1201585"); + + // Wait for header to load + await drugHeader.waitForHeaderLoad(); + }); + + test("Drug header is visible", async () => { + expect(await drugHeader.isHeaderVisible()).toBe(true); + }); + + test("Drug title is displayed", async () => { + const isVisible = await drugHeader.isDrugTitleVisible(); + expect(isVisible).toBe(true); + + const drugName = await drugHeader.getDrugName(); + expect(drugName).not.toBeNull(); + expect(drugName).not.toBe(""); + }); + + test("External links section is visible", async () => { + const hasLinks = await drugHeader.hasExternalLinks(); + expect(hasLinks).toBe(true); + }); + + test("ChEMBL link is displayed", async () => { + const hasChembl = await drugHeader.hasChemblLink(); + expect(hasChembl).toBe(true); + }); + + test("Can click ChEMBL link", async ({ page }) => { + const initialUrl = page.url(); + await drugHeader.clickChemblLink(); + + // Wait for navigation to external site + await page.waitForURL((url) => url.toString() !== initialUrl, { timeout: 5000 }); + + // Check that navigation occurred + const url = page.url(); + expect(url).not.toBe(initialUrl); + expect(url).toMatch(/^https?:\/\/.+/); + }); + + test("External links are displayed when available", async () => { + const linksCount = await drugHeader.getExternalLinksCount(); + expect(linksCount).toBeGreaterThan(0); + }); + + test("DrugBank link may be displayed", async () => { + const hasDrugBank = await drugHeader.hasDrugBankLink(); + // DrugBank link is optional, so we just check it doesn't throw + expect(typeof hasDrugBank).toBe("boolean"); + }); + + test("ChEBI link may be displayed", async () => { + const hasChebi = await drugHeader.hasChebiLink(); + // ChEBI link is optional, so we just check it doesn't throw + expect(typeof hasChebi).toBe("boolean"); + }); + + test("DailyMed link may be displayed", async () => { + const hasDailyMed = await drugHeader.hasDailyMedLink(); + // DailyMed link is optional, so we just check it doesn't throw + expect(typeof hasDailyMed).toBe("boolean"); + }); + + test("DrugCentral link may be displayed", async () => { + const hasDrugCentral = await drugHeader.hasDrugCentralLink(); + // DrugCentral link is optional, so we just check it doesn't throw + expect(typeof hasDrugCentral).toBe("boolean"); + }); + + test("Wikipedia link may be displayed", async () => { + const hasWikipedia = await drugHeader.hasWikipediaLink(); + // Wikipedia link is optional, so we just check it doesn't throw + expect(typeof hasWikipedia).toBe("boolean"); + }); +}); diff --git a/packages/platform-test/e2e/pages/drug/drugIndications.spec.ts b/packages/platform-test/e2e/pages/drug/drugIndications.spec.ts new file mode 100644 index 000000000..736a5572a --- /dev/null +++ b/packages/platform-test/e2e/pages/drug/drugIndications.spec.ts @@ -0,0 +1,66 @@ +import { expect, test } from "@playwright/test"; +import { IndicationsSection } from "../../../POM/objects/widgets/shared/indicationsSection"; +import { DrugPage } from "../../../POM/page/drug/drug"; + +test.describe("Drug Indications Section", () => { + let drugPage: DrugPage; + let indicationsSection: IndicationsSection; + + test.beforeEach(async ({ page }) => { + drugPage = new DrugPage(page); + indicationsSection = new IndicationsSection(page); + + // Navigate to a drug with indications data + await drugPage.goToDrugPage("CHEMBL1201585"); + + // Wait for the section to fully load + await indicationsSection.waitForLoad(); + }); + + test("Indications section is visible", async () => { + expect(await indicationsSection.isSectionVisible()).toBe(true); + }); + + test("Indications table displays data", async () => { + const rowCount = await indicationsSection.getTableRows(); + + expect(rowCount).toBeGreaterThan(0); + }); + + test("Indication name is displayed", async () => { + const indication = await indicationsSection.getIndicationName(0); + + expect(indication).not.toBeNull(); + expect(indication).not.toBe(""); + }); + + test("Can click indication link", async ({ page }) => { + await indicationsSection.clickIndicationLink(0); + + // Wait for navigation to disease page + await page.waitForURL((url) => url.toString().includes("/disease/"), { timeout: 5000 }); + + // Should navigate to disease page + expect(page.url()).toContain("/disease/"); + }); + + test("Max phase is displayed", async () => { + const maxPhase = await indicationsSection.getMaxPhase(0); + + expect(maxPhase).not.toBeNull(); + expect(maxPhase).not.toBe(""); + }); + + test("Can search/filter indications", async () => { + const initialRowCount = await indicationsSection.getTableRows(); + + // Search for a specific term + await indicationsSection.search("cancer"); + + // Row count should change + const filteredRowCount = await indicationsSection.getTableRows(); + + // At least the search should execute without error + expect(filteredRowCount).toBeGreaterThanOrEqual(0); + }); +}); diff --git a/packages/platform-test/e2e/pages/drug/drugMechanismsOfAction.spec.ts b/packages/platform-test/e2e/pages/drug/drugMechanismsOfAction.spec.ts new file mode 100644 index 000000000..ae748c137 --- /dev/null +++ b/packages/platform-test/e2e/pages/drug/drugMechanismsOfAction.spec.ts @@ -0,0 +1,69 @@ +import { expect, test } from "@playwright/test"; +import { MechanismsOfActionSection } from "../../../POM/objects/widgets/shared/mechanismsOfActionSection"; +import { DrugPage } from "../../../POM/page/drug/drug"; + +test.describe("Drug Mechanisms of Action Section", () => { + let drugPage: DrugPage; + let moaSection: MechanismsOfActionSection; + + test.beforeEach(async ({ page }) => { + drugPage = new DrugPage(page); + moaSection = new MechanismsOfActionSection(page); + + // Navigate to a drug with mechanisms of action data + await drugPage.goToDrugPage("CHEMBL1201585"); + + // Wait for the section to fully load + await moaSection.waitForLoad(); + }); + + test("Mechanisms of Action section is visible", async () => { + expect(await moaSection.isSectionVisible()).toBe(true); + }); + + test("Mechanisms of Action table displays data", async () => { + const rowCount = await moaSection.getTableRows(); + + expect(rowCount).toBeGreaterThan(0); + }); + + test("Mechanism of action is displayed", async () => { + const moa = await moaSection.getMechanismOfAction(0); + + expect(moa).not.toBeNull(); + expect(moa).not.toBe(""); + }); + + test("Target name is displayed", async () => { + const targetName = await moaSection.getTargetName(0); + + expect(targetName).not.toBeNull(); + }); + + test("Can click target link when available", async ({ page }) => { + const linkCount = await moaSection.getTargetLinksCount(0); + + if (linkCount > 0) { + await moaSection.clickTargetLink(0); + + // Wait for navigation to target page + await page.waitForURL((url) => url.toString().includes("/target/"), { timeout: 5000 }); + + // Should navigate to target/gene page + expect(page.url()).toContain("/target/"); + } + }); + + test("Can search/filter mechanisms of action", async () => { + const initialRowCount = await moaSection.getTableRows(); + + // Search for a specific term + await moaSection.search("inhibitor"); + + // Row count should change + const filteredRowCount = await moaSection.getTableRows(); + + // At least the search should execute without error + expect(filteredRowCount).toBeGreaterThanOrEqual(0); + }); +}); diff --git a/packages/platform-test/e2e/pages/drug/drugPharmacogenomics.spec.ts b/packages/platform-test/e2e/pages/drug/drugPharmacogenomics.spec.ts new file mode 100644 index 000000000..3564ca060 --- /dev/null +++ b/packages/platform-test/e2e/pages/drug/drugPharmacogenomics.spec.ts @@ -0,0 +1,83 @@ +import { expect, test } from "@playwright/test"; +import { PharmacogenomicsSection } from "../../../POM/objects/widgets/shared/pharmacogenomicsSection"; +import { DrugPage } from "../../../POM/page/drug/drug"; + +test.describe("Drug Pharmacogenomics Section", () => { + let drugPage: DrugPage; + let pharmacoSection: PharmacogenomicsSection; + + test.beforeEach(async ({ page }) => { + drugPage = new DrugPage(page); + pharmacoSection = new PharmacogenomicsSection(page); + + // Navigate to a drug with pharmacogenomics data + await drugPage.goToDrugPage("CHEMBL1201585"); + + // Check if section is visible + const isVisible = await pharmacoSection.isSectionVisible(); + if (isVisible) { + await pharmacoSection.waitForLoad(); + } else { + test.skip(); + } + }); + + test("Pharmacogenomics section is visible when data available", async () => { + const isVisible = await pharmacoSection.isSectionVisible(); + expect(isVisible).toBe(true); + }); + + test("Pharmacogenomics table displays data", async () => { + const rowCount = await pharmacoSection.getTableRows(); + + expect(rowCount).toBeGreaterThan(0); + }); + + test("Genotype ID is displayed in table", async () => { + const genotypeId = await pharmacoSection.getGenotypeId(0); + + expect(genotypeId).not.toBeNull(); + expect(genotypeId).not.toBe(""); + }); + + test("Gene/Target link is displayed in table", async () => { + const hasGeneLink = await pharmacoSection.hasGeneLink(0); + + expect(hasGeneLink).toBe(true); + }); + + test("Can click gene link in table", async ({ page }) => { + const hasGeneLink = await pharmacoSection.hasGeneLink(0); + + if (hasGeneLink) { + await pharmacoSection.clickGeneLink(0); + + // Wait for navigation to target page + await page.waitForURL((url) => url.toString().includes("/target/"), { timeout: 5000 }); + + // Should navigate to target/gene page + expect(page.url()).toContain("/target/"); + } + }); + + test("Phenotype link is displayed when available", async () => { + const hasPhenotypeLink = await pharmacoSection.hasPhenotypeLink(0); + + if (hasPhenotypeLink) { + expect(hasPhenotypeLink).toBe(true); + } + }); + + test("Can search/filter pharmacogenomics data", async () => { + const initialRowCount = await pharmacoSection.getTableRows(); + + // Search for a specific term + await pharmacoSection.search("response"); + + // Row count should change + const filteredRowCount = await pharmacoSection.getTableRows(); + + // At least the search should execute without error + expect(filteredRowCount).toBeGreaterThanOrEqual(0); + }); +}); diff --git a/packages/platform-test/e2e/pages/drug/drugPharmacovigilance.spec.ts b/packages/platform-test/e2e/pages/drug/drugPharmacovigilance.spec.ts new file mode 100644 index 000000000..854698103 --- /dev/null +++ b/packages/platform-test/e2e/pages/drug/drugPharmacovigilance.spec.ts @@ -0,0 +1,67 @@ +import { expect, test } from "@playwright/test"; +import { PharmacovigilanceSection } from "../../../POM/objects/widgets/shared/adverseEventsSection"; +import { DrugPage } from "../../../POM/page/drug/drug"; + +test.describe("Drug Pharmacovigilance Section", () => { + let drugPage: DrugPage; + let Pharmacovigilance: PharmacovigilanceSection; + + test.beforeEach(async ({ page }) => { + drugPage = new DrugPage(page); + Pharmacovigilance = new PharmacovigilanceSection(page); + + // Navigate to a drug with adverse events data + await drugPage.goToDrugPage("CHEMBL1201585"); + + // Wait for the section to fully load + await Pharmacovigilance.waitForLoad(); + }); + + test("Adverse Events section is visible", async () => { + expect(await Pharmacovigilance.isSectionVisible()).toBe(true); + }); + + test("Adverse Events table displays data", async () => { + const rowCount = await Pharmacovigilance.getTableRows(); + + expect(rowCount).toBeGreaterThan(0); + }); + + test("Adverse event name is displayed", async () => { + const eventName = await Pharmacovigilance.getAdverseEventName(0); + + expect(eventName).not.toBeNull(); + expect(eventName).not.toBe(""); + }); + + test("Can click adverse event link when available", async ({ page }) => { + const hasLink = await Pharmacovigilance.hasAdverseEventLink(0); + + if (hasLink) { + const initialUrl = page.url(); + await Pharmacovigilance.clickAdverseEventLink(0); + + // Wait for navigation + await page.waitForURL((url) => url.toString() !== initialUrl, { timeout: 5000 }); + + // Check that navigation occurred and URL is valid + const url = page.url(); + expect(url).not.toBe(initialUrl); + expect(url).toMatch(/^https?:\/\/.+/); + } + }); + + test("Number of reported events is displayed", async () => { + const count = await Pharmacovigilance.getReportedEventsCount(0); + + expect(count).not.toBeNull(); + expect(count).not.toBe(""); + }); + + test("Log likelihood ratio is displayed", async () => { + const llr = await Pharmacovigilance.getLogLikelihoodRatio(0); + + expect(llr).not.toBeNull(); + expect(llr).not.toBe(""); + }); +}); diff --git a/packages/platform-test/e2e/pages/drug/drugWarnings.spec.ts b/packages/platform-test/e2e/pages/drug/drugWarnings.spec.ts new file mode 100644 index 000000000..01daeb41b --- /dev/null +++ b/packages/platform-test/e2e/pages/drug/drugWarnings.spec.ts @@ -0,0 +1,69 @@ +import { expect, test } from "@playwright/test"; +import { DrugWarningsSection } from "../../../POM/objects/widgets/shared/drugWarningsSection"; +import { DrugPage } from "../../../POM/page/drug/drug"; + +test.describe("Drug Warnings Section", () => { + let drugPage: DrugPage; + let warningsSection: DrugWarningsSection; + + test.beforeEach(async ({ page }) => { + drugPage = new DrugPage(page); + warningsSection = new DrugWarningsSection(page); + + // Navigate to a drug with warnings data + await drugPage.goToDrugPage("CHEMBL1201585"); + + // Check if section is visible + const isVisible = await warningsSection.isSectionVisible(); + if (isVisible) { + await warningsSection.waitForLoad(); + } else { + test.skip(); + } + }); + + test("Drug Warnings section is visible when data available", async () => { + const isVisible = await warningsSection.isSectionVisible(); + expect(isVisible).toBe(true); + }); + + test("Drug Warnings table displays data", async () => { + const rowCount = await warningsSection.getTableRows(); + + expect(rowCount).toBeGreaterThan(0); + }); + + test("Warning type is displayed", async () => { + const warningType = await warningsSection.getWarningType(0); + + expect(warningType).not.toBeNull(); + expect(warningType).not.toBe(""); + }); + + test("Can click adverse event link when available", async ({ page }) => { + const hasLink = await warningsSection.hasAdverseEventLink(0); + + if (hasLink) { + await warningsSection.clickAdverseEventLink(0); + + // Wait for navigation to disease page + await page.waitForURL((url) => url.toString().includes("/disease/"), { timeout: 5000 }); + + // Should navigate to disease page + expect(page.url()).toContain("/disease/"); + } + }); + + test("Can search/filter warnings", async () => { + const initialRowCount = await warningsSection.getTableRows(); + + // Search for a specific term + await warningsSection.search("warning"); + + // Row count should change + const filteredRowCount = await warningsSection.getTableRows(); + + // At least the search should execute without error + expect(filteredRowCount).toBeGreaterThanOrEqual(0); + }); +}); diff --git a/packages/platform-test/playwright-report/index.html b/packages/platform-test/playwright-report/index.html index cd71d4641..bee703ef3 100644 --- a/packages/platform-test/playwright-report/index.html +++ b/packages/platform-test/playwright-report/index.html @@ -73,4 +73,4 @@
    - \ No newline at end of file + \ No newline at end of file diff --git a/packages/ui/src/components/Header.tsx b/packages/ui/src/components/Header.tsx index b27933541..45b87f180 100644 --- a/packages/ui/src/components/Header.tsx +++ b/packages/ui/src/components/Header.tsx @@ -95,7 +95,7 @@ function Header({
    - + {loading ? : externalLinks} diff --git a/packages/ui/src/components/PublicationsDrawer/PublicationWrapper.jsx b/packages/ui/src/components/PublicationsDrawer/PublicationWrapper.jsx index 709e9cee3..7f5ce4c04 100644 --- a/packages/ui/src/components/PublicationsDrawer/PublicationWrapper.jsx +++ b/packages/ui/src/components/PublicationsDrawer/PublicationWrapper.jsx @@ -78,7 +78,7 @@ function PublicationWrapper({ const externalURL = pmUrl + sourceScope + europePmcId; return ( - + {/* paper title */} From 620e4648b9a08217c9601d09507c5a752dde0a4e Mon Sep 17 00:00:00 2001 From: david oluwasusi Date: Mon, 5 Jan 2026 07:23:52 +0000 Subject: [PATCH 09/34] feat(e2e-fixtures): use fixtures to manage test data --- .../POM/objects/widgets/AOTF/aotfTable.ts | 57 +++- .../widgets/shared/uniprotVariantsSection.ts | 2 +- .../pages/disease/associatedTargets.spec.ts | 238 ++++++++++++++- .../disease/associatedTargetsActions.spec.ts | 47 +++ .../associatedTargetsEvidenceWidgets.spec.ts | 274 ++++++++++++++++++ .../disease/associatedTargetsHeader.spec.ts | 47 +++ .../associatedTargetsPrioritization.spec.ts | 29 ++ .../disease/associatedTargetsTable.spec.ts | 107 +++++++ .../e2e/pages/disease/diseaseProfile.spec.ts | 8 +- .../e2e/pages/drug/drugAdverseEvents.spec.ts | 67 +++++ .../e2e/pages/drug/drugBibliography.spec.ts | 8 +- .../pages/drug/drugClinicalPrecedence.spec.ts | 10 +- .../e2e/pages/drug/drugHeader.spec.ts | 6 +- .../e2e/pages/drug/drugIndications.spec.ts | 8 +- .../pages/drug/drugMechanismsOfAction.spec.ts | 6 +- .../pages/drug/drugPharmacogenomics.spec.ts | 8 +- .../pages/drug/drugPharmacovigilance.spec.ts | 9 +- .../e2e/pages/drug/drugWarnings.spec.ts | 6 +- .../e2e/pages/study/studyPageGWAS.spec.ts | 11 +- .../e2e/pages/study/studyPageQTL.spec.ts | 13 +- .../e2e/pages/variant/variantEVA.spec.ts | 6 +- .../e2e/pages/variant/variantEffect.spec.ts | 6 +- .../variant/variantEffectPredictor.spec.ts | 6 +- .../variantEnhancerToGenePredictions.spec.ts | 10 +- .../variant/variantGWASCredibleSets.spec.ts | 6 +- .../variant/variantMolecularStructure.spec.ts | 6 +- .../variant/variantPageNavigation.spec.ts | 15 +- .../variant/variantPharmacogenomics.spec.ts | 20 +- .../variant/variantProfileHeader.spec.ts | 6 +- .../variant/variantQTLCredibleSets.spec.ts | 6 +- .../variant/variantUniProtVariants.spec.ts | 20 +- packages/platform-test/fixtures/index.ts | 23 ++ packages/platform-test/fixtures/testConfig.ts | 116 ++++++++ .../playwright-report/index.html | 2 +- .../PublicationsDrawer/PublicationsDrawer.jsx | 1 + 35 files changed, 1085 insertions(+), 125 deletions(-) create mode 100644 packages/platform-test/e2e/pages/disease/associatedTargetsActions.spec.ts create mode 100644 packages/platform-test/e2e/pages/disease/associatedTargetsEvidenceWidgets.spec.ts create mode 100644 packages/platform-test/e2e/pages/disease/associatedTargetsHeader.spec.ts create mode 100644 packages/platform-test/e2e/pages/disease/associatedTargetsPrioritization.spec.ts create mode 100644 packages/platform-test/e2e/pages/disease/associatedTargetsTable.spec.ts create mode 100644 packages/platform-test/e2e/pages/drug/drugAdverseEvents.spec.ts create mode 100644 packages/platform-test/fixtures/index.ts create mode 100644 packages/platform-test/fixtures/testConfig.ts diff --git a/packages/platform-test/POM/objects/widgets/AOTF/aotfTable.ts b/packages/platform-test/POM/objects/widgets/AOTF/aotfTable.ts index d6309f23a..7d813ecba 100644 --- a/packages/platform-test/POM/objects/widgets/AOTF/aotfTable.ts +++ b/packages/platform-test/POM/objects/widgets/AOTF/aotfTable.ts @@ -140,6 +140,44 @@ export class AotfTable { return this.page.locator(`[data-testid*='table-row']`, { hasText: name }); } + // Find row index by gene symbol + async findRowIndexByGeneSymbol( + geneSymbol: string, + prefix: string = "core" + ): Promise { + const rowCount = await this.getRowCount(prefix); + + for (let i = 0; i < rowCount; i++) { + const nameCell = this.getNameCell(i, prefix); + const text = await nameCell.textContent(); + + if (text?.includes(geneSymbol)) { + return i; + } + } + + return null; + } + + // Wait for specific gene to appear in table + async waitForGeneInTable( + geneSymbol: string, + prefix: string = "core", + timeout: number = 10000 + ): Promise { + const startTime = Date.now(); + + while (Date.now() - startTime < timeout) { + const rowIndex = await this.findRowIndexByGeneSymbol(geneSymbol, prefix); + if (rowIndex !== null) { + return rowIndex; + } + await this.page.waitForTimeout(500); + } + + return null; + } + // Pin/Unpin row getPinButtonForRow(rowIndex: number): Locator { return this.page.locator(`[data-testid='pin-button-${rowIndex}']`); @@ -209,7 +247,11 @@ export class AotfTable { } // Get the score value from a data cell - async getDataCellScore(rowIndex: number, columnId: string, prefix: string = "core"): Promise { + async getDataCellScore( + rowIndex: number, + columnId: string, + prefix: string = "core" + ): Promise { const cell = this.getDataCell(rowIndex, columnId, prefix); const score = await cell.getAttribute("data-score"); return score ? parseFloat(score) : null; @@ -222,7 +264,10 @@ export class AotfTable { } // Get all data cells with score > 0 in a row - async getDataCellsWithScores(rowIndex: number, prefix: string = "core"): Promise> { + async getDataCellsWithScores( + rowIndex: number, + prefix: string = "core" + ): Promise> { const cells = this.getDataCellsInRow(rowIndex, prefix); const count = await cells.count(); const cellsWithScores: Array<{ columnId: string; score: number }> = []; @@ -231,7 +276,7 @@ export class AotfTable { const cell = cells.nth(i); const scoreAttr = await cell.getAttribute("data-score"); const testId = await cell.getAttribute("data-testid"); - + if (scoreAttr && testId) { const score = parseFloat(scoreAttr); if (score > 0) { @@ -248,14 +293,14 @@ export class AotfTable { // Find first row with data cells that have score > 0 async findFirstRowWithData(prefix: string = "core"): Promise { const rowCount = await this.getRowCount(prefix); - - for (let i = 0; i < rowCount; i++) { + + for (let i = 1; i < rowCount; i++) { const cellsWithScores = await this.getDataCellsWithScores(i, prefix); if (cellsWithScores.length > 0) { return i; } } - + return null; } } diff --git a/packages/platform-test/POM/objects/widgets/shared/uniprotVariantsSection.ts b/packages/platform-test/POM/objects/widgets/shared/uniprotVariantsSection.ts index 6faf90c5d..0d6866abf 100644 --- a/packages/platform-test/POM/objects/widgets/shared/uniprotVariantsSection.ts +++ b/packages/platform-test/POM/objects/widgets/shared/uniprotVariantsSection.ts @@ -59,7 +59,7 @@ export class UniProtVariantsSection { // Get target gene link async getTargetGeneLink(rowIndex: number): Promise { const row = await this.getTableRow(rowIndex); - return row.locator("a[href*='/target/']"); + return row.locator("button"); } async clickTargetGeneLink(rowIndex: number): Promise { diff --git a/packages/platform-test/e2e/pages/disease/associatedTargets.spec.ts b/packages/platform-test/e2e/pages/disease/associatedTargets.spec.ts index d8193ba50..4a0816ec1 100644 --- a/packages/platform-test/e2e/pages/disease/associatedTargets.spec.ts +++ b/packages/platform-test/e2e/pages/disease/associatedTargets.spec.ts @@ -1,15 +1,14 @@ -import { test } from "@playwright/test"; +import { test } from "../../../fixtures"; import { EvidenceSection } from "../../../POM/objects/components/EvidenceSection/evidenceSection"; import { AotfActions } from "../../../POM/objects/widgets/AOTF/aotfActions"; import { AotfTable } from "../../../POM/objects/widgets/AOTF/aotfTable"; import { DiseasePage } from "../../../POM/page/disease/disease"; -const DISEASE_EFO_ID = "EFO_0000612"; const DISEASE_NAME = "myocardial infarction"; test.describe("Disease Page", () => { - test.beforeEach(async ({ page, baseURL }) => { - await page.goto(`${baseURL}/disease/${DISEASE_EFO_ID}/associations`); + test.beforeEach(async ({ page, baseURL, testConfig }) => { + await page.goto(`${baseURL}/disease/${testConfig.disease.primary}/associations`); }); test.describe("Aotf main actions functionality", () => { @@ -194,8 +193,11 @@ test.describe("Disease Page", () => { const dataCells = await aotfTable.getDataCellsWithScores(rowIndex); test.expect(dataCells.length).toBeGreaterThan(0); - // Skip the first cell since it's the total association score - const cellsToTest = dataCells.slice(1, dataCells.length); + // Filter out non-evidence columns + // 'score' = total association score (not an evidence section) + // 'maxClinicalTrialPhase', 'tractability' = prioritization metrics (link to target page sections) + const nonEvidenceColumns = ["score"]; + const cellsToTest = dataCells.filter((cell) => !nonEvidenceColumns.includes(cell.columnId)); for (const cell of cellsToTest) { // Click on the data cell to open the evidence section @@ -222,6 +224,226 @@ test.describe("Disease Page", () => { await page.waitForTimeout(300); } }); + + test("specified genes have correct evidence widgets for their data cells", async ({ + page, + testConfig, + }) => { + const aotfTable = new AotfTable(page); + const aotfActions = new AotfActions(page); + const evidenceSection = new EvidenceSection(page); + const genesToTest = testConfig.disease.aotfGenes || []; + + if (genesToTest.length === 0) { + test.skip(true, "No genes specified in test config"); + return; + } + + for (const geneSymbol of genesToTest) { + // Search for the specific gene + await aotfActions.searchByName(geneSymbol); + + // Wait for table to load with filtered results + await aotfTable.waitForTableLoad(); + + // Find the row for this gene + const rowIndex = await aotfTable.findRowIndexByGeneSymbol(geneSymbol); + + if (rowIndex === null) { + test.fail(true, `Gene ${geneSymbol} not found in table`); + continue; + } + + // Get all data cells with scores for this gene + const dataCells = await aotfTable.getDataCellsWithScores(rowIndex); + + if (dataCells.length === 0) { + test.fail(true, `Gene ${geneSymbol} has no data cells with scores`); + continue; + } + + // Filter out non-evidence columns + const nonEvidenceColumns = ["score"]; + const cellsToTest = dataCells.filter((cell) => !nonEvidenceColumns.includes(cell.columnId)); + + if (cellsToTest.length === 0) { + test.fail(true, `Gene ${geneSymbol} has no evidence data cells`); + continue; + } + + for (const cell of cellsToTest) { + // Click on the data cell to open the evidence section + await aotfTable.clickDataCell(rowIndex, cell.columnId); + + // Wait for section to load + await evidenceSection.waitForSectionLoad(cell.columnId); + + // Verify that an evidence section is visible + const hasSections = await evidenceSection.hasAnyEvidenceSection(); + test + .expect( + hasSections, + `Gene ${geneSymbol} - ${cell.columnId}: Should have evidence sections` + ) + .toBe(true); + + // Verify the specific section for this data source is visible + const isVisible = await evidenceSection.isEvidenceSectionVisible(cell.columnId); + test + .expect( + isVisible, + `Gene ${geneSymbol} - ${cell.columnId}: Evidence section should be visible` + ) + .toBe(true); + + // Verify no loader is visible + const hasLoader = await evidenceSection.isLoaderVisible(); + test + .expect( + hasLoader, + `Gene ${geneSymbol} - ${cell.columnId}: Loader should not be visible` + ) + .toBe(false); + + // Click the same cell again to close/toggle the section + await aotfTable.clickDataCell(rowIndex, cell.columnId); + + // Wait for evidence section to close + await evidenceSection.waitForLoaderToDisappear(); + } + + // Clear the search filter for next gene + await aotfActions.clearNameFilter(); + + // Wait for table to reload with all results + await aotfTable.waitForTableLoad(); + } + }); + + test("specified genes in target prioritization view have correct evidence widgets", async ({ + page, + testConfig, + }) => { + const aotfTable = new AotfTable(page); + const aotfActions = new AotfActions(page); + const evidenceSection = new EvidenceSection(page); + const genesToTest = testConfig.disease.aotfGenes || []; + + // Map prioritization column IDs to their section IDs + const prioritizationColumnToSection: Record = { + maxClinicalTrialPhase: "knownDrugs", + isInMembrane: "subcellularLocation", + isSecreted: "subcellularLocation", + hasLigand: "tractability", + hasSmallMoleculeBinder: "tractability", + hasPocket: "tractability", + mouseOrthologMaxIdentityPercentage: "compGenomics", + hasHighQualityChemicalProbes: "chemicalProbes", + mouseKOScore: "mousePhenotypes", + geneticConstraint: "geneticConstraint", + geneEssentiality: "depMapEssentiality", + hasSafetyEvent: "safety", + isCancerDriverGene: "cancerHallmarks", + paralogMaxIdentityPercentage: "compGenomics", + tissueSpecificity: "expressions", + tissueDistribution: "expressions", + }; + + if (genesToTest.length === 0) { + test.skip(true, "No genes specified in test config"); + return; + } + + // Switch to target prioritization view + await aotfActions.switchToPrioritisationView(); + await aotfTable.waitForTableLoad(); + + for (const geneSymbol of genesToTest) { + // Search for the specific gene + await aotfActions.searchByName(geneSymbol); + + // Wait for table to load with filtered results + await aotfTable.waitForTableLoad(); + + // Find the row for this gene + const rowIndex = await aotfTable.findRowIndexByGeneSymbol(geneSymbol); + + if (rowIndex === null) { + test.fail(true, `Gene ${geneSymbol} not found in prioritization table`); + continue; + } + + // Get all data cells with scores for this gene + const dataCells = await aotfTable.getDataCellsWithScores(rowIndex); + + if (dataCells.length === 0) { + test.fail( + true, + `Gene ${geneSymbol} has no data cells with scores in prioritization view` + ); + continue; + } + + // Filter out non-evidence columns + const nonEvidenceColumns = ["score"]; + const cellsToTest = dataCells.filter((cell) => !nonEvidenceColumns.includes(cell.columnId)); + + if (cellsToTest.length === 0) { + test.fail(true, `Gene ${geneSymbol} has no evidence data cells in prioritization view`); + continue; + } + + for (const cell of cellsToTest) { + // Click on the data cell to open the evidence section + await aotfTable.clickDataCell(rowIndex, cell.columnId); + + // Map the column ID to the section ID for prioritization columns + const sectionId = prioritizationColumnToSection[cell.columnId] || cell.columnId; + + // Wait for section to load + await evidenceSection.waitForSectionLoad(sectionId); + + // Verify that an evidence section is visible + const hasSections = await evidenceSection.hasAnyEvidenceSection(); + test + .expect( + hasSections, + `Prioritization - Gene ${geneSymbol} - ${cell.columnId} (section: ${sectionId}): Should have evidence sections` + ) + .toBe(true); + + // Verify the specific section for this data source is visible + const isVisible = await evidenceSection.isEvidenceSectionVisible(sectionId); + test + .expect( + isVisible, + `Prioritization - Gene ${geneSymbol} - ${cell.columnId} (section: ${sectionId}): Evidence section should be visible` + ) + .toBe(true); + + // Verify no loader is visible + const hasLoader = await evidenceSection.isLoaderVisible(); + test + .expect( + hasLoader, + `Prioritization - Gene ${geneSymbol} - ${cell.columnId} (section: ${sectionId}): Loader should not be visible` + ) + .toBe(false); + + // Click the same cell again to close/toggle the section + await aotfTable.clickDataCell(rowIndex, cell.columnId); + + // Wait for evidence section to close + await evidenceSection.waitForLoaderToDisappear(); + } + + // Clear the search filter for next gene + await aotfActions.clearNameFilter(); + + // Wait for table to reload with all results + await aotfTable.waitForTableLoad(); + } + }); }); test("Disease header is correctly displayed", async ({ page }) => { @@ -237,7 +459,7 @@ test.describe("Disease Page", () => { await test.expect(page.url()).toBe(diseasePage.getProfilePage()); }); - test("External links in header are displayed and working", async ({ page }) => { + test("External links in header are displayed and working", async ({ page, testConfig }) => { const diseasePage = new DiseasePage(page); // Check for EFO external link @@ -246,7 +468,7 @@ test.describe("Disease Page", () => { // Verify the EFO link has the correct href const efoHref = await diseasePage.getEfoLinkHref(); - test.expect(efoHref).toContain(DISEASE_EFO_ID); + test.expect(efoHref).toContain(testConfig.disease.primary); test.expect(efoHref).toContain("ebi.ac.uk/ols4/ontologies/efo/terms"); // Check for cross-reference links (e.g., MONDO, MeSH, etc.) diff --git a/packages/platform-test/e2e/pages/disease/associatedTargetsActions.spec.ts b/packages/platform-test/e2e/pages/disease/associatedTargetsActions.spec.ts new file mode 100644 index 000000000..5b761651f --- /dev/null +++ b/packages/platform-test/e2e/pages/disease/associatedTargetsActions.spec.ts @@ -0,0 +1,47 @@ +import { test } from "../../../fixtures"; +import { AotfActions } from "../../../POM/objects/widgets/AOTF/aotfActions"; + +test.describe("Disease Page - AOTF Actions", () => { + test.beforeEach(async ({ page, baseURL, testConfig }) => { + await page.goto(`${baseURL}/disease/${testConfig.disease.primary}/associations`); + }); + + test("Can search through targets in the disease associations page", async ({ page }) => { + const aotfActions = new AotfActions(page); + await aotfActions.searchByName("ADRB1"); + const filterValue = await aotfActions.getNameFilterValue(); + test.expect(filterValue).toBe("ADRB1"); + }); + + test("Can toggle advance filters", async ({ page }) => { + const aotfActions = new AotfActions(page); + await aotfActions.openFacetsSearch(); + const isOpen = await aotfActions.isFacetsPopoverOpen(); + test.expect(isOpen).toBe(true); + }); + + test("Can toggle column options", async ({ page }) => { + const aotfActions = new AotfActions(page); + await aotfActions.openColumnOptions(); + const isActive = await aotfActions.isColumnOptionsActive(); + test.expect(isActive).toBe(true); + }); + + test("Can toggle export options", async ({ page }) => { + const aotfActions = new AotfActions(page); + await aotfActions.openExportMenu(); + const isActive = await aotfActions.isExportMenuOpen(); + test.expect(isActive).toBe(true); + }); + + test("Can toggle between association and target prioritization options", async ({ page }) => { + const aotfActions = new AotfActions(page); + await aotfActions.switchToAssociationsView(); + const currentView = await aotfActions.getCurrentDisplayMode(); + test.expect(currentView).toBe("associations"); + + await aotfActions.switchToPrioritisationView(); + const newView = await aotfActions.getCurrentDisplayMode(); + test.expect(newView).toBe("prioritisations"); + }); +}); diff --git a/packages/platform-test/e2e/pages/disease/associatedTargetsEvidenceWidgets.spec.ts b/packages/platform-test/e2e/pages/disease/associatedTargetsEvidenceWidgets.spec.ts new file mode 100644 index 000000000..f3361f8f3 --- /dev/null +++ b/packages/platform-test/e2e/pages/disease/associatedTargetsEvidenceWidgets.spec.ts @@ -0,0 +1,274 @@ +import { EvidenceSection } from "../../../POM/objects/components/EvidenceSection/evidenceSection"; +import { AotfActions } from "../../../POM/objects/widgets/AOTF/aotfActions"; +import { AotfTable } from "../../../POM/objects/widgets/AOTF/aotfTable"; +import { test } from "../../../fixtures"; + +test.describe("Disease Page - AOTF Evidence Widgets", () => { + test.beforeEach(async ({ page, baseURL, testConfig }) => { + await page.goto(`${baseURL}/disease/${testConfig.disease.primary}/associations`); + }); + + test("if a target has data, the corresponding widget is shown", async ({ page }) => { + const aotfTable = new AotfTable(page); + const evidenceSection = new EvidenceSection(page); + await aotfTable.waitForTableLoad(); + + // Find the first row that has data cells with score > 0 + const rowIndex = await aotfTable.findFirstRowWithData(); + test.expect(rowIndex).not.toBeNull(); + + if (rowIndex === null) { + test.skip(true, "No rows with data found"); + return; + } + + // Get all data cells with scores in that row + const dataCells = await aotfTable.getDataCellsWithScores(rowIndex); + test.expect(dataCells.length).toBeGreaterThan(0); + + // Filter out non-evidence columns + // 'score' = total association score (not an evidence section) + // 'maxClinicalTrialPhase', 'tractability' = prioritization metrics (link to target page sections) + const nonEvidenceColumns = ["score"]; + const cellsToTest = dataCells.filter((cell) => !nonEvidenceColumns.includes(cell.columnId)); + + for (const cell of cellsToTest) { + // Click on the data cell to open the evidence section + await aotfTable.clickDataCell(rowIndex, cell.columnId); + + // Wait for section to load (no loader, no error) + await evidenceSection.waitForSectionLoad(cell.columnId); + + // Verify that an evidence section is visible + const hasSections = await evidenceSection.hasAnyEvidenceSection(); + test.expect(hasSections).toBe(true); + + // Verify the specific section for this data source is visible + // The section ID is typically the data source ID (e.g., 'gwas', 'eva', etc.) + const isVisible = await evidenceSection.isEvidenceSectionVisible(cell.columnId); + test.expect(isVisible).toBe(true); + + // Verify no loader is visible + const hasLoader = await evidenceSection.isLoaderVisible(); + test.expect(hasLoader).toBe(false); + + // Click the same cell again to close/toggle the section + await aotfTable.clickDataCell(rowIndex, cell.columnId); + await page.waitForTimeout(300); + } + }); + + test("specified genes have correct evidence widgets for their data cells", async ({ + page, + testConfig, + }) => { + const aotfTable = new AotfTable(page); + const aotfActions = new AotfActions(page); + const evidenceSection = new EvidenceSection(page); + const genesToTest = testConfig.disease.aotfGenes || []; + + if (genesToTest.length === 0) { + test.skip(true, "No genes specified in test config"); + return; + } + + for (const geneSymbol of genesToTest) { + // Search for the specific gene + await aotfActions.searchByName(geneSymbol); + + // Wait for table to load with filtered results + await aotfTable.waitForTableLoad(); + + // Find the row for this gene + const rowIndex = await aotfTable.findRowIndexByGeneSymbol(geneSymbol); + + if (rowIndex === null) { + test.fail(true, `Gene ${geneSymbol} not found in table`); + continue; + } + + // Get all data cells with scores for this gene + const dataCells = await aotfTable.getDataCellsWithScores(rowIndex); + + if (dataCells.length === 0) { + test.fail(true, `Gene ${geneSymbol} has no data cells with scores`); + continue; + } + + // Filter out non-evidence columns + const nonEvidenceColumns = ["score"]; + const cellsToTest = dataCells.filter((cell) => !nonEvidenceColumns.includes(cell.columnId)); + + if (cellsToTest.length === 0) { + test.fail(true, `Gene ${geneSymbol} has no evidence data cells`); + continue; + } + + for (const cell of cellsToTest) { + // Click on the data cell to open the evidence section + await aotfTable.clickDataCell(rowIndex, cell.columnId); + + // Wait for section to load + await evidenceSection.waitForSectionLoad(cell.columnId); + + // Verify that an evidence section is visible + const hasSections = await evidenceSection.hasAnyEvidenceSection(); + test + .expect( + hasSections, + `Gene ${geneSymbol} - ${cell.columnId}: Should have evidence sections` + ) + .toBe(true); + + // Verify the specific section for this data source is visible + const isVisible = await evidenceSection.isEvidenceSectionVisible(cell.columnId); + test + .expect( + isVisible, + `Gene ${geneSymbol} - ${cell.columnId}: Evidence section should be visible` + ) + .toBe(true); + + // Verify no loader is visible + const hasLoader = await evidenceSection.isLoaderVisible(); + test + .expect(hasLoader, `Gene ${geneSymbol} - ${cell.columnId}: Loader should not be visible`) + .toBe(false); + + // Click the same cell again to close/toggle the section + await aotfTable.clickDataCell(rowIndex, cell.columnId); + + // Wait for evidence section to close + await evidenceSection.waitForLoaderToDisappear(); + } + + // Clear the search filter for next gene + await aotfActions.clearNameFilter(); + + // Wait for table to reload with all results + await aotfTable.waitForTableLoad(); + } + }); + + test("specified genes in target prioritization view have correct evidence widgets", async ({ + page, + testConfig, + }) => { + const aotfTable = new AotfTable(page); + const aotfActions = new AotfActions(page); + const evidenceSection = new EvidenceSection(page); + const genesToTest = testConfig.disease.aotfGenes || []; + + // Map prioritization column IDs to their section IDs + const prioritizationColumnToSection: Record = { + maxClinicalTrialPhase: "knownDrugs", + isInMembrane: "subcellularLocation", + isSecreted: "subcellularLocation", + hasLigand: "tractability", + hasSmallMoleculeBinder: "tractability", + hasPocket: "tractability", + mouseOrthologMaxIdentityPercentage: "compGenomics", + hasHighQualityChemicalProbes: "chemicalProbes", + mouseKOScore: "mousePhenotypes", + geneticConstraint: "geneticConstraint", + geneEssentiality: "depMapEssentiality", + hasSafetyEvent: "safety", + isCancerDriverGene: "cancerHallmarks", + paralogMaxIdentityPercentage: "compGenomics", + tissueSpecificity: "expressions", + tissueDistribution: "expressions", + }; + + if (genesToTest.length === 0) { + test.skip(true, "No genes specified in test config"); + return; + } + + // Switch to target prioritization view + await aotfActions.switchToPrioritisationView(); + await aotfTable.waitForTableLoad(); + + for (const geneSymbol of genesToTest) { + // Search for the specific gene + await aotfActions.searchByName(geneSymbol); + + // Wait for table to load with filtered results + await aotfTable.waitForTableLoad(); + + // Find the row for this gene + const rowIndex = await aotfTable.findRowIndexByGeneSymbol(geneSymbol); + + if (rowIndex === null) { + test.fail(true, `Gene ${geneSymbol} not found in prioritization table`); + continue; + } + + // Get all data cells with scores for this gene + const dataCells = await aotfTable.getDataCellsWithScores(rowIndex); + + if (dataCells.length === 0) { + test.fail(true, `Gene ${geneSymbol} has no data cells with scores in prioritization view`); + continue; + } + + // Filter out non-evidence columns + const nonEvidenceColumns = ["score"]; + const cellsToTest = dataCells.filter((cell) => !nonEvidenceColumns.includes(cell.columnId)); + + if (cellsToTest.length === 0) { + test.fail(true, `Gene ${geneSymbol} has no evidence data cells in prioritization view`); + continue; + } + + for (const cell of cellsToTest) { + // Click on the data cell to open the evidence section + await aotfTable.clickDataCell(rowIndex, cell.columnId); + + // Map the column ID to the section ID for prioritization columns + const sectionId = prioritizationColumnToSection[cell.columnId] || cell.columnId; + + // Wait for section to load + await evidenceSection.waitForSectionLoad(sectionId); + + // Verify that an evidence section is visible + const hasSections = await evidenceSection.hasAnyEvidenceSection(); + test + .expect( + hasSections, + `Prioritization - Gene ${geneSymbol} - ${cell.columnId} (section: ${sectionId}): Should have evidence sections` + ) + .toBe(true); + + // Verify the specific section for this data source is visible + const isVisible = await evidenceSection.isEvidenceSectionVisible(sectionId); + test + .expect( + isVisible, + `Prioritization - Gene ${geneSymbol} - ${cell.columnId} (section: ${sectionId}): Evidence section should be visible` + ) + .toBe(true); + + // Verify no loader is visible + const hasLoader = await evidenceSection.isLoaderVisible(); + test + .expect( + hasLoader, + `Prioritization - Gene ${geneSymbol} - ${cell.columnId} (section: ${sectionId}): Loader should not be visible` + ) + .toBe(false); + + // Click the same cell again to close/toggle the section + await aotfTable.clickDataCell(rowIndex, cell.columnId); + + // Wait for evidence section to close + await evidenceSection.waitForLoaderToDisappear(); + } + + // Clear the search filter for next gene + await aotfActions.clearNameFilter(); + + // Wait for table to reload with all results + await aotfTable.waitForTableLoad(); + } + }); +}); diff --git a/packages/platform-test/e2e/pages/disease/associatedTargetsHeader.spec.ts b/packages/platform-test/e2e/pages/disease/associatedTargetsHeader.spec.ts new file mode 100644 index 000000000..92c53b6fa --- /dev/null +++ b/packages/platform-test/e2e/pages/disease/associatedTargetsHeader.spec.ts @@ -0,0 +1,47 @@ +import { test } from "../../../fixtures"; +import { DiseasePage } from "../../../POM/page/disease/disease"; + +test.describe("Disease Page - Header and Navigation", () => { + test.beforeEach(async ({ page, baseURL, testConfig }) => { + await page.goto(`${baseURL}/disease/${testConfig.disease.primary}/associations`); + }); + + test("Disease header is correctly displayed", async ({ page, testConfig }) => { + const diseaseName = page.getByTestId("profile-page-header-text"); + await test.expect(diseaseName).toHaveText(testConfig.disease.name || ""); + }); + + test("Can navigate to the profile page from the disease page", async ({ page }) => { + const diseasePage = new DiseasePage(page); + await diseasePage.goToProfilePage(); + + // Verify that the URL is the profile page URL + await test.expect(page.url()).toBe(diseasePage.getProfilePage()); + }); + + test("External links in header are displayed and working", async ({ page, testConfig }) => { + const diseasePage = new DiseasePage(page); + + // Check for EFO external link + const efoLink = diseasePage.getEfoLink(); + await test.expect(efoLink).toBeVisible(); + + // Verify the EFO link has the correct href + const efoHref = await diseasePage.getEfoLinkHref(); + test.expect(efoHref).toContain(testConfig.disease.primary); + test.expect(efoHref).toContain("ebi.ac.uk/ols4/ontologies/efo/terms"); + + // Check for cross-reference links (e.g., MONDO, MeSH, etc.) + const xrefCount = await diseasePage.getXrefLinksCount(); + + // Verify that at least one cross-reference link exists + test.expect(xrefCount).toBeGreaterThan(0); + + // Verify the first xref link has a valid href + if (xrefCount > 0) { + const firstXrefHref = await diseasePage.getFirstXrefLinkHref(); + test.expect(firstXrefHref).toBeTruthy(); + test.expect(firstXrefHref).toMatch(/^https?:\/\//); + } + }); +}); diff --git a/packages/platform-test/e2e/pages/disease/associatedTargetsPrioritization.spec.ts b/packages/platform-test/e2e/pages/disease/associatedTargetsPrioritization.spec.ts new file mode 100644 index 000000000..f3ca05b25 --- /dev/null +++ b/packages/platform-test/e2e/pages/disease/associatedTargetsPrioritization.spec.ts @@ -0,0 +1,29 @@ +import { test } from "../../../fixtures"; +import { AotfActions } from "../../../POM/objects/widgets/AOTF/aotfActions"; +import { AotfTable } from "../../../POM/objects/widgets/AOTF/aotfTable"; + +test.describe("Disease Page - AOTF Prioritization", () => { + test.beforeEach(async ({ page, baseURL, testConfig }) => { + await page.goto(`${baseURL}/disease/${testConfig.disease.primary}/associations`); + }); + + test("Can switch to target prioritisation view and see data", async ({ page }) => { + const aotfActions = new AotfActions(page); + const aotfTable = new AotfTable(page); + + // Switch to prioritisation view + await aotfActions.switchToPrioritisationView(); + await aotfTable.waitForTableLoad(); + + // Verify table is visible + await test.expect(aotfTable.getTable()).toBeVisible(); + + // Verify rows are loaded + const rowCount = await aotfTable.getRowCount(); + test.expect(rowCount).toBeGreaterThan(0); + + // Verify first row has data + const firstRowName = await aotfTable.getEntityName(0); + test.expect(firstRowName).toBeTruthy(); + }); +}); diff --git a/packages/platform-test/e2e/pages/disease/associatedTargetsTable.spec.ts b/packages/platform-test/e2e/pages/disease/associatedTargetsTable.spec.ts new file mode 100644 index 000000000..af36ccd81 --- /dev/null +++ b/packages/platform-test/e2e/pages/disease/associatedTargetsTable.spec.ts @@ -0,0 +1,107 @@ +import { test } from "../../../fixtures"; +import { AotfActions } from "../../../POM/objects/widgets/AOTF/aotfActions"; +import { AotfTable } from "../../../POM/objects/widgets/AOTF/aotfTable"; + +test.describe("Disease Page - AOTF Table", () => { + test.beforeEach(async ({ page, baseURL, testConfig }) => { + await page.goto(`${baseURL}/disease/${testConfig.disease.primary}/associations`); + }); + + test("targets are displayed in the associations table", async ({ page }) => { + const aotfTable = new AotfTable(page); + + // Wait for table to load + await aotfTable.waitForTableLoad(); + + // Verify table is visible + await test.expect(aotfTable.getTable()).toBeVisible(); + + // Verify header is present + await test.expect(aotfTable.getTargetOrDiseaseHeader()).toBeVisible(); + const headerText = await aotfTable.getHeaderText("table-header-name"); + test.expect(headerText).toBe("Target"); + + // Verify rows are loaded + const rowCount = await aotfTable.getRowCount(); + test.expect(rowCount).toBeGreaterThan(0); + + // Verify first row has data + const firstRowName = await aotfTable.getEntityName(0); + test.expect(firstRowName).toBeTruthy(); + }); + + test("can sort by GWAS score in the associations table", async ({ page }) => { + const aotfTable = new AotfTable(page); + const aotfActions = new AotfActions(page); + await aotfTable.waitForTableLoad(); + + // Verify no sort filter is active initially (default sort by Association Score) + const initialSortActive = await aotfActions.hasSortFilter(); + test.expect(initialSortActive).toBe(false); + + // Click to sort by a different column (GWAS associations) + await aotfTable.sortByColumn("GWAS associations"); + await page.waitForTimeout(1000); // Wait for sort to complete + + // Verify sort filter is now active in the ActiveFiltersPanel + const sortActive = await aotfActions.hasSortFilter(); + test.expect(sortActive).toBe(true); + + // Verify the sort filter shows the correct column name + const sortFilterText = await aotfActions.getSortFilterText(); + test.expect(sortFilterText).toContain("GWAS associations"); + }); + + test("can paginate through the associations table", async ({ page }) => { + const aotfTable = new AotfTable(page); + await aotfTable.waitForTableLoad(); + + // Get first page data + const firstPageFirstRow = await aotfTable.getEntityName(0); + + // Go to next page + await aotfTable.clickNextPage(); + await page.waitForTimeout(1000); // Wait for new data to load + + // Get second page data + const secondPageFirstRow = await aotfTable.getEntityName(0); + + // First row on different pages should be different + test.expect(firstPageFirstRow).not.toBe(secondPageFirstRow); + + // Go back to previous page + await aotfTable.clickPreviousPage(); + await page.waitForTimeout(1000); + + const backToFirstRow = await aotfTable.getEntityName(0); + test.expect(backToFirstRow).toBe(firstPageFirstRow); + }); + + test("can change page size", async ({ page }) => { + const aotfTable = new AotfTable(page); + await aotfTable.waitForTableLoad(); + + // Change page size to 25 + await aotfTable.selectPageSize("25"); + await page.waitForTimeout(1000); + + // Verify more rows are displayed (up to 25) + const rowCount = await aotfTable.getRowCount(); + test.expect(rowCount).toBeGreaterThanOrEqual(10); // Default is usually 10 + }); + + test("can filter targets by name", async ({ page }) => { + const aotfActions = new AotfActions(page); + const aotfTable = new AotfTable(page); + + await aotfTable.waitForTableLoad(); + + // Search for a specific target + await aotfActions.searchByName("IL6"); + await page.waitForTimeout(1000); // Wait for filter + debounce + + // Verify filtered results contain the search term + const firstRowName = await aotfTable.getEntityName(0); + test.expect(firstRowName?.toLowerCase()).toContain("il6"); + }); +}); diff --git a/packages/platform-test/e2e/pages/disease/diseaseProfile.spec.ts b/packages/platform-test/e2e/pages/disease/diseaseProfile.spec.ts index 71624b3c7..0510b5e54 100644 --- a/packages/platform-test/e2e/pages/disease/diseaseProfile.spec.ts +++ b/packages/platform-test/e2e/pages/disease/diseaseProfile.spec.ts @@ -1,4 +1,4 @@ -import { test } from "@playwright/test"; +import { test } from "../../../fixtures"; import { ProfileHeader } from "../../../POM/objects/components/ProfileHeader/profileHeader"; import { BibliographySection } from "../../../POM/objects/widgets/Bibliography/bibliographySection"; import { GWASStudiesSection } from "../../../POM/objects/widgets/GWAS/gwasStudiesSection"; @@ -7,12 +7,10 @@ import { OntologySection } from "../../../POM/objects/widgets/Ontology/ontologyS import { OTProjectsSection } from "../../../POM/objects/widgets/OTProjects/otProjectsSection"; import { PhenotypesSection } from "../../../POM/objects/widgets/Phenotypes/phenotypesSection"; -const DISEASE_EFO_ID = "EFO_0000612"; -const DISEASE_NAME = "myocardial infarction"; test.describe("Disease Profile Page", () => { - test.beforeEach(async ({ page, baseURL }) => { - await page.goto(`${baseURL}/disease/${DISEASE_EFO_ID}`); + test.beforeEach(async ({ page, baseURL, testConfig }) => { + await page.goto(`${baseURL}/disease/${testConfig.disease.primary}`); }); test.describe("Profile Header", () => { diff --git a/packages/platform-test/e2e/pages/drug/drugAdverseEvents.spec.ts b/packages/platform-test/e2e/pages/drug/drugAdverseEvents.spec.ts new file mode 100644 index 000000000..f2df45dda --- /dev/null +++ b/packages/platform-test/e2e/pages/drug/drugAdverseEvents.spec.ts @@ -0,0 +1,67 @@ +import { expect, test } from "../../../fixtures"; +import { PharmacovigilanceSection } from "../../../POM/objects/widgets/shared/adverseEventsSection"; +import { DrugPage } from "../../../POM/page/drug/drug"; + +test.describe("Drug Pharmacovigilance Section", () => { + let drugPage: DrugPage; + let Pharmacovigilance: PharmacovigilanceSection; + + test.beforeEach(async ({ page, testConfig }) => { + drugPage = new DrugPage(page); + Pharmacovigilance = new PharmacovigilanceSection(page); + + // Navigate to a drug with adverse events data + await drugPage.goToDrugPage(testConfig.drug.alternatives?.withAdverseEvents ?? testConfig.drug.primary); + + // Wait for the section to fully load + await Pharmacovigilance.waitForLoad(); + }); + + test("Adverse Events section is visible", async () => { + expect(await Pharmacovigilance.isSectionVisible()).toBe(true); + }); + + test("Adverse Events table displays data", async () => { + const rowCount = await Pharmacovigilance.getTableRows(); + + expect(rowCount).toBeGreaterThan(0); + }); + + test("Adverse event name is displayed", async () => { + const eventName = await Pharmacovigilance.getAdverseEventName(0); + + expect(eventName).not.toBeNull(); + expect(eventName).not.toBe(""); + }); + + test("Can click adverse event link when available", async ({ page }) => { + const hasLink = await Pharmacovigilance.hasAdverseEventLink(0); + + if (hasLink) { + const initialUrl = page.url(); + await Pharmacovigilance.clickAdverseEventLink(0); + + // Wait for navigation + await page.waitForURL((url) => url.toString() !== initialUrl, { timeout: 5000 }); + + // Check that navigation occurred and URL is valid + const url = page.url(); + expect(url).not.toBe(initialUrl); + expect(url).toMatch(/^https?:\/\/.+/); + } + }); + + test("Number of reported events is displayed", async () => { + const count = await Pharmacovigilance.getReportedEventsCount(0); + + expect(count).not.toBeNull(); + expect(count).not.toBe(""); + }); + + test("Log likelihood ratio is displayed", async () => { + const llr = await Pharmacovigilance.getLogLikelihoodRatio(0); + + expect(llr).not.toBeNull(); + expect(llr).not.toBe(""); + }); +}); diff --git a/packages/platform-test/e2e/pages/drug/drugBibliography.spec.ts b/packages/platform-test/e2e/pages/drug/drugBibliography.spec.ts index da60db8e6..0ff2bf3b0 100644 --- a/packages/platform-test/e2e/pages/drug/drugBibliography.spec.ts +++ b/packages/platform-test/e2e/pages/drug/drugBibliography.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from "@playwright/test"; +import { expect, test } from "../../../fixtures"; import { BibliographySection } from "../../../POM/objects/widgets/Bibliography/bibliographySection"; import { DrugPage } from "../../../POM/page/drug/drug"; @@ -6,12 +6,12 @@ test.describe("Drug Bibliography Section", () => { let drugPage: DrugPage; let bibliographySection: BibliographySection; - test.beforeEach(async ({ page }) => { + test.beforeEach(async ({ page, testConfig }) => { drugPage = new DrugPage(page); bibliographySection = new BibliographySection(page); - // Navigate to a drug with bibliography data - await drugPage.goToDrugPage("CHEMBL1201585"); + // Navigate to a drug with bibliography + await drugPage.goToDrugPage(testConfig.drug.primary); // Check if section is visible const isVisible = await bibliographySection.isSectionVisible(); diff --git a/packages/platform-test/e2e/pages/drug/drugClinicalPrecedence.spec.ts b/packages/platform-test/e2e/pages/drug/drugClinicalPrecedence.spec.ts index e807819a1..68dd1d7d5 100644 --- a/packages/platform-test/e2e/pages/drug/drugClinicalPrecedence.spec.ts +++ b/packages/platform-test/e2e/pages/drug/drugClinicalPrecedence.spec.ts @@ -1,17 +1,17 @@ -import { expect, test } from "@playwright/test"; +import { expect, test } from "../../../fixtures"; import { ClinicalPrecedenceSection } from "../../../POM/objects/widgets/KnownDrugs/knownDrugsSection"; import { DrugPage } from "../../../POM/page/drug/drug"; -test.describe("Clinical Precedence Section", () => { +test.describe("Drug Clinical Precedence Section", () => { let drugPage: DrugPage; let clinicalPrecedence: ClinicalPrecedenceSection; - test.beforeEach(async ({ page }) => { + test.beforeEach(async ({ page, testConfig }) => { drugPage = new DrugPage(page); clinicalPrecedence = new ClinicalPrecedenceSection(page); - // Navigate to a drug with Clinical precedence data - await drugPage.goToDrugPage("CHEMBL1201585"); + // Navigate to a drug with clinical precedence data + await drugPage.goToDrugPage(testConfig.drug.primary); // Check if section is visible const isVisible = await clinicalPrecedence.isSectionVisible(); diff --git a/packages/platform-test/e2e/pages/drug/drugHeader.spec.ts b/packages/platform-test/e2e/pages/drug/drugHeader.spec.ts index beb86bf29..8e571ba2e 100644 --- a/packages/platform-test/e2e/pages/drug/drugHeader.spec.ts +++ b/packages/platform-test/e2e/pages/drug/drugHeader.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from "@playwright/test"; +import { expect, test } from "../../../fixtures"; import { DrugHeader } from "../../../POM/objects/components/DrugHeader/drugHeader"; import { DrugPage } from "../../../POM/page/drug/drug"; @@ -6,12 +6,12 @@ test.describe("Drug Header", () => { let drugPage: DrugPage; let drugHeader: DrugHeader; - test.beforeEach(async ({ page }) => { + test.beforeEach(async ({ page, testConfig }) => { drugPage = new DrugPage(page); drugHeader = new DrugHeader(page); // Navigate to a drug page - await drugPage.goToDrugPage("CHEMBL1201585"); + await drugPage.goToDrugPage(testConfig.drug.primary); // Wait for header to load await drugHeader.waitForHeaderLoad(); diff --git a/packages/platform-test/e2e/pages/drug/drugIndications.spec.ts b/packages/platform-test/e2e/pages/drug/drugIndications.spec.ts index 736a5572a..73ccc9dce 100644 --- a/packages/platform-test/e2e/pages/drug/drugIndications.spec.ts +++ b/packages/platform-test/e2e/pages/drug/drugIndications.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from "@playwright/test"; +import { expect, test } from "../../../fixtures"; import { IndicationsSection } from "../../../POM/objects/widgets/shared/indicationsSection"; import { DrugPage } from "../../../POM/page/drug/drug"; @@ -6,12 +6,12 @@ test.describe("Drug Indications Section", () => { let drugPage: DrugPage; let indicationsSection: IndicationsSection; - test.beforeEach(async ({ page }) => { + test.beforeEach(async ({ page, testConfig }) => { drugPage = new DrugPage(page); indicationsSection = new IndicationsSection(page); // Navigate to a drug with indications data - await drugPage.goToDrugPage("CHEMBL1201585"); + await drugPage.goToDrugPage(testConfig.drug.primary); // Wait for the section to fully load await indicationsSection.waitForLoad(); @@ -52,8 +52,6 @@ test.describe("Drug Indications Section", () => { }); test("Can search/filter indications", async () => { - const initialRowCount = await indicationsSection.getTableRows(); - // Search for a specific term await indicationsSection.search("cancer"); diff --git a/packages/platform-test/e2e/pages/drug/drugMechanismsOfAction.spec.ts b/packages/platform-test/e2e/pages/drug/drugMechanismsOfAction.spec.ts index ae748c137..89fda096e 100644 --- a/packages/platform-test/e2e/pages/drug/drugMechanismsOfAction.spec.ts +++ b/packages/platform-test/e2e/pages/drug/drugMechanismsOfAction.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from "@playwright/test"; +import { expect, test } from "../../../fixtures"; import { MechanismsOfActionSection } from "../../../POM/objects/widgets/shared/mechanismsOfActionSection"; import { DrugPage } from "../../../POM/page/drug/drug"; @@ -6,12 +6,12 @@ test.describe("Drug Mechanisms of Action Section", () => { let drugPage: DrugPage; let moaSection: MechanismsOfActionSection; - test.beforeEach(async ({ page }) => { + test.beforeEach(async ({ page, testConfig }) => { drugPage = new DrugPage(page); moaSection = new MechanismsOfActionSection(page); // Navigate to a drug with mechanisms of action data - await drugPage.goToDrugPage("CHEMBL1201585"); + await drugPage.goToDrugPage(testConfig.drug.primary); // Wait for the section to fully load await moaSection.waitForLoad(); diff --git a/packages/platform-test/e2e/pages/drug/drugPharmacogenomics.spec.ts b/packages/platform-test/e2e/pages/drug/drugPharmacogenomics.spec.ts index 3564ca060..c64ba34cd 100644 --- a/packages/platform-test/e2e/pages/drug/drugPharmacogenomics.spec.ts +++ b/packages/platform-test/e2e/pages/drug/drugPharmacogenomics.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from "@playwright/test"; +import { expect, test } from "../../../fixtures"; import { PharmacogenomicsSection } from "../../../POM/objects/widgets/shared/pharmacogenomicsSection"; import { DrugPage } from "../../../POM/page/drug/drug"; @@ -6,12 +6,12 @@ test.describe("Drug Pharmacogenomics Section", () => { let drugPage: DrugPage; let pharmacoSection: PharmacogenomicsSection; - test.beforeEach(async ({ page }) => { + test.beforeEach(async ({ page, testConfig }) => { drugPage = new DrugPage(page); pharmacoSection = new PharmacogenomicsSection(page); // Navigate to a drug with pharmacogenomics data - await drugPage.goToDrugPage("CHEMBL1201585"); + await drugPage.goToDrugPage(testConfig.drug.primary); // Check if section is visible const isVisible = await pharmacoSection.isSectionVisible(); @@ -69,8 +69,6 @@ test.describe("Drug Pharmacogenomics Section", () => { }); test("Can search/filter pharmacogenomics data", async () => { - const initialRowCount = await pharmacoSection.getTableRows(); - // Search for a specific term await pharmacoSection.search("response"); diff --git a/packages/platform-test/e2e/pages/drug/drugPharmacovigilance.spec.ts b/packages/platform-test/e2e/pages/drug/drugPharmacovigilance.spec.ts index 854698103..3769fbec3 100644 --- a/packages/platform-test/e2e/pages/drug/drugPharmacovigilance.spec.ts +++ b/packages/platform-test/e2e/pages/drug/drugPharmacovigilance.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from "@playwright/test"; +import { expect, test } from "../../../fixtures"; import { PharmacovigilanceSection } from "../../../POM/objects/widgets/shared/adverseEventsSection"; import { DrugPage } from "../../../POM/page/drug/drug"; @@ -6,12 +6,11 @@ test.describe("Drug Pharmacovigilance Section", () => { let drugPage: DrugPage; let Pharmacovigilance: PharmacovigilanceSection; - test.beforeEach(async ({ page }) => { + test.beforeEach(async ({ page, testConfig }) => { drugPage = new DrugPage(page); Pharmacovigilance = new PharmacovigilanceSection(page); - - // Navigate to a drug with adverse events data - await drugPage.goToDrugPage("CHEMBL1201585"); + // Navigate to a drug with pharmacovigilance data + await drugPage.goToDrugPage(testConfig.drug.primary); // Wait for the section to fully load await Pharmacovigilance.waitForLoad(); diff --git a/packages/platform-test/e2e/pages/drug/drugWarnings.spec.ts b/packages/platform-test/e2e/pages/drug/drugWarnings.spec.ts index 01daeb41b..bb5046270 100644 --- a/packages/platform-test/e2e/pages/drug/drugWarnings.spec.ts +++ b/packages/platform-test/e2e/pages/drug/drugWarnings.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from "@playwright/test"; +import { expect, test } from "../../../fixtures"; import { DrugWarningsSection } from "../../../POM/objects/widgets/shared/drugWarningsSection"; import { DrugPage } from "../../../POM/page/drug/drug"; @@ -6,12 +6,12 @@ test.describe("Drug Warnings Section", () => { let drugPage: DrugPage; let warningsSection: DrugWarningsSection; - test.beforeEach(async ({ page }) => { + test.beforeEach(async ({ page, testConfig }) => { drugPage = new DrugPage(page); warningsSection = new DrugWarningsSection(page); // Navigate to a drug with warnings data - await drugPage.goToDrugPage("CHEMBL1201585"); + await drugPage.goToDrugPage(testConfig.drug.alternatives?.withWarnings ?? testConfig.drug.primary); // Check if section is visible const isVisible = await warningsSection.isSectionVisible(); diff --git a/packages/platform-test/e2e/pages/study/studyPageGWAS.spec.ts b/packages/platform-test/e2e/pages/study/studyPageGWAS.spec.ts index 27676607d..4d5b76cb6 100644 --- a/packages/platform-test/e2e/pages/study/studyPageGWAS.spec.ts +++ b/packages/platform-test/e2e/pages/study/studyPageGWAS.spec.ts @@ -1,17 +1,14 @@ -import { test } from "@playwright/test"; +import { test } from "../../../fixtures"; import { StudyProfileHeader } from "../../../POM/objects/components/StudyProfileHeader/studyProfileHeader"; import { SharedTraitStudiesSection } from "../../../POM/objects/widgets/Study/sharedTraitStudiesSection"; import { GWASCredibleSetsSection } from "../../../POM/objects/widgets/shared/GWASCredibleSetsSection"; import { StudyPage } from "../../../POM/page/study/study"; -// Test Disease study -const DISEASE_EFO_ID = "EFO_0000612"; - test.describe("Study Page - GWAS Study", () => { - test.beforeEach(async ({ page, baseURL }) => { + test.beforeEach(async ({ page, baseURL, testConfig }) => { const studyPage = new StudyPage(page); - // await studyPage.goToStudyPageFromGWASWidgetOnDiseasePage(DISEASE_EFO_ID); - await studyPage.goToStudyPage(baseURL!, "GCST90475211"); + // await studyPage.goToStudyPageFromGWASWidgetOnDiseasePage(testConfig.disease.primary); + await studyPage.goToStudyPage(baseURL!, testConfig.study.gwas.primary); await studyPage.waitForStudyPageLoad(); }); diff --git a/packages/platform-test/e2e/pages/study/studyPageQTL.spec.ts b/packages/platform-test/e2e/pages/study/studyPageQTL.spec.ts index f307cdc40..7635fe272 100644 --- a/packages/platform-test/e2e/pages/study/studyPageQTL.spec.ts +++ b/packages/platform-test/e2e/pages/study/studyPageQTL.spec.ts @@ -1,15 +1,12 @@ -import { test } from "@playwright/test"; +import { test } from "../../../fixtures"; import { StudyProfileHeader } from "../../../POM/objects/components/StudyProfileHeader/studyProfileHeader"; import { QTLCredibleSetsSection } from "../../../POM/objects/widgets/Study/qtlCredibleSetsSection"; import { StudyPage } from "../../../POM/page/study/study"; -// Test QTL study - eQTL -const QTL_STUDY_ID = "UKB_PPP_EUR_LPA_P08519_OID30747_v1"; - test.describe("Study Page - QTL Study", () => { - test.beforeEach(async ({ page, baseURL }) => { + test.beforeEach(async ({ page, baseURL, testConfig }) => { const studyPage = new StudyPage(page); - await studyPage.goToStudyPage(baseURL as string, QTL_STUDY_ID); + await studyPage.goToStudyPage(baseURL as string, testConfig.study.qtl!.primary!); await studyPage.waitForStudyPageLoad(); }); @@ -295,11 +292,11 @@ test.describe("Study Page - QTL Study", () => { test.expect(isActive).toBe(true); }); - test("Study page displays correct study ID in header", async ({ page }) => { + test("Study page displays correct study ID in header", async ({ page, testConfig }) => { const studyPage = new StudyPage(page); const studyId = await studyPage.getStudyIdFromHeader(); - test.expect(studyId).toContain(QTL_STUDY_ID); + test.expect(studyId).toContain(testConfig.study.qtl!.primary!); }); }); }); diff --git a/packages/platform-test/e2e/pages/variant/variantEVA.spec.ts b/packages/platform-test/e2e/pages/variant/variantEVA.spec.ts index 777bb654c..ed17271bb 100644 --- a/packages/platform-test/e2e/pages/variant/variantEVA.spec.ts +++ b/packages/platform-test/e2e/pages/variant/variantEVA.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from "@playwright/test"; +import { expect, test } from "../../../fixtures"; import { EVASection } from "../../../POM/objects/widgets/shared/evaSection"; import { VariantPage } from "../../../POM/page/variant/variant"; @@ -6,12 +6,12 @@ test.describe("EVA / ClinVar Section", () => { let variantPage: VariantPage; let evaSection: EVASection; - test.beforeEach(async ({ page }) => { + test.beforeEach(async ({ page, testConfig }) => { variantPage = new VariantPage(page); evaSection = new EVASection(page); // Navigate to a variant with ClinVar data - await variantPage.goToVariantPage("19_44908822_C_T"); + await variantPage.goToVariantPage(testConfig.variant.withEVA ?? testConfig.variant.primary); // Check if section is visible const isVisible = await evaSection.isSectionVisible(); diff --git a/packages/platform-test/e2e/pages/variant/variantEffect.spec.ts b/packages/platform-test/e2e/pages/variant/variantEffect.spec.ts index 22f0bd7eb..02919bebd 100644 --- a/packages/platform-test/e2e/pages/variant/variantEffect.spec.ts +++ b/packages/platform-test/e2e/pages/variant/variantEffect.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from "@playwright/test"; +import { expect, test } from "../../../fixtures"; import { VariantEffectSection } from "../../../POM/objects/widgets/Variant/variantEffectSection"; import { VariantPage } from "../../../POM/page/variant/variant"; @@ -6,12 +6,12 @@ test.describe("Variant Effect Section", () => { let variantPage: VariantPage; let variantEffectSection: VariantEffectSection; - test.beforeEach(async ({ page }) => { + test.beforeEach(async ({ page, testConfig }) => { variantPage = new VariantPage(page); variantEffectSection = new VariantEffectSection(page); // Navigate to a variant with variant effect data - await variantPage.goToVariantPage("1_154453788_C_T"); + await variantPage.goToVariantPage(testConfig.variant.primary); // Wait for the section to fully load await variantEffectSection.waitForLoad(); diff --git a/packages/platform-test/e2e/pages/variant/variantEffectPredictor.spec.ts b/packages/platform-test/e2e/pages/variant/variantEffectPredictor.spec.ts index 13e880921..c8d8e4f34 100644 --- a/packages/platform-test/e2e/pages/variant/variantEffectPredictor.spec.ts +++ b/packages/platform-test/e2e/pages/variant/variantEffectPredictor.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from "@playwright/test"; +import { expect, test } from "../../../fixtures"; import { VariantEffectPredictorSection } from "../../../POM/objects/widgets/shared/variantEffectPredictorSection"; import { VariantPage } from "../../../POM/page/variant/variant"; @@ -6,12 +6,12 @@ test.describe("Variant Effect Predictor / Transcript Consequences Section", () = let variantPage: VariantPage; let vepSection: VariantEffectPredictorSection; - test.beforeEach(async ({ page }) => { + test.beforeEach(async ({ page, testConfig }) => { variantPage = new VariantPage(page); vepSection = new VariantEffectPredictorSection(page); // Navigate to a variant with transcript consequence data - await variantPage.goToVariantPage("1_154453788_C_T"); + await variantPage.goToVariantPage(testConfig.variant.primary); // Wait for the section to fully load await vepSection.waitForLoad(); diff --git a/packages/platform-test/e2e/pages/variant/variantEnhancerToGenePredictions.spec.ts b/packages/platform-test/e2e/pages/variant/variantEnhancerToGenePredictions.spec.ts index f0f71bca0..cb3fcc74b 100644 --- a/packages/platform-test/e2e/pages/variant/variantEnhancerToGenePredictions.spec.ts +++ b/packages/platform-test/e2e/pages/variant/variantEnhancerToGenePredictions.spec.ts @@ -1,17 +1,17 @@ -import { expect, test } from "@playwright/test"; +import { expect, test } from "../../../fixtures"; import { EnhancerToGenePredictionsSection } from "../../../POM/objects/widgets/shared/enhancerToGenePredictionsSection"; import { VariantPage } from "../../../POM/page/variant/variant"; -test.describe("Enhancer-to-Gene Predictions Section", () => { +test.describe("Enhancer To Gene Predictions Section", () => { let variantPage: VariantPage; let e2gSection: EnhancerToGenePredictionsSection; - test.beforeEach(async ({ page }) => { + test.beforeEach(async ({ page, testConfig }) => { variantPage = new VariantPage(page); e2gSection = new EnhancerToGenePredictionsSection(page); - // Navigate to a variant with E2G predictions - await variantPage.goToVariantPage("19_44908822_C_T"); + // Navigate to a variant with E2G predictions data + await variantPage.goToVariantPage(testConfig.variant.withMolecularStructure); // Check if section is visible const isVisible = await e2gSection.isSectionVisible(); diff --git a/packages/platform-test/e2e/pages/variant/variantGWASCredibleSets.spec.ts b/packages/platform-test/e2e/pages/variant/variantGWASCredibleSets.spec.ts index 0955a2782..06cd9a346 100644 --- a/packages/platform-test/e2e/pages/variant/variantGWASCredibleSets.spec.ts +++ b/packages/platform-test/e2e/pages/variant/variantGWASCredibleSets.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from "@playwright/test"; +import { expect, test } from "../../../fixtures"; import { GWASCredibleSetsSection as VariantGWASCredibleSetsSection } from "../../../POM/objects/widgets/shared/GWASCredibleSetsSection"; import { VariantPage } from "../../../POM/page/variant/variant"; @@ -6,12 +6,12 @@ test.describe("Variant GWAS Credible Sets Section", () => { let variantPage: VariantPage; let gwasSection: VariantGWASCredibleSetsSection; - test.beforeEach(async ({ page }) => { + test.beforeEach(async ({ page, testConfig }) => { variantPage = new VariantPage(page); gwasSection = new VariantGWASCredibleSetsSection(page); // Navigate to a variant with GWAS credible sets data - await variantPage.goToVariantPage("1_154453788_C_T"); + await variantPage.goToVariantPage(testConfig.variant.primary); // Wait for the section to fully load await gwasSection.waitForLoad(); diff --git a/packages/platform-test/e2e/pages/variant/variantMolecularStructure.spec.ts b/packages/platform-test/e2e/pages/variant/variantMolecularStructure.spec.ts index 7bf28af06..47a7bf3d0 100644 --- a/packages/platform-test/e2e/pages/variant/variantMolecularStructure.spec.ts +++ b/packages/platform-test/e2e/pages/variant/variantMolecularStructure.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from "@playwright/test"; +import { test, expect } from "../../../fixtures"; import { MolecularStructureSection } from "../../../POM/objects/widgets/shared/molecularStructureSection"; import { VariantPage } from "../../../POM/page/variant/variant"; @@ -7,12 +7,12 @@ test.describe("Molecular Structure Section", () => { let variantPage: VariantPage; let structureSection: MolecularStructureSection; - test.beforeEach(async ({ page }) => { + test.beforeEach(async ({ page, testConfig }) => { variantPage = new VariantPage(page); structureSection = new MolecularStructureSection(page); // Navigate to a variant with molecular structure data - await variantPage.goToVariantPage("19_44908822_C_T"); + await variantPage.goToVariantPage(testConfig.variant.withMolecularStructure ?? testConfig.variant.primary); // Check if section is visible const isVisible = await structureSection.isSectionVisible(); diff --git a/packages/platform-test/e2e/pages/variant/variantPageNavigation.spec.ts b/packages/platform-test/e2e/pages/variant/variantPageNavigation.spec.ts index d52ae4205..61f4484f6 100644 --- a/packages/platform-test/e2e/pages/variant/variantPageNavigation.spec.ts +++ b/packages/platform-test/e2e/pages/variant/variantPageNavigation.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from "@playwright/test"; +import { expect, test } from "../../../fixtures"; import { VariantPage } from "../../../POM/page/variant/variant"; test.describe("Variant Page Navigation", () => { @@ -8,8 +8,8 @@ test.describe("Variant Page Navigation", () => { variantPage = new VariantPage(page); }); - test("Can navigate to variant page directly by variant ID", async ({ page }) => { - const variantId = "1_154453788_C_T"; + test("Can navigate to variant page directly by variant ID", async ({ page, testConfig }) => { + const variantId = testConfig.variant.primary; await variantPage.goToVariantPage(variantId); @@ -20,8 +20,8 @@ test.describe("Variant Page Navigation", () => { expect(page.url()).toContain(`/variant/${variantId}`); }); - test("Can get variant ID from page header", async () => { - const variantId = "1_154453788_C_T"; + test("Can get variant ID from page header", async ({ testConfig }) => { + const variantId = testConfig.variant.primary; await variantPage.goToVariantPage(variantId); @@ -32,8 +32,8 @@ test.describe("Variant Page Navigation", () => { expect(headerVariantId).toContain("154453788"); }); - test("Variant page loads successfully with profile header", async ({ page }) => { - const variantId = "1_154453788_C_T"; + test("Variant page loads successfully with profile header", async ({ page, testConfig }) => { + const variantId = testConfig.variant.primary; await variantPage.goToVariantPage(variantId); @@ -44,5 +44,4 @@ test.describe("Variant Page Navigation", () => { const profileHeaderBlock = page.locator("[data-testid='profile-page-header-block']"); await expect(profileHeaderBlock).toBeVisible(); }); - }); diff --git a/packages/platform-test/e2e/pages/variant/variantPharmacogenomics.spec.ts b/packages/platform-test/e2e/pages/variant/variantPharmacogenomics.spec.ts index 09742fbf0..bfd027247 100644 --- a/packages/platform-test/e2e/pages/variant/variantPharmacogenomics.spec.ts +++ b/packages/platform-test/e2e/pages/variant/variantPharmacogenomics.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from "@playwright/test"; +import { expect, test } from "../../../fixtures"; import { PharmacogenomicsSection } from "../../../POM/objects/widgets/shared/pharmacogenomicsSection"; import { VariantPage } from "../../../POM/page/variant/variant"; @@ -6,14 +6,14 @@ test.describe("Pharmacogenomics Section", () => { let variantPage: VariantPage; let pharmacoSection: PharmacogenomicsSection; - test.beforeEach(async ({ page }) => { + test.beforeEach(async ({ page, testConfig }) => { variantPage = new VariantPage(page); pharmacoSection = new PharmacogenomicsSection(page); // Navigate to a variant with pharmacogenomics data // Using rs662 (PON1 gene) which should have pharmaco data - await variantPage.goToVariantPage("7_95308134_T_C"); - + await variantPage.goToVariantPage(testConfig.variant.withPharmacogenomics ?? testConfig.variant.primary); + // Wait for the section to load if it's visible const isVisible = await pharmacoSection.isSectionVisible(); if (isVisible) { @@ -69,9 +69,9 @@ test.describe("Pharmacogenomics Section", () => { await pharmacoSection.clickDrugLink(0); // Wait for navigation to drug page - await page.waitForURL(url => url.toString().includes("/drug/"), { timeout: 5000 }); - } -}); + await page.waitForURL((url) => url.toString().includes("/drug/"), { timeout: 5000 }); + } + }); test("Gene/Target link is displayed in table", async () => { const isVisible = await pharmacoSection.isSectionVisible(); @@ -98,8 +98,7 @@ test.describe("Pharmacogenomics Section", () => { await pharmacoSection.clickGeneLink(0); // Wait for navigation to target page - await page.waitForURL(url => url.toString().includes("/target/"), { timeout: 5000 }); - + await page.waitForURL((url) => url.toString().includes("/target/"), { timeout: 5000 }); } } }); @@ -161,5 +160,4 @@ test.describe("Pharmacogenomics Section", () => { test.skip(); } }); - -}); \ No newline at end of file +}); diff --git a/packages/platform-test/e2e/pages/variant/variantProfileHeader.spec.ts b/packages/platform-test/e2e/pages/variant/variantProfileHeader.spec.ts index 0817c5e5a..8e93a0a7f 100644 --- a/packages/platform-test/e2e/pages/variant/variantProfileHeader.spec.ts +++ b/packages/platform-test/e2e/pages/variant/variantProfileHeader.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from "@playwright/test"; +import { expect, test } from "../../../fixtures"; import { VariantProfileHeader } from "../../../POM/objects/components/VariantProfileHeader/variantProfileHeader"; import { VariantPage } from "../../../POM/page/variant/variant"; @@ -6,12 +6,12 @@ test.describe("Variant Profile Header", () => { let variantPage: VariantPage; let variantProfileHeader: VariantProfileHeader; - test.beforeEach(async ({ page }) => { + test.beforeEach(async ({ page, testConfig }) => { variantPage = new VariantPage(page); variantProfileHeader = new VariantProfileHeader(page); // Navigate to a variant page with known data - await variantPage.goToVariantPage("1_154453788_C_T"); + await variantPage.goToVariantPage(testConfig.variant.primary); }); test("Profile header is visible", async () => { diff --git a/packages/platform-test/e2e/pages/variant/variantQTLCredibleSets.spec.ts b/packages/platform-test/e2e/pages/variant/variantQTLCredibleSets.spec.ts index c271d6f57..15946d1fd 100644 --- a/packages/platform-test/e2e/pages/variant/variantQTLCredibleSets.spec.ts +++ b/packages/platform-test/e2e/pages/variant/variantQTLCredibleSets.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from "@playwright/test"; +import { expect, test } from "../../../fixtures"; import { QTLCredibleSetsSection } from "../../../POM/objects/widgets/shared/qtlCredibleSetsSection"; import { VariantPage } from "../../../POM/page/variant/variant"; @@ -6,12 +6,12 @@ test.describe("Variant QTL Credible Sets Section", () => { let variantPage: VariantPage; let qtlSection: QTLCredibleSetsSection; - test.beforeEach(async ({ page }) => { + test.beforeEach(async ({ page, testConfig }) => { variantPage = new VariantPage(page); qtlSection = new QTLCredibleSetsSection(page); // Navigate to a variant with QTL credible sets data - await variantPage.goToVariantPage("1_154453788_C_T"); + await variantPage.goToVariantPage(testConfig.variant.withQTL ?? testConfig.variant.primary); // Check if section is visible const isVisible = await qtlSection.isSectionVisible(); diff --git a/packages/platform-test/e2e/pages/variant/variantUniProtVariants.spec.ts b/packages/platform-test/e2e/pages/variant/variantUniProtVariants.spec.ts index caa6cc2ce..f2bdeb313 100644 --- a/packages/platform-test/e2e/pages/variant/variantUniProtVariants.spec.ts +++ b/packages/platform-test/e2e/pages/variant/variantUniProtVariants.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from "@playwright/test"; +import { expect, test } from "../../../fixtures"; import { UniProtVariantsSection } from "../../../POM/objects/widgets/shared/uniprotVariantsSection"; import { VariantPage } from "../../../POM/page/variant/variant"; @@ -6,12 +6,12 @@ test.describe("UniProt Variants Section", () => { let variantPage: VariantPage; let uniprotSection: UniProtVariantsSection; - test.beforeEach(async ({ page }) => { + test.beforeEach(async ({ page, testConfig }) => { variantPage = new VariantPage(page); uniprotSection = new UniProtVariantsSection(page); // Navigate to a variant with UniProt data - await variantPage.goToVariantPage("19_44908822_C_T"); + await variantPage.goToVariantPage(testConfig.variant.withMolecularStructure); // Check if section is visible const isVisible = await uniprotSection.isSectionVisible(); @@ -35,18 +35,18 @@ test.describe("UniProt Variants Section", () => { test("Target gene link is displayed in table", async () => { const geneLink = await uniprotSection.getTargetGeneLink(0); - expect(await geneLink.isVisible()).toBe(true); }); - test("Can click target gene link in table", async ({ page }) => { + test("Can click target gene button in table", async ({ page }) => { await uniprotSection.clickTargetGeneLink(0); - // Wait for navigation to target page - await page.waitForURL((url) => url.toString().includes("/target/"), { timeout: 5000 }); + // Wait for drawer to open (button opens a drawer instead of navigating) + const drawer = page.locator("[data-testid='publications-drawer']"); + await drawer.waitFor({ state: "visible", timeout: 5000 }); - // Should navigate to target/gene page - expect(page.url()).toContain("/target/"); + // Drawer should be visible + expect(await drawer.isVisible()).toBe(true); }); test("Disease links are displayed in table", async () => { @@ -70,8 +70,6 @@ test.describe("UniProt Variants Section", () => { }); test("Can search/filter UniProt variants", async () => { - const initialRowCount = await uniprotSection.getTableRows(); - // Search for a specific term await uniprotSection.search("disease"); diff --git a/packages/platform-test/fixtures/index.ts b/packages/platform-test/fixtures/index.ts new file mode 100644 index 000000000..04d6fae47 --- /dev/null +++ b/packages/platform-test/fixtures/index.ts @@ -0,0 +1,23 @@ +import { test as base } from "@playwright/test"; +import { getTestConfig, type TestConfig } from "./testConfig"; + +/** + * Extended test fixtures with test configuration + */ +type TestFixtures = { + testConfig: TestConfig; +}; + +/** + * Extend Playwright test with custom fixtures + */ +export const test = base.extend({ + // biome-ignore lint/correctness/noEmptyPattern: + testConfig: async ({}, use) => { + // Fetch configuration once per test + const config = await getTestConfig(); + await use(config); + }, +}); + +export { expect } from "@playwright/test"; diff --git a/packages/platform-test/fixtures/testConfig.ts b/packages/platform-test/fixtures/testConfig.ts new file mode 100644 index 000000000..1375a8ab7 --- /dev/null +++ b/packages/platform-test/fixtures/testConfig.ts @@ -0,0 +1,116 @@ +/** + * Test configuration interface + * Defines the structure of test data used across E2E tests + */ +export interface TestConfig { + drug: { + /** Drug with comprehensive data across all sections */ + primary: string; + alternatives?: { + withWarnings: string; + withAdverseEvents: string; + }; + }; + variant: { + /** Variant with GWAS and general data */ + primary: string; + /** Variant with molecular structure data */ + withMolecularStructure: string; + /** Variant with pharmacogenomics data */ + withPharmacogenomics: string; + /** Variant with QTL data */ + withQTL?: string; + /** Variant with EVA/ClinVar data */ + withEVA?: string; + }; + target?: { + primary?: string; + alternatives?: string[]; + }; + disease: { + primary: string; + name?: string; + alternatives?: string[]; + aotfGenes?: string[]; + }; + study: { + gwas: { + primary: string; + alternatives?: string[]; + }; + qtl?: { + primary?: string; + alternatives?: string[]; + }; + }; +} + +/** + * Mock function to simulate fetching config from external source + * In real implementation, this would make an API call to retrieve test data + */ +async function fetchTestConfig(): Promise { + // Simulate network delay + await new Promise((resolve) => setTimeout(resolve, 100)); + + // Return mock configuration + return { + drug: { + primary: "CHEMBL1201585", // TRASTUZUMAB - has comprehensive data + alternatives: { + withWarnings: "CHEMBL1201585", + withAdverseEvents: "CHEMBL1201585", + }, + }, + variant: { + primary: "1_154453788_C_T", // Intron variant overlapping with IL6R. + withMolecularStructure: "19_44908822_C_T", // Missense variant overlapping with APOE, causing amino-acid change: R176C with moderate impact. + withPharmacogenomics: "7_95308134_T_C", // PON1 variant + withQTL: "1_154453788_C_T", + withEVA: "19_44908822_C_T", + }, + target: { + primary: "ENSG00000157764", // BRAF + alternatives: ["ENSG00000139618"], // BRCA2 + }, + disease: { + primary: "EFO_0000612", // Myocardial infarction + name: "myocardial infarction", + alternatives: ["EFO_0000305", "MONDO_0007254"], // Breast carcinoma, Alzheimer disease + aotfGenes: ["IL6", "ADRB1", "APOE"], // Genes with evidence for testing AOTF table + }, + study: { + gwas: { + primary: "GCST90475211", // Example credible set study + alternatives: [], + }, + qtl: { + primary: "UKB_PPP_EUR_LPA_P08519_OID30747_v1", // eQTL study + alternatives: [], + }, + }, + }; +} + +/** + * Cached test configuration + */ +let cachedConfig: TestConfig | null = null; + +/** + * Get test configuration (with caching) + * @returns Test configuration object + */ +export async function getTestConfig(): Promise { + if (!cachedConfig) { + cachedConfig = await fetchTestConfig(); + } + return cachedConfig; +} + +/** + * Reset cached configuration (useful for testing) + */ +export function resetTestConfig(): void { + cachedConfig = null; +} diff --git a/packages/platform-test/playwright-report/index.html b/packages/platform-test/playwright-report/index.html index bee703ef3..307b5a993 100644 --- a/packages/platform-test/playwright-report/index.html +++ b/packages/platform-test/playwright-report/index.html @@ -73,4 +73,4 @@
    - \ No newline at end of file + \ No newline at end of file diff --git a/packages/ui/src/components/PublicationsDrawer/PublicationsDrawer.jsx b/packages/ui/src/components/PublicationsDrawer/PublicationsDrawer.jsx index c9984c167..dab835ee3 100644 --- a/packages/ui/src/components/PublicationsDrawer/PublicationsDrawer.jsx +++ b/packages/ui/src/components/PublicationsDrawer/PublicationsDrawer.jsx @@ -243,6 +243,7 @@ function PublicationsDrawer({ classes={{ modal: classes.drawerModal, paper: classes.drawerPaper }} open={open} onClose={closeDrawer} + data-testid="publications-drawer" > From 8565ba4b4008344e393cb79dac32437218e90fc5 Mon Sep 17 00:00:00 2001 From: david oluwasusi Date: Mon, 5 Jan 2026 08:14:49 +0000 Subject: [PATCH 10/34] feat(e2e-annotation): annotate smoke test to reduce CI time --- .github/workflows/e2e-ci.yml | 23 ++- package.json | 2 + .../disease/associatedTargetsHeader.spec.ts | 2 +- .../disease/associatedTargetsTable.spec.ts | 2 +- .../pages/drug/drugMechanismsOfAction.spec.ts | 2 +- .../platform-test/e2e/pages/homepage.spec.ts | 2 +- .../e2e/pages/study/studyPageGWAS.spec.ts | 2 +- .../variant/variantProfileHeader.spec.ts | 2 +- packages/platform-test/package.json | 4 +- .../playwright-report/index.html | 2 +- packages/platform-test/readme.md | 178 +++++++++++++++++- turbo.json | 6 + 12 files changed, 208 insertions(+), 19 deletions(-) diff --git a/.github/workflows/e2e-ci.yml b/.github/workflows/e2e-ci.yml index 45be472fc..462507a1b 100644 --- a/.github/workflows/e2e-ci.yml +++ b/.github/workflows/e2e-ci.yml @@ -3,6 +3,15 @@ name: "Tests: E2E" on: pull_request: workflow_dispatch: + inputs: + test_scope: + description: 'Test scope to run' + required: true + default: 'smoke' + type: choice + options: + - smoke + - all jobs: get_branch_name: @@ -49,9 +58,17 @@ jobs: - name: install playwright browsers run: | cd packages/platform-test - yarn playwright install --with-deps chromium webkit - - name: yarn dev:test:platform:e2e - run: yarn dev:test:platform:e2e + yarn playwright install --with-deps chromium + - name: Run E2E tests + run: | + if [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ github.event.inputs.test_scope }}" == "all" ]]; then + yarn dev:test:platform:e2e + else + yarn dev:test:platform:e2e:smoke + fi + env: + PLAYWRIGHT_TEST_BASE_URL: "https://${{ needs.get_branch_name.outputs.branch }}--ot-platform.netlify.app" + DEBUG: pw:api - uses: actions/upload-artifact@v4 if: ${{ !cancelled() }} with: diff --git a/package.json b/package.json index f6598cb9c..9a51bc3c0 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,9 @@ "format:fix": "npx biome format --write .", "format:check": "npx biome ci", "dev:test:platform:e2e": "turbo run dev-test", + "dev:test:platform:e2e:smoke": "turbo run dev-test:smoke", "test:platform:e2e": "turbo run test:platform:e2e", + "test:platform:e2e:smoke": "turbo run test:platform:e2e:smoke", "check": "npx biome check --formatter-enabled=true .", "check:fix": "npx biome check --formatter-enabled=true --write .", "prepare": "husky install", diff --git a/packages/platform-test/e2e/pages/disease/associatedTargetsHeader.spec.ts b/packages/platform-test/e2e/pages/disease/associatedTargetsHeader.spec.ts index 92c53b6fa..fe0d91eae 100644 --- a/packages/platform-test/e2e/pages/disease/associatedTargetsHeader.spec.ts +++ b/packages/platform-test/e2e/pages/disease/associatedTargetsHeader.spec.ts @@ -6,7 +6,7 @@ test.describe("Disease Page - Header and Navigation", () => { await page.goto(`${baseURL}/disease/${testConfig.disease.primary}/associations`); }); - test("Disease header is correctly displayed", async ({ page, testConfig }) => { + test("Disease header is correctly displayed", { tag: "@smoke" }, async ({ page, testConfig }) => { const diseaseName = page.getByTestId("profile-page-header-text"); await test.expect(diseaseName).toHaveText(testConfig.disease.name || ""); }); diff --git a/packages/platform-test/e2e/pages/disease/associatedTargetsTable.spec.ts b/packages/platform-test/e2e/pages/disease/associatedTargetsTable.spec.ts index af36ccd81..7251b3430 100644 --- a/packages/platform-test/e2e/pages/disease/associatedTargetsTable.spec.ts +++ b/packages/platform-test/e2e/pages/disease/associatedTargetsTable.spec.ts @@ -7,7 +7,7 @@ test.describe("Disease Page - AOTF Table", () => { await page.goto(`${baseURL}/disease/${testConfig.disease.primary}/associations`); }); - test("targets are displayed in the associations table", async ({ page }) => { + test("targets are displayed in the associations table", { tag: "@smoke" }, async ({ page }) => { const aotfTable = new AotfTable(page); // Wait for table to load diff --git a/packages/platform-test/e2e/pages/drug/drugMechanismsOfAction.spec.ts b/packages/platform-test/e2e/pages/drug/drugMechanismsOfAction.spec.ts index 89fda096e..867f3c8e6 100644 --- a/packages/platform-test/e2e/pages/drug/drugMechanismsOfAction.spec.ts +++ b/packages/platform-test/e2e/pages/drug/drugMechanismsOfAction.spec.ts @@ -17,7 +17,7 @@ test.describe("Drug Mechanisms of Action Section", () => { await moaSection.waitForLoad(); }); - test("Mechanisms of Action section is visible", async () => { + test("Mechanisms of Action section is visible", { tag: "@smoke" }, async () => { expect(await moaSection.isSectionVisible()).toBe(true); }); diff --git a/packages/platform-test/e2e/pages/homepage.spec.ts b/packages/platform-test/e2e/pages/homepage.spec.ts index 23b1bb2ec..4506812da 100644 --- a/packages/platform-test/e2e/pages/homepage.spec.ts +++ b/packages/platform-test/e2e/pages/homepage.spec.ts @@ -2,7 +2,7 @@ import { expect, type Locator, test } from "@playwright/test"; import { fillPolling } from "../../utils/fillPolling"; test.describe("Home page actions", () => { - test("Validate page title", async ({ page, baseURL }) => { + test("Validate page title", { tag: "@smoke" }, async ({ page, baseURL }) => { await page.goto(baseURL!); const title = await page.title(); await expect(title).toBe("Open Targets Platform"); diff --git a/packages/platform-test/e2e/pages/study/studyPageGWAS.spec.ts b/packages/platform-test/e2e/pages/study/studyPageGWAS.spec.ts index 4d5b76cb6..54cef0e87 100644 --- a/packages/platform-test/e2e/pages/study/studyPageGWAS.spec.ts +++ b/packages/platform-test/e2e/pages/study/studyPageGWAS.spec.ts @@ -13,7 +13,7 @@ test.describe("Study Page - GWAS Study", () => { }); test.describe("GWAS Study Profile Header", () => { - test("Profile header is visible and displays study information", async ({ page }) => { + test("Profile header is visible and displays study information", { tag: '@smoke' }, async ({ page }) => { const profileHeader = new StudyProfileHeader(page); await profileHeader.waitForProfileHeaderLoad(); diff --git a/packages/platform-test/e2e/pages/variant/variantProfileHeader.spec.ts b/packages/platform-test/e2e/pages/variant/variantProfileHeader.spec.ts index 8e93a0a7f..ada1eec7b 100644 --- a/packages/platform-test/e2e/pages/variant/variantProfileHeader.spec.ts +++ b/packages/platform-test/e2e/pages/variant/variantProfileHeader.spec.ts @@ -14,7 +14,7 @@ test.describe("Variant Profile Header", () => { await variantPage.goToVariantPage(testConfig.variant.primary); }); - test("Profile header is visible", async () => { + test("Profile header is visible", { tag: "@smoke" }, async () => { expect(await variantProfileHeader.isProfileHeaderVisible()).toBe(true); }); diff --git a/packages/platform-test/package.json b/packages/platform-test/package.json index aa56d8174..6ef527469 100644 --- a/packages/platform-test/package.json +++ b/packages/platform-test/package.json @@ -12,7 +12,9 @@ }, "scripts": { "dev-test": "playwright test", - "test:platform:e2e": "start-server-and-test \"(cd ../../apps/platform && yarn run serve)\" http://localhost:4173 \"playwright test\"" + "dev-test:smoke": "playwright test --grep @smoke", + "test:platform:e2e": "start-server-and-test \"(cd ../../apps/platform && yarn run serve)\" http://localhost:4173 \"playwright test\"", + "test:platform:e2e:smoke": "start-server-and-test \"(cd ../../apps/platform && yarn run serve)\" http://localhost:4173 \"playwright test --grep @smoke\"" }, "keywords": [], "author": "", diff --git a/packages/platform-test/playwright-report/index.html b/packages/platform-test/playwright-report/index.html index 307b5a993..a6d9be4e7 100644 --- a/packages/platform-test/playwright-report/index.html +++ b/packages/platform-test/playwright-report/index.html @@ -73,4 +73,4 @@
    - \ No newline at end of file + \ No newline at end of file diff --git a/packages/platform-test/readme.md b/packages/platform-test/readme.md index 61c569bf1..6976d8757 100644 --- a/packages/platform-test/readme.md +++ b/packages/platform-test/readme.md @@ -4,6 +4,167 @@ The goal of this package is to setup automated E2E tests to check the platform w ## Technologies used Playwright +## Folder Structure + +``` +packages/platform-test/ +├── e2e/ # Test files organized by page +│ └── pages/ +│ ├── disease/ # Disease page tests +│ ├── drug/ # Drug page tests +│ ├── study/ # Study page tests +│ ├── variant/ # Variant page tests +│ └── homepage.spec.ts # Homepage tests +├── fixtures/ # Test configuration and fixtures +│ ├── index.ts # Playwright fixture extensions +│ └── testConfig.ts # Centralized test data configuration +├── POM/ # Page Object Model implementation +│ ├── objects/ # Reusable UI component interactors +│ │ ├── components/ # Shared components (headers, sections) +│ │ └── widgets/ # Feature-specific widgets (AOTF, evidence sections) +│ └── page/ # Page-level abstractions +│ ├── disease/ +│ ├── drug/ +│ ├── study/ +│ └── variant/ +└── playwright.config.ts # Playwright configuration +``` + +## Page Object Model + +This project uses the **Page Object Model (POM)** pattern to create maintainable and reusable test code. Our POM implementation is organized into three layers: + +### 1. **Page Objects** (`POM/page/`) +High-level page abstractions that represent entire pages and provide navigation methods. + +```typescript +// Example: VariantPage +class VariantPage { + goToVariantPage(variantId: string) { ... } + getProfilePage() { ... } +} +``` + +### 2. **Component Objects** (`POM/objects/components/`) +Reusable UI components that appear across multiple pages (headers, evidence sections, etc.). + +```typescript +// Example: EvidenceSection +class EvidenceSection { + waitForSectionLoad(sectionId: string) { ... } + isEvidenceSectionVisible(sectionId: string) { ... } +} +``` + +### 3. **Widget Objects** (`POM/objects/widgets/`) +Feature-specific, interactive components with complex behavior (tables, forms, drawers). + +```typescript +// Example: AotfTable +class AotfTable { + getDataCellsWithScores(rowIndex: number) { ... } + clickDataCell(rowIndex: number, columnId: string) { ... } +} +``` + +### Best Practices + +- **Prefer `data-testid` attributes** over generic selectors (classes, XPath) for stability and readability + ```typescript + // ✅ Good - uses data-testid + page.locator("[data-testid='evidence-section-gwas']") + + // ❌ Avoid - fragile CSS selectors + page.locator(".MuiBox-root .css-8b1dmf") + ``` + +- Keep selectors in Page Objects, not in test files +- Return Locators or primitives, avoid complex logic in Page Objects +- One file per component/page for clarity + +For more details on POM, see the [References](#references) section. + +## Testing Different Behaviours with Fixtures + +We use **Playwright fixtures** to manage test data and make tests flexible and maintainable. All test entity IDs are centralized in `fixtures/testConfig.ts`. + +### Configuration Structure + +```typescript +interface TestConfig { + drug: { + primary: string; // Drug with comprehensive data + }; + variant: { + primary: string; // Variant with GWAS data + withMolecularStructure: string; + withPharmacogenomics: string; + }; + disease: { + primary: string; + name?: string; + aotfGenes?: string[]; // Genes for AOTF table testing + }; + study: { + gwas: { primary: string }; + qtl?: { primary: string }; + }; +} +``` + +### Using Fixtures in Tests + +```typescript +test("Disease header is correctly displayed", async ({ page, testConfig }) => { + // Use testConfig instead of hardcoded IDs + await page.goto(`/disease/${testConfig.disease.primary}`); + + const diseaseName = page.getByTestId("profile-page-header-text"); + await expect(diseaseName).toHaveText(testConfig.disease.name); +}); +``` + +### Benefits + +- **Single source of truth** for test data +- **Easy to update** entity IDs without touching test files +- **Test different scenarios** by swapping configurations +- **Supports future external config** (API, environment variables) + +## Annotating Critical Tests + +We use Playwright's tagging system to identify **smoke tests** - critical tests that must pass before running the full suite. + +### Marking Smoke Tests + +Add `{ tag: '@smoke' }` to critical tests that validate core functionality: + +```typescript +test("Homepage loads successfully", { tag: '@smoke' }, async ({ page }) => { + await page.goto("/"); + const title = await page.title(); + expect(title).toBe("Open Targets Platform"); +}); +``` + +### Running Tests + +```bash +# Run all tests +yarn dev:test:platform:e2e + +# Run smoke tests only (fast feedback) +yarn dev:test:platform:e2e:smoke + +# Run specific test file +npx playwright test e2e/pages/homepage.spec.ts +``` + +### CI Behavior + +- **Pull Requests**: Automatically run smoke tests only (faster CI) +- **Manual Trigger**: Choose between `smoke` or `all` tests in GitHub Actions + ## Testing Pattern - User flow based testing - [Page Object Model](https://testomat.io/blog/page-object-model-pattern-javascript-with-playwright/) @@ -47,11 +208,12 @@ gantt Run E2E Tests :active, after test, 25 ``` -## References. -- https://www.joranquinten.nl/tutorials/ -- [running-e2e-test-suite-on-a-netlify-preview-url](https://www.joranquinten.nl/tutorials/running-e2e-test-suite-on-a-netlify-preview-url) -- https://snyk.io/es/blog/how-to-add-playwright-tests-pr-ci-github-actions/ -- https://dev.to/thiernope/trigger-netlify-deploys-using-github-workflow-cicd-lcm -- https://github.com/nwtgck/actions-netlify/issues/1220 -- https://testomat.io/blog/grouping-playwright-tests-for-improved-framework-efficiency/ -- https://playwright.dev/docs/test-annotations For writing conditional tests \ No newline at end of file +## References +- [Page Object Model Pattern with Playwright](https://testomat.io/blog/page-object-model-pattern-javascript-with-playwright/) - Comprehensive guide on implementing POM in Playwright +- [Joran Quinten Tutorials](https://www.joranquinten.nl/tutorials/) - Playwright and testing tutorials +- [Running E2E Tests on Netlify Preview URL](https://www.joranquinten.nl/tutorials/running-e2e-test-suite-on-a-netlify-preview-url) - Guide for integrating Playwright with Netlify deployments +- [Adding Playwright Tests to GitHub Actions CI](https://snyk.io/es/blog/how-to-add-playwright-tests-pr-ci-github-actions/) - CI/CD integration tutorial +- [Triggering Netlify Deploys with GitHub Workflows](https://dev.to/thiernope/trigger-netlify-deploys-using-github-workflow-cicd-lcm) - Netlify automation guide +- [Actions Netlify Issue #1220](https://github.com/nwtgck/actions-netlify/issues/1220) - Discussion on Netlify GitHub Actions integration +- [Grouping Playwright Tests](https://testomat.io/blog/grouping-playwright-tests-for-improved-framework-efficiency/) - Test organization strategies +- [Playwright Test Annotations](https://playwright.dev/docs/test-annotations) - Official documentation for conditional tests and tagging \ No newline at end of file diff --git a/turbo.json b/turbo.json index cd7adc36f..d816c54b9 100644 --- a/turbo.json +++ b/turbo.json @@ -11,9 +11,15 @@ "dev-test": { "cache": false }, + "dev-test:smoke": { + "cache": false + }, "test:platform:e2e": { "dependsOn": ["^build"] }, + "test:platform:e2e:smoke": { + "dependsOn": ["^build"] + }, "prettier:all": { "outputs": [] }, From 261a5e52d8e49b3d7532f89aab207d561822f958 Mon Sep 17 00:00:00 2001 From: david oluwasusi Date: Tue, 6 Jan 2026 15:13:15 +0000 Subject: [PATCH 11/34] chore(biome): lint fixes --- packages/platform-test/POM/page/study/study.ts | 5 ++--- .../platform-test/e2e/pages/study/studyPageGWAS.spec.ts | 9 ++------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/packages/platform-test/POM/page/study/study.ts b/packages/platform-test/POM/page/study/study.ts index 21f21d493..0d8246aeb 100644 --- a/packages/platform-test/POM/page/study/study.ts +++ b/packages/platform-test/POM/page/study/study.ts @@ -1,11 +1,11 @@ import type { Locator, Page } from "@playwright/test"; -import { GWASStudiesSection } from "../../objects/widgets/GWAS/gwasStudiesSection" +import { GWASStudiesSection } from "../../objects/widgets/GWAS/gwasStudiesSection"; export class StudyPage { page: Page; originalURL: string; STUDY_BASE_URL = "/study/"; - CHOSEN_STUDY_ID = "" + CHOSEN_STUDY_ID = ""; constructor(page: Page) { this.page = page; @@ -42,7 +42,6 @@ export class StudyPage { await this.goToStudyPageFromEvidence(trimmedStudyId); } }); - } // Tab navigation diff --git a/packages/platform-test/e2e/pages/study/studyPageGWAS.spec.ts b/packages/platform-test/e2e/pages/study/studyPageGWAS.spec.ts index 8d27588f3..b0da43e57 100644 --- a/packages/platform-test/e2e/pages/study/studyPageGWAS.spec.ts +++ b/packages/platform-test/e2e/pages/study/studyPageGWAS.spec.ts @@ -4,14 +4,11 @@ import { GWASCredibleSetsSection } from "../../../POM/objects/widgets/Study/gwas import { SharedTraitStudiesSection } from "../../../POM/objects/widgets/Study/sharedTraitStudiesSection"; import { StudyPage } from "../../../POM/page/study/study"; -// Test Disease study -const DISEASE_EFO_ID = "EFO_0000612"; - test.describe("Study Page - GWAS Study", () => { - test.beforeEach(async ({ page, baseURL }) => { + test.beforeEach(async ({ page, baseURL = "" }) => { const studyPage = new StudyPage(page); // await studyPage.goToStudyPageFromGWASWidgetOnDiseasePage(DISEASE_EFO_ID); - await studyPage.goToStudyPage(baseURL!, "GCST90475211"); + await studyPage.goToStudyPage(baseURL, "GCST90475211"); await studyPage.waitForStudyPageLoad(); }); @@ -414,8 +411,6 @@ test.describe("Study Page - GWAS Study", () => { const rowCount = await sharedTraitStudies.getRowCount(); if (rowCount > 0) { - const studyId = await sharedTraitStudies.getStudyId(0); - // Click study link await sharedTraitStudies.clickStudyLink(0); await page.waitForURL("**/study/**"); From de546930a168bacc8eb4b4f4892ca1535c02bb2f Mon Sep 17 00:00:00 2001 From: david oluwasusi Date: Tue, 6 Jan 2026 15:18:31 +0000 Subject: [PATCH 12/34] chore(biome): lint fixes --- .../shared/enhancerToGenePredictionsSection.ts | 4 +++- .../pages/variant/variantGWASCredibleSets.spec.ts | 1 - .../pages/variant/variantPharmacogenomics.spec.ts | 14 ++++++-------- .../pages/variant/variantQTLCredibleSets.spec.ts | 1 - .../pages/variant/variantUniProtVariants.spec.ts | 1 - 5 files changed, 9 insertions(+), 12 deletions(-) diff --git a/packages/platform-test/POM/objects/widgets/shared/enhancerToGenePredictionsSection.ts b/packages/platform-test/POM/objects/widgets/shared/enhancerToGenePredictionsSection.ts index 888319116..c784c5cd3 100644 --- a/packages/platform-test/POM/objects/widgets/shared/enhancerToGenePredictionsSection.ts +++ b/packages/platform-test/POM/objects/widgets/shared/enhancerToGenePredictionsSection.ts @@ -28,7 +28,9 @@ export class EnhancerToGenePredictionsSection { await this.page .waitForFunction( () => { - const sect = document.querySelector("[data-testid='section-enhancer-to-gene-predictions']"); + const sect = document.querySelector( + "[data-testid='section-enhancer-to-gene-predictions']" + ); if (!sect) return false; const skeletons = sect.querySelectorAll(".MuiSkeleton-root"); return skeletons.length === 0; diff --git a/packages/platform-test/e2e/pages/variant/variantGWASCredibleSets.spec.ts b/packages/platform-test/e2e/pages/variant/variantGWASCredibleSets.spec.ts index 0955a2782..fd247cca8 100644 --- a/packages/platform-test/e2e/pages/variant/variantGWASCredibleSets.spec.ts +++ b/packages/platform-test/e2e/pages/variant/variantGWASCredibleSets.spec.ts @@ -45,7 +45,6 @@ test.describe("Variant GWAS Credible Sets Section", () => { }); test("Lead variant column displays correctly", async () => { - const hasLeadVariant = await gwasSection.hasLeadVariantLink(0); // Lead variant might be the current variant (no link) or a different variant (with link) // Either way, the cell should have content diff --git a/packages/platform-test/e2e/pages/variant/variantPharmacogenomics.spec.ts b/packages/platform-test/e2e/pages/variant/variantPharmacogenomics.spec.ts index 09742fbf0..e3ed4e570 100644 --- a/packages/platform-test/e2e/pages/variant/variantPharmacogenomics.spec.ts +++ b/packages/platform-test/e2e/pages/variant/variantPharmacogenomics.spec.ts @@ -13,7 +13,7 @@ test.describe("Pharmacogenomics Section", () => { // Navigate to a variant with pharmacogenomics data // Using rs662 (PON1 gene) which should have pharmaco data await variantPage.goToVariantPage("7_95308134_T_C"); - + // Wait for the section to load if it's visible const isVisible = await pharmacoSection.isSectionVisible(); if (isVisible) { @@ -69,9 +69,9 @@ test.describe("Pharmacogenomics Section", () => { await pharmacoSection.clickDrugLink(0); // Wait for navigation to drug page - await page.waitForURL(url => url.toString().includes("/drug/"), { timeout: 5000 }); - } -}); + await page.waitForURL((url) => url.toString().includes("/drug/"), { timeout: 5000 }); + } + }); test("Gene/Target link is displayed in table", async () => { const isVisible = await pharmacoSection.isSectionVisible(); @@ -98,8 +98,7 @@ test.describe("Pharmacogenomics Section", () => { await pharmacoSection.clickGeneLink(0); // Wait for navigation to target page - await page.waitForURL(url => url.toString().includes("/target/"), { timeout: 5000 }); - + await page.waitForURL((url) => url.toString().includes("/target/"), { timeout: 5000 }); } } }); @@ -161,5 +160,4 @@ test.describe("Pharmacogenomics Section", () => { test.skip(); } }); - -}); \ No newline at end of file +}); diff --git a/packages/platform-test/e2e/pages/variant/variantQTLCredibleSets.spec.ts b/packages/platform-test/e2e/pages/variant/variantQTLCredibleSets.spec.ts index c271d6f57..43ed4794a 100644 --- a/packages/platform-test/e2e/pages/variant/variantQTLCredibleSets.spec.ts +++ b/packages/platform-test/e2e/pages/variant/variantQTLCredibleSets.spec.ts @@ -76,7 +76,6 @@ test.describe("Variant QTL Credible Sets Section", () => { }); test("Can search/filter QTL credible sets", async () => { - const initialRowCount = await qtlSection.getTableRows(); // Search for a specific term await qtlSection.search("ENSG"); diff --git a/packages/platform-test/e2e/pages/variant/variantUniProtVariants.spec.ts b/packages/platform-test/e2e/pages/variant/variantUniProtVariants.spec.ts index caa6cc2ce..7ad77761a 100644 --- a/packages/platform-test/e2e/pages/variant/variantUniProtVariants.spec.ts +++ b/packages/platform-test/e2e/pages/variant/variantUniProtVariants.spec.ts @@ -70,7 +70,6 @@ test.describe("UniProt Variants Section", () => { }); test("Can search/filter UniProt variants", async () => { - const initialRowCount = await uniprotSection.getTableRows(); // Search for a specific term await uniprotSection.search("disease"); From d357dedbbabd09b8678019a823d14de404d31961 Mon Sep 17 00:00:00 2001 From: david oluwasusi Date: Tue, 6 Jan 2026 15:19:02 +0000 Subject: [PATCH 13/34] chore(biome): lint fixes --- .../e2e/pages/variant/variantGWASCredibleSets.spec.ts | 1 - .../e2e/pages/variant/variantQTLCredibleSets.spec.ts | 1 - .../e2e/pages/variant/variantUniProtVariants.spec.ts | 1 - 3 files changed, 3 deletions(-) diff --git a/packages/platform-test/e2e/pages/variant/variantGWASCredibleSets.spec.ts b/packages/platform-test/e2e/pages/variant/variantGWASCredibleSets.spec.ts index fd247cca8..f43a23cc1 100644 --- a/packages/platform-test/e2e/pages/variant/variantGWASCredibleSets.spec.ts +++ b/packages/platform-test/e2e/pages/variant/variantGWASCredibleSets.spec.ts @@ -45,7 +45,6 @@ test.describe("Variant GWAS Credible Sets Section", () => { }); test("Lead variant column displays correctly", async () => { - // Lead variant might be the current variant (no link) or a different variant (with link) // Either way, the cell should have content const row = await gwasSection.getTableRow(0); diff --git a/packages/platform-test/e2e/pages/variant/variantQTLCredibleSets.spec.ts b/packages/platform-test/e2e/pages/variant/variantQTLCredibleSets.spec.ts index 43ed4794a..67eccb2b5 100644 --- a/packages/platform-test/e2e/pages/variant/variantQTLCredibleSets.spec.ts +++ b/packages/platform-test/e2e/pages/variant/variantQTLCredibleSets.spec.ts @@ -76,7 +76,6 @@ test.describe("Variant QTL Credible Sets Section", () => { }); test("Can search/filter QTL credible sets", async () => { - // Search for a specific term await qtlSection.search("ENSG"); diff --git a/packages/platform-test/e2e/pages/variant/variantUniProtVariants.spec.ts b/packages/platform-test/e2e/pages/variant/variantUniProtVariants.spec.ts index 7ad77761a..3f25eefa4 100644 --- a/packages/platform-test/e2e/pages/variant/variantUniProtVariants.spec.ts +++ b/packages/platform-test/e2e/pages/variant/variantUniProtVariants.spec.ts @@ -70,7 +70,6 @@ test.describe("UniProt Variants Section", () => { }); test("Can search/filter UniProt variants", async () => { - // Search for a specific term await uniprotSection.search("disease"); From 225d7fc6430b2476b372c020a2811784935b776e Mon Sep 17 00:00:00 2001 From: david oluwasusi Date: Tue, 6 Jan 2026 15:26:26 +0000 Subject: [PATCH 14/34] chore(biome): lint fixes --- packages/platform-test/e2e/pages/study/studyPageGWAS.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/platform-test/e2e/pages/study/studyPageGWAS.spec.ts b/packages/platform-test/e2e/pages/study/studyPageGWAS.spec.ts index b0da43e57..b4b01e733 100644 --- a/packages/platform-test/e2e/pages/study/studyPageGWAS.spec.ts +++ b/packages/platform-test/e2e/pages/study/studyPageGWAS.spec.ts @@ -5,10 +5,10 @@ import { SharedTraitStudiesSection } from "../../../POM/objects/widgets/Study/sh import { StudyPage } from "../../../POM/page/study/study"; test.describe("Study Page - GWAS Study", () => { - test.beforeEach(async ({ page, baseURL = "" }) => { + test.beforeEach(async ({ page, baseURL }) => { const studyPage = new StudyPage(page); // await studyPage.goToStudyPageFromGWASWidgetOnDiseasePage(DISEASE_EFO_ID); - await studyPage.goToStudyPage(baseURL, "GCST90475211"); + await studyPage.goToStudyPage(baseURL ?? '', "GCST90475211"); await studyPage.waitForStudyPageLoad(); }); From 0a9a63caaefcc61099d33da23e46fcb8ce82b349 Mon Sep 17 00:00:00 2001 From: david oluwasusi Date: Tue, 6 Jan 2026 15:27:01 +0000 Subject: [PATCH 15/34] chore(biome): lint fixes --- packages/platform-test/e2e/pages/study/studyPageGWAS.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/platform-test/e2e/pages/study/studyPageGWAS.spec.ts b/packages/platform-test/e2e/pages/study/studyPageGWAS.spec.ts index b4b01e733..d8066e7a0 100644 --- a/packages/platform-test/e2e/pages/study/studyPageGWAS.spec.ts +++ b/packages/platform-test/e2e/pages/study/studyPageGWAS.spec.ts @@ -8,7 +8,7 @@ test.describe("Study Page - GWAS Study", () => { test.beforeEach(async ({ page, baseURL }) => { const studyPage = new StudyPage(page); // await studyPage.goToStudyPageFromGWASWidgetOnDiseasePage(DISEASE_EFO_ID); - await studyPage.goToStudyPage(baseURL ?? '', "GCST90475211"); + await studyPage.goToStudyPage(baseURL ?? "", "GCST90475211"); await studyPage.waitForStudyPageLoad(); }); From f965d6d4abad763b917072f5eac2f126a81d12cd Mon Sep 17 00:00:00 2001 From: david oluwasusi Date: Tue, 6 Jan 2026 15:44:40 +0000 Subject: [PATCH 16/34] chore(biome): lint fixes --- packages/platform-test/e2e/pages/variant/variantEVA.spec.ts | 2 -- .../e2e/pages/variant/variantEffectPredictor.spec.ts | 1 - .../e2e/pages/variant/variantEnhancerToGenePredictions.spec.ts | 1 - .../e2e/pages/variant/variantPageNavigation.spec.ts | 1 - 4 files changed, 5 deletions(-) diff --git a/packages/platform-test/e2e/pages/variant/variantEVA.spec.ts b/packages/platform-test/e2e/pages/variant/variantEVA.spec.ts index 777bb654c..dc606463e 100644 --- a/packages/platform-test/e2e/pages/variant/variantEVA.spec.ts +++ b/packages/platform-test/e2e/pages/variant/variantEVA.spec.ts @@ -57,8 +57,6 @@ test.describe("EVA / ClinVar Section", () => { }); test("Can search/filter ClinVar entries", async () => { - const initialRowCount = await evaSection.getTableRows(); - // Search for a specific term await evaSection.search("pathogenic"); diff --git a/packages/platform-test/e2e/pages/variant/variantEffectPredictor.spec.ts b/packages/platform-test/e2e/pages/variant/variantEffectPredictor.spec.ts index 13e880921..a607ee171 100644 --- a/packages/platform-test/e2e/pages/variant/variantEffectPredictor.spec.ts +++ b/packages/platform-test/e2e/pages/variant/variantEffectPredictor.spec.ts @@ -58,7 +58,6 @@ test.describe("Variant Effect Predictor / Transcript Consequences Section", () = }); test("Can search/filter transcript consequences", async () => { - const initialRowCount = await vepSection.getTableRows(); // Search for a specific term await vepSection.search("missense"); diff --git a/packages/platform-test/e2e/pages/variant/variantEnhancerToGenePredictions.spec.ts b/packages/platform-test/e2e/pages/variant/variantEnhancerToGenePredictions.spec.ts index f0f71bca0..5560a373f 100644 --- a/packages/platform-test/e2e/pages/variant/variantEnhancerToGenePredictions.spec.ts +++ b/packages/platform-test/e2e/pages/variant/variantEnhancerToGenePredictions.spec.ts @@ -58,7 +58,6 @@ test.describe("Enhancer-to-Gene Predictions Section", () => { }); test("Can search/filter E2G predictions", async () => { - const initialRowCount = await e2gSection.getTableRows(); // Search for a specific term await e2gSection.search("ENSG"); diff --git a/packages/platform-test/e2e/pages/variant/variantPageNavigation.spec.ts b/packages/platform-test/e2e/pages/variant/variantPageNavigation.spec.ts index d52ae4205..99e6a7a00 100644 --- a/packages/platform-test/e2e/pages/variant/variantPageNavigation.spec.ts +++ b/packages/platform-test/e2e/pages/variant/variantPageNavigation.spec.ts @@ -44,5 +44,4 @@ test.describe("Variant Page Navigation", () => { const profileHeaderBlock = page.locator("[data-testid='profile-page-header-block']"); await expect(profileHeaderBlock).toBeVisible(); }); - }); From b07f34ca882c77c09e23542e983de632b9056dda Mon Sep 17 00:00:00 2001 From: david oluwasusi Date: Tue, 6 Jan 2026 15:52:19 +0000 Subject: [PATCH 17/34] chore(biome): lint fixes --- .../e2e/pages/variant/variantEffectPredictor.spec.ts | 1 - .../e2e/pages/variant/variantEnhancerToGenePredictions.spec.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/platform-test/e2e/pages/variant/variantEffectPredictor.spec.ts b/packages/platform-test/e2e/pages/variant/variantEffectPredictor.spec.ts index a607ee171..5da6bfb5a 100644 --- a/packages/platform-test/e2e/pages/variant/variantEffectPredictor.spec.ts +++ b/packages/platform-test/e2e/pages/variant/variantEffectPredictor.spec.ts @@ -58,7 +58,6 @@ test.describe("Variant Effect Predictor / Transcript Consequences Section", () = }); test("Can search/filter transcript consequences", async () => { - // Search for a specific term await vepSection.search("missense"); diff --git a/packages/platform-test/e2e/pages/variant/variantEnhancerToGenePredictions.spec.ts b/packages/platform-test/e2e/pages/variant/variantEnhancerToGenePredictions.spec.ts index 5560a373f..3da16eb77 100644 --- a/packages/platform-test/e2e/pages/variant/variantEnhancerToGenePredictions.spec.ts +++ b/packages/platform-test/e2e/pages/variant/variantEnhancerToGenePredictions.spec.ts @@ -58,7 +58,6 @@ test.describe("Enhancer-to-Gene Predictions Section", () => { }); test("Can search/filter E2G predictions", async () => { - // Search for a specific term await e2gSection.search("ENSG"); From a4459d6ec507d6b9df9b86cc555cf85ae9d76d34 Mon Sep 17 00:00:00 2001 From: david oluwasusi Date: Tue, 6 Jan 2026 16:50:36 +0000 Subject: [PATCH 18/34] chore(test): fix playwright dep error --- .github/workflows/e2e-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e-ci.yml b/.github/workflows/e2e-ci.yml index 462507a1b..d908e0028 100644 --- a/.github/workflows/e2e-ci.yml +++ b/.github/workflows/e2e-ci.yml @@ -58,7 +58,7 @@ jobs: - name: install playwright browsers run: | cd packages/platform-test - yarn playwright install --with-deps chromium + yarn playwright install --with-deps chromium webkit - name: Run E2E tests run: | if [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ github.event.inputs.test_scope }}" == "all" ]]; then From 217adce1b45f38814520a908e3c6ba489497a3ca Mon Sep 17 00:00:00 2001 From: david oluwasusi Date: Thu, 8 Jan 2026 10:03:17 +0000 Subject: [PATCH 19/34] chore(e2e-annotation): remove duplicate and unused methods --- .../POM/objects/widgets/AOTF/aotfTable.ts | 117 +---- .../pages/disease/associatedTargets.spec.ts | 487 ------------------ .../playwright-report/index.html | 2 +- 3 files changed, 3 insertions(+), 603 deletions(-) delete mode 100644 packages/platform-test/e2e/pages/disease/associatedTargets.spec.ts diff --git a/packages/platform-test/POM/objects/widgets/AOTF/aotfTable.ts b/packages/platform-test/POM/objects/widgets/AOTF/aotfTable.ts index 7d813ecba..07bd9dc41 100644 --- a/packages/platform-test/POM/objects/widgets/AOTF/aotfTable.ts +++ b/packages/platform-test/POM/objects/widgets/AOTF/aotfTable.ts @@ -17,10 +17,6 @@ export class AotfTable { return this.page.locator("[data-testid='table-header-name']"); } - getAssociationScoreHeader(): Locator { - return this.page.locator("[data-testid='table-header-score']"); - } - async getHeaderText(headerTestId: string): Promise { return await this.page.locator(`[data-testid='${headerTestId}']`).textContent(); } @@ -30,21 +26,11 @@ export class AotfTable { return this.page.locator(`[data-testid^='table-row-${prefix}']`); } - getTableRowByIndex(index: number, prefix: string = "core"): Locator { - return this.page.locator(`[data-testid='table-row-${prefix}-${index}']`); - } - async getRowCount(prefix: string = "core"): Promise { return await this.getTableRows(prefix).count(); } // Cell accessors - getCellByRowAndColumn(rowIndex: number, columnName: string, prefix: string = "core"): Locator { - return this.page.locator( - `[data-testid='table-row-${prefix}-${rowIndex}'] [data-testid*='${columnName}']` - ); - } - getNameCell(rowIndex: number, prefix: string = "core"): Locator { return this.page.locator( `[data-testid='table-row-${prefix}-${rowIndex}'] [data-testid='name-cell']` @@ -58,10 +44,6 @@ export class AotfTable { } // Pagination - getPaginationContainer(): Locator { - return this.page.locator("[data-testid='pagination-container']"); - } - getPageSizeSelector(): Locator { return this.page.locator("[data-testid='page-size-selector']"); } @@ -87,59 +69,12 @@ export class AotfTable { await this.page.getByRole("option", { name: size }).click(); } - // Section controls (Pinned, Uploaded, Core) - getPinnedSection(): Locator { - return this.page.locator("text=Pinned").first(); - } - - getUploadedSection(): Locator { - return this.page.locator("text=Uploaded").first(); - } - - getCoreSection(): Locator { - return this.page.locator("text=All").first(); - } - - async togglePinnedSection(): Promise { - await this.getPinnedSection().click(); - } - - async toggleUploadedSection(): Promise { - await this.getUploadedSection().click(); - } - - async toggleCoreSection(): Promise { - await this.getCoreSection().click(); - } - - // Delete actions - getDeletePinnedButton(): Locator { - return this.page.locator("[data-testid='delete-pinned-button']").first(); - } - - getDeleteUploadedButton(): Locator { - return this.page.locator("[data-testid='delete-uploaded-button']").first(); - } - - async deletePinnedEntries(): Promise { - await this.getDeletePinnedButton().click(); - } - - async deleteUploadedEntries(): Promise { - await this.getDeleteUploadedButton().click(); - } - // Sorting async sortByColumn(columnName: string): Promise { const header = this.page.getByText(columnName, { exact: true }); await header.click(); } - // Search/Filter in specific row - async getRowByName(name: string): Promise { - return this.page.locator(`[data-testid*='table-row']`, { hasText: name }); - } - // Find row index by gene symbol async findRowIndexByGeneSymbol( geneSymbol: string, @@ -159,45 +94,6 @@ export class AotfTable { return null; } - // Wait for specific gene to appear in table - async waitForGeneInTable( - geneSymbol: string, - prefix: string = "core", - timeout: number = 10000 - ): Promise { - const startTime = Date.now(); - - while (Date.now() - startTime < timeout) { - const rowIndex = await this.findRowIndexByGeneSymbol(geneSymbol, prefix); - if (rowIndex !== null) { - return rowIndex; - } - await this.page.waitForTimeout(500); - } - - return null; - } - - // Pin/Unpin row - getPinButtonForRow(rowIndex: number): Locator { - return this.page.locator(`[data-testid='pin-button-${rowIndex}']`); - } - - async pinRow(rowIndex: number): Promise { - await this.getPinButtonForRow(rowIndex).click(); - } - - // Get association score value - async getAssociationScoreValue( - rowIndex: number, - prefix: string = "core" - ): Promise { - const scoreCell = this.getScoreCell(rowIndex, prefix); - await scoreCell.waitFor({ state: "visible" }); - const text = await scoreCell.textContent(); - return text?.trim() || null; - } - // Get entity name (target or disease) async getEntityName(rowIndex: number, prefix: string = "core"): Promise { const nameCell = this.getNameCell(rowIndex, prefix); @@ -207,24 +103,15 @@ export class AotfTable { return text?.trim() || null; } - // Check if table is loading - getLoadingIndicator(): Locator { - return this.page.locator("[data-testid='table-loading']"); - } - - async isLoading(): Promise { - return await this.getLoadingIndicator().isVisible(); - } - // Wait for table to load async waitForTableLoad(): Promise { await this.page.waitForSelector(".TAssociations", { state: "visible" }); // Wait for loading to finish if present - const loadingVisible = await this.getLoadingIndicator() + const loadingVisible = await this.page.locator("[data-testid='table-loading']") .isVisible() .catch(() => false); if (loadingVisible) { - await this.getLoadingIndicator().waitFor({ state: "hidden" }); + await this.page.locator("[data-testid='table-loading']").waitFor({ state: "hidden" }); } // Wait for at least one row to be present with actual content await this.page.waitForSelector("[data-testid^='table-row-core']", { state: "visible" }); diff --git a/packages/platform-test/e2e/pages/disease/associatedTargets.spec.ts b/packages/platform-test/e2e/pages/disease/associatedTargets.spec.ts deleted file mode 100644 index 4a0816ec1..000000000 --- a/packages/platform-test/e2e/pages/disease/associatedTargets.spec.ts +++ /dev/null @@ -1,487 +0,0 @@ -import { test } from "../../../fixtures"; -import { EvidenceSection } from "../../../POM/objects/components/EvidenceSection/evidenceSection"; -import { AotfActions } from "../../../POM/objects/widgets/AOTF/aotfActions"; -import { AotfTable } from "../../../POM/objects/widgets/AOTF/aotfTable"; -import { DiseasePage } from "../../../POM/page/disease/disease"; - -const DISEASE_NAME = "myocardial infarction"; - -test.describe("Disease Page", () => { - test.beforeEach(async ({ page, baseURL, testConfig }) => { - await page.goto(`${baseURL}/disease/${testConfig.disease.primary}/associations`); - }); - - test.describe("Aotf main actions functionality", () => { - test("Can search through targets in the disease associations page", async ({ page }) => { - const aotfActions = new AotfActions(page); - await aotfActions.searchByName("ADRB1"); - const filterValue = await aotfActions.getNameFilterValue(); - test.expect(filterValue).toBe("ADRB1"); - }); - - test("Can toggle advance filters", async ({ page }) => { - const aotfActions = new AotfActions(page); - await aotfActions.openFacetsSearch(); - const isOpen = await aotfActions.isFacetsPopoverOpen(); - test.expect(isOpen).toBe(true); - }); - - test("Can toggle column options", async ({ page }) => { - const aotfActions = new AotfActions(page); - await aotfActions.openColumnOptions(); - const isActive = await aotfActions.isColumnOptionsActive(); - test.expect(isActive).toBe(true); - }); - - test("Can toggle export options", async ({ page }) => { - const aotfActions = new AotfActions(page); - await aotfActions.openExportMenu(); - const isActive = await aotfActions.isExportMenuOpen(); - test.expect(isActive).toBe(true); - }); - - test("Can toggle between association and target prioritization options", async ({ page }) => { - const aotfActions = new AotfActions(page); - await aotfActions.switchToAssociationsView(); - const currentView = await aotfActions.getCurrentDisplayMode(); - test.expect(currentView).toBe("associations"); - - await aotfActions.switchToPrioritisationView(); - const newView = await aotfActions.getCurrentDisplayMode(); - test.expect(newView).toBe("prioritisations"); - }); - }); - - test.describe("Aotf table functionality", () => { - test("targets are displayed in the associations table", async ({ page }) => { - const aotfTable = new AotfTable(page); - - // Wait for table to load - await aotfTable.waitForTableLoad(); - - // Verify table is visible - await test.expect(aotfTable.getTable()).toBeVisible(); - - // Verify header is present - await test.expect(aotfTable.getTargetOrDiseaseHeader()).toBeVisible(); - const headerText = await aotfTable.getHeaderText("table-header-name"); - test.expect(headerText).toBe("Target"); - - // Verify rows are loaded - const rowCount = await aotfTable.getRowCount(); - test.expect(rowCount).toBeGreaterThan(0); - - // Verify first row has data - const firstRowName = await aotfTable.getEntityName(0); - test.expect(firstRowName).toBeTruthy(); - }); - - test("can sort by GWAS score in the associations table", async ({ page }) => { - const aotfTable = new AotfTable(page); - const aotfActions = new AotfActions(page); - await aotfTable.waitForTableLoad(); - - // Verify no sort filter is active initially (default sort by Association Score) - const initialSortActive = await aotfActions.hasSortFilter(); - test.expect(initialSortActive).toBe(false); - - // Click to sort by a different column (GWAS associations) - await aotfTable.sortByColumn("GWAS associations"); - await page.waitForTimeout(1000); // Wait for sort to complete - - // Verify sort filter is now active in the ActiveFiltersPanel - const sortActive = await aotfActions.hasSortFilter(); - test.expect(sortActive).toBe(true); - - // Verify the sort filter shows the correct column name - const sortFilterText = await aotfActions.getSortFilterText(); - test.expect(sortFilterText).toContain("GWAS associations"); - }); - - test("can paginate through the associations table", async ({ page }) => { - const aotfTable = new AotfTable(page); - await aotfTable.waitForTableLoad(); - - // Get first page data - const firstPageFirstRow = await aotfTable.getEntityName(0); - - // Go to next page - await aotfTable.clickNextPage(); - await page.waitForTimeout(1000); // Wait for new data to load - - // Get second page data - const secondPageFirstRow = await aotfTable.getEntityName(0); - - // First row on different pages should be different - test.expect(firstPageFirstRow).not.toBe(secondPageFirstRow); - - // Go back to previous page - await aotfTable.clickPreviousPage(); - await page.waitForTimeout(1000); - - const backToFirstRow = await aotfTable.getEntityName(0); - test.expect(backToFirstRow).toBe(firstPageFirstRow); - }); - - test("can change page size", async ({ page }) => { - const aotfTable = new AotfTable(page); - await aotfTable.waitForTableLoad(); - - // Change page size to 25 - await aotfTable.selectPageSize("25"); - await page.waitForTimeout(1000); - - // Verify more rows are displayed (up to 25) - const rowCount = await aotfTable.getRowCount(); - test.expect(rowCount).toBeGreaterThanOrEqual(10); // Default is usually 10 - }); - - test("can filter targets by name", async ({ page }) => { - const aotfActions = new AotfActions(page); - const aotfTable = new AotfTable(page); - - await aotfTable.waitForTableLoad(); - - // Search for a specific target - await aotfActions.searchByName("IL6"); - await page.waitForTimeout(1000); // Wait for filter + debounce - - // Verify filtered results contain the search term - const firstRowName = await aotfTable.getEntityName(0); - test.expect(firstRowName?.toLowerCase()).toContain("il6"); - }); - }); - - test.describe("Aotf target prioritisation functionality", () => { - test("Can switch to target prioritisation view and see data", async ({ page }) => { - const aotfActions = new AotfActions(page); - const aotfTable = new AotfTable(page); - - // Switch to prioritisation view - await aotfActions.switchToPrioritisationView(); - await aotfTable.waitForTableLoad(); - - // Verify table is visible - await test.expect(aotfTable.getTable()).toBeVisible(); - - // Verify rows are loaded - const rowCount = await aotfTable.getRowCount(); - test.expect(rowCount).toBeGreaterThan(0); - - // Verify first row has data - const firstRowName = await aotfTable.getEntityName(0); - test.expect(firstRowName).toBeTruthy(); - }); - }); - - test.describe("Aotf table section is rendered when data is present", () => { - test("if a target has data, the corresponding widget is shown", async ({ page }) => { - const aotfTable = new AotfTable(page); - const evidenceSection = new EvidenceSection(page); - await aotfTable.waitForTableLoad(); - - // Find the first row that has data cells with score > 0 - const rowIndex = await aotfTable.findFirstRowWithData(); - test.expect(rowIndex).not.toBeNull(); - - if (rowIndex === null) { - test.skip(true, "No rows with data found"); - return; - } - - // Get all data cells with scores in that row - const dataCells = await aotfTable.getDataCellsWithScores(rowIndex); - test.expect(dataCells.length).toBeGreaterThan(0); - - // Filter out non-evidence columns - // 'score' = total association score (not an evidence section) - // 'maxClinicalTrialPhase', 'tractability' = prioritization metrics (link to target page sections) - const nonEvidenceColumns = ["score"]; - const cellsToTest = dataCells.filter((cell) => !nonEvidenceColumns.includes(cell.columnId)); - - for (const cell of cellsToTest) { - // Click on the data cell to open the evidence section - await aotfTable.clickDataCell(rowIndex, cell.columnId); - - // Wait for section to load (no loader, no error) - await evidenceSection.waitForSectionLoad(cell.columnId); - - // Verify that an evidence section is visible - const hasSections = await evidenceSection.hasAnyEvidenceSection(); - test.expect(hasSections).toBe(true); - - // Verify the specific section for this data source is visible - // The section ID is typically the data source ID (e.g., 'gwas', 'eva', etc.) - const isVisible = await evidenceSection.isEvidenceSectionVisible(cell.columnId); - test.expect(isVisible).toBe(true); - - // Verify no loader is visible - const hasLoader = await evidenceSection.isLoaderVisible(); - test.expect(hasLoader).toBe(false); - - // Click the same cell again to close/toggle the section - await aotfTable.clickDataCell(rowIndex, cell.columnId); - await page.waitForTimeout(300); - } - }); - - test("specified genes have correct evidence widgets for their data cells", async ({ - page, - testConfig, - }) => { - const aotfTable = new AotfTable(page); - const aotfActions = new AotfActions(page); - const evidenceSection = new EvidenceSection(page); - const genesToTest = testConfig.disease.aotfGenes || []; - - if (genesToTest.length === 0) { - test.skip(true, "No genes specified in test config"); - return; - } - - for (const geneSymbol of genesToTest) { - // Search for the specific gene - await aotfActions.searchByName(geneSymbol); - - // Wait for table to load with filtered results - await aotfTable.waitForTableLoad(); - - // Find the row for this gene - const rowIndex = await aotfTable.findRowIndexByGeneSymbol(geneSymbol); - - if (rowIndex === null) { - test.fail(true, `Gene ${geneSymbol} not found in table`); - continue; - } - - // Get all data cells with scores for this gene - const dataCells = await aotfTable.getDataCellsWithScores(rowIndex); - - if (dataCells.length === 0) { - test.fail(true, `Gene ${geneSymbol} has no data cells with scores`); - continue; - } - - // Filter out non-evidence columns - const nonEvidenceColumns = ["score"]; - const cellsToTest = dataCells.filter((cell) => !nonEvidenceColumns.includes(cell.columnId)); - - if (cellsToTest.length === 0) { - test.fail(true, `Gene ${geneSymbol} has no evidence data cells`); - continue; - } - - for (const cell of cellsToTest) { - // Click on the data cell to open the evidence section - await aotfTable.clickDataCell(rowIndex, cell.columnId); - - // Wait for section to load - await evidenceSection.waitForSectionLoad(cell.columnId); - - // Verify that an evidence section is visible - const hasSections = await evidenceSection.hasAnyEvidenceSection(); - test - .expect( - hasSections, - `Gene ${geneSymbol} - ${cell.columnId}: Should have evidence sections` - ) - .toBe(true); - - // Verify the specific section for this data source is visible - const isVisible = await evidenceSection.isEvidenceSectionVisible(cell.columnId); - test - .expect( - isVisible, - `Gene ${geneSymbol} - ${cell.columnId}: Evidence section should be visible` - ) - .toBe(true); - - // Verify no loader is visible - const hasLoader = await evidenceSection.isLoaderVisible(); - test - .expect( - hasLoader, - `Gene ${geneSymbol} - ${cell.columnId}: Loader should not be visible` - ) - .toBe(false); - - // Click the same cell again to close/toggle the section - await aotfTable.clickDataCell(rowIndex, cell.columnId); - - // Wait for evidence section to close - await evidenceSection.waitForLoaderToDisappear(); - } - - // Clear the search filter for next gene - await aotfActions.clearNameFilter(); - - // Wait for table to reload with all results - await aotfTable.waitForTableLoad(); - } - }); - - test("specified genes in target prioritization view have correct evidence widgets", async ({ - page, - testConfig, - }) => { - const aotfTable = new AotfTable(page); - const aotfActions = new AotfActions(page); - const evidenceSection = new EvidenceSection(page); - const genesToTest = testConfig.disease.aotfGenes || []; - - // Map prioritization column IDs to their section IDs - const prioritizationColumnToSection: Record = { - maxClinicalTrialPhase: "knownDrugs", - isInMembrane: "subcellularLocation", - isSecreted: "subcellularLocation", - hasLigand: "tractability", - hasSmallMoleculeBinder: "tractability", - hasPocket: "tractability", - mouseOrthologMaxIdentityPercentage: "compGenomics", - hasHighQualityChemicalProbes: "chemicalProbes", - mouseKOScore: "mousePhenotypes", - geneticConstraint: "geneticConstraint", - geneEssentiality: "depMapEssentiality", - hasSafetyEvent: "safety", - isCancerDriverGene: "cancerHallmarks", - paralogMaxIdentityPercentage: "compGenomics", - tissueSpecificity: "expressions", - tissueDistribution: "expressions", - }; - - if (genesToTest.length === 0) { - test.skip(true, "No genes specified in test config"); - return; - } - - // Switch to target prioritization view - await aotfActions.switchToPrioritisationView(); - await aotfTable.waitForTableLoad(); - - for (const geneSymbol of genesToTest) { - // Search for the specific gene - await aotfActions.searchByName(geneSymbol); - - // Wait for table to load with filtered results - await aotfTable.waitForTableLoad(); - - // Find the row for this gene - const rowIndex = await aotfTable.findRowIndexByGeneSymbol(geneSymbol); - - if (rowIndex === null) { - test.fail(true, `Gene ${geneSymbol} not found in prioritization table`); - continue; - } - - // Get all data cells with scores for this gene - const dataCells = await aotfTable.getDataCellsWithScores(rowIndex); - - if (dataCells.length === 0) { - test.fail( - true, - `Gene ${geneSymbol} has no data cells with scores in prioritization view` - ); - continue; - } - - // Filter out non-evidence columns - const nonEvidenceColumns = ["score"]; - const cellsToTest = dataCells.filter((cell) => !nonEvidenceColumns.includes(cell.columnId)); - - if (cellsToTest.length === 0) { - test.fail(true, `Gene ${geneSymbol} has no evidence data cells in prioritization view`); - continue; - } - - for (const cell of cellsToTest) { - // Click on the data cell to open the evidence section - await aotfTable.clickDataCell(rowIndex, cell.columnId); - - // Map the column ID to the section ID for prioritization columns - const sectionId = prioritizationColumnToSection[cell.columnId] || cell.columnId; - - // Wait for section to load - await evidenceSection.waitForSectionLoad(sectionId); - - // Verify that an evidence section is visible - const hasSections = await evidenceSection.hasAnyEvidenceSection(); - test - .expect( - hasSections, - `Prioritization - Gene ${geneSymbol} - ${cell.columnId} (section: ${sectionId}): Should have evidence sections` - ) - .toBe(true); - - // Verify the specific section for this data source is visible - const isVisible = await evidenceSection.isEvidenceSectionVisible(sectionId); - test - .expect( - isVisible, - `Prioritization - Gene ${geneSymbol} - ${cell.columnId} (section: ${sectionId}): Evidence section should be visible` - ) - .toBe(true); - - // Verify no loader is visible - const hasLoader = await evidenceSection.isLoaderVisible(); - test - .expect( - hasLoader, - `Prioritization - Gene ${geneSymbol} - ${cell.columnId} (section: ${sectionId}): Loader should not be visible` - ) - .toBe(false); - - // Click the same cell again to close/toggle the section - await aotfTable.clickDataCell(rowIndex, cell.columnId); - - // Wait for evidence section to close - await evidenceSection.waitForLoaderToDisappear(); - } - - // Clear the search filter for next gene - await aotfActions.clearNameFilter(); - - // Wait for table to reload with all results - await aotfTable.waitForTableLoad(); - } - }); - }); - - test("Disease header is correctly displayed", async ({ page }) => { - const diseaseName = page.getByTestId("profile-page-header-text"); - await test.expect(diseaseName).toHaveText(DISEASE_NAME); - }); - - test("Can navigate to the profile page from the disease page", async ({ page }) => { - const diseasePage = new DiseasePage(page); - await diseasePage.goToProfilePage(); - - // Verify that the URL is the profile page URL - await test.expect(page.url()).toBe(diseasePage.getProfilePage()); - }); - - test("External links in header are displayed and working", async ({ page, testConfig }) => { - const diseasePage = new DiseasePage(page); - - // Check for EFO external link - const efoLink = diseasePage.getEfoLink(); - await test.expect(efoLink).toBeVisible(); - - // Verify the EFO link has the correct href - const efoHref = await diseasePage.getEfoLinkHref(); - test.expect(efoHref).toContain(testConfig.disease.primary); - test.expect(efoHref).toContain("ebi.ac.uk/ols4/ontologies/efo/terms"); - - // Check for cross-reference links (e.g., MONDO, MeSH, etc.) - const xrefCount = await diseasePage.getXrefLinksCount(); - - // Verify that at least one cross-reference link exists - test.expect(xrefCount).toBeGreaterThan(0); - - // Verify the first xref link has a valid href - if (xrefCount > 0) { - const firstXrefHref = await diseasePage.getFirstXrefLinkHref(); - test.expect(firstXrefHref).toBeTruthy(); - test.expect(firstXrefHref).toMatch(/^https?:\/\//); - } - }); -}); diff --git a/packages/platform-test/playwright-report/index.html b/packages/platform-test/playwright-report/index.html index a6d9be4e7..5fca9fec5 100644 --- a/packages/platform-test/playwright-report/index.html +++ b/packages/platform-test/playwright-report/index.html @@ -73,4 +73,4 @@
    - \ No newline at end of file + \ No newline at end of file From 3e8d2369e6d2fefdf8edccd42ad736d9eb55fdc0 Mon Sep 17 00:00:00 2001 From: david oluwasusi Date: Tue, 13 Jan 2026 14:53:19 +0000 Subject: [PATCH 20/34] feat(e2e): add tests and interactors for target page --- .../components/Table/CellName.jsx | 5 +- .../components/Table/TableAssociations.jsx | 5 +- .../components/Table/TableHeader.jsx | 2 +- .../components/ProfileHeader/profileHeader.ts | 17 +- .../POM/objects/widgets/AOTF/aotfTable.ts | 135 +++++- .../platform-test/POM/page/disease/disease.ts | 2 +- .../platform-test/POM/page/target/index.ts | 1 + .../platform-test/POM/page/target/target.ts | 257 +++++++++++ .../pages/target/targetAssociations.spec.ts | 415 ++++++++++++++++++ .../e2e/pages/target/targetPage.spec.ts | 223 ++++++++++ .../e2e/pages/target/targetProfile.spec.ts | 299 +++++++++++++ packages/platform-test/fixtures/testConfig.ts | 1 + .../playwright-report/index.html | 2 +- .../src/components/ExternalLink/XRefLinks.tsx | 2 +- 14 files changed, 1350 insertions(+), 16 deletions(-) create mode 100644 packages/platform-test/POM/page/target/index.ts create mode 100644 packages/platform-test/POM/page/target/target.ts create mode 100644 packages/platform-test/e2e/pages/target/targetAssociations.spec.ts create mode 100644 packages/platform-test/e2e/pages/target/targetPage.spec.ts create mode 100644 packages/platform-test/e2e/pages/target/targetProfile.spec.ts diff --git a/apps/platform/src/components/AssociationsToolkit/components/Table/CellName.jsx b/apps/platform/src/components/AssociationsToolkit/components/Table/CellName.jsx index 77c80ff3b..49dc15abf 100644 --- a/apps/platform/src/components/AssociationsToolkit/components/Table/CellName.jsx +++ b/apps/platform/src/components/AssociationsToolkit/components/Table/CellName.jsx @@ -215,6 +215,7 @@ function CellName({ cell, colorScale }) { onClick={handleToggle} onContextMenu={handleContextMenu} active={openContext} + data-testid={`context-menu-${cell.row.index}`} > @@ -241,7 +242,7 @@ function CellName({ cell, colorScale }) {
    {!isPinned && ( - + @@ -249,7 +250,7 @@ function CellName({ cell, colorScale }) { )} {isPinned && ( - + diff --git a/apps/platform/src/components/AssociationsToolkit/components/Table/TableAssociations.jsx b/apps/platform/src/components/AssociationsToolkit/components/Table/TableAssociations.jsx index dd6a8d910..df746529d 100644 --- a/apps/platform/src/components/AssociationsToolkit/components/Table/TableAssociations.jsx +++ b/apps/platform/src/components/AssociationsToolkit/components/Table/TableAssociations.jsx @@ -80,6 +80,7 @@ const TableIndicatorControl = ({ )} {label}
    + {label} ) : (
    - {label} + {label}
    ), diff --git a/apps/platform/src/components/AssociationsToolkit/components/Table/TableHeader.jsx b/apps/platform/src/components/AssociationsToolkit/components/Table/TableHeader.jsx index c93acaa43..4a948f7f1 100644 --- a/apps/platform/src/components/AssociationsToolkit/components/Table/TableHeader.jsx +++ b/apps/platform/src/components/AssociationsToolkit/components/Table/TableHeader.jsx @@ -42,7 +42,7 @@ function TableHeader({ table, cols }) { const highLevelHeaders = table.getHeaderGroups()[0].headers; return ( -
    +
    {highLevelHeaders.map(highLevelHeader => ( false); } - // Description section + // Description section - locate by the "Description" heading + // Note: Description heading is outside the profile-header container, so search at page level + getDescriptionHeading(): Locator { + return this.page.getByRole("heading", { name: "Description", level: 6 }); + } + getDescriptionSection(): Locator { - return this.page.locator("[data-testid='profile-description']"); + // Get the parent container that includes both heading and description text + return this.getDescriptionHeading().locator(".."); } async isDescriptionVisible(): Promise { - return await this.getDescriptionSection() + return await this.getDescriptionHeading() .isVisible() .catch(() => false); } async getDescriptionText(): Promise { - return await this.getDescriptionSection().textContent(); + // Get the paragraph element that is a sibling following the Description heading + // Use xpath to find the paragraph sibling after the heading + const descriptionParagraph = this.getDescriptionHeading().locator("xpath=following-sibling::p[1]"); + return await descriptionParagraph.textContent(); } // Synonyms section diff --git a/packages/platform-test/POM/objects/widgets/AOTF/aotfTable.ts b/packages/platform-test/POM/objects/widgets/AOTF/aotfTable.ts index 07bd9dc41..073652c48 100644 --- a/packages/platform-test/POM/objects/widgets/AOTF/aotfTable.ts +++ b/packages/platform-test/POM/objects/widgets/AOTF/aotfTable.ts @@ -17,6 +17,10 @@ export class AotfTable { return this.page.locator("[data-testid='table-header-name']"); } + getAssociationScoreHeader(): Locator { + return this.page.locator("[data-testid='table-header-score']"); + } + async getHeaderText(headerTestId: string): Promise { return await this.page.locator(`[data-testid='${headerTestId}']`).textContent(); } @@ -26,11 +30,21 @@ export class AotfTable { return this.page.locator(`[data-testid^='table-row-${prefix}']`); } + getTableRowByIndex(index: number, prefix: string = "core"): Locator { + return this.page.locator(`[data-testid='table-row-${prefix}-${index}']`); + } + async getRowCount(prefix: string = "core"): Promise { return await this.getTableRows(prefix).count(); } // Cell accessors + getCellByRowAndColumn(rowIndex: number, columnName: string, prefix: string = "core"): Locator { + return this.page.locator( + `[data-testid='table-row-${prefix}-${rowIndex}'] [data-testid*='${columnName}']` + ); + } + getNameCell(rowIndex: number, prefix: string = "core"): Locator { return this.page.locator( `[data-testid='table-row-${prefix}-${rowIndex}'] [data-testid='name-cell']` @@ -39,11 +53,15 @@ export class AotfTable { getScoreCell(rowIndex: number, prefix: string = "core"): Locator { return this.page.locator( - `[data-testid='table-row-${prefix}-${rowIndex}'] [data-testid*='score']` + `[data-testid='table-row-${prefix}-${rowIndex}'] [data-testid='score-cell-score']` ); } // Pagination + getPaginationContainer(): Locator { + return this.page.locator("[data-testid='pagination-container']"); + } + getPageSizeSelector(): Locator { return this.page.locator("[data-testid='page-size-selector']"); } @@ -66,7 +84,50 @@ export class AotfTable { async selectPageSize(size: string): Promise { await this.getPageSizeSelector().click(); - await this.page.getByRole("option", { name: size }).click(); + // MUI Select renders options in a listbox with data-value attribute + await this.page.locator(`[role='listbox'] [data-value='${size}']`).click(); + } + + // Section controls (Pinned, Uploaded, Core) + getPinnedSection(): Locator { + return this.page.locator("[data-testid='section-pinning']"); + } + + getUploadedSection(): Locator { + return this.page.locator("[data-testid='section-uploaded']"); + } + + getCoreSection(): Locator { + return this.page.locator("[data-testid='section-core']"); + } + + async togglePinnedSection(): Promise { + await this.getPinnedSection().click(); + } + + async toggleUploadedSection(): Promise { + await this.getUploadedSection().click(); + } + + async toggleCoreSection(): Promise { + await this.getCoreSection().click(); + } + + // Delete actions + getDeletePinnedButton(): Locator { + return this.page.locator("[data-testid='delete-pinning-button']"); + } + + getDeleteUploadedButton(): Locator { + return this.page.locator("[data-testid='delete-uploaded-button']"); + } + + async deletePinnedEntries(): Promise { + await this.getDeletePinnedButton().click(); + } + + async deleteUploadedEntries(): Promise { + await this.getDeleteUploadedButton().click(); } // Sorting @@ -75,6 +136,11 @@ export class AotfTable { await header.click(); } + // Search/Filter in specific row + getRowByName(name: string): Locator { + return this.page.locator(`[data-testid*='table-row']`, { hasText: name }).first(); + } + // Find row index by gene symbol async findRowIndexByGeneSymbol( geneSymbol: string, @@ -94,6 +160,58 @@ export class AotfTable { return null; } + // Wait for specific gene to appear in table + async waitForGeneInTable( + geneSymbol: string, + prefix: string = "core", + timeout: number = 10000 + ): Promise { + const startTime = Date.now(); + + while (Date.now() - startTime < timeout) { + const rowIndex = await this.findRowIndexByGeneSymbol(geneSymbol, prefix); + if (rowIndex !== null) { + return rowIndex; + } + await this.page.waitForTimeout(500); + } + + return null; + } + + // Pin/Unpin row via context menu + getContextMenuForRow(rowIndex: number): Locator { + return this.page.locator(`[data-testid='context-menu-${rowIndex}']`); + } + + getPinEntityButton(): Locator { + return this.page.locator("[data-testid='pin-entity-button']"); + } + + getUnpinEntityButton(): Locator { + return this.page.locator("[data-testid='unpin-entity-button']"); + } + + async pinRow(rowIndex: number): Promise { + await this.getContextMenuForRow(rowIndex).click(); + await this.getPinEntityButton().click(); + } + + async unpinRow(rowIndex: number): Promise { + await this.getContextMenuForRow(rowIndex).click(); + await this.getUnpinEntityButton().click(); + } + + // Get association score value + async getAssociationScoreValue( + rowIndex: number, + prefix: string = "core" + ): Promise { + const scoreCell = this.getScoreCell(rowIndex, prefix); + await scoreCell.waitFor({ state: "visible" }); + return scoreCell.getAttribute("data-score"); + } + // Get entity name (target or disease) async getEntityName(rowIndex: number, prefix: string = "core"): Promise { const nameCell = this.getNameCell(rowIndex, prefix); @@ -103,15 +221,24 @@ export class AotfTable { return text?.trim() || null; } + // Check if table is loading + getLoadingIndicator(): Locator { + return this.page.locator("[data-testid='table-loading']"); + } + + async isLoading(): Promise { + return await this.getLoadingIndicator().isVisible(); + } + // Wait for table to load async waitForTableLoad(): Promise { await this.page.waitForSelector(".TAssociations", { state: "visible" }); // Wait for loading to finish if present - const loadingVisible = await this.page.locator("[data-testid='table-loading']") + const loadingVisible = await this.getLoadingIndicator() .isVisible() .catch(() => false); if (loadingVisible) { - await this.page.locator("[data-testid='table-loading']").waitFor({ state: "hidden" }); + await this.getLoadingIndicator().waitFor({ state: "hidden" }); } // Wait for at least one row to be present with actual content await this.page.waitForSelector("[data-testid^='table-row-core']", { state: "visible" }); diff --git a/packages/platform-test/POM/page/disease/disease.ts b/packages/platform-test/POM/page/disease/disease.ts index 8341e4ff9..3f066e8f8 100644 --- a/packages/platform-test/POM/page/disease/disease.ts +++ b/packages/platform-test/POM/page/disease/disease.ts @@ -31,7 +31,7 @@ export class DiseasePage { } getXrefLinks(): Locator { - return this.page.locator('[data-testid="header-external-links"] a[href*="identifiers.org"]'); + return this.page.locator('[data-testid^="header-external-links-"] a'); } async getXrefLinksCount(): Promise { diff --git a/packages/platform-test/POM/page/target/index.ts b/packages/platform-test/POM/page/target/index.ts new file mode 100644 index 000000000..f88cc2544 --- /dev/null +++ b/packages/platform-test/POM/page/target/index.ts @@ -0,0 +1 @@ +export { TargetPage } from "./target"; diff --git a/packages/platform-test/POM/page/target/target.ts b/packages/platform-test/POM/page/target/target.ts new file mode 100644 index 000000000..fbc699660 --- /dev/null +++ b/packages/platform-test/POM/page/target/target.ts @@ -0,0 +1,257 @@ +import type { Locator, Page } from "@playwright/test"; + +/** + * Target Page Object Model + * Handles navigation and interactions on the Target page + */ +export class TargetPage { + page: Page; + originalURL: string; + + constructor(page: Page) { + this.page = page; + this.originalURL = page.url(); + } + + /** + * Navigate to a target page by Ensembl ID + * @param ensgId - Ensembl gene ID (e.g., "ENSG00000157764") + */ + async goToTargetPage(ensgId: string): Promise { + await this.page.goto(`/target/${ensgId}`); + await this.page.waitForLoadState("networkidle"); + } + + /** + * Get the URL for the profile page (removes /associations if present) + */ + getProfilePageUrl(): string { + return this.originalURL.replace(/\/associations$/, ""); + } + + /** + * Navigate to the profile tab + */ + async goToProfilePage(): Promise { + await this.page.goto(this.getProfilePageUrl()); + await this.page.waitForLoadState("networkidle"); + } + + /** + * Navigate to the associations tab + */ + async goToAssociationsPage(): Promise { + const baseUrl = this.getProfilePageUrl(); + await this.page.goto(`${baseUrl}/associations`); + await this.page.waitForLoadState("networkidle"); + } + + // Tab navigation + /** + * Get the Profile tab element + */ + getProfileTab(): Locator { + return this.page.locator("[role='tab']").filter({ hasText: /profile/i }); + } + + /** + * Get the Associated diseases tab element + */ + getAssociationsTab(): Locator { + return this.page.locator("[role='tab']").filter({ hasText: /associated diseases/i }); + } + + /** + * Click on the Profile tab + */ + async clickProfileTab(): Promise { + await this.getProfileTab().click(); + await this.page.waitForURL(/\/target\/[^/]+$/); + } + + /** + * Click on the Associated diseases tab + */ + async clickAssociationsTab(): Promise { + await this.getAssociationsTab().click(); + await this.page.waitForURL(/\/target\/[^/]+\/associations/); + } + + /** + * Check if Profile tab is active + */ + async isProfileTabActive(): Promise { + const tab = this.getProfileTab(); + const isSelected = await tab.getAttribute("aria-selected"); + return isSelected === "true"; + } + + /** + * Check if Associations tab is active + */ + async isAssociationsTabActive(): Promise { + const tab = this.getAssociationsTab(); + const isSelected = await tab.getAttribute("aria-selected"); + return isSelected === "true"; + } + + // External links in header + /** + * Get the Ensembl external link + */ + getEnsemblLink(): Locator { + return this.page.locator('a[href*="identifiers.org/ensembl"]'); + } + + /** + * Get the Ensembl link href attribute + */ + async getEnsemblLinkHref(): Promise { + return await this.getEnsemblLink().getAttribute("href"); + } + + /** + * Get all UniProt external links + */ + getUniProtLinks(): Locator { + return this.page.locator('a[href*="identifiers.org/uniprot"]'); + } + + /** + * Get the count of UniProt links + */ + async getUniProtLinksCount(): Promise { + return await this.getUniProtLinks().count(); + } + + /** + * Get the first UniProt link href attribute + */ + async getFirstUniProtLinkHref(): Promise { + return await this.getUniProtLinks().first().getAttribute("href"); + } + + /** + * Get the GeneCards external link + */ + getGeneCardsLink(): Locator { + return this.page.locator('a[href*="identifiers.org/genecards"]'); + } + + /** + * Get the GeneCards link href attribute + */ + async getGeneCardsLinkHref(): Promise { + return await this.getGeneCardsLink().getAttribute("href"); + } + + /** + * Get the HGNC external link + */ + getHGNCLink(): Locator { + return this.page.locator('a[href*="identifiers.org/hgnc.symbol"]'); + } + + /** + * Get the HGNC link href attribute + */ + async getHGNCLinkHref(): Promise { + return await this.getHGNCLink().getAttribute("href"); + } + + /** + * Get the CRISPR DepMap external link + */ + getCrisprDepMapLink(): Locator { + return this.page.locator('a[href*="depmap.org"]'); + } + + /** + * Get the CRISPR DepMap link href attribute (if available) + */ + async getCrisprDepMapLinkHref(): Promise { + const isVisible = await this.getCrisprDepMapLink() + .isVisible() + .catch(() => false); + if (!isVisible) return null; + return await this.getCrisprDepMapLink().getAttribute("href"); + } + + /** + * Get the TEP (Target Enabling Package) link + */ + getTEPLink(): Locator { + return this.page.locator('a[href*="thesgc.org/tep"]'); + } + + /** + * Get the TEP link href attribute (if available) + */ + async getTEPLinkHref(): Promise { + const isVisible = await this.getTEPLink() + .isVisible() + .catch(() => false); + if (!isVisible) return null; + return await this.getTEPLink().getAttribute("href"); + } + + /** + * Get all external links in the header + */ + getExternalLinks(): Locator { + return this.page.locator("[data-testid='external-links'] a"); + } + + /** + * Get the count of all external links + */ + async getExternalLinksCount(): Promise { + return await this.getExternalLinks().count(); + } + + // Page header elements + /** + * Get the target symbol (main title) in the header + */ + getTargetSymbol(): Locator { + return this.page.locator("[data-testid='profile-page-header-text']"); + } + + /** + * Get the target symbol text + */ + async getTargetSymbolText(): Promise { + return await this.getTargetSymbol().textContent(); + } + + /** + * Get the target name (subtitle) in the header + */ + getTargetName(): Locator { + return this.page.locator("[data-testid='profile-page-header-block'] h5"); + } + + /** + * Get the target name text + */ + async getTargetNameText(): Promise { + return await this.getTargetName().textContent(); + } + + /** + * Wait for the target page to load completely + */ + async waitForPageLoad(): Promise { + await this.page.waitForSelector("[data-testid='profile-page-header-text']", { state: "visible" }); + await this.page.waitForLoadState("networkidle"); + } + + /** + * Check if the page has loaded (header is visible) + */ + async isPageLoaded(): Promise { + return await this.getTargetSymbol() + .isVisible() + .catch(() => false); + } +} diff --git a/packages/platform-test/e2e/pages/target/targetAssociations.spec.ts b/packages/platform-test/e2e/pages/target/targetAssociations.spec.ts new file mode 100644 index 000000000..ef1ef6202 --- /dev/null +++ b/packages/platform-test/e2e/pages/target/targetAssociations.spec.ts @@ -0,0 +1,415 @@ +import { test, expect } from "../../../fixtures"; +import { TargetPage } from "../../../POM/page/target/target"; +import { AotfTable } from "../../../POM/objects/widgets/AOTF/aotfTable"; +import { AotfActions } from "../../../POM/objects/widgets/AOTF/aotfActions"; + +test.describe("Target Associated Diseases", () => { + test.beforeEach(async ({ page, baseURL, testConfig }) => { + const targetId = testConfig.target?.primary || "ENSG00000157764"; + await page.goto(`${baseURL}/target/${targetId}/associations`); + }); + + test.describe("Associations Table Visibility", () => { + test("Associations table is visible on the page", async ({ page }) => { + const aotfTable = new AotfTable(page); + await aotfTable.waitForTableLoad(); + + const table = aotfTable.getTable(); + await expect(table).toBeVisible(); + }); + + test("Table has the correct headers", async ({ page }) => { + const aotfTable = new AotfTable(page); + await aotfTable.waitForTableLoad(); + + // Check main headers (Disease/Target and Association Score) + const diseaseHeader = aotfTable.getTargetOrDiseaseHeader(); + await expect(diseaseHeader).toBeVisible(); + + const scoreHeader = aotfTable.getAssociationScoreHeader(); + await expect(scoreHeader).toBeVisible(); + + // Check that data source headers are present (entity columns) + const entityColumnHeaders = page.locator("[data-testid='associations-table-header'] [data-testid^='table-header-']"); + const headerCount = await entityColumnHeaders.count(); + + // Should have: Disease/Target, Association Score, plus multiple data source columns + // (genetic_association, somatic_mutation, known_drug, etc.) + expect(headerCount).toBeGreaterThan(2); + }); + + test("Table loads with data rows", async ({ page }) => { + const aotfTable = new AotfTable(page); + await aotfTable.waitForTableLoad(); + + const rowCount = await aotfTable.getRowCount(); + expect(rowCount).toBeGreaterThan(0); + }); + + test("Table is not in loading state after load", async ({ page }) => { + const aotfTable = new AotfTable(page); + await aotfTable.waitForTableLoad(); + + const isLoading = await aotfTable.isLoading(); + expect(isLoading).toBe(false); + }); + }); + + test.describe("Table Data", () => { + test("Each row displays disease name", async ({ page }) => { + const aotfTable = new AotfTable(page); + await aotfTable.waitForTableLoad(); + + const rowCount = await aotfTable.getRowCount(); + + if (rowCount > 0) { + const diseaseName = await aotfTable.getEntityName(0); + expect(diseaseName).toBeTruthy(); + expect(diseaseName?.trim().length).toBeGreaterThan(0); + } + }); + + test("Each row displays association score", async ({ page }) => { + const aotfTable = new AotfTable(page); + await aotfTable.waitForTableLoad(); + + const rowCount = await aotfTable.getRowCount(); + + if (rowCount > 0) { + const score = await aotfTable.getAssociationScoreValue(0); + expect(score).toBeTruthy(); + } + }); + + test("Association scores are numeric values", async ({ page }) => { + const aotfTable = new AotfTable(page); + await aotfTable.waitForTableLoad(); + + const rowCount = await aotfTable.getRowCount(); + + if (rowCount > 0) { + const score = await aotfTable.getAssociationScoreValue(0); + if (score) { + const numericScore = parseFloat(score); + expect(numericScore).toBeGreaterThan(0); + expect(numericScore).toBeLessThanOrEqual(1); + } + } + }); + + test("Can retrieve data from multiple rows", async ({ page }) => { + const aotfTable = new AotfTable(page); + await aotfTable.waitForTableLoad(); + + const rowCount = await aotfTable.getRowCount(); + + if (rowCount >= 2) { + const firstDisease = await aotfTable.getEntityName(0); + const secondDisease = await aotfTable.getEntityName(1); + + expect(firstDisease).toBeTruthy(); + expect(secondDisease).toBeTruthy(); + expect(firstDisease).not.toBe(secondDisease); + } + }); + }); + + test.describe("Table Pagination", () => { + test("Pagination controls are visible when there are multiple pages", async ({ page }) => { + const aotfTable = new AotfTable(page); + await aotfTable.waitForTableLoad(); + + const paginationContainer = aotfTable.getPaginationContainer(); + const isVisible = await paginationContainer.isVisible().catch(() => false); + + // Pagination should be visible if there's enough data + if (isVisible) { + await expect(paginationContainer).toBeVisible(); + } + }); + + test("Can navigate to next page if available", async ({ page }) => { + const aotfTable = new AotfTable(page); + await aotfTable.waitForTableLoad(); + + const nextButton = aotfTable.getNextPageButton(); + const isEnabled = await nextButton.isEnabled().catch(() => false); + + if (isEnabled) { + // Get first row data before pagination + const firstRowBeforePage = await aotfTable.getEntityName(0); + + await aotfTable.clickNextPage(); + await page.waitForTimeout(1000); // Wait for data to load + + // Get first row data after pagination + const firstRowAfterPage = await aotfTable.getEntityName(0); + + // Data should be different after pagination + expect(firstRowBeforePage).not.toBe(firstRowAfterPage); + } + }); + + test("Can navigate back to previous page", async ({ page }) => { + const aotfTable = new AotfTable(page); + await aotfTable.waitForTableLoad(); + + const nextButton = aotfTable.getNextPageButton(); + const isNextEnabled = await nextButton.isEnabled().catch(() => false); + + if (isNextEnabled) { + // Go to next page + await aotfTable.clickNextPage(); + await page.waitForTimeout(1000); + + // Go back to previous page + const prevButton = aotfTable.getPreviousPageButton(); + const isPrevEnabled = await prevButton.isEnabled(); + expect(isPrevEnabled).toBe(true); + + await aotfTable.clickPreviousPage(); + await page.waitForTimeout(1000); + + // Verify we're back on first page + const rowCount = await aotfTable.getRowCount(); + expect(rowCount).toBeGreaterThan(0); + } + }); + + test("Can change page size", async ({ page }) => { + const aotfTable = new AotfTable(page); + await aotfTable.waitForTableLoad(); + + const pageSelector = aotfTable.getPageSizeSelector(); + const isVisible = await pageSelector.isVisible().catch(() => false); + + if (isVisible) { + const rowCountBefore = await aotfTable.getRowCount(); + + await aotfTable.selectPageSize("50"); + await page.waitForTimeout(1000); + + const rowCountAfter = await aotfTable.getRowCount(); + + // Row count should increase when changing from default (25) to 50 + // unless dataset has fewer than 25 rows + if (rowCountBefore === 25) { + expect(rowCountAfter).toBeGreaterThanOrEqual(rowCountBefore); + } else { + expect(rowCountAfter).toBeGreaterThan(0); + } + } + }); + }); + + test.describe("Table Sorting", () => { + test("Can sort by GWAS associations", async ({ page }) => { + const aotfTable = new AotfTable(page); + const aotfActions = new AotfActions(page); + await aotfTable.waitForTableLoad(); + + // Verify no sort filter is active initially (default sort by Association Score) + const initialSortActive = await aotfActions.hasSortFilter(); + expect(initialSortActive).toBe(false); + + // Click to sort by a different column (GWAS associations) + await aotfTable.sortByColumn("GWAS associations"); + await page.waitForTimeout(1000); + + // Verify sort filter is now active in the ActiveFiltersPanel + const sortActive = await aotfActions.hasSortFilter(); + expect(sortActive).toBe(true); + + // Verify the sort filter shows the correct column name + const sortFilterText = await aotfActions.getSortFilterText(); + expect(sortFilterText).toContain("GWAS associations"); + }); + }); + + test.describe("Table Row Interactions", () => { + test("Can find a specific disease in the table by name", async ({ page }) => { + const aotfTable = new AotfTable(page); + await aotfTable.waitForTableLoad(); + + // Get the first disease name + const firstDisease = await aotfTable.getEntityName(0); + + if (firstDisease) { + // Find the row by name + const row = await aotfTable.getRowByName(firstDisease); + await expect(row).toBeVisible(); + } + }); + + test("Can pin a disease row", async ({ page }) => { + const aotfTable = new AotfTable(page); + await aotfTable.waitForTableLoad(); + + // Open context menu for first row and pin + const contextMenu = aotfTable.getContextMenuForRow(0); + await contextMenu.click(); + + const pinButton = aotfTable.getPinEntityButton(); + await expect(pinButton).toBeVisible(); + await pinButton.click(); + + await page.waitForTimeout(500); + + // Verify pinned section appears + const pinnedSection = aotfTable.getPinnedSection(); + await expect(pinnedSection).toBeVisible(); + }); + + test("Pinned entries can be deleted", async ({ page }) => { + const aotfTable = new AotfTable(page); + await aotfTable.waitForTableLoad(); + + // Pin a row first via context menu + await aotfTable.pinRow(0); + await page.waitForTimeout(500); + + // Verify pinned section appears + const pinnedSection = aotfTable.getPinnedSection(); + await expect(pinnedSection).toBeVisible(); + + // Delete pinned entries + const deleteButton = aotfTable.getDeletePinnedButton(); + await expect(deleteButton).toBeVisible(); + await deleteButton.click(); + + await page.waitForTimeout(500); + + // Pinned section should no longer be visible + await expect(pinnedSection).not.toBeVisible(); + }); + }); + + test.describe("Table Data Cells", () => { + test("Rows have data cells with evidence scores", async ({ page }) => { + const aotfTable = new AotfTable(page); + await aotfTable.waitForTableLoad(); + + const rowWithData = await aotfTable.findFirstRowWithData(); + + if (rowWithData !== null) { + const cellsWithScores = await aotfTable.getDataCellsWithScores(rowWithData); + expect(cellsWithScores.length).toBeGreaterThan(0); + + // Verify scores are valid numbers + cellsWithScores.forEach(cell => { + expect(cell.score).toBeGreaterThan(0); + expect(cell.score).toBeLessThanOrEqual(1); + }); + } + }); + + test("Can click on data cells", async ({ page }) => { + const aotfTable = new AotfTable(page); + await aotfTable.waitForTableLoad(); + + const rowWithData = await aotfTable.findFirstRowWithData(); + + if (rowWithData !== null) { + const cellsWithScores = await aotfTable.getDataCellsWithScores(rowWithData); + + if (cellsWithScores.length > 0) { + const firstCell = cellsWithScores[0]; + + // Click on the data cell + await aotfTable.clickDataCell(rowWithData, firstCell.columnId); + await page.waitForTimeout(500); + + // This should trigger some UI change (evidence panel, etc.) + // Just verify the click was registered without error + expect(true).toBe(true); + } + } + }); + }); + + test.describe("Table Sections", () => { + test("Core section toggle appears after pinning an entity", async ({ page }) => { + const aotfTable = new AotfTable(page); + await aotfTable.waitForTableLoad(); + + // Core section toggle only appears when pinned/uploaded sections exist + // First, pin a row to make the section toggles appear + await aotfTable.pinRow(0); + await page.waitForTimeout(500); + + // Now the core section toggle should be visible + const coreSection = aotfTable.getCoreSection(); + await expect(coreSection).toBeVisible(); + + // Cleanup: delete pinned entries + await aotfTable.deletePinnedEntries(); + }); + + test("Can toggle core section visibility after pinning", async ({ page }) => { + const aotfTable = new AotfTable(page); + await aotfTable.waitForTableLoad(); + + // Pin a row to make section toggles appear + await aotfTable.pinRow(0); + await page.waitForTimeout(500); + + // Toggle core section off + await aotfTable.toggleCoreSection(); + await page.waitForTimeout(500); + + // Toggle it back on + await aotfTable.toggleCoreSection(); + await page.waitForTimeout(500); + + // Table should still be visible + const table = aotfTable.getTable(); + await expect(table).toBeVisible(); + + // Cleanup + await aotfTable.deletePinnedEntries(); + }); + }); + + test.describe("Integration with Target Page", () => { + test("Can navigate from associations back to profile", async ({ page, testConfig }) => { + const targetPage = new TargetPage(page); + await targetPage.waitForPageLoad(); + + // Verify we're on associations page + const isAssociationsActive = await targetPage.isAssociationsTabActive(); + expect(isAssociationsActive).toBe(true); + + // Navigate to profile + await targetPage.clickProfileTab(); + + // Verify we're on profile page + const isProfileActive = await targetPage.isProfileTabActive(); + expect(isProfileActive).toBe(true); + }); + + test("Associations table maintains state when navigating away and back", async ({ + page, + testConfig, + }) => { + const aotfTable = new AotfTable(page); + await aotfTable.waitForTableLoad(); + + const targetPage = new TargetPage(page); + + // Get first disease name + const firstDisease = await aotfTable.getEntityName(0); + + // Navigate to profile + await targetPage.clickProfileTab(); + await page.waitForTimeout(500); + + // Navigate back to associations + await targetPage.clickAssociationsTab(); + await aotfTable.waitForTableLoad(); + + // Verify data is still there + const firstDiseaseAfter = await aotfTable.getEntityName(0); + expect(firstDiseaseAfter).toBe(firstDisease); + }); + }); +}); diff --git a/packages/platform-test/e2e/pages/target/targetPage.spec.ts b/packages/platform-test/e2e/pages/target/targetPage.spec.ts new file mode 100644 index 000000000..2a233b3fe --- /dev/null +++ b/packages/platform-test/e2e/pages/target/targetPage.spec.ts @@ -0,0 +1,223 @@ +import { test, expect } from "../../../fixtures"; +import { TargetPage } from "../../../POM/page/target/target"; + +test.describe("Target Page - Header and Navigation", () => { + test.beforeEach(async ({ page, baseURL, testConfig }) => { + const targetId = testConfig.target?.primary || "ENSG00000157764"; + await page.goto(`${baseURL}/target/${targetId}`); + }); + + test.describe("Page Header", () => { + test("Target page loads successfully with correct title and name", async ({ + page, + testConfig, + }) => { + const targetPage = new TargetPage(page); + await targetPage.waitForPageLoad(); + + const isLoaded = await targetPage.isPageLoaded(); + expect(isLoaded).toBe(true); + + const symbolText = await targetPage.getTargetSymbolText(); + expect(symbolText).toBeTruthy(); + expect(symbolText?.trim().length).toBeGreaterThan(0); + + const nameText = await targetPage.getTargetNameText(); + expect(nameText).toBeTruthy(); + expect(nameText?.trim().length).toBeGreaterThan(0); + }); + }); + + test.describe("External Links", () => { + test("Ensembl link is present and correct", async ({ page, testConfig }) => { + const targetPage = new TargetPage(page); + await targetPage.waitForPageLoad(); + + const ensemblLink = targetPage.getEnsemblLink(); + await expect(ensemblLink).toBeVisible(); + + const href = await targetPage.getEnsemblLinkHref(); + const targetId = testConfig.target?.primary || "ENSG00000157764"; + expect(href).toContain("identifiers.org/ensembl"); + expect(href).toContain(targetId); + }); + + test("UniProt links are present when protein IDs exist", async ({ page }) => { + const targetPage = new TargetPage(page); + await targetPage.waitForPageLoad(); + + const uniprotLinksCount = await targetPage.getUniProtLinksCount(); + expect(uniprotLinksCount).toBeGreaterThanOrEqual(1); + + if (uniprotLinksCount > 0) { + const firstHref = await targetPage.getFirstUniProtLinkHref(); + expect(firstHref).toContain("identifiers.org/uniprot"); + } + }); + + test("GeneCards link is present and correct", async ({ page }) => { + const targetPage = new TargetPage(page); + await targetPage.waitForPageLoad(); + + const geneCardsLink = targetPage.getGeneCardsLink(); + await expect(geneCardsLink).toBeVisible(); + + const href = await targetPage.getGeneCardsLinkHref(); + expect(href).toContain("identifiers.org/genecards"); + }); + + test("HGNC link is present and correct", async ({ page }) => { + const targetPage = new TargetPage(page); + await targetPage.waitForPageLoad(); + + const hgncLink = targetPage.getHGNCLink(); + await expect(hgncLink).toBeVisible(); + + const href = await targetPage.getHGNCLinkHref(); + expect(href).toContain("identifiers.org/hgnc.symbol"); + }); + + test("External links section has multiple links", async ({ page }) => { + const targetPage = new TargetPage(page); + await targetPage.waitForPageLoad(); + + const linksCount = await targetPage.getExternalLinksCount(); + // At minimum: Ensembl, UniProt, GeneCards, HGNC + expect(linksCount).toBeGreaterThanOrEqual(4); + }); + + test("CRISPR DepMap link is present when target is in DepMap", async ({ page }) => { + const targetPage = new TargetPage(page); + await targetPage.waitForPageLoad(); + + // This link may not always be present, so we check conditionally + const crisprHref = await targetPage.getCrisprDepMapLinkHref(); + if (crisprHref) { + expect(crisprHref).toContain("depmap.org"); + } + }); + + test("TEP link is present when target has TEP data", async ({ page }) => { + const targetPage = new TargetPage(page); + await targetPage.waitForPageLoad(); + + // This link may not always be present, so we check conditionally + const tepHref = await targetPage.getTEPLinkHref(); + if (tepHref) { + expect(tepHref).toContain("thesgc.org/tep"); + } + }); + }); + + test.describe("Tab Navigation", () => { + test("Both Profile and Associated diseases tabs are visible", async ({ page }) => { + const targetPage = new TargetPage(page); + await targetPage.waitForPageLoad(); + + const profileTab = targetPage.getProfileTab(); + await expect(profileTab).toBeVisible(); + + const associationsTab = targetPage.getAssociationsTab(); + await expect(associationsTab).toBeVisible(); + }); + + test("Profile tab is active by default", async ({ page }) => { + const targetPage = new TargetPage(page); + await targetPage.waitForPageLoad(); + + const isActive = await targetPage.isProfileTabActive(); + expect(isActive).toBe(true); + }); + + test("Can navigate to Associated diseases tab", async ({ page, testConfig }) => { + const targetPage = new TargetPage(page); + await targetPage.waitForPageLoad(); + + await targetPage.clickAssociationsTab(); + + const isActive = await targetPage.isAssociationsTabActive(); + expect(isActive).toBe(true); + + const targetId = testConfig.target?.primary || "ENSG00000157764"; + await expect(page).toHaveURL(new RegExp(`/target/${targetId}/associations`)); + }); + + test("Can navigate back to Profile tab from Associated diseases", async ({ page }) => { + const targetPage = new TargetPage(page); + await targetPage.waitForPageLoad(); + + // Navigate to associations + await targetPage.clickAssociationsTab(); + await page.waitForTimeout(500); + + // Navigate back to profile + await targetPage.clickProfileTab(); + + const isActive = await targetPage.isProfileTabActive(); + expect(isActive).toBe(true); + }); + + test("URL changes when switching between tabs", async ({ page, testConfig }) => { + const targetPage = new TargetPage(page); + await targetPage.waitForPageLoad(); + + const targetId = testConfig.target?.primary || "ENSG00000157764"; + + // Check profile URL + await expect(page).toHaveURL(new RegExp(`/target/${targetId}$`)); + + // Switch to associations + await targetPage.clickAssociationsTab(); + await expect(page).toHaveURL(new RegExp(`/target/${targetId}/associations`)); + + // Switch back to profile + await targetPage.clickProfileTab(); + await expect(page).toHaveURL(new RegExp(`/target/${targetId}$`)); + }); + }); + + test.describe("Direct Navigation", () => { + test("Can navigate directly to profile page", async ({ page, baseURL, testConfig }) => { + const targetId = testConfig.target?.primary || "ENSG00000157764"; + const targetPage = new TargetPage(page); + + await targetPage.goToTargetPage(targetId); + await targetPage.waitForPageLoad(); + + const isLoaded = await targetPage.isPageLoaded(); + expect(isLoaded).toBe(true); + + const isActive = await targetPage.isProfileTabActive(); + expect(isActive).toBe(true); + }); + + test("Can navigate directly to associations page", async ({ page, baseURL, testConfig }) => { + const targetId = testConfig.target?.primary || "ENSG00000157764"; + await page.goto(`${baseURL}/target/${targetId}/associations`); + + const targetPage = new TargetPage(page); + await targetPage.waitForPageLoad(); + + const isActive = await targetPage.isAssociationsTabActive(); + expect(isActive).toBe(true); + }); + }); + + test.describe("Page Title and Meta", () => { + test("Profile page has correct title", async ({ page }) => { + const targetPage = new TargetPage(page); + await targetPage.waitForPageLoad(); + + const title = await page.title(); + expect(title).toContain("profile page"); + }); + + test("Associations page has correct title", async ({ page, baseURL, testConfig }) => { + const targetId = testConfig.target?.primary || "ENSG00000157764"; + await page.goto(`${baseURL}/target/${targetId}/associations`); + + const title = await page.title(); + expect(title).toContain("Diseases associated with"); + }); + }); +}); diff --git a/packages/platform-test/e2e/pages/target/targetProfile.spec.ts b/packages/platform-test/e2e/pages/target/targetProfile.spec.ts new file mode 100644 index 000000000..0b2fe3235 --- /dev/null +++ b/packages/platform-test/e2e/pages/target/targetProfile.spec.ts @@ -0,0 +1,299 @@ +import { test, expect } from "../../../fixtures"; +import { ProfileHeader } from "../../../POM/objects/components/ProfileHeader/profileHeader"; +import { ClinicalPrecedenceSection } from "../../../POM/objects/widgets/KnownDrugs/knownDrugsSection"; +import { PharmacogenomicsSection } from "../../../POM/objects/widgets/shared/pharmacogenomicsSection"; + +test.describe("Target Profile Page", () => { + test.beforeEach(async ({ page, baseURL, testConfig }) => { + const targetId = testConfig.target?.primary || "ENSG00000157764"; + await page.goto(`${baseURL}/target/${targetId}`); + }); + + test.describe("Profile Header", () => { + test("Profile header is visible and loads correctly", async ({ page }) => { + const profileHeader = new ProfileHeader(page); + await profileHeader.waitForProfileHeaderLoad(); + + const isVisible = await profileHeader.isProfileHeaderVisible(); + expect(isVisible).toBe(true); + }); + + test("Description section is visible and has content", async ({ page }) => { + const profileHeader = new ProfileHeader(page); + await profileHeader.waitForProfileHeaderLoad(); + + const isDescriptionVisible = await profileHeader.isDescriptionVisible(); + expect(isDescriptionVisible).toBe(true); + + const descriptionText = await profileHeader.getDescriptionText(); + expect(descriptionText).toBeTruthy(); + expect(descriptionText?.length).toBeGreaterThan(0); + }); + + test("Description contains relevant target information", async ({ page }) => { + const profileHeader = new ProfileHeader(page); + await profileHeader.waitForProfileHeaderLoad(); + + const descriptionText = await profileHeader.getDescriptionText(); + + // Check that description is not the default "No description available" message + if (descriptionText) { + expect(descriptionText.toLowerCase()).not.toContain("no description available"); + } + }); + + test("Synonyms section displays when synonyms exist", async ({ page }) => { + const profileHeader = new ProfileHeader(page); + await profileHeader.waitForProfileHeaderLoad(); + + // Check if synonyms section exists + const isSynonymsVisible = await profileHeader.isSynonymsSectionVisible(); + + // If synonyms are visible, verify they have content + if (isSynonymsVisible) { + const synonymsCount = await profileHeader.getSynonymsCount(); + expect(synonymsCount).toBeGreaterThan(0); + + // Check first synonym has text + const firstSynonymText = await profileHeader.getSynonymText(0); + expect(firstSynonymText).toBeTruthy(); + } + }); + + test("Can interact with synonym chips", async ({ page }) => { + const profileHeader = new ProfileHeader(page); + await profileHeader.waitForProfileHeaderLoad(); + + const isSynonymsVisible = await profileHeader.isSynonymsSectionVisible(); + + if (isSynonymsVisible) { + const synonymsCount = await profileHeader.getSynonymsCount(); + + if (synonymsCount > 0) { + // Hover over first synonym to trigger tooltip + await profileHeader.hoverSynonymChip(0); + await page.waitForTimeout(300); // Wait for tooltip + + // Verify chip is still visible after hover + const chipVisible = await profileHeader.getSynonymChip(0).isVisible(); + expect(chipVisible).toBe(true); + } + } + }); + + test("Genomic location is displayed when available", async ({ page }) => { + const profileHeader = new ProfileHeader(page); + await profileHeader.waitForProfileHeaderLoad(); + + // Check for genomic location badge (GRCh38) + const genomicLocationBadge = page.locator("[data-testid='profile-header']").getByText("GRCh38"); + const isVisible = await genomicLocationBadge.isVisible().catch(() => false); + + if (isVisible) { + // Verify the badge is present + await expect(genomicLocationBadge).toBeVisible(); + } + }); + + test("Core essential gene chip is displayed when target is essential", async ({ page }) => { + const profileHeader = new ProfileHeader(page); + await profileHeader.waitForProfileHeaderLoad(); + + // Check for core essential gene chip + const essentialChip = page.locator("[data-testid='profile-header']").getByText("Core essential gene"); + const isVisible = await essentialChip.isVisible().catch(() => false); + + if (isVisible) { + await expect(essentialChip).toBeVisible(); + } + }); + }); + + test.describe("Profile Sections Visibility", () => { + test("Known Drugs section is visible when target has drug data", async ({ page }) => { + const knownDrugsSection = new ClinicalPrecedenceSection(page); + + // Wait a bit for section to potentially load + await page.waitForTimeout(2000); + + const isVisible = await knownDrugsSection.isSectionVisible().catch(() => false); + + if (isVisible) { + await knownDrugsSection.waitForSectionLoad(); + const title = await knownDrugsSection.getSectionTitle(); + expect(title).toBeTruthy(); + } + }); + + test("Pharmacogenomics section is visible when target has pharmacogenomics data", async ({ + page, + }) => { + const pharmacoSection = new PharmacogenomicsSection(page); + + // Wait a bit for section to potentially load + await page.waitForTimeout(2000); + + const isVisible = await pharmacoSection.isSectionVisible().catch(() => false); + + if (isVisible) { + await pharmacoSection.waitForLoad(); + expect(isVisible).toBe(true); + } + }); + + test("Profile page displays section summaries", async ({ page }) => { + // Check for summary container + const summaryContainer = page.locator("[data-testid='summary-container']"); + const isVisible = await summaryContainer.isVisible().catch(() => false); + + if (isVisible) { + await expect(summaryContainer).toBeVisible(); + } + }); + + test("Section container is present on profile page", async ({ page }) => { + // Check for section container + const sectionContainer = page.locator("[data-testid='section-container']"); + const isVisible = await sectionContainer.isVisible().catch(() => false); + + if (isVisible) { + await expect(sectionContainer).toBeVisible(); + } + }); + }); + + test.describe("Profile Content Loading", () => { + test("Profile page loads without errors", async ({ page }) => { + const profileHeader = new ProfileHeader(page); + await profileHeader.waitForProfileHeaderLoad(); + + // Verify basic content is loaded + const isLoaded = await profileHeader.isProfileHeaderVisible(); + expect(isLoaded).toBe(true); + }); + + test("Profile description does not show loading state after page load", async ({ page }) => { + const profileHeader = new ProfileHeader(page); + await profileHeader.waitForProfileHeaderLoad(); + + // Check that skeleton loader is not present + const skeleton = page.locator(".MuiSkeleton-root"); + const hasSkeletons = await skeleton.count(); + + // Give time for skeletons to disappear + await page.waitForTimeout(2000); + + // Verify description is loaded (not skeleton) + const isDescriptionVisible = await profileHeader.isDescriptionVisible(); + expect(isDescriptionVisible).toBe(true); + }); + + test("Multiple synonyms can be displayed", async ({ page }) => { + const profileHeader = new ProfileHeader(page); + await profileHeader.waitForProfileHeaderLoad(); + + const isSynonymsVisible = await profileHeader.isSynonymsSectionVisible(); + + if (isSynonymsVisible) { + const synonymsCount = await profileHeader.getSynonymsCount(); + + // Verify we can get text from multiple synonyms if they exist + if (synonymsCount > 1) { + const firstSynonym = await profileHeader.getSynonymText(0); + const secondSynonym = await profileHeader.getSynonymText(1); + + expect(firstSynonym).toBeTruthy(); + expect(secondSynonym).toBeTruthy(); + expect(firstSynonym).not.toBe(secondSynonym); + } + } + }); + }); + + test.describe("Profile Interaction", () => { + test("Can expand description if it's truncated", async ({ page }) => { + const profileHeader = new ProfileHeader(page); + await profileHeader.waitForProfileHeaderLoad(); + + // Check for "show more" link + const showMoreLink = page.locator("[data-testid='profile-description']").getByText("show more"); + const hasShowMore = await showMoreLink.isVisible().catch(() => false); + + if (hasShowMore) { + await showMoreLink.click(); + await page.waitForTimeout(300); + + // Check for "hide" link after expansion + const hideLink = page.locator("[data-testid='profile-description']").getByText("hide"); + await expect(hideLink).toBeVisible(); + } + }); + + test("Can collapse description after expanding", async ({ page }) => { + const profileHeader = new ProfileHeader(page); + await profileHeader.waitForProfileHeaderLoad(); + + const showMoreLink = page.locator("[data-testid='profile-description']").getByText("show more"); + const hasShowMore = await showMoreLink.isVisible().catch(() => false); + + if (hasShowMore) { + // Expand + await showMoreLink.click(); + await page.waitForTimeout(300); + + // Collapse + const hideLink = page.locator("[data-testid='profile-description']").getByText("hide"); + await hideLink.click(); + await page.waitForTimeout(300); + + // Verify "show more" is back + const showMoreAgain = page.locator("[data-testid='profile-description']").getByText("show more"); + await expect(showMoreAgain).toBeVisible(); + } + }); + }); + + test.describe("Profile Data Validation", () => { + test("Target profile displays unique content based on target ID", async ({ + page, + testConfig, + }) => { + const profileHeader = new ProfileHeader(page); + await profileHeader.waitForProfileHeaderLoad(); + + const descriptionText = await profileHeader.getDescriptionText(); + + // Verify we got some description + expect(descriptionText).toBeTruthy(); + + // Description should be meaningful (more than just whitespace) + expect(descriptionText?.trim().length).toBeGreaterThan(10); + }); + + test("Synonyms have source information in tooltips", async ({ page }) => { + const profileHeader = new ProfileHeader(page); + await profileHeader.waitForProfileHeaderLoad(); + + const isSynonymsVisible = await profileHeader.isSynonymsSectionVisible(); + + if (isSynonymsVisible) { + const synonymsCount = await profileHeader.getSynonymsCount(); + + if (synonymsCount > 0) { + // Hover to potentially see tooltip + await profileHeader.hoverSynonymChip(0); + await page.waitForTimeout(500); + + // Check if tooltip appeared (it should contain "Source:") + const tooltip = page.locator("[role='tooltip']"); + const tooltipVisible = await tooltip.isVisible().catch(() => false); + + if (tooltipVisible) { + const tooltipText = await tooltip.textContent(); + expect(tooltipText).toContain("Source"); + } + } + } + }); + }); +}); diff --git a/packages/platform-test/fixtures/testConfig.ts b/packages/platform-test/fixtures/testConfig.ts index 1375a8ab7..cb40141a4 100644 --- a/packages/platform-test/fixtures/testConfig.ts +++ b/packages/platform-test/fixtures/testConfig.ts @@ -26,6 +26,7 @@ export interface TestConfig { target?: { primary?: string; alternatives?: string[]; + aotfDiseases?: string[]; }; disease: { primary: string; diff --git a/packages/platform-test/playwright-report/index.html b/packages/platform-test/playwright-report/index.html index 5fca9fec5..7ca8822ca 100644 --- a/packages/platform-test/playwright-report/index.html +++ b/packages/platform-test/playwright-report/index.html @@ -73,4 +73,4 @@
    - \ No newline at end of file + \ No newline at end of file diff --git a/packages/ui/src/components/ExternalLink/XRefLinks.tsx b/packages/ui/src/components/ExternalLink/XRefLinks.tsx index 533abce38..dfe184efa 100644 --- a/packages/ui/src/components/ExternalLink/XRefLinks.tsx +++ b/packages/ui/src/components/ExternalLink/XRefLinks.tsx @@ -27,7 +27,7 @@ function XRefLinks({ label, urlBuilder, urlStem, ids, names, limit }: XRefLinksP }; return ( - + {label}:{" "} {ids.map((id, i) => ( limit - 1 && !showMore ? displayNone : {}}> From 107105dbd4199bf9c499650900b8347f11ab52fb Mon Sep 17 00:00:00 2001 From: david oluwasusi Date: Wed, 14 Jan 2026 10:18:40 +0000 Subject: [PATCH 21/34] chore(linting): fix lint errors and warnings --- .../components/ProfileHeader/profileHeader.ts | 4 ++- .../platform-test/POM/page/target/target.ts | 4 ++- .../pages/target/targetAssociations.spec.ts | 23 +++++++-------- .../e2e/pages/target/targetPage.spec.ts | 9 ++---- .../e2e/pages/target/targetProfile.spec.ts | 29 ++++++++++++------- 5 files changed, 38 insertions(+), 31 deletions(-) diff --git a/packages/platform-test/POM/objects/components/ProfileHeader/profileHeader.ts b/packages/platform-test/POM/objects/components/ProfileHeader/profileHeader.ts index 0b2e48996..63abfd2aa 100644 --- a/packages/platform-test/POM/objects/components/ProfileHeader/profileHeader.ts +++ b/packages/platform-test/POM/objects/components/ProfileHeader/profileHeader.ts @@ -38,7 +38,9 @@ export class ProfileHeader { async getDescriptionText(): Promise { // Get the paragraph element that is a sibling following the Description heading // Use xpath to find the paragraph sibling after the heading - const descriptionParagraph = this.getDescriptionHeading().locator("xpath=following-sibling::p[1]"); + const descriptionParagraph = this.getDescriptionHeading().locator( + "xpath=following-sibling::p[1]" + ); return await descriptionParagraph.textContent(); } diff --git a/packages/platform-test/POM/page/target/target.ts b/packages/platform-test/POM/page/target/target.ts index fbc699660..43ca989ed 100644 --- a/packages/platform-test/POM/page/target/target.ts +++ b/packages/platform-test/POM/page/target/target.ts @@ -242,7 +242,9 @@ export class TargetPage { * Wait for the target page to load completely */ async waitForPageLoad(): Promise { - await this.page.waitForSelector("[data-testid='profile-page-header-text']", { state: "visible" }); + await this.page.waitForSelector("[data-testid='profile-page-header-text']", { + state: "visible", + }); await this.page.waitForLoadState("networkidle"); } diff --git a/packages/platform-test/e2e/pages/target/targetAssociations.spec.ts b/packages/platform-test/e2e/pages/target/targetAssociations.spec.ts index ef1ef6202..395c7f523 100644 --- a/packages/platform-test/e2e/pages/target/targetAssociations.spec.ts +++ b/packages/platform-test/e2e/pages/target/targetAssociations.spec.ts @@ -1,7 +1,7 @@ -import { test, expect } from "../../../fixtures"; -import { TargetPage } from "../../../POM/page/target/target"; -import { AotfTable } from "../../../POM/objects/widgets/AOTF/aotfTable"; +import { expect, test } from "../../../fixtures"; import { AotfActions } from "../../../POM/objects/widgets/AOTF/aotfActions"; +import { AotfTable } from "../../../POM/objects/widgets/AOTF/aotfTable"; +import { TargetPage } from "../../../POM/page/target/target"; test.describe("Target Associated Diseases", () => { test.beforeEach(async ({ page, baseURL, testConfig }) => { @@ -30,9 +30,11 @@ test.describe("Target Associated Diseases", () => { await expect(scoreHeader).toBeVisible(); // Check that data source headers are present (entity columns) - const entityColumnHeaders = page.locator("[data-testid='associations-table-header'] [data-testid^='table-header-']"); + const entityColumnHeaders = page.locator( + "[data-testid='associations-table-header'] [data-testid^='table-header-']" + ); const headerCount = await entityColumnHeaders.count(); - + // Should have: Disease/Target, Association Score, plus multiple data source columns // (genetic_association, somatic_mutation, known_drug, etc.) expect(headerCount).toBeGreaterThan(2); @@ -296,10 +298,10 @@ test.describe("Target Associated Diseases", () => { expect(cellsWithScores.length).toBeGreaterThan(0); // Verify scores are valid numbers - cellsWithScores.forEach(cell => { + for (const cell of cellsWithScores) { expect(cell.score).toBeGreaterThan(0); expect(cell.score).toBeLessThanOrEqual(1); - }); + } } }); @@ -371,7 +373,7 @@ test.describe("Target Associated Diseases", () => { }); test.describe("Integration with Target Page", () => { - test("Can navigate from associations back to profile", async ({ page, testConfig }) => { + test("Can navigate from associations back to profile", async ({ page }) => { const targetPage = new TargetPage(page); await targetPage.waitForPageLoad(); @@ -387,10 +389,7 @@ test.describe("Target Associated Diseases", () => { expect(isProfileActive).toBe(true); }); - test("Associations table maintains state when navigating away and back", async ({ - page, - testConfig, - }) => { + test("Associations table maintains state when navigating away and back", async ({ page }) => { const aotfTable = new AotfTable(page); await aotfTable.waitForTableLoad(); diff --git a/packages/platform-test/e2e/pages/target/targetPage.spec.ts b/packages/platform-test/e2e/pages/target/targetPage.spec.ts index 2a233b3fe..dd1d846d5 100644 --- a/packages/platform-test/e2e/pages/target/targetPage.spec.ts +++ b/packages/platform-test/e2e/pages/target/targetPage.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from "../../../fixtures"; +import { expect, test } from "../../../fixtures"; import { TargetPage } from "../../../POM/page/target/target"; test.describe("Target Page - Header and Navigation", () => { @@ -8,10 +8,7 @@ test.describe("Target Page - Header and Navigation", () => { }); test.describe("Page Header", () => { - test("Target page loads successfully with correct title and name", async ({ - page, - testConfig, - }) => { + test("Target page loads successfully with correct title and name", async ({ page }) => { const targetPage = new TargetPage(page); await targetPage.waitForPageLoad(); @@ -177,7 +174,7 @@ test.describe("Target Page - Header and Navigation", () => { }); test.describe("Direct Navigation", () => { - test("Can navigate directly to profile page", async ({ page, baseURL, testConfig }) => { + test("Can navigate directly to profile page", async ({ page, testConfig }) => { const targetId = testConfig.target?.primary || "ENSG00000157764"; const targetPage = new TargetPage(page); diff --git a/packages/platform-test/e2e/pages/target/targetProfile.spec.ts b/packages/platform-test/e2e/pages/target/targetProfile.spec.ts index 0b2fe3235..bf7c7abcc 100644 --- a/packages/platform-test/e2e/pages/target/targetProfile.spec.ts +++ b/packages/platform-test/e2e/pages/target/targetProfile.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from "../../../fixtures"; +import { expect, test } from "../../../fixtures"; import { ProfileHeader } from "../../../POM/objects/components/ProfileHeader/profileHeader"; import { ClinicalPrecedenceSection } from "../../../POM/objects/widgets/KnownDrugs/knownDrugsSection"; import { PharmacogenomicsSection } from "../../../POM/objects/widgets/shared/pharmacogenomicsSection"; @@ -86,7 +86,9 @@ test.describe("Target Profile Page", () => { await profileHeader.waitForProfileHeaderLoad(); // Check for genomic location badge (GRCh38) - const genomicLocationBadge = page.locator("[data-testid='profile-header']").getByText("GRCh38"); + const genomicLocationBadge = page + .locator("[data-testid='profile-header']") + .getByText("GRCh38"); const isVisible = await genomicLocationBadge.isVisible().catch(() => false); if (isVisible) { @@ -100,7 +102,9 @@ test.describe("Target Profile Page", () => { await profileHeader.waitForProfileHeaderLoad(); // Check for core essential gene chip - const essentialChip = page.locator("[data-testid='profile-header']").getByText("Core essential gene"); + const essentialChip = page + .locator("[data-testid='profile-header']") + .getByText("Core essential gene"); const isVisible = await essentialChip.isVisible().catch(() => false); if (isVisible) { @@ -178,7 +182,7 @@ test.describe("Target Profile Page", () => { // Check that skeleton loader is not present const skeleton = page.locator(".MuiSkeleton-root"); - const hasSkeletons = await skeleton.count(); + const _hasSkeletons = await skeleton.count(); // Give time for skeletons to disappear await page.waitForTimeout(2000); @@ -216,7 +220,9 @@ test.describe("Target Profile Page", () => { await profileHeader.waitForProfileHeaderLoad(); // Check for "show more" link - const showMoreLink = page.locator("[data-testid='profile-description']").getByText("show more"); + const showMoreLink = page + .locator("[data-testid='profile-description']") + .getByText("show more"); const hasShowMore = await showMoreLink.isVisible().catch(() => false); if (hasShowMore) { @@ -233,7 +239,9 @@ test.describe("Target Profile Page", () => { const profileHeader = new ProfileHeader(page); await profileHeader.waitForProfileHeaderLoad(); - const showMoreLink = page.locator("[data-testid='profile-description']").getByText("show more"); + const showMoreLink = page + .locator("[data-testid='profile-description']") + .getByText("show more"); const hasShowMore = await showMoreLink.isVisible().catch(() => false); if (hasShowMore) { @@ -247,17 +255,16 @@ test.describe("Target Profile Page", () => { await page.waitForTimeout(300); // Verify "show more" is back - const showMoreAgain = page.locator("[data-testid='profile-description']").getByText("show more"); + const showMoreAgain = page + .locator("[data-testid='profile-description']") + .getByText("show more"); await expect(showMoreAgain).toBeVisible(); } }); }); test.describe("Profile Data Validation", () => { - test("Target profile displays unique content based on target ID", async ({ - page, - testConfig, - }) => { + test("Target profile displays unique content based on target ID", async ({ page }) => { const profileHeader = new ProfileHeader(page); await profileHeader.waitForProfileHeaderLoad(); From 47774f3f26d84837b4b7763ac0a784f1a8f1625f Mon Sep 17 00:00:00 2001 From: david oluwasusi Date: Thu, 22 Jan 2026 13:44:36 +0000 Subject: [PATCH 22/34] feat(tests): add ability to configure scenarios from action --- .github/workflows/e2e-ci.yml | 11 ++ packages/platform-test/fixtures/testConfig.ts | 145 +++++++++++++++++- packages/platform-test/package.json | 6 +- yarn.lock | 24 +++ 4 files changed, 178 insertions(+), 8 deletions(-) diff --git a/.github/workflows/e2e-ci.yml b/.github/workflows/e2e-ci.yml index d908e0028..90815202b 100644 --- a/.github/workflows/e2e-ci.yml +++ b/.github/workflows/e2e-ci.yml @@ -12,6 +12,15 @@ on: options: - smoke - all + test_config_url: + description: 'Google Sheet CSV URL for test configuration (leave empty for defaults)' + required: false + type: string + test_scenario: + description: 'Scenario name from the CSV to use for tests' + required: false + default: 'Happy_Path_Full_Data' + type: string jobs: get_branch_name: @@ -69,6 +78,8 @@ jobs: env: PLAYWRIGHT_TEST_BASE_URL: "https://${{ needs.get_branch_name.outputs.branch }}--ot-platform.netlify.app" DEBUG: pw:api + TEST_CONFIG_URL: ${{ github.event.inputs.test_config_url }} + TEST_SCENARIO: ${{ github.event.inputs.test_scenario }} - uses: actions/upload-artifact@v4 if: ${{ !cancelled() }} with: diff --git a/packages/platform-test/fixtures/testConfig.ts b/packages/platform-test/fixtures/testConfig.ts index cb40141a4..09dbb24a6 100644 --- a/packages/platform-test/fixtures/testConfig.ts +++ b/packages/platform-test/fixtures/testConfig.ts @@ -1,3 +1,5 @@ +import Papa from "papaparse"; + /** * Test configuration interface * Defines the structure of test data used across E2E tests @@ -47,14 +49,127 @@ export interface TestConfig { } /** - * Mock function to simulate fetching config from external source - * In real implementation, this would make an API call to retrieve test data + * CSV row structure from Google Sheet + */ +interface CSVRow { + "Scenario Name": string; + drug_primary: string; + drug_with_warning: string; + drug_adverse_events: string; + variant_primary: string; + variant_with_molecular_structure: string; + variant_with_pharmacogenetics: string; + variant_with_qtl: string; + variant_with_eva: string; + target_primary: string; + target_alternative: string; + target_aotf_diseases: string; + disease_primary: string; + disease_name: string; + disease_alternatives: string; + disease_aotf_genes: string; + study_gwas: string; + study_qtl: string; +} + +/** + * Parse comma-separated values into an array */ -async function fetchTestConfig(): Promise { - // Simulate network delay - await new Promise((resolve) => setTimeout(resolve, 100)); +function parseArray(value: string): string[] { + if (!value) return []; + return value + .split(",") + .map(v => v.trim()) + .filter(v => v.length > 0); +} - // Return mock configuration +/** + * Convert a CSV row to TestConfig structure + */ +function csvRowToTestConfig(row: CSVRow): TestConfig { + return { + drug: { + primary: row.drug_primary, + alternatives: { + withWarnings: row.drug_with_warning || row.drug_primary, + withAdverseEvents: row.drug_adverse_events || row.drug_primary, + }, + }, + variant: { + primary: row.variant_primary, + withMolecularStructure: row.variant_with_molecular_structure, + withPharmacogenomics: row.variant_with_pharmacogenetics, + withQTL: row.variant_with_qtl || undefined, + withEVA: row.variant_with_eva || undefined, + }, + target: { + primary: row.target_primary || undefined, + alternatives: parseArray(row.target_alternative), + aotfDiseases: parseArray(row.target_aotf_diseases), + }, + disease: { + primary: row.disease_primary, + name: row.disease_name || undefined, + alternatives: parseArray(row.disease_alternatives), + aotfGenes: parseArray(row.disease_aotf_genes), + }, + study: { + gwas: { + primary: row.study_gwas, + alternatives: [], + }, + qtl: row.study_qtl + ? { + primary: row.study_qtl, + alternatives: [], + } + : undefined, + }, + }; +} + +/** + * Fetch test configuration from Google Sheet CSV URL + */ +async function fetchConfigFromSheet(url: string, scenarioName: string): Promise { + try { + const response = await fetch(url); + if (!response.ok) { + console.error(`Failed to fetch config from ${url}: ${response.status}`); + return null; + } + + const csvText = await response.text(); + const parseResult = Papa.parse(csvText, { + header: true, + skipEmptyLines: true, + transformHeader: header => header.trim(), + }); + + if (parseResult.errors.length > 0) { + console.error("CSV parsing errors:", parseResult.errors); + } + + const rows = parseResult.data; + + // Find the row matching the scenario name + const matchingRow = rows.find(row => row["Scenario Name"] === scenarioName); + if (!matchingRow) { + console.error(`Scenario "${scenarioName}" not found in CSV. Available scenarios: ${rows.map(r => r["Scenario Name"]).join(", ")}`); + return null; + } + + return csvRowToTestConfig(matchingRow); + } catch (error) { + console.error("Error fetching config from sheet:", error); + return null; + } +} + +/** + * Default test configuration (fallback) + */ +function getDefaultConfig(): TestConfig { return { drug: { primary: "CHEMBL1201585", // TRASTUZUMAB - has comprehensive data @@ -100,11 +215,27 @@ let cachedConfig: TestConfig | null = null; /** * Get test configuration (with caching) + * Reads from environment variables TEST_CONFIG_URL and TEST_SCENARIO if available, + * otherwise falls back to default configuration. * @returns Test configuration object */ export async function getTestConfig(): Promise { if (!cachedConfig) { - cachedConfig = await fetchTestConfig(); + const configUrl = process.env.TEST_CONFIG_URL; + const scenarioName = process.env.TEST_SCENARIO || "Happy_Path_Full_Data"; + + if (configUrl) { + console.log(`Fetching test config from: ${configUrl}`); + console.log(`Using scenario: ${scenarioName}`); + const fetchedConfig = await fetchConfigFromSheet(configUrl, scenarioName); + if (fetchedConfig) { + cachedConfig = fetchedConfig; + return cachedConfig; + } + console.log("Falling back to default configuration"); + } + + cachedConfig = getDefaultConfig(); } return cachedConfig; } diff --git a/packages/platform-test/package.json b/packages/platform-test/package.json index 6ef527469..9b7f56a5e 100644 --- a/packages/platform-test/package.json +++ b/packages/platform-test/package.json @@ -8,6 +8,7 @@ }, "devDependencies": { "@playwright/test": "^1.36.2", + "@types/papaparse": "^5.5.2", "start-server-and-test": "^2.0.4" }, "scripts": { @@ -18,5 +19,8 @@ }, "keywords": [], "author": "", - "license": "ISC" + "license": "ISC", + "dependencies": { + "papaparse": "^5.5.3" + } } diff --git a/yarn.lock b/yarn.lock index d1d9eb910..fa44a9c7c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1473,6 +1473,13 @@ resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.11.tgz#56588b17ae8f50c53983a524fc3cc47437969d64" integrity sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA== +"@types/node@*": + version "25.0.10" + resolved "https://registry.yarnpkg.com/@types/node/-/node-25.0.10.tgz#4864459c3c9459376b8b75fd051315071c8213e7" + integrity sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg== + dependencies: + undici-types "~7.16.0" + "@types/node@^18.0.0": version "18.19.130" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.130.tgz#da4c6324793a79defb7a62cba3947ec5add00d59" @@ -1480,6 +1487,13 @@ dependencies: undici-types "~5.26.4" +"@types/papaparse@^5.5.2": + version "5.5.2" + resolved "https://registry.yarnpkg.com/@types/papaparse/-/papaparse-5.5.2.tgz#cb450a1cd183deb43728e593eb1ac2da60f4fa4d" + integrity sha512-gFnFp/JMzLHCwRf7tQHrNnfhN4eYBVYYI897CGX4MY1tzY9l2aLkVyx2IlKZ/SAqDbB3I1AOZW5gTMGGsqWliA== + dependencies: + "@types/node" "*" + "@types/parse-json@^4.0.0": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239" @@ -3878,6 +3892,11 @@ pako@^2.1.0: resolved "https://registry.yarnpkg.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86" integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug== +papaparse@^5.5.3: + version "5.5.3" + resolved "https://registry.yarnpkg.com/papaparse/-/papaparse-5.5.3.tgz#07f8994dec516c6dab266e952bed68e1de59fa9a" + integrity sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A== + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" @@ -5307,6 +5326,11 @@ undici-types@~5.26.4: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== +undici-types@~7.16.0: + version "7.16.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.16.0.tgz#ffccdff36aea4884cbfce9a750a0580224f58a46" + integrity sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw== + universalify@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" From da4929c7912bdee31b2f5b6efee889fe5a885dcb Mon Sep 17 00:00:00 2001 From: david oluwasusi Date: Thu, 22 Jan 2026 13:45:08 +0000 Subject: [PATCH 23/34] feat(tests): formatting --- packages/platform-test/fixtures/testConfig.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/platform-test/fixtures/testConfig.ts b/packages/platform-test/fixtures/testConfig.ts index 09dbb24a6..95bb27765 100644 --- a/packages/platform-test/fixtures/testConfig.ts +++ b/packages/platform-test/fixtures/testConfig.ts @@ -79,8 +79,8 @@ function parseArray(value: string): string[] { if (!value) return []; return value .split(",") - .map(v => v.trim()) - .filter(v => v.length > 0); + .map((v) => v.trim()) + .filter((v) => v.length > 0); } /** @@ -143,7 +143,7 @@ async function fetchConfigFromSheet(url: string, scenarioName: string): Promise< const parseResult = Papa.parse(csvText, { header: true, skipEmptyLines: true, - transformHeader: header => header.trim(), + transformHeader: (header) => header.trim(), }); if (parseResult.errors.length > 0) { @@ -153,9 +153,11 @@ async function fetchConfigFromSheet(url: string, scenarioName: string): Promise< const rows = parseResult.data; // Find the row matching the scenario name - const matchingRow = rows.find(row => row["Scenario Name"] === scenarioName); + const matchingRow = rows.find((row) => row["Scenario Name"] === scenarioName); if (!matchingRow) { - console.error(`Scenario "${scenarioName}" not found in CSV. Available scenarios: ${rows.map(r => r["Scenario Name"]).join(", ")}`); + console.error( + `Scenario "${scenarioName}" not found in CSV. Available scenarios: ${rows.map((r) => r["Scenario Name"]).join(", ")}` + ); return null; } From 6081ac74adfed449590957df00f2d4a69e60c150 Mon Sep 17 00:00:00 2001 From: david oluwasusi Date: Thu, 22 Jan 2026 14:20:28 +0000 Subject: [PATCH 24/34] feat(tests): add base url input --- .github/workflows/e2e-ci.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e-ci.yml b/.github/workflows/e2e-ci.yml index 90815202b..76c901096 100644 --- a/.github/workflows/e2e-ci.yml +++ b/.github/workflows/e2e-ci.yml @@ -21,6 +21,10 @@ on: required: false default: 'Happy_Path_Full_Data' type: string + base_url: + description: 'Base URL to run tests against (defaults to Netlify branch preview)' + required: false + type: string jobs: get_branch_name: @@ -76,7 +80,7 @@ jobs: yarn dev:test:platform:e2e:smoke fi env: - PLAYWRIGHT_TEST_BASE_URL: "https://${{ needs.get_branch_name.outputs.branch }}--ot-platform.netlify.app" + PLAYWRIGHT_TEST_BASE_URL: ${{ github.event.inputs.base_url || format('https://{0}--ot-platform.netlify.app', needs.get_branch_name.outputs.branch) }} DEBUG: pw:api TEST_CONFIG_URL: ${{ github.event.inputs.test_config_url }} TEST_SCENARIO: ${{ github.event.inputs.test_scenario }} @@ -87,5 +91,5 @@ jobs: path: ${{ github.workspace }}/packages/platform-test/playwright-report retention-days: 7 env: - PLAYWRIGHT_TEST_BASE_URL: "https://${{ needs.get_branch_name.outputs.branch }}--ot-platform.netlify.app" + PLAYWRIGHT_TEST_BASE_URL: ${{ github.event.inputs.base_url || format('https://{0}--ot-platform.netlify.app', needs.get_branch_name.outputs.branch) }} DEBUG: pw:api \ No newline at end of file From cbfc4a1d14947a6ff96c0c8b6e65b826f04babd3 Mon Sep 17 00:00:00 2001 From: david oluwasusi Date: Fri, 23 Jan 2026 13:43:20 +0000 Subject: [PATCH 25/34] feat(tests): configure actions --- .github/workflows/e2e-ci.yml | 2 +- .../e2e/pages/drug/drugAdverseEvents.spec.ts | 7 ++ .../pages/drug/drugPharmacovigilance.spec.ts | 7 ++ .../platform-test/e2e/pages/homepage.spec.ts | 7 +- packages/platform-test/fixtures/index.ts | 10 ++ packages/platform-test/fixtures/testConfig.ts | 118 +++++++++++++++--- .../playwright-report/index.html | 2 +- 7 files changed, 130 insertions(+), 23 deletions(-) diff --git a/.github/workflows/e2e-ci.yml b/.github/workflows/e2e-ci.yml index 76c901096..e06e65f2d 100644 --- a/.github/workflows/e2e-ci.yml +++ b/.github/workflows/e2e-ci.yml @@ -60,7 +60,7 @@ jobs: needs: [tests_e2e_netlify_prepare, get_branch_name] name: Run end-to-end tests on Netlify PR preview runs-on: ubuntu-latest - timeout-minutes: 20 + timeout-minutes: 120 steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 diff --git a/packages/platform-test/e2e/pages/drug/drugAdverseEvents.spec.ts b/packages/platform-test/e2e/pages/drug/drugAdverseEvents.spec.ts index 28aa58393..8bc6e347f 100644 --- a/packages/platform-test/e2e/pages/drug/drugAdverseEvents.spec.ts +++ b/packages/platform-test/e2e/pages/drug/drugAdverseEvents.spec.ts @@ -15,6 +15,13 @@ test.describe("Drug Pharmacovigilance Section", () => { testConfig.drug.alternatives?.withAdverseEvents ?? testConfig.drug.primary ); + // Check if adverse events section exists, skip if not + const sectionVisible = await Pharmacovigilance.isSectionVisible(); + if (!sectionVisible) { + test.skip(true, "No adverse events section found for this drug"); + return; + } + // Wait for the section to fully load await Pharmacovigilance.waitForLoad(); }); diff --git a/packages/platform-test/e2e/pages/drug/drugPharmacovigilance.spec.ts b/packages/platform-test/e2e/pages/drug/drugPharmacovigilance.spec.ts index 3769fbec3..03ae29938 100644 --- a/packages/platform-test/e2e/pages/drug/drugPharmacovigilance.spec.ts +++ b/packages/platform-test/e2e/pages/drug/drugPharmacovigilance.spec.ts @@ -12,6 +12,13 @@ test.describe("Drug Pharmacovigilance Section", () => { // Navigate to a drug with pharmacovigilance data await drugPage.goToDrugPage(testConfig.drug.primary); + // Check if pharmacovigilance section exists, skip if not + const sectionVisible = await Pharmacovigilance.isSectionVisible(); + if (!sectionVisible) { + test.skip(true, "No pharmacovigilance section found for this drug"); + return; + } + // Wait for the section to fully load await Pharmacovigilance.waitForLoad(); }); diff --git a/packages/platform-test/e2e/pages/homepage.spec.ts b/packages/platform-test/e2e/pages/homepage.spec.ts index d70fe177c..8071054f6 100644 --- a/packages/platform-test/e2e/pages/homepage.spec.ts +++ b/packages/platform-test/e2e/pages/homepage.spec.ts @@ -1,9 +1,10 @@ -import { expect, type Locator, test } from "@playwright/test"; +import { expect, type Locator } from "@playwright/test"; +import { test } from "../../fixtures"; import { fillPolling } from "../../utils/fillPolling"; test.describe("Home page actions", () => { test("Validate page title", { tag: "@smoke" }, async ({ page, baseURL }) => { - await page.goto(baseURL ?? "/"); + await page.goto(baseURL!); const title = await page.title(); await expect(title).toBe("Open Targets Platform"); }); @@ -12,7 +13,7 @@ test.describe("Home page actions", () => { let searchInput: Locator; test.beforeEach(async ({ page, baseURL }) => { - await page.goto(baseURL ?? "/"); + await page.goto(baseURL!); await page.getByTestId("global-search-input-trigger").click(); // Verify that the global search input is rendered diff --git a/packages/platform-test/fixtures/index.ts b/packages/platform-test/fixtures/index.ts index 04d6fae47..a13695412 100644 --- a/packages/platform-test/fixtures/index.ts +++ b/packages/platform-test/fixtures/index.ts @@ -1,11 +1,17 @@ import { test as base } from "@playwright/test"; import { getTestConfig, type TestConfig } from "./testConfig"; +/** + * Default base URL for tests + */ +const DEFAULT_BASE_URL = "http://localhost:3000"; + /** * Extended test fixtures with test configuration */ type TestFixtures = { testConfig: TestConfig; + baseURL: string; }; /** @@ -18,6 +24,10 @@ export const test = base.extend({ const config = await getTestConfig(); await use(config); }, + baseURL: async ({}, use) => { + const url = process.env.PLAYWRIGHT_TEST_BASE_URL || DEFAULT_BASE_URL; + await use(url); + }, }); export { expect } from "@playwright/test"; diff --git a/packages/platform-test/fixtures/testConfig.ts b/packages/platform-test/fixtures/testConfig.ts index 95bb27765..1782bd00c 100644 --- a/packages/platform-test/fixtures/testConfig.ts +++ b/packages/platform-test/fixtures/testConfig.ts @@ -133,7 +133,14 @@ function csvRowToTestConfig(row: CSVRow): TestConfig { */ async function fetchConfigFromSheet(url: string, scenarioName: string): Promise { try { - const response = await fetch(url); + // Add cache-busting parameter to ensure fresh data + const cacheBustUrl = `${url}${url.includes("?") ? "&" : "?"}_t=${Date.now()}`; + const response = await fetch(cacheBustUrl, { + cache: "no-store", + headers: { + "Cache-Control": "no-cache", + }, + }); if (!response.ok) { console.error(`Failed to fetch config from ${url}: ${response.status}`); return null; @@ -210,35 +217,110 @@ function getDefaultConfig(): TestConfig { }; } +/** + * Merge fetched config with default config, filling in empty values from default + */ +function mergeWithDefaults(fetched: TestConfig, defaults: TestConfig): TestConfig { + return { + drug: { + primary: fetched.drug.primary || defaults.drug.primary, + alternatives: { + withWarnings: + fetched.drug.alternatives?.withWarnings || defaults.drug.alternatives?.withWarnings || "", + withAdverseEvents: + fetched.drug.alternatives?.withAdverseEvents || + defaults.drug.alternatives?.withAdverseEvents || + "", + }, + }, + variant: { + primary: fetched.variant.primary || defaults.variant.primary, + withMolecularStructure: + fetched.variant.withMolecularStructure || defaults.variant.withMolecularStructure, + withPharmacogenomics: + fetched.variant.withPharmacogenomics || defaults.variant.withPharmacogenomics, + withQTL: fetched.variant.withQTL || defaults.variant.withQTL, + withEVA: fetched.variant.withEVA || defaults.variant.withEVA, + }, + target: { + primary: fetched.target?.primary || defaults.target?.primary, + alternatives: fetched.target?.alternatives?.length + ? fetched.target.alternatives + : defaults.target?.alternatives, + aotfDiseases: fetched.target?.aotfDiseases?.length + ? fetched.target.aotfDiseases + : defaults.target?.aotfDiseases, + }, + disease: { + primary: fetched.disease.primary || defaults.disease.primary, + name: fetched.disease.name || defaults.disease.name, + alternatives: fetched.disease.alternatives?.length + ? fetched.disease.alternatives + : defaults.disease.alternatives, + aotfGenes: fetched.disease.aotfGenes?.length + ? fetched.disease.aotfGenes + : defaults.disease.aotfGenes, + }, + study: { + gwas: { + primary: fetched.study.gwas.primary || defaults.study.gwas.primary, + alternatives: fetched.study.gwas.alternatives?.length + ? fetched.study.gwas.alternatives + : defaults.study.gwas.alternatives, + }, + qtl: + fetched.study.qtl?.primary || defaults.study.qtl + ? { + primary: fetched.study.qtl?.primary || defaults.study.qtl?.primary, + alternatives: fetched.study.qtl?.alternatives?.length + ? fetched.study.qtl.alternatives + : defaults.study.qtl?.alternatives, + } + : undefined, + }, + }; +} + /** * Cached test configuration */ let cachedConfig: TestConfig | null = null; /** - * Get test configuration (with caching) + * Default configuration constants + */ +const DEFAULT_CONFIG_URL = + "https://docs.google.com/spreadsheets/d/1oWYlb_o0AZBYOFUCd8k5-whZpKLpicZ-UyyNkLUbUk8/export?format=csv"; +const DEFAULT_SCENARIO = "Target-Disease Links"; + +/** + * Get test configuration (always fetches fresh) * Reads from environment variables TEST_CONFIG_URL and TEST_SCENARIO if available, - * otherwise falls back to default configuration. + * otherwise uses hardcoded defaults. * @returns Test configuration object */ export async function getTestConfig(): Promise { - if (!cachedConfig) { - const configUrl = process.env.TEST_CONFIG_URL; - const scenarioName = process.env.TEST_SCENARIO || "Happy_Path_Full_Data"; - - if (configUrl) { - console.log(`Fetching test config from: ${configUrl}`); - console.log(`Using scenario: ${scenarioName}`); - const fetchedConfig = await fetchConfigFromSheet(configUrl, scenarioName); - if (fetchedConfig) { - cachedConfig = fetchedConfig; - return cachedConfig; - } - console.log("Falling back to default configuration"); - } + // Always reset to fetch fresh config for every run + cachedConfig = null; - cachedConfig = getDefaultConfig(); + const configUrl = process.env.TEST_CONFIG_URL || DEFAULT_CONFIG_URL; + const scenarioName = process.env.TEST_SCENARIO || DEFAULT_SCENARIO; + const defaults = getDefaultConfig(); + + if (configUrl) { + console.log(`Fetching test config from: ${configUrl}`); + console.log(`Using scenario: ${scenarioName}`); + const fetchedConfig = await fetchConfigFromSheet(configUrl, scenarioName); + if (fetchedConfig) { + // Merge fetched config with defaults to fill any empty cells + cachedConfig = mergeWithDefaults(fetchedConfig, defaults); + console.log(cachedConfig); + return cachedConfig; + } + console.log("Falling back to default configuration"); } + + cachedConfig = defaults; return cachedConfig; } diff --git a/packages/platform-test/playwright-report/index.html b/packages/platform-test/playwright-report/index.html index 7ca8822ca..108ee240a 100644 --- a/packages/platform-test/playwright-report/index.html +++ b/packages/platform-test/playwright-report/index.html @@ -73,4 +73,4 @@
    - \ No newline at end of file + \ No newline at end of file From 91c6d599290b28b00f0df939de8ed4628f7e5779 Mon Sep 17 00:00:00 2001 From: david oluwasusi Date: Mon, 26 Jan 2026 11:18:17 +0000 Subject: [PATCH 26/34] chore(test-config): refactor test config --- .../disease/associatedTargetsActions.spec.ts | 4 + .../associatedTargetsEvidenceWidgets.spec.ts | 4 + .../disease/associatedTargetsHeader.spec.ts | 4 + .../associatedTargetsPrioritization.spec.ts | 4 + .../disease/associatedTargetsTable.spec.ts | 4 + .../e2e/pages/disease/diseaseProfile.spec.ts | 4 + packages/platform-test/fixtures/index.ts | 3 +- packages/platform-test/fixtures/testConfig.ts | 252 +----------------- .../playwright-report/index.html | 2 +- packages/platform-test/readme.md | 48 ++++ packages/platform-test/types/index.ts | 70 +++++ .../platform-test/utils/csvRowToTestConfig.ts | 48 ++++ .../utils/fetchConfigFromSheet.ts | 53 ++++ .../platform-test/utils/mergeWithDefaults.ts | 65 +++++ .../utils/parseCsvStringToArray.ts | 10 + 15 files changed, 328 insertions(+), 247 deletions(-) create mode 100644 packages/platform-test/types/index.ts create mode 100644 packages/platform-test/utils/csvRowToTestConfig.ts create mode 100644 packages/platform-test/utils/fetchConfigFromSheet.ts create mode 100644 packages/platform-test/utils/mergeWithDefaults.ts create mode 100644 packages/platform-test/utils/parseCsvStringToArray.ts diff --git a/packages/platform-test/e2e/pages/disease/associatedTargetsActions.spec.ts b/packages/platform-test/e2e/pages/disease/associatedTargetsActions.spec.ts index 5b761651f..0ee4d2237 100644 --- a/packages/platform-test/e2e/pages/disease/associatedTargetsActions.spec.ts +++ b/packages/platform-test/e2e/pages/disease/associatedTargetsActions.spec.ts @@ -3,6 +3,10 @@ import { AotfActions } from "../../../POM/objects/widgets/AOTF/aotfActions"; test.describe("Disease Page - AOTF Actions", () => { test.beforeEach(async ({ page, baseURL, testConfig }) => { + //if no disease id, skip all tests + if (!testConfig.disease.primary) { + test.skip(); + } await page.goto(`${baseURL}/disease/${testConfig.disease.primary}/associations`); }); diff --git a/packages/platform-test/e2e/pages/disease/associatedTargetsEvidenceWidgets.spec.ts b/packages/platform-test/e2e/pages/disease/associatedTargetsEvidenceWidgets.spec.ts index 18d8ad69c..6f4855622 100644 --- a/packages/platform-test/e2e/pages/disease/associatedTargetsEvidenceWidgets.spec.ts +++ b/packages/platform-test/e2e/pages/disease/associatedTargetsEvidenceWidgets.spec.ts @@ -5,6 +5,10 @@ import { AotfTable } from "../../../POM/objects/widgets/AOTF/aotfTable"; test.describe("Disease Page - AOTF Evidence Widgets", () => { test.beforeEach(async ({ page, baseURL, testConfig }) => { + //if no disease id, skip all tests + if (!testConfig.disease.primary) { + test.skip(); + } await page.goto(`${baseURL}/disease/${testConfig.disease.primary}/associations`); }); diff --git a/packages/platform-test/e2e/pages/disease/associatedTargetsHeader.spec.ts b/packages/platform-test/e2e/pages/disease/associatedTargetsHeader.spec.ts index fe0d91eae..ced6e967d 100644 --- a/packages/platform-test/e2e/pages/disease/associatedTargetsHeader.spec.ts +++ b/packages/platform-test/e2e/pages/disease/associatedTargetsHeader.spec.ts @@ -3,6 +3,10 @@ import { DiseasePage } from "../../../POM/page/disease/disease"; test.describe("Disease Page - Header and Navigation", () => { test.beforeEach(async ({ page, baseURL, testConfig }) => { + //if no disease id, skip all tests + if (!testConfig.disease.primary) { + test.skip(); + } await page.goto(`${baseURL}/disease/${testConfig.disease.primary}/associations`); }); diff --git a/packages/platform-test/e2e/pages/disease/associatedTargetsPrioritization.spec.ts b/packages/platform-test/e2e/pages/disease/associatedTargetsPrioritization.spec.ts index f3ca05b25..bd7cd2379 100644 --- a/packages/platform-test/e2e/pages/disease/associatedTargetsPrioritization.spec.ts +++ b/packages/platform-test/e2e/pages/disease/associatedTargetsPrioritization.spec.ts @@ -4,6 +4,10 @@ import { AotfTable } from "../../../POM/objects/widgets/AOTF/aotfTable"; test.describe("Disease Page - AOTF Prioritization", () => { test.beforeEach(async ({ page, baseURL, testConfig }) => { + //if no disease id, skip all tests + if (!testConfig.disease.primary) { + test.skip(); + } await page.goto(`${baseURL}/disease/${testConfig.disease.primary}/associations`); }); diff --git a/packages/platform-test/e2e/pages/disease/associatedTargetsTable.spec.ts b/packages/platform-test/e2e/pages/disease/associatedTargetsTable.spec.ts index 7251b3430..57c016671 100644 --- a/packages/platform-test/e2e/pages/disease/associatedTargetsTable.spec.ts +++ b/packages/platform-test/e2e/pages/disease/associatedTargetsTable.spec.ts @@ -4,6 +4,10 @@ import { AotfTable } from "../../../POM/objects/widgets/AOTF/aotfTable"; test.describe("Disease Page - AOTF Table", () => { test.beforeEach(async ({ page, baseURL, testConfig }) => { + //if no disease id, skip all tests + if (!testConfig.disease.primary) { + test.skip(); + } await page.goto(`${baseURL}/disease/${testConfig.disease.primary}/associations`); }); diff --git a/packages/platform-test/e2e/pages/disease/diseaseProfile.spec.ts b/packages/platform-test/e2e/pages/disease/diseaseProfile.spec.ts index 162b7cebc..6529fe894 100644 --- a/packages/platform-test/e2e/pages/disease/diseaseProfile.spec.ts +++ b/packages/platform-test/e2e/pages/disease/diseaseProfile.spec.ts @@ -9,6 +9,10 @@ import { PhenotypesSection } from "../../../POM/objects/widgets/Phenotypes/pheno test.describe("Disease Profile Page", () => { test.beforeEach(async ({ page, baseURL, testConfig }) => { + //if no disease id, skip all tests + if (!testConfig.disease.primary) { + test.skip(); + } await page.goto(`${baseURL}/disease/${testConfig.disease.primary}`); }); diff --git a/packages/platform-test/fixtures/index.ts b/packages/platform-test/fixtures/index.ts index a13695412..ea63454ab 100644 --- a/packages/platform-test/fixtures/index.ts +++ b/packages/platform-test/fixtures/index.ts @@ -1,5 +1,6 @@ import { test as base } from "@playwright/test"; -import { getTestConfig, type TestConfig } from "./testConfig"; +import type { TestConfig } from "../types"; +import { getTestConfig } from "./testConfig"; /** * Default base URL for tests diff --git a/packages/platform-test/fixtures/testConfig.ts b/packages/platform-test/fixtures/testConfig.ts index 1782bd00c..cd56e99bd 100644 --- a/packages/platform-test/fixtures/testConfig.ts +++ b/packages/platform-test/fixtures/testConfig.ts @@ -1,179 +1,6 @@ -import Papa from "papaparse"; - -/** - * Test configuration interface - * Defines the structure of test data used across E2E tests - */ -export interface TestConfig { - drug: { - /** Drug with comprehensive data across all sections */ - primary: string; - alternatives?: { - withWarnings: string; - withAdverseEvents: string; - }; - }; - variant: { - /** Variant with GWAS and general data */ - primary: string; - /** Variant with molecular structure data */ - withMolecularStructure: string; - /** Variant with pharmacogenomics data */ - withPharmacogenomics: string; - /** Variant with QTL data */ - withQTL?: string; - /** Variant with EVA/ClinVar data */ - withEVA?: string; - }; - target?: { - primary?: string; - alternatives?: string[]; - aotfDiseases?: string[]; - }; - disease: { - primary: string; - name?: string; - alternatives?: string[]; - aotfGenes?: string[]; - }; - study: { - gwas: { - primary: string; - alternatives?: string[]; - }; - qtl?: { - primary?: string; - alternatives?: string[]; - }; - }; -} - -/** - * CSV row structure from Google Sheet - */ -interface CSVRow { - "Scenario Name": string; - drug_primary: string; - drug_with_warning: string; - drug_adverse_events: string; - variant_primary: string; - variant_with_molecular_structure: string; - variant_with_pharmacogenetics: string; - variant_with_qtl: string; - variant_with_eva: string; - target_primary: string; - target_alternative: string; - target_aotf_diseases: string; - disease_primary: string; - disease_name: string; - disease_alternatives: string; - disease_aotf_genes: string; - study_gwas: string; - study_qtl: string; -} - -/** - * Parse comma-separated values into an array - */ -function parseArray(value: string): string[] { - if (!value) return []; - return value - .split(",") - .map((v) => v.trim()) - .filter((v) => v.length > 0); -} - -/** - * Convert a CSV row to TestConfig structure - */ -function csvRowToTestConfig(row: CSVRow): TestConfig { - return { - drug: { - primary: row.drug_primary, - alternatives: { - withWarnings: row.drug_with_warning || row.drug_primary, - withAdverseEvents: row.drug_adverse_events || row.drug_primary, - }, - }, - variant: { - primary: row.variant_primary, - withMolecularStructure: row.variant_with_molecular_structure, - withPharmacogenomics: row.variant_with_pharmacogenetics, - withQTL: row.variant_with_qtl || undefined, - withEVA: row.variant_with_eva || undefined, - }, - target: { - primary: row.target_primary || undefined, - alternatives: parseArray(row.target_alternative), - aotfDiseases: parseArray(row.target_aotf_diseases), - }, - disease: { - primary: row.disease_primary, - name: row.disease_name || undefined, - alternatives: parseArray(row.disease_alternatives), - aotfGenes: parseArray(row.disease_aotf_genes), - }, - study: { - gwas: { - primary: row.study_gwas, - alternatives: [], - }, - qtl: row.study_qtl - ? { - primary: row.study_qtl, - alternatives: [], - } - : undefined, - }, - }; -} - -/** - * Fetch test configuration from Google Sheet CSV URL - */ -async function fetchConfigFromSheet(url: string, scenarioName: string): Promise { - try { - // Add cache-busting parameter to ensure fresh data - const cacheBustUrl = `${url}${url.includes("?") ? "&" : "?"}_t=${Date.now()}`; - const response = await fetch(cacheBustUrl, { - cache: "no-store", - headers: { - "Cache-Control": "no-cache", - }, - }); - if (!response.ok) { - console.error(`Failed to fetch config from ${url}: ${response.status}`); - return null; - } - - const csvText = await response.text(); - const parseResult = Papa.parse(csvText, { - header: true, - skipEmptyLines: true, - transformHeader: (header) => header.trim(), - }); - - if (parseResult.errors.length > 0) { - console.error("CSV parsing errors:", parseResult.errors); - } - - const rows = parseResult.data; - - // Find the row matching the scenario name - const matchingRow = rows.find((row) => row["Scenario Name"] === scenarioName); - if (!matchingRow) { - console.error( - `Scenario "${scenarioName}" not found in CSV. Available scenarios: ${rows.map((r) => r["Scenario Name"]).join(", ")}` - ); - return null; - } - - return csvRowToTestConfig(matchingRow); - } catch (error) { - console.error("Error fetching config from sheet:", error); - return null; - } -} +import type { TestConfig } from "../types"; +import { fetchConfigFromSheet } from "../utils/fetchConfigFromSheet"; +import { mergeWithDefaults } from "../utils/mergeWithDefaults"; /** * Default test configuration (fallback) @@ -181,10 +8,10 @@ async function fetchConfigFromSheet(url: string, scenarioName: string): Promise< function getDefaultConfig(): TestConfig { return { drug: { - primary: "CHEMBL1201585", // TRASTUZUMAB - has comprehensive data + primary: "CHEMBL3353410", // TRASTUZUMAB - has comprehensive data alternatives: { - withWarnings: "CHEMBL1201585", - withAdverseEvents: "CHEMBL1201585", + withWarnings: "CHEMBL3353410", + withAdverseEvents: "CHEMBL3353410", }, }, variant: { @@ -217,70 +44,6 @@ function getDefaultConfig(): TestConfig { }; } -/** - * Merge fetched config with default config, filling in empty values from default - */ -function mergeWithDefaults(fetched: TestConfig, defaults: TestConfig): TestConfig { - return { - drug: { - primary: fetched.drug.primary || defaults.drug.primary, - alternatives: { - withWarnings: - fetched.drug.alternatives?.withWarnings || defaults.drug.alternatives?.withWarnings || "", - withAdverseEvents: - fetched.drug.alternatives?.withAdverseEvents || - defaults.drug.alternatives?.withAdverseEvents || - "", - }, - }, - variant: { - primary: fetched.variant.primary || defaults.variant.primary, - withMolecularStructure: - fetched.variant.withMolecularStructure || defaults.variant.withMolecularStructure, - withPharmacogenomics: - fetched.variant.withPharmacogenomics || defaults.variant.withPharmacogenomics, - withQTL: fetched.variant.withQTL || defaults.variant.withQTL, - withEVA: fetched.variant.withEVA || defaults.variant.withEVA, - }, - target: { - primary: fetched.target?.primary || defaults.target?.primary, - alternatives: fetched.target?.alternatives?.length - ? fetched.target.alternatives - : defaults.target?.alternatives, - aotfDiseases: fetched.target?.aotfDiseases?.length - ? fetched.target.aotfDiseases - : defaults.target?.aotfDiseases, - }, - disease: { - primary: fetched.disease.primary || defaults.disease.primary, - name: fetched.disease.name || defaults.disease.name, - alternatives: fetched.disease.alternatives?.length - ? fetched.disease.alternatives - : defaults.disease.alternatives, - aotfGenes: fetched.disease.aotfGenes?.length - ? fetched.disease.aotfGenes - : defaults.disease.aotfGenes, - }, - study: { - gwas: { - primary: fetched.study.gwas.primary || defaults.study.gwas.primary, - alternatives: fetched.study.gwas.alternatives?.length - ? fetched.study.gwas.alternatives - : defaults.study.gwas.alternatives, - }, - qtl: - fetched.study.qtl?.primary || defaults.study.qtl - ? { - primary: fetched.study.qtl?.primary || defaults.study.qtl?.primary, - alternatives: fetched.study.qtl?.alternatives?.length - ? fetched.study.qtl.alternatives - : defaults.study.qtl?.alternatives, - } - : undefined, - }, - }; -} - /** * Cached test configuration */ @@ -291,7 +54,7 @@ let cachedConfig: TestConfig | null = null; */ const DEFAULT_CONFIG_URL = "https://docs.google.com/spreadsheets/d/1oWYlb_o0AZBYOFUCd8k5-whZpKLpicZ-UyyNkLUbUk8/export?format=csv"; -const DEFAULT_SCENARIO = "Target-Disease Links"; +const DEFAULT_SCENARIO = "testing_scenario_1"; /** * Get test configuration (always fetches fresh) @@ -314,7 +77,6 @@ export async function getTestConfig(): Promise { if (fetchedConfig) { // Merge fetched config with defaults to fill any empty cells cachedConfig = mergeWithDefaults(fetchedConfig, defaults); - console.log(cachedConfig); return cachedConfig; } console.log("Falling back to default configuration"); diff --git a/packages/platform-test/playwright-report/index.html b/packages/platform-test/playwright-report/index.html index 108ee240a..ae15eed17 100644 --- a/packages/platform-test/playwright-report/index.html +++ b/packages/platform-test/playwright-report/index.html @@ -73,4 +73,4 @@
    - \ No newline at end of file + \ No newline at end of file diff --git a/packages/platform-test/readme.md b/packages/platform-test/readme.md index 6976d8757..b23cd7208 100644 --- a/packages/platform-test/readme.md +++ b/packages/platform-test/readme.md @@ -88,6 +88,54 @@ For more details on POM, see the [References](#references) section. We use **Playwright fixtures** to manage test data and make tests flexible and maintainable. All test entity IDs are centralized in `fixtures/testConfig.ts`. +### CSV Configuration Schema + +Test configurations are loaded from a Google Sheet CSV. Each row represents a testing scenario, and columns define the entity IDs to use for different test types. + +| Column Name | Type | Description | +|-------------|------|-------------| +| `Testing Scenario` | string | | +| `drug_page_primary` | string | | +| `variant_primary` | string | | +| `variant_with_pharmacogenetics` | string | | +| `variant_with_qtl` | string | | +| `target_primary` | string | | +| `target_incomplete` | string (comma-separated) | | +| `target_aotf_diseases` | string (comma-separated) | | +| `disease_primary` | string | | +| `disease_name` | string | | +| `disease_alternatives` | string (comma-separated) | | +| `disease_aotf_genes` | string (comma-separated) | | +| `study_gwas` | string | | +| `study_qtl` | string | | +| `credible_set` | string | | +| `credible_set_GWAS_coloc` | string | | +| `credible_set_QTL_coloc` | string | | + +### GitHub Actions Integration + +The test configuration integrates with GitHub Actions through environment variables: + +| Environment Variable | Description | Default | +|---------------------|-------------|---------| +| `TEST_CONFIG_URL` | URL to the Google Sheet CSV export | Hardcoded default URL | +| `TEST_SCENARIO` | Name of the scenario row to use | `testing_scenario_1` | + +**Workflow usage example:** + +```yaml +- name: Run E2E Tests + env: + TEST_CONFIG_URL: ${{ secrets.TEST_CONFIG_SHEET_URL }} + TEST_SCENARIO: ${{ github.event.inputs.test_scenario || 'testing_scenario_1' }} + run: yarn dev:test:platform:e2e +``` + +This allows: +- Running tests with different entity configurations per environment +- Manual workflow dispatch with scenario selection +- Easy updates to test data without code changes + ### Configuration Structure ```typescript diff --git a/packages/platform-test/types/index.ts b/packages/platform-test/types/index.ts new file mode 100644 index 000000000..ae105cf3c --- /dev/null +++ b/packages/platform-test/types/index.ts @@ -0,0 +1,70 @@ +/** + * Test configuration interface + * Defines the structure of test data used across E2E tests + */ + +export interface TestConfig { + drug: { + /** Drug with comprehensive data across all sections */ + primary: string; + alternatives?: { + withWarnings: string; + withAdverseEvents: string; + }; + }; + variant: { + /** Variant with GWAS and general data */ + primary: string; + /** Variant with molecular structure data */ + withMolecularStructure: string; + /** Variant with pharmacogenomics data */ + withPharmacogenomics: string; + /** Variant with QTL data */ + withQTL?: string; + /** Variant with EVA/ClinVar data */ + withEVA?: string; + }; + target?: { + primary?: string; + alternatives?: string[]; + aotfDiseases?: string[]; + }; + disease: { + primary: string; + name?: string; + alternatives?: string[]; + aotfGenes?: string[]; + }; + study: { + gwas: { + primary: string; + alternatives?: string[]; + }; + qtl?: { + primary?: string; + alternatives?: string[]; + }; + }; +} /** + * CSV row structure from Google Sheet + */ + +export interface CSVRow { + "Testing Scenario": string; + drug_page_primary: string; + variant_primary: string; + variant_with_pharmacogenetics: string; + variant_with_qtl: string; + target_primary: string; + target_incomplete: string; + target_aotf_diseases: string; + disease_primary: string; + disease_name: string; + disease_alternatives: string; + disease_aotf_genes: string; + study_gwas: string; + study_qtl: string; + credible_set: string; + credible_set_GWAS_coloc: string; + credible_set_QTL_coloc: string; +} diff --git a/packages/platform-test/utils/csvRowToTestConfig.ts b/packages/platform-test/utils/csvRowToTestConfig.ts new file mode 100644 index 000000000..464837150 --- /dev/null +++ b/packages/platform-test/utils/csvRowToTestConfig.ts @@ -0,0 +1,48 @@ +import type { CSVRow, TestConfig } from "../types"; +import { parseCsvStringToArray } from "../utils/parseCsvStringToArray"; + +/** + * Convert a CSV row to TestConfig structure + */ + +export function csvRowToTestConfig(row: CSVRow): TestConfig { + return { + drug: { + primary: row.drug_page_primary, + alternatives: { + withWarnings: row.drug_page_primary, + withAdverseEvents: row.drug_page_primary, + }, + }, + variant: { + primary: row.variant_primary, + withMolecularStructure: row.variant_primary, + withPharmacogenomics: row.variant_with_pharmacogenetics, + withQTL: row.variant_with_qtl || undefined, + withEVA: row.variant_primary || undefined, + }, + target: { + primary: row.target_primary || undefined, + alternatives: parseCsvStringToArray(row.target_incomplete), + aotfDiseases: parseCsvStringToArray(row.target_aotf_diseases), + }, + disease: { + primary: row.disease_primary, + name: row.disease_name || undefined, + alternatives: parseCsvStringToArray(row.disease_alternatives), + aotfGenes: parseCsvStringToArray(row.disease_aotf_genes), + }, + study: { + gwas: { + primary: row.study_gwas, + alternatives: [], + }, + qtl: row.study_qtl + ? { + primary: row.study_qtl, + alternatives: [], + } + : undefined, + }, + }; +} diff --git a/packages/platform-test/utils/fetchConfigFromSheet.ts b/packages/platform-test/utils/fetchConfigFromSheet.ts new file mode 100644 index 000000000..893145bce --- /dev/null +++ b/packages/platform-test/utils/fetchConfigFromSheet.ts @@ -0,0 +1,53 @@ +import Papa from "papaparse"; +import { csvRowToTestConfig } from "./csvRowToTestConfig"; +import type { CSVRow, TestConfig } from "../types"; + +/** + * Fetch test configuration from Google Sheet CSV URL + */ +export async function fetchConfigFromSheet( + url: string, + scenarioName: string +): Promise { + try { + // Add cache-busting parameter to ensure fresh data + const cacheBustUrl = `${url}${url.includes("?") ? "&" : "?"}_t=${Date.now()}`; + const response = await fetch(cacheBustUrl, { + cache: "no-store", + headers: { + "Cache-Control": "no-cache", + }, + }); + if (!response.ok) { + console.error(`Failed to fetch config from ${url}: ${response.status}`); + return null; + } + + const csvText = await response.text(); + const parseResult = Papa.parse(csvText, { + header: true, + skipEmptyLines: true, + transformHeader: (header) => header.trim(), + }); + + if (parseResult.errors.length > 0) { + console.error("CSV parsing errors:", parseResult.errors); + } + + const rows = parseResult.data; + + // Find the row matching the scenario name + const matchingRow = rows.find((row) => row["Testing Scenario"] === scenarioName); + if (!matchingRow) { + console.error( + `Scenario "${scenarioName}" not found in CSV. Available scenarios: ${rows.map((r) => r["Testing Scenario"]).join(", ")}` + ); + return null; + } + + return csvRowToTestConfig(matchingRow); + } catch (error) { + console.error("Error fetching config from sheet:", error); + return null; + } +} diff --git a/packages/platform-test/utils/mergeWithDefaults.ts b/packages/platform-test/utils/mergeWithDefaults.ts new file mode 100644 index 000000000..23a21f6d0 --- /dev/null +++ b/packages/platform-test/utils/mergeWithDefaults.ts @@ -0,0 +1,65 @@ +import type { TestConfig } from "../types"; + +/** + * Merge fetched config with default config, filling in empty values from default + */ +export function mergeWithDefaults(fetched: TestConfig, defaults: TestConfig): TestConfig { + return { + drug: { + primary: fetched.drug.primary || defaults.drug.primary, + alternatives: { + withWarnings: + fetched.drug.alternatives?.withWarnings || defaults.drug.alternatives?.withWarnings || "", + withAdverseEvents: + fetched.drug.alternatives?.withAdverseEvents || + defaults.drug.alternatives?.withAdverseEvents || + "", + }, + }, + variant: { + primary: fetched.variant.primary || defaults.variant.primary, + withMolecularStructure: + fetched.variant.withMolecularStructure || defaults.variant.withMolecularStructure, + withPharmacogenomics: + fetched.variant.withPharmacogenomics || defaults.variant.withPharmacogenomics, + withQTL: fetched.variant.withQTL || defaults.variant.withQTL, + withEVA: fetched.variant.withEVA || defaults.variant.withEVA, + }, + target: { + primary: fetched.target?.primary || defaults.target?.primary, + alternatives: fetched.target?.alternatives?.length + ? fetched.target.alternatives + : defaults.target?.alternatives, + aotfDiseases: fetched.target?.aotfDiseases?.length + ? fetched.target.aotfDiseases + : defaults.target?.aotfDiseases, + }, + disease: { + primary: fetched.disease.primary || defaults.disease.primary, + name: fetched.disease.name || defaults.disease.name, + alternatives: fetched.disease.alternatives?.length + ? fetched.disease.alternatives + : defaults.disease.alternatives, + aotfGenes: fetched.disease.aotfGenes?.length + ? fetched.disease.aotfGenes + : defaults.disease.aotfGenes, + }, + study: { + gwas: { + primary: fetched.study.gwas.primary || defaults.study.gwas.primary, + alternatives: fetched.study.gwas.alternatives?.length + ? fetched.study.gwas.alternatives + : defaults.study.gwas.alternatives, + }, + qtl: + fetched.study.qtl?.primary || defaults.study.qtl + ? { + primary: fetched.study.qtl?.primary || defaults.study.qtl?.primary, + alternatives: fetched.study.qtl?.alternatives?.length + ? fetched.study.qtl.alternatives + : defaults.study.qtl?.alternatives, + } + : undefined, + }, + }; +} diff --git a/packages/platform-test/utils/parseCsvStringToArray.ts b/packages/platform-test/utils/parseCsvStringToArray.ts new file mode 100644 index 000000000..da0f66283 --- /dev/null +++ b/packages/platform-test/utils/parseCsvStringToArray.ts @@ -0,0 +1,10 @@ +/** + * Parse comma-separated values into an array + */ +export function parseCsvStringToArray(value: string): string[] { + if (!value) return []; + return value + .split(",") + .map((v) => v.trim()) + .filter((v) => v.length > 0); +} From 5d8042439f90f97d1e1baa9832edde1898db9246 Mon Sep 17 00:00:00 2001 From: david oluwasusi Date: Tue, 27 Jan 2026 10:08:13 +0000 Subject: [PATCH 27/34] feat(crediblesets):add interactors for credible set and missing study test case to credible set --- .../enhancerToGenePredictionsSection.ts | 145 +++++++++ .../widgets/CredibleSet/gwasColocSection.ts | 242 +++++++++++++++ .../POM/objects/widgets/CredibleSet/index.ts | 4 + .../widgets/CredibleSet/locus2GeneSection.ts | 109 +++++++ .../widgets/CredibleSet/molQTLColocSection.ts | 276 ++++++++++++++++++ .../POM/page/credibleSet/credibleSet.ts | 146 +++++++++ .../POM/page/credibleSet/index.ts | 1 + .../e2e/pages/study/studyPageGWAS.spec.ts | 29 ++ .../playwright-report/index.html | 2 +- 9 files changed, 953 insertions(+), 1 deletion(-) create mode 100644 packages/platform-test/POM/objects/widgets/CredibleSet/enhancerToGenePredictionsSection.ts create mode 100644 packages/platform-test/POM/objects/widgets/CredibleSet/gwasColocSection.ts create mode 100644 packages/platform-test/POM/objects/widgets/CredibleSet/index.ts create mode 100644 packages/platform-test/POM/objects/widgets/CredibleSet/locus2GeneSection.ts create mode 100644 packages/platform-test/POM/objects/widgets/CredibleSet/molQTLColocSection.ts create mode 100644 packages/platform-test/POM/page/credibleSet/credibleSet.ts create mode 100644 packages/platform-test/POM/page/credibleSet/index.ts diff --git a/packages/platform-test/POM/objects/widgets/CredibleSet/enhancerToGenePredictionsSection.ts b/packages/platform-test/POM/objects/widgets/CredibleSet/enhancerToGenePredictionsSection.ts new file mode 100644 index 000000000..de899134b --- /dev/null +++ b/packages/platform-test/POM/objects/widgets/CredibleSet/enhancerToGenePredictionsSection.ts @@ -0,0 +1,145 @@ +import type { Locator, Page } from "@playwright/test"; + +/** + * Interactor for Enhancer-to-Gene Predictions section on Credible Set page + * Section ID: Enhancer_to_gene_predictions + */ +export class CredibleSetEnhancerToGenePredictionsSection { + constructor(private page: Page) {} + + // Section container + getSection(): Locator { + return this.page.locator("[data-testid='section-Enhancer_to_gene_predictions']"); + } + + async isSectionVisible(): Promise { + return await this.getSection() + .isVisible() + .catch(() => false); + } + + /** + * Wait for the section to finish loading (no skeleton loaders) + */ + async waitForLoad(): Promise { + const section = this.getSection(); + await section.waitFor({ state: "visible", timeout: 10000 }); + + // Wait for skeleton loaders to disappear + await this.page + .waitForFunction( + () => { + const sect = document.querySelector( + "[data-testid='section-Enhancer_to_gene_predictions']" + ); + if (!sect) return false; + const skeletons = sect.querySelectorAll(".MuiSkeleton-root"); + return skeletons.length === 0; + }, + { timeout: 15000 } + ) + .catch(() => { + // If no skeletons found, section already loaded + }); + } + + // Table + getTable(): Locator { + return this.getSection().locator("table"); + } + + async isTableVisible(): Promise { + return await this.getTable() + .isVisible() + .catch(() => false); + } + + async getTableRows(): Promise { + const tbody = this.getTable().locator("tbody"); + const rows = tbody.locator("tr"); + return await rows.count(); + } + + async getTableRow(index: number): Promise { + const tbody = this.getTable().locator("tbody"); + return tbody.locator("tr").nth(index); + } + + // Target gene link + async getTargetGeneLink(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + return row.locator("a[href*='/target/']"); + } + + async clickTargetGeneLink(rowIndex: number): Promise { + const link = await this.getTargetGeneLink(rowIndex); + await link.click(); + } + + async getTargetGeneName(rowIndex: number): Promise { + const link = await this.getTargetGeneLink(rowIndex); + return await link.textContent(); + } + + async hasTargetGeneLink(rowIndex: number): Promise { + const link = await this.getTargetGeneLink(rowIndex); + return await link.isVisible().catch(() => false); + } + + // E2G Score + async getE2GScore(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + // Score is typically in a specific column + const cell = row.locator("td").nth(2); + return await cell.textContent(); + } + + // Cell text by column index + async getCellText(rowIndex: number, columnIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + const cell = row.locator("td").nth(columnIndex); + return await cell.textContent(); + } + + // Global filter/search + getSearchInput(): Locator { + return this.getSection().locator("input[placeholder*='Search']"); + } + + async search(searchTerm: string): Promise { + await this.getSearchInput().fill(searchTerm); + await this.waitForLoad(); + } + + async clearSearch(): Promise { + await this.getSearchInput().clear(); + await this.waitForLoad(); + } + + // Pagination + getNextPageButton(): Locator { + return this.getSection().locator("button[aria-label='Next Page']"); + } + + getPreviousPageButton(): Locator { + return this.getSection().locator("button[aria-label='Previous Page']"); + } + + async clickNextPage(): Promise { + await this.getNextPageButton().click(); + await this.waitForLoad(); + } + + async clickPreviousPage(): Promise { + await this.getPreviousPageButton().click(); + await this.waitForLoad(); + } + + async isNextPageEnabled(): Promise { + return await this.getNextPageButton().isEnabled(); + } + + async isPreviousPageEnabled(): Promise { + return await this.getPreviousPageButton().isEnabled(); + } +} diff --git a/packages/platform-test/POM/objects/widgets/CredibleSet/gwasColocSection.ts b/packages/platform-test/POM/objects/widgets/CredibleSet/gwasColocSection.ts new file mode 100644 index 000000000..3238bffc0 --- /dev/null +++ b/packages/platform-test/POM/objects/widgets/CredibleSet/gwasColocSection.ts @@ -0,0 +1,242 @@ +import type { Locator, Page } from "@playwright/test"; + +/** + * Interactor for GWAS Colocalisation section on Credible Set page + * Section ID: gwas_coloc + */ +export class GWASColocSection { + constructor(private page: Page) {} + + // Section container + getSection(): Locator { + return this.page.locator("[data-testid='section-gwas_coloc']"); + } + + async isSectionVisible(): Promise { + return await this.getSection() + .isVisible() + .catch(() => false); + } + + /** + * Wait for the section to finish loading (no skeleton loaders) + */ + async waitForLoad(): Promise { + const section = this.getSection(); + await section.waitFor({ state: "visible", timeout: 10000 }); + + // Wait for skeleton loaders to disappear + await this.page + .waitForFunction( + () => { + const sect = document.querySelector("[data-testid='section-gwas_coloc']"); + if (!sect) return false; + const skeletons = sect.querySelectorAll(".MuiSkeleton-root"); + return skeletons.length === 0; + }, + { timeout: 15000 } + ) + .catch(() => { + // If no skeletons found, section already loaded + }); + } + + // Table + getTable(): Locator { + return this.getSection().locator("table"); + } + + async isTableVisible(): Promise { + return await this.getTable() + .isVisible() + .catch(() => false); + } + + async getTableRows(): Promise { + const tbody = this.getTable().locator("tbody"); + const rows = tbody.locator("tr"); + return await rows.count(); + } + + async getTableRow(index: number): Promise { + const tbody = this.getTable().locator("tbody"); + return tbody.locator("tr").nth(index); + } + + // Credible set link (Navigate component) + async getCredibleSetLink(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + return row.locator("a[href*='/credible-set/']"); + } + + async clickCredibleSetLink(rowIndex: number): Promise { + const link = await this.getCredibleSetLink(rowIndex); + await link.click(); + } + + async hasCredibleSetLink(rowIndex: number): Promise { + const link = await this.getCredibleSetLink(rowIndex); + return await link.isVisible().catch(() => false); + } + + // Study link + async getStudyLink(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + return row.locator("a[href*='/study/']"); + } + + async clickStudyLink(rowIndex: number): Promise { + const link = await this.getStudyLink(rowIndex); + await link.click(); + } + + async getStudyId(rowIndex: number): Promise { + const link = await this.getStudyLink(rowIndex); + return await link.textContent(); + } + + async hasStudyLink(rowIndex: number): Promise { + const link = await this.getStudyLink(rowIndex); + return await link.isVisible().catch(() => false); + } + + // Lead variant link + async getLeadVariantLink(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + return row.locator("a[href*='/variant/']"); + } + + async clickLeadVariantLink(rowIndex: number): Promise { + const link = await this.getLeadVariantLink(rowIndex); + await link.click(); + } + + async hasLeadVariantLink(rowIndex: number): Promise { + const link = await this.getLeadVariantLink(rowIndex); + return await link.isVisible().catch(() => false); + } + + // Reported trait + async getReportedTrait(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + // Reported trait is typically in column 2 (0-indexed) + const cell = row.locator("td").nth(2); + return await cell.textContent(); + } + + // First author + async getFirstAuthor(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + // First author is typically in column 3 (0-indexed) + const cell = row.locator("td").nth(3); + return await cell.textContent(); + } + + // P-Value + async getPValue(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + // P-Value column index may vary + const cell = row.locator("td").nth(5); + return await cell.textContent(); + } + + // Colocalising variants count + async getColocalisingVariantsCount(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + const cell = row.locator("td").nth(6); + return await cell.textContent(); + } + + // Colocalisation method + async getColocalisationMethod(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + const cell = row.locator("td").nth(7); + return await cell.textContent(); + } + + // Directionality + async getDirectionality(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + const cell = row.locator("td").nth(8); + return await cell.textContent(); + } + + // H3 value + async getH3(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + const cell = row.locator("td").nth(9); + return await cell.textContent(); + } + + // H4 value + async getH4(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + const cell = row.locator("td").nth(10); + return await cell.textContent(); + } + + // CLPP value + async getCLPP(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + const cell = row.locator("td").nth(11); + return await cell.textContent(); + } + + // Cell text by column index + async getCellText(rowIndex: number, columnIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + const cell = row.locator("td").nth(columnIndex); + return await cell.textContent(); + } + + // Global filter/search + getSearchInput(): Locator { + return this.getSection().locator("input[placeholder*='Search']"); + } + + async search(searchTerm: string): Promise { + await this.getSearchInput().fill(searchTerm); + await this.waitForLoad(); + } + + async clearSearch(): Promise { + await this.getSearchInput().clear(); + await this.waitForLoad(); + } + + // Pagination + getNextPageButton(): Locator { + return this.getSection().locator("button[aria-label='Next Page']"); + } + + getPreviousPageButton(): Locator { + return this.getSection().locator("button[aria-label='Previous Page']"); + } + + async clickNextPage(): Promise { + await this.getNextPageButton().click(); + await this.waitForLoad(); + } + + async clickPreviousPage(): Promise { + await this.getPreviousPageButton().click(); + await this.waitForLoad(); + } + + async isNextPageEnabled(): Promise { + return await this.getNextPageButton().isEnabled(); + } + + async isPreviousPageEnabled(): Promise { + return await this.getPreviousPageButton().isEnabled(); + } + + // Data downloader + getDataDownloaderButton(): Locator { + return this.getSection().locator("button[aria-label*='download']"); + } + + async clickDataDownloader(): Promise { + await this.getDataDownloaderButton().click(); + } +} diff --git a/packages/platform-test/POM/objects/widgets/CredibleSet/index.ts b/packages/platform-test/POM/objects/widgets/CredibleSet/index.ts new file mode 100644 index 000000000..9921e322b --- /dev/null +++ b/packages/platform-test/POM/objects/widgets/CredibleSet/index.ts @@ -0,0 +1,4 @@ +export { CredibleSetEnhancerToGenePredictionsSection } from "./enhancerToGenePredictionsSection"; +export { GWASColocSection } from "./gwasColocSection"; +export { Locus2GeneSection } from "./locus2GeneSection"; +export { MolQTLColocSection } from "./molQTLColocSection"; diff --git a/packages/platform-test/POM/objects/widgets/CredibleSet/locus2GeneSection.ts b/packages/platform-test/POM/objects/widgets/CredibleSet/locus2GeneSection.ts new file mode 100644 index 000000000..d54f3832d --- /dev/null +++ b/packages/platform-test/POM/objects/widgets/CredibleSet/locus2GeneSection.ts @@ -0,0 +1,109 @@ +import type { Locator, Page } from "@playwright/test"; + +/** + * Interactor for Locus-to-Gene (L2G) section on Credible Set page + * Section ID: locus2gene + */ +export class Locus2GeneSection { + constructor(private page: Page) {} + + // Section container + getSection(): Locator { + return this.page.locator("[data-testid='section-locus2gene']"); + } + + async isSectionVisible(): Promise { + return await this.getSection() + .isVisible() + .catch(() => false); + } + + /** + * Wait for the section to finish loading (no skeleton loaders) + */ + async waitForLoad(): Promise { + const section = this.getSection(); + await section.waitFor({ state: "visible", timeout: 10000 }); + + // Wait for skeleton loaders to disappear + await this.page + .waitForFunction( + () => { + const sect = document.querySelector("[data-testid='section-locus2gene']"); + if (!sect) return false; + const skeletons = sect.querySelectorAll(".MuiSkeleton-root"); + return skeletons.length === 0; + }, + { timeout: 15000 } + ) + .catch(() => { + // If no skeletons found, section already loaded + }); + } + + // L2G uses a HeatmapTable, so we interact with it differently + getHeatmapTable(): Locator { + return this.getSection().locator("table"); + } + + async isHeatmapVisible(): Promise { + return await this.getHeatmapTable() + .isVisible() + .catch(() => false); + } + + async getTableRows(): Promise { + const tbody = this.getHeatmapTable().locator("tbody"); + const rows = tbody.locator("tr"); + return await rows.count(); + } + + async getTableRow(index: number): Promise { + const tbody = this.getHeatmapTable().locator("tbody"); + return tbody.locator("tr").nth(index); + } + + // Target gene link + async getTargetGeneLink(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + return row.locator("a[href*='/target/']"); + } + + async clickTargetGeneLink(rowIndex: number): Promise { + const link = await this.getTargetGeneLink(rowIndex); + await link.click(); + } + + async getTargetGeneName(rowIndex: number): Promise { + const link = await this.getTargetGeneLink(rowIndex); + return await link.textContent(); + } + + async hasTargetGeneLink(rowIndex: number): Promise { + const link = await this.getTargetGeneLink(rowIndex); + return await link.isVisible().catch(() => false); + } + + // L2G Score + async getL2GScore(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + // L2G score is typically in the second column + const cell = row.locator("td").nth(1); + return await cell.textContent(); + } + + // Global filter/search + getSearchInput(): Locator { + return this.getSection().locator("input[placeholder*='Search']"); + } + + async search(searchTerm: string): Promise { + await this.getSearchInput().fill(searchTerm); + await this.waitForLoad(); + } + + async clearSearch(): Promise { + await this.getSearchInput().clear(); + await this.waitForLoad(); + } +} diff --git a/packages/platform-test/POM/objects/widgets/CredibleSet/molQTLColocSection.ts b/packages/platform-test/POM/objects/widgets/CredibleSet/molQTLColocSection.ts new file mode 100644 index 000000000..71687e29f --- /dev/null +++ b/packages/platform-test/POM/objects/widgets/CredibleSet/molQTLColocSection.ts @@ -0,0 +1,276 @@ +import type { Locator, Page } from "@playwright/test"; + +/** + * Interactor for MolQTL Colocalisation section on Credible Set page + * Section ID: molqtl_coloc + */ +export class MolQTLColocSection { + constructor(private page: Page) {} + + // Section container + getSection(): Locator { + return this.page.locator("[data-testid='section-molqtl_coloc']"); + } + + async isSectionVisible(): Promise { + return await this.getSection() + .isVisible() + .catch(() => false); + } + + /** + * Wait for the section to finish loading (no skeleton loaders) + */ + async waitForLoad(): Promise { + const section = this.getSection(); + await section.waitFor({ state: "visible", timeout: 10000 }); + + // Wait for skeleton loaders to disappear + await this.page + .waitForFunction( + () => { + const sect = document.querySelector("[data-testid='section-molqtl_coloc']"); + if (!sect) return false; + const skeletons = sect.querySelectorAll(".MuiSkeleton-root"); + return skeletons.length === 0; + }, + { timeout: 15000 } + ) + .catch(() => { + // If no skeletons found, section already loaded + }); + } + + // Table + getTable(): Locator { + return this.getSection().locator("table"); + } + + async isTableVisible(): Promise { + return await this.getTable() + .isVisible() + .catch(() => false); + } + + async getTableRows(): Promise { + const tbody = this.getTable().locator("tbody"); + const rows = tbody.locator("tr"); + return await rows.count(); + } + + async getTableRow(index: number): Promise { + const tbody = this.getTable().locator("tbody"); + return tbody.locator("tr").nth(index); + } + + // Credible set link (Navigate component) + async getCredibleSetLink(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + return row.locator("a[href*='/credible-set/']"); + } + + async clickCredibleSetLink(rowIndex: number): Promise { + const link = await this.getCredibleSetLink(rowIndex); + await link.click(); + } + + async hasCredibleSetLink(rowIndex: number): Promise { + const link = await this.getCredibleSetLink(rowIndex); + return await link.isVisible().catch(() => false); + } + + // Study link + async getStudyLink(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + return row.locator("a[href*='/study/']"); + } + + async clickStudyLink(rowIndex: number): Promise { + const link = await this.getStudyLink(rowIndex); + await link.click(); + } + + async getStudyId(rowIndex: number): Promise { + const link = await this.getStudyLink(rowIndex); + return await link.textContent(); + } + + async hasStudyLink(rowIndex: number): Promise { + const link = await this.getStudyLink(rowIndex); + return await link.isVisible().catch(() => false); + } + + // Study type + async getStudyType(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + const cell = row.locator("td").nth(2); + return await cell.textContent(); + } + + // Affected gene (target) link + async getAffectedGeneLink(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + return row.locator("a[href*='/target/']"); + } + + async clickAffectedGeneLink(rowIndex: number): Promise { + const link = await this.getAffectedGeneLink(rowIndex); + await link.click(); + } + + async getAffectedGeneName(rowIndex: number): Promise { + const link = await this.getAffectedGeneLink(rowIndex); + return await link.textContent(); + } + + async hasAffectedGeneLink(rowIndex: number): Promise { + const link = await this.getAffectedGeneLink(rowIndex); + return await link.isVisible().catch(() => false); + } + + // Affected tissue/cell (external link to OLS) + async getAffectedTissueLink(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + return row.locator("a[href*='ebi.ac.uk/ols4']"); + } + + async getAffectedTissueName(rowIndex: number): Promise { + const link = await this.getAffectedTissueLink(rowIndex); + return await link.textContent(); + } + + async hasAffectedTissueLink(rowIndex: number): Promise { + const link = await this.getAffectedTissueLink(rowIndex); + return await link.isVisible().catch(() => false); + } + + // Condition + async getCondition(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + const cell = row.locator("td").nth(5); + return await cell.textContent(); + } + + // Lead variant link + async getLeadVariantLink(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + return row.locator("a[href*='/variant/']"); + } + + async clickLeadVariantLink(rowIndex: number): Promise { + const link = await this.getLeadVariantLink(rowIndex); + await link.click(); + } + + async hasLeadVariantLink(rowIndex: number): Promise { + const link = await this.getLeadVariantLink(rowIndex); + return await link.isVisible().catch(() => false); + } + + // P-Value + async getPValue(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + const cell = row.locator("td").nth(7); + return await cell.textContent(); + } + + // Colocalising variants count + async getColocalisingVariantsCount(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + const cell = row.locator("td").nth(8); + return await cell.textContent(); + } + + // Colocalisation method + async getColocalisationMethod(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + const cell = row.locator("td").nth(9); + return await cell.textContent(); + } + + // Directionality + async getDirectionality(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + const cell = row.locator("td").nth(10); + return await cell.textContent(); + } + + // H3 value + async getH3(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + const cell = row.locator("td").nth(11); + return await cell.textContent(); + } + + // H4 value + async getH4(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + const cell = row.locator("td").nth(12); + return await cell.textContent(); + } + + // CLPP value + async getCLPP(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + const cell = row.locator("td").nth(13); + return await cell.textContent(); + } + + // Cell text by column index + async getCellText(rowIndex: number, columnIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + const cell = row.locator("td").nth(columnIndex); + return await cell.textContent(); + } + + // Global filter/search + getSearchInput(): Locator { + return this.getSection().locator("input[placeholder*='Search']"); + } + + async search(searchTerm: string): Promise { + await this.getSearchInput().fill(searchTerm); + await this.waitForLoad(); + } + + async clearSearch(): Promise { + await this.getSearchInput().clear(); + await this.waitForLoad(); + } + + // Pagination + getNextPageButton(): Locator { + return this.getSection().locator("button[aria-label='Next Page']"); + } + + getPreviousPageButton(): Locator { + return this.getSection().locator("button[aria-label='Previous Page']"); + } + + async clickNextPage(): Promise { + await this.getNextPageButton().click(); + await this.waitForLoad(); + } + + async clickPreviousPage(): Promise { + await this.getPreviousPageButton().click(); + await this.waitForLoad(); + } + + async isNextPageEnabled(): Promise { + return await this.getNextPageButton().isEnabled(); + } + + async isPreviousPageEnabled(): Promise { + return await this.getPreviousPageButton().isEnabled(); + } + + // Data downloader + getDataDownloaderButton(): Locator { + return this.getSection().locator("button[aria-label*='download']"); + } + + async clickDataDownloader(): Promise { + await this.getDataDownloaderButton().click(); + } +} diff --git a/packages/platform-test/POM/page/credibleSet/credibleSet.ts b/packages/platform-test/POM/page/credibleSet/credibleSet.ts new file mode 100644 index 000000000..28ca3ee5a --- /dev/null +++ b/packages/platform-test/POM/page/credibleSet/credibleSet.ts @@ -0,0 +1,146 @@ +import type { Locator, Page } from "@playwright/test"; + +export class CredibleSetPage { + page: Page; + originalURL: string; + CREDIBLE_SET_BASE_URL = "/credible-set/"; + + constructor(page: Page) { + this.page = page; + this.originalURL = page.url(); + } + + /** + * Navigate directly to a credible set page + * @param studyLocusId - The study locus ID to navigate to + */ + async goToCredibleSetPage(studyLocusId: string): Promise { + await this.page.goto(`${this.CREDIBLE_SET_BASE_URL}${studyLocusId}`); + await this.waitForCredibleSetPageLoad(); + } + + /** + * Navigate to a credible set page from a table link + * @param studyLocusId - The study locus ID to navigate to + */ + async goToCredibleSetPageFromTable(studyLocusId: string): Promise { + const link = this.page + .locator(`a[href*="${this.CREDIBLE_SET_BASE_URL}${studyLocusId}"]`) + .first(); + await link.click(); + await this.waitForCredibleSetPageLoad(); + } + + /** + * Wait for the credible set page to load + */ + async waitForCredibleSetPageLoad(): Promise { + await this.page.waitForLoadState("networkidle"); + // Wait for header to be visible + await this.page + .waitForSelector("[data-testid='profile-page-header-block']", { + state: "visible", + timeout: 10000, + }) + .catch(() => { + // Fallback if test-id not present + }); + await this.page.waitForTimeout(500); + } + + /** + * Check if we're on a credible set page + */ + async isCredibleSetPage(): Promise { + const url = this.page.url(); + return url.includes("/credible-set/"); + } + + // Header elements + getHeader(): Locator { + return this.page.locator("[data-testid='profile-page-header']"); + } + + async isHeaderVisible(): Promise { + return await this.getHeader() + .isVisible() + .catch(() => false); + } + + getPageTitle(): Locator { + return this.page.locator("[data-testid='profile-page-header-text']"); + } + + async getPageTitleText(): Promise { + return await this.getPageTitle().textContent(); + } + + // External links in header + getLeadVariantLink(): Locator { + return this.page.locator("a[href*='/variant/']").first(); + } + + async clickLeadVariantLink(): Promise { + await this.getLeadVariantLink().click(); + } + + async getLeadVariantId(): Promise { + const link = this.getLeadVariantLink(); + return await link.textContent(); + } + + getStudyLink(): Locator { + return this.page.locator("a[href*='/study/']").first(); + } + + async clickStudyLink(): Promise { + await this.getStudyLink().click(); + } + + async getStudyId(): Promise { + const link = this.getStudyLink(); + return await link.textContent(); + } + + // Tab navigation + getProfileTab(): Locator { + return this.page.locator(`a[href*="${this.CREDIBLE_SET_BASE_URL}"][role="tab"]`).first(); + } + + async isProfileTabActive(): Promise { + const tab = this.getProfileTab(); + const ariaSelected = await tab.getAttribute("aria-selected"); + return ariaSelected === "true"; + } + + async clickProfileTab(): Promise { + await this.getProfileTab().click(); + } + + /** + * Wait for a specific section to finish loading + * @param sectionTestId - The test-id of the section + */ + async waitForSectionToLoad(sectionTestId: string): Promise { + const section = this.page.locator(`[data-testid='${sectionTestId}']`); + + // Wait for section to be visible + await section.waitFor({ state: "visible", timeout: 10000 }); + + // Wait for skeleton loaders to disappear + await this.page + .waitForFunction( + (testId) => { + const sect = document.querySelector(`[data-testid='${testId}']`); + if (!sect) return false; + const skeletons = sect.querySelectorAll(".MuiSkeleton-root"); + return skeletons.length === 0; + }, + sectionTestId, + { timeout: 15000 } + ) + .catch(() => { + // If no skeletons found, section already loaded + }); + } +} diff --git a/packages/platform-test/POM/page/credibleSet/index.ts b/packages/platform-test/POM/page/credibleSet/index.ts new file mode 100644 index 000000000..7e2c55dbc --- /dev/null +++ b/packages/platform-test/POM/page/credibleSet/index.ts @@ -0,0 +1 @@ +export { CredibleSetPage } from "./credibleSet"; diff --git a/packages/platform-test/e2e/pages/study/studyPageGWAS.spec.ts b/packages/platform-test/e2e/pages/study/studyPageGWAS.spec.ts index fc365d7d2..ae4a71fda 100644 --- a/packages/platform-test/e2e/pages/study/studyPageGWAS.spec.ts +++ b/packages/platform-test/e2e/pages/study/studyPageGWAS.spec.ts @@ -267,6 +267,35 @@ test.describe("Study Page - GWAS Study", () => { } }); + test("Credible sets link is clickable in the table", async ({ page }) => { + const gwasCredibleSets = new GWASCredibleSetsSection(page); + await gwasCredibleSets.waitForSectionLoad(); + + const isTableVisible = await gwasCredibleSets.isTableVisible(); + + if (isTableVisible) { + const rowCount = await gwasCredibleSets.getRowCount(); + + if (rowCount > 0) { + const credibleSetId = await gwasCredibleSets.getCredibleSetId(0); + test.expect(credibleSetId).toBeTruthy(); + + // Click credible set link + await gwasCredibleSets.clickCredibleSetLink(0); + await page.waitForURL("**/credible-set/**"); + + // Verify we're on credible set page + test.expect(page.url()).toContain("/credible-set/"); + // verify credible set page is fully loaded and contains clicked id + const credibleSetPage = new StudyPage(page); + await credibleSetPage.waitForStudyPageLoad(); + // expect credible set header to contain credible set id + const headerId = await credibleSetPage.getStudyIdFromHeader(); + test.expect(headerId).toContain(credibleSetId); + } + } + }); + test("GWAS Credible Sets table displays data", async ({ page }) => { const gwasCredibleSets = new GWASCredibleSetsSection(page); await gwasCredibleSets.waitForSectionLoad(); diff --git a/packages/platform-test/playwright-report/index.html b/packages/platform-test/playwright-report/index.html index ae15eed17..88bfb6baa 100644 --- a/packages/platform-test/playwright-report/index.html +++ b/packages/platform-test/playwright-report/index.html @@ -73,4 +73,4 @@
    - \ No newline at end of file + \ No newline at end of file From 5180dea4d0087a4bbf2d30de2def2214b11dc627 Mon Sep 17 00:00:00 2001 From: david oluwasusi Date: Fri, 30 Jan 2026 10:54:58 +0000 Subject: [PATCH 28/34] feature(crediblesets): add tests and interactors for credible sets --- .../enhancerToGenePredictionsSection.ts | 5 +- .../widgets/CredibleSet/gwasColocSection.ts | 5 +- .../POM/objects/widgets/CredibleSet/index.ts | 1 + .../widgets/CredibleSet/molQTLColocSection.ts | 5 +- .../widgets/CredibleSet/variantsSection.ts | 216 +++++++++++++ .../widgets/shared/GWASCredibleSetsSection.ts | 4 +- .../POM/page/credibleSet/credibleSet.ts | 2 +- .../pages/credibleSet/credibleSetPage.spec.ts | 102 ++++++ .../enhancerToGenePredictionsSection.spec.ts | 147 +++++++++ .../sections/gwasColocSection.spec.ts | 240 ++++++++++++++ .../sections/locus2GeneSection.spec.ts | 126 ++++++++ .../sections/molQTLColocSection.spec.ts | 301 ++++++++++++++++++ .../sections/variantsSection.spec.ts | 234 ++++++++++++++ .../associatedTargetsEvidenceWidgets.spec.ts | 8 - .../e2e/pages/study/studyPageGWAS.spec.ts | 9 +- packages/platform-test/fixtures/testConfig.ts | 5 + .../playwright-report/index.html | 2 +- packages/platform-test/types/index.ts | 5 + .../platform-test/utils/csvRowToTestConfig.ts | 5 + .../platform-test/utils/mergeWithDefaults.ts | 7 + 20 files changed, 1405 insertions(+), 24 deletions(-) create mode 100644 packages/platform-test/POM/objects/widgets/CredibleSet/variantsSection.ts create mode 100644 packages/platform-test/e2e/pages/credibleSet/credibleSetPage.spec.ts create mode 100644 packages/platform-test/e2e/pages/credibleSet/sections/enhancerToGenePredictionsSection.spec.ts create mode 100644 packages/platform-test/e2e/pages/credibleSet/sections/gwasColocSection.spec.ts create mode 100644 packages/platform-test/e2e/pages/credibleSet/sections/locus2GeneSection.spec.ts create mode 100644 packages/platform-test/e2e/pages/credibleSet/sections/molQTLColocSection.spec.ts create mode 100644 packages/platform-test/e2e/pages/credibleSet/sections/variantsSection.spec.ts diff --git a/packages/platform-test/POM/objects/widgets/CredibleSet/enhancerToGenePredictionsSection.ts b/packages/platform-test/POM/objects/widgets/CredibleSet/enhancerToGenePredictionsSection.ts index de899134b..16a6843ea 100644 --- a/packages/platform-test/POM/objects/widgets/CredibleSet/enhancerToGenePredictionsSection.ts +++ b/packages/platform-test/POM/objects/widgets/CredibleSet/enhancerToGenePredictionsSection.ts @@ -3,13 +3,14 @@ import type { Locator, Page } from "@playwright/test"; /** * Interactor for Enhancer-to-Gene Predictions section on Credible Set page * Section ID: Enhancer_to_gene_predictions + * data-testid: section-enhancer-to-gene-predictions (lowercase with hyphens) */ export class CredibleSetEnhancerToGenePredictionsSection { constructor(private page: Page) {} // Section container getSection(): Locator { - return this.page.locator("[data-testid='section-Enhancer_to_gene_predictions']"); + return this.page.locator("[data-testid='section-enhancer-to-gene-predictions']"); } async isSectionVisible(): Promise { @@ -30,7 +31,7 @@ export class CredibleSetEnhancerToGenePredictionsSection { .waitForFunction( () => { const sect = document.querySelector( - "[data-testid='section-Enhancer_to_gene_predictions']" + "[data-testid='section-enhancer-to-gene-predictions']" ); if (!sect) return false; const skeletons = sect.querySelectorAll(".MuiSkeleton-root"); diff --git a/packages/platform-test/POM/objects/widgets/CredibleSet/gwasColocSection.ts b/packages/platform-test/POM/objects/widgets/CredibleSet/gwasColocSection.ts index 3238bffc0..c0d40af3f 100644 --- a/packages/platform-test/POM/objects/widgets/CredibleSet/gwasColocSection.ts +++ b/packages/platform-test/POM/objects/widgets/CredibleSet/gwasColocSection.ts @@ -3,13 +3,14 @@ import type { Locator, Page } from "@playwright/test"; /** * Interactor for GWAS Colocalisation section on Credible Set page * Section ID: gwas_coloc + * data-testid: section-gwas-coloc (underscore replaced with hyphen) */ export class GWASColocSection { constructor(private page: Page) {} // Section container getSection(): Locator { - return this.page.locator("[data-testid='section-gwas_coloc']"); + return this.page.locator("[data-testid='section-gwas-coloc']"); } async isSectionVisible(): Promise { @@ -29,7 +30,7 @@ export class GWASColocSection { await this.page .waitForFunction( () => { - const sect = document.querySelector("[data-testid='section-gwas_coloc']"); + const sect = document.querySelector("[data-testid='section-gwas-coloc']"); if (!sect) return false; const skeletons = sect.querySelectorAll(".MuiSkeleton-root"); return skeletons.length === 0; diff --git a/packages/platform-test/POM/objects/widgets/CredibleSet/index.ts b/packages/platform-test/POM/objects/widgets/CredibleSet/index.ts index 9921e322b..3e9d7f851 100644 --- a/packages/platform-test/POM/objects/widgets/CredibleSet/index.ts +++ b/packages/platform-test/POM/objects/widgets/CredibleSet/index.ts @@ -2,3 +2,4 @@ export { CredibleSetEnhancerToGenePredictionsSection } from "./enhancerToGenePre export { GWASColocSection } from "./gwasColocSection"; export { Locus2GeneSection } from "./locus2GeneSection"; export { MolQTLColocSection } from "./molQTLColocSection"; +export { CredibleSetVariantsSection } from "./variantsSection"; diff --git a/packages/platform-test/POM/objects/widgets/CredibleSet/molQTLColocSection.ts b/packages/platform-test/POM/objects/widgets/CredibleSet/molQTLColocSection.ts index 71687e29f..0eb18f563 100644 --- a/packages/platform-test/POM/objects/widgets/CredibleSet/molQTLColocSection.ts +++ b/packages/platform-test/POM/objects/widgets/CredibleSet/molQTLColocSection.ts @@ -3,13 +3,14 @@ import type { Locator, Page } from "@playwright/test"; /** * Interactor for MolQTL Colocalisation section on Credible Set page * Section ID: molqtl_coloc + * data-testid: section-molqtl-coloc (underscore replaced with hyphen) */ export class MolQTLColocSection { constructor(private page: Page) {} // Section container getSection(): Locator { - return this.page.locator("[data-testid='section-molqtl_coloc']"); + return this.page.locator("[data-testid='section-molqtl-coloc']"); } async isSectionVisible(): Promise { @@ -29,7 +30,7 @@ export class MolQTLColocSection { await this.page .waitForFunction( () => { - const sect = document.querySelector("[data-testid='section-molqtl_coloc']"); + const sect = document.querySelector("[data-testid='section-molqtl-coloc']"); if (!sect) return false; const skeletons = sect.querySelectorAll(".MuiSkeleton-root"); return skeletons.length === 0; diff --git a/packages/platform-test/POM/objects/widgets/CredibleSet/variantsSection.ts b/packages/platform-test/POM/objects/widgets/CredibleSet/variantsSection.ts new file mode 100644 index 000000000..7f5daa2ad --- /dev/null +++ b/packages/platform-test/POM/objects/widgets/CredibleSet/variantsSection.ts @@ -0,0 +1,216 @@ +import type { Locator, Page } from "@playwright/test"; + +/** + * Interactor for Credible Set Variants section on Credible Set page + * Section ID: variants + * data-testid: section-variants + */ +export class CredibleSetVariantsSection { + constructor(private page: Page) {} + + // Section container + getSection(): Locator { + return this.page.locator("[data-testid='section-variants']"); + } + + async isSectionVisible(): Promise { + return await this.getSection() + .isVisible() + .catch(() => false); + } + + /** + * Wait for the section to finish loading (no skeleton loaders) + */ + async waitForLoad(): Promise { + const section = this.getSection(); + await section.waitFor({ state: "visible", timeout: 10000 }); + + // Wait for skeleton loaders to disappear + await this.page + .waitForFunction( + () => { + const sect = document.querySelector("[data-testid='section-variants']"); + if (!sect) return false; + const skeletons = sect.querySelectorAll(".MuiSkeleton-root"); + return skeletons.length === 0; + }, + { timeout: 15000 } + ) + .catch(() => { + // If no skeletons found, section already loaded + }); + } + + // Table + getTable(): Locator { + return this.getSection().locator("table"); + } + + async isTableVisible(): Promise { + return await this.getTable() + .isVisible() + .catch(() => false); + } + + async getTableRows(): Promise { + const tbody = this.getTable().locator("tbody"); + const rows = tbody.locator("tr"); + return await rows.count(); + } + + async getTableRow(index: number): Promise { + const tbody = this.getTable().locator("tbody"); + return tbody.locator("tr").nth(index); + } + + // Variant link + async getVariantLink(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + return row.locator("a[href*='/variant/']"); + } + + async clickVariantLink(rowIndex: number): Promise { + const link = await this.getVariantLink(rowIndex); + await link.click(); + } + + async getVariantId(rowIndex: number): Promise { + const link = await this.getVariantLink(rowIndex); + return await link.textContent(); + } + + async hasVariantLink(rowIndex: number): Promise { + const link = await this.getVariantLink(rowIndex); + return await link.isVisible().catch(() => false); + } + + // Lead variant badge + async isLeadVariant(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + const leadBadge = row.locator("text=lead"); + return await leadBadge.isVisible().catch(() => false); + } + + // P-value + async getPValue(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + const cell = row.locator("td").nth(1); + return await cell.textContent(); + } + + // Beta + async getBeta(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + const cell = row.locator("td").nth(2); + return await cell.textContent(); + } + + // Standard error + async getStandardError(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + const cell = row.locator("td").nth(3); + return await cell.textContent(); + } + + // LD (r²) + async getLDR2(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + const cell = row.locator("td").nth(4); + return await cell.textContent(); + } + + // Posterior Probability + async getPosteriorProbability(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + const cell = row.locator("td").nth(5); + return await cell.textContent(); + } + + // log(BF) + async getLogBF(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + const cell = row.locator("td").nth(6); + return await cell.textContent(); + } + + // Predicted consequence link + async getPredictedConsequenceLink(rowIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + return row.locator("a[href*='identifiers.org/SO']"); + } + + async getPredictedConsequence(rowIndex: number): Promise { + const link = await this.getPredictedConsequenceLink(rowIndex); + return await link.textContent(); + } + + async hasPredictedConsequenceLink(rowIndex: number): Promise { + const link = await this.getPredictedConsequenceLink(rowIndex); + return await link.isVisible().catch(() => false); + } + + // Cell text by column index + async getCellText(rowIndex: number, columnIndex: number): Promise { + const row = await this.getTableRow(rowIndex); + const cell = row.locator("td").nth(columnIndex); + return await cell.textContent(); + } + + // Global filter/search + getSearchInput(): Locator { + return this.getSection().locator("input[placeholder*='Search']"); + } + + async search(searchTerm: string): Promise { + await this.getSearchInput().fill(searchTerm); + await this.waitForLoad(); + } + + async clearSearch(): Promise { + await this.getSearchInput().clear(); + await this.waitForLoad(); + } + + // Pagination + getNextPageButton(): Locator { + return this.getSection().locator("button[aria-label='Next Page']"); + } + + getPreviousPageButton(): Locator { + return this.getSection().locator("button[aria-label='Previous Page']"); + } + + async clickNextPage(): Promise { + await this.getNextPageButton().click(); + await this.waitForLoad(); + } + + async clickPreviousPage(): Promise { + await this.getPreviousPageButton().click(); + await this.waitForLoad(); + } + + async isNextPageEnabled(): Promise { + return await this.getNextPageButton().isEnabled(); + } + + async isPreviousPageEnabled(): Promise { + return await this.getPreviousPageButton().isEnabled(); + } + + // Column visibility button + getColumnsButton(): Locator { + return this.getSection().locator("button:has-text('Columns')"); + } + + // Export button + getExportButton(): Locator { + return this.getSection().locator("button:has-text('Export')"); + } + + // API query button + getAPIQueryButton(): Locator { + return this.getSection().locator("button:has-text('API query')"); + } +} diff --git a/packages/platform-test/POM/objects/widgets/shared/GWASCredibleSetsSection.ts b/packages/platform-test/POM/objects/widgets/shared/GWASCredibleSetsSection.ts index 71dd8a53a..0a711c638 100644 --- a/packages/platform-test/POM/objects/widgets/shared/GWASCredibleSetsSection.ts +++ b/packages/platform-test/POM/objects/widgets/shared/GWASCredibleSetsSection.ts @@ -70,7 +70,9 @@ export class GWASCredibleSetsSection { async getCredibleSetId(rowIndex: number): Promise { const link = await this.getCredibleSetLink(rowIndex); - return await link.textContent(); + const href = await link.getAttribute("href"); + // Extract ID from href like "/credible-set/3afad64401516cb9221ad8d17656d547" + return href?.split("/credible-set/")[1] || null; } // Lead variant diff --git a/packages/platform-test/POM/page/credibleSet/credibleSet.ts b/packages/platform-test/POM/page/credibleSet/credibleSet.ts index 28ca3ee5a..c10b836ce 100644 --- a/packages/platform-test/POM/page/credibleSet/credibleSet.ts +++ b/packages/platform-test/POM/page/credibleSet/credibleSet.ts @@ -58,7 +58,7 @@ export class CredibleSetPage { // Header elements getHeader(): Locator { - return this.page.locator("[data-testid='profile-page-header']"); + return this.page.locator("[data-testid='profile-page-header-block']"); } async isHeaderVisible(): Promise { diff --git a/packages/platform-test/e2e/pages/credibleSet/credibleSetPage.spec.ts b/packages/platform-test/e2e/pages/credibleSet/credibleSetPage.spec.ts new file mode 100644 index 000000000..dc2922f9b --- /dev/null +++ b/packages/platform-test/e2e/pages/credibleSet/credibleSetPage.spec.ts @@ -0,0 +1,102 @@ +import { expect, test } from "../../../fixtures"; +import { CredibleSetPage } from "../../../POM/page/credibleSet/credibleSet"; + +test.describe("Credible Set Page", () => { + test.beforeEach(async ({ page, baseURL, testConfig }) => { + const credibleSetId = testConfig.credibleSet?.primary; + await page.goto(`${baseURL}/credible-set/${credibleSetId}`); + await page.waitForLoadState("networkidle"); + }); + + test("Page loads successfully", async ({ page }) => { + const credibleSetPage = new CredibleSetPage(page); + const isOnPage = await credibleSetPage.isCredibleSetPage(); + expect(isOnPage).toBe(true); + }); + + test("Header is visible", async ({ page }) => { + const credibleSetPage = new CredibleSetPage(page); + await credibleSetPage.waitForCredibleSetPageLoad(); + const isHeaderVisible = await credibleSetPage.isHeaderVisible(); + expect(isHeaderVisible).toBe(true); + }); + + test("Page title is displayed", async ({ page }) => { + const credibleSetPage = new CredibleSetPage(page); + await credibleSetPage.waitForCredibleSetPageLoad(); + const title = await credibleSetPage.getPageTitleText(); + expect(title).toBeTruthy(); + }); + + test("Lead variant link is present", async ({ page }) => { + const credibleSetPage = new CredibleSetPage(page); + await credibleSetPage.waitForCredibleSetPageLoad(); + const leadVariantLink = credibleSetPage.getLeadVariantLink(); + const isVisible = await leadVariantLink.isVisible().catch(() => false); + + if (isVisible) { + const variantId = await credibleSetPage.getLeadVariantId(); + expect(variantId).toBeTruthy(); + } else { + test.skip(); + } + }); + + test("Study link is present", async ({ page }) => { + const credibleSetPage = new CredibleSetPage(page); + await credibleSetPage.waitForCredibleSetPageLoad(); + const studyLink = credibleSetPage.getStudyLink(); + const isVisible = await studyLink.isVisible().catch(() => false); + + if (isVisible) { + const studyId = await credibleSetPage.getStudyId(); + expect(studyId).toBeTruthy(); + } else { + test.skip(); + } + }); + + test("Profile tab is visible and active", async ({ page }) => { + const credibleSetPage = new CredibleSetPage(page); + await credibleSetPage.waitForCredibleSetPageLoad(); + const profileTab = credibleSetPage.getProfileTab(); + const isVisible = await profileTab.isVisible().catch(() => false); + + if (isVisible) { + const isActive = await credibleSetPage.isProfileTabActive(); + expect(isActive).toBe(true); + } else { + test.skip(); + } + }); + + test("Lead variant link navigates to variant page", async ({ page }) => { + const credibleSetPage = new CredibleSetPage(page); + await credibleSetPage.waitForCredibleSetPageLoad(); + const leadVariantLink = credibleSetPage.getLeadVariantLink(); + const isVisible = await leadVariantLink.isVisible().catch(() => false); + + if (isVisible) { + await credibleSetPage.clickLeadVariantLink(); + await page.waitForLoadState("networkidle"); + expect(page.url()).toContain("/variant/"); + } else { + test.skip(); + } + }); + + test("Study link navigates to study page", async ({ page }) => { + const credibleSetPage = new CredibleSetPage(page); + await credibleSetPage.waitForCredibleSetPageLoad(); + const studyLink = credibleSetPage.getStudyLink(); + const isVisible = await studyLink.isVisible().catch(() => false); + + if (isVisible) { + await credibleSetPage.clickStudyLink(); + await page.waitForLoadState("networkidle"); + expect(page.url()).toContain("/study/"); + } else { + test.skip(); + } + }); +}); diff --git a/packages/platform-test/e2e/pages/credibleSet/sections/enhancerToGenePredictionsSection.spec.ts b/packages/platform-test/e2e/pages/credibleSet/sections/enhancerToGenePredictionsSection.spec.ts new file mode 100644 index 000000000..2e4376a10 --- /dev/null +++ b/packages/platform-test/e2e/pages/credibleSet/sections/enhancerToGenePredictionsSection.spec.ts @@ -0,0 +1,147 @@ +import { expect, test } from "../../../../fixtures"; +import { CredibleSetEnhancerToGenePredictionsSection } from "../../../../POM/objects/widgets/CredibleSet/enhancerToGenePredictionsSection"; + +test.describe("Enhancer to Gene Predictions Section", () => { + test.beforeEach(async ({ page, baseURL, testConfig }) => { + const credibleSetId = testConfig.credibleSet?.primary; + await page.goto(`${baseURL}/credible-set/${credibleSetId}`); + await page.waitForLoadState("networkidle"); + }); + + test("Section is visible when credible set has E2G data", async ({ page }) => { + const e2gSection = new CredibleSetEnhancerToGenePredictionsSection(page); + const isVisible = await e2gSection.isSectionVisible(); + + if (isVisible) { + await e2gSection.waitForLoad(); + expect(isVisible).toBe(true); + } else { + test.skip(); + } + }); + + test("Table is displayed with E2G predictions", async ({ page }) => { + const e2gSection = new CredibleSetEnhancerToGenePredictionsSection(page); + const isVisible = await e2gSection.isSectionVisible(); + + if (isVisible) { + await e2gSection.waitForLoad(); + const isTableVisible = await e2gSection.isTableVisible(); + expect(isTableVisible).toBe(true); + } else { + test.skip(); + } + }); + + test("Table contains rows with E2G predictions", async ({ page }) => { + const e2gSection = new CredibleSetEnhancerToGenePredictionsSection(page); + const isVisible = await e2gSection.isSectionVisible(); + + if (isVisible) { + await e2gSection.waitForLoad(); + const rowCount = await e2gSection.getTableRows(); + expect(rowCount).toBeGreaterThanOrEqual(0); + } else { + test.skip(); + } + }); + + test("Target gene link is displayed in table rows", async ({ page }) => { + const e2gSection = new CredibleSetEnhancerToGenePredictionsSection(page); + const isVisible = await e2gSection.isSectionVisible(); + + if (isVisible) { + await e2gSection.waitForLoad(); + const rowCount = await e2gSection.getTableRows(); + + if (rowCount > 0) { + const hasLink = await e2gSection.hasTargetGeneLink(0); + if (hasLink) { + const geneName = await e2gSection.getTargetGeneName(0); + expect(geneName).toBeTruthy(); + } + } + } else { + test.skip(); + } + }); + + test("E2G score is displayed in table rows", async ({ page }) => { + const e2gSection = new CredibleSetEnhancerToGenePredictionsSection(page); + const isVisible = await e2gSection.isSectionVisible(); + + if (isVisible) { + await e2gSection.waitForLoad(); + const rowCount = await e2gSection.getTableRows(); + + if (rowCount > 0) { + const e2gScore = await e2gSection.getE2GScore(0); + expect(e2gScore).toBeTruthy(); + } + } else { + test.skip(); + } + }); + + test("Target gene link navigates to target page", async ({ page }) => { + const e2gSection = new CredibleSetEnhancerToGenePredictionsSection(page); + const isVisible = await e2gSection.isSectionVisible(); + + if (isVisible) { + await e2gSection.waitForLoad(); + const rowCount = await e2gSection.getTableRows(); + + if (rowCount > 0) { + const hasLink = await e2gSection.hasTargetGeneLink(0); + if (hasLink) { + await e2gSection.clickTargetGeneLink(0); + await page.waitForLoadState("networkidle"); + expect(page.url()).toContain("/target/"); + } + } + } else { + test.skip(); + } + }); + + test("Search functionality filters E2G predictions", async ({ page }) => { + const e2gSection = new CredibleSetEnhancerToGenePredictionsSection(page); + const isVisible = await e2gSection.isSectionVisible(); + + if (isVisible) { + await e2gSection.waitForLoad(); + const searchInput = e2gSection.getSearchInput(); + const searchVisible = await searchInput.isVisible().catch(() => false); + + if (searchVisible) { + await e2gSection.search("test"); + await page.waitForTimeout(500); + const inputValue = await searchInput.inputValue(); + expect(inputValue).toBe("test"); + } + } else { + test.skip(); + } + }); + + test("Pagination controls are functional", async ({ page }) => { + const e2gSection = new CredibleSetEnhancerToGenePredictionsSection(page); + const isVisible = await e2gSection.isSectionVisible(); + + if (isVisible) { + await e2gSection.waitForLoad(); + const rowCount = await e2gSection.getTableRows(); + + if (rowCount >= 10) { + const isNextEnabled = await e2gSection.isNextPageEnabled(); + if (isNextEnabled) { + await e2gSection.clickNextPage(); + const isPrevEnabled = await e2gSection.isPreviousPageEnabled(); + expect(isPrevEnabled).toBe(true); + } + } + } else { + test.skip(); + } + }); +}); diff --git a/packages/platform-test/e2e/pages/credibleSet/sections/gwasColocSection.spec.ts b/packages/platform-test/e2e/pages/credibleSet/sections/gwasColocSection.spec.ts new file mode 100644 index 000000000..2fa77e80a --- /dev/null +++ b/packages/platform-test/e2e/pages/credibleSet/sections/gwasColocSection.spec.ts @@ -0,0 +1,240 @@ +import { expect, test } from "../../../../fixtures"; +import { GWASColocSection } from "../../../../POM/objects/widgets/CredibleSet/gwasColocSection"; + +test.describe("GWAS Colocalisation Section", () => { + test.beforeEach(async ({ page, baseURL, testConfig }) => { + const credibleSetId = testConfig.credibleSet?.primary; + await page.goto(`${baseURL}/credible-set/${credibleSetId}`); + await page.waitForLoadState("networkidle"); + }); + + test("Section is visible when credible set has GWAS coloc data", async ({ page }) => { + const gwasColocSection = new GWASColocSection(page); + const isVisible = await gwasColocSection.isSectionVisible(); + + if (isVisible) { + await gwasColocSection.waitForLoad(); + expect(isVisible).toBe(true); + } else { + test.skip(); + } + }); + + test("Table is displayed with colocalisation data", async ({ page }) => { + const gwasColocSection = new GWASColocSection(page); + const isVisible = await gwasColocSection.isSectionVisible(); + + if (isVisible) { + await gwasColocSection.waitForLoad(); + const isTableVisible = await gwasColocSection.isTableVisible(); + expect(isTableVisible).toBe(true); + } else { + test.skip(); + } + }); + + test("Table contains rows with colocalisation data", async ({ page }) => { + const gwasColocSection = new GWASColocSection(page); + const isVisible = await gwasColocSection.isSectionVisible(); + + if (isVisible) { + await gwasColocSection.waitForLoad(); + const rowCount = await gwasColocSection.getTableRows(); + expect(rowCount).toBeGreaterThanOrEqual(0); + } else { + test.skip(); + } + }); + + test("Credible set link is displayed in table rows", async ({ page }) => { + const gwasColocSection = new GWASColocSection(page); + const isVisible = await gwasColocSection.isSectionVisible(); + + if (isVisible) { + await gwasColocSection.waitForLoad(); + const rowCount = await gwasColocSection.getTableRows(); + + if (rowCount > 0) { + const hasLink = await gwasColocSection.hasCredibleSetLink(0); + expect(hasLink).toBe(true); + } + } else { + test.skip(); + } + }); + + test("Study link is displayed in table rows", async ({ page }) => { + const gwasColocSection = new GWASColocSection(page); + const isVisible = await gwasColocSection.isSectionVisible(); + + if (isVisible) { + await gwasColocSection.waitForLoad(); + const rowCount = await gwasColocSection.getTableRows(); + + if (rowCount > 0) { + const hasLink = await gwasColocSection.hasStudyLink(0); + if (hasLink) { + const studyId = await gwasColocSection.getStudyId(0); + expect(studyId).toBeTruthy(); + } + } + } else { + test.skip(); + } + }); + + test("Lead variant link is displayed in table rows", async ({ page }) => { + const gwasColocSection = new GWASColocSection(page); + const isVisible = await gwasColocSection.isSectionVisible(); + + if (isVisible) { + await gwasColocSection.waitForLoad(); + const rowCount = await gwasColocSection.getTableRows(); + + if (rowCount > 0) { + const hasLink = await gwasColocSection.hasLeadVariantLink(0); + expect(hasLink).toBe(true); + } + } else { + test.skip(); + } + }); + + test("Reported trait is displayed in table rows", async ({ page }) => { + const gwasColocSection = new GWASColocSection(page); + const isVisible = await gwasColocSection.isSectionVisible(); + + if (isVisible) { + await gwasColocSection.waitForLoad(); + const rowCount = await gwasColocSection.getTableRows(); + + if (rowCount > 0) { + const reportedTrait = await gwasColocSection.getReportedTrait(0); + expect(reportedTrait).toBeTruthy(); + } + } else { + test.skip(); + } + }); + + test("Colocalisation method is displayed", async ({ page }) => { + const gwasColocSection = new GWASColocSection(page); + const isVisible = await gwasColocSection.isSectionVisible(); + + if (isVisible) { + await gwasColocSection.waitForLoad(); + const rowCount = await gwasColocSection.getTableRows(); + + if (rowCount > 0) { + const method = await gwasColocSection.getColocalisationMethod(0); + expect(method).toBeTruthy(); + } + } else { + test.skip(); + } + }); + + test("Credible set link navigates to credible set page", async ({ page }) => { + const gwasColocSection = new GWASColocSection(page); + const isVisible = await gwasColocSection.isSectionVisible(); + + if (isVisible) { + await gwasColocSection.waitForLoad(); + const rowCount = await gwasColocSection.getTableRows(); + + if (rowCount > 0) { + const hasLink = await gwasColocSection.hasCredibleSetLink(0); + if (hasLink) { + await gwasColocSection.clickCredibleSetLink(0); + await page.waitForLoadState("networkidle"); + expect(page.url()).toContain("/credible-set/"); + } + } + } else { + test.skip(); + } + }); + + test("Study link navigates to study page", async ({ page }) => { + const gwasColocSection = new GWASColocSection(page); + const isVisible = await gwasColocSection.isSectionVisible(); + + if (isVisible) { + await gwasColocSection.waitForLoad(); + const rowCount = await gwasColocSection.getTableRows(); + + if (rowCount > 0) { + const hasLink = await gwasColocSection.hasStudyLink(0); + if (hasLink) { + await gwasColocSection.clickStudyLink(0); + await page.waitForLoadState("networkidle"); + expect(page.url()).toContain("/study/"); + } + } + } else { + test.skip(); + } + }); + + test("Lead variant link navigates to variant page", async ({ page }) => { + const gwasColocSection = new GWASColocSection(page); + const isVisible = await gwasColocSection.isSectionVisible(); + + if (isVisible) { + await gwasColocSection.waitForLoad(); + const rowCount = await gwasColocSection.getTableRows(); + + if (rowCount > 0) { + const hasLink = await gwasColocSection.hasLeadVariantLink(0); + if (hasLink) { + await gwasColocSection.clickLeadVariantLink(0); + await page.waitForLoadState("networkidle"); + expect(page.url()).toContain("/variant/"); + } + } + } else { + test.skip(); + } + }); + + test("Search functionality filters colocalisation data", async ({ page }) => { + const gwasColocSection = new GWASColocSection(page); + const isVisible = await gwasColocSection.isSectionVisible(); + + if (isVisible) { + await gwasColocSection.waitForLoad(); + const searchInput = gwasColocSection.getSearchInput(); + const searchVisible = await searchInput.isVisible().catch(() => false); + + if (searchVisible) { + await gwasColocSection.search("test"); + await page.waitForTimeout(500); + const inputValue = await searchInput.inputValue(); + expect(inputValue).toBe("test"); + } + } else { + test.skip(); + } + }); + + test("Pagination controls are functional", async ({ page }) => { + const gwasColocSection = new GWASColocSection(page); + const isVisible = await gwasColocSection.isSectionVisible(); + + if (isVisible) { + await gwasColocSection.waitForLoad(); + const rowCount = await gwasColocSection.getTableRows(); + + if (rowCount >= 10) { + const isNextEnabled = await gwasColocSection.isNextPageEnabled(); + if (isNextEnabled) { + await gwasColocSection.clickNextPage(); + const isPrevEnabled = await gwasColocSection.isPreviousPageEnabled(); + expect(isPrevEnabled).toBe(true); + } + } + } else { + test.skip(); + } + }); +}); diff --git a/packages/platform-test/e2e/pages/credibleSet/sections/locus2GeneSection.spec.ts b/packages/platform-test/e2e/pages/credibleSet/sections/locus2GeneSection.spec.ts new file mode 100644 index 000000000..65889eb21 --- /dev/null +++ b/packages/platform-test/e2e/pages/credibleSet/sections/locus2GeneSection.spec.ts @@ -0,0 +1,126 @@ +import { expect, test } from "../../../../fixtures"; +import { Locus2GeneSection } from "../../../../POM/objects/widgets/CredibleSet/locus2GeneSection"; + +test.describe("Locus2Gene Section", () => { + test.beforeEach(async ({ page, baseURL, testConfig }) => { + const credibleSetId = testConfig.credibleSet?.primary; + await page.goto(`${baseURL}/credible-set/${credibleSetId}`); + await page.waitForLoadState("networkidle"); + }); + + test("Section is visible when credible set has L2G data", async ({ page }) => { + const l2gSection = new Locus2GeneSection(page); + const isVisible = await l2gSection.isSectionVisible(); + + if (isVisible) { + await l2gSection.waitForLoad(); + expect(isVisible).toBe(true); + } else { + test.skip(); + } + }); + + test("Heatmap table is displayed", async ({ page }) => { + const l2gSection = new Locus2GeneSection(page); + const isVisible = await l2gSection.isSectionVisible(); + + if (isVisible) { + await l2gSection.waitForLoad(); + const isHeatmapVisible = await l2gSection.isHeatmapVisible(); + expect(isHeatmapVisible).toBe(true); + } else { + test.skip(); + } + }); + + test("Table contains rows with L2G predictions", async ({ page }) => { + const l2gSection = new Locus2GeneSection(page); + const isVisible = await l2gSection.isSectionVisible(); + + if (isVisible) { + await l2gSection.waitForLoad(); + const rowCount = await l2gSection.getTableRows(); + expect(rowCount).toBeGreaterThanOrEqual(0); + } else { + test.skip(); + } + }); + + test("Target gene link is displayed in table rows", async ({ page }) => { + const l2gSection = new Locus2GeneSection(page); + const isVisible = await l2gSection.isSectionVisible(); + + if (isVisible) { + await l2gSection.waitForLoad(); + const rowCount = await l2gSection.getTableRows(); + + if (rowCount > 0) { + const hasLink = await l2gSection.hasTargetGeneLink(0); + if (hasLink) { + const geneName = await l2gSection.getTargetGeneName(0); + expect(geneName).toBeTruthy(); + } + } + } else { + test.skip(); + } + }); + + test("L2G score is displayed in table rows", async ({ page }) => { + const l2gSection = new Locus2GeneSection(page); + const isVisible = await l2gSection.isSectionVisible(); + + if (isVisible) { + await l2gSection.waitForLoad(); + const rowCount = await l2gSection.getTableRows(); + + if (rowCount > 0) { + const l2gScore = await l2gSection.getL2GScore(0); + expect(l2gScore).toBeTruthy(); + } + } else { + test.skip(); + } + }); + + test("Target gene link navigates to target page", async ({ page }) => { + const l2gSection = new Locus2GeneSection(page); + const isVisible = await l2gSection.isSectionVisible(); + + if (isVisible) { + await l2gSection.waitForLoad(); + const rowCount = await l2gSection.getTableRows(); + + if (rowCount > 0) { + const hasLink = await l2gSection.hasTargetGeneLink(0); + if (hasLink) { + await l2gSection.clickTargetGeneLink(0); + await page.waitForLoadState("networkidle"); + expect(page.url()).toContain("/target/"); + } + } + } else { + test.skip(); + } + }); + + test("Search functionality filters L2G predictions", async ({ page }) => { + const l2gSection = new Locus2GeneSection(page); + const isVisible = await l2gSection.isSectionVisible(); + + if (isVisible) { + await l2gSection.waitForLoad(); + const searchInput = l2gSection.getSearchInput(); + const searchVisible = await searchInput.isVisible().catch(() => false); + + if (searchVisible) { + await l2gSection.search("test"); + await page.waitForTimeout(500); + const inputValue = await searchInput.inputValue(); + expect(inputValue).toBe("test"); + } + } else { + test.skip(); + } + }); +}); diff --git a/packages/platform-test/e2e/pages/credibleSet/sections/molQTLColocSection.spec.ts b/packages/platform-test/e2e/pages/credibleSet/sections/molQTLColocSection.spec.ts new file mode 100644 index 000000000..1f7773802 --- /dev/null +++ b/packages/platform-test/e2e/pages/credibleSet/sections/molQTLColocSection.spec.ts @@ -0,0 +1,301 @@ +import { expect, test } from "../../../../fixtures"; +import { MolQTLColocSection } from "../../../../POM/objects/widgets/CredibleSet/molQTLColocSection"; + +test.describe("MolQTL Colocalisation Section", () => { + test.beforeEach(async ({ page, baseURL, testConfig }) => { + const credibleSetId = testConfig.credibleSet?.primary; + await page.goto(`${baseURL}/credible-set/${credibleSetId}`); + await page.waitForLoadState("networkidle"); + }); + + test("Section is visible when credible set has MolQTL coloc data", async ({ page }) => { + const molQTLSection = new MolQTLColocSection(page); + const isVisible = await molQTLSection.isSectionVisible(); + + if (isVisible) { + await molQTLSection.waitForLoad(); + expect(isVisible).toBe(true); + } else { + test.skip(); + } + }); + + test("Table is displayed with colocalisation data", async ({ page }) => { + const molQTLSection = new MolQTLColocSection(page); + const isVisible = await molQTLSection.isSectionVisible(); + + if (isVisible) { + await molQTLSection.waitForLoad(); + const isTableVisible = await molQTLSection.isTableVisible(); + expect(isTableVisible).toBe(true); + } else { + test.skip(); + } + }); + + test("Table contains rows with colocalisation data", async ({ page }) => { + const molQTLSection = new MolQTLColocSection(page); + const isVisible = await molQTLSection.isSectionVisible(); + + if (isVisible) { + await molQTLSection.waitForLoad(); + const rowCount = await molQTLSection.getTableRows(); + expect(rowCount).toBeGreaterThanOrEqual(0); + } else { + test.skip(); + } + }); + + test("Credible set link is displayed in table rows", async ({ page }) => { + const molQTLSection = new MolQTLColocSection(page); + const isVisible = await molQTLSection.isSectionVisible(); + + if (isVisible) { + await molQTLSection.waitForLoad(); + const rowCount = await molQTLSection.getTableRows(); + + if (rowCount > 0) { + const hasLink = await molQTLSection.hasCredibleSetLink(0); + expect(hasLink).toBe(true); + } + } else { + test.skip(); + } + }); + + test("Study link is displayed in table rows", async ({ page }) => { + const molQTLSection = new MolQTLColocSection(page); + const isVisible = await molQTLSection.isSectionVisible(); + + if (isVisible) { + await molQTLSection.waitForLoad(); + const rowCount = await molQTLSection.getTableRows(); + + if (rowCount > 0) { + const hasLink = await molQTLSection.hasStudyLink(0); + if (hasLink) { + const studyId = await molQTLSection.getStudyId(0); + expect(studyId).toBeTruthy(); + } + } + } else { + test.skip(); + } + }); + + test("Study type is displayed in table rows", async ({ page }) => { + const molQTLSection = new MolQTLColocSection(page); + const isVisible = await molQTLSection.isSectionVisible(); + + if (isVisible) { + await molQTLSection.waitForLoad(); + const rowCount = await molQTLSection.getTableRows(); + + if (rowCount > 0) { + const studyType = await molQTLSection.getStudyType(0); + expect(studyType).toBeTruthy(); + } + } else { + test.skip(); + } + }); + + test("Affected gene link is displayed in table rows", async ({ page }) => { + const molQTLSection = new MolQTLColocSection(page); + const isVisible = await molQTLSection.isSectionVisible(); + + if (isVisible) { + await molQTLSection.waitForLoad(); + const rowCount = await molQTLSection.getTableRows(); + + if (rowCount > 0) { + const hasLink = await molQTLSection.hasAffectedGeneLink(0); + if (hasLink) { + const geneName = await molQTLSection.getAffectedGeneName(0); + expect(geneName).toBeTruthy(); + } + } + } else { + test.skip(); + } + }); + + test("Affected tissue link is displayed when present", async ({ page }) => { + const molQTLSection = new MolQTLColocSection(page); + const isVisible = await molQTLSection.isSectionVisible(); + + if (isVisible) { + await molQTLSection.waitForLoad(); + const rowCount = await molQTLSection.getTableRows(); + + if (rowCount > 0) { + const hasLink = await molQTLSection.hasAffectedTissueLink(0); + if (hasLink) { + const tissueName = await molQTLSection.getAffectedTissueName(0); + expect(tissueName).toBeTruthy(); + } + } + } else { + test.skip(); + } + }); + + test("Lead variant link is displayed in table rows", async ({ page }) => { + const molQTLSection = new MolQTLColocSection(page); + const isVisible = await molQTLSection.isSectionVisible(); + + if (isVisible) { + await molQTLSection.waitForLoad(); + const rowCount = await molQTLSection.getTableRows(); + + if (rowCount > 0) { + const hasLink = await molQTLSection.hasLeadVariantLink(0); + expect(hasLink).toBe(true); + } + } else { + test.skip(); + } + }); + + test("Colocalisation method is displayed", async ({ page }) => { + const molQTLSection = new MolQTLColocSection(page); + const isVisible = await molQTLSection.isSectionVisible(); + + if (isVisible) { + await molQTLSection.waitForLoad(); + const rowCount = await molQTLSection.getTableRows(); + + if (rowCount > 0) { + const method = await molQTLSection.getColocalisationMethod(0); + expect(method).toBeTruthy(); + } + } else { + test.skip(); + } + }); + + test("Credible set link navigates to credible set page", async ({ page }) => { + const molQTLSection = new MolQTLColocSection(page); + const isVisible = await molQTLSection.isSectionVisible(); + + if (isVisible) { + await molQTLSection.waitForLoad(); + const rowCount = await molQTLSection.getTableRows(); + + if (rowCount > 0) { + const hasLink = await molQTLSection.hasCredibleSetLink(0); + if (hasLink) { + await molQTLSection.clickCredibleSetLink(0); + await page.waitForLoadState("networkidle"); + expect(page.url()).toContain("/credible-set/"); + } + } + } else { + test.skip(); + } + }); + + test("Study link navigates to study page", async ({ page }) => { + const molQTLSection = new MolQTLColocSection(page); + const isVisible = await molQTLSection.isSectionVisible(); + + if (isVisible) { + await molQTLSection.waitForLoad(); + const rowCount = await molQTLSection.getTableRows(); + + if (rowCount > 0) { + const hasLink = await molQTLSection.hasStudyLink(0); + if (hasLink) { + await molQTLSection.clickStudyLink(0); + await page.waitForLoadState("networkidle"); + expect(page.url()).toContain("/study/"); + } + } + } else { + test.skip(); + } + }); + + test("Affected gene link navigates to target page", async ({ page }) => { + const molQTLSection = new MolQTLColocSection(page); + const isVisible = await molQTLSection.isSectionVisible(); + + if (isVisible) { + await molQTLSection.waitForLoad(); + const rowCount = await molQTLSection.getTableRows(); + + if (rowCount > 0) { + const hasLink = await molQTLSection.hasAffectedGeneLink(0); + if (hasLink) { + await molQTLSection.clickAffectedGeneLink(0); + await page.waitForLoadState("networkidle"); + expect(page.url()).toContain("/target/"); + } + } + } else { + test.skip(); + } + }); + + test("Lead variant link navigates to variant page", async ({ page }) => { + const molQTLSection = new MolQTLColocSection(page); + const isVisible = await molQTLSection.isSectionVisible(); + + if (isVisible) { + await molQTLSection.waitForLoad(); + const rowCount = await molQTLSection.getTableRows(); + + if (rowCount > 0) { + const hasLink = await molQTLSection.hasLeadVariantLink(0); + if (hasLink) { + await molQTLSection.clickLeadVariantLink(0); + await page.waitForLoadState("networkidle"); + expect(page.url()).toContain("/variant/"); + } + } + } else { + test.skip(); + } + }); + + test("Search functionality filters colocalisation data", async ({ page }) => { + const molQTLSection = new MolQTLColocSection(page); + const isVisible = await molQTLSection.isSectionVisible(); + + if (isVisible) { + await molQTLSection.waitForLoad(); + const searchInput = molQTLSection.getSearchInput(); + const searchVisible = await searchInput.isVisible().catch(() => false); + + if (searchVisible) { + await molQTLSection.search("eQTL"); + await page.waitForTimeout(500); + const inputValue = await searchInput.inputValue(); + expect(inputValue).toBe("eQTL"); + } + } else { + test.skip(); + } + }); + + test("Pagination controls are functional", async ({ page }) => { + const molQTLSection = new MolQTLColocSection(page); + const isVisible = await molQTLSection.isSectionVisible(); + + if (isVisible) { + await molQTLSection.waitForLoad(); + const rowCount = await molQTLSection.getTableRows(); + + if (rowCount >= 10) { + const isNextEnabled = await molQTLSection.isNextPageEnabled(); + if (isNextEnabled) { + await molQTLSection.clickNextPage(); + const isPrevEnabled = await molQTLSection.isPreviousPageEnabled(); + expect(isPrevEnabled).toBe(true); + } + } + } else { + test.skip(); + } + }); +}); diff --git a/packages/platform-test/e2e/pages/credibleSet/sections/variantsSection.spec.ts b/packages/platform-test/e2e/pages/credibleSet/sections/variantsSection.spec.ts new file mode 100644 index 000000000..703e02f7e --- /dev/null +++ b/packages/platform-test/e2e/pages/credibleSet/sections/variantsSection.spec.ts @@ -0,0 +1,234 @@ +import { expect, test } from "../../../../fixtures"; +import { CredibleSetVariantsSection } from "../../../../POM/objects/widgets/CredibleSet/variantsSection"; + +test.describe("Credible Set Variants Section", () => { + test.beforeEach(async ({ page, baseURL, testConfig }) => { + const credibleSetId = testConfig.credibleSet?.primary; + await page.goto(`${baseURL}/credible-set/${credibleSetId}`); + await page.waitForLoadState("networkidle"); + }); + + test("Section is visible when credible set has variants data", async ({ page }) => { + const variantsSection = new CredibleSetVariantsSection(page); + const isVisible = await variantsSection.isSectionVisible(); + + if (isVisible) { + await variantsSection.waitForLoad(); + expect(isVisible).toBe(true); + } else { + test.skip(); + } + }); + + test("Table is displayed with variants data", async ({ page }) => { + const variantsSection = new CredibleSetVariantsSection(page); + const isVisible = await variantsSection.isSectionVisible(); + + if (isVisible) { + await variantsSection.waitForLoad(); + const isTableVisible = await variantsSection.isTableVisible(); + expect(isTableVisible).toBe(true); + } else { + test.skip(); + } + }); + + test("Table contains rows with variants", async ({ page }) => { + const variantsSection = new CredibleSetVariantsSection(page); + const isVisible = await variantsSection.isSectionVisible(); + + if (isVisible) { + await variantsSection.waitForLoad(); + const rowCount = await variantsSection.getTableRows(); + expect(rowCount).toBeGreaterThanOrEqual(1); + } else { + test.skip(); + } + }); + + test("Variant link is displayed in table rows", async ({ page }) => { + const variantsSection = new CredibleSetVariantsSection(page); + const isVisible = await variantsSection.isSectionVisible(); + + if (isVisible) { + await variantsSection.waitForLoad(); + const rowCount = await variantsSection.getTableRows(); + + if (rowCount > 0) { + const hasLink = await variantsSection.hasVariantLink(0); + if (hasLink) { + const variantId = await variantsSection.getVariantId(0); + expect(variantId).toBeTruthy(); + } + } + } else { + test.skip(); + } + }); + + test("Lead variant is marked in the table", async ({ page }) => { + const variantsSection = new CredibleSetVariantsSection(page); + const isVisible = await variantsSection.isSectionVisible(); + + if (isVisible) { + await variantsSection.waitForLoad(); + const rowCount = await variantsSection.getTableRows(); + + if (rowCount > 0) { + // At least one variant should be marked as lead + let hasLeadVariant = false; + for (let i = 0; i < rowCount; i++) { + const isLead = await variantsSection.isLeadVariant(i); + if (isLead) { + hasLeadVariant = true; + break; + } + } + expect(hasLeadVariant).toBe(true); + } + } else { + test.skip(); + } + }); + + test("Posterior probability is displayed in table rows", async ({ page }) => { + const variantsSection = new CredibleSetVariantsSection(page); + const isVisible = await variantsSection.isSectionVisible(); + + if (isVisible) { + await variantsSection.waitForLoad(); + const rowCount = await variantsSection.getTableRows(); + + if (rowCount > 0) { + const posteriorProb = await variantsSection.getPosteriorProbability(0); + expect(posteriorProb).toBeTruthy(); + } + } else { + test.skip(); + } + }); + + test("Predicted consequence link is displayed when available", async ({ page }) => { + const variantsSection = new CredibleSetVariantsSection(page); + const isVisible = await variantsSection.isSectionVisible(); + + if (isVisible) { + await variantsSection.waitForLoad(); + const rowCount = await variantsSection.getTableRows(); + + if (rowCount > 0) { + const hasLink = await variantsSection.hasPredictedConsequenceLink(0); + if (hasLink) { + const consequence = await variantsSection.getPredictedConsequence(0); + expect(consequence).toBeTruthy(); + } + } + } else { + test.skip(); + } + }); + + test("Variant link navigates to variant page", async ({ page }) => { + const variantsSection = new CredibleSetVariantsSection(page); + const isVisible = await variantsSection.isSectionVisible(); + + if (isVisible) { + await variantsSection.waitForLoad(); + const rowCount = await variantsSection.getTableRows(); + + if (rowCount > 0) { + const hasLink = await variantsSection.hasVariantLink(0); + if (hasLink) { + await variantsSection.clickVariantLink(0); + await page.waitForLoadState("networkidle"); + expect(page.url()).toContain("/variant/"); + } + } + } else { + test.skip(); + } + }); + + test("Search functionality filters variants", async ({ page }) => { + const variantsSection = new CredibleSetVariantsSection(page); + const isVisible = await variantsSection.isSectionVisible(); + + if (isVisible) { + await variantsSection.waitForLoad(); + const searchInput = variantsSection.getSearchInput(); + const searchVisible = await searchInput.isVisible().catch(() => false); + + if (searchVisible) { + await variantsSection.search("intron"); + await page.waitForTimeout(500); + const inputValue = await searchInput.inputValue(); + expect(inputValue).toBe("intron"); + } + } else { + test.skip(); + } + }); + + test("Pagination controls are functional", async ({ page }) => { + const variantsSection = new CredibleSetVariantsSection(page); + const isVisible = await variantsSection.isSectionVisible(); + + if (isVisible) { + await variantsSection.waitForLoad(); + const rowCount = await variantsSection.getTableRows(); + + if (rowCount >= 10) { + const isNextEnabled = await variantsSection.isNextPageEnabled(); + if (isNextEnabled) { + await variantsSection.clickNextPage(); + const isPrevEnabled = await variantsSection.isPreviousPageEnabled(); + expect(isPrevEnabled).toBe(true); + } + } + } else { + test.skip(); + } + }); + + test("Columns button is visible", async ({ page }) => { + const variantsSection = new CredibleSetVariantsSection(page); + const isVisible = await variantsSection.isSectionVisible(); + + if (isVisible) { + await variantsSection.waitForLoad(); + const columnsButton = variantsSection.getColumnsButton(); + const isColumnsVisible = await columnsButton.isVisible().catch(() => false); + expect(isColumnsVisible).toBe(true); + } else { + test.skip(); + } + }); + + test("Export button is visible", async ({ page }) => { + const variantsSection = new CredibleSetVariantsSection(page); + const isVisible = await variantsSection.isSectionVisible(); + + if (isVisible) { + await variantsSection.waitForLoad(); + const exportButton = variantsSection.getExportButton(); + const isExportVisible = await exportButton.isVisible().catch(() => false); + expect(isExportVisible).toBe(true); + } else { + test.skip(); + } + }); + + test("API query button is visible", async ({ page }) => { + const variantsSection = new CredibleSetVariantsSection(page); + const isVisible = await variantsSection.isSectionVisible(); + + if (isVisible) { + await variantsSection.waitForLoad(); + const apiButton = variantsSection.getAPIQueryButton(); + const isAPIVisible = await apiButton.isVisible().catch(() => false); + expect(isAPIVisible).toBe(true); + } else { + test.skip(); + } + }); +}); diff --git a/packages/platform-test/e2e/pages/disease/associatedTargetsEvidenceWidgets.spec.ts b/packages/platform-test/e2e/pages/disease/associatedTargetsEvidenceWidgets.spec.ts index 5b6fee934..c33a9f3df 100644 --- a/packages/platform-test/e2e/pages/disease/associatedTargetsEvidenceWidgets.spec.ts +++ b/packages/platform-test/e2e/pages/disease/associatedTargetsEvidenceWidgets.spec.ts @@ -78,11 +78,7 @@ test.describe("Disease Page - AOTF Evidence Widgets", () => { for (const geneSymbol of genesToTest) { // Search for the specific gene -<<<<<<< HEAD - await aotfActions.searchByName(geneSymbol); -======= await aotfActions.applyNameFilterAndWaitForResults(geneSymbol); ->>>>>>> origin/main // Wait for table to load with filtered results await aotfTable.waitForTableLoad(); @@ -198,11 +194,7 @@ test.describe("Disease Page - AOTF Evidence Widgets", () => { for (const geneSymbol of genesToTest) { // Search for the specific gene -<<<<<<< HEAD - await aotfActions.searchByName(geneSymbol); -======= await aotfActions.applyNameFilterAndWaitForResults(geneSymbol); ->>>>>>> origin/main // Wait for table to load with filtered results await aotfTable.waitForTableLoad(); diff --git a/packages/platform-test/e2e/pages/study/studyPageGWAS.spec.ts b/packages/platform-test/e2e/pages/study/studyPageGWAS.spec.ts index db2ead80a..4b7487147 100644 --- a/packages/platform-test/e2e/pages/study/studyPageGWAS.spec.ts +++ b/packages/platform-test/e2e/pages/study/studyPageGWAS.spec.ts @@ -284,14 +284,9 @@ test.describe("Study Page - GWAS Study", () => { await gwasCredibleSets.clickCredibleSetLink(0); await page.waitForURL("**/credible-set/**"); - // Verify we're on credible set page + // Verify we're on the correct credible set page using URL test.expect(page.url()).toContain("/credible-set/"); - // verify credible set page is fully loaded and contains clicked id - const credibleSetPage = new StudyPage(page); - await credibleSetPage.waitForStudyPageLoad(); - // expect credible set header to contain credible set id - const headerId = await credibleSetPage.getStudyIdFromHeader(); - test.expect(headerId).toContain(credibleSetId); + test.expect(page.url()).toContain(credibleSetId!); } } }); diff --git a/packages/platform-test/fixtures/testConfig.ts b/packages/platform-test/fixtures/testConfig.ts index cd56e99bd..0195444cb 100644 --- a/packages/platform-test/fixtures/testConfig.ts +++ b/packages/platform-test/fixtures/testConfig.ts @@ -41,6 +41,11 @@ function getDefaultConfig(): TestConfig { alternatives: [], }, }, + credibleSet: { + primary: "1aac7a781b1395adad7daa04e5d13970", + withGWASColoc: "8990f5e6515a5e24a3534da8ab339a92", + withQTLColoc: "5f4e2f4f4e2f4e2f4e2f4e2f4e2f4e2f", + }, }; } diff --git a/packages/platform-test/playwright-report/index.html b/packages/platform-test/playwright-report/index.html index a60eb2fbc..afaf54f04 100644 --- a/packages/platform-test/playwright-report/index.html +++ b/packages/platform-test/playwright-report/index.html @@ -73,4 +73,4 @@
    - + \ No newline at end of file diff --git a/packages/platform-test/types/index.ts b/packages/platform-test/types/index.ts index ae105cf3c..a64cb6484 100644 --- a/packages/platform-test/types/index.ts +++ b/packages/platform-test/types/index.ts @@ -45,6 +45,11 @@ export interface TestConfig { alternatives?: string[]; }; }; + credibleSet?: { + primary?: string; + withGWASColoc?: string; + withQTLColoc?: string; + }; } /** * CSV row structure from Google Sheet */ diff --git a/packages/platform-test/utils/csvRowToTestConfig.ts b/packages/platform-test/utils/csvRowToTestConfig.ts index 464837150..cd9216c49 100644 --- a/packages/platform-test/utils/csvRowToTestConfig.ts +++ b/packages/platform-test/utils/csvRowToTestConfig.ts @@ -44,5 +44,10 @@ export function csvRowToTestConfig(row: CSVRow): TestConfig { } : undefined, }, + credibleSet: { + primary: row.credible_set || undefined, + withGWASColoc: row.credible_set_GWAS_coloc || undefined, + withQTLColoc: row.credible_set_QTL_coloc || undefined, + }, }; } diff --git a/packages/platform-test/utils/mergeWithDefaults.ts b/packages/platform-test/utils/mergeWithDefaults.ts index 23a21f6d0..f463c366f 100644 --- a/packages/platform-test/utils/mergeWithDefaults.ts +++ b/packages/platform-test/utils/mergeWithDefaults.ts @@ -61,5 +61,12 @@ export function mergeWithDefaults(fetched: TestConfig, defaults: TestConfig): Te } : undefined, }, + credibleSet: { + primary: fetched.credibleSet?.primary || defaults.credibleSet?.primary, + withGWASColoc: + fetched.credibleSet?.withGWASColoc || defaults.credibleSet?.withGWASColoc, + withQTLColoc: + fetched.credibleSet?.withQTLColoc || defaults.credibleSet?.withQTLColoc, + }, }; } From e2df7257985b695431cebe98762cc13d44c1ceee Mon Sep 17 00:00:00 2001 From: david oluwasusi Date: Fri, 30 Jan 2026 11:06:00 +0000 Subject: [PATCH 29/34] chore(linting): fix linting --- .../pages/disease/associatedTargetsPrioritization.spec.ts | 2 +- .../platform-test/e2e/pages/disease/diseaseProfile.spec.ts | 2 +- packages/platform-test/e2e/pages/homepage.spec.ts | 2 +- .../platform-test/e2e/pages/study/studyPageGWAS.spec.ts | 2 +- packages/platform-test/fixtures/index.ts | 1 + packages/platform-test/types/index.ts | 2 +- packages/platform-test/utils/fetchConfigFromSheet.ts | 2 +- packages/platform-test/utils/mergeWithDefaults.ts | 6 ++---- 8 files changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/platform-test/e2e/pages/disease/associatedTargetsPrioritization.spec.ts b/packages/platform-test/e2e/pages/disease/associatedTargetsPrioritization.spec.ts index bd7cd2379..ab4c29f5c 100644 --- a/packages/platform-test/e2e/pages/disease/associatedTargetsPrioritization.spec.ts +++ b/packages/platform-test/e2e/pages/disease/associatedTargetsPrioritization.spec.ts @@ -4,7 +4,7 @@ import { AotfTable } from "../../../POM/objects/widgets/AOTF/aotfTable"; test.describe("Disease Page - AOTF Prioritization", () => { test.beforeEach(async ({ page, baseURL, testConfig }) => { - //if no disease id, skip all tests + //if no disease id, skip all tests if (!testConfig.disease.primary) { test.skip(); } diff --git a/packages/platform-test/e2e/pages/disease/diseaseProfile.spec.ts b/packages/platform-test/e2e/pages/disease/diseaseProfile.spec.ts index 6529fe894..62215f0e9 100644 --- a/packages/platform-test/e2e/pages/disease/diseaseProfile.spec.ts +++ b/packages/platform-test/e2e/pages/disease/diseaseProfile.spec.ts @@ -9,7 +9,7 @@ import { PhenotypesSection } from "../../../POM/objects/widgets/Phenotypes/pheno test.describe("Disease Profile Page", () => { test.beforeEach(async ({ page, baseURL, testConfig }) => { - //if no disease id, skip all tests + //if no disease id, skip all tests if (!testConfig.disease.primary) { test.skip(); } diff --git a/packages/platform-test/e2e/pages/homepage.spec.ts b/packages/platform-test/e2e/pages/homepage.spec.ts index 195121321..c592b1eec 100644 --- a/packages/platform-test/e2e/pages/homepage.spec.ts +++ b/packages/platform-test/e2e/pages/homepage.spec.ts @@ -13,7 +13,7 @@ test.describe("Home page actions", () => { let searchInput: Locator; test.beforeEach(async ({ page, baseURL }) => { - await page.goto(baseURL!); + await page.goto(baseURL ?? "/"); await page.getByTestId("global-search-input-trigger").click(); // Verify that the global search input is rendered diff --git a/packages/platform-test/e2e/pages/study/studyPageGWAS.spec.ts b/packages/platform-test/e2e/pages/study/studyPageGWAS.spec.ts index 4b7487147..d42c08ed3 100644 --- a/packages/platform-test/e2e/pages/study/studyPageGWAS.spec.ts +++ b/packages/platform-test/e2e/pages/study/studyPageGWAS.spec.ts @@ -286,7 +286,7 @@ test.describe("Study Page - GWAS Study", () => { // Verify we're on the correct credible set page using URL test.expect(page.url()).toContain("/credible-set/"); - test.expect(page.url()).toContain(credibleSetId!); + test.expect(page.url()).toContain(credibleSetId); } } }); diff --git a/packages/platform-test/fixtures/index.ts b/packages/platform-test/fixtures/index.ts index ea63454ab..7839af1d6 100644 --- a/packages/platform-test/fixtures/index.ts +++ b/packages/platform-test/fixtures/index.ts @@ -25,6 +25,7 @@ export const test = base.extend({ const config = await getTestConfig(); await use(config); }, + // biome-ignore lint/correctness/noEmptyPattern: baseURL: async ({}, use) => { const url = process.env.PLAYWRIGHT_TEST_BASE_URL || DEFAULT_BASE_URL; await use(url); diff --git a/packages/platform-test/types/index.ts b/packages/platform-test/types/index.ts index a64cb6484..6f155692b 100644 --- a/packages/platform-test/types/index.ts +++ b/packages/platform-test/types/index.ts @@ -48,7 +48,7 @@ export interface TestConfig { credibleSet?: { primary?: string; withGWASColoc?: string; - withQTLColoc?: string; + withQTLColoc?: string; }; } /** * CSV row structure from Google Sheet diff --git a/packages/platform-test/utils/fetchConfigFromSheet.ts b/packages/platform-test/utils/fetchConfigFromSheet.ts index 893145bce..14ba7b43f 100644 --- a/packages/platform-test/utils/fetchConfigFromSheet.ts +++ b/packages/platform-test/utils/fetchConfigFromSheet.ts @@ -1,6 +1,6 @@ import Papa from "papaparse"; -import { csvRowToTestConfig } from "./csvRowToTestConfig"; import type { CSVRow, TestConfig } from "../types"; +import { csvRowToTestConfig } from "./csvRowToTestConfig"; /** * Fetch test configuration from Google Sheet CSV URL diff --git a/packages/platform-test/utils/mergeWithDefaults.ts b/packages/platform-test/utils/mergeWithDefaults.ts index f463c366f..4fccd0f52 100644 --- a/packages/platform-test/utils/mergeWithDefaults.ts +++ b/packages/platform-test/utils/mergeWithDefaults.ts @@ -63,10 +63,8 @@ export function mergeWithDefaults(fetched: TestConfig, defaults: TestConfig): Te }, credibleSet: { primary: fetched.credibleSet?.primary || defaults.credibleSet?.primary, - withGWASColoc: - fetched.credibleSet?.withGWASColoc || defaults.credibleSet?.withGWASColoc, - withQTLColoc: - fetched.credibleSet?.withQTLColoc || defaults.credibleSet?.withQTLColoc, + withGWASColoc: fetched.credibleSet?.withGWASColoc || defaults.credibleSet?.withGWASColoc, + withQTLColoc: fetched.credibleSet?.withQTLColoc || defaults.credibleSet?.withQTLColoc, }, }; } From 3b7589b50aedaa48678ae7b047706c4e9a737c8b Mon Sep 17 00:00:00 2001 From: david oluwasusi Date: Tue, 3 Feb 2026 15:55:38 +0000 Subject: [PATCH 30/34] feat(docs): add docs generation for interactors --- package.json | 4 +- packages/platform-test/.gitignore | 13 + .../POM/objects/widgets/AOTF/aotfActions.ts | 20 + .../POM/objects/widgets/AOTF/aotfTable.ts | 18 + .../Bibliography/bibliographySection.ts | 18 + .../widgets/CredibleSet/locus2GeneSection.ts | 28 +- .../widgets/KnownDrugs/knownDrugsSection.ts | 19 + .../widgets/Variant/variantEffectSection.ts | 27 +- .../widgets/shared/GWASCredibleSetsSection.ts | 31 +- .../widgets/shared/adverseEventsSection.ts | 22 +- .../widgets/shared/cancerHallmarksSection.ts | 29 +- .../widgets/shared/chemicalProbesSection.ts | 32 +- .../shared/comparativeGenomicsSection.ts | 34 +- .../objects/widgets/shared/depMapSection.ts | 27 +- .../widgets/shared/drugWarningsSection.ts | 26 +- .../enhancerToGenePredictionsSection.ts | 26 +- .../POM/objects/widgets/shared/evaSection.ts | 27 +- .../widgets/shared/expressionSection.ts | 30 +- .../widgets/shared/geneOntologySection.ts | 30 +- .../shared/geneticConstraintSection.ts | 29 +- .../widgets/shared/indicationsSection.ts | 27 +- .../shared/mechanismsOfActionSection.ts | 28 +- .../shared/molecularInteractionsSection.ts | 32 +- .../shared/molecularStructureSection.ts | 24 +- .../widgets/shared/mousePhenotypesSection.ts | 29 +- .../objects/widgets/shared/pathwaysSection.ts | 28 +- .../widgets/shared/pharmacogenomicsSection.ts | 28 +- .../widgets/shared/qtlCredibleSetsSection.ts | 28 +- .../objects/widgets/shared/safetySection.ts | 31 +- .../shared/subcellularLocationSection.ts | 32 +- .../widgets/shared/tractabilitySection.ts | 32 +- .../widgets/shared/uniprotVariantsSection.ts | 26 +- .../shared/variantEffectPredictorSection.ts | 30 +- packages/platform-test/docs/WIDGETS_README.md | 94 ++++ packages/platform-test/package.json | 10 +- packages/platform-test/tsconfig.docs.json | 18 + packages/platform-test/typedoc.json | 26 + turbo.json | 7 + yarn.lock | 520 +++++++++++++++++- 39 files changed, 1459 insertions(+), 81 deletions(-) create mode 100644 packages/platform-test/.gitignore create mode 100644 packages/platform-test/docs/WIDGETS_README.md create mode 100644 packages/platform-test/tsconfig.docs.json create mode 100644 packages/platform-test/typedoc.json diff --git a/package.json b/package.json index 9a51bc3c0..fd1c9428a 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,9 @@ "check": "npx biome check --formatter-enabled=true .", "check:fix": "npx biome check --formatter-enabled=true --write .", "prepare": "husky install", - "test_workflow": "act -W '.github/workflows/e2e-ci.yml' --action-offline-mode --container-architecture linux/amd64" + "test_workflow": "act -W '.github/workflows/e2e-ci.yml' --action-offline-mode --container-architecture linux/amd64", + "docs:widgets": "turbo run docs:widgets --filter=platform-test", + "docs:widgets:serve": "turbo run docs:widgets:serve --filter=platform-test" }, "devDependencies": { "@biomejs/biome": "2.2.4", diff --git a/packages/platform-test/.gitignore b/packages/platform-test/.gitignore new file mode 100644 index 000000000..c70a83975 --- /dev/null +++ b/packages/platform-test/.gitignore @@ -0,0 +1,13 @@ +# Generated documentation +docs/widgets-api/ + +# Test results +test-results/ +playwright-report/ + +# Node +node_modules/ + +# Environment +.env +.env.local diff --git a/packages/platform-test/POM/objects/widgets/AOTF/aotfActions.ts b/packages/platform-test/POM/objects/widgets/AOTF/aotfActions.ts index 7b3cc6c50..62b735e12 100644 --- a/packages/platform-test/POM/objects/widgets/AOTF/aotfActions.ts +++ b/packages/platform-test/POM/objects/widgets/AOTF/aotfActions.ts @@ -1,5 +1,25 @@ import type { Locator, Page } from "@playwright/test"; +/** + * Interactor for AOTF (Association On-The-Fly) table action controls. + * + * Provides methods to interact with the AOTF table's control panel including: + * - Name filtering + * - Advanced facets/filters + * - Column options and weight controls + * - Export functionality + * - Display mode switching (Associations/Prioritisation view) + * + * @example + * ```typescript + * const actions = new AotfActions(page); + * await actions.searchByName("BRAF"); + * await actions.switchToPrioritisationView(); + * await actions.openExportMenu(); + * ``` + * + * @category AOTF + */ export class AotfActions { page: Page; diff --git a/packages/platform-test/POM/objects/widgets/AOTF/aotfTable.ts b/packages/platform-test/POM/objects/widgets/AOTF/aotfTable.ts index 073652c48..5272e9fa9 100644 --- a/packages/platform-test/POM/objects/widgets/AOTF/aotfTable.ts +++ b/packages/platform-test/POM/objects/widgets/AOTF/aotfTable.ts @@ -1,5 +1,23 @@ import type { Locator, Page } from "@playwright/test"; +/** + * Interactor for the Association On-The-Fly (AOTF) Table component. + * + * The AOTF table displays target-disease associations with scores across + * different data types. It supports multiple sections (Pinned, Uploaded, Core) + * and provides functionality for sorting, filtering, and pagination. + * + * @example + * ```typescript + * const table = new AotfTable(page); + * await table.waitForTableLoad(); + * const rowCount = await table.getRowCount(); + * const entityName = await table.getEntityName(0); + * ``` + * + * @category AOTF + * @remarks Section ID: `associations-table` + */ export class AotfTable { page: Page; diff --git a/packages/platform-test/POM/objects/widgets/Bibliography/bibliographySection.ts b/packages/platform-test/POM/objects/widgets/Bibliography/bibliographySection.ts index afbc92d63..4dc713630 100644 --- a/packages/platform-test/POM/objects/widgets/Bibliography/bibliographySection.ts +++ b/packages/platform-test/POM/objects/widgets/Bibliography/bibliographySection.ts @@ -1,5 +1,23 @@ import type { Locator, Page } from "@playwright/test"; +/** + * Interactor for the Bibliography section widget. + * + * Displays literature references and publications related to a target, disease, + * or drug. Supports searching through literature, pagination, and external + * PubMed links. + * + * @example + * ```typescript + * const biblio = new BibliographySection(page); + * await biblio.waitForSectionLoad(); + * const count = await biblio.getLiteratureCount(); + * await biblio.searchLiterature("BRAF mutation"); + * ``` + * + * @category Bibliography + * @remarks Section ID: `bibliography` + */ export class BibliographySection { page: Page; diff --git a/packages/platform-test/POM/objects/widgets/CredibleSet/locus2GeneSection.ts b/packages/platform-test/POM/objects/widgets/CredibleSet/locus2GeneSection.ts index d54f3832d..79b7a64a0 100644 --- a/packages/platform-test/POM/objects/widgets/CredibleSet/locus2GeneSection.ts +++ b/packages/platform-test/POM/objects/widgets/CredibleSet/locus2GeneSection.ts @@ -1,17 +1,39 @@ import type { Locator, Page } from "@playwright/test"; /** - * Interactor for Locus-to-Gene (L2G) section on Credible Set page - * Section ID: locus2gene + * Interactor for Locus-to-Gene (L2G) section on Credible Set page. + * + * The L2G section displays gene prioritization scores based on various + * evidence sources. Uses a HeatmapTable to visualize scores across + * different features. + * + * @example + * ```typescript + * const l2g = new Locus2GeneSection(page); + * await l2g.waitForLoad(); + * const geneCount = await l2g.getTableRows(); + * const topGene = await l2g.getTargetGeneName(0); + * const score = await l2g.getL2GScore(0); + * ``` + * + * @category CredibleSet + * @remarks Section ID: `locus2gene` */ export class Locus2GeneSection { constructor(private page: Page) {} - // Section container + /** + * Get the main section container element. + * @returns Locator for the section container + */ getSection(): Locator { return this.page.locator("[data-testid='section-locus2gene']"); } + /** + * Check if the section is currently visible on the page. + * @returns Promise resolving to true if visible, false otherwise + */ async isSectionVisible(): Promise { return await this.getSection() .isVisible() diff --git a/packages/platform-test/POM/objects/widgets/KnownDrugs/knownDrugsSection.ts b/packages/platform-test/POM/objects/widgets/KnownDrugs/knownDrugsSection.ts index 7e5aeb373..945db167e 100644 --- a/packages/platform-test/POM/objects/widgets/KnownDrugs/knownDrugsSection.ts +++ b/packages/platform-test/POM/objects/widgets/KnownDrugs/knownDrugsSection.ts @@ -1,5 +1,24 @@ import type { Locator, Page } from "@playwright/test"; +/** + * Interactor for the Known Drugs / Clinical Precedence section. + * + * Displays drugs that have been used to treat a disease or target, + * including clinical trial information and approval status. + * Supports searching, filtering, and pagination. + * + * @example + * ```typescript + * const drugs = new ClinicalPrecedenceSection(page); + * await drugs.waitForSectionLoad(); + * const rowCount = await drugs.getRowCount(); + * const drugName = await drugs.getDrugName(0); + * await drugs.searchDrug("aspirin"); + * ``` + * + * @category KnownDrugs + * @remarks Section ID: `knowndrugs` + */ export class ClinicalPrecedenceSection { page: Page; diff --git a/packages/platform-test/POM/objects/widgets/Variant/variantEffectSection.ts b/packages/platform-test/POM/objects/widgets/Variant/variantEffectSection.ts index 9e6a5cecd..335e14a66 100644 --- a/packages/platform-test/POM/objects/widgets/Variant/variantEffectSection.ts +++ b/packages/platform-test/POM/objects/widgets/Variant/variantEffectSection.ts @@ -1,16 +1,39 @@ import type { Locator, Page } from "@playwright/test"; /** - * Interactor for Variant Effect section + * Interactor for Variant Effect / In-Silico Predictors section. + * + * Displays computational predictions of variant effects from various + * in-silico prediction methods. Supports both table and chart views + * for visualizing prediction scores. + * + * @example + * ```typescript + * const variantEffect = new VariantEffectSection(page); + * await variantEffect.waitForLoad(); + * const methodName = await variantEffect.getMethodName(0); + * const prediction = await variantEffect.getPrediction(0); + * await variantEffect.switchToChartView(); + * ``` + * + * @category Variant + * @remarks Section ID: `in-silico-predictors` */ export class VariantEffectSection { constructor(private page: Page) {} - // Section container + /** + * Get the main section container element. + * @returns Locator for the section container + */ getSection(): Locator { return this.page.locator("[data-testid='section-in-silico-predictors']"); } + /** + * Check if the section is currently visible. + * @returns Promise resolving to true if visible, false otherwise + */ async isSectionVisible(): Promise { return await this.getSection() .isVisible() diff --git a/packages/platform-test/POM/objects/widgets/shared/GWASCredibleSetsSection.ts b/packages/platform-test/POM/objects/widgets/shared/GWASCredibleSetsSection.ts index 0a711c638..5b8359266 100644 --- a/packages/platform-test/POM/objects/widgets/shared/GWASCredibleSetsSection.ts +++ b/packages/platform-test/POM/objects/widgets/shared/GWASCredibleSetsSection.ts @@ -1,8 +1,35 @@ import type { Locator, Page } from "@playwright/test"; /** - * Shared interactor for GWAS Credible Sets section - * Used in both Variant and Study pages + * Interactor for the GWAS Credible Sets section (shared across Variant and Study pages). + * + * Displays fine-mapped credible sets from GWAS studies, representing the set of + * variants most likely to contain the causal variant. Information includes: + * - **Credible set ID**: Unique identifier linking to detailed credible set page + * - **Lead variant**: The most significant variant in the set + * - **Study information**: GWAS study ID and reported trait + * - **L2G gene**: Top gene from Locus-to-Gene prediction + * - **Posterior probability**: Statistical confidence for causal variants + * + * Used for both Variant pages (showing credible sets containing the variant) + * and Study pages (showing all credible sets from a study). + * + * @example + * ```typescript + * const gwasCredibleSets = new GWASCredibleSetsSection(page); + * await gwasCredibleSets.waitForLoad(); + * + * // Get credible set details + * const rowCount = await gwasCredibleSets.getTableRows(); + * const credibleSetId = await gwasCredibleSets.getCredibleSetId(0); + * + * // Navigate to related pages + * await gwasCredibleSets.clickCredibleSetLink(0); + * await gwasCredibleSets.clickStudyLink(0); + * ``` + * + * @category shared + * @remarks Section ID: `gwas-credible-sets` */ export class GWASCredibleSetsSection { constructor(private page: Page) {} diff --git a/packages/platform-test/POM/objects/widgets/shared/adverseEventsSection.ts b/packages/platform-test/POM/objects/widgets/shared/adverseEventsSection.ts index 39f557d20..009db5cdc 100644 --- a/packages/platform-test/POM/objects/widgets/shared/adverseEventsSection.ts +++ b/packages/platform-test/POM/objects/widgets/shared/adverseEventsSection.ts @@ -1,7 +1,27 @@ import type { Locator, Page } from "@playwright/test"; /** - * Interactor for Adverse Events section + * Interactor for the Adverse Events (Pharmacovigilance) section on Drug pages. + * + * Displays adverse event data from the FDA Adverse Event Reporting System (FAERS), + * including event names with MedDRA links, reported event counts, and log likelihood + * ratios indicating the strength of the drug-adverse event association. + * + * @example + * ```typescript + * const adverseEvents = new PharmacovigilanceSection(page); + * await adverseEvents.waitForLoad(); + * + * // Get number of adverse events + * const rowCount = await adverseEvents.getTableRows(); + * + * // Get details of first adverse event + * const eventName = await adverseEvents.getAdverseEventName(0); + * const llr = await adverseEvents.getLogLikelihoodRatio(0); + * ``` + * + * @category shared + * @remarks Section ID: `adverseevents` */ export class PharmacovigilanceSection { constructor(private page: Page) {} diff --git a/packages/platform-test/POM/objects/widgets/shared/cancerHallmarksSection.ts b/packages/platform-test/POM/objects/widgets/shared/cancerHallmarksSection.ts index 01317d159..d7fada21e 100644 --- a/packages/platform-test/POM/objects/widgets/shared/cancerHallmarksSection.ts +++ b/packages/platform-test/POM/objects/widgets/shared/cancerHallmarksSection.ts @@ -1,9 +1,32 @@ import type { Locator, Page } from "@playwright/test"; /** - * Interactor for Cancer Hallmarks section on Target page - * Displays cancer hallmarks with role in cancer chips and a table of hallmark effects - * Uses only data-testid selectors for reliable, predictable testing + * Interactor for the Cancer Hallmarks section on Target pages. + * + * Displays cancer hallmark annotations based on the "Hallmarks of Cancer" framework, + * showing the target's role in cancer progression. The section includes: + * - **Role in Cancer chips**: Visual indicators of oncogene/tumor suppressor roles + * - **Hallmark effects table**: Detailed breakdown of hallmark impacts with literature evidence + * + * Data is sourced from manually curated literature with links to supporting publications. + * + * @example + * ```typescript + * const cancerHallmarks = new CancerHallmarksSection(page); + * await cancerHallmarks.waitForLoad(); + * + * // Check role in cancer + * const roleLabel = await cancerHallmarks.getRoleInCancerChipLabel(0); + * + * // Search hallmarks table + * await cancerHallmarks.search("proliferation"); + * + * // Access publications + * await cancerHallmarks.clickPublicationsDrawer(); + * ``` + * + * @category shared + * @remarks Section ID: `cancerhallmarks` */ export class CancerHallmarksSection { constructor(private page: Page) {} diff --git a/packages/platform-test/POM/objects/widgets/shared/chemicalProbesSection.ts b/packages/platform-test/POM/objects/widgets/shared/chemicalProbesSection.ts index d04e695ca..5b9c42505 100644 --- a/packages/platform-test/POM/objects/widgets/shared/chemicalProbesSection.ts +++ b/packages/platform-test/POM/objects/widgets/shared/chemicalProbesSection.ts @@ -1,9 +1,35 @@ import type { Locator, Page } from "@playwright/test"; /** - * Interactor for Chemical Probes section on Target page - * Displays chemical probes with quality ratings and mechanisms of action - * Uses only data-testid selectors for reliable, predictable testing + * Interactor for the Chemical Probes section on Target pages. + * + * Displays high-quality chemical probes that can be used to investigate + * target biology. Data is sourced from the Chemical Probes Portal and includes: + * - **Probe names**: Identifiers with links to external resources + * - **Quality ratings**: Probe quality assessments (e.g., "Best available", "Historical") + * - **Mechanisms of action**: How the probe interacts with the target + * - **Selectivity information**: Off-target effects and specificity data + * + * Chemical probes are small molecules designed to selectively modulate protein + * function for research purposes. + * + * @example + * ```typescript + * const chemicalProbes = new ChemicalProbesSection(page); + * await chemicalProbes.waitForLoad(); + * + * // Check if probes are available + * const hasProbes = await chemicalProbes.isTableVisible(); + * + * // Search for specific probe + * await chemicalProbes.search("JQ1"); + * + * // Download probe data + * await chemicalProbes.clickDataDownloader(); + * ``` + * + * @category shared + * @remarks Section ID: `chemicalprobes` */ export class ChemicalProbesSection { constructor(private page: Page) {} diff --git a/packages/platform-test/POM/objects/widgets/shared/comparativeGenomicsSection.ts b/packages/platform-test/POM/objects/widgets/shared/comparativeGenomicsSection.ts index cb4f5682c..63e022c7c 100644 --- a/packages/platform-test/POM/objects/widgets/shared/comparativeGenomicsSection.ts +++ b/packages/platform-test/POM/objects/widgets/shared/comparativeGenomicsSection.ts @@ -1,9 +1,37 @@ import type { Locator, Page } from "@playwright/test"; /** - * Interactor for Comparative Genomics section on Target page - * Displays orthologues and paralogues with a chart and table view - * Uses only data-testid selectors for reliable, predictable testing + * Interactor for the Comparative Genomics section on Target pages. + * + * Displays evolutionary conservation data showing homologous genes across species. + * Data is sourced from Ensembl Compara and includes: + * - **Orthologues**: Genes in other species that evolved from a common ancestor + * - **Paralogues**: Genes within the same species arising from gene duplication + * - **Homology types**: One-to-one, one-to-many, many-to-many relationships + * - **Sequence identity**: Percentage similarity between protein sequences + * + * The section supports two visualization modes: + * - **Chart view**: Phylogenetic tree visualization of homologues + * - **Table view**: Detailed tabular data with search and pagination + * + * @example + * ```typescript + * const compGenomics = new ComparativeGenomicsSection(page); + * await compGenomics.waitForLoad(); + * + * // Switch between views + * await compGenomics.switchToChartView(); + * const chartVisible = await compGenomics.isChartVisible(); + * + * await compGenomics.switchToTableView(); + * await compGenomics.search("mouse"); + * + * // Download data + * await compGenomics.clickDataDownloader(); + * ``` + * + * @category shared + * @remarks Section ID: `compgenomics` */ export class ComparativeGenomicsSection { constructor(private page: Page) {} diff --git a/packages/platform-test/POM/objects/widgets/shared/depMapSection.ts b/packages/platform-test/POM/objects/widgets/shared/depMapSection.ts index 9ecef669a..e334c645c 100644 --- a/packages/platform-test/POM/objects/widgets/shared/depMapSection.ts +++ b/packages/platform-test/POM/objects/widgets/shared/depMapSection.ts @@ -1,8 +1,31 @@ import type { Locator, Page } from "@playwright/test"; /** - * Interactor for Cancer DepMap section on Target page - * Displays gene essentiality data from DepMap as a visualization plot + * Interactor for the Cancer DepMap (Dependency Map) section on Target pages. + * + * Displays gene essentiality data from the Broad Institute's DepMap project, + * showing how essential a gene is for cancer cell survival across different + * cell lines and cancer types. The visualization helps identify: + * - **Essential genes**: Required for cell survival (potential drug targets) + * - **Non-essential genes**: Not required for survival + * - **Cancer-specific dependencies**: Genes essential only in certain cancer types + * + * Data is derived from genome-wide CRISPR-Cas9 knockout screens. + * + * @example + * ```typescript + * const depMap = new DepMapSection(page); + * await depMap.waitForLoad(); + * + * // Check if essentiality plot is displayed + * const plotVisible = await depMap.isPlotVisible(); + * + * // Export data + * await depMap.clickExportData(); + * ``` + * + * @category shared + * @remarks Section ID: `depmapessentiality` */ export class DepMapSection { constructor(private page: Page) {} diff --git a/packages/platform-test/POM/objects/widgets/shared/drugWarningsSection.ts b/packages/platform-test/POM/objects/widgets/shared/drugWarningsSection.ts index 067e36ee1..a79b4db44 100644 --- a/packages/platform-test/POM/objects/widgets/shared/drugWarningsSection.ts +++ b/packages/platform-test/POM/objects/widgets/shared/drugWarningsSection.ts @@ -1,7 +1,31 @@ import type { Locator, Page } from "@playwright/test"; /** - * Interactor for Drug Warnings section + * Interactor for the Drug Warnings section on Drug pages. + * + * Displays regulatory safety warnings and alerts associated with a drug, + * including FDA black box warnings, withdrawals, and safety communications. + * Information includes: + * - **Warning type**: Category of the safety alert + * - **Warning description**: Details about the safety concern + * - **Adverse events**: Links to related MedDRA terms + * - **References**: Links to regulatory sources + * + * @example + * ```typescript + * const warnings = new DrugWarningsSection(page); + * await warnings.waitForLoad(); + * + * // Get warning details + * const rowCount = await warnings.getTableRows(); + * const warningType = await warnings.getWarningType(0); + * + * // Search warnings + * await warnings.search("cardiac"); + * ``` + * + * @category shared + * @remarks Section ID: `drugwarnings` */ export class DrugWarningsSection { constructor(private page: Page) {} diff --git a/packages/platform-test/POM/objects/widgets/shared/enhancerToGenePredictionsSection.ts b/packages/platform-test/POM/objects/widgets/shared/enhancerToGenePredictionsSection.ts index cf96239b3..71b0a3722 100644 --- a/packages/platform-test/POM/objects/widgets/shared/enhancerToGenePredictionsSection.ts +++ b/packages/platform-test/POM/objects/widgets/shared/enhancerToGenePredictionsSection.ts @@ -1,7 +1,31 @@ import type { Locator, Page } from "@playwright/test"; /** - * Interactor for Enhancer-to-Gene Predictions section on Variant page + * Interactor for the Enhancer-to-Gene (E2G) Predictions section on Variant pages. + * + * Displays predictions linking non-coding variants in enhancer regions to their + * potential target genes. Uses machine learning models to predict regulatory + * relationships based on: + * - **E2G Score**: Confidence score for the enhancer-gene link + * - **Target gene**: The predicted regulated gene + * - **Tissue/cell type**: Context where the regulation is predicted + * - **Distance**: Genomic distance between variant and gene + * + * Helps interpret the functional impact of non-coding GWAS variants. + * + * @example + * ```typescript + * const e2g = new EnhancerToGenePredictionsSection(page); + * await e2g.waitForLoad(); + * + * // Get predictions + * const rowCount = await e2g.getTableRows(); + * const geneName = await e2g.getTargetGeneName(0); + * const score = await e2g.getE2GScore(0); + * ``` + * + * @category shared + * @remarks Section ID: `enhancer-to-gene-predictions` */ export class EnhancerToGenePredictionsSection { constructor(private page: Page) {} diff --git a/packages/platform-test/POM/objects/widgets/shared/evaSection.ts b/packages/platform-test/POM/objects/widgets/shared/evaSection.ts index 8b3d43795..cd9db34ea 100644 --- a/packages/platform-test/POM/objects/widgets/shared/evaSection.ts +++ b/packages/platform-test/POM/objects/widgets/shared/evaSection.ts @@ -1,7 +1,32 @@ import type { Locator, Page } from "@playwright/test"; /** - * Interactor for EVA/ClinVar section on Variant page + * Interactor for the EVA/ClinVar section on Variant pages. + * + * Displays clinical variant annotations from the European Variation Archive (EVA) + * and NCBI ClinVar database. Information includes: + * - **Clinical significance**: Pathogenic, benign, uncertain significance, etc. + * - **Associated diseases**: Conditions linked to the variant + * - **Review status**: Level of evidence supporting the classification + * - **Submitter information**: Organizations that submitted the annotation + * + * Essential for understanding the clinical relevance of genetic variants. + * + * @example + * ```typescript + * const eva = new EVASection(page); + * await eva.waitForLoad(); + * + * // Get clinical annotations + * const rowCount = await eva.getTableRows(); + * const significance = await eva.getClinicalSignificance(0); + * + * // Navigate to disease + * await eva.clickDiseaseLink(0); + * ``` + * + * @category shared + * @remarks Section ID: `eva` */ export class EVASection { constructor(private page: Page) {} diff --git a/packages/platform-test/POM/objects/widgets/shared/expressionSection.ts b/packages/platform-test/POM/objects/widgets/shared/expressionSection.ts index faf18f404..28a5890c6 100644 --- a/packages/platform-test/POM/objects/widgets/shared/expressionSection.ts +++ b/packages/platform-test/POM/objects/widgets/shared/expressionSection.ts @@ -1,18 +1,40 @@ import type { Locator, Page } from "@playwright/test"; /** - * Interactor for Baseline Expression section on Target page - * Displays RNA and protein expression data with tabs (Summary, GTEx) - * Uses only data-testid selectors for reliable, predictable testing + * Interactor for Baseline Expression section on Target page. + * + * Displays RNA and protein expression data with multiple tabs: + * - **Summary**: Overview of expression across tissues + * - **GTEx**: Detailed GTEx expression data + * + * Uses data-testid selectors for reliable, predictable testing. + * + * @example + * ```typescript + * const expression = new ExpressionSection(page); + * await expression.waitForLoad(); + * await expression.clickGtexTab(); + * const isGtexVisible = await expression.isGtexContentVisible(); + * ``` + * + * @category shared + * @remarks Section ID: `expressions` */ export class ExpressionSection { constructor(private page: Page) {} - // Section container + /** + * Get the main section container element. + * @returns Locator for the section container + */ getSection(): Locator { return this.page.locator("[data-testid='section-expressions']"); } + /** + * Check if the section is currently visible. + * @returns Promise resolving to true if visible, false otherwise + */ async isSectionVisible(): Promise { return await this.getSection() .isVisible() diff --git a/packages/platform-test/POM/objects/widgets/shared/geneOntologySection.ts b/packages/platform-test/POM/objects/widgets/shared/geneOntologySection.ts index 715776656..bfa736f63 100644 --- a/packages/platform-test/POM/objects/widgets/shared/geneOntologySection.ts +++ b/packages/platform-test/POM/objects/widgets/shared/geneOntologySection.ts @@ -1,9 +1,33 @@ import type { Locator, Page } from "@playwright/test"; /** - * Interactor for Gene Ontology section on Target page - * Displays GO annotations organized by aspect (Molecular Function, Biological Process, Cellular Component) - * Uses only data-testid selectors for reliable, predictable testing + * Interactor for the Gene Ontology (GO) section on Target pages. + * + * Displays functional annotations from the Gene Ontology Consortium, organized + * by three aspects: + * - **Molecular Function (MF)**: Activities at the molecular level (e.g., kinase activity) + * - **Biological Process (BP)**: Larger biological programs (e.g., cell division) + * - **Cellular Component (CC)**: Locations within the cell (e.g., nucleus) + * + * Each annotation includes evidence codes and links to supporting publications. + * + * @example + * ```typescript + * const geneOntology = new GeneOntologySection(page); + * await geneOntology.waitForLoad(); + * + * // Search GO terms + * await geneOntology.search("kinase"); + * + * // Access publications + * await geneOntology.clickPublicationsDrawer(); + * + * // Download data + * await geneOntology.clickDataDownloader(); + * ``` + * + * @category shared + * @remarks Section ID: `geneontology` */ export class GeneOntologySection { constructor(private page: Page) {} diff --git a/packages/platform-test/POM/objects/widgets/shared/geneticConstraintSection.ts b/packages/platform-test/POM/objects/widgets/shared/geneticConstraintSection.ts index 86af7cfd3..dd87689d4 100644 --- a/packages/platform-test/POM/objects/widgets/shared/geneticConstraintSection.ts +++ b/packages/platform-test/POM/objects/widgets/shared/geneticConstraintSection.ts @@ -1,8 +1,33 @@ import type { Locator, Page } from "@playwright/test"; /** - * Interactor for Genetic Constraint section on Target page - * Displays constraint metrics from gnomAD + * Interactor for the Genetic Constraint section on Target pages. + * + * Displays evolutionary constraint metrics from gnomAD (Genome Aggregation Database), + * indicating how tolerant a gene is to different types of mutations. Metrics include: + * - **pLI (Loss-of-function intolerance)**: Probability gene is intolerant to LoF variants + * - **LOEUF**: Loss-of-function observed/expected upper bound fraction + * - **Missense constraint**: Z-score for missense variant depletion + * - **Synonymous constraint**: Z-score for synonymous variant depletion + * + * High constraint scores suggest the gene is essential and mutations may be disease-causing. + * + * @example + * ```typescript + * const constraint = new GeneticConstraintSection(page); + * await constraint.waitForLoad(); + * + * // Get constraint metrics + * const rowCount = await constraint.getTableRows(); + * const constraintType = await constraint.getConstraintType(0); + * const score = await constraint.getScore(0); + * + * // Link to gnomAD + * await constraint.clickGnomadLink(); + * ``` + * + * @category shared + * @remarks Section ID: `geneticconstraint` */ export class GeneticConstraintSection { constructor(private page: Page) {} diff --git a/packages/platform-test/POM/objects/widgets/shared/indicationsSection.ts b/packages/platform-test/POM/objects/widgets/shared/indicationsSection.ts index 1fe765009..ce327fb63 100644 --- a/packages/platform-test/POM/objects/widgets/shared/indicationsSection.ts +++ b/packages/platform-test/POM/objects/widgets/shared/indicationsSection.ts @@ -1,7 +1,32 @@ import type { Locator, Page } from "@playwright/test"; /** - * Interactor for Indications section + * Interactor for the Indications section on Drug pages. + * + * Displays therapeutic indications (approved uses) for a drug, sourced from + * ChEMBL and other regulatory databases. Information includes: + * - **Indication name**: Disease or condition the drug treats + * - **Max phase**: Highest clinical trial phase reached + * - **References**: Links to supporting regulatory sources + * + * Shows both approved indications and those in clinical development. + * + * @example + * ```typescript + * const indications = new IndicationsSection(page); + * await indications.waitForLoad(); + * + * // Get indication details + * const rowCount = await indications.getTableRows(); + * const indicationName = await indications.getIndicationName(0); + * const maxPhase = await indications.getMaxPhase(0); + * + * // Navigate to disease page + * await indications.clickIndicationLink(0); + * ``` + * + * @category shared + * @remarks Section ID: `indications` */ export class IndicationsSection { constructor(private page: Page) {} diff --git a/packages/platform-test/POM/objects/widgets/shared/mechanismsOfActionSection.ts b/packages/platform-test/POM/objects/widgets/shared/mechanismsOfActionSection.ts index 9dd7c29f0..099010300 100644 --- a/packages/platform-test/POM/objects/widgets/shared/mechanismsOfActionSection.ts +++ b/packages/platform-test/POM/objects/widgets/shared/mechanismsOfActionSection.ts @@ -1,7 +1,33 @@ import type { Locator, Page } from "@playwright/test"; /** - * Interactor for Mechanisms of Action section + * Interactor for the Mechanisms of Action section on Drug pages. + * + * Displays how a drug exerts its therapeutic effect, including the molecular + * targets and the type of interaction. Data sourced from ChEMBL includes: + * - **Mechanism of action**: Description of drug-target interaction + * - **Target name**: Protein or gene the drug acts upon + * - **Action type**: Inhibitor, agonist, antagonist, etc. + * - **References**: Supporting literature and database links + * + * Essential for understanding drug pharmacology and potential off-target effects. + * + * @example + * ```typescript + * const moa = new MechanismsOfActionSection(page); + * await moa.waitForLoad(); + * + * // Get mechanism details + * const rowCount = await moa.getTableRows(); + * const mechanism = await moa.getMechanismOfAction(0); + * const targetName = await moa.getTargetName(0); + * + * // Navigate to target page + * await moa.clickTargetLink(0, 0); + * ``` + * + * @category shared + * @remarks Section ID: `mechanismsofaction` */ export class MechanismsOfActionSection { constructor(private page: Page) {} diff --git a/packages/platform-test/POM/objects/widgets/shared/molecularInteractionsSection.ts b/packages/platform-test/POM/objects/widgets/shared/molecularInteractionsSection.ts index 6a7848b14..126a5ea9a 100644 --- a/packages/platform-test/POM/objects/widgets/shared/molecularInteractionsSection.ts +++ b/packages/platform-test/POM/objects/widgets/shared/molecularInteractionsSection.ts @@ -1,9 +1,35 @@ import type { Locator, Page } from "@playwright/test"; /** - * Interactor for Molecular Interactions section on Target page - * Displays protein-protein interactions with tabs for different sources (IntAct, Signor, Reactome, String) - * Uses only data-testid selectors for reliable, predictable testing + * Interactor for the Molecular Interactions section on Target pages. + * + * Displays protein-protein interactions (PPIs) from multiple curated databases, + * organized in tabs by data source: + * - **IntAct**: Experimentally validated physical interactions + * - **Signor**: Causal signaling relationships + * - **Reactome**: Pathway-based functional interactions + * - **String**: Predicted and experimental interaction networks + * + * Each interaction includes confidence scores, interaction types, and literature evidence. + * + * @example + * ```typescript + * const interactions = new MolecularInteractionsSection(page); + * await interactions.waitForLoad(); + * + * // Switch between data sources + * await interactions.clickIntactTab(); + * await interactions.clickSignorTab(); + * + * // Search interactions + * await interactions.search("MAPK"); + * + * // Download data + * await interactions.clickDataDownloader(); + * ``` + * + * @category shared + * @remarks Section ID: `interactions` */ export class MolecularInteractionsSection { constructor(private page: Page) {} diff --git a/packages/platform-test/POM/objects/widgets/shared/molecularStructureSection.ts b/packages/platform-test/POM/objects/widgets/shared/molecularStructureSection.ts index f2f309595..cf77d265d 100644 --- a/packages/platform-test/POM/objects/widgets/shared/molecularStructureSection.ts +++ b/packages/platform-test/POM/objects/widgets/shared/molecularStructureSection.ts @@ -1,7 +1,29 @@ import type { Locator, Page } from "@playwright/test"; /** - * Interactor for Molecular Structure section on Variant page + * Interactor for the Molecular Structure section on Variant pages. + * + * Displays 3D protein structure visualization showing the variant's location + * within the protein. Uses AlphaFold predicted structures when experimental + * structures are unavailable. Features include: + * - **3D structure viewer**: Interactive protein structure visualization + * - **Variant mapping**: Highlights the variant position on the structure + * - **Confidence scores**: AlphaFold pLDDT scores for structure reliability + * + * Helps understand the potential structural impact of amino acid changes. + * + * @example + * ```typescript + * const structure = new MolecularStructureSection(page); + * await structure.waitForLoad(); + * + * // Check if structure is available + * const hasStructure = await structure.hasStructureViewer(); + * const viewerVisible = await structure.isAlphaFoldViewerVisible(); + * ``` + * + * @category shared + * @remarks Section ID: `molecular-structure` */ export class MolecularStructureSection { constructor(private page: Page) {} diff --git a/packages/platform-test/POM/objects/widgets/shared/mousePhenotypesSection.ts b/packages/platform-test/POM/objects/widgets/shared/mousePhenotypesSection.ts index 09a3f6d88..92c2cfed1 100644 --- a/packages/platform-test/POM/objects/widgets/shared/mousePhenotypesSection.ts +++ b/packages/platform-test/POM/objects/widgets/shared/mousePhenotypesSection.ts @@ -1,8 +1,33 @@ import type { Locator, Page } from "@playwright/test"; /** - * Interactor for Mouse Phenotypes section on Target page - * Displays phenotypes observed in mouse models with gene knockouts + * Interactor for the Mouse Phenotypes section on Target pages. + * + * Displays phenotypes observed in mouse models with gene knockouts or mutations, + * sourced from the International Mouse Phenotyping Consortium (IMPC) and MGI. + * Information includes: + * - **Phenotype categories**: Grouped by biological system (e.g., cardiovascular, nervous) + * - **Phenotype labels**: Specific observed effects with Mammalian Phenotype Ontology terms + * - **Biological models**: Details about the mouse strains and genetic modifications + * + * Valuable for predicting potential effects of target modulation in humans. + * + * @example + * ```typescript + * const mousePhenotypes = new MousePhenotypesSection(page); + * await mousePhenotypes.waitForLoad(); + * + * // Get phenotype details + * const rowCount = await mousePhenotypes.getTableRows(); + * const category = await mousePhenotypes.getPhenotypeCategory(0); + * const label = await mousePhenotypes.getPhenotypeLabel(0); + * + * // View biological models + * await mousePhenotypes.clickBiologicalModelsDrawer(0); + * ``` + * + * @category shared + * @remarks Section ID: `mousephenotypes` */ export class MousePhenotypesSection { constructor(private page: Page) {} diff --git a/packages/platform-test/POM/objects/widgets/shared/pathwaysSection.ts b/packages/platform-test/POM/objects/widgets/shared/pathwaysSection.ts index 1b4fc84b8..6353b10c0 100644 --- a/packages/platform-test/POM/objects/widgets/shared/pathwaysSection.ts +++ b/packages/platform-test/POM/objects/widgets/shared/pathwaysSection.ts @@ -1,8 +1,32 @@ import type { Locator, Page } from "@playwright/test"; /** - * Interactor for Pathways section on Target page - * Displays Reactome pathway membership + * Interactor for the Pathways section on Target pages. + * + * Displays biological pathway membership from Reactome, showing which cellular + * processes and signaling cascades involve the target gene. Information includes: + * - **Pathway name**: Specific pathway the target participates in + * - **Top-level pathway**: Parent category (e.g., Signal Transduction, Metabolism) + * - **Pathway browser link**: Direct link to interactive Reactome pathway diagram + * + * Helps understand the broader biological context and functions of a target. + * + * @example + * ```typescript + * const pathways = new PathwaysSection(page); + * await pathways.waitForLoad(); + * + * // Get pathway details + * const rowCount = await pathways.getTableRows(); + * const pathwayName = await pathways.getPathwayName(0); + * const topLevel = await pathways.getTopLevelPathway(0); + * + * // Navigate to Reactome + * await pathways.clickPathwayBrowserLink(0); + * ``` + * + * @category shared + * @remarks Section ID: `pathways` */ export class PathwaysSection { constructor(private page: Page) {} diff --git a/packages/platform-test/POM/objects/widgets/shared/pharmacogenomicsSection.ts b/packages/platform-test/POM/objects/widgets/shared/pharmacogenomicsSection.ts index 280922be4..5f4d51c77 100644 --- a/packages/platform-test/POM/objects/widgets/shared/pharmacogenomicsSection.ts +++ b/packages/platform-test/POM/objects/widgets/shared/pharmacogenomicsSection.ts @@ -2,7 +2,33 @@ import type { Locator, Page } from "@playwright/test"; import { fillPolling } from "../../../../utils/fillPolling"; /** - * Interactor for Pharmacogenomics section + * Interactor for the Pharmacogenomics section on Target and Drug pages. + * + * Displays genetic variants that affect drug response, sourced from PharmGKB. + * This information is crucial for personalized medicine and includes: + * - **Genotype/Haplotype**: Specific genetic variant or combination + * - **Drug**: Medication affected by the genetic variant + * - **Phenotype**: Clinical outcome or drug response + * - **Evidence level**: Strength of the pharmacogenomic association + * - **Gene**: The gene containing the variant + * + * @example + * ```typescript + * const pharmaco = new PharmacogenomicsSection(page); + * await pharmaco.waitForLoad(); + * + * // Get pharmacogenomic associations + * const rowCount = await pharmaco.getTableRows(); + * const genotype = await pharmaco.getGenotypeId(0); + * const drugName = await pharmaco.getDrugName(0, 0); + * + * // Navigate to related pages + * await pharmaco.clickDrugLink(0, 0); + * await pharmaco.clickGeneLink(0); + * ``` + * + * @category shared + * @remarks Section ID: `pharmacogenetics` */ export class PharmacogenomicsSection { constructor(private page: Page) {} diff --git a/packages/platform-test/POM/objects/widgets/shared/qtlCredibleSetsSection.ts b/packages/platform-test/POM/objects/widgets/shared/qtlCredibleSetsSection.ts index 917c75a1b..f00f4eb8d 100644 --- a/packages/platform-test/POM/objects/widgets/shared/qtlCredibleSetsSection.ts +++ b/packages/platform-test/POM/objects/widgets/shared/qtlCredibleSetsSection.ts @@ -1,7 +1,33 @@ import type { Locator, Page } from "@playwright/test"; /** - * Interactor for QTL Credible Sets section on Variant page + * Interactor for the QTL Credible Sets section on Variant pages. + * + * Displays molecular QTL (Quantitative Trait Loci) credible sets containing + * the variant, including expression QTLs (eQTLs), splicing QTLs (sQTLs), + * and protein QTLs (pQTLs). Information includes: + * - **Credible set ID**: Links to detailed credible set analysis + * - **Study**: QTL study identifier and type (eQTL, sQTL, pQTL) + * - **Affected gene**: Gene whose expression/splicing is affected + * - **Tissue**: Biological context where the QTL effect is observed + * + * Helps link non-coding variants to their molecular effects on gene regulation. + * + * @example + * ```typescript + * const qtlCredibleSets = new QTLCredibleSetsSection(page); + * await qtlCredibleSets.waitForLoad(); + * + * // Get QTL data + * const rowCount = await qtlCredibleSets.getTableRows(); + * + * // Navigate to related pages + * await qtlCredibleSets.clickCredibleSetLink(0); + * await qtlCredibleSets.clickAffectedGeneLink(0); + * ``` + * + * @category shared + * @remarks Section ID: `qtl-credible-sets` */ export class QTLCredibleSetsSection { constructor(private page: Page) {} diff --git a/packages/platform-test/POM/objects/widgets/shared/safetySection.ts b/packages/platform-test/POM/objects/widgets/shared/safetySection.ts index 8b0531160..d3fc8f6a9 100644 --- a/packages/platform-test/POM/objects/widgets/shared/safetySection.ts +++ b/packages/platform-test/POM/objects/widgets/shared/safetySection.ts @@ -1,9 +1,34 @@ import type { Locator, Page } from "@playwright/test"; /** - * Interactor for Safety section on Target page - * Displays known safety liabilities with effects and biosamples - * Uses only data-testid selectors for reliable, predictable testing + * Interactor for the Safety section on Target pages. + * + * Displays known safety liabilities associated with modulating the target, + * aggregated from multiple sources including literature and toxicology databases. + * Information includes: + * - **Safety event**: Type of adverse effect observed + * - **Biosystems/Organs**: Affected biological systems + * - **Effects**: Specific observed outcomes + * - **Studies**: Links to supporting studies and publications + * + * Critical for target safety assessment in drug discovery. + * + * @example + * ```typescript + * const safety = new SafetySection(page); + * await safety.waitForLoad(); + * + * // Get safety liabilities + * const rowCount = await safety.getTableRowCount(); + * const eventName = await safety.getSafetyEventName(0); + * + * // Access supporting evidence + * await safety.clickBiosystemsDrawer(0); + * await safety.clickPublicationsDrawer(0); + * ``` + * + * @category shared + * @remarks Section ID: `safety` */ export class SafetySection { constructor(private page: Page) {} diff --git a/packages/platform-test/POM/objects/widgets/shared/subcellularLocationSection.ts b/packages/platform-test/POM/objects/widgets/shared/subcellularLocationSection.ts index 3d6733856..751268f5f 100644 --- a/packages/platform-test/POM/objects/widgets/shared/subcellularLocationSection.ts +++ b/packages/platform-test/POM/objects/widgets/shared/subcellularLocationSection.ts @@ -1,9 +1,35 @@ import type { Locator, Page } from "@playwright/test"; /** - * Interactor for Subcellular Location section on Target page - * Displays protein localization data with a visualization - * Uses only data-testid selectors for reliable, predictable testing + * Interactor for the Subcellular Location section on Target pages. + * + * Displays protein localization data from the Human Protein Atlas (HPA) and + * UniProt, showing where in the cell the protein is found. Features include: + * - **Cell diagram**: Visual representation of protein location + * - **Location tabs**: HPA Main, HPA Additional, HPA Extracellular, UniProt + * - **Confidence levels**: Reliability of localization data + * + * Important for understanding protein function and drug accessibility. + * + * @example + * ```typescript + * const location = new SubcellularLocationSection(page); + * await location.waitForLoad(); + * + * // Check visualization + * const diagramVisible = await location.isCellDiagramVisible(); + * + * // Switch between data sources + * await location.clickHpaMainTab(); + * await location.clickUniprotTab(); + * + * // Get location details + * const locationCount = await location.getLocationCount(); + * const locationName = await location.getLocationName(0); + * ``` + * + * @category shared + * @remarks Section ID: `subcellularlocation` */ export class SubcellularLocationSection { constructor(private page: Page) {} diff --git a/packages/platform-test/POM/objects/widgets/shared/tractabilitySection.ts b/packages/platform-test/POM/objects/widgets/shared/tractabilitySection.ts index d015f5da3..392febef6 100644 --- a/packages/platform-test/POM/objects/widgets/shared/tractabilitySection.ts +++ b/packages/platform-test/POM/objects/widgets/shared/tractabilitySection.ts @@ -1,18 +1,42 @@ import type { Locator, Page } from "@playwright/test"; /** - * Interactor for Tractability section on Target page - * Displays tractability assessment across different modalities (Small molecule, Antibody, PROTAC, Other) - * Uses only data-testid selectors for reliable, predictable testing + * Interactor for Tractability section on Target page. + * + * Displays tractability assessment across different therapeutic modalities: + * - **Small molecule (SM)**: Druggability for small molecule interventions + * - **Antibody (AB)**: Accessibility for antibody-based therapies + * - **PROTAC (PR)**: Suitability for proteolysis targeting chimeras + * - **Other (OC)**: Other clinical modalities + * + * Uses data-testid selectors for reliable, predictable testing. + * + * @example + * ```typescript + * const tractability = new TractabilitySection(page); + * await tractability.waitForLoad(); + * const smEnabled = await tractability.getSmallMoleculeEnabledCount(); + * const abEnabled = await tractability.getAntibodyEnabledCount(); + * ``` + * + * @category shared + * @remarks Section ID: `tractability` */ export class TractabilitySection { constructor(private page: Page) {} - // Section container + /** + * Get the main section container element. + * @returns Locator for the section container + */ getSection(): Locator { return this.page.locator("[data-testid='section-tractability']"); } + /** + * Check if the section is currently visible. + * @returns Promise resolving to true if visible, false otherwise + */ async isSectionVisible(): Promise { return await this.getSection() .isVisible() diff --git a/packages/platform-test/POM/objects/widgets/shared/uniprotVariantsSection.ts b/packages/platform-test/POM/objects/widgets/shared/uniprotVariantsSection.ts index 85000f79b..fe88df461 100644 --- a/packages/platform-test/POM/objects/widgets/shared/uniprotVariantsSection.ts +++ b/packages/platform-test/POM/objects/widgets/shared/uniprotVariantsSection.ts @@ -1,7 +1,31 @@ import type { Locator, Page } from "@playwright/test"; /** - * Interactor for UniProt Variants section on Variant page + * Interactor for the UniProt Variants section on Variant pages. + * + * Displays variant annotations from UniProtKB, including natural variants, + * disease-associated mutations, and sequence conflicts. Information includes: + * - **Disease/Phenotype**: Associated conditions from UniProt annotations + * - **Variant description**: Functional impact and literature evidence + * - **Cross-references**: Links to disease databases (OMIM, etc.) + * + * Complements ClinVar data with protein-focused variant annotations. + * + * @example + * ```typescript + * const uniprotVariants = new UniProtVariantsSection(page); + * await uniprotVariants.waitForLoad(); + * + * // Get variant annotations + * const rowCount = await uniprotVariants.getTableRows(); + * + * // Navigate to disease pages + * const diseaseCount = await uniprotVariants.getDiseaseLinksCount(0); + * await uniprotVariants.clickDiseaseLink(0, 0); + * ``` + * + * @category shared + * @remarks Section ID: `uniprot-variants` */ export class UniProtVariantsSection { constructor(private page: Page) {} diff --git a/packages/platform-test/POM/objects/widgets/shared/variantEffectPredictorSection.ts b/packages/platform-test/POM/objects/widgets/shared/variantEffectPredictorSection.ts index 9adb07b11..98e836ecd 100644 --- a/packages/platform-test/POM/objects/widgets/shared/variantEffectPredictorSection.ts +++ b/packages/platform-test/POM/objects/widgets/shared/variantEffectPredictorSection.ts @@ -1,7 +1,35 @@ import type { Locator, Page } from "@playwright/test"; /** - * Interactor for Variant Effect Predictor/Transcript Consequences section + * Interactor for the Variant Effect Predictor (VEP) / Transcript Consequences section. + * + * Displays predicted consequences of a variant on overlapping transcripts, + * computed using Ensembl's Variant Effect Predictor. Information includes: + * - **Gene**: Affected gene with link to target page + * - **Transcript ID**: Specific transcript affected + * - **Predicted consequence**: SO term describing the variant effect + * - **Impact**: Severity classification (HIGH, MODERATE, LOW, MODIFIER) + * - **Amino acid change**: For coding variants, the resulting protein change + * + * Essential for understanding the functional impact of genetic variants. + * + * @example + * ```typescript + * const vep = new VariantEffectPredictorSection(page); + * await vep.waitForLoad(); + * + * // Get transcript consequences + * const rowCount = await vep.getTableRows(); + * const geneName = await vep.getGeneName(0); + * const consequence = await vep.getPredictedConsequence(0); + * const impact = await vep.getImpact(0); + * + * // Navigate to gene + * await vep.clickGeneLink(0); + * ``` + * + * @category shared + * @remarks Section ID: `variant-effect-predictor` */ export class VariantEffectPredictorSection { constructor(private page: Page) {} diff --git a/packages/platform-test/docs/WIDGETS_README.md b/packages/platform-test/docs/WIDGETS_README.md new file mode 100644 index 000000000..8a8cf562f --- /dev/null +++ b/packages/platform-test/docs/WIDGETS_README.md @@ -0,0 +1,94 @@ +# Platform Test Widgets API + +Welcome to the API documentation for the Platform Test widget classes. + +## Overview + +This documentation covers all Page Object Model (POM) widget classes used for end-to-end testing in the Open Targets Platform. These widgets provide a clean API for interacting with various UI sections during Playwright tests. + +The documentation is generated using [TypeDoc](https://typedoc.org/) from JSDoc comments in the TypeScript source files. + +## Widget Categories + +### AOTF (Association On-The-Fly) +Widgets for the associations table and its controls: +- **AotfTable** - Main associations table interactions +- **AotfActions** - Table control panel (filters, export, view modes) + +### CredibleSet +Credible Set page section widgets for variant analysis: +- **Locus2GeneSection** - L2G gene prioritization scores +- **GWASColocSection** - GWAS colocalisation data +- **MolQTLColocSection** - Molecular QTL colocalisation +- **CredibleSetVariantsSection** - Variants in the credible set +- **CredibleSetEnhancerToGenePredictionsSection** - E2G predictions + +### Shared Widgets +Widgets used across multiple entity pages (Target, Disease, Drug, etc.): +- **ExpressionSection** - Baseline expression data +- **TractabilitySection** - Tractability assessment +- **SafetySection** - Safety liabilities +- **BibliographySection** - Literature references +- And many more... + +### Study +Study page section widgets: +- **QTLCredibleSetsSection** - QTL credible sets +- **SharedTraitStudiesSection** - Shared trait studies + +### Variant +Variant page section widgets: +- **VariantEffectSection** - In-silico predictions + +## Usage Example + +```typescript +import { test, expect } from "@playwright/test"; +import { Locus2GeneSection } from "../POM/objects/widgets/CredibleSet/locus2GeneSection"; + +test("L2G section displays data", async ({ page }) => { + await page.goto("/credible-set/GCST005647_12_56486014_C_T"); + + const l2gSection = new Locus2GeneSection(page); + + // Wait for section to load + await l2gSection.waitForLoad(); + + // Verify section is visible + expect(await l2gSection.isSectionVisible()).toBe(true); + + // Check table has data + const rowCount = await l2gSection.getTableRows(); + expect(rowCount).toBeGreaterThan(0); +}); +``` + +## Contributing + +When adding new widget classes: + +1. Add comprehensive JSDoc comments to the class and its methods +2. Follow the existing naming conventions +3. Run `yarn docs:widgets` to regenerate documentation +4. Ensure all public methods have JSDoc descriptions + +## Regenerating Documentation + +```bash +# Generate documentation +yarn docs:widgets + +# Generate and serve documentation locally +yarn docs:widgets:serve +``` + +## JSDoc Tags + +TypeDoc supports standard JSDoc tags. Here are the most useful ones: + +- `@param` - Document function parameters +- `@returns` - Document return values +- `@example` - Provide usage examples +- `@category` - Group classes by category +- `@see` - Reference related items +- `@deprecated` - Mark deprecated methods diff --git a/packages/platform-test/package.json b/packages/platform-test/package.json index b88e199e7..1c588f9db 100644 --- a/packages/platform-test/package.json +++ b/packages/platform-test/package.json @@ -8,15 +8,21 @@ }, "devDependencies": { "@playwright/test": "^1.36.2", + "@types/node": "^25.2.0", "@types/papaparse": "^5.5.2", "dotenv": "^17.2.3", - "start-server-and-test": "^2.0.4" + "start-server-and-test": "^2.0.4", + "ts-node": "^10.9.2", + "typedoc": "^0.28.16", + "typescript": "^5.9.3" }, "scripts": { "dev-test": "playwright test", "dev-test:smoke": "playwright test --grep @smoke", "test:platform:e2e": "start-server-and-test \"(cd ../../apps/platform && yarn run serve)\" http://localhost:4173 \"playwright test\"", - "test:platform:e2e:smoke": "start-server-and-test \"(cd ../../apps/platform && yarn run serve)\" http://localhost:4173 \"playwright test --grep @smoke\"" + "test:platform:e2e:smoke": "start-server-and-test \"(cd ../../apps/platform && yarn run serve)\" http://localhost:4173 \"playwright test --grep @smoke\"", + "docs:widgets": "typedoc", + "docs:widgets:serve": "yarn docs:widgets && npx serve docs/widgets-api" }, "keywords": [], "author": "", diff --git a/packages/platform-test/tsconfig.docs.json b/packages/platform-test/tsconfig.docs.json new file mode 100644 index 000000000..b68ee612e --- /dev/null +++ b/packages/platform-test/tsconfig.docs.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "node", + "lib": ["ES2020", "DOM"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "." + }, + "include": ["POM/objects/widgets/**/*.ts"], + "exclude": ["node_modules", "dist", "docs"] +} diff --git a/packages/platform-test/typedoc.json b/packages/platform-test/typedoc.json new file mode 100644 index 000000000..b1c76454c --- /dev/null +++ b/packages/platform-test/typedoc.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://typedoc.org/schema.json", + "entryPoints": ["POM/objects/widgets"], + "entryPointStrategy": "expand", + "out": "docs/widgets-api", + "name": "Platform Test Widgets API", + "readme": "docs/WIDGETS_README.md", + "includeVersion": true, + "excludePrivate": true, + "excludeProtected": false, + "excludeInternal": true, + "disableSources": false, + "categorizeByGroup": true, + "defaultCategory": "Other", + "categoryOrder": ["AOTF", "CredibleSet", "shared", "Study", "Variant", "*"], + "groupOrder": ["Classes", "Functions", "Variables", "*"], + "navigation": { + "includeCategories": true, + "includeGroups": true + }, + "searchInComments": true, + "searchInDocuments": true, + "plugin": [], + "exclude": ["**/index.ts", "**/*.spec.ts", "**/*.test.ts"], + "tsconfig": "tsconfig.docs.json" +} diff --git a/turbo.json b/turbo.json index d816c54b9..1c4f09167 100644 --- a/turbo.json +++ b/turbo.json @@ -37,6 +37,13 @@ "build-ppp": { "dependsOn": ["^build"], "outputs": ["bundle-platform/**"] + }, + "docs:widgets": { + "outputs": ["docs/widgets-api/**"] + }, + "docs:widgets:serve": { + "cache": false, + "persistent": true } } } diff --git a/yarn.lock b/yarn.lock index 43275785a..20c5a233c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -153,6 +153,13 @@ dependencies: "@babel/types" "^7.28.5" +"@babel/parser@^7.20.15": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.29.0.tgz#669ef345add7d057e92b7ed15f0bac07611831b6" + integrity sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww== + dependencies: + "@babel/types" "^7.29.0" + "@babel/plugin-syntax-jsx@^7.22.5": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz#2f9beb5eff30fa507c5532d107daac7b888fa34c" @@ -216,6 +223,14 @@ "@babel/helper-string-parser" "^7.27.1" "@babel/helper-validator-identifier" "^7.28.5" +"@babel/types@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.29.0.tgz#9f5b1e838c446e72cf3cd4b918152b8c605e37c7" + integrity sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A== + dependencies: + "@babel/helper-string-parser" "^7.27.1" + "@babel/helper-validator-identifier" "^7.28.5" + "@biomejs/biome@2.2.4": version "2.2.4" resolved "https://registry.yarnpkg.com/@biomejs/biome/-/biome-2.2.4.tgz#184e4b83f89bd0d4151682a5aa3840df37748e17" @@ -270,6 +285,13 @@ resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-x64/-/cli-win32-x64-2.2.4.tgz#c8e21413120fe073fa49b78fdd987022941ff66f" integrity sha512-3Y4V4zVRarVh/B/eSHczR4LYoSVyv3Dfuvm3cWs5w/HScccS0+Wt/lHOcDTRYeHjQmMYVC3rIRWqyN2EI52+zg== +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + "@ebi-gene-expression-group/anatomogram@^2.4.0": version "2.4.0" resolved "https://registry.yarnpkg.com/@ebi-gene-expression-group/anatomogram/-/anatomogram-2.4.0.tgz#1bf29826fc5639e5873c7fe1919059dfcec93f46" @@ -651,6 +673,17 @@ dependencies: prop-types "^15.8.1" +"@gerrit0/mini-shiki@^3.17.0": + version "3.22.0" + resolved "https://registry.yarnpkg.com/@gerrit0/mini-shiki/-/mini-shiki-3.22.0.tgz#017179c8ebebd572321e734feb0de143d21c8bfc" + integrity sha512-jMpciqEVUBKE1QwU64S4saNMzpsSza6diNCk4MWAeCxO2+LFi2FIFmL2S0VDLzEJCxuvCbU783xi8Hp/gkM5CQ== + dependencies: + "@shikijs/engine-oniguruma" "^3.22.0" + "@shikijs/langs" "^3.22.0" + "@shikijs/themes" "^3.22.0" + "@shikijs/types" "^3.22.0" + "@shikijs/vscode-textmate" "^10.0.2" + "@graphiql/react@^0.10.0": version "0.10.0" resolved "https://registry.yarnpkg.com/@graphiql/react/-/react-0.10.0.tgz#8d888949dc6c9ddebe0817aeba3e2c164bfbb1bb" @@ -706,17 +739,33 @@ "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.24" -"@jridgewell/resolve-uri@^3.1.0": +"@jridgewell/resolve-uri@^3.0.3", "@jridgewell/resolve-uri@^3.1.0": version "3.1.2" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== -"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0": +"@jridgewell/source-map@^0.3.3": + version "0.3.11" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.11.tgz#b21835cbd36db656b857c2ad02ebd413cc13a9ba" + integrity sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0": version "1.5.5" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== -"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.28": +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25", "@jridgewell/trace-mapping@^0.3.28": version "0.3.31" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0" integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== @@ -724,6 +773,13 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@jsdoc/salty@^0.2.1", "@jsdoc/salty@^0.2.4": + version "0.2.9" + resolved "https://registry.yarnpkg.com/@jsdoc/salty/-/salty-0.2.9.tgz#4d8c147f7ca011532681ce86352a77a0178f1dec" + integrity sha512-yYxMVH7Dqw6nO0d5NIV8OQWnitU8k6vXH8NtgqAfIa/IUqRMxRv/NUJJ08VEKbAakwxlgBl5PJdrU0dMPStsnw== + dependencies: + lodash "^4.17.21" + "@mui/base@5.0.0-beta.40-1": version "5.0.0-beta.40-1" resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-beta.40-1.tgz#6da6229e5e675e811f319149f6e29d7a77522851" @@ -1091,22 +1147,40 @@ resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz#38ae84f4c04226c1d56a3b17296ef1e0460ecdfe" integrity sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ== -"@sideway/address@^4.1.5": - version "4.1.5" - resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.5.tgz#4bc149a0076623ced99ca8208ba780d65a99b9d5" - integrity sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q== +"@shikijs/engine-oniguruma@^3.22.0": + version "3.22.0" + resolved "https://registry.yarnpkg.com/@shikijs/engine-oniguruma/-/engine-oniguruma-3.22.0.tgz#d16b66ed18470bc99f5026ec9f635695a10cb7f5" + integrity sha512-DyXsOG0vGtNtl7ygvabHd7Mt5EY8gCNqR9Y7Lpbbd/PbJvgWrqaKzH1JW6H6qFkuUa8aCxoiYVv8/YfFljiQxA== dependencies: - "@hapi/hoek" "^9.0.0" + "@shikijs/types" "3.22.0" + "@shikijs/vscode-textmate" "^10.0.2" -"@sideway/formula@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f" - integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg== +"@shikijs/langs@^3.22.0": + version "3.22.0" + resolved "https://registry.yarnpkg.com/@shikijs/langs/-/langs-3.22.0.tgz#949338647714b89314efbd333070b0c0263b232a" + integrity sha512-x/42TfhWmp6H00T6uwVrdTJGKgNdFbrEdhaDwSR5fd5zhQ1Q46bHq9EO61SCEWJR0HY7z2HNDMaBZp8JRmKiIA== + dependencies: + "@shikijs/types" "3.22.0" -"@sideway/pinpoint@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df" - integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== +"@shikijs/themes@^3.22.0": + version "3.22.0" + resolved "https://registry.yarnpkg.com/@shikijs/themes/-/themes-3.22.0.tgz#0a316f0b1bda2dea378dd0c9d7e0a703f36af2c3" + integrity sha512-o+tlOKqsr6FE4+mYJG08tfCFDS+3CG20HbldXeVoyP+cYSUxDhrFf3GPjE60U55iOkkjbpY2uC3It/eeja35/g== + dependencies: + "@shikijs/types" "3.22.0" + +"@shikijs/types@3.22.0", "@shikijs/types@^3.22.0": + version "3.22.0" + resolved "https://registry.yarnpkg.com/@shikijs/types/-/types-3.22.0.tgz#43fe92d163742424e794894cb27ce6ce1b4ca8a8" + integrity sha512-491iAekgKDBFE67z70Ok5a8KBMsQ2IJwOWw3us/7ffQkIBCyOQfm/aNwVMBUriP02QshIfgHCBSIYAl3u2eWjg== + dependencies: + "@shikijs/vscode-textmate" "^10.0.2" + "@types/hast" "^3.0.4" + +"@shikijs/vscode-textmate@^10.0.2": + version "10.0.2" + resolved "https://registry.yarnpkg.com/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz#a90ab31d0cc1dfb54c66a69e515bf624fa7b2224" + integrity sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg== "@sideway/address@^4.1.5": version "4.1.5" @@ -1232,6 +1306,26 @@ resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.21.3.tgz#2977727d8fc8dfa079112d9f4d4c019110f1732c" integrity sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg== +"@tsconfig/node10@^1.0.7": + version "1.0.12" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.12.tgz#be57ceac1e4692b41be9de6be8c32a106636dba4" + integrity sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" + integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== + "@types/babel__core@^7.20.5": version "7.20.5" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" @@ -1485,11 +1579,36 @@ resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.16.tgz#8ebe53d69efada7044454e3305c19017d97ced2a" integrity sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg== +"@types/hast@^3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/hast/-/hast-3.0.4.tgz#1d6b39993b82cea6ad783945b0508c25903e15aa" + integrity sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ== + dependencies: + "@types/unist" "*" + "@types/history@^4.7.11": version "4.7.11" resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.11.tgz#56588b17ae8f50c53983a524fc3cc47437969d64" integrity sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA== +"@types/linkify-it@^5": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-5.0.0.tgz#21413001973106cda1c3a9b91eedd4ccd5469d76" + integrity sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q== + +"@types/markdown-it@^14.1.1": + version "14.1.2" + resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-14.1.2.tgz#57f2532a0800067d9b934f3521429a2e8bfb4c61" + integrity sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog== + dependencies: + "@types/linkify-it" "^5" + "@types/mdurl" "^2" + +"@types/mdurl@^2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-2.0.0.tgz#d43878b5b20222682163ae6f897b20447233bdfd" + integrity sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg== + "@types/node@*": version "25.0.10" resolved "https://registry.yarnpkg.com/@types/node/-/node-25.0.10.tgz#4864459c3c9459376b8b75fd051315071c8213e7" @@ -1504,6 +1623,13 @@ dependencies: undici-types "~5.26.4" +"@types/node@^25.2.0": + version "25.2.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-25.2.0.tgz#015b7d228470c1dcbfc17fe9c63039d216b4d782" + integrity sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w== + dependencies: + undici-types "~7.16.0" + "@types/papaparse@^5.5.2": version "5.5.2" resolved "https://registry.yarnpkg.com/@types/papaparse/-/papaparse-5.5.2.tgz#cb450a1cd183deb43728e593eb1ac2da60f4fa4d" @@ -1570,6 +1696,11 @@ "@types/prop-types" "*" csstype "^3.2.2" +"@types/unist@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.3.tgz#acaab0f919ce69cce629c2d4ed2eb4adc1b6c20c" + integrity sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q== + "@types/uuid@^9.0.1": version "9.0.8" resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.8.tgz#7545ba4fc3c003d6c756f651f3bf163d8f0f29ba" @@ -1627,6 +1758,18 @@ dependencies: tslib "^2.3.0" +acorn-walk@^8.1.1: + version "8.3.4" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.4.tgz#794dd169c3977edf4ba4ea47583587c5866236b7" + integrity sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g== + dependencies: + acorn "^8.11.0" + +acorn@^8.11.0, acorn@^8.15.0, acorn@^8.4.1: + version "8.15.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816" + integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== + add-dom-event-listener@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/add-dom-event-listener/-/add-dom-event-listener-1.1.0.tgz#6a92db3a0dd0abc254e095c0f1dc14acbbaae310" @@ -1678,6 +1821,11 @@ apollo-utilities@^1.3.4: ts-invariant "^0.4.0" tslib "^1.10.0" +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + arg@^5.0.2: version "5.0.2" resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c" @@ -1760,7 +1908,7 @@ binary-search-bounds@^2.0.0: resolved "https://registry.yarnpkg.com/binary-search-bounds/-/binary-search-bounds-2.0.5.tgz#125e5bd399882f71e6660d4bf1186384e989fba7" integrity sha512-H0ea4Fd3lS1+sTEB2TgcLoK21lLhwEJzlQv3IN47pJS976Gx4zoWe0ak3q+uYh60ppQxg9F16Ri4tS1sfD4+jA== -bluebird@3.7.2: +bluebird@3.7.2, bluebird@^3.7.2: version "3.7.2" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== @@ -1803,6 +1951,11 @@ btoa@^1.2.1: resolved "https://registry.yarnpkg.com/btoa/-/btoa-1.2.1.tgz#01a9909f8b2c93f6bf680ba26131eb30f7fa3d73" integrity sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g== +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" @@ -1816,6 +1969,14 @@ callsites@^3.0.0: resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== +camel-case@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-4.1.2.tgz#9728072a954f805228225a6deea6b38461e1bd5a" + integrity sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw== + dependencies: + pascal-case "^3.1.2" + tslib "^2.0.3" + camelcase@^6.2.0: version "6.3.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" @@ -1831,6 +1992,13 @@ caniuse-lite@^1.0.30001754: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz#a46ff91449c69522a462996c6aac4ef95d7ccc5e" integrity sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ== +catharsis@^0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/catharsis/-/catharsis-0.9.0.tgz#40382a168be0e6da308c277d3a2b3eb40c7d2121" + integrity sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A== + dependencies: + lodash "^4.17.15" + cfb@~1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/cfb/-/cfb-1.2.2.tgz#94e687628c700e5155436dac05f74e08df23bc44" @@ -1874,6 +2042,25 @@ classnames@^2.2.5, classnames@^2.2.6, classnames@^2.3.2: resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== +clean-css@~5.3.2: + version "5.3.3" + resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.3.tgz#b330653cd3bd6b75009cc25c714cae7b93351ccd" + integrity sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg== + dependencies: + source-map "~0.6.0" + +clean-jsdoc-theme@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/clean-jsdoc-theme/-/clean-jsdoc-theme-4.3.0.tgz#6cd55ff7b25ff6d1719ae0ff9f1cdb5ef07bf640" + integrity sha512-QMrBdZ2KdPt6V2Ytg7dIt0/q32U4COpxvR0UDhPjRRKRL0o0MvRCR5YpY37/4rPF1SI1AYEKAWyof7ndCb/dzA== + dependencies: + "@jsdoc/salty" "^0.2.4" + fs-extra "^10.1.0" + html-minifier-terser "^7.2.0" + klaw-sync "^6.0.0" + lodash "^4.17.21" + showdown "^2.1.0" + cli-cursor@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-4.0.0.tgz#3cecfe3734bf4fe02a8361cbdc0f6fe28c6a57ea" @@ -1986,7 +2173,7 @@ commander@11.0.0: resolved "https://registry.yarnpkg.com/commander/-/commander-11.0.0.tgz#43e19c25dbedc8256203538e8d7e9346877a6f67" integrity sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ== -commander@2: +commander@2, commander@^2.20.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== @@ -1996,6 +2183,16 @@ commander@7: resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== +commander@^10.0.0: + version "10.0.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" + integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== + +commander@^9.0.0: + version "9.5.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-9.5.0.tgz#bc08d1eb5cedf7ccb797a96199d41c7bc3e60d30" + integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ== + component-classes@^1.2.5: version "1.2.6" resolved "https://registry.yarnpkg.com/component-classes/-/component-classes-1.2.6.tgz#c642394c3618a4d8b0b8919efccbbd930e5cd691" @@ -2056,6 +2253,11 @@ crc-32@~1.2.0, crc-32@~1.2.1: resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.2.tgz#3cad35a934b8bf71f25ca524b6da51fb7eace2ff" integrity sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ== +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + cross-fetch@^3.1.4: version "3.2.0" resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.2.0.tgz#34e9192f53bc757d6614304d9e5e6fb4edb782e3" @@ -2662,6 +2864,11 @@ detect-libc@^1.0.3: resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg== +diff@^4.0.1: + version "4.0.4" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.4.tgz#7a6dbfda325f25f07517e9b518f897c08332e07d" + integrity sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ== + dom-align@^1.7.0: version "1.12.4" resolved "https://registry.yarnpkg.com/dom-align/-/dom-align-1.12.4.tgz#3503992eb2a7cfcb2ed3b2a6d21e0b9c00d54511" @@ -2719,6 +2926,14 @@ domutils@^2.0.0: domelementtype "^2.2.0" domhandler "^4.2.0" +dot-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" + integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + dotenv@^17.2.3: version "17.2.3" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-17.2.3.tgz#ad995d6997f639b11065f419a22fabf567cdb9a2" @@ -2868,6 +3083,11 @@ escape-html@^1.0.3: resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== +escape-string-regexp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + escape-string-regexp@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" @@ -3023,7 +3243,7 @@ from@~0: resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe" integrity sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g== -fs-extra@^10.0.0: +fs-extra@^10.0.0, fs-extra@^10.1.0: version "10.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== @@ -3113,7 +3333,7 @@ gopd@^1.2.0: resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== -graceful-fs@^4.1.6, graceful-fs@^4.2.0: +graceful-fs@^4.1.11, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -3229,6 +3449,19 @@ hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react- dependencies: react-is "^16.7.0" +html-minifier-terser@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz#18752e23a2f0ed4b0f550f217bb41693e975b942" + integrity sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA== + dependencies: + camel-case "^4.1.2" + clean-css "~5.3.2" + commander "^10.0.0" + entities "^4.4.0" + param-case "^3.0.4" + relateurl "^0.2.7" + terser "^5.15.1" + htmlparser2@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-4.1.0.tgz#9a4ef161f2e4625ebf7dfbe6c0a2f52d18a59e78" @@ -3474,6 +3707,39 @@ joi@^17.13.3: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== +js2xmlparser@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/js2xmlparser/-/js2xmlparser-4.0.2.tgz#2a1fdf01e90585ef2ae872a01bc169c6a8d5e60a" + integrity sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA== + dependencies: + xmlcreate "^2.0.4" + +jsdoc-ts-utils@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/jsdoc-ts-utils/-/jsdoc-ts-utils-6.0.0.tgz#08633a1fd37cec64c6c6305eb40feebf98d261f8" + integrity sha512-iwYN06ZBtLfdl1KyhnDa+SXLHuQ8vwLTGY/M3cDGrczzPcWadj5XfCPMEUADSBX4ACjsc05IjD5vNePyUJEAIw== + +jsdoc@^4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/jsdoc/-/jsdoc-4.0.5.tgz#fbed70e04a3abcf2143dad6b184947682bbc7315" + integrity sha512-P4C6MWP9yIlMiK8nwoZvxN84vb6MsnXcHuy7XzVOvQoCizWX5JFCBsWIIWKXBltpoRZXddUOVQmCTOZt9yDj9g== + dependencies: + "@babel/parser" "^7.20.15" + "@jsdoc/salty" "^0.2.1" + "@types/markdown-it" "^14.1.1" + bluebird "^3.7.2" + catharsis "^0.9.0" + escape-string-regexp "^2.0.0" + js2xmlparser "^4.0.2" + klaw "^3.0.0" + markdown-it "^14.1.0" + markdown-it-anchor "^8.6.7" + marked "^4.0.10" + mkdirp "^1.0.4" + requizzle "^0.2.3" + strip-json-comments "^3.1.0" + underscore "~1.13.2" + jsesc@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" @@ -3573,6 +3839,20 @@ keycode@^2.2.0: resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.2.1.tgz#09c23b2be0611d26117ea2501c2c391a01f39eff" integrity sha512-Rdgz9Hl9Iv4QKi8b0OlCRQEzp4AgVxyCtz5S/+VIHezDmrDhkp2N2TqBWOLz0/gbeREXOOiI9/4b8BY9uw2vFg== +klaw-sync@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/klaw-sync/-/klaw-sync-6.0.0.tgz#1fd2cfd56ebb6250181114f0a581167099c2b28c" + integrity sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ== + dependencies: + graceful-fs "^4.1.11" + +klaw@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/klaw/-/klaw-3.0.0.tgz#b11bec9cf2492f06756d6e809ab73a2910259146" + integrity sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g== + dependencies: + graceful-fs "^4.1.9" + lazy-ass@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/lazy-ass/-/lazy-ass-1.6.0.tgz#7999655e8646c17f089fdd187d150d3324d54513" @@ -3595,6 +3875,13 @@ linkify-it@^3.0.1: dependencies: uc.micro "^1.0.1" +linkify-it@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-5.0.0.tgz#9ef238bfa6dc70bd8e7f9572b52d369af569b421" + integrity sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ== + dependencies: + uc.micro "^2.0.0" + lint-staged@^14.0.1: version "14.0.1" resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-14.0.1.tgz#57dfa3013a3d60762d9af5d9c83bdb51291a6232" @@ -3661,6 +3948,13 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: dependencies: js-tokens "^3.0.0 || ^4.0.0" +lower-case@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" + integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg== + dependencies: + tslib "^2.0.3" + lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" @@ -3668,11 +3962,26 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" +lunr@^2.3.9: + version "2.3.9" + resolved "https://registry.yarnpkg.com/lunr/-/lunr-2.3.9.tgz#18b123142832337dd6e964df1a5a7707b25d35e1" + integrity sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow== + +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + map-stream@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194" integrity sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g== +markdown-it-anchor@^8.6.7: + version "8.6.7" + resolved "https://registry.yarnpkg.com/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz#ee6926daf3ad1ed5e4e3968b1740eef1c6399634" + integrity sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA== + markdown-it@^12.2.0: version "12.3.2" resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-12.3.2.tgz#bf92ac92283fe983fe4de8ff8abfb5ad72cd0c90" @@ -3684,6 +3993,23 @@ markdown-it@^12.2.0: mdurl "^1.0.1" uc.micro "^1.0.5" +markdown-it@^14.1.0: + version "14.1.0" + resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-14.1.0.tgz#3c3c5992883c633db4714ccb4d7b5935d98b7d45" + integrity sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg== + dependencies: + argparse "^2.0.1" + entities "^4.4.0" + linkify-it "^5.0.0" + mdurl "^2.0.0" + punycode.js "^2.3.1" + uc.micro "^2.1.0" + +marked@^4.0.10: + version "4.3.0" + resolved "https://registry.yarnpkg.com/marked/-/marked-4.3.0.tgz#796362821b019f734054582038b116481b456cf3" + integrity sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A== + math-intrinsics@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" @@ -3694,6 +4020,11 @@ mdurl@^1.0.1: resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" integrity sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g== +mdurl@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-2.0.0.tgz#80676ec0433025dd3e17ee983d0fe8de5a2237e0" + integrity sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w== + memoize-one@^5.0.0: version "5.2.1" resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" @@ -3768,6 +4099,13 @@ minimatch@^5.0.1: dependencies: brace-expansion "^2.0.1" +minimatch@^9.0.5: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + dependencies: + brace-expansion "^2.0.1" + minimist@^1.2.5, minimist@^1.2.6, minimist@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" @@ -3780,6 +4118,11 @@ mkdirp@^0.5.1: dependencies: minimist "^1.2.6" +mkdirp@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" @@ -3802,6 +4145,14 @@ netcdfjs@^3.0.0: dependencies: iobuffer "^5.3.2" +no-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" + integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg== + dependencies: + lower-case "^2.0.2" + tslib "^2.0.3" + node-addon-api@^7.0.0: version "7.1.1" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558" @@ -3919,6 +4270,14 @@ papaparse@^5.5.3: resolved "https://registry.yarnpkg.com/papaparse/-/papaparse-5.5.3.tgz#07f8994dec516c6dab266e952bed68e1de59fa9a" integrity sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A== +param-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5" + integrity sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A== + dependencies: + dot-case "^3.0.4" + tslib "^2.0.3" + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" @@ -3941,6 +4300,14 @@ parse-srcset@^1.0.2: resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1" integrity sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q== +pascal-case@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-3.1.2.tgz#b48e0ef2b98e205e7c1dae747d0b1508237660eb" + integrity sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" @@ -4089,6 +4456,11 @@ ps-tree@1.2.0: dependencies: event-stream "=3.3.4" +punycode.js@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode.js/-/punycode.js-2.3.1.tgz#6b53e56ad75588234e79f4affa90972c7dd8cdb7" + integrity sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA== + quadprog@^1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/quadprog/-/quadprog-1.6.1.tgz#1cd3b13700de9553ef939a6fa73d0d55ddb2f082" @@ -4430,6 +4802,11 @@ rehackt@^0.1.0: resolved "https://registry.yarnpkg.com/rehackt/-/rehackt-0.1.0.tgz#a7c5e289c87345f70da8728a7eb878e5d03c696b" integrity sha512-7kRDOuLHB87D/JESKxQoRwv4DzbIdwkAGQ7p6QKGdVlY1IZheUnVhlk/4UZlNUVxdAXpyxikE3URsG067ybVzw== +relateurl@^0.2.7: + version "0.2.7" + resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" + integrity sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog== + remove-accents@0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/remove-accents/-/remove-accents-0.5.0.tgz#77991f37ba212afba162e375b627631315bed687" @@ -4440,6 +4817,13 @@ require-directory@^2.1.1: resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== +requizzle@^0.2.3: + version "0.2.4" + resolved "https://registry.yarnpkg.com/requizzle/-/requizzle-0.2.4.tgz#319eb658b28c370f0c20f968fa8ceab98c13d27c" + integrity sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw== + dependencies: + lodash "^4.17.21" + resize-observer-polyfill@^1.5.0: version "1.5.1" resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" @@ -4618,6 +5002,13 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== +showdown@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/showdown/-/showdown-2.1.0.tgz#1251f5ed8f773f0c0c7bfc8e6fd23581f9e545c5" + integrity sha512-/6NVYu4U819R2pUIk79n67SYgJHWCce0a5xTP979WbNp0FL9MN1I1QK662IDU1b6JzKTvmhgI7T7JYIxBi3kMQ== + dependencies: + commander "^9.0.0" + signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" @@ -4666,12 +5057,20 @@ source-map-explorer@^2.0.0: resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== +source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + source-map@^0.5.7: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== -source-map@^0.6.1: +source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== @@ -4773,6 +5172,11 @@ strip-final-newline@^3.0.0: resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz#52894c313fbff318835280aed60ff71ebf12b8fd" integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw== +strip-json-comments@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + styled-components@^4.4.1: version "4.4.1" resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-4.4.1.tgz#e0631e889f01db67df4de576fedaca463f05c2f2" @@ -4849,6 +5253,16 @@ temp@^0.9.4: mkdirp "^0.5.1" rimraf "~2.6.2" +terser@^5.15.1: + version "5.46.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.46.0.tgz#1b81e560d584bbdd74a8ede87b4d9477b0ff9695" + integrity sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg== + dependencies: + "@jridgewell/source-map" "^0.3.3" + acorn "^8.15.0" + commander "^2.20.0" + source-map-support "~0.5.20" + through@2, through@~2.3, through@~2.3.1: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" @@ -4905,12 +5319,31 @@ ts-invariant@^0.4.0: dependencies: tslib "^1.9.3" +ts-node@^10.9.2: + version "10.9.2" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" + integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + tslib@^1.10.0, tslib@^1.9.3: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0, tslib@^2.7.0: +tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0, tslib@^2.7.0: version "2.8.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== @@ -5308,6 +5741,17 @@ type-fest@^1.0.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1" integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA== +typedoc@^0.28.16: + version "0.28.16" + resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.28.16.tgz#3901672c48746587fa24390077d07317a1fd180f" + integrity sha512-x4xW77QC3i5DUFMBp0qjukOTnr/sSg+oEs86nB3LjDslvAmwe/PUGDWbe3GrIqt59oTqoXK5GRK9tAa0sYMiog== + dependencies: + "@gerrit0/mini-shiki" "^3.17.0" + lunr "^2.3.9" + markdown-it "^14.1.0" + minimatch "^9.0.5" + yaml "^2.8.1" + typeface-inter@^3.3.0: version "3.18.1" resolved "https://registry.yarnpkg.com/typeface-inter/-/typeface-inter-3.18.1.tgz#24cccdf29923f318589783997be20a662cd3ab9c" @@ -5318,7 +5762,7 @@ typeface-roboto-mono@^1.1.13: resolved "https://registry.yarnpkg.com/typeface-roboto-mono/-/typeface-roboto-mono-1.1.13.tgz#2af8662db8f9119c00efd55d6ed8877d2a69ec94" integrity sha512-pnzDc70b7ywJHin/BUFL7HZX8DyOTBLT2qxlJ92eH1UJOFcENIBXa9IZrxsJX/gEKjbEDKhW5vz/TKRBNk/ufQ== -typescript@^5.0.0, typescript@^5.2.2: +typescript@^5.0.0, typescript@^5.2.2, typescript@^5.9.3: version "5.9.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f" integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== @@ -5333,6 +5777,11 @@ uc.micro@^1.0.1, uc.micro@^1.0.5: resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== +uc.micro@^2.0.0, uc.micro@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-2.1.0.tgz#f8d3f7d0ec4c3dea35a7e3c8efa4cb8b45c9e7ee" + integrity sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A== + uncontrollable@^7.0.2, uncontrollable@^7.1.1: version "7.2.1" resolved "https://registry.yarnpkg.com/uncontrollable/-/uncontrollable-7.2.1.tgz#1fa70ba0c57a14d5f78905d533cf63916dc75738" @@ -5343,6 +5792,11 @@ uncontrollable@^7.0.2, uncontrollable@^7.1.1: invariant "^2.2.4" react-lifecycles-compat "^3.0.4" +underscore@~1.13.2: + version "1.13.7" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.7.tgz#970e33963af9a7dda228f17ebe8399e5fbe63a10" + integrity sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g== + undici-types@~5.26.4: version "5.26.5" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" @@ -5383,6 +5837,11 @@ uuid@^9.0.0, uuid@^9.0.1: resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + vite-plugin-compression@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/vite-plugin-compression/-/vite-plugin-compression-0.5.1.tgz#a75b0d8f48357ebb377b65016da9f20885ef39b6" @@ -5520,6 +5979,11 @@ xlsx@^0.18.5: wmf "~1.0.1" word "~0.3.0" +xmlcreate@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/xmlcreate/-/xmlcreate-2.0.4.tgz#0c5ab0f99cdd02a81065fa9cd8f8ae87624889be" + integrity sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg== + y18n@^5.0.5: version "5.0.8" resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" @@ -5540,6 +6004,11 @@ yaml@^1.10.0: resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== +yaml@^2.8.1: + version "2.8.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.8.2.tgz#5694f25eca0ce9c3e7a9d9e00ce0ddabbd9e35c5" + integrity sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A== + yargs-parser@^20.2.2: version "20.2.9" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" @@ -5576,6 +6045,11 @@ yargs@^17.5.1: y18n "^5.0.5" yargs-parser "^21.1.1" +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + zen-observable-ts@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-1.2.5.tgz#6c6d9ea3d3a842812c6e9519209365a122ba8b58" From cb8fd324f6d1b9e3e4001b17d1bf7b6ffa8c894b Mon Sep 17 00:00:00 2001 From: oluwasusi david damilola Date: Wed, 18 Feb 2026 10:11:36 +0000 Subject: [PATCH 31/34] [Platform-test]: Test Interactors documentation deployment (#896) --- .github/workflows/deploy-docs.yaml | 60 +++++++++++++++++++ package.json | 4 +- packages/platform-test/docs/WIDGETS_README.md | 6 +- packages/platform-test/package.json | 4 +- turbo.json | 4 +- 5 files changed, 69 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/deploy-docs.yaml diff --git a/.github/workflows/deploy-docs.yaml b/.github/workflows/deploy-docs.yaml new file mode 100644 index 000000000..017a5c33b --- /dev/null +++ b/.github/workflows/deploy-docs.yaml @@ -0,0 +1,60 @@ +name: Deploy Docs to GitHub Pages + +on: + push: + branches: ["main"] + paths: + - "packages/platform-test/POM/objects/widgets/**" + - "packages/platform-test/docs/**" + - "packages/platform-test/typedoc.json" + - "packages/platform-test/tsconfig.docs.json" + - ".github/workflows/deploy-docs.yaml" + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build: + name: Build Docs 📚 + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 23 + + - uses: mskelton/setup-yarn@v3 + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Generate docs + run: yarn docs:test_interactors + + - name: Setup Pages + uses: actions/configure-pages@v4 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: packages/platform-test/docs/widgets-api + + deploy: + name: Deploy to GitHub Pages 🚀 + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-22.04 + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/package.json b/package.json index fd1c9428a..3f0e3e975 100644 --- a/package.json +++ b/package.json @@ -32,8 +32,8 @@ "check:fix": "npx biome check --formatter-enabled=true --write .", "prepare": "husky install", "test_workflow": "act -W '.github/workflows/e2e-ci.yml' --action-offline-mode --container-architecture linux/amd64", - "docs:widgets": "turbo run docs:widgets --filter=platform-test", - "docs:widgets:serve": "turbo run docs:widgets:serve --filter=platform-test" + "docs:test_interactors": "turbo run docs:test_interactors --filter=platform-test", + "docs:test_interactors:serve": "turbo run docs:test_interactors:serve --filter=platform-test" }, "devDependencies": { "@biomejs/biome": "2.2.4", diff --git a/packages/platform-test/docs/WIDGETS_README.md b/packages/platform-test/docs/WIDGETS_README.md index 8a8cf562f..9dda6bded 100644 --- a/packages/platform-test/docs/WIDGETS_README.md +++ b/packages/platform-test/docs/WIDGETS_README.md @@ -69,17 +69,17 @@ When adding new widget classes: 1. Add comprehensive JSDoc comments to the class and its methods 2. Follow the existing naming conventions -3. Run `yarn docs:widgets` to regenerate documentation +3. Run `yarn docs:test_interactors` to regenerate documentation 4. Ensure all public methods have JSDoc descriptions ## Regenerating Documentation ```bash # Generate documentation -yarn docs:widgets +yarn docs:test_interactors # Generate and serve documentation locally -yarn docs:widgets:serve +yarn docs:test_interactors:serve ``` ## JSDoc Tags diff --git a/packages/platform-test/package.json b/packages/platform-test/package.json index 1c588f9db..734703612 100644 --- a/packages/platform-test/package.json +++ b/packages/platform-test/package.json @@ -21,8 +21,8 @@ "dev-test:smoke": "playwright test --grep @smoke", "test:platform:e2e": "start-server-and-test \"(cd ../../apps/platform && yarn run serve)\" http://localhost:4173 \"playwright test\"", "test:platform:e2e:smoke": "start-server-and-test \"(cd ../../apps/platform && yarn run serve)\" http://localhost:4173 \"playwright test --grep @smoke\"", - "docs:widgets": "typedoc", - "docs:widgets:serve": "yarn docs:widgets && npx serve docs/widgets-api" + "docs:test_interactors": "typedoc", + "docs:test_interactors:serve": "yarn docs:test_interactors && npx serve docs/widgets-api" }, "keywords": [], "author": "", diff --git a/turbo.json b/turbo.json index 1c4f09167..465e16e64 100644 --- a/turbo.json +++ b/turbo.json @@ -38,10 +38,10 @@ "dependsOn": ["^build"], "outputs": ["bundle-platform/**"] }, - "docs:widgets": { + "docs:test_interactors": { "outputs": ["docs/widgets-api/**"] }, - "docs:widgets:serve": { + "docs:test_interactors:serve": { "cache": false, "persistent": true } From 86607ee4e08db9e680f5b277edd21b0bc2574d80 Mon Sep 17 00:00:00 2001 From: oluwasusi david damilola Date: Wed, 18 Feb 2026 10:24:10 +0000 Subject: [PATCH 32/34] Revert "[Platform-test]: Test Interactors documentation deployment (#896)" This reverts commit cb8fd324f6d1b9e3e4001b17d1bf7b6ffa8c894b. --- .github/workflows/deploy-docs.yaml | 60 ------------------- package.json | 4 +- packages/platform-test/docs/WIDGETS_README.md | 6 +- packages/platform-test/package.json | 4 +- turbo.json | 4 +- 5 files changed, 9 insertions(+), 69 deletions(-) delete mode 100644 .github/workflows/deploy-docs.yaml diff --git a/.github/workflows/deploy-docs.yaml b/.github/workflows/deploy-docs.yaml deleted file mode 100644 index 017a5c33b..000000000 --- a/.github/workflows/deploy-docs.yaml +++ /dev/null @@ -1,60 +0,0 @@ -name: Deploy Docs to GitHub Pages - -on: - push: - branches: ["main"] - paths: - - "packages/platform-test/POM/objects/widgets/**" - - "packages/platform-test/docs/**" - - "packages/platform-test/typedoc.json" - - "packages/platform-test/tsconfig.docs.json" - - ".github/workflows/deploy-docs.yaml" - workflow_dispatch: - -permissions: - contents: read - pages: write - id-token: write - -concurrency: - group: "pages" - cancel-in-progress: false - -jobs: - build: - name: Build Docs 📚 - runs-on: ubuntu-22.04 - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 - with: - node-version: 23 - - - uses: mskelton/setup-yarn@v3 - - - name: Install dependencies - run: yarn install --frozen-lockfile - - - name: Generate docs - run: yarn docs:test_interactors - - - name: Setup Pages - uses: actions/configure-pages@v4 - - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 - with: - path: packages/platform-test/docs/widgets-api - - deploy: - name: Deploy to GitHub Pages 🚀 - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-22.04 - needs: build - steps: - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 diff --git a/package.json b/package.json index 3f0e3e975..fd1c9428a 100644 --- a/package.json +++ b/package.json @@ -32,8 +32,8 @@ "check:fix": "npx biome check --formatter-enabled=true --write .", "prepare": "husky install", "test_workflow": "act -W '.github/workflows/e2e-ci.yml' --action-offline-mode --container-architecture linux/amd64", - "docs:test_interactors": "turbo run docs:test_interactors --filter=platform-test", - "docs:test_interactors:serve": "turbo run docs:test_interactors:serve --filter=platform-test" + "docs:widgets": "turbo run docs:widgets --filter=platform-test", + "docs:widgets:serve": "turbo run docs:widgets:serve --filter=platform-test" }, "devDependencies": { "@biomejs/biome": "2.2.4", diff --git a/packages/platform-test/docs/WIDGETS_README.md b/packages/platform-test/docs/WIDGETS_README.md index 9dda6bded..8a8cf562f 100644 --- a/packages/platform-test/docs/WIDGETS_README.md +++ b/packages/platform-test/docs/WIDGETS_README.md @@ -69,17 +69,17 @@ When adding new widget classes: 1. Add comprehensive JSDoc comments to the class and its methods 2. Follow the existing naming conventions -3. Run `yarn docs:test_interactors` to regenerate documentation +3. Run `yarn docs:widgets` to regenerate documentation 4. Ensure all public methods have JSDoc descriptions ## Regenerating Documentation ```bash # Generate documentation -yarn docs:test_interactors +yarn docs:widgets # Generate and serve documentation locally -yarn docs:test_interactors:serve +yarn docs:widgets:serve ``` ## JSDoc Tags diff --git a/packages/platform-test/package.json b/packages/platform-test/package.json index 734703612..1c588f9db 100644 --- a/packages/platform-test/package.json +++ b/packages/platform-test/package.json @@ -21,8 +21,8 @@ "dev-test:smoke": "playwright test --grep @smoke", "test:platform:e2e": "start-server-and-test \"(cd ../../apps/platform && yarn run serve)\" http://localhost:4173 \"playwright test\"", "test:platform:e2e:smoke": "start-server-and-test \"(cd ../../apps/platform && yarn run serve)\" http://localhost:4173 \"playwright test --grep @smoke\"", - "docs:test_interactors": "typedoc", - "docs:test_interactors:serve": "yarn docs:test_interactors && npx serve docs/widgets-api" + "docs:widgets": "typedoc", + "docs:widgets:serve": "yarn docs:widgets && npx serve docs/widgets-api" }, "keywords": [], "author": "", diff --git a/turbo.json b/turbo.json index 465e16e64..1c4f09167 100644 --- a/turbo.json +++ b/turbo.json @@ -38,10 +38,10 @@ "dependsOn": ["^build"], "outputs": ["bundle-platform/**"] }, - "docs:test_interactors": { + "docs:widgets": { "outputs": ["docs/widgets-api/**"] }, - "docs:test_interactors:serve": { + "docs:widgets:serve": { "cache": false, "persistent": true } From c8f914e060040abafa69897175b07b4aa8bebc75 Mon Sep 17 00:00:00 2001 From: oluwasusi david damilola Date: Thu, 5 Mar 2026 11:38:05 +0000 Subject: [PATCH 33/34] [Platform-Test]: Harden csv column parsing against column name change (#900) --- package.json | 4 +- packages/platform-test/docs/WIDGETS_README.md | 6 +-- packages/platform-test/package.json | 4 +- .../playwright-report/index.html | 2 +- packages/platform-test/types/index.ts | 46 +++++++++-------- .../platform-test/utils/csvRowToTestConfig.ts | 49 ++++++++++--------- .../utils/fetchConfigFromSheet.ts | 21 +++++--- turbo.json | 4 +- 8 files changed, 77 insertions(+), 59 deletions(-) diff --git a/package.json b/package.json index fd1c9428a..3f0e3e975 100644 --- a/package.json +++ b/package.json @@ -32,8 +32,8 @@ "check:fix": "npx biome check --formatter-enabled=true --write .", "prepare": "husky install", "test_workflow": "act -W '.github/workflows/e2e-ci.yml' --action-offline-mode --container-architecture linux/amd64", - "docs:widgets": "turbo run docs:widgets --filter=platform-test", - "docs:widgets:serve": "turbo run docs:widgets:serve --filter=platform-test" + "docs:test_interactors": "turbo run docs:test_interactors --filter=platform-test", + "docs:test_interactors:serve": "turbo run docs:test_interactors:serve --filter=platform-test" }, "devDependencies": { "@biomejs/biome": "2.2.4", diff --git a/packages/platform-test/docs/WIDGETS_README.md b/packages/platform-test/docs/WIDGETS_README.md index 8a8cf562f..9dda6bded 100644 --- a/packages/platform-test/docs/WIDGETS_README.md +++ b/packages/platform-test/docs/WIDGETS_README.md @@ -69,17 +69,17 @@ When adding new widget classes: 1. Add comprehensive JSDoc comments to the class and its methods 2. Follow the existing naming conventions -3. Run `yarn docs:widgets` to regenerate documentation +3. Run `yarn docs:test_interactors` to regenerate documentation 4. Ensure all public methods have JSDoc descriptions ## Regenerating Documentation ```bash # Generate documentation -yarn docs:widgets +yarn docs:test_interactors # Generate and serve documentation locally -yarn docs:widgets:serve +yarn docs:test_interactors:serve ``` ## JSDoc Tags diff --git a/packages/platform-test/package.json b/packages/platform-test/package.json index 1c588f9db..734703612 100644 --- a/packages/platform-test/package.json +++ b/packages/platform-test/package.json @@ -21,8 +21,8 @@ "dev-test:smoke": "playwright test --grep @smoke", "test:platform:e2e": "start-server-and-test \"(cd ../../apps/platform && yarn run serve)\" http://localhost:4173 \"playwright test\"", "test:platform:e2e:smoke": "start-server-and-test \"(cd ../../apps/platform && yarn run serve)\" http://localhost:4173 \"playwright test --grep @smoke\"", - "docs:widgets": "typedoc", - "docs:widgets:serve": "yarn docs:widgets && npx serve docs/widgets-api" + "docs:test_interactors": "typedoc", + "docs:test_interactors:serve": "yarn docs:test_interactors && npx serve docs/widgets-api" }, "keywords": [], "author": "", diff --git a/packages/platform-test/playwright-report/index.html b/packages/platform-test/playwright-report/index.html index dcfcdb11a..1819b1b07 100644 --- a/packages/platform-test/playwright-report/index.html +++ b/packages/platform-test/playwright-report/index.html @@ -73,4 +73,4 @@
    - + \ No newline at end of file diff --git a/packages/platform-test/types/index.ts b/packages/platform-test/types/index.ts index 6f155692b..7142cf84e 100644 --- a/packages/platform-test/types/index.ts +++ b/packages/platform-test/types/index.ts @@ -51,25 +51,31 @@ export interface TestConfig { withQTLColoc?: string; }; } /** - * CSV row structure from Google Sheet + * CSV row as array of strings (using indices instead of header names for resilience) */ +export type CSVRow = string[]; -export interface CSVRow { - "Testing Scenario": string; - drug_page_primary: string; - variant_primary: string; - variant_with_pharmacogenetics: string; - variant_with_qtl: string; - target_primary: string; - target_incomplete: string; - target_aotf_diseases: string; - disease_primary: string; - disease_name: string; - disease_alternatives: string; - disease_aotf_genes: string; - study_gwas: string; - study_qtl: string; - credible_set: string; - credible_set_GWAS_coloc: string; - credible_set_QTL_coloc: string; -} +/** + * Column indices for the Google Sheet CSV + * Using indices instead of header names makes parsing resilient to column name changes + * Update these indices if column order changes in the sheet + */ +export const CSV_COLUMNS = { + TESTING_SCENARIO: 0, + DRUG_PAGE_PRIMARY: 1, + VARIANT_PRIMARY: 2, + VARIANT_WITH_PHARMACOGENETICS: 3, + VARIANT_WITH_QTL: 4, + TARGET_PRIMARY: 5, + TARGET_INCOMPLETE: 6, + TARGET_AOTF_DISEASES: 7, + DISEASE_PRIMARY: 8, + DISEASE_NAME: 10, + DISEASE_ALTERNATIVES: 11, + DISEASE_AOTF_GENES: 9, + STUDY_GWAS: 12, + STUDY_QTL: 13, + CREDIBLE_SET: 14, + CREDIBLE_SET_GWAS_COLOC: 15, + CREDIBLE_SET_QTL_COLOC: 16, +} as const; diff --git a/packages/platform-test/utils/csvRowToTestConfig.ts b/packages/platform-test/utils/csvRowToTestConfig.ts index cd9216c49..2cc390938 100644 --- a/packages/platform-test/utils/csvRowToTestConfig.ts +++ b/packages/platform-test/utils/csvRowToTestConfig.ts @@ -1,53 +1,56 @@ import type { CSVRow, TestConfig } from "../types"; +import { CSV_COLUMNS } from "../types"; import { parseCsvStringToArray } from "../utils/parseCsvStringToArray"; /** - * Convert a CSV row to TestConfig structure + * Convert a CSV row (array of strings) to TestConfig structure + * Uses column indices for resilience against header name changes */ - export function csvRowToTestConfig(row: CSVRow): TestConfig { + const col = CSV_COLUMNS; + return { drug: { - primary: row.drug_page_primary, + primary: row[col.DRUG_PAGE_PRIMARY] || "", alternatives: { - withWarnings: row.drug_page_primary, - withAdverseEvents: row.drug_page_primary, + withWarnings: row[col.DRUG_PAGE_PRIMARY] || "", + withAdverseEvents: row[col.DRUG_PAGE_PRIMARY] || "", }, }, variant: { - primary: row.variant_primary, - withMolecularStructure: row.variant_primary, - withPharmacogenomics: row.variant_with_pharmacogenetics, - withQTL: row.variant_with_qtl || undefined, - withEVA: row.variant_primary || undefined, + primary: row[col.VARIANT_PRIMARY] || "", + withMolecularStructure: row[col.VARIANT_PRIMARY] || "", + withPharmacogenomics: row[col.VARIANT_WITH_PHARMACOGENETICS] || "", + withQTL: row[col.VARIANT_WITH_QTL] || undefined, + withEVA: row[col.VARIANT_PRIMARY] || undefined, }, target: { - primary: row.target_primary || undefined, - alternatives: parseCsvStringToArray(row.target_incomplete), - aotfDiseases: parseCsvStringToArray(row.target_aotf_diseases), + primary: row[col.TARGET_PRIMARY] || undefined, + alternatives: parseCsvStringToArray(row[col.TARGET_INCOMPLETE] || ""), + aotfDiseases: parseCsvStringToArray(row[col.TARGET_AOTF_DISEASES] || ""), }, disease: { - primary: row.disease_primary, - name: row.disease_name || undefined, - alternatives: parseCsvStringToArray(row.disease_alternatives), - aotfGenes: parseCsvStringToArray(row.disease_aotf_genes), + primary: row[col.DISEASE_PRIMARY] || "", + name: row[col.DISEASE_NAME] || undefined, + alternatives: parseCsvStringToArray(row[col.DISEASE_ALTERNATIVES] || ""), + aotfGenes: parseCsvStringToArray(row[col.DISEASE_AOTF_GENES] || ""), }, study: { gwas: { - primary: row.study_gwas, + primary: row[col.STUDY_GWAS] || "", alternatives: [], }, - qtl: row.study_qtl + qtl: row[col.STUDY_QTL] ? { - primary: row.study_qtl, + primary: row[col.STUDY_QTL], alternatives: [], } : undefined, }, credibleSet: { - primary: row.credible_set || undefined, - withGWASColoc: row.credible_set_GWAS_coloc || undefined, - withQTLColoc: row.credible_set_QTL_coloc || undefined, + primary: row[col.CREDIBLE_SET] || undefined, + withGWASColoc: row[col.CREDIBLE_SET_GWAS_COLOC] || undefined, + withQTLColoc: row[col.CREDIBLE_SET_QTL_COLOC] || undefined, }, }; } diff --git a/packages/platform-test/utils/fetchConfigFromSheet.ts b/packages/platform-test/utils/fetchConfigFromSheet.ts index 14ba7b43f..5a0a707af 100644 --- a/packages/platform-test/utils/fetchConfigFromSheet.ts +++ b/packages/platform-test/utils/fetchConfigFromSheet.ts @@ -1,9 +1,11 @@ import Papa from "papaparse"; import type { CSVRow, TestConfig } from "../types"; +import { CSV_COLUMNS } from "../types"; import { csvRowToTestConfig } from "./csvRowToTestConfig"; /** * Fetch test configuration from Google Sheet CSV URL + * Uses column indices instead of header names for resilience against column name changes */ export async function fetchConfigFromSheet( url: string, @@ -24,23 +26,30 @@ export async function fetchConfigFromSheet( } const csvText = await response.text(); + // Parse without headers - we'll use indices instead for resilience const parseResult = Papa.parse(csvText, { - header: true, + header: false, skipEmptyLines: true, - transformHeader: (header) => header.trim(), }); if (parseResult.errors.length > 0) { console.error("CSV parsing errors:", parseResult.errors); } - const rows = parseResult.data; + // Skip the header row (index 0) and get data rows + const rows = parseResult.data.slice(1); - // Find the row matching the scenario name - const matchingRow = rows.find((row) => row["Testing Scenario"] === scenarioName); + // Find the row matching the scenario name using the TESTING_SCENARIO column index + const matchingRow = rows.find( + (row) => row[CSV_COLUMNS.TESTING_SCENARIO]?.trim() === scenarioName + ); if (!matchingRow) { + const availableScenarios = rows + .map((r) => r[CSV_COLUMNS.TESTING_SCENARIO]) + .filter(Boolean) + .join(", "); console.error( - `Scenario "${scenarioName}" not found in CSV. Available scenarios: ${rows.map((r) => r["Testing Scenario"]).join(", ")}` + `Scenario "${scenarioName}" not found in CSV. Available scenarios: ${availableScenarios}` ); return null; } diff --git a/turbo.json b/turbo.json index 1c4f09167..465e16e64 100644 --- a/turbo.json +++ b/turbo.json @@ -38,10 +38,10 @@ "dependsOn": ["^build"], "outputs": ["bundle-platform/**"] }, - "docs:widgets": { + "docs:test_interactors": { "outputs": ["docs/widgets-api/**"] }, - "docs:widgets:serve": { + "docs:test_interactors:serve": { "cache": false, "persistent": true } From 124318cf2714d4c5456ace1fc6acba56ca0381e1 Mon Sep 17 00:00:00 2001 From: oluwasusi david damilola Date: Thu, 5 Mar 2026 12:21:08 +0000 Subject: [PATCH 34/34] Delete packages/platform-test/playwright-report/index.html --- .../playwright-report/index.html | 76 ------------------- 1 file changed, 76 deletions(-) delete mode 100644 packages/platform-test/playwright-report/index.html diff --git a/packages/platform-test/playwright-report/index.html b/packages/platform-test/playwright-report/index.html deleted file mode 100644 index 1819b1b07..000000000 --- a/packages/platform-test/playwright-report/index.html +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - - - - Playwright Test Report - - - - -
    - - - \ No newline at end of file