import { describe, test, expect } from 'vitest'; import { StarRating } from './StarRating'; const toHtml = (StarRating as any).toHtml; describe('StarRating.toHtml accessibility (F2.2)', () => { test('wrapper has role="img" and a "Rating: N out of maxStars" aria-label', () => { const { html } = toHtml({ rating: 4.5, maxStars: 5 }, ''); expect(html).toMatch(/ { const { html } = toHtml({ rating: 3, maxStars: 5 }, ''); const glyphs = html.match(/]*>/g) || []; expect(glyphs.length).toBeGreaterThan(0); glyphs.forEach((tag: string) => expect(tag).toContain('aria-hidden="true"')); }); test('respects custom maxStars in the aria-label', () => { const { html } = toHtml({ rating: 2, maxStars: 10 }, ''); expect(html).toContain('aria-label="Rating: 2 out of 10"'); }); }); describe('StarRating.toHtml XSS hardening (filledColor/emptyColor/size into style=)', () => { test('a filledColor value containing a quote breakout is neutralized', () => { const malicious = '#f00" onmouseover="alert(1)'; const { html } = toHtml({ rating: 3, maxStars: 5, filledColor: malicious }, ''); expect(html).not.toMatch(/style="[^"]*"[^>]*onmouseover/); }); test('a size value containing '; const { html } = toHtml({ rating: 3, maxStars: 5, size: malicious }, ''); expect(html).not.toContain(''); }); test('a normal filled color still renders', () => { const { html } = toHtml({ rating: 5, maxStars: 5, filledColor: '#ff9900' }, ''); expect(html).toContain('color:#ff9900'); }); }); describe('StarRating.toHtml XSS hardening (rating/maxStars into aria-label, F2.2 CONFIRMED sink)', () => { test('a maxStars value with an attribute-breakout string is neutralized in aria-label', () => { const malicious = '5" onmouseover="alert(1)'; const { html } = toHtml({ rating: 3, maxStars: malicious as any }, ''); expect(html).not.toMatch(/onmouseover/); expect(html).not.toMatch(/aria-label="Rating: 3 out of 5" onmouseover/); }); test('a rating value with an attribute-breakout string is neutralized in aria-label', () => { const malicious = '4.5" onmouseover="alert(1)'; const { html } = toHtml({ rating: malicious as any, maxStars: 5 }, ''); expect(html).not.toMatch(/onmouseover/); }); test('a non-numeric maxStars does not blow up the star loop (no NaN glyph count, no huge output)', () => { const malicious = '5" onmouseover="alert(1)'; const { html } = toHtml({ rating: 3, maxStars: malicious as any }, ''); const glyphs = html.match(/ { const { html } = toHtml({ rating: 3, maxStars: 1e9 as any }, ''); const glyphs = html.match(/ { const { html } = toHtml({ rating: 4.5, maxStars: 5 }, ''); expect(html).toMatch(/ { test('animation, animationDelay, and all 3 hideOn* flags are declared (blank/false defaults)', () => { const props = (StarRating as any).craft.props; expect(props).toHaveProperty('animation', ''); expect(props).toHaveProperty('animationDelay', ''); expect(props).toHaveProperty('hideOnDesktop', false); expect(props).toHaveProperty('hideOnTablet', false); expect(props).toHaveProperty('hideOnMobile', false); }); });