Files
bcs-website/tests/site.spec.ts
T
bmr_bluecap 0f769d7bdd feat(contact): wire the form to a real endpoint at build time
The contact form could not be made to work by configuration. Two reasons,
both fixed here.

The endpoint never reached the browser. k8s/configmap.yaml carried
PUBLIC_CONTACT_FORM_ENDPOINT, but the Deployment has no env/envFrom pointing
at that ConfigMap, so it was dead config. Wiring it up would not have helped
either: this is a static Astro build, so import.meta.env.PUBLIC_* is inlined
when the image is built and the pod only serves pre-built HTML. Anything the
browser needs has to arrive as a build arg. Add ARG/ENV to the Dockerfile and
pass --build-arg from CI, sourced from a CONTACT_FORM_ENDPOINT Actions
secret. Rewrite the ConfigMap as documentation of what the build expects,
with an explicit warning not to add envFrom.

Submission behaviour was also wrong for visitors. A native POST would have
navigated them to the form provider's page; post in the background with
fetch() and send them to our own /thank-you/ instead. On failure the form now
stays put, re-enables the button and offers the phone and email fallback,
rather than silently losing the inquiry. Add a honeypot (_gotcha, which
Formspree treats as spam) positioned off-screen rather than display:none, so
bots that skip hidden fields still fill it.

Read data-endpoint at submit time rather than caching it at init, keeping the
attribute the single source of truth.

Tests: replace the old contact assertion, which faked a native GET
navigation, with one that mocks the endpoint and exercises the real fetch
path including the posted payload. Add a case for endpoint failure.

typecheck 0/0/0, build 0/0, seo:lint clean on 20 pages, e2e 44 passed.
The 4 remaining failures are the pre-existing stale nav assertions in
docs/launch-checklist.md section 2.1, untouched here.
2026-10-09 07:04:59 -04:00

165 lines
6.7 KiB
TypeScript

