Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"main": "src/server.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"test": "vitest",
"test:coverage": "vitest run --coverage",
"start": "node .",
"dev": "nodemon . --ignore 'src/assets/js/*.js' --ignore 'cypress/**/*.js' --ignore 'test/**/*.js'",
"css": "npx tailwindcss -i ./src/tailwind.css -o ./src/assets/css/index.css --watch",
Expand Down Expand Up @@ -57,5 +58,15 @@
"supertest": "^6.3.3",
"tailwindcss": "^3.1.8",
"vitest": "^3.0.7"
},
"vitest": {
"include": [
"src/assets/js/__tests__/**/*.test.js"
],
"environment": "node",
"globals": true,
"transformMode": {
"web": [".js", ".jsx"]
}
}
}
}
67 changes: 67 additions & 0 deletions src/assets/js/__tests__/colorContrastValidator.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import ColorContrastValidator from '../colorContrastValidator.js';

describe('ColorContrastValidator', () => {
describe('hexToRgb', () => {
test('converts 6-digit hex to RGB', () => {
const rgb = ColorContrastValidator.hexToRgb('#FF0000');
expect(rgb).toEqual({ r: 255, g: 0, b: 0 });
});

test('converts 3-digit hex to RGB', () => {
const rgb = ColorContrastValidator.hexToRgb('#F00');
expect(rgb).toEqual({ r: 255, g: 0, b: 0 });
});
});

describe('calculateRelativeLuminance', () => {
test('calculates luminance correctly', () => {
const white = ColorContrastValidator.calculateRelativeLuminance({ r: 255, g: 255, b: 255 });
const black = ColorContrastValidator.calculateRelativeLuminance({ r: 0, g: 0, b: 0 });

expect(white).toBeCloseTo(1);
expect(black).toBeCloseTo(0);
});
});

describe('calculateContrastRatio', () => {
test('calculates contrast ratio between black and white', () => {
const contrastRatio = ColorContrastValidator.calculateContrastRatio('#FFFFFF', '#000000');
expect(contrastRatio).toBeCloseTo(21);
});

test('calculates contrast ratio between similar colors', () => {
const contrastRatio = ColorContrastValidator.calculateContrastRatio('#888888', '#A0A0A0');
expect(contrastRatio).toBeGreaterThan(1);
expect(contrastRatio).toBeLessThan(5);
});
});

describe('meetsContrastRequirements', () => {
test('validates high contrast colors', () => {
expect(ColorContrastValidator.meetsContrastRequirements('#FFFFFF', '#000000')).toBe(true);
expect(ColorContrastValidator.meetsContrastRequirements('#000000', '#FFFFFF')).toBe(true);
});

test('invalidates low contrast colors', () => {
expect(ColorContrastValidator.meetsContrastRequirements('#888888', '#A0A0A0')).toBe(false);
});
});

describe('findCompliantColor', () => {
test('finds a compliant color from a palette', () => {
const palette = ['#FF0000', '#00FF00', '#0000FF', '#FFFFFF'];
const backgroundColor = '#000000';

const compliantColor = ColorContrastValidator.findCompliantColor('#888888', backgroundColor, palette);
expect(compliantColor).toBe('#FFFFFF');
});

test('returns null if no compliant color found', () => {
const palette = ['#888888', '#999999', '#AAAAAA'];
const backgroundColor = '#000000';

const compliantColor = ColorContrastValidator.findCompliantColor('#888888', backgroundColor, palette);
expect(compliantColor).toBeNull();
});
});
});
88 changes: 88 additions & 0 deletions src/assets/js/colorContrastValidator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/**
* Color Contrast Validation Utility
* Implements WCAG 2.1 Level AA contrast ratio calculation and validation
*/
class ColorContrastValidator {
/**
* Convert a hex color to RGB
* @param {string} hex - Hex color code
* @returns {Object} RGB color object
*/
static hexToRgb(hex) {
// Remove # if present
hex = hex.replace(/^#/, '');

// Handle 3-digit and 6-digit hex codes
const bigint = parseInt(hex, 16);
const r = (bigint >> 16) & 255;
const g = (bigint >> 8) & 255;
const b = bigint & 255;

return { r, g, b };
}

/**
* Calculate relative luminance of a color
* @param {Object} rgb - RGB color object
* @returns {number} Relative luminance value
*/
static calculateRelativeLuminance(rgb) {
const { r, g, b } = rgb;
const sRGB = [r, g, b].map(color => {
const sRGBValue = color / 255;
return sRGBValue <= 0.03928
? sRGBValue / 12.92
: Math.pow((sRGBValue + 0.055) / 1.055, 2.4);
});

return 0.2126 * sRGB[0] + 0.7152 * sRGB[1] + 0.0722 * sRGB[2];
}

/**
* Calculate contrast ratio between two colors
* @param {string} color1 - First color hex code
* @param {string} color2 - Second color hex code
* @returns {number} Contrast ratio
*/
static calculateContrastRatio(color1, color2) {
const rgb1 = this.hexToRgb(color1);
const rgb2 = this.hexToRgb(color2);

const l1 = this.calculateRelativeLuminance(rgb1);
const l2 = this.calculateRelativeLuminance(rgb2);

const lighter = Math.max(l1, l2);
const darker = Math.min(l1, l2);

return (lighter + 0.05) / (darker + 0.05);
}

/**
* Check if colors meet WCAG 2.1 Level AA contrast requirements
* @param {string} foreground - Foreground color hex code
* @param {string} background - Background color hex code
* @returns {boolean} Whether colors meet contrast requirements
*/
static meetsContrastRequirements(foreground, background) {
const contrastRatio = this.calculateContrastRatio(foreground, background);
return contrastRatio >= 4.5; // WCAG 2.1 Level AA standard
}

/**
* Find a compliant alternative color
* @param {string} originalColor - Original color hex code
* @param {string} backgroundColor - Background color hex code
* @param {string[]} colorPalette - Array of potential alternative colors
* @returns {string|null} A compliant color or null
*/
static findCompliantColor(originalColor, backgroundColor, colorPalette) {
for (const color of colorPalette) {
if (this.meetsContrastRequirements(color, backgroundColor)) {
return color;
}
}
return null;
}
}

export default ColorContrastValidator;