feat: Add UpdateCredentialsDialog component for re-authenticating accounts
Implements a self-contained dialog that allows users to update credentials
for an existing account without deleting and re-adding it. Supports two
modes: token-based (Tab A) and credentials JSON (Tab B).
Also sets up web component test infrastructure: vitest now includes
apps/web/**/*.test.tsx files with happy-dom environment, @vitejs/plugin-react
for JSX, and @testing-library/{react,jest-dom,user-event} packages.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
175
apps/web/src/components/UpdateCredentialsDialog.test.tsx
Normal file
175
apps/web/src/components/UpdateCredentialsDialog.test.tsx
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
// @vitest-environment happy-dom
|
||||||
|
import '@testing-library/jest-dom/vitest'
|
||||||
|
import { render, screen, fireEvent, waitFor, act } from '@testing-library/react'
|
||||||
|
import { vi, describe, it, expect, beforeEach } from 'vitest'
|
||||||
|
import { UpdateCredentialsDialog } from './UpdateCredentialsDialog'
|
||||||
|
|
||||||
|
// Captured mutation options from the latest useMutation call
|
||||||
|
let capturedOnSuccess: (() => void) | undefined
|
||||||
|
let capturedOnError: ((err: { message: string }) => void) | undefined
|
||||||
|
|
||||||
|
const mockMutate = vi.fn()
|
||||||
|
const mockInvalidate = vi.fn()
|
||||||
|
|
||||||
|
vi.mock('@/lib/trpc', () => ({
|
||||||
|
trpc: {
|
||||||
|
updateAccountAuth: {
|
||||||
|
useMutation: vi.fn((opts?: {
|
||||||
|
onSuccess?: () => void
|
||||||
|
onError?: (err: { message: string }) => void
|
||||||
|
}) => {
|
||||||
|
capturedOnSuccess = opts?.onSuccess
|
||||||
|
capturedOnError = opts?.onError
|
||||||
|
return { mutate: mockMutate, isPending: false }
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
useUtils: vi.fn(() => ({
|
||||||
|
systemHealthCheck: { invalidate: mockInvalidate },
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('sonner', () => ({
|
||||||
|
toast: { success: vi.fn() },
|
||||||
|
}))
|
||||||
|
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
|
const account = {
|
||||||
|
id: 'acc-1',
|
||||||
|
email: 'alice@example.com',
|
||||||
|
provider: 'claude',
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('UpdateCredentialsDialog', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
capturedOnSuccess = undefined
|
||||||
|
capturedOnError = undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test 1: Renders with read-only identity
|
||||||
|
it('renders account email and provider as non-editable', () => {
|
||||||
|
render(<UpdateCredentialsDialog open={true} onOpenChange={vi.fn()} account={account} />)
|
||||||
|
|
||||||
|
expect(screen.getByText('alice@example.com')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('claude')).toBeInTheDocument()
|
||||||
|
|
||||||
|
// They must not be editable inputs
|
||||||
|
const emailEl = screen.getByText('alice@example.com')
|
||||||
|
expect(emailEl.tagName).not.toBe('INPUT')
|
||||||
|
const providerEl = screen.getByText('claude')
|
||||||
|
expect(providerEl.tagName).not.toBe('INPUT')
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test 2: Tab A submit — empty token — shows Required, no mutation
|
||||||
|
it('shows Required error when token is empty and does not call mutation', async () => {
|
||||||
|
render(<UpdateCredentialsDialog open={true} onOpenChange={vi.fn()} account={account} />)
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: /update credentials/i }))
|
||||||
|
|
||||||
|
await waitFor(() => expect(screen.getByText('Required')).toBeInTheDocument())
|
||||||
|
expect(mockMutate).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test 3: Tab A submit — valid token — calls mutation with correct input
|
||||||
|
it('calls mutation with correct token input', async () => {
|
||||||
|
render(<UpdateCredentialsDialog open={true} onOpenChange={vi.fn()} account={account} />)
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByLabelText(/setup token/i), { target: { value: 'test-token' } })
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: /update credentials/i }))
|
||||||
|
|
||||||
|
await waitFor(() => expect(mockMutate).toHaveBeenCalledWith({
|
||||||
|
id: 'acc-1',
|
||||||
|
configJson: '{"hasCompletedOnboarding":true}',
|
||||||
|
credentials: '{"claudeAiOauth":{"accessToken":"test-token"}}',
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test 4: Tab B submit — invalid config JSON — shows "Invalid JSON", no mutation
|
||||||
|
it('shows Invalid JSON error for bad config JSON and does not call mutation', async () => {
|
||||||
|
render(<UpdateCredentialsDialog open={true} onOpenChange={vi.fn()} account={account} />)
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('By credentials JSON'))
|
||||||
|
fireEvent.change(screen.getByLabelText(/config json/i), { target: { value: 'bad json' } })
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: /update credentials/i }))
|
||||||
|
|
||||||
|
await waitFor(() => expect(screen.getByText('Invalid JSON')).toBeInTheDocument())
|
||||||
|
expect(mockMutate).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test 5: Tab B submit — empty textareas — calls mutation with '{}' defaults
|
||||||
|
it("calls mutation with '{}' defaults when credentials textareas are empty", async () => {
|
||||||
|
render(<UpdateCredentialsDialog open={true} onOpenChange={vi.fn()} account={account} />)
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('By credentials JSON'))
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: /update credentials/i }))
|
||||||
|
|
||||||
|
await waitFor(() => expect(mockMutate).toHaveBeenCalledWith({
|
||||||
|
id: 'acc-1',
|
||||||
|
configJson: '{}',
|
||||||
|
credentials: '{}',
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test 6: Server error — shows inline error, dialog stays open
|
||||||
|
it('shows server error inline and does not close dialog on error', async () => {
|
||||||
|
const onOpenChange = vi.fn()
|
||||||
|
render(<UpdateCredentialsDialog open={true} onOpenChange={onOpenChange} account={account} />)
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByLabelText(/setup token/i), { target: { value: 'some-token' } })
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: /update credentials/i }))
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
capturedOnError?.({ message: 'Token expired' })
|
||||||
|
})
|
||||||
|
|
||||||
|
await waitFor(() => expect(screen.getByText('Token expired')).toBeInTheDocument())
|
||||||
|
expect(onOpenChange).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test 7: Success — toasts and closes dialog
|
||||||
|
it('shows success toast and closes dialog on success', async () => {
|
||||||
|
const onOpenChange = vi.fn()
|
||||||
|
render(<UpdateCredentialsDialog open={true} onOpenChange={onOpenChange} account={account} />)
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByLabelText(/setup token/i), { target: { value: 'some-token' } })
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: /update credentials/i }))
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
capturedOnSuccess?.()
|
||||||
|
})
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(toast.success).toHaveBeenCalledWith('Credentials updated for alice@example.com.')
|
||||||
|
expect(onOpenChange).toHaveBeenCalledWith(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test 8: State reset on re-open
|
||||||
|
it('resets state when dialog is re-opened', async () => {
|
||||||
|
const { rerender } = render(
|
||||||
|
<UpdateCredentialsDialog open={true} onOpenChange={vi.fn()} account={account} />
|
||||||
|
)
|
||||||
|
|
||||||
|
// Type a token and trigger server error
|
||||||
|
fireEvent.change(screen.getByLabelText(/setup token/i), { target: { value: 'old-token' } })
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: /update credentials/i }))
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
capturedOnError?.({ message: 'Server error' })
|
||||||
|
})
|
||||||
|
await waitFor(() => expect(screen.getByText('Server error')).toBeInTheDocument())
|
||||||
|
|
||||||
|
// Close dialog
|
||||||
|
rerender(<UpdateCredentialsDialog open={false} onOpenChange={vi.fn()} account={account} />)
|
||||||
|
// Re-open dialog
|
||||||
|
rerender(<UpdateCredentialsDialog open={true} onOpenChange={vi.fn()} account={account} />)
|
||||||
|
|
||||||
|
// Token should be cleared
|
||||||
|
const tokenInput = screen.getByLabelText(/setup token/i) as HTMLInputElement
|
||||||
|
expect(tokenInput.value).toBe('')
|
||||||
|
// Server error should be gone
|
||||||
|
expect(screen.queryByText('Server error')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
239
apps/web/src/components/UpdateCredentialsDialog.tsx
Normal file
239
apps/web/src/components/UpdateCredentialsDialog.tsx
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { Loader2 } from 'lucide-react'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
import { trpc } from '@/lib/trpc'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogFooter,
|
||||||
|
} from '@/components/ui/dialog'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
interface UpdateCredentialsDialogProps {
|
||||||
|
open: boolean
|
||||||
|
onOpenChange: (open: boolean) => void
|
||||||
|
account: { id: string; email: string; provider: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UpdateCredentialsDialog({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
account,
|
||||||
|
}: UpdateCredentialsDialogProps) {
|
||||||
|
const [tab, setTab] = useState<'token' | 'credentials'>('token')
|
||||||
|
|
||||||
|
// Tab A — token
|
||||||
|
const [token, setToken] = useState('')
|
||||||
|
const [tokenError, setTokenError] = useState('')
|
||||||
|
|
||||||
|
// Tab B — credentials JSON
|
||||||
|
const [configJsonText, setConfigJsonText] = useState('')
|
||||||
|
const [credentialsText, setCredentialsText] = useState('')
|
||||||
|
const [configJsonError, setConfigJsonError] = useState('')
|
||||||
|
const [credentialsError, setCredentialsError] = useState('')
|
||||||
|
|
||||||
|
// Shared
|
||||||
|
const [serverError, setServerError] = useState('')
|
||||||
|
|
||||||
|
const utils = trpc.useUtils()
|
||||||
|
|
||||||
|
const mutation = trpc.updateAccountAuth.useMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success(`Credentials updated for ${account.email}.`)
|
||||||
|
void utils.systemHealthCheck.invalidate()
|
||||||
|
onOpenChange(false)
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
setServerError(err.message)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
setTab('token')
|
||||||
|
setToken('')
|
||||||
|
setTokenError('')
|
||||||
|
setConfigJsonText('')
|
||||||
|
setCredentialsText('')
|
||||||
|
setConfigJsonError('')
|
||||||
|
setCredentialsError('')
|
||||||
|
setServerError('')
|
||||||
|
}
|
||||||
|
}, [open])
|
||||||
|
|
||||||
|
function handleSubmit() {
|
||||||
|
if (tab === 'token') {
|
||||||
|
if (!token.trim()) {
|
||||||
|
setTokenError('Required')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setTokenError('')
|
||||||
|
mutation.mutate({
|
||||||
|
id: account.id,
|
||||||
|
configJson: JSON.stringify({ hasCompletedOnboarding: true }),
|
||||||
|
credentials: JSON.stringify({ claudeAiOauth: { accessToken: token } }),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
let hasError = false
|
||||||
|
|
||||||
|
if (configJsonText.trim()) {
|
||||||
|
try {
|
||||||
|
JSON.parse(configJsonText)
|
||||||
|
setConfigJsonError('')
|
||||||
|
} catch {
|
||||||
|
setConfigJsonError('Invalid JSON')
|
||||||
|
hasError = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setConfigJsonError('')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (credentialsText.trim()) {
|
||||||
|
try {
|
||||||
|
JSON.parse(credentialsText)
|
||||||
|
setCredentialsError('')
|
||||||
|
} catch {
|
||||||
|
setCredentialsError('Invalid JSON')
|
||||||
|
hasError = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setCredentialsError('')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasError) return
|
||||||
|
|
||||||
|
mutation.mutate({
|
||||||
|
id: account.id,
|
||||||
|
configJson: configJsonText.trim() || '{}',
|
||||||
|
credentials: credentialsText.trim() || '{}',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Update Credentials</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Read-only account identity */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<p className="text-sm font-medium">{account.email}</p>
|
||||||
|
<Badge variant="outline">{account.provider}</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tab navigation */}
|
||||||
|
<div className="flex border-b">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setTab('token')}
|
||||||
|
className={cn(
|
||||||
|
'px-4 py-2 text-sm font-medium border-b-2 transition-colors',
|
||||||
|
tab === 'token'
|
||||||
|
? 'border-primary text-foreground'
|
||||||
|
: 'border-transparent text-muted-foreground hover:text-foreground',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
By token
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setTab('credentials')}
|
||||||
|
className={cn(
|
||||||
|
'px-4 py-2 text-sm font-medium border-b-2 transition-colors',
|
||||||
|
tab === 'credentials'
|
||||||
|
? 'border-primary text-foreground'
|
||||||
|
: 'border-transparent text-muted-foreground hover:text-foreground',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
By credentials JSON
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tab A — By token */}
|
||||||
|
{tab === 'token' && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="setup-token">
|
||||||
|
Setup token (from `claude setup-token`)
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="setup-token"
|
||||||
|
type="text"
|
||||||
|
value={token}
|
||||||
|
onChange={(e) => setToken(e.target.value)}
|
||||||
|
/>
|
||||||
|
{tokenError && (
|
||||||
|
<p className="text-sm text-destructive">{tokenError}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Tab B — By credentials JSON */}
|
||||||
|
{tab === 'credentials' && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="config-json">
|
||||||
|
Config JSON (`.claude.json` content)
|
||||||
|
</Label>
|
||||||
|
<Textarea
|
||||||
|
id="config-json"
|
||||||
|
value={configJsonText}
|
||||||
|
onChange={(e) => setConfigJsonText(e.target.value)}
|
||||||
|
placeholder='{ "oauthAccount": { ... } }'
|
||||||
|
/>
|
||||||
|
{configJsonError && (
|
||||||
|
<p className="text-sm text-destructive">{configJsonError}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="credentials-json">
|
||||||
|
Credentials JSON (`.credentials.json` content)
|
||||||
|
</Label>
|
||||||
|
<Textarea
|
||||||
|
id="credentials-json"
|
||||||
|
value={credentialsText}
|
||||||
|
onChange={(e) => setCredentialsText(e.target.value)}
|
||||||
|
placeholder='{ "claudeAiOauth": { ... } }'
|
||||||
|
/>
|
||||||
|
{credentialsError && (
|
||||||
|
<p className="text-sm text-destructive">{credentialsError}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Run `cw account extract` in your terminal to obtain these values.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Server error */}
|
||||||
|
{serverError && (
|
||||||
|
<p className="text-sm text-destructive">{serverError}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSubmit} disabled={mutation.isPending}>
|
||||||
|
{mutation.isPending && (
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
)}
|
||||||
|
Update Credentials
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
250
package-lock.json
generated
250
package-lock.json
generated
@@ -37,6 +37,9 @@
|
|||||||
"cw": "apps/server/dist/bin/cw.js"
|
"cw": "apps/server/dist/bin/cw.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
|
"@testing-library/react": "^16.3.2",
|
||||||
|
"@testing-library/user-event": "^14.6.1",
|
||||||
"@types/better-sqlite3": "^7.6.13",
|
"@types/better-sqlite3": "^7.6.13",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/node": "^22.10.7",
|
"@types/node": "^22.10.7",
|
||||||
@@ -673,6 +676,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@adobe/css-tools": {
|
||||||
|
"version": "4.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz",
|
||||||
|
"integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@alloc/quick-lru": {
|
"node_modules/@alloc/quick-lru": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
||||||
@@ -982,6 +992,16 @@
|
|||||||
"@babel/core": "^7.0.0-0"
|
"@babel/core": "^7.0.0-0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/runtime": {
|
||||||
|
"version": "7.28.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
|
||||||
|
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/template": {
|
"node_modules/@babel/template": {
|
||||||
"version": "7.28.6",
|
"version": "7.28.6",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
|
||||||
@@ -4377,6 +4397,96 @@
|
|||||||
"url": "https://github.com/sponsors/tannerlinsley"
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@testing-library/dom": {
|
||||||
|
"version": "10.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
|
||||||
|
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/code-frame": "^7.10.4",
|
||||||
|
"@babel/runtime": "^7.12.5",
|
||||||
|
"@types/aria-query": "^5.0.1",
|
||||||
|
"aria-query": "5.3.0",
|
||||||
|
"dom-accessibility-api": "^0.5.9",
|
||||||
|
"lz-string": "^1.5.0",
|
||||||
|
"picocolors": "1.1.1",
|
||||||
|
"pretty-format": "^27.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@testing-library/jest-dom": {
|
||||||
|
"version": "6.9.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz",
|
||||||
|
"integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@adobe/css-tools": "^4.4.0",
|
||||||
|
"aria-query": "^5.0.0",
|
||||||
|
"css.escape": "^1.5.1",
|
||||||
|
"dom-accessibility-api": "^0.6.3",
|
||||||
|
"picocolors": "^1.1.1",
|
||||||
|
"redent": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14",
|
||||||
|
"npm": ">=6",
|
||||||
|
"yarn": ">=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": {
|
||||||
|
"version": "0.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz",
|
||||||
|
"integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@testing-library/react": {
|
||||||
|
"version": "16.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz",
|
||||||
|
"integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.12.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@testing-library/dom": "^10.0.0",
|
||||||
|
"@types/react": "^18.0.0 || ^19.0.0",
|
||||||
|
"@types/react-dom": "^18.0.0 || ^19.0.0",
|
||||||
|
"react": "^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@testing-library/user-event": {
|
||||||
|
"version": "14.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz",
|
||||||
|
"integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12",
|
||||||
|
"npm": ">=6"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@testing-library/dom": ">=7.21.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tiptap/core": {
|
"node_modules/@tiptap/core": {
|
||||||
"version": "3.19.0",
|
"version": "3.19.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.19.0.tgz",
|
||||||
@@ -4926,6 +5036,14 @@
|
|||||||
"typescript": ">=5.7.2"
|
"typescript": ">=5.7.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/aria-query": {
|
||||||
|
"version": "5.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
||||||
|
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/@types/babel__core": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||||
@@ -5291,6 +5409,31 @@
|
|||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ansi-regex": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ansi-styles": {
|
||||||
|
"version": "5.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
|
||||||
|
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ansis": {
|
"node_modules/ansis": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz",
|
||||||
@@ -5360,6 +5503,16 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/aria-query": {
|
||||||
|
"version": "5.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
|
||||||
|
"integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"dequal": "^2.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/assertion-error": {
|
"node_modules/assertion-error": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
||||||
@@ -5816,6 +5969,13 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/css.escape": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/cssesc": {
|
"node_modules/cssesc": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||||
@@ -5947,6 +6107,14 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/dom-accessibility-api": {
|
||||||
|
"version": "0.5.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
|
||||||
|
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/drizzle-kit": {
|
"node_modules/drizzle-kit": {
|
||||||
"version": "0.31.8",
|
"version": "0.31.8",
|
||||||
"resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.8.tgz",
|
"resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.8.tgz",
|
||||||
@@ -7212,6 +7380,16 @@
|
|||||||
],
|
],
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause"
|
||||||
},
|
},
|
||||||
|
"node_modules/indent-string": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/inherits": {
|
"node_modules/inherits": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
@@ -7513,6 +7691,17 @@
|
|||||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lz-string": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"bin": {
|
||||||
|
"lz-string": "bin/bin.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/magic-string": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.21",
|
"version": "0.30.21",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||||
@@ -7745,6 +7934,16 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/min-indent": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/minimatch": {
|
"node_modules/minimatch": {
|
||||||
"version": "10.1.1",
|
"version": "10.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
|
||||||
@@ -8511,6 +8710,22 @@
|
|||||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pretty-format": {
|
||||||
|
"version": "27.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
|
||||||
|
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": "^5.0.1",
|
||||||
|
"ansi-styles": "^5.0.0",
|
||||||
|
"react-is": "^17.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pretty-ms": {
|
"node_modules/pretty-ms": {
|
||||||
"version": "9.3.0",
|
"version": "9.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz",
|
||||||
@@ -8829,6 +9044,14 @@
|
|||||||
"react": "^19.2.4"
|
"react": "^19.2.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-is": {
|
||||||
|
"version": "17.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||||
|
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/react-refresh": {
|
"node_modules/react-refresh": {
|
||||||
"version": "0.17.0",
|
"version": "0.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
||||||
@@ -8984,6 +9207,20 @@
|
|||||||
"node": ">= 4"
|
"node": ">= 4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/redent": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"indent-string": "^4.0.0",
|
||||||
|
"strip-indent": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/regex": {
|
"node_modules/regex": {
|
||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz",
|
||||||
@@ -9540,6 +9777,19 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/strip-indent": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"min-indent": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/strip-json-comments": {
|
"node_modules/strip-json-comments": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
||||||
|
|||||||
@@ -50,6 +50,9 @@
|
|||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
|
"@testing-library/react": "^16.3.2",
|
||||||
|
"@testing-library/user-event": "^14.6.1",
|
||||||
"@types/better-sqlite3": "^7.6.13",
|
"@types/better-sqlite3": "^7.6.13",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/node": "^22.10.7",
|
"@types/node": "^22.10.7",
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
import { defineConfig } from 'vitest/config';
|
import { defineConfig } from 'vitest/config';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './apps/web/src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
test: {
|
test: {
|
||||||
// Enable test globals (describe, it, expect without imports)
|
// Enable test globals (describe, it, expect without imports)
|
||||||
globals: true,
|
globals: true,
|
||||||
@@ -8,8 +16,11 @@ export default defineConfig({
|
|||||||
CW_LOG_LEVEL: 'silent',
|
CW_LOG_LEVEL: 'silent',
|
||||||
},
|
},
|
||||||
// Test file pattern
|
// Test file pattern
|
||||||
include: ['**/*.test.ts'],
|
include: ['**/*.test.ts', '**/*.test.tsx'],
|
||||||
exclude: ['**/node_modules/**', '**/dist/**', 'apps/web/**', 'packages/**'],
|
exclude: ['**/node_modules/**', '**/dist/**', 'packages/**'],
|
||||||
|
environmentMatchGlobs: [
|
||||||
|
['apps/web/**', 'happy-dom'],
|
||||||
|
],
|
||||||
// TypeScript support uses tsconfig.json automatically
|
// TypeScript support uses tsconfig.json automatically
|
||||||
// Coverage reporter (optional, for future use)
|
// Coverage reporter (optional, for future use)
|
||||||
coverage: {
|
coverage: {
|
||||||
|
|||||||
Reference in New Issue
Block a user