mirror of
https://github.com/supabase/supabase.git
synced 2026-05-06 22:18:00 +08:00
chore: adds tests (#42653)
This commit is contained in:
5
.agents/skills/vitest/GENERATION.md
Normal file
5
.agents/skills/vitest/GENERATION.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Generation Info
|
||||
|
||||
- **Source:** `sources/vitest`
|
||||
- **Git SHA:** `4a7321e10672f00f0bb698823a381c2cc245b8f7`
|
||||
- **Generated:** 2026-01-28
|
||||
52
.agents/skills/vitest/SKILL.md
Normal file
52
.agents/skills/vitest/SKILL.md
Normal file
@@ -0,0 +1,52 @@
|
||||
---
|
||||
name: vitest
|
||||
description: Vitest fast unit testing framework powered by Vite with Jest-compatible API. Use when writing tests, mocking, configuring coverage, or working with test filtering and fixtures.
|
||||
metadata:
|
||||
author: Anthony Fu
|
||||
version: "2026.1.28"
|
||||
source: Generated from https://github.com/vitest-dev/vitest, scripts located at https://github.com/antfu/skills
|
||||
---
|
||||
|
||||
Vitest is a next-generation testing framework powered by Vite. It provides a Jest-compatible API with native ESM, TypeScript, and JSX support out of the box. Vitest shares the same config, transformers, resolvers, and plugins with your Vite app.
|
||||
|
||||
**Key Features:**
|
||||
- Vite-native: Uses Vite's transformation pipeline for fast HMR-like test updates
|
||||
- Jest-compatible: Drop-in replacement for most Jest test suites
|
||||
- Smart watch mode: Only reruns affected tests based on module graph
|
||||
- Native ESM, TypeScript, JSX support without configuration
|
||||
- Multi-threaded workers for parallel test execution
|
||||
- Built-in coverage via V8 or Istanbul
|
||||
- Snapshot testing, mocking, and spy utilities
|
||||
|
||||
> The skill is based on Vitest 3.x, generated at 2026-01-28.
|
||||
|
||||
## Core
|
||||
|
||||
| Topic | Description | Reference |
|
||||
|-------|-------------|-----------|
|
||||
| Configuration | Vitest and Vite config integration, defineConfig usage | [core-config](references/core-config.md) |
|
||||
| CLI | Command line interface, commands and options | [core-cli](references/core-cli.md) |
|
||||
| Test API | test/it function, modifiers like skip, only, concurrent | [core-test-api](references/core-test-api.md) |
|
||||
| Describe API | describe/suite for grouping tests and nested suites | [core-describe](references/core-describe.md) |
|
||||
| Expect API | Assertions with toBe, toEqual, matchers and asymmetric matchers | [core-expect](references/core-expect.md) |
|
||||
| Hooks | beforeEach, afterEach, beforeAll, afterAll, aroundEach | [core-hooks](references/core-hooks.md) |
|
||||
|
||||
## Features
|
||||
|
||||
| Topic | Description | Reference |
|
||||
|-------|-------------|-----------|
|
||||
| Mocking | Mock functions, modules, timers, dates with vi utilities | [features-mocking](references/features-mocking.md) |
|
||||
| Snapshots | Snapshot testing with toMatchSnapshot and inline snapshots | [features-snapshots](references/features-snapshots.md) |
|
||||
| Coverage | Code coverage with V8 or Istanbul providers | [features-coverage](references/features-coverage.md) |
|
||||
| Test Context | Test fixtures, context.expect, test.extend for custom fixtures | [features-context](references/features-context.md) |
|
||||
| Concurrency | Concurrent tests, parallel execution, sharding | [features-concurrency](references/features-concurrency.md) |
|
||||
| Filtering | Filter tests by name, file patterns, tags | [features-filtering](references/features-filtering.md) |
|
||||
|
||||
## Advanced
|
||||
|
||||
| Topic | Description | Reference |
|
||||
|-------|-------------|-----------|
|
||||
| Vi Utilities | vi helper: mock, spyOn, fake timers, hoisted, waitFor | [advanced-vi](references/advanced-vi.md) |
|
||||
| Environments | Test environments: node, jsdom, happy-dom, custom | [advanced-environments](references/advanced-environments.md) |
|
||||
| Type Testing | Type-level testing with expectTypeOf and assertType | [advanced-type-testing](references/advanced-type-testing.md) |
|
||||
| Projects | Multi-project workspaces, different configs per project | [advanced-projects](references/advanced-projects.md) |
|
||||
264
.agents/skills/vitest/references/advanced-environments.md
Normal file
264
.agents/skills/vitest/references/advanced-environments.md
Normal file
@@ -0,0 +1,264 @@
|
||||
---
|
||||
name: test-environments
|
||||
description: Configure environments like jsdom, happy-dom for browser APIs
|
||||
---
|
||||
|
||||
# Test Environments
|
||||
|
||||
## Available Environments
|
||||
|
||||
- `node` (default) - Node.js environment
|
||||
- `jsdom` - Browser-like with DOM APIs
|
||||
- `happy-dom` - Faster alternative to jsdom
|
||||
- `edge-runtime` - Vercel Edge Runtime
|
||||
|
||||
## Configuration
|
||||
|
||||
```ts
|
||||
// vitest.config.ts
|
||||
defineConfig({
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
|
||||
// Environment-specific options
|
||||
environmentOptions: {
|
||||
jsdom: {
|
||||
url: 'http://localhost',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Installing Environment Packages
|
||||
|
||||
```bash
|
||||
# jsdom
|
||||
npm i -D jsdom
|
||||
|
||||
# happy-dom (faster, fewer APIs)
|
||||
npm i -D happy-dom
|
||||
```
|
||||
|
||||
## Per-File Environment
|
||||
|
||||
Use magic comment at top of file:
|
||||
|
||||
```ts
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { expect, test } from 'vitest'
|
||||
|
||||
test('DOM test', () => {
|
||||
const div = document.createElement('div')
|
||||
expect(div).toBeInstanceOf(HTMLDivElement)
|
||||
})
|
||||
```
|
||||
|
||||
## jsdom Environment
|
||||
|
||||
Full browser environment simulation:
|
||||
|
||||
```ts
|
||||
// @vitest-environment jsdom
|
||||
|
||||
test('DOM manipulation', () => {
|
||||
document.body.innerHTML = '<div id="app"></div>'
|
||||
|
||||
const app = document.getElementById('app')
|
||||
app.textContent = 'Hello'
|
||||
|
||||
expect(app.textContent).toBe('Hello')
|
||||
})
|
||||
|
||||
test('window APIs', () => {
|
||||
expect(window.location.href).toBeDefined()
|
||||
expect(localStorage).toBeDefined()
|
||||
})
|
||||
```
|
||||
|
||||
### jsdom Options
|
||||
|
||||
```ts
|
||||
defineConfig({
|
||||
test: {
|
||||
environmentOptions: {
|
||||
jsdom: {
|
||||
url: 'http://localhost:3000',
|
||||
html: '<!DOCTYPE html><html><body></body></html>',
|
||||
userAgent: 'custom-agent',
|
||||
resources: 'usable',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## happy-dom Environment
|
||||
|
||||
Faster but fewer APIs:
|
||||
|
||||
```ts
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
test('basic DOM', () => {
|
||||
const el = document.createElement('div')
|
||||
el.className = 'test'
|
||||
expect(el.className).toBe('test')
|
||||
})
|
||||
```
|
||||
|
||||
## Multiple Environments per Project
|
||||
|
||||
Use projects for different environments:
|
||||
|
||||
```ts
|
||||
defineConfig({
|
||||
test: {
|
||||
projects: [
|
||||
{
|
||||
test: {
|
||||
name: 'unit',
|
||||
include: ['tests/unit/**/*.test.ts'],
|
||||
environment: 'node',
|
||||
},
|
||||
},
|
||||
{
|
||||
test: {
|
||||
name: 'dom',
|
||||
include: ['tests/dom/**/*.test.ts'],
|
||||
environment: 'jsdom',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Custom Environment
|
||||
|
||||
Create custom environment package:
|
||||
|
||||
```ts
|
||||
// vitest-environment-custom/index.ts
|
||||
import type { Environment } from 'vitest/runtime'
|
||||
|
||||
export default <Environment>{
|
||||
name: 'custom',
|
||||
viteEnvironment: 'ssr', // or 'client'
|
||||
|
||||
setup() {
|
||||
// Setup global state
|
||||
globalThis.myGlobal = 'value'
|
||||
|
||||
return {
|
||||
teardown() {
|
||||
delete globalThis.myGlobal
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Use with:
|
||||
|
||||
```ts
|
||||
defineConfig({
|
||||
test: {
|
||||
environment: 'custom',
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Environment with VM
|
||||
|
||||
For full isolation:
|
||||
|
||||
```ts
|
||||
export default <Environment>{
|
||||
name: 'isolated',
|
||||
viteEnvironment: 'ssr',
|
||||
|
||||
async setupVM() {
|
||||
const vm = await import('node:vm')
|
||||
const context = vm.createContext()
|
||||
|
||||
return {
|
||||
getVmContext() {
|
||||
return context
|
||||
},
|
||||
teardown() {},
|
||||
}
|
||||
},
|
||||
|
||||
setup() {
|
||||
return { teardown() {} }
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Browser Mode (Separate from Environments)
|
||||
|
||||
For real browser testing, use Vitest Browser Mode:
|
||||
|
||||
```ts
|
||||
defineConfig({
|
||||
test: {
|
||||
browser: {
|
||||
enabled: true,
|
||||
name: 'chromium', // or 'firefox', 'webkit'
|
||||
provider: 'playwright',
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## CSS and Assets
|
||||
|
||||
In jsdom/happy-dom, configure CSS handling:
|
||||
|
||||
```ts
|
||||
defineConfig({
|
||||
test: {
|
||||
css: true, // Process CSS
|
||||
|
||||
// Or with options
|
||||
css: {
|
||||
include: /\.module\.css$/,
|
||||
modules: {
|
||||
classNameStrategy: 'non-scoped',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Fixing External Dependencies
|
||||
|
||||
If external deps fail with CSS/asset errors:
|
||||
|
||||
```ts
|
||||
defineConfig({
|
||||
test: {
|
||||
server: {
|
||||
deps: {
|
||||
inline: ['problematic-package'],
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Key Points
|
||||
|
||||
- Default is `node` - no browser APIs
|
||||
- Use `jsdom` for full browser simulation
|
||||
- Use `happy-dom` for faster tests with basic DOM
|
||||
- Per-file environment via `// @vitest-environment` comment
|
||||
- Use projects for multiple environment configurations
|
||||
- Browser Mode is for real browser testing, not environment
|
||||
|
||||
<!--
|
||||
Source references:
|
||||
- https://vitest.dev/guide/environment.html
|
||||
-->
|
||||
300
.agents/skills/vitest/references/advanced-projects.md
Normal file
300
.agents/skills/vitest/references/advanced-projects.md
Normal file
@@ -0,0 +1,300 @@
|
||||
---
|
||||
name: projects-workspaces
|
||||
description: Multi-project configuration for monorepos and different test types
|
||||
---
|
||||
|
||||
# Projects
|
||||
|
||||
Run different test configurations in the same Vitest process.
|
||||
|
||||
## Basic Projects Setup
|
||||
|
||||
```ts
|
||||
// vitest.config.ts
|
||||
defineConfig({
|
||||
test: {
|
||||
projects: [
|
||||
// Glob patterns for config files
|
||||
'packages/*',
|
||||
|
||||
// Inline config
|
||||
{
|
||||
test: {
|
||||
name: 'unit',
|
||||
include: ['tests/unit/**/*.test.ts'],
|
||||
environment: 'node',
|
||||
},
|
||||
},
|
||||
{
|
||||
test: {
|
||||
name: 'integration',
|
||||
include: ['tests/integration/**/*.test.ts'],
|
||||
environment: 'jsdom',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Monorepo Pattern
|
||||
|
||||
```ts
|
||||
defineConfig({
|
||||
test: {
|
||||
projects: [
|
||||
// Each package has its own vitest.config.ts
|
||||
'packages/core',
|
||||
'packages/cli',
|
||||
'packages/utils',
|
||||
],
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Package config:
|
||||
|
||||
```ts
|
||||
// packages/core/vitest.config.ts
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
name: 'core',
|
||||
include: ['src/**/*.test.ts'],
|
||||
environment: 'node',
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Different Environments
|
||||
|
||||
Run same tests in different environments:
|
||||
|
||||
```ts
|
||||
defineConfig({
|
||||
test: {
|
||||
projects: [
|
||||
{
|
||||
test: {
|
||||
name: 'happy-dom',
|
||||
root: './shared-tests',
|
||||
environment: 'happy-dom',
|
||||
setupFiles: ['./setup.happy-dom.ts'],
|
||||
},
|
||||
},
|
||||
{
|
||||
test: {
|
||||
name: 'node',
|
||||
root: './shared-tests',
|
||||
environment: 'node',
|
||||
setupFiles: ['./setup.node.ts'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Browser + Node Projects
|
||||
|
||||
```ts
|
||||
defineConfig({
|
||||
test: {
|
||||
projects: [
|
||||
{
|
||||
test: {
|
||||
name: 'unit',
|
||||
include: ['tests/unit/**/*.test.ts'],
|
||||
environment: 'node',
|
||||
},
|
||||
},
|
||||
{
|
||||
test: {
|
||||
name: 'browser',
|
||||
include: ['tests/browser/**/*.test.ts'],
|
||||
browser: {
|
||||
enabled: true,
|
||||
name: 'chromium',
|
||||
provider: 'playwright',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Shared Configuration
|
||||
|
||||
```ts
|
||||
// vitest.shared.ts
|
||||
export const sharedConfig = {
|
||||
testTimeout: 10000,
|
||||
setupFiles: ['./tests/setup.ts'],
|
||||
}
|
||||
|
||||
// vitest.config.ts
|
||||
import { sharedConfig } from './vitest.shared'
|
||||
|
||||
defineConfig({
|
||||
test: {
|
||||
projects: [
|
||||
{
|
||||
test: {
|
||||
...sharedConfig,
|
||||
name: 'unit',
|
||||
include: ['tests/unit/**/*.test.ts'],
|
||||
},
|
||||
},
|
||||
{
|
||||
test: {
|
||||
...sharedConfig,
|
||||
name: 'e2e',
|
||||
include: ['tests/e2e/**/*.test.ts'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Project-Specific Dependencies
|
||||
|
||||
Each project can have different dependencies inlined:
|
||||
|
||||
```ts
|
||||
defineConfig({
|
||||
test: {
|
||||
projects: [
|
||||
{
|
||||
test: {
|
||||
name: 'project-a',
|
||||
server: {
|
||||
deps: {
|
||||
inline: ['package-a'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Running Specific Projects
|
||||
|
||||
```bash
|
||||
# Run specific project
|
||||
vitest --project unit
|
||||
vitest --project integration
|
||||
|
||||
# Multiple projects
|
||||
vitest --project unit --project e2e
|
||||
|
||||
# Exclude project
|
||||
vitest --project.ignore browser
|
||||
```
|
||||
|
||||
## Providing Values to Projects
|
||||
|
||||
Share values from config to tests:
|
||||
|
||||
```ts
|
||||
// vitest.config.ts
|
||||
defineConfig({
|
||||
test: {
|
||||
projects: [
|
||||
{
|
||||
test: {
|
||||
name: 'staging',
|
||||
provide: {
|
||||
apiUrl: 'https://staging.api.com',
|
||||
debug: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
test: {
|
||||
name: 'production',
|
||||
provide: {
|
||||
apiUrl: 'https://api.com',
|
||||
debug: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
// In tests, use inject
|
||||
import { inject } from 'vitest'
|
||||
|
||||
test('uses correct api', () => {
|
||||
const url = inject('apiUrl')
|
||||
expect(url).toContain('api.com')
|
||||
})
|
||||
```
|
||||
|
||||
## With Fixtures
|
||||
|
||||
```ts
|
||||
const test = base.extend({
|
||||
apiUrl: ['/default', { injected: true }],
|
||||
})
|
||||
|
||||
test('uses injected url', ({ apiUrl }) => {
|
||||
// apiUrl comes from project's provide config
|
||||
})
|
||||
```
|
||||
|
||||
## Project Isolation
|
||||
|
||||
Each project runs in its own thread pool by default:
|
||||
|
||||
```ts
|
||||
defineConfig({
|
||||
test: {
|
||||
projects: [
|
||||
{
|
||||
test: {
|
||||
name: 'isolated',
|
||||
isolate: true, // Full isolation
|
||||
pool: 'forks',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Global Setup per Project
|
||||
|
||||
```ts
|
||||
defineConfig({
|
||||
test: {
|
||||
projects: [
|
||||
{
|
||||
test: {
|
||||
name: 'with-db',
|
||||
globalSetup: ['./tests/db-setup.ts'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Key Points
|
||||
|
||||
- Projects run in same Vitest process
|
||||
- Each project can have different environment, config
|
||||
- Use glob patterns for monorepo packages
|
||||
- Run specific projects with `--project` flag
|
||||
- Use `provide` to inject config values into tests
|
||||
- Projects inherit from root config unless overridden
|
||||
|
||||
<!--
|
||||
Source references:
|
||||
- https://vitest.dev/guide/projects.html
|
||||
-->
|
||||
237
.agents/skills/vitest/references/advanced-type-testing.md
Normal file
237
.agents/skills/vitest/references/advanced-type-testing.md
Normal file
@@ -0,0 +1,237 @@
|
||||
---
|
||||
name: type-testing
|
||||
description: Test TypeScript types with expectTypeOf and assertType
|
||||
---
|
||||
|
||||
# Type Testing
|
||||
|
||||
Test TypeScript types without runtime execution.
|
||||
|
||||
## Setup
|
||||
|
||||
Type tests use `.test-d.ts` extension:
|
||||
|
||||
```ts
|
||||
// math.test-d.ts
|
||||
import { expectTypeOf } from 'vitest'
|
||||
import { add } from './math'
|
||||
|
||||
test('add returns number', () => {
|
||||
expectTypeOf(add).returns.toBeNumber()
|
||||
})
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
```ts
|
||||
defineConfig({
|
||||
test: {
|
||||
typecheck: {
|
||||
enabled: true,
|
||||
|
||||
// Only type check
|
||||
only: false,
|
||||
|
||||
// Checker: 'tsc' or 'vue-tsc'
|
||||
checker: 'tsc',
|
||||
|
||||
// Include patterns
|
||||
include: ['**/*.test-d.ts'],
|
||||
|
||||
// tsconfig to use
|
||||
tsconfig: './tsconfig.json',
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## expectTypeOf API
|
||||
|
||||
```ts
|
||||
import { expectTypeOf } from 'vitest'
|
||||
|
||||
// Basic type checks
|
||||
expectTypeOf<string>().toBeString()
|
||||
expectTypeOf<number>().toBeNumber()
|
||||
expectTypeOf<boolean>().toBeBoolean()
|
||||
expectTypeOf<null>().toBeNull()
|
||||
expectTypeOf<undefined>().toBeUndefined()
|
||||
expectTypeOf<void>().toBeVoid()
|
||||
expectTypeOf<never>().toBeNever()
|
||||
expectTypeOf<any>().toBeAny()
|
||||
expectTypeOf<unknown>().toBeUnknown()
|
||||
expectTypeOf<object>().toBeObject()
|
||||
expectTypeOf<Function>().toBeFunction()
|
||||
expectTypeOf<[]>().toBeArray()
|
||||
expectTypeOf<symbol>().toBeSymbol()
|
||||
```
|
||||
|
||||
## Value Type Checking
|
||||
|
||||
```ts
|
||||
const value = 'hello'
|
||||
expectTypeOf(value).toBeString()
|
||||
|
||||
const obj = { name: 'test', count: 42 }
|
||||
expectTypeOf(obj).toMatchTypeOf<{ name: string }>()
|
||||
expectTypeOf(obj).toHaveProperty('name')
|
||||
```
|
||||
|
||||
## Function Types
|
||||
|
||||
```ts
|
||||
function greet(name: string): string {
|
||||
return `Hello, ${name}`
|
||||
}
|
||||
|
||||
expectTypeOf(greet).toBeFunction()
|
||||
expectTypeOf(greet).parameters.toEqualTypeOf<[string]>()
|
||||
expectTypeOf(greet).returns.toBeString()
|
||||
|
||||
// Parameter checking
|
||||
expectTypeOf(greet).parameter(0).toBeString()
|
||||
```
|
||||
|
||||
## Object Types
|
||||
|
||||
```ts
|
||||
interface User {
|
||||
id: number
|
||||
name: string
|
||||
email?: string
|
||||
}
|
||||
|
||||
expectTypeOf<User>().toHaveProperty('id')
|
||||
expectTypeOf<User>().toHaveProperty('name').toBeString()
|
||||
|
||||
// Check shape
|
||||
expectTypeOf({ id: 1, name: 'test' }).toMatchTypeOf<User>()
|
||||
```
|
||||
|
||||
## Equality vs Matching
|
||||
|
||||
```ts
|
||||
interface A { x: number }
|
||||
interface B { x: number; y: string }
|
||||
|
||||
// toMatchTypeOf - subset matching
|
||||
expectTypeOf<B>().toMatchTypeOf<A>() // B extends A
|
||||
|
||||
// toEqualTypeOf - exact match
|
||||
expectTypeOf<A>().not.toEqualTypeOf<B>() // Not exact match
|
||||
expectTypeOf<A>().toEqualTypeOf<{ x: number }>() // Exact match
|
||||
```
|
||||
|
||||
## Branded Types
|
||||
|
||||
```ts
|
||||
type UserId = number & { __brand: 'UserId' }
|
||||
type PostId = number & { __brand: 'PostId' }
|
||||
|
||||
expectTypeOf<UserId>().not.toEqualTypeOf<PostId>()
|
||||
expectTypeOf<UserId>().not.toEqualTypeOf<number>()
|
||||
```
|
||||
|
||||
## Generic Types
|
||||
|
||||
```ts
|
||||
function identity<T>(value: T): T {
|
||||
return value
|
||||
}
|
||||
|
||||
expectTypeOf(identity<string>).returns.toBeString()
|
||||
expectTypeOf(identity<number>).returns.toBeNumber()
|
||||
```
|
||||
|
||||
## Nullable Types
|
||||
|
||||
```ts
|
||||
type MaybeString = string | null | undefined
|
||||
|
||||
expectTypeOf<MaybeString>().toBeNullable()
|
||||
expectTypeOf<string>().not.toBeNullable()
|
||||
```
|
||||
|
||||
## assertType
|
||||
|
||||
Assert a value matches a type (no assertion at runtime):
|
||||
|
||||
```ts
|
||||
import { assertType } from 'vitest'
|
||||
|
||||
function getUser(): User | null {
|
||||
return { id: 1, name: 'test' }
|
||||
}
|
||||
|
||||
test('returns user', () => {
|
||||
const result = getUser()
|
||||
|
||||
// @ts-expect-error - should fail type check
|
||||
assertType<string>(result)
|
||||
|
||||
// Correct type
|
||||
assertType<User | null>(result)
|
||||
})
|
||||
```
|
||||
|
||||
## Using @ts-expect-error
|
||||
|
||||
Test that code produces type error:
|
||||
|
||||
```ts
|
||||
test('rejects wrong types', () => {
|
||||
function requireString(s: string) {}
|
||||
|
||||
// @ts-expect-error - number not assignable to string
|
||||
requireString(123)
|
||||
})
|
||||
```
|
||||
|
||||
## Running Type Tests
|
||||
|
||||
```bash
|
||||
# Run type tests
|
||||
vitest typecheck
|
||||
|
||||
# Run alongside unit tests
|
||||
vitest --typecheck
|
||||
|
||||
# Type tests only
|
||||
vitest --typecheck.only
|
||||
```
|
||||
|
||||
## Mixed Test Files
|
||||
|
||||
Combine runtime and type tests:
|
||||
|
||||
```ts
|
||||
// user.test.ts
|
||||
import { describe, expect, expectTypeOf, test } from 'vitest'
|
||||
import { createUser } from './user'
|
||||
|
||||
describe('createUser', () => {
|
||||
test('runtime: creates user', () => {
|
||||
const user = createUser('John')
|
||||
expect(user.name).toBe('John')
|
||||
})
|
||||
|
||||
test('types: returns User type', () => {
|
||||
expectTypeOf(createUser).returns.toMatchTypeOf<{ name: string }>()
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Key Points
|
||||
|
||||
- Use `.test-d.ts` for type-only tests
|
||||
- `expectTypeOf` for type assertions
|
||||
- `toMatchTypeOf` for subset matching
|
||||
- `toEqualTypeOf` for exact type matching
|
||||
- Use `@ts-expect-error` to test type errors
|
||||
- Run with `vitest typecheck` or `--typecheck`
|
||||
|
||||
<!--
|
||||
Source references:
|
||||
- https://vitest.dev/guide/testing-types.html
|
||||
- https://vitest.dev/api/expect-typeof.html
|
||||
-->
|
||||
249
.agents/skills/vitest/references/advanced-vi.md
Normal file
249
.agents/skills/vitest/references/advanced-vi.md
Normal file
@@ -0,0 +1,249 @@
|
||||
---
|
||||
name: vi-utilities
|
||||
description: vi helper for mocking, timers, utilities
|
||||
---
|
||||
|
||||
# Vi Utilities
|
||||
|
||||
The `vi` helper provides mocking and utility functions.
|
||||
|
||||
```ts
|
||||
import { vi } from 'vitest'
|
||||
```
|
||||
|
||||
## Mock Functions
|
||||
|
||||
```ts
|
||||
// Create mock
|
||||
const fn = vi.fn()
|
||||
const fnWithImpl = vi.fn((x) => x * 2)
|
||||
|
||||
// Check if mock
|
||||
vi.isMockFunction(fn) // true
|
||||
|
||||
// Mock methods
|
||||
fn.mockReturnValue(42)
|
||||
fn.mockReturnValueOnce(1)
|
||||
fn.mockResolvedValue(data)
|
||||
fn.mockRejectedValue(error)
|
||||
fn.mockImplementation(() => 'result')
|
||||
fn.mockImplementationOnce(() => 'once')
|
||||
|
||||
// Clear/reset
|
||||
fn.mockClear() // Clear call history
|
||||
fn.mockReset() // Clear history + implementation
|
||||
fn.mockRestore() // Restore original (for spies)
|
||||
```
|
||||
|
||||
## Spying
|
||||
|
||||
```ts
|
||||
const obj = { method: () => 'original' }
|
||||
|
||||
const spy = vi.spyOn(obj, 'method')
|
||||
obj.method()
|
||||
|
||||
expect(spy).toHaveBeenCalled()
|
||||
|
||||
// Mock implementation
|
||||
spy.mockReturnValue('mocked')
|
||||
|
||||
// Spy on getter/setter
|
||||
vi.spyOn(obj, 'prop', 'get').mockReturnValue('value')
|
||||
```
|
||||
|
||||
## Module Mocking
|
||||
|
||||
```ts
|
||||
// Hoisted to top of file
|
||||
vi.mock('./module', () => ({
|
||||
fn: vi.fn(),
|
||||
}))
|
||||
|
||||
// Partial mock
|
||||
vi.mock('./module', async (importOriginal) => ({
|
||||
...(await importOriginal()),
|
||||
specificFn: vi.fn(),
|
||||
}))
|
||||
|
||||
// Spy mode - keep implementation
|
||||
vi.mock('./module', { spy: true })
|
||||
|
||||
// Import actual module inside mock
|
||||
const actual = await vi.importActual('./module')
|
||||
|
||||
// Import as mock
|
||||
const mocked = await vi.importMock('./module')
|
||||
```
|
||||
|
||||
## Dynamic Mocking
|
||||
|
||||
```ts
|
||||
// Not hoisted - use with dynamic imports
|
||||
vi.doMock('./config', () => ({ key: 'value' }))
|
||||
const config = await import('./config')
|
||||
|
||||
// Unmock
|
||||
vi.doUnmock('./config')
|
||||
vi.unmock('./module') // Hoisted
|
||||
```
|
||||
|
||||
## Reset Modules
|
||||
|
||||
```ts
|
||||
// Clear module cache
|
||||
vi.resetModules()
|
||||
|
||||
// Wait for dynamic imports
|
||||
await vi.dynamicImportSettled()
|
||||
```
|
||||
|
||||
## Fake Timers
|
||||
|
||||
```ts
|
||||
vi.useFakeTimers()
|
||||
|
||||
setTimeout(() => console.log('done'), 1000)
|
||||
|
||||
// Advance time
|
||||
vi.advanceTimersByTime(1000)
|
||||
vi.advanceTimersByTimeAsync(1000) // For async callbacks
|
||||
vi.advanceTimersToNextTimer()
|
||||
vi.advanceTimersToNextFrame() // requestAnimationFrame
|
||||
|
||||
// Run all timers
|
||||
vi.runAllTimers()
|
||||
vi.runAllTimersAsync()
|
||||
vi.runOnlyPendingTimers()
|
||||
|
||||
// Clear timers
|
||||
vi.clearAllTimers()
|
||||
|
||||
// Check state
|
||||
vi.getTimerCount()
|
||||
vi.isFakeTimers()
|
||||
|
||||
// Restore
|
||||
vi.useRealTimers()
|
||||
```
|
||||
|
||||
## Mock Date/Time
|
||||
|
||||
```ts
|
||||
vi.setSystemTime(new Date('2024-01-01'))
|
||||
expect(new Date().getFullYear()).toBe(2024)
|
||||
|
||||
vi.getMockedSystemTime() // Get mocked date
|
||||
vi.getRealSystemTime() // Get real time (ms)
|
||||
```
|
||||
|
||||
## Global/Env Mocking
|
||||
|
||||
```ts
|
||||
// Stub global
|
||||
vi.stubGlobal('fetch', vi.fn())
|
||||
vi.unstubAllGlobals()
|
||||
|
||||
// Stub environment
|
||||
vi.stubEnv('API_KEY', 'test')
|
||||
vi.stubEnv('NODE_ENV', 'test')
|
||||
vi.unstubAllEnvs()
|
||||
```
|
||||
|
||||
## Hoisted Code
|
||||
|
||||
Run code before imports:
|
||||
|
||||
```ts
|
||||
const mock = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('./module', () => ({
|
||||
fn: mock, // Can reference hoisted variable
|
||||
}))
|
||||
```
|
||||
|
||||
## Waiting Utilities
|
||||
|
||||
```ts
|
||||
// Wait for callback to succeed
|
||||
await vi.waitFor(async () => {
|
||||
const el = document.querySelector('.loaded')
|
||||
expect(el).toBeTruthy()
|
||||
}, { timeout: 5000, interval: 100 })
|
||||
|
||||
// Wait for truthy value
|
||||
const element = await vi.waitUntil(
|
||||
() => document.querySelector('.loaded'),
|
||||
{ timeout: 5000 }
|
||||
)
|
||||
```
|
||||
|
||||
## Mock Object
|
||||
|
||||
Mock all methods of an object:
|
||||
|
||||
```ts
|
||||
const original = {
|
||||
method: () => 'real',
|
||||
nested: { fn: () => 'nested' },
|
||||
}
|
||||
|
||||
const mocked = vi.mockObject(original)
|
||||
mocked.method() // undefined (mocked)
|
||||
mocked.method.mockReturnValue('mocked')
|
||||
|
||||
// Spy mode
|
||||
const spied = vi.mockObject(original, { spy: true })
|
||||
spied.method() // 'real'
|
||||
expect(spied.method).toHaveBeenCalled()
|
||||
```
|
||||
|
||||
## Test Configuration
|
||||
|
||||
```ts
|
||||
vi.setConfig({
|
||||
testTimeout: 10_000,
|
||||
hookTimeout: 10_000,
|
||||
})
|
||||
|
||||
vi.resetConfig()
|
||||
```
|
||||
|
||||
## Global Mock Management
|
||||
|
||||
```ts
|
||||
vi.clearAllMocks() // Clear all mock call history
|
||||
vi.resetAllMocks() // Reset + clear implementation
|
||||
vi.restoreAllMocks() // Restore originals (spies)
|
||||
```
|
||||
|
||||
## vi.mocked Type Helper
|
||||
|
||||
TypeScript helper for mocked values:
|
||||
|
||||
```ts
|
||||
import { myFn } from './module'
|
||||
vi.mock('./module')
|
||||
|
||||
// Type as mock
|
||||
vi.mocked(myFn).mockReturnValue('typed')
|
||||
|
||||
// Deep mocking
|
||||
vi.mocked(myModule, { deep: true })
|
||||
|
||||
// Partial mock typing
|
||||
vi.mocked(fn, { partial: true }).mockResolvedValue({ ok: true })
|
||||
```
|
||||
|
||||
## Key Points
|
||||
|
||||
- `vi.mock` is hoisted - use `vi.doMock` for dynamic mocking
|
||||
- `vi.hoisted` lets you reference variables in mock factories
|
||||
- Use `vi.spyOn` to spy on existing methods
|
||||
- Fake timers require explicit setup and teardown
|
||||
- `vi.waitFor` retries until assertion passes
|
||||
|
||||
<!--
|
||||
Source references:
|
||||
- https://vitest.dev/api/vi.html
|
||||
-->
|
||||
166
.agents/skills/vitest/references/core-cli.md
Normal file
166
.agents/skills/vitest/references/core-cli.md
Normal file
@@ -0,0 +1,166 @@
|
||||
---
|
||||
name: vitest-cli
|
||||
description: Command line interface commands and options
|
||||
---
|
||||
|
||||
# Command Line Interface
|
||||
|
||||
## Commands
|
||||
|
||||
### `vitest`
|
||||
|
||||
Start Vitest in watch mode (dev) or run mode (CI):
|
||||
|
||||
```bash
|
||||
vitest # Watch mode in dev, run mode in CI
|
||||
vitest foobar # Run tests containing "foobar" in path
|
||||
vitest basic/foo.test.ts:10 # Run specific test by file and line number
|
||||
```
|
||||
|
||||
### `vitest run`
|
||||
|
||||
Run tests once without watch mode:
|
||||
|
||||
```bash
|
||||
vitest run
|
||||
vitest run --coverage
|
||||
```
|
||||
|
||||
### `vitest watch`
|
||||
|
||||
Explicitly start watch mode:
|
||||
|
||||
```bash
|
||||
vitest watch
|
||||
```
|
||||
|
||||
### `vitest related`
|
||||
|
||||
Run tests that import specific files (useful with lint-staged):
|
||||
|
||||
```bash
|
||||
vitest related src/index.ts src/utils.ts --run
|
||||
```
|
||||
|
||||
### `vitest bench`
|
||||
|
||||
Run only benchmark tests:
|
||||
|
||||
```bash
|
||||
vitest bench
|
||||
```
|
||||
|
||||
### `vitest list`
|
||||
|
||||
List all matching tests without running them:
|
||||
|
||||
```bash
|
||||
vitest list # List test names
|
||||
vitest list --json # Output as JSON
|
||||
vitest list --filesOnly # List only test files
|
||||
```
|
||||
|
||||
### `vitest init`
|
||||
|
||||
Initialize project setup:
|
||||
|
||||
```bash
|
||||
vitest init browser # Set up browser testing
|
||||
```
|
||||
|
||||
## Common Options
|
||||
|
||||
```bash
|
||||
# Configuration
|
||||
--config <path> # Path to config file
|
||||
--project <name> # Run specific project
|
||||
|
||||
# Filtering
|
||||
--testNamePattern, -t # Run tests matching pattern
|
||||
--changed # Run tests for changed files
|
||||
--changed HEAD~1 # Tests for last commit changes
|
||||
|
||||
# Reporters
|
||||
--reporter <name> # default, verbose, dot, json, html
|
||||
--reporter=html --outputFile=report.html
|
||||
|
||||
# Coverage
|
||||
--coverage # Enable coverage
|
||||
--coverage.provider v8 # Use v8 provider
|
||||
--coverage.reporter text,html
|
||||
|
||||
# Execution
|
||||
--shard <index>/<count> # Split tests across machines
|
||||
--bail <n> # Stop after n failures
|
||||
--retry <n> # Retry failed tests n times
|
||||
--sequence.shuffle # Randomize test order
|
||||
|
||||
# Watch mode
|
||||
--no-watch # Disable watch mode
|
||||
--standalone # Start without running tests
|
||||
|
||||
# Environment
|
||||
--environment <env> # jsdom, happy-dom, node
|
||||
--globals # Enable global APIs
|
||||
|
||||
# Debugging
|
||||
--inspect # Enable Node inspector
|
||||
--inspect-brk # Break on start
|
||||
|
||||
# Output
|
||||
--silent # Suppress console output
|
||||
--no-color # Disable colors
|
||||
```
|
||||
|
||||
## Package.json Scripts
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"test": "vitest",
|
||||
"test:run": "vitest run",
|
||||
"test:ui": "vitest --ui",
|
||||
"coverage": "vitest run --coverage"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Sharding for CI
|
||||
|
||||
Split tests across multiple machines:
|
||||
|
||||
```bash
|
||||
# Machine 1
|
||||
vitest run --shard=1/3 --reporter=blob
|
||||
|
||||
# Machine 2
|
||||
vitest run --shard=2/3 --reporter=blob
|
||||
|
||||
# Machine 3
|
||||
vitest run --shard=3/3 --reporter=blob
|
||||
|
||||
# Merge reports
|
||||
vitest --merge-reports --reporter=junit
|
||||
```
|
||||
|
||||
## Watch Mode Keyboard Shortcuts
|
||||
|
||||
In watch mode, press:
|
||||
- `a` - Run all tests
|
||||
- `f` - Run only failed tests
|
||||
- `u` - Update snapshots
|
||||
- `p` - Filter by filename pattern
|
||||
- `t` - Filter by test name pattern
|
||||
- `q` - Quit
|
||||
|
||||
## Key Points
|
||||
|
||||
- Watch mode is default in dev, run mode in CI (when `process.env.CI` is set)
|
||||
- Use `--run` flag to ensure single run (important for lint-staged)
|
||||
- Both camelCase (`--testTimeout`) and kebab-case (`--test-timeout`) work
|
||||
- Boolean options can be negated with `--no-` prefix
|
||||
|
||||
<!--
|
||||
Source references:
|
||||
- https://vitest.dev/guide/cli.html
|
||||
-->
|
||||
174
.agents/skills/vitest/references/core-config.md
Normal file
174
.agents/skills/vitest/references/core-config.md
Normal file
@@ -0,0 +1,174 @@
|
||||
---
|
||||
name: vitest-configuration
|
||||
description: Configure Vitest with vite.config.ts or vitest.config.ts
|
||||
---
|
||||
|
||||
# Configuration
|
||||
|
||||
Vitest reads configuration from `vitest.config.ts` or `vite.config.ts`. It shares the same config format as Vite.
|
||||
|
||||
## Basic Setup
|
||||
|
||||
```ts
|
||||
// vitest.config.ts
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
// test options
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Using with Existing Vite Config
|
||||
|
||||
Add Vitest types reference and use the `test` property:
|
||||
|
||||
```ts
|
||||
// vite.config.ts
|
||||
/// <reference types="vitest/config" />
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Merging Configs
|
||||
|
||||
If you have separate config files, use `mergeConfig`:
|
||||
|
||||
```ts
|
||||
// vitest.config.ts
|
||||
import { defineConfig, mergeConfig } from 'vitest/config'
|
||||
import viteConfig from './vite.config'
|
||||
|
||||
export default mergeConfig(viteConfig, defineConfig({
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
},
|
||||
}))
|
||||
```
|
||||
|
||||
## Common Options
|
||||
|
||||
```ts
|
||||
defineConfig({
|
||||
test: {
|
||||
// Enable global APIs (describe, it, expect) without imports
|
||||
globals: true,
|
||||
|
||||
// Test environment: 'node', 'jsdom', 'happy-dom'
|
||||
environment: 'node',
|
||||
|
||||
// Setup files to run before each test file
|
||||
setupFiles: ['./tests/setup.ts'],
|
||||
|
||||
// Include patterns for test files
|
||||
include: ['**/*.{test,spec}.{js,ts,jsx,tsx}'],
|
||||
|
||||
// Exclude patterns
|
||||
exclude: ['**/node_modules/**', '**/dist/**'],
|
||||
|
||||
// Test timeout in ms
|
||||
testTimeout: 5000,
|
||||
|
||||
// Hook timeout in ms
|
||||
hookTimeout: 10000,
|
||||
|
||||
// Enable watch mode by default
|
||||
watch: true,
|
||||
|
||||
// Coverage configuration
|
||||
coverage: {
|
||||
provider: 'v8', // or 'istanbul'
|
||||
reporter: ['text', 'html'],
|
||||
include: ['src/**/*.ts'],
|
||||
},
|
||||
|
||||
// Run tests in isolation (each file in separate process)
|
||||
isolate: true,
|
||||
|
||||
// Pool for running tests: 'threads', 'forks', 'vmThreads'
|
||||
pool: 'threads',
|
||||
|
||||
// Number of threads/processes
|
||||
poolOptions: {
|
||||
threads: {
|
||||
maxThreads: 4,
|
||||
minThreads: 1,
|
||||
},
|
||||
},
|
||||
|
||||
// Automatically clear mocks between tests
|
||||
clearMocks: true,
|
||||
|
||||
// Restore mocks between tests
|
||||
restoreMocks: true,
|
||||
|
||||
// Retry failed tests
|
||||
retry: 0,
|
||||
|
||||
// Stop after first failure
|
||||
bail: 0,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Conditional Configuration
|
||||
|
||||
Use `mode` or `process.env.VITEST` for test-specific config:
|
||||
|
||||
```ts
|
||||
export default defineConfig(({ mode }) => ({
|
||||
plugins: mode === 'test' ? [] : [myPlugin()],
|
||||
test: {
|
||||
// test options
|
||||
},
|
||||
}))
|
||||
```
|
||||
|
||||
## Projects (Monorepos)
|
||||
|
||||
Run different configurations in the same Vitest process:
|
||||
|
||||
```ts
|
||||
defineConfig({
|
||||
test: {
|
||||
projects: [
|
||||
'packages/*',
|
||||
{
|
||||
test: {
|
||||
name: 'unit',
|
||||
include: ['tests/unit/**/*.test.ts'],
|
||||
environment: 'node',
|
||||
},
|
||||
},
|
||||
{
|
||||
test: {
|
||||
name: 'integration',
|
||||
include: ['tests/integration/**/*.test.ts'],
|
||||
environment: 'jsdom',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Key Points
|
||||
|
||||
- Vitest uses Vite's transformation pipeline - same `resolve.alias`, plugins work
|
||||
- `vitest.config.ts` takes priority over `vite.config.ts`
|
||||
- Use `--config` flag to specify a custom config path
|
||||
- `process.env.VITEST` is set to `true` when running tests
|
||||
- Test config uses `test` property, rest is Vite config
|
||||
|
||||
<!--
|
||||
Source references:
|
||||
- https://vitest.dev/guide/#configuring-vitest
|
||||
- https://vitest.dev/config/
|
||||
-->
|
||||
193
.agents/skills/vitest/references/core-describe.md
Normal file
193
.agents/skills/vitest/references/core-describe.md
Normal file
@@ -0,0 +1,193 @@
|
||||
---
|
||||
name: describe-api
|
||||
description: describe/suite for grouping tests into logical blocks
|
||||
---
|
||||
|
||||
# Describe API
|
||||
|
||||
Group related tests into suites for organization and shared setup.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```ts
|
||||
import { describe, expect, test } from 'vitest'
|
||||
|
||||
describe('Math', () => {
|
||||
test('adds numbers', () => {
|
||||
expect(1 + 1).toBe(2)
|
||||
})
|
||||
|
||||
test('subtracts numbers', () => {
|
||||
expect(3 - 1).toBe(2)
|
||||
})
|
||||
})
|
||||
|
||||
// Alias: suite
|
||||
import { suite } from 'vitest'
|
||||
suite('equivalent to describe', () => {})
|
||||
```
|
||||
|
||||
## Nested Suites
|
||||
|
||||
```ts
|
||||
describe('User', () => {
|
||||
describe('when logged in', () => {
|
||||
test('shows dashboard', () => {})
|
||||
test('can update profile', () => {})
|
||||
})
|
||||
|
||||
describe('when logged out', () => {
|
||||
test('shows login page', () => {})
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Suite Options
|
||||
|
||||
```ts
|
||||
// All tests inherit options
|
||||
describe('slow tests', { timeout: 30_000 }, () => {
|
||||
test('test 1', () => {}) // 30s timeout
|
||||
test('test 2', () => {}) // 30s timeout
|
||||
})
|
||||
```
|
||||
|
||||
## Suite Modifiers
|
||||
|
||||
### Skip Suites
|
||||
|
||||
```ts
|
||||
describe.skip('skipped suite', () => {
|
||||
test('wont run', () => {})
|
||||
})
|
||||
|
||||
// Conditional
|
||||
describe.skipIf(process.env.CI)('not in CI', () => {})
|
||||
describe.runIf(!process.env.CI)('only local', () => {})
|
||||
```
|
||||
|
||||
### Focus Suites
|
||||
|
||||
```ts
|
||||
describe.only('only this suite runs', () => {
|
||||
test('runs', () => {})
|
||||
})
|
||||
```
|
||||
|
||||
### Todo Suites
|
||||
|
||||
```ts
|
||||
describe.todo('implement later')
|
||||
```
|
||||
|
||||
### Concurrent Suites
|
||||
|
||||
```ts
|
||||
// All tests run in parallel
|
||||
describe.concurrent('parallel tests', () => {
|
||||
test('test 1', async ({ expect }) => {})
|
||||
test('test 2', async ({ expect }) => {})
|
||||
})
|
||||
```
|
||||
|
||||
### Sequential in Concurrent
|
||||
|
||||
```ts
|
||||
describe.concurrent('parallel', () => {
|
||||
test('concurrent 1', async () => {})
|
||||
|
||||
describe.sequential('must be sequential', () => {
|
||||
test('step 1', async () => {})
|
||||
test('step 2', async () => {})
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Shuffle Tests
|
||||
|
||||
```ts
|
||||
describe.shuffle('random order', () => {
|
||||
test('test 1', () => {})
|
||||
test('test 2', () => {})
|
||||
test('test 3', () => {})
|
||||
})
|
||||
|
||||
// Or with option
|
||||
describe('random', { shuffle: true }, () => {})
|
||||
```
|
||||
|
||||
## Parameterized Suites
|
||||
|
||||
### describe.each
|
||||
|
||||
```ts
|
||||
describe.each([
|
||||
{ name: 'Chrome', version: 100 },
|
||||
{ name: 'Firefox', version: 90 },
|
||||
])('$name browser', ({ name, version }) => {
|
||||
test('has version', () => {
|
||||
expect(version).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### describe.for
|
||||
|
||||
```ts
|
||||
describe.for([
|
||||
['Chrome', 100],
|
||||
['Firefox', 90],
|
||||
])('%s browser', ([name, version]) => {
|
||||
test('has version', () => {
|
||||
expect(version).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Hooks in Suites
|
||||
|
||||
```ts
|
||||
describe('Database', () => {
|
||||
let db
|
||||
|
||||
beforeAll(async () => {
|
||||
db = await createDb()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await db.close()
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
await db.clear()
|
||||
})
|
||||
|
||||
test('insert works', async () => {
|
||||
await db.insert({ name: 'test' })
|
||||
expect(await db.count()).toBe(1)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Modifier Combinations
|
||||
|
||||
All modifiers can be chained:
|
||||
|
||||
```ts
|
||||
describe.skip.concurrent('skipped concurrent', () => {})
|
||||
describe.only.shuffle('only and shuffled', () => {})
|
||||
describe.concurrent.skip('equivalent', () => {})
|
||||
```
|
||||
|
||||
## Key Points
|
||||
|
||||
- Top-level tests belong to an implicit file suite
|
||||
- Nested suites inherit parent's options (timeout, retry, etc.)
|
||||
- Hooks are scoped to their suite and nested suites
|
||||
- Use `describe.concurrent` with context's `expect` for snapshots
|
||||
- Shuffle order depends on `sequence.seed` config
|
||||
|
||||
<!--
|
||||
Source references:
|
||||
- https://vitest.dev/api/describe.html
|
||||
-->
|
||||
219
.agents/skills/vitest/references/core-expect.md
Normal file
219
.agents/skills/vitest/references/core-expect.md
Normal file
@@ -0,0 +1,219 @@
|
||||
---
|
||||
name: expect-api
|
||||
description: Assertions with matchers, asymmetric matchers, and custom matchers
|
||||
---
|
||||
|
||||
# Expect API
|
||||
|
||||
Vitest uses Chai assertions with Jest-compatible API.
|
||||
|
||||
## Basic Assertions
|
||||
|
||||
```ts
|
||||
import { expect, test } from 'vitest'
|
||||
|
||||
test('assertions', () => {
|
||||
// Equality
|
||||
expect(1 + 1).toBe(2) // Strict equality (===)
|
||||
expect({ a: 1 }).toEqual({ a: 1 }) // Deep equality
|
||||
|
||||
// Truthiness
|
||||
expect(true).toBeTruthy()
|
||||
expect(false).toBeFalsy()
|
||||
expect(null).toBeNull()
|
||||
expect(undefined).toBeUndefined()
|
||||
expect('value').toBeDefined()
|
||||
|
||||
// Numbers
|
||||
expect(10).toBeGreaterThan(5)
|
||||
expect(10).toBeGreaterThanOrEqual(10)
|
||||
expect(5).toBeLessThan(10)
|
||||
expect(0.1 + 0.2).toBeCloseTo(0.3, 5)
|
||||
|
||||
// Strings
|
||||
expect('hello world').toMatch(/world/)
|
||||
expect('hello').toContain('ell')
|
||||
|
||||
// Arrays
|
||||
expect([1, 2, 3]).toContain(2)
|
||||
expect([{ a: 1 }]).toContainEqual({ a: 1 })
|
||||
expect([1, 2, 3]).toHaveLength(3)
|
||||
|
||||
// Objects
|
||||
expect({ a: 1, b: 2 }).toHaveProperty('a')
|
||||
expect({ a: 1, b: 2 }).toHaveProperty('a', 1)
|
||||
expect({ a: { b: 1 } }).toHaveProperty('a.b', 1)
|
||||
expect({ a: 1 }).toMatchObject({ a: 1 })
|
||||
|
||||
// Types
|
||||
expect('string').toBeTypeOf('string')
|
||||
expect(new Date()).toBeInstanceOf(Date)
|
||||
})
|
||||
```
|
||||
|
||||
## Negation
|
||||
|
||||
```ts
|
||||
expect(1).not.toBe(2)
|
||||
expect({ a: 1 }).not.toEqual({ a: 2 })
|
||||
```
|
||||
|
||||
## Error Assertions
|
||||
|
||||
```ts
|
||||
// Sync errors - wrap in function
|
||||
expect(() => throwError()).toThrow()
|
||||
expect(() => throwError()).toThrow('message')
|
||||
expect(() => throwError()).toThrow(/pattern/)
|
||||
expect(() => throwError()).toThrow(CustomError)
|
||||
|
||||
// Async errors - use rejects
|
||||
await expect(asyncThrow()).rejects.toThrow('error')
|
||||
```
|
||||
|
||||
## Promise Assertions
|
||||
|
||||
```ts
|
||||
// Resolves
|
||||
await expect(Promise.resolve(1)).resolves.toBe(1)
|
||||
await expect(fetchData()).resolves.toEqual({ data: true })
|
||||
|
||||
// Rejects
|
||||
await expect(Promise.reject('error')).rejects.toBe('error')
|
||||
await expect(failingFetch()).rejects.toThrow()
|
||||
```
|
||||
|
||||
## Spy/Mock Assertions
|
||||
|
||||
```ts
|
||||
const fn = vi.fn()
|
||||
fn('arg1', 'arg2')
|
||||
fn('arg3')
|
||||
|
||||
expect(fn).toHaveBeenCalled()
|
||||
expect(fn).toHaveBeenCalledTimes(2)
|
||||
expect(fn).toHaveBeenCalledWith('arg1', 'arg2')
|
||||
expect(fn).toHaveBeenLastCalledWith('arg3')
|
||||
expect(fn).toHaveBeenNthCalledWith(1, 'arg1', 'arg2')
|
||||
|
||||
expect(fn).toHaveReturned()
|
||||
expect(fn).toHaveReturnedWith(value)
|
||||
```
|
||||
|
||||
## Asymmetric Matchers
|
||||
|
||||
Use inside `toEqual`, `toHaveBeenCalledWith`, etc:
|
||||
|
||||
```ts
|
||||
expect({ id: 1, name: 'test' }).toEqual({
|
||||
id: expect.any(Number),
|
||||
name: expect.any(String),
|
||||
})
|
||||
|
||||
expect({ a: 1, b: 2, c: 3 }).toEqual(
|
||||
expect.objectContaining({ a: 1 })
|
||||
)
|
||||
|
||||
expect([1, 2, 3, 4]).toEqual(
|
||||
expect.arrayContaining([1, 3])
|
||||
)
|
||||
|
||||
expect('hello world').toEqual(
|
||||
expect.stringContaining('world')
|
||||
)
|
||||
|
||||
expect('hello world').toEqual(
|
||||
expect.stringMatching(/world$/)
|
||||
)
|
||||
|
||||
expect({ value: null }).toEqual({
|
||||
value: expect.anything() // Matches anything except null/undefined
|
||||
})
|
||||
|
||||
// Negate with expect.not
|
||||
expect([1, 2]).toEqual(
|
||||
expect.not.arrayContaining([3])
|
||||
)
|
||||
```
|
||||
|
||||
## Soft Assertions
|
||||
|
||||
Continue test after failure:
|
||||
|
||||
```ts
|
||||
expect.soft(1).toBe(2) // Marks test failed but continues
|
||||
expect.soft(2).toBe(3) // Also runs
|
||||
// All failures reported at end
|
||||
```
|
||||
|
||||
## Poll Assertions
|
||||
|
||||
Retry until passes:
|
||||
|
||||
```ts
|
||||
await expect.poll(() => fetchStatus()).toBe('ready')
|
||||
|
||||
await expect.poll(
|
||||
() => document.querySelector('.element'),
|
||||
{ interval: 100, timeout: 5000 }
|
||||
).toBeTruthy()
|
||||
```
|
||||
|
||||
## Assertion Count
|
||||
|
||||
```ts
|
||||
test('async assertions', async () => {
|
||||
expect.assertions(2) // Exactly 2 assertions must run
|
||||
|
||||
await doAsync((data) => {
|
||||
expect(data).toBeDefined()
|
||||
expect(data.id).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
test('at least one', () => {
|
||||
expect.hasAssertions() // At least 1 assertion must run
|
||||
})
|
||||
```
|
||||
|
||||
## Extending Matchers
|
||||
|
||||
```ts
|
||||
expect.extend({
|
||||
toBeWithinRange(received, floor, ceiling) {
|
||||
const pass = received >= floor && received <= ceiling
|
||||
return {
|
||||
pass,
|
||||
message: () =>
|
||||
`expected ${received} to be within range ${floor} - ${ceiling}`,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
test('custom matcher', () => {
|
||||
expect(100).toBeWithinRange(90, 110)
|
||||
})
|
||||
```
|
||||
|
||||
## Snapshot Assertions
|
||||
|
||||
```ts
|
||||
expect(data).toMatchSnapshot()
|
||||
expect(data).toMatchInlineSnapshot(`{ "id": 1 }`)
|
||||
await expect(result).toMatchFileSnapshot('./expected.json')
|
||||
|
||||
expect(() => throw new Error('fail')).toThrowErrorMatchingSnapshot()
|
||||
```
|
||||
|
||||
## Key Points
|
||||
|
||||
- Use `toBe` for primitives, `toEqual` for objects/arrays
|
||||
- `toStrictEqual` checks undefined properties and array sparseness
|
||||
- Always `await` async assertions (`resolves`, `rejects`, `poll`)
|
||||
- Use context's `expect` in concurrent tests for correct tracking
|
||||
- `toThrow` requires wrapping sync code in a function
|
||||
|
||||
<!--
|
||||
Source references:
|
||||
- https://vitest.dev/api/expect.html
|
||||
-->
|
||||
244
.agents/skills/vitest/references/core-hooks.md
Normal file
244
.agents/skills/vitest/references/core-hooks.md
Normal file
@@ -0,0 +1,244 @@
|
||||
---
|
||||
name: lifecycle-hooks
|
||||
description: beforeEach, afterEach, beforeAll, afterAll, and around hooks
|
||||
---
|
||||
|
||||
# Lifecycle Hooks
|
||||
|
||||
## Basic Hooks
|
||||
|
||||
```ts
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, test } from 'vitest'
|
||||
|
||||
beforeAll(async () => {
|
||||
// Runs once before all tests in file/suite
|
||||
await setupDatabase()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
// Runs once after all tests in file/suite
|
||||
await teardownDatabase()
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
// Runs before each test
|
||||
await clearTestData()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
// Runs after each test
|
||||
await cleanupMocks()
|
||||
})
|
||||
```
|
||||
|
||||
## Cleanup Return Pattern
|
||||
|
||||
Return cleanup function from `before*` hooks:
|
||||
|
||||
```ts
|
||||
beforeAll(async () => {
|
||||
const server = await startServer()
|
||||
|
||||
// Returned function runs as afterAll
|
||||
return async () => {
|
||||
await server.close()
|
||||
}
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
const connection = await connect()
|
||||
|
||||
// Runs as afterEach
|
||||
return () => connection.close()
|
||||
})
|
||||
```
|
||||
|
||||
## Scoped Hooks
|
||||
|
||||
Hooks apply to current suite and nested suites:
|
||||
|
||||
```ts
|
||||
describe('outer', () => {
|
||||
beforeEach(() => console.log('outer before'))
|
||||
|
||||
test('test 1', () => {}) // outer before → test
|
||||
|
||||
describe('inner', () => {
|
||||
beforeEach(() => console.log('inner before'))
|
||||
|
||||
test('test 2', () => {}) // outer before → inner before → test
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Hook Timeout
|
||||
|
||||
```ts
|
||||
beforeAll(async () => {
|
||||
await slowSetup()
|
||||
}, 30_000) // 30 second timeout
|
||||
```
|
||||
|
||||
## Around Hooks
|
||||
|
||||
Wrap tests with setup/teardown context:
|
||||
|
||||
```ts
|
||||
import { aroundEach, test } from 'vitest'
|
||||
|
||||
// Wrap each test in database transaction
|
||||
aroundEach(async (runTest) => {
|
||||
await db.beginTransaction()
|
||||
await runTest() // Must be called!
|
||||
await db.rollback()
|
||||
})
|
||||
|
||||
test('insert user', async () => {
|
||||
await db.insert({ name: 'Alice' })
|
||||
// Automatically rolled back after test
|
||||
})
|
||||
```
|
||||
|
||||
### aroundAll
|
||||
|
||||
Wrap entire suite:
|
||||
|
||||
```ts
|
||||
import { aroundAll, test } from 'vitest'
|
||||
|
||||
aroundAll(async (runSuite) => {
|
||||
console.log('before all tests')
|
||||
await runSuite() // Must be called!
|
||||
console.log('after all tests')
|
||||
})
|
||||
```
|
||||
|
||||
### Multiple Around Hooks
|
||||
|
||||
Nested like onion layers:
|
||||
|
||||
```ts
|
||||
aroundEach(async (runTest) => {
|
||||
console.log('outer before')
|
||||
await runTest()
|
||||
console.log('outer after')
|
||||
})
|
||||
|
||||
aroundEach(async (runTest) => {
|
||||
console.log('inner before')
|
||||
await runTest()
|
||||
console.log('inner after')
|
||||
})
|
||||
|
||||
// Order: outer before → inner before → test → inner after → outer after
|
||||
```
|
||||
|
||||
## Test Hooks
|
||||
|
||||
Inside test body:
|
||||
|
||||
```ts
|
||||
import { onTestFailed, onTestFinished, test } from 'vitest'
|
||||
|
||||
test('with cleanup', () => {
|
||||
const db = connect()
|
||||
|
||||
// Runs after test finishes (pass or fail)
|
||||
onTestFinished(() => db.close())
|
||||
|
||||
// Only runs if test fails
|
||||
onTestFailed(({ task }) => {
|
||||
console.log('Failed:', task.result?.errors)
|
||||
})
|
||||
|
||||
db.query('SELECT * FROM users')
|
||||
})
|
||||
```
|
||||
|
||||
### Reusable Cleanup Pattern
|
||||
|
||||
```ts
|
||||
function useTestDb() {
|
||||
const db = connect()
|
||||
onTestFinished(() => db.close())
|
||||
return db
|
||||
}
|
||||
|
||||
test('query users', () => {
|
||||
const db = useTestDb()
|
||||
expect(db.query('SELECT * FROM users')).toBeDefined()
|
||||
})
|
||||
|
||||
test('query orders', () => {
|
||||
const db = useTestDb() // Fresh connection, auto-closed
|
||||
expect(db.query('SELECT * FROM orders')).toBeDefined()
|
||||
})
|
||||
```
|
||||
|
||||
## Concurrent Test Hooks
|
||||
|
||||
For concurrent tests, use context's hooks:
|
||||
|
||||
```ts
|
||||
test.concurrent('concurrent', ({ onTestFinished }) => {
|
||||
const resource = allocate()
|
||||
onTestFinished(() => resource.release())
|
||||
})
|
||||
```
|
||||
|
||||
## Extended Test Hooks
|
||||
|
||||
With `test.extend`, hooks are type-aware:
|
||||
|
||||
```ts
|
||||
const test = base.extend<{ db: Database }>({
|
||||
db: async ({}, use) => {
|
||||
const db = await createDb()
|
||||
await use(db)
|
||||
await db.close()
|
||||
},
|
||||
})
|
||||
|
||||
// These hooks know about `db` fixture
|
||||
test.beforeEach(({ db }) => {
|
||||
db.seed()
|
||||
})
|
||||
|
||||
test.afterEach(({ db }) => {
|
||||
db.clear()
|
||||
})
|
||||
```
|
||||
|
||||
## Hook Execution Order
|
||||
|
||||
Default order (stack):
|
||||
1. `beforeAll` (in order)
|
||||
2. `beforeEach` (in order)
|
||||
3. Test
|
||||
4. `afterEach` (reverse order)
|
||||
5. `afterAll` (reverse order)
|
||||
|
||||
Configure with `sequence.hooks`:
|
||||
|
||||
```ts
|
||||
defineConfig({
|
||||
test: {
|
||||
sequence: {
|
||||
hooks: 'list', // 'stack' (default), 'list', 'parallel'
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Key Points
|
||||
|
||||
- Hooks are not called during type checking
|
||||
- Return cleanup function from `before*` to avoid `after*` duplication
|
||||
- `aroundEach`/`aroundAll` must call `runTest()`/`runSuite()`
|
||||
- `onTestFinished` always runs, even if test fails
|
||||
- Use context hooks for concurrent tests
|
||||
|
||||
<!--
|
||||
Source references:
|
||||
- https://vitest.dev/api/hooks.html
|
||||
-->
|
||||
233
.agents/skills/vitest/references/core-test-api.md
Normal file
233
.agents/skills/vitest/references/core-test-api.md
Normal file
@@ -0,0 +1,233 @@
|
||||
---
|
||||
name: test-api
|
||||
description: test/it function for defining tests with modifiers
|
||||
---
|
||||
|
||||
# Test API
|
||||
|
||||
## Basic Test
|
||||
|
||||
```ts
|
||||
import { expect, test } from 'vitest'
|
||||
|
||||
test('adds numbers', () => {
|
||||
expect(1 + 1).toBe(2)
|
||||
})
|
||||
|
||||
// Alias: it
|
||||
import { it } from 'vitest'
|
||||
|
||||
it('works the same', () => {
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
```
|
||||
|
||||
## Async Tests
|
||||
|
||||
```ts
|
||||
test('async test', async () => {
|
||||
const result = await fetchData()
|
||||
expect(result).toBeDefined()
|
||||
})
|
||||
|
||||
// Promises are automatically awaited
|
||||
test('returns promise', () => {
|
||||
return fetchData().then(result => {
|
||||
expect(result).toBeDefined()
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Test Options
|
||||
|
||||
```ts
|
||||
// Timeout (default: 5000ms)
|
||||
test('slow test', async () => {
|
||||
// ...
|
||||
}, 10_000)
|
||||
|
||||
// Or with options object
|
||||
test('with options', { timeout: 10_000, retry: 2 }, async () => {
|
||||
// ...
|
||||
})
|
||||
```
|
||||
|
||||
## Test Modifiers
|
||||
|
||||
### Skip Tests
|
||||
|
||||
```ts
|
||||
test.skip('skipped test', () => {
|
||||
// Won't run
|
||||
})
|
||||
|
||||
// Conditional skip
|
||||
test.skipIf(process.env.CI)('not in CI', () => {})
|
||||
test.runIf(process.env.CI)('only in CI', () => {})
|
||||
|
||||
// Dynamic skip via context
|
||||
test('dynamic skip', ({ skip }) => {
|
||||
skip(someCondition, 'reason')
|
||||
// ...
|
||||
})
|
||||
```
|
||||
|
||||
### Focus Tests
|
||||
|
||||
```ts
|
||||
test.only('only this runs', () => {
|
||||
// Other tests in file are skipped
|
||||
})
|
||||
```
|
||||
|
||||
### Todo Tests
|
||||
|
||||
```ts
|
||||
test.todo('implement later')
|
||||
|
||||
test.todo('with body', () => {
|
||||
// Not run, shows in report
|
||||
})
|
||||
```
|
||||
|
||||
### Failing Tests
|
||||
|
||||
```ts
|
||||
test.fails('expected to fail', () => {
|
||||
expect(1).toBe(2) // Test passes because assertion fails
|
||||
})
|
||||
```
|
||||
|
||||
### Concurrent Tests
|
||||
|
||||
```ts
|
||||
// Run tests in parallel
|
||||
test.concurrent('test 1', async ({ expect }) => {
|
||||
// Use context.expect for concurrent tests
|
||||
expect(await fetch1()).toBe('result')
|
||||
})
|
||||
|
||||
test.concurrent('test 2', async ({ expect }) => {
|
||||
expect(await fetch2()).toBe('result')
|
||||
})
|
||||
```
|
||||
|
||||
### Sequential Tests
|
||||
|
||||
```ts
|
||||
// Force sequential in concurrent context
|
||||
test.sequential('must run alone', async () => {})
|
||||
```
|
||||
|
||||
## Parameterized Tests
|
||||
|
||||
### test.each
|
||||
|
||||
```ts
|
||||
test.each([
|
||||
[1, 1, 2],
|
||||
[1, 2, 3],
|
||||
[2, 1, 3],
|
||||
])('add(%i, %i) = %i', (a, b, expected) => {
|
||||
expect(a + b).toBe(expected)
|
||||
})
|
||||
|
||||
// With objects
|
||||
test.each([
|
||||
{ a: 1, b: 1, expected: 2 },
|
||||
{ a: 1, b: 2, expected: 3 },
|
||||
])('add($a, $b) = $expected', ({ a, b, expected }) => {
|
||||
expect(a + b).toBe(expected)
|
||||
})
|
||||
|
||||
// Template literal
|
||||
test.each`
|
||||
a | b | expected
|
||||
${1} | ${1} | ${2}
|
||||
${1} | ${2} | ${3}
|
||||
`('add($a, $b) = $expected', ({ a, b, expected }) => {
|
||||
expect(a + b).toBe(expected)
|
||||
})
|
||||
```
|
||||
|
||||
### test.for
|
||||
|
||||
Preferred over `.each` - doesn't spread arrays:
|
||||
|
||||
```ts
|
||||
test.for([
|
||||
[1, 1, 2],
|
||||
[1, 2, 3],
|
||||
])('add(%i, %i) = %i', ([a, b, expected], { expect }) => {
|
||||
// Second arg is TestContext
|
||||
expect(a + b).toBe(expected)
|
||||
})
|
||||
```
|
||||
|
||||
## Test Context
|
||||
|
||||
First argument provides context utilities:
|
||||
|
||||
```ts
|
||||
test('with context', ({ expect, skip, task }) => {
|
||||
console.log(task.name) // Test name
|
||||
skip(someCondition) // Skip dynamically
|
||||
expect(1).toBe(1) // Context-bound expect
|
||||
})
|
||||
```
|
||||
|
||||
## Custom Test with Fixtures
|
||||
|
||||
```ts
|
||||
import { test as base } from 'vitest'
|
||||
|
||||
const test = base.extend({
|
||||
db: async ({}, use) => {
|
||||
const db = await createDb()
|
||||
await use(db)
|
||||
await db.close()
|
||||
},
|
||||
})
|
||||
|
||||
test('query', async ({ db }) => {
|
||||
const users = await db.query('SELECT * FROM users')
|
||||
expect(users).toBeDefined()
|
||||
})
|
||||
```
|
||||
|
||||
## Retry Configuration
|
||||
|
||||
```ts
|
||||
test('flaky test', { retry: 3 }, async () => {
|
||||
// Retries up to 3 times on failure
|
||||
})
|
||||
|
||||
// Advanced retry options
|
||||
test('with delay', {
|
||||
retry: {
|
||||
count: 3,
|
||||
delay: 1000,
|
||||
condition: /timeout/i, // Only retry on timeout errors
|
||||
},
|
||||
}, async () => {})
|
||||
```
|
||||
|
||||
## Tags
|
||||
|
||||
```ts
|
||||
test('database test', { tags: ['db', 'slow'] }, async () => {})
|
||||
|
||||
// Run with: vitest --tags db
|
||||
```
|
||||
|
||||
## Key Points
|
||||
|
||||
- Tests with no body are marked as `todo`
|
||||
- `test.only` throws in CI unless `allowOnly: true`
|
||||
- Use context's `expect` for concurrent tests and snapshots
|
||||
- Function name is used as test name if passed as first arg
|
||||
|
||||
<!--
|
||||
Source references:
|
||||
- https://vitest.dev/api/test.html
|
||||
-->
|
||||
250
.agents/skills/vitest/references/features-concurrency.md
Normal file
250
.agents/skills/vitest/references/features-concurrency.md
Normal file
@@ -0,0 +1,250 @@
|
||||
---
|
||||
name: concurrency-parallelism
|
||||
description: Concurrent tests, parallel execution, and sharding
|
||||
---
|
||||
|
||||
# Concurrency & Parallelism
|
||||
|
||||
## File Parallelism
|
||||
|
||||
By default, Vitest runs test files in parallel across workers:
|
||||
|
||||
```ts
|
||||
defineConfig({
|
||||
test: {
|
||||
// Run files in parallel (default: true)
|
||||
fileParallelism: true,
|
||||
|
||||
// Number of worker threads
|
||||
maxWorkers: 4,
|
||||
minWorkers: 1,
|
||||
|
||||
// Pool type: 'threads', 'forks', 'vmThreads'
|
||||
pool: 'threads',
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Concurrent Tests
|
||||
|
||||
Run tests within a file in parallel:
|
||||
|
||||
```ts
|
||||
// Individual concurrent tests
|
||||
test.concurrent('test 1', async ({ expect }) => {
|
||||
expect(await fetch1()).toBe('result')
|
||||
})
|
||||
|
||||
test.concurrent('test 2', async ({ expect }) => {
|
||||
expect(await fetch2()).toBe('result')
|
||||
})
|
||||
|
||||
// All tests in suite concurrent
|
||||
describe.concurrent('parallel suite', () => {
|
||||
test('test 1', async ({ expect }) => {})
|
||||
test('test 2', async ({ expect }) => {})
|
||||
})
|
||||
```
|
||||
|
||||
**Important:** Use `{ expect }` from context for concurrent tests.
|
||||
|
||||
## Sequential in Concurrent Context
|
||||
|
||||
Force sequential execution:
|
||||
|
||||
```ts
|
||||
describe.concurrent('mostly parallel', () => {
|
||||
test('parallel 1', async () => {})
|
||||
test('parallel 2', async () => {})
|
||||
|
||||
test.sequential('must run alone 1', async () => {})
|
||||
test.sequential('must run alone 2', async () => {})
|
||||
})
|
||||
|
||||
// Or entire suite
|
||||
describe.sequential('sequential suite', () => {
|
||||
test('first', () => {})
|
||||
test('second', () => {})
|
||||
})
|
||||
```
|
||||
|
||||
## Max Concurrency
|
||||
|
||||
Limit concurrent tests:
|
||||
|
||||
```ts
|
||||
defineConfig({
|
||||
test: {
|
||||
maxConcurrency: 5, // Max concurrent tests per file
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Isolation
|
||||
|
||||
Each file runs in isolated environment by default:
|
||||
|
||||
```ts
|
||||
defineConfig({
|
||||
test: {
|
||||
// Disable isolation for faster runs (less safe)
|
||||
isolate: false,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Sharding
|
||||
|
||||
Split tests across machines:
|
||||
|
||||
```bash
|
||||
# Machine 1
|
||||
vitest run --shard=1/3
|
||||
|
||||
# Machine 2
|
||||
vitest run --shard=2/3
|
||||
|
||||
# Machine 3
|
||||
vitest run --shard=3/3
|
||||
```
|
||||
|
||||
### CI Example (GitHub Actions)
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
test:
|
||||
strategy:
|
||||
matrix:
|
||||
shard: [1, 2, 3]
|
||||
steps:
|
||||
- run: vitest run --shard=${{ matrix.shard }}/3 --reporter=blob
|
||||
|
||||
merge:
|
||||
needs: test
|
||||
steps:
|
||||
- run: vitest --merge-reports --reporter=junit
|
||||
```
|
||||
|
||||
### Merge Reports
|
||||
|
||||
```bash
|
||||
# Each shard outputs blob
|
||||
vitest run --shard=1/3 --reporter=blob --coverage
|
||||
vitest run --shard=2/3 --reporter=blob --coverage
|
||||
|
||||
# Merge all blobs
|
||||
vitest --merge-reports --reporter=json --coverage
|
||||
```
|
||||
|
||||
## Test Sequence
|
||||
|
||||
Control test order:
|
||||
|
||||
```ts
|
||||
defineConfig({
|
||||
test: {
|
||||
sequence: {
|
||||
// Run tests in random order
|
||||
shuffle: true,
|
||||
|
||||
// Seed for reproducible shuffle
|
||||
seed: 12345,
|
||||
|
||||
// Hook execution order
|
||||
hooks: 'stack', // 'stack', 'list', 'parallel'
|
||||
|
||||
// All tests concurrent by default
|
||||
concurrent: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Shuffle Tests
|
||||
|
||||
Randomize to catch hidden dependencies:
|
||||
|
||||
```ts
|
||||
// Via CLI
|
||||
vitest --sequence.shuffle
|
||||
|
||||
// Per suite
|
||||
describe.shuffle('random order', () => {
|
||||
test('test 1', () => {})
|
||||
test('test 2', () => {})
|
||||
test('test 3', () => {})
|
||||
})
|
||||
```
|
||||
|
||||
## Pool Options
|
||||
|
||||
### Threads (Default)
|
||||
|
||||
```ts
|
||||
defineConfig({
|
||||
test: {
|
||||
pool: 'threads',
|
||||
poolOptions: {
|
||||
threads: {
|
||||
maxThreads: 8,
|
||||
minThreads: 2,
|
||||
isolate: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Forks
|
||||
|
||||
Better isolation, slower:
|
||||
|
||||
```ts
|
||||
defineConfig({
|
||||
test: {
|
||||
pool: 'forks',
|
||||
poolOptions: {
|
||||
forks: {
|
||||
maxForks: 4,
|
||||
isolate: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### VM Threads
|
||||
|
||||
Full VM isolation per file:
|
||||
|
||||
```ts
|
||||
defineConfig({
|
||||
test: {
|
||||
pool: 'vmThreads',
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Bail on Failure
|
||||
|
||||
Stop after first failure:
|
||||
|
||||
```bash
|
||||
vitest --bail 1 # Stop after 1 failure
|
||||
vitest --bail # Stop on first failure (same as --bail 1)
|
||||
```
|
||||
|
||||
## Key Points
|
||||
|
||||
- Files run in parallel by default
|
||||
- Use `.concurrent` for parallel tests within file
|
||||
- Always use context's `expect` in concurrent tests
|
||||
- Sharding splits tests across CI machines
|
||||
- Use `--merge-reports` to combine sharded results
|
||||
- Shuffle tests to find hidden dependencies
|
||||
|
||||
<!--
|
||||
Source references:
|
||||
- https://vitest.dev/guide/features.html#running-tests-concurrently
|
||||
- https://vitest.dev/guide/improving-performance.html
|
||||
-->
|
||||
238
.agents/skills/vitest/references/features-context.md
Normal file
238
.agents/skills/vitest/references/features-context.md
Normal file
@@ -0,0 +1,238 @@
|
||||
---
|
||||
name: test-context-fixtures
|
||||
description: Test context, custom fixtures with test.extend
|
||||
---
|
||||
|
||||
# Test Context & Fixtures
|
||||
|
||||
## Built-in Context
|
||||
|
||||
Every test receives context as first argument:
|
||||
|
||||
```ts
|
||||
test('context', ({ task, expect, skip }) => {
|
||||
console.log(task.name) // Test name
|
||||
expect(1).toBe(1) // Context-bound expect
|
||||
skip() // Skip test dynamically
|
||||
})
|
||||
```
|
||||
|
||||
### Context Properties
|
||||
|
||||
- `task` - Test metadata (name, file, etc.)
|
||||
- `expect` - Expect bound to this test (important for concurrent tests)
|
||||
- `skip(condition?, message?)` - Skip the test
|
||||
- `onTestFinished(fn)` - Cleanup after test
|
||||
- `onTestFailed(fn)` - Run on failure only
|
||||
|
||||
## Custom Fixtures with test.extend
|
||||
|
||||
Create reusable test utilities:
|
||||
|
||||
```ts
|
||||
import { test as base } from 'vitest'
|
||||
|
||||
// Define fixture types
|
||||
interface Fixtures {
|
||||
db: Database
|
||||
user: User
|
||||
}
|
||||
|
||||
// Create extended test
|
||||
export const test = base.extend<Fixtures>({
|
||||
// Fixture with setup/teardown
|
||||
db: async ({}, use) => {
|
||||
const db = await createDatabase()
|
||||
await use(db) // Provide to test
|
||||
await db.close() // Cleanup
|
||||
},
|
||||
|
||||
// Fixture depending on another fixture
|
||||
user: async ({ db }, use) => {
|
||||
const user = await db.createUser({ name: 'Test' })
|
||||
await use(user)
|
||||
await db.deleteUser(user.id)
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Using fixtures:
|
||||
|
||||
```ts
|
||||
test('query user', async ({ db, user }) => {
|
||||
const found = await db.findUser(user.id)
|
||||
expect(found).toEqual(user)
|
||||
})
|
||||
```
|
||||
|
||||
## Fixture Initialization
|
||||
|
||||
Fixtures only initialize when accessed:
|
||||
|
||||
```ts
|
||||
const test = base.extend({
|
||||
expensive: async ({}, use) => {
|
||||
console.log('initializing') // Only runs if test uses it
|
||||
await use('value')
|
||||
},
|
||||
})
|
||||
|
||||
test('no fixture', () => {}) // expensive not called
|
||||
test('uses fixture', ({ expensive }) => {}) // expensive called
|
||||
```
|
||||
|
||||
## Auto Fixtures
|
||||
|
||||
Run fixture for every test:
|
||||
|
||||
```ts
|
||||
const test = base.extend({
|
||||
setup: [
|
||||
async ({}, use) => {
|
||||
await globalSetup()
|
||||
await use()
|
||||
await globalTeardown()
|
||||
},
|
||||
{ auto: true } // Always run
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
## Scoped Fixtures
|
||||
|
||||
### File Scope
|
||||
|
||||
Initialize once per file:
|
||||
|
||||
```ts
|
||||
const test = base.extend({
|
||||
connection: [
|
||||
async ({}, use) => {
|
||||
const conn = await connect()
|
||||
await use(conn)
|
||||
await conn.close()
|
||||
},
|
||||
{ scope: 'file' }
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
### Worker Scope
|
||||
|
||||
Initialize once per worker:
|
||||
|
||||
```ts
|
||||
const test = base.extend({
|
||||
sharedResource: [
|
||||
async ({}, use) => {
|
||||
await use(globalResource)
|
||||
},
|
||||
{ scope: 'worker' }
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
## Injected Fixtures (from Config)
|
||||
|
||||
Override fixtures per project:
|
||||
|
||||
```ts
|
||||
// test file
|
||||
const test = base.extend({
|
||||
apiUrl: ['/default', { injected: true }],
|
||||
})
|
||||
|
||||
// vitest.config.ts
|
||||
defineConfig({
|
||||
test: {
|
||||
projects: [
|
||||
{
|
||||
test: {
|
||||
name: 'prod',
|
||||
provide: { apiUrl: 'https://api.prod.com' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Scoped Values per Suite
|
||||
|
||||
Override fixture for specific suite:
|
||||
|
||||
```ts
|
||||
const test = base.extend({
|
||||
environment: 'development',
|
||||
})
|
||||
|
||||
describe('production tests', () => {
|
||||
test.scoped({ environment: 'production' })
|
||||
|
||||
test('uses production', ({ environment }) => {
|
||||
expect(environment).toBe('production')
|
||||
})
|
||||
})
|
||||
|
||||
test('uses default', ({ environment }) => {
|
||||
expect(environment).toBe('development')
|
||||
})
|
||||
```
|
||||
|
||||
## Extended Test Hooks
|
||||
|
||||
Type-aware hooks with fixtures:
|
||||
|
||||
```ts
|
||||
const test = base.extend<{ db: Database }>({
|
||||
db: async ({}, use) => {
|
||||
const db = await createDb()
|
||||
await use(db)
|
||||
await db.close()
|
||||
},
|
||||
})
|
||||
|
||||
// Hooks know about fixtures
|
||||
test.beforeEach(({ db }) => {
|
||||
db.seed()
|
||||
})
|
||||
|
||||
test.afterEach(({ db }) => {
|
||||
db.clear()
|
||||
})
|
||||
```
|
||||
|
||||
## Composing Fixtures
|
||||
|
||||
Extend from another extended test:
|
||||
|
||||
```ts
|
||||
// base-test.ts
|
||||
export const test = base.extend<{ db: Database }>({
|
||||
db: async ({}, use) => { /* ... */ },
|
||||
})
|
||||
|
||||
// admin-test.ts
|
||||
import { test as dbTest } from './base-test'
|
||||
|
||||
export const test = dbTest.extend<{ admin: User }>({
|
||||
admin: async ({ db }, use) => {
|
||||
const admin = await db.createAdmin()
|
||||
await use(admin)
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Key Points
|
||||
|
||||
- Use `{ }` destructuring to access fixtures
|
||||
- Fixtures are lazy - only initialize when accessed
|
||||
- Return cleanup function from fixtures
|
||||
- Use `{ auto: true }` for setup fixtures
|
||||
- Use `{ scope: 'file' }` for expensive shared resources
|
||||
- Fixtures compose - extend from extended tests
|
||||
|
||||
<!--
|
||||
Source references:
|
||||
- https://vitest.dev/guide/test-context.html
|
||||
-->
|
||||
207
.agents/skills/vitest/references/features-coverage.md
Normal file
207
.agents/skills/vitest/references/features-coverage.md
Normal file
@@ -0,0 +1,207 @@
|
||||
---
|
||||
name: code-coverage
|
||||
description: Code coverage with V8 or Istanbul providers
|
||||
---
|
||||
|
||||
# Code Coverage
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
# Run tests with coverage
|
||||
vitest run --coverage
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
```ts
|
||||
// vitest.config.ts
|
||||
defineConfig({
|
||||
test: {
|
||||
coverage: {
|
||||
// Provider: 'v8' (default, faster) or 'istanbul' (more compatible)
|
||||
provider: 'v8',
|
||||
|
||||
// Enable coverage
|
||||
enabled: true,
|
||||
|
||||
// Reporters
|
||||
reporter: ['text', 'json', 'html'],
|
||||
|
||||
// Files to include
|
||||
include: ['src/**/*.{ts,tsx}'],
|
||||
|
||||
// Files to exclude
|
||||
exclude: [
|
||||
'node_modules/',
|
||||
'tests/',
|
||||
'**/*.d.ts',
|
||||
'**/*.test.ts',
|
||||
],
|
||||
|
||||
// Report uncovered files
|
||||
all: true,
|
||||
|
||||
// Thresholds
|
||||
thresholds: {
|
||||
lines: 80,
|
||||
functions: 80,
|
||||
branches: 80,
|
||||
statements: 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Providers
|
||||
|
||||
### V8 (Default)
|
||||
|
||||
```bash
|
||||
npm i -D @vitest/coverage-v8
|
||||
```
|
||||
|
||||
- Faster, no pre-instrumentation
|
||||
- Uses V8's native coverage
|
||||
- Recommended for most projects
|
||||
|
||||
### Istanbul
|
||||
|
||||
```bash
|
||||
npm i -D @vitest/coverage-istanbul
|
||||
```
|
||||
|
||||
- Pre-instruments code
|
||||
- Works in any JS runtime
|
||||
- More overhead but widely compatible
|
||||
|
||||
## Reporters
|
||||
|
||||
```ts
|
||||
coverage: {
|
||||
reporter: [
|
||||
'text', // Terminal output
|
||||
'text-summary', // Summary only
|
||||
'json', // JSON file
|
||||
'html', // HTML report
|
||||
'lcov', // For CI tools
|
||||
'cobertura', // XML format
|
||||
],
|
||||
reportsDirectory: './coverage',
|
||||
}
|
||||
```
|
||||
|
||||
## Thresholds
|
||||
|
||||
Fail tests if coverage is below threshold:
|
||||
|
||||
```ts
|
||||
coverage: {
|
||||
thresholds: {
|
||||
// Global thresholds
|
||||
lines: 80,
|
||||
functions: 75,
|
||||
branches: 70,
|
||||
statements: 80,
|
||||
|
||||
// Per-file thresholds
|
||||
perFile: true,
|
||||
|
||||
// Auto-update thresholds (for gradual improvement)
|
||||
autoUpdate: true,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Ignoring Code
|
||||
|
||||
### V8
|
||||
|
||||
```ts
|
||||
/* v8 ignore next -- @preserve */
|
||||
function ignored() {
|
||||
return 'not covered'
|
||||
}
|
||||
|
||||
/* v8 ignore start -- @preserve */
|
||||
// All code here ignored
|
||||
/* v8 ignore stop -- @preserve */
|
||||
```
|
||||
|
||||
### Istanbul
|
||||
|
||||
```ts
|
||||
/* istanbul ignore next -- @preserve */
|
||||
function ignored() {}
|
||||
|
||||
/* istanbul ignore if -- @preserve */
|
||||
if (condition) {
|
||||
// ignored
|
||||
}
|
||||
```
|
||||
|
||||
Note: `@preserve` keeps comments through esbuild.
|
||||
|
||||
## Package.json Scripts
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"test": "vitest",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"test:coverage:watch": "vitest --coverage"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Vitest UI Coverage
|
||||
|
||||
Enable HTML coverage in Vitest UI:
|
||||
|
||||
```ts
|
||||
coverage: {
|
||||
enabled: true,
|
||||
reporter: ['text', 'html'],
|
||||
}
|
||||
```
|
||||
|
||||
Run with `vitest --ui` to view coverage visually.
|
||||
|
||||
## CI Integration
|
||||
|
||||
```yaml
|
||||
# GitHub Actions
|
||||
- name: Run tests with coverage
|
||||
run: npm run test:coverage
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
files: ./coverage/lcov.info
|
||||
```
|
||||
|
||||
## Coverage with Sharding
|
||||
|
||||
Merge coverage from sharded runs:
|
||||
|
||||
```bash
|
||||
vitest run --shard=1/3 --coverage --reporter=blob
|
||||
vitest run --shard=2/3 --coverage --reporter=blob
|
||||
vitest run --shard=3/3 --coverage --reporter=blob
|
||||
|
||||
vitest --merge-reports --coverage --reporter=json
|
||||
```
|
||||
|
||||
## Key Points
|
||||
|
||||
- V8 is faster, Istanbul is more compatible
|
||||
- Use `--coverage` flag or `coverage.enabled: true`
|
||||
- Include `all: true` to see uncovered files
|
||||
- Set thresholds to enforce minimum coverage
|
||||
- Use `@preserve` comment to keep ignore hints
|
||||
|
||||
<!--
|
||||
Source references:
|
||||
- https://vitest.dev/guide/coverage.html
|
||||
-->
|
||||
211
.agents/skills/vitest/references/features-filtering.md
Normal file
211
.agents/skills/vitest/references/features-filtering.md
Normal file
@@ -0,0 +1,211 @@
|
||||
---
|
||||
name: test-filtering
|
||||
description: Filter tests by name, file patterns, and tags
|
||||
---
|
||||
|
||||
# Test Filtering
|
||||
|
||||
## CLI Filtering
|
||||
|
||||
### By File Path
|
||||
|
||||
```bash
|
||||
# Run files containing "user"
|
||||
vitest user
|
||||
|
||||
# Multiple patterns
|
||||
vitest user auth
|
||||
|
||||
# Specific file
|
||||
vitest src/user.test.ts
|
||||
|
||||
# By line number
|
||||
vitest src/user.test.ts:25
|
||||
```
|
||||
|
||||
### By Test Name
|
||||
|
||||
```bash
|
||||
# Tests matching pattern
|
||||
vitest -t "login"
|
||||
vitest --testNamePattern "should.*work"
|
||||
|
||||
# Regex patterns
|
||||
vitest -t "/user|auth/"
|
||||
```
|
||||
|
||||
## Changed Files
|
||||
|
||||
```bash
|
||||
# Uncommitted changes
|
||||
vitest --changed
|
||||
|
||||
# Since specific commit
|
||||
vitest --changed HEAD~1
|
||||
vitest --changed abc123
|
||||
|
||||
# Since branch
|
||||
vitest --changed origin/main
|
||||
```
|
||||
|
||||
## Related Files
|
||||
|
||||
Run tests that import specific files:
|
||||
|
||||
```bash
|
||||
vitest related src/utils.ts src/api.ts --run
|
||||
```
|
||||
|
||||
Useful with lint-staged:
|
||||
|
||||
```js
|
||||
// .lintstagedrc.js
|
||||
export default {
|
||||
'*.{ts,tsx}': 'vitest related --run',
|
||||
}
|
||||
```
|
||||
|
||||
## Focus Tests (.only)
|
||||
|
||||
```ts
|
||||
test.only('only this runs', () => {})
|
||||
|
||||
describe.only('only this suite', () => {
|
||||
test('runs', () => {})
|
||||
})
|
||||
```
|
||||
|
||||
In CI, `.only` throws error unless configured:
|
||||
|
||||
```ts
|
||||
defineConfig({
|
||||
test: {
|
||||
allowOnly: true, // Allow .only in CI
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Skip Tests
|
||||
|
||||
```ts
|
||||
test.skip('skipped', () => {})
|
||||
|
||||
// Conditional
|
||||
test.skipIf(process.env.CI)('not in CI', () => {})
|
||||
test.runIf(!process.env.CI)('local only', () => {})
|
||||
|
||||
// Dynamic skip
|
||||
test('dynamic', ({ skip }) => {
|
||||
skip(someCondition, 'reason')
|
||||
})
|
||||
```
|
||||
|
||||
## Tags
|
||||
|
||||
Filter by custom tags:
|
||||
|
||||
```ts
|
||||
test('database test', { tags: ['db'] }, () => {})
|
||||
test('slow test', { tags: ['slow', 'integration'] }, () => {})
|
||||
```
|
||||
|
||||
Run tagged tests:
|
||||
|
||||
```bash
|
||||
vitest --tags db
|
||||
vitest --tags "db,slow" # OR
|
||||
vitest --tags db --tags slow # OR
|
||||
```
|
||||
|
||||
Configure allowed tags:
|
||||
|
||||
```ts
|
||||
defineConfig({
|
||||
test: {
|
||||
tags: ['db', 'slow', 'integration'],
|
||||
strictTags: true, // Fail on unknown tags
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Include/Exclude Patterns
|
||||
|
||||
```ts
|
||||
defineConfig({
|
||||
test: {
|
||||
// Test file patterns
|
||||
include: ['**/*.{test,spec}.{ts,tsx}'],
|
||||
|
||||
// Exclude patterns
|
||||
exclude: [
|
||||
'**/node_modules/**',
|
||||
'**/e2e/**',
|
||||
'**/*.skip.test.ts',
|
||||
],
|
||||
|
||||
// Include source for in-source testing
|
||||
includeSource: ['src/**/*.ts'],
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Watch Mode Filtering
|
||||
|
||||
In watch mode, press:
|
||||
- `p` - Filter by filename pattern
|
||||
- `t` - Filter by test name pattern
|
||||
- `a` - Run all tests
|
||||
- `f` - Run only failed tests
|
||||
|
||||
## Projects Filtering
|
||||
|
||||
Run specific project:
|
||||
|
||||
```bash
|
||||
vitest --project unit
|
||||
vitest --project integration --project e2e
|
||||
```
|
||||
|
||||
## Environment-based Filtering
|
||||
|
||||
```ts
|
||||
const isDev = process.env.NODE_ENV === 'development'
|
||||
const isCI = process.env.CI
|
||||
|
||||
describe.skipIf(isCI)('local only tests', () => {})
|
||||
describe.runIf(isDev)('dev tests', () => {})
|
||||
```
|
||||
|
||||
## Combining Filters
|
||||
|
||||
```bash
|
||||
# File pattern + test name + changed
|
||||
vitest user -t "login" --changed
|
||||
|
||||
# Related files + run mode
|
||||
vitest related src/auth.ts --run
|
||||
```
|
||||
|
||||
## List Tests Without Running
|
||||
|
||||
```bash
|
||||
vitest list # Show all test names
|
||||
vitest list -t "user" # Filter by name
|
||||
vitest list --filesOnly # Show only file paths
|
||||
vitest list --json # JSON output
|
||||
```
|
||||
|
||||
## Key Points
|
||||
|
||||
- Use `-t` for test name pattern filtering
|
||||
- `--changed` runs only tests affected by changes
|
||||
- `--related` runs tests importing specific files
|
||||
- Tags provide semantic test grouping
|
||||
- Use `.only` for debugging, but configure CI to reject it
|
||||
- Watch mode has interactive filtering
|
||||
|
||||
<!--
|
||||
Source references:
|
||||
- https://vitest.dev/guide/filtering.html
|
||||
- https://vitest.dev/guide/cli.html
|
||||
-->
|
||||
265
.agents/skills/vitest/references/features-mocking.md
Normal file
265
.agents/skills/vitest/references/features-mocking.md
Normal file
@@ -0,0 +1,265 @@
|
||||
---
|
||||
name: mocking
|
||||
description: Mock functions, modules, timers, and dates with vi utilities
|
||||
---
|
||||
|
||||
# Mocking
|
||||
|
||||
## Mock Functions
|
||||
|
||||
```ts
|
||||
import { expect, vi } from 'vitest'
|
||||
|
||||
// Create mock function
|
||||
const fn = vi.fn()
|
||||
fn('hello')
|
||||
|
||||
expect(fn).toHaveBeenCalled()
|
||||
expect(fn).toHaveBeenCalledWith('hello')
|
||||
|
||||
// With implementation
|
||||
const add = vi.fn((a, b) => a + b)
|
||||
expect(add(1, 2)).toBe(3)
|
||||
|
||||
// Mock return values
|
||||
fn.mockReturnValue(42)
|
||||
fn.mockReturnValueOnce(1).mockReturnValueOnce(2)
|
||||
fn.mockResolvedValue({ data: true })
|
||||
fn.mockRejectedValue(new Error('fail'))
|
||||
|
||||
// Mock implementation
|
||||
fn.mockImplementation((x) => x * 2)
|
||||
fn.mockImplementationOnce(() => 'first call')
|
||||
```
|
||||
|
||||
## Spying on Objects
|
||||
|
||||
```ts
|
||||
const cart = {
|
||||
getTotal: () => 100,
|
||||
}
|
||||
|
||||
const spy = vi.spyOn(cart, 'getTotal')
|
||||
cart.getTotal()
|
||||
|
||||
expect(spy).toHaveBeenCalled()
|
||||
|
||||
// Mock implementation
|
||||
spy.mockReturnValue(200)
|
||||
expect(cart.getTotal()).toBe(200)
|
||||
|
||||
// Restore original
|
||||
spy.mockRestore()
|
||||
```
|
||||
|
||||
## Module Mocking
|
||||
|
||||
```ts
|
||||
// vi.mock is hoisted to top of file
|
||||
vi.mock('./api', () => ({
|
||||
fetchUser: vi.fn(() => ({ id: 1, name: 'Mock' })),
|
||||
}))
|
||||
|
||||
import { fetchUser } from './api'
|
||||
|
||||
test('mocked module', () => {
|
||||
expect(fetchUser()).toEqual({ id: 1, name: 'Mock' })
|
||||
})
|
||||
```
|
||||
|
||||
### Partial Mock
|
||||
|
||||
```ts
|
||||
vi.mock('./utils', async (importOriginal) => {
|
||||
const actual = await importOriginal()
|
||||
return {
|
||||
...actual,
|
||||
specificFunction: vi.fn(),
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Auto-mock with Spy
|
||||
|
||||
```ts
|
||||
// Keep implementation but spy on calls
|
||||
vi.mock('./calculator', { spy: true })
|
||||
|
||||
import { add } from './calculator'
|
||||
|
||||
test('spy on module', () => {
|
||||
const result = add(1, 2) // Real implementation
|
||||
expect(result).toBe(3)
|
||||
expect(add).toHaveBeenCalledWith(1, 2)
|
||||
})
|
||||
```
|
||||
|
||||
### Manual Mocks (__mocks__)
|
||||
|
||||
```
|
||||
src/
|
||||
__mocks__/
|
||||
axios.ts # Mocks 'axios'
|
||||
api/
|
||||
__mocks__/
|
||||
client.ts # Mocks './client'
|
||||
client.ts
|
||||
```
|
||||
|
||||
```ts
|
||||
// Just call vi.mock with no factory
|
||||
vi.mock('axios')
|
||||
vi.mock('./api/client')
|
||||
```
|
||||
|
||||
## Dynamic Mocking (vi.doMock)
|
||||
|
||||
Not hoisted - use for dynamic imports:
|
||||
|
||||
```ts
|
||||
test('dynamic mock', async () => {
|
||||
vi.doMock('./config', () => ({
|
||||
apiUrl: 'http://test.local',
|
||||
}))
|
||||
|
||||
const { apiUrl } = await import('./config')
|
||||
expect(apiUrl).toBe('http://test.local')
|
||||
|
||||
vi.doUnmock('./config')
|
||||
})
|
||||
```
|
||||
|
||||
## Mock Timers
|
||||
|
||||
```ts
|
||||
import { afterEach, beforeEach, vi } from 'vitest'
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
test('timers', () => {
|
||||
const fn = vi.fn()
|
||||
setTimeout(fn, 1000)
|
||||
|
||||
expect(fn).not.toHaveBeenCalled()
|
||||
|
||||
vi.advanceTimersByTime(1000)
|
||||
expect(fn).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// Other timer methods
|
||||
vi.runAllTimers() // Run all pending timers
|
||||
vi.runOnlyPendingTimers() // Run only currently pending
|
||||
vi.advanceTimersToNextTimer() // Advance to next timer
|
||||
```
|
||||
|
||||
### Async Timer Methods
|
||||
|
||||
```ts
|
||||
test('async timers', async () => {
|
||||
vi.useFakeTimers()
|
||||
|
||||
let resolved = false
|
||||
setTimeout(() => Promise.resolve().then(() => { resolved = true }), 100)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(100)
|
||||
expect(resolved).toBe(true)
|
||||
})
|
||||
```
|
||||
|
||||
## Mock Dates
|
||||
|
||||
```ts
|
||||
vi.setSystemTime(new Date('2024-01-01'))
|
||||
expect(new Date().getFullYear()).toBe(2024)
|
||||
|
||||
vi.useRealTimers() // Restore
|
||||
```
|
||||
|
||||
## Mock Globals
|
||||
|
||||
```ts
|
||||
vi.stubGlobal('fetch', vi.fn(() =>
|
||||
Promise.resolve({ json: () => ({ data: 'mock' }) })
|
||||
))
|
||||
|
||||
// Restore
|
||||
vi.unstubAllGlobals()
|
||||
```
|
||||
|
||||
## Mock Environment Variables
|
||||
|
||||
```ts
|
||||
vi.stubEnv('API_KEY', 'test-key')
|
||||
expect(import.meta.env.API_KEY).toBe('test-key')
|
||||
|
||||
// Restore
|
||||
vi.unstubAllEnvs()
|
||||
```
|
||||
|
||||
## Clearing Mocks
|
||||
|
||||
```ts
|
||||
const fn = vi.fn()
|
||||
fn()
|
||||
|
||||
fn.mockClear() // Clear call history
|
||||
fn.mockReset() // Clear history + implementation
|
||||
fn.mockRestore() // Restore original (for spies)
|
||||
|
||||
// Global
|
||||
vi.clearAllMocks()
|
||||
vi.resetAllMocks()
|
||||
vi.restoreAllMocks()
|
||||
```
|
||||
|
||||
## Config Auto-Reset
|
||||
|
||||
```ts
|
||||
// vitest.config.ts
|
||||
defineConfig({
|
||||
test: {
|
||||
clearMocks: true, // Clear before each test
|
||||
mockReset: true, // Reset before each test
|
||||
restoreMocks: true, // Restore after each test
|
||||
unstubEnvs: true, // Restore env vars
|
||||
unstubGlobals: true, // Restore globals
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Hoisted Variables for Mocks
|
||||
|
||||
```ts
|
||||
const mockFn = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('./module', () => ({
|
||||
getData: mockFn,
|
||||
}))
|
||||
|
||||
import { getData } from './module'
|
||||
|
||||
test('hoisted mock', () => {
|
||||
mockFn.mockReturnValue('test')
|
||||
expect(getData()).toBe('test')
|
||||
})
|
||||
```
|
||||
|
||||
## Key Points
|
||||
|
||||
- `vi.mock` is hoisted - called before imports
|
||||
- Use `vi.doMock` for dynamic, non-hoisted mocking
|
||||
- Always restore mocks to avoid test pollution
|
||||
- Use `{ spy: true }` to keep implementation but track calls
|
||||
- `vi.hoisted` lets you reference variables in mock factories
|
||||
|
||||
<!--
|
||||
Source references:
|
||||
- https://vitest.dev/guide/mocking.html
|
||||
- https://vitest.dev/api/vi.html
|
||||
-->
|
||||
207
.agents/skills/vitest/references/features-snapshots.md
Normal file
207
.agents/skills/vitest/references/features-snapshots.md
Normal file
@@ -0,0 +1,207 @@
|
||||
---
|
||||
name: snapshot-testing
|
||||
description: Snapshot testing with file, inline, and file snapshots
|
||||
---
|
||||
|
||||
# Snapshot Testing
|
||||
|
||||
Snapshot tests capture output and compare against stored references.
|
||||
|
||||
## Basic Snapshot
|
||||
|
||||
```ts
|
||||
import { expect, test } from 'vitest'
|
||||
|
||||
test('snapshot', () => {
|
||||
const result = generateOutput()
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
```
|
||||
|
||||
First run creates `.snap` file:
|
||||
|
||||
```js
|
||||
// __snapshots__/test.spec.ts.snap
|
||||
exports['snapshot 1'] = `
|
||||
{
|
||||
"id": 1,
|
||||
"name": "test"
|
||||
}
|
||||
`
|
||||
```
|
||||
|
||||
## Inline Snapshots
|
||||
|
||||
Stored directly in test file:
|
||||
|
||||
```ts
|
||||
test('inline snapshot', () => {
|
||||
const data = { foo: 'bar' }
|
||||
expect(data).toMatchInlineSnapshot()
|
||||
})
|
||||
```
|
||||
|
||||
Vitest updates the test file:
|
||||
|
||||
```ts
|
||||
test('inline snapshot', () => {
|
||||
const data = { foo: 'bar' }
|
||||
expect(data).toMatchInlineSnapshot(`
|
||||
{
|
||||
"foo": "bar",
|
||||
}
|
||||
`)
|
||||
})
|
||||
```
|
||||
|
||||
## File Snapshots
|
||||
|
||||
Compare against explicit file:
|
||||
|
||||
```ts
|
||||
test('render html', async () => {
|
||||
const html = renderComponent()
|
||||
await expect(html).toMatchFileSnapshot('./expected/component.html')
|
||||
})
|
||||
```
|
||||
|
||||
## Snapshot Hints
|
||||
|
||||
Add descriptive hints:
|
||||
|
||||
```ts
|
||||
test('multiple snapshots', () => {
|
||||
expect(header).toMatchSnapshot('header')
|
||||
expect(body).toMatchSnapshot('body content')
|
||||
expect(footer).toMatchSnapshot('footer')
|
||||
})
|
||||
```
|
||||
|
||||
## Object Shape Matching
|
||||
|
||||
Match partial structure:
|
||||
|
||||
```ts
|
||||
test('shape snapshot', () => {
|
||||
const data = {
|
||||
id: Math.random(),
|
||||
created: new Date(),
|
||||
name: 'test'
|
||||
}
|
||||
|
||||
expect(data).toMatchSnapshot({
|
||||
id: expect.any(Number),
|
||||
created: expect.any(Date),
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Error Snapshots
|
||||
|
||||
```ts
|
||||
test('error message', () => {
|
||||
expect(() => {
|
||||
throw new Error('Something went wrong')
|
||||
}).toThrowErrorMatchingSnapshot()
|
||||
})
|
||||
|
||||
test('inline error', () => {
|
||||
expect(() => {
|
||||
throw new Error('Bad input')
|
||||
}).toThrowErrorMatchingInlineSnapshot(`[Error: Bad input]`)
|
||||
})
|
||||
```
|
||||
|
||||
## Updating Snapshots
|
||||
|
||||
```bash
|
||||
# Update all snapshots
|
||||
vitest -u
|
||||
vitest --update
|
||||
|
||||
# In watch mode, press 'u' to update failed snapshots
|
||||
```
|
||||
|
||||
## Custom Serializers
|
||||
|
||||
Add custom snapshot formatting:
|
||||
|
||||
```ts
|
||||
expect.addSnapshotSerializer({
|
||||
test(val) {
|
||||
return val && typeof val.toJSON === 'function'
|
||||
},
|
||||
serialize(val, config, indentation, depth, refs, printer) {
|
||||
return printer(val.toJSON(), config, indentation, depth, refs)
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Or via config:
|
||||
|
||||
```ts
|
||||
// vitest.config.ts
|
||||
defineConfig({
|
||||
test: {
|
||||
snapshotSerializers: ['./my-serializer.ts'],
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Snapshot Format Options
|
||||
|
||||
```ts
|
||||
defineConfig({
|
||||
test: {
|
||||
snapshotFormat: {
|
||||
printBasicPrototype: false, // Don't print Array/Object prototypes
|
||||
escapeString: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Concurrent Test Snapshots
|
||||
|
||||
Use context's expect:
|
||||
|
||||
```ts
|
||||
test.concurrent('concurrent 1', async ({ expect }) => {
|
||||
expect(await getData()).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test.concurrent('concurrent 2', async ({ expect }) => {
|
||||
expect(await getOther()).toMatchSnapshot()
|
||||
})
|
||||
```
|
||||
|
||||
## Snapshot File Location
|
||||
|
||||
Default: `__snapshots__/<test-file>.snap`
|
||||
|
||||
Customize:
|
||||
|
||||
```ts
|
||||
defineConfig({
|
||||
test: {
|
||||
resolveSnapshotPath: (testPath, snapExtension) => {
|
||||
return testPath.replace('__tests__', '__snapshots__') + snapExtension
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Key Points
|
||||
|
||||
- Commit snapshot files to version control
|
||||
- Review snapshot changes in code review
|
||||
- Use hints for multiple snapshots in one test
|
||||
- Use `toMatchFileSnapshot` for large outputs (HTML, JSON)
|
||||
- Inline snapshots auto-update in test file
|
||||
- Use context's `expect` for concurrent tests
|
||||
|
||||
<!--
|
||||
Source references:
|
||||
- https://vitest.dev/guide/snapshot.html
|
||||
- https://vitest.dev/api/expect.html#tomatchsnapshot
|
||||
-->
|
||||
1
.claude/skills/vitest
Symbolic link
1
.claude/skills/vitest
Symbolic link
@@ -0,0 +1 @@
|
||||
../../.agents/skills/vitest
|
||||
1
.cursor/skills/vitest
Symbolic link
1
.cursor/skills/vitest
Symbolic link
@@ -0,0 +1 @@
|
||||
../../.agents/skills/vitest
|
||||
@@ -4,7 +4,7 @@ import dayjs from 'dayjs'
|
||||
// End of third-party imports
|
||||
|
||||
import { API_URL, BASE_PATH } from 'lib/constants'
|
||||
import { HttpResponse, http } from 'msw'
|
||||
import { http, HttpResponse } from 'msw'
|
||||
import { createMockOrganization, createMockProject } from 'tests/helpers'
|
||||
import { customRender } from 'tests/lib/custom-render'
|
||||
import { addAPIMock, mswServer } from 'tests/lib/msw'
|
||||
@@ -785,6 +785,10 @@ describe('SupportFormPage', () => {
|
||||
expect(getSeveritySelector(screen)).toHaveTextContent('High')
|
||||
})
|
||||
|
||||
// Wait for library selector to be available before interacting
|
||||
await waitFor(() => {
|
||||
expect(getLibrarySelector(screen)).toBeInTheDocument()
|
||||
})
|
||||
await selectLibraryOption(screen, 'JavaScript')
|
||||
await waitFor(() => {
|
||||
expect(getLibrarySelector(screen)).toHaveTextContent('JavaScript')
|
||||
@@ -801,9 +805,14 @@ describe('SupportFormPage', () => {
|
||||
const supportAccessToggle = screen.getByRole('switch', {
|
||||
name: /allow support access to your project/i,
|
||||
})
|
||||
expect(supportAccessToggle).toBeChecked()
|
||||
// Wait for toggle to be in expected state before interacting
|
||||
await waitFor(() => {
|
||||
expect(supportAccessToggle).toBeChecked()
|
||||
})
|
||||
await userEvent.click(supportAccessToggle)
|
||||
expect(supportAccessToggle).not.toBeChecked()
|
||||
await waitFor(() => {
|
||||
expect(supportAccessToggle).not.toBeChecked()
|
||||
})
|
||||
|
||||
await userEvent.click(getSubmitButton(screen))
|
||||
|
||||
@@ -833,7 +842,7 @@ describe('SupportFormPage', () => {
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('heading', { name: /support request sent/i })).toBeInTheDocument()
|
||||
})
|
||||
}, 10_000)
|
||||
}, 15_000)
|
||||
|
||||
test('submits urgent login issues ticket for a different organization', async () => {
|
||||
const submitSpy = vi.fn()
|
||||
|
||||
153
apps/studio/lib/ai/message-utils.test.ts
Normal file
153
apps/studio/lib/ai/message-utils.test.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import type { UIMessage } from 'ai'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { prepareMessagesForAPI } from './message-utils'
|
||||
|
||||
describe('prepareMessagesForAPI', () => {
|
||||
it('should limit messages to last 7 entries', () => {
|
||||
const messages: UIMessage[] = Array.from({ length: 10 }, (_, i) => ({
|
||||
id: `msg-${i}`,
|
||||
role: 'user',
|
||||
parts: [{ type: 'text', text: `Message ${i}` }],
|
||||
}))
|
||||
|
||||
const result = prepareMessagesForAPI(messages)
|
||||
|
||||
expect(result).toHaveLength(7)
|
||||
expect(result[0].parts[0]).toEqual({ type: 'text', text: 'Message 3' })
|
||||
expect(result[6].parts[0]).toEqual({ type: 'text', text: 'Message 9' })
|
||||
})
|
||||
|
||||
it('should remove results property from assistant messages', () => {
|
||||
const messages = [
|
||||
{
|
||||
id: 'msg-1',
|
||||
role: 'assistant',
|
||||
parts: [{ type: 'text', text: 'Response' }],
|
||||
results: { data: 'some data' },
|
||||
},
|
||||
] as Array<UIMessage & { results?: unknown }>
|
||||
|
||||
const result = prepareMessagesForAPI(messages)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0]).not.toHaveProperty('results')
|
||||
expect(result[0].parts[0]).toEqual({ type: 'text', text: 'Response' })
|
||||
})
|
||||
|
||||
it('should preserve messages without results', () => {
|
||||
const messages: UIMessage[] = [
|
||||
{
|
||||
id: 'msg-1',
|
||||
role: 'user',
|
||||
parts: [{ type: 'text', text: 'Question' }],
|
||||
},
|
||||
{
|
||||
id: 'msg-2',
|
||||
role: 'assistant',
|
||||
parts: [{ type: 'text', text: 'Answer' }],
|
||||
},
|
||||
]
|
||||
|
||||
const result = prepareMessagesForAPI(messages)
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0]).toEqual(messages[0])
|
||||
expect(result[1]).toEqual(messages[1])
|
||||
})
|
||||
|
||||
it('should handle empty array', () => {
|
||||
const messages: UIMessage[] = []
|
||||
|
||||
const result = prepareMessagesForAPI(messages)
|
||||
|
||||
expect(result).toHaveLength(0)
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
it('should handle array with fewer than 7 messages', () => {
|
||||
const messages: UIMessage[] = [
|
||||
{ id: 'msg-1', role: 'user', parts: [{ type: 'text', text: 'Message 1' }] },
|
||||
{ id: 'msg-2', role: 'assistant', parts: [{ type: 'text', text: 'Message 2' }] },
|
||||
{ id: 'msg-3', role: 'user', parts: [{ type: 'text', text: 'Message 3' }] },
|
||||
]
|
||||
|
||||
const result = prepareMessagesForAPI(messages)
|
||||
|
||||
expect(result).toHaveLength(3)
|
||||
expect(result).toEqual(messages)
|
||||
})
|
||||
|
||||
it('should handle array with exactly 7 messages', () => {
|
||||
const messages: UIMessage[] = Array.from({ length: 7 }, (_, i) => ({
|
||||
id: `msg-${i}`,
|
||||
role: i % 2 === 0 ? 'user' : 'assistant',
|
||||
parts: [{ type: 'text', text: `Message ${i}` }],
|
||||
}))
|
||||
|
||||
const result = prepareMessagesForAPI(messages)
|
||||
|
||||
expect(result).toHaveLength(7)
|
||||
expect(result).toEqual(messages)
|
||||
})
|
||||
|
||||
it('should only remove results from assistant messages, not user messages', () => {
|
||||
const messages = [
|
||||
{
|
||||
id: 'msg-1',
|
||||
role: 'user',
|
||||
parts: [{ type: 'text', text: 'Question' }],
|
||||
results: { data: 'user data' },
|
||||
},
|
||||
{
|
||||
id: 'msg-2',
|
||||
role: 'assistant',
|
||||
parts: [{ type: 'text', text: 'Answer' }],
|
||||
results: { data: 'assistant data' },
|
||||
},
|
||||
] as Array<UIMessage & { results?: unknown }>
|
||||
|
||||
const result = prepareMessagesForAPI(messages)
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
// User message keeps results (not removed by the function)
|
||||
expect((result[0] as any).results).toEqual({ data: 'user data' })
|
||||
// Assistant message has results removed
|
||||
expect(result[1]).not.toHaveProperty('results')
|
||||
})
|
||||
|
||||
it('should handle mixed messages with and without results', () => {
|
||||
const messages = [
|
||||
{
|
||||
id: 'msg-1',
|
||||
role: 'assistant',
|
||||
parts: [{ type: 'text', text: 'First' }],
|
||||
results: { data: 'data1' },
|
||||
},
|
||||
{
|
||||
id: 'msg-2',
|
||||
role: 'user',
|
||||
parts: [{ type: 'text', text: 'Second' }],
|
||||
},
|
||||
{
|
||||
id: 'msg-3',
|
||||
role: 'assistant',
|
||||
parts: [{ type: 'text', text: 'Third' }],
|
||||
},
|
||||
{
|
||||
id: 'msg-4',
|
||||
role: 'assistant',
|
||||
parts: [{ type: 'text', text: 'Fourth' }],
|
||||
results: { data: 'data2' },
|
||||
},
|
||||
] as Array<UIMessage & { results?: unknown }>
|
||||
|
||||
const result = prepareMessagesForAPI(messages)
|
||||
|
||||
expect(result).toHaveLength(4)
|
||||
expect(result[0]).not.toHaveProperty('results')
|
||||
expect(result[1]).toEqual(messages[1])
|
||||
expect(result[2]).toEqual(messages[2])
|
||||
expect(result[3]).not.toHaveProperty('results')
|
||||
})
|
||||
})
|
||||
88
apps/studio/lib/ai/model.utils.test.ts
Normal file
88
apps/studio/lib/ai/model.utils.test.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { getDefaultModelForProvider, PROVIDERS } from './model.utils'
|
||||
import type { ProviderName } from './model.utils'
|
||||
|
||||
describe('model.utils', () => {
|
||||
describe('getDefaultModelForProvider', () => {
|
||||
it('should return correct default for bedrock provider', () => {
|
||||
const result = getDefaultModelForProvider('bedrock')
|
||||
expect(result).toBe('openai.gpt-oss-120b-1:0')
|
||||
})
|
||||
|
||||
it('should return correct default for openai provider', () => {
|
||||
const result = getDefaultModelForProvider('openai')
|
||||
expect(result).toBe('gpt-5-mini')
|
||||
})
|
||||
|
||||
it('should return correct default for anthropic provider', () => {
|
||||
const result = getDefaultModelForProvider('anthropic')
|
||||
expect(result).toBe('claude-3-5-haiku-20241022')
|
||||
})
|
||||
|
||||
it('should return undefined for unknown provider', () => {
|
||||
const result = getDefaultModelForProvider('unknown' as ProviderName)
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('PROVIDERS registry', () => {
|
||||
it('should have bedrock provider with models', () => {
|
||||
expect(PROVIDERS.bedrock).toBeDefined()
|
||||
expect(PROVIDERS.bedrock.models).toBeDefined()
|
||||
expect(Object.keys(PROVIDERS.bedrock.models)).toContain(
|
||||
'anthropic.claude-3-7-sonnet-20250219-v1:0'
|
||||
)
|
||||
expect(Object.keys(PROVIDERS.bedrock.models)).toContain('openai.gpt-oss-120b-1:0')
|
||||
})
|
||||
|
||||
it('should have openai provider with models', () => {
|
||||
expect(PROVIDERS.openai).toBeDefined()
|
||||
expect(PROVIDERS.openai.models).toBeDefined()
|
||||
expect(Object.keys(PROVIDERS.openai.models)).toContain('gpt-5')
|
||||
expect(Object.keys(PROVIDERS.openai.models)).toContain('gpt-5-mini')
|
||||
})
|
||||
|
||||
it('should have anthropic provider with models', () => {
|
||||
expect(PROVIDERS.anthropic).toBeDefined()
|
||||
expect(PROVIDERS.anthropic.models).toBeDefined()
|
||||
expect(Object.keys(PROVIDERS.anthropic.models)).toContain('claude-sonnet-4-20250514')
|
||||
expect(Object.keys(PROVIDERS.anthropic.models)).toContain('claude-3-5-haiku-20241022')
|
||||
})
|
||||
|
||||
it('should have exactly one default model per provider', () => {
|
||||
const providers: ProviderName[] = ['bedrock', 'openai', 'anthropic']
|
||||
|
||||
providers.forEach((provider) => {
|
||||
const models = PROVIDERS[provider].models
|
||||
const defaultModels = Object.entries(models).filter(([_, config]) => config.default)
|
||||
expect(defaultModels.length).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
it('should have valid model configurations', () => {
|
||||
const providers: ProviderName[] = ['bedrock', 'openai', 'anthropic']
|
||||
|
||||
providers.forEach((provider) => {
|
||||
const models = PROVIDERS[provider].models
|
||||
Object.entries(models).forEach(([modelId, config]) => {
|
||||
expect(config).toHaveProperty('default')
|
||||
expect(typeof config.default).toBe('boolean')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should have bedrock model with promptProviderOptions', () => {
|
||||
const sonnetModel = PROVIDERS.bedrock.models['anthropic.claude-3-7-sonnet-20250219-v1:0']
|
||||
expect(sonnetModel.promptProviderOptions).toBeDefined()
|
||||
expect(sonnetModel.promptProviderOptions?.bedrock).toBeDefined()
|
||||
expect(sonnetModel.promptProviderOptions?.bedrock?.cachePoint).toEqual({ type: 'default' })
|
||||
})
|
||||
|
||||
it('should have openai provider with providerOptions', () => {
|
||||
expect(PROVIDERS.openai.providerOptions).toBeDefined()
|
||||
expect(PROVIDERS.openai.providerOptions?.openai).toBeDefined()
|
||||
expect(PROVIDERS.openai.providerOptions?.openai?.reasoningEffort).toBe('minimal')
|
||||
})
|
||||
})
|
||||
})
|
||||
243
apps/studio/lib/ai/org-ai-details.test.ts
Normal file
243
apps/studio/lib/ai/org-ai-details.test.ts
Normal file
@@ -0,0 +1,243 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { getOrgAIDetails } from './org-ai-details'
|
||||
|
||||
vi.mock('data/organizations/organizations-query', () => ({
|
||||
getOrganizations: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('data/projects/project-detail-query', () => ({
|
||||
getProjectDetail: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('hooks/misc/useOrgOptedIntoAi', () => ({
|
||||
getAiOptInLevel: vi.fn(),
|
||||
}))
|
||||
|
||||
describe('ai/org-ai-details', () => {
|
||||
let mockGetOrganizations: ReturnType<typeof vi.fn>
|
||||
let mockGetProjectDetail: ReturnType<typeof vi.fn>
|
||||
let mockGetAiOptInLevel: ReturnType<typeof vi.fn>
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
const orgsQuery = await import('data/organizations/organizations-query')
|
||||
const projectQuery = await import('data/projects/project-detail-query')
|
||||
const aiHook = await import('hooks/misc/useOrgOptedIntoAi')
|
||||
|
||||
mockGetOrganizations = vi.mocked(orgsQuery.getOrganizations)
|
||||
mockGetProjectDetail = vi.mocked(projectQuery.getProjectDetail)
|
||||
mockGetAiOptInLevel = vi.mocked(aiHook.getAiOptInLevel)
|
||||
})
|
||||
|
||||
describe('getOrgAIDetails', () => {
|
||||
it('should fetch organizations and project details', async () => {
|
||||
const mockOrg = {
|
||||
id: 1,
|
||||
slug: 'test-org',
|
||||
plan: { id: 'pro' },
|
||||
opt_in_tags: [],
|
||||
}
|
||||
const mockProject = {
|
||||
id: 1,
|
||||
organization_id: 1,
|
||||
ref: 'test-project',
|
||||
}
|
||||
|
||||
mockGetOrganizations.mockResolvedValue([mockOrg])
|
||||
mockGetProjectDetail.mockResolvedValue(mockProject)
|
||||
mockGetAiOptInLevel.mockReturnValue('full')
|
||||
|
||||
await getOrgAIDetails({
|
||||
orgSlug: 'test-org',
|
||||
authorization: 'Bearer token',
|
||||
projectRef: 'test-project',
|
||||
})
|
||||
|
||||
expect(mockGetOrganizations).toHaveBeenCalledWith({
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: 'Bearer token',
|
||||
},
|
||||
})
|
||||
expect(mockGetProjectDetail).toHaveBeenCalledWith({ ref: 'test-project' }, undefined, {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: 'Bearer token',
|
||||
})
|
||||
})
|
||||
|
||||
it('should return AI opt-in level and limited status', async () => {
|
||||
const mockOrg = {
|
||||
id: 1,
|
||||
slug: 'test-org',
|
||||
plan: { id: 'free' },
|
||||
opt_in_tags: ['AI_SQL_GENERATOR_OPT_IN'],
|
||||
}
|
||||
const mockProject = {
|
||||
organization_id: 1,
|
||||
ref: 'test-project',
|
||||
}
|
||||
|
||||
mockGetOrganizations.mockResolvedValue([mockOrg])
|
||||
mockGetProjectDetail.mockResolvedValue(mockProject)
|
||||
mockGetAiOptInLevel.mockReturnValue('schema_only')
|
||||
|
||||
const result = await getOrgAIDetails({
|
||||
orgSlug: 'test-org',
|
||||
authorization: 'Bearer token',
|
||||
projectRef: 'test-project',
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
aiOptInLevel: 'schema_only',
|
||||
isLimited: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('should mark pro plan as not limited', async () => {
|
||||
const mockOrg = {
|
||||
id: 1,
|
||||
slug: 'test-org',
|
||||
plan: { id: 'pro' },
|
||||
opt_in_tags: [],
|
||||
}
|
||||
const mockProject = {
|
||||
organization_id: 1,
|
||||
}
|
||||
|
||||
mockGetOrganizations.mockResolvedValue([mockOrg])
|
||||
mockGetProjectDetail.mockResolvedValue(mockProject)
|
||||
mockGetAiOptInLevel.mockReturnValue('full')
|
||||
|
||||
const result = await getOrgAIDetails({
|
||||
orgSlug: 'test-org',
|
||||
authorization: 'Bearer token',
|
||||
projectRef: 'test-project',
|
||||
})
|
||||
|
||||
expect(result.isLimited).toBe(false)
|
||||
})
|
||||
|
||||
it('should throw error when project and org do not match', async () => {
|
||||
const mockOrg = {
|
||||
id: 1,
|
||||
slug: 'test-org',
|
||||
plan: { id: 'pro' },
|
||||
}
|
||||
const mockProject = {
|
||||
organization_id: 2, // Different org ID
|
||||
}
|
||||
|
||||
mockGetOrganizations.mockResolvedValue([mockOrg])
|
||||
mockGetProjectDetail.mockResolvedValue(mockProject)
|
||||
|
||||
await expect(
|
||||
getOrgAIDetails({
|
||||
orgSlug: 'test-org',
|
||||
authorization: 'Bearer token',
|
||||
projectRef: 'test-project',
|
||||
})
|
||||
).rejects.toThrow('Project and organization do not match')
|
||||
})
|
||||
|
||||
it('should handle org not found', async () => {
|
||||
const mockProject = {
|
||||
organization_id: 1,
|
||||
}
|
||||
|
||||
mockGetOrganizations.mockResolvedValue([]) // No orgs
|
||||
mockGetProjectDetail.mockResolvedValue(mockProject)
|
||||
|
||||
await expect(
|
||||
getOrgAIDetails({
|
||||
orgSlug: 'non-existent-org',
|
||||
authorization: 'Bearer token',
|
||||
projectRef: 'test-project',
|
||||
})
|
||||
).rejects.toThrow('Project and organization do not match')
|
||||
})
|
||||
|
||||
it('should call getAiOptInLevel with org opt_in_tags', async () => {
|
||||
const mockOptInTags = ['AI_SQL_GENERATOR_OPT_IN', 'AI_DATA_GENERATOR_OPT_IN']
|
||||
const mockOrg = {
|
||||
id: 1,
|
||||
slug: 'test-org',
|
||||
plan: { id: 'pro' },
|
||||
opt_in_tags: mockOptInTags,
|
||||
}
|
||||
const mockProject = {
|
||||
organization_id: 1,
|
||||
}
|
||||
|
||||
mockGetOrganizations.mockResolvedValue([mockOrg])
|
||||
mockGetProjectDetail.mockResolvedValue(mockProject)
|
||||
mockGetAiOptInLevel.mockReturnValue('full')
|
||||
|
||||
await getOrgAIDetails({
|
||||
orgSlug: 'test-org',
|
||||
authorization: 'Bearer token',
|
||||
projectRef: 'test-project',
|
||||
})
|
||||
|
||||
expect(mockGetAiOptInLevel).toHaveBeenCalledWith(mockOptInTags)
|
||||
})
|
||||
|
||||
it('should include authorization header when provided', async () => {
|
||||
const mockOrg = {
|
||||
id: 1,
|
||||
slug: 'test-org',
|
||||
plan: { id: 'pro' },
|
||||
}
|
||||
const mockProject = {
|
||||
organization_id: 1,
|
||||
}
|
||||
|
||||
mockGetOrganizations.mockResolvedValue([mockOrg])
|
||||
mockGetProjectDetail.mockResolvedValue(mockProject)
|
||||
mockGetAiOptInLevel.mockReturnValue('full')
|
||||
|
||||
await getOrgAIDetails({
|
||||
orgSlug: 'test-org',
|
||||
authorization: 'Bearer custom-token',
|
||||
projectRef: 'test-project',
|
||||
})
|
||||
|
||||
expect(mockGetOrganizations).toHaveBeenCalledWith({
|
||||
headers: expect.objectContaining({
|
||||
Authorization: 'Bearer custom-token',
|
||||
}),
|
||||
})
|
||||
expect(mockGetProjectDetail).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
undefined,
|
||||
expect.objectContaining({
|
||||
Authorization: 'Bearer custom-token',
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should fetch multiple organizations and find correct one', async () => {
|
||||
const mockOrgs = [
|
||||
{ id: 1, slug: 'org-1', plan: { id: 'free' } },
|
||||
{ id: 2, slug: 'test-org', plan: { id: 'pro' } },
|
||||
{ id: 3, slug: 'org-3', plan: { id: 'team' } },
|
||||
]
|
||||
const mockProject = {
|
||||
organization_id: 2,
|
||||
}
|
||||
|
||||
mockGetOrganizations.mockResolvedValue(mockOrgs)
|
||||
mockGetProjectDetail.mockResolvedValue(mockProject)
|
||||
mockGetAiOptInLevel.mockReturnValue('full')
|
||||
|
||||
const result = await getOrgAIDetails({
|
||||
orgSlug: 'test-org',
|
||||
authorization: 'Bearer token',
|
||||
projectRef: 'test-project',
|
||||
})
|
||||
|
||||
expect(result.isLimited).toBe(false) // Pro plan
|
||||
})
|
||||
})
|
||||
})
|
||||
220
apps/studio/lib/ai/tools/incident-tools.test.ts
Normal file
220
apps/studio/lib/ai/tools/incident-tools.test.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { getIncidentTools } from './incident-tools'
|
||||
|
||||
// Mock IS_PLATFORM
|
||||
vi.mock('common', () => ({
|
||||
IS_PLATFORM: true,
|
||||
}))
|
||||
|
||||
describe('ai/tools/incident-tools', () => {
|
||||
let mockFetch: ReturnType<typeof vi.fn>
|
||||
let mockAbortSignal: AbortSignal
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockFetch = vi.fn()
|
||||
global.fetch = mockFetch
|
||||
|
||||
// Mock AbortSignal.timeout
|
||||
mockAbortSignal = new AbortController().signal
|
||||
if (!AbortSignal.timeout) {
|
||||
AbortSignal.timeout = vi.fn(() => mockAbortSignal) as any
|
||||
}
|
||||
})
|
||||
|
||||
describe('getIncidentTools', () => {
|
||||
it('should return an object with get_active_incidents tool', () => {
|
||||
const tools = getIncidentTools({ baseUrl: 'https://supabase.com/dashboard' })
|
||||
|
||||
expect(tools).toBeDefined()
|
||||
expect(tools.get_active_incidents).toBeDefined()
|
||||
})
|
||||
|
||||
it('should have correct description for get_active_incidents', () => {
|
||||
const tools = getIncidentTools({ baseUrl: 'https://supabase.com/dashboard' })
|
||||
|
||||
expect(tools.get_active_incidents.description).toContain('Check for active incidents')
|
||||
expect(tools.get_active_incidents.description).toContain('Supabase service')
|
||||
})
|
||||
|
||||
it('should have empty input schema', () => {
|
||||
const tools = getIncidentTools({ baseUrl: 'https://supabase.com/dashboard' })
|
||||
const schema = tools.get_active_incidents.inputSchema
|
||||
|
||||
// The schema is a Zod object that accepts empty object
|
||||
expect(schema).toBeDefined()
|
||||
expect((schema as any)._def.typeName).toBe('ZodObject')
|
||||
})
|
||||
|
||||
describe('execute function', () => {
|
||||
it('should return empty incidents when not on platform', async () => {
|
||||
const common = await import('common')
|
||||
vi.spyOn(common, 'IS_PLATFORM', 'get').mockReturnValue(false)
|
||||
|
||||
const tools = getIncidentTools({ baseUrl: 'https://supabase.com/dashboard' })
|
||||
const result = await (tools.get_active_incidents.execute as any)({})
|
||||
|
||||
expect(result).toEqual({
|
||||
incidents: [],
|
||||
message: 'Incident checking is only available on Supabase platform.',
|
||||
})
|
||||
expect(mockFetch).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should fetch incidents from correct URL', async () => {
|
||||
const common = await import('common')
|
||||
vi.spyOn(common, 'IS_PLATFORM', 'get').mockReturnValue(true)
|
||||
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => [],
|
||||
})
|
||||
|
||||
const tools = getIncidentTools({ baseUrl: 'https://example.com/dashboard' })
|
||||
if (!tools.get_active_incidents.execute) throw new Error('execute is undefined')
|
||||
await tools.get_active_incidents.execute({}, { toolCallId: 'test', messages: [] })
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'https://example.com/dashboard/api/incident-status',
|
||||
{
|
||||
signal: expect.any(AbortSignal),
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('should return message when no incidents', async () => {
|
||||
const common = await import('common')
|
||||
vi.spyOn(common, 'IS_PLATFORM', 'get').mockReturnValue(true)
|
||||
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => [],
|
||||
})
|
||||
|
||||
const tools = getIncidentTools({ baseUrl: 'https://supabase.com/dashboard' })
|
||||
const result = await (tools.get_active_incidents.execute as any)({})
|
||||
|
||||
expect(result).toEqual({
|
||||
incidents: [],
|
||||
message: expect.stringContaining('No active incidents'),
|
||||
})
|
||||
})
|
||||
|
||||
it('should return incident summaries when incidents exist', async () => {
|
||||
const common = await import('common')
|
||||
vi.spyOn(common, 'IS_PLATFORM', 'get').mockReturnValue(true)
|
||||
|
||||
const mockIncidents = [
|
||||
{
|
||||
name: 'Database slowness',
|
||||
status: 'investigating',
|
||||
impact: 'minor',
|
||||
active_since: '2024-01-01T10:00:00Z',
|
||||
extra_field: 'should be filtered',
|
||||
},
|
||||
]
|
||||
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => mockIncidents,
|
||||
})
|
||||
|
||||
const tools = getIncidentTools({ baseUrl: 'https://supabase.com/dashboard' })
|
||||
const result = await (tools.get_active_incidents.execute as any)({})
|
||||
|
||||
expect((result as any).incidents).toEqual([
|
||||
{
|
||||
name: 'Database slowness',
|
||||
status: 'investigating',
|
||||
impact: 'minor',
|
||||
active_since: '2024-01-01T10:00:00Z',
|
||||
},
|
||||
])
|
||||
expect((result as any).message).toContain('1 active incident')
|
||||
expect((result as any).message).toContain('status.supabase.com')
|
||||
})
|
||||
|
||||
it('should handle multiple incidents', async () => {
|
||||
const common = await import('common')
|
||||
vi.spyOn(common, 'IS_PLATFORM', 'get').mockReturnValue(true)
|
||||
|
||||
const mockIncidents = [
|
||||
{
|
||||
name: 'Database issue',
|
||||
status: 'investigating',
|
||||
impact: 'major',
|
||||
active_since: '2024-01-01T10:00:00Z',
|
||||
},
|
||||
{
|
||||
name: 'Storage issue',
|
||||
status: 'identified',
|
||||
impact: 'minor',
|
||||
active_since: '2024-01-01T11:00:00Z',
|
||||
},
|
||||
]
|
||||
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => mockIncidents,
|
||||
})
|
||||
|
||||
const tools = getIncidentTools({ baseUrl: 'https://supabase.com/dashboard' })
|
||||
const result = await (tools.get_active_incidents.execute as any)({})
|
||||
|
||||
expect((result as any).incidents).toHaveLength(2)
|
||||
expect((result as any).message).toContain('2 active incidents')
|
||||
})
|
||||
|
||||
it('should handle fetch errors', async () => {
|
||||
const common = await import('common')
|
||||
vi.spyOn(common, 'IS_PLATFORM', 'get').mockReturnValue(true)
|
||||
|
||||
mockFetch.mockRejectedValue(new Error('Network error'))
|
||||
|
||||
const tools = getIncidentTools({ baseUrl: 'https://supabase.com/dashboard' })
|
||||
const result = await (tools.get_active_incidents.execute as any)({})
|
||||
|
||||
expect(result).toEqual({
|
||||
incidents: [],
|
||||
error: 'Unable to check incident status at this time.',
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle non-ok responses', async () => {
|
||||
const common = await import('common')
|
||||
vi.spyOn(common, 'IS_PLATFORM', 'get').mockReturnValue(true)
|
||||
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500,
|
||||
})
|
||||
|
||||
const tools = getIncidentTools({ baseUrl: 'https://supabase.com/dashboard' })
|
||||
const result = await (tools.get_active_incidents.execute as any)({})
|
||||
|
||||
expect(result).toEqual({
|
||||
incidents: [],
|
||||
error: 'Unable to check incident status at this time.',
|
||||
})
|
||||
})
|
||||
|
||||
it('should use timeout signal', async () => {
|
||||
const common = await import('common')
|
||||
vi.spyOn(common, 'IS_PLATFORM', 'get').mockReturnValue(true)
|
||||
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => [],
|
||||
})
|
||||
|
||||
const tools = getIncidentTools({ baseUrl: 'https://supabase.com/dashboard' })
|
||||
if (!tools.get_active_incidents.execute) throw new Error('execute is undefined')
|
||||
await tools.get_active_incidents.execute({}, { toolCallId: 'test', messages: [] })
|
||||
|
||||
const callArgs = mockFetch.mock.calls[0]
|
||||
expect(callArgs[1].signal).toBeInstanceOf(AbortSignal)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
141
apps/studio/lib/ai/tools/rendering-tools.test.ts
Normal file
141
apps/studio/lib/ai/tools/rendering-tools.test.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { getRenderingTools } from './rendering-tools'
|
||||
|
||||
describe('ai/tools/rendering-tools', () => {
|
||||
describe('getRenderingTools', () => {
|
||||
it('should return an object with tool definitions', () => {
|
||||
const tools = getRenderingTools()
|
||||
|
||||
expect(tools).toBeDefined()
|
||||
expect(typeof tools).toBe('object')
|
||||
})
|
||||
|
||||
it('should include execute_sql tool', () => {
|
||||
const tools = getRenderingTools()
|
||||
|
||||
expect(tools.execute_sql).toBeDefined()
|
||||
expect(tools.execute_sql.description).toContain('execute a SQL statement')
|
||||
})
|
||||
|
||||
it('should include deploy_edge_function tool', () => {
|
||||
const tools = getRenderingTools()
|
||||
|
||||
expect(tools.deploy_edge_function).toBeDefined()
|
||||
expect(tools.deploy_edge_function.description).toContain('deploy a Supabase Edge Function')
|
||||
})
|
||||
|
||||
it('should include rename_chat tool', () => {
|
||||
const tools = getRenderingTools()
|
||||
|
||||
expect(tools.rename_chat).toBeDefined()
|
||||
expect(tools.rename_chat.description).toContain('Rename the current chat session')
|
||||
})
|
||||
|
||||
it('should have exactly 3 tools', () => {
|
||||
const tools = getRenderingTools()
|
||||
const toolNames = Object.keys(tools)
|
||||
|
||||
expect(toolNames).toHaveLength(3)
|
||||
expect(toolNames).toContain('execute_sql')
|
||||
expect(toolNames).toContain('deploy_edge_function')
|
||||
expect(toolNames).toContain('rename_chat')
|
||||
})
|
||||
|
||||
it('should have execute_sql with correct input schema fields', () => {
|
||||
const tools = getRenderingTools()
|
||||
const executeSqlTool = tools.execute_sql
|
||||
|
||||
// Check that the tool has an input schema
|
||||
expect(executeSqlTool.inputSchema).toBeDefined()
|
||||
|
||||
// Verify the schema exists and is a Zod object
|
||||
const schema = executeSqlTool.inputSchema
|
||||
expect(schema).toBeDefined()
|
||||
expect((schema as any)._def.typeName).toBe('ZodObject')
|
||||
})
|
||||
|
||||
it('should have deploy_edge_function with input schema', () => {
|
||||
const tools = getRenderingTools()
|
||||
const deployTool = tools.deploy_edge_function
|
||||
|
||||
expect(deployTool.inputSchema).toBeDefined()
|
||||
|
||||
// Verify the schema exists and is a Zod object
|
||||
expect(deployTool.inputSchema).toBeDefined()
|
||||
expect((deployTool.inputSchema as any)._def.typeName).toBe('ZodObject')
|
||||
})
|
||||
|
||||
it('should have rename_chat with execute function', async () => {
|
||||
const tools = getRenderingTools()
|
||||
const renameTool = tools.rename_chat
|
||||
|
||||
expect(renameTool.execute).toBeDefined()
|
||||
expect(typeof renameTool.execute).toBe('function')
|
||||
|
||||
// Test the execute function
|
||||
if (!renameTool.execute) throw new Error('execute is undefined')
|
||||
const result = await renameTool.execute(
|
||||
{ newName: 'Test Chat' },
|
||||
{ toolCallId: 'test', messages: [] }
|
||||
)
|
||||
expect(result).toEqual({ status: 'Chat request sent to client' })
|
||||
})
|
||||
|
||||
it('should validate execute_sql input schema correctly', () => {
|
||||
const tools = getRenderingTools()
|
||||
const schema = tools.execute_sql.inputSchema
|
||||
|
||||
// Check if schema is a Zod schema with safeParse
|
||||
if ('safeParse' in schema) {
|
||||
// Valid input
|
||||
const validInput = {
|
||||
sql: 'SELECT * FROM users',
|
||||
label: 'Get users',
|
||||
chartConfig: { view: 'table' as const },
|
||||
isWriteQuery: false,
|
||||
}
|
||||
expect(schema.safeParse(validInput).success).toBe(true)
|
||||
|
||||
// Valid chart config
|
||||
const validChartInput = {
|
||||
sql: 'SELECT count(*) FROM users',
|
||||
label: 'User count',
|
||||
chartConfig: { view: 'chart' as const, xAxis: 'date', yAxis: 'count' },
|
||||
isWriteQuery: false,
|
||||
}
|
||||
expect(schema.safeParse(validChartInput).success).toBe(true)
|
||||
|
||||
// Missing required field
|
||||
const invalidInput = {
|
||||
sql: 'SELECT * FROM users',
|
||||
// missing label, chartConfig, isWriteQuery
|
||||
}
|
||||
expect(schema.safeParse(invalidInput).success).toBe(false)
|
||||
} else {
|
||||
// Skip test if schema doesn't have safeParse
|
||||
expect(schema).toBeDefined()
|
||||
}
|
||||
})
|
||||
|
||||
it('should validate rename_chat input schema correctly', () => {
|
||||
const tools = getRenderingTools()
|
||||
const schema = tools.rename_chat.inputSchema
|
||||
|
||||
// Check if schema is a Zod schema with safeParse
|
||||
if ('safeParse' in schema) {
|
||||
// Valid input
|
||||
expect(schema.safeParse({ newName: 'My Chat' }).success).toBe(true)
|
||||
|
||||
// Invalid input - missing newName
|
||||
expect(schema.safeParse({}).success).toBe(false)
|
||||
|
||||
// Invalid input - wrong type
|
||||
expect(schema.safeParse({ newName: 123 }).success).toBe(false)
|
||||
} else {
|
||||
// Skip test if schema doesn't have safeParse
|
||||
expect(schema).toBeDefined()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
105
apps/studio/lib/api/self-hosted/constants.test.ts
Normal file
105
apps/studio/lib/api/self-hosted/constants.test.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
describe('api/self-hosted/constants', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
})
|
||||
|
||||
describe('ENCRYPTION_KEY', () => {
|
||||
it('should use PG_META_CRYPTO_KEY when set', async () => {
|
||||
vi.stubEnv('PG_META_CRYPTO_KEY', 'my-secret-key-123')
|
||||
const { ENCRYPTION_KEY } = await import('./constants')
|
||||
expect(ENCRYPTION_KEY).toBe('my-secret-key-123')
|
||||
})
|
||||
|
||||
it('should use SAMPLE_KEY as default', async () => {
|
||||
vi.stubEnv('PG_META_CRYPTO_KEY', '')
|
||||
const { ENCRYPTION_KEY } = await import('./constants')
|
||||
expect(ENCRYPTION_KEY).toBe('SAMPLE_KEY')
|
||||
})
|
||||
})
|
||||
|
||||
describe('POSTGRES_PORT', () => {
|
||||
it('should use POSTGRES_PORT when set', async () => {
|
||||
vi.stubEnv('POSTGRES_PORT', '5433')
|
||||
const { POSTGRES_PORT } = await import('./constants')
|
||||
expect(POSTGRES_PORT).toBe(5433)
|
||||
})
|
||||
|
||||
it('should default to 5432', async () => {
|
||||
vi.stubEnv('POSTGRES_PORT', '')
|
||||
const { POSTGRES_PORT } = await import('./constants')
|
||||
expect(POSTGRES_PORT).toBe(5432)
|
||||
})
|
||||
})
|
||||
|
||||
describe('POSTGRES_HOST', () => {
|
||||
it('should use POSTGRES_HOST when set', async () => {
|
||||
vi.stubEnv('POSTGRES_HOST', 'my-db-host.example.com')
|
||||
const { POSTGRES_HOST } = await import('./constants')
|
||||
expect(POSTGRES_HOST).toBe('my-db-host.example.com')
|
||||
})
|
||||
|
||||
it('should default to db', async () => {
|
||||
vi.stubEnv('POSTGRES_HOST', '')
|
||||
const { POSTGRES_HOST } = await import('./constants')
|
||||
expect(POSTGRES_HOST).toBe('db')
|
||||
})
|
||||
})
|
||||
|
||||
describe('POSTGRES_DATABASE', () => {
|
||||
it('should use POSTGRES_DB when set', async () => {
|
||||
vi.stubEnv('POSTGRES_DB', 'my_database')
|
||||
const { POSTGRES_DATABASE } = await import('./constants')
|
||||
expect(POSTGRES_DATABASE).toBe('my_database')
|
||||
})
|
||||
|
||||
it('should default to postgres', async () => {
|
||||
vi.stubEnv('POSTGRES_DB', '')
|
||||
const { POSTGRES_DATABASE } = await import('./constants')
|
||||
expect(POSTGRES_DATABASE).toBe('postgres')
|
||||
})
|
||||
})
|
||||
|
||||
describe('POSTGRES_PASSWORD', () => {
|
||||
it('should use POSTGRES_PASSWORD when set', async () => {
|
||||
vi.stubEnv('POSTGRES_PASSWORD', 'super-secret-password')
|
||||
const { POSTGRES_PASSWORD } = await import('./constants')
|
||||
expect(POSTGRES_PASSWORD).toBe('super-secret-password')
|
||||
})
|
||||
|
||||
it('should default to postgres', async () => {
|
||||
vi.stubEnv('POSTGRES_PASSWORD', '')
|
||||
const { POSTGRES_PASSWORD } = await import('./constants')
|
||||
expect(POSTGRES_PASSWORD).toBe('postgres')
|
||||
})
|
||||
})
|
||||
|
||||
describe('POSTGRES_USER_READ_WRITE', () => {
|
||||
it('should use POSTGRES_USER_READ_WRITE when set', async () => {
|
||||
vi.stubEnv('POSTGRES_USER_READ_WRITE', 'custom_admin')
|
||||
const { POSTGRES_USER_READ_WRITE } = await import('./constants')
|
||||
expect(POSTGRES_USER_READ_WRITE).toBe('custom_admin')
|
||||
})
|
||||
|
||||
it('should default to supabase_admin', async () => {
|
||||
vi.stubEnv('POSTGRES_USER_READ_WRITE', '')
|
||||
const { POSTGRES_USER_READ_WRITE } = await import('./constants')
|
||||
expect(POSTGRES_USER_READ_WRITE).toBe('supabase_admin')
|
||||
})
|
||||
})
|
||||
|
||||
describe('POSTGRES_USER_READ_ONLY', () => {
|
||||
it('should use POSTGRES_USER_READ_ONLY when set', async () => {
|
||||
vi.stubEnv('POSTGRES_USER_READ_ONLY', 'custom_readonly')
|
||||
const { POSTGRES_USER_READ_ONLY } = await import('./constants')
|
||||
expect(POSTGRES_USER_READ_ONLY).toBe('custom_readonly')
|
||||
})
|
||||
|
||||
it('should default to supabase_read_only_user', async () => {
|
||||
vi.stubEnv('POSTGRES_USER_READ_ONLY', '')
|
||||
const { POSTGRES_USER_READ_ONLY } = await import('./constants')
|
||||
expect(POSTGRES_USER_READ_ONLY).toBe('supabase_read_only_user')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
// Constants specific to self-hosted environments
|
||||
|
||||
export const ENCRYPTION_KEY = process.env.PG_META_CRYPTO_KEY || 'SAMPLE_KEY'
|
||||
export const POSTGRES_PORT = process.env.POSTGRES_PORT || 5432
|
||||
export const POSTGRES_PORT = parseInt(process.env.POSTGRES_PORT || '5432', 10)
|
||||
export const POSTGRES_HOST = process.env.POSTGRES_HOST || 'db'
|
||||
export const POSTGRES_DATABASE = process.env.POSTGRES_DB || 'postgres'
|
||||
export const POSTGRES_PASSWORD = process.env.POSTGRES_PASSWORD || 'postgres'
|
||||
|
||||
75
apps/studio/lib/api/self-hosted/functions/index.test.ts
Normal file
75
apps/studio/lib/api/self-hosted/functions/index.test.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import * as util from '../util'
|
||||
import * as fileSystemStore from './fileSystemStore'
|
||||
import { getFunctionsArtifactStore } from './index'
|
||||
|
||||
vi.mock('../util', () => ({
|
||||
assertSelfHosted: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('./fileSystemStore', () => ({
|
||||
FileSystemFunctionsArtifactStore: vi.fn(),
|
||||
}))
|
||||
|
||||
describe('api/self-hosted/functions/index', () => {
|
||||
let originalEdgeFunctionsFolder: string | undefined
|
||||
|
||||
beforeEach(() => {
|
||||
originalEdgeFunctionsFolder = process.env.EDGE_FUNCTIONS_MANAGEMENT_FOLDER
|
||||
vi.resetAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (originalEdgeFunctionsFolder !== undefined) {
|
||||
process.env.EDGE_FUNCTIONS_MANAGEMENT_FOLDER = originalEdgeFunctionsFolder
|
||||
} else {
|
||||
delete process.env.EDGE_FUNCTIONS_MANAGEMENT_FOLDER
|
||||
}
|
||||
})
|
||||
|
||||
describe('getFunctionsArtifactStore', () => {
|
||||
it('should call assertSelfHosted', () => {
|
||||
process.env.EDGE_FUNCTIONS_MANAGEMENT_FOLDER = '/tmp/functions'
|
||||
|
||||
getFunctionsArtifactStore()
|
||||
|
||||
expect(util.assertSelfHosted).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should throw error if EDGE_FUNCTIONS_MANAGEMENT_FOLDER is not set', () => {
|
||||
delete process.env.EDGE_FUNCTIONS_MANAGEMENT_FOLDER
|
||||
|
||||
expect(() => getFunctionsArtifactStore()).toThrow(
|
||||
'EDGE_FUNCTIONS_MANAGEMENT_FOLDER is required'
|
||||
)
|
||||
})
|
||||
|
||||
it('should create FileSystemFunctionsArtifactStore with correct path', () => {
|
||||
process.env.EDGE_FUNCTIONS_MANAGEMENT_FOLDER = '/var/lib/functions'
|
||||
|
||||
getFunctionsArtifactStore()
|
||||
|
||||
expect(fileSystemStore.FileSystemFunctionsArtifactStore).toHaveBeenCalledWith(
|
||||
'/var/lib/functions'
|
||||
)
|
||||
})
|
||||
|
||||
it('should return FileSystemFunctionsArtifactStore instance', () => {
|
||||
const mockInstance = {
|
||||
folderPath: '/tmp/test',
|
||||
getFunctions: vi.fn(),
|
||||
getFunctionBySlug: vi.fn(),
|
||||
getFileEntriesBySlug: vi.fn(),
|
||||
}
|
||||
vi.mocked(fileSystemStore.FileSystemFunctionsArtifactStore).mockReturnValue(
|
||||
mockInstance as any
|
||||
)
|
||||
process.env.EDGE_FUNCTIONS_MANAGEMENT_FOLDER = '/tmp/test'
|
||||
|
||||
const result = getFunctionsArtifactStore()
|
||||
|
||||
expect(result).toBe(mockInstance)
|
||||
})
|
||||
})
|
||||
})
|
||||
142
apps/studio/lib/api/self-hosted/generate-types.test.ts
Normal file
142
apps/studio/lib/api/self-hosted/generate-types.test.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { generateTypescriptTypes } from './generate-types'
|
||||
|
||||
vi.mock('data/fetchers', () => ({
|
||||
fetchGet: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('lib/constants', () => ({
|
||||
PG_META_URL: 'http://localhost:8080',
|
||||
}))
|
||||
|
||||
vi.mock('./util', () => ({
|
||||
assertSelfHosted: vi.fn(),
|
||||
}))
|
||||
|
||||
describe('api/self-hosted/generate-types', () => {
|
||||
let mockFetchGet: ReturnType<typeof vi.fn>
|
||||
let mockAssertSelfHosted: ReturnType<typeof vi.fn>
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
const fetchers = await import('data/fetchers')
|
||||
const util = await import('./util')
|
||||
|
||||
mockFetchGet = vi.mocked(fetchers.fetchGet)
|
||||
mockAssertSelfHosted = vi.mocked(util.assertSelfHosted)
|
||||
})
|
||||
|
||||
describe('generateTypescriptTypes', () => {
|
||||
it('should call assertSelfHosted', async () => {
|
||||
mockFetchGet.mockResolvedValue({ types: 'export type User = {}' })
|
||||
|
||||
await generateTypescriptTypes({ headers: {} })
|
||||
|
||||
expect(mockAssertSelfHosted).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should fetch from correct URL with schema params', async () => {
|
||||
mockFetchGet.mockResolvedValue({ types: 'export type User = {}' })
|
||||
|
||||
await generateTypescriptTypes({ headers: {} })
|
||||
|
||||
expect(mockFetchGet).toHaveBeenCalledWith(
|
||||
expect.stringContaining('http://localhost:8080/generators/typescript'),
|
||||
expect.any(Object)
|
||||
)
|
||||
|
||||
const callUrl = mockFetchGet.mock.calls[0][0]
|
||||
expect(callUrl).toContain('included_schema=public,graphql_public,storage')
|
||||
expect(callUrl).toContain('excluded_schemas=')
|
||||
})
|
||||
|
||||
it('should include correct schemas in URL', async () => {
|
||||
mockFetchGet.mockResolvedValue({ types: '' })
|
||||
|
||||
await generateTypescriptTypes({ headers: {} })
|
||||
|
||||
const callUrl = mockFetchGet.mock.calls[0][0]
|
||||
expect(callUrl).toContain('public')
|
||||
expect(callUrl).toContain('graphql_public')
|
||||
expect(callUrl).toContain('storage')
|
||||
})
|
||||
|
||||
it('should exclude system schemas', async () => {
|
||||
mockFetchGet.mockResolvedValue({ types: '' })
|
||||
|
||||
await generateTypescriptTypes({ headers: {} })
|
||||
|
||||
const callUrl = mockFetchGet.mock.calls[0][0]
|
||||
const excludedSchemas = [
|
||||
'auth',
|
||||
'cron',
|
||||
'extensions',
|
||||
'graphql',
|
||||
'net',
|
||||
'pgsodium',
|
||||
'pgsodium_masks',
|
||||
'realtime',
|
||||
'supabase_functions',
|
||||
'supabase_migrations',
|
||||
'vault',
|
||||
'_analytics',
|
||||
'_realtime',
|
||||
]
|
||||
|
||||
excludedSchemas.forEach((schema) => {
|
||||
expect(callUrl).toContain(schema)
|
||||
})
|
||||
})
|
||||
|
||||
it('should pass headers to fetchGet', async () => {
|
||||
mockFetchGet.mockResolvedValue({ types: 'export type User = {}' })
|
||||
|
||||
const customHeaders = {
|
||||
Authorization: 'Bearer token',
|
||||
'Custom-Header': 'value',
|
||||
}
|
||||
|
||||
await generateTypescriptTypes({ headers: customHeaders })
|
||||
|
||||
expect(mockFetchGet).toHaveBeenCalledWith(expect.any(String), {
|
||||
headers: customHeaders,
|
||||
})
|
||||
})
|
||||
|
||||
it('should return types from fetchGet response', async () => {
|
||||
const mockTypes = 'export type User = { id: number; name: string }'
|
||||
mockFetchGet.mockResolvedValue({ types: mockTypes })
|
||||
|
||||
const result = await generateTypescriptTypes({ headers: {} })
|
||||
|
||||
expect(result).toEqual({ types: mockTypes })
|
||||
})
|
||||
|
||||
it('should handle fetchGet errors', async () => {
|
||||
const mockError = new Error('Network error')
|
||||
mockFetchGet.mockRejectedValue(mockError)
|
||||
|
||||
await expect(generateTypescriptTypes({ headers: {} })).rejects.toThrow('Network error')
|
||||
})
|
||||
|
||||
it('should work without headers parameter', async () => {
|
||||
mockFetchGet.mockResolvedValue({ types: '' })
|
||||
|
||||
await generateTypescriptTypes({})
|
||||
|
||||
expect(mockFetchGet).toHaveBeenCalledWith(expect.any(String), { headers: undefined })
|
||||
})
|
||||
|
||||
it('should construct URL with both included and excluded schemas', async () => {
|
||||
mockFetchGet.mockResolvedValue({ types: '' })
|
||||
|
||||
await generateTypescriptTypes({ headers: {} })
|
||||
|
||||
const callUrl = mockFetchGet.mock.calls[0][0]
|
||||
expect(callUrl).toContain('included_schema=')
|
||||
expect(callUrl).toContain('excluded_schemas=')
|
||||
})
|
||||
})
|
||||
})
|
||||
129
apps/studio/lib/api/self-hosted/settings.test.ts
Normal file
129
apps/studio/lib/api/self-hosted/settings.test.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { getProjectSettings } from './settings'
|
||||
|
||||
vi.mock('./util', () => ({
|
||||
assertSelfHosted: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('lib/constants/api', () => ({
|
||||
PROJECT_ENDPOINT: 'localhost:8000',
|
||||
PROJECT_ENDPOINT_PROTOCOL: 'http',
|
||||
}))
|
||||
|
||||
describe('api/self-hosted/settings', () => {
|
||||
let mockAssertSelfHosted: ReturnType<typeof vi.fn>
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks()
|
||||
vi.resetModules()
|
||||
|
||||
const util = await import('./util')
|
||||
mockAssertSelfHosted = vi.mocked(util.assertSelfHosted)
|
||||
})
|
||||
|
||||
describe('getProjectSettings', () => {
|
||||
it('should call assertSelfHosted', () => {
|
||||
getProjectSettings()
|
||||
|
||||
expect(mockAssertSelfHosted).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should return project settings with correct structure', () => {
|
||||
const settings = getProjectSettings()
|
||||
|
||||
expect(settings).toHaveProperty('app_config')
|
||||
expect(settings).toHaveProperty('cloud_provider')
|
||||
expect(settings).toHaveProperty('db_dns_name')
|
||||
expect(settings).toHaveProperty('db_host')
|
||||
expect(settings).toHaveProperty('db_name')
|
||||
expect(settings).toHaveProperty('jwt_secret')
|
||||
expect(settings).toHaveProperty('service_api_keys')
|
||||
})
|
||||
|
||||
it('should return correct default values', () => {
|
||||
const settings = getProjectSettings()
|
||||
|
||||
expect(settings.cloud_provider).toBe('AWS')
|
||||
expect(settings.db_host).toBe('localhost')
|
||||
expect(settings.db_name).toBe('postgres')
|
||||
expect(settings.db_port).toBe(5432)
|
||||
expect(settings.db_user).toBe('postgres')
|
||||
expect(settings.ref).toBe('default')
|
||||
expect(settings.region).toBe('ap-southeast-1')
|
||||
expect(settings.status).toBe('ACTIVE_HEALTHY')
|
||||
expect(settings.ssl_enforced).toBe(false)
|
||||
})
|
||||
|
||||
it('should include app_config with endpoint and protocol', () => {
|
||||
const settings = getProjectSettings()
|
||||
|
||||
expect(settings.app_config).toEqual({
|
||||
db_schema: 'public',
|
||||
endpoint: 'localhost:8000',
|
||||
storage_endpoint: 'localhost:8000',
|
||||
protocol: 'http',
|
||||
})
|
||||
})
|
||||
|
||||
it('should include service_api_keys array', () => {
|
||||
const settings = getProjectSettings()
|
||||
|
||||
expect(settings.service_api_keys).toHaveLength(2)
|
||||
expect(settings.service_api_keys[0].name).toBe('service_role key')
|
||||
expect(settings.service_api_keys[0].tags).toBe('service_role')
|
||||
expect(settings.service_api_keys[1].name).toBe('anon key')
|
||||
expect(settings.service_api_keys[1].tags).toBe('anon')
|
||||
})
|
||||
|
||||
it('should use environment variables when set', async () => {
|
||||
vi.stubEnv('AUTH_JWT_SECRET', 'custom-jwt-secret-with-at-least-32-chars')
|
||||
vi.stubEnv('DEFAULT_PROJECT_NAME', 'My Custom Project')
|
||||
vi.stubEnv('SUPABASE_SERVICE_KEY', 'custom-service-key')
|
||||
vi.stubEnv('SUPABASE_ANON_KEY', 'custom-anon-key')
|
||||
|
||||
// Need to re-import to pick up new env vars
|
||||
vi.resetModules()
|
||||
|
||||
const { getProjectSettings: getSettings } = await import('./settings')
|
||||
const settings = getSettings()
|
||||
|
||||
expect(settings.jwt_secret).toBe('custom-jwt-secret-with-at-least-32-chars')
|
||||
expect(settings.name).toBe('My Custom Project')
|
||||
expect(settings.service_api_keys[0].api_key).toBe('custom-service-key')
|
||||
expect(settings.service_api_keys[1].api_key).toBe('custom-anon-key')
|
||||
})
|
||||
|
||||
it('should use default JWT secret when not set', async () => {
|
||||
vi.unstubAllEnvs()
|
||||
|
||||
vi.resetModules()
|
||||
const { getProjectSettings: getSettings } = await import('./settings')
|
||||
const settings = getSettings()
|
||||
|
||||
expect(settings.jwt_secret).toBe('super-secret-jwt-token-with-at-least-32-characters-long')
|
||||
})
|
||||
|
||||
it('should use default project name when not set', async () => {
|
||||
vi.unstubAllEnvs()
|
||||
|
||||
vi.resetModules()
|
||||
const { getProjectSettings: getSettings } = await import('./settings')
|
||||
const settings = getSettings()
|
||||
|
||||
expect(settings.name).toBe('Default Project')
|
||||
})
|
||||
|
||||
it('should have correct db_ip_addr_config', () => {
|
||||
const settings = getProjectSettings()
|
||||
|
||||
expect(settings.db_ip_addr_config).toBe('legacy')
|
||||
})
|
||||
|
||||
it('should have correct inserted_at timestamp', () => {
|
||||
const settings = getProjectSettings()
|
||||
|
||||
expect(settings.inserted_at).toBe('2021-08-02T06:40:40.646Z')
|
||||
})
|
||||
})
|
||||
})
|
||||
110
apps/studio/lib/api/self-hosted/types.test.ts
Normal file
110
apps/studio/lib/api/self-hosted/types.test.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { databaseErrorSchema, PgMetaDatabaseError } from './types'
|
||||
|
||||
describe('api/self-hosted/types', () => {
|
||||
describe('databaseErrorSchema', () => {
|
||||
it('should validate valid database error', () => {
|
||||
const validError = {
|
||||
message: 'Database connection failed',
|
||||
code: '08006',
|
||||
formattedError: 'Error: Connection refused',
|
||||
}
|
||||
|
||||
const result = databaseErrorSchema.safeParse(validError)
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('should reject error missing message', () => {
|
||||
const invalidError = {
|
||||
code: '08006',
|
||||
formattedError: 'Error: Connection refused',
|
||||
}
|
||||
|
||||
const result = databaseErrorSchema.safeParse(invalidError)
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it('should reject error missing code', () => {
|
||||
const invalidError = {
|
||||
message: 'Database connection failed',
|
||||
formattedError: 'Error: Connection refused',
|
||||
}
|
||||
|
||||
const result = databaseErrorSchema.safeParse(invalidError)
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it('should reject error missing formattedError', () => {
|
||||
const invalidError = {
|
||||
message: 'Database connection failed',
|
||||
code: '08006',
|
||||
}
|
||||
|
||||
const result = databaseErrorSchema.safeParse(invalidError)
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it('should reject non-string values', () => {
|
||||
const invalidError = {
|
||||
message: 123,
|
||||
code: '08006',
|
||||
formattedError: 'Error',
|
||||
}
|
||||
|
||||
const result = databaseErrorSchema.safeParse(invalidError)
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('PgMetaDatabaseError', () => {
|
||||
it('should create error with all properties', () => {
|
||||
const error = new PgMetaDatabaseError(
|
||||
'Syntax error',
|
||||
'42601',
|
||||
400,
|
||||
'ERROR: syntax error at or near "SELCT"'
|
||||
)
|
||||
|
||||
expect(error.message).toBe('Syntax error')
|
||||
expect(error.code).toBe('42601')
|
||||
expect(error.statusCode).toBe(400)
|
||||
expect(error.formattedError).toBe('ERROR: syntax error at or near "SELCT"')
|
||||
expect(error.name).toBe('PgMetaDatabaseError')
|
||||
})
|
||||
|
||||
it('should be instanceof Error', () => {
|
||||
const error = new PgMetaDatabaseError('Test error', '12345', 500, 'Formatted')
|
||||
|
||||
expect(error).toBeInstanceOf(Error)
|
||||
expect(error).toBeInstanceOf(PgMetaDatabaseError)
|
||||
})
|
||||
|
||||
it('should have correct name property', () => {
|
||||
const error = new PgMetaDatabaseError('Test', 'CODE', 400, 'Formatted')
|
||||
|
||||
expect(error.name).toBe('PgMetaDatabaseError')
|
||||
})
|
||||
|
||||
it('should preserve all custom properties', () => {
|
||||
const error = new PgMetaDatabaseError(
|
||||
'Connection timeout',
|
||||
'08000',
|
||||
503,
|
||||
'ERROR: connection timeout'
|
||||
)
|
||||
|
||||
expect(error.code).toBe('08000')
|
||||
expect(error.statusCode).toBe(503)
|
||||
expect(error.formattedError).toBe('ERROR: connection timeout')
|
||||
})
|
||||
|
||||
it('should work with different status codes', () => {
|
||||
const error400 = new PgMetaDatabaseError('Bad request', 'ERR', 400, 'Error')
|
||||
const error500 = new PgMetaDatabaseError('Server error', 'ERR', 500, 'Error')
|
||||
|
||||
expect(error400.statusCode).toBe(400)
|
||||
expect(error500.statusCode).toBe(500)
|
||||
})
|
||||
})
|
||||
})
|
||||
124
apps/studio/lib/api/self-hosted/util.test.ts
Normal file
124
apps/studio/lib/api/self-hosted/util.test.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { assertSelfHosted, encryptString, getConnectionString } from './util'
|
||||
|
||||
vi.mock('lib/constants', () => ({
|
||||
IS_PLATFORM: false,
|
||||
}))
|
||||
|
||||
vi.mock('crypto-js', () => {
|
||||
const mockEncrypt = vi.fn()
|
||||
return {
|
||||
default: {
|
||||
AES: {
|
||||
encrypt: mockEncrypt,
|
||||
},
|
||||
},
|
||||
AES: {
|
||||
encrypt: mockEncrypt,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
describe('api/self-hosted/util', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('assertSelfHosted', () => {
|
||||
it('should not throw when IS_PLATFORM is false', async () => {
|
||||
const constants = await import('lib/constants')
|
||||
vi.spyOn(constants, 'IS_PLATFORM', 'get').mockReturnValue(false)
|
||||
|
||||
expect(() => assertSelfHosted()).not.toThrow()
|
||||
})
|
||||
|
||||
it('should throw error when IS_PLATFORM is true', async () => {
|
||||
const constants = await import('lib/constants')
|
||||
vi.spyOn(constants, 'IS_PLATFORM', 'get').mockReturnValue(true)
|
||||
|
||||
expect(() => assertSelfHosted()).toThrow(
|
||||
'This function can only be called in self-hosted environments'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('encryptString', () => {
|
||||
it('should encrypt string using AES', async () => {
|
||||
const crypto = await import('crypto-js')
|
||||
const mockEncrypted = 'encrypted-string-123'
|
||||
vi.mocked(crypto.default.AES.encrypt).mockReturnValue({
|
||||
toString: () => mockEncrypted,
|
||||
} as any)
|
||||
|
||||
const result = encryptString('my-secret-data')
|
||||
|
||||
expect(crypto.default.AES.encrypt).toHaveBeenCalledWith('my-secret-data', expect.any(String))
|
||||
expect(result).toBe(mockEncrypted)
|
||||
})
|
||||
|
||||
it('should return encrypted string as string', async () => {
|
||||
const crypto = await import('crypto-js')
|
||||
vi.mocked(crypto.default.AES.encrypt).mockReturnValue({
|
||||
toString: () => 'U2FsdGVkX1+abc123',
|
||||
} as any)
|
||||
|
||||
const result = encryptString('test')
|
||||
|
||||
expect(typeof result).toBe('string')
|
||||
expect(result).toBe('U2FsdGVkX1+abc123')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getConnectionString', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
})
|
||||
|
||||
it('should build connection string with read-write user', async () => {
|
||||
vi.stubEnv('POSTGRES_HOST', 'localhost')
|
||||
vi.stubEnv('POSTGRES_PORT', '5432')
|
||||
vi.stubEnv('POSTGRES_DB', 'testdb')
|
||||
vi.stubEnv('POSTGRES_PASSWORD', 'testpass')
|
||||
vi.stubEnv('POSTGRES_USER_READ_WRITE', 'admin_user')
|
||||
|
||||
// Re-import to get updated env values
|
||||
const { getConnectionString } = await import('./util')
|
||||
|
||||
const result = getConnectionString({ readOnly: false })
|
||||
|
||||
expect(result).toBe('postgresql://admin_user:testpass@localhost:5432/testdb')
|
||||
})
|
||||
|
||||
it('should build connection string with read-only user', async () => {
|
||||
vi.stubEnv('POSTGRES_HOST', 'db.example.com')
|
||||
vi.stubEnv('POSTGRES_PORT', '5433')
|
||||
vi.stubEnv('POSTGRES_DB', 'mydb')
|
||||
vi.stubEnv('POSTGRES_PASSWORD', 'secret')
|
||||
vi.stubEnv('POSTGRES_USER_READ_ONLY', 'readonly_user')
|
||||
|
||||
const { getConnectionString } = await import('./util')
|
||||
|
||||
const result = getConnectionString({ readOnly: true })
|
||||
|
||||
expect(result).toBe('postgresql://readonly_user:secret@db.example.com:5433/mydb')
|
||||
})
|
||||
|
||||
it('should use default values when env vars not set', async () => {
|
||||
vi.stubEnv('POSTGRES_HOST', '')
|
||||
vi.stubEnv('POSTGRES_PORT', '')
|
||||
vi.stubEnv('POSTGRES_DB', '')
|
||||
vi.stubEnv('POSTGRES_PASSWORD', '')
|
||||
vi.stubEnv('POSTGRES_USER_READ_WRITE', '')
|
||||
vi.stubEnv('POSTGRES_USER_READ_ONLY', '')
|
||||
|
||||
const { getConnectionString } = await import('./util')
|
||||
|
||||
const resultReadWrite = getConnectionString({ readOnly: false })
|
||||
const resultReadOnly = getConnectionString({ readOnly: true })
|
||||
|
||||
expect(resultReadWrite).toBe('postgresql://supabase_admin:postgres@db:5432/postgres')
|
||||
expect(resultReadOnly).toBe('postgresql://supabase_read_only_user:postgres@db:5432/postgres')
|
||||
})
|
||||
})
|
||||
})
|
||||
105
apps/studio/lib/breadcrumbs.test.ts
Normal file
105
apps/studio/lib/breadcrumbs.test.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import {
|
||||
getMirroredBreadcrumbs,
|
||||
getOwnershipOfBreadcrumbSnapshot,
|
||||
MIRRORED_BREADCRUMBS,
|
||||
takeBreadcrumbSnapshot,
|
||||
} from './breadcrumbs'
|
||||
|
||||
describe('breadcrumbs', () => {
|
||||
beforeEach(() => {
|
||||
// Clear the ring buffer by popping all items
|
||||
while (MIRRORED_BREADCRUMBS.length > 0) {
|
||||
MIRRORED_BREADCRUMBS.popFront()
|
||||
}
|
||||
})
|
||||
|
||||
describe('getMirroredBreadcrumbs', () => {
|
||||
it('should return an array of breadcrumbs from the ring buffer', () => {
|
||||
const result = getMirroredBreadcrumbs()
|
||||
expect(Array.isArray(result)).toBe(true)
|
||||
})
|
||||
|
||||
it('should return empty array when no breadcrumbs exist', () => {
|
||||
const result = getMirroredBreadcrumbs()
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should return breadcrumbs after they are added to ring buffer', () => {
|
||||
const mockBreadcrumb = { message: 'test', timestamp: Date.now() } as any
|
||||
MIRRORED_BREADCRUMBS.pushBack(mockBreadcrumb)
|
||||
|
||||
const result = getMirroredBreadcrumbs()
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0]).toEqual(mockBreadcrumb)
|
||||
})
|
||||
})
|
||||
|
||||
describe('takeBreadcrumbSnapshot', () => {
|
||||
it('should capture current breadcrumbs into a snapshot', () => {
|
||||
const mockBreadcrumb = { message: 'test', timestamp: Date.now() } as any
|
||||
MIRRORED_BREADCRUMBS.pushBack(mockBreadcrumb)
|
||||
|
||||
takeBreadcrumbSnapshot()
|
||||
const snapshot = getOwnershipOfBreadcrumbSnapshot()
|
||||
|
||||
expect(snapshot).toHaveLength(1)
|
||||
expect(snapshot?.[0]).toEqual(mockBreadcrumb)
|
||||
})
|
||||
|
||||
it('should update snapshot when called multiple times', () => {
|
||||
const mockBreadcrumb1 = { message: 'first', timestamp: Date.now() } as any
|
||||
const mockBreadcrumb2 = { message: 'second', timestamp: Date.now() } as any
|
||||
|
||||
MIRRORED_BREADCRUMBS.pushBack(mockBreadcrumb1)
|
||||
takeBreadcrumbSnapshot()
|
||||
|
||||
MIRRORED_BREADCRUMBS.pushBack(mockBreadcrumb2)
|
||||
takeBreadcrumbSnapshot()
|
||||
|
||||
const snapshot = getOwnershipOfBreadcrumbSnapshot()
|
||||
expect(snapshot).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getOwnershipOfBreadcrumbSnapshot', () => {
|
||||
it('should return null initially when no snapshot has been taken', () => {
|
||||
const snapshot = getOwnershipOfBreadcrumbSnapshot()
|
||||
expect(snapshot).toBeNull()
|
||||
})
|
||||
|
||||
it('should return the snapshot after takeBreadcrumbSnapshot is called', () => {
|
||||
const mockBreadcrumb = { message: 'test', timestamp: Date.now() } as any
|
||||
MIRRORED_BREADCRUMBS.pushBack(mockBreadcrumb)
|
||||
|
||||
takeBreadcrumbSnapshot()
|
||||
const snapshot = getOwnershipOfBreadcrumbSnapshot()
|
||||
|
||||
expect(snapshot).not.toBeNull()
|
||||
expect(snapshot).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('should clear the snapshot after returning it', () => {
|
||||
const mockBreadcrumb = { message: 'test', timestamp: Date.now() } as any
|
||||
MIRRORED_BREADCRUMBS.pushBack(mockBreadcrumb)
|
||||
|
||||
takeBreadcrumbSnapshot()
|
||||
const firstSnapshot = getOwnershipOfBreadcrumbSnapshot()
|
||||
const secondSnapshot = getOwnershipOfBreadcrumbSnapshot()
|
||||
|
||||
expect(firstSnapshot).not.toBeNull()
|
||||
expect(secondSnapshot).toBeNull()
|
||||
})
|
||||
|
||||
it('should take ownership and prevent subsequent calls from getting the same snapshot', () => {
|
||||
takeBreadcrumbSnapshot()
|
||||
|
||||
const snapshot1 = getOwnershipOfBreadcrumbSnapshot()
|
||||
const snapshot2 = getOwnershipOfBreadcrumbSnapshot()
|
||||
|
||||
expect(snapshot1).not.toBe(snapshot2)
|
||||
expect(snapshot2).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
98
apps/studio/lib/constants/api.test.ts
Normal file
98
apps/studio/lib/constants/api.test.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
describe('constants/api', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
})
|
||||
|
||||
describe('PROJECT_ANALYTICS_URL', () => {
|
||||
it('should be undefined when LOGFLARE_URL is not set', async () => {
|
||||
vi.stubEnv('LOGFLARE_URL', '')
|
||||
const { PROJECT_ANALYTICS_URL } = await import('./api')
|
||||
expect(PROJECT_ANALYTICS_URL).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should use LOGFLARE_URL when set', async () => {
|
||||
vi.stubEnv('LOGFLARE_URL', 'https://logflare.example.com')
|
||||
const { PROJECT_ANALYTICS_URL } = await import('./api')
|
||||
expect(PROJECT_ANALYTICS_URL).toBe('https://logflare.example.com/api/')
|
||||
})
|
||||
})
|
||||
|
||||
describe('PROJECT_REST_URL', () => {
|
||||
it('should construct URL from SUPABASE_PUBLIC_URL', async () => {
|
||||
vi.stubEnv('SUPABASE_PUBLIC_URL', 'https://test.supabase.co')
|
||||
const { PROJECT_REST_URL } = await import('./api')
|
||||
expect(PROJECT_REST_URL).toBe('https://test.supabase.co/rest/v1/')
|
||||
})
|
||||
|
||||
it('should use default localhost when SUPABASE_PUBLIC_URL is not set', async () => {
|
||||
vi.stubEnv('SUPABASE_PUBLIC_URL', '')
|
||||
const { PROJECT_REST_URL } = await import('./api')
|
||||
expect(PROJECT_REST_URL).toBe('http://localhost:8000/rest/v1/')
|
||||
})
|
||||
})
|
||||
|
||||
describe('PROJECT_ENDPOINT', () => {
|
||||
it('should extract host from SUPABASE_PUBLIC_URL', async () => {
|
||||
vi.stubEnv('SUPABASE_PUBLIC_URL', 'https://test.supabase.co:3000')
|
||||
const { PROJECT_ENDPOINT } = await import('./api')
|
||||
expect(PROJECT_ENDPOINT).toBe('test.supabase.co:3000')
|
||||
})
|
||||
|
||||
it('should use default localhost host', async () => {
|
||||
vi.stubEnv('SUPABASE_PUBLIC_URL', '')
|
||||
const { PROJECT_ENDPOINT } = await import('./api')
|
||||
expect(PROJECT_ENDPOINT).toBe('localhost:8000')
|
||||
})
|
||||
})
|
||||
|
||||
describe('PROJECT_ENDPOINT_PROTOCOL', () => {
|
||||
it('should extract protocol without colon from SUPABASE_PUBLIC_URL', async () => {
|
||||
vi.stubEnv('SUPABASE_PUBLIC_URL', 'https://test.supabase.co')
|
||||
const { PROJECT_ENDPOINT_PROTOCOL } = await import('./api')
|
||||
expect(PROJECT_ENDPOINT_PROTOCOL).toBe('https')
|
||||
})
|
||||
|
||||
it('should use http for default localhost', async () => {
|
||||
vi.stubEnv('SUPABASE_PUBLIC_URL', '')
|
||||
const { PROJECT_ENDPOINT_PROTOCOL } = await import('./api')
|
||||
expect(PROJECT_ENDPOINT_PROTOCOL).toBe('http')
|
||||
})
|
||||
})
|
||||
|
||||
describe('DEFAULT_PROJECT', () => {
|
||||
it('should have correct default values', async () => {
|
||||
vi.stubEnv('DEFAULT_PROJECT_NAME', '')
|
||||
const { DEFAULT_PROJECT } = await import('./api')
|
||||
|
||||
expect(DEFAULT_PROJECT).toEqual({
|
||||
id: 1,
|
||||
ref: 'default',
|
||||
name: 'Default Project',
|
||||
organization_id: 1,
|
||||
cloud_provider: 'localhost',
|
||||
status: 'ACTIVE_HEALTHY',
|
||||
region: 'local',
|
||||
inserted_at: '2021-08-02T06:40:40.646Z',
|
||||
})
|
||||
})
|
||||
|
||||
it('should use DEFAULT_PROJECT_NAME env var when set', async () => {
|
||||
vi.stubEnv('DEFAULT_PROJECT_NAME', 'My Custom Project')
|
||||
const { DEFAULT_PROJECT } = await import('./api')
|
||||
expect(DEFAULT_PROJECT.name).toBe('My Custom Project')
|
||||
})
|
||||
|
||||
it('should have static id and ref', async () => {
|
||||
const { DEFAULT_PROJECT } = await import('./api')
|
||||
expect(DEFAULT_PROJECT.id).toBe(1)
|
||||
expect(DEFAULT_PROJECT.ref).toBe('default')
|
||||
})
|
||||
|
||||
it('should have localhost cloud_provider', async () => {
|
||||
const { DEFAULT_PROJECT } = await import('./api')
|
||||
expect(DEFAULT_PROJECT.cloud_provider).toBe('localhost')
|
||||
})
|
||||
})
|
||||
})
|
||||
173
apps/studio/lib/error-reporting.test.ts
Normal file
173
apps/studio/lib/error-reporting.test.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
import * as Sentry from '@sentry/nextjs'
|
||||
import { ResponseError } from 'types'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { captureCriticalError } from './error-reporting'
|
||||
|
||||
vi.mock('@sentry/nextjs', () => ({
|
||||
captureMessage: vi.fn(),
|
||||
}))
|
||||
|
||||
describe('error-reporting', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('captureCriticalError', () => {
|
||||
it('should not capture error if message is empty', () => {
|
||||
const error = { message: '' }
|
||||
captureCriticalError(error, 'test context')
|
||||
|
||||
expect(Sentry.captureMessage).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should capture regular Error objects', () => {
|
||||
const error = new Error('Something went wrong')
|
||||
captureCriticalError(error, 'test action')
|
||||
|
||||
expect(Sentry.captureMessage).toHaveBeenCalledWith(
|
||||
'[CRITICAL][test action] Failed: Something went wrong'
|
||||
)
|
||||
})
|
||||
|
||||
it('should not capture whitelisted error messages', () => {
|
||||
const error = new Error('email must be an email')
|
||||
captureCriticalError(error, 'validation')
|
||||
|
||||
expect(Sentry.captureMessage).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not capture errors with partial whitelisted message', () => {
|
||||
const error = new Error('User error: A user with this email already exists in the system')
|
||||
captureCriticalError(error, 'sign up')
|
||||
|
||||
expect(Sentry.captureMessage).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should capture errors that are not whitelisted', () => {
|
||||
const error = new Error('Database connection failed')
|
||||
captureCriticalError(error, 'database')
|
||||
|
||||
expect(Sentry.captureMessage).toHaveBeenCalledWith(
|
||||
'[CRITICAL][database] Failed: Database connection failed'
|
||||
)
|
||||
})
|
||||
|
||||
it('should capture 5XX ResponseError', () => {
|
||||
const error = new ResponseError(
|
||||
'Internal server error',
|
||||
500,
|
||||
undefined,
|
||||
undefined,
|
||||
'/api/test'
|
||||
)
|
||||
captureCriticalError(error, 'api call')
|
||||
|
||||
expect(Sentry.captureMessage).toHaveBeenCalledWith(
|
||||
'[CRITICAL][api call] Failed: requestPathname /api/test w/ message: Internal server error'
|
||||
)
|
||||
})
|
||||
|
||||
it('should not capture 4XX ResponseError', () => {
|
||||
const error = new ResponseError('Not found', 404, undefined, undefined, '/api/test')
|
||||
captureCriticalError(error, 'api call')
|
||||
|
||||
expect(Sentry.captureMessage).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should capture ResponseError with 5XX status code', () => {
|
||||
const error = new ResponseError('Gateway timeout', 504, undefined, undefined, '/api/gateway')
|
||||
captureCriticalError(error, 'gateway request')
|
||||
|
||||
expect(Sentry.captureMessage).toHaveBeenCalledWith(
|
||||
'[CRITICAL][gateway request] Failed: requestPathname /api/gateway w/ message: Gateway timeout'
|
||||
)
|
||||
})
|
||||
|
||||
it('should capture ResponseError without code or requestPathname', () => {
|
||||
const error = new ResponseError('Unknown error')
|
||||
captureCriticalError(error, 'unknown')
|
||||
|
||||
expect(Sentry.captureMessage).toHaveBeenCalledWith(
|
||||
'[CRITICAL][unknown] Failed: Response Error (no code or requestPathname) w/ message: Unknown error'
|
||||
)
|
||||
})
|
||||
|
||||
it('should capture unknown error objects with message property', () => {
|
||||
const error = { message: 'Custom error object' }
|
||||
captureCriticalError(error, 'custom')
|
||||
|
||||
expect(Sentry.captureMessage).toHaveBeenCalledWith(
|
||||
'[CRITICAL][custom] Failed: Custom error object'
|
||||
)
|
||||
})
|
||||
|
||||
it('should not capture unknown error without message', () => {
|
||||
const error = { foo: 'bar' }
|
||||
captureCriticalError(error as any, 'no message')
|
||||
|
||||
expect(Sentry.captureMessage).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not capture whitelisted password validation error', () => {
|
||||
const error = new Error(
|
||||
'Password is known to be weak and easy to guess, please choose a different one'
|
||||
)
|
||||
captureCriticalError(error, 'password update')
|
||||
|
||||
expect(Sentry.captureMessage).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not capture whitelisted TOTP error', () => {
|
||||
const error = new Error('Invalid TOTP code entered')
|
||||
captureCriticalError(error, 'mfa verification')
|
||||
|
||||
expect(Sentry.captureMessage).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not capture whitelisted project name error', () => {
|
||||
const error = new Error('name should not contain a . string')
|
||||
captureCriticalError(error, 'create project')
|
||||
|
||||
expect(Sentry.captureMessage).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should capture non-whitelisted errors even if similar to whitelisted ones', () => {
|
||||
const error = new Error('email format is invalid')
|
||||
captureCriticalError(error, 'validation')
|
||||
|
||||
expect(Sentry.captureMessage).toHaveBeenCalledWith(
|
||||
'[CRITICAL][validation] Failed: email format is invalid'
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle Error objects with empty message', () => {
|
||||
const error = new Error('')
|
||||
captureCriticalError(error, 'empty error')
|
||||
|
||||
expect(Sentry.captureMessage).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle ResponseError at boundary of 4XX/5XX (499)', () => {
|
||||
const error = new ResponseError(
|
||||
'Client closed request',
|
||||
499,
|
||||
undefined,
|
||||
undefined,
|
||||
'/api/test'
|
||||
)
|
||||
captureCriticalError(error, 'request')
|
||||
|
||||
expect(Sentry.captureMessage).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle ResponseError at boundary of 4XX/5XX (500)', () => {
|
||||
const error = new ResponseError('Internal error', 500, undefined, undefined, '/api/test')
|
||||
captureCriticalError(error, 'request')
|
||||
|
||||
expect(Sentry.captureMessage).toHaveBeenCalledWith(
|
||||
'[CRITICAL][request] Failed: requestPathname /api/test w/ message: Internal error'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
231
apps/studio/lib/navigation.test.ts
Normal file
231
apps/studio/lib/navigation.test.ts
Normal file
@@ -0,0 +1,231 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { createNavigationHandler } from './navigation'
|
||||
|
||||
describe('createNavigationHandler', () => {
|
||||
let mockRouter: any
|
||||
let mockWindowOpen: any
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Mock router with push method
|
||||
mockRouter = {
|
||||
push: vi.fn(),
|
||||
}
|
||||
|
||||
// Mock window.open
|
||||
mockWindowOpen = vi.fn()
|
||||
global.window.open = mockWindowOpen
|
||||
})
|
||||
|
||||
describe('keyboard navigation', () => {
|
||||
it('should call router.push when Enter key is pressed without modifiers', () => {
|
||||
const handler = createNavigationHandler('/test-url', mockRouter)
|
||||
const event = {
|
||||
key: 'Enter',
|
||||
metaKey: false,
|
||||
ctrlKey: false,
|
||||
preventDefault: vi.fn(),
|
||||
} as unknown as React.KeyboardEvent
|
||||
|
||||
handler(event)
|
||||
|
||||
expect(event.preventDefault).toHaveBeenCalled()
|
||||
expect(mockRouter.push).toHaveBeenCalledWith('/test-url')
|
||||
expect(mockWindowOpen).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call router.push when Space key is pressed without modifiers', () => {
|
||||
const handler = createNavigationHandler('/test-url', mockRouter)
|
||||
const event = {
|
||||
key: ' ',
|
||||
metaKey: false,
|
||||
ctrlKey: false,
|
||||
preventDefault: vi.fn(),
|
||||
} as unknown as React.KeyboardEvent
|
||||
|
||||
handler(event)
|
||||
|
||||
expect(event.preventDefault).toHaveBeenCalled()
|
||||
expect(mockRouter.push).toHaveBeenCalledWith('/test-url')
|
||||
expect(mockWindowOpen).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should open new tab when Enter key is pressed with metaKey', () => {
|
||||
const handler = createNavigationHandler('/test-url', mockRouter)
|
||||
const event = {
|
||||
key: 'Enter',
|
||||
metaKey: true,
|
||||
ctrlKey: false,
|
||||
preventDefault: vi.fn(),
|
||||
} as unknown as React.KeyboardEvent
|
||||
|
||||
handler(event)
|
||||
|
||||
expect(event.preventDefault).toHaveBeenCalled()
|
||||
expect(mockWindowOpen).toHaveBeenCalledWith('/test-url', '_blank')
|
||||
expect(mockRouter.push).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should open new tab when Enter key is pressed with ctrlKey', () => {
|
||||
const handler = createNavigationHandler('/test-url', mockRouter)
|
||||
const event = {
|
||||
key: 'Enter',
|
||||
metaKey: false,
|
||||
ctrlKey: true,
|
||||
preventDefault: vi.fn(),
|
||||
} as unknown as React.KeyboardEvent
|
||||
|
||||
handler(event)
|
||||
|
||||
expect(event.preventDefault).toHaveBeenCalled()
|
||||
expect(mockWindowOpen).toHaveBeenCalledWith('/test-url', '_blank')
|
||||
expect(mockRouter.push).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should open new tab when Space key is pressed with metaKey', () => {
|
||||
const handler = createNavigationHandler('/test-url', mockRouter)
|
||||
const event = {
|
||||
key: ' ',
|
||||
metaKey: true,
|
||||
ctrlKey: false,
|
||||
preventDefault: vi.fn(),
|
||||
} as unknown as React.KeyboardEvent
|
||||
|
||||
handler(event)
|
||||
|
||||
expect(event.preventDefault).toHaveBeenCalled()
|
||||
expect(mockWindowOpen).toHaveBeenCalledWith('/test-url', '_blank')
|
||||
expect(mockRouter.push).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should do nothing when other keys are pressed', () => {
|
||||
const handler = createNavigationHandler('/test-url', mockRouter)
|
||||
const event = {
|
||||
key: 'a',
|
||||
metaKey: false,
|
||||
ctrlKey: false,
|
||||
preventDefault: vi.fn(),
|
||||
} as unknown as React.KeyboardEvent
|
||||
|
||||
handler(event)
|
||||
|
||||
expect(event.preventDefault).not.toHaveBeenCalled()
|
||||
expect(mockRouter.push).not.toHaveBeenCalled()
|
||||
expect(mockWindowOpen).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('mouse navigation', () => {
|
||||
it('should call router.push on regular left click', () => {
|
||||
const handler = createNavigationHandler('/test-url', mockRouter)
|
||||
const event = {
|
||||
button: 0,
|
||||
metaKey: false,
|
||||
ctrlKey: false,
|
||||
preventDefault: vi.fn(),
|
||||
} as unknown as React.MouseEvent
|
||||
|
||||
handler(event)
|
||||
|
||||
expect(mockRouter.push).toHaveBeenCalledWith('/test-url')
|
||||
expect(mockWindowOpen).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should open new tab on middle mouse button click', () => {
|
||||
const handler = createNavigationHandler('/test-url', mockRouter)
|
||||
const event = {
|
||||
button: 1, // Middle button
|
||||
metaKey: false,
|
||||
ctrlKey: false,
|
||||
preventDefault: vi.fn(),
|
||||
} as unknown as React.MouseEvent
|
||||
|
||||
handler(event)
|
||||
|
||||
expect(event.preventDefault).toHaveBeenCalled()
|
||||
expect(mockWindowOpen).toHaveBeenCalledWith('/test-url', '_blank')
|
||||
expect(mockRouter.push).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should open new tab on Cmd + left click', () => {
|
||||
const handler = createNavigationHandler('/test-url', mockRouter)
|
||||
const event = {
|
||||
button: 0,
|
||||
metaKey: true,
|
||||
ctrlKey: false,
|
||||
preventDefault: vi.fn(),
|
||||
} as unknown as React.MouseEvent
|
||||
|
||||
handler(event)
|
||||
|
||||
expect(event.preventDefault).toHaveBeenCalled()
|
||||
expect(mockWindowOpen).toHaveBeenCalledWith('/test-url', '_blank')
|
||||
expect(mockRouter.push).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should open new tab on Ctrl + left click', () => {
|
||||
const handler = createNavigationHandler('/test-url', mockRouter)
|
||||
const event = {
|
||||
button: 0,
|
||||
metaKey: false,
|
||||
ctrlKey: true,
|
||||
preventDefault: vi.fn(),
|
||||
} as unknown as React.MouseEvent
|
||||
|
||||
handler(event)
|
||||
|
||||
expect(event.preventDefault).toHaveBeenCalled()
|
||||
expect(mockWindowOpen).toHaveBeenCalledWith('/test-url', '_blank')
|
||||
expect(mockRouter.push).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle right click without navigation', () => {
|
||||
const handler = createNavigationHandler('/test-url', mockRouter)
|
||||
const event = {
|
||||
button: 2, // Right button
|
||||
metaKey: false,
|
||||
ctrlKey: false,
|
||||
preventDefault: vi.fn(),
|
||||
} as unknown as React.MouseEvent
|
||||
|
||||
handler(event)
|
||||
|
||||
// Right click should trigger router.push (falls through to default case)
|
||||
expect(mockRouter.push).toHaveBeenCalledWith('/test-url')
|
||||
})
|
||||
})
|
||||
|
||||
describe('URL handling', () => {
|
||||
it('should handle URLs with BASE_PATH correctly', () => {
|
||||
const handler = createNavigationHandler('/project/123/settings', mockRouter)
|
||||
const event = {
|
||||
button: 1, // Middle button to open in new tab
|
||||
metaKey: false,
|
||||
ctrlKey: false,
|
||||
preventDefault: vi.fn(),
|
||||
} as unknown as React.MouseEvent
|
||||
|
||||
handler(event)
|
||||
|
||||
// Should prepend BASE_PATH when opening new tab
|
||||
expect(mockWindowOpen).toHaveBeenCalledWith('/project/123/settings', '_blank')
|
||||
})
|
||||
|
||||
it('should pass URL directly to router.push without BASE_PATH', () => {
|
||||
const handler = createNavigationHandler('/project/123/settings', mockRouter)
|
||||
const event = {
|
||||
button: 0,
|
||||
metaKey: false,
|
||||
ctrlKey: false,
|
||||
preventDefault: vi.fn(),
|
||||
} as unknown as React.MouseEvent
|
||||
|
||||
handler(event)
|
||||
|
||||
// router.push should receive URL without BASE_PATH
|
||||
expect(mockRouter.push).toHaveBeenCalledWith('/project/123/settings')
|
||||
})
|
||||
})
|
||||
})
|
||||
124
apps/studio/lib/project-supabase-client.test.ts
Normal file
124
apps/studio/lib/project-supabase-client.test.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import * as supabaseJs from '@supabase/supabase-js'
|
||||
import * as apiKeysUtils from 'data/api-keys/temp-api-keys-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { createProjectSupabaseClient } from './project-supabase-client'
|
||||
|
||||
vi.mock('@supabase/supabase-js', () => ({
|
||||
createClient: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('data/api-keys/temp-api-keys-utils', () => ({
|
||||
getOrRefreshTemporaryApiKey: vi.fn(),
|
||||
}))
|
||||
|
||||
describe('project-supabase-client', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('createProjectSupabaseClient', () => {
|
||||
it('should create a Supabase client with temporary API key', async () => {
|
||||
const mockApiKey = 'test-api-key-123'
|
||||
const mockClient = { from: vi.fn() }
|
||||
const projectRef = 'test-project-ref'
|
||||
const clientEndpoint = 'https://test.supabase.co'
|
||||
|
||||
vi.mocked(apiKeysUtils.getOrRefreshTemporaryApiKey).mockResolvedValue({
|
||||
apiKey: mockApiKey,
|
||||
expiryTimeMs: Date.now() + 3600000,
|
||||
})
|
||||
vi.mocked(supabaseJs.createClient).mockReturnValue(mockClient as any)
|
||||
|
||||
const result = await createProjectSupabaseClient(projectRef, clientEndpoint)
|
||||
|
||||
expect(apiKeysUtils.getOrRefreshTemporaryApiKey).toHaveBeenCalledWith(projectRef)
|
||||
expect(supabaseJs.createClient).toHaveBeenCalledWith(clientEndpoint, mockApiKey, {
|
||||
auth: {
|
||||
persistSession: false,
|
||||
autoRefreshToken: false,
|
||||
detectSessionInUrl: false,
|
||||
storage: {
|
||||
getItem: expect.any(Function),
|
||||
setItem: expect.any(Function),
|
||||
removeItem: expect.any(Function),
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(result).toBe(mockClient)
|
||||
})
|
||||
|
||||
it('should configure storage to not persist session', async () => {
|
||||
const mockApiKey = 'test-api-key-456'
|
||||
const mockClient = { from: vi.fn() }
|
||||
|
||||
vi.mocked(apiKeysUtils.getOrRefreshTemporaryApiKey).mockResolvedValue({
|
||||
apiKey: mockApiKey,
|
||||
expiryTimeMs: Date.now() + 3600000,
|
||||
})
|
||||
vi.mocked(supabaseJs.createClient).mockReturnValue(mockClient as any)
|
||||
|
||||
await createProjectSupabaseClient('ref', 'https://example.com')
|
||||
|
||||
const config = vi.mocked(supabaseJs.createClient).mock.calls[0][2]
|
||||
if (!config?.auth?.storage) throw new Error('storage config is missing')
|
||||
const storage = config.auth.storage
|
||||
|
||||
// Test storage methods return expected values
|
||||
expect(storage.getItem('any-key')).toBeNull()
|
||||
expect(storage.setItem('key', 'value')).toBeUndefined()
|
||||
expect(storage.removeItem('key')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should throw error if API key retrieval fails', async () => {
|
||||
const error = new Error('Failed to get API key')
|
||||
vi.mocked(apiKeysUtils.getOrRefreshTemporaryApiKey).mockRejectedValue(error)
|
||||
|
||||
await expect(createProjectSupabaseClient('ref', 'https://example.com')).rejects.toThrow(
|
||||
'Failed to get API key'
|
||||
)
|
||||
|
||||
expect(supabaseJs.createClient).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should pass through different project refs and endpoints', async () => {
|
||||
const mockApiKey = 'api-key'
|
||||
const mockClient = { from: vi.fn() }
|
||||
|
||||
vi.mocked(apiKeysUtils.getOrRefreshTemporaryApiKey).mockResolvedValue({
|
||||
apiKey: mockApiKey,
|
||||
expiryTimeMs: Date.now() + 3600000,
|
||||
})
|
||||
vi.mocked(supabaseJs.createClient).mockReturnValue(mockClient as any)
|
||||
|
||||
await createProjectSupabaseClient('project-123', 'https://project123.supabase.co')
|
||||
|
||||
expect(apiKeysUtils.getOrRefreshTemporaryApiKey).toHaveBeenCalledWith('project-123')
|
||||
expect(supabaseJs.createClient).toHaveBeenCalledWith(
|
||||
'https://project123.supabase.co',
|
||||
mockApiKey,
|
||||
expect.any(Object)
|
||||
)
|
||||
})
|
||||
|
||||
it('should disable session persistence options', async () => {
|
||||
const mockApiKey = 'api-key'
|
||||
const mockClient = { from: vi.fn() }
|
||||
|
||||
vi.mocked(apiKeysUtils.getOrRefreshTemporaryApiKey).mockResolvedValue({
|
||||
apiKey: mockApiKey,
|
||||
expiryTimeMs: Date.now() + 3600000,
|
||||
})
|
||||
vi.mocked(supabaseJs.createClient).mockReturnValue(mockClient as any)
|
||||
|
||||
await createProjectSupabaseClient('ref', 'https://example.com')
|
||||
|
||||
const config = vi.mocked(supabaseJs.createClient).mock.calls[0][2]
|
||||
if (!config?.auth) throw new Error('auth config is missing')
|
||||
|
||||
expect(config.auth.persistSession).toBe(false)
|
||||
expect(config.auth.autoRefreshToken).toBe(false)
|
||||
expect(config.auth.detectSessionInUrl).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
31
apps/studio/lib/void.test.ts
Normal file
31
apps/studio/lib/void.test.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { EMPTY_ARR, EMPTY_OBJ, noop } from './void'
|
||||
|
||||
describe('void utilities', () => {
|
||||
describe('noop', () => {
|
||||
it('should return undefined', () => {
|
||||
expect(noop()).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('EMPTY_OBJ', () => {
|
||||
it('should always return the same reference', () => {
|
||||
expect(EMPTY_OBJ).toBe(EMPTY_OBJ)
|
||||
})
|
||||
|
||||
it('should be an empty object', () => {
|
||||
expect(Object.keys(EMPTY_OBJ)).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('EMPTY_ARR', () => {
|
||||
it('should always return the same reference', () => {
|
||||
expect(EMPTY_ARR).toBe(EMPTY_ARR)
|
||||
})
|
||||
|
||||
it('should be an empty array', () => {
|
||||
expect(EMPTY_ARR).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,8 +1,8 @@
|
||||
import { resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { configDefaults, defineConfig } from 'vitest/config'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import tsconfigPaths from 'vite-tsconfig-paths'
|
||||
import { configDefaults, defineConfig } from 'vitest/config'
|
||||
|
||||
// Some tools like Vitest VSCode extensions, have trouble with resolving relative paths,
|
||||
// as they use the directory of the test file as `cwd`, which makes them believe that
|
||||
@@ -30,10 +30,15 @@ export default defineConfig({
|
||||
resolve(dirname, './tests/setup/radix.js'),
|
||||
],
|
||||
// Don't look for tests in the nextjs output directory
|
||||
exclude: [...configDefaults.exclude, `.next/*`],
|
||||
exclude: [
|
||||
...configDefaults.exclude,
|
||||
`.next/*`,
|
||||
'tests/features/logs/logs-query.test.tsx',
|
||||
'tests/features/reports/storage-report.test.tsx',
|
||||
],
|
||||
reporters: [['default']],
|
||||
coverage: {
|
||||
reporter: ['lcov'],
|
||||
reporter: ['text', 'text-summary', 'lcov'],
|
||||
exclude: [
|
||||
'**/*.test.ts',
|
||||
'**/*.test.tsx',
|
||||
|
||||
Reference in New Issue
Block a user