From 1f138bb2de191f669e1e5c7c885d025cdfaac7f1 Mon Sep 17 00:00:00 2001 From: etanb Date: Fri, 13 Sep 2024 17:54:29 -0500 Subject: [PATCH 01/14] add logic to evaluate non http 400 responses --- .../public-pages-link-check.spec.ts | 18 +++++++- .../error/legacy-content/ErrorNoPage.tsx | 41 +++++++------------ 2 files changed, 31 insertions(+), 28 deletions(-) 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 a53fdbc0b7a..400a8e3db05 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,6 +1,7 @@ /* eslint-disable playwright/no-networkidle */ import axios, { AxiosError } from "axios"; import * as fs from "fs"; +import { pageNotFound } from "../../../src/pages/error/legacy-content/ErrorNoPage"; import { test as baseTest, expect } from "../../test"; const test = baseTest.extend({}); @@ -51,7 +52,7 @@ test.describe("Evaluate links on public facing pages", { tag: "@warning" }, () = frontendWarningsLogPath, isFrontendWarningsLog, }) => { - let aggregateHref = []; + let aggregateHref = ["/opfsefopkfokpepofks"]; // Set test timeout to be 1 minute instead of 30 seconds test.setTimeout(60000); for (const path of urlPaths) { @@ -82,9 +83,24 @@ test.describe("Evaluate links on public facing pages", { tag: "@warning" }, () = const validateLink = async (url: string) => { try { const response = await axiosInstance.get(url); + const pageContent = response.data; + + // For internal links, we cannot check for a 400 response code directly + // and must check if the content includes the "Page not found" string + if (pageContent.includes(pageNotFound) && pageContent.includes('data-testid="error-page-wrapper"')) { + const errorString = "Internal link: Page not found"; + console.error(`Error accessing ${url}:`, errorString); + const warning = { url: url, message: errorString }; + warnings.push(warning); + return { url, status: 404 }; + } + + // Return the status if everything is fine return { url, status: response.status }; } catch (error) { const e = error as AxiosError; + + // Log the error message and status for failed requests (4xx or 5xx) console.error(`Error accessing ${url}:`, e.message); const warning = { url: url, message: e.message }; warnings.push(warning); diff --git a/frontend-react/src/pages/error/legacy-content/ErrorNoPage.tsx b/frontend-react/src/pages/error/legacy-content/ErrorNoPage.tsx index a076f75d3f4..514d44ebe9b 100644 --- a/frontend-react/src/pages/error/legacy-content/ErrorNoPage.tsx +++ b/frontend-react/src/pages/error/legacy-content/ErrorNoPage.tsx @@ -4,47 +4,38 @@ import { useNavigate } from "react-router-dom"; import site from "../../../content/site.json"; +export const pageNotFound = "Page not found"; + export const ErrorNoPage = () => { const navigate = useNavigate(); return ( <> - Page Not Found | {import.meta.env.VITE_TITLE} + + {pageNotFound} | {import.meta.env.VITE_TITLE} + -
+
-

Page not found

+

{pageNotFound}

- We’re sorry, we can’t find the page you're - looking for. It might have been removed, changed - names, or is otherwise unavailable. + We’re sorry, we can’t find the page you're looking for. It might have been removed, + changed names, or is otherwise unavailable.

- If you typed the URL directly, check your - spelling and capitalization. Our URLs look like - this:{" "} - - reportstream.cdc.gov/example-one - - . + If you typed the URL directly, check your spelling and capitalization. Our URLs look + like this: reportstream.cdc.gov/example-one.

- Visit our homepage or contact us at{" "} - {site.orgs.RS.email} and we’ll point you in the + Visit our homepage or contact us at {site.orgs.RS.email} and we’ll point you in the right direction.{" "}

  • -
  • @@ -52,11 +43,7 @@ export const ErrorNoPage = () => { From 610e0aefddb72fa29ee9a0e70cbb276d1063a831 Mon Sep 17 00:00:00 2001 From: etanb Date: Wed, 18 Sep 2024 14:27:42 -0700 Subject: [PATCH 02/14] update link checker logic to read page content --- .../e2e/spec/chromium-only/public-pages-link-check.spec.ts | 4 ++-- frontend-react/src/content/error/ErrorMessages.ts | 5 +++-- .../src/pages/error/legacy-content/ErrorNoPage.tsx | 3 +-- 3 files changed, 6 insertions(+), 6 deletions(-) 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 400a8e3db05..2394a0ed1c8 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,7 +1,7 @@ /* eslint-disable playwright/no-networkidle */ import axios, { AxiosError } from "axios"; import * as fs from "fs"; -import { pageNotFound } from "../../../src/pages/error/legacy-content/ErrorNoPage"; +import { pageNotFound } from "../../../src/content/error/ErrorMessages"; import { test as baseTest, expect } from "../../test"; const test = baseTest.extend({}); @@ -52,7 +52,7 @@ test.describe("Evaluate links on public facing pages", { tag: "@warning" }, () = frontendWarningsLogPath, isFrontendWarningsLog, }) => { - let aggregateHref = ["/opfsefopkfokpepofks"]; + let aggregateHref = []; // Set test timeout to be 1 minute instead of 30 seconds test.setTimeout(60000); for (const path of urlPaths) { diff --git a/frontend-react/src/content/error/ErrorMessages.ts b/frontend-react/src/content/error/ErrorMessages.ts index b6ebe5ea076..4e0e5586e05 100644 --- a/frontend-react/src/content/error/ErrorMessages.ts +++ b/frontend-react/src/content/error/ErrorMessages.ts @@ -11,8 +11,7 @@ export interface ParagraphWithTitle { export type ErrorDisplayMessage = ParagraphWithTitle | string; /** Default message for an error */ -export const GENERIC_ERROR_STRING = - "Our apologies, there was an error loading this content."; +export const GENERIC_ERROR_STRING = "Our apologies, there was an error loading this content."; /** Default content for an error page */ export const GENERIC_ERROR_PAGE_CONFIG: ErrorDisplayMessage = { header: "An error has occurred", @@ -21,3 +20,5 @@ export const GENERIC_ERROR_PAGE_CONFIG: ErrorDisplayMessage = { have been automatically notified and will be looking into this with the utmost urgency.`, }; + +export const pageNotFound = "Page not found"; diff --git a/frontend-react/src/pages/error/legacy-content/ErrorNoPage.tsx b/frontend-react/src/pages/error/legacy-content/ErrorNoPage.tsx index 514d44ebe9b..aceb33d7c51 100644 --- a/frontend-react/src/pages/error/legacy-content/ErrorNoPage.tsx +++ b/frontend-react/src/pages/error/legacy-content/ErrorNoPage.tsx @@ -2,10 +2,9 @@ import { Button } from "@trussworks/react-uswds"; import { Helmet } from "react-helmet-async"; import { useNavigate } from "react-router-dom"; +import { pageNotFound } from "../../../content/error/ErrorMessages"; import site from "../../../content/site.json"; -export const pageNotFound = "Page not found"; - export const ErrorNoPage = () => { const navigate = useNavigate(); return ( From f16b64b0f781ed71ac87879ecddf6360903a6511 Mon Sep 17 00:00:00 2001 From: etanb Date: Thu, 19 Sep 2024 17:16:06 -0700 Subject: [PATCH 03/14] completely redo link checker logic --- frontend-react/404.html | 12 +- frontend-react/e2e/helpers/utils.ts | 11 ++ .../public-pages-link-check.spec.ts | 91 +++++---- frontend-react/package.json | 2 +- frontend-react/src/AppRouter.tsx | 185 ++++-------------- frontend-react/src/content/home/index.mdx | 2 +- 6 files changed, 118 insertions(+), 185 deletions(-) diff --git a/frontend-react/404.html b/frontend-react/404.html index 6e352fe8836..3250d693a92 100644 --- a/frontend-react/404.html +++ b/frontend-react/404.html @@ -5,10 +5,14 @@ - %VITE_TITLE% - - - + Page not found + + + + diff --git a/frontend-react/e2e/helpers/utils.ts b/frontend-react/e2e/helpers/utils.ts index eaa20ef8e30..75570e0cb17 100644 --- a/frontend-react/e2e/helpers/utils.ts +++ b/frontend-react/e2e/helpers/utils.ts @@ -154,3 +154,14 @@ export function removeDateTime(filename: string) { return filename; } + +export function isAbsoluteURL(url: string): boolean { + return /^(https?|ftp|file|mailto):/.test(url); +} + +export function isAssetURL(url: string): boolean { + // Regular expression to match common asset file extensions at the end of a URL + const assetExtensions = /\.(pdf|png|jpg|jpeg|gif|bmp|svg|webp|mp4|mp3|wav|ogg|avi|mov|mkv|zip|rar|tar|gz|iso)$/i; + + return assetExtensions.test(url); +} 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 2394a0ed1c8..fb8d2200a84 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 @@ -2,7 +2,8 @@ import axios, { AxiosError } from "axios"; import * as fs from "fs"; import { pageNotFound } from "../../../src/content/error/ErrorMessages"; -import { test as baseTest, expect } from "../../test"; +import { isAbsoluteURL, isAssetURL } from "../../helpers/utils"; +import { test as baseTest, Browser, chromium, expect } from "../../test"; const test = baseTest.extend({}); @@ -17,7 +18,6 @@ const test = baseTest.extend({}); test.describe("Evaluate links on public facing pages", { tag: "@warning" }, () => { let urlPaths: string[] = []; - 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 @@ -51,6 +51,7 @@ test.describe("Evaluate links on public facing pages", { tag: "@warning" }, () = page, frontendWarningsLogPath, isFrontendWarningsLog, + baseURL, }) => { let aggregateHref = []; // Set test timeout to be 1 minute instead of 30 seconds @@ -59,7 +60,6 @@ test.describe("Evaluate links on public facing pages", { tag: "@warning" }, () = await page.goto(path, { waitUntil: "networkidle", }); - const baseUrl = new URL(page.url()).origin; const allATags = await page.getByRole("link", { includeHidden: true }).elementHandles(); @@ -67,11 +67,12 @@ test.describe("Evaluate links on public facing pages", { tag: "@warning" }, () = const href = await aTag.getAttribute("href"); // ONLY include http, https and relative path names if (href && /^(https?:|\/)/.test(href)) { - aggregateHref.push(normalizeUrl(href, baseUrl)); + aggregateHref.push(href); } } } - // Remove any link duplicates to save resources + + // Remove duplicate links aggregateHref = [...new Set(aggregateHref)]; const axiosInstance = axios.create({ @@ -80,39 +81,61 @@ test.describe("Evaluate links on public facing pages", { tag: "@warning" }, () = const warnings: { url: string; message: string }[] = []; - const validateLink = async (url: string) => { - try { - const response = await axiosInstance.get(url); - const pageContent = response.data; - - // For internal links, we cannot check for a 400 response code directly - // and must check if the content includes the "Page not found" string - if (pageContent.includes(pageNotFound) && pageContent.includes('data-testid="error-page-wrapper"')) { - const errorString = "Internal link: Page not found"; - console.error(`Error accessing ${url}:`, errorString); - const warning = { url: url, message: errorString }; - warnings.push(warning); - return { url, status: 404 }; + const validateLink = async (browser: Browser, url: string) => { + // Our app does not properly handle 200 vs 400 HTTP codes for our pages + // so we cannot simply use Axios since it's an HTTP client only. + // This means we must actually navigate to the page(s) with Playwright + // to then decipher the rendered HTML DOM content to then determine + // if the page is valid or not. isAbsoluteURL determines if the page + // is an internal link or external one by determining if it's an + // absolute URL or a relative URL. + if (isAbsoluteURL(url) || isAssetURL(url)) { + try { + const normalizedURL = new URL(url, baseURL).toString(); + const response = await axiosInstance.get(normalizedURL); + return { url, status: response.status }; + } catch (error) { + const e = error as AxiosError; + console.error(`Error accessing external link ${url}:`, e.message); + warnings.push({ url, message: e.message }); + return { url, status: e.response ? e.response.status : 400 }; + } + } else { + const page = await browser.newPage(); + + try { + await page.goto(url, { waitUntil: "networkidle" }); + + const pageContent = await page.content(); + const hasPageNotFoundText = pageContent.includes(pageNotFound); + const isErrorWrapperVisible = await page.locator('[data-testid="error-page-wrapper"]').isVisible(); + + if (hasPageNotFoundText && isErrorWrapperVisible) { + console.error(`Error accessing ${url}: Page not found`); + warnings.push({ url, message: "Internal link: Page not found" }); + return { url, status: 404 }; + } + + return { url, status: 200 }; + } catch (error) { + console.error(`Error accessing internal link ${url}: Page error`); + warnings.push({ url, message: "Internal link: Page error" }); + return { url, status: 400 }; + } finally { + await page.close(); } - - // Return the status if everything is fine - return { url, status: response.status }; - } catch (error) { - const e = error as AxiosError; - - // Log the error message and status for failed requests (4xx or 5xx) - console.error(`Error accessing ${url}:`, e.message); - const warning = { url: url, message: e.message }; - warnings.push(warning); - - return { - url, - status: e.response ? e.response.status : "Request failed", - }; } }; - const results = await Promise.all(aggregateHref.map((href) => validateLink(href))); + // Since we're trying to parallelize these tests by using Promise.all + // we need multiple, separate instances of the Playwright browser + const browser = await chromium.launch(); + let results; + try { + results = await Promise.all(aggregateHref.map((href) => validateLink(browser, href))); + } finally { + await browser.close(); + } if (isFrontendWarningsLog && warnings.length > 0) { fs.writeFileSync(frontendWarningsLogPath, `${JSON.stringify(warnings)}\n`); diff --git a/frontend-react/package.json b/frontend-react/package.json index 74e016d89d4..4bb4608f0d0 100644 --- a/frontend-react/package.json +++ b/frontend-react/package.json @@ -72,7 +72,7 @@ "test:debug": "cross-env DEBUG_PRINT_LIMIT=100000 vitest --run --no-file-parallelism", "test:ci": "cross-env VITE_BACKEND_URL=http://localhost vitest --coverage", "test:ui": "cross-env vitest --ui", - "test:e2e": "playwright test", + "test:e2e": "playwright test --trace on", "test:e2e-smoke": "MOCK_DISABLED=true playwright test --project chromium --grep @smoke", "test:e2e-ui": "playwright test --ui", "test:e2e-ui:smoke": "MOCK_DISABLED=true playwright test --project chromium --grep @smoke --ui", diff --git a/frontend-react/src/AppRouter.tsx b/frontend-react/src/AppRouter.tsx index 2a65608fb01..b6d41cd6487 100644 --- a/frontend-react/src/AppRouter.tsx +++ b/frontend-react/src/AppRouter.tsx @@ -9,172 +9,85 @@ import { PERMISSIONS } from "./utils/UsefulTypes"; /* Content Pages */ const Home = lazy(lazyRouteMarkdown(() => import("./content/home/index.mdx"))); -const About = lazy( - lazyRouteMarkdown(() => import("./content/about/index.mdx")), -); -const OurNetwork = lazy( - lazyRouteMarkdown(() => import("./content/about/our-network.mdx")), -); -const Roadmap = lazy( - lazyRouteMarkdown(() => import("./content/about/roadmap.mdx")), -); +const About = lazy(lazyRouteMarkdown(() => import("./content/about/index.mdx"))); +const OurNetwork = lazy(lazyRouteMarkdown(() => import("./content/about/our-network.mdx"))); +const Roadmap = lazy(lazyRouteMarkdown(() => import("./content/about/roadmap.mdx"))); const News = lazy(lazyRouteMarkdown(() => import("./content/about/news.mdx"))); -const Security = lazy( - lazyRouteMarkdown(() => import("./content/about/security.mdx")), -); -const ReleaseNotes = lazy( - lazyRouteMarkdown(() => import("./content/about/release-notes.mdx")), -); -const CaseStudies = lazy( - lazyRouteMarkdown(() => import("./content/about/case-studies.mdx")), -); +const Security = lazy(lazyRouteMarkdown(() => import("./content/about/security.mdx"))); +const ReleaseNotes = lazy(lazyRouteMarkdown(() => import("./content/about/release-notes.mdx"))); +const CaseStudies = lazy(lazyRouteMarkdown(() => import("./content/about/case-studies.mdx"))); const ReferHealthcareOrganizations = lazy( - lazyRouteMarkdown( - () => - import( - "./content/managing-your-connection/refer-healthcare-organizations.mdx" - ), - ), + lazyRouteMarkdown(() => import("./content/managing-your-connection/refer-healthcare-organizations.mdx")), ); -const GettingStartedSendingData = lazy( - lazyRouteMarkdown( - () => import("./content/getting-started/sending-data.mdx"), - ), -); +const GettingStartedSendingData = lazy(lazyRouteMarkdown(() => import("./content/getting-started/sending-data.mdx"))); const GettingStartedReceivingData = lazy( - lazyRouteMarkdown( - () => import("./content/getting-started/receiving-data.mdx"), - ), + lazyRouteMarkdown(() => import("./content/getting-started/receiving-data.mdx")), ); const ReportStreamApiIndex = lazy( - lazyRouteMarkdown( - () => - import( - "./content/developer-resources/reportstream-api/ReportStreamApi.mdx" - ), - ), -); -const DeveloperResourcesIndex = lazy( - lazyRouteMarkdown( - () => import("./content/developer-resources/index-page.mdx"), - ), + lazyRouteMarkdown(() => import("./content/developer-resources/reportstream-api/ReportStreamApi.mdx")), ); +const DeveloperResourcesIndex = lazy(lazyRouteMarkdown(() => import("./content/developer-resources/index-page.mdx"))); const ReportStreamApiGettingStarted = lazy( lazyRouteMarkdown( - () => - import( - "./content/developer-resources/reportstream-api/getting-started/GettingStarted.mdx" - ), + () => import("./content/developer-resources/reportstream-api/getting-started/GettingStarted.mdx"), ), ); const ReportStreamApiDocumentation = lazy( - lazyRouteMarkdown( - () => - import( - "./content/developer-resources/reportstream-api/documentation/Documentation.mdx" - ), - ), + lazyRouteMarkdown(() => import("./content/developer-resources/reportstream-api/documentation/Documentation.mdx")), ); const ReportStreamApiDocumentationResponses = lazy( lazyRouteMarkdown( - () => - import( - "./content/developer-resources/reportstream-api/documentation/ResponsesFromReportStream.mdx" - ), + () => import("./content/developer-resources/reportstream-api/documentation/ResponsesFromReportStream.mdx"), ), ); const ManagingYourConnectionIndex = lazy( - lazyRouteMarkdown( - () => import("./content/managing-your-connection/index.mdx"), - ), -); -const SupportIndex = lazy( - lazyRouteMarkdown(() => import("./content/support/index.mdx")), + lazyRouteMarkdown(() => import("./content/managing-your-connection/index.mdx")), ); +const SupportIndex = lazy(lazyRouteMarkdown(() => import("./content/support/index.mdx"))); const ReportStreamApiDocumentationPayloads = lazy( lazyRouteMarkdown( - () => - import( - "./content/developer-resources/reportstream-api/documentation/SamplePayloadsAndOutput.mdx" - ), + () => import("./content/developer-resources/reportstream-api/documentation/SamplePayloadsAndOutput.mdx"), ), ); /* Public Pages */ const TermsOfService = lazy(() => import("./pages/TermsOfService")); -const LoginCallback = lazy( - () => import("./shared/LoginCallback/LoginCallback"), -); -const LogoutCallback = lazy( - () => import("./shared/LogoutCallback/LogoutCallback"), -); +const LoginCallback = lazy(() => import("./shared/LoginCallback/LoginCallback")); +const LogoutCallback = lazy(() => import("./shared/LogoutCallback/LogoutCallback")); const Login = lazy(() => import("./pages/Login")); -const ErrorNoPage = lazy( - () => import("./pages/error/legacy-content/ErrorNoPage"), -); +const ErrorNoPage = lazy(() => import("./pages/error/legacy-content/ErrorNoPage")); /* Auth Pages */ const FeatureFlagsPage = lazy(() => import("./pages/misc/FeatureFlags")); -const SubmissionDetailsPage = lazy( - () => import("./pages/submissions/SubmissionDetails"), -); +const SubmissionDetailsPage = lazy(() => import("./pages/submissions/SubmissionDetails")); const SubmissionsPage = lazy(() => import("./pages/submissions/Submissions")); const AdminMainPage = lazy(() => import("./pages/admin/AdminMain")); const AdminOrgNewPage = lazy(() => import("./pages/admin/AdminOrgNew")); const AdminOrgEditPage = lazy(() => import("./pages/admin/AdminOrgEdit")); -const EditSenderSettingsPage = lazy( - () => import("./components/Admin/EditSenderSettings"), -); +const EditSenderSettingsPage = lazy(() => import("./components/Admin/EditSenderSettings")); const AdminLMFPage = lazy(() => import("./pages/admin/AdminLastMileFailures")); -const AdminMessageTrackerPage = lazy( - () => import("./pages/admin/AdminMessageTracker"), -); +const AdminMessageTrackerPage = lazy(() => import("./pages/admin/AdminMessageTracker")); const AdminReceiverDashPage = lazy( - () => - import( - "./pages/admin/receiver-dashboard/AdminReceiverDashboardPage/AdminReceiverDashboardPage" - ), -); -const DeliveryDetailPage = lazy( - () => import("./pages/deliveries/details/DeliveryDetail"), -); -const ValueSetsDetailPage = lazy( - () => import("./pages/admin/value-set-editor/ValueSetsDetail"), -); -const ValueSetsIndexPage = lazy( - () => import("./pages/admin/value-set-editor/ValueSetsIndex"), + () => import("./pages/admin/receiver-dashboard/AdminReceiverDashboardPage/AdminReceiverDashboardPage"), ); +const DeliveryDetailPage = lazy(() => import("./pages/deliveries/details/DeliveryDetail")); +const ValueSetsDetailPage = lazy(() => import("./pages/admin/value-set-editor/ValueSetsDetail")); +const ValueSetsIndexPage = lazy(() => import("./pages/admin/value-set-editor/ValueSetsIndex")); const DeliveriesPage = lazy(() => import("./pages/deliveries/Deliveries")); -const EditReceiverSettingsPage = lazy( - () => import("./components/Admin/EditReceiverSettings"), -); +const EditReceiverSettingsPage = lazy(() => import("./components/Admin/EditReceiverSettings")); const AdminRevHistoryPage = lazy(() => import("./pages/admin/AdminRevHistory")); -const MessageDetailsPage = lazy( - () => import("./components/MessageTracker/MessageDetails"), -); -const ManagePublicKeyPage = lazy( - () => import("./components/ManagePublicKey/ManagePublicKey"), -); -const DataDashboardPage = lazy( - () => import("./pages/data-dashboard/DataDashboard"), -); -const ReportDetailsPage = lazy( - () => import("./components/DataDashboard/ReportDetails/ReportDetails"), -); +const MessageDetailsPage = lazy(() => import("./components/MessageTracker/MessageDetails")); +const ManagePublicKeyPage = lazy(() => import("./components/ManagePublicKey/ManagePublicKey")); +const DataDashboardPage = lazy(() => import("./pages/data-dashboard/DataDashboard")); +const ReportDetailsPage = lazy(() => import("./components/DataDashboard/ReportDetails/ReportDetails")); const FacilitiesProvidersPage = lazy( - () => - import( - "./components/DataDashboard/FacilitiesProviders/FacilitiesProviders" - ), + () => import("./components/DataDashboard/FacilitiesProviders/FacilitiesProviders"), ); const FacilityProviderSubmitterDetailsPage = lazy( - () => - import( - "./components/DataDashboard/FacilityProviderSubmitterDetails/FacilityProviderSubmitterDetails" - ), + () => import("./components/DataDashboard/FacilityProviderSubmitterDetails/FacilityProviderSubmitterDetails"), ); const NewSettingPage = lazy(() => import("./components/Admin/NewSetting")); @@ -343,9 +256,7 @@ export const appRoutes: RouteObject[] = [ children: [ { path: "", - element: ( - - ), + element: , index: true, handle: { isContentPage: true, @@ -353,18 +264,14 @@ export const appRoutes: RouteObject[] = [ }, { path: "responses-from-reportstream", - element: ( - - ), + element: , handle: { isContentPage: true, }, }, { path: "sample-payloads-and-output", - element: ( - - ), + element: , handle: { isContentPage: true, }, @@ -491,27 +398,15 @@ export const appRoutes: RouteObject[] = [ }, { path: "facility/:senderId", - element: ( - - ), + element: , }, { path: "provider/:senderId", - element: ( - - ), + element: , }, { path: "submitter/:senderId", - element: ( - - ), + element: , }, ], }, diff --git a/frontend-react/src/content/home/index.mdx b/frontend-react/src/content/home/index.mdx index 5d196b9e14c..c17cecde963 100644 --- a/frontend-react/src/content/home/index.mdx +++ b/frontend-react/src/content/home/index.mdx @@ -25,7 +25,7 @@ import site from "../site.json";
    -
    +

    How we help you

    From 57ca6066bb9a3853606bde6a96d07c1faf999ddd Mon Sep 17 00:00:00 2001 From: etanb Date: Mon, 23 Sep 2024 00:38:10 -0700 Subject: [PATCH 04/14] revert capitalization --- frontend-react/src/content/error/ErrorMessages.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend-react/src/content/error/ErrorMessages.ts b/frontend-react/src/content/error/ErrorMessages.ts index 4e0e5586e05..671b4529388 100644 --- a/frontend-react/src/content/error/ErrorMessages.ts +++ b/frontend-react/src/content/error/ErrorMessages.ts @@ -21,4 +21,4 @@ export const GENERIC_ERROR_PAGE_CONFIG: ErrorDisplayMessage = { urgency.`, }; -export const pageNotFound = "Page not found"; +export const pageNotFound = "Page Not Found"; From 6a12f8d4796f41a3f766e855898da437c20abad8 Mon Sep 17 00:00:00 2001 From: etanb Date: Mon, 23 Sep 2024 01:06:18 -0700 Subject: [PATCH 05/14] double test timeout --- .../e2e/spec/chromium-only/public-pages-link-check.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 fb8d2200a84..4892f74878c 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 @@ -55,7 +55,7 @@ test.describe("Evaluate links on public facing pages", { tag: "@warning" }, () = }) => { let aggregateHref = []; // Set test timeout to be 1 minute instead of 30 seconds - test.setTimeout(60000); + test.setTimeout(120000); for (const path of urlPaths) { await page.goto(path, { waitUntil: "networkidle", From 9eb9f55b2d6f2ae17ac3ea143ebeb810dcc9ac61 Mon Sep 17 00:00:00 2001 From: etanb Date: Tue, 24 Sep 2024 15:16:17 -0700 Subject: [PATCH 06/14] make sure expect does not block --- .../chromium-only/public-pages-link-check.spec.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) 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 4892f74878c..529e01baca9 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 @@ -101,7 +101,8 @@ test.describe("Evaluate links on public facing pages", { tag: "@warning" }, () = return { url, status: e.response ? e.response.status : 400 }; } } else { - const page = await browser.newPage(); + const context = await browser.newContext(); + const page = await context.newPage(); try { await page.goto(url, { waitUntil: "networkidle" }); @@ -123,6 +124,7 @@ test.describe("Evaluate links on public facing pages", { tag: "@warning" }, () = return { url, status: 400 }; } finally { await page.close(); + await context.close(); } } }; @@ -138,15 +140,12 @@ test.describe("Evaluate links on public facing pages", { tag: "@warning" }, () = } if (isFrontendWarningsLog && warnings.length > 0) { - fs.writeFileSync(frontendWarningsLogPath, `${JSON.stringify(warnings)}\n`); + await fs.promises.writeFile(frontendWarningsLogPath, `${JSON.stringify(warnings)}\n`); } results.forEach((result) => { - try { - expect(result.status).toBe(200); - } catch (error) { - const e = error as AxiosError; - console.warn(`Non-fatal: ${e.message}`); + if (result.status !== 200) { + console.warn(`Warning: ${result.url} returned status ${result.status}`); } }); }); From 2f28d735847382d3e34403c39605e3b2d1da5e3d Mon Sep 17 00:00:00 2001 From: etanb Date: Thu, 26 Sep 2024 08:08:54 -0700 Subject: [PATCH 07/14] remove so many console.error --- .../e2e/spec/chromium-only/public-pages-link-check.spec.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) 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 529e01baca9..9082ce43282 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 @@ -96,7 +96,6 @@ test.describe("Evaluate links on public facing pages", { tag: "@warning" }, () = return { url, status: response.status }; } catch (error) { const e = error as AxiosError; - console.error(`Error accessing external link ${url}:`, e.message); warnings.push({ url, message: e.message }); return { url, status: e.response ? e.response.status : 400 }; } @@ -112,14 +111,12 @@ test.describe("Evaluate links on public facing pages", { tag: "@warning" }, () = const isErrorWrapperVisible = await page.locator('[data-testid="error-page-wrapper"]').isVisible(); if (hasPageNotFoundText && isErrorWrapperVisible) { - console.error(`Error accessing ${url}: Page not found`); warnings.push({ url, message: "Internal link: Page not found" }); return { url, status: 404 }; } return { url, status: 200 }; } catch (error) { - console.error(`Error accessing internal link ${url}: Page error`); warnings.push({ url, message: "Internal link: Page error" }); return { url, status: 400 }; } finally { @@ -140,7 +137,7 @@ test.describe("Evaluate links on public facing pages", { tag: "@warning" }, () = } if (isFrontendWarningsLog && warnings.length > 0) { - await fs.promises.writeFile(frontendWarningsLogPath, `${JSON.stringify(warnings)}\n`); + fs.writeFileSync(frontendWarningsLogPath, `${JSON.stringify(warnings)}\n`); } results.forEach((result) => { From 66606cf544e40d88b9d19e7ab247db21ced6d9ea Mon Sep 17 00:00:00 2001 From: etanb Date: Thu, 26 Sep 2024 10:40:47 -0700 Subject: [PATCH 08/14] formal expect statement --- .../spec/chromium-only/public-pages-link-check.spec.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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 9082ce43282..d6b4cfd4634 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 @@ -27,7 +27,7 @@ test.describe("Evaluate links on public facing pages", { tag: "@warning" }, () = const response = await page.goto("/sitemap.xml"); const sitemapXml = await response!.text(); // Since we don't want to use any external XML parsing libraries, - // we can use page.evaluate, but that creates it's own execution context + // we can use page.evaluate, but that creates its own execution context // wherein we need to explicitly return something, which is why // we have the convoluted // elem.textContent ? new URL(elem.textContent).pathname : null, @@ -44,7 +44,7 @@ test.describe("Evaluate links on public facing pages", { tag: "@warning" }, () = }); test("Check if paths were fetched", () => { - expect(urlPaths.length).toBeGreaterThan(0); + expect(urlPaths.length).toBeGreaterThan(0); // Ensure that paths were fetched correctly }); test("Check all public-facing URLs and their links for a valid 200 response", async ({ @@ -145,5 +145,9 @@ test.describe("Evaluate links on public facing pages", { tag: "@warning" }, () = console.warn(`Warning: ${result.url} returned status ${result.status}`); } }); + + // Required expect statement + if somehow the warnings and number of links + // are the same, that's a huge problem. + expect(warnings.length).toBeLessThan(aggregateHref.length); }); }); From aa21731e4a56aeb391d7e8956bb2135b07d8d451 Mon Sep 17 00:00:00 2001 From: etanb Date: Thu, 26 Sep 2024 11:37:57 -0700 Subject: [PATCH 09/14] change uppercase --- frontend-react/src/content/error/ErrorMessages.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend-react/src/content/error/ErrorMessages.ts b/frontend-react/src/content/error/ErrorMessages.ts index 671b4529388..4e0e5586e05 100644 --- a/frontend-react/src/content/error/ErrorMessages.ts +++ b/frontend-react/src/content/error/ErrorMessages.ts @@ -21,4 +21,4 @@ export const GENERIC_ERROR_PAGE_CONFIG: ErrorDisplayMessage = { urgency.`, }; -export const pageNotFound = "Page Not Found"; +export const pageNotFound = "Page not found"; From c973ad3c03444b17599ef98489f49d5d0076b96c Mon Sep 17 00:00:00 2001 From: etanb Date: Fri, 27 Sep 2024 09:40:23 -0700 Subject: [PATCH 10/14] try p limit --- .../public-pages-link-check.spec.ts | 24 ++++++++++++------- frontend-react/package.json | 1 + frontend-react/yarn.lock | 17 +++++++++++++ 3 files changed, 34 insertions(+), 8 deletions(-) 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 d6b4cfd4634..77dcf69c9a2 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,5 +1,6 @@ /* eslint-disable playwright/no-networkidle */ import axios, { AxiosError } from "axios"; +import pLimit from "p-limit"; import * as fs from "fs"; import { pageNotFound } from "../../../src/content/error/ErrorMessages"; import { isAbsoluteURL, isAssetURL } from "../../helpers/utils"; @@ -104,7 +105,8 @@ test.describe("Evaluate links on public facing pages", { tag: "@warning" }, () = const page = await context.newPage(); try { - await page.goto(url, { waitUntil: "networkidle" }); + const absoluteUrl = new URL(url, baseURL).toString(); + await page.goto(absoluteUrl, { waitUntil: "networkidle" }); const pageContent = await page.content(); const hasPageNotFoundText = pageContent.includes(pageNotFound); @@ -129,13 +131,19 @@ test.describe("Evaluate links on public facing pages", { tag: "@warning" }, () = // Since we're trying to parallelize these tests by using Promise.all // we need multiple, separate instances of the Playwright browser const browser = await chromium.launch(); - let results; - try { - results = await Promise.all(aggregateHref.map((href) => validateLink(browser, href))); - } finally { - await browser.close(); - } - + const limit = pLimit(10); + const results = await Promise.all( + aggregateHref.map((href) => + limit(async () => { + try { + return await validateLink(browser, href); + } catch (error) { + console.error(`Error validating link: ${href}`, error); + return { url: href, status: 500 }; + } + }), + ), + ); if (isFrontendWarningsLog && warnings.length > 0) { fs.writeFileSync(frontendWarningsLogPath, `${JSON.stringify(warnings)}\n`); } diff --git a/frontend-react/package.json b/frontend-react/package.json index 4bb4608f0d0..47d201b9a26 100644 --- a/frontend-react/package.json +++ b/frontend-react/package.json @@ -25,6 +25,7 @@ "history": "^5.3.0", "html-to-text": "^9.0.5", "lodash": "^4.17.21", + "p-limit": "^6.1.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-helmet-async": "^2.0.5", diff --git a/frontend-react/yarn.lock b/frontend-react/yarn.lock index 262a13d07be..c22b4708f09 100644 --- a/frontend-react/yarn.lock +++ b/frontend-react/yarn.lock @@ -11429,6 +11429,15 @@ __metadata: languageName: node linkType: hard +"p-limit@npm:^6.1.0": + version: 6.1.0 + resolution: "p-limit@npm:6.1.0" + dependencies: + yocto-queue: ^1.1.1 + checksum: 0c98d8fc1006b70fc7423232a47e8d026dc69279b06fe7ff8b4c0cc8023de2b6bb8991b609d93c3dec691a7a362ab0f0157df521d931a01fec192a5e404b9ee5 + languageName: node + linkType: hard + "p-locate@npm:^3.0.0": version: 3.0.0 resolution: "p-locate@npm:3.0.0" @@ -12268,6 +12277,7 @@ __metadata: msw-storybook-addon: beta npm-run-all: ^4.1.5 otpauth: ^9.3.2 + p-limit: ^6.1.0 patch-package: ^8.0.0 postcss: ^8.4.45 prettier: ^3.3.3 @@ -15612,6 +15622,13 @@ __metadata: languageName: node linkType: hard +"yocto-queue@npm:^1.1.1": + version: 1.1.1 + resolution: "yocto-queue@npm:1.1.1" + checksum: f2e05b767ed3141e6372a80af9caa4715d60969227f38b1a4370d60bffe153c9c5b33a862905609afc9b375ec57cd40999810d20e5e10229a204e8bde7ef255c + languageName: node + linkType: hard + "zwitch@npm:^2.0.0": version: 2.0.2 resolution: "zwitch@npm:2.0.2" From fb23c1e4624c6f821755924c3fa73f516be40cda Mon Sep 17 00:00:00 2001 From: etanb Date: Fri, 27 Sep 2024 12:01:56 -0700 Subject: [PATCH 11/14] replace page not found with variable --- .../admin/last-mile-failures-page.spec.ts | 5 +- .../admin/message-id-search-page.spec.ts | 5 +- .../organization-settings-edit-page.spec.ts | 119 +++++++-- .../admin/organization-settings-page.spec.ts | 13 +- .../admin/receiver-status-page.spec.ts | 236 +++++++++--------- 5 files changed, 217 insertions(+), 161 deletions(-) diff --git a/frontend-react/e2e/spec/all/authenticated/admin/last-mile-failures-page.spec.ts b/frontend-react/e2e/spec/all/authenticated/admin/last-mile-failures-page.spec.ts index 60bc2032f68..c6b94cbb9cd 100644 --- a/frontend-react/e2e/spec/all/authenticated/admin/last-mile-failures-page.spec.ts +++ b/frontend-react/e2e/spec/all/authenticated/admin/last-mile-failures-page.spec.ts @@ -1,3 +1,4 @@ +import { pageNotFound } from "../../../../../src/content/error/ErrorMessages"; import { tableRows } from "../../../../helpers/utils"; import { LastMileFailuresPage } from "../../../../pages/authenticated/admin/last-mile-failures"; import { test as baseTest, expect } from "../../../../test"; @@ -95,7 +96,7 @@ test.describe("Last Mile Failure page", () => { test.use({ storageState: "e2e/.auth/receiver.json" }); test("returns Page Not Found", async ({ lastMileFailuresPage }) => { - await expect(lastMileFailuresPage.page).toHaveTitle(/Page Not Found/); + await expect(lastMileFailuresPage.page).toHaveTitle(new RegExp(pageNotFound)); }); }); @@ -103,7 +104,7 @@ test.describe("Last Mile Failure page", () => { test.use({ storageState: "e2e/.auth/sender.json" }); test("returns Page Not Found", async ({ lastMileFailuresPage }) => { - await expect(lastMileFailuresPage.page).toHaveTitle(/Page Not Found/); + await expect(lastMileFailuresPage.page).toHaveTitle(new RegExp(pageNotFound)); }); }); diff --git a/frontend-react/e2e/spec/all/authenticated/admin/message-id-search-page.spec.ts b/frontend-react/e2e/spec/all/authenticated/admin/message-id-search-page.spec.ts index 6af81d230eb..f5f19cc8620 100644 --- a/frontend-react/e2e/spec/all/authenticated/admin/message-id-search-page.spec.ts +++ b/frontend-react/e2e/spec/all/authenticated/admin/message-id-search-page.spec.ts @@ -1,3 +1,4 @@ +import { pageNotFound } from "../../../../../src/content/error/ErrorMessages"; import { noData, tableRows } from "../../../../helpers/utils"; import { MOCK_GET_MESSAGES } from "../../../../mocks/messages"; import { MessageIDSearchPage } from "../../../../pages/authenticated/admin/message-id-search"; @@ -160,7 +161,7 @@ test.describe("Message ID Search Page", () => { messageIDSearchPage.mockError = true; await messageIDSearchPage.reload(); - await expect(messageIDSearchPage.page).toHaveTitle(/Page Not Found/); + await expect(messageIDSearchPage.page).toHaveTitle(new RegExp(pageNotFound)); }); }); @@ -171,7 +172,7 @@ test.describe("Message ID Search Page", () => { messageIDSearchPage.mockError = true; await messageIDSearchPage.reload(); - await expect(messageIDSearchPage.page).toHaveTitle(/Page Not Found/); + await expect(messageIDSearchPage.page).toHaveTitle(new RegExp(pageNotFound)); }); }); }); diff --git a/frontend-react/e2e/spec/all/authenticated/admin/organization-settings-edit-page.spec.ts b/frontend-react/e2e/spec/all/authenticated/admin/organization-settings-edit-page.spec.ts index 4068bc091dd..ab33d105683 100644 --- a/frontend-react/e2e/spec/all/authenticated/admin/organization-settings-edit-page.spec.ts +++ b/frontend-react/e2e/spec/all/authenticated/admin/organization-settings-edit-page.spec.ts @@ -1,4 +1,5 @@ import { expect } from "@playwright/test"; +import { pageNotFound } from "../../../../../src/content/error/ErrorMessages"; import { tableDataCellValue } from "../../../../helpers/utils"; import { MOCK_GET_ORGANIZATION_IGNORE } from "../../../../mocks/organizations"; import { OrganizationEditPage } from "../../../../pages/authenticated/admin/organization-edit"; @@ -47,14 +48,14 @@ test.describe("Organization Edit Page", () => { test.describe("receiver user", () => { test.use({ storageState: "e2e/.auth/receiver.json" }); test("returns Page Not Found", async ({ organizationEditPage }) => { - await expect(organizationEditPage.page).toHaveTitle(/Page Not Found/); + await expect(organizationEditPage.page).toHaveTitle(new RegExp(pageNotFound)); }); }); test.describe("sender user", () => { test.use({ storageState: "e2e/.auth/sender.json" }); test("returns Page Not Found", async ({ organizationEditPage }) => { - await expect(organizationEditPage.page).toHaveTitle(/Page Not Found/); + await expect(organizationEditPage.page).toHaveTitle(new RegExp(pageNotFound)); }); }); @@ -84,18 +85,24 @@ test.describe("Organization Edit Page", () => { test("has expected 'Meta'", async ({ organizationEditPage }) => { const meta = organizationEditPage.page.getByTestId("gridContainer").getByTestId("grid").nth(2); await expect(meta).toHaveText(organizationEditPage.getOrgMeta(MOCK_GET_ORGANIZATION_IGNORE)); - }); + }); test("has expected 'Description'", async ({ organizationEditPage }) => { - await expect(organizationEditPage.page.getByTestId("description")).toHaveValue(MOCK_GET_ORGANIZATION_IGNORE.description); + await expect(organizationEditPage.page.getByTestId("description")).toHaveValue( + MOCK_GET_ORGANIZATION_IGNORE.description, + ); }); test("has expected 'Jurisdiction'", async ({ organizationEditPage }) => { - await expect(organizationEditPage.page.getByTestId("jurisdiction")).toHaveValue(MOCK_GET_ORGANIZATION_IGNORE.jurisdiction); + await expect(organizationEditPage.page.getByTestId("jurisdiction")).toHaveValue( + MOCK_GET_ORGANIZATION_IGNORE.jurisdiction, + ); }); test("has expected 'Filters'", async ({ organizationEditPage }) => { - await expect(organizationEditPage.page.getByTestId("filters")).toHaveValue(JSON.stringify(MOCK_GET_ORGANIZATION_IGNORE.filters, null, 2)); + await expect(organizationEditPage.page.getByTestId("filters")).toHaveValue( + JSON.stringify(MOCK_GET_ORGANIZATION_IGNORE.filters, null, 2), + ); }); }); @@ -129,9 +136,12 @@ test.describe("Organization Edit Page", () => { test.describe("'Organization Sender Settings' section", () => { test("can create a new organization sender", async ({ organizationEditPage }) => { await organizationEditPage.page - .locator('#orgsendersettings').getByRole('link', { name: 'New' }) + .locator("#orgsendersettings") + .getByRole("link", { name: "New" }) .click(); - await expect(organizationEditPage.page).toHaveURL(`/admin/orgnewsetting/org/ignore/settingtype/sender`); + await expect(organizationEditPage.page).toHaveURL( + `/admin/orgnewsetting/org/ignore/settingtype/sender`, + ); await expect(organizationEditPage.page.getByText(/Org name: ignore/)).toBeVisible(); await expect(organizationEditPage.page.getByText(/Setting Type: sender/)).toBeVisible(); @@ -141,9 +151,20 @@ test.describe("Organization Edit Page", () => { }); test("can edit an organization sender", async ({ organizationEditPage }) => { - const firstOrgSender = await organizationEditPage.page.locator("#orgsendersettings").nth(0).locator("td").nth(0).innerText(); - await organizationEditPage.page.locator('#orgsendersettings').getByRole('link', { name: 'Edit' }).nth(0).click(); - await expect(organizationEditPage.page).toHaveURL(`/admin/orgsendersettings/org/ignore/sender/${firstOrgSender}/action/edit`); + const firstOrgSender = await organizationEditPage.page + .locator("#orgsendersettings") + .nth(0) + .locator("td") + .nth(0) + .innerText(); + await organizationEditPage.page + .locator("#orgsendersettings") + .getByRole("link", { name: "Edit" }) + .nth(0) + .click(); + await expect(organizationEditPage.page).toHaveURL( + `/admin/orgsendersettings/org/ignore/sender/${firstOrgSender}/action/edit`, + ); await expect(organizationEditPage.page.getByText(`Org name: ignore`)).toBeVisible(); await expect(organizationEditPage.page.getByText(`Sender name: ${firstOrgSender}`)).toBeVisible(); @@ -162,15 +183,24 @@ test.describe("Organization Edit Page", () => { const orgSenderLocator = firstOrgSender.replace("-", "_"); - await expect(organizationEditPage.page.locator(`#id_Item__${orgSenderLocator}__has_been_saved`).getByTestId("alerttoast")).toHaveText(`Item '${firstOrgSender}' has been saved`); + await expect( + organizationEditPage.page + .locator(`#id_Item__${orgSenderLocator}__has_been_saved`) + .getByTestId("alerttoast"), + ).toHaveText(`Item '${firstOrgSender}' has been saved`); await expect(organizationEditPage.page).toHaveURL(organizationEditPage.url); }); test("can cancel when editing an organization sender", async ({ organizationEditPage }) => { const firstOrgSender = await tableDataCellValue(organizationEditPage.page, 0, 0); - await organizationEditPage.page. - locator('#orgsendersettings').getByRole('link', { name: 'Edit' }).nth(0).click(); - await expect(organizationEditPage.page).toHaveURL(`/admin/orgsendersettings/org/ignore/sender/${firstOrgSender}/action/edit`); + await organizationEditPage.page + .locator("#orgsendersettings") + .getByRole("link", { name: "Edit" }) + .nth(0) + .click(); + await expect(organizationEditPage.page).toHaveURL( + `/admin/orgsendersettings/org/ignore/sender/${firstOrgSender}/action/edit`, + ); await expect(organizationEditPage.page.getByText(`Org name: ignore`)).toBeVisible(); await expect(organizationEditPage.page.getByText(`Sender name: ${firstOrgSender}`)).toBeVisible(); @@ -182,9 +212,12 @@ test.describe("Organization Edit Page", () => { test.describe("'Organization Receiver Settings' section", () => { test("can create a new organization receiver", async ({ organizationEditPage }) => { await organizationEditPage.page - .locator('#orgreceiversettings').getByRole('link', { name: 'New' }) + .locator("#orgreceiversettings") + .getByRole("link", { name: "New" }) .click(); - await expect(organizationEditPage.page).toHaveURL(`/admin/orgnewsetting/org/ignore/settingtype/receiver`); + await expect(organizationEditPage.page).toHaveURL( + `/admin/orgnewsetting/org/ignore/settingtype/receiver`, + ); await expect(organizationEditPage.page.getByText(/Org name: ignore/)).toBeVisible(); await expect(organizationEditPage.page.getByText(/Setting Type: receiver/)).toBeVisible(); @@ -194,11 +227,24 @@ test.describe("Organization Edit Page", () => { }); test("can edit an organization receiver", async ({ organizationEditPage }) => { - const firstOrgReceiver = await organizationEditPage.page.locator("#orgreceiversettings").nth(0).locator("td").nth(0).innerText(); - await organizationEditPage.page.locator('#orgreceiversettings').getByRole('link', { name: 'Edit' }).nth(0).click(); - await expect(organizationEditPage.page).toHaveURL(`/admin/orgreceiversettings/org/ignore/receiver/${firstOrgReceiver}/action/edit`); + const firstOrgReceiver = await organizationEditPage.page + .locator("#orgreceiversettings") + .nth(0) + .locator("td") + .nth(0) + .innerText(); + await organizationEditPage.page + .locator("#orgreceiversettings") + .getByRole("link", { name: "Edit" }) + .nth(0) + .click(); + await expect(organizationEditPage.page).toHaveURL( + `/admin/orgreceiversettings/org/ignore/receiver/${firstOrgReceiver}/action/edit`, + ); await expect(organizationEditPage.page.getByText(`Org name: ignore`)).toBeVisible(); - await expect(organizationEditPage.page.getByText(`Receiver name: ${firstOrgReceiver}`)).toBeVisible(); + await expect( + organizationEditPage.page.getByText(`Receiver name: ${firstOrgReceiver}`), + ).toBeVisible(); await organizationEditPage.orgReceiverEdit.editJsonButton.click(); const modal = organizationEditPage.page.getByTestId("modalWindow").nth(0); @@ -208,23 +254,42 @@ test.describe("Organization Edit Page", () => { await expect(organizationEditPage.orgReceiverEdit.editJsonModal.save).toHaveAttribute("disabled"); await organizationEditPage.orgReceiverEdit.editJsonModal.checkSyntax.click(); - await expect(organizationEditPage.orgReceiverEdit.editJsonModal.save).not.toHaveAttribute("disabled"); + await expect(organizationEditPage.orgReceiverEdit.editJsonModal.save).not.toHaveAttribute( + "disabled", + ); await organizationEditPage.orgReceiverEdit.editJsonModal.save.click(); await expect(modal).toBeHidden(); const orgReceiverLocator = firstOrgReceiver.replace("-", "_"); - await expect(organizationEditPage.page.locator(`#id_Item__${orgReceiverLocator}__has_been_updated`).getByTestId("alerttoast")).toHaveText(`Item '${firstOrgReceiver}' has been updated`); + await expect( + organizationEditPage.page + .locator(`#id_Item__${orgReceiverLocator}__has_been_updated`) + .getByTestId("alerttoast"), + ).toHaveText(`Item '${firstOrgReceiver}' has been updated`); await expect(organizationEditPage.page).toHaveURL(organizationEditPage.url); }); test("can cancel when editing an organization receiver", async ({ organizationEditPage }) => { - const firstOrgReceiver = await organizationEditPage.page.locator("#orgreceiversettings").nth(0).locator("td").nth(0).innerText(); - await organizationEditPage.page.locator('#orgreceiversettings').getByRole('link', { name: 'Edit' }).nth(0).click(); - await expect(organizationEditPage.page).toHaveURL(`/admin/orgreceiversettings/org/ignore/receiver/${firstOrgReceiver}/action/edit`); + const firstOrgReceiver = await organizationEditPage.page + .locator("#orgreceiversettings") + .nth(0) + .locator("td") + .nth(0) + .innerText(); + await organizationEditPage.page + .locator("#orgreceiversettings") + .getByRole("link", { name: "Edit" }) + .nth(0) + .click(); + await expect(organizationEditPage.page).toHaveURL( + `/admin/orgreceiversettings/org/ignore/receiver/${firstOrgReceiver}/action/edit`, + ); await expect(organizationEditPage.page.getByText(`Org name: ignore`)).toBeVisible(); - await expect(organizationEditPage.page.getByText(`Receiver name: ${firstOrgReceiver}`)).toBeVisible(); + await expect( + organizationEditPage.page.getByText(`Receiver name: ${firstOrgReceiver}`), + ).toBeVisible(); await organizationEditPage.orgReceiverEdit.cancelButton.click(); await expect(organizationEditPage.page).toHaveURL(organizationEditPage.url); diff --git a/frontend-react/e2e/spec/all/authenticated/admin/organization-settings-page.spec.ts b/frontend-react/e2e/spec/all/authenticated/admin/organization-settings-page.spec.ts index 70c8cf8079f..cb9ee546ffe 100644 --- a/frontend-react/e2e/spec/all/authenticated/admin/organization-settings-page.spec.ts +++ b/frontend-react/e2e/spec/all/authenticated/admin/organization-settings-page.spec.ts @@ -2,6 +2,7 @@ import { expect } from "@playwright/test"; import { readFileSync } from "node:fs"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; +import { pageNotFound } from "../../../../../src/content/error/ErrorMessages"; import { MOCK_GET_ORGANIZATION_SETTINGS_LIST } from "../../../../mocks/organizations"; import { OrganizationPage } from "../../../../pages/authenticated/admin/organization"; import { test as baseTest } from "../../../../test"; @@ -51,14 +52,14 @@ test.describe("Admin Organization Settings Page", () => { test.describe("receiver user", () => { test.use({ storageState: "e2e/.auth/receiver.json" }); test("returns Page Not Found", async ({ organizationPage }) => { - await expect(organizationPage.page).toHaveTitle(/Page Not Found/); + await expect(organizationPage.page).toHaveTitle(new RegExp(pageNotFound)); }); }); test.describe("sender user", () => { test.use({ storageState: "e2e/.auth/sender.json" }); test("returns Page Not Found", async ({ organizationPage }) => { - await expect(organizationPage.page).toHaveTitle(/Page Not Found/); + await expect(organizationPage.page).toHaveTitle(new RegExp(pageNotFound)); }); }); @@ -79,8 +80,8 @@ test.describe("Admin Organization Settings Page", () => { test.describe("when there is no error", () => { test("nav contains the 'Admin tools' dropdown with 'Organization Settings' option", async ({ - organizationPage, - }) => { + organizationPage, + }) => { const navItems = organizationPage.page.locator(".usa-nav li"); await expect(navItems).toContainText(["Admin tools"]); @@ -126,8 +127,8 @@ test.describe("Admin Organization Settings Page", () => { i === 0 ? MOCK_GET_ORGANIZATION_SETTINGS_LIST[0] : (MOCK_GET_ORGANIZATION_SETTINGS_LIST.find((i) => i.name === cols[0]) ?? { - name: "INVALID", - }); + name: "INVALID", + }); // if first row, we expect column headers. else, the data row matching id (name) // SetEdit is text of buttons in button column const expectedColContents = diff --git a/frontend-react/e2e/spec/all/authenticated/admin/receiver-status-page.spec.ts b/frontend-react/e2e/spec/all/authenticated/admin/receiver-status-page.spec.ts index edba29a56c0..5f9049f8a09 100644 --- a/frontend-react/e2e/spec/all/authenticated/admin/receiver-status-page.spec.ts +++ b/frontend-react/e2e/spec/all/authenticated/admin/receiver-status-page.spec.ts @@ -1,6 +1,7 @@ -import {addDays, endOfDay, startOfDay, subDays} from "date-fns"; -import {AdminReceiverStatusPage} from "../../../../pages/authenticated/admin/receiver-status"; -import {test as baseTest, expect, logins} from "../../../../test"; +import { addDays, endOfDay, startOfDay, subDays } from "date-fns"; +import { pageNotFound } from "../../../../../src/content/error/ErrorMessages"; +import { AdminReceiverStatusPage } from "../../../../pages/authenticated/admin/receiver-status"; +import { test as baseTest, expect, logins } from "../../../../test"; export interface AdminReceiverStatusPageFixtures { adminReceiverStatusPage: AdminReceiverStatusPage; @@ -38,29 +39,29 @@ const test = baseTest.extend({ test.describe("Admin Receiver Status Page", () => { test.describe("not authenticated", () => { - test("redirects to login", async ({adminReceiverStatusPage}) => { + test("redirects to login", async ({ adminReceiverStatusPage }) => { await expect(adminReceiverStatusPage.page).toHaveURL("/login"); }); }); test.describe("authenticated receiver", () => { - test.use({storageState: logins.receiver.path}); - test("returns Page Not Found", async ({adminReceiverStatusPage}) => { - await expect(adminReceiverStatusPage.page).toHaveTitle(/Page Not Found/); + test.use({ storageState: logins.receiver.path }); + test("returns Page Not Found", async ({ adminReceiverStatusPage }) => { + await expect(adminReceiverStatusPage.page).toHaveTitle(new RegExp(pageNotFound)); }); }); test.describe("authenticated sender", () => { - test.use({storageState: logins.sender.path}); - test("returns Page Not Found", async ({adminReceiverStatusPage}) => { - await expect(adminReceiverStatusPage.page).toHaveTitle(/Page Not Found/); + test.use({ storageState: logins.sender.path }); + test("returns Page Not Found", async ({ adminReceiverStatusPage }) => { + await expect(adminReceiverStatusPage.page).toHaveTitle(new RegExp(pageNotFound)); }); }); test.describe("authenticated admin", () => { - test.use({storageState: logins.admin.path}); + test.use({ storageState: logins.admin.path }); - test("If there is an error, the error is shown on the page", async ({adminReceiverStatusPage}) => { + test("If there is an error, the error is shown on the page", async ({ adminReceiverStatusPage }) => { adminReceiverStatusPage.mockError = true; await adminReceiverStatusPage.reload(); @@ -68,72 +69,66 @@ test.describe("Admin Receiver Status Page", () => { }); test.describe("Header", () => { - test( - "has correct title + heading", - async ({adminReceiverStatusPage}) => { - await adminReceiverStatusPage.testHeader(); - }, - ); + test("has correct title + heading", async ({ adminReceiverStatusPage }) => { + await adminReceiverStatusPage.testHeader(); + }); }); test.describe("When there is no error", () => { test.describe("Displays correctly", () => { - test.describe( - "filters", - () => { - test("date range", async ({adminReceiverStatusPage}) => { - const {button, label, modalOverlay, valueDisplay} = - adminReceiverStatusPage.filterFormInputs.dateRange; - await expect(label).toBeVisible(); - await expect(button).toBeVisible(); - await expect(valueDisplay).toHaveText(adminReceiverStatusPage.expectedDateRangeLabelText); - await expect(modalOverlay).toBeHidden(); - }); + test.describe("filters", () => { + test("date range", async ({ adminReceiverStatusPage }) => { + const { button, label, modalOverlay, valueDisplay } = + adminReceiverStatusPage.filterFormInputs.dateRange; + await expect(label).toBeVisible(); + await expect(button).toBeVisible(); + await expect(valueDisplay).toHaveText(adminReceiverStatusPage.expectedDateRangeLabelText); + await expect(modalOverlay).toBeHidden(); + }); - test("receiver name", async ({adminReceiverStatusPage}) => { - const {input, expectedTooltipText, label, tooltip, expectedDefaultValue} = - adminReceiverStatusPage.filterFormInputs.receiverName; - await expect(label).toBeVisible(); - await expect(input).toBeVisible(); - await expect(input).toHaveValue(expectedDefaultValue); - - await expect(tooltip).toBeHidden(); - await input.hover(); - await expect(tooltip).toBeVisible(); - await expect(tooltip).toHaveText(expectedTooltipText); - }); + test("receiver name", async ({ adminReceiverStatusPage }) => { + const { input, expectedTooltipText, label, tooltip, expectedDefaultValue } = + adminReceiverStatusPage.filterFormInputs.receiverName; + await expect(label).toBeVisible(); + await expect(input).toBeVisible(); + await expect(input).toHaveValue(expectedDefaultValue); + + await expect(tooltip).toBeHidden(); + await input.hover(); + await expect(tooltip).toBeVisible(); + await expect(tooltip).toHaveText(expectedTooltipText); + }); - test("results message", async ({adminReceiverStatusPage}) => { - const {input, expectedTooltipText, label, tooltip, expectedDefaultValue} = - adminReceiverStatusPage.filterFormInputs.resultMessage; - await expect(label).toBeVisible(); - await expect(input).toBeVisible(); - await expect(input).toHaveValue(expectedDefaultValue); - - await expect(tooltip).toBeHidden(); - await input.hover(); - await expect(tooltip).toBeVisible(); - await expect(tooltip).toHaveText(expectedTooltipText); - }); + test("results message", async ({ adminReceiverStatusPage }) => { + const { input, expectedTooltipText, label, tooltip, expectedDefaultValue } = + adminReceiverStatusPage.filterFormInputs.resultMessage; + await expect(label).toBeVisible(); + await expect(input).toBeVisible(); + await expect(input).toHaveValue(expectedDefaultValue); + + await expect(tooltip).toBeHidden(); + await input.hover(); + await expect(tooltip).toBeVisible(); + await expect(tooltip).toHaveText(expectedTooltipText); + }); - test("success type", async ({adminReceiverStatusPage}) => { - const {input, expectedTooltipText, label, tooltip, expectedDefaultValue} = - adminReceiverStatusPage.filterFormInputs.successType; - await expect(label).toBeVisible(); - await expect(input).toBeVisible(); - await expect(input).toHaveValue(expectedDefaultValue); - - await expect(tooltip).toBeHidden(); - await input.hover(); - await expect(tooltip).toBeVisible(); - await expect(tooltip).toHaveText(expectedTooltipText); - }); - }, - ); + test("success type", async ({ adminReceiverStatusPage }) => { + const { input, expectedTooltipText, label, tooltip, expectedDefaultValue } = + adminReceiverStatusPage.filterFormInputs.successType; + await expect(label).toBeVisible(); + await expect(input).toBeVisible(); + await expect(input).toHaveValue(expectedDefaultValue); + + await expect(tooltip).toBeHidden(); + await input.hover(); + await expect(tooltip).toBeVisible(); + await expect(tooltip).toHaveText(expectedTooltipText); + }); + }); // Failures here indicate potential misalignment of playwright/browser timezone test.describe("receiver statuses", () => { - test("time periods", async ({adminReceiverStatusPage}) => { + test("time periods", async ({ adminReceiverStatusPage }) => { const result = await adminReceiverStatusPage.testReceiverStatusDisplay(); expect(result).toBe(true); }); @@ -142,64 +137,57 @@ test.describe("Admin Receiver Status Page", () => { test.describe("Footer", () => { test("has footer and explicit scroll to footer and scroll to top", async ({ - adminReceiverStatusPage, - }) => { + adminReceiverStatusPage, + }) => { await adminReceiverStatusPage.testFooter(); }); }); test.describe("Functions correctly", () => { test.describe("filters", () => { - test.describe( - "date range", - () => { - test("works through calendar", async ({adminReceiverStatusPage}) => { - const {valueDisplay} = adminReceiverStatusPage.filterFormInputs.dateRange; - const now = new Date(); - const targetFrom = startOfDay(subDays(now, 3)); - const targetTo = addDays(endOfDay(now), 1); - - const reqUrl = await adminReceiverStatusPage.updateFilters({ - dateRange: { - value: [targetFrom, targetTo], - }, - }); - expect(reqUrl).toBeDefined(); - - await expect(valueDisplay).toHaveText( - adminReceiverStatusPage.expectedDateRangeLabelText, - ); - expect(Object.fromEntries(reqUrl!.searchParams.entries())).toMatchObject({ - start_date: targetFrom.toISOString(), - end_date: targetTo.toISOString(), - }); + test.describe("date range", () => { + test("works through calendar", async ({ adminReceiverStatusPage }) => { + const { valueDisplay } = adminReceiverStatusPage.filterFormInputs.dateRange; + const now = new Date(); + const targetFrom = startOfDay(subDays(now, 3)); + const targetTo = addDays(endOfDay(now), 1); + + const reqUrl = await adminReceiverStatusPage.updateFilters({ + dateRange: { + value: [targetFrom, targetTo], + }, }); + expect(reqUrl).toBeDefined(); - test("works through textboxes", async ({adminReceiverStatusPage}) => { - const {valueDisplay} = adminReceiverStatusPage.filterFormInputs.dateRange; - await expect(adminReceiverStatusPage.receiverStatusRowsLocator).not.toHaveCount(0); - const now = new Date(); - const targetFrom = startOfDay(subDays(now, 3)); - const targetTo = addDays(endOfDay(now), 1); - - const reqUrl = await adminReceiverStatusPage.updateFilters({ - dateRange: { - value: [targetFrom, targetTo], - }, - }); - - expect(reqUrl).toBeDefined(); - - await expect(valueDisplay).toHaveText( - adminReceiverStatusPage.expectedDateRangeLabelText, - ); - expect(Object.fromEntries(reqUrl!.searchParams.entries())).toMatchObject({ - start_date: targetFrom.toISOString(), - end_date: targetTo.toISOString(), - }); + await expect(valueDisplay).toHaveText(adminReceiverStatusPage.expectedDateRangeLabelText); + expect(Object.fromEntries(reqUrl!.searchParams.entries())).toMatchObject({ + start_date: targetFrom.toISOString(), + end_date: targetTo.toISOString(), }); - }, - ); + }); + + test("works through textboxes", async ({ adminReceiverStatusPage }) => { + const { valueDisplay } = adminReceiverStatusPage.filterFormInputs.dateRange; + await expect(adminReceiverStatusPage.receiverStatusRowsLocator).not.toHaveCount(0); + const now = new Date(); + const targetFrom = startOfDay(subDays(now, 3)); + const targetTo = addDays(endOfDay(now), 1); + + const reqUrl = await adminReceiverStatusPage.updateFilters({ + dateRange: { + value: [targetFrom, targetTo], + }, + }); + + expect(reqUrl).toBeDefined(); + + await expect(valueDisplay).toHaveText(adminReceiverStatusPage.expectedDateRangeLabelText); + expect(Object.fromEntries(reqUrl!.searchParams.entries())).toMatchObject({ + start_date: targetFrom.toISOString(), + end_date: targetTo.toISOString(), + }); + }); + }); test("receiver name", async ({ adminReceiverStatusPage }) => { const { organizationName, receiverName, successRate } = @@ -229,12 +217,12 @@ test.describe("Admin Receiver Status Page", () => { await expect(receiversStatusRows).toHaveCount(adminReceiverStatusPage.timePeriodData.length); }); - test("result message", async ({adminReceiverStatusPage}) => { + test("result message", async ({ adminReceiverStatusPage }) => { const result = await adminReceiverStatusPage.testReceiverMessage(); expect(result).toBe(true); }); - test("success type", async ({adminReceiverStatusPage}) => { + test("success type", async ({ adminReceiverStatusPage }) => { const [failRow, , mixedRow] = adminReceiverStatusPage.timePeriodData; const failRowTitle = adminReceiverStatusPage.getExpectedReceiverStatusRowTitle( failRow.organizationName, @@ -272,7 +260,7 @@ test.describe("Admin Receiver Status Page", () => { test.describe("receiver statuses", () => { test.describe("date range length changes", () => { - test("increases", async ({adminReceiverStatusPage}) => { + test("increases", async ({ adminReceiverStatusPage }) => { const rows = adminReceiverStatusPage.receiverStatusRowsLocator; const days = rows.nthCustom(0).days; await expect(rows).not.toHaveCount(0); @@ -287,7 +275,7 @@ test.describe("Admin Receiver Status Page", () => { await expect(days).toHaveCount(4); }); - test("decreases", async ({adminReceiverStatusPage}) => { + test("decreases", async ({ adminReceiverStatusPage }) => { const rows = adminReceiverStatusPage.receiverStatusRowsLocator; const days = rows.nthCustom(0).days; await expect(rows).not.toHaveCount(0); @@ -303,17 +291,17 @@ test.describe("Admin Receiver Status Page", () => { }); }); - test("time period modals", async ({adminReceiverStatusPage}) => { + test("time period modals", async ({ adminReceiverStatusPage }) => { const result = await adminReceiverStatusPage.testReceiverTimePeriodModals(); expect(result).toBe(true); }); - test("receiver org links", async ({adminReceiverStatusPage}) => { + test("receiver org links", async ({ adminReceiverStatusPage }) => { const result = await adminReceiverStatusPage.testReceiverOrgLinks(); expect(result).toBe(true); }); - test("receiver links", async ({adminReceiverStatusPage}) => { + test("receiver links", async ({ adminReceiverStatusPage }) => { const result = await adminReceiverStatusPage.testReceiverLinks(); expect(result).toBe(true); }); From 81f5e7c2c9dc6fb0f66e263e3781615ccc1e4c34 Mon Sep 17 00:00:00 2001 From: etanb Date: Fri, 27 Sep 2024 12:58:00 -0700 Subject: [PATCH 12/14] DO NOT MERGE THIS: testing core issue --- .../public-pages-link-check.spec.ts | 25 +------------------ 1 file changed, 1 insertion(+), 24 deletions(-) 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 77dcf69c9a2..0f5b7e23e53 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 @@ -101,30 +101,7 @@ test.describe("Evaluate links on public facing pages", { tag: "@warning" }, () = return { url, status: e.response ? e.response.status : 400 }; } } else { - const context = await browser.newContext(); - const page = await context.newPage(); - - try { - const absoluteUrl = new URL(url, baseURL).toString(); - await page.goto(absoluteUrl, { waitUntil: "networkidle" }); - - const pageContent = await page.content(); - const hasPageNotFoundText = pageContent.includes(pageNotFound); - const isErrorWrapperVisible = await page.locator('[data-testid="error-page-wrapper"]').isVisible(); - - if (hasPageNotFoundText && isErrorWrapperVisible) { - warnings.push({ url, message: "Internal link: Page not found" }); - return { url, status: 404 }; - } - - return { url, status: 200 }; - } catch (error) { - warnings.push({ url, message: "Internal link: Page error" }); - return { url, status: 400 }; - } finally { - await page.close(); - await context.close(); - } + return { url, status: 400 }; } }; From 64a8bbe14606483d6d1b13fb6d2bab8ad025a116 Mon Sep 17 00:00:00 2001 From: etanb Date: Fri, 27 Sep 2024 14:58:04 -0700 Subject: [PATCH 13/14] remove async logic, single thread link checker --- .../public-pages-link-check.spec.ts | 57 +++++++++++++------ 1 file changed, 40 insertions(+), 17 deletions(-) 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 0f5b7e23e53..34486c7505d 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,6 +1,5 @@ /* eslint-disable playwright/no-networkidle */ import axios, { AxiosError } from "axios"; -import pLimit from "p-limit"; import * as fs from "fs"; import { pageNotFound } from "../../../src/content/error/ErrorMessages"; import { isAbsoluteURL, isAssetURL } from "../../helpers/utils"; @@ -90,6 +89,7 @@ test.describe("Evaluate links on public facing pages", { tag: "@warning" }, () = // if the page is valid or not. isAbsoluteURL determines if the page // is an internal link or external one by determining if it's an // absolute URL or a relative URL. + if (isAbsoluteURL(url) || isAssetURL(url)) { try { const normalizedURL = new URL(url, baseURL).toString(); @@ -101,26 +101,49 @@ test.describe("Evaluate links on public facing pages", { tag: "@warning" }, () = return { url, status: e.response ? e.response.status : 400 }; } } else { - return { url, status: 400 }; + // For internal relative URLs, use Playwright to navigate and check the page content + const context = await browser.newContext(); + const page = await context.newPage(); + + try { + const absoluteUrl = new URL(url, baseURL).toString(); + await page.goto(absoluteUrl, { waitUntil: "load" }); + + const pageContent = await page.content(); + const hasPageNotFoundText = pageContent.includes(pageNotFound); + const isErrorWrapperVisible = await page.locator('[data-testid="error-page-wrapper"]').isVisible(); + + if (hasPageNotFoundText && isErrorWrapperVisible) { + warnings.push({ url, message: "Internal link: Page not found" }); + return { url, status: 404 }; + } + + return { url, status: 200 }; + } catch (error) { + warnings.push({ url, message: "Internal link: Page error" }); + return { url, status: 400 }; + } finally { + await page.close(); + await context.close(); + } } }; - // Since we're trying to parallelize these tests by using Promise.all - // we need multiple, separate instances of the Playwright browser const browser = await chromium.launch(); - const limit = pLimit(10); - const results = await Promise.all( - aggregateHref.map((href) => - limit(async () => { - try { - return await validateLink(browser, href); - } catch (error) { - console.error(`Error validating link: ${href}`, error); - return { url: href, status: 500 }; - } - }), - ), - ); + + const results = []; + for (const href of aggregateHref) { + try { + const result = await validateLink(browser, href); + results.push(result); + } catch (error) { + console.error(`Issue validating link: ${href}`, error); + results.push({ url: href, status: 500 }); + } + } + + await browser.close(); + if (isFrontendWarningsLog && warnings.length > 0) { fs.writeFileSync(frontendWarningsLogPath, `${JSON.stringify(warnings)}\n`); } From 23a7fcf4ad166bc2db88c9de13853e1d8c8464d8 Mon Sep 17 00:00:00 2001 From: etanb Date: Mon, 30 Sep 2024 07:53:17 -0700 Subject: [PATCH 14/14] increase timeout --- frontend-react/playwright.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend-react/playwright.config.ts b/frontend-react/playwright.config.ts index 428b8dbb7ef..2a57ce85c36 100644 --- a/frontend-react/playwright.config.ts +++ b/frontend-react/playwright.config.ts @@ -25,6 +25,7 @@ export default defineConfig({ // Tests sharded in CI runner and reported as blobs that are later turned into html report reporter: isCi ? [["blob", { outputDir: "e2e-data/report" }]] : [["html", { outputFolder: "e2e-data/report" }]], outputDir: "e2e-data/results", + timeout: 1000 * 180, use: { // keep playwright and browser timezones aligned. set preferably UTC by env var timezoneId: Intl.DateTimeFormat().resolvedOptions().timeZone,