← Back to team overview

sts-sponsors team mailing list archive

[Merge] ~petermakowski/maas-site-manager:add-a11y-tests into maas-site-manager:main

 

Peter Makowski has proposed merging ~petermakowski/maas-site-manager:add-a11y-tests into maas-site-manager:main.

Commit message:
add e2e accessibility tests
- add @axe-core/playwright
- add .u-visually-hidden class
- fix a11y issues on requests and tokens pages
- fix Pagination styles


Requested reviews:
  MAAS Committers (maas-committers)

For more details, see:
https://code.launchpad.net/~petermakowski/maas-site-manager/+git/site-manager/+merge/441890
-- 
Your team MAAS Committers is requested to review the proposed merge of ~petermakowski/maas-site-manager:add-a11y-tests into maas-site-manager:main.
diff --git a/frontend/package.json b/frontend/package.json
index c4cc3ca..123987d 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -34,6 +34,7 @@
     "yup": "1.0.2"
   },
   "devDependencies": {
+    "@axe-core/playwright": "4.6.1",
     "@playwright/test": "1.32.3",
     "@remix-run/web-fetch": "4.3.3",
     "@testing-library/jest-dom": "5.16.5",
diff --git a/frontend/src/_utils.scss b/frontend/src/_utils.scss
index b666356..1cb04c8 100644
--- a/frontend/src/_utils.scss
+++ b/frontend/src/_utils.scss
@@ -50,3 +50,12 @@
 .u-no-padding {
   padding: 0 !important;
 }
+// hide elements visually, but keep them accessible to screen readers
+.u-visually-hidden {
+  position: absolute !important;
+  height: 1px !important;
+  width: 1px !important;
+  overflow: hidden !important;
+  clip: rect(1px, 1px, 1px, 1px) !important;
+  white-space: nowrap !important;
+}
diff --git a/frontend/src/components/MainLayout/MainLayout.tsx b/frontend/src/components/MainLayout/MainLayout.tsx
index 93364ab..0737df1 100644
--- a/frontend/src/components/MainLayout/MainLayout.tsx
+++ b/frontend/src/components/MainLayout/MainLayout.tsx
@@ -89,7 +89,7 @@ const MainLayout: React.FC = () => {
       <div className="l-application">
         <Navigation />
         <main className="l-main is-maas-site-manager">
-          <h1 className="u-hide">{getPageTitle(pathname as RoutePath)}</h1>
+          <h1 className="u-visually-hidden">{getPageTitle(pathname as RoutePath)}</h1>
           <div className={classNames("l-main__nav", { "is-open": isSideNavVisible })}>
             <SecondaryNavigation isOpen={!!isSideNavVisible} />
           </div>
diff --git a/frontend/src/components/TokensList/components/TokensTable/TokensTable.tsx b/frontend/src/components/TokensList/components/TokensTable/TokensTable.tsx
index a4c2b15..2c608ec 100644
--- a/frontend/src/components/TokensList/components/TokensTable/TokensTable.tsx
+++ b/frontend/src/components/TokensList/components/TokensTable/TokensTable.tsx
@@ -46,15 +46,20 @@ const TokensTable = ({
       {
         id: "select",
         header: ({ table }) => <SelectAllCheckbox table={table} />,
-        cell: ({ row }) => (
-          <div>
-            <Input
-              checked={row.getIsSelected()}
-              disabled={!row.getCanSelect()}
-              onChange={row.getToggleSelectedHandler()}
+        cell: ({ row, getValue }) => (
+          <label className="p-checkbox--inline">
+            <input
+              aria-label={`select ${getValue()}`}
+              className="p-checkbox__input"
               type="checkbox"
+              {...{
+                checked: row.getIsSelected(),
+                disabled: !row.getCanSelect(),
+                onChange: row.getToggleSelectedHandler(),
+              }}
             />
-          </div>
+            <span className="p-checkbox__label" />
+          </label>
         ),
       },
       {
diff --git a/frontend/src/components/base/PaginationBar/_PaginationBar.scss b/frontend/src/components/base/PaginationBar/_PaginationBar.scss
index 2a76986..2dbd5c1 100644
--- a/frontend/src/components/base/PaginationBar/_PaginationBar.scss
+++ b/frontend/src/components/base/PaginationBar/_PaginationBar.scss
@@ -4,7 +4,6 @@
   align-items: center;
   padding: $spv--small 0;
   margin-bottom: $spv--large;
-  padding-bottom: $spv--large;
 
   @media screen and (max-width: $breakpoint-small) {
     padding-bottom: $spv--small;
diff --git a/frontend/src/components/base/TablePagination/TablePagination.tsx b/frontend/src/components/base/TablePagination/TablePagination.tsx
index a77f96c..c716638 100644
--- a/frontend/src/components/base/TablePagination/TablePagination.tsx
+++ b/frontend/src/components/base/TablePagination/TablePagination.tsx
@@ -61,43 +61,37 @@ const TablePagination = ({
 
   return (
     <nav aria-label="pagination" className="table-pagination">
-      <ul>
-        <li>
-          <Button
-            appearance="base"
-            aria-label="previous page"
-            disabled={currentPage === 1 || isLoading}
-            hasIcon
-            onClick={handlePreviousClick}
-          >
-            <Icon className="u__left-rotate" name="chevron-down" />
-          </Button>
-        </li>
-        <strong>Page</strong>
-        <Input
-          aria-label="current page"
-          className="current-page"
-          disabled={isLoading}
-          error={error}
-          min={1}
-          onBlur={handleInputBlur}
-          onChange={handlePageInput}
-          type="number"
-          value={pageNumber}
-        />
-        <strong className="u-no-wrap"> of {totalPages}</strong>
-        <li>
-          <Button
-            appearance="base"
-            aria-label="next page"
-            disabled={currentPage === totalPages || isLoading}
-            hasIcon
-            onClick={handleNextClick}
-          >
-            <Icon className="u__right-rotate" name="chevron-up" />
-          </Button>
-        </li>
-      </ul>
+      <Button
+        appearance="base"
+        aria-label="previous page"
+        disabled={currentPage === 1 || isLoading}
+        hasIcon
+        onClick={handlePreviousClick}
+      >
+        <Icon className="u__left-rotate" name="chevron-down" />
+      </Button>
+      <strong>Page</strong>
+      <Input
+        aria-label="current page"
+        className="current-page"
+        disabled={isLoading}
+        error={error}
+        min={1}
+        onBlur={handleInputBlur}
+        onChange={handlePageInput}
+        type="number"
+        value={pageNumber}
+      />
+      <strong className="u-no-wrap"> of {totalPages}</strong>
+      <Button
+        appearance="base"
+        aria-label="next page"
+        disabled={currentPage === totalPages || isLoading}
+        hasIcon
+        onClick={handleNextClick}
+      >
+        <Icon className="u__right-rotate" name="chevron-up" />
+      </Button>
     </nav>
   );
 };
diff --git a/frontend/src/components/base/TablePagination/_TablePagination.scss b/frontend/src/components/base/TablePagination/_TablePagination.scss
index 0375ec5..c663d8f 100644
--- a/frontend/src/components/base/TablePagination/_TablePagination.scss
+++ b/frontend/src/components/base/TablePagination/_TablePagination.scss
@@ -1,20 +1,16 @@
 .table-pagination {
   position: relative;
-  margin-right: $sph--x-large;
+  margin: 0 $sph--x-large 0 0;
+  display: flex;
+  gap: $sph--small;
+  align-items: center;
+  list-style: none;
+  padding: 0;
 
   @media screen and (max-width: $breakpoint-small) {
     margin-bottom: $spv--large;
   }
 
-  ul {
-    display: flex;
-    gap: $sph--small;
-    align-items: center;
-    list-style: none;
-    margin: 0;
-    padding: 0;
-  }
-
   button {
     margin: 0;
   }
diff --git a/frontend/tests/a11y.spec.ts b/frontend/tests/a11y.spec.ts
new file mode 100644
index 0000000..8f3f8c1
--- /dev/null
+++ b/frontend/tests/a11y.spec.ts
@@ -0,0 +1,26 @@
+import { test, expect } from "@playwright/test";
+import AxeBuilder from "@axe-core/playwright"; // 1
+import { protectedRoutes, publicRoutes } from "@/base/routesConfig";
+import { adminAuthFile } from "./constants";
+
+const a11yTest = async ({ title, path }: { title: string; path: string }) =>
+  await test(`${title} page does not have any automatically detectable accessibility issues`, async ({ page }) => {
+    await page.goto(path, { waitUntil: "networkidle" });
+
+    const accessibilityScanResults = await new AxeBuilder({ page })
+      // @canonical/react-components Accordion is known to have accessibility issues
+      .exclude(".p-accordion")
+      .withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"])
+      .analyze();
+
+    expect(accessibilityScanResults.violations).toEqual([]);
+  });
+
+test.describe("protected routes", () => {
+  test.use({ storageState: adminAuthFile });
+  Object.values(protectedRoutes).forEach(a11yTest);
+});
+
+test.describe("public routes", () => {
+  Object.values(publicRoutes).forEach(a11yTest);
+});
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index 2fc181c..a554da0 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -20,6 +20,13 @@
   resolved "https://registry.yarnpkg.com/@antfu/utils/-/utils-0.7.2.tgz#3bb6f37a6b188056fe9e2f363b6aa735ed65d7ca";
   integrity sha512-vy9fM3pIxZmX07dL+VX1aZe7ynZ+YyB0jY+jE6r3hOK6GNY2t6W8rzpFC4tgpbXUYABkFQwgJq2XYXlxbXAI0g==
 
+"@axe-core/playwright@4.6.1":
+  version "4.6.1"
+  resolved "https://registry.yarnpkg.com/@axe-core/playwright/-/playwright-4.6.1.tgz#1740bc705972517d4331c520a341436ed44364fb";
+  integrity sha512-XMKP2OzGfGIYpU9G9FgI2ulyjEXQDP6qtZerOwdQ7YC1w4SFgofK3ogSk0qVoy0QI+q6XWLUJMfMMkUwdTR2dA==
+  dependencies:
+    axe-core "^4.6.3"
+
 "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.18.6":
   version "7.18.6"
   resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a";
@@ -2448,6 +2455,11 @@ axe-core@^4.6.2:
   resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.6.3.tgz#fc0db6fdb65cc7a80ccf85286d91d64ababa3ece";
   integrity sha512-/BQzOX780JhsxDnPpH4ZiyrJAzcd8AfzFPkv+89veFSr1rcMjuq2JDCwypKaPeB6ljHp9KjXhPpjgCvQlWYuqg==
 
+axe-core@^4.6.3:
+  version "4.7.0"
+  resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.0.tgz#34ba5a48a8b564f67e103f0aa5768d76e15bbbbf";
+  integrity sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==
+
 axios@*:
   version "1.3.4"
   resolved "https://registry.yarnpkg.com/axios/-/axios-1.3.4.tgz#f5760cefd9cfb51fd2481acf88c05f67c4523024";

Follow ups