@astryxdesign/vega 0.5.2-canary.701fd39 → 0.5.2-canary.72fc8cd
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/VegaChart.test.d.ts +2 -0
- package/dist/VegaChart.test.d.ts.map +1 -0
- package/dist/schema.test.d.ts +2 -0
- package/dist/schema.test.d.ts.map +1 -0
- package/dist/vegaLiteConfig.test.d.ts +2 -0
- package/dist/vegaLiteConfig.test.d.ts.map +1 -0
- package/package.json +3 -1
- package/src/VegaChart.test.tsx +345 -0
- package/src/schema.test.ts +85 -0
- package/src/vegaLiteConfig.test.ts +94 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VegaChart.test.d.ts","sourceRoot":"","sources":["../src/VegaChart.test.tsx"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.test.d.ts","sourceRoot":"","sources":["../src/schema.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vegaLiteConfig.test.d.ts","sourceRoot":"","sources":["../src/vegaLiteConfig.test.ts"],"names":[],"mappings":""}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/vega",
|
|
3
|
-
"version": "0.5.2-canary.
|
|
3
|
+
"version": "0.5.2-canary.72fc8cd",
|
|
4
4
|
"description": "Astryx Vega wrapper — chart and data visualization components. Published to npm only under the @canary dist-tag for early testing; never released as a stable (latest) version.",
|
|
5
5
|
"author": "Meta Open Source",
|
|
6
6
|
"license": "MIT",
|
|
@@ -57,6 +57,8 @@
|
|
|
57
57
|
"scripts": {
|
|
58
58
|
"build": "tsup && tsc --project tsconfig.json --emitDeclarationOnly --declaration --noEmit false --outDir dist",
|
|
59
59
|
"dev": "tsup --watch",
|
|
60
|
+
"test": "vitest run --root ../..",
|
|
61
|
+
"test:watch": "vitest --root ../..",
|
|
60
62
|
"typecheck": "tsc --noEmit"
|
|
61
63
|
}
|
|
62
64
|
}
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file VegaChart.test.tsx
|
|
5
|
+
* @input Uses vitest, @testing-library/react, VegaChart, mocked vega + vega-lite
|
|
6
|
+
* @output Functional tests for the VegaChart View lifecycle and error contract
|
|
7
|
+
* @position Colocated test for VegaChart.tsx (issue #4295 vega coverage)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {describe, it, expect, vi, beforeEach} from 'vitest';
|
|
11
|
+
import {render, act} from '@testing-library/react';
|
|
12
|
+
import React from 'react';
|
|
13
|
+
import {VegaChart} from './VegaChart';
|
|
14
|
+
import type {AnySpec} from './types';
|
|
15
|
+
|
|
16
|
+
const {parseMock, compileMock, views, FakeView} = vi.hoisted(() => {
|
|
17
|
+
/**
|
|
18
|
+
* Stand-in for vega's View. Records constructor args and lifecycle calls,
|
|
19
|
+
* and exposes a manually controlled runAsync() promise so tests decide
|
|
20
|
+
* when (and how) each run settles.
|
|
21
|
+
*/
|
|
22
|
+
class FakeView {
|
|
23
|
+
runtime: unknown;
|
|
24
|
+
options: Record<string, unknown>;
|
|
25
|
+
runPromise: Promise<void>;
|
|
26
|
+
resolveRun!: () => void;
|
|
27
|
+
rejectRun!: (err: unknown) => void;
|
|
28
|
+
data = vi.fn().mockReturnThis();
|
|
29
|
+
finalize = vi.fn();
|
|
30
|
+
runAsync = vi.fn(() => this.runPromise);
|
|
31
|
+
|
|
32
|
+
constructor(runtime: unknown, options: Record<string, unknown>) {
|
|
33
|
+
this.runtime = runtime;
|
|
34
|
+
this.options = options;
|
|
35
|
+
this.runPromise = new Promise<void>((resolve, reject) => {
|
|
36
|
+
this.resolveRun = resolve;
|
|
37
|
+
this.rejectRun = reject;
|
|
38
|
+
});
|
|
39
|
+
views.push(this);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const views: FakeView[] = [];
|
|
44
|
+
const parseMock = vi.fn((spec: unknown) => ({runtimeFor: spec}));
|
|
45
|
+
const compileMock = vi.fn((spec: unknown) => ({spec: {compiledFrom: spec}}));
|
|
46
|
+
return {parseMock, compileMock, views, FakeView};
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
vi.mock('vega', () => ({parse: parseMock, View: FakeView}));
|
|
50
|
+
vi.mock('vega-lite', () => ({compile: compileMock}));
|
|
51
|
+
|
|
52
|
+
const VEGA_LITE_SPEC = {
|
|
53
|
+
$schema: 'https://vega.github.io/schema/vega-lite/v5.json',
|
|
54
|
+
mark: 'bar',
|
|
55
|
+
} as unknown as AnySpec;
|
|
56
|
+
|
|
57
|
+
const VEGA_SPEC = {
|
|
58
|
+
$schema: 'https://vega.github.io/schema/vega/v6.json',
|
|
59
|
+
marks: [],
|
|
60
|
+
} as unknown as AnySpec;
|
|
61
|
+
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
vi.clearAllMocks();
|
|
64
|
+
views.length = 0;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('VegaChart', () => {
|
|
68
|
+
describe('spec handling', () => {
|
|
69
|
+
it('compiles vega-lite specs with compileOptions and parses the compiled output', () => {
|
|
70
|
+
const compileOptions = {config: {background: 'red'}};
|
|
71
|
+
render(
|
|
72
|
+
<VegaChart spec={VEGA_LITE_SPEC} compileOptions={compileOptions} />,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
expect(compileMock).toHaveBeenCalledTimes(1);
|
|
76
|
+
expect(compileMock).toHaveBeenCalledWith(VEGA_LITE_SPEC, compileOptions);
|
|
77
|
+
const compiled = compileMock.mock.results[0].value.spec;
|
|
78
|
+
expect(parseMock).toHaveBeenCalledTimes(1);
|
|
79
|
+
expect(parseMock.mock.calls[0][0]).toBe(compiled);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('renders plain vega specs directly without invoking the vega-lite compiler', () => {
|
|
83
|
+
render(<VegaChart spec={VEGA_SPEC} />);
|
|
84
|
+
|
|
85
|
+
expect(compileMock).not.toHaveBeenCalled();
|
|
86
|
+
expect(parseMock).toHaveBeenCalledTimes(1);
|
|
87
|
+
expect(parseMock.mock.calls[0][0]).toBe(VEGA_SPEC);
|
|
88
|
+
// No data prop -> no dataset loading either.
|
|
89
|
+
expect(views).toHaveLength(1);
|
|
90
|
+
expect(views[0].data).not.toHaveBeenCalled();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('forwards parseConfig and parseOptions to vega.parse', () => {
|
|
94
|
+
const parseConfig = {background: '#1a1a1a'};
|
|
95
|
+
const parseOptions = {ast: true};
|
|
96
|
+
render(
|
|
97
|
+
<VegaChart
|
|
98
|
+
spec={VEGA_SPEC}
|
|
99
|
+
parseConfig={parseConfig}
|
|
100
|
+
parseOptions={parseOptions}
|
|
101
|
+
/>,
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
expect(parseMock).toHaveBeenCalledWith(
|
|
105
|
+
VEGA_SPEC,
|
|
106
|
+
parseConfig,
|
|
107
|
+
parseOptions,
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('View construction', () => {
|
|
113
|
+
it('constructs the View from the parsed runtime with hover on and the rendered div as container', () => {
|
|
114
|
+
const {container} = render(<VegaChart spec={VEGA_SPEC} />);
|
|
115
|
+
|
|
116
|
+
expect(views).toHaveLength(1);
|
|
117
|
+
expect(views[0].runtime).toBe(parseMock.mock.results[0].value);
|
|
118
|
+
expect(views[0].options).toEqual({
|
|
119
|
+
hover: true,
|
|
120
|
+
container: container.firstElementChild,
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('lets viewOptions override the hover default while the container stays injected', () => {
|
|
125
|
+
const {container} = render(
|
|
126
|
+
<VegaChart
|
|
127
|
+
spec={VEGA_SPEC}
|
|
128
|
+
viewOptions={{hover: false, logLevel: 2}}
|
|
129
|
+
/>,
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
expect(views).toHaveLength(1);
|
|
133
|
+
expect(views[0].options).toEqual({
|
|
134
|
+
hover: false,
|
|
135
|
+
logLevel: 2,
|
|
136
|
+
container: container.firstElementChild,
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('loads each named dataset via view.data before the first run', () => {
|
|
141
|
+
const table = [{a: 'A', b: 28}];
|
|
142
|
+
const other = [{x: 1}];
|
|
143
|
+
render(<VegaChart spec={VEGA_SPEC} data={{table, other}} />);
|
|
144
|
+
|
|
145
|
+
expect(views).toHaveLength(1);
|
|
146
|
+
const view = views[0];
|
|
147
|
+
expect(view.data).toHaveBeenCalledTimes(2);
|
|
148
|
+
expect(view.data).toHaveBeenCalledWith('table', table);
|
|
149
|
+
expect(view.data).toHaveBeenCalledWith('other', other);
|
|
150
|
+
// Datasets are loaded before the run kicks off.
|
|
151
|
+
expect(Math.max(...view.data.mock.invocationCallOrder)).toBeLessThan(
|
|
152
|
+
view.runAsync.mock.invocationCallOrder[0],
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('tears down the old View and builds a new one when the spec identity changes', () => {
|
|
157
|
+
const {rerender} = render(<VegaChart spec={VEGA_SPEC} />);
|
|
158
|
+
expect(views).toHaveLength(1);
|
|
159
|
+
|
|
160
|
+
rerender(<VegaChart spec={{...VEGA_SPEC}} />);
|
|
161
|
+
|
|
162
|
+
expect(views).toHaveLength(2);
|
|
163
|
+
expect(views[0].finalize).toHaveBeenCalledTimes(1);
|
|
164
|
+
expect(views[1].finalize).not.toHaveBeenCalled();
|
|
165
|
+
expect(parseMock).toHaveBeenCalledTimes(2);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('error contract', () => {
|
|
170
|
+
it('reports an unrecognized $schema through onError without constructing a View', () => {
|
|
171
|
+
const onError = vi.fn();
|
|
172
|
+
const badSpec = {$schema: 'https://example.com/nope.json'} as AnySpec;
|
|
173
|
+
const {container} = render(
|
|
174
|
+
<VegaChart spec={badSpec} onError={onError} />,
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
expect(onError).toHaveBeenCalledTimes(1);
|
|
178
|
+
const error = onError.mock.calls[0][0];
|
|
179
|
+
expect(error).toBeInstanceOf(Error);
|
|
180
|
+
expect(error.message).toContain('Unrecognized $schema URL');
|
|
181
|
+
expect(compileMock).not.toHaveBeenCalled();
|
|
182
|
+
expect(parseMock).not.toHaveBeenCalled();
|
|
183
|
+
expect(views).toHaveLength(0);
|
|
184
|
+
// The container div still renders; it just stays empty.
|
|
185
|
+
expect(container.firstElementChild?.tagName).toBe('DIV');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('routes a synchronous compile failure to onError without constructing a View', () => {
|
|
189
|
+
const onError = vi.fn();
|
|
190
|
+
const boom = new Error('compile exploded');
|
|
191
|
+
compileMock.mockImplementationOnce(() => {
|
|
192
|
+
throw boom;
|
|
193
|
+
});
|
|
194
|
+
render(<VegaChart spec={VEGA_LITE_SPEC} onError={onError} />);
|
|
195
|
+
|
|
196
|
+
expect(onError).toHaveBeenCalledTimes(1);
|
|
197
|
+
expect(onError).toHaveBeenCalledWith(boom);
|
|
198
|
+
expect(views).toHaveLength(0);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('routes a rejected runAsync to onError and suppresses onReady', async () => {
|
|
202
|
+
const onReady = vi.fn();
|
|
203
|
+
const onError = vi.fn();
|
|
204
|
+
render(
|
|
205
|
+
<VegaChart spec={VEGA_SPEC} onReady={onReady} onError={onError} />,
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const failure = new Error('render failed');
|
|
209
|
+
await act(async () => {
|
|
210
|
+
views[0].rejectRun(failure);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
expect(onError).toHaveBeenCalledTimes(1);
|
|
214
|
+
expect(onError).toHaveBeenCalledWith(failure);
|
|
215
|
+
expect(onReady).not.toHaveBeenCalled();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('wraps non-Error rejection values in an Error before calling onError', async () => {
|
|
219
|
+
const onError = vi.fn();
|
|
220
|
+
render(<VegaChart spec={VEGA_SPEC} onError={onError} />);
|
|
221
|
+
|
|
222
|
+
await act(async () => {
|
|
223
|
+
views[0].rejectRun('oops');
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
expect(onError).toHaveBeenCalledTimes(1);
|
|
227
|
+
const error = onError.mock.calls[0][0];
|
|
228
|
+
expect(error).toBeInstanceOf(Error);
|
|
229
|
+
expect(error.message).toBe('oops');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('suppresses onError for rejections that settle after unmount', async () => {
|
|
233
|
+
const onError = vi.fn();
|
|
234
|
+
const {unmount} = render(
|
|
235
|
+
<VegaChart spec={VEGA_SPEC} onError={onError} />,
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
unmount();
|
|
239
|
+
await act(async () => {
|
|
240
|
+
views[0].rejectRun(new Error('too late'));
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
expect(onError).not.toHaveBeenCalled();
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
describe('ready + teardown lifecycle', () => {
|
|
248
|
+
it('calls onReady with the live View once runAsync resolves', async () => {
|
|
249
|
+
const onReady = vi.fn();
|
|
250
|
+
render(<VegaChart spec={VEGA_SPEC} onReady={onReady} />);
|
|
251
|
+
|
|
252
|
+
expect(onReady).not.toHaveBeenCalled();
|
|
253
|
+
await act(async () => {
|
|
254
|
+
views[0].resolveRun();
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
expect(onReady).toHaveBeenCalledTimes(1);
|
|
258
|
+
expect(onReady).toHaveBeenCalledWith(views[0]);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('invokes the latest onReady identity without rebuilding the View', async () => {
|
|
262
|
+
const first = vi.fn();
|
|
263
|
+
const second = vi.fn();
|
|
264
|
+
const {rerender} = render(<VegaChart spec={VEGA_SPEC} onReady={first} />);
|
|
265
|
+
rerender(<VegaChart spec={VEGA_SPEC} onReady={second} />);
|
|
266
|
+
|
|
267
|
+
// Same spec identity -> the Effect must not re-run.
|
|
268
|
+
expect(views).toHaveLength(1);
|
|
269
|
+
await act(async () => {
|
|
270
|
+
views[0].resolveRun();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
expect(first).not.toHaveBeenCalled();
|
|
274
|
+
expect(second).toHaveBeenCalledTimes(1);
|
|
275
|
+
expect(second).toHaveBeenCalledWith(views[0]);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('finalizes the View on unmount after a completed run', async () => {
|
|
279
|
+
const {unmount} = render(<VegaChart spec={VEGA_SPEC} />);
|
|
280
|
+
await act(async () => {
|
|
281
|
+
views[0].resolveRun();
|
|
282
|
+
});
|
|
283
|
+
expect(views[0].finalize).not.toHaveBeenCalled();
|
|
284
|
+
|
|
285
|
+
unmount();
|
|
286
|
+
|
|
287
|
+
expect(views[0].finalize).toHaveBeenCalledTimes(1);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('finalizes again when a cancelled run settles, and never fires onReady', async () => {
|
|
291
|
+
const onReady = vi.fn();
|
|
292
|
+
const {unmount} = render(
|
|
293
|
+
<VegaChart spec={VEGA_SPEC} onReady={onReady} />,
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
unmount();
|
|
297
|
+
// Cleanup finalizes immediately, before the pending run settles.
|
|
298
|
+
expect(views[0].finalize).toHaveBeenCalledTimes(1);
|
|
299
|
+
|
|
300
|
+
await act(async () => {
|
|
301
|
+
views[0].resolveRun();
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// The cancelled-run branch finalizes a second time -- current behavior.
|
|
305
|
+
expect(views[0].finalize).toHaveBeenCalledTimes(2);
|
|
306
|
+
expect(onReady).not.toHaveBeenCalled();
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
describe('container div', () => {
|
|
311
|
+
it('renders a single div and lands className, style, and rest props on it', () => {
|
|
312
|
+
const {container} = render(
|
|
313
|
+
<VegaChart
|
|
314
|
+
spec={VEGA_SPEC}
|
|
315
|
+
className="chart"
|
|
316
|
+
style={{width: 100}}
|
|
317
|
+
id="viz"
|
|
318
|
+
aria-label="Sales chart"
|
|
319
|
+
/>,
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
expect(container.children).toHaveLength(1);
|
|
323
|
+
const div = container.firstElementChild as HTMLElement;
|
|
324
|
+
expect(div.tagName).toBe('DIV');
|
|
325
|
+
expect(div).toHaveClass('chart');
|
|
326
|
+
expect(div).toHaveStyle({width: '100px'});
|
|
327
|
+
expect(div).toHaveAttribute('id', 'viz');
|
|
328
|
+
expect(div).toHaveAttribute('aria-label', 'Sales chart');
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('forwards both function and object refs to the container div', () => {
|
|
332
|
+
const seen: Array<HTMLDivElement | null> = [];
|
|
333
|
+
const {container: c1} = render(
|
|
334
|
+
<VegaChart spec={VEGA_SPEC} ref={node => void seen.push(node)} />,
|
|
335
|
+
);
|
|
336
|
+
expect(seen).toEqual([c1.firstElementChild]);
|
|
337
|
+
|
|
338
|
+
const objectRef = React.createRef<HTMLDivElement>();
|
|
339
|
+
const {container: c2} = render(
|
|
340
|
+
<VegaChart spec={VEGA_SPEC} ref={objectRef} />,
|
|
341
|
+
);
|
|
342
|
+
expect(objectRef.current).toBe(c2.firstElementChild);
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file schema.test.ts
|
|
5
|
+
* @input Uses vitest, parseSchema
|
|
6
|
+
* @output Functional tests for the $schema URL parser
|
|
7
|
+
* @position Colocated test for schema.ts (issue #4295 vega coverage)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {describe, it, expect} from 'vitest';
|
|
11
|
+
import {parseSchema} from './schema';
|
|
12
|
+
|
|
13
|
+
describe('parseSchema', () => {
|
|
14
|
+
it('parses a vega-lite schema URL into library and version', () => {
|
|
15
|
+
expect(
|
|
16
|
+
parseSchema('https://vega.github.io/schema/vega-lite/v5.json'),
|
|
17
|
+
).toEqual({ok: true, library: 'vega-lite', version: 'v5'});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('parses a plain vega schema URL into library and version', () => {
|
|
21
|
+
expect(parseSchema('https://vega.github.io/schema/vega/v6.json')).toEqual({
|
|
22
|
+
ok: true,
|
|
23
|
+
library: 'vega',
|
|
24
|
+
version: 'v6',
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('extracts multi-part versions from fully versioned URLs', () => {
|
|
29
|
+
expect(
|
|
30
|
+
parseSchema('https://vega.github.io/schema/vega-lite/v5.2.0.json'),
|
|
31
|
+
).toEqual({ok: true, library: 'vega-lite', version: 'v5.2.0'});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('tolerates proxy prefixes before the schema path', () => {
|
|
35
|
+
expect(
|
|
36
|
+
parseSchema(
|
|
37
|
+
'https://internal-proxy.example.com/assets/vega.github.io/schema/vega/v5.json',
|
|
38
|
+
),
|
|
39
|
+
).toEqual({ok: true, library: 'vega', version: 'v5'});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('rejects a missing $schema with a fix-it message', () => {
|
|
43
|
+
const expected = {
|
|
44
|
+
ok: false,
|
|
45
|
+
error:
|
|
46
|
+
'Spec is missing a $schema field. Add "$schema": "https://vega.github.io/schema/vega/v5.json" or the vega-lite equivalent.',
|
|
47
|
+
};
|
|
48
|
+
expect(parseSchema(undefined)).toEqual(expected);
|
|
49
|
+
expect(parseSchema(null)).toEqual(expected);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('rejects a non-string $schema and reports the received type', () => {
|
|
53
|
+
expect(parseSchema(5)).toEqual({
|
|
54
|
+
ok: false,
|
|
55
|
+
error: '$schema must be a string, got number.',
|
|
56
|
+
});
|
|
57
|
+
expect(parseSchema({url: 'x'})).toEqual({
|
|
58
|
+
ok: false,
|
|
59
|
+
error: '$schema must be a string, got object.',
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('rejects URLs that do not match the official schema format', () => {
|
|
64
|
+
expect(parseSchema('https://example.com/other.json')).toEqual({
|
|
65
|
+
ok: false,
|
|
66
|
+
error:
|
|
67
|
+
'Unrecognized $schema URL: "https://example.com/other.json". Expected format: https://vega.github.io/schema/{vega|vega-lite}/{version}.json',
|
|
68
|
+
});
|
|
69
|
+
expect(parseSchema('')).toEqual({
|
|
70
|
+
ok: false,
|
|
71
|
+
error:
|
|
72
|
+
'Unrecognized $schema URL: "". Expected format: https://vega.github.io/schema/{vega|vega-lite}/{version}.json',
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('rejects unknown libraries inside the schema URL namespace', () => {
|
|
77
|
+
expect(
|
|
78
|
+
parseSchema('https://vega.github.io/schema/vega-embed/v2.json'),
|
|
79
|
+
).toEqual({
|
|
80
|
+
ok: false,
|
|
81
|
+
error:
|
|
82
|
+
'Unknown schema library "vega-embed". Must be "vega" or "vega-lite".',
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file vegaLiteConfig.test.ts
|
|
5
|
+
* @input Uses vitest, buildVegaLiteConfig and its exported layout constants
|
|
6
|
+
* @output Functional tests for the Astryx-themed Vega-Lite config builder
|
|
7
|
+
* @position Colocated test for vegaLiteConfig.ts (issue #4295 vega coverage)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {describe, it, expect} from 'vitest';
|
|
11
|
+
import {
|
|
12
|
+
buildVegaLiteConfig,
|
|
13
|
+
DEFAULT_STROKE_WIDTH,
|
|
14
|
+
DEFAULT_POINT_SIZE,
|
|
15
|
+
DEFAULT_LEGEND_ORIENT,
|
|
16
|
+
LEGEND_OFFSET,
|
|
17
|
+
TITLE_OFFSET,
|
|
18
|
+
} from './vegaLiteConfig';
|
|
19
|
+
|
|
20
|
+
/** Resolver that makes every resolved token traceable to its name. */
|
|
21
|
+
const token = (name: string) => `resolved(${name})`;
|
|
22
|
+
|
|
23
|
+
describe('buildVegaLiteConfig', () => {
|
|
24
|
+
it('resolves chart chrome colors through the provided token resolver', () => {
|
|
25
|
+
const config = buildVegaLiteConfig(token);
|
|
26
|
+
expect(config.axis?.domainColor).toBe('resolved(--color-icon-primary)');
|
|
27
|
+
expect(config.axis?.labelColor).toBe('resolved(--color-text-secondary)');
|
|
28
|
+
expect(config.background).toBe('resolved(--color-background-card)');
|
|
29
|
+
expect(config.text?.color).toBe('resolved(--color-text-primary)');
|
|
30
|
+
expect(config.point?.fill).toBe('resolved(--color-background-card)');
|
|
31
|
+
expect(config.legend?.labelFont).toBe('resolved(--font-family-body)');
|
|
32
|
+
expect(config.title?.subtitleColor).toBe(
|
|
33
|
+
'resolved(--color-text-secondary)',
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('orders the categorical palette across the ten categorical data tokens', () => {
|
|
38
|
+
const config = buildVegaLiteConfig(token);
|
|
39
|
+
expect(config.range?.category).toEqual([
|
|
40
|
+
'resolved(--color-data-categorical-blue)',
|
|
41
|
+
'resolved(--color-data-categorical-orange)',
|
|
42
|
+
'resolved(--color-data-categorical-purple)',
|
|
43
|
+
'resolved(--color-data-categorical-green)',
|
|
44
|
+
'resolved(--color-data-categorical-pink)',
|
|
45
|
+
'resolved(--color-data-categorical-cyan)',
|
|
46
|
+
'resolved(--color-data-categorical-red)',
|
|
47
|
+
'resolved(--color-data-categorical-teal)',
|
|
48
|
+
'resolved(--color-data-categorical-brown)',
|
|
49
|
+
'resolved(--color-data-categorical-indigo)',
|
|
50
|
+
]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('builds the diverging range as a blue-to-red ramp around a gray midpoint', () => {
|
|
54
|
+
const config = buildVegaLiteConfig(token);
|
|
55
|
+
expect(config.range?.diverging).toEqual([
|
|
56
|
+
'resolved(--color-data-blue-5)',
|
|
57
|
+
'resolved(--color-data-blue-4)',
|
|
58
|
+
'resolved(--color-data-blue-3)',
|
|
59
|
+
'resolved(--color-data-blue-2)',
|
|
60
|
+
'resolved(--color-data-blue-1)',
|
|
61
|
+
'resolved(--color-data-gray-1)',
|
|
62
|
+
'resolved(--color-data-red-1)',
|
|
63
|
+
'resolved(--color-data-red-2)',
|
|
64
|
+
'resolved(--color-data-red-3)',
|
|
65
|
+
'resolved(--color-data-red-4)',
|
|
66
|
+
'resolved(--color-data-red-5)',
|
|
67
|
+
]);
|
|
68
|
+
// Sequential ranges are ascending blues.
|
|
69
|
+
expect(config.range?.heatmap).toEqual([
|
|
70
|
+
'resolved(--color-data-blue-1)',
|
|
71
|
+
'resolved(--color-data-blue-2)',
|
|
72
|
+
'resolved(--color-data-blue-3)',
|
|
73
|
+
'resolved(--color-data-blue-4)',
|
|
74
|
+
'resolved(--color-data-blue-5)',
|
|
75
|
+
]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('applies the structural mark and layout constants', () => {
|
|
79
|
+
const config = buildVegaLiteConfig(token);
|
|
80
|
+
expect(config.line?.strokeWidth).toBe(DEFAULT_STROKE_WIDTH);
|
|
81
|
+
expect(config.line?.strokeWidth).toBe(2);
|
|
82
|
+
expect(config.point?.size).toBe(DEFAULT_POINT_SIZE);
|
|
83
|
+
expect(config.point?.size).toBe(64);
|
|
84
|
+
expect(config.legend?.orient).toBe(DEFAULT_LEGEND_ORIENT);
|
|
85
|
+
expect(config.legend?.orient).toBe('right');
|
|
86
|
+
expect(config.legend?.offset).toBe(LEGEND_OFFSET);
|
|
87
|
+
expect(config.legend?.offset).toBe(16);
|
|
88
|
+
expect(config.title?.offset).toBe(TITLE_OFFSET);
|
|
89
|
+
expect(config.title?.offset).toBe(16);
|
|
90
|
+
expect(config.padding).toBe(16);
|
|
91
|
+
expect(config.scale?.bandPaddingInner).toBe(0.1);
|
|
92
|
+
expect(config.view?.stroke).toBeNull();
|
|
93
|
+
});
|
|
94
|
+
});
|