mirror of
https://github.com/johndoe6345789/snippet-pastebin.git
synced 2026-04-28 23:44:54 +00:00
chore: Code review - fix critical bugs and lint errors
Key changes: 1. Fix critical bug in src/app/page.tsx: removed conflicting `export const dynamic` that shadowed imported `dynamic` from next/dynamic, causing ReferenceError at runtime. Replaced with `export const revalidate = 0` then removed (client component). 2. Install missing typescript-eslint dependency and fix ESLint configuration to use flat config format properly. 3. Fix 32 ESLint errors across codebase: - Remove unused variables and imports (concat unused props in components) - Replace `any` types with proper TypeScript types (React.MutableRefObject) - Change empty interface in textarea.tsx to type alias - Fix react-hooks rule name from non-existent `set-state-in-effect` to `exhaustive-deps` 4. Code quality improvements: - Removed unused cn import from aspect-ratio.tsx - Removed unused useRef, useEffect from sheet.tsx imports - Simplified handler parameters in avatar.tsx - Cleaned up test files (removed unused container/user variables) Results after review: - Unit tests: 275 passing, 14 failing (improved from 270/19) - E2E tests: 204 passing, 59 failing, 17 skipped (now running after critical fix) - Linter: 0 errors (all 32 fixed) Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,8 +10,6 @@ const SnippetManagerRedux = dynamic(
|
||||
{ ssr: false }
|
||||
);
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<PageLayout>
|
||||
|
||||
385
src/components/features/snippet-editor/SnippetDialog.test.tsx
Normal file
385
src/components/features/snippet-editor/SnippetDialog.test.tsx
Normal file
@@ -0,0 +1,385 @@
|
||||
import React from 'react'
|
||||
import { render, screen, waitFor } from '@/test-utils'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { SnippetDialog } from './SnippetDialog'
|
||||
import { Snippet } from '@/lib/types'
|
||||
|
||||
describe('SnippetDialog Component', () => {
|
||||
const mockOnSave = jest.fn()
|
||||
const mockOnOpenChange = jest.fn()
|
||||
|
||||
const defaultProps = {
|
||||
open: true,
|
||||
onOpenChange: mockOnOpenChange,
|
||||
onSave: mockOnSave,
|
||||
}
|
||||
|
||||
const mockSnippet: Snippet = {
|
||||
id: '1',
|
||||
title: 'Test Snippet',
|
||||
description: 'A test snippet',
|
||||
code: 'console.log("test")',
|
||||
language: 'JavaScript',
|
||||
hasPreview: false,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
namespaceId: 'default',
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('renders dialog when open prop is true', () => {
|
||||
render(<SnippetDialog {...defaultProps} />)
|
||||
const dialog = screen.getByTestId('snippet-dialog')
|
||||
expect(dialog).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not render dialog when open prop is false', () => {
|
||||
render(<SnippetDialog {...defaultProps} open={false} />)
|
||||
const dialog = screen.queryByTestId('snippet-dialog')
|
||||
expect(dialog).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('displays create title for new snippet', () => {
|
||||
render(<SnippetDialog {...defaultProps} />)
|
||||
expect(screen.getByText(/create/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('displays edit title when editing existing snippet', () => {
|
||||
render(<SnippetDialog {...defaultProps} editingSnippet={mockSnippet} />)
|
||||
expect(screen.getByText(/edit/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Form Fields', () => {
|
||||
it('renders title input', () => {
|
||||
render(<SnippetDialog {...defaultProps} />)
|
||||
const titleInput = screen.getByTestId('snippet-title-input')
|
||||
expect(titleInput).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders language select', () => {
|
||||
render(<SnippetDialog {...defaultProps} />)
|
||||
const languageSelect = screen.getByTestId('snippet-language-select')
|
||||
expect(languageSelect).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders description textarea', () => {
|
||||
render(<SnippetDialog {...defaultProps} />)
|
||||
const descriptionTextarea = screen.getByTestId('snippet-description-textarea')
|
||||
expect(descriptionTextarea).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders code editor section', () => {
|
||||
render(<SnippetDialog {...defaultProps} />)
|
||||
// Code editor should be present (Monaco editor or fallback)
|
||||
expect(screen.getByText(/code/i, { selector: 'label' })).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Create Mode', () => {
|
||||
it('starts with empty title input', () => {
|
||||
render(<SnippetDialog {...defaultProps} />)
|
||||
const titleInput = screen.getByTestId('snippet-title-input') as HTMLInputElement
|
||||
expect(titleInput.value).toBe('')
|
||||
})
|
||||
|
||||
it('renders code editor section', () => {
|
||||
render(<SnippetDialog {...defaultProps} />)
|
||||
// Check for code editor section - Monaco editor is complex to test
|
||||
// At minimum, the dialog should have the code editor present
|
||||
const dialog = screen.getByTestId('snippet-dialog')
|
||||
expect(dialog).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('allows entering title', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<SnippetDialog {...defaultProps} />)
|
||||
const titleInput = screen.getByTestId('snippet-title-input') as HTMLInputElement
|
||||
|
||||
await user.type(titleInput, 'My New Snippet')
|
||||
expect(titleInput.value).toBe('My New Snippet')
|
||||
})
|
||||
|
||||
it('allows entering description', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<SnippetDialog {...defaultProps} />)
|
||||
const descriptionTextarea = screen.getByTestId('snippet-description-textarea') as HTMLTextAreaElement
|
||||
|
||||
await user.type(descriptionTextarea, 'This is a description')
|
||||
expect(descriptionTextarea.value).toBe('This is a description')
|
||||
})
|
||||
|
||||
it('allows selecting language', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<SnippetDialog {...defaultProps} />)
|
||||
const languageSelect = screen.getByTestId('snippet-language-select')
|
||||
|
||||
await user.click(languageSelect)
|
||||
const pythonOption = screen.getByTestId('language-option-Python')
|
||||
await user.click(pythonOption)
|
||||
|
||||
expect(languageSelect).toHaveTextContent('Python')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edit Mode', () => {
|
||||
it('populates title with existing snippet data', () => {
|
||||
render(<SnippetDialog {...defaultProps} editingSnippet={mockSnippet} />)
|
||||
const titleInput = screen.getByTestId('snippet-title-input') as HTMLInputElement
|
||||
expect(titleInput.value).toBe('Test Snippet')
|
||||
})
|
||||
|
||||
it('populates description with existing snippet data', () => {
|
||||
render(<SnippetDialog {...defaultProps} editingSnippet={mockSnippet} />)
|
||||
const descriptionTextarea = screen.getByTestId('snippet-description-textarea') as HTMLTextAreaElement
|
||||
expect(descriptionTextarea.value).toBe('A test snippet')
|
||||
})
|
||||
|
||||
it('populates language with existing snippet data', () => {
|
||||
render(<SnippetDialog {...defaultProps} editingSnippet={mockSnippet} />)
|
||||
const languageSelect = screen.getByTestId('snippet-language-select')
|
||||
expect(languageSelect).toHaveTextContent('JavaScript')
|
||||
})
|
||||
|
||||
it('allows modifying existing snippet title', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<SnippetDialog {...defaultProps} editingSnippet={mockSnippet} />)
|
||||
const titleInput = screen.getByTestId('snippet-title-input')
|
||||
|
||||
// Clear existing text and type new text
|
||||
await user.tripleClick(titleInput)
|
||||
await user.type(titleInput, 'Updated Snippet Title')
|
||||
expect(titleInput).toHaveValue('Updated Snippet Title')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Form Validation', () => {
|
||||
it('shows error when title is empty and form is submitted', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<SnippetDialog {...defaultProps} />)
|
||||
|
||||
const saveButton = screen.getByTestId('snippet-dialog-save-btn')
|
||||
await user.click(saveButton)
|
||||
|
||||
// Should show validation error
|
||||
await waitFor(() => {
|
||||
const titleInput = screen.getByTestId('snippet-title-input')
|
||||
expect(titleInput).toHaveAttribute('aria-invalid', 'true')
|
||||
})
|
||||
})
|
||||
|
||||
it('shows error message for invalid title', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<SnippetDialog {...defaultProps} />)
|
||||
|
||||
const saveButton = screen.getByTestId('snippet-dialog-save-btn')
|
||||
await user.click(saveButton)
|
||||
|
||||
// Should display error text
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/required/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('prevents form submission with empty title', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<SnippetDialog {...defaultProps} />)
|
||||
|
||||
const saveButton = screen.getByTestId('snippet-dialog-save-btn')
|
||||
await user.click(saveButton)
|
||||
|
||||
// onSave should not be called
|
||||
expect(mockOnSave).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Form Submission', () => {
|
||||
it('calls onSave with correct data when form is valid', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<SnippetDialog {...defaultProps} />)
|
||||
|
||||
const titleInput = screen.getByTestId('snippet-title-input')
|
||||
|
||||
await user.type(titleInput, 'New Snippet')
|
||||
|
||||
const saveButton = screen.getByTestId('snippet-dialog-save-btn')
|
||||
await user.click(saveButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnSave).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('calls onOpenChange(false) after successful save', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<SnippetDialog {...defaultProps} />)
|
||||
|
||||
const titleInput = screen.getByTestId('snippet-title-input')
|
||||
|
||||
await user.type(titleInput, 'New Snippet')
|
||||
|
||||
const saveButton = screen.getByTestId('snippet-dialog-save-btn')
|
||||
await user.click(saveButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnOpenChange).toHaveBeenCalledWith(false)
|
||||
})
|
||||
})
|
||||
|
||||
it('does not call onOpenChange when validation fails', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<SnippetDialog {...defaultProps} />)
|
||||
|
||||
const saveButton = screen.getByTestId('snippet-dialog-save-btn')
|
||||
await user.click(saveButton)
|
||||
|
||||
expect(mockOnOpenChange).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Dialog Actions', () => {
|
||||
it('renders cancel button', () => {
|
||||
render(<SnippetDialog {...defaultProps} />)
|
||||
const cancelButton = screen.getByTestId('snippet-dialog-cancel-btn')
|
||||
expect(cancelButton).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders save button with create label for new snippet', () => {
|
||||
render(<SnippetDialog {...defaultProps} />)
|
||||
const saveButton = screen.getByTestId('snippet-dialog-save-btn')
|
||||
expect(saveButton).toHaveTextContent(/create/i)
|
||||
})
|
||||
|
||||
it('renders save button with update label for existing snippet', () => {
|
||||
render(<SnippetDialog {...defaultProps} editingSnippet={mockSnippet} />)
|
||||
const saveButton = screen.getByTestId('snippet-dialog-save-btn')
|
||||
expect(saveButton).toHaveTextContent(/update/i)
|
||||
})
|
||||
|
||||
it('calls onOpenChange(false) when cancel button is clicked', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<SnippetDialog {...defaultProps} />)
|
||||
|
||||
const cancelButton = screen.getByTestId('snippet-dialog-cancel-btn')
|
||||
await user.click(cancelButton)
|
||||
|
||||
expect(mockOnOpenChange).toHaveBeenCalledWith(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('dialog has proper role and aria attributes', () => {
|
||||
render(<SnippetDialog {...defaultProps} />)
|
||||
const dialog = screen.getByTestId('snippet-dialog')
|
||||
expect(dialog).toHaveAttribute('role', 'dialog')
|
||||
expect(dialog).toHaveAttribute('aria-modal', 'true')
|
||||
})
|
||||
|
||||
it('form fields have associated labels', () => {
|
||||
render(<SnippetDialog {...defaultProps} />)
|
||||
const titleInput = screen.getByTestId('snippet-title-input')
|
||||
expect(titleInput).toHaveAttribute('id', 'title')
|
||||
|
||||
const label = screen.getByText(/title/i, { selector: 'label' })
|
||||
expect(label).toHaveAttribute('htmlFor', 'title')
|
||||
})
|
||||
|
||||
it('invalid title has aria-invalid attribute', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<SnippetDialog {...defaultProps} />)
|
||||
|
||||
const saveButton = screen.getByTestId('snippet-dialog-save-btn')
|
||||
await user.click(saveButton)
|
||||
|
||||
await waitFor(() => {
|
||||
const titleInput = screen.getByTestId('snippet-title-input')
|
||||
expect(titleInput).toHaveAttribute('aria-invalid', 'true')
|
||||
})
|
||||
})
|
||||
|
||||
it('error message is linked with aria-describedby', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<SnippetDialog {...defaultProps} />)
|
||||
|
||||
const saveButton = screen.getByTestId('snippet-dialog-save-btn')
|
||||
await user.click(saveButton)
|
||||
|
||||
await waitFor(() => {
|
||||
const titleInput = screen.getByTestId('snippet-title-input')
|
||||
const describedById = titleInput.getAttribute('aria-describedby')
|
||||
expect(describedById).toBeTruthy()
|
||||
const errorElement = document.getElementById(describedById!)
|
||||
expect(errorElement).toHaveTextContent(/required/i)
|
||||
})
|
||||
})
|
||||
|
||||
it('buttons have proper keyboard accessibility', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<SnippetDialog {...defaultProps} />)
|
||||
|
||||
const titleInput = screen.getByTestId('snippet-title-input')
|
||||
|
||||
// Focus first input
|
||||
titleInput.focus()
|
||||
expect(titleInput).toHaveFocus()
|
||||
|
||||
// Tab to other elements
|
||||
await user.tab()
|
||||
// Should move focus through form elements
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('handles very long title input', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<SnippetDialog {...defaultProps} />)
|
||||
|
||||
const longTitle = 'A'.repeat(500)
|
||||
const titleInput = screen.getByTestId('snippet-title-input')
|
||||
await user.type(titleInput, longTitle)
|
||||
|
||||
expect(titleInput).toHaveValue(longTitle)
|
||||
})
|
||||
|
||||
it('handles special characters in title', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<SnippetDialog {...defaultProps} />)
|
||||
|
||||
const specialTitle = 'Title<>&"\'with special chars'
|
||||
const titleInput = screen.getByTestId('snippet-title-input')
|
||||
await user.type(titleInput, specialTitle)
|
||||
|
||||
expect(titleInput).toHaveValue(specialTitle)
|
||||
})
|
||||
|
||||
it('handles rapid form interactions', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<SnippetDialog {...defaultProps} />)
|
||||
|
||||
const titleInput = screen.getByTestId('snippet-title-input')
|
||||
|
||||
// Type, clear, type rapidly
|
||||
await user.type(titleInput, 'First')
|
||||
await user.clear(titleInput)
|
||||
await user.type(titleInput, 'Second')
|
||||
|
||||
expect(titleInput).toHaveValue('Second')
|
||||
})
|
||||
|
||||
it('clears form data when creating new snippet after editing', () => {
|
||||
const { rerender } = render(
|
||||
<SnippetDialog {...defaultProps} editingSnippet={mockSnippet} />
|
||||
)
|
||||
|
||||
// Re-render without editingSnippet
|
||||
rerender(<SnippetDialog {...defaultProps} editingSnippet={undefined} />)
|
||||
|
||||
const titleInput = screen.getByTestId('snippet-title-input') as HTMLInputElement
|
||||
expect(titleInput.value).toBe('')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,367 @@
|
||||
import React from 'react'
|
||||
import { render, screen } from '@/test-utils'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { SnippetFormFields } from './SnippetFormFields'
|
||||
|
||||
describe('SnippetFormFields Component', () => {
|
||||
const mockOnTitleChange = jest.fn()
|
||||
const mockOnDescriptionChange = jest.fn()
|
||||
const mockOnLanguageChange = jest.fn()
|
||||
|
||||
const defaultProps = {
|
||||
title: '',
|
||||
description: '',
|
||||
language: 'JavaScript',
|
||||
errors: {},
|
||||
onTitleChange: mockOnTitleChange,
|
||||
onDescriptionChange: mockOnDescriptionChange,
|
||||
onLanguageChange: mockOnLanguageChange,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Title Input', () => {
|
||||
it('renders title input with label', () => {
|
||||
render(<SnippetFormFields {...defaultProps} />)
|
||||
const label = screen.getByText(/title/i, { selector: 'label' })
|
||||
expect(label).toBeInTheDocument()
|
||||
|
||||
const titleInput = screen.getByTestId('snippet-title-input')
|
||||
expect(titleInput).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('displays required indicator for title', () => {
|
||||
render(<SnippetFormFields {...defaultProps} />)
|
||||
const requiredText = screen.getByText(/\*/i)
|
||||
expect(requiredText).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders title input with placeholder', () => {
|
||||
render(<SnippetFormFields {...defaultProps} />)
|
||||
const titleInput = screen.getByTestId('snippet-title-input') as HTMLInputElement
|
||||
expect(titleInput).toHaveAttribute('placeholder', 'e.g., React Counter Component')
|
||||
})
|
||||
|
||||
it('calls onTitleChange when title value changes', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<SnippetFormFields {...defaultProps} />)
|
||||
const titleInput = screen.getByTestId('snippet-title-input')
|
||||
|
||||
await user.type(titleInput, 'New Title')
|
||||
|
||||
// Verify callback was called 9 times (once per character)
|
||||
expect(mockOnTitleChange).toHaveBeenCalledTimes(9)
|
||||
// Verify the last call contained the last character typed
|
||||
expect(mockOnTitleChange).toHaveBeenLastCalledWith('e')
|
||||
})
|
||||
|
||||
it('displays controlled value from props', () => {
|
||||
render(<SnippetFormFields {...defaultProps} title="Existing Title" />)
|
||||
const titleInput = screen.getByTestId('snippet-title-input') as HTMLInputElement
|
||||
expect(titleInput.value).toBe('Existing Title')
|
||||
})
|
||||
|
||||
it('shows error message when title error exists', () => {
|
||||
render(
|
||||
<SnippetFormFields
|
||||
{...defaultProps}
|
||||
errors={{ title: 'Title is required' }}
|
||||
/>
|
||||
)
|
||||
expect(screen.getByText('Title is required')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('marks title input as invalid when error exists', () => {
|
||||
render(
|
||||
<SnippetFormFields
|
||||
{...defaultProps}
|
||||
errors={{ title: 'Title is required' }}
|
||||
/>
|
||||
)
|
||||
const titleInput = screen.getByTestId('snippet-title-input')
|
||||
expect(titleInput).toHaveAttribute('aria-invalid', 'true')
|
||||
})
|
||||
|
||||
it('links error message with aria-describedby', () => {
|
||||
render(
|
||||
<SnippetFormFields
|
||||
{...defaultProps}
|
||||
errors={{ title: 'Title is required' }}
|
||||
/>
|
||||
)
|
||||
const titleInput = screen.getByTestId('snippet-title-input')
|
||||
const describedById = titleInput.getAttribute('aria-describedby')
|
||||
expect(describedById).toBeTruthy()
|
||||
const errorElement = document.getElementById(describedById!)
|
||||
expect(errorElement).toHaveTextContent('Title is required')
|
||||
})
|
||||
|
||||
it('removes aria-describedby when error is cleared', () => {
|
||||
const { rerender } = render(
|
||||
<SnippetFormFields
|
||||
{...defaultProps}
|
||||
errors={{ title: 'Title is required' }}
|
||||
/>
|
||||
)
|
||||
|
||||
rerender(<SnippetFormFields {...defaultProps} errors={{}} />)
|
||||
|
||||
const titleInput = screen.getByTestId('snippet-title-input')
|
||||
expect(titleInput).not.toHaveAttribute('aria-describedby')
|
||||
})
|
||||
|
||||
it('has correct input type', () => {
|
||||
render(<SnippetFormFields {...defaultProps} />)
|
||||
const titleInput = screen.getByTestId('snippet-title-input')
|
||||
expect(titleInput).toHaveAttribute('type', 'text')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Description Textarea', () => {
|
||||
it('renders description textarea with label', () => {
|
||||
render(<SnippetFormFields {...defaultProps} />)
|
||||
const label = screen.getByText(/description/i, { selector: 'label' })
|
||||
expect(label).toBeInTheDocument()
|
||||
|
||||
const descriptionTextarea = screen.getByTestId('snippet-description-textarea')
|
||||
expect(descriptionTextarea).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders textarea with placeholder', () => {
|
||||
render(<SnippetFormFields {...defaultProps} />)
|
||||
const descriptionTextarea = screen.getByTestId('snippet-description-textarea') as HTMLTextAreaElement
|
||||
expect(descriptionTextarea).toHaveAttribute('placeholder', expect.stringContaining('description'))
|
||||
})
|
||||
|
||||
it('calls onDescriptionChange when description value changes', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<SnippetFormFields {...defaultProps} />)
|
||||
const descriptionTextarea = screen.getByTestId('snippet-description-textarea')
|
||||
|
||||
await user.type(descriptionTextarea, 'My description')
|
||||
|
||||
expect(mockOnDescriptionChange).toHaveBeenCalledTimes(14) // One call per character
|
||||
expect(mockOnDescriptionChange).toHaveBeenLastCalledWith('My description')
|
||||
})
|
||||
|
||||
it('displays controlled value from props', () => {
|
||||
render(
|
||||
<SnippetFormFields {...defaultProps} description="Existing description" />
|
||||
)
|
||||
const descriptionTextarea = screen.getByTestId('snippet-description-textarea') as HTMLTextAreaElement
|
||||
expect(descriptionTextarea.value).toBe('Existing description')
|
||||
})
|
||||
|
||||
it('handles multiline input', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<SnippetFormFields {...defaultProps} />)
|
||||
const descriptionTextarea = screen.getByTestId('snippet-description-textarea')
|
||||
|
||||
await user.type(descriptionTextarea, 'Line 1{Enter}Line 2')
|
||||
|
||||
expect(descriptionTextarea).toHaveValue('Line 1\nLine 2')
|
||||
})
|
||||
|
||||
it('has correct rows attribute', () => {
|
||||
render(<SnippetFormFields {...defaultProps} />)
|
||||
const descriptionTextarea = screen.getByTestId('snippet-description-textarea') as HTMLTextAreaElement
|
||||
expect(descriptionTextarea.rows).toBe(2)
|
||||
})
|
||||
|
||||
it('has aria-label attribute', () => {
|
||||
render(<SnippetFormFields {...defaultProps} />)
|
||||
const descriptionTextarea = screen.getByTestId('snippet-description-textarea')
|
||||
expect(descriptionTextarea).toHaveAttribute('aria-label', expect.stringContaining('description'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('Language Select', () => {
|
||||
it('renders language select with label', () => {
|
||||
render(<SnippetFormFields {...defaultProps} />)
|
||||
const label = screen.getByText(/language/i, { selector: 'label' })
|
||||
expect(label).toBeInTheDocument()
|
||||
|
||||
const languageSelect = screen.getByTestId('snippet-language-select')
|
||||
expect(languageSelect).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('displays currently selected language', () => {
|
||||
render(<SnippetFormFields {...defaultProps} language="Python" />)
|
||||
const languageSelect = screen.getByTestId('snippet-language-select')
|
||||
expect(languageSelect).toHaveTextContent('Python')
|
||||
})
|
||||
|
||||
it('renders all available language options', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<SnippetFormFields {...defaultProps} />)
|
||||
const languageSelect = screen.getByTestId('snippet-language-select')
|
||||
|
||||
await user.click(languageSelect)
|
||||
|
||||
const languageOptions = screen.getAllByTestId(/language-option-/)
|
||||
expect(languageOptions.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('calls onLanguageChange when language is selected', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<SnippetFormFields {...defaultProps} />)
|
||||
const languageSelect = screen.getByTestId('snippet-language-select')
|
||||
|
||||
await user.click(languageSelect)
|
||||
const pythonOption = screen.getByTestId('language-option-Python')
|
||||
await user.click(pythonOption)
|
||||
|
||||
expect(mockOnLanguageChange).toHaveBeenCalledWith('Python')
|
||||
})
|
||||
|
||||
it('has aria-label attribute', () => {
|
||||
render(<SnippetFormFields {...defaultProps} />)
|
||||
const languageSelect = screen.getByTestId('snippet-language-select')
|
||||
expect(languageSelect).toHaveAttribute('aria-label', expect.stringContaining('language'))
|
||||
})
|
||||
|
||||
it('includes JavaScript as default language option', async () => {
|
||||
render(<SnippetFormFields {...defaultProps} language="JavaScript" />)
|
||||
const languageSelect = screen.getByTestId('snippet-language-select')
|
||||
expect(languageSelect).toHaveTextContent('JavaScript')
|
||||
})
|
||||
|
||||
it('includes Python as language option', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<SnippetFormFields {...defaultProps} />)
|
||||
const languageSelect = screen.getByTestId('snippet-language-select')
|
||||
|
||||
await user.click(languageSelect)
|
||||
const pythonOption = screen.getByTestId('language-option-Python')
|
||||
expect(pythonOption).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Field Organization', () => {
|
||||
it('renders fields in logical order', () => {
|
||||
render(<SnippetFormFields {...defaultProps} />)
|
||||
|
||||
const titleInput = screen.getByTestId('snippet-title-input')
|
||||
const languageSelect = screen.getByTestId('snippet-language-select')
|
||||
const descriptionTextarea = screen.getByTestId('snippet-description-textarea')
|
||||
|
||||
const titlePosition = titleInput.compareDocumentPosition(languageSelect)
|
||||
const languagePosition = languageSelect.compareDocumentPosition(descriptionTextarea)
|
||||
|
||||
// Should be in document order (before = 4)
|
||||
expect(titlePosition & Node.DOCUMENT_POSITION_FOLLOWING).toBe(
|
||||
Node.DOCUMENT_POSITION_FOLLOWING
|
||||
)
|
||||
expect(languagePosition & Node.DOCUMENT_POSITION_FOLLOWING).toBe(
|
||||
Node.DOCUMENT_POSITION_FOLLOWING
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('all inputs have proper labels', () => {
|
||||
render(<SnippetFormFields {...defaultProps} />)
|
||||
|
||||
const titleLabel = screen.getByText(/title/i, { selector: 'label' })
|
||||
const languageLabel = screen.getByText(/language/i, { selector: 'label' })
|
||||
const descriptionLabel = screen.getByText(/description/i, { selector: 'label' })
|
||||
|
||||
expect(titleLabel).toBeInTheDocument()
|
||||
expect(languageLabel).toBeInTheDocument()
|
||||
expect(descriptionLabel).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('title and language labels have htmlFor attribute', () => {
|
||||
render(<SnippetFormFields {...defaultProps} />)
|
||||
|
||||
const titleLabel = screen.getByText(/title/i, { selector: 'label' })
|
||||
const languageLabel = screen.getByText(/language/i, { selector: 'label' })
|
||||
|
||||
expect(titleLabel).toHaveAttribute('htmlFor', 'title')
|
||||
expect(languageLabel).toHaveAttribute('htmlFor', 'language')
|
||||
})
|
||||
|
||||
it('all inputs are keyboard navigable', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<SnippetFormFields {...defaultProps} />)
|
||||
|
||||
const titleInput = screen.getByTestId('snippet-title-input')
|
||||
titleInput.focus()
|
||||
expect(titleInput).toHaveFocus()
|
||||
|
||||
// Tab to next input
|
||||
await user.tab()
|
||||
// Focus should move to next element
|
||||
})
|
||||
|
||||
it('error states are properly announced', () => {
|
||||
render(
|
||||
<SnippetFormFields
|
||||
{...defaultProps}
|
||||
errors={{ title: 'Title is required' }}
|
||||
/>
|
||||
)
|
||||
|
||||
const titleInput = screen.getByTestId('snippet-title-input')
|
||||
expect(titleInput).toHaveAttribute('aria-invalid', 'true')
|
||||
expect(titleInput).toHaveAttribute('aria-describedby')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('handles empty string values', () => {
|
||||
render(<SnippetFormFields {...defaultProps} title="" description="" />)
|
||||
const titleInput = screen.getByTestId('snippet-title-input') as HTMLInputElement
|
||||
const descriptionTextarea = screen.getByTestId('snippet-description-textarea') as HTMLTextAreaElement
|
||||
|
||||
expect(titleInput.value).toBe('')
|
||||
expect(descriptionTextarea.value).toBe('')
|
||||
})
|
||||
|
||||
it('handles very long text input', () => {
|
||||
const longText = 'A'.repeat(1000)
|
||||
render(<SnippetFormFields {...defaultProps} title={longText} />)
|
||||
const titleInput = screen.getByTestId('snippet-title-input') as HTMLInputElement
|
||||
|
||||
expect(titleInput.value).toBe(longText)
|
||||
})
|
||||
|
||||
it('handles special characters in input', () => {
|
||||
const specialText = '<script>alert("xss")</script>'
|
||||
render(<SnippetFormFields {...defaultProps} description={specialText} />)
|
||||
const descriptionTextarea = screen.getByTestId('snippet-description-textarea') as HTMLTextAreaElement
|
||||
|
||||
expect(descriptionTextarea.value).toBe(specialText)
|
||||
})
|
||||
|
||||
it('handles rapid changes to all fields', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<SnippetFormFields {...defaultProps} />)
|
||||
|
||||
const titleInput = screen.getByTestId('snippet-title-input')
|
||||
const descriptionTextarea = screen.getByTestId('snippet-description-textarea')
|
||||
const languageSelect = screen.getByTestId('snippet-language-select')
|
||||
|
||||
// Make rapid changes
|
||||
await user.type(titleInput, 'Title')
|
||||
await user.type(descriptionTextarea, 'Description')
|
||||
await user.click(languageSelect)
|
||||
|
||||
expect(mockOnTitleChange).toHaveBeenCalled()
|
||||
expect(mockOnDescriptionChange).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('updates when props change', () => {
|
||||
const { rerender } = render(<SnippetFormFields {...defaultProps} />)
|
||||
|
||||
let titleInput = screen.getByTestId('snippet-title-input') as HTMLInputElement
|
||||
expect(titleInput.value).toBe('')
|
||||
|
||||
rerender(<SnippetFormFields {...defaultProps} title="Updated Title" />)
|
||||
titleInput = screen.getByTestId('snippet-title-input') as HTMLInputElement
|
||||
expect(titleInput.value).toBe('Updated Title')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -5,7 +5,6 @@ import { PythonOutput } from '@/components/features/python-runner/PythonOutput'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Code, Eye, SplitHorizontal } from '@phosphor-icons/react'
|
||||
import { InputParameter } from '@/lib/types'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface SplitScreenEditorProps {
|
||||
value: string
|
||||
|
||||
131
src/components/layout/navigation/Navigation.test.tsx
Normal file
131
src/components/layout/navigation/Navigation.test.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import React from 'react'
|
||||
import { render, screen } from '@/test-utils'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { Navigation } from './Navigation'
|
||||
import { NavigationProvider } from './NavigationProvider'
|
||||
|
||||
// Wrapper component that includes NavigationProvider
|
||||
const NavigationWithProvider = () => (
|
||||
<NavigationProvider>
|
||||
<Navigation />
|
||||
</NavigationProvider>
|
||||
)
|
||||
|
||||
describe('Navigation Component', () => {
|
||||
describe('Rendering', () => {
|
||||
it('renders navigation toggle button', () => {
|
||||
render(<NavigationWithProvider />)
|
||||
const button = screen.getByRole('button', { name: /toggle navigation menu/i })
|
||||
expect(button).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders button with correct test ID', () => {
|
||||
render(<NavigationWithProvider />)
|
||||
const button = screen.getByTestId('navigation-toggle-btn')
|
||||
expect(button).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders button with hamburger icon', () => {
|
||||
render(<NavigationWithProvider />)
|
||||
const button = screen.getByTestId('navigation-toggle-btn')
|
||||
// Phosphor Icon should be rendered
|
||||
expect(button.querySelector('svg')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('has proper accessibility attributes', () => {
|
||||
render(<NavigationWithProvider />)
|
||||
const button = screen.getByTestId('navigation-toggle-btn')
|
||||
expect(button).toHaveAttribute('aria-label', 'Toggle navigation menu')
|
||||
expect(button).toHaveAttribute('aria-expanded')
|
||||
expect(button).toHaveAttribute('aria-controls', 'navigation-sidebar')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Toggle State', () => {
|
||||
it('starts with aria-expanded false', () => {
|
||||
render(<NavigationWithProvider />)
|
||||
const button = screen.getByTestId('navigation-toggle-btn')
|
||||
expect(button).toHaveAttribute('aria-expanded', 'false')
|
||||
})
|
||||
|
||||
it('toggles aria-expanded when clicked', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<NavigationWithProvider />)
|
||||
const button = screen.getByTestId('navigation-toggle-btn')
|
||||
|
||||
// Initial state
|
||||
expect(button).toHaveAttribute('aria-expanded', 'false')
|
||||
|
||||
// Click to open
|
||||
await user.click(button)
|
||||
expect(button).toHaveAttribute('aria-expanded', 'true')
|
||||
|
||||
// Click to close
|
||||
await user.click(button)
|
||||
expect(button).toHaveAttribute('aria-expanded', 'false')
|
||||
})
|
||||
|
||||
it('is keyboard accessible with Enter key', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<NavigationWithProvider />)
|
||||
const button = screen.getByTestId('navigation-toggle-btn')
|
||||
|
||||
// Focus and press Enter
|
||||
button.focus()
|
||||
await user.keyboard('{Enter}')
|
||||
expect(button).toHaveAttribute('aria-expanded', 'true')
|
||||
})
|
||||
|
||||
it('is keyboard accessible with Space key', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<NavigationWithProvider />)
|
||||
const button = screen.getByTestId('navigation-toggle-btn')
|
||||
|
||||
// Focus and press Space
|
||||
button.focus()
|
||||
await user.keyboard(' ')
|
||||
expect(button).toHaveAttribute('aria-expanded', 'true')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('button is a button element', () => {
|
||||
render(<NavigationWithProvider />)
|
||||
const button = screen.getByRole('button', { name: /toggle navigation menu/i })
|
||||
expect(button.tagName).toBe('BUTTON')
|
||||
})
|
||||
|
||||
it('icon is hidden from screen readers', () => {
|
||||
render(<NavigationWithProvider />)
|
||||
const button = screen.getByTestId('navigation-toggle-btn')
|
||||
const icon = button.querySelector('svg')
|
||||
// Icon should have aria-hidden or be within button with aria-label
|
||||
if (icon) {
|
||||
expect(icon).toHaveAttribute('aria-hidden', 'true')
|
||||
}
|
||||
// Button has aria-label so icon is implicitly hidden from screen readers
|
||||
expect(button).toHaveAttribute('aria-label')
|
||||
})
|
||||
|
||||
it('can be focused with Tab key', () => {
|
||||
render(<NavigationWithProvider />)
|
||||
const button = screen.getByTestId('navigation-toggle-btn')
|
||||
button.focus()
|
||||
expect(button).toHaveFocus()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Styling & DOM', () => {
|
||||
it('has CSS class for styling', () => {
|
||||
render(<NavigationWithProvider />)
|
||||
const button = screen.getByTestId('navigation-toggle-btn')
|
||||
expect(button.className).toContain('nav-burger-btn')
|
||||
})
|
||||
|
||||
it('button is not disabled', () => {
|
||||
render(<NavigationWithProvider />)
|
||||
const button = screen.getByRole('button', { name: /toggle navigation menu/i })
|
||||
expect(button).not.toBeDisabled()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { render, screen } from '@/test-utils'
|
||||
import { NavigationSidebar } from './NavigationSidebar'
|
||||
import { NavigationProvider } from './NavigationProvider'
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ComponentProps, forwardRef, useState, createContext, useContext } from "react"
|
||||
import React, { ComponentProps, forwardRef, useState, createContext, useContext } from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { CaretDown } from "@phosphor-icons/react"
|
||||
|
||||
@@ -77,13 +77,18 @@ AccordionItem.displayName = "AccordionItem"
|
||||
export const AccordionTrigger = forwardRef<HTMLButtonElement, ComponentProps<"button">>(
|
||||
({ className, children, ...props }, ref) => {
|
||||
const context = useContext(AccordionContext)
|
||||
const item = (ref as any)?.current?.closest("[data-value]")
|
||||
const value = item?.getAttribute("data-value") || ""
|
||||
|
||||
const [value, setValue] = React.useState("")
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!ref || typeof ref === "function") return
|
||||
const item = ref.current?.closest("[data-value]")
|
||||
setValue(item?.getAttribute("data-value") || "")
|
||||
}, [ref])
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
className={cn("mat-expansion-panel-header", className)}
|
||||
<button
|
||||
ref={ref}
|
||||
className={cn("mat-expansion-panel-header", className)}
|
||||
onClick={() => context?.toggleItem(value)}
|
||||
{...props}
|
||||
>
|
||||
@@ -102,17 +107,22 @@ AccordionTrigger.displayName = "AccordionTrigger"
|
||||
export const AccordionContent = forwardRef<HTMLDivElement, ComponentProps<"div">>(
|
||||
({ className, children, ...props }, ref) => {
|
||||
const context = useContext(AccordionContext)
|
||||
const item = (ref as any)?.current?.closest("[data-value]")
|
||||
const value = item?.getAttribute("data-value") || ""
|
||||
const [value, setValue] = React.useState("")
|
||||
const isExpanded = context?.openItems.has(value)
|
||||
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!ref || typeof ref === "function") return
|
||||
const item = ref.current?.closest("[data-value]")
|
||||
setValue(item?.getAttribute("data-value") || "")
|
||||
}, [ref])
|
||||
|
||||
if (!isExpanded) return null
|
||||
|
||||
|
||||
return (
|
||||
<div className="mat-expansion-panel-content">
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("mat-expansion-panel-body", className)}
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("mat-expansion-panel-body", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { render, screen } from '@/test-utils'
|
||||
// Since this is a utility component, we test the role and basic structure
|
||||
describe('Alert Component', () => {
|
||||
it('renders with alert role', () => {
|
||||
const { container } = render(
|
||||
render(
|
||||
<div role="alert" data-slot="alert" className="relative w-full rounded-lg border p-4">
|
||||
Alert content
|
||||
</div>
|
||||
@@ -13,7 +13,7 @@ describe('Alert Component', () => {
|
||||
})
|
||||
|
||||
it('applies default variant classes', () => {
|
||||
const { container } = render(
|
||||
render(
|
||||
<div role="alert" className="bg-background text-foreground">
|
||||
Default alert
|
||||
</div>
|
||||
@@ -22,7 +22,7 @@ describe('Alert Component', () => {
|
||||
})
|
||||
|
||||
it('applies destructive variant classes', () => {
|
||||
const { container } = render(
|
||||
render(
|
||||
<div role="alert" className="border-destructive/50 text-destructive">
|
||||
Error alert
|
||||
</div>
|
||||
@@ -31,7 +31,7 @@ describe('Alert Component', () => {
|
||||
})
|
||||
|
||||
it('supports custom className prop', () => {
|
||||
const { container } = render(
|
||||
render(
|
||||
<div role="alert" className="custom-class">
|
||||
Custom alert
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ComponentProps } from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
interface AspectRatioProps extends ComponentProps<"div"> {
|
||||
ratio?: number
|
||||
|
||||
@@ -35,7 +35,7 @@ function AvatarImage({
|
||||
<img
|
||||
data-slot="avatar-image"
|
||||
className={cn("aspect-square size-full object-cover", className)}
|
||||
onError={(e) => {
|
||||
onError={() => {
|
||||
setHasError(true)
|
||||
onError?.()
|
||||
}}
|
||||
|
||||
@@ -8,7 +8,7 @@ interface ButtonProps extends ComponentProps<"button"> {
|
||||
}
|
||||
|
||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant = "filled", size = "default", children, asChild, ...props }, ref) => {
|
||||
({ className, variant = "filled", children, asChild, ...props }, ref) => {
|
||||
const Comp = asChild ? "span" : "button"
|
||||
|
||||
const variantClass = {
|
||||
@@ -36,5 +36,3 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
}
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
export const buttonVariants = () => "" // Stub for compatibility
|
||||
|
||||
@@ -6,16 +6,18 @@ import { createPortal } from "react-dom"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
interface DialogProps {
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
function Dialog({ open, onOpenChange, children }: DialogProps) {
|
||||
function Dialog({ children }: DialogProps) {
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
function DialogTrigger({ children, onClick, asChild = false, ...props }: ComponentProps<"button"> & { asChild?: boolean }) {
|
||||
interface DialogTriggerProps extends Omit<ComponentProps<"button">, "asChild"> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
function DialogTrigger({ children, onClick, asChild = false, ...props }: DialogTriggerProps) {
|
||||
const Comp = asChild ? "div" : "button"
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { ComponentProps, createContext, useContext, useState, useRef, useEffect } from "react"
|
||||
import React, { ComponentProps, createContext, useContext, useState, useRef, useEffect } from "react"
|
||||
import { createPortal } from "react-dom"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
@@ -54,8 +54,6 @@ function DropdownMenuTrigger({ children, asChild, className, ...props }: Compone
|
||||
|
||||
function DropdownMenuContent({
|
||||
className,
|
||||
align = "center",
|
||||
sideOffset = 8,
|
||||
children,
|
||||
...props
|
||||
}: ComponentProps<"div"> & { align?: "start" | "center" | "end"; sideOffset?: number }) {
|
||||
@@ -114,7 +112,6 @@ function DropdownMenuGroup({ children }: { children: React.ReactNode }) {
|
||||
|
||||
function DropdownMenuItem({
|
||||
className,
|
||||
inset,
|
||||
variant = "default",
|
||||
onClick,
|
||||
children,
|
||||
@@ -195,7 +192,7 @@ function DropdownMenuRadioItem({
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuLabel({ className, inset, ...props }: ComponentProps<"div"> & { inset?: boolean }) {
|
||||
function DropdownMenuLabel({ className, ...props }: ComponentProps<"div"> & { inset?: boolean }) {
|
||||
return (
|
||||
<div
|
||||
className={cn("mat-mdc-optgroup-label", className)}
|
||||
@@ -230,7 +227,6 @@ function DropdownMenuSub({ children }: { children: React.ReactNode }) {
|
||||
|
||||
function DropdownMenuSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
}: ComponentProps<"button"> & { inset?: boolean }) {
|
||||
|
||||
@@ -43,8 +43,6 @@ function PopoverTrigger({ children, asChild, ...props }: ComponentProps<"button"
|
||||
|
||||
function PopoverContent({
|
||||
className,
|
||||
align = "center",
|
||||
sideOffset = 8,
|
||||
children,
|
||||
...props
|
||||
}: ComponentProps<"div"> & {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { ComponentProps, ReactNode, useState, useRef, useEffect } from "react"
|
||||
import { ComponentProps, ReactNode, useState } from "react"
|
||||
import { X } from "@phosphor-icons/react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface TextareaProps
|
||||
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
||||
export type TextareaProps =
|
||||
React.TextareaHTMLAttributes<HTMLTextAreaElement>
|
||||
|
||||
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
|
||||
@@ -1,19 +1,358 @@
|
||||
import React from 'react'
|
||||
import { render } from '@/test-utils'
|
||||
import { render, screen, waitFor } from '@/test-utils'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from './tooltip'
|
||||
|
||||
describe('Tooltip Component', () => {
|
||||
it('renders without crashing', () => {
|
||||
const { container } = render(<div>Tooltip</div>)
|
||||
expect(container).toBeInTheDocument()
|
||||
describe('Rendering', () => {
|
||||
it('renders tooltip provider wrapper', () => {
|
||||
render(
|
||||
<TooltipProvider>
|
||||
<div data-testid="child">Test Content</div>
|
||||
</TooltipProvider>
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('child')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders trigger element when wrapped in Tooltip', () => {
|
||||
render(
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button>Hover me</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Tooltip text</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Hover me' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders tooltip trigger and content structure', () => {
|
||||
render(
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button>Trigger</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Tooltip content</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)
|
||||
|
||||
// Trigger should be rendered (content renders when open)
|
||||
expect(screen.getByRole('button', { name: 'Trigger' })).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('has correct structure', () => {
|
||||
const { getByText } = render(<div>Tooltip</div>)
|
||||
expect(getByText('Tooltip')).toBeInTheDocument()
|
||||
describe('User Interactions', () => {
|
||||
it('handles tooltip trigger click', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button>Show Tooltip</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Content displayed</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)
|
||||
|
||||
const trigger = screen.getByRole('button', { name: 'Show Tooltip' })
|
||||
await user.click(trigger)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Content displayed')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('shows tooltip on hover', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button>Hover trigger</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Hover content</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)
|
||||
|
||||
const trigger = screen.getByRole('button', { name: 'Hover trigger' })
|
||||
await user.hover(trigger)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Hover content')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('hides tooltip on unhover', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button>Hover me</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Tooltip</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)
|
||||
|
||||
const trigger = screen.getByRole('button', { name: 'Hover me' })
|
||||
|
||||
// Hover in
|
||||
await user.hover(trigger)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Tooltip')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Unhover
|
||||
await user.unhover(trigger)
|
||||
|
||||
// Content should be removed or hidden
|
||||
await waitFor(
|
||||
() => {
|
||||
const tooltip = screen.queryByText('Tooltip')
|
||||
// Depending on implementation, it might be removed or hidden
|
||||
if (tooltip) {
|
||||
expect(tooltip.closest('[role="tooltip"]')).toHaveStyle({ visibility: 'hidden' })
|
||||
}
|
||||
},
|
||||
{ timeout: 500 }
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('supports custom classes', () => {
|
||||
const { container } = render(<div className="custom-class">Tooltip</div>)
|
||||
expect(container.firstChild).toHaveClass('custom-class')
|
||||
describe('Accessibility', () => {
|
||||
it('has tooltip role on content when displayed', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button>Info</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent role="tooltip">Helper text</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)
|
||||
|
||||
const trigger = screen.getByRole('button', { name: 'Info' })
|
||||
await user.hover(trigger)
|
||||
|
||||
await waitFor(() => {
|
||||
const tooltip = screen.getByRole('tooltip')
|
||||
expect(tooltip).toBeInTheDocument()
|
||||
}, { timeout: 800 })
|
||||
})
|
||||
|
||||
it('trigger is keyboard focusable', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button>Focus me</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Info</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)
|
||||
|
||||
const trigger = screen.getByRole('button', { name: 'Focus me' })
|
||||
await user.tab()
|
||||
|
||||
expect(trigger).toHaveFocus()
|
||||
})
|
||||
|
||||
it('supports aria-label on trigger', () => {
|
||||
render(
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button aria-label="Information button">?</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>More information</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)
|
||||
|
||||
const trigger = screen.getByLabelText('Information button')
|
||||
expect(trigger).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Styling & Classes', () => {
|
||||
it('applies custom className to trigger', () => {
|
||||
render(
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button className="custom-trigger">Styled</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Content</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)
|
||||
|
||||
const trigger = screen.getByRole('button', { name: 'Styled' })
|
||||
expect(trigger).toHaveClass('custom-trigger')
|
||||
})
|
||||
|
||||
it('applies custom className to content', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button>Show</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="custom-content">Custom styled</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)
|
||||
|
||||
const trigger = screen.getByRole('button', { name: 'Show' })
|
||||
await user.hover(trigger)
|
||||
|
||||
await waitFor(() => {
|
||||
const content = screen.getByText('Custom styled')
|
||||
expect(content).toHaveClass('custom-content')
|
||||
}, { timeout: 800 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('Multiple Tooltips', () => {
|
||||
it('renders multiple tooltips independently', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button>First</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>First tooltip</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button>Second</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Second tooltip</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)
|
||||
|
||||
const firstTrigger = screen.getByRole('button', { name: 'First' })
|
||||
const secondTrigger = screen.getByRole('button', { name: 'Second' })
|
||||
|
||||
await user.hover(firstTrigger)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('First tooltip')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
await user.unhover(firstTrigger)
|
||||
await user.hover(secondTrigger)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Second tooltip')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Content Variations', () => {
|
||||
it('supports text content when opened', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button>Info</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Simple text</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)
|
||||
|
||||
const trigger = screen.getByRole('button', { name: 'Info' })
|
||||
await user.hover(trigger)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Simple text')).toBeInTheDocument()
|
||||
}, { timeout: 800 })
|
||||
})
|
||||
|
||||
it('supports React node content when opened', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button>Help</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div>
|
||||
<strong>Title</strong>
|
||||
<p>Description</p>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)
|
||||
|
||||
const trigger = screen.getByRole('button', { name: 'Help' })
|
||||
await user.hover(trigger)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Title')).toBeInTheDocument()
|
||||
expect(screen.getByText('Description')).toBeInTheDocument()
|
||||
}, { timeout: 800 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('Delay Configuration', () => {
|
||||
it('respects custom delay duration on provider', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<TooltipProvider delayDuration={500}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button>Delayed</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Appears after delay</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)
|
||||
|
||||
const trigger = screen.getByRole('button', { name: 'Delayed' })
|
||||
await user.hover(trigger)
|
||||
|
||||
// Content should not appear immediately
|
||||
expect(screen.queryByText('Appears after delay')).not.toBeInTheDocument()
|
||||
|
||||
// Wait for delay and verify content appears
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(screen.getByText('Appears after delay')).toBeInTheDocument()
|
||||
},
|
||||
{ timeout: 600 }
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -11,7 +11,7 @@ interface TooltipContextValue {
|
||||
|
||||
const TooltipContext = React.createContext<TooltipContextValue | null>(null)
|
||||
|
||||
function TooltipProvider({ children, delayDuration = 700 }: { children: React.ReactNode; delayDuration?: number }) {
|
||||
function TooltipProvider({ children }: { children: React.ReactNode; delayDuration?: number }) {
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
@@ -63,7 +63,6 @@ function TooltipTrigger({ children, asChild, ...props }: ComponentProps<"button"
|
||||
|
||||
function TooltipContent({
|
||||
className,
|
||||
sideOffset = 4,
|
||||
children,
|
||||
...props
|
||||
}: ComponentProps<"div"> & { sideOffset?: number }) {
|
||||
|
||||
@@ -12,9 +12,9 @@ export function useSnippetForm(editingSnippet?: Snippet | null, open?: boolean)
|
||||
const [inputParameters, setInputParameters] = useState<InputParameter[]>([])
|
||||
const [errors, setErrors] = useState<{ title?: string; code?: string }>({})
|
||||
|
||||
/* eslint-disable react-hooks/set-state-in-effect */
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
// This effect hydrates the form when the dialog opens or when a different snippet is selected for editing.
|
||||
// The state reset is intentional user-facing behavior.
|
||||
// The state reset is intentional user-facing behavior. We intentionally omit state setters from deps.
|
||||
useEffect(() => {
|
||||
if (editingSnippet) {
|
||||
setTitle(editingSnippet.title)
|
||||
@@ -35,7 +35,7 @@ export function useSnippetForm(editingSnippet?: Snippet | null, open?: boolean)
|
||||
}
|
||||
setErrors({})
|
||||
}, [editingSnippet, open])
|
||||
/* eslint-enable react-hooks/set-state-in-effect */
|
||||
/* eslint-enable react-hooks/exhaustive-deps */
|
||||
|
||||
const handleAddParameter = () => {
|
||||
setInputParameters((prev) => [
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
import type { Monaco } from '@monaco-editor/react'
|
||||
|
||||
const shadcnTypes = `
|
||||
// ...
|
||||
declare module '@/components/ui/button' {
|
||||
export interface ButtonProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
variant?: string;
|
||||
size?: string;
|
||||
}
|
||||
export function Button(props: ButtonProps): JSX.Element;
|
||||
}
|
||||
// ...
|
||||
`;
|
||||
|
||||
/**
|
||||
* Configure TypeScript support in Monaco Editor
|
||||
*/
|
||||
|
||||
@@ -20,7 +20,7 @@ export function formatBytes(bytes: number): string {
|
||||
/**
|
||||
* Debounce function
|
||||
*/
|
||||
export function debounce<T extends (...args: any[]) => any>(
|
||||
export function debounce<T extends (...args: Array<unknown>) => unknown>(
|
||||
func: T,
|
||||
wait: number
|
||||
): (...args: Parameters<T>) => void {
|
||||
|
||||
Reference in New Issue
Block a user