feat(core): add dataviz bundled skill (#6198)

* feat(core): add dataviz bundled skill

* fix(core): harden dataviz palette validator

* fix(core): harden dataviz validator review gaps

* test(core): declare node global in dataviz test

* fix(core): harden dataviz validator cli

* fix(core): tighten dataviz validator packaging

* fix(core): tighten dataviz skill packaging
This commit is contained in:
qqqys 2026-07-03 09:06:39 +08:00 committed by GitHub
parent b16baf1ffc
commit 62e11a5732
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 832 additions and 5 deletions

View file

@ -45,4 +45,22 @@ describe('bundled SKILL.md files', () => {
expect(Array.isArray(cfg.allowedTools)).toBe(true);
}
});
it('ships dataviz validator and references with the bundled skill', () => {
const datavizDir = path.join(bundledDir, 'dataviz');
expect(fs.existsSync(path.join(datavizDir, 'SKILL.md'))).toBe(true);
expect(
fs.existsSync(path.join(datavizDir, 'scripts', 'validate_palette.js')),
).toBe(true);
expect(
fs.existsSync(path.join(datavizDir, 'references', 'palette.md')),
).toBe(true);
expect(
fs.existsSync(path.join(datavizDir, 'references', 'choosing-a-form.md')),
).toBe(true);
expect(
fs.existsSync(path.join(datavizDir, 'references', 'anti-patterns.md')),
).toBe(true);
});
});

View file

@ -0,0 +1,50 @@
---
name: dataviz
description: Design guidance for charts, graphs, dashboards, maps, and data visualizations, including a local palette validator.
when_to_use: When creating or revising charts, graphs, dashboards, maps, plots, inline SVG, D3, Plotly, Recharts, matplotlib, or any Artifact page that visualizes data.
allowedTools:
- read_file
---
# Dataviz
Use this skill before producing a chart, dashboard, map, or data visualization.
## Workflow
1. Identify the analytic task: comparison, trend, distribution, relationship,
ranking, part-to-whole, geography, or status monitoring.
2. Choose the simplest chart form that answers that task. Read
`references/choosing-a-form.md` when the form is not obvious.
3. Write the finding into the title, subtitle, axis label, or direct annotation.
A viewer should know what changed, what is high or low, or what decision the
chart supports.
4. Pick colors from `references/palette.md`, or validate any custom palette with
the script below.
5. Check `references/anti-patterns.md` before finalizing the design.
## Palette Validation
Resolve paths relative to the skill base directory shown above this skill body.
Do not assume `$QWEN_SKILL_ROOT` is set for normal shell commands.
Run:
```bash
node <skill-base-directory>/scripts/validate_palette.js '#1d4ed8,#b45309,#166534' --mode light
```
Treat `FAIL` as a required palette change. Treat `WARN` as acceptable only when
the chart also uses labels, shape, texture, ordering, or another secondary
encoding.
## Mark Rules
- Use categorical palettes for unordered groups; use sequential or diverging
ramps only for ordered values.
- Prefer direct labels over legends when the chart has a small number of series.
- Keep gridlines subtle and fewer than the data marks.
- Avoid dual axes unless both series share a clearly explained transformation.
- Do not rely on color alone for critical distinctions.
- Keep dashboards scan-friendly: align cards, use consistent number formats,
and reserve saturated color for important state changes.

View file

@ -0,0 +1,12 @@
# Dataviz Anti-Patterns
- Do not use rainbow palettes for ordered data.
- Do not use sequential ramps for unrelated categories.
- Do not encode critical differences with color only.
- Do not start a bar chart above zero unless the truncation is explicit and
justified.
- Do not use dual axes when the visual comparison implies a relationship that
is not in the data.
- Do not hide units, denominators, sample sizes, or time windows.
- Do not let chart decoration compete with marks, labels, and annotations.
- Do not use a dashboard card if a compact table is clearer.

View file

@ -0,0 +1,16 @@
# Choosing A Form
| Task | Prefer | Avoid |
| --------------------- | --------------------------------- | --------------------------------------- |
| Compare categories | sorted bar chart | pie with many slices |
| Show change over time | line chart | disconnected bars for dense time series |
| Show ranking | horizontal bar chart | unsorted columns |
| Show distribution | histogram, box plot, violin plot | averages without spread |
| Show relationship | scatter plot | dual-axis line chart |
| Show part-to-whole | stacked bar for few parts | nested donuts |
| Show geography | choropleth or symbol map | map when location is irrelevant |
| Monitor status | compact dashboard with thresholds | decorative KPI cards |
Prefer the chart that makes the comparison visible without interaction. Add
interaction only when it reveals detail, filters clutter, or supports a real
workflow.

