diff --git a/.github/actions/build-backend/action.yml b/.github/actions/build-backend/action.yml index c453c028cb1..aa3e5d95152 100644 --- a/.github/actions/build-backend/action.yml +++ b/.github/actions/build-backend/action.yml @@ -39,7 +39,7 @@ runs: distribution: "temurin" cache: "gradle" - - uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582 + - uses: gradle/actions/setup-gradle@16bf8bc8fe830fa669c3c9f914d3eb147c629707 - name: Lint if: inputs.run-integration-tests == 'true' diff --git a/.github/actions/build-submissions/action.yml b/.github/actions/build-submissions/action.yml new file mode 100644 index 00000000000..630c1e46f85 --- /dev/null +++ b/.github/actions/build-submissions/action.yml @@ -0,0 +1,70 @@ +# action.yml +name: "Build Submissions" +description: "Build submissions microservice" +inputs: + version: + description: "Version tag" + required: true + upload-build: + default: true + run-integration-tests: + default: false + run-qc: + default: false + github-token: + default: false + sp-creds: + description: "Azure Service Principal creds" + +runs: + using: "composite" + steps: + # These are for CI and not credentials of any system + - name: Set Environment Variables + working-directory: prime-router + shell: bash + run: | + echo >> $GITHUB_ENV DB_USER='prime' + echo >> $GITHUB_ENV DB_PASSWORD='changeIT!' + + - name: Remove unnecessary software + shell: bash + run: | + sudo rm -rf /usr/local/lib/android + + - name: Set up JDK 17 + uses: actions/setup-java@6a0805fcefea3d4657a47ac4c165951e33482018 + with: + java-version: "17" + distribution: "temurin" + cache: "gradle" + + - uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582 + + - name: Lint + if: inputs.run-integration-tests == 'true' + run: ./gradlew :submissions:ktlintCheck + shell: bash + + - name: Spin up build containers + working-directory: prime-router + shell: bash + run: docker compose -f docker-compose.postgres.yml up -d + + - name: Build Submissions Package + uses: ./.github/actions/retry + with: + timeout_minutes: 10 + max_attempts: 2 + retry_wait_seconds: 30 + command: | + ./gradlew :submissions:build -x test + shell: bash + + - name: Cleanup Gradle Cache + if: inputs.run-integration-tests == 'true' + working-directory: prime-router + run: | + rm -f .gradle/caches/modules-2/modules-2.lock + rm -f .gradle/caches/modules-2/gc.properties + shell: bash diff --git a/.github/actions/sonarcloud/action.yml b/.github/actions/sonarcloud/action.yml index b88aa91b77a..8f1791a3c4c 100644 --- a/.github/actions/sonarcloud/action.yml +++ b/.github/actions/sonarcloud/action.yml @@ -20,13 +20,13 @@ runs: SONAR_TOKEN: ${{ inputs.sonar-token }} with: args: > - -Dsonar.coverage.exclusions=prime-router/src/test/**,prime-router/src/testIntegration/**,prime-router/src/main/kotlin/cli/tests/**,frontend-react/**/__mocks__/**,frontend-react/**/mocks/**,frontend-react/**/*.test.* - -Dsonar.cpd.exclusions=frontend-react/**/*.test.*,prime-router/src/test/**,prime-router/src/testIntegration/**,prime-router/src/main/kotlin/cli/tests/** - -Dsonar.sources=frontend-react/src,prime-router/src + -Dsonar.coverage.exclusions=prime-router/src/test/**,prime-router/src/testIntegration/**,prime-router/src/main/kotlin/cli/tests/**,frontend-react/**/__mocks__/**,frontend-react/**/mocks/**,frontend-react/**/*.test.*,submissions/src/test/** + -Dsonar.cpd.exclusions=frontend-react/**/*.test.*,prime-router/src/test/**,prime-router/src/testIntegration/**,prime-router/src/main/kotlin/cli/tests/**,submissions/src/test/** + -Dsonar.sources=frontend-react/src,prime-router/src,submissions/src,shared/src -Dsonar.projectKey=CDCgov_prime-data-hub -Dsonar.organization=cdcgov - -Dsonar.java.binaries=prime-router/build/classes/java/main,prime-router/build/classes/kotlin/main - -Dsonar.java.libraries=prime-router/build/libs/*.jar,prime-router/build/**/*.jar + -Dsonar.java.binaries=prime-router/build/classes/java/main,prime-router/build/classes/kotlin/main,submissions/build/classes/kotlin/main,shared/build/classes/kotlin/main + -Dsonar.java.libraries=prime-router/build/libs/*.jar,prime-router/build/**/*.jar,submissions/build/**/*.jar,shared/build/**/*.jar -Dsonar.coverage.jacoco.xmlReportPaths=prime-router/build/reports/jacoco/test/jacocoTestReport.xml -Dsonar.javascript.lcov.reportPaths=frontend-react/coverage/lcov.info diff --git a/.github/workflows/snyk.yml b/.github/workflows/snyk.yml index 5f0551d1034..555553c1bcd 100644 --- a/.github/workflows/snyk.yml +++ b/.github/workflows/snyk.yml @@ -40,7 +40,7 @@ jobs: java-version: "17" distribution: "temurin" cache: "gradle" - - uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582 + - uses: gradle/actions/setup-gradle@16bf8bc8fe830fa669c3c9f914d3eb147c629707 - name: Snyk Monitor working-directory: ${{ matrix.folder }} run: snyk monitor --org=prime-reportstream diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index eb7ab3820af..9a16052d958 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -70,7 +70,7 @@ jobs: - name: Gradle setup if: steps.changed-files-yaml.outputs.backend_any_changed == 'true' || steps.branch-name.outputs.is_default == 'true' - uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582 + uses: gradle/actions/setup-gradle@16bf8bc8fe830fa669c3c9f914d3eb147c629707 - name: Spin up build containers if: steps.changed-files-yaml.outputs.backend_any_changed == 'true' || steps.branch-name.outputs.is_default == 'true' @@ -93,6 +93,10 @@ jobs: command: ./gradlew -Dorg.gradle.jvmargs="-Xmx6g" :prime-router:package -x fatjar shell: bash + - name: Build Submissions Package + if: steps.changed-files-yaml.outputs.backend_any_changed == 'true' || steps.branch-name.outputs.is_default == 'true' + uses: ./.github/actions/build-submissions + - name: Perform Java CodeQL Analysis if: steps.changed-files-yaml.outputs.backend_any_changed == 'true' || steps.branch-name.outputs.is_default == 'true' uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/validate_terraform.yml b/.github/workflows/validate_terraform.yml index 46b5748de6e..14bdbf2dd74 100644 --- a/.github/workflows/validate_terraform.yml +++ b/.github/workflows/validate_terraform.yml @@ -48,7 +48,7 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 - name: Run Checkov action - uses: bridgecrewio/checkov-action@b57df8031953b36872c225e6627691100b03bcde + uses: bridgecrewio/checkov-action@1b813e8f72afe2b6263a6ea10c873707e21ebe44 with: directory: operations/app/terraform skip_check: CKV_AZURE_139,CKV_AZURE_137,CKV_AZURE_103,CKV_AZURE_104,CKV_AZURE_102,CKV_AZURE_130,CKV_AZURE_121,CKV_AZURE_67,CKV_AZURE_56,CKV_AZURE_17,CKV_AZURE_63,CKV_AZURE_18,CKV_AZURE_88,CKV_AZURE_65,CKV_AZURE_13,CKV_AZURE_66,CKV_AZURE_33,CKV_AZURE_35,CKV_AZURE_36,CKV_AZURE_98,CKV2_AZURE_1,CKV2_AZURE_15,CKV2_AZURE_21,CKV_AZURE_213,CKV_AZURE_59,CKV2_AZURE_33,CKV2_AZURE_32,CKV2_AZURE_28,CKV_AZURE_206,CKV_AZURE_42,CKV_AZURE_110,CKV_AZURE_109,CKV_AZURE_166,CKV2_AZURE_38,CKV2_AZURE_40,CKV2_AZURE_41,CKV_AZURE_235 diff --git a/frontend-react/.eslintrc.cjs b/frontend-react/.eslintrc.cjs index 4a4debd4a41..ab82b5316c1 100644 --- a/frontend-react/.eslintrc.cjs +++ b/frontend-react/.eslintrc.cjs @@ -82,6 +82,7 @@ module.exports = { // TODO: investigate these for reconsideration or per-module ignoring "playwright/no-conditional-in-test": ["off"], "playwright/no-force-option": ["off"], + "playwright/expect-expect": ["off"], }, }, ], diff --git a/frontend-react/e2e/helpers/internal-links.ts b/frontend-react/e2e/helpers/internal-links.ts index f3dc9e4af23..3ad68bb073a 100644 --- a/frontend-react/e2e/helpers/internal-links.ts +++ b/frontend-react/e2e/helpers/internal-links.ts @@ -1,17 +1,58 @@ import { Page } from "@playwright/test"; -export const ELC = - "https://www.cdc.gov/epidemiology-laboratory-capacity/php/about/"; +export const ELC = "https://www.cdc.gov/epidemiology-laboratory-capacity/php/about/"; -export async function clickOnInternalLink( - locator: string, - dataTestId: string, - linkName: string, - page: Page, -) { - await page - .locator(locator) - .getByTestId(dataTestId) - .getByRole("link", { name: linkName }) - .click(); +export async function clickOnInternalLink(locator: string, dataTestId: string, linkName: string, page: Page) { + await page.locator(locator).getByTestId(dataTestId).getByRole("link", { name: linkName }).click(); } + +export interface SideNavItem { + name: string; + path: string; +} + +export const aboutSideNav = [ + { + name: "About", + path: "/about", + }, + { + name: "Our network", + path: "/about/our-network", + }, + { + name: "Product roadmap", + path: "/about/roadmap", + }, + { + name: "News", + path: "/about/news", + }, + { + name: "Case studies", + path: "/about/case-studies", + }, + { + name: "Security", + path: "/about/security", + }, + { + name: "Release notes", + path: "/about/release-notes", + }, +]; + +export const gettingStartedSideNav = [ + { + name: "Getting started", + path: "/getting-started", + }, + { + name: "Sending data", + path: "/getting-started/sending-data", + }, + { + name: "Receiving data", + path: "/getting-started/receiving-data", + }, +]; diff --git a/frontend-react/e2e/helpers/utils.ts b/frontend-react/e2e/helpers/utils.ts index 25ab43778d4..00324bebd68 100644 --- a/frontend-react/e2e/helpers/utils.ts +++ b/frontend-react/e2e/helpers/utils.ts @@ -85,8 +85,8 @@ export async function tableColumnDateTimeInRange( columnNumber: number, fromDate: string, toDate: string, - startTime: string, - endTime: string, + startTime?: string, + endTime?: string, ) { let datesInRange = true; const rowCount = await tableRows(page).count(); @@ -106,7 +106,7 @@ export async function tableColumnDateTimeInRange( return datesInRange; } -export function fromDateWithTime(date: string, time: string) { +export function fromDateWithTime(date: string, time?: string) { const fromDateTime = new Date(date); if (time) { @@ -123,7 +123,7 @@ export function fromDateWithTime(date: string, time: string) { return fromDateTime; } -export function toDateWithTime(date: string, time: string) { +export function toDateWithTime(date: string, time?: string) { const toDateTime = new Date(date); if (time) { diff --git a/frontend-react/e2e/pages/BasePage.ts b/frontend-react/e2e/pages/BasePage.ts index 69a5441c8e6..ea831731a04 100644 --- a/frontend-react/e2e/pages/BasePage.ts +++ b/frontend-react/e2e/pages/BasePage.ts @@ -1,6 +1,7 @@ +import { SideNavItem } from "../helpers/internal-links"; import { selectTestOrg } from "../helpers/utils"; import appInsightsConfig from "../mocks/appInsightsConfig.json" assert { type: "json" }; -import { Locator, Page, Request, Response, Route, TestArgs } from "../test"; +import { expect, Locator, Page, Request, Response, Route, TestArgs } from "../test"; export type RouteHandlers = Record[1]>; export type MockRouteCache = Record; @@ -12,25 +13,12 @@ export interface BasePageProps { heading?: Locator; } -export type RouteFulfillOptions = Exclude< - Parameters[0], - undefined -> & { isMock?: boolean }; -export type RouteFulfillOptionsFn = ( - request: Request, -) => Promise | RouteFulfillOptions; +export type RouteFulfillOptions = Exclude[0], undefined> & { isMock?: boolean }; +export type RouteFulfillOptionsFn = (request: Request) => Promise | RouteFulfillOptions; export type RouteHandlerFn = (route: Route, request: Request) => Promise; -export type RouteHandlerFulfillOptions = - | RouteFulfillOptions - | RouteFulfillOptionsFn; -export type RouteHandlerFulfillEntry = [ - url: string, - fulfillOptions: RouteHandlerFulfillOptions, -]; -export type ResponseHandlerEntry = [ - url: string, - handler: (response: Response) => Promise | void, -]; +export type RouteHandlerFulfillOptions = RouteFulfillOptions | RouteFulfillOptionsFn; +export type RouteHandlerFulfillEntry = [url: string, fulfillOptions: RouteHandlerFulfillOptions]; +export type ResponseHandlerEntry = [url: string, handler: (response: Response) => Promise | void]; export type RouteHandlerEntry = [url: string, handler: RouteHandlerFn]; export interface GotoRouteHandlerOptions { @@ -70,10 +58,7 @@ export abstract class BasePage { readonly heading: Locator; readonly footer: Locator; - constructor( - { url, title, heading }: BasePageProps, - testArgs: BasePageTestArgs, - ) { + constructor({ url, title, heading }: BasePageProps, testArgs: BasePageTestArgs) { this.page = testArgs.page; this.url = url; this.title = title; @@ -94,9 +79,7 @@ export abstract class BasePage { return this._mockError; } - set mockError( - err: boolean | number | RouteHandlerFulfillOptions | undefined, - ) { + set mockError(err: boolean | number | RouteHandlerFulfillOptions | undefined) { if (err == null || err === false) { this._mockError = undefined; return; @@ -151,6 +134,39 @@ export abstract class BasePage { ); } + async testHeader() { + await expect(this.page).toHaveTitle(this.title); + await expect(this.heading).toBeVisible(); + } + + async testCard(card: { name: string }) { + const cardHeader = this.page.locator(".usa-card__header", { + hasText: card.name, + }); + + await expect(cardHeader).toBeVisible(); + } + + async testSidenav(navItems: SideNavItem[]) { + const sideNav = this.page.getByTestId("sidenav"); + + for (const navItem of navItems) { + const link = sideNav.locator(`a`, { hasText: navItem.name }); + + await expect(link).toBeVisible(); + await expect(link).toHaveAttribute("href", navItem.path); + } + } + + async testFooter() { + await expect(this.page.locator("footer")).toBeAttached(); + await this.page.locator("footer").scrollIntoViewIfNeeded(); + await expect(this.page.locator("footer")).toBeInViewport(); + await expect(this.page.getByTestId("govBanner")).not.toBeInViewport(); + await this.page.evaluate(() => window.scrollTo(0, 0)); + await expect(this.page.getByTestId("govBanner")).toBeInViewport(); + } + /** * Used to select the test org if logged-in user is Admin and the isTestOrg prop is set to true. * This is needed for smoke tests since they use live data. @@ -210,19 +226,11 @@ export abstract class BasePage { const wrapped = items.map(([url, _fulfillOptions]) => { const fn = async (request: Request) => { const fulfillOptions = - typeof _fulfillOptions === "function" - ? await _fulfillOptions(request) - : _fulfillOptions; + typeof _fulfillOptions === "function" ? await _fulfillOptions(request) : _fulfillOptions; const mockErrorFulfillOptions = - typeof this.mockError === "function" - ? await this.mockError(request) - : this.mockError; - const mockCacheFulfillOptions = this.getMockCacheFulfillOptions( - url, - fulfillOptions, - ); - const mockOverrideFulfillOptions = - mockErrorFulfillOptions ?? mockCacheFulfillOptions; + typeof this.mockError === "function" ? await this.mockError(request) : this.mockError; + const mockCacheFulfillOptions = this.getMockCacheFulfillOptions(url, fulfillOptions); + const mockOverrideFulfillOptions = mockErrorFulfillOptions ?? mockCacheFulfillOptions; return { isMock: true, @@ -233,9 +241,7 @@ export abstract class BasePage { }); wrapped.forEach(([url, fn]) => - this.mockRouteHandlers.set(url, async (route, req) => - route.fulfill(await fn(req)), - ), + this.mockRouteHandlers.set(url, async (route, req) => route.fulfill(await fn(req))), ); return wrapped; @@ -244,15 +250,10 @@ export abstract class BasePage { /** * Helper function to convert RouteHandlerFulfillEntries to RouteHandlerEntries. */ - createRouteHandlers( - items: RouteHandlerFulfillEntry[], - ): RouteHandlerEntry[] { + createRouteHandlers(items: RouteHandlerFulfillEntry[]): RouteHandlerEntry[] { return items.map(([url, _fulfill]) => { const handler = async (route: Route, request: Request) => { - const fulfill = - typeof _fulfill === "function" - ? await _fulfill(request) - : _fulfill; + const fulfill = typeof _fulfill === "function" ? await _fulfill(request) : _fulfill; return route.fulfill(fulfill); }; @@ -302,10 +303,7 @@ export abstract class BasePage { * Get or warm the cache for a particular mock URL's fulfillOptions. This * allows for dynamic options to persist across page reloads for consistency. */ - getMockCacheFulfillOptions( - url: string, - fulfillOptions: RouteFulfillOptions, - ) { + getMockCacheFulfillOptions(url: string, fulfillOptions: RouteFulfillOptions) { const cache = this._mockRouteCache[url]; if (!cache) { this._mockRouteCache[url] = fulfillOptions; diff --git a/frontend-react/e2e/pages/about-side-navigation.ts b/frontend-react/e2e/pages/about-side-navigation.ts deleted file mode 100644 index cd685dbad0e..00000000000 --- a/frontend-react/e2e/pages/about-side-navigation.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Page } from "@playwright/test"; - -export async function clickNetwork(page: Page) { - await page - .getByTestId("sidenav") - .getByRole("link", { name: /Our network/ }) - .click(); -} - -export async function clickRoadmap(page: Page) { - await page - .getByTestId("sidenav") - .getByRole("link", { name: /Product roadmap/ }) - .click(); -} - -export async function clickNews(page: Page) { - await page.getByTestId("sidenav").getByRole("link", { name: /News/ }).click(); -} - -export async function clickCaseStudies(page: Page) { - await page - .getByTestId("sidenav") - .getByRole("link", { name: /Case studies/ }) - .click(); -} - -export async function clickSecurity(page: Page) { - await page - .getByTestId("sidenav") - .getByRole("link", { name: /Security/ }) - .click(); -} - -export async function clickReleaseNotes(page: Page) { - await page - .getByTestId("sidenav") - .getByRole("link", { name: /Release notes/ }) - .click(); -} diff --git a/frontend-react/e2e/pages/admin/receiver-status.ts b/frontend-react/e2e/pages/authenticated/admin/receiver-status.ts similarity index 75% rename from frontend-react/e2e/pages/admin/receiver-status.ts rename to frontend-react/e2e/pages/authenticated/admin/receiver-status.ts index 0f69f4bb691..a937092350c 100644 --- a/frontend-react/e2e/pages/admin/receiver-status.ts +++ b/frontend-react/e2e/pages/authenticated/admin/receiver-status.ts @@ -1,18 +1,13 @@ import { expect, Locator } from "@playwright/test"; import { endOfDay, format, startOfDay, subDays } from "date-fns"; -import { RSReceiverStatus } from "../../../src/hooks/api/UseReceiversConnectionStatus/UseReceiversConnectionStatus"; +import { RSReceiverStatus } from "../../../../src/hooks/api/UseReceiversConnectionStatus/UseReceiversConnectionStatus"; import { createStatusTimePeriodData, SUCCESS_RATE_CLASSNAME_MAP, -} from "../../../src/pages/admin/receiver-dashboard/utils"; -import { DatePair, dateShortFormat } from "../../../src/utils/DateTimeUtils"; -import { createMockGetReceiverStatus } from "../../mocks/receiverStatus"; -import { - BasePage, - BasePageTestArgs, - type ResponseHandlerEntry, - type RouteHandlerFulfillEntry, -} from "../BasePage"; +} from "../../../../src/pages/admin/receiver-dashboard/utils"; +import { DatePair, dateShortFormat } from "../../../../src/utils/DateTimeUtils"; +import { createMockGetReceiverStatus } from "../../../mocks/receiverStatus"; +import { BasePage, BasePageTestArgs, type ResponseHandlerEntry, type RouteHandlerFulfillEntry } from "../../BasePage"; export interface AdminReceiverStatusPageUpdateFiltersProps { dateRange?: { @@ -90,9 +85,7 @@ export class AdminReceiverStatusPage extends BasePage { ); this.addMockRouteHandlers([this.createMockReceiverStatusHandler()]); - this.addResponseHandlers([ - this.createMockReceiverStatusResponseHandler(), - ]); + this.addResponseHandlers([this.createMockReceiverStatusResponseHandler()]); const now = new Date(); @@ -100,13 +93,8 @@ export class AdminReceiverStatusPage extends BasePage { this._timePeriodData = []; this.filterForm = this.page.getByRole("form", { name: "filter" }); - const dateRangeOverlay = this.page - .getByRole("dialog") - .locator(".usa-modal-overlay"); - const dateRangeDefaultValue = [ - startOfDay(subDays(now, 2)), - endOfDay(now), - ] as DatePair; + const dateRangeOverlay = this.page.getByRole("dialog").locator(".usa-modal-overlay"); + const dateRangeDefaultValue = [startOfDay(subDays(now, 2)), endOfDay(now)] as DatePair; this.filterFormInputs = { dateRange: { label: this.page.locator("label", { @@ -116,8 +104,7 @@ export class AdminReceiverStatusPage extends BasePage { name: "Change...", }), modalOverlay: dateRangeOverlay, - expectedModalOverlayText: - "Select date range to show. (Max 10 days span)FromToUpdate", + expectedModalOverlayText: "Select date range to show. (Max 10 days span)FromToUpdate", modalOverlayCalendar: dateRangeOverlay.getByRole("application"), modalPrimaryButton: dateRangeOverlay.getByRole("button", { name: "Update", @@ -144,22 +131,15 @@ export class AdminReceiverStatusPage extends BasePage { name: "Receiver Name:", }), expectedDefaultValue: "", - tooltip: this.page - .getByTestId("tooltipWrapper") - .nth(0) - .getByRole("tooltip"), + tooltip: this.page.getByTestId("tooltipWrapper").nth(0).getByRole("tooltip"), value: "", }, resultMessage: { label: this.page.locator("label", { hasText: "Results Message:", }), - expectedTooltipText: - "Filter rows on the Result Message details. This value is found in the details.", - tooltip: this.page - .getByTestId("tooltipWrapper") - .nth(1) - .getByRole("tooltip"), + expectedTooltipText: "Filter rows on the Result Message details. This value is found in the details.", + tooltip: this.page.getByTestId("tooltipWrapper").nth(1).getByRole("tooltip"), input: this.page.getByRole("textbox", { name: "Results Message:", }), @@ -171,10 +151,7 @@ export class AdminReceiverStatusPage extends BasePage { hasText: "Success Type:", }), expectedTooltipText: "Show only rows in one of these states.", - tooltip: this.page - .getByTestId("tooltipWrapper") - .nth(2) - .getByRole("tooltip"), + tooltip: this.page.getByTestId("tooltipWrapper").nth(2).getByRole("tooltip"), input: this.page.getByRole("combobox", { name: "Success Type:", }), @@ -192,10 +169,7 @@ export class AdminReceiverStatusPage extends BasePage { * Error expected additionally if user context isn't admin */ get isPageLoadExpected() { - return ( - super.isPageLoadExpected && - this.testArgs.storageState === this.testArgs.adminLogin.path - ); + return super.isPageLoadExpected && this.testArgs.storageState === this.testArgs.adminLogin.path; } get receiverStatus() { @@ -213,10 +187,7 @@ export class AdminReceiverStatusPage extends BasePage { const url = new URL(request.url()); const startDate = url.searchParams.get("start_date"); const endDate = url.searchParams.get("end_date"); - const range = - startDate && endDate - ? ([new Date(startDate), new Date(endDate)] as DatePair) - : undefined; + const range = startDate && endDate ? ([new Date(startDate), new Date(endDate)] as DatePair) : undefined; return { json: this.createMockReceiverStatuses(range), @@ -233,27 +204,18 @@ export class AdminReceiverStatusPage extends BasePage { const url = new URL(apiRes.url()); const startDate = url.searchParams.get("start_date"); const endDate = url.searchParams.get("end_date"); - const range = - startDate && endDate - ? ([new Date(startDate), new Date(endDate)] as DatePair) - : undefined; + const range = startDate && endDate ? ([new Date(startDate), new Date(endDate)] as DatePair) : undefined; this._receiverStatus = data; - this._timePeriodData = range - ? this.createTimePeriodData({ data, range }) - : []; + this._timePeriodData = range ? this.createTimePeriodData({ data, range }) : []; }, ]; } - createMockReceiverStatuses( - ...args: Parameters - ) { + createMockReceiverStatuses(...args: Parameters) { return createMockGetReceiverStatus(...args); } - createTimePeriodData( - ...args: Parameters - ) { + createTimePeriodData(...args: Parameters) { return createStatusTimePeriodData(...args); } @@ -281,11 +243,7 @@ export class AdminReceiverStatusPage extends BasePage { }); } - getExpectedReceiverStatusRowTitle( - organizationName: string, - receiverName: string, - successRate: number | string, - ) { + getExpectedReceiverStatusRowTitle(organizationName: string, receiverName: string, successRate: number | string) { return [organizationName, receiverName, successRate, "%"].join(""); } @@ -303,16 +261,14 @@ export class AdminReceiverStatusPage extends BasePage { allCustom: async () => { return (await days.all()).map((d) => Object.assign(d, { - timePeriods: - this.getReceiverStatusRowDisplayDayTimePeriods(d), + timePeriods: this.getReceiverStatusRowDisplayDayTimePeriods(d), }), ); }, nthCustom: (nth: number) => { const day = days.nth(nth); return Object.assign(day, { - timePeriods: - this.getReceiverStatusRowDisplayDayTimePeriods(day), + timePeriods: this.getReceiverStatusRowDisplayDayTimePeriods(day), }); }, }); @@ -334,39 +290,21 @@ export class AdminReceiverStatusPage extends BasePage { }: AdminReceiverStatusPageUpdateFiltersProps) { // API request will only fire if date ranges are different const isDateRangeDifferent = - dateRange == null || - this.getIsDateRangesDifferent( - this.filterFormInputs.dateRange.value, - dateRange.value, - ); - const isRequestAwaitedBool = - dateRange != null && - isDateRangeDifferent && - dateRange.isRequestAwaited !== false; + dateRange == null || this.getIsDateRangesDifferent(this.filterFormInputs.dateRange.value, dateRange.value); + const isRequestAwaitedBool = dateRange != null && isDateRangeDifferent && dateRange.isRequestAwaited !== false; const p = isRequestAwaitedBool - ? this.page.waitForRequest( - AdminReceiverStatusPage.API_RECEIVER_STATUS, - ) + ? this.page.waitForRequest(AdminReceiverStatusPage.API_RECEIVER_STATUS) : Promise.resolve(); if (dateRange && isDateRangeDifferent) { const { value, inputMethod } = dateRange; await this.updateFilterDateRange(...value, inputMethod); } - if ( - receiverName != null && - receiverName !== this.filterFormInputs.receiverName.value - ) + if (receiverName != null && receiverName !== this.filterFormInputs.receiverName.value) await this.updateFilterReceiverName(receiverName); - if ( - resultMessage != null && - resultMessage !== this.filterFormInputs.resultMessage.value - ) + if (resultMessage != null && resultMessage !== this.filterFormInputs.resultMessage.value) await this.updateFilterResultMessage(resultMessage); - if ( - successType != null && - successType !== this.filterFormInputs.successType.value - ) + if (successType != null && successType !== this.filterFormInputs.successType.value) await this.updateFilterSuccessType(successType); if (!isRequestAwaitedBool) return undefined as void; @@ -376,11 +314,7 @@ export class AdminReceiverStatusPage extends BasePage { return reqUrl; } - async updateFilterDateRange( - start: Date, - end: Date, - inputMethod: "textbox" | "calendar" = "textbox", - ) { + async updateFilterDateRange(start: Date, end: Date, inputMethod: "textbox" | "calendar" = "textbox") { const { button, modalStartInput, @@ -438,10 +372,8 @@ export class AdminReceiverStatusPage extends BasePage { dateRange: { value: this.filterFormInputs.dateRange.expectedDefaultValue, }, - receiverName: - this.filterFormInputs.receiverName.expectedDefaultValue, - resultMessage: - this.filterFormInputs.resultMessage.expectedDefaultValue, + receiverName: this.filterFormInputs.receiverName.expectedDefaultValue, + resultMessage: this.filterFormInputs.resultMessage.expectedDefaultValue, successType: this.filterFormInputs.successType.expectedDefaultValue, }; return await this.updateFilters(resetValues); @@ -471,9 +403,7 @@ export class AdminReceiverStatusPage extends BasePage { async testReceiverStatusDisplay() { const [startDate, endDate] = this.filterFormInputs.dateRange.value; const statusRows = this.receiverStatusRowsLocator; - await expect(statusRows).toHaveCount( - new Set(this.receiverStatus?.map((r) => r.receiverId)).size, - ); + await expect(statusRows).toHaveCount(new Set(this.receiverStatus?.map((r) => r.receiverId)).size); const expectedDaysText = [ dateShortFormat(startDate), @@ -482,13 +412,7 @@ export class AdminReceiverStatusPage extends BasePage { ].join(" "); for (const [ i, - { - days, - successRate, - organizationName, - receiverName, - successRateType, - }, + { days, successRate, organizationName, receiverName, successRateType }, ] of this.timePeriodData.entries()) { const { title, display, days: daysLoc } = statusRows.nthCustom(i); @@ -497,9 +421,7 @@ export class AdminReceiverStatusPage extends BasePage { receiverName, successRate, ); - const expectedClass = new RegExp( - SUCCESS_RATE_CLASSNAME_MAP[successRateType], - ); + const expectedClass = new RegExp(SUCCESS_RATE_CLASSNAME_MAP[successRateType]); await expect(title).toBeVisible(); await expect(title).toHaveText(expectedTitleText); @@ -516,9 +438,7 @@ export class AdminReceiverStatusPage extends BasePage { for (const [i, { successRateType }] of timePeriods.entries()) { const sliceEle = daySlices.nth(i); - const expectedClass = new RegExp( - SUCCESS_RATE_CLASSNAME_MAP[successRateType], - ); + const expectedClass = new RegExp(SUCCESS_RATE_CLASSNAME_MAP[successRateType]); await expect(sliceEle).toBeVisible(); await expect(sliceEle).toHaveClass(expectedClass); diff --git a/frontend-react/e2e/pages/daily-data-details.ts b/frontend-react/e2e/pages/authenticated/daily-data-details.ts similarity index 90% rename from frontend-react/e2e/pages/daily-data-details.ts rename to frontend-react/e2e/pages/authenticated/daily-data-details.ts index 06d382632b9..ed98347e902 100644 --- a/frontend-react/e2e/pages/daily-data-details.ts +++ b/frontend-react/e2e/pages/authenticated/daily-data-details.ts @@ -1,8 +1,8 @@ -import { BasePage, BasePageTestArgs, type RouteHandlerFulfillEntry } from "./BasePage"; import { API_WATERS_REPORT, URL_REPORT_DETAILS } from "./report-details"; -import { RSDelivery, RSFacility } from "../../src/config/endpoints/deliveries"; -import { MOCK_GET_DELIVERY } from "../mocks/delivery"; -import { MOCK_GET_FACILITIES } from "../mocks/facilities"; +import { RSDelivery, RSFacility } from "../../../src/config/endpoints/deliveries"; +import { MOCK_GET_DELIVERY } from "../../mocks/delivery"; +import { MOCK_GET_FACILITIES } from "../../mocks/facilities"; +import { BasePage, BasePageTestArgs, type RouteHandlerFulfillEntry } from "../BasePage"; const id = "73e3cbc8-9920-4ab7-871f-843a1db4c074"; diff --git a/frontend-react/e2e/pages/daily-data.ts b/frontend-react/e2e/pages/authenticated/daily-data.ts similarity index 96% rename from frontend-react/e2e/pages/daily-data.ts rename to frontend-react/e2e/pages/authenticated/daily-data.ts index 647a20ede97..540b2eb6396 100644 --- a/frontend-react/e2e/pages/daily-data.ts +++ b/frontend-react/e2e/pages/authenticated/daily-data.ts @@ -1,10 +1,9 @@ import { expect, Page } from "@playwright/test"; import { format } from "date-fns"; -import { BasePage, BasePageTestArgs, type RouteHandlerFulfillEntry } from "./BasePage"; import { DailyDataDetailsPage } from "./daily-data-details"; import { API_WATERS_ORG } from "./report-details"; -import { RSReceiver } from "../../src/config/endpoints/settings"; -import { TEST_ORG_AK, TEST_ORG_AK_RECEIVER, TEST_ORG_IGNORE, TEST_ORG_UP_RECEIVER_UP } from "../helpers/utils"; +import { RSReceiver } from "../../../src/config/endpoints/settings"; +import { TEST_ORG_AK, TEST_ORG_AK_RECEIVER, TEST_ORG_IGNORE, TEST_ORG_UP_RECEIVER_UP } from "../../helpers/utils"; import { MOCK_GET_DELIVERIES_AK, MOCK_GET_DELIVERIES_AK_FILENAME, @@ -14,9 +13,10 @@ import { MOCK_GET_DELIVERIES_IGNORE_FILENAME, MOCK_GET_DELIVERIES_IGNORE_FULL_ELR, MOCK_GET_DELIVERIES_IGNORE_REPORT_ID, -} from "../mocks/deliveries"; -import { MOCK_GET_DELIVERY } from "../mocks/delivery"; -import { MOCK_GET_FACILITIES } from "../mocks/facilities"; +} from "../../mocks/deliveries"; +import { MOCK_GET_DELIVERY } from "../../mocks/delivery"; +import { MOCK_GET_FACILITIES } from "../../mocks/facilities"; +import { BasePage, BasePageTestArgs, type RouteHandlerFulfillEntry } from "../BasePage"; export class DailyDataPage extends BasePage { static readonly URL_DAILY_DATA = "/daily-data"; diff --git a/frontend-react/e2e/pages/authenticated/last-mile-failures.ts b/frontend-react/e2e/pages/authenticated/last-mile-failures.ts new file mode 100644 index 00000000000..ab8e38e5f92 --- /dev/null +++ b/frontend-react/e2e/pages/authenticated/last-mile-failures.ts @@ -0,0 +1,45 @@ +import { MOCK_GET_RESEND, MOCK_GET_SEND_FAILURES } from "../../mocks/lastMilefailures"; +import { BasePage, BasePageTestArgs, RouteHandlerFulfillEntry } from "../BasePage"; + +export class LastMileFailuresPage extends BasePage { + static readonly URL_LAST_MILE = "/admin/lastmile"; + static readonly API_GET_SEND_FAILURES = "/api/adm/getsendfailures?days_to_show=15"; + static readonly API_GET_RESEND = "/api/adm/getresend?days_to_show=15"; + + constructor(testArgs: BasePageTestArgs) { + super( + { + url: LastMileFailuresPage.URL_LAST_MILE, + title: "Last Mile Failures", + heading: testArgs.page.getByRole("heading", { + name: "Last Mile Failures", + }), + }, + testArgs, + ); + + this.addMockRouteHandlers([this.createMockGetSendFailuresHandler(), this.createMockGetResendHandler()]); + } + + createMockGetSendFailuresHandler(): RouteHandlerFulfillEntry { + return [ + LastMileFailuresPage.API_GET_SEND_FAILURES, + () => { + return { + json: MOCK_GET_SEND_FAILURES, + }; + }, + ]; + } + + createMockGetResendHandler(): RouteHandlerFulfillEntry { + return [ + LastMileFailuresPage.API_GET_RESEND, + () => { + return { + json: MOCK_GET_RESEND, + }; + }, + ]; + } +} diff --git a/frontend-react/e2e/pages/authenticated/message-details.ts b/frontend-react/e2e/pages/authenticated/message-details.ts new file mode 100644 index 00000000000..124831a1829 --- /dev/null +++ b/frontend-react/e2e/pages/authenticated/message-details.ts @@ -0,0 +1,34 @@ +import { MessageIDSearchPage } from "./message-id-search"; +import { MOCK_GET_MESSAGE } from "../../mocks/messages"; + +import { BasePage, BasePageTestArgs, RouteHandlerFulfillEntry } from "../BasePage"; + +export class MessageDetailsPage extends BasePage { + static readonly URL_MESSAGE_DETAILS = `/message-details/${MessageIDSearchPage.MESSAGE_ID}`; + + constructor(testArgs: BasePageTestArgs) { + super( + { + url: MessageDetailsPage.URL_MESSAGE_DETAILS, + title: "ReportStream - CDC's free, interoperable data transfer platform", + heading: testArgs.page.getByRole("heading", { + name: "Message ID Search", + }), + }, + testArgs, + ); + + this.addMockRouteHandlers([this.createMessageIDSearchAPIHandler()]); + } + + createMessageIDSearchAPIHandler(): RouteHandlerFulfillEntry { + return [ + MessageIDSearchPage.API_MESSAGE, + () => { + return { + json: MOCK_GET_MESSAGE, + }; + }, + ]; + } +} diff --git a/frontend-react/e2e/pages/authenticated/message-id-search.ts b/frontend-react/e2e/pages/authenticated/message-id-search.ts new file mode 100644 index 00000000000..d708f3f10c3 --- /dev/null +++ b/frontend-react/e2e/pages/authenticated/message-id-search.ts @@ -0,0 +1,46 @@ +import { MOCK_GET_MESSAGE, MOCK_GET_MESSAGES } from "../../mocks/messages"; +import { BasePage, BasePageTestArgs, RouteHandlerFulfillEntry } from "../BasePage"; + +export class MessageIDSearchPage extends BasePage { + static readonly URL_MESSAGE_ID_SEARCH = "/admin/message-tracker"; + static readonly API_MESSAGES = "**/api/messages?messageId=*"; + static readonly API_MESSAGE = "**/api/message/*"; + static readonly MESSAGE_ID = "582098"; + + constructor(testArgs: BasePageTestArgs) { + super( + { + url: MessageIDSearchPage.URL_MESSAGE_ID_SEARCH, + title: "Message ID search - Admin", + heading: testArgs.page.getByRole("heading", { + name: "Message ID Search", + }), + }, + testArgs, + ); + + this.addMockRouteHandlers([this.createMessageIDSearchAPIHandler(), this.createMessagesIDSearchAPIHandler()]); + } + + createMessageIDSearchAPIHandler(): RouteHandlerFulfillEntry { + return [ + MessageIDSearchPage.API_MESSAGE, + () => { + return { + json: MOCK_GET_MESSAGE, + }; + }, + ]; + } + + createMessagesIDSearchAPIHandler(): RouteHandlerFulfillEntry { + return [ + MessageIDSearchPage.API_MESSAGES, + () => { + return { + json: MOCK_GET_MESSAGES, + }; + }, + ]; + } +} diff --git a/frontend-react/e2e/pages/organization.ts b/frontend-react/e2e/pages/authenticated/organization.ts similarity index 61% rename from frontend-react/e2e/pages/organization.ts rename to frontend-react/e2e/pages/authenticated/organization.ts index bbac970dd64..6d24305989d 100644 --- a/frontend-react/e2e/pages/organization.ts +++ b/frontend-react/e2e/pages/authenticated/organization.ts @@ -1,10 +1,6 @@ -import { - BasePage, - BasePageTestArgs, - type RouteHandlerFulfillEntry, -} from "./BasePage"; -import { RSOrganizationSettings } from "../../src/config/endpoints/settings"; -import { MOCK_GET_ORGANIZATION_SETTINGS_LIST } from "../mocks/organizations"; +import { RSOrganizationSettings } from "../../../src/config/endpoints/settings"; +import { MOCK_GET_ORGANIZATION_SETTINGS_LIST } from "../../mocks/organizations"; +import { BasePage, BasePageTestArgs, type RouteHandlerFulfillEntry } from "../BasePage"; export class OrganizationPage extends BasePage { static readonly API_ORGANIZATIONS = "/api/settings/organizations"; @@ -13,7 +9,7 @@ export class OrganizationPage extends BasePage { super( { url: "/admin/settings", - title: "Organizations", + title: "Admin-Organizations", heading: testArgs.page.getByRole("heading", { name: "Organizations", }), @@ -23,19 +19,13 @@ export class OrganizationPage extends BasePage { this._organizationSettings = []; this.addResponseHandlers([ - [ - OrganizationPage.API_ORGANIZATIONS, - async (res) => (this._organizationSettings = await res.json()), - ], + [OrganizationPage.API_ORGANIZATIONS, async (res) => (this._organizationSettings = await res.json())], ]); this.addMockRouteHandlers([this.createMockOrganizationHandler()]); } get isPageLoadExpected() { - return ( - super.isPageLoadExpected && - this.testArgs.storageState === this.testArgs.adminLogin.path - ); + return super.isPageLoadExpected && this.testArgs.storageState === this.testArgs.adminLogin.path; } createMockOrganizationHandler(): RouteHandlerFulfillEntry { diff --git a/frontend-react/e2e/pages/report-details.ts b/frontend-react/e2e/pages/authenticated/report-details.ts similarity index 90% rename from frontend-react/e2e/pages/report-details.ts rename to frontend-react/e2e/pages/authenticated/report-details.ts index 01f342b9383..757335b0c7d 100644 --- a/frontend-react/e2e/pages/report-details.ts +++ b/frontend-react/e2e/pages/authenticated/report-details.ts @@ -1,8 +1,8 @@ import { expect, Page } from "@playwright/test"; import fs from "node:fs"; -import { MOCK_GET_DELIVERY } from "../mocks/delivery"; -import { MOCK_GET_HISTORY_REPORT } from "../mocks/historyReport"; -import { MOCK_GET_SUBMISSION_HISTORY } from "../mocks/submissionHistory"; +import { MOCK_GET_DELIVERY } from "../../mocks/delivery"; +import { MOCK_GET_HISTORY_REPORT } from "../../mocks/historyReport"; +import { MOCK_GET_SUBMISSION_HISTORY } from "../../mocks/submissionHistory"; export const URL_REPORT_DETAILS = "/report-details"; export const API_WATERS_REPORT = "**/api/waters/report"; diff --git a/frontend-react/e2e/pages/authenticated/submission-history.ts b/frontend-react/e2e/pages/authenticated/submission-history.ts new file mode 100644 index 00000000000..ac18da76c26 --- /dev/null +++ b/frontend-react/e2e/pages/authenticated/submission-history.ts @@ -0,0 +1,113 @@ +import { expect, Page } from "@playwright/test"; +import { TEST_ORG_IGNORE } from "../../helpers/utils"; +import { MOCK_GET_SUBMISSION_HISTORY } from "../../mocks/submissionHistory"; +import { MOCK_GET_SUBMISSIONS } from "../../mocks/submissions"; +import { BasePage, BasePageTestArgs, type RouteHandlerFulfillEntry } from "../BasePage"; + +export const URL_SUBMISSION_HISTORY = "/submissions"; +export const API_GET_REPORT_HISTORY = `**/api/waters/report/**`; +export const id = "73e3cbc8-9920-4ab7-871f-843a1db4c074"; + +export class SubmissionHistoryPage extends BasePage { + static readonly URL_SUBMISSION_HISTORY = "/submissions"; + + constructor(testArgs: BasePageTestArgs) { + super( + { + url: SubmissionHistoryPage.URL_SUBMISSION_HISTORY, + title: "ReportStream - CDC's free, interoperable data transfer platform", + heading: testArgs.page.getByRole("heading", { + name: "Submission history", + }), + }, + testArgs, + ); + + this.addMockRouteHandlers([ + // Ignore Org + this.createMockSubmissionsForOrgHandler(TEST_ORG_IGNORE, MOCK_GET_SUBMISSIONS), + this.createMockSubmissionHistoryHandler(), + ]); + } + + createMockSubmissionsForOrgHandler( + organization: string, + mockFileName: any, + responseStatus = 200, + ): RouteHandlerFulfillEntry { + return [ + `**/api/waters/org/${organization}/submissions?*`, + () => { + return { + json: mockFileName, + status: responseStatus, + }; + }, + ]; + } + + createMockSubmissionHistoryHandler(responseStatus = 200): RouteHandlerFulfillEntry { + return [ + API_GET_REPORT_HISTORY, + () => { + return { + json: MOCK_GET_SUBMISSION_HISTORY, + status: responseStatus, + }; + }, + ]; + } + + get filterButton() { + return this.page.getByRole("button", { + name: "Filter", + }); + } + + get clearButton() { + return this.page.getByRole("button", { + name: "Clear", + }); + } + + /** + * Error expected additionally if user context isn't admin + */ + get isPageLoadExpected() { + return super.isPageLoadExpected && this.testArgs.storageState === this.testArgs.adminLogin.path; + } +} + +export async function goto(page: Page) { + await page.goto(URL_SUBMISSION_HISTORY, { + waitUntil: "domcontentloaded", + }); +} + +export async function mockGetReportHistoryResponse(page: Page, responseStatus = 200) { + await page.route(API_GET_REPORT_HISTORY, async (route) => { + const json = MOCK_GET_SUBMISSION_HISTORY; + await route.fulfill({ json, status: responseStatus }); + }); +} + +export async function openReportIdDetailPage(page: Page, id: string) { + await expect(page).toHaveURL(`/submissions/${id}`); + await expect(page.getByText(`Details: ${id}`)).toBeVisible(); +} + +export async function tableHeaders(page: Page) { + await expect(page.locator(".usa-table th").nth(0)).toHaveText(/Report ID/); + await expect(page.locator(".usa-table th").nth(1)).toHaveText("Date/time submitted"); + await expect(page.locator(".usa-table th").nth(2)).toHaveText(/File/); + await expect(page.locator(".usa-table th").nth(3)).toHaveText(/Records/); + await expect(page.locator(".usa-table th").nth(4)).toHaveText(/Status/); +} + +export async function breadcrumbLink(page: Page, index: number, linkName: string, expectedUrl: string) { + const breadcrumbLinks = page.locator(".usa-breadcrumb ol li"); + await expect(breadcrumbLinks.nth(index)).toHaveText(linkName); + await breadcrumbLinks.nth(index).getByRole("link", { name: linkName }).click(); + await expect(page.locator("h1")).toBeAttached(); + await expect(page).toHaveURL(expectedUrl); +} diff --git a/frontend-react/e2e/pages/authenticated/submissions-details.ts b/frontend-react/e2e/pages/authenticated/submissions-details.ts new file mode 100644 index 00000000000..35af206373a --- /dev/null +++ b/frontend-react/e2e/pages/authenticated/submissions-details.ts @@ -0,0 +1,40 @@ +import { API_WATERS_REPORT } from "./report-details"; +import { URL_SUBMISSION_HISTORY } from "./submission-history"; +import { MOCK_GET_SUBMISSION_HISTORY } from "../../mocks/submissionHistory"; +import { BasePage, BasePageTestArgs, type RouteHandlerFulfillEntry } from "../BasePage"; + +export const id = "73e3cbc8-9920-4ab7-871f-843a1db4c074"; +export class SubmissionsDetailsPage extends BasePage { + static readonly URL_SUBMISSIONS_DETAILS = `${URL_SUBMISSION_HISTORY}/${id}`; + + constructor(testArgs: BasePageTestArgs) { + super( + { + url: SubmissionsDetailsPage.URL_SUBMISSIONS_DETAILS, + title: "ReportStream - CDC's free, interoperable data transfer platform", + }, + testArgs, + ); + + this.addMockRouteHandlers([this.createMockSubmissionHistoryHandler(MOCK_GET_SUBMISSION_HISTORY)]); + } + + createMockSubmissionHistoryHandler(mockFileName: any, responseStatus = 200): RouteHandlerFulfillEntry { + return [ + `${API_WATERS_REPORT}/${id}/history`, + () => { + return { + json: mockFileName, + status: responseStatus, + }; + }, + ]; + } + + /** + * Error expected additionally if user context isn't admin + */ + get isPageLoadExpected() { + return super.isPageLoadExpected && this.testArgs.storageState === this.testArgs.adminLogin.path; + } +} diff --git a/frontend-react/e2e/pages/homepage.ts b/frontend-react/e2e/pages/homepage.ts deleted file mode 100644 index 5a22616b8ba..00000000000 --- a/frontend-react/e2e/pages/homepage.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Page } from "@playwright/test"; - -export async function goto(page: Page) { - await page.goto("/", { - waitUntil: "domcontentloaded", - }); -} diff --git a/frontend-react/e2e/pages/last-mile-failures.ts b/frontend-react/e2e/pages/last-mile-failures.ts deleted file mode 100644 index 86baae6ac4f..00000000000 --- a/frontend-react/e2e/pages/last-mile-failures.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Page } from "@playwright/test"; -import { - MOCK_GET_RESEND, - MOCK_GET_SEND_FAILURES, -} from "../mocks/lastMilefailures"; - -const URL_LAST_MILE = "/admin/lastmile"; -const API_GET_RESEND = "/api/adm/getresend?days_to_show=15"; -export const API_GET_SEND_FAILURES = "/api/adm/getsendfailures?days_to_show=15"; - -export async function goto(page: Page) { - await page.goto(URL_LAST_MILE, { - waitUntil: "domcontentloaded", - }); -} - -export async function mockGetSendFailuresResponse( - page: Page, - responseStatus = 200, -) { - await page.route(API_GET_SEND_FAILURES, async (route) => { - const json = MOCK_GET_SEND_FAILURES; - await route.fulfill({ json, status: responseStatus }); - }); -} - -export async function mockGetResendResponse(page: Page) { - await page.route(API_GET_RESEND, async (route) => { - const json = MOCK_GET_RESEND; - await route.fulfill({ json }); - }); -} diff --git a/frontend-react/e2e/pages/message-details.ts b/frontend-react/e2e/pages/message-details.ts deleted file mode 100644 index f989d00d1f7..00000000000 --- a/frontend-react/e2e/pages/message-details.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Page } from "@playwright/test"; - -import { MESSAGE_ID } from "../pages/message-id-search"; - -export const URL_MESSAGE_DETAILS = `/message-details/${MESSAGE_ID}`; - -export async function goto(page: Page) { - await page.goto(URL_MESSAGE_DETAILS, { - waitUntil: "domcontentloaded", - }); -} diff --git a/frontend-react/e2e/pages/message-id-search.ts b/frontend-react/e2e/pages/message-id-search.ts deleted file mode 100644 index a71a877b395..00000000000 --- a/frontend-react/e2e/pages/message-id-search.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Page } from "@playwright/test"; - -export const URL_MESSAGE_ID_SEARCH = "/admin/message-tracker"; -export const API_MESSAGES = "**/api/messages?messageId=*"; -export const API_MESSAGE = "**/api/message/*"; - -export const MESSAGE_ID = "582098"; - -export async function goto(page: Page) { - await page.goto(URL_MESSAGE_ID_SEARCH, { - waitUntil: "domcontentloaded", - }); -} diff --git a/frontend-react/e2e/pages/our-network.ts b/frontend-react/e2e/pages/our-network.ts deleted file mode 100644 index 7a8f3d7dbc7..00000000000 --- a/frontend-react/e2e/pages/our-network.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { expect, Page } from "@playwright/test"; - -const URL_OUR_NETWORK = "/about/our-network"; -export async function goto(page: Page) { - await page.goto(URL_OUR_NETWORK, { - waitUntil: "domcontentloaded", - }); -} -export async function onLoad(page: Page) { - await expect(page).toHaveURL(/our-network/); - await expect(page).toHaveTitle(/Our network/); -} - -export async function clickOnLiveMap(page: Page) { - await page.getByTestId("map").click(); - await expect(page).toHaveURL(URL_OUR_NETWORK); -} diff --git a/frontend-react/e2e/pages/public-pages-link-check.ts b/frontend-react/e2e/pages/public-pages-link-check.ts deleted file mode 100644 index 64224aad02d..00000000000 --- a/frontend-react/e2e/pages/public-pages-link-check.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Page } from "@playwright/test"; - -export async function publicPageGoto(page: Page, path: string) { - await page.goto(path, { - waitUntil: "networkidle", - }); -} diff --git a/frontend-react/e2e/pages/about.ts b/frontend-react/e2e/pages/public/about/about.ts similarity index 85% rename from frontend-react/e2e/pages/about.ts rename to frontend-react/e2e/pages/public/about/about.ts index 2189c703f39..c1c0d3b6082 100644 --- a/frontend-react/e2e/pages/about.ts +++ b/frontend-react/e2e/pages/public/about/about.ts @@ -1,4 +1,4 @@ -import { BasePage, BasePageTestArgs } from "./BasePage"; +import { BasePage, BasePageTestArgs } from "../../BasePage"; export class AboutPage extends BasePage { constructor(testArgs: BasePageTestArgs) { diff --git a/frontend-react/e2e/pages/public/about/our-network.ts b/frontend-react/e2e/pages/public/about/our-network.ts new file mode 100644 index 00000000000..c36c21b8b87 --- /dev/null +++ b/frontend-react/e2e/pages/public/about/our-network.ts @@ -0,0 +1,16 @@ +import { BasePage, BasePageTestArgs } from "../../BasePage"; + +export class OurNetworkPage extends BasePage { + constructor(testArgs: BasePageTestArgs) { + super( + { + url: "/about/our-network", + title: "Our network - ReportStream", + heading: testArgs.page.getByRole("heading", { + name: "Our network", + }), + }, + testArgs, + ); + } +} diff --git a/frontend-react/e2e/pages/public/about/roadmap.ts b/frontend-react/e2e/pages/public/about/roadmap.ts new file mode 100644 index 00000000000..80c6227940f --- /dev/null +++ b/frontend-react/e2e/pages/public/about/roadmap.ts @@ -0,0 +1,16 @@ +import { BasePage, BasePageTestArgs } from "../../BasePage"; + +export class RoadmapPage extends BasePage { + constructor(testArgs: BasePageTestArgs) { + super( + { + url: "/about/roadmap", + title: "Product roadmap", + heading: testArgs.page.getByRole("heading", { + name: "Product roadmap", + }), + }, + testArgs, + ); + } +} diff --git a/frontend-react/e2e/pages/security.ts b/frontend-react/e2e/pages/public/about/security.ts similarity index 91% rename from frontend-react/e2e/pages/security.ts rename to frontend-react/e2e/pages/public/about/security.ts index 8294a43283e..5d494e97795 100644 --- a/frontend-react/e2e/pages/security.ts +++ b/frontend-react/e2e/pages/public/about/security.ts @@ -1,5 +1,5 @@ import { expect, Page } from "@playwright/test"; -import { BasePage, BasePageTestArgs } from "./BasePage"; +import { BasePage, BasePageTestArgs } from "../../BasePage"; export class SecurityPage extends BasePage { constructor(testArgs: BasePageTestArgs) { diff --git a/frontend-react/e2e/pages/getting-started/receiving-data.ts b/frontend-react/e2e/pages/public/getting-started/receiving-data.ts similarity index 88% rename from frontend-react/e2e/pages/getting-started/receiving-data.ts rename to frontend-react/e2e/pages/public/getting-started/receiving-data.ts index d87fb8acdb7..3ccd227e24f 100644 --- a/frontend-react/e2e/pages/getting-started/receiving-data.ts +++ b/frontend-react/e2e/pages/public/getting-started/receiving-data.ts @@ -1,4 +1,4 @@ -import { BasePage, BasePageTestArgs } from "../BasePage"; +import { BasePage, BasePageTestArgs } from "../../BasePage"; export class ReceivingDataPage extends BasePage { constructor(testArgs: BasePageTestArgs) { diff --git a/frontend-react/e2e/pages/getting-started/sending-data.ts b/frontend-react/e2e/pages/public/getting-started/sending-data.ts similarity index 88% rename from frontend-react/e2e/pages/getting-started/sending-data.ts rename to frontend-react/e2e/pages/public/getting-started/sending-data.ts index 9bb57acf9a5..47aa28823d5 100644 --- a/frontend-react/e2e/pages/getting-started/sending-data.ts +++ b/frontend-react/e2e/pages/public/getting-started/sending-data.ts @@ -1,4 +1,4 @@ -import { BasePage, BasePageTestArgs } from "../BasePage"; +import { BasePage, BasePageTestArgs } from "../../BasePage"; export class SendingDataPage extends BasePage { constructor(testArgs: BasePageTestArgs) { diff --git a/frontend-react/e2e/pages/header.ts b/frontend-react/e2e/pages/public/header.ts similarity index 63% rename from frontend-react/e2e/pages/header.ts rename to frontend-react/e2e/pages/public/header.ts index b4929379008..1ed78a50c6e 100644 --- a/frontend-react/e2e/pages/header.ts +++ b/frontend-react/e2e/pages/public/header.ts @@ -6,11 +6,7 @@ export async function clickOnHome(page: Page) { } export async function clickOnAbout(page: Page) { - await page - .getByTestId("header") - .getByTestId("navDropDownButton") - .getByText("About") - .click(); + await page.getByTestId("header").getByTestId("navDropDownButton").getByText("About").click(); expect(page.getByText("About ReportStream")).toBeTruthy(); expect(page.getByText("Our network")).toBeTruthy(); @@ -21,34 +17,22 @@ export async function clickOnAbout(page: Page) { } export async function clickOnGettingStarted(page: Page) { - await page - .getByTestId("header") - .getByRole("link", { name: "Getting started" }) - .click(); + await page.getByTestId("header").getByRole("link", { name: "Getting started" }).click(); await expect(page).toHaveURL(/getting-started/); } export async function clickOnDevelopers(page: Page) { - await page - .getByTestId("header") - .getByRole("link", { name: "Developers" }) - .click(); + await page.getByTestId("header").getByRole("link", { name: "Developers" }).click(); await expect(page).toHaveURL(/.*developer-resources/); } export async function clickOnYourConnection(page: Page) { - await page - .getByTestId("header") - .getByRole("link", { name: "Your Connection" }) - .click(); + await page.getByTestId("header").getByRole("link", { name: "Your Connection" }).click(); await expect(page).toHaveURL(/.*managing-your-connection/); } export async function clickOnSupport(page: Page) { - await page - .getByTestId("header") - .getByRole("link", { name: "Support" }) - .click(); + await page.getByTestId("header").getByRole("link", { name: "Support" }).click(); await expect(page).toHaveURL(/.*support/); } diff --git a/frontend-react/e2e/pages/public/homepage.ts b/frontend-react/e2e/pages/public/homepage.ts new file mode 100644 index 00000000000..994f14f5116 --- /dev/null +++ b/frontend-react/e2e/pages/public/homepage.ts @@ -0,0 +1,17 @@ +import { BasePage, BasePageTestArgs } from "../BasePage"; + +export class HomePage extends BasePage { + constructor(testArgs: BasePageTestArgs) { + super( + { + url: "/", + title: "ReportStream - CDC's free, interoperable data transfer platform", + heading: testArgs.page.getByRole("heading", { + name: "CDC’s free, single connection to streamline your data transfer and improve public health", + exact: true, + }), + }, + testArgs, + ); + } +} diff --git a/frontend-react/e2e/pages/managing-your-connection.ts b/frontend-react/e2e/pages/public/managing-your-connection/managing-your-connection.ts similarity index 91% rename from frontend-react/e2e/pages/managing-your-connection.ts rename to frontend-react/e2e/pages/public/managing-your-connection/managing-your-connection.ts index 6098bec9e63..31427cb506b 100644 --- a/frontend-react/e2e/pages/managing-your-connection.ts +++ b/frontend-react/e2e/pages/public/managing-your-connection/managing-your-connection.ts @@ -1,5 +1,5 @@ import { expect, Page } from "@playwright/test"; -import { BasePage, BasePageTestArgs } from "./BasePage"; +import { BasePage, BasePageTestArgs } from "../../BasePage"; export async function onLoad(page: Page) { await expect(page).toHaveURL(/managing-your-connection/); diff --git a/frontend-react/e2e/pages/refer-healthcare.ts b/frontend-react/e2e/pages/public/managing-your-connection/refer-healthcare.ts similarity index 88% rename from frontend-react/e2e/pages/refer-healthcare.ts rename to frontend-react/e2e/pages/public/managing-your-connection/refer-healthcare.ts index b86152b914f..a5805d5a9c0 100644 --- a/frontend-react/e2e/pages/refer-healthcare.ts +++ b/frontend-react/e2e/pages/public/managing-your-connection/refer-healthcare.ts @@ -1,4 +1,4 @@ -import { BasePage, BasePageTestArgs } from "./BasePage"; +import { BasePage, BasePageTestArgs } from "../../BasePage"; export class ReferHealthcarePage extends BasePage { constructor(testArgs: BasePageTestArgs) { diff --git a/frontend-react/e2e/pages/public/resources.ts b/frontend-react/e2e/pages/public/resources.ts new file mode 100644 index 00000000000..5accb844500 --- /dev/null +++ b/frontend-react/e2e/pages/public/resources.ts @@ -0,0 +1,17 @@ +import { BasePage, BasePageTestArgs } from "../BasePage"; + +export class DeveloperResourcesPage extends BasePage { + constructor(testArgs: BasePageTestArgs) { + super( + { + url: "/developer-resources", + title: "ReportStream developer resources", + heading: testArgs.page.getByRole("heading", { + name: "Developer resources", + exact: true, + }), + }, + testArgs, + ); + } +} diff --git a/frontend-react/e2e/pages/support.ts b/frontend-react/e2e/pages/public/support.ts similarity index 87% rename from frontend-react/e2e/pages/support.ts rename to frontend-react/e2e/pages/public/support.ts index ae5bc223708..119815da6b2 100644 --- a/frontend-react/e2e/pages/support.ts +++ b/frontend-react/e2e/pages/public/support.ts @@ -1,4 +1,4 @@ -import { BasePage, BasePageTestArgs } from "./BasePage"; +import { BasePage, BasePageTestArgs } from "../BasePage"; export class SupportPage extends BasePage { constructor(testArgs: BasePageTestArgs) { diff --git a/frontend-react/e2e/pages/resources.ts b/frontend-react/e2e/pages/resources.ts deleted file mode 100644 index c96baa6d17f..00000000000 --- a/frontend-react/e2e/pages/resources.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Page } from "@playwright/test"; - -export async function goto(page: Page) { - await page.goto("/developer-resources", { - waitUntil: "domcontentloaded", - }); -} diff --git a/frontend-react/e2e/pages/roadmap.ts b/frontend-react/e2e/pages/roadmap.ts deleted file mode 100644 index af49e5aefa5..00000000000 --- a/frontend-react/e2e/pages/roadmap.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Page } from "@playwright/test"; - -export const URL_ROADMAP = "/about/roadmap"; -export async function goto(page: Page) { - await page.goto(URL_ROADMAP, { - waitUntil: "domcontentloaded", - }); -} diff --git a/frontend-react/e2e/pages/submission-history.ts b/frontend-react/e2e/pages/submission-history.ts deleted file mode 100644 index e65b4792397..00000000000 --- a/frontend-react/e2e/pages/submission-history.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { expect, Page } from "@playwright/test"; -import { MOCK_GET_SUBMISSION_HISTORY } from "../mocks/submissionHistory"; -import { MOCK_GET_SUBMISSIONS } from "../mocks/submissions"; - -export const URL_SUBMISSION_HISTORY = "/submissions"; -export const API_GET_REPORT_HISTORY = `**/api/waters/report/**`; - -export async function goto(page: Page) { - await page.goto(URL_SUBMISSION_HISTORY, { - waitUntil: "domcontentloaded", - }); -} - -export async function gotoDetails(page: Page, id: string) { - await page.goto(`${URL_SUBMISSION_HISTORY}/${id}`, { - waitUntil: "domcontentloaded", - }); -} - -export function getOrgSubmissionsAPI(org: string) { - return `**/api/waters/org/${org}/submissions?*`; -} - -export async function mockGetSubmissionsResponse( - page: Page, - org: string, - responseStatus = 200, -) { - const submissionsApi = getOrgSubmissionsAPI(org); - await page.route(submissionsApi, async (route) => { - const json = MOCK_GET_SUBMISSIONS; - await route.fulfill({ json, status: responseStatus }); - }); -} - -export async function mockGetReportHistoryResponse( - page: Page, - responseStatus = 200, -) { - await page.route(API_GET_REPORT_HISTORY, async (route) => { - const json = MOCK_GET_SUBMISSION_HISTORY; - await route.fulfill({ json, status: responseStatus }); - }); -} - -export async function openReportIdDetailPage(page: Page, id: string) { - await expect(page).toHaveURL(`/submissions/${id}`); - await expect(page.getByText(`Details: ${id}`)).toBeVisible(); -} - -export async function title(page: Page) { - await expect(page).toHaveTitle( - /ReportStream - CDC's free, interoperable data transfer platform/, - ); -} - -export async function tableHeaders(page: Page) { - await expect(page.locator(".usa-table th").nth(0)).toHaveText(/Report ID/); - await expect(page.locator(".usa-table th").nth(1)).toHaveText( - "Date/time submitted", - ); - await expect(page.locator(".usa-table th").nth(2)).toHaveText(/File/); - await expect(page.locator(".usa-table th").nth(3)).toHaveText(/Records/); - await expect(page.locator(".usa-table th").nth(4)).toHaveText(/Status/); -} - -export async function breadcrumbLink( - page: Page, - index: number, - linkName: string, - expectedUrl: string, -) { - const breadcrumbLinks = page.locator(".usa-breadcrumb ol li"); - await expect(breadcrumbLinks.nth(index)).toHaveText(linkName); - await breadcrumbLinks - .nth(index) - .getByRole("link", { name: linkName }) - .click(); - await expect(page.locator("h1")).toBeAttached(); - await expect(page).toHaveURL(expectedUrl); -} diff --git a/frontend-react/e2e/spec/all/admin/receiver-status-page.spec.ts b/frontend-react/e2e/spec/all/authenticated/admin/receiver-status-page.spec.ts similarity index 95% rename from frontend-react/e2e/spec/all/admin/receiver-status-page.spec.ts rename to frontend-react/e2e/spec/all/authenticated/admin/receiver-status-page.spec.ts index 33fcf977608..f8f92d2615b 100644 --- a/frontend-react/e2e/spec/all/admin/receiver-status-page.spec.ts +++ b/frontend-react/e2e/spec/all/authenticated/admin/receiver-status-page.spec.ts @@ -1,10 +1,10 @@ import { addDays, endOfDay, startOfDay, subDays } from "date-fns"; -import type { RSOrganizationSettings } from "../../../../src/config/endpoints/settings"; -import { SuccessRate } from "../../../../src/pages/admin/receiver-dashboard/utils"; -import { durationFormatShort } from "../../../../src/utils/DateTimeUtils"; -import { formatDate } from "../../../../src/utils/misc"; -import { AdminReceiverStatusPage } from "../../../pages/admin/receiver-status"; -import { test as baseTest, expect, logins } from "../../../test"; +import type { RSOrganizationSettings } from "../../../../../src/config/endpoints/settings"; +import { SuccessRate } from "../../../../../src/pages/admin/receiver-dashboard/utils"; +import { durationFormatShort } from "../../../../../src/utils/DateTimeUtils"; +import { formatDate } from "../../../../../src/utils/misc"; +import { AdminReceiverStatusPage } from "../../../../pages/authenticated/admin/receiver-status"; +import { test as baseTest, expect, logins } from "../../../../test"; export interface AdminReceiverStatusPageFixtures { adminReceiverStatusPage: AdminReceiverStatusPage; @@ -71,23 +71,20 @@ test.describe("Admin Receiver Status Page", () => { await expect(adminReceiverStatusPage.page.getByText("there was an error")).toBeVisible(); }); - test( - "Has correct title", - { - tag: "@smoke", - }, - async ({ adminReceiverStatusPage }) => { - await expect(adminReceiverStatusPage.page).toHaveURL(adminReceiverStatusPage.url); - await expect(adminReceiverStatusPage.page).toHaveTitle(adminReceiverStatusPage.title); - }, - ); + test.describe("Header", () => { + test( + "has correct title + heading", + { + tag: "@smoke", + }, + async ({ adminReceiverStatusPage }) => { + await adminReceiverStatusPage.testHeader(); + }, + ); + }); test.describe("When there is no error", () => { test.describe("Displays correctly", () => { - test("header", async ({ adminReceiverStatusPage }) => { - await expect(adminReceiverStatusPage.heading).toBeVisible(); - }); - test.describe( "filters", { @@ -153,6 +150,14 @@ test.describe("Admin Receiver Status Page", () => { }); }); + test.describe("Footer", () => { + test("has footer and explicit scroll to footer and scroll to top", async ({ + adminReceiverStatusPage, + }) => { + await adminReceiverStatusPage.testFooter(); + }); + }); + test.describe("Functions correctly", () => { test.describe("filters", () => { test.describe( diff --git a/frontend-react/e2e/spec/all/daily-data-details-page.spec.ts b/frontend-react/e2e/spec/all/authenticated/daily-data-details-page.spec.ts similarity index 96% rename from frontend-react/e2e/spec/all/daily-data-details-page.spec.ts rename to frontend-react/e2e/spec/all/authenticated/daily-data-details-page.spec.ts index 0d8930c39a7..bf2dc36b129 100644 --- a/frontend-react/e2e/spec/all/daily-data-details-page.spec.ts +++ b/frontend-react/e2e/spec/all/authenticated/daily-data-details-page.spec.ts @@ -1,9 +1,9 @@ import { expect } from "@playwright/test"; -import { tableDataCellValue } from "../../helpers/utils"; -import { detailsTableHeaders } from "../../pages/daily-data"; -import { DailyDataDetailsPage } from "../../pages/daily-data-details"; -import * as reportDetails from "../../pages/report-details"; -import { test as baseTest } from "../../test"; +import { tableDataCellValue } from "../../../helpers/utils"; +import { detailsTableHeaders } from "../../../pages/authenticated/daily-data"; +import { DailyDataDetailsPage } from "../../../pages/authenticated/daily-data-details"; +import * as reportDetails from "../../../pages/authenticated/report-details"; +import { test as baseTest } from "../../../test"; export interface DailyDataDetailsPageFixtures { dailyDataDetailsPage: DailyDataDetailsPage; diff --git a/frontend-react/e2e/spec/all/daily-data-page-user-flow.spec.ts b/frontend-react/e2e/spec/all/authenticated/daily-data-page-user-flow.spec.ts similarity index 99% rename from frontend-react/e2e/spec/all/daily-data-page-user-flow.spec.ts rename to frontend-react/e2e/spec/all/authenticated/daily-data-page-user-flow.spec.ts index 03156ebc07f..891a2d47340 100644 --- a/frontend-react/e2e/spec/all/daily-data-page-user-flow.spec.ts +++ b/frontend-react/e2e/spec/all/authenticated/daily-data-page-user-flow.spec.ts @@ -11,7 +11,7 @@ import { TEST_ORG_ELIMS_RECEIVER_ELIMS, TEST_ORG_IGNORE, TEST_ORG_UP_RECEIVER_UP, -} from "../../helpers/utils"; +} from "../../../helpers/utils"; import { applyButton, DailyDataPage, @@ -28,9 +28,9 @@ import { setTime, startDate, startTime, -} from "../../pages/daily-data"; -import { URL_REPORT_DETAILS } from "../../pages/report-details"; -import { test as baseTest } from "../../test"; +} from "../../../pages/authenticated/daily-data.js"; +import { URL_REPORT_DETAILS } from "../../../pages/authenticated/report-details.js"; +import { test as baseTest } from "../../../test"; const defaultStartTime = "9:00am"; const defaultEndTime = "11:00pm"; diff --git a/frontend-react/e2e/spec/all/daily-data-page.spec.ts b/frontend-react/e2e/spec/all/authenticated/daily-data-page.spec.ts similarity index 99% rename from frontend-react/e2e/spec/all/daily-data-page.spec.ts rename to frontend-react/e2e/spec/all/authenticated/daily-data-page.spec.ts index 71a59e3f0d0..f6b4e05803d 100644 --- a/frontend-react/e2e/spec/all/daily-data-page.spec.ts +++ b/frontend-react/e2e/spec/all/authenticated/daily-data-page.spec.ts @@ -8,8 +8,8 @@ import { TEST_ORG_AK_RECEIVER, TEST_ORG_IGNORE, TEST_ORG_UP_RECEIVER_UP, -} from "../../helpers/utils"; -import * as dailyData from "../../pages/daily-data"; +} from "../../../helpers/utils"; +import * as dailyData from "../../../pages/authenticated/daily-data.js"; import { applyButton, DailyDataPage, @@ -28,8 +28,8 @@ import { startTime, startTimeClear, tableHeaders, -} from "../../pages/daily-data"; -import { test as baseTest } from "../../test"; +} from "../../../pages/authenticated/daily-data.js"; +import { test as baseTest } from "../../../test"; const defaultStartTime = "9:00am"; const defaultEndTime = "11:00pm"; diff --git a/frontend-react/e2e/spec/all/authenticated/last-mile-failures-page.spec.ts b/frontend-react/e2e/spec/all/authenticated/last-mile-failures-page.spec.ts new file mode 100644 index 00000000000..df8503376bb --- /dev/null +++ b/frontend-react/e2e/spec/all/authenticated/last-mile-failures-page.spec.ts @@ -0,0 +1,115 @@ +import { tableRows } from "../../../helpers/utils"; +import { LastMileFailuresPage } from "../../../pages/authenticated/last-mile-failures"; +import { test as baseTest, expect } from "../../../test"; + +export interface LastMileFailuresPageFixtures { + lastMileFailuresPage: LastMileFailuresPage; +} + +const test = baseTest.extend({ + lastMileFailuresPage: async ( + { + page: _page, + isMockDisabled, + adminLogin, + senderLogin, + receiverLogin, + storageState, + frontendWarningsLogPath, + isFrontendWarningsLog, + }, + use, + ) => { + const page = new LastMileFailuresPage({ + page: _page, + isMockDisabled, + adminLogin, + senderLogin, + receiverLogin, + storageState, + frontendWarningsLogPath, + isFrontendWarningsLog, + }); + await page.goto(); + await use(page); + }, +}); + +test.describe("Last Mile Failure page", () => { + test.describe("admin user - happy path", () => { + test.use({ storageState: "e2e/.auth/admin.json" }); + + test.describe("Header", () => { + test("has correct title + heading", async ({ lastMileFailuresPage }) => { + await lastMileFailuresPage.testHeader(); + }); + }); + + test("table has correct headers", async ({ lastMileFailuresPage }) => { + await expect(lastMileFailuresPage.page.locator(".column-header-text").nth(0)).toHaveText(/Failed At/); + await expect(lastMileFailuresPage.page.locator(".column-header-text").nth(1)).toHaveText(/ReportId/); + await expect(lastMileFailuresPage.page.locator(".column-header-text").nth(2)).toHaveText(/Receiver/); + }); + + test("table column 'Failed At' has expected data", async ({ lastMileFailuresPage }) => { + await expect(tableRows(lastMileFailuresPage.page).nth(0).locator("td").nth(0)).toHaveText( + "Tue, 2/20/2024, 9:35 PM", + ); + }); + + test("table column 'ReportId' will open a modal with report details", async ({ lastMileFailuresPage }) => { + const reportId = tableRows(lastMileFailuresPage.page).nth(0).locator("td").nth(1); + await expect(reportId).toContainText(/e5ce49c0-b230-4364-8230-964273249fa1/); + await reportId.click(); + + const modal = lastMileFailuresPage.page.getByTestId("modalWindow").nth(0); + await expect(modal).toContainText(/Report ID:e5ce49c0-b230-4364-8230-964273249fa1/); + }); + + test("table column 'Receiver' will open receiver edit page", async ({ lastMileFailuresPage }) => { + const receiver = tableRows(lastMileFailuresPage.page).nth(0).locator("td").nth(2); + await expect(receiver).toContainText(/flexion.etor-service-receiver-results/); + await receiver.click(); + + await expect(lastMileFailuresPage.page).toHaveURL( + "/admin/orgreceiversettings/org/flexion/receiver/etor-service-receiver-results/action/edit", + ); + }); + }); + + test.describe("admin user - server error", () => { + test.use({ storageState: "e2e/.auth/admin.json" }); + + test("has alert", async ({ lastMileFailuresPage }) => { + lastMileFailuresPage.mockError = true; + await lastMileFailuresPage.reload(); + + await expect(lastMileFailuresPage.page.getByTestId("alert")).toBeAttached(); + await expect( + lastMileFailuresPage.page.getByText(/Our apologies, there was an error loading this content./), + ).toBeAttached(); + }); + }); + + test.describe("receiver user", () => { + test.use({ storageState: "e2e/.auth/receiver.json" }); + + test("returns Page Not Found", async ({ lastMileFailuresPage }) => { + await expect(lastMileFailuresPage.page).toHaveTitle(/Page Not Found/); + }); + }); + + test.describe("sender user", () => { + test.use({ storageState: "e2e/.auth/sender.json" }); + + test("returns Page Not Found", async ({ lastMileFailuresPage }) => { + await expect(lastMileFailuresPage.page).toHaveTitle(/Page Not Found/); + }); + }); + + test.describe("Footer", () => { + test("has footer and explicit scroll to footer and scroll to top", async ({ lastMileFailuresPage }) => { + await lastMileFailuresPage.testFooter(); + }); + }); +}); diff --git a/frontend-react/e2e/spec/all/authenticated/message-details-page.spec.ts b/frontend-react/e2e/spec/all/authenticated/message-details-page.spec.ts new file mode 100644 index 00000000000..916b14b1096 --- /dev/null +++ b/frontend-react/e2e/spec/all/authenticated/message-details-page.spec.ts @@ -0,0 +1,187 @@ +import fs from "node:fs"; +import { parseFileLocation } from "../../../../src/utils/misc"; +import { tableRows } from "../../../helpers/utils"; +import { MOCK_GET_MESSAGE } from "../../../mocks/messages"; +import { MessageDetailsPage } from "../../../pages/authenticated/message-details"; +import { MessageIDSearchPage } from "../../../pages/authenticated/message-id-search"; +import { mockGetHistoryReportResponse } from "../../../pages/authenticated/report-details"; + +import { test as baseTest, expect } from "../../../test"; + +export interface MessageDetailsPageFixtures { + messageDetailsPage: MessageDetailsPage; +} + +const test = baseTest.extend({ + messageDetailsPage: async ( + { + page: _page, + isMockDisabled, + adminLogin, + senderLogin, + receiverLogin, + storageState, + frontendWarningsLogPath, + isFrontendWarningsLog, + }, + use, + ) => { + const page = new MessageDetailsPage({ + page: _page, + isMockDisabled, + adminLogin, + senderLogin, + receiverLogin, + storageState, + frontendWarningsLogPath, + isFrontendWarningsLog, + }); + await page.goto(); + await use(page); + }, +}); + +test.describe("Message Details Page", () => { + test.describe("not authenticated", () => { + test("redirects to login", async ({ messageDetailsPage }) => { + await expect(messageDetailsPage.page).toHaveURL("/login"); + }); + }); + + test.describe("authenticated admin", () => { + test.use({ storageState: "e2e/.auth/admin.json" }); + + test.describe("Header", () => { + test("has correct title + heading", async ({ messageDetailsPage }) => { + await messageDetailsPage.testHeader(); + }); + }); + + test("has message id section", async ({ messageDetailsPage }) => { + await expect(messageDetailsPage.page.getByText("Message ID", { exact: true })).toBeVisible(); + await expect(messageDetailsPage.page.getByText(MessageIDSearchPage.MESSAGE_ID)).toBeVisible(); + }); + + test("has sender section", async ({ messageDetailsPage }) => { + const { sender, reportId, submittedDate } = MOCK_GET_MESSAGE; + + await expect(messageDetailsPage.page.getByText("Sender:")).toBeVisible(); + await expect(messageDetailsPage.page.getByText(sender)).toBeVisible(); + await expect(messageDetailsPage.page.getByText("Incoming Report ID")).toBeVisible(); + await expect(messageDetailsPage.page.getByText(reportId, { exact: true })).toBeVisible(); + await expect(messageDetailsPage.page.getByText("Date/Time Submitted")).toBeVisible(); + await expect(messageDetailsPage.page.getByText(new Date(submittedDate).toLocaleString())).toBeVisible(); + await expect(messageDetailsPage.page.getByText("File Location")).toBeVisible(); + await expect(messageDetailsPage.page.getByText("RECEIVE", { exact: true })).toBeVisible(); + await expect(messageDetailsPage.page.getByText("ignore.ignore-simple-report")).toBeVisible(); + await expect(messageDetailsPage.page.getByText("Incoming File Name")).toBeVisible(); + await expect( + messageDetailsPage.page.getByText( + "pdi-covid-19-d9a57df0-2702-4e28-9d80-ff8c9ec51816-20240514142655.csv", + ), + ).toBeVisible(); + }); + + test.describe("authenticated admin", () => { + test("displays expected table headers and data", async ({ messageDetailsPage }) => { + // include header row + const rowCount = MOCK_GET_MESSAGE.receiverData.length + 1; + const table = messageDetailsPage.page.getByRole("table"); + await expect(table).toBeVisible(); + const rows = await table.getByRole("row").all(); + expect(rows).toHaveLength(rowCount); + + const colHeaders = [ + "Name", + "Service", + "Date", + "Report Id", + "Main", + "Sub", + "File Name", + "Transport Results", + ]; + for (const [i, row] of rows.entries()) { + const cols = await row.getByRole("cell").allTextContents(); + expect(cols).toHaveLength(colHeaders.length); + + const { receivingOrg, receivingOrgSvc, createdAt, reportId, fileUrl, transportResult } = + i === 0 + ? MOCK_GET_MESSAGE.receiverData[0] + : (MOCK_GET_MESSAGE.receiverData.find((i) => i.reportId === cols[3]) ?? { + reportId: "INVALID", + }); + + // if first row, we expect column headers. else, the data row matching the report id + const expectedColContents = + i === 0 + ? colHeaders + : [ + receivingOrg ?? "", + receivingOrgSvc ?? "", + createdAt ? new Date(createdAt).toLocaleString() : "", + reportId, + parseFileLocation(fileUrl ?? "N/A").folderLocation.toLocaleUpperCase(), + parseFileLocation(fileUrl ?? "N/A").sendingOrg, + parseFileLocation(fileUrl ?? "N/A").fileName, + transportResult ?? "", + ]; + + for (const [i, col] of cols.entries()) { + expect(col).toBe(expectedColContents[i]); + } + } + }); + + test("table column 'FileName' will download file", async ({ messageDetailsPage }) => { + const downloadProm = messageDetailsPage.page.waitForEvent("download"); + await mockGetHistoryReportResponse(messageDetailsPage.page, "*"); + + await tableRows(messageDetailsPage.page).nth(0).locator("td").nth(6).getByRole("button").click(); + + const download = await downloadProm; + + // assert filename + expect(download.suggestedFilename()).toBe( + "hhsprotect-covid-19-73e3cbc8-9920-4ab7-871f-843a1db4c074.csv", + ); + // get and assert stats + expect((await fs.promises.stat(await download.path())).size).toBeGreaterThan(200); + }); + }); + + test.describe("Footer", () => { + test("has footer and explicit scroll to footer and scroll to top", async ({ messageDetailsPage }) => { + await messageDetailsPage.testFooter(); + }); + }); + }); + + test.describe("receiver user", () => { + test.use({ storageState: "e2e/.auth/receiver.json" }); + + test("has alert", async ({ messageDetailsPage }) => { + messageDetailsPage.mockError = true; + await messageDetailsPage.reload(); + + await expect(messageDetailsPage.page.getByTestId("alert")).toBeAttached(); + await expect( + messageDetailsPage.page.getByText(/Our apologies, there was an error loading this content./), + ).toBeAttached(); + }); + }); + + test.describe("sender user", () => { + test.use({ storageState: "e2e/.auth/sender.json" }); + + test("has alert", async ({ messageDetailsPage }) => { + messageDetailsPage.mockError = true; + await messageDetailsPage.reload(); + + await expect(messageDetailsPage.page.getByTestId("alert")).toBeAttached(); + await expect( + messageDetailsPage.page.getByText(/Our apologies, there was an error loading this content./), + ).toBeAttached(); + }); + }); +}); diff --git a/frontend-react/e2e/spec/all/authenticated/message-id-search-page.spec.ts b/frontend-react/e2e/spec/all/authenticated/message-id-search-page.spec.ts new file mode 100644 index 00000000000..9de8e807739 --- /dev/null +++ b/frontend-react/e2e/spec/all/authenticated/message-id-search-page.spec.ts @@ -0,0 +1,177 @@ +import { noData, tableRows } from "../../../helpers/utils"; +import { MOCK_GET_MESSAGES } from "../../../mocks/messages"; +import { MessageIDSearchPage } from "../../../pages/authenticated/message-id-search"; +import { openReportIdDetailPage } from "../../../pages/authenticated/submission-history"; + +import { test as baseTest, expect } from "../../../test"; + +export interface MessageIDSearchPageFixtures { + messageIDSearchPage: MessageIDSearchPage; +} + +const test = baseTest.extend({ + messageIDSearchPage: async ( + { + page: _page, + isMockDisabled, + adminLogin, + senderLogin, + receiverLogin, + storageState, + frontendWarningsLogPath, + isFrontendWarningsLog, + }, + use, + ) => { + const page = new MessageIDSearchPage({ + page: _page, + isMockDisabled, + adminLogin, + senderLogin, + receiverLogin, + storageState, + frontendWarningsLogPath, + isFrontendWarningsLog, + }); + await page.goto(); + await use(page); + }, +}); + +test.describe("Message ID Search Page", () => { + test.describe("not authenticated", () => { + test("redirects to login", async ({ messageIDSearchPage }) => { + await expect(messageIDSearchPage.page).toHaveURL("/login"); + }); + }); + + test.describe("authenticated admin", () => { + test.use({ storageState: "e2e/.auth/admin.json" }); + test.beforeEach(async ({ messageIDSearchPage }) => { + await messageIDSearchPage.page.locator("#search-field").fill(MessageIDSearchPage.MESSAGE_ID); + await messageIDSearchPage.page + .getByRole("button", { + name: "Search", + }) + .click(); + }); + + test.describe("on search with results", () => { + test.describe("Header", () => { + test("has correct title + heading", async ({ messageIDSearchPage }) => { + await messageIDSearchPage.testHeader(); + }); + }); + + test("displays expected table headers and data", async ({ messageIDSearchPage }) => { + // include header row + const rowCount = MOCK_GET_MESSAGES.length + 1; + const table = messageIDSearchPage.page.getByRole("table"); + await expect(table).toBeVisible(); + const rows = await table.getByRole("row").all(); + expect(rows).toHaveLength(rowCount); + + const colHeaders = ["Message ID", "Sender", "Date/time submitted", "Incoming Report Id"]; + for (const [i, row] of rows.entries()) { + const cols = await row.getByRole("cell").allTextContents(); + expect(cols).toHaveLength(colHeaders.length); + + const { messageId, sender, submittedDate, reportId } = + i === 0 + ? MOCK_GET_MESSAGES[0] + : (MOCK_GET_MESSAGES.find((i) => i.reportId === cols[3]) ?? { reportId: "INVALID" }); + // if first row, we expect column headers. else, the data row matching the report id + const expectedColContents = + i === 0 + ? colHeaders + : [ + messageId, + sender ?? "", + submittedDate ? new Date(submittedDate).toLocaleString() : "", + reportId ?? "", + ]; + + for (const [i, col] of cols.entries()) { + expect(col).toBe(expectedColContents[i]); + } + } + }); + + test("table column 'Message ID' will open message id details", async ({ messageIDSearchPage }) => { + const messageIdCell = tableRows(messageIDSearchPage.page) + .nth(0) + .locator("td") + .nth(0) + .getByRole("link", { name: MessageIDSearchPage.MESSAGE_ID }); + await messageIdCell.click(); + await expect(messageIDSearchPage.page).toHaveURL("/message-details/0"); + expect(messageIDSearchPage.page.locator("h1").getByText(MessageIDSearchPage.MESSAGE_ID)).toBeTruthy(); + }); + + test("table column 'Incoming Report Id' will open report id details", async ({ messageIDSearchPage }) => { + const reportId = "73e3cbc8-9920-4ab7-871f-843a1db4c074"; + const reportIdCell = tableRows(messageIDSearchPage.page).nth(0).locator("td").nth(3).getByRole("link", { + name: reportId, + }); + await reportIdCell.click(); + await openReportIdDetailPage(messageIDSearchPage.page, reportId); + }); + }); + + test.describe("on search without results", () => { + test.beforeEach(async ({ messageIDSearchPage }) => { + await messageIDSearchPage.page.route(MessageIDSearchPage.API_MESSAGES, (route) => + route.fulfill({ + status: 200, + json: [], + }), + ); + await messageIDSearchPage.page.goto(MessageIDSearchPage.URL_MESSAGE_ID_SEARCH); + + await messageIDSearchPage.page.locator("#search-field").fill(MessageIDSearchPage.MESSAGE_ID); + await messageIDSearchPage.page + .getByRole("button", { + name: "Search", + }) + .click(); + }); + + test("has correct title", async ({ page }) => { + await expect(page).toHaveURL(MessageIDSearchPage.URL_MESSAGE_ID_SEARCH); + await expect(page).toHaveTitle(/Message ID search - Admin/); + }); + + test("shows no data", async ({ page }) => { + await expect(noData(page)).toBeAttached(); + }); + }); + + test.describe("Footer", () => { + test("has footer and explicit scroll to footer and scroll to top", async ({ messageIDSearchPage }) => { + await messageIDSearchPage.testFooter(); + }); + }); + }); + + test.describe("receiver user", () => { + test.use({ storageState: "e2e/.auth/receiver.json" }); + + test("has alert", async ({ messageIDSearchPage }) => { + messageIDSearchPage.mockError = true; + await messageIDSearchPage.reload(); + + await expect(messageIDSearchPage.page).toHaveTitle(/Page Not Found/); + }); + }); + + test.describe("sender user", () => { + test.use({ storageState: "e2e/.auth/sender.json" }); + + test("has alert", async ({ messageIDSearchPage }) => { + messageIDSearchPage.mockError = true; + await messageIDSearchPage.reload(); + + await expect(messageIDSearchPage.page).toHaveTitle(/Page Not Found/); + }); + }); +}); diff --git a/frontend-react/e2e/spec/all/organization-settings-page.spec.ts b/frontend-react/e2e/spec/all/authenticated/organization-settings-page.spec.ts similarity index 93% rename from frontend-react/e2e/spec/all/organization-settings-page.spec.ts rename to frontend-react/e2e/spec/all/authenticated/organization-settings-page.spec.ts index e8906c66fe2..83ce97b9e2a 100644 --- a/frontend-react/e2e/spec/all/organization-settings-page.spec.ts +++ b/frontend-react/e2e/spec/all/authenticated/organization-settings-page.spec.ts @@ -2,9 +2,9 @@ import { expect } from "@playwright/test"; import { readFileSync } from "node:fs"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; -import { MOCK_GET_ORGANIZATION_SETTINGS_LIST } from "../../mocks/organizations"; -import { OrganizationPage } from "../../pages/organization"; -import { test as baseTest } from "../../test"; +import { MOCK_GET_ORGANIZATION_SETTINGS_LIST } from "../../../mocks/organizations"; +import { OrganizationPage } from "../../../pages/authenticated/organization"; +import { test as baseTest } from "../../../test"; const __dirname = fileURLToPath(import.meta.url); @@ -65,6 +65,12 @@ test.describe("Admin Organization Settings Page", () => { test.describe("authenticated admin", () => { test.use({ storageState: "e2e/.auth/admin.json" }); + test.describe("Header", () => { + test("has correct title + heading", async ({ organizationPage }) => { + await organizationPage.testHeader(); + }); + }); + test("If there is an error, the error is shown on the page", async ({ organizationPage }) => { organizationPage.mockError = true; await organizationPage.reload(); @@ -159,7 +165,7 @@ test.describe("Admin Organization Settings Page", () => { await saveButton.click(); const download = await downloadProm; - const expectedFile = readFileSync(join(__dirname, "../../../mocks/prime-orgs.csv"), { + const expectedFile = readFileSync(join(__dirname, "../../../../mocks/prime-orgs.csv"), { encoding: "utf-8", }); const stream = await download.createReadStream(); @@ -229,4 +235,10 @@ test.describe("Admin Organization Settings Page", () => { }); }); }); + + test.describe("Footer", () => { + test("has footer and explicit scroll to footer and scroll to top", async ({ organizationPage }) => { + await organizationPage.testFooter(); + }); + }); }); diff --git a/frontend-react/e2e/spec/all/authenticated/submission-history-page-user-flow.spec.ts b/frontend-react/e2e/spec/all/authenticated/submission-history-page-user-flow.spec.ts new file mode 100644 index 00000000000..613836d3560 --- /dev/null +++ b/frontend-react/e2e/spec/all/authenticated/submission-history-page-user-flow.spec.ts @@ -0,0 +1,157 @@ +import { expect } from "@playwright/test"; + +import { + FALLBACK_FROM_DATE_STRING, + FALLBACK_TO_DATE_STRING, +} from "../../../../src/hooks/filters/UseDateRange/UseDateRange"; +import { tableColumnDateTimeInRange, tableDataCellValue, TEST_ORG_IGNORE } from "../../../helpers/utils"; +import { endDate, setDate, startDate } from "../../../pages/authenticated/daily-data"; +import * as submissionHistory from "../../../pages/authenticated/submission-history"; +import { openReportIdDetailPage, SubmissionHistoryPage } from "../../../pages/authenticated/submission-history"; +import { test as baseTest } from "../../../test"; + +export interface SubmissionHistoryPageFixtures { + submissionHistoryPage: SubmissionHistoryPage; +} + +const test = baseTest.extend({ + submissionHistoryPage: async ( + { + page: _page, + isMockDisabled, + adminLogin, + senderLogin, + receiverLogin, + storageState, + frontendWarningsLogPath, + isFrontendWarningsLog, + }, + use, + ) => { + const page = new SubmissionHistoryPage({ + page: _page, + isMockDisabled, + adminLogin, + senderLogin, + receiverLogin, + storageState, + frontendWarningsLogPath, + isFrontendWarningsLog, + isTestOrg: true, + }); + await page.goto(); + await use(page); + }, +}); + +test.describe( + "Submission history page - user flow smoke tests", + { + tag: "@smoke", + }, + () => { + test.describe("admin user", () => { + test.use({ storageState: "e2e/.auth/admin.json" }); + + test.beforeAll(({ browserName }) => { + test.skip(browserName !== "chromium"); + }); + + test.describe(`${TEST_ORG_IGNORE} org`, () => { + test("nav contains the 'Submission History' option", async ({ submissionHistoryPage }) => { + const navItems = submissionHistoryPage.page.locator(".usa-nav li"); + await expect(navItems).toContainText(["Submission History"]); + }); + + test("has correct title", async ({ submissionHistoryPage }) => { + await expect(submissionHistoryPage.page).toHaveTitle(submissionHistoryPage.title); + await expect(submissionHistoryPage.heading).toBeVisible(); + }); + + test("has filter", async ({ submissionHistoryPage }) => { + await expect(submissionHistoryPage.page.getByTestId("filter-container")).toBeAttached(); + }); + + test("has footer", async ({ submissionHistoryPage }) => { + await expect(submissionHistoryPage.footer).toBeAttached(); + }); + + test.describe("table", () => { + test.beforeEach(async ({ submissionHistoryPage }) => { + await submissionHistoryPage.page.locator(".usa-table tbody").waitFor({ state: "visible" }); + }); + + test("table has correct headers", async ({ submissionHistoryPage }) => { + await submissionHistory.tableHeaders(submissionHistoryPage.page); + }); + + test("table column 'ReportId' will open the report details", async ({ submissionHistoryPage }) => { + const reportId = await tableDataCellValue(submissionHistoryPage.page, 0, 0); + + await submissionHistoryPage.page.getByRole("link", { name: reportId }).click(); + const responsePromise = await submissionHistoryPage.page.waitForResponse( + (res) => res.status() === 200 && res.url().includes("/history"), + ); + + if (responsePromise) { + await openReportIdDetailPage(submissionHistoryPage.page, reportId); + } else { + console.error("Request not received within the timeout period"); + } + }); + + test("table has pagination", async ({ submissionHistoryPage }) => { + await expect(submissionHistoryPage.page.getByTestId("Submissions pagination")).toBeAttached(); + }); + }); + + test.describe("filter", () => { + test.describe("on 'onLoad'", () => { + test("'From' date has a default value", async ({ submissionHistoryPage }) => { + await expect(startDate(submissionHistoryPage.page)).toBeAttached(); + await expect(startDate(submissionHistoryPage.page)).toHaveValue(FALLBACK_FROM_DATE_STRING); + }); + + test("'To' date has a default value", async ({ submissionHistoryPage }) => { + await expect(endDate(submissionHistoryPage.page)).toBeAttached(); + await expect(endDate(submissionHistoryPage.page)).toHaveValue(FALLBACK_TO_DATE_STRING); + }); + }); + + test.describe("on 'Filter'", () => { + test("with 'From' date, 'To' date", async ({ submissionHistoryPage }) => { + const fromDate = await setDate(submissionHistoryPage.page, "#start-date", 180); + const toDate = await setDate(submissionHistoryPage.page, "#end-date", 0); + + // Apply button is enabled + await submissionHistoryPage.filterButton.click(); + await submissionHistoryPage.page.locator(".usa-table tbody").waitFor({ state: "visible" }); + + // Check that table data contains the dates/times that were selected + const areDatesInRange = await tableColumnDateTimeInRange( + submissionHistoryPage.page, + 1, + fromDate, + toDate, + ); + expect(areDatesInRange).toBe(true); + }); + + test("on 'clear' resets the dates", async ({ submissionHistoryPage }) => { + await expect(startDate(submissionHistoryPage.page)).toHaveValue(FALLBACK_FROM_DATE_STRING); + await expect(endDate(submissionHistoryPage.page)).toHaveValue(FALLBACK_TO_DATE_STRING); + + await setDate(submissionHistoryPage.page, "#start-date", 14); + await setDate(submissionHistoryPage.page, "#end-date", 14); + + await submissionHistoryPage.clearButton.click(); + + await expect(startDate(submissionHistoryPage.page)).toHaveValue(FALLBACK_FROM_DATE_STRING); + await expect(endDate(submissionHistoryPage.page)).toHaveValue(FALLBACK_TO_DATE_STRING); + }); + }); + }); + }); + }); + }, +); diff --git a/frontend-react/e2e/spec/all/authenticated/submission-history-page.spec.ts b/frontend-react/e2e/spec/all/authenticated/submission-history-page.spec.ts new file mode 100644 index 00000000000..eeb9a260ffd --- /dev/null +++ b/frontend-react/e2e/spec/all/authenticated/submission-history-page.spec.ts @@ -0,0 +1,194 @@ +import { expect } from "@playwright/test"; + +import { noData, tableDataCellValue, tableRows } from "../../../helpers/utils"; +import * as submissionHistory from "../../../pages/authenticated/submission-history"; +import { id, openReportIdDetailPage, SubmissionHistoryPage } from "../../../pages/authenticated/submission-history"; +import { test as baseTest, logins } from "../../../test"; + +export interface SubmissionHistoryPageFixtures { + submissionHistoryPage: SubmissionHistoryPage; +} + +const test = baseTest.extend({ + submissionHistoryPage: async ( + { + page: _page, + isMockDisabled, + adminLogin, + senderLogin, + receiverLogin, + storageState, + frontendWarningsLogPath, + isFrontendWarningsLog, + }, + use, + ) => { + const page = new SubmissionHistoryPage({ + page: _page, + isMockDisabled, + adminLogin, + senderLogin, + receiverLogin, + storageState, + frontendWarningsLogPath, + isFrontendWarningsLog, + isTestOrg: true, + }); + await page.goto(); + await use(page); + }, +}); + +test.describe("Submission history page", () => { + test.describe("not authenticated", () => { + test("redirects to login", async ({ submissionHistoryPage }) => { + await expect(submissionHistoryPage.page).toHaveURL("/login"); + }); + }); + + test.describe("admin user", () => { + test.use({ storageState: "e2e/.auth/admin.json" }); + + test.describe("without org selected", () => { + test.beforeEach(async ({ page }) => { + await submissionHistory.goto(page); + }); + + test("will not load page", async ({ page }) => { + await expect(page.getByText("Cannot fetch Organization data as admin")).toBeVisible(); + }); + + test("has footer", async ({ submissionHistoryPage }) => { + await expect(submissionHistoryPage.footer).toBeAttached(); + }); + }); + + test.describe("with org selected", () => { + test("nav contains the 'Submission History' option", async ({ submissionHistoryPage }) => { + const navItems = submissionHistoryPage.page.locator(".usa-nav li"); + await expect(navItems).toContainText(["Submission History"]); + }); + + test("has correct title", async ({ submissionHistoryPage }) => { + await expect(submissionHistoryPage.page).toHaveTitle(/Submission history/); + }); + + test("has filter", async ({ submissionHistoryPage }) => { + await expect(submissionHistoryPage.page.getByTestId("filter-container")).toBeAttached(); + }); + + test.describe("table", () => { + test("table has correct headers", async ({ submissionHistoryPage }) => { + await submissionHistory.tableHeaders(submissionHistoryPage.page); + }); + + test("table column 'ReportId' will open the report details", async ({ submissionHistoryPage }) => { + const reportId = tableRows(submissionHistoryPage.page).nth(0).locator("td").nth(0); + await expect(reportId).toContainText(id); + await reportId.getByRole("link", { name: id }).click(); + + await openReportIdDetailPage(submissionHistoryPage.page, id); + }); + + test("table column 'Date/time submitted' has expected data", async ({ submissionHistoryPage }) => { + expect(await tableDataCellValue(submissionHistoryPage.page, 0, 1)).toEqual("3/7/2024, 6:00:22 PM"); + }); + + test("table column 'File' has expected data", async ({ submissionHistoryPage }) => { + expect(await tableDataCellValue(submissionHistoryPage.page, 0, 2)).toEqual("myfile.hl7"); + expect(await tableDataCellValue(submissionHistoryPage.page, 1, 2)).toEqual( + "None-03c3b7ab-7c65-4174-bea7-9195cbb7ed01-20240314174050.hl7", + ); + }); + + test("table column 'Records' has expected data", async ({ submissionHistoryPage }) => { + expect(await tableDataCellValue(submissionHistoryPage.page, 0, 3)).toEqual("1"); + }); + + test("table column 'Status' has expected data", async ({ submissionHistoryPage }) => { + expect(await tableDataCellValue(submissionHistoryPage.page, 0, 4)).toEqual("Success"); + }); + + test("table has pagination", async ({ submissionHistoryPage }) => { + await expect(submissionHistoryPage.page.getByTestId("Submissions pagination")).toBeAttached(); + }); + }); + + test("has footer", async ({ submissionHistoryPage }) => { + await expect(submissionHistoryPage.footer).toBeAttached(); + }); + }); + }); + + test.describe("receiver user", () => { + test.use({ storageState: logins.receiver.path }); + + test("nav does not contain the Submissions option", async ({ submissionHistoryPage }) => { + const navItems = submissionHistoryPage.page.locator(".usa-nav li"); + await expect(navItems).not.toContainText(["Submissions"]); + }); + + test("displays no data message", async ({ submissionHistoryPage }) => { + await expect(noData(submissionHistoryPage.page)).toBeAttached(); + }); + + test("has correct title", async ({ submissionHistoryPage }) => { + await expect(submissionHistoryPage.page).toHaveTitle(/Submission history/); + }); + + test("has footer", async ({ submissionHistoryPage }) => { + await expect(submissionHistoryPage.footer).toBeAttached(); + }); + }); + + test.describe("sender user", () => { + test.use({ storageState: logins.sender.path }); + + test("nav contains the Submission History option", async ({ submissionHistoryPage }) => { + const navItems = submissionHistoryPage.page.locator(".usa-nav li"); + await expect(navItems).toContainText(["Submission History"]); + }); + + test("has correct title", async ({ submissionHistoryPage }) => { + await expect(submissionHistoryPage.page).toHaveTitle(/Submission history/); + }); + + test("has filter", async ({ submissionHistoryPage }) => { + await expect(submissionHistoryPage.page.getByTestId("filter-container")).toBeAttached(); + }); + + test.describe("table", () => { + test("table has correct headers", async ({ submissionHistoryPage }) => { + await submissionHistory.tableHeaders(submissionHistoryPage.page); + }); + + test("table column 'ReportId' will open the report details", async ({ submissionHistoryPage }) => { + const reportId = tableRows(submissionHistoryPage.page).nth(0).locator("td").nth(0); + await expect(reportId).toContainText(id); + await reportId.getByRole("link", { name: id }).click(); + + await openReportIdDetailPage(submissionHistoryPage.page, id); + }); + + test("table column 'Date/time submitted' has expected data", async ({ submissionHistoryPage }) => { + expect(await tableDataCellValue(submissionHistoryPage.page, 0, 1)).toEqual("3/7/2024, 6:00:22 PM"); + }); + + test("table column 'Records' has expected data", async ({ submissionHistoryPage }) => { + expect(await tableDataCellValue(submissionHistoryPage.page, 0, 3)).toEqual("1"); + }); + + test("table column 'Status' has expected data", async ({ submissionHistoryPage }) => { + expect(await tableDataCellValue(submissionHistoryPage.page, 0, 4)).toEqual("Success"); + }); + + test("table has pagination", async ({ submissionHistoryPage }) => { + await expect(submissionHistoryPage.page.getByTestId("Submissions pagination")).toBeAttached(); + }); + }); + + test("has footer", async ({ submissionHistoryPage }) => { + await expect(submissionHistoryPage.footer).toBeAttached(); + }); + }); +}); diff --git a/frontend-react/e2e/spec/all/authenticated/submissions-details-page.spec.ts b/frontend-react/e2e/spec/all/authenticated/submissions-details-page.spec.ts new file mode 100644 index 00000000000..1a543f2f238 --- /dev/null +++ b/frontend-react/e2e/spec/all/authenticated/submissions-details-page.spec.ts @@ -0,0 +1,143 @@ +import { expect } from "@playwright/test"; +import * as submissionDetails from "../../../pages/authenticated/submission-history"; +import { URL_SUBMISSION_HISTORY } from "../../../pages/authenticated/submission-history"; +import { id, SubmissionsDetailsPage } from "../../../pages/authenticated/submissions-details"; +import { test as baseTest, logins } from "../../../test"; + +export interface SubmissionsDetailsPageFixtures { + submissionsDetailsPage: SubmissionsDetailsPage; +} + +const test = baseTest.extend({ + submissionsDetailsPage: async ( + { + page: _page, + isMockDisabled, + adminLogin, + senderLogin, + receiverLogin, + storageState, + frontendWarningsLogPath, + isFrontendWarningsLog, + }, + use, + ) => { + const page = new SubmissionsDetailsPage({ + page: _page, + isMockDisabled, + adminLogin, + senderLogin, + receiverLogin, + storageState, + frontendWarningsLogPath, + isFrontendWarningsLog, + isTestOrg: true, + }); + await page.goto(); + await use(page); + }, +}); + +test.describe("Submissions Details page", () => { + test.describe("not authenticated", () => { + test("redirects to login", async ({ submissionsDetailsPage }) => { + await expect(submissionsDetailsPage.page).toHaveURL("/login"); + }); + }); + + test.describe("admin user - happy path", () => { + test.use({ storageState: logins.admin.path }); + + test.describe("without org selected", () => { + test("has correct title", async ({ submissionsDetailsPage }) => { + await expect(submissionsDetailsPage.page).toHaveTitle(submissionsDetailsPage.title); + }); + + test("has reportId in breadcrumb", async ({ submissionsDetailsPage }) => { + await expect(submissionsDetailsPage.page.locator(".usa-breadcrumb ol li").nth(1)).toHaveText( + `Details: ${id}`, + ); + }); + + test("has footer", async ({ submissionsDetailsPage }) => { + await expect(submissionsDetailsPage.page.locator("footer")).toBeAttached(); + }); + }); + + test.describe("with org selected", () => { + test("breadcrumb navigates to Submission History page", async ({ submissionsDetailsPage }) => { + await submissionDetails.breadcrumbLink( + submissionsDetailsPage.page, + 0, + "Submissions", + URL_SUBMISSION_HISTORY, + ); + }); + + test("has footer", async ({ submissionsDetailsPage }) => { + await expect(submissionsDetailsPage.page.locator("footer")).toBeAttached(); + }); + }); + }); + + test.describe("admin user - server error", () => { + test.use({ storageState: logins.admin.path }); + + test("error is shown on the page", async ({ submissionsDetailsPage }) => { + submissionsDetailsPage.mockError = true; + await submissionsDetailsPage.reload(); + + await expect(submissionsDetailsPage.page.getByText(/An error has occurred./)).toBeAttached(); + await expect(submissionsDetailsPage.page.locator("footer")).toBeAttached(); + }); + }); + + test.describe("sender user - happy path", () => { + test.use({ storageState: logins.sender.path }); + + test("has correct title", async ({ submissionsDetailsPage }) => { + await expect(submissionsDetailsPage.page).toHaveTitle(submissionsDetailsPage.title); + }); + + test("has reportId in breadcrumb", async ({ submissionsDetailsPage }) => { + await expect(submissionsDetailsPage.page.locator(".usa-breadcrumb ol li").nth(1)).toHaveText( + `Details: ${id}`, + ); + }); + + test("breadcrumb navigates to Submission History page", async ({ submissionsDetailsPage }) => { + await submissionDetails.breadcrumbLink( + submissionsDetailsPage.page, + 0, + "Submissions", + URL_SUBMISSION_HISTORY, + ); + }); + + test("has footer", async ({ submissionsDetailsPage }) => { + await expect(submissionsDetailsPage.page.locator("footer")).toBeAttached(); + }); + }); + + test.describe("sender user - server error", () => { + test.use({ storageState: logins.sender.path }); + + test("has error message", async ({ submissionsDetailsPage }) => { + submissionsDetailsPage.mockError = true; + await submissionsDetailsPage.reload(); + await expect(submissionsDetailsPage.page.getByText(/An error has occurred./)).toBeAttached(); + await expect(submissionsDetailsPage.page.locator("footer")).toBeAttached(); + }); + }); + + test.describe("receiver user", () => { + test.use({ storageState: logins.receiver.path }); + + test("has error message", async ({ submissionsDetailsPage }) => { + submissionsDetailsPage.mockError = true; + await submissionsDetailsPage.reload(); + await expect(submissionsDetailsPage.page.getByText(/An error has occurred./)).toBeAttached(); + await expect(submissionsDetailsPage.page.locator("footer")).toBeAttached(); + }); + }); +}); diff --git a/frontend-react/e2e/spec/all/homepage.spec.ts b/frontend-react/e2e/spec/all/homepage.spec.ts deleted file mode 100644 index 6d1b4bb9d08..00000000000 --- a/frontend-react/e2e/spec/all/homepage.spec.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { expect, test } from "@playwright/test"; - -import { scrollToFooter, scrollToTop } from "../../helpers/utils"; -import * as header from "../../pages/header"; -import * as homepage from "../../pages/homepage"; -import * as managingYourConnection from "../../pages/managing-your-connection"; -import * as ourNetwork from "../../pages/our-network"; -import * as security from "../../pages/security"; - -test.describe( - "Homepage", - { - tag: "@smoke", - }, - () => { - test.beforeEach(async ({ page }) => { - await homepage.goto(page); - }); - - test("has correct title", async ({ page }) => { - await expect(page).toHaveTitle( - /ReportStream - CDC's free, interoperable data transfer platform/, - ); - }); - - test("opens the Security page on 'security of your data' click", async ({ - page, - }) => { - await page - .getByRole("link", { name: "security of your data" }) - .click(); - await security.onLoad(page); - // Go back to the homepage - await header.clickOnHome(page); - - expect(true).toBe(true); - }); - - test("opens the managing-your-connection page on 'our tools' click", async ({ - page, - }) => { - await page.getByRole("link", { name: "our tools" }).click(); - await managingYourConnection.onLoad(page); - // Go back to the homepage - await header.clickOnHome(page); - - expect(true).toBe(true); - }); - - test("opens Our Network page on 'See our full network' click", async ({ - page, - }) => { - await page - .getByRole("link", { name: "See our full network" }) - .click(); - await ourNetwork.onLoad(page); - // Go back to the homepage - await header.clickOnHome(page); - - expect(true).toBe(true); - }); - - test("is clickable Where were live map", async ({ page }) => { - // Trigger map click and go to our network page - await ourNetwork.clickOnLiveMap(page); - // Go back to the homepage - await header.clickOnHome(page); - - expect(true).toBe(true); - }); - - test("explicit scroll to footer and then scroll to top", async ({ - page, - }) => { - await expect(page.locator("footer")).not.toBeInViewport(); - await scrollToFooter(page); - await expect(page.locator("footer")).toBeInViewport(); - await expect(page.getByTestId("govBanner")).not.toBeInViewport(); - await scrollToTop(page); - await expect(page.getByTestId("govBanner")).toBeInViewport(); - }); - }, -); diff --git a/frontend-react/e2e/spec/all/idletimeout.spec.ts b/frontend-react/e2e/spec/all/idletimeout.spec.ts index 3d2ae35eb96..0b35f0fd34d 100644 --- a/frontend-react/e2e/spec/all/idletimeout.spec.ts +++ b/frontend-react/e2e/spec/all/idletimeout.spec.ts @@ -1,7 +1,7 @@ import { expect } from "@playwright/test"; import process from "node:process"; -import { OrganizationPage } from "../../pages/organization"; +import { OrganizationPage } from "../../pages/authenticated/organization"; import { test as baseTest } from "../../test"; const timeout = parseInt(process.env.VITE_IDLE_TIMEOUT ?? "20000"); @@ -35,7 +35,7 @@ const test = baseTest.extend({ receiverLogin, storageState, frontendWarningsLogPath, - isFrontendWarningsLog + isFrontendWarningsLog, }); await page.goto(); await use(page); @@ -45,19 +45,14 @@ const test = baseTest.extend({ test.use({ storageState: "e2e/.auth/admin.json" }); test.skip("Does not trigger early", async ({ organizationPage }) => { - await expect( - organizationPage.page.getByRole("banner").first(), - ).toBeVisible(); + await expect(organizationPage.page.getByRole("banner").first()).toBeVisible(); await organizationPage.page.keyboard.down("Tab"); const start = new Date(); - await organizationPage.page.waitForRequest( - /\/oauth2\/default\/v1\/revoke/, - { - timeout: timeoutHigh, - }, - ); + await organizationPage.page.waitForRequest(/\/oauth2\/default\/v1\/revoke/, { + timeout: timeoutHigh, + }); const end = new Date(); diff --git a/frontend-react/e2e/spec/all/last-mile-failures-page.spec.ts b/frontend-react/e2e/spec/all/last-mile-failures-page.spec.ts deleted file mode 100644 index 8af5e5b64af..00000000000 --- a/frontend-react/e2e/spec/all/last-mile-failures-page.spec.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { expect, test } from "@playwright/test"; - -import { tableRows } from "../../helpers/utils"; -import * as lastMileFailures from "../../pages/last-mile-failures"; - -test.describe("Last Mile Failure page", () => { - test.describe("not authenticated", () => { - test("redirects to login", async ({ page }) => { - await lastMileFailures.goto(page); - await expect(page).toHaveURL("/login"); - }); - }); - - test.describe("admin user - happy path", () => { - test.use({ storageState: "e2e/.auth/admin.json" }); - - test.beforeEach(async ({ page }) => { - // Mock the api call before navigating - await lastMileFailures.mockGetSendFailuresResponse(page); - await lastMileFailures.mockGetResendResponse(page); - await lastMileFailures.goto(page); - }); - - test("has correct title", async ({ page }) => { - await expect(page).toHaveTitle(/Last Mile Failures/); - }); - - test("has footer", async ({ page }) => { - await expect(page.locator("footer")).toBeAttached(); - }); - - test("table has correct headers", async ({ page }) => { - await expect(page.locator(".column-header-text").nth(0)).toHaveText(/Failed At/); - await expect(page.locator(".column-header-text").nth(1)).toHaveText(/ReportId/); - await expect(page.locator(".column-header-text").nth(2)).toHaveText(/Receiver/); - }); - - test("table column 'Failed At' has expected data", async ({ page }) => { - await expect(tableRows(page).nth(0).locator("td").nth(0)).toHaveText("Tue, 2/20/2024, 9:35 PM"); - }); - - test("table column 'ReportId' will open a modal with report details", async ({ page }) => { - const reportId = tableRows(page).nth(0).locator("td").nth(1); - await expect(reportId).toContainText(/e5ce49c0-b230-4364-8230-964273249fa1/); - await reportId.click(); - - const modal = page.getByTestId("modalWindow").nth(0); - await expect(modal).toContainText(/Report ID:e5ce49c0-b230-4364-8230-964273249fa1/); - }); - - test("table column 'Receiver' will open receiver edit page", async ({ page }) => { - const receiver = tableRows(page).nth(0).locator("td").nth(2); - await expect(receiver).toContainText(/flexion.etor-service-receiver-results/); - await receiver.click(); - - await expect(page).toHaveURL( - "/admin/orgreceiversettings/org/flexion/receiver/etor-service-receiver-results/action/edit", - ); - }); - }); - - test.describe("admin user - server error", () => { - test.use({ storageState: "e2e/.auth/admin.json" }); - - test.beforeEach(async ({ page }) => { - await lastMileFailures.mockGetSendFailuresResponse(page, 500); - await lastMileFailures.goto(page); - }); - - test("has correct title", async ({ page }) => { - await expect(page).toHaveTitle(/Last Mile Failures/); - }); - - test("has alert", async ({ page }) => { - await expect(page.getByTestId("alert")).toBeAttached(); - await expect(page.getByText(/Our apologies, there was an error loading this content./)).toBeAttached(); - }); - - test("has footer", async ({ page }) => { - await expect(page.locator("footer")).toBeAttached(); - }); - }); - - test.describe("receiver user", () => { - test.use({ storageState: "e2e/.auth/receiver.json" }); - - test.beforeEach(async ({ page }) => { - await lastMileFailures.goto(page); - }); - - test("returns Page Not Found", async ({ page }) => { - await expect(page).toHaveTitle(/Page Not Found/); - }); - - test("has footer", async ({ page }) => { - await expect(page.locator("footer")).toBeAttached(); - }); - }); - - test.describe("sender user", () => { - test.use({ storageState: "e2e/.auth/sender.json" }); - - test.beforeEach(async ({ page }) => { - await lastMileFailures.goto(page); - }); - - test("returns Page Not Found", async ({ page }) => { - await expect(page).toHaveTitle(/Page Not Found/); - }); - - test("has footer", async ({ page }) => { - await expect(page.locator("footer")).toBeAttached(); - }); - }); -}); diff --git a/frontend-react/e2e/spec/all/managing-your-connection-page.spec.ts b/frontend-react/e2e/spec/all/managing-your-connection-page.spec.ts deleted file mode 100644 index 9e3d7ceabc0..00000000000 --- a/frontend-react/e2e/spec/all/managing-your-connection-page.spec.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { scrollToFooter, scrollToTop } from "../../helpers/utils"; -import { ManagingYourConnectionPage } from "../../pages/managing-your-connection"; -import { test as baseTest, expect } from "../../test"; - -const cards = [ - { - name: "For healthcare organizations", - links: [ - "Manage your public key", - "View your submission history", - "Login", - "contact us", - ], - }, - { - name: "For public health agencies", - links: [ - "Refer healthcare organizations", - "View your dashboard", - "Login", - "contact us", - ], - }, -]; - -export interface ManagingYourConnectionPageFixtures { - managingYourConnectionPage: ManagingYourConnectionPage; -} - -const test = baseTest.extend({ - managingYourConnectionPage: async ( - { - page: _page, - isMockDisabled, - adminLogin, - senderLogin, - receiverLogin, - storageState, - isFrontendWarningsLog, - frontendWarningsLogPath, - }, - use, - ) => { - const page = new ManagingYourConnectionPage({ - page: _page, - isMockDisabled, - adminLogin, - senderLogin, - receiverLogin, - storageState, - isFrontendWarningsLog, - frontendWarningsLogPath, - }); - await page.goto(); - await use(page); - }, -}); - -test.describe( - "Managing Your Connection page", - { - tag: "@smoke", - }, - () => { - test("has correct title", async ({ managingYourConnectionPage }) => { - await expect(managingYourConnectionPage.page).toHaveTitle( - managingYourConnectionPage.title, - ); - await expect(managingYourConnectionPage.heading).toBeVisible(); - }); - - test.describe("Quick links", () => { - for (const card of cards) { - test(`should have ${card.name} links`, async ({ - managingYourConnectionPage, - }) => { - const cardHeader = managingYourConnectionPage.page.locator( - ".usa-card__header", - { - hasText: card.name, - }, - ); - - await expect(cardHeader).toBeVisible(); - - const cardContainer = cardHeader.locator(".."); - - for (const link of card.links) { - await expect( - cardContainer.getByRole("link", { - name: `${link}`, - }), - ).toBeVisible(); - } - }); - } - }); - - test.describe("Footer", () => { - test("has footer", async ({ managingYourConnectionPage }) => { - await expect(managingYourConnectionPage.footer).toBeAttached(); - }); - - test("explicit scroll to footer and then scroll to top", async ({ - managingYourConnectionPage, - }) => { - await expect( - managingYourConnectionPage.footer, - ).not.toBeInViewport(); - await scrollToFooter(managingYourConnectionPage.page); - await expect( - managingYourConnectionPage.footer, - ).toBeInViewport(); - await expect( - managingYourConnectionPage.page.getByTestId("govBanner"), - ).not.toBeInViewport(); - await scrollToTop(managingYourConnectionPage.page); - await expect( - managingYourConnectionPage.page.getByTestId("govBanner"), - ).toBeInViewport(); - }); - }); - }, -); diff --git a/frontend-react/e2e/spec/all/message-details-page.spec.ts b/frontend-react/e2e/spec/all/message-details-page.spec.ts deleted file mode 100644 index 77291601b05..00000000000 --- a/frontend-react/e2e/spec/all/message-details-page.spec.ts +++ /dev/null @@ -1,215 +0,0 @@ -import { expect, test } from "@playwright/test"; -import fs from "node:fs"; -import { parseFileLocation } from "../../../src/utils/misc"; -import { tableRows } from "../../helpers/utils"; -import { MOCK_GET_MESSAGE } from "../../mocks/messages"; -import * as messageDetails from "../../pages/message-details"; -import { URL_MESSAGE_DETAILS } from "../../pages/message-details"; -import * as messageIdSearch from "../../pages/message-id-search"; -import { MESSAGE_ID } from "../../pages/message-id-search"; -import { mockGetHistoryReportResponse } from "../../pages/report-details"; -test.describe("Message Details Page", () => { - test.describe("not authenticated", () => { - test("redirects to login", async ({ page }) => { - await messageDetails.goto(page); - await expect(page).toHaveURL("/login"); - }); - }); - - test.describe("authenticated admin", () => { - test.use({ storageState: "e2e/.auth/admin.json" }); - - test.beforeEach(async ({ page }) => { - await page.route(messageIdSearch.API_MESSAGE, (route) => - route.fulfill({ - status: 200, - json: MOCK_GET_MESSAGE, - }), - ); - await messageDetails.goto(page); - }); - - test("has correct title", async ({ page }) => { - await expect(page).toHaveURL(URL_MESSAGE_DETAILS); - await expect(page).toHaveTitle( - /ReportStream - CDC's free, interoperable data transfer platform/, - ); - }); - - test("has message id section", async ({ page }) => { - await expect( - page.getByText("Message ID", { exact: true }), - ).toBeVisible(); - await expect(page.getByText(MESSAGE_ID)).toBeVisible(); - }); - - test("has sender section", async ({ page }) => { - const { sender, reportId, submittedDate } = MOCK_GET_MESSAGE; - - await expect(page.getByText("Sender:")).toBeVisible(); - await expect(page.getByText(sender)).toBeVisible(); - await expect(page.getByText("Incoming Report ID")).toBeVisible(); - await expect( - page.getByText(reportId, { exact: true }), - ).toBeVisible(); - await expect(page.getByText("Date/Time Submitted")).toBeVisible(); - await expect( - page.getByText(new Date(submittedDate).toLocaleString()), - ).toBeVisible(); - await expect(page.getByText("File Location")).toBeVisible(); - await expect( - page.getByText("RECEIVE", { exact: true }), - ).toBeVisible(); - await expect( - page.getByText("ignore.ignore-simple-report"), - ).toBeVisible(); - await expect(page.getByText("Incoming File Name")).toBeVisible(); - await expect( - page.getByText( - "pdi-covid-19-d9a57df0-2702-4e28-9d80-ff8c9ec51816-20240514142655.csv", - ), - ).toBeVisible(); - }); - - test.describe("authenticated admin", () => { - test("has receiver title", async ({ page }) => { - await expect(page.getByText("Receivers:")).toBeVisible(); - }); - - test("displays expected table headers and data", async ({ - page, - }) => { - // include header row - const rowCount = MOCK_GET_MESSAGE.receiverData.length + 1; - const table = page.getByRole("table"); - await expect(table).toBeVisible(); - const rows = await table.getByRole("row").all(); - expect(rows).toHaveLength(rowCount); - - const colHeaders = [ - "Name", - "Service", - "Date", - "Report Id", - "Main", - "Sub", - "File Name", - "Transport Results", - ]; - for (const [i, row] of rows.entries()) { - const cols = await row.getByRole("cell").allTextContents(); - expect(cols).toHaveLength(colHeaders.length); - - const { - receivingOrg, - receivingOrgSvc, - createdAt, - reportId, - fileUrl, - transportResult, - } = - i === 0 - ? MOCK_GET_MESSAGE.receiverData[0] - : MOCK_GET_MESSAGE.receiverData.find( - (i) => i.reportId === cols[3], - ) ?? { reportId: "INVALID" }; - - // if first row, we expect column headers. else, the data row matching the report id - const expectedColContents = - i === 0 - ? colHeaders - : [ - receivingOrg ?? "", - receivingOrgSvc ?? "", - createdAt - ? new Date(createdAt).toLocaleString() - : "", - reportId, - parseFileLocation( - fileUrl ?? "N/A", - ).folderLocation.toLocaleUpperCase(), - parseFileLocation(fileUrl ?? "N/A") - .sendingOrg, - parseFileLocation(fileUrl ?? "N/A").fileName, - transportResult ?? "", - ]; - - for (const [i, col] of cols.entries()) { - expect(col).toBe(expectedColContents[i]); - } - } - }); - - test("table column 'FileName' will download file", async ({ - page, - }) => { - const downloadProm = page.waitForEvent("download"); - await mockGetHistoryReportResponse(page, "*"); - - await tableRows(page) - .nth(0) - .locator("td") - .nth(6) - .getByRole("button") - .click(); - - const download = await downloadProm; - - // assert filename - expect(download.suggestedFilename()).toBe( - "hhsprotect-covid-19-73e3cbc8-9920-4ab7-871f-843a1db4c074.csv", - ); - // get and assert stats - expect( - (await fs.promises.stat(await download.path())).size, - ).toBeGreaterThan(200); - }); - }); - - test("has footer", async ({ page }) => { - await expect(page.locator("footer")).toBeAttached(); - }); - }); - - test.describe("receiver user", () => { - test.use({ storageState: "e2e/.auth/receiver.json" }); - - test.beforeEach(async ({ page }) => { - await messageDetails.goto(page); - }); - - test("has alert", async ({ page }) => { - await expect(page.getByTestId("alert")).toBeAttached(); - await expect( - page.getByText( - /Our apologies, there was an error loading this content./, - ), - ).toBeAttached(); - }); - - test("has footer", async ({ page }) => { - await expect(page.locator("footer")).toBeAttached(); - }); - }); - - test.describe("sender user", () => { - test.use({ storageState: "e2e/.auth/sender.json" }); - - test.beforeEach(async ({ page }) => { - await messageDetails.goto(page); - }); - - test("has alert", async ({ page }) => { - await expect(page.getByTestId("alert")).toBeAttached(); - await expect( - page.getByText( - /Our apologies, there was an error loading this content./, - ), - ).toBeAttached(); - }); - - test("has footer", async ({ page }) => { - await expect(page.locator("footer")).toBeAttached(); - }); - }); -}); diff --git a/frontend-react/e2e/spec/all/message-id-search-page.spec.ts b/frontend-react/e2e/spec/all/message-id-search-page.spec.ts deleted file mode 100644 index c9057d80d2a..00000000000 --- a/frontend-react/e2e/spec/all/message-id-search-page.spec.ts +++ /dev/null @@ -1,195 +0,0 @@ -import { expect, test } from "@playwright/test"; -import { noData, tableRows } from "../../helpers/utils"; -import { MOCK_GET_MESSAGE, MOCK_GET_MESSAGES } from "../../mocks/messages"; -import * as messageIdSearch from "../../pages/message-id-search"; -import { - MESSAGE_ID, - URL_MESSAGE_ID_SEARCH, -} from "../../pages/message-id-search"; -import { openReportIdDetailPage } from "../../pages/submission-history"; -import * as submissionHistory from "../../pages/submission-history"; - -test.describe("Message ID Search Page", () => { - test.describe("not authenticated", () => { - test("redirects to login", async ({ page }) => { - await messageIdSearch.goto(page); - await expect(page).toHaveURL("/login"); - }); - }); - - test.describe("authenticated admin", () => { - test.use({ storageState: "e2e/.auth/admin.json" }); - - test.describe("on search with results", () => { - test.beforeEach(async ({ page }) => { - await page.route(messageIdSearch.API_MESSAGES, (route) => - route.fulfill({ - status: 200, - json: MOCK_GET_MESSAGES, - }), - ); - await page.route(messageIdSearch.API_MESSAGE, (route) => - route.fulfill({ - status: 200, - json: MOCK_GET_MESSAGE, - }), - ); - await submissionHistory.mockGetReportHistoryResponse(page); - await messageIdSearch.goto(page); - - await page.locator("#search-field").fill(MESSAGE_ID); - await page - .getByRole("button", { - name: "Search", - }) - .click(); - }); - - test("has correct title", async ({ page }) => { - await expect(page).toHaveURL(URL_MESSAGE_ID_SEARCH); - await expect(page).toHaveTitle(/Message ID search - Admin/); - }); - - test("has footer", async ({ page }) => { - await expect(page.locator("footer")).toBeAttached(); - }); - - test("displays expected table headers and data", async ({ - page, - }) => { - // include header row - const rowCount = MOCK_GET_MESSAGES.length + 1; - const table = page.getByRole("table"); - await expect(table).toBeVisible(); - const rows = await table.getByRole("row").all(); - expect(rows).toHaveLength(rowCount); - - const colHeaders = [ - "Message ID", - "Sender", - "Date/time submitted", - "Incoming Report Id", - ]; - for (const [i, row] of rows.entries()) { - const cols = await row.getByRole("cell").allTextContents(); - expect(cols).toHaveLength(colHeaders.length); - - const { messageId, sender, submittedDate, reportId } = - i === 0 - ? MOCK_GET_MESSAGES[0] - : MOCK_GET_MESSAGES.find( - (i) => i.reportId === cols[3], - ) ?? { reportId: "INVALID" }; - // if first row, we expect column headers. else, the data row matching the report id - const expectedColContents = - i === 0 - ? colHeaders - : [ - messageId, - sender ?? "", - submittedDate - ? new Date(submittedDate).toLocaleString() - : "", - reportId ?? "", - ]; - - for (const [i, col] of cols.entries()) { - expect(col).toBe(expectedColContents[i]); - } - } - }); - - test("table column 'Message ID' will open message id details", async ({ - page, - }) => { - const messageIdCell = tableRows(page) - .nth(0) - .locator("td") - .nth(0) - .getByRole("link", { name: MESSAGE_ID }); - await messageIdCell.click(); - await expect(page).toHaveURL("/message-details/0"); - expect(page.locator("h1").getByText(MESSAGE_ID)).toBeTruthy(); - }); - - test("table column 'Incoming Report Id' will open report id details", async ({ - page, - }) => { - const reportId = "73e3cbc8-9920-4ab7-871f-843a1db4c074"; - const reportIdCell = tableRows(page) - .nth(0) - .locator("td") - .nth(3) - .getByRole("link", { - name: reportId, - }); - await reportIdCell.click(); - await openReportIdDetailPage(page, reportId); - }); - }); - - test.describe("on search without results", () => { - test.beforeEach(async ({ page }) => { - await page.route(messageIdSearch.API_MESSAGES, (route) => - route.fulfill({ - status: 200, - json: [], - }), - ); - await messageIdSearch.goto(page); - - await page.locator("#search-field").fill(MESSAGE_ID); - await page - .getByRole("button", { - name: "Search", - }) - .click(); - }); - - test("has correct title", async ({ page }) => { - await expect(page).toHaveURL(URL_MESSAGE_ID_SEARCH); - await expect(page).toHaveTitle(/Message ID search - Admin/); - }); - - test("has footer", async ({ page }) => { - await expect(page.locator("footer")).toBeAttached(); - }); - - test("shows no data", async ({ page }) => { - await expect(noData(page)).toBeAttached(); - }); - }); - }); - - test.describe("receiver user", () => { - test.use({ storageState: "e2e/.auth/receiver.json" }); - - test.beforeEach(async ({ page }) => { - await messageIdSearch.goto(page); - }); - - test("returns Page Not Found", async ({ page }) => { - await expect(page).toHaveTitle(/Page Not Found/); - }); - - test("has footer", async ({ page }) => { - await expect(page.locator("footer")).toBeAttached(); - }); - }); - - test.describe("sender user", () => { - test.use({ storageState: "e2e/.auth/sender.json" }); - - test.beforeEach(async ({ page }) => { - await messageIdSearch.goto(page); - }); - - test("returns Page Not Found", async ({ page }) => { - await expect(page).toHaveTitle(/Page Not Found/); - }); - - test("has footer", async ({ page }) => { - await expect(page.locator("footer")).toBeAttached(); - }); - }); -}); diff --git a/frontend-react/e2e/spec/all/our-network-page.spec.ts b/frontend-react/e2e/spec/all/our-network-page.spec.ts deleted file mode 100644 index d8345618291..00000000000 --- a/frontend-react/e2e/spec/all/our-network-page.spec.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { expect, test } from "@playwright/test"; - -import * as sideNav from "../../pages/about-side-navigation"; -import * as ourNetwork from "../../pages/our-network"; -test.describe( - "Our network page", - { - tag: "@smoke", - }, - () => { - test.beforeEach(async ({ page }) => { - await ourNetwork.goto(page); - }); - - test("has correct title", async ({ page }) => { - await expect(page).toHaveTitle(/Our network/); - }); - - test.describe("Side navigation", () => { - test("has Our network link", async ({ page }) => { - await sideNav.clickNetwork(page); - await expect(page).toHaveURL(/.*about\/our-network/); - }); - - test("has Product roadmap link", async ({ page }) => { - await sideNav.clickRoadmap(page); - await expect(page).toHaveURL(/.*about\/roadmap/); - }); - - test("has News link", async ({ page }) => { - await sideNav.clickNews(page); - await expect(page).toHaveURL(/.*about\/news/); - }); - - test("has Case studies link", async ({ page }) => { - await sideNav.clickCaseStudies(page); - await expect(page).toHaveURL(/.*about\/case-studies/); - }); - - test("has Security link", async ({ page }) => { - await sideNav.clickSecurity(page); - await expect(page).toHaveURL(/.*about\/security/); - }); - - test("has Release notes link", async ({ page }) => { - await sideNav.clickReleaseNotes(page); - await expect(page).toHaveURL(/.*about\/release-notes/); - }); - }); - }, -); diff --git a/frontend-react/e2e/spec/all/about-page.spec.ts b/frontend-react/e2e/spec/all/public/about/about-page.spec.ts similarity index 84% rename from frontend-react/e2e/spec/all/about-page.spec.ts rename to frontend-react/e2e/spec/all/public/about/about-page.spec.ts index bf32d2b158b..f62b5f1db19 100644 --- a/frontend-react/e2e/spec/all/about-page.spec.ts +++ b/frontend-react/e2e/spec/all/public/about/about-page.spec.ts @@ -1,6 +1,5 @@ -import { scrollToFooter, scrollToTop } from "../../helpers/utils"; -import { AboutPage } from "../../pages/about"; -import { test as baseTest, expect } from "../../test"; +import { AboutPage } from "../../../../pages/public/about/about"; +import { test as baseTest, expect } from "../../../../test"; const URL_ABOUT = "/about"; @@ -38,6 +37,12 @@ const test = baseTest.extend({ }); test.describe("About page", () => { + test.describe("Header", () => { + test("has correct title + heading", async ({ aboutPage }) => { + await aboutPage.testHeader(); + }); + }); + test("nav contains the 'About' dropdown with 'About Reportstream' option", async ({ aboutPage }) => { const navItems = aboutPage.page.locator(".usa-nav li"); await expect(navItems).toContainText(["About"]); @@ -48,11 +53,6 @@ test.describe("About page", () => { await expect(aboutPage.page).toHaveURL(URL_ABOUT); }); - test("has correct title", async ({ aboutPage }) => { - await expect(aboutPage.page).toHaveTitle(aboutPage.title); - await expect(aboutPage.heading).toBeVisible(); - }); - test.describe("In this section", () => { test("has 'Our network' link", async ({ aboutPage }) => { await aboutPage.page.getByRole("link", { name: /Our network/ }).click(); @@ -156,17 +156,8 @@ test.describe("About page", () => { }); test.describe("Footer", () => { - test("has footer", async ({ aboutPage }) => { - await expect(aboutPage.footer).toBeAttached(); - }); - - test("explicit scroll to footer and then scroll to top", async ({ aboutPage }) => { - await expect(aboutPage.footer).not.toBeInViewport(); - await scrollToFooter(aboutPage.page); - await expect(aboutPage.footer).toBeInViewport(); - await expect(aboutPage.page.getByTestId("govBanner")).not.toBeInViewport(); - await scrollToTop(aboutPage.page); - await expect(aboutPage.page.getByTestId("govBanner")).toBeInViewport(); + test("has footer and explicit scroll to footer and scroll to top", async ({ aboutPage }) => { + await aboutPage.testFooter(); }); }); }); diff --git a/frontend-react/e2e/spec/all/public/about/our-network-page.spec.ts b/frontend-react/e2e/spec/all/public/about/our-network-page.spec.ts new file mode 100644 index 00000000000..d826f09b006 --- /dev/null +++ b/frontend-react/e2e/spec/all/public/about/our-network-page.spec.ts @@ -0,0 +1,62 @@ +import { aboutSideNav } from "../../../../helpers/internal-links"; +import { OurNetworkPage } from "../../../../pages/public/about/our-network"; +import { test as baseTest } from "../../../../test"; + +export interface OurNetworkPageFixtures { + ourNetworkPage: OurNetworkPage; +} + +const test = baseTest.extend({ + ourNetworkPage: async ( + { + page: _page, + isMockDisabled, + adminLogin, + senderLogin, + receiverLogin, + storageState, + isFrontendWarningsLog, + frontendWarningsLogPath, + }, + use, + ) => { + const page = new OurNetworkPage({ + page: _page, + isMockDisabled, + adminLogin, + senderLogin, + receiverLogin, + storageState, + isFrontendWarningsLog, + frontendWarningsLogPath, + }); + await page.goto(); + await use(page); + }, +}); + +test.describe( + "Our network page", + { + tag: "@smoke", + }, + () => { + test.describe("Header", () => { + test("has correct title + heading", async ({ ourNetworkPage }) => { + await ourNetworkPage.testHeader(); + }); + }); + + test.describe("Side navigation", () => { + test("has correct About sidenav items", async ({ ourNetworkPage }) => { + await ourNetworkPage.testSidenav(aboutSideNav); + }); + }); + + test.describe("Footer", () => { + test("has footer and explicit scroll to footer and scroll to top", async ({ ourNetworkPage }) => { + await ourNetworkPage.testFooter(); + }); + }); + }, +); diff --git a/frontend-react/e2e/spec/all/public/about/roadmap.spec.ts b/frontend-react/e2e/spec/all/public/about/roadmap.spec.ts new file mode 100644 index 00000000000..d3f71cbf108 --- /dev/null +++ b/frontend-react/e2e/spec/all/public/about/roadmap.spec.ts @@ -0,0 +1,82 @@ +import { aboutSideNav } from "../../../../helpers/internal-links"; +import { RoadmapPage } from "../../../../pages/public/about/roadmap"; +import { test as baseTest } from "../../../../test"; + +export interface RoadmapPageFixtures { + roadmapPage: RoadmapPage; +} + +const test = baseTest.extend({ + roadmapPage: async ( + { + page: _page, + isMockDisabled, + adminLogin, + senderLogin, + receiverLogin, + storageState, + isFrontendWarningsLog, + frontendWarningsLogPath, + }, + use, + ) => { + const page = new RoadmapPage({ + page: _page, + isMockDisabled, + adminLogin, + senderLogin, + receiverLogin, + storageState, + isFrontendWarningsLog, + frontendWarningsLogPath, + }); + await page.goto(); + await use(page); + }, +}); + +const cards = [ + { + name: "News", + }, + { + name: "Release notes", + }, + { + name: "Developer resources", + }, +]; + +test.describe( + "Product roadmap page", + { + tag: "@smoke", + }, + () => { + test.describe("Header", () => { + test("has correct title + heading", async ({ roadmapPage }) => { + await roadmapPage.testHeader(); + }); + }); + + test.describe("Side navigation", () => { + test("has correct About sidenav items", async ({ roadmapPage }) => { + await roadmapPage.testSidenav(aboutSideNav); + }); + }); + + test.describe("CTA", () => { + for (const card of cards) { + test(`should have ${card.name}`, async ({ roadmapPage }) => { + await roadmapPage.testCard(card); + }); + } + }); + + test.describe("Footer", () => { + test("has footer and explicit scroll to footer and scroll to top", async ({ roadmapPage }) => { + await roadmapPage.testFooter(); + }); + }); + }, +); diff --git a/frontend-react/e2e/spec/all/security.spec.ts b/frontend-react/e2e/spec/all/public/about/security.spec.ts similarity index 73% rename from frontend-react/e2e/spec/all/security.spec.ts rename to frontend-react/e2e/spec/all/public/about/security.spec.ts index a22b9b5c737..08976df88e4 100644 --- a/frontend-react/e2e/spec/all/security.spec.ts +++ b/frontend-react/e2e/spec/all/public/about/security.spec.ts @@ -1,6 +1,5 @@ -import { scrollToFooter, scrollToTop } from "../../helpers/utils"; -import { SecurityPage } from "../../pages/security"; -import { test as baseTest, expect } from "../../test"; +import { SecurityPage } from "../../../../pages/public/about/security"; +import { test as baseTest, expect } from "../../../../test"; const URL_SECURITY = "/about/security"; @@ -43,6 +42,12 @@ test.describe( tag: "@smoke", }, () => { + test.describe("Header", () => { + test("has correct title + heading", async ({ securityPage }) => { + await securityPage.testHeader(); + }); + }); + test("nav contains the 'About' dropdown with 'Security Reportstream' option", async ({ securityPage }) => { const navItems = securityPage.page.locator(".usa-nav li"); await expect(navItems).toContainText(["About"]); @@ -53,11 +58,6 @@ test.describe( await expect(securityPage.page).toHaveURL(URL_SECURITY); }); - test("has correct title", async ({ securityPage }) => { - await expect(securityPage.page).toHaveTitle(securityPage.title); - await expect(securityPage.heading).toBeVisible(); - }); - test.describe("Security section", () => { test("Accordion sections expand", async ({ securityPage }) => { // Not necessary to test all expansions. @@ -90,17 +90,8 @@ test.describe( }); test.describe("Footer", () => { - test("has footer", async ({ securityPage }) => { - await expect(securityPage.footer).toBeAttached(); - }); - - test("explicit scroll to footer and then scroll to top", async ({ securityPage }) => { - await expect(securityPage.footer).not.toBeInViewport(); - await scrollToFooter(securityPage.page); - await expect(securityPage.footer).toBeInViewport(); - await expect(securityPage.page.getByTestId("govBanner")).not.toBeInViewport(); - await scrollToTop(securityPage.page); - await expect(securityPage.page.getByTestId("govBanner")).toBeInViewport(); + test("has footer + test bottom-to-top page scroll", async ({ securityPage }) => { + await securityPage.testFooter(); }); }); }, diff --git a/frontend-react/e2e/spec/all/getting-started/receiving-data.spec.ts b/frontend-react/e2e/spec/all/public/getting-started/receiving-data.spec.ts similarity index 63% rename from frontend-react/e2e/spec/all/getting-started/receiving-data.spec.ts rename to frontend-react/e2e/spec/all/public/getting-started/receiving-data.spec.ts index 3e1eb2c7660..0a7755d0c4f 100644 --- a/frontend-react/e2e/spec/all/getting-started/receiving-data.spec.ts +++ b/frontend-react/e2e/spec/all/public/getting-started/receiving-data.spec.ts @@ -1,7 +1,6 @@ -import site from "../../../../src/content/site.json" assert { type: "json" }; -import { scrollToFooter, scrollToTop } from "../../../helpers/utils"; -import { ReceivingDataPage } from "../../../pages/getting-started/receiving-data"; -import { test as baseTest, expect } from "../../../test"; +import site from "../../../../../src/content/site.json" assert { type: "json" }; +import { ReceivingDataPage } from "../../../../pages/public/getting-started/receiving-data"; +import { test as baseTest, expect } from "../../../../test"; export interface ReceivingDataPageFixtures { receivingDataPage: ReceivingDataPage; @@ -37,9 +36,10 @@ const test = baseTest.extend({ }); test.describe("Receiving data page", () => { - test("has correct title", async ({ receivingDataPage }) => { - await expect(receivingDataPage.page).toHaveTitle(receivingDataPage.title); - await expect(receivingDataPage.heading).toBeVisible(); + test.describe("Header", () => { + test("has correct title + heading", async ({ receivingDataPage }) => { + await receivingDataPage.testHeader(); + }); }); test("has link to onboarding form", async ({ receivingDataPage }) => { @@ -73,17 +73,8 @@ test.describe("Receiving data page", () => { }); test.describe("Footer", () => { - test("has footer", async ({ receivingDataPage }) => { - await expect(receivingDataPage.footer).toBeAttached(); - }); - - test("explicit scroll to footer and then scroll to top", async ({ receivingDataPage }) => { - await expect(receivingDataPage.footer).not.toBeInViewport(); - await scrollToFooter(receivingDataPage.page); - await expect(receivingDataPage.footer).toBeInViewport(); - await expect(receivingDataPage.page.getByTestId("govBanner")).not.toBeInViewport(); - await scrollToTop(receivingDataPage.page); - await expect(receivingDataPage.page.getByTestId("govBanner")).toBeInViewport(); + test("has footer and explicit scroll to footer and scroll to top", async ({ receivingDataPage }) => { + await receivingDataPage.testFooter(); }); }); }); diff --git a/frontend-react/e2e/spec/all/getting-started/sending-data.spec.ts b/frontend-react/e2e/spec/all/public/getting-started/sending-data.spec.ts similarity index 64% rename from frontend-react/e2e/spec/all/getting-started/sending-data.spec.ts rename to frontend-react/e2e/spec/all/public/getting-started/sending-data.spec.ts index 3e1b9f5544d..fc915c2c7cd 100644 --- a/frontend-react/e2e/spec/all/getting-started/sending-data.spec.ts +++ b/frontend-react/e2e/spec/all/public/getting-started/sending-data.spec.ts @@ -1,7 +1,6 @@ -import site from "../../../../src/content/site.json" assert { type: "json" }; -import { scrollToFooter, scrollToTop } from "../../../helpers/utils"; -import { SendingDataPage } from "../../../pages/getting-started/sending-data.js"; -import { test as baseTest, expect } from "../../../test"; +import site from "../../../../../src/content/site.json" assert { type: "json" }; +import { SendingDataPage } from "../../../../pages/public/getting-started/sending-data.js"; +import { test as baseTest, expect } from "../../../../test"; export interface SendingDataPageFixtures { sendingDataPage: SendingDataPage; @@ -37,9 +36,10 @@ const test = baseTest.extend({ }); test.describe("Sending data page", () => { - test("has correct title", async ({ sendingDataPage }) => { - await expect(sendingDataPage.page).toHaveTitle(sendingDataPage.title); - await expect(sendingDataPage.heading).toBeVisible(); + test.describe("Header", () => { + test("has correct title + heading", async ({ sendingDataPage }) => { + await sendingDataPage.testHeader(); + }); }); test("has link to get started with ReportStream", async ({ sendingDataPage }) => { @@ -73,17 +73,8 @@ test.describe("Sending data page", () => { }); test.describe("Footer", () => { - test("has footer", async ({ sendingDataPage }) => { - await expect(sendingDataPage.footer).toBeAttached(); - }); - - test("explicit scroll to footer and then scroll to top", async ({ sendingDataPage }) => { - await expect(sendingDataPage.footer).not.toBeInViewport(); - await scrollToFooter(sendingDataPage.page); - await expect(sendingDataPage.footer).toBeInViewport(); - await expect(sendingDataPage.page.getByTestId("govBanner")).not.toBeInViewport(); - await scrollToTop(sendingDataPage.page); - await expect(sendingDataPage.page.getByTestId("govBanner")).toBeInViewport(); + test("has footer and explicit scroll to footer and scroll to top", async ({ sendingDataPage }) => { + await sendingDataPage.testFooter(); }); }); }); diff --git a/frontend-react/e2e/spec/all/public/homepage.spec.ts b/frontend-react/e2e/spec/all/public/homepage.spec.ts new file mode 100644 index 00000000000..6efc9037c11 --- /dev/null +++ b/frontend-react/e2e/spec/all/public/homepage.spec.ts @@ -0,0 +1,66 @@ +import site from "../../../../src/content/site.json" assert { type: "json" }; +import { HomePage } from "../../../pages/public/homepage"; +import { test as baseTest, expect } from "../../../test"; + +export interface HomePageFixtures { + homePage: HomePage; +} + +const test = baseTest.extend({ + homePage: async ( + { + page: _page, + isMockDisabled, + adminLogin, + senderLogin, + receiverLogin, + storageState, + isFrontendWarningsLog, + frontendWarningsLogPath, + }, + use, + ) => { + const page = new HomePage({ + page: _page, + isMockDisabled, + adminLogin, + senderLogin, + receiverLogin, + storageState, + isFrontendWarningsLog, + frontendWarningsLogPath, + }); + await page.goto(); + await use(page); + }, +}); + +test.describe( + "Homepage", + { + tag: "@smoke", + }, + () => { + test.describe("Header", () => { + test("has correct title + heading", async ({ homePage }) => { + await homePage.testHeader(); + }); + }); + + test.describe("CTA", () => { + test("has 'Contact us' button", async ({ homePage }) => { + const heroLocator = homePage.page.locator('[class*="hero-wrapper"]'); + const ctaURL = site.forms.connectWithRS.url; + const ctaLink = heroLocator.locator(`a[href="${ctaURL}"]`).first(); + + await expect(ctaLink).toBeVisible(); + }); + }); + + test.describe("Footer", () => { + test("has footer and explicit scroll to footer and scroll to top", async ({ homePage }) => { + await homePage.testFooter(); + }); + }); + }, +); diff --git a/frontend-react/e2e/spec/all/public/managing-your-connection/managing-your-connection-page.spec.ts b/frontend-react/e2e/spec/all/public/managing-your-connection/managing-your-connection-page.spec.ts new file mode 100644 index 00000000000..e10692aa357 --- /dev/null +++ b/frontend-react/e2e/spec/all/public/managing-your-connection/managing-your-connection-page.spec.ts @@ -0,0 +1,90 @@ +import { ManagingYourConnectionPage } from "../../../../pages/public/managing-your-connection/managing-your-connection"; +import { test as baseTest, expect } from "../../../../test"; + +const cards = [ + { + name: "For healthcare organizations", + links: ["Manage your public key", "View your submission history", "Login", "contact us"], + }, + { + name: "For public health agencies", + links: ["Refer healthcare organizations", "View your dashboard", "Login", "contact us"], + }, +]; + +export interface ManagingYourConnectionPageFixtures { + managingYourConnectionPage: ManagingYourConnectionPage; +} + +const test = baseTest.extend({ + managingYourConnectionPage: async ( + { + page: _page, + isMockDisabled, + adminLogin, + senderLogin, + receiverLogin, + storageState, + isFrontendWarningsLog, + frontendWarningsLogPath, + }, + use, + ) => { + const page = new ManagingYourConnectionPage({ + page: _page, + isMockDisabled, + adminLogin, + senderLogin, + receiverLogin, + storageState, + isFrontendWarningsLog, + frontendWarningsLogPath, + }); + await page.goto(); + await use(page); + }, +}); + +test.describe( + "Managing Your Connection page", + { + tag: "@smoke", + }, + () => { + test.describe("Header", () => { + test("has correct title + heading", async ({ managingYourConnectionPage }) => { + await managingYourConnectionPage.testHeader(); + }); + }); + + test.describe("Quick links", () => { + for (const card of cards) { + test(`should have ${card.name} links`, async ({ managingYourConnectionPage }) => { + const cardHeader = managingYourConnectionPage.page.locator(".usa-card__header", { + hasText: card.name, + }); + + await expect(cardHeader).toBeVisible(); + + const cardContainer = cardHeader.locator(".."); + + for (const link of card.links) { + await expect( + cardContainer.getByRole("link", { + name: `${link}`, + }), + ).toBeVisible(); + } + }); + } + }); + + test.describe("Footer", () => { + test("has footer and explicit scroll to footer and scroll to top", async ({ + managingYourConnectionPage, + }) => { + await managingYourConnectionPage.testFooter(); + }); + }); + }, +); diff --git a/frontend-react/e2e/spec/all/refer-healthcare-page.spec.ts b/frontend-react/e2e/spec/all/public/managing-your-connection/refer-healthcare-page.spec.ts similarity index 63% rename from frontend-react/e2e/spec/all/refer-healthcare-page.spec.ts rename to frontend-react/e2e/spec/all/public/managing-your-connection/refer-healthcare-page.spec.ts index 37421d676e4..27c3c74899c 100644 --- a/frontend-react/e2e/spec/all/refer-healthcare-page.spec.ts +++ b/frontend-react/e2e/spec/all/public/managing-your-connection/refer-healthcare-page.spec.ts @@ -1,6 +1,5 @@ -import { scrollToFooter, scrollToTop } from "../../helpers/utils"; -import { ReferHealthcarePage } from "../../pages/refer-healthcare"; -import { test as baseTest, expect } from "../../test"; +import { ReferHealthcarePage } from "../../../../pages/public/managing-your-connection/refer-healthcare"; +import { test as baseTest, expect } from "../../../../test"; export interface ReferHealthcarePageFixtures { referHealthcarePage: ReferHealthcarePage; @@ -41,9 +40,10 @@ test.describe( tag: "@smoke", }, () => { - test("has correct title", async ({ referHealthcarePage }) => { - await expect(referHealthcarePage.page).toHaveTitle(referHealthcarePage.title); - await expect(referHealthcarePage.heading).toBeVisible(); + test.describe("Header", () => { + test("has correct title + heading", async ({ referHealthcarePage }) => { + await referHealthcarePage.testHeader(); + }); }); test("has correct sidenav items", async ({ referHealthcarePage }) => { @@ -68,17 +68,8 @@ test.describe( }); test.describe("Footer", () => { - test("has footer", async ({ referHealthcarePage }) => { - await expect(referHealthcarePage.footer).toBeAttached(); - }); - - test("explicit scroll to footer and then scroll to top", async ({ referHealthcarePage }) => { - await expect(referHealthcarePage.footer).not.toBeInViewport(); - await scrollToFooter(referHealthcarePage.page); - await expect(referHealthcarePage.footer).toBeInViewport(); - await expect(referHealthcarePage.page.getByTestId("govBanner")).not.toBeInViewport(); - await scrollToTop(referHealthcarePage.page); - await expect(referHealthcarePage.page.getByTestId("govBanner")).toBeInViewport(); + test("has footer and explicit scroll to footer and scroll to top", async ({ referHealthcarePage }) => { + await referHealthcarePage.testFooter(); }); }); }, diff --git a/frontend-react/e2e/spec/all/public/resources-page.spec.ts b/frontend-react/e2e/spec/all/public/resources-page.spec.ts new file mode 100644 index 00000000000..1acd4e1883f --- /dev/null +++ b/frontend-react/e2e/spec/all/public/resources-page.spec.ts @@ -0,0 +1,69 @@ +import { DeveloperResourcesPage } from "../../../pages/public/resources"; +import { test as baseTest } from "../../../test"; + +export interface DeveloperResourcesPageFixtures { + developerResourcesPage: DeveloperResourcesPage; +} + +const test = baseTest.extend({ + developerResourcesPage: async ( + { + page: _page, + isMockDisabled, + adminLogin, + senderLogin, + receiverLogin, + storageState, + isFrontendWarningsLog, + frontendWarningsLogPath, + }, + use, + ) => { + const page = new DeveloperResourcesPage({ + page: _page, + isMockDisabled, + adminLogin, + senderLogin, + receiverLogin, + storageState, + isFrontendWarningsLog, + frontendWarningsLogPath, + }); + await page.goto(); + await use(page); + }, +}); + +const cards = [ + { + name: "API guide", + }, + { + name: "GitHub", + }, + { + name: "Release notes", + }, +]; + +test.describe("Developer Resources page", () => { + test.describe("Header", () => { + test("has correct title + heading", async ({ developerResourcesPage }) => { + await developerResourcesPage.testHeader(); + }); + }); + + test.describe("CTA", () => { + for (const card of cards) { + test(`should have ${card.name}`, async ({ developerResourcesPage }) => { + await developerResourcesPage.testCard(card); + }); + } + }); + + test.describe("Footer", () => { + test("has footer and explicit scroll to footer and scroll to top", async ({ developerResourcesPage }) => { + await developerResourcesPage.testFooter(); + }); + }); +}); diff --git a/frontend-react/e2e/spec/all/support-page.spec.ts b/frontend-react/e2e/spec/all/public/support-page.spec.ts similarity index 57% rename from frontend-react/e2e/spec/all/support-page.spec.ts rename to frontend-react/e2e/spec/all/public/support-page.spec.ts index 0948a85b187..fc18518233f 100644 --- a/frontend-react/e2e/spec/all/support-page.spec.ts +++ b/frontend-react/e2e/spec/all/public/support-page.spec.ts @@ -1,7 +1,6 @@ -import site from "../../../src/content/site.json" assert { type: "json" }; -import { scrollToFooter, scrollToTop } from "../../helpers/utils"; -import { SupportPage } from "../../pages/support.js"; -import { test as baseTest, expect } from "../../test"; +import site from "../../../../src/content/site.json" assert { type: "json" }; +import { SupportPage } from "../../../pages/public/support.js"; +import { test as baseTest, expect } from "../../../test"; const cards = [ { @@ -52,15 +51,14 @@ const test = baseTest.extend({ }); test.describe("Support page", () => { - test("has correct title", async ({ supportPage }) => { - await expect(supportPage.page).toHaveTitle(supportPage.title); - await expect(supportPage.heading).toBeVisible(); + test.describe("Header", () => { + test("has correct title + heading", async ({ supportPage }) => { + await supportPage.testHeader(); + }); }); test("Should have a way of contacting support", async ({ supportPage }) => { - const contactLink = supportPage.page - .locator(`a[href="${site.forms.contactUs.url}"]`) - .first(); + const contactLink = supportPage.page.locator(`a[href="${site.forms.contactUs.url}"]`).first(); await contactLink.scrollIntoViewIfNeeded(); await expect(contactLink).toBeVisible(); @@ -78,30 +76,13 @@ test.describe("Support page", () => { const viewAllLink = cardContainer.locator("a").last(); await viewAllLink.click(); - await expect( - supportPage.page.locator(`#${card.anchorID}`), - ).toBeVisible(); + await expect(supportPage.page.locator(`#${card.anchorID}`)).toBeVisible(); }); } test.describe("Footer", () => { - test("has footer", async ({ supportPage }) => { - await expect(supportPage.footer).toBeAttached(); - }); - - test("explicit scroll to footer and then scroll to top", async ({ - supportPage, - }) => { - await expect(supportPage.footer).not.toBeInViewport(); - await scrollToFooter(supportPage.page); - await expect(supportPage.footer).toBeInViewport(); - await expect( - supportPage.page.getByTestId("govBanner"), - ).not.toBeInViewport(); - await scrollToTop(supportPage.page); - await expect( - supportPage.page.getByTestId("govBanner"), - ).toBeInViewport(); + test("has footer and explicit scroll to footer and scroll to top", async ({ supportPage }) => { + await supportPage.testFooter(); }); }); }); diff --git a/frontend-react/e2e/spec/all/resources-page.spec.ts b/frontend-react/e2e/spec/all/resources-page.spec.ts deleted file mode 100644 index ab99383c089..00000000000 --- a/frontend-react/e2e/spec/all/resources-page.spec.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { expect, test } from "@playwright/test"; -import * as resources from "../../pages/resources"; - -// eslint-disable-next-line playwright/no-skipped-test -test.describe.skip("Developer Resources page", () => { - test.beforeEach(async ({ page }) => { - await resources.goto(page); - }); - - test("should have correct title", async ({ page }) => { - await expect(page).toHaveURL(/developer-resources/); - await expect(page).toHaveTitle(/Developer resources/); - }); - - // TODO: Fix - test.describe("Card navigation", () => { - const cardLinks = [ - { - name: "Security practices", - url: "/resources/security-practices", - }, - { - name: "System and settings", - url: "/resources/system-and-settings", - }, - { - name: "ReportStream API", - url: "/resources/api", - }, - { - name: "Guide to submitting data to ReportStream", - url: "/resources/getting-started-submitting-data", - }, - { - name: "Account Registration Guide", - url: "/resources/account-registration-guide", - }, - { - name: "ELR Onboarding Checklist", - url: "/resources/elr-checklist", - }, - { - name: "Guide to receiving ReportStream data", - url: "/resources/getting-started-public-health-departments", - }, - { - name: "Manual data download guide", - url: "/resources/data-download-guide", - }, - { - name: "ReportStream Referral Guide", - url: "/resources/referral-guide", - }, - ]; - - for (const cardLink of cardLinks) { - test(`should have ${cardLink.name} link`, async ({ page }) => { - await page.getByRole("link", { name: cardLink.name }).click(); - - await expect(page).toHaveURL(cardLink.url); - }); - } - - test("should redirect unauthenticated users to login page on managing public key", async ({ - page, - }) => { - await page - .getByRole("link", { - name: "Manage your public key", - }) - .click(); - - await expect(page).toHaveURL("/login"); - }); - }); -}); diff --git a/frontend-react/e2e/spec/all/roadmap.spec.ts b/frontend-react/e2e/spec/all/roadmap.spec.ts deleted file mode 100644 index a2905db3ce5..00000000000 --- a/frontend-react/e2e/spec/all/roadmap.spec.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { expect, test } from "@playwright/test"; - -import * as internalLinks from "../../helpers/internal-links"; -import * as sideNav from "../../pages/about-side-navigation"; -import * as roadmap from "../../pages/roadmap"; -import { URL_ROADMAP } from "../../pages/roadmap"; - -test.describe( - "Product roadmap page", - { - tag: "@smoke", - }, - () => { - test.beforeEach(async ({ page }) => { - await roadmap.goto(page); - }); - - test("has correct title", async ({ page }) => { - await expect(page).toHaveURL(URL_ROADMAP); - await expect(page).toHaveTitle(/Product roadmap/); - }); - - test.describe("Side navigation", () => { - test("has Our network link", async ({ page }) => { - await sideNav.clickNetwork(page); - await expect(page).toHaveURL(/.*about\/our-network/); - }); - - test("has Product roadmap link", async ({ page }) => { - await sideNav.clickRoadmap(page); - await expect(page).toHaveURL(/.*about\/roadmap/); - }); - - test("has News link", async ({ page }) => { - await sideNav.clickNews(page); - await expect(page).toHaveURL(/.*about\/news/); - }); - - test("has Case studies link", async ({ page }) => { - await sideNav.clickCaseStudies(page); - await expect(page).toHaveURL(/.*about\/case-studies/); - }); - - test("has Security link", async ({ page }) => { - await sideNav.clickSecurity(page); - await expect(page).toHaveURL(/.*about\/security/); - }); - - test("has Release notes link", async ({ page }) => { - await sideNav.clickReleaseNotes(page); - await expect(page).toHaveURL(/.*about\/release-notes/); - }); - }); - - test.describe("Additional resources Links", () => { - test("has News", async ({ page }) => { - await internalLinks.clickOnInternalLink("div", "CardGroup", "News", page); - await expect(page).toHaveURL(/.*about\/news/); - }); - - test("has Release notes", async ({ page }) => { - await internalLinks.clickOnInternalLink("div", "CardGroup", "Release notes", page); - await expect(page).toHaveURL(/.*about\/release-notes/); - }); - - test("has Developer resources", async ({ page }) => { - await internalLinks.clickOnInternalLink("div", "CardGroup", "Developer resources", page); - await expect(page).toHaveURL(/.*developer-resources/); - }); - }); - }, -); diff --git a/frontend-react/e2e/spec/all/submission-history-page.spec.ts b/frontend-react/e2e/spec/all/submission-history-page.spec.ts deleted file mode 100644 index 25b3043a6d1..00000000000 --- a/frontend-react/e2e/spec/all/submission-history-page.spec.ts +++ /dev/null @@ -1,233 +0,0 @@ -import { expect, test } from "@playwright/test"; - -import { - noData, - selectTestOrg, - tableDataCellValue, - tableRows, - TEST_ORG_IGNORE, -} from "../../helpers/utils"; -import * as submissionHistory from "../../pages/submission-history"; -import { openReportIdDetailPage } from "../../pages/submission-history"; - -const id = "73e3cbc8-9920-4ab7-871f-843a1db4c074"; -test.describe("Submission history page", () => { - test.describe("not authenticated", () => { - test("redirects to login", async ({ page }) => { - await submissionHistory.goto(page); - await expect(page).toHaveURL("/login"); - }); - }); - - test.describe("admin user", () => { - test.use({ storageState: "e2e/.auth/admin.json" }); - - test.describe("without org selected", () => { - test.beforeEach(async ({ page }) => { - await submissionHistory.goto(page); - }); - - test("will not load page", async ({ page }) => { - await expect( - page.getByText("Cannot fetch Organization data as admin"), - ).toBeVisible(); - }); - - test("has footer", async ({ page }) => { - await expect(page.locator("footer")).toBeAttached(); - }); - }); - - test.describe("with org selected", () => { - test.beforeEach(async ({ page }) => { - await selectTestOrg(page); - await submissionHistory.mockGetSubmissionsResponse( - page, - TEST_ORG_IGNORE, - ); - await submissionHistory.mockGetReportHistoryResponse(page); - // abort all app insight calls - await page.route("**/v2/track", (route) => route.abort()); - await submissionHistory.goto(page); - }); - - test("nav contains the 'Submission History' option", async ({ - page, - }) => { - const navItems = page.locator(".usa-nav li"); - await expect(navItems).toContainText(["Submission History"]); - }); - - test("has correct title", async ({ page }) => { - await expect(page).toHaveTitle(/Submission history/); - }); - - test("has filter", async ({ page }) => { - await expect( - page.getByTestId("filter-container"), - ).toBeAttached(); - }); - - test.describe("table", () => { - test("table has correct headers", async ({ page }) => { - await submissionHistory.tableHeaders(page); - }); - - test("table column 'ReportId' will open the report details", async ({ - page, - }) => { - const reportId = tableRows(page) - .nth(0) - .locator("td") - .nth(0); - await expect(reportId).toContainText(id); - await reportId.getByRole("link", { name: id }).click(); - - await openReportIdDetailPage(page, id); - }); - - test("table column 'Date/time submitted' has expected data", async ({ - page, - }) => { - expect(await tableDataCellValue(page, 0, 1)).toEqual( - "3/7/2024, 6:00:22 PM", - ); - }); - - test("table column 'File' has expected data", async ({ - page, - }) => { - expect(await tableDataCellValue(page, 0, 2)).toEqual( - "myfile.hl7", - ); - expect(await tableDataCellValue(page, 1, 2)).toEqual( - "None-03c3b7ab-7c65-4174-bea7-9195cbb7ed01-20240314174050.hl7", - ); - }); - - test("table column 'Records' has expected data", async ({ - page, - }) => { - expect(await tableDataCellValue(page, 0, 3)).toEqual("1"); - }); - - test("table column 'Status' has expected data", async ({ - page, - }) => { - expect(await tableDataCellValue(page, 0, 4)).toEqual( - "Success", - ); - }); - - test("table has pagination", async ({ page }) => { - await expect( - page.getByTestId("Submissions pagination"), - ).toBeAttached(); - }); - }); - - test("has footer", async ({ page }) => { - await expect(page.locator("footer")).toBeAttached(); - }); - }); - }); - - test.describe("receiver user", () => { - test.use({ storageState: "e2e/.auth/receiver.json" }); - - test.beforeEach(async ({ page }) => { - await submissionHistory.goto(page); - }); - - test("nav does not contain the Submissions option", async ({ - page, - }) => { - const navItems = page.locator(".usa-nav li"); - await expect(navItems).not.toContainText(["Submissions"]); - }); - - test("displays no data message", async ({ page }) => { - await expect(noData(page)).toBeAttached(); - }); - - test("has correct title", async ({ page }) => { - await expect(page).toHaveTitle(/Submission history/); - }); - - test("has footer", async ({ page }) => { - await expect(page.locator("footer")).toBeAttached(); - }); - }); - - test.describe("sender user", () => { - test.use({ storageState: "e2e/.auth/sender.json" }); - - test.beforeEach(async ({ page }) => { - await submissionHistory.mockGetSubmissionsResponse( - page, - TEST_ORG_IGNORE, - ); - await submissionHistory.mockGetReportHistoryResponse(page); - await submissionHistory.goto(page); - }); - - test("nav contains the Submission History option", async ({ page }) => { - const navItems = page.locator(".usa-nav li"); - await expect(navItems).toContainText(["Submission History"]); - }); - - test("has correct title", async ({ page }) => { - await expect(page).toHaveTitle(/Submission history/); - }); - - test("has filter", async ({ page }) => { - await expect(page.getByTestId("filter-container")).toBeAttached(); - }); - - test.describe("table", () => { - test("table has correct headers", async ({ page }) => { - await submissionHistory.tableHeaders(page); - }); - - test("table column 'ReportId' will open the report details", async ({ - page, - }) => { - const reportId = tableRows(page).nth(0).locator("td").nth(0); - await expect(reportId).toContainText(id); - await reportId.getByRole("link", { name: id }).click(); - - await openReportIdDetailPage(page, id); - }); - - test("table column 'Date/time submitted' has expected data", async ({ - page, - }) => { - expect(await tableDataCellValue(page, 0, 1)).toEqual( - "3/7/2024, 6:00:22 PM", - ); - }); - - test("table column 'Records' has expected data", async ({ - page, - }) => { - expect(await tableDataCellValue(page, 0, 3)).toEqual("1"); - }); - - test("table column 'Status' has expected data", async ({ - page, - }) => { - expect(await tableDataCellValue(page, 0, 4)).toEqual("Success"); - }); - - test("table has pagination", async ({ page }) => { - await expect( - page.getByTestId("Submissions pagination"), - ).toBeAttached(); - }); - }); - - test("has footer", async ({ page }) => { - await expect(page.locator("footer")).toBeAttached(); - }); - }); -}); diff --git a/frontend-react/e2e/spec/all/submissions-details-page.spec.ts b/frontend-react/e2e/spec/all/submissions-details-page.spec.ts deleted file mode 100644 index 7f4d76b3464..00000000000 --- a/frontend-react/e2e/spec/all/submissions-details-page.spec.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { expect, test } from "@playwright/test"; -import { selectTestOrg } from "../../helpers/utils"; -import * as reportDetails from "../../pages/report-details"; -import * as submissionDetails from "../../pages/submission-history"; -import { URL_SUBMISSION_HISTORY } from "../../pages/submission-history"; - -const id = "73e3cbc8-9920-4ab7-871f-843a1db4c074"; -test.describe("Submissions Details page", () => { - test.describe("not authenticated", () => { - test("redirects to login", async ({ page }) => { - await submissionDetails.gotoDetails(page, id); - await expect(page).toHaveURL("/login"); - }); - }); - - test.describe("admin user - happy path", () => { - test.use({ storageState: "e2e/.auth/admin.json" }); - - test.describe("without org selected", () => { - test.beforeEach(async ({ page }) => { - await reportDetails.mockGetSubmissionHistoryResponse(page, id); - await submissionDetails.gotoDetails(page, id); - }); - - test("has correct title", async ({ page }) => { - await submissionDetails.title(page); - }); - - test("has reportId in breadcrumb", async ({ page }) => { - await expect( - page.locator(".usa-breadcrumb ol li").nth(1), - ).toHaveText(`Details: ${id}`); - }); - - test("has footer", async ({ page }) => { - await expect(page.locator("footer")).toBeAttached(); - }); - }); - - test.describe("with org selected", () => { - test.beforeEach(async ({ page }) => { - await selectTestOrg(page); - await reportDetails.mockGetSubmissionHistoryResponse(page, id); - await submissionDetails.gotoDetails(page, id); - }); - - test("has correct title", async ({ page }) => { - await submissionDetails.title(page); - }); - - test("breadcrumb navigates to Submission History page", async ({ - page, - }) => { - await submissionDetails.breadcrumbLink( - page, - 0, - "Submissions", - URL_SUBMISSION_HISTORY, - ); - }); - - test("has footer", async ({ page }) => { - await expect(page.locator("footer")).toBeAttached(); - }); - }); - }); - - test.describe("admin user - server error", () => { - test.use({ storageState: "e2e/.auth/admin.json" }); - - test.beforeEach(async ({ page }) => { - await reportDetails.mockGetSubmissionHistoryResponse(page, id, 500); - await submissionDetails.gotoDetails(page, id); - }); - - test("has error message", async ({ page }) => { - await expect( - page.getByText(/An error has occurred./), - ).toBeAttached(); - }); - - test("has footer", async ({ page }) => { - await expect(page.locator("footer")).toBeAttached(); - }); - }); - - test.describe("sender user - happy path", () => { - test.use({ storageState: "e2e/.auth/sender.json" }); - - test.beforeEach(async ({ page }) => { - await reportDetails.mockGetSubmissionHistoryResponse(page, id); - await submissionDetails.gotoDetails(page, id); - }); - - test("has correct title", async ({ page }) => { - await submissionDetails.title(page); - }); - - test("has reportId in breadcrumb", async ({ page }) => { - await expect( - page.locator(".usa-breadcrumb ol li").nth(1), - ).toHaveText(`Details: ${id}`); - }); - - test("breadcrumb navigates to Submission History page", async ({ - page, - }) => { - await submissionDetails.breadcrumbLink( - page, - 0, - "Submissions", - URL_SUBMISSION_HISTORY, - ); - }); - - test("has footer", async ({ page }) => { - await expect(page.locator("footer")).toBeAttached(); - }); - }); - - test.describe("sender user - server error", () => { - test.use({ storageState: "e2e/.auth/sender.json" }); - - test.beforeEach(async ({ page }) => { - await reportDetails.mockGetSubmissionHistoryResponse(page, id, 500); - await submissionDetails.gotoDetails(page, id); - }); - - test("has error message", async ({ page }) => { - await expect( - page.getByText(/An error has occurred./), - ).toBeAttached(); - }); - - test("has footer", async ({ page }) => { - await expect(page.locator("footer")).toBeAttached(); - }); - }); - - test.describe("receiver user", () => { - test.use({ storageState: "e2e/.auth/receiver.json" }); - - test.beforeEach(async ({ page }) => { - await submissionDetails.gotoDetails(page, id); - }); - - test("has error message", async ({ page }) => { - await expect( - page.getByText(/An error has occurred./), - ).toBeAttached(); - }); - - test("has footer", async ({ page }) => { - await expect(page.locator("footer")).toBeAttached(); - }); - }); -}); diff --git a/frontend-react/e2e/spec/all/timezone.spec.ts b/frontend-react/e2e/spec/all/timezone.spec.ts index 8c7c8cd3597..273227412c4 100644 --- a/frontend-react/e2e/spec/all/timezone.spec.ts +++ b/frontend-react/e2e/spec/all/timezone.spec.ts @@ -1,5 +1,7 @@ -import { expect, test } from "@playwright/test"; import { endOfDay, startOfDay } from "date-fns"; +import { test as baseTest, expect } from "../../test"; + +const test = baseTest.extend({}); test("playwright/browser timezone parity", async ({ page }) => { const now = new Date(); @@ -7,9 +9,7 @@ test("playwright/browser timezone parity", async ({ page }) => { const browserNow = new Date(browserNowIso); const timezoneId = Intl.DateTimeFormat().resolvedOptions().timeZoneName; - const browserTimezoneId = await page.evaluate( - () => Intl.DateTimeFormat().resolvedOptions().timeZoneName, - ); + const browserTimezoneId = await page.evaluate(() => Intl.DateTimeFormat().resolvedOptions().timeZoneName); const nowStart = startOfDay(now); const browserStartIso = await page.evaluate(() => { diff --git a/frontend-react/e2e/spec/chromium-only/public-pages-link-check.spec.ts b/frontend-react/e2e/spec/chromium-only/public-pages-link-check.spec.ts index 9c5cd9d9865..8c615cfa158 100644 --- a/frontend-react/e2e/spec/chromium-only/public-pages-link-check.spec.ts +++ b/frontend-react/e2e/spec/chromium-only/public-pages-link-check.spec.ts @@ -1,8 +1,9 @@ /* eslint-disable playwright/no-conditional-in-test */ import axios, { AxiosError } from "axios"; import * as fs from "fs"; -import * as publicPagesLinkCheck from "../../pages/public-pages-link-check"; -import { expect, test } from "../../test"; +import { test as baseTest, expect } from "../../test"; + +const test = baseTest.extend({}); // To save bandwidth, this test is within the /spec/chromium-only/ folder // Since we're just checking link validity. This is specified within our @@ -18,6 +19,8 @@ test.describe("Evaluate links on public facing pages", { tag: "@warning" }, () = const normalizeUrl = (href: string, baseUrl: string) => new URL(href, baseUrl).toString(); // Using our sitemap.xml, we'll create a pathnames array + // We cannot use our POM, we must + // create context manually with browser.newContext() test.beforeAll(async ({ browser }) => { const page = await browser.newPage(); const response = await page.goto("/sitemap.xml"); @@ -52,7 +55,7 @@ test.describe("Evaluate links on public facing pages", { tag: "@warning" }, () = // Set test timeout to be 1 minute instead of 30 seconds test.setTimeout(60000); for (const path of urlPaths) { - await publicPagesLinkCheck.publicPageGoto(page, path); + await page.goto(path); const baseUrl = new URL(page.url()).origin; const allATags = await page.getByRole("link", { includeHidden: true }).elementHandles(); diff --git a/frontend-react/package.json b/frontend-react/package.json index fc58fd24fbc..7d6cb3b34bf 100644 --- a/frontend-react/package.json +++ b/frontend-react/package.json @@ -174,7 +174,7 @@ "msw": "^2.3.5", "msw-storybook-addon": "beta", "npm-run-all": "^4.1.5", - "otpauth": "^9.3.1", + "otpauth": "^9.3.2", "patch-package": "^8.0.0", "postcss": "^8.4.41", "prettier": "^3.3.3", diff --git a/frontend-react/yarn.lock b/frontend-react/yarn.lock index 948b48ef785..9349f9a4f9a 100644 --- a/frontend-react/yarn.lock +++ b/frontend-react/yarn.lock @@ -11263,12 +11263,12 @@ __metadata: languageName: node linkType: hard -"otpauth@npm:^9.3.1": - version: 9.3.1 - resolution: "otpauth@npm:9.3.1" +"otpauth@npm:^9.3.2": + version: 9.3.2 + resolution: "otpauth@npm:9.3.2" dependencies: "@noble/hashes": 1.4.0 - checksum: 36542e12a6bb43d6d9d446fe567e41ecfb16dc914aed56ce1ba0a2d03e9445994685cfeb0dd125a91c47ab3f5e669f8ef47a8dc3847b75e43ba0dbac66633ee7 + checksum: 66661f169a73de6df23b697bcdc23ec8395b16f1e66cc359fa3b77db468f5114f283517b7ff45ee903b7a95faf7b3c3045d2597c26c94cfea0e240d0a453f605 languageName: node linkType: hard @@ -12140,7 +12140,7 @@ __metadata: msw: ^2.3.5 msw-storybook-addon: beta npm-run-all: ^4.1.5 - otpauth: ^9.3.1 + otpauth: ^9.3.2 patch-package: ^8.0.0 postcss: ^8.4.41 prettier: ^3.3.3 diff --git a/operations/slack-boltjs-app b/operations/slack-boltjs-app index d123ef28e22..a5f682a3381 160000 --- a/operations/slack-boltjs-app +++ b/operations/slack-boltjs-app @@ -1 +1 @@ -Subproject commit d123ef28e22c52abc0ca27a5c65292010a18d204 +Subproject commit a5f682a3381fc1b5c07dce1a6a99350175e15719 diff --git a/prime-router/docs/onboarding-users/transport/rest.md b/prime-router/docs/onboarding-users/transport/rest.md index 18dbd8d0ac2..0c15b3527f6 100644 --- a/prime-router/docs/onboarding-users/transport/rest.md +++ b/prime-router/docs/onboarding-users/transport/rest.md @@ -23,14 +23,15 @@ For authentication, you need to do both step a and step b.
  1. Generate the "Credential in JSON format" for authentication
  2. -Currently, RESTTransport uses the one of the three options: +Currently, RESTTransport uses the one of the following options: i) UserPass, - ii) UserApiKey with JKS, or + ii) UserApiKey with JKS iii) UserApiKey with two-legged credential type to authenticate and obtain Bearer token from STLT. -User can use **primeCLI** command with credential-create option to generate the "Credential in JSON format" as given below. + iv) UserJks +User can use the **primeCLI** command with credential-create option to generate the "Credential in JSON format" as given below. -- With STLT's credential username and password given to us by STLT, user needs to run the following command to generate the UserPass credential type object: +- With STLT's credential username and password given to us by the STLT, user needs to run the following command to generate the UserPass credential type object: Command: ./prime credential-create --type UserPass --user --pass @@ -118,9 +119,9 @@ The receiver's RESTTransport includes the following fields: }" type: "REST" - c) See UserApiKey+Tow-legged RESTTransport setting Example below: + c) See UserApiKey+Two-legged RESTTransport setting Example below: - FLEXION--ETOR-SERVICE-RECEIVER uses UserApiKey + Tow-legged authentication type: + FLEXION--ETOR-SERVICE-RECEIVER uses UserApiKey + Two-legged authentication type: ================================================================================ transport: ! reportUrl: "https://sample.net/v1/etor/orders" @@ -139,6 +140,31 @@ The receiver's RESTTransport includes the following fields: sourceLabName: "CDC PRIME REPORTSTREAM" type: "REST" + d) See UserApiKey without OAuth RESTTransport setting Example below: + + FLEXION--ETOR-SERVICE-RECEIVER uses UserApiKey + ================================================================================ + transport: ! + reportUrl: "https://sample.net/v1/etor/orders" + authType: "apiKey" + tlsKeystore: null + headers: + Content-Type: "elims/json" + type: "REST" + + e) See JKS without OAuth RESTTransport setting Example below: + + CA-DPH--FULL-ELR-REST-JKS uses UserJKS + ================================================================================ + transport: ! + reportUrl: "https://sample.net/v1/etor/orders" + authType: "jks" + tlsKeystore: "jks" + tlsKeystore: "CA-DPH--FULL-ELR-REST-JKS" + headers: + Content-Type: "text/plain" + type: "REST" + ## 4. Final Step is to test/check the receiver's REST transport is connected successfully Now that you have completed/created REST Transport setting please do the following: - Create PR - which includes the receiver's setting code diff --git a/prime-router/docs/onboarding-users/transport/working-with-keys.md b/prime-router/docs/onboarding-users/transport/working-with-keys.md new file mode 100644 index 00000000000..9ec12cc09f6 --- /dev/null +++ b/prime-router/docs/onboarding-users/transport/working-with-keys.md @@ -0,0 +1,66 @@ +### Working with Keys + +#### Introduction +Each STLT has a unique configuration for server authentication. This documentation provides examples of how we've configured keys to successfully authenticate with different STLTs. + +### Public/Private Key Pair +Most STLTs use public/private key pairs for authentication. Here's how to generate and configure these keys: + +1. **Generate a PEM file:** + This command creates a PEM file containing both a private and public key: + ```bash + openssl genrsa -out my_rsa_private_key.pem 2048 + ``` + +2. **Extract the Public Key:** + To extract the public key from the PEM file and share it with the STLT, run the following command: + ```bash + ssh-keygen -y -f my_rsa_private_key.pem > my_rsa_public_key.pub + ``` + +3. **Convert PEM to PPK:** + The PEM file needs to be converted into a PPK file and stored in Azure for ReportStream authentication. Use this command to convert the file: + ```bash + puttygen my_rsa_private_key.pem -o my_rsa_private_key.ppk + ``` +4. **Create ReportStream Credential:** + Use the primeCLI create-credential command to store the ppk file in JSON to be able to store it in Azure so that ReportStream can use it. + ```bash + ./prime create-credential --type UserPpk --ppk-file /Users/vic/Downloads/texas/tx_rsa_private_key.ppk + ``` + +### STLT generates Public/Private key pair +Sometimes a STLT will generate and public private key pair and send them to ReportStream to authenticate. The format in which they are sent can differ from STLT to STLT. +Some STLTs will send a PFX file and that file will need to be converted to JKS so that ReportStream can use it. + +1. **Convert PFX to JKS:** + This command creates a PEM file containing both a private and public key: + ```bash + keytool -importkeystore -srckeystore mypfxfile.pfx -srcstoretype pkcs12 -destkeystore clientcert.jks -deststoretype JKS + ``` +2. **Create ReportStream Credential:** + Use the primeCLI create-credential command to store the jks file in JSON to be able to store it in Azure so that ReportStream can use it. + ```bash + ./prime credential-create --type UserJks --jks-use --jks-file-pass --jks-file + ``` + +### Importing STLTs Self-Signed Certificate to ReportStream + +As of the time this document was written, the process for importing self-signed certificates into ReportStream involves adding the certificate to ReportStream's Docker container. + +1. **Add the Certificate:** + Place the certificate in the following directory: + ``` + prime-reportstream/prime-router/certs/ + ``` + +2. **Update the Dockerfile:** + Add the certificate to the `prime-reportstream/prime-router/Dockerfile.dev` by including the following line: + ```bash + COPY ./certs/CDC-G2-S1.crt $JAVA_HOME/conf/security + RUN cd $JAVA_HOME/conf/security \ + && $JAVA_HOME/bin/keytool -cacerts -storepass changeit -noprompt -trustcacerts -importcert -alias -file + ``` + + + diff --git a/prime-router/src/main/kotlin/cli/tests/AuthTests.kt b/prime-router/src/main/kotlin/cli/tests/AuthTests.kt index d679e57fbfd..8a74452beee 100644 --- a/prime-router/src/main/kotlin/cli/tests/AuthTests.kt +++ b/prime-router/src/main/kotlin/cli/tests/AuthTests.kt @@ -26,11 +26,9 @@ import gov.cdc.prime.router.tokens.AuthUtils import gov.cdc.prime.router.tokens.DatabaseJtiCache import gov.cdc.prime.router.tokens.Scope import io.ktor.client.plugins.timeout -import io.ktor.client.request.accept import io.ktor.client.request.get import io.ktor.http.ContentType import io.ktor.http.HttpStatusCode -import kotlinx.coroutines.runBlocking import java.io.File import java.io.IOException import java.net.URLEncoder @@ -1172,107 +1170,84 @@ class Server2ServerAuthTests : CoolTest() { ) val orgEndpoint = "${environment.url}/api/settings/organizations" - val client = HttpClientUtils.createDefaultHttpClient( - userToken - ) - - val clientAdmin = HttpClientUtils.createDefaultHttpClient( - adminToken - ) - // Case: GET All Org Settings (Admin-only endpoint) // Unhappy Path: user on admin-only endpoint - val response = runBlocking { - client.get(orgEndpoint) { - timeout { - requestTimeoutMillis = 45000 - // default timeout is 15s; raising higher due to slow Function startup issues - } - accept(ContentType.Application.Json) - } - } + val response = HttpClientUtils.get( + url = orgEndpoint, + accessToken = userToken, + timeout = 45000, // default timeout is 15s; raising higher due to slow Function startup issues + acceptedContent = ContentType.Application.Json + ) if (response.status != HttpStatusCode.Unauthorized) { bad( "***$name Test settings/organizations Unhappy Path (user-GET All Orgs) FAILED:" + - " Expected HttpStatus ${HttpStatusCode.Unauthorized}. Got ${response.status.value}" + " Expected HttpStatus ${HttpStatusCode.Unauthorized}. Got ${response.status.value}" ) return false } // Happy Path: admin on admin-only endpoint - val response2 = runBlocking { - clientAdmin.get(orgEndpoint) { - timeout { - requestTimeoutMillis = 45000 - // default timeout is 15s; raising higher due to slow Function startup issues - } - accept(ContentType.Application.Json) - } - } + val response2 = HttpClientUtils.get( + url = orgEndpoint, + accessToken = adminToken, + timeout = 45000, // default timeout is 15s; raising higher due to slow Function startup issues + acceptedContent = ContentType.Application.Json + ) if (response2.status != HttpStatusCode.OK) { bad( "***$name Test settings/organizations Happy Path (admin-GET All Orgs) FAILED:" + - " Expected HttpStatus ${HttpStatusCode.OK}. Got ${response2.status.value}" + " Expected HttpStatus ${HttpStatusCode.OK}. Got ${response2.status.value}" ) return false } // Case: GET Receivers for an Org (Endpoint allowed for admins and members of the org) // Happy Path: user on user-allowed endpoint - val response3 = runBlocking { - client.get("$orgEndpoint/${authorizedOrg.name}/receivers") { - timeout { - requestTimeoutMillis = 45000 - // default timeout is 15s; raising higher due to slow Function startup issues - } - accept(ContentType.Application.Json) - } - } + val response3 = HttpClientUtils.get( + url = "$orgEndpoint/${authorizedOrg.name}/receivers", + accessToken = userToken, + timeout = 45000, // default timeout is 15s; raising higher due to slow Function startup issues + acceptedContent = ContentType.Application.Json + ) if (response3.status != HttpStatusCode.OK) { bad( "***$name Test settings/organizations Happy Path (user-GET Org Receivers) FAILED:" + - " Expected HttpStatus ${HttpStatusCode.OK}. Got ${response3.status.value}" + " Expected HttpStatus ${HttpStatusCode.OK}. Got ${response3.status.value}" ) return false } // Happy Path: admin on user-allowed endpoint - val response4 = runBlocking { - clientAdmin.get("$orgEndpoint/${authorizedOrg.name}/receivers") { - timeout { - requestTimeoutMillis = 45000 - // default timeout is 15s; raising higher due to slow Function startup issues - } - accept(ContentType.Application.Json) - } - } + val response4 = HttpClientUtils.get( + url = "$orgEndpoint/${authorizedOrg.name}/receivers", + accessToken = adminToken, + timeout = 45000, // default timeout is 15s; raising higher due to slow Function startup issues + acceptedContent = ContentType.Application.Json + ) if (response4.status != HttpStatusCode.OK) { bad( "***$name Test settings/organizations Happy Path (admin-GET Org Receivers) FAILED:" + - " Expected HttpStatus ${HttpStatusCode.OK}. Got ${response4.status.value}" + " Expected HttpStatus ${HttpStatusCode.OK}. Got ${response4.status.value}" ) return false } // UnhappyPath: user on an unauthorized org name - val response5 = runBlocking { - client.get("$orgEndpoint/${unauthorizedOrg.name}/receivers") { - timeout { - requestTimeoutMillis = 45000 - // default timeout is 15s; raising higher due to slow Function startup issues - } - accept(ContentType.Application.Json) - } - } + val response5 = HttpClientUtils.get( + url = "$orgEndpoint/${unauthorizedOrg.name}/receivers", + accessToken = userToken, + timeout = 45000, // default timeout is 15s; raising higher due to slow Function startup issues + acceptedContent = ContentType.Application.Json + ) if (response5.status != HttpStatusCode.Unauthorized) { bad( "***$name Test settings/organizations Unhappy Path (user-GET Unauthorized Org Receivers) FAILED:" + - " Expected HttpStatus ${HttpStatusCode.Unauthorized}. Got ${response5.status.value}" + " Expected HttpStatus ${HttpStatusCode.Unauthorized}. Got ${response5.status.value}" ) return false } diff --git a/prime-router/src/main/kotlin/common/HttpClientUtils.kt b/prime-router/src/main/kotlin/common/HttpClientUtils.kt index 4e80e8f64f2..b406ef0bcb3 100644 --- a/prime-router/src/main/kotlin/common/HttpClientUtils.kt +++ b/prime-router/src/main/kotlin/common/HttpClientUtils.kt @@ -5,11 +5,9 @@ import io.ktor.client.call.body import io.ktor.client.engine.apache.Apache import io.ktor.client.plugins.HttpTimeout import io.ktor.client.plugins.contentnegotiation.ContentNegotiation -import io.ktor.client.plugins.defaultRequest import io.ktor.client.plugins.timeout import io.ktor.client.request.accept import io.ktor.client.request.forms.submitForm -import io.ktor.client.request.header import io.ktor.client.request.headers import io.ktor.client.request.parameter import io.ktor.client.request.request @@ -32,10 +30,30 @@ class HttpClientUtils { const val REQUEST_TIMEOUT_MILLIS: Long = 130000 // need to be public to be used by inline const val SETTINGS_REQUEST_TIMEOUT_MILLIS = 30000 + private val httpClient: HttpClient = + HttpClient(Apache) { + install(ContentNegotiation) { + json( + Json { + prettyPrint = true + isLenient = true + ignoreUnknownKeys = true + } + ) + } + install(HttpTimeout) + engine { + followRedirects = true + socketTimeout = TIMEOUT + connectTimeout = TIMEOUT + connectionRequestTimeout = TIMEOUT + } + } + /** * GET (query resource) operation to the given endpoint resource [url] * @param url: required, the url to the resource endpoint - * @param tokens: null default, the access token needed to call the endpoint + * @param accessToken: null default, the access token needed to call the endpoint * @param headers: null default, the headers of the request * @param acceptedContent: default application/json the accepted content type * @param timeout: default to a system base value in millis @@ -73,7 +91,7 @@ class HttpClientUtils { /** * GET (query resource) operation to the given endpoint resource [url] * @param url: required, the url to the resource endpoint - * @param tokens: null default, the access token needed to call the endpoint + * @param accessToken: null default, the access token needed to call the endpoint * @param headers: null default, the headers of the request * @param acceptedContent: default application/json the accepted content type * @param timeout: default to a system base value in millis @@ -106,7 +124,7 @@ class HttpClientUtils { /** * PUT (modify resource) operation to the given endpoint resource [url] * @param url: required, the url to the resource endpoint - * @param tokens: null default, the access token needed to call the endpoint + * @param accessToken: null default, the access token needed to call the endpoint * @param headers: null default, the headers of the request * @param acceptedContent: default application/json the accepted content type * @param timeout: default to a system base value in millis @@ -147,7 +165,7 @@ class HttpClientUtils { /** * PUT (modify resource) operation to the given endpoint resource [url] * @param url: required, the url to the resource endpoint - * @param tokens: null default, the access token needed to call the endpoint + * @param accessToken: null default, the access token needed to call the endpoint * @param headers: null default, the headers of the request * @param acceptedContent: default application/json the accepted content type * @param timeout: default to a system base value in millis @@ -183,7 +201,7 @@ class HttpClientUtils { /** * POST (create resource) operation to the given endpoint resource [url] * @param url: required, the url to the resource endpoint - * @param tokens: null default, the access token needed to call the endpoint + * @param accessToken: null default, the access token needed to call the endpoint * @param headers: null default, the headers of the request * @param acceptedContent: default application/json the accepted content type * @param timeout: default to a system base value in millis @@ -223,7 +241,7 @@ class HttpClientUtils { /** * POST (create resource) operation to the given endpoint resource [url] * @param url: required, the url to the resource endpoint - * @param tokens: null default, the access token needed to call the endpoint + * @param accessToken: null default, the access token needed to call the endpoint * @param headers: null default, the headers of the request * @param acceptedContent: default application/json the accepted content type * @param timeout: default to a system base value in millis @@ -260,7 +278,7 @@ class HttpClientUtils { * Submit form to the endpoint as indicated by [url] * * @param url: required, the url to the resource endpoint - * @param tokens: null default, the access token needed to call the endpoint + * @param accessToken: null default, the access token needed to call the endpoint * @param headers: null default, the headers of the request * @param acceptedContent: default application/json the accepted content type * @param timeout: default to a system base value in millis @@ -294,7 +312,7 @@ class HttpClientUtils { * Submit form to the endpoint as indicated by [url] * * @param url: required, the url to the resource endpoint - * @param tokens: null default, the access token needed to call the endpoint + * @param accessToken: null default, the access token needed to call the endpoint * @param headers: null default, the headers of the request * @param acceptedContent: default application/json the accepted content type * @param timeout: default to a system base value in millis @@ -312,7 +330,7 @@ class HttpClientUtils { httpClient: HttpClient? = null, ): HttpResponse { return runBlocking { - (httpClient ?: createDefaultHttpClient(accessToken)).submitForm( + (httpClient ?: getDefaultHttpClient()).submitForm( url, formParameters = Parameters.build { formParams?.forEach { param -> @@ -331,7 +349,11 @@ class HttpClientUtils { } } } - + accessToken?.let { + headers { + append("Authorization", "Bearer $accessToken") + } + } accept(acceptedContent) } } @@ -340,7 +362,7 @@ class HttpClientUtils { /** * HEAD operation to the given endpoint resource [url] * @param url: required, the url to the resource endpoint - * @param tokens: null default, the access token needed to call the endpoint + * @param accessToken: null default, the access token needed to call the endpoint * @param headers: null default, the headers of the request * @param acceptedContent: default application/json the accepted content type * @param timeout: default to a system base value in millis @@ -376,9 +398,9 @@ class HttpClientUtils { /** * HEAD operation to the given endpoint resource [url] * @param url: required, the url to the resource endpoint - * @param tokens: null default, the access token needed to call the endpoint + * @param accessToken: null default, the access token needed to call the endpoint * @param headers: null default, the headers of the request - * @param acceptContent: default application/json the accepted content type + * @param acceptedContent: default application/json the accepted content type * @param timeout: default to a system base value in millis * @param queryParameters: null default, query parameters of the request * @param httpClient: null default, a http client injected by caller @@ -411,7 +433,7 @@ class HttpClientUtils { * A thin wrapper on top of the underlying 3rd party http client, e.g. ktor http client * with: * @param url: required, the url to the resource endpoint - * @param tokens: null default, the access token needed to call the endpoint + * @param accessToken: null default, the access token needed to call the endpoint * @param headers: null default, the headers of the request * @param acceptedContent: default application/json the accepted content type * @param timeout: default to a system base value in millis @@ -450,7 +472,7 @@ class HttpClientUtils { * A thin wrapper on top of the underlying 3rd party http client, e.g. ktor http client * with: * @param url: required, the url to the resource endpoint - * @param tokens: null default, the access token needed to call the endpoint + * @param accessToken: null default, the access token needed to call the endpoint * @param headers: null default, the headers of the request * @param acceptedContent: default application/json the accepted content type * @param timeout: default to a system base value in millis @@ -496,17 +518,16 @@ class HttpClientUtils { httpClient: HttpClient? = null, ): HttpResponse { return runBlocking { - (httpClient ?: createDefaultHttpClient(accessToken)).request(url) { + (httpClient ?: getDefaultHttpClient()).request(url) { this.method = method timeout { requestTimeoutMillis = timeout } url { queryParameters?.forEach { - parameter(it.key, it.value.toString()) + parameter(it.key, it.value) } } - headers?.let { headers { headers.forEach { @@ -514,6 +535,11 @@ class HttpClientUtils { } } } + accessToken?.let { + headers { + append("Authorization", "Bearer $accessToken") + } + } acceptedContent?.let { accept(acceptedContent) contentType(acceptedContent) @@ -526,48 +552,15 @@ class HttpClientUtils { } /** - * Create a http client with sensible default settings + * Get a http client with sensible default settings * note: most configuration parameters are overridable * e.g. expectSuccess default to false because most of the time * the caller wants to handle the whole range of response status - * @param bearerTokens null default, the access token needed to call the endpoint + * * @return a HttpClient with all sensible defaults */ - fun createDefaultHttpClient(accessToken: String?): HttpClient { - return HttpClient(Apache) { - // installs logging into the call to post to the server - // commented out - not to override underlying default logger settings - // enable to trace http client internals when needed - // install(Logging) { - // logger = Logger.SIMPLE - // level = LogLevel.INFO - // } - // not using Bearer Auth handler due to refresh token behavior - accessToken?.let { - defaultRequest { - header("Authorization", "Bearer $it") - } - } - install(ContentNegotiation) { - json( - Json { - prettyPrint = true - isLenient = true - ignoreUnknownKeys = true - } - ) - } - - install(HttpTimeout) - engine { - followRedirects = true - socketTimeout = TIMEOUT - connectTimeout = TIMEOUT - connectionRequestTimeout = TIMEOUT - customizeClient { - } - } - } + fun getDefaultHttpClient(): HttpClient { + return httpClient } } } \ No newline at end of file