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/src/lib/components/Arc.svelte.test.ts b/packages/layerchart/src/lib/components/Arc.svelte.test.ts index bafb4658c..ce780932a 100644 --- a/packages/layerchart/src/lib/components/Arc.svelte.test.ts +++ b/packages/layerchart/src/lib/components/Arc.svelte.test.ts @@ -1,662 +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(); - } - }); - - it('should render Chart element', async () => { - render(TestHarness, { - layer, - component: TestComponent, - componentProps: { - track: { class: 'fill-none stroke-surface-content/10' }, - value: 50, - }, - }); +describe(`Arc`, () => { + it('should render Arc element', async () => { + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + 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' }, + }, + }); + + el = page.getByTestId('arc-track'); + 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, + }, }); - 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(componentTestId); - await expect.element(el).toBeInTheDocument(); - }); - }); - - 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(); - }); - } - }); - - /*------------------------------------------------------------------------- - / PROPS TESTS - / ------------------------------------------------------------------------- */ - describe.skipIf(layer === 'canvas')('Checking Props', () => { - 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(); - const element = el.element() as SVGPathElement; - if (layer === 'svg') { - const d = element.getAttribute('d'); - expect(d).toBe('M0,-150A150,150,0,1,1,0,150L0,0Z'); - } - }); - - 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(); - const element = el.element() as SVGPathElement; - if (layer === 'svg') { - const d = element.getAttribute('d'); - expect(d).toBe('M0,-150A150,150,0,0,1,150,0L0,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(); - const element = el.element() as SVGPathElement; - if (layer === 'svg') { - const d = element.getAttribute('d'); - expect(d).toBe('M0,-50A50,50,0,1,1,-50,0L-30,0A30,30,0,1,0,0,-30Z'); - } - }); - - 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(); - const element = el.element() as SVGPathElement; - if (layer === 'svg') { - const d = 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' - ); - } - }); - - 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 track when track prop is true', 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'); - } - }); - - it('should render track with custom props', 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'); - } - }); - - it('should support custom track radius values', async () => { - render(TestHarness, { - layer, - component: TestComponent, - componentProps: { - value: 50, - track: { class: 'fill-none stroke-surface-content/10', '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' - ); - } - }); - - it('should support custom track angles', async () => { - render(TestHarness, { - layer, - component: TestComponent, - componentProps: { - value: 50, - track: { class: 'fill-none stroke-surface-content/10', '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'); - } - }); - - // 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'); - // } - // }); - - it('should apply fill color', async () => { - render(TestHarness, { - layer, - component: TestComponent, - componentProps: { - value: 50, - fill: 'red', - }, - }); + 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'); + }); - const el = page.getByTestId(componentTestId); - await expect.element(el).toHaveAttribute('fill', 'red'); + // domain + it('should render with custom domain', async () => { + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + value: 50, + domain: [0, 200] as [number, number], + }, }); - it('should apply opacity', async () => { - render(TestHarness, { - layer, - component: TestComponent, - componentProps: { - value: 50, - opacity: 0.5, - }, - }); + 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'); + }); - const el = page.getByTestId(componentTestId); - await expect.element(el).toBeInTheDocument(); - await expect.element(el).toHaveAttribute('opacity', '0.5'); + // range + it('should render with custom range', async () => { + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + value: 50, + range: [0, 180] as [number, number], + }, }); - it('should apply fillOpacity', async () => { - render(TestHarness, { - layer, - component: TestComponent, - componentProps: { - value: 50, - fillOpacity: 0.7, - }, - }); + 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'); + }); - const el = page.getByTestId(componentTestId); - await expect.element(el).toBeInTheDocument(); - await expect.element(el).toHaveAttribute('fill-opacity', '0.7'); + 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], + }, }); - 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).toHaveClass('custom-arc-class'); - }); - - // 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 () => { - render(TestHarness, { - layer, - component: TestComponent, - componentProps: { - value: 50, - range: [0, 180] 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,0,1,150,0L0,0Z'); - } - }); - - it('should scale value to angle based on domain and range', 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'); - } - }); - - 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 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)'); - } - }); - - 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)'); - } - }); - - it('should have stroke="none" by default', async () => { - render(TestHarness, { - layer, - component: TestComponent, - componentProps: { - value: 50, - }, - }); + 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'); + }); - const el = page.getByTestId(componentTestId); - await expect.element(el).toBeInTheDocument(); - await expect.element(el).toHaveAttribute('stroke', 'none'); + it('should handle custom start angle in range', async () => { + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + value: 50, + range: [90, 270] as [number, number], + }, }); + + 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'); }); - }); - /*------------------------------------------------------------------------- - / 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 () => { + // startAngle + it('should render with startAngle in radians', async () => { render(TestHarness, { - layer, - component: TestComponent, + component: Arc, componentProps: { - motion: { type: 'tween', duration: 300 }, - value: 100, - initialValue: 0, + ...defaultProps, + value: 50, + startAngle: Math.PI / 2, // 90 degrees }, }); - // Initially should render (motion will animate from initialValue to value) + + 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'); + }); + + // 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 + }, + }); + + 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'); + }); + + 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 + }, + }); + + 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'); + }); + + // innerRadius + it('should render with innerRadius', async () => { + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + value: 50, + innerRadius: 50, + }, + }); + + 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'); + }); + + // outerRadius + it('should render with outerRadius', async () => { + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + value: 50, + outerRadius: 100, + }, + }); + + 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, + }, + }); + + 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, + }, + }); + + 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, + }, + }); + + 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 + }, + }); + + 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 + }, + }); + + 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, + }, + }); + + 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, + }, + }); + + 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, + }, + }); + + 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, + }, + }); + + 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' + ); + }); + + // 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, + }, + }); + + 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'); + }); + + // 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/); + }); + + // offset + it('should apply offset to arc position', async () => { + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + value: 50, + offset: 10, + }, + }); + 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'); - } - }); - // 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'); - // } - // }); + const transform = el.element()?.getAttribute('transform'); + expect(transform).toContain('translate(-4.539904997395462, 8.91006524188368)'); + }); + + it('should apply zero offset by default', async () => { + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + value: 50, + }, + }); + + const el = page.getByTestId(componentTestId); + await expect.element(el).toBeInTheDocument(); + const transform = el.element()?.getAttribute('transform'); + expect(transform).toBe('translate(0, 0)'); + }); + + // track + it('should render track when track prop is provided', async () => { + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + value: 50, + track: { 'data-testid': 'arc-track' }, + }, + }); + + 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'); + }); + + 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' }, + }, + }); + + 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'); + }); + + // 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, + }, + }); + + const el = page.getByTestId(componentTestId); + await expect.element(el).toBeInTheDocument(); + await el.hover(); + + 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 }, + }, + }); + + 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(); + }); + + // fill + it('should apply fill color', async () => { + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + value: 50, + fill: 'red', + }, + }); + + const el = page.getByTestId(componentTestId); + await expect.element(el).toHaveAttribute('fill', 'red'); + }); + + // 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'); + }); + + // stroke + it('should have stroke="none" by default', async () => { + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + value: 50, + }, + }); + + const el = page.getByTestId(componentTestId); + await expect.element(el).toBeInTheDocument(); + await expect.element(el).toHaveAttribute('stroke', 'none'); + }); + + it('should apply stroke color', async () => { + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + value: 50, + stroke: 'blue', + }, + }); + + const el = page.getByTestId(componentTestId); + await expect.element(el).toBeInTheDocument(); + await expect.element(el).toHaveAttribute('stroke', 'blue'); + }); + + // strokeWidth + it('should apply strokeWidth', async () => { + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + value: 50, + stroke: 'blue', + strokeWidth: 3, + }, + }); + + const el = page.getByTestId(componentTestId); + await expect.element(el).toBeInTheDocument(); + await expect.element(el).toHaveAttribute('stroke-width', '3'); + }); + + // opacity + it('should apply opacity', async () => { + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + value: 50, + opacity: 0.5, + }, + }); + + const el = page.getByTestId(componentTestId); + await expect.element(el).toBeInTheDocument(); + await expect.element(el).toHaveAttribute('opacity', '0.5'); + }); + + // class + it('should apply custom class', async () => { + render(TestHarness, { + component: Arc, + componentProps: { + ...defaultProps, + value: 50, + class: 'custom-arc-class', + }, + }); + + 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', @@ -672,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', @@ -690,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, }, @@ -706,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], }, @@ -739,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], }, @@ -757,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, }, @@ -775,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], @@ -794,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], @@ -813,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], }, @@ -831,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], }, @@ -849,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, @@ -917,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()} 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}'],