View file

@ -0,0 +1,57 @@
# Palette Reference
## Categorical
Use these first for independent categories:
```text
#1d4ed8 blue
#b45309 orange
#166534 green
#7c3aed purple
#0891b2 cyan
#be123c rose
```
Start with three to five colors. If the chart needs more categories than that,
prefer direct labels, grouping, faceting, or filtering over adding more hues.
## Sequential
Use one hue ramp for ordered magnitude:
```text
#eff6ff #bfdbfe #60a5fa #2563eb #1e3a8a
```
The lightest and darkest ramp endpoints are for backgrounds, area fills, and
labels. When choosing exact line, point, or bar mark colors, validate the marks
you actually use rather than the full ramp.
## Diverging
Use a diverging ramp only when there is a meaningful center such as zero,
target, or baseline:
```text
#b91c1c #fca5a5 #f8fafc #93c5fd #1d4ed8
```
The center and endpoint colors provide range context. For exact chart marks,
validate the selected mark colors and add direct labels or another encoding when
the validator reports `WARN`.
## Validation
Run the validator whenever exact mark colors are chosen:
```bash
node <skill-base-directory>/scripts/validate_palette.js '#1d4ed8,#b45309,#166534' --mode light
```
The validator's 2.5:1 contrast floor is a practical chart-mark heuristic, not a
WCAG AA guarantee. Use 3:1 or higher when chart marks must satisfy WCAG 2.1
non-text contrast without relying on labels or secondary encodings.
The validator also enforces OKLCH lightness bands so marks are neither too pale
nor too dark for the selected surface, even when contrast alone looks adequate.

View file

