From d89bd41dabca47a00d486e847eed0d3df8b5456b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 02:03:57 +0900 Subject: [PATCH 01/10] test(accessibility): define forced-colors state contract --- src/forcedColorsStyles.test.ts | 65 ++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 src/forcedColorsStyles.test.ts diff --git a/src/forcedColorsStyles.test.ts b/src/forcedColorsStyles.test.ts new file mode 100644 index 0000000..a92ff02 --- /dev/null +++ b/src/forcedColorsStyles.test.ts @@ -0,0 +1,65 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +import { describe, expect, it } from 'vitest'; + +const styles = readFileSync(resolve(process.cwd(), 'src/styles.css'), 'utf8'); +const forcedColorsIndex = styles.indexOf('@media (forced-colors: active)'); +const forcedColorsStyles = + forcedColorsIndex >= 0 ? styles.slice(forcedColorsIndex) : ''; + +describe('forced-colors stylesheet contract', () => { + it('defines a forced-colors boundary using system colors', () => { + expect(forcedColorsIndex).toBeGreaterThan(-1); + expect(forcedColorsStyles).toContain('Canvas'); + expect(forcedColorsStyles).toContain('CanvasText'); + expect(forcedColorsStyles).toContain('ButtonFace'); + expect(forcedColorsStyles).toContain('ButtonText'); + expect(forcedColorsStyles).toContain('Highlight'); + expect(forcedColorsStyles).toContain('HighlightText'); + expect(forcedColorsStyles).toContain('GrayText'); + expect(forcedColorsStyles).toContain('LinkText'); + }); + + it('keeps keyboard focus and active toolbar state visible without theme colors', () => { + expect(forcedColorsStyles).toMatch( + /\.cwl-tb-btn:focus-visible\s*\{[^}]*outline:\s*2px\s+solid\s+Highlight\s*;[^}]*outline-offset:\s*2px\s*;/u, + ); + expect(forcedColorsStyles).toMatch( + /\.cwl-tb-btn\.is-active\s*\{[^}]*background:\s*Highlight\s*;[^}]*border-color:\s*Highlight\s*;[^}]*color:\s*HighlightText\s*;/u, + ); + }); + + it('does not use opacity as the only disabled-state cue', () => { + expect(forcedColorsStyles).toMatch( + /\.cwl-tb-btn:disabled\s*\{[^}]*opacity:\s*1\s*;[^}]*color:\s*GrayText\s*;[^}]*border-color:\s*GrayText\s*;/u, + ); + }); + + it('preserves high-contrast chrome, document links, and collaboration cues', () => { + expect(forcedColorsStyles).toMatch( + /\.cwl-editor\s*\{[^}]*color:\s*CanvasText\s*;[^}]*background:\s*Canvas\s*;[^}]*border-color:\s*CanvasText\s*;/u, + ); + expect(forcedColorsStyles).toMatch( + /\.cwl-toolbar[\s\S]*\.cwl-collaboration-status[\s\S]*\{[^}]*background:\s*ButtonFace\s*;[^}]*color:\s*ButtonText\s*;/u, + ); + expect(forcedColorsStyles).toMatch( + /\.cwl-editor__content a\s*\{[^}]*color:\s*LinkText\s*;/u, + ); + expect(forcedColorsStyles).toMatch( + /\.collaboration-cursor__caret\s*\{[^}]*border-left-color:\s*Highlight\s*;/u, + ); + expect(forcedColorsStyles).toMatch( + /\.collaboration-cursor__label\s*\{[^}]*background:\s*Highlight\s*;[^}]*color:\s*HighlightText\s*;/u, + ); + }); + + it('preserves authored structural boundaries in forced colors', () => { + expect(forcedColorsStyles).toMatch( + /\.cwl-tb-group\s*\{[^}]*border-right-color:\s*CanvasText\s*;/u, + ); + expect(forcedColorsStyles).toMatch( + /\.cwl-editor__content code,[\s\S]*\.cwl-editor__content pre,[\s\S]*\.cwl-editor__content th,[\s\S]*\.cwl-editor__content td\s*\{[^}]*border-color:\s*CanvasText\s*;/u, + ); + }); +}); From d94c6b5f6aaf496b8495eed87cb544cf53b6bb82 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 02:09:36 +0900 Subject: [PATCH 02/10] fix(accessibility): preserve forced-colors state cues --- src/styles.css | 79 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/src/styles.css b/src/styles.css index 0970a93..8d5ea18 100644 --- a/src/styles.css +++ b/src/styles.css @@ -107,8 +107,85 @@ } @media (forced-colors: active) { + .cwl-editor { + color: CanvasText; + background: Canvas; + border-color: CanvasText; + } + + .cwl-toolbar, + .cwl-collaboration-status { + background: ButtonFace; + color: ButtonText; + border-bottom-color: CanvasText; + } + + .cwl-tb-group { + border-right-color: CanvasText; + } + + .cwl-tb-btn { + background: ButtonFace; + color: ButtonText; + border-color: ButtonText; + } + + .cwl-tb-btn:hover:not(:disabled) { + background: Highlight; + color: HighlightText; + } + .cwl-tb-btn:focus-visible { - outline-color: CanvasText; + outline: 2px solid Highlight; + outline-offset: 2px; + } + + .cwl-tb-btn.is-active { + background: Highlight; + border-color: Highlight; + color: HighlightText; + } + + .cwl-tb-btn:disabled { + opacity: 1; + color: GrayText; + border-color: GrayText; + } + + .cwl-editor__content a { + color: LinkText; + } + + .cwl-editor__content blockquote { + border-left-color: CanvasText; + color: GrayText; + } + + .cwl-editor__content .is-editor-empty:first-child::before { + color: GrayText; + } + + .cwl-editor__content code, + .cwl-editor__content pre, + .cwl-editor__content th, + .cwl-editor__content td { + border-color: CanvasText; + } + + .cwl-editor__content code, + .cwl-editor__content pre, + .cwl-editor__content th { + background: Canvas; + color: CanvasText; + } + + .collaboration-cursor__caret { + border-left-color: Highlight; + } + + .collaboration-cursor__label { + background: Highlight; + color: HighlightText; } } From 1b34fe4f2a772e97d967af9d90113930bcde18c2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 02:13:13 +0900 Subject: [PATCH 03/10] test(accessibility): verify forced-colors in real engines --- .../specs/forced-colors.browser.spec.ts | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 tests/browser/specs/forced-colors.browser.spec.ts diff --git a/tests/browser/specs/forced-colors.browser.spec.ts b/tests/browser/specs/forced-colors.browser.spec.ts new file mode 100644 index 0000000..7a966dc --- /dev/null +++ b/tests/browser/specs/forced-colors.browser.spec.ts @@ -0,0 +1,136 @@ +import { expect, test } from '@playwright/test'; + +const HARNESS_URL = 'http://127.0.0.1:4173/tests/browser/harness.html'; +const STYLES_URL = 'http://127.0.0.1:4173/dist/cwl-editor.css'; + +const allowHarnessRequest = (requestUrl: string): boolean => { + const url = new URL(requestUrl); + return url.hostname === '127.0.0.1' && url.port === '4173'; +}; + +test.describe.configure({ mode: 'serial' }); + +test.beforeEach(async ({ page }) => { + const rejectedExternalRequests: string[] = []; + await page.route('**/*', async (route) => { + if (allowHarnessRequest(route.request().url())) { + await route.continue(); + return; + } + rejectedExternalRequests.push(new URL(route.request().url()).origin); + await route.abort('blockedbyclient'); + }); + + await page.goto(HARNESS_URL); + await page.addStyleTag({ url: STYLES_URL }); + await page.locator('#harness').evaluate((element) => { + element.innerHTML = ` +
+ +
Connected
+
+
+

+

Link code

+
pre
+
quote
+
HeadCell
+ + Remote + +
+
+
+ `; + }); + expect(rejectedExternalRequests).toEqual([]); +}); + +test('preserves state and structural cues in forced colors', async ({ page }) => { + await page.emulateMedia({ forcedColors: 'active' }); + expect( + await page.evaluate(() => matchMedia('(forced-colors: active)').matches), + ).toBe(true); + + await page.getByRole('button', { name: 'Plain' }).focus(); + + const evidence = await page.evaluate(() => { + const get = (selector: string): T => { + const element = document.querySelector(selector); + if (!element) { + throw new Error(`Missing forced-colors fixture: ${selector}`); + } + return element; + }; + + const editor = get('.cwl-editor'); + const toolbar = get('.cwl-toolbar'); + const group = get('.cwl-tb-group'); + const plainButton = get( + '.cwl-tb-btn:not(.is-active):not(:disabled)', + ); + const activeButton = get('.cwl-tb-btn.is-active'); + const disabledButton = get('.cwl-tb-btn:disabled'); + const collaboration = get('.cwl-collaboration-status'); + const link = get('.cwl-editor__content a'); + const code = get('.cwl-editor__content code'); + const cell = get('.cwl-editor__content td'); + const caret = get('.collaboration-cursor__caret'); + const label = get('.collaboration-cursor__label'); + + const editorStyle = getComputedStyle(editor); + const toolbarStyle = getComputedStyle(toolbar); + const groupStyle = getComputedStyle(group); + const plainStyle = getComputedStyle(plainButton); + const activeStyle = getComputedStyle(activeButton); + const disabledStyle = getComputedStyle(disabledButton); + const collaborationStyle = getComputedStyle(collaboration); + const linkStyle = getComputedStyle(link); + const codeStyle = getComputedStyle(code); + const cellStyle = getComputedStyle(cell); + const caretStyle = getComputedStyle(caret); + const labelStyle = getComputedStyle(label); + + return { + editorBorderStyle: editorStyle.borderTopStyle, + editorBorderWidth: editorStyle.borderTopWidth, + toolbarBorderWidth: toolbarStyle.borderBottomWidth, + groupBorderWidth: groupStyle.borderRightWidth, + focusOutlineStyle: plainStyle.outlineStyle, + focusOutlineWidth: plainStyle.outlineWidth, + activeBorderWidth: activeStyle.borderTopWidth, + disabledOpacity: disabledStyle.opacity, + disabledBorderWidth: disabledStyle.borderTopWidth, + collaborationBorderWidth: collaborationStyle.borderBottomWidth, + linkDecoration: linkStyle.textDecorationLine, + codeBorderWidth: codeStyle.borderTopWidth, + cellBorderWidth: cellStyle.borderTopWidth, + caretBorderWidth: caretStyle.borderLeftWidth, + labelVisible: labelStyle.display !== 'none' && labelStyle.visibility !== 'hidden', + }; + }); + + expect(evidence).toMatchObject({ + editorBorderStyle: 'solid', + editorBorderWidth: '1px', + toolbarBorderWidth: '1px', + groupBorderWidth: '1px', + focusOutlineStyle: 'solid', + focusOutlineWidth: '2px', + activeBorderWidth: '1px', + disabledOpacity: '1', + disabledBorderWidth: '1px', + collaborationBorderWidth: '1px', + linkDecoration: 'underline', + codeBorderWidth: '1px', + cellBorderWidth: '1px', + caretBorderWidth: '2px', + labelVisible: true, + }); +}); From 3fdd42d7ea81a285d7b43b0cd0dc098c3f005942 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 02:18:29 +0900 Subject: [PATCH 04/10] test(accessibility): define editor focus-visible contract --- src/editorFocusStyles.test.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/editorFocusStyles.test.ts diff --git a/src/editorFocusStyles.test.ts b/src/editorFocusStyles.test.ts new file mode 100644 index 0000000..e5d67a8 --- /dev/null +++ b/src/editorFocusStyles.test.ts @@ -0,0 +1,23 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +import { describe, expect, it } from 'vitest'; + +const styles = readFileSync(resolve(process.cwd(), 'src/styles.css'), 'utf8'); +const forcedColorsIndex = styles.indexOf('@media (forced-colors: active)'); +const forcedColorsStyles = + forcedColorsIndex >= 0 ? styles.slice(forcedColorsIndex) : ''; + +describe('editable surface focus stylesheet contract', () => { + it('replaces the removed browser outline with a visible keyboard focus cue', () => { + expect(styles).toMatch( + /\.cwl-editor__content:focus-visible\s*\{[^}]*outline:\s*2px\s+solid\s+var\(--cwl-accent\)\s*;[^}]*outline-offset:\s*-2px\s*;/u, + ); + }); + + it('keeps the editable focus cue visible in forced-colors mode', () => { + expect(forcedColorsStyles).toMatch( + /\.cwl-editor__content:focus-visible\s*\{[^}]*outline:\s*2px\s+solid\s+Highlight\s*;[^}]*outline-offset:\s*-2px\s*;/u, + ); + }); +}); From 3b44e670f3df73fb859ba5b3df43408b7e2688ec Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 02:23:10 +0900 Subject: [PATCH 05/10] fix(accessibility): restore editor focus-visible cue --- src/styles.css | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/styles.css b/src/styles.css index 8d5ea18..dea7616 100644 --- a/src/styles.css +++ b/src/styles.css @@ -206,6 +206,18 @@ outline: none; } +.cwl-editor__content:focus-visible { + outline: 2px solid var(--cwl-accent); + outline-offset: -2px; +} + +@media (forced-colors: active) { + .cwl-editor__content:focus-visible { + outline: 2px solid Highlight; + outline-offset: -2px; + } +} + .cwl-editor__content > * + * { margin-top: 0.75em; } From 23a24f5f20475543e51c97a83a5dc7fc08db9ddb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 02:33:51 +0900 Subject: [PATCH 06/10] test(accessibility): prove editable forced-colors focus cue in browser --- .../browser/specs/forced-colors.browser.spec.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/tests/browser/specs/forced-colors.browser.spec.ts b/tests/browser/specs/forced-colors.browser.spec.ts index 7a966dc..b5c5d52 100644 --- a/tests/browser/specs/forced-colors.browser.spec.ts +++ b/tests/browser/specs/forced-colors.browser.spec.ts @@ -35,7 +35,7 @@ test.beforeEach(async ({ page }) => {
Connected
-
+

Link code

pre
@@ -133,4 +133,18 @@ test('preserves state and structural cues in forced colors', async ({ page }) => caretBorderWidth: '2px', labelVisible: true, }); + + const editable = page.locator('.cwl-editor__content'); + await editable.focus(); + const editableFocusEvidence = await editable.evaluate((element) => { + const style = getComputedStyle(element); + return { + outlineStyle: style.outlineStyle, + outlineWidth: style.outlineWidth, + }; + }); + expect(editableFocusEvidence).toEqual({ + outlineStyle: 'solid', + outlineWidth: '2px', + }); }); From bcd993a1d521d3f44fe07eaeaa5acda10dce3d6b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 02:59:49 +0900 Subject: [PATCH 07/10] test(a11y): prove forced-colors cascade ordering --- src/forcedColorsStyles.test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/forcedColorsStyles.test.ts b/src/forcedColorsStyles.test.ts index a92ff02..3442f37 100644 --- a/src/forcedColorsStyles.test.ts +++ b/src/forcedColorsStyles.test.ts @@ -7,8 +7,22 @@ const styles = readFileSync(resolve(process.cwd(), 'src/styles.css'), 'utf8'); const forcedColorsIndex = styles.indexOf('@media (forced-colors: active)'); const forcedColorsStyles = forcedColorsIndex >= 0 ? styles.slice(forcedColorsIndex) : ''; +const forcedColorsBlocks = styles.match(/@media \(forced-colors: active\)/gu) ?? []; +const lastBaseStateIndex = Math.max( + styles.lastIndexOf('.cwl-editor__content a {'), + styles.lastIndexOf('.cwl-editor__content blockquote {'), + styles.lastIndexOf('.cwl-collaboration-status {'), + styles.lastIndexOf('.collaboration-cursor__label {'), +); describe('forced-colors stylesheet contract', () => { + it('defines one final forced-colors override layer after the base state rules', () => { + expect(forcedColorsIndex).toBeGreaterThan(-1); + expect(forcedColorsBlocks).toHaveLength(1); + expect(lastBaseStateIndex).toBeGreaterThan(-1); + expect(forcedColorsIndex).toBeGreaterThan(lastBaseStateIndex); + }); + it('defines a forced-colors boundary using system colors', () => { expect(forcedColorsIndex).toBeGreaterThan(-1); expect(forcedColorsStyles).toContain('Canvas'); From bd109253141bcda7b0514de95d25f43ee2710602 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 03:03:55 +0900 Subject: [PATCH 08/10] fix(a11y): place forced-colors overrides after base rules --- src/styles.css | 178 ++++++++++++++++++++++++------------------------- 1 file changed, 88 insertions(+), 90 deletions(-) diff --git a/src/styles.css b/src/styles.css index dea7616..a406bb0 100644 --- a/src/styles.css +++ b/src/styles.css @@ -106,89 +106,6 @@ cursor: not-allowed; } -@media (forced-colors: active) { - .cwl-editor { - color: CanvasText; - background: Canvas; - border-color: CanvasText; - } - - .cwl-toolbar, - .cwl-collaboration-status { - background: ButtonFace; - color: ButtonText; - border-bottom-color: CanvasText; - } - - .cwl-tb-group { - border-right-color: CanvasText; - } - - .cwl-tb-btn { - background: ButtonFace; - color: ButtonText; - border-color: ButtonText; - } - - .cwl-tb-btn:hover:not(:disabled) { - background: Highlight; - color: HighlightText; - } - - .cwl-tb-btn:focus-visible { - outline: 2px solid Highlight; - outline-offset: 2px; - } - - .cwl-tb-btn.is-active { - background: Highlight; - border-color: Highlight; - color: HighlightText; - } - - .cwl-tb-btn:disabled { - opacity: 1; - color: GrayText; - border-color: GrayText; - } - - .cwl-editor__content a { - color: LinkText; - } - - .cwl-editor__content blockquote { - border-left-color: CanvasText; - color: GrayText; - } - - .cwl-editor__content .is-editor-empty:first-child::before { - color: GrayText; - } - - .cwl-editor__content code, - .cwl-editor__content pre, - .cwl-editor__content th, - .cwl-editor__content td { - border-color: CanvasText; - } - - .cwl-editor__content code, - .cwl-editor__content pre, - .cwl-editor__content th { - background: Canvas; - color: CanvasText; - } - - .collaboration-cursor__caret { - border-left-color: Highlight; - } - - .collaboration-cursor__label { - background: Highlight; - color: HighlightText; - } -} - .cwl-editor__surface { overflow-y: auto; max-height: 70vh; @@ -211,13 +128,6 @@ outline-offset: -2px; } -@media (forced-colors: active) { - .cwl-editor__content:focus-visible { - outline: 2px solid Highlight; - outline-offset: -2px; - } -} - .cwl-editor__content > * + * { margin-top: 0.75em; } @@ -349,6 +259,94 @@ padding-top: calc(16px + 1.6em); } +@media (forced-colors: active) { + .cwl-editor { + color: CanvasText; + background: Canvas; + border-color: CanvasText; + } + + .cwl-toolbar, + .cwl-collaboration-status { + background: ButtonFace; + color: ButtonText; + border-bottom-color: CanvasText; + } + + .cwl-tb-group { + border-right-color: CanvasText; + } + + .cwl-tb-btn { + background: ButtonFace; + color: ButtonText; + border-color: ButtonText; + } + + .cwl-tb-btn:hover:not(:disabled) { + background: Highlight; + color: HighlightText; + } + + .cwl-tb-btn:focus-visible { + outline: 2px solid Highlight; + outline-offset: 2px; + } + + .cwl-tb-btn.is-active { + background: Highlight; + border-color: Highlight; + color: HighlightText; + } + + .cwl-tb-btn:disabled { + opacity: 1; + color: GrayText; + border-color: GrayText; + } + + .cwl-editor__content:focus-visible { + outline: 2px solid Highlight; + outline-offset: -2px; + } + + .cwl-editor__content a { + color: LinkText; + } + + .cwl-editor__content blockquote { + border-left-color: CanvasText; + color: GrayText; + } + + .cwl-editor__content .is-editor-empty:first-child::before { + color: GrayText; + } + + .cwl-editor__content code, + .cwl-editor__content pre, + .cwl-editor__content th, + .cwl-editor__content td { + border-color: CanvasText; + } + + .cwl-editor__content code, + .cwl-editor__content pre, + .cwl-editor__content th { + background: Canvas; + color: CanvasText; + } + + .collaboration-cursor__caret { + border-left-color: Highlight; + } + + .collaboration-cursor__label { + background: Highlight; + color: HighlightText; + } +} + @media print { .cwl-editor { --cwl-fg: #000000; From 940c2e1341ad46c66f1cdc4cfab3603fd7dfc1aa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 03:08:24 +0900 Subject: [PATCH 09/10] test(a11y): scope forced-colors order before print media --- src/forcedColorsStyles.test.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/forcedColorsStyles.test.ts b/src/forcedColorsStyles.test.ts index 3442f37..809bbf0 100644 --- a/src/forcedColorsStyles.test.ts +++ b/src/forcedColorsStyles.test.ts @@ -4,19 +4,23 @@ import { resolve } from 'node:path'; import { describe, expect, it } from 'vitest'; const styles = readFileSync(resolve(process.cwd(), 'src/styles.css'), 'utf8'); -const forcedColorsIndex = styles.indexOf('@media (forced-colors: active)'); +const printIndex = styles.indexOf('@media print'); +const screenStyles = printIndex >= 0 ? styles.slice(0, printIndex) : styles; +const forcedColorsIndex = screenStyles.indexOf('@media (forced-colors: active)'); const forcedColorsStyles = - forcedColorsIndex >= 0 ? styles.slice(forcedColorsIndex) : ''; -const forcedColorsBlocks = styles.match(/@media \(forced-colors: active\)/gu) ?? []; + forcedColorsIndex >= 0 ? screenStyles.slice(forcedColorsIndex) : ''; +const forcedColorsBlocks = + screenStyles.match(/@media \(forced-colors: active\)/gu) ?? []; const lastBaseStateIndex = Math.max( - styles.lastIndexOf('.cwl-editor__content a {'), - styles.lastIndexOf('.cwl-editor__content blockquote {'), - styles.lastIndexOf('.cwl-collaboration-status {'), - styles.lastIndexOf('.collaboration-cursor__label {'), + screenStyles.lastIndexOf('.cwl-editor__content a {'), + screenStyles.lastIndexOf('.cwl-editor__content blockquote {'), + screenStyles.lastIndexOf('.cwl-collaboration-status {'), + screenStyles.lastIndexOf('.collaboration-cursor__label {'), ); describe('forced-colors stylesheet contract', () => { - it('defines one final forced-colors override layer after the base state rules', () => { + it('defines one final screen forced-colors override layer after base state rules', () => { + expect(printIndex).toBeGreaterThan(-1); expect(forcedColorsIndex).toBeGreaterThan(-1); expect(forcedColorsBlocks).toHaveLength(1); expect(lastBaseStateIndex).toBeGreaterThan(-1); From 862db1d4ec2f910fcc65dbd3aabdfcd49877d98d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 03:11:20 +0900 Subject: [PATCH 10/10] test(a11y): distinguish media overrides from later screen rules --- src/forcedColorsStyles.test.ts | 44 +++++++++++++++++++++++++++------- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/src/forcedColorsStyles.test.ts b/src/forcedColorsStyles.test.ts index 809bbf0..8d19f08 100644 --- a/src/forcedColorsStyles.test.ts +++ b/src/forcedColorsStyles.test.ts @@ -7,24 +7,50 @@ const styles = readFileSync(resolve(process.cwd(), 'src/styles.css'), 'utf8'); const printIndex = styles.indexOf('@media print'); const screenStyles = printIndex >= 0 ? styles.slice(0, printIndex) : styles; const forcedColorsIndex = screenStyles.indexOf('@media (forced-colors: active)'); + +const findCssBlockEnd = (source: string, startIndex: number): number => { + const openingBrace = source.indexOf('{', startIndex); + if (openingBrace < 0) return -1; + + let depth = 0; + for (let index = openingBrace; index < source.length; index += 1) { + if (source[index] === '{') depth += 1; + if (source[index] !== '}') continue; + depth -= 1; + if (depth === 0) return index + 1; + } + return -1; +}; + +const forcedColorsEnd = + forcedColorsIndex >= 0 ? findCssBlockEnd(screenStyles, forcedColorsIndex) : -1; const forcedColorsStyles = - forcedColorsIndex >= 0 ? screenStyles.slice(forcedColorsIndex) : ''; + forcedColorsEnd > forcedColorsIndex + ? screenStyles.slice(forcedColorsIndex, forcedColorsEnd) + : ''; +const beforeForcedColorsStyles = + forcedColorsIndex >= 0 ? screenStyles.slice(0, forcedColorsIndex) : screenStyles; +const afterForcedColorsStyles = + forcedColorsEnd >= 0 ? screenStyles.slice(forcedColorsEnd) : screenStyles; const forcedColorsBlocks = screenStyles.match(/@media \(forced-colors: active\)/gu) ?? []; -const lastBaseStateIndex = Math.max( - screenStyles.lastIndexOf('.cwl-editor__content a {'), - screenStyles.lastIndexOf('.cwl-editor__content blockquote {'), - screenStyles.lastIndexOf('.cwl-collaboration-status {'), - screenStyles.lastIndexOf('.collaboration-cursor__label {'), -); +const baseStateSelectors = [ + '.cwl-editor__content a {', + '.cwl-editor__content blockquote {', + '.cwl-collaboration-status {', + '.collaboration-cursor__label {', +]; describe('forced-colors stylesheet contract', () => { it('defines one final screen forced-colors override layer after base state rules', () => { expect(printIndex).toBeGreaterThan(-1); expect(forcedColorsIndex).toBeGreaterThan(-1); + expect(forcedColorsEnd).toBeGreaterThan(forcedColorsIndex); expect(forcedColorsBlocks).toHaveLength(1); - expect(lastBaseStateIndex).toBeGreaterThan(-1); - expect(forcedColorsIndex).toBeGreaterThan(lastBaseStateIndex); + for (const selector of baseStateSelectors) { + expect(beforeForcedColorsStyles).toContain(selector); + expect(afterForcedColorsStyles).not.toContain(selector); + } }); it('defines a forced-colors boundary using system colors', () => {