Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 10 additions & 28 deletions packages/raystack/components/calendar/calendar.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
'use client';

import {
ChevronDownIcon,
ChevronLeftIcon,
ChevronRightIcon,
ChevronUpIcon
} from '@radix-ui/react-icons';
import { ChevronLeftIcon, ChevronRightIcon } from '@radix-ui/react-icons';
import { cva, cx } from 'class-variance-authority';
import { ChangeEvent, ReactNode, useEffect, useState } from 'react';
import {
Expand All @@ -15,7 +10,6 @@ import {
dateLib
} from 'react-day-picker';

import { Flex } from '../flex/flex';
import { IconButton } from '../icon-button';
import { Select } from '../select';
import { Skeleton } from '../skeleton';
Expand Down Expand Up @@ -82,27 +76,15 @@ function DropDown({
<Select.Value />
</Select.Trigger>
<Select.Content className={styles.dropdownContent}>
<Select.ScrollUpButton asChild>
<Flex justify='center'>
<ChevronUpIcon />
</Flex>
</Select.ScrollUpButton>
<Select.Viewport>
{options.map(opt => (
<Select.Item
value={opt.value.toString()}
key={opt.value}
disabled={opt.disabled}
>
{opt.label}
</Select.Item>
))}
</Select.Viewport>
<Select.ScrollDownButton asChild>
<Flex justify='center'>
<ChevronDownIcon />
</Flex>
</Select.ScrollDownButton>
{options.map(opt => (
<Select.Item
value={opt.value.toString()}
key={opt.value}
disabled={opt.disabled}
>
{opt.label}
</Select.Item>
))}
</Select.Content>
</Select>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, expect, it, vi } from 'vitest';

import { ComponentProps } from 'react';
import { describe, expect, it, vi } from 'vitest';
import { CodeBlock } from '../code-block';

// Mock the clipboard API
Expand Down Expand Up @@ -146,13 +145,14 @@ describe('CodeBlock', () => {
it('updates language when language select is changed', () => {
const { rerender } = render(<LanguageSelectCodeBlock value='jsx' />);

expect(screen.getByText('JavaScript')).toBeInTheDocument();
const trigger = screen.getByRole('combobox');
expect(trigger).toHaveTextContent('JavaScript');
expect(screen.getByText('function')).toBeInTheDocument();
expect(screen.queryByText('def')).not.toBeInTheDocument();

rerender(<LanguageSelectCodeBlock value='python' />);

expect(screen.getByText('Python')).toBeInTheDocument();
expect(trigger).toHaveTextContent('Python');
expect(screen.getByText('def')).toBeInTheDocument();
expect(screen.queryByText('function')).not.toBeInTheDocument();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,9 @@ describe('FilterChip', () => {
<FilterChip label='Name' columnType={FilterType.string} />
);

const input = container.querySelector('input');
const input = container.querySelector(
`.${styles.inputFieldWrapper} input`
);
expect(input).toBeInTheDocument();
});

Expand All @@ -96,7 +98,9 @@ describe('FilterChip', () => {
/>
);

const input = container.querySelector('input') as HTMLInputElement;
const input = container.querySelector(
`.${styles.inputFieldWrapper} input`
) as HTMLInputElement;
fireEvent.change(input, { target: { value: 'test value' } });

expect(onValueChange).toHaveBeenCalledWith(
Expand All @@ -114,7 +118,9 @@ describe('FilterChip', () => {
/>
);

const input = container.querySelector('input');
const input = container.querySelector(
`.${styles.inputFieldWrapper} input`
);
expect(input).toHaveValue('initial value');
});
});
Expand Down
164 changes: 98 additions & 66 deletions packages/raystack/components/select/__tests__/select.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { act, fireEvent, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, expect, it, vi } from 'vitest';
import { Select } from '../select';
Expand All @@ -10,6 +10,12 @@ Object.defineProperty(Element.prototype, 'scrollIntoView', {
writable: true
});

const flushMicrotasks = async () => {
await act(async () => {
await new Promise(r => setTimeout(r, 0));
});
};

const TRIGGER_TEXT = 'Select a fruit';
const FRUIT_OPTIONS = [
{ value: 'apple', label: 'Apple' },
Expand Down Expand Up @@ -37,8 +43,11 @@ const BasicSelect = ({ ...props }: SelectRootProps) => {
</Select>
);
};
const renderAndOpenSelect = async (Select: any) => {
await fireEvent.click(render(Select).getByRole('combobox'));

const openSelect = async () => {
const trigger = screen.getByRole('combobox');
fireEvent.click(trigger);
await flushMicrotasks();
};

describe('Select', () => {
Expand All @@ -63,63 +72,63 @@ describe('Select', () => {
expect(trigger).toHaveClass('custom-trigger');
});

it('does not show content initially', () => {
render(<BasicSelect />);
FRUIT_OPTIONS.forEach(option => {
expect(screen.queryByText(option.label)).not.toBeInTheDocument();
});
});

it('shows content when trigger is clicked', async () => {
await renderAndOpenSelect(<BasicSelect />);
render(<BasicSelect />);
await openSelect();

expect(screen.getByRole('listbox')).toBeInTheDocument();
FRUIT_OPTIONS.forEach(option => {
expect(screen.getByText(option.label)).toBeInTheDocument();
});
});

it('renders in portal', async () => {
await renderAndOpenSelect(<BasicSelect />);

const content = screen.getByRole('listbox');
expect(content.closest('body')).toBe(document.body);
});
});

describe('Single Selection', () => {
it('displays selected value', () => {
it('displays selected value in trigger', async () => {
render(<BasicSelect defaultValue='apple' />);
expect(screen.getByText('Apple')).toBeInTheDocument();
await flushMicrotasks();

const trigger = screen.getByRole('combobox');
expect(trigger).toHaveTextContent('Apple');
});

it('works as controlled component', () => {
it('works as controlled component', async () => {
const handleValueChange = vi.fn();
render(<BasicSelect value='apple' onValueChange={handleValueChange} />);
expect(screen.getByText('Apple')).toBeInTheDocument();
await flushMicrotasks();

const trigger = screen.getByRole('combobox');
expect(trigger).toHaveTextContent('Apple');
});

it('selects option when clicked', async () => {
const handleValueChange = vi.fn();
renderAndOpenSelect(
render(
<BasicSelect defaultValue='apple' onValueChange={handleValueChange} />
);
await openSelect();

const options = await screen.findAllByRole('option');
fireEvent.click(options[1]);
const options = screen.getAllByRole('option');
await act(async () => {
await userEvent.click(options[1]);
});
await flushMicrotasks();

expect(handleValueChange).toHaveBeenCalledWith('banana');
expect(handleValueChange).toHaveBeenCalledTimes(1);
expect(screen.getByText('Banana')).toBeInTheDocument();
});

it('closes content after selection', async () => {
renderAndOpenSelect(<BasicSelect />);
render(<BasicSelect />);
await openSelect();

expect(screen.getByRole('listbox')).toBeInTheDocument();

const options = await screen.findAllByRole('option');
fireEvent.click(options[1]);
const options = screen.getAllByRole('option');
await act(async () => {
await userEvent.click(options[1]);
});
await flushMicrotasks();

expect(screen.queryByRole('listbox')).not.toBeInTheDocument();
});
Expand All @@ -128,15 +137,21 @@ describe('Select', () => {
describe('Multiple Selection', () => {
it('supports multiple selection', async () => {
const handleValueChange = vi.fn();
renderAndOpenSelect(
<BasicSelect multiple onValueChange={handleValueChange} />
);
const options = await screen.findAllByRole('option');
render(<BasicSelect multiple onValueChange={handleValueChange} />);
await openSelect();

const options = screen.getAllByRole('option');

fireEvent.click(options[1]);
await act(async () => {
await userEvent.click(options[1]);
});
await flushMicrotasks();
expect(handleValueChange).toHaveBeenCalledWith(['banana']);

fireEvent.click(options[4]);
await act(async () => {
await userEvent.click(options[4]);
});
await flushMicrotasks();
expect(handleValueChange).toHaveBeenCalledWith(['banana', 'pineapple']);

expect(options[1]).toHaveAttribute('aria-selected', 'true');
Expand All @@ -145,15 +160,21 @@ describe('Select', () => {

it('allows deselecting items in multiple mode', async () => {
const handleValueChange = vi.fn();
renderAndOpenSelect(
<BasicSelect multiple onValueChange={handleValueChange} />
);
const options = await screen.findAllByRole('option');
render(<BasicSelect multiple onValueChange={handleValueChange} />);
await openSelect();

fireEvent.click(options[1]);
const options = screen.getAllByRole('option');

await act(async () => {
await userEvent.click(options[1]);
});
await flushMicrotasks();
expect(handleValueChange).toHaveBeenCalledWith(['banana']);

fireEvent.click(options[1]);
await act(async () => {
await userEvent.click(options[1]);
});
await flushMicrotasks();
expect(handleValueChange).toHaveBeenCalledWith([]);
});
});
Expand Down Expand Up @@ -183,7 +204,8 @@ describe('Select', () => {

it('closes with Escape key', async () => {
const user = userEvent.setup();
renderAndOpenSelect(<BasicSelect />);
render(<BasicSelect />);
await openSelect();

await user.keyboard('{Escape}');

Expand All @@ -193,54 +215,64 @@ describe('Select', () => {
it('selects option with Enter key', async () => {
const user = userEvent.setup();
const handleValueChange = vi.fn();
renderAndOpenSelect(
render(
<BasicSelect defaultValue='apple' onValueChange={handleValueChange} />
);
await openSelect();

const options = await screen.findAllByRole('option');
options[1].focus();
const options = screen.getAllByRole('option');
await act(() => options[1].focus());
await user.keyboard('{Enter}');
await flushMicrotasks();

expect(handleValueChange).toHaveBeenCalledWith('banana');
expect(handleValueChange).toHaveBeenCalledTimes(1);
expect(screen.getByText('Banana')).toBeInTheDocument();
});

it('navigates options with arrow keys', async () => {
const user = userEvent.setup();
renderAndOpenSelect(<BasicSelect defaultValue='apple' />);
const handleValueChange = vi.fn();
render(
<BasicSelect defaultValue='apple' onValueChange={handleValueChange} />
);
await openSelect();

await user.keyboard('{ArrowDown}{ArrowDown}{Enter}');
await flushMicrotasks();

expect(screen.getByText('Blueberry')).toBeInTheDocument();
expect(handleValueChange).toHaveBeenCalledWith('blueberry');
});
});

describe('Autocomplete Mode', () => {
it('renders search input in autocomplete mode', async () => {
renderAndOpenSelect(<BasicSelect autocomplete />);
render(<BasicSelect autocomplete />);

expect(screen.getByRole('dialog')).toBeInTheDocument();
expect(screen.getByRole('combobox')).toBeInTheDocument();
expect(screen.getByRole('combobox')).toHaveAttribute(
'placeholder',
'Search...'
);
const trigger = screen.getByLabelText('Select option');
fireEvent.click(trigger);
await flushMicrotasks();

expect(screen.getByRole('listbox')).toBeInTheDocument();
const searchInput = screen.getByPlaceholderText('Search...');
expect(searchInput).toBeInTheDocument();
});
});

it('filters options based on search', async () => {
const user = userEvent.setup();
renderAndOpenSelect(<BasicSelect autocomplete />);
it('filters options based on search', async () => {
const user = userEvent.setup();
render(<BasicSelect autocomplete />);

expect(screen.getByRole('dialog')).toBeInTheDocument();
const trigger = screen.getByLabelText('Select option');
fireEvent.click(trigger);
await flushMicrotasks();

const searchInput = screen.getByPlaceholderText('Search...');
await user.type(searchInput, 'app');
const searchInput = screen.getByPlaceholderText('Search...');
await user.type(searchInput, 'app');
await flushMicrotasks();

const options = await screen.findAllByRole('option');
expect(options.length).toBe(2);
expect(options[0].textContent).toBe('Apple');
expect(options[1].textContent).toBe('Pineapple');
const options = screen.getAllByRole('option');
expect(options.length).toBe(2);
expect(options[0]).toHaveTextContent('Apple');
expect(options[1]).toHaveTextContent('Pineapple');
});
});
});
Loading