@ -0,0 +1,328 @@
#!/usr/bin/env node
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { resolve } from 'node:path';
import process from 'node:process';
import { fileURLToPath } from 'node:url';
const MODE_CONFIG = {
light: {
surface: [255, 255, 255],
minLightness: 0.25,
maxLightness: 0.88,
minContrast: 2.5,
},
dark: {
surface: [17, 24, 39],
minLightness: 0.45,
maxLightness: 0.92,
minContrast: 2.5,
},
};
const CVD_MATRICES = {
protanopia: [
[0.56667, 0.43333, 0],
[0.55833, 0.44167, 0],
[0, 0, 1],
],
deuteranopia: [
[0.625, 0.375, 0],
[0.7, 0.3, 0],
[0, 0.3, 0.7],
],
tritanopia: [
[0.95, 0.05, 0],
[0, 0.43333, 0.56667],
[0, 0.475, 0.525],
],
};
const MAX_PALETTE_COLORS = 50;
export function validatePalette(input, options = {}) {
const mode = options.mode ?? 'light';
const config = modeConfig(mode);
const failures = [];
const warnings = [];
if (!config) {
return {
status: 'FAIL',
failures: [`Unsupported mode "${mode}". Use "light" or "dark".`],
warnings,
};
}
if (input.length === 0) {
return { status: 'FAIL', failures: ['No colors provided.'], warnings };
}
if (input.length > MAX_PALETTE_COLORS) {
return {
status: 'FAIL',
failures: [
`Palette has ${input.length} colors; maximum supported is ${MAX_PALETTE_COLORS}.`,
],
warnings,
};
}
const colors = input.map((value, index) => {
const rgb = parseHexColor(value);
if (!rgb) {
failures.push(
`Color ${index + 1} has invalid hex color syntax: ${value}`,
);
return null;
}
const oklch = rgbToOklch(rgb);
return { value: normalizeHex(rgb), rgb, oklch };
});
if (failures.length > 0) {
return { status: 'FAIL', failures, warnings };
}
for (const color of colors) {
if (
color.oklch.l < config.minLightness ||
color.oklch.l > config.maxLightness
) {
failures.push(
`${color.value} has OKLCH lightness ${format(color.oklch.l)}, outside the ${mode} mark band ${config.minLightness}-${config.maxLightness}.`,
);
}
if (color.oklch.c < 0.04) {
warnings.push(
`${color.value} has low OKLCH chroma ${format(color.oklch.c)} and may read as gray.`,
);
}
const contrast = contrastRatio(color.rgb, config.surface);
if (contrast < config.minContrast) {
failures.push(
`${color.value} contrast ${format(contrast)} is below ${config.minContrast}:1 against the ${mode} chart surface.`,
);
}
}
for (const [kind, matrix] of Object.entries(CVD_MATRICES)) {
const simulated = colors.map((color) => {
const rgb = applyMatrix(color.rgb, matrix);
return { value: color.value, rgb, lab: rgbToLab(rgb) };
});
for (const color of simulated) {
const contrast = contrastRatio(color.rgb, config.surface);
if (contrast < config.minContrast) {
warnings.push(
`${color.value} contrast ${format(contrast)} is below ${config.minContrast}:1 against the ${mode} chart surface under colorblind ${kind} simulation.`,
);
}
}
for (let i = 0; i < simulated.length; i++) {
for (let j = i + 1; j < simulated.length; j++) {
const distance = labDistance(simulated[i].lab, simulated[j].lab);
if (distance < 40) {
warnings.push(
`${simulated[i].value} and ${simulated[j].value} are close under colorblind ${kind} simulation (DeltaE ${format(distance)}); add secondary encoding such as labels, shape, or texture.`,
);
}
}
}
}
const uniqueFailures = [...new Set(failures)];
const uniqueWarnings = [...new Set(warnings)];
return {
status:
uniqueFailures.length > 0
? 'FAIL'
: uniqueWarnings.length > 0
? 'WARN'
: 'PASS',
failures: uniqueFailures,
warnings: uniqueWarnings,
};
}
function parseHexColor(value) {
if (typeof value !== 'string') return null;
const hex = value.trim();
const short = /^#([0-9a-f]{3})$/i.exec(hex);
if (short) {
return short[1].split('').map((part) => parseInt(part + part, 16));
}
const long = /^#([0-9a-f]{6})$/i.exec(hex);
if (!long) return null;
return [0, 2, 4].map((offset) =>
parseInt(long[1].slice(offset, offset + 2), 16),
);
}
function normalizeHex(rgb) {
return `#${rgb.map((channel) => channel.toString(16).padStart(2, '0')).join('')}`;
}
function applyMatrix(rgb, matrix) {
const linear = rgb.map(srgbChannelToLinear);
return matrix
.map((row) => row[0] * linear[0] + row[1] * linear[1] + row[2] * linear[2])
.map(linearChannelToSrgb);
}
function contrastRatio(left, right) {
const l1 = relativeLuminance(left);
const l2 = relativeLuminance(right);
const lighter = Math.max(l1, l2);
const darker = Math.min(l1, l2);
return (lighter + 0.05) / (darker + 0.05);
}
function relativeLuminance(rgb) {
const [r, g, b] = rgb.map(srgbChannelToLinear);
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
function rgbToLab(rgb) {
const [x, y, z] = rgbToXyz(rgb);
const xn = 0.95047;
const yn = 1;
const zn = 1.08883;
const fx = labPivot(x / xn);
const fy = labPivot(y / yn);
const fz = labPivot(z / zn);
return {
l: 116 * fy - 16,
a: 500 * (fx - fy),
b: 200 * (fy - fz),
};
}
function rgbToXyz(rgb) {
const [r, g, b] = rgb.map(srgbChannelToLinear);
return [
r * 0.4124564 + g * 0.3575761 + b * 0.1804375,
r * 0.2126729 + g * 0.7151522 + b * 0.072175,
r * 0.0193339 + g * 0.119192 + b * 0.9503041,
];
}
function labPivot(value) {
return value > 0.008856 ? Math.cbrt(value) : 7.787 * value + 16 / 116;
}
function rgbToOklch(rgb) {
const [r, g, b] = rgb.map(srgbChannelToLinear);
const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
const okL = 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s;
const okA = 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s;
const okB = 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s;
return {
l: okL,
c: Math.sqrt(okA * okA + okB * okB),
};
}
function srgbChannelToLinear(channel) {
const value = channel / 255;
return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
}
function linearChannelToSrgb(channel) {
const value = Math.max(0, Math.min(1, channel));
const encoded =
value <= 0.0031308 ? value * 12.92 : 1.055 * value ** (1 / 2.4) - 0.055;
return Math.round(encoded * 255);
}
function labDistance(left, right) {
return Math.sqrt(
(left.l - right.l) ** 2 + (left.a - right.a) ** 2 + (left.b - right.b) ** 2,
);
}
function format(value) {
return Number(value.toFixed(3)).toString();
}
function parseArgs(argv) {
let mode = 'light';
let palette;
for (let i = 0; i < argv.length; i++) {
if (argv[i] === '--mode') {
if (!argv[i + 1] || argv[i + 1].startsWith('--')) {
process.stderr.write(
'Error: --mode requires a value (light or dark)\n',
);
process.exit(2);
}
mode = argv[++i];
} else if (argv[i].startsWith('--mode=')) {
mode = argv[i].slice('--mode='.length);
if (!mode) {
process.stderr.write(
'Error: --mode requires a value (light or dark)\n',
);
process.exit(2);
}
} else if (!palette) {
palette = argv[i];
}
}
return { palette, mode };
}
function printResult(result) {
process.stdout.write(`${result.status}\n`);
for (const failure of result.failures) {
process.stderr.write(`FAIL: ${failure}\n`);
}
for (const warning of result.warnings) {
process.stdout.write(`WARN: ${warning}\n`);
}
}
function modeConfig(mode) {
return Object.hasOwn(MODE_CONFIG, mode) ? MODE_CONFIG[mode] : undefined;
}
if (
process.argv[1] &&
fileURLToPath(import.meta.url) === resolve(process.argv[1])
) {
const { palette, mode } = parseArgs(process.argv.slice(2));
if (!palette) {
process.stderr.write(
"Usage: node validate_palette.js '#1d4ed8,#b45309,#166534' --mode light\n",
);
process.exit(2);
}
if (!modeConfig(mode)) {
process.stderr.write(
`Error: Unsupported mode "${mode}". Use "light" or "dark".\n`,
);
process.exit(2);
}
const result = validatePalette(
palette
.split(',')
.map((color) => color.trim())
.filter(Boolean),
{ mode },
);
printResult(result);
process.exit(result.status === 'FAIL' ? 1 : 0);
}

View file

@ -0,0 +1,183 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
/* global process */
import { execFileSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import { validatePalette } from './validate_palette.js';
const scriptPath = fileURLToPath(import.meta.url).replace(/\.test\.js$/, '.js');
describe('validate_palette', () => {
it('passes a varied categorical palette on a light chart surface', () => {
const result = validatePalette(['#1d4ed8', '#b45309', '#166534'], {
mode: 'light',
});
expect(result.status).toBe('PASS');
expect(result.failures).toEqual([]);
});
it('passes a varied categorical palette on a dark chart surface', () => {
const result = validatePalette(['#60a5fa', '#fbbf24'], {
mode: 'dark',
});
expect(result.status).toBe('PASS');
expect(result.failures).toEqual([]);
});
it('fails invalid hex colors', () => {
const result = validatePalette(['#2563eb', 'blue'], { mode: 'light' });
expect(result.status).toBe('FAIL');
expect(result.failures.join('\n')).toMatch(/invalid hex/i);
});
it('warns when colors are too gray to read as categorical marks', () => {
const result = validatePalette(['#777777', '#999999'], { mode: 'light' });
expect(result.status).toBe('WARN');
expect(result.warnings.join('\n')).toMatch(/chroma/i);
});
it('fails low-contrast light marks on a light chart surface', () => {
const result = validatePalette(['#eeeeee', '#f7f7f7'], { mode: 'light' });
expect(result.status).toBe('FAIL');
expect(result.failures.join('\n')).toMatch(/contrast/i);
});
it('fails low-contrast dark marks on a dark chart surface', () => {
const result = validatePalette(['#111827', '#1f2937'], { mode: 'dark' });
expect(result.status).toBe('FAIL');
expect(result.failures.join('\n')).toMatch(/contrast/i);
});
it('fails marks outside the OKLCH lightness band', () => {
const result = validatePalette(['#1a1a2e'], { mode: 'light' });
expect(result.status).toBe('FAIL');
expect(result.failures.join('\n')).toMatch(/lightness/i);
});
it('fails when no colors are provided', () => {
const result = validatePalette([], { mode: 'light' });
expect(result.status).toBe('FAIL');
expect(result.failures).toEqual(['No colors provided.']);
});
it('fails unsupported modes without reading object prototypes', () => {
const result = validatePalette(['#2563eb'], { mode: '__proto__' });
expect(result.status).toBe('FAIL');
expect(result.failures.join('\n')).toMatch(/unsupported mode/i);
});
it('normalizes 3-digit hex shorthand in messages', () => {
const result = validatePalette(['#777', '#999'], { mode: 'light' });
expect(result.status).toBe('WARN');
expect(result.warnings.join('\n')).toContain('#777777');
expect(result.warnings.join('\n')).toContain('#999999');
});
it('deduplicates repeated validation messages', () => {
const result = validatePalette(['#777777', '#777777'], { mode: 'light' });
expect(result.status).toBe('WARN');
expect(
result.warnings.filter((warning) =>
warning.includes('#777777 has low OKLCH chroma'),
),
).toHaveLength(1);
});
it('rejects palettes that are too large for pairwise checks', () => {
const result = validatePalette(Array(51).fill('#2563eb'), {
mode: 'light',
});
expect(result.status).toBe('FAIL');
expect(result.failures).toEqual([
'Palette has 51 colors; maximum supported is 50.',
]);
});
it('warns when colorblind simulation makes colors too close', () => {
const result = validatePalette(['#2563eb', '#7c3aed'], { mode: 'light' });
expect(result.status).toBe('WARN');
expect(result.warnings.join('\n')).toMatch(/colorblind/i);
});
it('warns when simulated colors lose chart-surface contrast', () => {
const result = validatePalette(['#d97706'], { mode: 'light' });
expect(result.status).toBe('WARN');
expect(result.warnings.join('\n')).toMatch(/contrast/i);
});
it('accepts --mode before the palette in CLI usage', () => {
const output = execFileSync(process.execPath, [
scriptPath,
'--mode',
'dark',
'#60a5fa,#fbbf24',
]).toString();
expect(output).toMatch(/^PASS$/m);
});
it('accepts --mode=value in CLI usage', () => {
const output = execFileSync(process.execPath, [
scriptPath,
'--mode=dark',
'#60a5fa,#fbbf24',
]).toString();
expect(output).toMatch(/^PASS$/m);
});
it('rejects --mode without a value in CLI usage', () => {
expect(() =>
execFileSync(process.execPath, [scriptPath, '#2563eb', '--mode'], {
stdio: 'pipe',
}),
).toThrow();
});
it('treats invalid CLI modes as usage errors', () => {
try {
execFileSync(process.execPath, [
scriptPath,
'--mode',
'midnight',
'#2563eb',
]);
throw new Error('expected command to fail');
} catch (err) {
expect(err.status).toBe(2);
expect(err.stderr.toString()).toMatch(/unsupported mode/i);
}
});
it('prints validation failure details to stderr', () => {
try {
execFileSync(process.execPath, [scriptPath, '#eeeeee'], {
stdio: 'pipe',
});
throw new Error('expected command to fail');
} catch (err) {
expect(err.status).toBe(1);
expect(err.stdout.toString()).toMatch(/^FAIL$/m);
expect(err.stderr.toString()).toMatch(/FAIL:/);
}
});
});

View file

@ -25,6 +25,8 @@ import fs from 'node:fs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const defaultRoot = join(__dirname, '..');
const BUNDLED_SKILL_TEST_FILE_RE =
/\.(?:test|spec)\.(?:d\.)?[cm]?[jt]sx?(?:\.map)?$/;
export function copyBundleAssets({ root = defaultRoot } = {}) {
const distDir = join(root, 'dist');
@ -66,7 +68,10 @@ export function copyBundleAssets({ root = defaultRoot } = {}) {
);
if (existsSync(bundledSkillsDir)) {
const destBundledDir = join(distDir, 'bundled');
copyRecursiveSync(bundledSkillsDir, destBundledDir);
fs.rmSync(destBundledDir, { recursive: true, force: true });
copyRecursiveSync(bundledSkillsDir, destBundledDir, {
skipEntry: isBundledSkillTestFile,
});
console.log('Copied bundled skills to dist/bundled/');
} else {
console.warn(
@ -159,7 +164,7 @@ function isDirectRun() {
/**
* Recursively copy directory
*/
function copyRecursiveSync(src, dest) {
function copyRecursiveSync(src, dest, options = {}) {
if (!existsSync(src)) {
return;
}
@ -173,14 +178,13 @@ function copyRecursiveSync(src, dest) {
const entries = fs.readdirSync(src);
for (const entry of entries) {
// Skip .DS_Store files
if (entry === '.DS_Store') {
if (entry === '.DS_Store' || options.skipEntry?.(entry)) {
continue;
}
const srcPath = join(src, entry);
const destPath = join(dest, entry);
copyRecursiveSync(srcPath, destPath);
copyRecursiveSync(srcPath, destPath, options);
}
} else {
copyFileSync(src, dest);
@ -191,3 +195,7 @@ function copyRecursiveSync(src, dest) {
}
}
}
function isBundledSkillTestFile(fileName) {
return BUNDLED_SKILL_TEST_FILE_RE.test(fileName);
}

View file

@ -31,6 +31,11 @@ describe('package asset scripts', () => {
it('copies extension examples into the bundled runtime dist', () => {
const rootDir = createFixtureRoot();
writeFile(
rootDir,
'packages/cli/src/commands/extensions/examples/mcp-server/keep.test.js',
'console.log("example test fixture");\n',
);
stubConsole();
copyBundleAssets({ root: rootDir });
@ -47,6 +52,156 @@ describe('package asset scripts', () => {
path.join(rootDir, 'dist', 'examples', 'mcp-server', 'package.json'),
),
).toBe(true);
expect(
existsSync(
path.join(rootDir, 'dist', 'examples', 'mcp-server', 'keep.test.js'),
),
).toBe(true);
});
it('copies bundled skill scripts and references into the runtime dist', () => {
const rootDir = createFixtureRoot();
writeFile(
rootDir,
'packages/core/src/skills/bundled/dataviz/SKILL.md',
'---\nname: dataviz\ndescription: Chart guidance\n---\nBody\n',
);
writeFile(
rootDir,
'packages/core/src/skills/bundled/dataviz/scripts/validate_palette.js',
'console.log("ok");\n',
);
writeFile(
rootDir,
'packages/core/src/skills/bundled/dataviz/scripts/validate_palette.test.js',
'import { it } from "vitest";\n',
);
writeFile(
rootDir,
'packages/core/src/skills/bundled/dataviz/scripts/validate_palette.test.d.ts',
'export {};\n',
);
writeFile(
rootDir,
'packages/core/src/skills/bundled/dataviz/scripts/validate_palette.test.js.map',
'{}\n',
);
writeFile(
rootDir,
'packages/core/src/skills/bundled/dataviz/scripts/chart.spec.tsx',
'export {};\n',
);
writeFile(
rootDir,
'packages/core/src/skills/bundled/dataviz/scripts/font-test-regular.woff2',
'font\n',
);
writeFile(
rootDir,
'packages/core/src/skills/bundled/dataviz/references/palette.md',
'# Palette\n',
);
writeFile(rootDir, 'dist/bundled/dataviz/scripts/stale.test.js', 'stale\n');
stubConsole();
copyBundleAssets({ root: rootDir });
expect(
existsSync(
path.join(
rootDir,
'dist',
'bundled',
'dataviz',
'scripts',
'validate_palette.js',
),
),
).toBe(true);
expect(
existsSync(
path.join(
rootDir,
'dist',
'bundled',
'dataviz',
'scripts',
'validate_palette.test.js',
),
),
).toBe(false);
expect(
existsSync(
path.join(
rootDir,
'dist',
'bundled',
'dataviz',
'scripts',
'validate_palette.test.d.ts',
),
),
).toBe(false);
expect(
existsSync(
path.join(
rootDir,
'dist',
'bundled',
'dataviz',
'scripts',
'validate_palette.test.js.map',
),
),
).toBe(false);
expect(
existsSync(
path.join(
rootDir,
'dist',
'bundled',
'dataviz',
'scripts',
'chart.spec.tsx',
),
),
).toBe(false);
expect(
existsSync(
path.join(
rootDir,
'dist',
'bundled',
'dataviz',
'scripts',
'stale.test.js',
),
),
).toBe(false);
expect(
existsSync(
path.join(
rootDir,
'dist',
'bundled',
'dataviz',
'scripts',
'font-test-regular.woff2',
),
),
).toBe(true);
expect(
existsSync(
path.join(
rootDir,
'dist',
'bundled',
'dataviz',
'references',
'palette.md',
),
),
).toBe(true);
});
it('includes extension examples in the prepared dist package', () => {