From b4132b54ca67217f706c512c921bf70f86302e31 Mon Sep 17 00:00:00 2001 From: Scott Rhamy Date: Sun, 11 Jan 2026 09:01:17 -0500 Subject: [PATCH 1/4] fix for empty layers toggle showing small circle --- docs/src/routes/docs/components/[name]/+layout.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/routes/docs/components/[name]/+layout.svelte b/docs/src/routes/docs/components/[name]/+layout.svelte index 6d4b92132..cd50a4c2b 100644 --- a/docs/src/routes/docs/components/[name]/+layout.svelte +++ b/docs/src/routes/docs/components/[name]/+layout.svelte @@ -94,7 +94,7 @@ {page.params.example?.replaceAll('-', ' ') ?? metadata.name} - {#if layers} + {#if layers?.length} Date: Sun, 25 Jan 2026 09:22:34 -0500 Subject: [PATCH 2/4] further arc tests Now covers all props. Here are all the new tests added: Motion Tests (fixed/rewritten): 1. should accept spring motion config - Rewritten to test animation from initial to final state using expect.poll() 2. should accept tween motion config - Uncommented and fixed to test animation Props Tests (new): 3. should render with custom domain - Tests domain prop individually 4. should render with custom range - Tests range prop individually 5. should render with startAngle in radians - Tests startAngle prop 6. should render with endAngle in radians - Tests endAngle prop 7. should render with both startAngle and endAngle - Tests combined usage 8. should render with innerRadius - Tests innerRadius prop individually 9. should render with outerRadius - Tests outerRadius prop individually 10. should render track with trackStartAngle - Tests trackStartAngle prop individually 11. should render track with trackEndAngle - Tests trackEndAngle prop individually 12. should render track with trackStartAngle and trackEndAngle - Tests combined usage 13. should render track with trackInnerRadius - Tests trackInnerRadius prop individually 14. should render track with trackOuterRadius - Tests trackOuterRadius prop individually 15. should render track with trackCornerRadius - Tests trackCornerRadius prop (was commented out) 16. should render track with trackPadAngle - Tests trackPadAngle prop (was missing) 17. should call tooltipContext.show on pointer enter with data - Tests tooltipContext + data props 18. should call tooltipContext.hide on pointer leave - Tests tooltipContext hide behavior 19. should apply stroke color - Tests stroke prop explicitly 20. should apply strokeWidth - Tests strokeWidth prop (was missing) --- .../src/lib/components/Arc.svelte.test.ts | 704 +++++++++++------- 1 file changed, 454 insertions(+), 250 deletions(-) diff --git a/packages/layerchart/src/lib/components/Arc.svelte.test.ts b/packages/layerchart/src/lib/components/Arc.svelte.test.ts index bafb4658c..d6a10ee46 100644 --- a/packages/layerchart/src/lib/components/Arc.svelte.test.ts +++ b/packages/layerchart/src/lib/components/Arc.svelte.test.ts @@ -93,9 +93,10 @@ for (const layer of supportedLayers) { }); /*------------------------------------------------------------------------- - / PROPS TESTS + / PROPS TESTS (ordered by API) / ------------------------------------------------------------------------- */ describe.skipIf(layer === 'canvas')('Checking Props', () => { + // value it('should render an arc path with value', async () => { render(TestHarness, { layer, @@ -107,13 +108,52 @@ for (const layer of supportedLayers) { el = page.getByTestId(componentTestId).first(); await expect.element(el).toBeInTheDocument(); - const element = el.element() as SVGPathElement; if (layer === 'svg') { - const d = element.getAttribute('d'); + const d = el.element()?.getAttribute('d'); expect(d).toBe('M0,-150A150,150,0,1,1,0,150L0,0Z'); } }); + // domain + it('should render with custom domain', async () => { + render(TestHarness, { + layer, + component: TestComponent, + componentProps: { + value: 50, + domain: [0, 200] as [number, number], + }, + }); + + el = page.getByTestId(componentTestId).first(); + await expect.element(el).toBeInTheDocument(); + if (layer === 'svg') { + const d = el.element()?.getAttribute('d'); + // 50 out of 200 = 25% of 360 = 90 degrees + expect(d).toBe('M0,-150A150,150,0,0,1,150,0L0,0Z'); + } + }); + + // range + it('should render with custom range', async () => { + render(TestHarness, { + layer, + component: TestComponent, + componentProps: { + value: 50, + range: [0, 180] as [number, number], + }, + }); + + el = page.getByTestId(componentTestId).first(); + await expect.element(el).toBeInTheDocument(); + if (layer === 'svg') { + const d = el.element()?.getAttribute('d'); + // 50% of 180 degrees = 90 degrees + expect(d).toBe('M0,-150A150,150,0,0,1,150,0L0,0Z'); + } + }); + it('should render with custom domain and range', async () => { render(TestHarness, { layer, @@ -127,13 +167,127 @@ for (const layer of supportedLayers) { el = page.getByTestId(componentTestId).first(); await expect.element(el).toBeInTheDocument(); - const element = el.element() as SVGPathElement; if (layer === 'svg') { - const d = element.getAttribute('d'); + const d = el.element()?.getAttribute('d'); + expect(d).toBe('M0,-150A150,150,0,0,1,150,0L0,0Z'); + } + }); + + it('should handle custom start angle in range', async () => { + render(TestHarness, { + layer, + component: TestComponent, + componentProps: { + value: 50, + range: [90, 270] as [number, number], + }, + }); + + const el = page.getByTestId(componentTestId); + await expect.element(el).toBeInTheDocument(); + if (layer === 'svg') { + const d = el.element()?.getAttribute('d'); + expect(d).toBe('M150,0A150,150,0,0,1,0,150L0,0Z'); + } + }); + + // startAngle + it('should render with startAngle in radians', async () => { + render(TestHarness, { + layer, + component: TestComponent, + componentProps: { + value: 50, + startAngle: Math.PI / 2, // 90 degrees + }, + }); + + el = page.getByTestId(componentTestId).first(); + await expect.element(el).toBeInTheDocument(); + if (layer === 'svg') { + const d = el.element()?.getAttribute('d'); + // Arc starts at 90 degrees (pointing right), value=50 maps to 180 degrees of arc + expect(d).toBe('M150,0A150,150,0,0,1,0,150L0,0Z'); + } + }); + + // endAngle + it('should render with endAngle in radians', async () => { + render(TestHarness, { + layer, + component: TestComponent, + componentProps: { + value: 50, + endAngle: Math.PI, // 180 degrees, overrides value-based calculation + }, + }); + + el = page.getByTestId(componentTestId).first(); + await expect.element(el).toBeInTheDocument(); + if (layer === 'svg') { + const d = el.element()?.getAttribute('d'); + // endAngle overrides value, arc ends at 180 degrees + expect(d).toBe('M0,-150A150,150,0,1,1,0,150L0,0Z'); + } + }); + + it('should render with both startAngle and endAngle', async () => { + render(TestHarness, { + layer, + component: TestComponent, + componentProps: { + value: 50, + startAngle: 0, + endAngle: Math.PI / 2, // 90 degree arc + }, + }); + + el = page.getByTestId(componentTestId).first(); + await expect.element(el).toBeInTheDocument(); + if (layer === 'svg') { + const d = el.element()?.getAttribute('d'); expect(d).toBe('M0,-150A150,150,0,0,1,150,0L0,0Z'); } }); + // innerRadius + it('should render with innerRadius', async () => { + render(TestHarness, { + layer, + component: TestComponent, + componentProps: { + value: 50, + innerRadius: 50, + }, + }); + + el = page.getByTestId(componentTestId).first(); + await expect.element(el).toBeInTheDocument(); + if (layer === 'svg') { + const d = el.element()?.getAttribute('d'); + expect(d).toBe('M0,-150A150,150,0,1,1,0,150L0,50A50,50,0,1,0,0,-50Z'); + } + }); + + // outerRadius + it('should render with outerRadius', async () => { + render(TestHarness, { + layer, + component: TestComponent, + componentProps: { + value: 50, + outerRadius: 100, + }, + }); + + el = page.getByTestId(componentTestId).first(); + await expect.element(el).toBeInTheDocument(); + if (layer === 'svg') { + const d = el.element()?.getAttribute('d'); + expect(d).toBe('M0,-100A100,100,0,1,1,0,100L0,0Z'); + } + }); + it('should render with innerRadius and outerRadius', async () => { render(TestHarness, { layer, @@ -147,13 +301,13 @@ for (const layer of supportedLayers) { el = page.getByTestId(componentTestId).first(); await expect.element(el).toBeInTheDocument(); - const element = el.element() as SVGPathElement; if (layer === 'svg') { - const d = element.getAttribute('d'); + const d = el.element()?.getAttribute('d'); expect(d).toBe('M0,-50A50,50,0,1,1,-50,0L-30,0A30,30,0,1,0,0,-30Z'); } }); + // cornerRadius it('should render with cornerRadius', async () => { render(TestHarness, { layer, @@ -166,15 +320,15 @@ for (const layer of supportedLayers) { el = page.getByTestId(componentTestId).first(); await expect.element(el).toBeInTheDocument(); - const element = el.element() as SVGPathElement; if (layer === 'svg') { - const d = element.getAttribute('d'); + const d = el.element()?.getAttribute('d'); expect(d).toBe( 'M0,-144.914A5,5,0,0,1,5.172,-149.911A150,150,0,0,1,5.172,149.911A5,5,0,0,1,0,144.914L0,0Z' ); } }); + // padAngle it('should render with padAngle', async () => { render(TestHarness, { layer, @@ -193,13 +347,15 @@ for (const layer of supportedLayers) { } }); - it('should render track when track prop is true', async () => { + // trackStartAngle + it('should render track with trackStartAngle', async () => { render(TestHarness, { layer, component: TestComponent, componentProps: { value: 50, track: { 'data-testid': 'arc-track' }, + trackStartAngle: Math.PI / 2, // 90 degrees }, }); @@ -207,35 +363,101 @@ for (const layer of supportedLayers) { await expect.element(el).toBeInTheDocument(); if (layer === 'svg') { const d = el.element()?.getAttribute('d'); - expect(d).toBe('M0,-150A150,150,0,1,1,0,150A150,150,0,1,1,0,-150Z'); + // Track starts at 90 degrees (right), ends at default 360 degrees (top) + expect(d).toBe('M150,0A150,150,0,1,1,0,-150L0,0Z'); } }); - it('should render track with custom props', async () => { + // trackEndAngle + it('should render track with trackEndAngle', async () => { render(TestHarness, { layer, component: TestComponent, componentProps: { value: 50, - track: { class: 'fill-none stroke-surface-content/10', 'data-testid': 'arc-track' }, + track: { 'data-testid': 'arc-track' }, + trackEndAngle: Math.PI, // 180 degrees }, }); - const el = page.getByTestId('arc-track'); + el = page.getByTestId('arc-track'); await expect.element(el).toBeInTheDocument(); if (layer === 'svg') { - await expect.element(el).toHaveClass('fill-none'); - await expect.element(el).toHaveClass('stroke-surface-content/10'); + const d = el.element()?.getAttribute('d'); + // Track ends at 180 degrees + expect(d).toBe('M0,-150A150,150,0,1,1,0,150L0,0Z'); } }); - it('should support custom track radius values', async () => { + it('should render track with trackStartAngle and trackEndAngle', async () => { render(TestHarness, { layer, component: TestComponent, componentProps: { value: 50, - track: { class: 'fill-none stroke-surface-content/10', 'data-testid': 'arc-track' }, + track: { 'data-testid': 'arc-track' }, + trackStartAngle: 0, + trackEndAngle: Math.PI, + }, + }); + + el = page.getByTestId('arc-track'); + await expect.element(el).toBeInTheDocument(); + if (layer === 'svg') { + const d = el.element()?.getAttribute('d'); + expect(d).toBe('M0,-150A150,150,0,1,1,0,150L0,0Z'); + } + }); + + // trackInnerRadius + it('should render track with trackInnerRadius', async () => { + render(TestHarness, { + layer, + component: TestComponent, + componentProps: { + value: 50, + track: { 'data-testid': 'arc-track' }, + trackInnerRadius: 50, + }, + }); + + el = page.getByTestId('arc-track'); + await expect.element(el).toBeInTheDocument(); + if (layer === 'svg') { + const d = el.element()?.getAttribute('d'); + expect(d).toBe( + 'M0,-150A150,150,0,1,1,0,150A150,150,0,1,1,0,-150M0,-50A50,50,0,1,0,0,50A50,50,0,1,0,0,-50Z' + ); + } + }); + + // trackOuterRadius + it('should render track with trackOuterRadius', async () => { + render(TestHarness, { + layer, + component: TestComponent, + componentProps: { + value: 50, + track: { 'data-testid': 'arc-track' }, + trackOuterRadius: 100, + }, + }); + + el = page.getByTestId('arc-track'); + await expect.element(el).toBeInTheDocument(); + if (layer === 'svg') { + const d = el.element()?.getAttribute('d'); + expect(d).toBe('M0,-100A100,100,0,1,1,0,100A100,100,0,1,1,0,-100Z'); + } + }); + + it('should render track with trackInnerRadius and trackOuterRadius', async () => { + render(TestHarness, { + layer, + component: TestComponent, + componentProps: { + value: 50, + track: { 'data-testid': 'arc-track' }, trackInnerRadius: 20, trackOuterRadius: 60, }, @@ -251,15 +473,17 @@ for (const layer of supportedLayers) { } }); - it('should support custom track angles', async () => { + // trackCornerRadius + it('should render track with trackCornerRadius', async () => { render(TestHarness, { layer, component: TestComponent, componentProps: { value: 50, - track: { class: 'fill-none stroke-surface-content/10', 'data-testid': 'arc-track' }, + track: { 'data-testid': 'arc-track' }, trackStartAngle: 0, - trackEndAngle: Math.PI, + trackEndAngle: Math.PI, // half circle to make corner radius visible + trackCornerRadius: 10, }, }); @@ -267,320 +491,286 @@ for (const layer of supportedLayers) { await expect.element(el).toBeInTheDocument(); if (layer === 'svg') { const d = el.element()?.getAttribute('d'); - expect(d).toBe('M0,-150A150,150,0,1,1,0,150L0,0Z'); + // With corner radius, the path should have additional curve commands + expect(d).toContain('A10,10'); } }); - // TODO: Implement this test - // IT PASSES WITH ANY CORNER RADIUS, NOT SURE HOW TO TEST THIS - // it('should support track corner radius', async () => { - // render(TestHarness, { - // layer, - // component: TestComponent, - // componentProps: { - // value: 50, - // track: { class: 'fill-none stroke-surface-content/10', dataTestId: 'arc-track' }, - // trackCornerRadius: 10, - // }, - // }); - - // el = page.getByTestId('arc-track'); - // await expect.element(el).toBeInTheDocument(); - // if (layer === 'svg') { - // const d = el.element()?.getAttribute('d'); - // expect(d).toBe('M0,-100A100,100,0,1,1,0,100A100,100,0,1,1,0,-100Z'); - // } - // }); + // trackPadAngle + it('should render track with trackPadAngle', async () => { + render(TestHarness, { + layer, + component: TestComponent, + componentProps: { + value: 50, + track: { 'data-testid': 'arc-track' }, + trackInnerRadius: 50, + trackStartAngle: 0, + trackEndAngle: Math.PI, // half circle so padAngle has visible effect + trackPadAngle: 0.1, + }, + }); + + el = page.getByTestId('arc-track'); + await expect.element(el).toBeInTheDocument(); + if (layer === 'svg') { + const d = el.element()?.getAttribute('d'); + // With pad angle, the arc path coordinates should be offset from start + expect(d).toContain('A150,150'); + expect(d).toContain('A50,50'); + // The start coordinates should not be exactly at 0,-150 due to padding + expect(d).not.toMatch(/^M0,-150/); + } + }); - it('should apply fill color', async () => { + // offset + it('should apply offset to arc position', async () => { render(TestHarness, { layer, component: TestComponent, componentProps: { value: 50, - fill: 'red', + offset: 10, }, }); const el = page.getByTestId(componentTestId); - await expect.element(el).toHaveAttribute('fill', 'red'); + await expect.element(el).toBeInTheDocument(); + if (layer === 'svg') { + const transform = el.element()?.getAttribute('transform'); + expect(transform).toContain('translate(-4.539904997395462, 8.91006524188368)'); + } }); - it('should apply opacity', async () => { + it('should apply zero offset by default', async () => { render(TestHarness, { layer, component: TestComponent, componentProps: { value: 50, - opacity: 0.5, }, }); const el = page.getByTestId(componentTestId); await expect.element(el).toBeInTheDocument(); - await expect.element(el).toHaveAttribute('opacity', '0.5'); + if (layer === 'svg') { + const transform = el.element()?.getAttribute('transform'); + expect(transform).toBe('translate(0, 0)'); + } }); - it('should apply fillOpacity', async () => { + // track + it('should render track when track prop is provided', async () => { render(TestHarness, { layer, component: TestComponent, componentProps: { value: 50, - fillOpacity: 0.7, + track: { 'data-testid': 'arc-track' }, + }, + }); + + el = page.getByTestId('arc-track'); + await expect.element(el).toBeInTheDocument(); + if (layer === 'svg') { + const d = el.element()?.getAttribute('d'); + expect(d).toBe('M0,-150A150,150,0,1,1,0,150A150,150,0,1,1,0,-150Z'); + } + }); + + it('should render track with custom class', async () => { + render(TestHarness, { + layer, + component: TestComponent, + componentProps: { + value: 50, + track: { class: 'fill-none stroke-surface-content/10', 'data-testid': 'arc-track' }, + }, + }); + + const el = page.getByTestId('arc-track'); + await expect.element(el).toBeInTheDocument(); + if (layer === 'svg') { + await expect.element(el).toHaveClass('fill-none'); + await expect.element(el).toHaveClass('stroke-surface-content/10'); + } + }); + + // tooltipContext + data + it('should call tooltipContext.show on pointer enter with data', async () => { + const mockTooltipContext = { + show: vi.fn(), + hide: vi.fn(), + x: 0, + y: 0, + data: null, + payload: [], + mode: 'manual' as const, + isHoveringTooltipArea: false, + isHoveringTooltipContent: false, + }; + const testData = { label: 'Test', value: 50 }; + + render(TestHarness, { + layer, + component: TestComponent, + componentProps: { + value: 50, + fill: 'blue', + tooltipContext: mockTooltipContext, + data: testData, }, }); const el = page.getByTestId(componentTestId); await expect.element(el).toBeInTheDocument(); - await expect.element(el).toHaveAttribute('fill-opacity', '0.7'); + await el.hover(); + + expect(mockTooltipContext.show).toHaveBeenCalled(); + expect(mockTooltipContext.show.mock.calls[0][1]).toEqual(testData); }); - it('should apply custom class', async () => { + it('should call tooltipContext.hide on pointer leave', async () => { + const mockTooltipContext = { + show: vi.fn(), + hide: vi.fn(), + x: 0, + y: 0, + data: null, + payload: [], + mode: 'manual' as const, + isHoveringTooltipArea: false, + isHoveringTooltipContent: false, + }; + render(TestHarness, { layer, component: TestComponent, componentProps: { value: 50, - class: 'custom-arc-class', + fill: 'blue', + tooltipContext: mockTooltipContext, + data: { value: 50 }, }, }); const el = page.getByTestId(componentTestId); await expect.element(el).toBeInTheDocument(); - await expect.element(el).toHaveClass('custom-arc-class'); + await el.hover(); + // Move away from the element to trigger pointer leave + await page.getByTestId('test-lc-chart').hover({ position: { x: 0, y: 0 } }); + + expect(mockTooltipContext.hide).toHaveBeenCalled(); }); - // TODO: - // PASSES WITH ANY OUTER RADIUS, NOT SURE HOW TO TEST THIS - // it('should calculate outerRadius from discrete value (>= 1)', async () => { - // render(TestHarness, { - // layer, - // component: TestComponent, - // componentProps: { - // value: 50, - // outerRadius: 80, - // }, - // }); - - // const el = page.getByTestId(componentTestId); - // await expect.element(el).toBeInTheDocument(); - // if (layer === 'svg') { - // const d = el.element()?.getAttribute('d'); - // expect(d).toBe('M0,-80A80,80,0,1,1,0,80A80,80,0,1,1,0,-80Z'); - // } - // }); - - // it('should calculate outerRadius from percentage (0 < value < 1)', async () => { - // render(TestHarness, { - // layer, - // component: TestComponent, - // componentProps: { - // value: 50, - // outerRadius: 0.8, - // }, - // }); - - // const el = page.getByTestId(componentTestId); - // await expect.element(el).toBeInTheDocument(); - // if (layer === 'svg') { - // const d = el.element()?.getAttribute('d'); - // expect(d).toBe('M0,-80A80,80,0,1,1,0,80A80,80,0,1,1,0,-80Z'); - // } - // }); - - // it('should calculate outerRadius from offset (< 0)', async () => { - // render(TestHarness, { - // layer, - // component: TestComponent, - // componentProps: { - // value: 50, - // outerRadius: -10, - // }, - // }); - - // const el = page.getByTestId(componentTestId); - // await expect.element(el).toBeInTheDocument(); - // if (layer === 'svg') { - // const d = el.element()?.getAttribute('d'); - // expect(d).toBe('M0,-10A10,10,0,1,0,0,10A10,10,0,1,0,0,-10Z'); - // } - // }); - - // it('should calculate innerRadius from discrete value (>= 1)', async () => { - // render(TestHarness, { - // layer, - // component: TestComponent, - // componentProps: { - // value: 50, - // innerRadius: 30, - // }, - // }); - - // const el = page.getByTestId(componentTestId); - // await expect.element(el).toBeInTheDocument(); - // if (layer === 'svg') { - // const d = el.element()?.getAttribute('d'); - // expect(d).toBe('M0,-30A30,30,0,1,0,0,30A30,30,0,1,0,0,-30Z'); - // } - // }); - - // it('should calculate innerRadius from percentage (0 < value < 1)', async () => { - // render(TestHarness, { - // layer, - // component: TestComponent, - // componentProps: { - // value: 50, - // innerRadius: 0.5, - // }, - // }); - - // const el = page.getByTestId(componentTestId); - // await expect.element(el).toBeInTheDocument(); - // if (layer === 'svg') { - // const d = el.element()?.getAttribute('d'); - // expect(d).toBe('M0,-30A30,30,0,1,0,0,30A30,30,0,1,0,0,-30Z'); - // } - // }); - - // it('should calculate innerRadius from offset (< 0)', async () => { - // render(TestHarness, { - // layer, - // component: TestComponent, - // componentProps: { - // value: 50, - // innerRadius: -20, - // }, - // }); - - // const el = page.getByTestId(componentTestId); - // await expect.element(el).toBeInTheDocument(); - // if (layer === 'svg') { - // const d = el.element()?.getAttribute('d'); - // expect(d).toBe('M0,-30A30,30,0,1,0,0,30A30,30,0,1,0,0,-30Z'); - // } - // }); - - // it('should use startAngle and endAngle in radians', async () => { - // render(TestHarness, { - // layer, - // component: TestComponent, - // componentProps: { - // startAngle: 0, - // endAngle: Math.PI / 2, - // }, - // }); - - // const el = page.getByTestId(componentTestId); - // await expect.element(el).toBeInTheDocument(); - // if (layer === 'svg') { - // const d = el.element()?.getAttribute('d'); - // expect(d).toBe('M0,-100A100,100,0,1,1,0,100A100,100,0,1,1,0,-100Z'); - // } - // }); - - it('should convert range degrees to radians', async () => { + // fill + it('should apply fill color', async () => { render(TestHarness, { layer, component: TestComponent, componentProps: { value: 50, - range: [0, 180] as [number, number], + fill: 'red', + }, + }); + + const el = page.getByTestId(componentTestId); + await expect.element(el).toHaveAttribute('fill', 'red'); + }); + + // fillOpacity + it('should apply fillOpacity', async () => { + render(TestHarness, { + layer, + component: TestComponent, + componentProps: { + value: 50, + fillOpacity: 0.7, }, }); const el = page.getByTestId(componentTestId); await expect.element(el).toBeInTheDocument(); - if (layer === 'svg') { - const d = el.element()?.getAttribute('d'); - expect(d).toBe('M0,-150A150,150,0,0,1,150,0L0,0Z'); - } + await expect.element(el).toHaveAttribute('fill-opacity', '0.7'); }); - it('should scale value to angle based on domain and range', async () => { + // stroke + it('should have stroke="none" by default', async () => { render(TestHarness, { layer, component: TestComponent, componentProps: { value: 50, - domain: [0, 100] as [number, number], - range: [0, 360] as [number, number], }, }); const el = page.getByTestId(componentTestId); await expect.element(el).toBeInTheDocument(); - if (layer === 'svg') { - const d = el.element()?.getAttribute('d'); - expect(d).toBe('M0,-150A150,150,0,1,1,0,150L0,0Z'); - } + await expect.element(el).toHaveAttribute('stroke', 'none'); }); - it('should handle custom start angle in range', async () => { + it('should apply stroke color', async () => { render(TestHarness, { layer, component: TestComponent, componentProps: { value: 50, - range: [90, 270] as [number, number], + stroke: 'blue', }, }); const el = page.getByTestId(componentTestId); await expect.element(el).toBeInTheDocument(); - if (layer === 'svg') { - const d = el.element()?.getAttribute('d'); - expect(d).toBe('M150,0A150,150,0,0,1,0,150L0,0Z'); - } + await expect.element(el).toHaveAttribute('stroke', 'blue'); }); - it('should apply offset to arc position', async () => { + // strokeWidth + it('should apply strokeWidth', async () => { render(TestHarness, { layer, component: TestComponent, componentProps: { value: 50, - offset: 10, + stroke: 'blue', + strokeWidth: 3, }, }); const el = page.getByTestId(componentTestId); await expect.element(el).toBeInTheDocument(); - if (layer === 'svg') { - const transform = el.element()?.getAttribute('transform'); - expect(transform).toContain('translate(-4.539904997395462, 8.91006524188368)'); - } + await expect.element(el).toHaveAttribute('stroke-width', '3'); }); - it('should apply zero offset by default', async () => { + // opacity + it('should apply opacity', async () => { render(TestHarness, { layer, component: TestComponent, componentProps: { value: 50, + opacity: 0.5, }, }); const el = page.getByTestId(componentTestId); await expect.element(el).toBeInTheDocument(); - if (layer === 'svg') { - const transform = el.element()?.getAttribute('transform'); - expect(transform).toBe('translate(0, 0)'); - } + await expect.element(el).toHaveAttribute('opacity', '0.5'); }); - it('should have stroke="none" by default', async () => { + // class + it('should apply custom class', async () => { render(TestHarness, { layer, component: TestComponent, componentProps: { value: 50, + class: 'custom-arc-class', }, }); const el = page.getByTestId(componentTestId); await expect.element(el).toBeInTheDocument(); - await expect.element(el).toHaveAttribute('stroke', 'none'); + await expect.element(el).toHaveClass('custom-arc-class'); }); }); }); @@ -609,42 +799,56 @@ for (const layer of supportedLayers) { expect(d).toBe('M0,-150L0,0Z'); } }); - // TODO - // not sure how to test this - // it('should accept spring motion config', async () => { - // render(TestHarness, { - // layer, - // component: TestComponent, - // componentProps: { - // value: 50, - // motion: { type: 'spring', stiffness: 100, damping: 10 }, - // }, - // }); - // const el = page.getByTestId(componentTestId); - // await expect.element(el).toBeInTheDocument(); - // if (layer === 'svg') { - // const d = el.element()?.getAttribute('d'); - // expect(d).toBe('MM0,-100A100,100,0,1,1,0,100L0,0Z'); - // } - // }); - // TODO - // not sure how to test this - // it('should accept tween motion config', async () => { - // render(TestHarness, { - // layer, - // component: TestComponent, - // componentProps: { - // value: 50, - // motion: { type: 'tween', duration: 300 }, - // }, - // }); - // const el = page.getByTestId(componentTestId); - // await expect.element(el).toBeInTheDocument(); - // if (layer === 'svg') { - // const d = el.element()?.getAttribute('d'); - // expect(d).toBe('M0,-100A100,100,0,1,1,0,100L0,0Z'); - // } - // }); + it('should accept spring motion config', async () => { + render(TestHarness, { + layer, + component: TestComponent, + componentProps: { + value: 50, + initialValue: 0, + motion: { type: 'spring', stiffness: 100, damping: 10 }, + }, + }); + + const el = page.getByTestId(componentTestId); + await expect.element(el).toBeInTheDocument(); + + if (layer === 'svg') { + // Initial state (value=0) - just a line + const initialD = el.element()?.getAttribute('d'); + expect(initialD).toBe('M0,-150L0,0Z'); + + // Wait for spring animation to settle at final state (value=50) + await expect + .poll(() => el.element()?.getAttribute('d'), { timeout: 3000 }) + .toBe('M0,-150A150,150,0,1,1,0,150L0,0Z'); + } + }); + it('should accept tween motion config', async () => { + render(TestHarness, { + layer, + component: TestComponent, + componentProps: { + value: 50, + initialValue: 0, + motion: { type: 'tween', duration: 300 }, + }, + }); + + const el = page.getByTestId(componentTestId); + await expect.element(el).toBeInTheDocument(); + + if (layer === 'svg') { + // Initial state (value=0) - just a line + const initialD = el.element()?.getAttribute('d'); + expect(initialD).toBe('M0,-150L0,0Z'); + + // Wait for tween animation to complete at final state (value=50) + await expect + .poll(() => el.element()?.getAttribute('d'), { timeout: 1000 }) + .toBe('M0,-150A150,150,0,1,1,0,150L0,0Z'); + } + }); }); /*------------------------------------------------------------------------- From 934193b751ddc9193704a06b9b52433d05170f18 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Mon, 26 Jan 2026 10:45:25 -0500 Subject: [PATCH 3/4] simplify --- .../src/lib/components/Arc.svelte.test.ts | 1472 ++++++++--------- .../lib/components/tests/TestHarness.svelte | 9 +- 2 files changed, 675 insertions(+), 806 deletions(-) diff --git a/packages/layerchart/src/lib/components/Arc.svelte.test.ts b/packages/layerchart/src/lib/components/Arc.svelte.test.ts index d6a10ee46..ce780932a 100644 --- a/packages/layerchart/src/lib/components/Arc.svelte.test.ts +++ b/packages/layerchart/src/lib/components/Arc.svelte.test.ts @@ -1,866 +1,682 @@ import { describe, expect, it, vi } from 'vitest'; import { render } from 'vitest-browser-svelte'; import { page, type Locator } from 'vitest/browser'; -import TestHarness from './tests/TestHarness.svelte'; +import type { ComponentProps } from 'svelte'; -// Component-specific configuration --------------------------------------- -import TestComponent from './Arc.svelte'; -const componentName = 'Arc'; +import TestHarness, { componentTestId } from './tests/TestHarness.svelte'; +import Arc from './Arc.svelte'; import Text from './Text.svelte'; -const supportedLayers: Array<'svg' | 'html' | 'canvas'> = ['svg']; -const componentTestId = 'test-lc-component'; -const accessoryTestIds = ['arc-track']; // optional -// ------------------------------------------------------------------------- + +const defaultProps: Partial> = { + fill: 'currentColor', +}; + let el: Locator | HTMLElement | SVGElement | null; // resuable -for (const layer of supportedLayers) { - const isCanvas = layer === 'canvas'; - - /*------------------------------------------------------------------------- - / INITIAL LAYERS RENDERINGS TESTS - / ------------------------------------------------------------------------- */ - describe(`${componentName} Testing (${layer})`, () => { - describe('Initial Layers Renderings', () => { - it(`should render correct in layer ${layer}`, async () => { - const { container } = render(TestHarness, { - layer, - component: TestComponent, - componentProps: { - value: 50, - }, - }); - - if (layer === 'canvas') { - // Canvas-specific tests - el = container.querySelector('canvas') as HTMLCanvasElement | null; - await expect.element(el).toBeInTheDocument(); - } else if (layer === 'svg') { - // SVG-specific tests - el = container.querySelector('svg') as SVGElement | null; - await expect.element(el).toBeInTheDocument(); - } else if (layer === 'html') { - // HTML-specific tests - el = container.querySelector('div') as HTMLElement | null; - await expect.element(el).toBeInTheDocument(); - } - }); +describe(`Arc`, () => { + it('should render Arc element', async () => { + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + value: 50, + }, + }); - it('should render Chart element', async () => { - render(TestHarness, { - layer, - component: TestComponent, - componentProps: { - track: { class: 'fill-none stroke-surface-content/10' }, - value: 50, - }, - }); + el = page.getByTestId(componentTestId); + await expect.element(el).toBeInTheDocument(); + }); - el = page.getByTestId('test-lc-chart'); - await expect.element(el).toBeInTheDocument(); - }); + it('should render track', async () => { + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + value: 50, + track: { class: 'fill-none stroke-surface-content/10', 'data-testid': 'arc-track' }, + }, + }); - it('should render Component element', async () => { - render(TestHarness, { - layer, - component: TestComponent, - componentProps: { - track: { class: 'fill-none stroke-surface-content/10' }, - value: 50, - }, - }); + el = page.getByTestId('arc-track'); + await expect.element(el).toBeInTheDocument(); + }); - el = page.getByTestId(componentTestId); - await expect.element(el).toBeInTheDocument(); + describe('props', () => { + // value + it('should render an arc path with value', async () => { + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + value: 50, + }, }); - }); - describe.skipIf(isCanvas)(`It should render accessory elements`, () => { - for (const testId of accessoryTestIds) { - it(`${testId} should be rendered`, async () => { - render(TestHarness, { - layer, - component: TestComponent, - componentProps: { - value: 50, - track: { class: 'fill-none stroke-surface-content/10', 'data-testid': testId }, - }, - }); - - el = page.getByTestId(testId); - await expect.element(el).toBeInTheDocument(); - }); - } + el = page.getByTestId(componentTestId).first(); + await expect.element(el).toBeInTheDocument(); + const d = el.element()?.getAttribute('d'); + expect(d).toBe('M0,-150A150,150,0,1,1,0,150L0,0Z'); }); - /*------------------------------------------------------------------------- - / PROPS TESTS (ordered by API) - / ------------------------------------------------------------------------- */ - describe.skipIf(layer === 'canvas')('Checking Props', () => { - // value - it('should render an arc path with value', async () => { - render(TestHarness, { - layer, - component: TestComponent, - componentProps: { - value: 50, - }, - }); - - el = page.getByTestId(componentTestId).first(); - await expect.element(el).toBeInTheDocument(); - if (layer === 'svg') { - const d = el.element()?.getAttribute('d'); - expect(d).toBe('M0,-150A150,150,0,1,1,0,150L0,0Z'); - } + // domain + it('should render with custom domain', async () => { + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + value: 50, + domain: [0, 200] as [number, number], + }, }); - // domain - it('should render with custom domain', async () => { - render(TestHarness, { - layer, - component: TestComponent, - componentProps: { - value: 50, - domain: [0, 200] as [number, number], - }, - }); - - el = page.getByTestId(componentTestId).first(); - await expect.element(el).toBeInTheDocument(); - if (layer === 'svg') { - const d = el.element()?.getAttribute('d'); - // 50 out of 200 = 25% of 360 = 90 degrees - expect(d).toBe('M0,-150A150,150,0,0,1,150,0L0,0Z'); - } - }); + el = page.getByTestId(componentTestId).first(); + await expect.element(el).toBeInTheDocument(); + const d = el.element()?.getAttribute('d'); + // 50 out of 200 = 25% of 360 = 90 degrees + expect(d).toBe('M0,-150A150,150,0,0,1,150,0L0,0Z'); + }); - // range - it('should render with custom range', async () => { - render(TestHarness, { - layer, - component: TestComponent, - componentProps: { - value: 50, - range: [0, 180] as [number, number], - }, - }); - - el = page.getByTestId(componentTestId).first(); - await expect.element(el).toBeInTheDocument(); - if (layer === 'svg') { - const d = el.element()?.getAttribute('d'); - // 50% of 180 degrees = 90 degrees - expect(d).toBe('M0,-150A150,150,0,0,1,150,0L0,0Z'); - } + // range + it('should render with custom range', async () => { + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + value: 50, + range: [0, 180] as [number, number], + }, }); - it('should render with custom domain and range', async () => { - render(TestHarness, { - layer, - component: TestComponent, - componentProps: { - value: 25, - domain: [0, 50] as [number, number], - range: [0, 180] as [number, number], - }, - }); - - el = page.getByTestId(componentTestId).first(); - await expect.element(el).toBeInTheDocument(); - if (layer === 'svg') { - const d = el.element()?.getAttribute('d'); - expect(d).toBe('M0,-150A150,150,0,0,1,150,0L0,0Z'); - } - }); + el = page.getByTestId(componentTestId).first(); + await expect.element(el).toBeInTheDocument(); + const d = el.element()?.getAttribute('d'); + // 50% of 180 degrees = 90 degrees + expect(d).toBe('M0,-150A150,150,0,0,1,150,0L0,0Z'); + }); - it('should handle custom start angle in range', async () => { - render(TestHarness, { - layer, - component: TestComponent, - componentProps: { - value: 50, - range: [90, 270] as [number, number], - }, - }); - - const el = page.getByTestId(componentTestId); - await expect.element(el).toBeInTheDocument(); - if (layer === 'svg') { - const d = el.element()?.getAttribute('d'); - expect(d).toBe('M150,0A150,150,0,0,1,0,150L0,0Z'); - } + it('should render with custom domain and range', async () => { + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + value: 25, + domain: [0, 50] as [number, number], + range: [0, 180] as [number, number], + }, }); - // startAngle - it('should render with startAngle in radians', async () => { - render(TestHarness, { - layer, - component: TestComponent, - componentProps: { - value: 50, - startAngle: Math.PI / 2, // 90 degrees - }, - }); - - el = page.getByTestId(componentTestId).first(); - await expect.element(el).toBeInTheDocument(); - if (layer === 'svg') { - const d = el.element()?.getAttribute('d'); - // Arc starts at 90 degrees (pointing right), value=50 maps to 180 degrees of arc - expect(d).toBe('M150,0A150,150,0,0,1,0,150L0,0Z'); - } - }); + el = page.getByTestId(componentTestId).first(); + await expect.element(el).toBeInTheDocument(); + const d = el.element()?.getAttribute('d'); + expect(d).toBe('M0,-150A150,150,0,0,1,150,0L0,0Z'); + }); - // endAngle - it('should render with endAngle in radians', async () => { - render(TestHarness, { - layer, - component: TestComponent, - componentProps: { - value: 50, - endAngle: Math.PI, // 180 degrees, overrides value-based calculation - }, - }); - - el = page.getByTestId(componentTestId).first(); - await expect.element(el).toBeInTheDocument(); - if (layer === 'svg') { - const d = el.element()?.getAttribute('d'); - // endAngle overrides value, arc ends at 180 degrees - expect(d).toBe('M0,-150A150,150,0,1,1,0,150L0,0Z'); - } + it('should handle custom start angle in range', async () => { + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + value: 50, + range: [90, 270] as [number, number], + }, }); - it('should render with both startAngle and endAngle', async () => { - render(TestHarness, { - layer, - component: TestComponent, - componentProps: { - value: 50, - startAngle: 0, - endAngle: Math.PI / 2, // 90 degree arc - }, - }); - - el = page.getByTestId(componentTestId).first(); - await expect.element(el).toBeInTheDocument(); - if (layer === 'svg') { - const d = el.element()?.getAttribute('d'); - expect(d).toBe('M0,-150A150,150,0,0,1,150,0L0,0Z'); - } - }); + const el = page.getByTestId(componentTestId); + await expect.element(el).toBeInTheDocument(); + const d = el.element()?.getAttribute('d'); + expect(d).toBe('M150,0A150,150,0,0,1,0,150L0,0Z'); + }); - // innerRadius - it('should render with innerRadius', async () => { - render(TestHarness, { - layer, - component: TestComponent, - componentProps: { - value: 50, - innerRadius: 50, - }, - }); - - el = page.getByTestId(componentTestId).first(); - await expect.element(el).toBeInTheDocument(); - if (layer === 'svg') { - const d = el.element()?.getAttribute('d'); - expect(d).toBe('M0,-150A150,150,0,1,1,0,150L0,50A50,50,0,1,0,0,-50Z'); - } + // startAngle + it('should render with startAngle in radians', async () => { + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + value: 50, + startAngle: Math.PI / 2, // 90 degrees + }, }); - // outerRadius - it('should render with outerRadius', async () => { - render(TestHarness, { - layer, - component: TestComponent, - componentProps: { - value: 50, - outerRadius: 100, - }, - }); - - el = page.getByTestId(componentTestId).first(); - await expect.element(el).toBeInTheDocument(); - if (layer === 'svg') { - const d = el.element()?.getAttribute('d'); - expect(d).toBe('M0,-100A100,100,0,1,1,0,100L0,0Z'); - } - }); + el = page.getByTestId(componentTestId).first(); + await expect.element(el).toBeInTheDocument(); + const d = el.element()?.getAttribute('d'); + // Arc starts at 90 degrees (pointing right), value=50 maps to 180 degrees of arc + expect(d).toBe('M150,0A150,150,0,0,1,0,150L0,0Z'); + }); - it('should render with innerRadius and outerRadius', async () => { - render(TestHarness, { - layer, - component: TestComponent, - componentProps: { - value: 75, - innerRadius: 30, - outerRadius: 50, - }, - }); - - el = page.getByTestId(componentTestId).first(); - await expect.element(el).toBeInTheDocument(); - if (layer === 'svg') { - const d = el.element()?.getAttribute('d'); - expect(d).toBe('M0,-50A50,50,0,1,1,-50,0L-30,0A30,30,0,1,0,0,-30Z'); - } + // endAngle + it('should render with endAngle in radians', async () => { + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + value: 50, + endAngle: Math.PI, // 180 degrees, overrides value-based calculation + }, }); - // cornerRadius - it('should render with cornerRadius', async () => { - render(TestHarness, { - layer, - component: TestComponent, - componentProps: { - value: 50, - cornerRadius: 5, - }, - }); - - el = page.getByTestId(componentTestId).first(); - await expect.element(el).toBeInTheDocument(); - if (layer === 'svg') { - const d = el.element()?.getAttribute('d'); - expect(d).toBe( - 'M0,-144.914A5,5,0,0,1,5.172,-149.911A150,150,0,0,1,5.172,149.911A5,5,0,0,1,0,144.914L0,0Z' - ); - } - }); + el = page.getByTestId(componentTestId).first(); + await expect.element(el).toBeInTheDocument(); + const d = el.element()?.getAttribute('d'); + // endAngle overrides value, arc ends at 180 degrees + expect(d).toBe('M0,-150A150,150,0,1,1,0,150L0,0Z'); + }); - // padAngle - it('should render with padAngle', async () => { - render(TestHarness, { - layer, - component: TestComponent, - componentProps: { - value: 50, - padAngle: 0.02, - }, - }); - - el = page.getByTestId(componentTestId).first(); - await expect.element(el).toBeInTheDocument(); - if (layer === 'svg') { - const d = el?.element()?.getAttribute('d'); - expect(d).toBe('M1.5,-149.993A150,150,0,0,1,1.5,149.993L0,0Z'); - } + it('should render with both startAngle and endAngle', async () => { + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + value: 50, + startAngle: 0, + endAngle: Math.PI / 2, // 90 degree arc + }, }); - // trackStartAngle - it('should render track with trackStartAngle', async () => { - render(TestHarness, { - layer, - component: TestComponent, - componentProps: { - value: 50, - track: { 'data-testid': 'arc-track' }, - trackStartAngle: Math.PI / 2, // 90 degrees - }, - }); - - el = page.getByTestId('arc-track'); - await expect.element(el).toBeInTheDocument(); - if (layer === 'svg') { - const d = el.element()?.getAttribute('d'); - // Track starts at 90 degrees (right), ends at default 360 degrees (top) - expect(d).toBe('M150,0A150,150,0,1,1,0,-150L0,0Z'); - } - }); + el = page.getByTestId(componentTestId).first(); + await expect.element(el).toBeInTheDocument(); + const d = el.element()?.getAttribute('d'); + expect(d).toBe('M0,-150A150,150,0,0,1,150,0L0,0Z'); + }); - // trackEndAngle - it('should render track with trackEndAngle', async () => { - render(TestHarness, { - layer, - component: TestComponent, - componentProps: { - value: 50, - track: { 'data-testid': 'arc-track' }, - trackEndAngle: Math.PI, // 180 degrees - }, - }); - - el = page.getByTestId('arc-track'); - await expect.element(el).toBeInTheDocument(); - if (layer === 'svg') { - const d = el.element()?.getAttribute('d'); - // Track ends at 180 degrees - expect(d).toBe('M0,-150A150,150,0,1,1,0,150L0,0Z'); - } + // innerRadius + it('should render with innerRadius', async () => { + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + value: 50, + innerRadius: 50, + }, }); - it('should render track with trackStartAngle and trackEndAngle', async () => { - render(TestHarness, { - layer, - component: TestComponent, - componentProps: { - value: 50, - track: { 'data-testid': 'arc-track' }, - trackStartAngle: 0, - trackEndAngle: Math.PI, - }, - }); - - el = page.getByTestId('arc-track'); - await expect.element(el).toBeInTheDocument(); - if (layer === 'svg') { - const d = el.element()?.getAttribute('d'); - expect(d).toBe('M0,-150A150,150,0,1,1,0,150L0,0Z'); - } - }); + el = page.getByTestId(componentTestId).first(); + await expect.element(el).toBeInTheDocument(); + const d = el.element()?.getAttribute('d'); + expect(d).toBe('M0,-150A150,150,0,1,1,0,150L0,50A50,50,0,1,0,0,-50Z'); + }); - // trackInnerRadius - it('should render track with trackInnerRadius', async () => { - render(TestHarness, { - layer, - component: TestComponent, - componentProps: { - value: 50, - track: { 'data-testid': 'arc-track' }, - trackInnerRadius: 50, - }, - }); - - el = page.getByTestId('arc-track'); - await expect.element(el).toBeInTheDocument(); - if (layer === 'svg') { - const d = el.element()?.getAttribute('d'); - expect(d).toBe( - 'M0,-150A150,150,0,1,1,0,150A150,150,0,1,1,0,-150M0,-50A50,50,0,1,0,0,50A50,50,0,1,0,0,-50Z' - ); - } + // outerRadius + it('should render with outerRadius', async () => { + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + value: 50, + outerRadius: 100, + }, }); - // trackOuterRadius - it('should render track with trackOuterRadius', async () => { - render(TestHarness, { - layer, - component: TestComponent, - componentProps: { - value: 50, - track: { 'data-testid': 'arc-track' }, - trackOuterRadius: 100, - }, - }); - - el = page.getByTestId('arc-track'); - await expect.element(el).toBeInTheDocument(); - if (layer === 'svg') { - const d = el.element()?.getAttribute('d'); - expect(d).toBe('M0,-100A100,100,0,1,1,0,100A100,100,0,1,1,0,-100Z'); - } + el = page.getByTestId(componentTestId).first(); + await expect.element(el).toBeInTheDocument(); + const d = el.element()?.getAttribute('d'); + expect(d).toBe('M0,-100A100,100,0,1,1,0,100L0,0Z'); + }); + + it('should render with innerRadius and outerRadius', async () => { + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + value: 75, + innerRadius: 30, + outerRadius: 50, + }, }); - it('should render track with trackInnerRadius and trackOuterRadius', async () => { - render(TestHarness, { - layer, - component: TestComponent, - componentProps: { - value: 50, - track: { 'data-testid': 'arc-track' }, - trackInnerRadius: 20, - trackOuterRadius: 60, - }, - }); - - el = page.getByTestId('arc-track'); - await expect.element(el).toBeInTheDocument(); - if (layer === 'svg') { - const d = el.element()?.getAttribute('d'); - expect(d).toBe( - 'M0,-60A60,60,0,1,1,0,60A60,60,0,1,1,0,-60M0,-20A20,20,0,1,0,0,20A20,20,0,1,0,0,-20Z' - ); - } + el = page.getByTestId(componentTestId).first(); + await expect.element(el).toBeInTheDocument(); + const d = el.element()?.getAttribute('d'); + expect(d).toBe('M0,-50A50,50,0,1,1,-50,0L-30,0A30,30,0,1,0,0,-30Z'); + }); + + // cornerRadius + it('should render with cornerRadius', async () => { + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + value: 50, + cornerRadius: 5, + }, }); - // trackCornerRadius - it('should render track with trackCornerRadius', async () => { - render(TestHarness, { - layer, - component: TestComponent, - componentProps: { - value: 50, - track: { 'data-testid': 'arc-track' }, - trackStartAngle: 0, - trackEndAngle: Math.PI, // half circle to make corner radius visible - trackCornerRadius: 10, - }, - }); - - el = page.getByTestId('arc-track'); - await expect.element(el).toBeInTheDocument(); - if (layer === 'svg') { - const d = el.element()?.getAttribute('d'); - // With corner radius, the path should have additional curve commands - expect(d).toContain('A10,10'); - } + el = page.getByTestId(componentTestId).first(); + await expect.element(el).toBeInTheDocument(); + const d = el.element()?.getAttribute('d'); + expect(d).toBe( + 'M0,-144.914A5,5,0,0,1,5.172,-149.911A150,150,0,0,1,5.172,149.911A5,5,0,0,1,0,144.914L0,0Z' + ); + }); + + // padAngle + it('should render with padAngle', async () => { + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + value: 50, + padAngle: 0.02, + }, }); - // trackPadAngle - it('should render track with trackPadAngle', async () => { - render(TestHarness, { - layer, - component: TestComponent, - componentProps: { - value: 50, - track: { 'data-testid': 'arc-track' }, - trackInnerRadius: 50, - trackStartAngle: 0, - trackEndAngle: Math.PI, // half circle so padAngle has visible effect - trackPadAngle: 0.1, - }, - }); - - el = page.getByTestId('arc-track'); - await expect.element(el).toBeInTheDocument(); - if (layer === 'svg') { - const d = el.element()?.getAttribute('d'); - // With pad angle, the arc path coordinates should be offset from start - expect(d).toContain('A150,150'); - expect(d).toContain('A50,50'); - // The start coordinates should not be exactly at 0,-150 due to padding - expect(d).not.toMatch(/^M0,-150/); - } + el = page.getByTestId(componentTestId).first(); + await expect.element(el).toBeInTheDocument(); + const d = el?.element()?.getAttribute('d'); + expect(d).toBe('M1.5,-149.993A150,150,0,0,1,1.5,149.993L0,0Z'); + }); + + // trackStartAngle + it('should render track with trackStartAngle', async () => { + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + value: 50, + track: { 'data-testid': 'arc-track' }, + trackStartAngle: Math.PI / 2, // 90 degrees + }, }); - // offset - it('should apply offset to arc position', async () => { - render(TestHarness, { - layer, - component: TestComponent, - componentProps: { - value: 50, - offset: 10, - }, - }); - - const el = page.getByTestId(componentTestId); - await expect.element(el).toBeInTheDocument(); - if (layer === 'svg') { - const transform = el.element()?.getAttribute('transform'); - expect(transform).toContain('translate(-4.539904997395462, 8.91006524188368)'); - } + el = page.getByTestId('arc-track'); + await expect.element(el).toBeInTheDocument(); + const d = el.element()?.getAttribute('d'); + // Track starts at 90 degrees (right), ends at default 360 degrees (top) + expect(d).toBe('M150,0A150,150,0,1,1,0,-150L0,0Z'); + }); + + // trackEndAngle + it('should render track with trackEndAngle', async () => { + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + value: 50, + track: { 'data-testid': 'arc-track' }, + trackEndAngle: Math.PI, // 180 degrees + }, }); - it('should apply zero offset by default', async () => { - render(TestHarness, { - layer, - component: TestComponent, - componentProps: { - value: 50, - }, - }); - - const el = page.getByTestId(componentTestId); - await expect.element(el).toBeInTheDocument(); - if (layer === 'svg') { - const transform = el.element()?.getAttribute('transform'); - expect(transform).toBe('translate(0, 0)'); - } + el = page.getByTestId('arc-track'); + await expect.element(el).toBeInTheDocument(); + const d = el.element()?.getAttribute('d'); + // Track ends at 180 degrees + expect(d).toBe('M0,-150A150,150,0,1,1,0,150L0,0Z'); + }); + + it('should render track with trackStartAngle and trackEndAngle', async () => { + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + value: 50, + track: { 'data-testid': 'arc-track' }, + trackStartAngle: 0, + trackEndAngle: Math.PI, + }, }); - // track - it('should render track when track prop is provided', async () => { - render(TestHarness, { - layer, - component: TestComponent, - componentProps: { - value: 50, - track: { 'data-testid': 'arc-track' }, - }, - }); - - el = page.getByTestId('arc-track'); - await expect.element(el).toBeInTheDocument(); - if (layer === 'svg') { - const d = el.element()?.getAttribute('d'); - expect(d).toBe('M0,-150A150,150,0,1,1,0,150A150,150,0,1,1,0,-150Z'); - } + el = page.getByTestId('arc-track'); + await expect.element(el).toBeInTheDocument(); + const d = el.element()?.getAttribute('d'); + expect(d).toBe('M0,-150A150,150,0,1,1,0,150L0,0Z'); + }); + + // trackInnerRadius + it('should render track with trackInnerRadius', async () => { + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + value: 50, + track: { 'data-testid': 'arc-track' }, + trackInnerRadius: 50, + }, }); - it('should render track with custom class', async () => { - render(TestHarness, { - layer, - component: TestComponent, - componentProps: { - value: 50, - track: { class: 'fill-none stroke-surface-content/10', 'data-testid': 'arc-track' }, - }, - }); - - const el = page.getByTestId('arc-track'); - await expect.element(el).toBeInTheDocument(); - if (layer === 'svg') { - await expect.element(el).toHaveClass('fill-none'); - await expect.element(el).toHaveClass('stroke-surface-content/10'); - } + el = page.getByTestId('arc-track'); + await expect.element(el).toBeInTheDocument(); + const d = el.element()?.getAttribute('d'); + expect(d).toBe( + 'M0,-150A150,150,0,1,1,0,150A150,150,0,1,1,0,-150M0,-50A50,50,0,1,0,0,50A50,50,0,1,0,0,-50Z' + ); + }); + + // trackOuterRadius + it('should render track with trackOuterRadius', async () => { + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + value: 50, + track: { 'data-testid': 'arc-track' }, + trackOuterRadius: 100, + }, }); - // tooltipContext + data - it('should call tooltipContext.show on pointer enter with data', async () => { - const mockTooltipContext = { - show: vi.fn(), - hide: vi.fn(), - x: 0, - y: 0, - data: null, - payload: [], - mode: 'manual' as const, - isHoveringTooltipArea: false, - isHoveringTooltipContent: false, - }; - const testData = { label: 'Test', value: 50 }; - - render(TestHarness, { - layer, - component: TestComponent, - componentProps: { - value: 50, - fill: 'blue', - tooltipContext: mockTooltipContext, - data: testData, - }, - }); + el = page.getByTestId('arc-track'); + await expect.element(el).toBeInTheDocument(); + const d = el.element()?.getAttribute('d'); + expect(d).toBe('M0,-100A100,100,0,1,1,0,100A100,100,0,1,1,0,-100Z'); + }); + + it('should render track with trackInnerRadius and trackOuterRadius', async () => { + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + value: 50, + track: { 'data-testid': 'arc-track' }, + trackInnerRadius: 20, + trackOuterRadius: 60, + }, + }); - const el = page.getByTestId(componentTestId); - await expect.element(el).toBeInTheDocument(); - await el.hover(); + el = page.getByTestId('arc-track'); + await expect.element(el).toBeInTheDocument(); + const d = el.element()?.getAttribute('d'); + expect(d).toBe( + 'M0,-60A60,60,0,1,1,0,60A60,60,0,1,1,0,-60M0,-20A20,20,0,1,0,0,20A20,20,0,1,0,0,-20Z' + ); + }); - expect(mockTooltipContext.show).toHaveBeenCalled(); - expect(mockTooltipContext.show.mock.calls[0][1]).toEqual(testData); + // trackCornerRadius + it('should render track with trackCornerRadius', async () => { + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + value: 50, + track: { 'data-testid': 'arc-track' }, + trackStartAngle: 0, + trackEndAngle: Math.PI, // half circle to make corner radius visible + trackCornerRadius: 10, + }, }); - it('should call tooltipContext.hide on pointer leave', async () => { - const mockTooltipContext = { - show: vi.fn(), - hide: vi.fn(), - x: 0, - y: 0, - data: null, - payload: [], - mode: 'manual' as const, - isHoveringTooltipArea: false, - isHoveringTooltipContent: false, - }; - - render(TestHarness, { - layer, - component: TestComponent, - componentProps: { - value: 50, - fill: 'blue', - tooltipContext: mockTooltipContext, - data: { value: 50 }, - }, - }); + el = page.getByTestId('arc-track'); + await expect.element(el).toBeInTheDocument(); + const d = el.element()?.getAttribute('d'); + // With corner radius, the path should have additional curve commands + expect(d).toContain('A10,10'); + }); - const el = page.getByTestId(componentTestId); - await expect.element(el).toBeInTheDocument(); - await el.hover(); - // Move away from the element to trigger pointer leave - await page.getByTestId('test-lc-chart').hover({ position: { x: 0, y: 0 } }); + // trackPadAngle + it('should render track with trackPadAngle', async () => { + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + value: 50, + track: { 'data-testid': 'arc-track' }, + trackInnerRadius: 50, + trackStartAngle: 0, + trackEndAngle: Math.PI, // half circle so padAngle has visible effect + trackPadAngle: 0.1, + }, + }); + + el = page.getByTestId('arc-track'); + await expect.element(el).toBeInTheDocument(); + const d = el.element()?.getAttribute('d'); + // With pad angle, the arc path coordinates should be offset from start + expect(d).toContain('A150,150'); + expect(d).toContain('A50,50'); + // The start coordinates should not be exactly at 0,-150 due to padding + expect(d).not.toMatch(/^M0,-150/); + }); - expect(mockTooltipContext.hide).toHaveBeenCalled(); + // offset + it('should apply offset to arc position', async () => { + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + value: 50, + offset: 10, + }, }); - // fill - it('should apply fill color', async () => { - render(TestHarness, { - layer, - component: TestComponent, - componentProps: { - value: 50, - fill: 'red', - }, - }); + const el = page.getByTestId(componentTestId); + await expect.element(el).toBeInTheDocument(); + const transform = el.element()?.getAttribute('transform'); + expect(transform).toContain('translate(-4.539904997395462, 8.91006524188368)'); + }); - const el = page.getByTestId(componentTestId); - await expect.element(el).toHaveAttribute('fill', 'red'); + it('should apply zero offset by default', async () => { + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + value: 50, + }, }); - // fillOpacity - it('should apply fillOpacity', async () => { - render(TestHarness, { - layer, - component: TestComponent, - componentProps: { - value: 50, - fillOpacity: 0.7, - }, - }); + const el = page.getByTestId(componentTestId); + await expect.element(el).toBeInTheDocument(); + const transform = el.element()?.getAttribute('transform'); + expect(transform).toBe('translate(0, 0)'); + }); - const el = page.getByTestId(componentTestId); - await expect.element(el).toBeInTheDocument(); - await expect.element(el).toHaveAttribute('fill-opacity', '0.7'); + // track + it('should render track when track prop is provided', async () => { + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + value: 50, + track: { 'data-testid': 'arc-track' }, + }, }); - // stroke - it('should have stroke="none" by default', async () => { - render(TestHarness, { - layer, - component: TestComponent, - componentProps: { - value: 50, - }, - }); + el = page.getByTestId('arc-track'); + await expect.element(el).toBeInTheDocument(); + const d = el.element()?.getAttribute('d'); + expect(d).toBe('M0,-150A150,150,0,1,1,0,150A150,150,0,1,1,0,-150Z'); + }); - const el = page.getByTestId(componentTestId); - await expect.element(el).toBeInTheDocument(); - await expect.element(el).toHaveAttribute('stroke', 'none'); + it('should render track with custom class', async () => { + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + value: 50, + track: { class: 'fill-none stroke-surface-content/10', 'data-testid': 'arc-track' }, + }, }); - it('should apply stroke color', async () => { - render(TestHarness, { - layer, - component: TestComponent, - componentProps: { - value: 50, - stroke: 'blue', - }, - }); + const el = page.getByTestId('arc-track'); + await expect.element(el).toBeInTheDocument(); + await expect.element(el).toHaveClass('fill-none'); + await expect.element(el).toHaveClass('stroke-surface-content/10'); + }); - const el = page.getByTestId(componentTestId); - await expect.element(el).toBeInTheDocument(); - await expect.element(el).toHaveAttribute('stroke', 'blue'); + // tooltipContext + data + it('should call tooltipContext.show on pointer enter with data', async () => { + const mockTooltipContext = { + show: vi.fn(), + hide: vi.fn(), + x: 0, + y: 0, + data: null, + payload: [], + mode: 'manual' as const, + isHoveringTooltipArea: false, + isHoveringTooltipContent: false, + }; + const testData = { label: 'Test', value: 50 }; + + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + value: 50, + fill: 'blue', + tooltipContext: mockTooltipContext, + data: testData, + }, }); - // strokeWidth - it('should apply strokeWidth', async () => { - render(TestHarness, { - layer, - component: TestComponent, - componentProps: { - value: 50, - stroke: 'blue', - strokeWidth: 3, - }, - }); + const el = page.getByTestId(componentTestId); + await expect.element(el).toBeInTheDocument(); + await el.hover(); - const el = page.getByTestId(componentTestId); - await expect.element(el).toBeInTheDocument(); - await expect.element(el).toHaveAttribute('stroke-width', '3'); + expect(mockTooltipContext.show).toHaveBeenCalled(); + expect(mockTooltipContext.show.mock.calls[0][1]).toEqual(testData); + }); + + it('should call tooltipContext.hide on pointer leave', async () => { + const mockTooltipContext = { + show: vi.fn(), + hide: vi.fn(), + x: 0, + y: 0, + data: null, + payload: [], + mode: 'manual' as const, + isHoveringTooltipArea: false, + isHoveringTooltipContent: false, + }; + + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + value: 50, + fill: 'blue', + tooltipContext: mockTooltipContext, + data: { value: 50 }, + }, }); - // opacity - it('should apply opacity', async () => { - render(TestHarness, { - layer, - component: TestComponent, - componentProps: { - value: 50, - opacity: 0.5, - }, - }); + const el = page.getByTestId(componentTestId); + await expect.element(el).toBeInTheDocument(); + await el.hover(); + // Move away from the element to trigger pointer leave + await page.getByTestId('test-lc-chart').hover({ position: { x: 0, y: 0 } }); + + expect(mockTooltipContext.hide).toHaveBeenCalled(); + }); - const el = page.getByTestId(componentTestId); - await expect.element(el).toBeInTheDocument(); - await expect.element(el).toHaveAttribute('opacity', '0.5'); + // fill + it('should apply fill color', async () => { + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + value: 50, + fill: 'red', + }, }); - // class - it('should apply custom class', async () => { - render(TestHarness, { - layer, - component: TestComponent, - componentProps: { - value: 50, - class: 'custom-arc-class', - }, - }); + const el = page.getByTestId(componentTestId); + await expect.element(el).toHaveAttribute('fill', 'red'); + }); - const el = page.getByTestId(componentTestId); - await expect.element(el).toBeInTheDocument(); - await expect.element(el).toHaveClass('custom-arc-class'); + // fillOpacity + it('should apply fillOpacity', async () => { + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + value: 50, + fillOpacity: 0.7, + }, }); + + const el = page.getByTestId(componentTestId); + await expect.element(el).toBeInTheDocument(); + await expect.element(el).toHaveAttribute('fill-opacity', '0.7'); }); - }); - /*------------------------------------------------------------------------- - / MOTION TESTS - / ------------------------------------------------------------------------- */ - describe.skipIf(isCanvas)('Motion Tests', () => { - // TODO: not sure how to test this, check before motion applied? - // no prop to control inital wait, may need screenshot testing? - it('should start at initialValue', async () => { + // stroke + it('should have stroke="none" by default', async () => { render(TestHarness, { - layer, - component: TestComponent, + component: Arc, componentProps: { - motion: { type: 'tween', duration: 300 }, - value: 100, - initialValue: 0, + ...defaultProps, + value: 50, }, }); - // Initially should render (motion will animate from initialValue to value) + const el = page.getByTestId(componentTestId); await expect.element(el).toBeInTheDocument(); - if (layer === 'svg') { - const d = el.element()?.getAttribute('d'); - expect(d).toBe('M0,-150L0,0Z'); - } + await expect.element(el).toHaveAttribute('stroke', 'none'); }); - it('should accept spring motion config', async () => { + + it('should apply stroke color', async () => { render(TestHarness, { - layer, - component: TestComponent, + component: Arc, componentProps: { + ...defaultProps, value: 50, - initialValue: 0, - motion: { type: 'spring', stiffness: 100, damping: 10 }, + stroke: 'blue', }, }); const el = page.getByTestId(componentTestId); await expect.element(el).toBeInTheDocument(); + await expect.element(el).toHaveAttribute('stroke', 'blue'); + }); - if (layer === 'svg') { - // Initial state (value=0) - just a line - const initialD = el.element()?.getAttribute('d'); - expect(initialD).toBe('M0,-150L0,0Z'); + // strokeWidth + it('should apply strokeWidth', async () => { + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + value: 50, + stroke: 'blue', + strokeWidth: 3, + }, + }); - // Wait for spring animation to settle at final state (value=50) - await expect - .poll(() => el.element()?.getAttribute('d'), { timeout: 3000 }) - .toBe('M0,-150A150,150,0,1,1,0,150L0,0Z'); - } + const el = page.getByTestId(componentTestId); + await expect.element(el).toBeInTheDocument(); + await expect.element(el).toHaveAttribute('stroke-width', '3'); }); - it('should accept tween motion config', async () => { + + // opacity + it('should apply opacity', async () => { render(TestHarness, { - layer, - component: TestComponent, + component: Arc, componentProps: { + ...defaultProps, value: 50, - initialValue: 0, - motion: { type: 'tween', duration: 300 }, + opacity: 0.5, }, }); const el = page.getByTestId(componentTestId); await expect.element(el).toBeInTheDocument(); + await expect.element(el).toHaveAttribute('opacity', '0.5'); + }); - if (layer === 'svg') { - // Initial state (value=0) - just a line - const initialD = el.element()?.getAttribute('d'); - expect(initialD).toBe('M0,-150L0,0Z'); + // class + it('should apply custom class', async () => { + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + value: 50, + class: 'custom-arc-class', + }, + }); - // Wait for tween animation to complete at final state (value=50) - await expect - .poll(() => el.element()?.getAttribute('d'), { timeout: 1000 }) - .toBe('M0,-150A150,150,0,1,1,0,150L0,0Z'); - } + const el = page.getByTestId(componentTestId); + await expect.element(el).toBeInTheDocument(); + await expect.element(el).toHaveClass('custom-arc-class'); }); }); - /*------------------------------------------------------------------------- - / EVENT TESTS - / ------------------------------------------------------------------------- */ - describe.skipIf(isCanvas)('Event Handling', () => { + describe('events', () => { it('should handle pointer enter events', async () => { const onPointerEnter = vi.fn(); render(TestHarness, { - layer, - component: TestComponent, + component: Arc, componentProps: { + ...defaultProps, value: 50, onpointerenter: onPointerEnter, fill: 'blue', @@ -876,9 +692,9 @@ for (const layer of supportedLayers) { it('should handle pointer move events', async () => { const onPointerMove = vi.fn(); render(TestHarness, { - layer, - component: TestComponent, + component: Arc, componentProps: { + ...defaultProps, value: 50, onpointermove: onPointerMove, fill: 'blue', @@ -894,9 +710,9 @@ for (const layer of supportedLayers) { it('should handle touch move events', async () => { const onTouchMove = vi.fn(); render(TestHarness, { - layer, - component: TestComponent, + component: Arc, componentProps: { + ...defaultProps, value: 50, ontouchmove: onTouchMove, }, @@ -910,32 +726,27 @@ for (const layer of supportedLayers) { }); }); - /* ------------------------------------------------------------ - / Edge Cases - / ------------------------------------------------------------ */ - describe.skipIf(isCanvas)('Edge Cases', () => { + describe('edge cases', () => { it('should handle value of 0', async () => { render(TestHarness, { - layer, - component: TestComponent, + component: Arc, componentProps: { + ...defaultProps, value: 0, }, }); const el = page.getByTestId(componentTestId); await expect.element(el).toBeInTheDocument(); - if (layer === 'svg') { - const d = el.element()?.getAttribute('d'); - expect(d).toBe('M0,-150L0,0Z'); - } + const d = el.element()?.getAttribute('d'); + expect(d).toBe('M0,-150L0,0Z'); }); it('should handle value at max domain', async () => { render(TestHarness, { - layer, - component: TestComponent, + component: Arc, componentProps: { + ...defaultProps, value: 100, domain: [0, 100] as [number, number], }, @@ -943,17 +754,15 @@ for (const layer of supportedLayers) { const el = page.getByTestId(componentTestId); await expect.element(el).toBeInTheDocument(); - if (layer === 'svg') { - const d = el.element()?.getAttribute('d'); - expect(d).toBe('M0,-150A150,150,0,1,1,0,150A150,150,0,1,1,0,-150Z'); - } + const d = el.element()?.getAttribute('d'); + expect(d).toBe('M0,-150A150,150,0,1,1,0,150A150,150,0,1,1,0,-150Z'); }); it('should handle negative domain values', async () => { render(TestHarness, { - layer, - component: TestComponent, + component: Arc, componentProps: { + ...defaultProps, value: 0, domain: [-50, 50] as [number, number], }, @@ -961,17 +770,15 @@ for (const layer of supportedLayers) { const el = page.getByTestId(componentTestId); await expect.element(el).toBeInTheDocument(); - if (layer === 'svg') { - const d = el.element()?.getAttribute('d'); - expect(d).toBe('M0,-150A150,150,0,1,1,0,150L0,0Z'); - } + const d = el.element()?.getAttribute('d'); + expect(d).toBe('M0,-150A150,150,0,1,1,0,150L0,0Z'); }); it('should handle innerRadius of 0 (pie slice)', async () => { render(TestHarness, { - layer, - component: TestComponent, + component: Arc, componentProps: { + ...defaultProps, value: 50, innerRadius: 0, }, @@ -979,17 +786,15 @@ for (const layer of supportedLayers) { const el = page.getByTestId(componentTestId); await expect.element(el).toBeInTheDocument(); - if (layer === 'svg') { - const d = el.element()?.getAttribute('d'); - expect(d).toBe('M0,-150A150,150,0,1,1,0,150L0,0Z'); - } + const d = el.element()?.getAttribute('d'); + expect(d).toBe('M0,-150A150,150,0,1,1,0,150L0,0Z'); }); it('should handle full circle (360 degree range)', async () => { render(TestHarness, { - layer, - component: TestComponent, + component: Arc, componentProps: { + ...defaultProps, value: 100, domain: [0, 100] as [number, number], range: [0, 360] as [number, number], @@ -998,17 +803,15 @@ for (const layer of supportedLayers) { const el = page.getByTestId(componentTestId); await expect.element(el).toBeInTheDocument(); - if (layer === 'svg') { - const d = el.element()?.getAttribute('d'); - expect(d).toBe('M0,-150A150,150,0,1,1,0,150A150,150,0,1,1,0,-150Z'); - } + const d = el.element()?.getAttribute('d'); + expect(d).toBe('M0,-150A150,150,0,1,1,0,150A150,150,0,1,1,0,-150Z'); }); it('should handle partial arc (e.g., 180 degrees)', async () => { render(TestHarness, { - layer, - component: TestComponent, + component: Arc, componentProps: { + ...defaultProps, value: 100, domain: [0, 100] as [number, number], range: [0, 180] as [number, number], @@ -1017,17 +820,15 @@ for (const layer of supportedLayers) { const el = page.getByTestId(componentTestId); await expect.element(el).toBeInTheDocument(); - if (layer === 'svg') { - const d = el.element()?.getAttribute('d'); - expect(d).toBe('M0,-150A150,150,0,1,1,0,150L0,0Z'); - } + const d = el.element()?.getAttribute('d'); + expect(d).toBe('M0,-150A150,150,0,1,1,0,150L0,0Z'); }); it('should handle value exceeding domain max', async () => { render(TestHarness, { - layer, - component: TestComponent, + component: Arc, componentProps: { + ...defaultProps, value: 150, domain: [0, 100] as [number, number], }, @@ -1035,17 +836,15 @@ for (const layer of supportedLayers) { const el = page.getByTestId(componentTestId); await expect.element(el).toBeInTheDocument(); - if (layer === 'svg') { - const d = el.element()?.getAttribute('d'); - expect(d).toBe('M0,-150A150,150,0,1,1,0,150A150,150,0,1,1,0,-150Z'); - } + const d = el.element()?.getAttribute('d'); + expect(d).toBe('M0,-150A150,150,0,1,1,0,150A150,150,0,1,1,0,-150Z'); }); it('should handle value below domain min', async () => { render(TestHarness, { - layer, - component: TestComponent, + component: Arc, componentProps: { + ...defaultProps, value: -10, domain: [0, 100] as [number, number], }, @@ -1053,26 +852,22 @@ for (const layer of supportedLayers) { const el = page.getByTestId(componentTestId); await expect.element(el).toBeInTheDocument(); - if (layer === 'svg') { - const d = el.element()?.getAttribute('d'); - expect(d).toBe('M0,-150A150,150,0,0,0,-88.168,-121.353L0,0Z'); - } + const d = el.element()?.getAttribute('d'); + expect(d).toBe('M0,-150A150,150,0,0,0,-88.168,-121.353L0,0Z'); }); }); - /*------------------------------------------------------------------------- - / Child Snippet Tests - / ------------------------------------------------------------------------- */ - describe('Snippets Renderings', () => { + describe('snippets', () => { it('should render text inner, middle, outer Text children', async () => { render(TestHarness, { chartProps: { height: 400, padding: '50', }, - layer, - component: TestComponent, + + component: Arc, componentProps: { + ...defaultProps, fill: 'blue', value: 50, innerRadius: 70, @@ -1121,4 +916,73 @@ for (const layer of supportedLayers) { await expect.element(outer).toHaveTextContent('Outer'); }); }); -} + + describe.skip('motion', () => { + // TODO: not sure how to test this, check before motion applied? + // no prop to control inital wait, may need screenshot testing? + it('should start at initialValue', async () => { + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + motion: { type: 'tween', duration: 300 }, + value: 100, + initialValue: 0, + }, + }); + // Initially should render (motion will animate from initialValue to value) + const el = page.getByTestId(componentTestId); + await expect.element(el).toBeInTheDocument(); + const d = el.element()?.getAttribute('d'); + expect(d).toBe('M0,-150L0,0Z'); + }); + + it('should accept spring motion config', async () => { + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + value: 50, + initialValue: 0, + motion: { type: 'spring', stiffness: 100, damping: 10 }, + }, + }); + + const el = page.getByTestId(componentTestId); + await expect.element(el).toBeInTheDocument(); + + // Initial state (value=0) - just a line + const initialD = el.element()?.getAttribute('d'); + expect(initialD).toBe('M0,-150L0,0Z'); + + // Wait for spring animation to settle at final state (value=50) + await expect + .poll(() => el.element()?.getAttribute('d'), { timeout: 3000 }) + .toBe('M0,-150A150,150,0,1,1,0,150L0,0Z'); + }); + + it('should accept tween motion config', async () => { + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + value: 50, + initialValue: 0, + motion: { type: 'tween', duration: 300 }, + }, + }); + + const el = page.getByTestId(componentTestId); + await expect.element(el).toBeInTheDocument(); + + // Initial state (value=0) - just a line + const initialD = el.element()?.getAttribute('d'); + expect(initialD).toBe('M0,-150L0,0Z'); + + // Wait for tween animation to complete at final state (value=50) + await expect + .poll(() => el.element()?.getAttribute('d'), { timeout: 1000 }) + .toBe('M0,-150A150,150,0,1,1,0,150L0,0Z'); + }); + }); +}); diff --git a/packages/layerchart/src/lib/components/tests/TestHarness.svelte b/packages/layerchart/src/lib/components/tests/TestHarness.svelte index 6e4ca9e04..51245c29d 100644 --- a/packages/layerchart/src/lib/components/tests/TestHarness.svelte +++ b/packages/layerchart/src/lib/components/tests/TestHarness.svelte @@ -1,3 +1,8 @@ + + {#snippet Component()} - + {#snippet children(snippetProps: any)} {#each childComponents as child} @@ -61,7 +66,7 @@ {/snippet} {#if useChart} - + {@render Component()} From c5fc431ef4a106b40c6cfbf7c0859db987583e73 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Mon, 26 Jan 2026 11:03:30 -0500 Subject: [PATCH 4/4] Change `pnpm test:unit` to be headless/no ui, and add `pnpm test:ui` --- packages/layerchart/package.json | 1 + packages/layerchart/vite.config.js | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/layerchart/package.json b/packages/layerchart/package.json index 7d8155fa5..fde6d6c77 100644 --- a/packages/layerchart/package.json +++ b/packages/layerchart/package.json @@ -17,6 +17,7 @@ "check": "svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-check --tsconfig ./tsconfig.json --watch", "test:unit": "TZ=UTC-5 vitest", + "test:ui": "TZ=UTC-5 vitest --ui", "lint": "prettier --check .", "format": "prettier --write .", "prepare": "svelte-kit sync" diff --git a/packages/layerchart/vite.config.js b/packages/layerchart/vite.config.js index 4d78f4ccb..9f9d0dea0 100644 --- a/packages/layerchart/vite.config.js +++ b/packages/layerchart/vite.config.js @@ -67,17 +67,16 @@ const config = defineConfig({ test: { name: 'client', testTimeout: 5000, - retry: 3, + retry: 1, browser: { enabled: true, provider: playwright(), - // Multiple browser instances for better performance - // Uses single Vite server with shared caching instances: [ { browser: 'chromium' }, // { browser: 'firefox' }, // { browser: 'webkit' }, ], + headless: true, }, include: ['src/**/*.svelte.{test,spec}.{js,ts}'], exclude: ['src/lib/server/**', 'src/**/*.ssr.{test,spec}.{js,ts}'],