fix(a11y): WCAG AA contrast retune for the Otter Orange palette (#567)
Fixes #557. Vivid fill, ink label: brand #E07832 stays on fills while primary-foreground flips to #1A1814 (5.83:1); new theme-aware ink tokens carry orange, destructive, and success text roles; opacity-modified text purged; landing, demo, and the docs fund button retuned. Guarded by a CSS-parsing unit contrast test, rebuilt axe baselines with zero contrast entries, a new landing axe smoke, and fully regenerated darwin visual baselines.
@@ -117,7 +117,7 @@ test.describe("No data leak when analytics disabled", () => {
|
||||
await processBtn.click();
|
||||
await waitForProcessingDone(page);
|
||||
|
||||
const error = page.locator(".text-red-500");
|
||||
const error = page.locator(".text-destructive-ink, .text-red-500");
|
||||
expect(await error.isVisible({ timeout: 2_000 }).catch(() => false)).toBe(false);
|
||||
|
||||
const downloadLink = page.locator(
|
||||
|
||||
@@ -65,7 +65,7 @@ async function verifyAccurateOcrTier(
|
||||
await processButton.click();
|
||||
await waitForProcessingDone(page, 120_000);
|
||||
await expect(page.getByText(/extracted text/i)).toBeVisible({ timeout: 120_000 });
|
||||
await expect(page.locator(".text-red-500")).toHaveCount(0);
|
||||
await expect(page.locator(".text-destructive-ink, .text-red-500")).toHaveCount(0);
|
||||
}
|
||||
|
||||
// ─── 1. Error messages should never show [object Object] ─────────────
|
||||
@@ -114,7 +114,7 @@ test.describe("Image-to-PDF", () => {
|
||||
await waitForProcessingDone(page);
|
||||
|
||||
// Should NOT see "Authentication required"
|
||||
const errorEl = page.locator(".text-red-500, [class*='text-red']");
|
||||
const errorEl = page.locator(".text-destructive-ink, .text-red-500, [class*='text-red']");
|
||||
if (await errorEl.isVisible({ timeout: 3000 })) {
|
||||
const text = await errorEl.textContent();
|
||||
expect(text).not.toContain("Authentication required");
|
||||
@@ -140,7 +140,7 @@ test.describe("Image-to-PDF", () => {
|
||||
await processBtn.click();
|
||||
await waitForProcessingDone(page);
|
||||
|
||||
const errorEl = page.locator(".text-red-500, [class*='text-red']");
|
||||
const errorEl = page.locator(".text-destructive-ink, .text-red-500, [class*='text-red']");
|
||||
if (await errorEl.isVisible({ timeout: 3000 })) {
|
||||
const text = await errorEl.textContent();
|
||||
expect(text).not.toContain("Authentication required");
|
||||
@@ -176,7 +176,7 @@ test.describe("Split tool", () => {
|
||||
await expect(page.getByRole("button", { name: /download all/i })).toBeVisible();
|
||||
|
||||
// No errors
|
||||
const error = page.locator(".text-red-500");
|
||||
const error = page.locator(".text-destructive-ink, .text-red-500");
|
||||
expect(await error.isVisible({ timeout: 1000 }).catch(() => false)).toBe(false);
|
||||
});
|
||||
|
||||
@@ -286,7 +286,9 @@ test.describe("Passport photo", () => {
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
// Check for error message
|
||||
const errorEl = page.locator(".text-red-500, [class*='text-red'], [class*='error']");
|
||||
const errorEl = page.locator(
|
||||
".text-destructive-ink, .text-red-500, [class*='text-red'], [class*='error']",
|
||||
);
|
||||
if (await errorEl.isVisible({ timeout: 10_000 })) {
|
||||
const text = await errorEl.textContent();
|
||||
expect(text).not.toContain("[object Object]");
|
||||
@@ -309,7 +311,7 @@ test.describe("Passport photo", () => {
|
||||
|
||||
// Face detection should succeed — look for the Generate button
|
||||
const generateBtn = page.getByRole("button", { name: /generate|create/i });
|
||||
const analyzeError = page.locator("p.text-red-500");
|
||||
const analyzeError = page.locator("p.text-destructive-ink, .text-red-500");
|
||||
const gotButton = await generateBtn.isVisible({ timeout: 10_000 }).catch(() => false);
|
||||
const gotError = await analyzeError.isVisible({ timeout: 1000 }).catch(() => false);
|
||||
if (gotError) {
|
||||
@@ -343,7 +345,7 @@ test.describe("OCR", () => {
|
||||
// this GUI regression must reach the result state, even when the fixture
|
||||
// legitimately contains no recognized text.
|
||||
await expect(page.getByText(/extracted text/i)).toBeVisible({ timeout: 120_000 });
|
||||
await expect(page.locator("p.text-red-500")).toHaveCount(0);
|
||||
await expect(page.locator("p.text-destructive-ink, .text-red-500")).toHaveCount(0);
|
||||
await expect(
|
||||
page.getByTestId("ocr-result-text").or(page.getByText(/no text detected/i)),
|
||||
).toBeVisible();
|
||||
@@ -382,7 +384,7 @@ test.describe("Regression checks", () => {
|
||||
await processBtn.click();
|
||||
await waitForProcessingDone(page);
|
||||
|
||||
const error = page.locator(".text-red-500");
|
||||
const error = page.locator(".text-destructive-ink, .text-red-500");
|
||||
expect(await error.isVisible({ timeout: 2000 }).catch(() => false)).toBe(false);
|
||||
});
|
||||
|
||||
@@ -395,7 +397,7 @@ test.describe("Regression checks", () => {
|
||||
await processBtn.click();
|
||||
await waitForProcessingDone(page);
|
||||
|
||||
const error = page.locator(".text-red-500");
|
||||
const error = page.locator(".text-destructive-ink, .text-red-500");
|
||||
expect(await error.isVisible({ timeout: 2000 }).catch(() => false)).toBe(false);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import AxeBuilder from "@axe-core/playwright";
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Color-contrast smoke for the landing site (issue #557). The landing shares
|
||||
* the Otter palette with the app but had no axe coverage, so AA regressions
|
||||
* shipped unnoticed. Scans the pages that exercise every palette role:
|
||||
* hero + CTA gradient + category cards (/), tool grid chips (/tools),
|
||||
* dark sections and comparison table (/enterprise), prose links (/faq),
|
||||
* and the SEO card grids (/self-hosted).
|
||||
*/
|
||||
|
||||
const PAGES = ["/", "/tools", "/enterprise", "/faq", "/self-hosted"];
|
||||
|
||||
for (const path of PAGES) {
|
||||
test(`landing ${path} has no color-contrast violations`, async ({ page }) => {
|
||||
await page.goto(path);
|
||||
await page.waitForLoadState("networkidle");
|
||||
const results = await new AxeBuilder({ page }).withTags(["wcag2aa"]).analyze();
|
||||
const contrast = results.violations.filter((v) => v.id === "color-contrast");
|
||||
const offenders = contrast.flatMap((v) =>
|
||||
v.nodes.map((n) => `${n.target.join(" ")}: ${n.failureSummary?.split("\n")[1] ?? ""}`),
|
||||
);
|
||||
expect(offenders, `color-contrast violations on ${path}`).toEqual([]);
|
||||
});
|
||||
}
|
||||
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 50 KiB |