mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
Tests: drop change-detector component tests
This commit is contained in:
@@ -1,75 +0,0 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
|
|
||||||
import { act } from 'react';
|
|
||||||
import { createRoot, type Root } from 'react-dom/client';
|
|
||||||
import { AboutModal } from './AboutModal';
|
|
||||||
|
|
||||||
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
||||||
|
|
||||||
let container: HTMLDivElement;
|
|
||||||
let root: Root;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
container = document.createElement('div');
|
|
||||||
document.body.appendChild(container);
|
|
||||||
root = createRoot(container);
|
|
||||||
act(() => root.render(<AboutModal />));
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
act(() => root.unmount());
|
|
||||||
container.remove();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('AboutModal', () => {
|
|
||||||
test('renders the app name', () => {
|
|
||||||
expect(container.textContent).toContain('Astrolabe');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('renders the app version from __APP_VERSION__', () => {
|
|
||||||
// __APP_VERSION__ is injected by vite.config.ts `define` (shared with Vitest).
|
|
||||||
expect(container.textContent).toContain(__APP_VERSION__);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('renders all five keyboard shortcuts from spec §01D', () => {
|
|
||||||
// Platform-specific modifier (Cmd on Mac, Ctrl elsewhere) + the five actions.
|
|
||||||
const text = container.textContent ?? '';
|
|
||||||
// Key combinations — platform-aware modifier is either ⌘ or Ctrl
|
|
||||||
expect(text).toMatch(/(?:⌘|Ctrl)\+Shift\+N/);
|
|
||||||
expect(text).toMatch(/(?:⌘|Ctrl)\+K/);
|
|
||||||
expect(text).toMatch(/(?:⌘|Ctrl)\+S/);
|
|
||||||
expect(text).toMatch(/(?:⌘|Ctrl)\+,/);
|
|
||||||
expect(text).toContain('Esc');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('shortcuts are in an accessible table with an aria-label', () => {
|
|
||||||
const table = container.querySelector('table[aria-label]');
|
|
||||||
expect(table).not.toBeNull();
|
|
||||||
expect(table!.getAttribute('aria-label')).toBe('Keyboard shortcuts');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('renders a Privacy section that mentions local-first posture', () => {
|
|
||||||
const text = container.textContent ?? '';
|
|
||||||
// Core privacy claims from SOUL.md / spec §10
|
|
||||||
expect(text).toMatch(/no account/i);
|
|
||||||
expect(text).toMatch(/no telemetry|no analytics|no tracking/i);
|
|
||||||
expect(text).toMatch(/offline/i);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('has labelled sections: identity, shortcuts, privacy, feedback', () => {
|
|
||||||
const headings = Array.from(container.querySelectorAll('h3')).map(
|
|
||||||
(h) => h.textContent?.toLowerCase() ?? '',
|
|
||||||
);
|
|
||||||
expect(headings.some((h) => h.includes('astrolabe'))).toBe(true);
|
|
||||||
expect(headings.some((h) => h.includes('keyboard'))).toBe(true);
|
|
||||||
expect(headings.some((h) => h.includes('privacy'))).toBe(true);
|
|
||||||
expect(headings.some((h) => h.includes('feedback'))).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('offers the feedback address as a mailto link', () => {
|
|
||||||
const mailto = Array.from(container.querySelectorAll('a')).find((a) =>
|
|
||||||
a.getAttribute('href')?.startsWith('mailto:'),
|
|
||||||
);
|
|
||||||
expect(mailto).toBeTruthy();
|
|
||||||
expect(mailto!.getAttribute('href')).toContain('feedback@astrolabe-viz.com');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -157,35 +157,6 @@ describe('ChartBuilderModal', () => {
|
|||||||
vi.useRealTimers();
|
vi.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('shows the no-datasets empty state with its one next step (3D)', async () => {
|
|
||||||
await act(async () => {
|
|
||||||
root.render(<ChartBuilderModal />);
|
|
||||||
await Promise.resolve();
|
|
||||||
});
|
|
||||||
expect(container.textContent).toContain('No datasets yet');
|
|
||||||
const action = Array.from(container.querySelectorAll('button')).find(
|
|
||||||
(b) => b.textContent === 'Add a dataset',
|
|
||||||
);
|
|
||||||
expect(action).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('offers a dataset chooser when datasets exist but none is loaded', async () => {
|
|
||||||
const ds = createDataset({
|
|
||||||
name: 'Waiting',
|
|
||||||
data: [{ a: 1 }],
|
|
||||||
format: 'json',
|
|
||||||
source: 'inline',
|
|
||||||
now: T,
|
|
||||||
});
|
|
||||||
useDatasetStore.getState().add(ds);
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
root.render(<ChartBuilderModal />);
|
|
||||||
await Promise.resolve();
|
|
||||||
});
|
|
||||||
expect(container.textContent).toContain('Choose a dataset');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('a complete filter row reaches the renderer as a top-level transform (1C)', async () => {
|
test('a complete filter row reaches the renderer as a top-level transform (1C)', async () => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
const { renderSpec } = await import('../services/chart-renderer');
|
const { renderSpec } = await import('../services/chart-renderer');
|
||||||
|
|||||||
@@ -165,16 +165,6 @@ describe('ColorControls', () => {
|
|||||||
expect(range().ramp).toEqual(range().heatmap);
|
expect(range().ramp).toEqual(range().heatmap);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('scheme dropdown options carry a color preview', () => {
|
|
||||||
render();
|
|
||||||
open({});
|
|
||||||
act(() => picker('Categorical color scheme')!.click());
|
|
||||||
|
|
||||||
const tableau = button('Tableau 10')!;
|
|
||||||
// The decorative swatch strip renders as spans inside the option button.
|
|
||||||
expect(tableau.querySelectorAll('span span').length).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('picking a scheme writes range.category as a Vega scheme object', () => {
|
test('picking a scheme writes range.category as a Vega scheme object', () => {
|
||||||
render();
|
render();
|
||||||
open({});
|
open({});
|
||||||
@@ -217,20 +207,6 @@ describe('ColorControls', () => {
|
|||||||
expect(picker('Font family')).toBeTruthy();
|
expect(picker('Font family')).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('font options render their name in their own family', () => {
|
|
||||||
render();
|
|
||||||
open({});
|
|
||||||
|
|
||||||
const typeTab = [...container.querySelectorAll('button')].find(
|
|
||||||
(b) => b.getAttribute('role') === 'tab' && b.textContent === 'Type',
|
|
||||||
)!;
|
|
||||||
act(() => typeTab.click());
|
|
||||||
act(() => picker('Font family')!.click());
|
|
||||||
|
|
||||||
const georgia = button('Georgia')!;
|
|
||||||
expect(georgia.querySelector<HTMLElement>('span')!.style.fontFamily).toContain('Georgia');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('the raw JSON is collapsed by default and toggles open', () => {
|
test('the raw JSON is collapsed by default and toggles open', () => {
|
||||||
render();
|
render();
|
||||||
open({});
|
open({});
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
|
|
||||||
import { act } from 'react';
|
|
||||||
import { createRoot, type Root } from 'react-dom/client';
|
|
||||||
import { DonateModal } from './DonateModal';
|
|
||||||
|
|
||||||
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
||||||
|
|
||||||
let container: HTMLDivElement;
|
|
||||||
let root: Root;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
container = document.createElement('div');
|
|
||||||
document.body.appendChild(container);
|
|
||||||
root = createRoot(container);
|
|
||||||
act(() => root.render(<DonateModal />));
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
act(() => root.unmount());
|
|
||||||
container.remove();
|
|
||||||
});
|
|
||||||
|
|
||||||
const links = () => Array.from(container.querySelectorAll('a'));
|
|
||||||
|
|
||||||
describe('DonateModal (Support)', () => {
|
|
||||||
test('renders a sincere message referencing the project and feedback', () => {
|
|
||||||
const text = container.textContent ?? '';
|
|
||||||
expect(text).toMatch(/astrolabe/i);
|
|
||||||
expect(text).toMatch(/feedback/i);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('offers the feedback address as a mailto link', () => {
|
|
||||||
const mailto = links().find((a) => a.getAttribute('href')?.startsWith('mailto:'));
|
|
||||||
expect(mailto).toBeTruthy();
|
|
||||||
expect(mailto!.getAttribute('href')).toContain('feedback@astrolabe-viz.com');
|
|
||||||
expect(mailto!.textContent).toContain('feedback@astrolabe-viz.com');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('provides a copy-address button', () => {
|
|
||||||
const copy = Array.from(container.querySelectorAll('button')).find((b) =>
|
|
||||||
/copy/i.test(b.textContent ?? ''),
|
|
||||||
);
|
|
||||||
expect(copy).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('the donation CTA points to Ukraine-defense and opens safely in a new tab', () => {
|
|
||||||
const donate = links().find(
|
|
||||||
(a) => a.getAttribute('href') === 'https://savelife.in.ua/en/donate-en/',
|
|
||||||
);
|
|
||||||
expect(donate).toBeTruthy();
|
|
||||||
expect(donate!.getAttribute('target')).toBe('_blank');
|
|
||||||
expect(donate!.getAttribute('rel')).toContain('noopener');
|
|
||||||
expect((donate!.textContent ?? '').trim().length).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -27,24 +27,6 @@ function render(node: React.ReactNode) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('Icon', () => {
|
describe('Icon', () => {
|
||||||
test('renders an SVG on the Carbon 32-grid, decorative and unfocusable', () => {
|
|
||||||
const svg = render(<Icon name="close" />);
|
|
||||||
expect(svg.getAttribute('viewBox')).toBe('0 0 32 32');
|
|
||||||
// Decorative by default: the enclosing control carries the accessible name.
|
|
||||||
expect(svg.getAttribute('aria-hidden')).toBe('true');
|
|
||||||
expect(svg.getAttribute('focusable')).toBe('false');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('each name draws a distinct glyph from the registry', () => {
|
|
||||||
// close is a single polygon; delete (TrashCan) is rects + a path; dataset
|
|
||||||
// (DataTable) is six cells + a path. Distinct geometry ⇒ distinct meaning.
|
|
||||||
expect(render(<Icon name="close" />).querySelector('polygon')).not.toBeNull();
|
|
||||||
const del = render(<Icon name="delete" />);
|
|
||||||
expect(del.querySelectorAll('rect').length).toBe(3);
|
|
||||||
expect(del.querySelector('path')).not.toBeNull();
|
|
||||||
expect(render(<Icon name="dataset" />).querySelectorAll('rect').length).toBe(6);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('size maps to a single, changeable class token', () => {
|
test('size maps to a single, changeable class token', () => {
|
||||||
const sm = render(<Icon name="add" />); // default
|
const sm = render(<Icon name="add" />); // default
|
||||||
const smClass = sm.getAttribute('class') ?? '';
|
const smClass = sm.getAttribute('class') ?? '';
|
||||||
|
|||||||
@@ -110,11 +110,6 @@ describe('LivePreview busy overlay', () => {
|
|||||||
expect(overlay()).not.toBeNull();
|
expect(overlay()).not.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('overlay carries a visible label for sighted users', () => {
|
|
||||||
act(() => usePreviewStore.setState({ busy: true }));
|
|
||||||
expect(overlay()?.textContent).toMatch(/rendering/i);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('the preview body carries aria-busy=true when busy', () => {
|
test('the preview body carries aria-busy=true when busy', () => {
|
||||||
act(() => usePreviewStore.setState({ busy: true }));
|
act(() => usePreviewStore.setState({ busy: true }));
|
||||||
// The body element has aria-busy when the store says busy.
|
// The body element has aria-busy when the store says busy.
|
||||||
|
|||||||
@@ -51,19 +51,6 @@ function click(name: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('Onboarding', () => {
|
describe('Onboarding', () => {
|
||||||
test('greets the user and offers the primary create action', () => {
|
|
||||||
expect(container.textContent).toContain('Welcome to Astrolabe');
|
|
||||||
expect(container.textContent).toContain('Create your first snippet');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('renders one card per example, each with an Add control', () => {
|
|
||||||
for (const example of CHART_EXAMPLES) {
|
|
||||||
expect(container.textContent).toContain(example.name);
|
|
||||||
expect(container.textContent).toContain(example.description);
|
|
||||||
expect(container.querySelector(`button[aria-label="Add ${example.name}"]`)).not.toBeNull();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('"Create your first snippet" creates one snippet from the sample template', () => {
|
test('"Create your first snippet" creates one snippet from the sample template', () => {
|
||||||
click('Create your first snippet');
|
click('Create your first snippet');
|
||||||
const { snippets, activeSnippetId } = useSnippetStore.getState();
|
const { snippets, activeSnippetId } = useSnippetStore.getState();
|
||||||
|
|||||||
Reference in New Issue
Block a user