mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
<!-- ccr-slack-attribution --> _Requested by **Charis Lam** · [Slack thread](https://supabase.slack.com/archives/C0161K73J1J/p1790599888647059?thread_ts=1790599888.647059&cid=C0161K73J1J)_ ## Problem The Studio ESLint "ratchet" (`apps/studio/scripts/ratchet-eslint-rules.ts`, baseline in `apps/studio/.github/eslint-rule-baselines.json`, tracked rules in `apps/studio/scripts/ratchet-rules.json`) lets certain rules stay at `warn` severity while CI blocks the *count* of violations from increasing. Several of those tracked rules had already reached a baseline of 0 allowed violations, meaning there's nothing left to ratchet — they should be enforced directly instead of tracked indirectly. ## Solution **Before:** `no-restricted-imports`, `jsx-a11y/aria-props`, `jsx-a11y/aria-proptypes`, `jsx-a11y/role-supports-aria-props`, `jsx-a11y/anchor-has-content`, `jsx-a11y/aria-role`, `jsx-a11y/no-aria-hidden-on-focusable`, `jsx-a11y/tabindex-no-positive`, `jsx-a11y/no-distracting-elements`, and `react-hook-form/no-use-watch` were all tracked in the ratchet baseline with a count of 0, and (apart from `no-restricted-imports`, see below) configured as ESLint `warn` in `apps/studio/eslint.config.cjs`. **After:** each of those rules is removed from `apps/studio/.github/eslint-rule-baselines.json` (both the `rules` count and the now-empty `ruleFiles` entry) and from `apps/studio/scripts/ratchet-rules.json`. Their severity in `apps/studio/eslint.config.cjs` is bumped from `warn` to `error` so they're enforced directly by lint going forward instead of being tracked via the ratchet. `no-restricted-imports` was a special case: a later config block in `apps/studio/eslint.config.cjs` already overrides the shared `warn` default with `error` (confirmed via `eslint --print-config`), so only the ratchet bookkeeping needed removing for that rule — no severity change was needed. Promoting `jsx-a11y/role-supports-aria-props` to `error` surfaced one real violation that the ratchet's non-test-file filter had been hiding: a mock `<button>` in `LocalDropdown.test.tsx` set `aria-checked`, which that role doesn't support. Removed the unused `aria-checked` attribute from the mock (it wasn't asserted on by any test). Every other rule still tracked by the ratchet (e.g. `@typescript-eslint/no-explicit-any`, `react-hooks/exhaustive-deps`, `no-restricted-exports`, …) has a baseline above 0 and was left untouched. ### How verified - `pnpm --filter studio run lint:ratchet` → `Stable: No regressions for selected rules.` - `pnpm --filter studio run lint` → `0 errors, 2430 warnings` (no new errors from the severity bumps) - `npx vitest run components/interfaces/LocalDropdown.test.tsx` → 3/3 passing after the mock fix - `npx prettier --check` on all touched files → clean - `npx tsc --noEmit` shows one pre-existing, unrelated error in `packages/ui-patterns` (reproduced identically on `master` before this change) ## Review instructions 1. Confirm `apps/studio/.github/eslint-rule-baselines.json` and `apps/studio/scripts/ratchet-rules.json` no longer list the 10 rules named above. 2. Confirm those same rules (except `no-restricted-imports`, already `error`) are now `'error'` in `apps/studio/eslint.config.cjs`. 3. Run `pnpm --filter studio run lint:ratchet` and `pnpm --filter studio run lint` locally to confirm both pass. ## Checklist Check all before review: - [x] I have read [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) - [ ] If I wrote a new docs topic or edited an existing topic, I used the `/write-the-docs` or `/edit-the-docs` skill, which applies the docs [style guide](https://github.com/supabase/supabase/tree/master/apps/docs/style-guide) 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01LZThbcWV5U1r5cvUDKPVQP --- _Generated by [Claude Code](https://claude.ai/code/session_01LZThbcWV5U1r5cvUDKPVQP)_ Co-authored-by: Claude <noreply@anthropic.com>
263 lines
7.1 KiB
TypeScript
263 lines
7.1 KiB
TypeScript
import { render, screen } from '@testing-library/react'
|
|
import userEvent from '@testing-library/user-event'
|
|
import type { MouseEventHandler, ReactElement, ReactNode } from 'react'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import { LocalDropdown } from './LocalDropdown'
|
|
|
|
const {
|
|
mockRouter,
|
|
mockSetTheme,
|
|
mockSetLastRoute,
|
|
mockToggleFeaturePreviewModal,
|
|
mockEnableToolbar,
|
|
mockDismissDevToolbar,
|
|
mockSetDevToolbarOpen,
|
|
mockUseDevToolbar,
|
|
} = vi.hoisted(() => ({
|
|
mockRouter: {
|
|
pathname: '/project/[ref]/editor',
|
|
asPath: '/project/default/editor',
|
|
},
|
|
mockSetTheme: vi.fn(),
|
|
mockSetLastRoute: vi.fn(),
|
|
mockToggleFeaturePreviewModal: vi.fn(),
|
|
mockEnableToolbar: vi.fn(),
|
|
mockDismissDevToolbar: vi.fn(),
|
|
mockSetDevToolbarOpen: vi.fn(),
|
|
mockUseDevToolbar: vi.fn(() => ({
|
|
isAvailable: false,
|
|
isEnabled: false,
|
|
isOpen: false,
|
|
setIsOpen: mockSetDevToolbarOpen,
|
|
enableToolbar: mockEnableToolbar,
|
|
dismissToolbar: mockDismissDevToolbar,
|
|
events: [],
|
|
setEvents: vi.fn(),
|
|
})),
|
|
}))
|
|
|
|
vi.mock('next/router', () => ({
|
|
useRouter: () => mockRouter,
|
|
}))
|
|
|
|
vi.mock('next/link', () => ({
|
|
default: ({
|
|
href,
|
|
children,
|
|
onClick,
|
|
}: {
|
|
href: string
|
|
children: ReactNode
|
|
onClick?: MouseEventHandler<HTMLAnchorElement>
|
|
}) => (
|
|
<a href={href} onClick={onClick}>
|
|
{children}
|
|
</a>
|
|
),
|
|
}))
|
|
|
|
vi.mock('next-themes', () => ({
|
|
useTheme: () => ({
|
|
theme: 'dark',
|
|
setTheme: mockSetTheme,
|
|
}),
|
|
}))
|
|
|
|
vi.mock('@/state/app-state', () => ({
|
|
useAppStateSnapshot: () => ({
|
|
setLastRouteBeforeVisitingAccountPage: mockSetLastRoute,
|
|
}),
|
|
}))
|
|
|
|
vi.mock('@/components/ui/ProfileImage', () => ({
|
|
ProfileImage: () => <div>Avatar</div>,
|
|
}))
|
|
|
|
vi.mock('./App/FeaturePreview/FeaturePreviewContext', () => ({
|
|
useFeaturePreviewModal: () => ({
|
|
toggleFeaturePreviewModal: mockToggleFeaturePreviewModal,
|
|
}),
|
|
}))
|
|
|
|
vi.mock('@/lib/telemetry/track', () => ({ useTrack: () => vi.fn() }))
|
|
|
|
vi.mock('dev-tools', () => ({
|
|
useDevToolbar: () => mockUseDevToolbar(),
|
|
}))
|
|
|
|
vi.mock('ui', async () => {
|
|
const React = await import('react')
|
|
|
|
return {
|
|
Button: ({
|
|
children,
|
|
...props
|
|
}: React.ButtonHTMLAttributes<HTMLButtonElement> & { children?: ReactNode }) => (
|
|
<button tabIndex={0} {...props}>
|
|
{children}
|
|
</button>
|
|
),
|
|
cn: (...classes: Array<string | false | null | undefined>) => classes.filter(Boolean).join(' '),
|
|
DropdownMenu: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
|
DropdownMenuTrigger: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
|
DropdownMenuContent: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
|
DropdownMenuGroup: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
|
DropdownMenuItem: ({
|
|
children,
|
|
asChild,
|
|
onClick,
|
|
onSelect,
|
|
}: {
|
|
children: ReactNode
|
|
asChild?: boolean
|
|
onClick?: () => void
|
|
onSelect?: () => void
|
|
}) =>
|
|
asChild ? (
|
|
<div>{children}</div>
|
|
) : (
|
|
<button
|
|
tabIndex={0}
|
|
onClick={() => {
|
|
onClick?.()
|
|
onSelect?.()
|
|
}}
|
|
>
|
|
{children}
|
|
</button>
|
|
),
|
|
DropdownMenuLabel: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
|
DropdownMenuCheckboxItem: ({
|
|
children,
|
|
checked,
|
|
onCheckedChange,
|
|
}: {
|
|
children: ReactNode
|
|
checked?: boolean
|
|
onCheckedChange?: (checked: boolean) => void
|
|
}) => (
|
|
<button tabIndex={0} onClick={() => onCheckedChange?.(!checked)}>
|
|
{children}
|
|
</button>
|
|
),
|
|
DropdownMenuSeparator: () => <hr />,
|
|
DropdownMenuRadioGroup: ({
|
|
children,
|
|
onValueChange,
|
|
}: {
|
|
children: ReactNode
|
|
onValueChange: (value: string) => void
|
|
}) => (
|
|
<div>
|
|
{React.Children.map(children, (child: ReactNode) =>
|
|
React.isValidElement<{ value: string; onClick?: () => void }>(child)
|
|
? React.cloneElement(child, {
|
|
onClick: () => onValueChange(child.props.value),
|
|
})
|
|
: (child as ReactElement)
|
|
)}
|
|
</div>
|
|
),
|
|
DropdownMenuRadioItem: ({
|
|
children,
|
|
onClick,
|
|
}: {
|
|
children: ReactNode
|
|
onClick?: () => void
|
|
}) => (
|
|
<button tabIndex={0} onClick={onClick}>
|
|
{children}
|
|
</button>
|
|
),
|
|
Tooltip: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
|
TooltipContent: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
|
TooltipTrigger: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
|
singleThemes: [
|
|
{ value: 'dark', name: 'Dark' },
|
|
{ value: 'light', name: 'Light' },
|
|
],
|
|
}
|
|
})
|
|
|
|
describe('LocalDropdown', () => {
|
|
beforeEach(() => {
|
|
mockUseDevToolbar.mockReturnValue({
|
|
isAvailable: false,
|
|
isEnabled: false,
|
|
isOpen: false,
|
|
setIsOpen: mockSetDevToolbarOpen,
|
|
enableToolbar: mockEnableToolbar,
|
|
dismissToolbar: mockDismissDevToolbar,
|
|
events: [],
|
|
setEvents: vi.fn(),
|
|
})
|
|
})
|
|
|
|
it('shows Preferences, removes Command menu, and keeps theme controls wired', async () => {
|
|
const user = userEvent.setup()
|
|
|
|
render(<LocalDropdown />)
|
|
|
|
expect(screen.getByText('Preferences')).toBeInTheDocument()
|
|
expect(screen.queryByText('Command menu')).not.toBeInTheDocument()
|
|
expect(screen.getByText('Theme')).toBeInTheDocument()
|
|
expect(screen.queryByText('Dev toolbar')).not.toBeInTheDocument()
|
|
|
|
await user.click(screen.getByText('Preferences'))
|
|
expect(mockSetLastRoute).toHaveBeenCalledWith('/project/default/editor')
|
|
|
|
await user.click(screen.getByText('Feature previews'))
|
|
expect(mockToggleFeaturePreviewModal).toHaveBeenCalledWith(true)
|
|
|
|
await user.click(screen.getByText('Light'))
|
|
expect(mockSetTheme).toHaveBeenCalledWith('light')
|
|
})
|
|
|
|
it('toggles Dev toolbar visibility from the menu', async () => {
|
|
mockUseDevToolbar.mockReturnValue({
|
|
isAvailable: true,
|
|
isEnabled: false,
|
|
isOpen: false,
|
|
setIsOpen: mockSetDevToolbarOpen,
|
|
enableToolbar: mockEnableToolbar,
|
|
dismissToolbar: mockDismissDevToolbar,
|
|
events: [],
|
|
setEvents: vi.fn(),
|
|
})
|
|
|
|
const user = userEvent.setup()
|
|
|
|
render(<LocalDropdown />)
|
|
|
|
expect(screen.getByText('Local tools')).toBeInTheDocument()
|
|
|
|
await user.click(screen.getByRole('button', { name: 'Dev toolbar' }))
|
|
|
|
expect(mockEnableToolbar).toHaveBeenCalled()
|
|
expect(mockDismissDevToolbar).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('hides Dev toolbar from the menu when toggled off', async () => {
|
|
mockUseDevToolbar.mockReturnValue({
|
|
isAvailable: true,
|
|
isEnabled: true,
|
|
isOpen: false,
|
|
setIsOpen: mockSetDevToolbarOpen,
|
|
enableToolbar: mockEnableToolbar,
|
|
dismissToolbar: mockDismissDevToolbar,
|
|
events: [],
|
|
setEvents: vi.fn(),
|
|
})
|
|
|
|
const user = userEvent.setup()
|
|
|
|
render(<LocalDropdown />)
|
|
|
|
await user.click(screen.getByRole('button', { name: 'Dev toolbar' }))
|
|
|
|
expect(mockDismissDevToolbar).toHaveBeenCalled()
|
|
expect(mockEnableToolbar).not.toHaveBeenCalled()
|
|
})
|
|
})
|