Skip to content

Commit

Permalink
feat(508-e2e): add auth setup and init 508 axe tests (#332)
Browse files Browse the repository at this point in the history
* add auth setup step to e2e tests and axe package

* fix homepage axe issues

* add loginpage and reviewer auth to e2e setup

* fix dashboard a11y issues

* add webform a11y tests and resolve issues

* fix typo
  • Loading branch information
benjaminpaige authored Jan 23, 2024
1 parent 0917852 commit b4ac3f4
Show file tree
Hide file tree
Showing 20 changed files with 179 additions and 77 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ build_run
.build
.turbo
coverage
lambda_layer.zip
lambda_layer.zip
**/.auth/*.json
1 change: 1 addition & 0 deletions src/services/ui/e2e/pages/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./loginPage";
21 changes: 21 additions & 0 deletions src/services/ui/e2e/pages/loginPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Page, expect } from "@playwright/test";

export class LoginPage {
constructor(private readonly page: Page) {}

async goto() {
await this.page.goto("/");
await this.page.getByRole("button", { name: "Sign In" }).click();
}

async login(email: string, password: string) {
await this.goto();
await this.page.getByRole("textbox", { name: "[email protected]" }).fill(email);
await this.page.getByRole("textbox", { name: "Password" }).fill(password);
await this.page.getByRole("button", { name: "submit" }).click();
await this.page.waitForLoadState("networkidle");
expect(
await this.page.getByRole("link", { name: "Dashboard" })
).toBeVisible();
}
}
46 changes: 46 additions & 0 deletions src/services/ui/e2e/tests/a11y/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { test, expect } from "@playwright/test";
import AxeBuilder from "@axe-core/playwright";

const staticRoutes = ["/", "/dashboard", "/faq", "/profile"];

test.describe("test a11y on static routes", () => {
for (const route of staticRoutes) {
test(`${route} should not have any automatically detectable accessibility issues`, async ({
page,
}) => {
await page.goto(route);
await page.waitForLoadState("networkidle"); // playwright is so fast this is sometimes helpful to slow it down to view results
const accessibilityScanResults = await new AxeBuilder({ page }).analyze();
console.log(
`${route} violations: `,
accessibilityScanResults.violations.length
);
expect(accessibilityScanResults.violations).toEqual([]);
});
}
});

const webformRoutes = [
"/guides/abp",
"/webform/abp10/1",
"/webform/abp3_1/1",
"/webform/abp3/1",
"/webform/abp1/1",
];

test.describe("test a11y on webform routes", () => {
for (const route of webformRoutes) {
test(`${route} should not have any automatically detectable accessibility issues`, async ({
page,
}) => {
await page.goto(route);
await page.waitForLoadState("networkidle");
const accessibilityScanResults = await new AxeBuilder({ page }).analyze();
console.log(
`${route} violations: `,
accessibilityScanResults.violations.length
);
expect(accessibilityScanResults.violations).toEqual([]);
});
}
});
27 changes: 0 additions & 27 deletions src/services/ui/e2e/tests/home/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,4 @@
import * as Libs from "../../../../../libs/secrets-manager-lib";
import { test, expect } from "@playwright/test";
import { testUsers } from "e2e/utils/users";
const stage =
process.env.STAGE_NAME === "production" || process.env.STAGE_NAME === "val"
? process.env.STAGE_NAME
: "default";
const secretId = `${process.env.PROJECT}/${stage}/bootstrapUsersPassword`;

const password = (await Libs.getSecretsValue(
process.env.REGION_A as string,
secretId
)) as string;

test("has title", async ({ page }) => {
await page.goto("/");
Expand All @@ -32,25 +20,10 @@ test("see frequently asked questions header when in faq page", async ({

test("see dashboard link when log in", async ({ page }) => {
await page.goto("/");
await page.getByRole("button", { name: "Sign In" }).click();
await page
.getByRole("textbox", { name: "[email protected]" })
.fill(testUsers.state);
await page.getByRole("textbox", { name: "Password" }).fill(password);
await page.getByRole("button", { name: "submit" }).click();
await page.getByRole("link", { name: "Dashboard" }).click();

const dashboardLinkVisible = await page
.getByRole("link", { name: "Dashboard" })
.isVisible();
expect(dashboardLinkVisible).toBeTruthy();
});

test("failed incorrect login username", async ({ page }) => {
await page.goto("/");
await page.getByRole("button", { name: "Sign In" }).click();
await page.getByRole("textbox", { name: "[email protected]" }).fill(".");
await page.getByRole("textbox", { name: "Password" }).fill(password);
await page.getByRole("button", { name: "submit" }).click();
await page.locator("#loginErrorMessage").first().isVisible();
});
35 changes: 35 additions & 0 deletions src/services/ui/e2e/utils/auth.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { test as setup } from "@playwright/test";
import * as Libs from "../../../../libs/secrets-manager-lib";
import { testUsers } from "./users";
import { LoginPage } from "../pages";

const stage =
process.env.STAGE_NAME === "production" || process.env.STAGE_NAME === "val"
? process.env.STAGE_NAME
: "default";
const secretId = `${process.env.PROJECT}/${stage}/bootstrapUsersPassword`;

const password = (await Libs.getSecretsValue(
process.env.REGION_A as string,
secretId
)) as string;

const stateSubmitterAuthFile = "playwright/.auth/state-user.json";

setup("authenticate state submitter", async ({ page, context }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();

await loginPage.login(testUsers.state, password);
await context.storageState({ path: stateSubmitterAuthFile });
});

const reviewerAuthFile = "playwright/.auth/reviewer-user.json";

setup("authenticate cms reviewer", async ({ page, context }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();

await loginPage.login(testUsers.reviewer, password);
await context.storageState({ path: reviewerAuthFile });
});
6 changes: 3 additions & 3 deletions src/services/ui/e2e/utils/users.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const testUsers = {
state: "[email protected]",
cmsAdmin: "cmsadmin@example.com"
};
state: "[email protected]",
reviewer: "reviewer@example.com",
};
1 change: 1 addition & 0 deletions src/services/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
"zod": "^3.22.3"
},
"devDependencies": {
"@axe-core/playwright": "^4.8.3",
"@playwright/test": "^1.38.0",
"@tailwindcss/typography": "^0.5.10",
"@testing-library/jest-dom": "^5.16.5",
Expand Down
21 changes: 11 additions & 10 deletions src/services/ui/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,19 @@ export default defineConfig({
/* Configure projects for major browsers */
// Note: we can test on multiple browsers and resolutions defined here
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
{
name: "firefox",
use: { ...devices["Desktop Firefox"] },
},
// Setup project
{ name: "setup", testMatch: /.*\.setup\.ts/, fullyParallel: true },

{
name: "webkit",
use: { ...devices["Desktop Safari"] },
// we can have different projects for different users/use cases
name: "logged in state user",
use: {
...devices["Desktop Chrome"],
// Use prepared auth state for state submitter.
storageState: "playwright/.auth/state-user.json",
},
// Tests start already authenticated because we specified storageState in the config.
dependencies: ["setup"],
},
],
});
2 changes: 1 addition & 1 deletion src/services/ui/src/components/HowItWorks/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export const HowItWorks = ({ children }: React.PropsWithChildren) => {
return (
<div className="py-8 px-6 border border-gray-300 rounded-md border-solid w-full">
<h4 className="text-bold text-xl">How it works</h4>
<h3 className="text-bold text-xl">How it works</h3>
{children}
</div>
);
Expand Down
10 changes: 5 additions & 5 deletions src/services/ui/src/components/Layout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ const UserDropdownMenu = () => {
asChild
className="hover:text-white/70 p-4 data-[state=open]:bg-white data-[state=open]:text-primary"
>
<div className="flex flex-row gap-4 items-center cursor-pointer">
<button className="flex flex-row gap-4 items-center cursor-pointer">
<p className="flex">My Account</p>
<svg
xmlns="http://www.w3.org/2000/svg"
Expand All @@ -73,7 +73,7 @@ const UserDropdownMenu = () => {
d="M19.5 8.25l-7.5 7.5-7.5-7.5"
/>
</svg>
</div>
</button>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content
Expand Down Expand Up @@ -108,20 +108,20 @@ export const Layout = () => {
return (
<div className="min-h-full flex flex-col">
<UsaBanner />
<div className="bg-primary">
<nav className="bg-primary">
<div className="max-w-screen-xl mx-auto px-4 lg:px-8">
<div className="h-[70px] flex gap-12 items-center text-white">
<Link to="/">
<img
className="h-10 w-28 min-w-[112px] resize-none"
src={oneMacLogo}
alt="One Mac Site Logo"
alt="onemac site logo"
/>
</Link>
<ResponsiveNav isDesktop={isDesktop} />
</div>
</div>
</div>
</nav>
<main className="flex-1">
<Outlet />
</main>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const OsFiltering: FC<{

return (
<div>
<p className="mb-1 text-sm text-slate-400">
<p className="mb-1 text-sm">
{"Search by Package ID, CPOC Name, or Submitter Name"}
</p>
<div className="flex flex-row content-between gap-2 mb-4">
Expand Down
6 changes: 4 additions & 2 deletions src/services/ui/src/components/Pagination/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export const Pagination: FC<Props> = (props) => {
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
display="none"
>
<path
fillRule="evenodd"
Expand All @@ -75,7 +76,7 @@ export const Pagination: FC<Props> = (props) => {
{state.pageRange.map((PAGE) => {
if (Array.isArray(PAGE))
return (
<button
<div
key={`PAGE-${PAGE}`}
className="relative inline-flex items-center px-4 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 focus:outline-offset-0 cursor-pointer"
id="morePagesButton"
Expand All @@ -97,7 +98,7 @@ export const Pagination: FC<Props> = (props) => {
</option>
))}
</select>
</button>
</div>
);

const isActive = props.pageNumber === PAGE - 1;
Expand Down Expand Up @@ -132,6 +133,7 @@ export const Pagination: FC<Props> = (props) => {
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
display="none"
>
<path
fillRule="evenodd"
Expand Down
4 changes: 2 additions & 2 deletions src/services/ui/src/components/RHF/Document.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ export const RHFDocument = <TFieldValues extends FieldValues>(props: {
<div className="h-[5px] bg-gradient-to-r from-primary from-50% to-[#02bfe7] to-[66%] rounded-t"></div>
<div className="py-4 px-8 border-2 border-t-0 mt-0">
<div className="mb-3 mt-9">
<FormLabel className="font-bold text-4xl px-8 inline-block leading-[48px]">
<h1 className="font-bold text-4xl px-8 inline-block leading-[48px]">
{props.document.header}
</FormLabel>
</h1>
</div>
{props.document.sections.map((SEC, index) => (
<RHFSection
Expand Down
17 changes: 12 additions & 5 deletions src/services/ui/src/components/RHF/Slot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,21 +77,23 @@ export const RHFSlot = <
{rhf === "Input" &&
(() => {
const hops = props as RHFComponentMap["Input"];
return <Input {...hops} {...field} />;
return <Input {...hops} {...field} aria-label={field.name} />;
})()}

{/* ----------------------------------------------------------------------------- */}
{rhf === "Textarea" &&
(() => {
const hops = props as RHFComponentMap["Textarea"];
return <Textarea {...hops} {...field} />;
return (
<Textarea {...hops} {...field} aria-label={field.name} />
);
})()}

{/* ----------------------------------------------------------------------------- */}
{rhf === "Switch" &&
(() => {
const hops = props as RHFComponentMap["Switch"];
return <Switch {...hops} {...field} />;
return <Switch {...hops} {...field} aria-label={field.name} />;
})()}

{/* ----------------------------------------------------------------------------- */}
Expand All @@ -115,7 +117,7 @@ export const RHFSlot = <
onValueChange={field.onChange}
defaultValue={field.value}
>
<SelectTrigger {...hops}>
<SelectTrigger {...hops} aria-label={field.name}>
<SelectValue {...hops} />
</SelectTrigger>
<SelectContent className="overflow-auto max-h-60">
Expand Down Expand Up @@ -143,7 +145,11 @@ export const RHFSlot = <
return (
<div key={`OPT-${OPT.value}`} className="flex flex-col">
<div className="flex gap-2 items-center">
<RadioGroupItem value={OPT.value} id={OPT.value} />
<RadioGroupItem
value={OPT.value}
id={OPT.value}
aria-label={OPT.value}
/>
{
<FormLabel
className="font-normal"
Expand Down Expand Up @@ -214,6 +220,7 @@ export const RHFSlot = <
dependency={OPT.dependency}
parentValue={field.value}
changeMethod={field.onChange}
aria-label={field.name}
/>
{field.value?.includes(OPT.value) &&
!!OPT.slots &&
Expand Down
Loading

0 comments on commit b4ac3f4

Please sign in to comment.