Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
328 lines
12 KiB
TypeScript
328 lines
12 KiB
TypeScript
import React from 'react';
|
|
import { describe, expect, test, vi } from 'vitest';
|
|
import { render, screen, within, waitFor } from '@testing-library/react';
|
|
import userEvent from '@testing-library/user-event';
|
|
import { CheckboxDemo } from '../src/demos/CheckboxDemo';
|
|
import { SwitchDemo } from '../src/demos/SwitchDemo';
|
|
import { TextAreaDemo } from '../src/demos/TextAreaDemo';
|
|
import { LabelDemo } from '../src/demos/LabelDemo';
|
|
import { SearchInputDemo } from '../src/demos/SearchInputDemo';
|
|
import { TabsDemo } from '../src/demos/TabsDemo';
|
|
import { DrawerDemo } from '../src/demos/DrawerDemo';
|
|
import { DropdownMenuDemo } from '../src/demos/DropdownMenuDemo';
|
|
|
|
describe('Checkbox demo', () => {
|
|
test('checkbox toggles between checked and unchecked', async () => {
|
|
const user = userEvent.setup();
|
|
render(<CheckboxDemo />);
|
|
const checkbox = screen.getByLabelText('Checked') as HTMLInputElement;
|
|
expect(checkbox.checked).toBe(true);
|
|
await user.click(checkbox);
|
|
expect(screen.getByLabelText('Unchecked')).toBeInTheDocument();
|
|
});
|
|
|
|
test('indeterminate state shows dash/minus icon', () => {
|
|
render(<CheckboxDemo />);
|
|
const input = screen.getByLabelText('Indeterminate') as HTMLInputElement;
|
|
expect(input.indeterminate).toBe(true);
|
|
});
|
|
|
|
test('label is clickable and toggles checkbox', async () => {
|
|
const user = userEvent.setup();
|
|
render(<CheckboxDemo />);
|
|
const checkbox = screen.getByLabelText('Checked') as HTMLInputElement;
|
|
await user.click(screen.getByText('Checked'));
|
|
expect(checkbox.checked).toBe(false);
|
|
});
|
|
|
|
test('disabled checkbox cannot be toggled', async () => {
|
|
const user = userEvent.setup();
|
|
render(<CheckboxDemo />);
|
|
const checkbox = screen.getByLabelText('Disabled') as HTMLInputElement;
|
|
await user.click(checkbox);
|
|
expect(checkbox.checked).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('Switch demo', () => {
|
|
test('switch toggles between on and off', async () => {
|
|
const user = userEvent.setup();
|
|
render(<SwitchDemo />);
|
|
const switchEl = screen.getAllByRole('switch')[0] as HTMLButtonElement;
|
|
await user.click(switchEl);
|
|
expect(switchEl.getAttribute('aria-checked')).toBe('false');
|
|
});
|
|
|
|
test('size variants render at different scales', () => {
|
|
render(<SwitchDemo />);
|
|
const switches = screen.getAllByRole('switch');
|
|
const sm = switches[1];
|
|
const lg = switches[3];
|
|
expect(Number(sm.style.width.replace('px', ''))).toBeLessThan(Number(lg.style.width.replace('px', '')));
|
|
});
|
|
|
|
test('disabled switch cannot be toggled', async () => {
|
|
const user = userEvent.setup();
|
|
render(<SwitchDemo />);
|
|
const disabled = screen.getAllByRole('switch')[4] as HTMLButtonElement;
|
|
await user.click(disabled);
|
|
expect(disabled.getAttribute('aria-checked')).toBe('true');
|
|
});
|
|
});
|
|
|
|
describe('TextArea demo', () => {
|
|
test('textarea accepts multi-line input', async () => {
|
|
const user = userEvent.setup();
|
|
render(<TextAreaDemo />);
|
|
const textarea = screen.getByPlaceholderText('Write a message...') as HTMLTextAreaElement;
|
|
await user.type(textarea, 'Line 1\nLine 2');
|
|
expect(textarea.value).toContain('Line 2');
|
|
});
|
|
|
|
test('rows prop changes visible rows', () => {
|
|
render(<TextAreaDemo />);
|
|
const rows2 = screen.getByPlaceholderText('2 rows') as HTMLTextAreaElement;
|
|
const rows4 = screen.getByPlaceholderText('4 rows') as HTMLTextAreaElement;
|
|
expect(rows2.rows).toBe(2);
|
|
expect(rows4.rows).toBe(4);
|
|
});
|
|
|
|
test('resize option controls resize behavior', () => {
|
|
render(<TextAreaDemo />);
|
|
const none = screen.getByPlaceholderText('No resize') as HTMLTextAreaElement;
|
|
const horizontal = screen.getByPlaceholderText('Horizontal resize') as HTMLTextAreaElement;
|
|
expect(none.style.resize).toBe('none');
|
|
expect(horizontal.style.resize).toBe('horizontal');
|
|
});
|
|
|
|
test('disabled textarea cannot be edited', async () => {
|
|
const user = userEvent.setup();
|
|
render(<TextAreaDemo />);
|
|
const disabled = screen.getByPlaceholderText('Disabled') as HTMLTextAreaElement;
|
|
await user.type(disabled, 'Text');
|
|
expect(disabled.value).toBe('');
|
|
});
|
|
});
|
|
|
|
describe('Label demo', () => {
|
|
test('label text is rendered', () => {
|
|
render(<LabelDemo />);
|
|
expect(screen.getByText('Email address')).toBeInTheDocument();
|
|
});
|
|
|
|
test('required indicator is shown when required=true', () => {
|
|
render(<LabelDemo />);
|
|
const required = screen.getByText('Company name').parentElement as HTMLElement;
|
|
expect(required.textContent).toContain('*');
|
|
});
|
|
|
|
test('size variants change font size', () => {
|
|
render(<LabelDemo />);
|
|
const sm = screen.getByText('Small');
|
|
const lg = screen.getByText('Large');
|
|
expect(Number(sm.style.fontSize.replace('px', ''))).toBeLessThan(Number(lg.style.fontSize.replace('px', '')));
|
|
});
|
|
});
|
|
|
|
describe('SearchInput demo', () => {
|
|
test('search icon is visible', () => {
|
|
render(<SearchInputDemo />);
|
|
const input = screen.getByPlaceholderText('Search accounts');
|
|
const container = input.parentElement as HTMLElement;
|
|
expect(container.querySelector('svg')).toBeTruthy();
|
|
});
|
|
|
|
test('clear button appears when input has value', async () => {
|
|
const user = userEvent.setup();
|
|
render(<SearchInputDemo />);
|
|
const input = screen.getByPlaceholderText('Search accounts') as HTMLInputElement;
|
|
await user.type(input, 'Query');
|
|
const container = input.parentElement as HTMLElement;
|
|
expect(container.querySelector('button')).toBeTruthy();
|
|
});
|
|
|
|
test('clear button clears input value', async () => {
|
|
const user = userEvent.setup();
|
|
render(<SearchInputDemo />);
|
|
const input = screen.getByPlaceholderText('Search accounts') as HTMLInputElement;
|
|
await user.type(input, 'Query');
|
|
const container = input.parentElement as HTMLElement;
|
|
const clearButton = container.querySelector('button') as HTMLButtonElement;
|
|
await user.click(clearButton);
|
|
expect(input.value).toBe('');
|
|
});
|
|
|
|
test('loading state shows spinner instead of clear button', () => {
|
|
render(<SearchInputDemo />);
|
|
const input = screen.getByPlaceholderText('Loading results');
|
|
const container = input.parentElement as HTMLElement;
|
|
expect(container.querySelector('button')).toBeNull();
|
|
expect(container.querySelector('svg')).toBeTruthy();
|
|
});
|
|
|
|
test('size variants render at different scales', () => {
|
|
render(<SearchInputDemo />);
|
|
const sm = screen.getByPlaceholderText('Small') as HTMLInputElement;
|
|
const lg = screen.getByPlaceholderText('Large') as HTMLInputElement;
|
|
expect(Number(sm.style.height.replace('px', ''))).toBeLessThan(Number(lg.style.height.replace('px', '')));
|
|
});
|
|
|
|
test('debounce delays onSearch callback', async () => {
|
|
vi.useFakeTimers();
|
|
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
|
|
render(<SearchInputDemo />);
|
|
const input = screen.getByPlaceholderText('Debounced search') as HTMLInputElement;
|
|
await user.type(input, 'abc');
|
|
expect(screen.getByText(/Debounced value: —/)).toBeInTheDocument();
|
|
vi.advanceTimersByTime(400);
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/Debounced value: abc/)).toBeInTheDocument();
|
|
});
|
|
vi.useRealTimers();
|
|
});
|
|
});
|
|
|
|
describe('Tabs demo', () => {
|
|
test('default variant shows border-bottom indicator', () => {
|
|
render(<TabsDemo />);
|
|
const active = screen.getAllByRole('button', { name: 'Overview' })[0];
|
|
expect(active.style.borderBottom).toContain('var(--alga-primary');
|
|
});
|
|
|
|
test('underline variant shows underline indicator', () => {
|
|
render(<TabsDemo />);
|
|
const active = screen.getAllByRole('button', { name: 'Overview' })[1];
|
|
expect(active.style.borderBottom).toContain('var(--alga-primary');
|
|
});
|
|
|
|
test('disabled tab cannot be selected', async () => {
|
|
render(<TabsDemo />);
|
|
const disabled = screen.getAllByRole('button', { name: 'Settings' })[0] as HTMLButtonElement;
|
|
expect(disabled.disabled).toBe(true);
|
|
});
|
|
|
|
test('tab content changes when tab is selected', async () => {
|
|
const user = userEvent.setup();
|
|
render(<TabsDemo />);
|
|
const details = screen.getAllByRole('button', { name: 'Details' })[0];
|
|
await user.click(details);
|
|
expect(screen.getByText('Details content')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('Drawer demo', () => {
|
|
test('drawer opens from right side', async () => {
|
|
const user = userEvent.setup();
|
|
render(<DrawerDemo />);
|
|
await user.click(screen.getByRole('button', { name: 'Default (400px)' }));
|
|
const drawer = screen.getByRole('dialog');
|
|
expect(drawer.style.right).toBe('0px');
|
|
expect(drawer.style.width).toBe('400px');
|
|
});
|
|
|
|
test('different widths change drawer width', async () => {
|
|
const user = userEvent.setup();
|
|
render(<DrawerDemo />);
|
|
await user.click(screen.getByRole('button', { name: 'Narrow (300px)' }));
|
|
const drawer = screen.getByRole('dialog');
|
|
expect(drawer.style.width).toBe('300px');
|
|
});
|
|
|
|
test('title is displayed in drawer header', async () => {
|
|
const user = userEvent.setup();
|
|
render(<DrawerDemo />);
|
|
await user.click(screen.getByRole('button', { name: 'Default (400px)' }));
|
|
expect(screen.getByText(/Drawer \(400px\)/)).toBeInTheDocument();
|
|
});
|
|
|
|
test('close button closes drawer', async () => {
|
|
const user = userEvent.setup();
|
|
render(<DrawerDemo />);
|
|
await user.click(screen.getByRole('button', { name: 'Default (400px)' }));
|
|
await user.click(screen.getByRole('button', { name: 'Close drawer' }));
|
|
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
|
});
|
|
|
|
test('escape key closes drawer', async () => {
|
|
const user = userEvent.setup();
|
|
render(<DrawerDemo />);
|
|
await user.click(screen.getByRole('button', { name: 'Default (400px)' }));
|
|
await user.keyboard('{Escape}');
|
|
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
|
});
|
|
|
|
test('overlay click closes drawer', async () => {
|
|
const user = userEvent.setup();
|
|
render(<DrawerDemo />);
|
|
await user.click(screen.getByRole('button', { name: 'Default (400px)' }));
|
|
const overlay = document.querySelector('[aria-hidden="true"]') as HTMLElement;
|
|
await user.click(overlay);
|
|
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('DropdownMenu demo', () => {
|
|
test('menu opens on trigger click', async () => {
|
|
const user = userEvent.setup();
|
|
render(<DropdownMenuDemo />);
|
|
await user.click(screen.getByRole('button', { name: 'Open Menu' }));
|
|
expect(screen.getByRole('menu')).toBeInTheDocument();
|
|
});
|
|
|
|
test('menu items are clickable', async () => {
|
|
const user = userEvent.setup();
|
|
render(<DropdownMenuDemo />);
|
|
await user.click(screen.getByRole('button', { name: 'Open Menu' }));
|
|
await user.click(screen.getByText('New item'));
|
|
expect(screen.getByText(/Last action: New item/)).toBeInTheDocument();
|
|
});
|
|
|
|
test('dividers separate menu sections', async () => {
|
|
const user = userEvent.setup();
|
|
render(<DropdownMenuDemo />);
|
|
await user.click(screen.getByRole('button', { name: 'Open Menu' }));
|
|
const menu = screen.getByRole('menu');
|
|
expect(menu.querySelectorAll('div').length).toBeGreaterThan(1);
|
|
});
|
|
|
|
test('disabled items are not clickable', async () => {
|
|
const user = userEvent.setup();
|
|
render(<DropdownMenuDemo />);
|
|
await user.click(screen.getByRole('button', { name: 'Open Menu' }));
|
|
await user.click(screen.getByText('Disabled action'));
|
|
expect(screen.queryByText(/Last action: Disabled action/)).not.toBeInTheDocument();
|
|
});
|
|
|
|
test('danger items have red text', async () => {
|
|
const user = userEvent.setup();
|
|
render(<DropdownMenuDemo />);
|
|
await user.click(screen.getByRole('button', { name: 'Open Menu' }));
|
|
const danger = screen.getByText('Delete item');
|
|
expect(danger.style.color).toContain('var(--alga-danger)');
|
|
});
|
|
|
|
test('right-aligned menu aligns to right edge of trigger', async () => {
|
|
const user = userEvent.setup();
|
|
render(<DropdownMenuDemo />);
|
|
await user.click(screen.getByRole('button', { name: 'Right Align' }));
|
|
const menu = screen.getByRole('menu');
|
|
expect(menu.style.right).toBe('0px');
|
|
});
|
|
|
|
test('menu closes on item click', async () => {
|
|
const user = userEvent.setup();
|
|
render(<DropdownMenuDemo />);
|
|
await user.click(screen.getByRole('button', { name: 'Open Menu' }));
|
|
await user.click(screen.getByText('New item'));
|
|
expect(screen.queryByRole('menu')).not.toBeInTheDocument();
|
|
});
|
|
|
|
test('menu closes on outside click', async () => {
|
|
const user = userEvent.setup();
|
|
render(<DropdownMenuDemo />);
|
|
await user.click(screen.getByRole('button', { name: 'Open Menu' }));
|
|
await user.click(document.body);
|
|
expect(screen.queryByRole('menu')).not.toBeInTheDocument();
|
|
});
|
|
});
|