import { expect, test } from "@playwright/test";
const expectedPages = [
"/",
"/about-us/",
"/contact/",
"/services/",
"/focus-areas/",
"/insights/",
"/market-intelligence-opportunity-analysis/",
"/develop-and-deploy-catalytic-capital/",
"/stakeholder-engagement-partnership-development/",
"/sustainable-technology-incubation/",
"/sustainable-business-model-innovation/",
"/project-management-implementation/",
"/impact-assessment-reporting/",
"/policy-advocacy-development/",
"/the-economics-of-pfas-remediation/",
"/structuring-marine-conservation-finance/",
"/sustainable-living-resource-utilization/",
"/economic-development-for-island-and-coastal-communities/",
];
test.describe("page rendering", () => {
for (const path of expectedPages) {
test(`${path} renders meaningful HTML without horizontal overflow`, async ({ page }) => {
const response = await page.goto(path);
expect(response?.status(), path).toBeLessThan(400);
await expect(page.locator("h1")).toBeVisible();
await expect(page.locator("main")).toContainText(/BlueCap|water|capital|aquatic|PFAS|marine/i);
const overflow = await page.evaluate(
() => document.documentElement.scrollWidth - document.documentElement.clientWidth,
);
expect(overflow, `${path} has horizontal overflow`).toBeLessThanOrEqual(1);
});
}
});
test("all internal href links resolve", async ({ page, request, baseURL }) => {
const found = new Set<string>();
for (const path of expectedPages) {
await page.goto(path);
const links = await page.locator("a[href]").evaluateAll((anchors) =>
anchors.map((anchor) => (anchor as HTMLAnchorElement).href),
);
for (const href of links) {
if (href.startsWith("mailto:") || href.startsWith("tel:")) continue;
const url = new URL(href);
if (url.origin !== baseURL) continue;
found.add(`${url.pathname}${url.search}`);
}
}
for (const href of found) {
const response = await request.get(href);
expect(response.status(), href).toBeLessThan(400);
}
});
test("desktop navigation and CTA links are clickable", async ({ page }) => {
await page.setViewportSize({ width: 1440, height: 900 });
await page.goto("/");
await page.getByRole("link", { name: "Explore services" }).click();
await expect(page).toHaveURL(/\/services\/$/);
await expect(page.getByRole("heading", { name: /Advisory services/ })).toBeVisible();
await page.goto("/");
await page.getByRole("link", { name: "Request a consultation" }).first().click();
await expect(page).toHaveURL(/\/contact\/$/);
await expect(page.getByRole("heading", { name: /Get in touch/ })).toBeVisible();
});
test("service and focus-area learn more links are wired to detail pages", async ({ page }) => {
for (const path of ["/services/", "/focus-areas/"]) {
await page.goto(path);
const learnMoreLinks = page.getByRole("link", { name: "Learn more" });
const count = await learnMoreLinks.count();
expect(count, `${path} should expose Learn more links`).toBeGreaterThan(0);
for (let index = 0; index < count; index += 1) {
const href = await learnMoreLinks.nth(index).getAttribute("href");
expect(href, `${path} Learn more ${index + 1}`).toMatch(/^\/.+\/$/);
const response = await page.request.get(href!);
expect(response.status(), href ?? "").toBeLessThan(400);
}
}
});
test("mobile menu opens and navigates", async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto("/");
await page.locator(".mobile-nav summary").click();
await expect(page.locator(".mobile-nav nav")).toBeVisible();
await page.locator(".mobile-nav").getByRole("link", { name: "Focus Areas" }).click();
await expect(page).toHaveURL(/\/focus-areas\/$/);
await expect(page.getByRole("heading", { name: /Focused expertise/ })).toBeVisible();
});
test("contact form validates required, invalid, and valid paths", async ({ page }) => {
await page.goto("/contact/");
const submit = page.getByRole("button", { name: "Submit" });
await submit.click();
const emptyValidation = await page.locator('input[name="name"]').evaluate((input) => {
return (input as HTMLInputElement).validationMessage.length > 0;
});
expect(emptyValidation).toBe(true);
await expect(page).toHaveURL(/\/contact\/$/);
await page.locator('input[name="name"]').fill("Test User");
await page.locator('input[name="email"]').fill("not-an-email");
await page.locator('textarea[name="message"]').fill("Too short");
await submit.click();
const invalidEmail = await page.locator('input[name="email"]').evaluate((input) => {
return !(input as HTMLInputElement).validity.valid;
});
expect(invalidEmail).toBe(true);
await page.locator('input[name="email"]').fill("test@example.com");
await page.locator('textarea[name="message"]').fill("This is a valid test inquiry for BlueCap Strategies.");
// Exercise the real submit path: the form POSTs to the configured endpoint in the background
// and only then sends the visitor to our own /thank-you/. Mock the endpoint so the test never
// touches the live form provider.
const endpoint = "https://forms.test.invalid/f/mock";
let posted: string | null = null;
await page.route(endpoint, async (route) => {
posted = route.request().postData();
await route.fulfill({ status: 200, contentType: "application/json", body: '{"ok":true}' });
});
await page.locator("form").evaluate((form, url) => {
form.setAttribute("data-endpoint", url);
}, endpoint);
await submit.click();
await expect(page).toHaveURL(/\/thank-you\/$/);
await expect(page.getByRole("heading", { name: "Thank you" })).toBeVisible();
expect(posted, "form fields should reach the endpoint").toContain("test@example.com");
});
test("contact form surfaces an error when the endpoint fails", async ({ page }) => {
await page.goto("/contact/");
const endpoint = "https://forms.test.invalid/f/mock";
await page.route(endpoint, (route) => route.fulfill({ status: 500, body: "nope" }));
await page.locator("form").evaluate((form, url) => {
form.setAttribute("data-endpoint", url);
}, endpoint);
await page.locator('input[name="name"]').fill("Test User");
await page.locator('input[name="email"]').fill("test@example.com");
await page.locator('textarea[name="message"]').fill("This is a valid test inquiry for BlueCap Strategies.");
await page.getByRole("button", { name: "Submit" }).click();
// A failed send must not strand the visitor: it stays on the page, says so, and offers the
// phone/email fallback rather than silently losing the inquiry.
await expect(page).toHaveURL(/\/contact\/$/);
await expect(page.locator("[data-form-status]")).toContainText("didn't send");
await expect(page.getByRole("button", { name: "Submit" })).toBeEnabled();
});