Skip to content

Commit d1c64b2

Browse files
authored
Merge branch 'main' into fix/resolve-svelte-check-issues-2
2 parents 165cb84 + f176a2a commit d1c64b2

File tree

2 files changed

+189
-2
lines changed

2 files changed

+189
-2
lines changed

src/lib/helpers/facets.test.ts

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { getEmptyFacets } from './facets.js';
3+
import type { GenericMarkOptions, Mark } from '../types/index.js';
4+
5+
/**
6+
* Creates a minimal mock mark for testing getEmptyFacets.
7+
* Only populates the fields that getEmptyFacets actually accesses:
8+
* - options.__firstFacet, options.automatic, options.fx, options.fy
9+
* - data (array of plain objects with fx/fy keys)
10+
*/
11+
function mockMark(
12+
opts: {
13+
data?: Record<string, unknown>[];
14+
fx?: string;
15+
fy?: string;
16+
__firstFacet?: boolean;
17+
automatic?: boolean;
18+
} = {}
19+
): Mark<GenericMarkOptions> {
20+
const { data = [], fx, fy, __firstFacet = true, automatic = false } = opts;
21+
return {
22+
id: Symbol('test-mark'),
23+
type: 'dot',
24+
channels: [],
25+
scales: new Set(),
26+
data,
27+
options: { __firstFacet, automatic, fx, fy }
28+
} as unknown as Mark<GenericMarkOptions>;
29+
}
30+
31+
describe('getEmptyFacets', () => {
32+
it('returns all false (non-empty) when no marks are provided', () => {
33+
const result = getEmptyFacets([], ['A', 'B'], ['X', 'Y']);
34+
// With no marks, no facetted marks exist, so hasFacettedData starts
35+
// as false for 2D facets — meaning all combinations are "empty"
36+
for (const [, fyMap] of result) {
37+
for (const [, isEmpty] of fyMap) {
38+
expect(isEmpty).toBe(true);
39+
}
40+
}
41+
});
42+
43+
it('marks all facets non-empty for single fx dimension with full coverage', () => {
44+
const mark = mockMark({
45+
fx: 'fx',
46+
data: [{ fx: 'A' }, { fx: 'B' }]
47+
});
48+
// When fyValues has exactly 1 entry, hasFacettedData starts as true
49+
const result = getEmptyFacets([mark], ['A', 'B'], [true]);
50+
expect(result.get('A')?.get(true)).toBe(false);
51+
expect(result.get('B')?.get(true)).toBe(false);
52+
});
53+
54+
it('detects empty facets when fx+fy combination has no data', () => {
55+
const mark = mockMark({
56+
fx: 'fx',
57+
fy: 'fy',
58+
data: [
59+
{ fx: 'A', fy: 'X' },
60+
{ fx: 'B', fy: 'Y' }
61+
]
62+
});
63+
const result = getEmptyFacets([mark], ['A', 'B'], ['X', 'Y']);
64+
// A-X and B-Y have data
65+
expect(result.get('A')?.get('X')).toBe(false);
66+
expect(result.get('B')?.get('Y')).toBe(false);
67+
// A-Y and B-X have no data → empty
68+
expect(result.get('A')?.get('Y')).toBe(true);
69+
expect(result.get('B')?.get('X')).toBe(true);
70+
});
71+
72+
it('filters out automatic marks', () => {
73+
const mark = mockMark({
74+
fx: 'fx',
75+
fy: 'fy',
76+
automatic: true,
77+
data: [{ fx: 'A', fy: 'X' }]
78+
});
79+
const result = getEmptyFacets([mark], ['A', 'B'], ['X', 'Y']);
80+
// Automatic mark is excluded → all 2D combos are empty
81+
for (const [, fyMap] of result) {
82+
for (const [, isEmpty] of fyMap) {
83+
expect(isEmpty).toBe(true);
84+
}
85+
}
86+
});
87+
88+
it('filters out marks with empty data', () => {
89+
const mark = mockMark({
90+
fx: 'fx',
91+
fy: 'fy',
92+
data: []
93+
});
94+
const result = getEmptyFacets([mark], ['A'], ['X']);
95+
// Mark has empty data → filtered out, but single-dimension so non-empty
96+
expect(result.get('A')?.get('X')).toBe(false);
97+
});
98+
99+
it('filters out marks without fx channel when fxValues has multiple entries', () => {
100+
// Mark has no fx accessor but there are multiple fx facets
101+
const mark = mockMark({
102+
fy: 'fy',
103+
data: [{ fy: 'X' }]
104+
});
105+
const result = getEmptyFacets([mark], ['A', 'B'], ['X']);
106+
// Mark is filtered out (fx is null, fxValues.length > 1)
107+
// But since fyValues has 1 entry, hasFacettedData starts as true
108+
expect(result.get('A')?.get('X')).toBe(false);
109+
expect(result.get('B')?.get('X')).toBe(false);
110+
});
111+
112+
it('filters out marks without fy channel when fyValues has multiple entries', () => {
113+
const mark = mockMark({
114+
fx: 'fx',
115+
data: [{ fx: 'A' }]
116+
});
117+
const result = getEmptyFacets([mark], ['A'], ['X', 'Y']);
118+
// Mark has no fy accessor, fyValues.length > 1 → filtered out
119+
// fxValues has 1 entry, so hasFacettedData starts as true
120+
expect(result.get('A')?.get('X')).toBe(false);
121+
expect(result.get('A')?.get('Y')).toBe(false);
122+
});
123+
124+
it('returns correct structure: outer map keyed by fx, inner by fy', () => {
125+
const result = getEmptyFacets([], ['A', 'B'], ['X', 'Y']);
126+
expect(result.size).toBe(2);
127+
expect(result.get('A')?.size).toBe(2);
128+
expect(result.get('B')?.size).toBe(2);
129+
expect(result.has('A')).toBe(true);
130+
expect(result.has('B')).toBe(true);
131+
expect(result.get('A')?.has('X')).toBe(true);
132+
expect(result.get('A')?.has('Y')).toBe(true);
133+
});
134+
135+
it('handles marks without __firstFacet flag', () => {
136+
const mark = mockMark({
137+
fx: 'fx',
138+
fy: 'fy',
139+
__firstFacet: false,
140+
data: [{ fx: 'A', fy: 'X' }]
141+
});
142+
const result = getEmptyFacets([mark], ['A', 'B'], ['X', 'Y']);
143+
// Mark is excluded because __firstFacet is false
144+
for (const [, fyMap] of result) {
145+
for (const [, isEmpty] of fyMap) {
146+
expect(isEmpty).toBe(true);
147+
}
148+
}
149+
});
150+
151+
it('handles full coverage across all fx+fy combinations', () => {
152+
const mark = mockMark({
153+
fx: 'fx',
154+
fy: 'fy',
155+
data: [
156+
{ fx: 'A', fy: 'X' },
157+
{ fx: 'A', fy: 'Y' },
158+
{ fx: 'B', fy: 'X' },
159+
{ fx: 'B', fy: 'Y' }
160+
]
161+
});
162+
const result = getEmptyFacets([mark], ['A', 'B'], ['X', 'Y']);
163+
// All combinations have data → none empty
164+
expect(result.get('A')?.get('X')).toBe(false);
165+
expect(result.get('A')?.get('Y')).toBe(false);
166+
expect(result.get('B')?.get('X')).toBe(false);
167+
expect(result.get('B')?.get('Y')).toBe(false);
168+
});
169+
170+
it('considers multiple marks for coverage', () => {
171+
const mark1 = mockMark({
172+
fx: 'fx',
173+
fy: 'fy',
174+
data: [{ fx: 'A', fy: 'X' }]
175+
});
176+
const mark2 = mockMark({
177+
fx: 'fx',
178+
fy: 'fy',
179+
data: [{ fx: 'B', fy: 'Y' }]
180+
});
181+
const result = getEmptyFacets([mark1, mark2], ['A', 'B'], ['X', 'Y']);
182+
// Both marks contribute to facetted data
183+
expect(result.get('A')?.get('X')).toBe(false);
184+
expect(result.get('B')?.get('Y')).toBe(false);
185+
// Gaps still detected
186+
expect(result.get('A')?.get('Y')).toBe(true);
187+
expect(result.get('B')?.get('X')).toBe(true);
188+
});
189+
});

src/lib/helpers/facets.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,3 @@ export function getEmptyFacets(
5656
}
5757
return out;
5858
}
59-
60-
// TODO: write unit tests

0 commit comments

Comments
 (0)