diff --git a/.changeset/dry-cats-watch.md b/.changeset/dry-cats-watch.md
new file mode 100644
index 00000000..c82dee47
--- /dev/null
+++ b/.changeset/dry-cats-watch.md
@@ -0,0 +1,5 @@
+---
+"@cobalt-ui/lint-a11y": patch
+---
+
+Fix issue with bodyText calculation
diff --git a/.changeset/selfish-coats-hug.md b/.changeset/selfish-coats-hug.md
new file mode 100644
index 00000000..890523d3
--- /dev/null
+++ b/.changeset/selfish-coats-hug.md
@@ -0,0 +1,5 @@
+---
+"@cobalt-ui/lint-a11y": patch
+---
+
+Fix silver <> bronze confusion
diff --git a/.prettierrc b/.prettierrc
index 331fd82d..e493a511 100644
--- a/.prettierrc
+++ b/.prettierrc
@@ -1,5 +1,13 @@
{
"printWidth": 180,
"singleQuote": true,
- "trailingComma": "all"
+ "trailingComma": "all",
+ "overrides": [
+ {
+ "files": ["*.md"],
+ "options": {
+ "singleQuote": false
+ }
+ }
+ ]
}
diff --git a/docs/integrations/a11y.md b/docs/integrations/a11y.md
index 7692e43f..0aba3d2e 100644
--- a/docs/integrations/a11y.md
+++ b/docs/integrations/a11y.md
@@ -16,12 +16,12 @@ Then add to your `tokens.config.js` file:
```js
// tokens.config.js
-import a11y from '@cobalt-ui/lint-a11y';
+import a11y from "@cobalt-ui/lint-a11y";
/** @type {import('@cobalt-ui/core').Config} */
export default {
- tokens: './tokens.json',
- outDir: './tokens/',
+ tokens: "./tokens.json",
+ outDir: "./tokens/",
plugins: [a11y()],
lint: {
// checks
@@ -34,27 +34,27 @@ export default {
The contrast check asserts your token combinations the latest WCAG 2.1 and [APCA](https://www.myndex.com/APCA/) (WCAG 3 proposal) formulae. Add an array of `checks` to test:
```js
-import a11y from '@cobalt-ui/lint-a11y';
+import a11y from "@cobalt-ui/lint-a11y";
/** @type {import('@cobalt-ui/core').Config} */
export default {
- tokens: './tokens.json',
- outDir: './tokens/',
+ tokens: "./tokens.json",
+ outDir: "./tokens/",
plugins: [a11y()],
lint: {
rules: {
- 'a11y/contrast': [
- 'error',
+ "a11y/contrast": [
+ "error",
{
checks: [
{
tokens: {
- foreground: 'color.semantic.text',
- background: 'color.semantic.bg',
- typography: 'typography.body',
- modes: ['light', 'dark'],
+ foreground: "color.semantic.text",
+ background: "color.semantic.bg",
+ typography: "typography.body",
+ modes: ["light", "dark"],
},
- wcag2: 'AAA',
+ wcag2: "AAA",
apca: true,
},
],
@@ -69,94 +69,107 @@ export default {
Within each check group, specify:
-| Name | Type | Description |
-| :------------------ | :--------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------- |
-| `tokens` | `object` | A group of tokens to test together. |
-| `tokens.foreground` | `string` | The ID of the foreground color. |
-| `tokens.background` | `string` | The ID of the background color. |
-| `tokens.typography` | `string` | (optional) The ID of a typography stack |
-| `tokens.modes` | `string[]` | (optional) Any modes you’d like to test |
-| `wcag2` | `string \| number \| false` | Specify `'AA'` or `'AAA'` compliance (or a minimum contrast), or `false` to disable (default: `'AA'`). See [WCAG 2](#wcag-2) |
-| `apca` | `'bronze' \| 'bronze-body' \| number \| false` | Enable with `'bronze'` or `'bronze-body'`, or a specific Lc `number` (default: `false`). See [APCA](#apca). |
+| Name | Type | Description |
+| :------------------ | :-----------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------- |
+| `tokens` | `object` | A group of tokens to test together. |
+| `tokens.foreground` | `string` | The ID of the foreground color. |
+| `tokens.background` | `string` | The ID of the background color. |
+| `tokens.typography` | `string` | (optional) The ID of a typography stack |
+| `tokens.modes` | `string[]` | (optional) Any modes you’d like to test |
+| `wcag2` | `string \| number \| false` | Specify `"AA"` or `"AAA"` compliance (or a minimum contrast), or `false` to disable (default: `"AA"`). See [WCAG 2](#wcag-2) |
+| `apca` | `"silver" \| "silver-nonbody" \| number \| false` | Specify Silver compliance or a specific Lc `number` (default: `false`). See [APCA](#apca). |
#### WCAG 2
The [WCAG 2 contrast formula](https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum) is represented by the `wcag2` setting and accepts either a string, number, or `false`:
-| Value | Number Equivalent | Minimum Contrast |
-| :------------------- | ----------------: | :--------------- |
-| `'AA'` (default) | `4.5` | 4.5:1 |
-| `'AA'` (large text) | `3` | 3:1 |
-| `'AAA'` | `7` | 7:1 |
-| `'AAA'` (large text) | `4.5` | 4.5:1 |
-| `false` | `0` | (any) |
-
-The WCAG 2 standard is the most common contrast standard, so `'AA'` level is enforced by default by this plugin.
-
-If you pass a `typography` token, it will take font size and weight into account (meaning for larger fonts, you don’t need as much color contrast). If no `typography` token is passed, it will assume default font size (16px, normal).
-
-#### APCA
-
-::: warning
+```ts
+{
+ checks: [
+ {
+ tokens: { /* … */ },
+ wcag2: 'AA'; // "AAA" | "AA" | number | false
+ },
+ ],
+}
+```
-APCA is still a draft, and not part of WCAG 3. But APCA _is_ [well-researched](https://github.com/Myndex/SAPC-APCA?tab=readme-ov-file) and [widely-regarded](https://evilmartians.com/opensource/polychrom) as an improvement over WCAG 2. Compliance with APCA doesn’t guarantee compliance with WCAG 3 when it releases.
+The WCAG 2 standard is the most common contrast standard, so `"AA"` level is enforced by default by this plugin.
-:::
+Add a `typography` token value to automatically figure out if you’re using [large text](https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum#dfn-large-scale) (which lowers the minimum contrast requirement).
-The [APCA contrast algorithm](https://www.myndex.com/APCA/) is still in beta, but is a likely candidate for the upcoming WCAG 3 contrast algorithm. APCA uses the [latest research in color science](https://git.apcacontrast.com/documentation/WhyAPCA) and implements more perceptual contrast ratios that yield more accurate results. But it can be tricky to understand.
+#### APCA (beta)
-| Setting | Description |
-| :-------------- | :------------------------------------------------------------------------------- |
-| `'bronze'` | Enforce [Bronze](https://www.myndex.com/APCA/#Bronze)-level conformance. |
-| `'bronze-body'` | Enforce Bronze-level compliance for body text (must hit `15 Lc` higher contrast) |
-| `number` | ⚠️ Advanced users: specify Lc Contrast (which will differ based on typography). |
-| `false` | Disable APCA. |
+The [APCA contrast algorithm](https://www.myndex.com/APCA/) is still in beta, but is a likely candidate for the upcoming WCAG 3 contrast algorithm. Like WCAG 2 there is a “contrast ratio” under-the-hood, but it’s referred to as “perceptual lightness contrast,” or **Lc**. It’s a number ranging from `0` – `100`, and like WCAG 2 your target number depends on a few factors. The rough equivalence is:
-##### Guide
+| Lc | WCAG 2 Contrast Ratio |
+| ------------: | :-------------------- |
+| `90` | 7:1 |
+| `75` | 4.5:1 |
+| `60` | 3:1 |
-The recommended setting is first starting with `'bronze'` enforcement (somewhat-equivalent to `'AA'` in WCAG 2):
+But the math isn’t as simple as WCAG 2; you’ll have to do some work to determine what your target Lc is for any color/font size/font weight trio. And so, APCA specifies “Bronze” and “Silver” levels of compliance (“Gold” isn’t outlined yet). Cobalt can determine Silver compliance automatically for you, but you’ll need to manually manage Bronze compliance for now (explained below).
-```js
+```ts
{
- apca: 'bronze', // or 'bronze-body' for body text
+ checks: [
+ {
+ tokens: { /* … */ },
+ apca: "silver"; // "silver" | "silver-nonbody" | number | false
+ },
+ ],
}
```
-Then, after running a check, you’ll either **pass** or **fail.** If you fail, the error message will show you by _how much_ you were off (e.g. `Expected 75, received 70`). You then have 3 courses of action:
+| Setting | Description |
+| :----------------- | :----------------------------------------------------------------------------------------------------------------- |
+| `"bronze"` | (not supported in Cobalt; set Lc manually [following “Bronze” guide](https://www.myndex.com/APCA/)) |
+| `"silver"` | Enforce [Silver](https://www.myndex.com/APCA/#Silver)-level compliance for body text. Requires `typography` token. |
+| `"silver-nonbody"` | Silver-level compliance for non-body text (less strict). Requires `typography` token. |
+| `"gold"` | (not supported in APCA yet) |
+| `number` | ⚠️ Advanced users: specify Lc. |
+| `false` | Disable APCA. |
-1. **Increase contrast.** Simply adjust the color to a higher contrast, or adjust the font size / font weight.
-2. **Accept the current level.** If you feel like Cobalt calculated your contrast wrong (see [FAQs](#faq)), you can change `apca` to a passing Lc number (e.g. `apca: 59`).
-3. **Disable APCA for this check**. Perhaps this particular color combination, or typography stack, isn’t performing well under the APCA working draft. Simply set `apca: false` to skip this check.
+::: warning
-##### FAQ
+APCA is still a draft, and not part of WCAG 3. But APCA _is_ [well-researched](https://github.com/Myndex/SAPC-APCA?tab=readme-ov-file) and [widely-regarded](https://evilmartians.com/opensource/polychrom) as an improvement over WCAG 2. Compliance with APCA doesn’t guarantee compliance with WCAG 3 when it releases.
-::: details Can I test APCA without typography?
+:::
-Yes! Just omit the `typography` token.
+##### Silver (auto) mode vs Number (manual) mode
-:::
+The same basic principle applies to both WCAG 2 and APCA: **contrast is a triangle of color–font size–font weight.** Lc only refers to **color contrast**—you then have to figure out the other two “points” of the triangle—font size and font weight—to complete the picture. It makes sense when you think about it: if you only factored in color contrast, a 2px-tall font would “pass” if the color contrast were good enough.
-::: details I’m failing contrast, but I can’t change my colors, font size, or font weight. What do I do?
+However, trying to reduce typography into pure math turns into a can of worms quickly, so APCA makes some concessions in letting you, the designer, have more say over what your target Lc is. More flexibility comes at the cost of more manual intervention. To navigate that, Cobalt has 2 “flavors” of APCA support: **Silver compliance** (auto), and **Number** (manual).
-APCA’s typography tables are based off **Helvetica.** Most people aren’t using Helvetica as their brand font, so APCA allows some wiggle room in interpreting your _actual_ contrast numbers (see [Notes on Font Size & Weight](https://www.myndex.com/APCA/)). Read the guide and see if your _actual_ Lc contrast numbers are different than what Cobalt is reporting, and adjust by-hand (`apca: 59`).
+###### Silver (auto) mode
-:::
+Silver mode requires a `typography` token; it will err without that. But with all that provided, Cobalt does the work for you. Set `apca` to `"silver"` (body text) or `"silver-nonbody"` (non-body text; less strict). If this works for you, _yay_.
-::: details What’s the difference between `'bronze'` and `'bronze-body'`?
+###### Number (manual) mode
-The [APCA Guide](https://www.myndex.com/APCA/) has stricter contrast requirements on body text because it’s your most frequently-occurring type style. However, to give designers control over the design itself, there’s no hard numbers on what constitutes a “body” type style. So you’ll have to tell Cobalt “this is my body text” with `'bronze-body'`.
+As mentioned earlier, APCA gives you more autonomy over declaring what your “body” font style is, but at the tradeoff of no concrete numbers. In Number (manual) mode, you’ll have to specify Lc manually. Using manual mode you can still pass Bronze and Silver levels, but all Cobalt can do is **catch regressions;** the compliance part is up to you. Here’s the guide copied directly from [APCA’s](https://www.myndex.com/APCA/) (all rights ©️ Myndex):
-:::
+| Lc | Description |
+| ------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `90` | Preferred level for fluent text and columns of body text with a font no smaller than 18px/weight 300 or 14px/weight 400 (normal), or non-body text with a font no smaller than 12px. Also a recommended minimum for extremely thin fonts with a minimum of 24px at weight 200. Lc 90 is a **suggested maximum** for **very large and bold fonts** (greater than 36px bold), and large areas of color. |
+| `75` | The _minimum_ level for columns of body text with a font no smaller than 24px/300 weight, 18px/400, 16px/500 and 14px/700. This level may be used with non-body text with a font no smaller than 15px/400. Also, Lc 75 should be considered a minimum for larger for any larger text where readability is important. |
+| `60` | The _minimum_ level recommended for content text that is not body, column, or block text. In other words, text you want people to read. The minimums: no smaller than 48px/200, 36px/300, 24px normal weight (400), 21px/500, 18px/600, 16px/700 (bold). These values based on the reference font Helvetica. To use these sizes as body text, add Lc 15 to the minimum contrast. |
+| `45` | The _minimum_ for larger, heavier text (36px normal weight or 24px bold) such as headlines, and large text that should be fluently readabile but is not body text. This is also the minimum for pictograms with fine details, or smaller outline icons, , no less than 4px in its smallest dimension. |
+| `30` | The _absolute minimum_ for any text not listed above, which means non-content text considered as "spot readable". This includes placeholder text and disabled element text, and some non-content like a copyright bug. This is also the minimum for large/solid semantic & understandable non-text elements such as "mostly solid" icons or pictograms, no less than 10px in its smallest dimension. |
+| `15` | The _absolute minimum_ for any non-text that needs to be **discernible** and differentiable, but does not apply to semantic non-text such as icons, and is no less than 15px in its smallest dimention. This may include dividers, and in some cases large buttons or thick focus visible outlines, but does not include fine details which have a higher minimum. **Designers should treat anything below this level as invisible,** as it will not be visible for many users. This minimum level should be avoided for any items important to the use, understanding, or interaction of the site. |
-::: details Can I test with Silver or Gold?
+::: details Note on how APCA handles typography
-Not currently. The Silver guidelines are only half-defined, and Gold currently isn’t defined at all. If Bronze isn’t enough of a challenge for you, you can always specify manual Lc numbers (e.g. `apca: 90`)!
+APCA’s typography tables are based off **Helvetica.** Most people aren’t using Helvetica as their brand font, so APCA allows some wiggle room in interpreting your _actual_ contrast numbers (see [Notes on Font Size & Weight](https://www.myndex.com/APCA/)). Read the guide and see if your _actual_ Lc is different than what Cobalt is reporting, and adjust by-hand (`apca: 59`).
:::
#### Bridge PCA
-Coming Soon!
+[Bridge PCA](https://www.myndex.com/BPCA/) is also a creation of Myndex (same as APCA) and is meant to help “bridge” the gap between WCAG 2 and APCA.
+
+Bridge PCA isn’t supported yet but will be in an upcoming release.
### Others
diff --git a/packages/lint-a11y/src/apca.ts b/packages/lint-a11y/src/apca.ts
index ee7ce180..63bffce9 100644
--- a/packages/lint-a11y/src/apca.ts
+++ b/packages/lint-a11y/src/apca.ts
@@ -189,7 +189,7 @@ export const APCA_FONT_LOOKUP_TABLE: [number, [number, number, number, number, n
* 777: non-text
* 999: unacceptable
*/
-export function getMinimumLc(fontSize: string | number, fontWeight: number, bodyText = false): number {
+export function getMinimumSilverLc(fontSize: string | number, fontWeight: number, bodyText = true): number {
if (!(fontWeight > 0 && fontWeight < 1000)) {
throw new Error(`Invalid font weight: ${fontWeight}`);
}
@@ -207,5 +207,7 @@ export function getMinimumLc(fontSize: string | number, fontWeight: number, body
// with baseLc calculated, we may need to interpolate between font sizes; do so with simple linear interpolation
const stepUpLc = sizeDelta > 0 ? APCA_FONT_LOOKUP_TABLE[sizeRowI - 1]![1][weightColI]! : baseLc;
const finalLc = sizeDelta > 0 ? baseLc + sizeDelta * (stepUpLc - baseLc) : baseLc;
- return bodyText ? finalLc + 15 : finalLc;
+
+ // note; the “add 15 Lc to body text” requirement is hard
+ return finalLc + (bodyText && finalLc <= 70 ? 15 : 0);
}
diff --git a/packages/lint-a11y/src/index.ts b/packages/lint-a11y/src/index.ts
index 4b162f27..7dc85015 100644
--- a/packages/lint-a11y/src/index.ts
+++ b/packages/lint-a11y/src/index.ts
@@ -2,7 +2,7 @@ import { type Plugin, type ParsedToken, type LintNotice, type ParsedColorToken,
import { blend, modeRgb, modeHsl, modeHsv, modeP3, modeOkhsl, modeOklch, modeOklab, modeXyz50, modeXyz65, modeLrgb, useMode, wcagContrast } from 'culori/fn';
import { APCAcontrast } from 'apca-w3';
import { isWCAG2LargeText, round } from './lib.js';
-import { getMinimumLc } from './apca.js';
+import { getMinimumSilverLc } from './apca.js';
// register colorspaces for Culori to parse (these are side-effect-y)
useMode(modeHsl);
@@ -45,7 +45,7 @@ export interface RuleContrastCheck {
/** Enforce WCAG 2 contrast checking? (default: 'AA') */
wcag2?: 'AA' | 'AAA' | number | false;
/** Enforce APCA contrast checking? (default: false) @see https://www.myndex.com/APCA/ */
- apca?: 'bronze' | 'bronze-body' | number | false;
+ apca?: 'silver' | 'silver-nonbody' | number | false;
}
function evaluateContrast(tokens: ParsedToken[], options: RuleContrastOptions): LintNotice[] {
@@ -92,9 +92,11 @@ function evaluateContrast(tokens: ParsedToken[], options: RuleContrastOptions):
const minContrast = typeof wcag2 === 'string' ? WCAG2_MIN_CONTRAST[wcag2][isLargeText ? 'large' : 'default'] : wcag2;
const defaultResult = wcagContrast(fg, bg);
if (defaultResult < minContrast) {
+ const modeText = mode === '.' ? '' : ` (mode: ${mode})`;
+ const levelText = typeof wcag2 === 'string' ? ` ("${wcag2}")` : '';
notices.push({
id: RULES.contrast,
- message: `WCAG 2: Token pair ${fg}, ${bg}${mode === '.' ? '' : ` (mode: ${mode})`} failed contrast. Expected ${minContrast}:1, received ${round(defaultResult)}:1`,
+ message: `WCAG 2: Token pair ${fg}, ${bg}${modeText} failed contrast. Expected ${minContrast}:1${levelText}, received ${round(defaultResult)}:1`,
});
}
}
@@ -102,8 +104,14 @@ function evaluateContrast(tokens: ParsedToken[], options: RuleContrastOptions):
// APCA
if (typeof apca === 'string' || (typeof apca === 'number' && Math.abs(apca) > 0)) {
- if (typeof apca === 'string' && apca !== 'bronze' && apca !== 'bronze-body') {
- throw new Error(`APCA: expected value \`'bronze'\` or \`'bronze-body'\`, received ${apca}`);
+ if ((apca as string) === 'gold') {
+ throw new Error(`APCA: "gold" not implemented; specify "silver", "silver-nonbody", Lc \`number\`, or \`false\`.`);
+ }
+ if ((apca as string) === 'bronze') {
+ throw new Error(`APCA: "bronze" not supported; specify an Lc \`number\` manually.`);
+ }
+ if (typeof apca === 'string' && apca !== 'silver' && apca !== 'silver-nonbody') {
+ throw new Error(`APCA: expected value "silver" or "silver-nonbody", received "${apca}"`);
}
const testSets: {
@@ -133,20 +141,20 @@ function evaluateContrast(tokens: ParsedToken[], options: RuleContrastOptions):
}
for (const { fgY, fgRaw, bgY, bgRaw, mode, fontSize, fontWeight } of testSets) {
+ if ((apca === 'silver' || apca === 'silver-nonbody') && (!fontSize || !fontWeight)) {
+ throw new Error(`APCA: "${apca}" compliance requires \`typography\` token. Use manual number if omitted.`);
+ }
const lc = APCAcontrast(fgY, bgY);
if (typeof lc === 'string') {
throw new Error(`Internal error: expected number, APCA returned "${lc}"`); // types are wrong?
}
- let minContrast = 60;
- if (typeof apca === 'number') {
- minContrast = apca;
- } else if (fontSize && fontWeight) {
- minContrast = getMinimumLc(fontSize, fontWeight, apca === 'bronze-body');
- }
+ const minContrast = typeof apca === 'number' ? apca : getMinimumSilverLc(fontSize!, fontWeight!, apca === 'silver');
if (Math.abs(lc) < minContrast) {
+ const modeText = mode === '.' ? '' : ` (mode: ${mode})`;
+ const levelText = typeof apca === 'string' ? ` ("${apca}")` : '';
notices.push({
id: RULES.contrast,
- message: `APCA: Token pair ${fgRaw}, ${bgRaw}${mode === '.' ? '' : ` (mode: ${mode})`} failed contrast. Expected ${minContrast}, received ${round(Math.abs(lc))}.`,
+ message: `APCA: Token pair ${fgRaw}, ${bgRaw}${modeText} failed contrast. Expected ${minContrast}${levelText}, received ${round(Math.abs(lc))}`,
});
}
}
diff --git a/packages/lint-a11y/test/apca.test.ts b/packages/lint-a11y/test/apca.test.ts
index 95ad05f8..2534fdf9 100644
--- a/packages/lint-a11y/test/apca.test.ts
+++ b/packages/lint-a11y/test/apca.test.ts
@@ -1,13 +1,20 @@
-import { expect, test } from 'vitest';
-import { getMinimumLc } from '../src/apca.js';
+import { describe, expect, test } from 'vitest';
+import { getMinimumSilverLc } from '../src/apca.js';
-test('getMinimumLc', () => {
- expect(getMinimumLc(16, 400)).toBe(90);
- expect(getMinimumLc('1rem', 400, true)).toBe(105);
- expect(getMinimumLc('18px', 700)).toBe(55);
- expect(getMinimumLc(18, 300)).toBe(100);
- expect(getMinimumLc(21, 300)).toBe(90);
- expect(getMinimumLc('19.5px', 300)).toBe(95);
- expect(getMinimumLc(28, 200)).toBe(100);
- expect(getMinimumLc('96px', 900)).toBe(30);
+describe('getMinimumSilverLc', () => {
+ const tests: [string, { given: Parameters; want: ReturnType }][] = [
+ ['14px/400', { given: [14, 400, true], want: 100 }],
+ ['16px/400', { given: [16, 400, true], want: 90 }],
+ ['1rem/400', { given: ['1rem', 400, true], want: 90 }],
+ ['18px/700', { given: ['18px', 700, false], want: 55 }],
+ ['18px/300', { given: [18, 300, false], want: 100 }],
+ ['21px/300', { given: [21, 300, false], want: 90 }],
+ ['19.5px/300', { given: ['19.5px', 300, false], want: 95 }],
+ ['28px/200', { given: [28, 200, false], want: 100 }],
+ ['96px/900', { given: ['96px', 900, false], want: 30 }],
+ ];
+
+ test.each(tests)('%s', (_, { given, want }) => {
+ expect(getMinimumSilverLc(...given)).toBe(want);
+ });
});
diff --git a/packages/lint-a11y/test/fixtures/tokens.config.js b/packages/lint-a11y/test/fixtures/tokens.config.js
index e029b84a..4e3d6a6f 100644
--- a/packages/lint-a11y/test/fixtures/tokens.config.js
+++ b/packages/lint-a11y/test/fixtures/tokens.config.js
@@ -18,7 +18,7 @@ export default {
typography: 'typography.body',
},
wcag2: 'AAA',
- apca: 'bronze',
+ apca: 'silver',
},
],
},
diff --git a/packages/lint-a11y/test/index.test.ts b/packages/lint-a11y/test/index.test.ts
index 051e1ed7..ae0044b5 100644
--- a/packages/lint-a11y/test/index.test.ts
+++ b/packages/lint-a11y/test/index.test.ts
@@ -1,4 +1,5 @@
import build from '@cobalt-ui/cli/dist/build.js';
+import { type ParseResult } from '@cobalt-ui/core';
import fs from 'node:fs';
import { describe, expect, test } from 'vitest';
import { execa } from 'execa';
@@ -20,7 +21,26 @@ describe('a11y plugin', () => {
},
][] = [
[
- 'passing',
+ 'passing (silver)',
+ {
+ options: {
+ checks: [
+ {
+ tokens: {
+ foreground: 'color.high-contrast-text',
+ background: 'color.high-contrast-bg',
+ typography: 'typography.large',
+ },
+ apca: 'silver',
+ wcag2: 'AAA',
+ },
+ ],
+ },
+ want: { success: true },
+ },
+ ],
+ [
+ 'passing (number)',
{
options: {
checks: [
@@ -58,8 +78,8 @@ describe('a11y plugin', () => {
},
want: {
errors: [
- '[@cobalt-ui/lint-a11y] Error a11y/contrast: WCAG 2: Token pair #606060, #101010 (mode: dark) failed contrast. Expected 7:1, received 3.03:1',
- '[@cobalt-ui/lint-a11y] Error a11y/contrast: APCA: Token pair #606060, #101010 (mode: dark) failed contrast. Expected 75, received 22.38.',
+ '[@cobalt-ui/lint-a11y] Error a11y/contrast: WCAG 2: Token pair #606060, #101010 (mode: dark) failed contrast. Expected 7:1 ("AAA"), received 3.03:1',
+ '[@cobalt-ui/lint-a11y] Error a11y/contrast: APCA: Token pair #606060, #101010 (mode: dark) failed contrast. Expected 75, received 22.38',
],
},
},
@@ -68,13 +88,14 @@ describe('a11y plugin', () => {
'no options provided',
{
options: undefined,
- want: { errors: ['Error: options.checks must be an array'] },
+ want: { errors: ['options.checks must be an array'] },
},
],
];
test.each(tests)('%s', async (name, { options, want }) => {
+ let buildResult: ParseResult;
try {
- const buildResult = await build(tokens, {
+ buildResult = await build(tokens, {
tokens: [tokensURL],
outDir: new URL('./index/', import.meta.url),
plugins: [a11y()],
@@ -89,18 +110,21 @@ describe('a11y plugin', () => {
},
color: {},
});
+ } catch (err) {
+ expect(err.message).toBe(want.errors?.[0]);
+ return;
+ }
- if (want.success) {
- if (buildResult.errors) {
- // eslint-disable-next-line no-console
- console.error(...buildResult.errors);
- }
- expect(buildResult.errors?.[0]).toBeUndefined();
- } else {
- expect(buildResult.errors).toEqual(want.errors);
+ if (want.success) {
+ if (buildResult.errors) {
+ // eslint-disable-next-line no-console
+ console.error(...buildResult.errors);
+ }
+ expect(buildResult.errors?.[0]).toBeUndefined();
+ } else {
+ for (const i in buildResult.errors) {
+ expect(buildResult.errors[i]).toBe(want.errors![i]);
}
- } catch (err) {
- expect(String(err)).toBe(want.errors?.[0]);
}
});
});