Skip to content

Commit e017bf5

Browse files
ljodeaclaude
andauthored
chore: add unit tests for Vector/Spike marks (#171) (#381)
## Summary - Adds 8 unit tests for the `Vector` mark covering path rendering, shape geometry, transforms, styling (stroke/fill), all 3 shape variants (arrow, spike, arrow-filled), anchor behavior, and empty data - Adds 5 unit tests for the `Spike` mark covering path rendering, default spike shape, default/custom stroke styling, and empty data - Follows existing `.test.svelte` wrapper + `.test.svelte.ts` pattern Replaces #380 (which included a duplicate Bollinger commit from the parent branch). Closes #171 ## Test plan - [x] `bun run test src/tests/vector.test.svelte.ts src/tests/spike.test.svelte.ts` — 13 tests pass - [x] `bun run test` — full suite passes (480 tests, no regressions) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7f3d3df commit e017bf5

File tree

4 files changed

+212
-0
lines changed

4 files changed

+212
-0
lines changed

src/tests/spike.test.svelte

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<script lang="ts">
2+
import { Spike, Plot } from '$lib/index.js';
3+
import type { ComponentProps } from 'svelte';
4+
let args: ComponentProps<typeof Spike> = $props();
5+
</script>
6+
7+
<Plot width={100} height={100} axes={false}>
8+
<Spike {...args} />
9+
</Plot>

src/tests/spike.test.svelte.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { render } from '@testing-library/svelte';
3+
import SpikeTest from './spike.test.svelte';
4+
5+
const data = [
6+
{ x: 10, y: 20, len: 15 },
7+
{ x: 50, y: 60, len: 25 },
8+
{ x: 80, y: 40, len: 10 }
9+
];
10+
11+
describe('Spike mark', () => {
12+
it('renders one path per datum', () => {
13+
const { container } = render(SpikeTest, {
14+
props: { data, x: 'x', y: 'y', length: 'len' }
15+
});
16+
17+
const paths = container.querySelectorAll('g.vector path');
18+
expect(paths.length).toBe(data.length);
19+
});
20+
21+
it('uses spike shape by default', () => {
22+
const { container } = render(SpikeTest, {
23+
props: { data, x: 'x', y: 'y', length: 'len' }
24+
});
25+
26+
const paths = container.querySelectorAll('g.vector path');
27+
for (const path of paths) {
28+
const d = path.getAttribute('d');
29+
expect(d).toBeTruthy();
30+
expect(d).toContain('M');
31+
expect(d).toContain('L');
32+
}
33+
});
34+
35+
it('applies default stroke', () => {
36+
const { container } = render(SpikeTest, {
37+
props: { data, x: 'x', y: 'y', length: 'len' }
38+
});
39+
40+
const paths = container.querySelectorAll('g.vector path');
41+
expect(paths.length).toBe(data.length);
42+
for (const path of paths) {
43+
const style = (path as SVGElement).style;
44+
expect(style.stroke).toBeTruthy();
45+
}
46+
});
47+
48+
it('applies custom stroke override', () => {
49+
const { container } = render(SpikeTest, {
50+
props: { data, x: 'x', y: 'y', length: 'len', stroke: 'green' }
51+
});
52+
53+
const paths = container.querySelectorAll('g.vector path');
54+
expect(paths.length).toBe(data.length);
55+
for (const path of paths) {
56+
expect((path as SVGElement).style.stroke).toBe('green');
57+
}
58+
});
59+
60+
it('handles empty data', () => {
61+
const { container } = render(SpikeTest, {
62+
props: { data: [], x: 'x', y: 'y' }
63+
});
64+
65+
const paths = container.querySelectorAll('g.vector path');
66+
expect(paths.length).toBe(0);
67+
});
68+
});

src/tests/vector.test.svelte

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<script lang="ts">
2+
import { Vector, Plot } from '$lib/index.js';
3+
import type { ComponentProps } from 'svelte';
4+
let args: ComponentProps<typeof Vector> = $props();
5+
</script>
6+
7+
<Plot width={100} height={100} axes={false}>
8+
<Vector {...args} />
9+
</Plot>

src/tests/vector.test.svelte.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { render } from '@testing-library/svelte';
3+
import VectorTest from './vector.test.svelte';
4+
5+
const data = [
6+
{ x: 10, y: 20, len: 15, angle: 45 },
7+
{ x: 50, y: 60, len: 25, angle: 90 },
8+
{ x: 80, y: 40, len: 10, angle: 180 }
9+
];
10+
11+
describe('Vector mark', () => {
12+
it('renders one path per datum', () => {
13+
const { container } = render(VectorTest, {
14+
props: { data, x: 'x', y: 'y', length: 'len', rotate: 'angle' }
15+
});
16+
17+
const paths = container.querySelectorAll('g.vector path');
18+
expect(paths.length).toBe(data.length);
19+
});
20+
21+
it('path has valid shape geometry', () => {
22+
const { container } = render(VectorTest, {
23+
props: { data, x: 'x', y: 'y', length: 'len', rotate: 'angle' }
24+
});
25+
26+
const paths = container.querySelectorAll('g.vector path');
27+
for (const path of paths) {
28+
const d = path.getAttribute('d');
29+
expect(d).toContain('M');
30+
expect(d).toContain('L');
31+
}
32+
});
33+
34+
it('path has translate transform', () => {
35+
const { container } = render(VectorTest, {
36+
props: { data, x: 'x', y: 'y', length: 'len', rotate: 'angle' }
37+
});
38+
39+
const paths = container.querySelectorAll('g.vector path');
40+
for (const path of paths) {
41+
const transform = path.getAttribute('transform');
42+
expect(transform).toMatch(/translate\(/);
43+
}
44+
});
45+
46+
it('applies custom stroke', () => {
47+
const { container } = render(VectorTest, {
48+
props: { data, x: 'x', y: 'y', length: 'len', rotate: 'angle', stroke: 'red' }
49+
});
50+
51+
const paths = container.querySelectorAll('g.vector path');
52+
expect(paths.length).toBe(data.length);
53+
for (const path of paths) {
54+
expect((path as SVGElement).style.stroke).toBe('red');
55+
}
56+
});
57+
58+
it('renders spike shape', () => {
59+
const { container } = render(VectorTest, {
60+
props: { data, x: 'x', y: 'y', length: 'len', shape: 'spike' }
61+
});
62+
63+
const paths = container.querySelectorAll('g.vector path');
64+
expect(paths.length).toBe(data.length);
65+
for (const path of paths) {
66+
const d = path.getAttribute('d');
67+
expect(d).toBeTruthy();
68+
expect(d).toContain('M');
69+
}
70+
});
71+
72+
it('renders arrow-filled shape with fill styling', () => {
73+
const { container } = render(VectorTest, {
74+
props: {
75+
data,
76+
x: 'x',
77+
y: 'y',
78+
length: 'len',
79+
shape: 'arrow-filled',
80+
fill: 'blue'
81+
}
82+
});
83+
84+
const paths = container.querySelectorAll('g.vector path');
85+
expect(paths.length).toBe(data.length);
86+
for (const path of paths) {
87+
expect((path as SVGElement).style.fill).toBe('blue');
88+
}
89+
});
90+
91+
it('respects anchor option', () => {
92+
const { container: startContainer } = render(VectorTest, {
93+
props: { data, x: 'x', y: 'y', length: 'len', anchor: 'start' }
94+
});
95+
96+
const { container: middleContainer } = render(VectorTest, {
97+
props: { data, x: 'x', y: 'y', length: 'len', anchor: 'middle' }
98+
});
99+
100+
const startPaths = startContainer.querySelectorAll('g.vector path');
101+
const middlePaths = middleContainer.querySelectorAll('g.vector path');
102+
103+
// anchor: 'start' should NOT have the extra translate offset that 'middle' adds
104+
for (const path of startPaths) {
105+
const transform = path.getAttribute('transform')!;
106+
// 'start' produces: translate(x,y) rotate(deg) with no trailing translate
107+
const parts = transform.split(') ').filter((p) => p.startsWith('translate('));
108+
expect(parts.length).toBe(1); // only the positional translate
109+
}
110+
111+
for (const path of middlePaths) {
112+
const transform = path.getAttribute('transform')!;
113+
// 'middle' produces: translate(x,y) rotate(deg) translate(0, length/2)
114+
expect(transform).toMatch(/translate\(0,/);
115+
}
116+
});
117+
118+
it('handles empty data', () => {
119+
const { container } = render(VectorTest, {
120+
props: { data: [], x: 'x', y: 'y' }
121+
});
122+
123+
const paths = container.querySelectorAll('g.vector path');
124+
expect(paths.length).toBe(0);
125+
});
126+
});

0 commit comments

Comments
 (0)