const assert = require("node:assert/strict"); const fs = require("node:fs"); const path = require("node:path"); const { test } = require("node:test"); function luminance(hex) { const rgb = hex.slice(1).match(/../g).map(value => parseInt(value, 16) / 255) .map(value => value <= .04045 ? value / 12.92 : ((value + .055) / 1.055) ** 2.4); return rgb[0] * .2126 + rgb[1] * .7152 + rgb[2] * .0722; } function contrast(a, b) { const values = [luminance(a), luminance(b)].sort((x, y) => y - x); return (values[0] + .05) / (values[1] + .05); } for (const feature of ["install-base", "service-requests"]) { test(`${feature}: explicit dark text colors cover cells, headings, counts and errors with readable contrast`, () => { const css = fs.readFileSync(path.join(__dirname, `../src/features/${feature}/styles.css`), "utf8"); const rules = [...css.matchAll(/([^{}]+)\{([^{}]+)\}/g)]; const prefix = `.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-${feature}`; function color(selector) { const matches = rules.filter(([, selectors, body]) => selectors.split(",").map(value => value.trim()).includes(selector) && /(?:^|;)\s*color\s*:/.test(body)); assert.ok(matches.length, `Missing explicit color for ${selector}`); return matches.at(-1)[2].match(/(?:^|;)\s*color\s*:\s*(#[a-f0-9]{6})/i)[1]; } for (const suffix of ["-content", "-table", "-table th", "-table td", "-message", '-message[role="alert"]']) { const foreground = color(prefix + suffix); const background = suffix.endsWith(" th") ? "#353331" : "#292827"; assert.ok(contrast(foreground, background) >= 4.5, `${suffix} needs at least 4.5:1 contrast`); } assert.match(css, /color: #17693a; background: #e1f1e5/, "light status colors remain unchanged"); assert.match(css, /color: #b8e4c6; background: #214d34/, "dark status colors remain unchanged"); }); }