From 5f3af71e52c746579cd1af960aee599745d3e3c4 Mon Sep 17 00:00:00 2001 From: Vincent Rubinetti Date: Thu, 3 Oct 2024 00:31:08 -0400 Subject: [PATCH] implement dark mode --- frontend/bun.lockb | Bin 325779 -> 326184 bytes frontend/package.json | 7 +- frontend/src/components/Alert.module.css | 2 +- frontend/src/components/DarkMode.tsx | 32 ++++ frontend/src/components/Footer.module.css | 2 +- frontend/src/components/Header.module.css | 2 +- frontend/src/components/Header.tsx | 2 + frontend/src/components/Network.module.css | 4 +- frontend/src/components/Network.tsx | 55 ++++--- frontend/src/components/Popover.module.css | 2 +- frontend/src/components/Section.module.css | 2 +- frontend/src/components/Select.module.css | 2 +- .../src/components/TableOfContents.module.css | 2 +- frontend/src/components/TextBox.module.css | 2 +- frontend/src/components/ViewCorner.module.css | 2 +- frontend/src/global/effects.css | 2 +- frontend/src/global/styles.css | 13 +- frontend/src/global/theme.css | 49 +++++-- frontend/src/pages/Testbed.tsx | 137 +++++++++--------- frontend/src/util/dom.ts | 23 --- frontend/src/util/hooks.ts | 38 +++++ frontend/src/vite-env.d.ts | 9 +- frontend/tests/accessibility.spec.ts | 42 +++--- 23 files changed, 269 insertions(+), 162 deletions(-) create mode 100644 frontend/src/components/DarkMode.tsx create mode 100644 frontend/src/util/hooks.ts diff --git a/frontend/bun.lockb b/frontend/bun.lockb index 5a08991dd0574da8632a189627bc288856bddeab..719075de7cc6a58a16eae0f38b774076741ccc88 100755 GIT binary patch delta 53763 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 delta 54199 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 svg { diff --git a/frontend/src/components/DarkMode.tsx b/frontend/src/components/DarkMode.tsx new file mode 100644 index 0000000..ec048a9 --- /dev/null +++ b/frontend/src/components/DarkMode.tsx @@ -0,0 +1,32 @@ +import { useEffect } from "react"; +import { FaRegMoon, FaRegSun } from "react-icons/fa6"; +import { useAtom } from "jotai"; +import { atomWithStorage } from "jotai/utils"; +import Tooltip from "@/components/Tooltip"; + +/** dark mode state */ +export const darkModeAtom = atomWithStorage("darkMode", false); + +/** dark mode toggle */ +export const DarkMode = () => { + const [darkMode, setDarkMode] = useAtom(darkModeAtom); + + /** update root element data attribute that switches css color vars */ + useEffect(() => { + document.documentElement.setAttribute("data-dark", String(darkMode)); + }); + + return ( + + + + ); +}; diff --git a/frontend/src/components/Footer.module.css b/frontend/src/components/Footer.module.css index 608cefd..a8f4377 100755 --- a/frontend/src/components/Footer.module.css +++ b/frontend/src/components/Footer.module.css @@ -3,7 +3,7 @@ padding: 40px 20px; gap: 10px; background: var(--deep); - box-shadow: var(--shadow); + box-shadow: var(--box-shadow); color: var(--white); text-align: center; } diff --git a/frontend/src/components/Header.module.css b/frontend/src/components/Header.module.css index 28973f9..9721180 100755 --- a/frontend/src/components/Header.module.css +++ b/frontend/src/components/Header.module.css @@ -13,7 +13,7 @@ margin-bottom: var(--header-shrink); padding: calc(20px - var(--header-padding-shrink)); background: var(--deep); - box-shadow: var(--shadow); + box-shadow: var(--box-shadow); color: var(--white); } diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx index d992a52..0c1d9e4 100755 --- a/frontend/src/components/Header.tsx +++ b/frontend/src/components/Header.tsx @@ -3,6 +3,7 @@ import { FaBars, FaXmark } from "react-icons/fa6"; import clsx from "clsx"; import { useElementSize } from "@reactuses/core"; import Logo from "@/assets/logo.svg?react"; +import { DarkMode } from "@/components/DarkMode"; import Flex from "@/components/Flex"; import Link from "@/components/Link"; import Tooltip from "@/components/Tooltip"; @@ -64,6 +65,7 @@ const Header = () => { About + ); diff --git a/frontend/src/components/Network.module.css b/frontend/src/components/Network.module.css index e47c73e..3cc9a4f 100644 --- a/frontend/src/components/Network.module.css +++ b/frontend/src/components/Network.module.css @@ -2,7 +2,7 @@ display: grid; grid-template-columns: max-content auto; width: 100%; - box-shadow: var(--shadow); + box-shadow: var(--box-shadow); } .expanded { @@ -19,7 +19,7 @@ overflow-x: hidden; overflow-y: auto; background: var(--white); - box-shadow: inset -5px 0 5px -5px #00000020; + box-shadow: inset -5px 0 5px -7px var(--black); overflow-wrap: anywhere; } diff --git a/frontend/src/components/Network.tsx b/frontend/src/components/Network.tsx index 05aa591..c57f403 100644 --- a/frontend/src/components/Network.tsx +++ b/frontend/src/components/Network.tsx @@ -17,12 +17,25 @@ import { } from "react-icons/fa6"; import clsx from "clsx"; import cytoscape from "cytoscape"; -import type { Core, Css, EdgeSingular, Layouts, NodeSingular } from "cytoscape"; -import avsdf from "cytoscape-avsdf"; +import type { + BreadthFirstLayoutOptions, + CircleLayoutOptions, + ConcentricLayoutOptions, + Core, + CoseLayoutOptions, + Css, + EdgeSingular, + GridLayoutOptions, + LayoutOptions, + Layouts, + NodeSingular, + RandomLayoutOptions, +} from "cytoscape"; +import avsdf, { type AvsdfLayoutOptions } from "cytoscape-avsdf"; import cola from "cytoscape-cola"; -import dagre from "cytoscape-dagre"; -import fcose from "cytoscape-fcose"; -import klay from "cytoscape-klay"; +import dagre, { type DagreLayoutOptions } from "cytoscape-dagre"; +import fcose, { type FcoseLayoutOptions } from "cytoscape-fcose"; +import klay, { type KlayLayoutOptions } from "cytoscape-klay"; import spread from "cytoscape-spread"; import { extent } from "d3"; import { omit, orderBy, startCase, truncate } from "lodash"; @@ -176,6 +189,9 @@ cytoscape.use(klay); cytoscape.use(avsdf); cytoscape.use(spread); +/** extra props on layout options */ +type LayoutExtras = { label: string }; + /** layout algorithms and their options */ const layouts = [ { @@ -184,7 +200,7 @@ const layouts = [ label: "Random", padding, boundingBox, - }, + } satisfies RandomLayoutOptions & LayoutExtras, { /** https://js.cytoscape.org/#layouts/grid */ name: "grid", @@ -196,7 +212,7 @@ const layouts = [ spacingFactor: 1.5, condense: true, sort: undefined, - }, + } satisfies GridLayoutOptions & LayoutExtras, { /** https://js.cytoscape.org/#layouts/circle */ name: "circle", @@ -209,7 +225,7 @@ const layouts = [ startAngle: (3 / 2) * Math.PI, clockwise: true, sort: undefined, - }, + } satisfies CircleLayoutOptions & LayoutExtras, { /** https://js.cytoscape.org/#layouts/concentric */ name: "concentric", @@ -221,7 +237,7 @@ const layouts = [ minNodeSpacing: minNodeSize, avoidOverlap: true, spacingFactor: 1, - }, + } satisfies ConcentricLayoutOptions & LayoutExtras, /** https://js.cytoscape.org/#layouts/breadthfirst */ { name: "breadthfirst", @@ -233,7 +249,7 @@ const layouts = [ grid: false, spacingFactor: 1.5, avoidOverlap: true, - }, + } satisfies BreadthFirstLayoutOptions & LayoutExtras, { /** https://js.cytoscape.org/#layouts/cose */ name: "cose", @@ -241,18 +257,19 @@ const layouts = [ padding, boundingBox, componentSpacing: maxNodeSize, - idealEdgeLength: edgeLength, - }, + idealEdgeLength: () => edgeLength, + } satisfies CoseLayoutOptions & LayoutExtras, { /** https://github.com/iVis-at-Bilkent/cytoscape.js-fcose?tab=readme-ov-file#api */ name: "fcose", label: "fCoSE", padding, - quality: "default", + quality: "proof", + randomize: false, animate: false, nodeSeparation: minNodeSize, idealEdgeLength: edgeLength, - }, + } satisfies FcoseLayoutOptions & LayoutExtras, { /** https://github.com/cytoscape/cytoscape.js-dagre?tab=readme-ov-file#api */ name: "dagre", @@ -260,7 +277,7 @@ const layouts = [ padding, boundingBox, spacingFactor: 1, - }, + } satisfies DagreLayoutOptions & LayoutExtras, { /** https://github.com/cytoscape/cytoscape.js-cola?tab=readme-ov-file#api */ name: "cola", @@ -273,7 +290,7 @@ const layouts = [ edgeLength: edgeLength, edgeSymDiffLength: edgeLength, edgeJaccardLength: edgeLength, - }, + } as LayoutOptions & LayoutExtras, { /** https://github.com/cytoscape/cytoscape.js-klay?tab=readme-ov-file#api */ name: "klay", @@ -288,7 +305,7 @@ const layouts = [ spacing: minNodeSize, thoroughness: 7, }, - }, + } satisfies KlayLayoutOptions & LayoutExtras, { /** https://github.com/iVis-at-Bilkent/cytoscape.js-avsdf?tab=readme-ov-file#api */ name: "avsdf", @@ -296,7 +313,7 @@ const layouts = [ padding, animate: false, nodeSeparation: edgeLength, - }, + } satisfies AvsdfLayoutOptions & LayoutExtras, { /** https://github.com/cytoscape/cytoscape.js-spread?tab=readme-ov-file#api */ name: "spread", @@ -304,7 +321,7 @@ const layouts = [ padding, boundingBox, minDist: edgeLength, - }, + } as LayoutOptions & LayoutExtras, ] as const; /** layout algorithm dropdown options */ diff --git a/frontend/src/components/Popover.module.css b/frontend/src/components/Popover.module.css index 13f1191..e76cded 100755 --- a/frontend/src/components/Popover.module.css +++ b/frontend/src/components/Popover.module.css @@ -7,7 +7,7 @@ gap: 20px; border-radius: var(--rounded); background: var(--white); - box-shadow: 0 0 0 9999px #00000040; + box-shadow: 0 0 0 9999px var(--shadow); } .arrow { diff --git a/frontend/src/components/Section.module.css b/frontend/src/components/Section.module.css index a1c4c23..acdd370 100755 --- a/frontend/src/components/Section.module.css +++ b/frontend/src/components/Section.module.css @@ -8,7 +8,7 @@ } .section:nth-child(even) { - background: #fafafa; + background: var(--alt-white); } .fill { diff --git a/frontend/src/components/Select.module.css b/frontend/src/components/Select.module.css index b0ad4dc..7e93f39 100755 --- a/frontend/src/components/Select.module.css +++ b/frontend/src/components/Select.module.css @@ -44,7 +44,7 @@ padding: 0; outline: none; background: var(--white); - box-shadow: var(--shadow); + box-shadow: var(--box-shadow); list-style: none; } diff --git a/frontend/src/components/TableOfContents.module.css b/frontend/src/components/TableOfContents.module.css index 7cefaa3..f0c53da 100755 --- a/frontend/src/components/TableOfContents.module.css +++ b/frontend/src/components/TableOfContents.module.css @@ -7,7 +7,7 @@ translate: 0 calc(-1 * var(--header-shrink)); border-radius: 0 0 var(--rounded) 0; background: var(--white); - box-shadow: var(--shadow); + box-shadow: var(--box-shadow); } .heading { diff --git a/frontend/src/components/TextBox.module.css b/frontend/src/components/TextBox.module.css index a1a9b7a..323a4ee 100755 --- a/frontend/src/components/TextBox.module.css +++ b/frontend/src/components/TextBox.module.css @@ -32,7 +32,7 @@ padding: 5px 10px; border: solid 2px transparent; border-radius: var(--rounded); - box-shadow: var(--shadow); + box-shadow: var(--box-shadow); transition: border-color var(--fast); } diff --git a/frontend/src/components/ViewCorner.module.css b/frontend/src/components/ViewCorner.module.css index 9def6a8..ec46790 100755 --- a/frontend/src/components/ViewCorner.module.css +++ b/frontend/src/components/ViewCorner.module.css @@ -10,7 +10,7 @@ height: 30px; border-radius: 999px; background: var(--deep); - box-shadow: var(--shadow); + box-shadow: var(--box-shadow); color: var(--white); transition: background var(--fast); } diff --git a/frontend/src/global/effects.css b/frontend/src/global/effects.css index 73d90e1..e4fd014 100755 --- a/frontend/src/global/effects.css +++ b/frontend/src/global/effects.css @@ -4,7 +4,7 @@ padding: 20px; border-radius: var(--rounded); background: var(--white); - box-shadow: var(--shadow); + box-shadow: var(--box-shadow); color: var(--black); } diff --git a/frontend/src/global/styles.css b/frontend/src/global/styles.css index 32b1803..4a444ef 100755 --- a/frontend/src/global/styles.css +++ b/frontend/src/global/styles.css @@ -16,6 +16,8 @@ html { body { margin: 0; + background: var(--white); + color: var(--black); } #app { @@ -177,8 +179,13 @@ td { border-right: solid 1px var(--light-gray); } +thead tr:nth-child(odd), tr:nth-child(even) { - background: #fafafa; + background: var(--white); +} + +tr:nth-child(odd) { + background: var(--alt-white); } thead tr { @@ -213,7 +220,9 @@ textarea { appearance: none; min-width: 0; padding: 5px; - border: solid 1px black; + border: solid 1px var(--black); + background: var(--white); + color: var(--black); font: inherit; } diff --git a/frontend/src/global/theme.css b/frontend/src/global/theme.css index 57856fa..fa14dbf 100755 --- a/frontend/src/global/theme.css +++ b/frontend/src/global/theme.css @@ -1,26 +1,58 @@ -:root { +:root[data-dark="false"] { /* accent */ - --accent: #2a9294; + --accent: hsl(180, 55%, 35%); /** deep */ - --deep: #5c4863; - --deep-mid: #9a7da5; - --deep-light: #f7ede2; + --deep: hsl(285, 20%, 35%); + --deep-mid: hsl(285, 20%, 55%); + --deep-light: hsl(30, 55%, 90%); /** grays */ + --shadow: #00000020; --black: #000000; --off-black: #303030; --dark-gray: #606060; --gray: #b0b0b0; --light-gray: #e0e0e0; --off-white: #f0f0f0; + --alt-white: #fafafa; --white: #ffffff; /** util colors */ - --success: #10b981; - --warning: #f59e0b; - --error: #f43f5e; + --box-shadow: 0 0 2px var(--shadow), 2px 2px 5px var(--shadow); + --success: hsl(160, 90%, 40%); + --warning: hsl(40, 90%, 50%); + --error: hsl(350, 90%, 60%); +} + +:root[data-dark="true"] { + /* accent */ + --accent: hsl(180, 50%, 60%); + /** deep */ + --deep: hsl(285, 20%, 65%); + --deep-mid: hsl(285, 20%, 45%); + --deep-light: hsl(285, 15%, 20%); + + /** grays */ + --shadow: #ffffff40; + --black: #ffffff; + --off-black: #f0f0f0; + --dark-gray: #e0e0e0; + --gray: #b0b0b0; + --light-gray: #606060; + --off-white: #303030; + --alt-white: #181818; + --white: #000000; + + /** util colors */ + --box-shadow: 0 0 2px var(--shadow), 2px 2px 5px var(--shadow); + --success: hsl(160, 90%, 40%); + --warning: hsl(40, 90%, 50%); + --error: hsl(350, 90%, 60%); +} + +:root { /** font families */ --sans: "Poppins", sans-serif; --mono: "IBM Plex Mono", monospace; @@ -34,7 +66,6 @@ --rounded: 5px; --fast: 0.15s ease; --slow: 0.35s ease; - --shadow: 0 0 2px #00000020, 2px 2px 5px #00000020; --focus: 0 0 5px var(--accent); --spacing: 2; --compact: 1.25; diff --git a/frontend/src/pages/Testbed.tsx b/frontend/src/pages/Testbed.tsx index 51a578a..54a3af3 100755 --- a/frontend/src/pages/Testbed.tsx +++ b/frontend/src/pages/Testbed.tsx @@ -52,7 +52,7 @@ import TextBox from "@/components/TextBox"; import Tile from "@/components/Tile"; import { toast } from "@/components/Toasts"; import Tooltip from "@/components/Tooltip"; -import { themeVariables } from "@/util/dom"; +import { useTheme } from "@/util/hooks"; import { formatDate, formatNumber } from "@/util/string"; import tableData from "../../fixtures/table.json"; @@ -61,69 +61,69 @@ const logChange = (...args: unknown[]) => { console.debug(...args); }; +/** generate fake node data */ +const nodes = Array(200) + .fill(null) + .map(() => ({ + id: uniqueId(), + label: sample([ + "Lbl.", + "Label", + "Long Label", + "Really Long Label", + undefined, + ]), + type: sample([ + "gene", + "disease", + "compound", + "anatomy", + "phenotype", + undefined, + ]), + strength: sample([0, 0.1, 0.02, 0.003, 0.0004, 0.00005, undefined]), + extra: sample(["cat", "dog", "bird"]), + })); +const ids = nodes.map((node) => node.id); +/** generate fake edge data */ +const edges = Array(500) + .fill(null) + .map(() => ({ + id: uniqueId(), + label: sample([ + "Lbl.", + "Label", + "Long Label", + "Really Long Label", + undefined, + ]), + source: sample(ids)!, + target: sample(ids)!, + type: sample([ + "causes", + "interacts with", + "upregulates", + "includes", + "presents", + undefined, + ]), + direction: sample([-1, 0, 1, undefined] as const), + strength: sample([10, 11, 12, 13, 14, 15, undefined]), + })); + +/** add some duplicate edges */ +for (let times = 0; times < 10; times++) + edges.push({ ...sample(edges)!, id: uniqueId() }); + +/** add some loop edges */ +for (let times = 0; times < 10; times++) { + const { id } = sample(nodes)!; + const edge = sample(edges)!; + edges.push({ ...edge, id: uniqueId(), source: id, target: id }); +} + /** test and example usage of formatting, elements, components, etc. */ const TestbedPage = () => { - /** generate fake node data */ - const nodes = Array(200) - .fill(null) - .map(() => ({ - id: uniqueId(), - label: sample([ - "Lbl.", - "Label", - "Long Label", - "Really Long Label", - undefined, - ]), - type: sample([ - "gene", - "disease", - "compound", - "anatomy", - "phenotype", - undefined, - ]), - strength: sample([0, 0.1, 0.02, 0.003, 0.0004, 0.00005, undefined]), - extra: sample(["cat", "dog", "bird"]), - })); - const ids = nodes.map((node) => node.id); - /** generate fake edge data */ - const edges = Array(500) - .fill(null) - .map(() => ({ - id: uniqueId(), - label: sample([ - "Lbl.", - "Label", - "Long Label", - "Really Long Label", - undefined, - ]), - source: sample(ids)!, - target: sample(ids)!, - type: sample([ - "causes", - "interacts with", - "upregulates", - "includes", - "presents", - undefined, - ]), - direction: sample([-1, 0, 1, undefined] as const), - strength: sample([10, 11, 12, 13, 14, 15, undefined]), - })); - - /** add some duplicate edges */ - for (let times = 0; times < 10; times++) - edges.push({ ...sample(edges)!, id: uniqueId() }); - - /** add some loop edges */ - for (let times = 0; times < 10; times++) { - const { id } = sample(nodes)!; - const edge = sample(edges)!; - edges.push({ ...edge, id: uniqueId(), source: id, target: id }); - } - return ( <> @@ -148,15 +148,19 @@ const TestbedPage = () => { {/* color palette */} - {Object.entries(themeVariables) + {Object.entries(useTheme()) .filter( ([, value]) => value.startsWith("#") || value.startsWith("hsl"), ) - .map(([variable, color], index) => ( + .map(([variable], index) => (
))} @@ -231,8 +235,7 @@ const TestbedPage = () => {

-          {`
-const popup = document.querySelector("#popup"); 
+          {`const popup = document.querySelector("#popup"); 
 popup.style.width = "100%";
 popup.innerText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.";
 `}
diff --git a/frontend/src/util/dom.ts b/frontend/src/util/dom.ts
index 33732c4..f8b0436 100755
--- a/frontend/src/util/dom.ts
+++ b/frontend/src/util/dom.ts
@@ -3,29 +3,6 @@ import reactToText from "react-to-text";
 import { debounce } from "lodash";
 import { sleep } from "@/util/misc";
 
-/** css on :root */
-const rootStyles = window.getComputedStyle(document.documentElement);
-
-/** theme css variables https://stackoverflow.com/a/78994961/2180570 */
-export const themeVariables = Object.fromEntries(
-  Array.from(document.styleSheets)
-    .flatMap((styleSheet) => {
-      try {
-        return Array.from(styleSheet.cssRules);
-      } catch (error) {
-        return [];
-      }
-    })
-    .filter((cssRule) => cssRule instanceof CSSStyleRule)
-    .flatMap((cssRule) => Array.from(cssRule.style))
-    .filter((style) => style.startsWith("--"))
-    .map((variable) => [variable, rootStyles.getPropertyValue(variable)]),
-);
-
-/** get css theme variable */
-export const theme = (variable: `--${string}`) =>
-  themeVariables[variable] ?? "";
-
 /** wait for element matching selector to appear, checking periodically */
 export const waitFor = async (
   selector: string,
diff --git a/frontend/src/util/hooks.ts b/frontend/src/util/hooks.ts
new file mode 100644
index 0000000..36010c7
--- /dev/null
+++ b/frontend/src/util/hooks.ts
@@ -0,0 +1,38 @@
+import { useEffect, useState } from "react";
+import { useAtom } from "jotai";
+import { darkModeAtom } from "@/components/DarkMode";
+
+/** https://stackoverflow.com/a/78994961/2180570 */
+export const getTheme = () => {
+  const rootStyles = window.getComputedStyle(document.documentElement);
+  return Object.fromEntries(
+    Array.from(document.styleSheets)
+      .flatMap((styleSheet) => {
+        try {
+          return Array.from(styleSheet.cssRules);
+        } catch (error) {
+          return [];
+        }
+      })
+      .filter((cssRule) => cssRule instanceof CSSStyleRule)
+      .flatMap((cssRule) => Array.from(cssRule.style))
+      .filter((style) => style.startsWith("--"))
+      .map((variable) => [variable, rootStyles.getPropertyValue(variable)]),
+  );
+};
+
+/** get theme css variables */
+export const useTheme = () => {
+  /** set of theme variable keys and values */
+  const [theme, setTheme] = useState>({});
+
+  /** dark mode state */
+  const [getDarkMode] = useAtom(darkModeAtom);
+
+  /** update theme variables when anything that could affect them changes */
+  useEffect(() => {
+    setTheme(getTheme());
+  }, [getDarkMode]);
+
+  return theme;
+};
diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts
index 51dfa48..1bc3a16 100755
--- a/frontend/src/vite-env.d.ts
+++ b/frontend/src/vite-env.d.ts
@@ -1,9 +1,6 @@
 /// 
 /// 
 
-/** cytoscape layout algorithm libraries */
-declare module "cytoscape-*" {
-  // eslint-disable-next-line
-  const x: any;
-  export = x;
-}
+/** no type def libraries for these libraries */
+declare module "cytoscape-cola";
+declare module "cytoscape-spread";
diff --git a/frontend/tests/accessibility.spec.ts b/frontend/tests/accessibility.spec.ts
index 3edd45e..fb0d4d1 100755
--- a/frontend/tests/accessibility.spec.ts
+++ b/frontend/tests/accessibility.spec.ts
@@ -1,4 +1,5 @@
-import { configureAxe, getViolations, injectAxe } from "axe-playwright";
+import registerAPCACheck from "apca-check";
+import { AxeBuilder } from "@axe-core/playwright";
 import { expect, test } from "@playwright/test";
 import analyses from "@/fixtures/analyses.json" with { type: "json" };
 import { log } from "./util";
@@ -15,25 +16,13 @@ const paths = [
   ...analyses.map((analysis) => `/analysis/${analysis.id}`),
 ];
 
-/** axe rule overrides */
-const rules = [
-  /**
-   * color contrast standards are often not correct:
-   *
-   * https://github.com/w3c/wcag/issues/695
-   * https://uxmovement.com/buttons/the-myths-of-color-contrast-accessibility/
-   * https://github.com/Myndex/SAPC-APCA
-   * https://twitter.com/AlPackah/status/1773375760125857949
-   * https://twitter.com/DanHollick/status/1417895151003865090
-   * https://twitter.com/DanHollick/status/1468958644364402702
-   * https://twitter.com/argyleink/status/1329091518032867328
-   */
-  { id: "color-contrast", enabled: false },
-];
+/** NOT WORKING YET https://github.com/StackExchange/apca-check/issues/143 */
+registerAPCACheck("bronze");
 
 /** generic page axe test */
 const checkPage = (path: string) =>
   test(`Accessibility check ${path}`, async ({ page, browserName }) => {
+    /** axe tests should be independent of browser, so only run one */
     test.skip(browserName !== "chromium", "Only test Axe on chromium");
 
     /** navigate to page */
@@ -43,14 +32,21 @@ const checkPage = (path: string) =>
     await page.waitForSelector("footer");
     await page.waitForTimeout(1000);
 
-    /** setup axe */
-    await injectAxe(page);
-    await configureAxe(page, { rules });
-
     /** axe check */
-    const violations = await getViolations(page);
-    const violationsMessage = JSON.stringify(violations, null, 2);
-    expect(violationsMessage).toBe("[]");
+    const check = async () => {
+      const { violations } = await new AxeBuilder({ page })
+        /** https://github.com/dequelabs/axe-core/issues/3325 */
+        .options({ rules: { "color-contrast": { enabled: false } } })
+        .analyze();
+      expect(violations).toEqual([]);
+    };
+
+    await check();
+    /** check dark mode */
+    await page
+      .locator("header button[role='switch'][aria-label*='mode']")
+      .click();
+    await check();
   });
 
 /** check all pages */