From ae0e3b271db225c02884349bc1b93658ebdf2373 Mon Sep 17 00:00:00 2001 From: Marcel Date: Sun, 19 Apr 2026 23:34:30 +0200 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20add=20ProgressRing=20componen?= =?UTF-8?q?t=20=E2=80=94=20SVG=20progress=20arc=20with=20percentage=20labe?= =?UTF-8?q?l?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .../src/lib/components/ProgressRing.svelte | 26 +++++++++++ .../components/ProgressRing.svelte.spec.ts | 44 +++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 frontend/src/lib/components/ProgressRing.svelte create mode 100644 frontend/src/lib/components/ProgressRing.svelte.spec.ts diff --git a/frontend/src/lib/components/ProgressRing.svelte b/frontend/src/lib/components/ProgressRing.svelte new file mode 100644 index 00000000..4b4c5d89 --- /dev/null +++ b/frontend/src/lib/components/ProgressRing.svelte @@ -0,0 +1,26 @@ + + +
+ + + + + + {percentage}% + +
diff --git a/frontend/src/lib/components/ProgressRing.svelte.spec.ts b/frontend/src/lib/components/ProgressRing.svelte.spec.ts new file mode 100644 index 00000000..8efc36e5 --- /dev/null +++ b/frontend/src/lib/components/ProgressRing.svelte.spec.ts @@ -0,0 +1,44 @@ +import { afterEach, describe, expect, it } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import { page } from 'vitest/browser'; +import ProgressRing from './ProgressRing.svelte'; + +afterEach(cleanup); + +describe('ProgressRing', () => { + it('renders the correct stroke-dasharray for 75%', async () => { + render(ProgressRing, { percentage: 75 }); + const arc = document.querySelector('circle.fill-arc') as SVGCircleElement | null; + expect(arc).not.toBeNull(); + // circumference = 2 * π * 7 ≈ 43.98; 75% of that ≈ 32.99 + const dasharray = arc!.getAttribute('stroke-dasharray') ?? ''; + const filled = parseFloat(dasharray.split(' ')[0]); + expect(filled).toBeCloseTo(32.99, 1); + }); + + it('renders a gray label when percentage is 0', async () => { + render(ProgressRing, { percentage: 0 }); + const label = page.getByText('0%'); + await expect.element(label).toBeInTheDocument(); + // Label should carry the gray class, not the mint class + const el = (await label.element()) as HTMLElement; + expect(el.className).toContain('text-gray-400'); + }); + + it('renders a mint-colored label when percentage is > 0', async () => { + render(ProgressRing, { percentage: 75 }); + const label = page.getByText('75%'); + await expect.element(label).toBeInTheDocument(); + const el = (await label.element()) as HTMLElement; + expect(el.className).toContain('text-accent'); + }); + + it('renders a fully filled arc for 100%', async () => { + render(ProgressRing, { percentage: 100 }); + const arc = document.querySelector('circle.fill-arc') as SVGCircleElement | null; + expect(arc).not.toBeNull(); + const dasharray = arc!.getAttribute('stroke-dasharray') ?? ''; + const filled = parseFloat(dasharray.split(' ')[0]); + expect(filled).toBeCloseTo(43.98, 1); + }); +});