| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314 |
- /**
- * Tests for the FilamentSwatch component (#1154).
- *
- * Covers the three independent inputs the swatch composes (rgba, extraColors,
- * effectType) and the buildFilamentBackground helper used to paint banners.
- */
- import { describe, it, expect } from 'vitest';
- import { screen } from '@testing-library/react';
- import { render } from '../utils';
- import { FilamentSwatch } from '../../components/FilamentSwatch';
- import { buildFilamentBackground } from '../../components/filamentSwatchHelpers';
- describe('FilamentSwatch', () => {
- it('renders a solid swatch when only rgba is set', () => {
- render(<FilamentSwatch rgba="ff0000ff" effectSize='table' />);
- const el = screen.getByTestId('filament-swatch');
- // Solid swatches are emitted as a 1-stop linear-gradient so the
- // checkerboard layer below is still visible through alpha.
- const bg = el.getAttribute('style') ?? '';
- expect(bg).toMatch(/linear-gradient/);
- expect(bg.toLowerCase()).toContain('#ff0000ff');
- });
- it('falls back to grey when nothing is set', () => {
- render(<FilamentSwatch effectSize='table' />);
- const el = screen.getByTestId('filament-swatch');
- expect(el.style.backgroundImage.toLowerCase()).toContain('#808080');
- });
- it('renders a linear gradient when extraColors has multiple stops', () => {
- render(<FilamentSwatch rgba="ff0000ff" extraColors="ec984c,6cd4bc,a66eb9,d87694" effectSize='table' />);
- const el = screen.getByTestId('filament-swatch');
- const bg = el.style.backgroundImage.toLowerCase();
- // Linear (not conic) for non-Multicolor subtype.
- expect(bg).toMatch(/linear-gradient/);
- expect(bg).toContain('#ec984c');
- expect(bg).toContain('#6cd4bc');
- expect(bg).toContain('#a66eb9');
- expect(bg).toContain('#d87694');
- });
- it('uses conic-gradient for Multicolor subtype', () => {
- render(
- <FilamentSwatch
- rgba="ff0000ff"
- extraColors="ec984c,6cd4bc,a66eb9"
- subtype="Multicolor"
- effectSize='table'
- />,
- );
- const el = screen.getByTestId('filament-swatch');
- expect(el.style.backgroundImage.toLowerCase()).toMatch(/conic-gradient/);
- });
- it('also uses conic-gradient when effectType is multicolor (catalog path)', () => {
- // Catalog entries don't have a `subtype`, so the multicolor effect_type
- // value also has to trigger conic rendering for parity with the spool path.
- render(<FilamentSwatch extraColors="ec984c,6cd4bc,a66eb9" effectType="multicolor" effectSize='table' />);
- const el = screen.getByTestId('filament-swatch');
- expect(el.style.backgroundImage.toLowerCase()).toMatch(/conic-gradient/);
- });
- it('layers an effect overlay on top of the colour layer for sparkle', () => {
- render(<FilamentSwatch rgba="ff0000ff" effectType="sparkle" effectSize='table' />);
- const el = screen.getByTestId('filament-swatch');
- // Sparkle overlay is built from radial-gradient layers — confirm at least
- // one is in the composed background, ahead of the colour layer.
- expect(el.style.backgroundImage).toMatch(/radial-gradient/);
- });
- it('renders an overlay for silk variant', () => {
- // Silk gets a soft sheen overlay (added in #1154 follow-up).
- render(<FilamentSwatch rgba="ff0000ff" effectType="silk" effectSize='table' />);
- const el = screen.getByTestId('filament-swatch');
- expect(el.style.backgroundImage).toMatch(/linear-gradient/);
- });
- it('treats categorical-only variants (gradient/dual-color) as labels without an overlay', () => {
- // No extra_colors set → swatch falls back to the solid colour layer; the
- // categorical effect value alone does not paint a sheen overlay.
- render(<FilamentSwatch rgba="ff0000ff" effectType="gradient" effectSize='table' />);
- const el = screen.getByTestId('filament-swatch');
- // No radial-gradient (sparkle/glow) and no rainbow/sheen overlay either —
- // gradient/dual-color/tri-color are pure labels until extra_colors is set.
- expect(el.style.backgroundImage).not.toMatch(/radial-gradient/);
- });
- it('ignores unknown effect types instead of throwing', () => {
- render(<FilamentSwatch rgba="ff0000ff" effectType="not-a-real-variant" effectSize='table' />);
- const el = screen.getByTestId('filament-swatch');
- expect(el.style.backgroundImage).not.toMatch(/radial-gradient/);
- });
- it('renders a checkerboard underneath so alpha is visible', () => {
- render(<FilamentSwatch rgba="ff000080" effectSize='table' />);
- const el = screen.getByTestId('filament-swatch');
- // The component always appends a checkerboard layer last so semi-
- // transparent rgba values actually look transparent to the user.
- expect(el.style.backgroundImage).toMatch(/repeating-conic-gradient/);
- });
- it('skips invalid hex tokens in extraColors instead of throwing', () => {
- render(<FilamentSwatch extraColors="ff0000,not-hex,00ff00" effectSize='table' />);
- const el = screen.getByTestId('filament-swatch');
- const bg = el.style.backgroundImage.toLowerCase();
- // The two valid stops survive; the garbage token is dropped.
- expect(bg).toContain('#ff0000');
- expect(bg).toContain('#00ff00');
- expect(bg).not.toContain('not-hex');
- });
- it('uses extra_colors for the title fallback when provided', () => {
- render(<FilamentSwatch extraColors="ff0000,00ff00" effectSize='table' />);
- const el = screen.getByTestId('filament-swatch');
- // Tooltip should show the comma-joined hex stops, not the (unset) rgba.
- expect(el.title.toLowerCase()).toContain('#ff0000');
- expect(el.title.toLowerCase()).toContain('#00ff00');
- });
- });
- describe('dual-color / tri-color hard-split bars (#1154 follow-up)', () => {
- // Bug: the original #1154 fix produced an identical
- // ``linear-gradient(135deg, A, B)`` for both Gradient and Dual Color
- // effects, so a "Dual Color" spool looked indistinguishable from a
- // "Gradient" one — both rendered as a smooth diagonal blend. Real
- // dual-colour spools have two visually distinct bars, not a blend.
- // These tests pin the corrected rendering: a horizontal hard split
- // for dual-color / tri-color, the original 135° smooth blend for
- // everything else.
- it('renders dual-color as a hard horizontal split, not a diagonal blend', () => {
- const bg = buildFilamentBackground({
- extraColors: '7f3696,006ec9',
- effectType: 'dual-color',
- effectSize: 'table',
- });
- const lower = bg.backgroundImage.toLowerCase();
- // Hard split direction — ``to right`` (or ``90deg``), never ``135deg``.
- expect(lower).toContain('to right');
- expect(lower).not.toContain('135deg');
- // Both colour stops present.
- expect(lower).toContain('#7f3696');
- expect(lower).toContain('#006ec9');
- // Each colour occupies its own segment via double-position stops, so
- // the colour change is a hard line rather than a blend region.
- expect(lower).toMatch(/#7f3696\s+0\.000%\s+50\.000%/);
- expect(lower).toMatch(/#006ec9\s+50\.000%\s+100\.000%/);
- });
- it('renders tri-color as three equal hard-split bars', () => {
- const bg = buildFilamentBackground({
- extraColors: 'ff0000,00ff00,0000ff',
- effectType: 'tri-color',
- effectSize: 'table',
- });
- const lower = bg.backgroundImage.toLowerCase();
- expect(lower).toContain('to right');
- // Each third gets its own contiguous segment.
- expect(lower).toMatch(/#ff0000\s+0\.000%\s+33\.333%/);
- expect(lower).toMatch(/#00ff00\s+33\.333%\s+66\.667%/);
- expect(lower).toMatch(/#0000ff\s+66\.667%\s+100\.000%/);
- });
- it('keeps the smooth 135° diagonal for the default Gradient effect', () => {
- const bg = buildFilamentBackground({
- extraColors: '7f3696,006ec9',
- effectType: 'gradient',
- effectSize: 'table',
- });
- const lower = bg.backgroundImage.toLowerCase();
- // Original visual preserved for non-dual / non-tri stops.
- expect(lower).toContain('135deg');
- expect(lower).not.toContain('to right');
- // Stops are concatenated without explicit positions — CSS does the
- // smooth blend across the diagonal.
- expect(lower).toContain('#7f3696');
- expect(lower).toContain('#006ec9');
- });
- it('regression: dual-color and gradient produce visually distinct backgrounds', () => {
- // Direct regression guard for the reporter's exact symptom — the two
- // effects must NOT collapse to the same CSS string. If a future refactor
- // accidentally drops the dual-color branch, this assertion fires before
- // anyone has to retest in a browser.
- const dual = buildFilamentBackground({
- extraColors: '7f3696,006ec9',
- effectType: 'dual-color',
- effectSize: 'table',
- });
- const grad = buildFilamentBackground({
- extraColors: '7f3696,006ec9',
- effectType: 'gradient',
- effectSize: 'table',
- });
- expect(dual.backgroundImage).not.toBe(grad.backgroundImage);
- });
- });
- describe('Sparkle prominence + checkerboard density (#1154 follow-up cosmetic)', () => {
- it('renders dense sparkle on card preset (at least 10 dots)', () => {
- // The original Sparkle pattern was 4 dots — too subtle on a 200×60px
- // banner. Now we use situation-aware dot counts: more dots for larger presets.
- // Verify the card preset produces a dense pattern with at least 10 dots.
- render(<FilamentSwatch rgba="ff0000ff" effectType="sparkle" effectSize="card" />);
- const el = screen.getByTestId('filament-swatch');
- const radialCount = (el.style.backgroundImage.match(/radial-gradient/g) ?? []).length;
- expect(radialCount).toBeGreaterThanOrEqual(10);
- });
- it('uses fixed-pixel checkerboard tile so cell density is independent of swatch size', () => {
- // Without per-layer background-size, ``cover`` stretched the conic
- // gradient over the whole element and a card-sized banner only showed
- // 4 huge cells. Verify the checker layer carries an explicit pixel
- // tile size.
- const bg = buildFilamentBackground({ rgba: 'ff0000ff', effectSize: 'table' });
- const sizes = bg.backgroundSize.split(',').map((s) => s.trim());
- // Last layer is the checker; should be a fixed pixel tile, not 'cover'.
- expect(sizes[sizes.length - 1]).toMatch(/^\d+px(\s+\d+px)?$/);
- expect(sizes[sizes.length - 1]).not.toContain('cover');
- });
- it('limits sparkle dot count per size preset (table/card/bar)', () => {
- const tableBg = buildFilamentBackground({
- rgba: 'ff0000ff',
- effectType: 'sparkle',
- effectSize: 'table',
- });
- const cardBg = buildFilamentBackground({
- rgba: 'ff0000ff',
- effectType: 'sparkle',
- effectSize: 'card',
- });
- const barBg = buildFilamentBackground({
- rgba: 'ff0000ff',
- effectType: 'sparkle',
- effectSize: 'bar',
- });
- const countRadial = (css: string) => (css.match(/radial-gradient/g) ?? []).length;
- expect(countRadial(tableBg.backgroundImage)).toBe(5);
- expect(countRadial(cardBg.backgroundImage)).toBe(40);
- expect(countRadial(barBg.backgroundImage)).toBe(20);
- });
- it('scales sparkle dot radii by size preset while keeping seeded output deterministic', () => {
- const tableBg = buildFilamentBackground({
- rgba: 'ff0000ff',
- effectType: 'sparkle',
- effectSize: 'table',
- });
- const barBg = buildFilamentBackground({
- rgba: 'ff0000ff',
- effectType: 'sparkle',
- effectSize: 'bar',
- });
- const tableBgRepeat = buildFilamentBackground({
- rgba: 'ff0000ff',
- effectType: 'sparkle',
- effectSize: 'table',
- });
- const tableBgOther = buildFilamentBackground({
- rgba: '00ff00ff',
- effectType: 'sparkle',
- effectSize: 'table',
- });
- // Same seed must produce byte-identical overlay output.
- expect(tableBg.backgroundImage).toBe(tableBgRepeat.backgroundImage);
- // Different seeds must produce different overlay output.
- expect(tableBg.backgroundImage).not.toBe(tableBgOther.backgroundImage);
- // Radius grows for bar preset, preventing sparse-looking large banners.
- // Extract the first radius from each CSS string by looking for "0 Xpx, transparent Ypx"
- const tableR = tableBg.backgroundImage.match(/0[ ]+(\d+\.?\d*)px[,][ ]*transparent[ ]+(\d+\.?\d*)px/);
- const barR = barBg.backgroundImage.match(/0[ ]+(\d+\.?\d*)px[,][ ]*transparent[ ]+(\d+\.?\d*)px/);
- if (!tableR || !barR) {
- throw new Error(`Failed to extract radii: tableR=${tableR}, barR=${barR}`);
- }
- expect(Number(tableR[1])).toBeLessThan(Number(barR[1]));
- expect(Number(tableR[2])).toBeLessThan(Number(barR[2]));
- });
- });
- describe('buildFilamentBackground', () => {
- it('emits a CSS-style object with layered images and per-layer sizes', () => {
- const bg = buildFilamentBackground({
- rgba: 'ff0000ff',
- extraColors: 'aabbcc,ddeeff',
- effectType: 'matte',
- effectSize: 'table',
- });
- // Effect overlay → colour layer → checkerboard, in that order.
- expect(bg.backgroundImage).toMatch(/linear-gradient/);
- expect(bg.backgroundImage).toMatch(/repeating-conic-gradient/);
- expect(bg.backgroundImage.toLowerCase()).toContain('#aabbcc');
- expect(bg.backgroundImage.toLowerCase()).toContain('#ddeeff');
- // Per-layer sizes — three comma-separated values (effect/colour/checker)
- // in the same order. The checker has a fixed pixel tile so the cell
- // density doesn't scale with the element (#1154 follow-up).
- const sizeParts = bg.backgroundSize.split(',').map((s) => s.trim());
- expect(sizeParts).toHaveLength(3);
- expect(sizeParts[2]).toMatch(/\d+px/);
- });
- it('returns a usable solid background when only rgba is provided', () => {
- const bg = buildFilamentBackground({ rgba: '00ff00ff', effectSize: 'table' });
- expect(bg.backgroundImage.toLowerCase()).toContain('#00ff00ff');
- expect(bg.backgroundImage).toMatch(/repeating-conic-gradient/);
- });
- });
|