|
| 1 | +// @ts-expect-error Incorrect khroma types |
| 2 | +import { luminance } from 'khroma'; |
| 3 | +import { describe, it, expect } from 'vitest'; |
| 4 | +import getStyles from './styles.js'; |
| 5 | + |
| 6 | +/** Comments sit above the rules they describe, so they would read as selector text. */ |
| 7 | +const withoutComments = (css: string): string => |
| 8 | + css |
| 9 | + .split('/*') |
| 10 | + .map((chunk, index) => (index === 0 ? chunk : chunk.slice(chunk.indexOf('*/') + 2))) |
| 11 | + .join(''); |
| 12 | + |
| 13 | +/** The generated stylesheet as selector/declaration pairs. */ |
| 14 | +const rulesOf = (css: string): { selector: string; declarations: string }[] => |
| 15 | + withoutComments(css) |
| 16 | + .split('}') |
| 17 | + .map((block) => block.split('{')) |
| 18 | + .filter((parts) => parts.length === 2) |
| 19 | + .map(([selector, declarations]) => ({ selector: selector.trim(), declarations })); |
| 20 | + |
| 21 | +/** |
| 22 | + * The value a rule gives one property. `matches` takes the whole selector text, so a |
| 23 | + * rule for the element group can be told apart from the rule for its parts. |
| 24 | + */ |
| 25 | +const valueFor = ( |
| 26 | + css: string, |
| 27 | + matches: (selector: string) => boolean, |
| 28 | + property: string |
| 29 | +): string => { |
| 30 | + const rule = rulesOf(css).find(({ selector }) => matches(selector)); |
| 31 | + if (!rule) { |
| 32 | + throw new Error(`no matching rule in\n${css}`); |
| 33 | + } |
| 34 | + const declaration = rule.declarations |
| 35 | + .split(';') |
| 36 | + .map((one) => one.trim()) |
| 37 | + .find((one) => one.startsWith(`${property}:`)); |
| 38 | + if (!declaration) { |
| 39 | + throw new Error(`no "${property}" on "${rule.selector}"`); |
| 40 | + } |
| 41 | + return declaration.slice(property.length + 1).trim(); |
| 42 | +}; |
| 43 | + |
| 44 | +const parts = (selectorStart: string) => (selector: string) => selector.startsWith(selectorStart); |
| 45 | +const group = (selectorText: string) => (selector: string) => selector === selectorText; |
| 46 | + |
| 47 | +describe('c4 styles', () => { |
| 48 | + it('puts the element body on the theme surface', () => { |
| 49 | + expect(valueFor(getStyles({ background: '#ffffff' }), parts('.c4-shape rect'), 'fill')).toBe( |
| 50 | + '#ffffff' |
| 51 | + ); |
| 52 | + }); |
| 53 | + |
| 54 | + it('derives each element type its own identity colour from the palette', () => { |
| 55 | + const css = getStyles({ background: '#ffffff' }); |
| 56 | + |
| 57 | + // person (#08427B) and container (#438DD5) are different palette entries, so the |
| 58 | + // rules must not collapse to a single colour. |
| 59 | + expect(valueFor(css, parts('.c4-shape.c4-person rect'), 'stroke')).not.toBe( |
| 60 | + valueFor(css, parts('.c4-shape.c4-container rect'), 'stroke') |
| 61 | + ); |
| 62 | + }); |
| 63 | + |
| 64 | + // The regression the outline look risks: deriving the identity colour without regard to |
| 65 | + // the surface puts a dark border and dark label text on a dark body. |
| 66 | + it('lightens the identity colour on a dark surface instead of darkening it', () => { |
| 67 | + const person = parts('.c4-shape.c4-person rect'); |
| 68 | + const onLight = valueFor(getStyles({ background: '#ffffff' }), person, 'stroke'); |
| 69 | + const onDark = valueFor(getStyles({ background: '#333333' }), person, 'stroke'); |
| 70 | + |
| 71 | + expect(luminance(onDark)).toBeGreaterThan(luminance(onLight)); |
| 72 | + }); |
| 73 | + |
| 74 | + it('carries the identity colour on the group so the label inherits it', () => { |
| 75 | + const css = getStyles({ background: '#ffffff' }); |
| 76 | + |
| 77 | + expect(valueFor(css, group('.c4-shape.c4-person'), 'color')).toBe( |
| 78 | + valueFor(css, parts('.c4-shape.c4-person rect'), 'stroke') |
| 79 | + ); |
| 80 | + expect(valueFor(css, parts('.c4-shape .label'), 'fill')).toBe('currentColor'); |
| 81 | + }); |
| 82 | +}); |
0 commit comments