import { expect } from 'storybook/test' export function resolveTokenColor(token: string, context?: Element): string { const probe = document.createElement('div') probe.style.color = `var(${token})` const parent = context?.parentElement ?? document.body parent.appendChild(probe) const resolved = window.getComputedStyle(probe).color parent.removeChild(probe) return resolved } export function colorToRgb(colorStr: string): [number, number, number] { const canvas = document.createElement('canvas') canvas.width = 1 canvas.height = 1 const ctx = canvas.getContext('2d')! ctx.fillStyle = colorStr ctx.fillRect(0, 0, 1, 1) const [r, g, b] = ctx.getImageData(0, 0, 1, 1).data return [r, g, b] } export async function assertColorMatch( actual: string, expected: string, label: string, ) { const [ar, ag, ab] = colorToRgb(actual) const [er, eg, eb] = colorToRgb(expected) await expect( ar, `${label} R (actual: ${actual}, expected: ${expected})`, ).toBe(er) await expect(ag, `${label} G`).toBe(eg) await expect(ab, `${label} B`).toBe(eb) } function queryWithFallback( canvasElement: HTMLElement, selector: string, ): NodeListOf { const els = canvasElement.querySelectorAll(selector) if (els.length > 0) return els return canvasElement.ownerDocument.querySelectorAll(selector) } async function assertAllMatchingColor( canvasElement: HTMLElement, selector: string, token: string, label: string, ) { const els = canvasElement.querySelectorAll(selector) for (let i = 0; i < els.length; i++) { const el = els[i] const style = window.getComputedStyle(el) const suffix = els.length > 1 ? ` (${i + 1})` : '' await assertColorMatch( style.color, resolveTokenColor(token, el), `${label}${suffix}`, ) } } export type StateTokens = { border: string bg: string text?: string placeholder?: string caret?: string badgeText?: string badgeRemoveColor?: string dropdown?: DropdownContentTokens } export async function assertElementTokens( el: Element, tokens: StateTokens, label: string, ) { const style = window.getComputedStyle(el) const resolve = (token: string) => resolveTokenColor(token, el) await assertColorMatch( style.borderTopColor, resolve(tokens.border), `${label} border`, ) await assertColorMatch( style.backgroundColor, resolve(tokens.bg), `${label} background`, ) const borderWidth = parseFloat(style.borderTopWidth) await expect(borderWidth, `${label} border-width`).toBeGreaterThan(0) await expect(Math.round(borderWidth), `${label} border-width rounded`).toBe(1) const tag = el.tagName.toLowerCase() const isTextInput = tag === 'input' || tag === 'textarea' if (tokens.text && isTextInput) { await assertColorMatch(style.color, resolve(tokens.text), `${label} text`) } if (tokens.placeholder && isTextInput) { const hasValue = (el as HTMLInputElement | HTMLTextAreaElement).value !== '' if (!hasValue) { const placeholderStyle = window.getComputedStyle(el, '::placeholder') await assertColorMatch( placeholderStyle.color, resolve(tokens.placeholder), `${label} placeholder`, ) } } if (tokens.caret && isTextInput) { await assertColorMatch( style.caretColor, resolve(tokens.caret), `${label} caret`, ) } } const INPUT_SELECTORS = [ { selector: '[data-slot="input"]', label: 'Input' }, { selector: '[data-slot="textarea"]', label: 'Textarea' }, { selector: '[data-slot="select-trigger"]', label: 'Select' }, { selector: '[data-slot="combobox-trigger"]', label: 'Combobox', }, ] as const export async function assertAllInputTokens( canvasElement: HTMLElement, tokens: StateTokens, ) { for (const { selector, label } of INPUT_SELECTORS) { const els = canvasElement.querySelectorAll(selector) for (let i = 0; i < els.length; i++) { const suffix = els.length > 1 ? ` (${i + 1})` : '' await assertElementTokens(els[i], tokens, `${label}${suffix}`) } } if (tokens.badgeText) { await assertAllMatchingColor( canvasElement, '[data-slot="combobox-badge"]', tokens.badgeText, 'Combobox badge text', ) } if (tokens.badgeRemoveColor) { await assertAllMatchingColor( canvasElement, '[data-slot="combobox-badge-remove"]', tokens.badgeRemoveColor, 'Combobox badge remove icon', ) } } export type DropdownContentTokens = { checkmarkColor: string unselectedText: string searchPlaceholder: string searchIconColor: string } const ITEM_SELECTORS = [ { item: '[data-slot="select-item"]', selected: '[data-state="checked"]', label: 'Select', }, { item: '[data-slot="combobox-item"]', selected: '[data-selected]', label: 'Combobox', }, ] as const export async function assertDropdownContentTokens( canvasElement: HTMLElement, tokens: DropdownContentTokens, ) { for (const { item, selected, label } of ITEM_SELECTORS) { const selectedItems = queryWithFallback(canvasElement, `${item}${selected}`) for (let i = 0; i < selectedItems.length; i++) { const el = selectedItems[i] const resolve = (t: string) => resolveTokenColor(t, el) const suffix = selectedItems.length > 1 ? ` (${i + 1})` : '' const checkSvg = el.querySelector('span svg') if (checkSvg) { const isMulti = label === 'Combobox' && checkSvg.classList.contains('size-2') if (!isMulti) { const checkStyle = window.getComputedStyle(checkSvg) await assertColorMatch( checkStyle.color, resolve(tokens.checkmarkColor), `${label} selected item${suffix} checkmark`, ) } } } const unselectedItems = queryWithFallback( canvasElement, `${item}:not(${selected})`, ) for (let i = 0; i < unselectedItems.length; i++) { const el = unselectedItems[i] const style = window.getComputedStyle(el) const resolve = (t: string) => resolveTokenColor(t, el) const suffix = unselectedItems.length > 1 ? ` (${i + 1})` : '' await assertColorMatch( style.color, resolve(tokens.unselectedText), `${label} unselected item${suffix} text`, ) } } const searchInputs = queryWithFallback( canvasElement, '[data-slot="combobox-input"]', ) for (let i = 0; i < searchInputs.length; i++) { const input = searchInputs[i] as HTMLInputElement const resolve = (t: string) => resolveTokenColor(t, input) const suffix = searchInputs.length > 1 ? ` (${i + 1})` : '' if (input.value === '') { const placeholderStyle = window.getComputedStyle(input, '::placeholder') await assertColorMatch( placeholderStyle.color, resolve(tokens.searchPlaceholder), `Combobox search input${suffix} placeholder`, ) } const searchIcon = input.parentElement?.querySelector('svg') if (searchIcon) { const iconStyle = window.getComputedStyle(searchIcon) await assertColorMatch( iconStyle.color, resolve(tokens.searchIconColor), `Combobox search${suffix} icon`, ) } } } export const selectOptions = [ { label: 'Option 1', value: 'option1' }, { label: 'Option 2', value: 'option2' }, { label: 'Option 3', value: 'option3' }, ] export const manyOptions = [ { label: 'Ethereum', value: 'ethereum' }, { label: 'Arbitrum', value: 'arbitrum' }, { label: 'Base', value: 'base' }, { label: 'Polygon', value: 'polygon' }, { label: 'Avalanche', value: 'avalanche' }, { label: 'Optimism', value: 'optimism' }, { label: 'Solana', value: 'solana' }, { label: 'BNB Chain', value: 'bnb' }, { label: 'Fantom', value: 'fantom' }, { label: 'Gnosis', value: 'gnosis' }, { label: 'Celo', value: 'celo' }, { label: 'Moonbeam', value: 'moonbeam' }, ] export const comboboxOptions = manyOptions.slice(0, 3) export const multiSelectOptions = [ { label: 'React', value: 'react' }, { label: 'Vue', value: 'vue' }, { label: 'Angular', value: 'angular' }, { label: 'Svelte', value: 'svelte' }, { label: 'Next.js', value: 'nextjs' }, { label: 'Nuxt', value: 'nuxt' }, { label: 'Remix', value: 'remix' }, { label: 'Astro', value: 'astro' }, { label: 'SolidJS', value: 'solidjs' }, { label: 'Qwik', value: 'qwik' }, ]