import { render } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { IsotypeDiagonal } from './IsotypeDiagonal';
import { ISOTYPE_COLORS } from './patterns';
import React from 'react';
type Point = [number, number];
function shapeWithFill(container: HTMLElement, fill: string) {
return container.querySelector(`[fill="${fill}"]`)!;
}
function rectCorners(rect: Element): Point[] {
const x = Number(rect.getAttribute('x'));
const y = Number(rect.getAttribute('y'));
const width = Number(rect.getAttribute('width'));
const height = Number(rect.getAttribute('height'));
return [
[x, y],
[x + width, y],
[x + width, y + height],
[x, y + height],
];
}
function polygonPoints(polygon: Element): Point[] {
return polygon
.getAttribute('points')!
.trim()
.split(/\s+/)
.map(pair => pair.split(',').map(Number) as Point);
}
function shapePoints(shape: Element): Point[] {
return shape.tagName === 'rect' ? rectCorners(shape) : polygonPoints(shape);
}
function shapeEdges(shape: Element): [Point, Point][] {
const points = shapePoints(shape);
return points.map((point, i) => [point, points[(i + 1) % points.length]]);
}
/** True if `shape` has an edge between `a` and `b`, in either direction. */
function hasEdge(shape: Element, a: Point, b: Point): boolean {
return shapeEdges(shape).some(
([p, q]) =>
(p[0] === a[0] && p[1] === a[1] && q[0] === b[0] && q[1] === b[1]) ||
(p[0] === b[0] && p[1] === b[1] && q[0] === a[0] && q[1] === a[1])
);
}
describe('IsotypeDiagonal', () => {
it('renders 4 rects (blue, red, purple, brown) and 4 polygons (green, magenta, yellow, blank)', () => {
const { container } = render();
expect(container.querySelectorAll('rect')).toHaveLength(4);
expect(container.querySelectorAll('polygon')).toHaveLength(4);
});
it('uses every ISOTYPE_COLORS value exactly once as a fill', () => {
const { container } = render();
Object.values(ISOTYPE_COLORS).forEach(color => {
expect(container.querySelectorAll(`[fill="${color}"]`)).toHaveLength(1);
});
});
it('renders exactly one shape with fill="none"', () => {
const { container } = render();
expect(container.querySelectorAll('[fill="none"]')).toHaveLength(1);
});
it('renders no shape with a stroke', () => {
const { container } = render();
container.querySelectorAll('rect, polygon').forEach(shape => {
expect(shape.getAttribute('stroke')).toBeNull();
});
});
it('passes className through to the root', () => {
const { container } = render();
expect((container.firstChild as HTMLElement).className).toContain('w-64');
});
it('is decorative by default (aria-hidden, no role)', () => {
const { container } = render();
const root = container.firstChild as HTMLElement;
expect(root.getAttribute('aria-hidden')).toBe('true');
expect(root.getAttribute('role')).toBeNull();
});
it('renders as an image with a label when aria-label is set', () => {
const { container } = render();
const root = container.firstChild as HTMLElement;
expect(root.getAttribute('role')).toBe('img');
expect(root.getAttribute('aria-label')).toBe('Xertica isotype');
expect(root.getAttribute('aria-hidden')).toBeNull();
});
it('uses a 0 0 100 100 viewBox', () => {
const { container } = render();
expect(container.querySelector('svg')!.getAttribute('viewBox')).toBe('0 0 100 100');
});
it('backs blue and red with full-width rectangles, wider than the triangular area they end up showing', () => {
const { container } = render();
const blue = shapeWithFill(container, ISOTYPE_COLORS.blue);
const red = shapeWithFill(container, ISOTYPE_COLORS.red);
expect(blue.tagName).toBe('rect');
expect(red.tagName).toBe('rect');
expect(rectCorners(blue)).toEqual([
[0, 0],
[100, 0],
[100, 25],
[0, 25],
]);
expect(rectCorners(red)).toEqual([
[0, 75],
[100, 75],
[100, 100],
[0, 100],
]);
});
it('paints purple/green after blue, and magenta/brown after red, so they overlap their backing rectangle instead of abutting it', () => {
const { container } = render();
const shapes = Array.from(container.querySelectorAll('rect, polygon'));
const indexOf = (color: string) => shapes.indexOf(shapeWithFill(container, color));
expect(indexOf(ISOTYPE_COLORS.blue)).toBeLessThan(indexOf(ISOTYPE_COLORS.purple));
expect(indexOf(ISOTYPE_COLORS.blue)).toBeLessThan(indexOf(ISOTYPE_COLORS.green));
expect(indexOf(ISOTYPE_COLORS.red)).toBeLessThan(indexOf(ISOTYPE_COLORS.magenta));
expect(indexOf(ISOTYPE_COLORS.red)).toBeLessThan(indexOf(ISOTYPE_COLORS.brown));
});
it('keeps purple and brown fully within the blue/red rectangles they sit on top of', () => {
const { container } = render();
const purple = shapeWithFill(container, ISOTYPE_COLORS.purple);
const brown = shapeWithFill(container, ISOTYPE_COLORS.brown);
expect(Number(purple.getAttribute('y'))).toBeGreaterThanOrEqual(0);
expect(Number(purple.getAttribute('y')) + Number(purple.getAttribute('height'))).toBeLessThanOrEqual(25);
expect(Number(brown.getAttribute('y'))).toBeGreaterThanOrEqual(75);
expect(Number(brown.getAttribute('y')) + Number(brown.getAttribute('height'))).toBeLessThanOrEqual(100);
});
it('keeps the untouched seams — purple/magenta, magenta/blank, yellow/green, and brown/green — matching exactly', () => {
const { container } = render();
const purple = shapeWithFill(container, ISOTYPE_COLORS.purple);
const magenta = shapeWithFill(container, ISOTYPE_COLORS.magenta);
const blank = shapeWithFill(container, 'none');
const yellow = shapeWithFill(container, ISOTYPE_COLORS.yellow);
const green = shapeWithFill(container, ISOTYPE_COLORS.green);
const brown = shapeWithFill(container, ISOTYPE_COLORS.brown);
expect(hasEdge(purple, [0, 25], [25, 25])).toBe(true);
expect(hasEdge(magenta, [0, 25], [25, 25])).toBe(true);
expect(hasEdge(magenta, [25, 25], [25, 75])).toBe(true);
expect(hasEdge(blank, [25, 25], [25, 75])).toBe(true);
expect(hasEdge(yellow, [75, 25], [75, 75])).toBe(true);
expect(hasEdge(green, [75, 25], [75, 75])).toBe(true);
expect(hasEdge(brown, [75, 75], [100, 75])).toBe(true);
expect(hasEdge(green, [75, 75], [100, 75])).toBe(true);
});
it('traces a single continuous diagonal through the blue/green tip, the magenta/red tip, and the yellow hypotenuse', () => {
const diagonalVertices: Point[] = [
[100, 0],
[0, 100],
[75, 25],
[25, 75],
];
diagonalVertices.forEach(([x, y]) => {
expect(x + y).toBe(100);
});
});
});