@@ -58,6 +58,93 @@ test.describe("Accessibility Audit", () => {
5858 expect ( results . violations ) . toEqual ( [ ] ) ;
5959 } ) ;
6060
61+ test ( "dashboard card headings and metric values share an inset" , async ( { admin } ) => {
62+ await admin . goToDashboard ( ) ;
63+ await admin . waitForLoading ( ) ;
64+
65+ const metricCards = admin . page . getByTestId ( "dashboard-metric" ) ;
66+ expect ( await metricCards . count ( ) ) . toBeGreaterThanOrEqual ( 3 ) ;
67+
68+ const layout = await admin . page . locator ( "main" ) . evaluate ( ( main ) => {
69+ const textStart = ( element : Element ) => {
70+ const textNode = [ ...element . childNodes ] . find (
71+ ( node ) => node . nodeType === Node . TEXT_NODE && node . textContent ?. trim ( ) ,
72+ ) ;
73+ if ( ! textNode ) throw new Error ( "Dashboard card text is missing" ) ;
74+ const range = document . createRange ( ) ;
75+ range . selectNodeContents ( textNode ) ;
76+ const rect = range . getBoundingClientRect ( ) ;
77+ return getComputedStyle ( element ) . direction === "rtl" ? rect . right : rect . left ;
78+ } ;
79+ const cardStart = ( card : Element ) => {
80+ const rect = card . getBoundingClientRect ( ) ;
81+ return getComputedStyle ( card ) . direction === "rtl" ? rect . right : rect . left ;
82+ } ;
83+ const contentHeading = [ ...main . querySelectorAll ( "h2" ) ] . find (
84+ ( heading ) => heading . textContent ?. trim ( ) === "Content" ,
85+ ) ;
86+ const contentCard = contentHeading ?. parentElement ?. parentElement ;
87+ if ( ! contentHeading || ! contentCard ) throw new Error ( "Content card heading is missing" ) ;
88+ const headingInset = Math . abs ( textStart ( contentHeading ) - cardStart ( contentCard ) ) ;
89+
90+ const metrics = Array . from (
91+ main . querySelectorAll ( '[data-testid="dashboard-metric"]' ) ,
92+ ( card ) => {
93+ const heading = card . querySelector ( "h2" ) ;
94+ const value = card . querySelector ( '[data-testid="dashboard-metric-value"]' ) ;
95+ if ( ! heading || ! value ) throw new Error ( "Metric label or value is missing" ) ;
96+
97+ return {
98+ headingInset : Math . abs ( textStart ( heading ) - cardStart ( card ) ) ,
99+ labelValueGap : Math . abs ( textStart ( heading ) - textStart ( value ) ) ,
100+ } ;
101+ } ,
102+ ) ;
103+
104+ return { headingInset, metrics } ;
105+ } ) ;
106+
107+ for ( const metric of layout . metrics ) {
108+ expect ( metric . headingInset ) . toBeCloseTo ( layout . headingInset , 1 ) ;
109+ expect ( metric . labelValueGap ) . toBeLessThanOrEqual ( 0.5 ) ;
110+ }
111+ } ) ;
112+
113+ test ( "dashboard headings keep the font's default tracking across scripts" , async ( {
114+ admin,
115+ } ) => {
116+ await admin . goToDashboard ( ) ;
117+ await admin . waitForLoading ( ) ;
118+
119+ const trackingByLocale = await admin . page . locator ( "main" ) . evaluate ( ( main ) => {
120+ const title = main . querySelector ( "h1" ) ;
121+ const metricValues = main . querySelectorAll ( '[data-testid="dashboard-metric-value"]' ) ;
122+ if ( ! title || metricValues . length === 0 ) throw new Error ( "Dashboard typography is missing" ) ;
123+
124+ const root = document . documentElement ;
125+ const originalLang = root . lang ;
126+ const locales = [ "en" , "ja" , "zh-CN" ] ;
127+
128+ try {
129+ return locales . map ( ( locale ) => {
130+ root . lang = locale ;
131+ return {
132+ locale,
133+ title : getComputedStyle ( title ) . letterSpacing ,
134+ metrics : Array . from ( metricValues , ( value ) => getComputedStyle ( value ) . letterSpacing ) ,
135+ } ;
136+ } ) ;
137+ } finally {
138+ root . lang = originalLang ;
139+ }
140+ } ) ;
141+
142+ for ( const tracking of trackingByLocale ) {
143+ expect ( tracking . title , tracking . locale ) . toBe ( "normal" ) ;
144+ expect ( tracking . metrics , tracking . locale ) . toEqual ( tracking . metrics . map ( ( ) => "normal" ) ) ;
145+ }
146+ } ) ;
147+
61148 test ( "content list should have no WCAG 2.x AA violations" , async ( { admin } ) => {
62149 await admin . goToContent ( "posts" ) ;
63150 await admin . waitForLoading ( ) ;
@@ -125,6 +212,55 @@ test.describe("Accessibility Audit", () => {
125212 expect ( results . violations ) . toEqual ( [ ] ) ;
126213 } ) ;
127214
215+ test ( "page descriptions meet regular-text contrast in the classic light theme" , async ( {
216+ admin,
217+ page,
218+ } ) => {
219+ await page . emulateMedia ( { colorScheme : "light" } ) ;
220+ await admin . goto ( "/plugins-manager" ) ;
221+ await admin . waitForShell ( ) ;
222+ await admin . waitForLoading ( ) ;
223+
224+ const description = page . getByText (
225+ "Manage installed plugins. Enable or disable plugins to control their functionality." ,
226+ { exact : true } ,
227+ ) ;
228+ await expect ( description ) . toBeVisible ( ) ;
229+
230+ const contrast = await description . evaluate ( ( element ) => {
231+ const canvas = document . createElement ( "canvas" ) ;
232+ canvas . width = 1 ;
233+ canvas . height = 1 ;
234+ const context = canvas . getContext ( "2d" ) ;
235+ if ( ! context ) throw new Error ( "Canvas context unavailable" ) ;
236+
237+ const toRgb = ( color : string ) => {
238+ context . clearRect ( 0 , 0 , 1 , 1 ) ;
239+ context . fillStyle = color ;
240+ context . fillRect ( 0 , 0 , 1 , 1 ) ;
241+ return context . getImageData ( 0 , 0 , 1 , 1 ) . data ;
242+ } ;
243+ const luminance = ( rgb : Uint8ClampedArray ) => {
244+ const toLinear = ( channel : number ) => {
245+ const value = channel / 255 ;
246+ return value <= 0.04045 ? value / 12.92 : ( ( value + 0.055 ) / 1.055 ) ** 2.4 ;
247+ } ;
248+ const red = toLinear ( rgb [ 0 ] ?? 0 ) ;
249+ const green = toLinear ( rgb [ 1 ] ?? 0 ) ;
250+ const blue = toLinear ( rgb [ 2 ] ?? 0 ) ;
251+ return 0.2126 * red + 0.7152 * green + 0.0722 * blue ;
252+ } ;
253+
254+ const foreground = luminance ( toRgb ( getComputedStyle ( element ) . color ) ) ;
255+ const background = luminance ( toRgb ( getComputedStyle ( document . body ) . backgroundColor ) ) ;
256+ return (
257+ ( Math . max ( foreground , background ) + 0.05 ) / ( Math . min ( foreground , background ) + 0.05 )
258+ ) ;
259+ } ) ;
260+
261+ expect ( contrast ) . toBeGreaterThanOrEqual ( 4.5 ) ;
262+ } ) ;
263+
128264 test ( "content list should be keyboard navigable" , async ( { admin } ) => {
129265 await admin . goToContent ( "posts" ) ;
130266 await admin . waitForLoading ( ) ;
0 commit comments