Add coveralls integration (#35424)
* update gh action, update vitest config * debug * debug cov * idk try something different * test2 * test3 * add base path * rm debug * add apiAuthenticate tests * supabaseClient tests * apiWrappers tests * add apiHelpers tests * add configcat tests * add formatSql tests * add github tests * add cloudprovider utils tests * add helpers tests * fix typeerr * add missing readonly err * fix typeerrrs * fix type errors in apiWrapper tests * fix apiHelpers test * add packages/ui tests * add coveralls flags * try coveralls parallel config * fix coveralls parallel config --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
29
.github/workflows/studio-unit-tests.yml
vendored
29
.github/workflows/studio-unit-tests.yml
vendored
@@ -24,9 +24,12 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
check:
|
||||
test:
|
||||
# Uses larger hosted runner as it significantly decreases build times
|
||||
runs-on: [larger-runner-4cpu]
|
||||
strategy:
|
||||
matrix:
|
||||
test_number: [1]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -50,5 +53,25 @@ jobs:
|
||||
env:
|
||||
# Default is 2 GB, increase to have less frequent OOM errors
|
||||
NODE_OPTIONS: '--max_old_space_size=3072'
|
||||
run: pnpm run test:studio
|
||||
working-directory: ./
|
||||
run: pnpm run test:ci
|
||||
working-directory: ./apps/studio
|
||||
|
||||
- name: Upload coverage results to Coveralls
|
||||
uses: coverallsapp/github-action@master
|
||||
with:
|
||||
parallel: true
|
||||
flag-name: studio-tests
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
path-to-lcov: ./apps/studio/coverage/lcov.info
|
||||
base-path: './apps/studio'
|
||||
|
||||
finish:
|
||||
needs: test
|
||||
if: ${{ always() }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Coveralls Finished
|
||||
uses: coverallsapp/github-action@master
|
||||
with:
|
||||
parallel-finished: true
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
28
.github/workflows/ui-tests.yml
vendored
28
.github/workflows/ui-tests.yml
vendored
@@ -15,8 +15,11 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
test_number: [1]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -39,4 +42,25 @@ jobs:
|
||||
run: pnpm i
|
||||
|
||||
- name: Run tests
|
||||
run: pnpm run test:ui
|
||||
run: pnpm run test:ci
|
||||
working-directory: ./packages/ui
|
||||
|
||||
- name: Upload coverage results to Coveralls
|
||||
uses: coverallsapp/github-action@master
|
||||
with:
|
||||
parallel: true
|
||||
flag-name: ui-tests
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
path-to-lcov: ./packages/ui/coverage/lcov.info
|
||||
base-path: './packages/ui'
|
||||
|
||||
finish:
|
||||
needs: test
|
||||
if: ${{ always() }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Coveralls Finished
|
||||
uses: coverallsapp/github-action@master
|
||||
with:
|
||||
parallel-finished: true
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
318
apps/studio/lib/api/apiAuthenticate.test.ts
Normal file
318
apps/studio/lib/api/apiAuthenticate.test.ts
Normal file
@@ -0,0 +1,318 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { apiAuthenticate } from './apiAuthenticate'
|
||||
import { readOnly } from './supabaseClient'
|
||||
|
||||
const mocks = vi.hoisted(() => {
|
||||
return {
|
||||
getAuthUser: vi.fn().mockResolvedValue({
|
||||
user: {
|
||||
id: 'test-gotrue-id',
|
||||
email: 'test@example.com',
|
||||
},
|
||||
error: null,
|
||||
}),
|
||||
getIdentity: vi.fn().mockReturnValue({
|
||||
identity: null,
|
||||
error: null,
|
||||
}),
|
||||
getAuth0Id: vi.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('./supabaseClient', () => ({
|
||||
readOnly: {
|
||||
from: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('lib/gotrue', () => ({
|
||||
getAuthUser: mocks.getAuthUser,
|
||||
getIdentity: mocks.getIdentity,
|
||||
getAuth0Id: mocks.getAuth0Id,
|
||||
}))
|
||||
|
||||
describe('apiAuthenticate', () => {
|
||||
const mockReq = {
|
||||
headers: {
|
||||
authorization: 'Bearer test-token',
|
||||
},
|
||||
query: {},
|
||||
} as any
|
||||
|
||||
const mockRes = {} as any
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mocks.getAuthUser.mockResolvedValue({
|
||||
user: {
|
||||
id: 'test-gotrue-id',
|
||||
email: 'test@example.com',
|
||||
},
|
||||
error: null,
|
||||
})
|
||||
mocks.getIdentity.mockReturnValue({
|
||||
identity: null,
|
||||
error: null,
|
||||
})
|
||||
})
|
||||
|
||||
it('should return error when request is not available', async () => {
|
||||
const result = await apiAuthenticate(null as any, mockRes)
|
||||
expect(result).toStrictEqual({ error: new Error('Request is not available') })
|
||||
})
|
||||
|
||||
it('should return error when response is not available', async () => {
|
||||
const result = await apiAuthenticate(mockReq, null as any)
|
||||
expect(result).toStrictEqual({ error: new Error('Response is not available') })
|
||||
})
|
||||
|
||||
it('should return error when authorization token is missing', async () => {
|
||||
const reqWithoutToken = { ...mockReq, headers: {} }
|
||||
const result = await apiAuthenticate(reqWithoutToken, mockRes)
|
||||
expect(result).toStrictEqual({ error: { name: 'Error', message: 'missing access token' } })
|
||||
})
|
||||
|
||||
it('should return error when auth user fetch fails', async () => {
|
||||
mocks.getAuthUser.mockResolvedValue({
|
||||
user: null,
|
||||
error: new Error('Auth failed'),
|
||||
})
|
||||
|
||||
const result = await apiAuthenticate(mockReq, mockRes)
|
||||
expect(result).toStrictEqual({ error: { name: 'Error', message: 'Auth failed' } })
|
||||
})
|
||||
|
||||
it('should handle identity error', async () => {
|
||||
mocks.getIdentity.mockReturnValue({
|
||||
identity: null,
|
||||
error: new Error('Identity error'),
|
||||
})
|
||||
|
||||
const result = await apiAuthenticate(mockReq, mockRes)
|
||||
expect(result).toStrictEqual({ error: { name: 'Error', message: 'Identity error' } })
|
||||
})
|
||||
|
||||
it('should set auth0 id when identity provider is present', async () => {
|
||||
mocks.getIdentity.mockReturnValue({
|
||||
identity: {
|
||||
provider: 'auth0',
|
||||
id: 'auth0-id',
|
||||
},
|
||||
error: null,
|
||||
})
|
||||
mocks.getAuth0Id.mockReturnValue('auth0-user-id')
|
||||
|
||||
// Mock user query
|
||||
vi.mocked(readOnly.from).mockReturnValue({
|
||||
select: vi.fn().mockReturnThis(),
|
||||
eq: vi.fn().mockReturnThis(),
|
||||
single: vi
|
||||
.fn()
|
||||
.mockResolvedValue({ data: { id: 'test-user-id', primary_email: 'test@example.com' } }),
|
||||
} as any)
|
||||
|
||||
const result = await apiAuthenticate(mockReq, mockRes)
|
||||
expect(result).toStrictEqual({
|
||||
id: 'test-user-id',
|
||||
primary_email: 'test@example.com',
|
||||
})
|
||||
expect(mocks.getAuth0Id).toHaveBeenCalledWith('auth0', 'auth0-id')
|
||||
})
|
||||
|
||||
it('should return user when user_id_supabase is present', async () => {
|
||||
// Mock identity to return auth0 provider
|
||||
mocks.getIdentity.mockReturnValue({
|
||||
identity: {
|
||||
provider: 'auth0',
|
||||
id: 'auth0-id',
|
||||
},
|
||||
error: null,
|
||||
})
|
||||
mocks.getAuth0Id.mockReturnValue('auth0-user-id')
|
||||
|
||||
// Mock user query to return a user with auth0_id
|
||||
vi.mocked(readOnly.from).mockReturnValue({
|
||||
select: vi.fn().mockReturnThis(),
|
||||
eq: vi.fn().mockReturnThis(),
|
||||
single: vi.fn().mockResolvedValue({
|
||||
data: {
|
||||
id: 'supabase-user-id',
|
||||
auth0_id: 'auth0-user-id',
|
||||
primary_email: 'test@example.com',
|
||||
},
|
||||
}),
|
||||
} as any)
|
||||
|
||||
const result = await apiAuthenticate(mockReq, mockRes)
|
||||
expect(result).toStrictEqual({
|
||||
id: 'supabase-user-id',
|
||||
auth0_id: 'auth0-user-id',
|
||||
primary_email: 'test@example.com',
|
||||
})
|
||||
})
|
||||
|
||||
it('should return error when user does not exist', async () => {
|
||||
vi.mocked(readOnly.from).mockReturnValue({
|
||||
select: vi.fn().mockReturnThis(),
|
||||
eq: vi.fn().mockReturnThis(),
|
||||
single: vi.fn().mockResolvedValue({ data: null }),
|
||||
} as any)
|
||||
|
||||
const result = await apiAuthenticate(mockReq, mockRes)
|
||||
expect(result).toStrictEqual({ error: new Error('The user does not exist') })
|
||||
})
|
||||
|
||||
it('should check organization permissions when orgSlug is provided', async () => {
|
||||
const reqWithOrg = {
|
||||
...mockReq,
|
||||
query: { slug: 'test-org' },
|
||||
}
|
||||
|
||||
// Mock user query
|
||||
vi.mocked(readOnly.from).mockReturnValueOnce({
|
||||
select: vi.fn().mockReturnThis(),
|
||||
eq: vi.fn().mockReturnThis(),
|
||||
single: vi
|
||||
.fn()
|
||||
.mockResolvedValue({ data: { id: 'test-user-id', primary_email: 'test@example.com' } }),
|
||||
} as any)
|
||||
|
||||
// Mock organization query
|
||||
vi.mocked(readOnly.from).mockReturnValueOnce({
|
||||
select: vi.fn().mockReturnThis(),
|
||||
match: vi.fn().mockReturnThis(),
|
||||
single: vi.fn().mockResolvedValue({ data: { id: 'org-id' } }),
|
||||
} as any)
|
||||
|
||||
// Mock member check
|
||||
vi.mocked(readOnly.from).mockReturnValueOnce({
|
||||
select: vi.fn().mockReturnThis(),
|
||||
match: vi.fn().mockReturnThis(),
|
||||
single: vi.fn().mockResolvedValue({ data: { id: 'member-id' }, status: 200 }),
|
||||
} as any)
|
||||
|
||||
const result = await apiAuthenticate(reqWithOrg, mockRes)
|
||||
expect(result).toStrictEqual({
|
||||
id: 'test-user-id',
|
||||
primary_email: 'test@example.com',
|
||||
})
|
||||
})
|
||||
|
||||
it('should return error when user lacks organization permissions', async () => {
|
||||
const reqWithOrg = {
|
||||
...mockReq,
|
||||
query: { slug: 'test-org' },
|
||||
}
|
||||
|
||||
// Mock user query
|
||||
vi.mocked(readOnly.from).mockReturnValueOnce({
|
||||
select: vi.fn().mockReturnThis(),
|
||||
eq: vi.fn().mockReturnThis(),
|
||||
single: vi
|
||||
.fn()
|
||||
.mockResolvedValue({ data: { id: 'test-user-id', primary_email: 'test@example.com' } }),
|
||||
} as any)
|
||||
|
||||
// Mock organization query
|
||||
vi.mocked(readOnly.from).mockReturnValueOnce({
|
||||
select: vi.fn().mockReturnThis(),
|
||||
match: vi.fn().mockReturnThis(),
|
||||
single: vi.fn().mockResolvedValue({ data: { id: 'org-id' } }),
|
||||
} as any)
|
||||
|
||||
// Mock member check failure
|
||||
vi.mocked(readOnly.from).mockReturnValueOnce({
|
||||
select: vi.fn().mockReturnThis(),
|
||||
match: vi.fn().mockReturnThis(),
|
||||
single: vi.fn().mockRejectedValue(new Error('Permission denied')),
|
||||
} as any)
|
||||
|
||||
const result = await apiAuthenticate(reqWithOrg, mockRes)
|
||||
expect(result).toStrictEqual({
|
||||
error: { name: 'Error', message: 'The user does not have permission' },
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle unknown errors gracefully', async () => {
|
||||
mocks.getAuthUser.mockRejectedValue(new Error('Unexpected error'))
|
||||
|
||||
const result = await apiAuthenticate(mockReq, mockRes)
|
||||
expect(result).toStrictEqual({ error: { name: 'Error', message: 'Unexpected error' } })
|
||||
})
|
||||
|
||||
it('should get organization from project reference', async () => {
|
||||
const reqWithProject = {
|
||||
...mockReq,
|
||||
query: { ref: 'test-project' },
|
||||
}
|
||||
|
||||
// Mock user query
|
||||
vi.mocked(readOnly.from).mockReturnValueOnce({
|
||||
select: vi.fn().mockReturnThis(),
|
||||
eq: vi.fn().mockReturnThis(),
|
||||
single: vi
|
||||
.fn()
|
||||
.mockResolvedValue({ data: { id: 'test-user-id', primary_email: 'test@example.com' } }),
|
||||
} as any)
|
||||
|
||||
// Mock project query
|
||||
vi.mocked(readOnly.from).mockReturnValueOnce({
|
||||
select: vi.fn().mockReturnThis(),
|
||||
match: vi.fn().mockReturnThis(),
|
||||
single: vi.fn().mockResolvedValue({ data: { organization_id: 'org-id' } }),
|
||||
} as any)
|
||||
|
||||
// Mock member check
|
||||
vi.mocked(readOnly.from).mockReturnValueOnce({
|
||||
select: vi.fn().mockReturnThis(),
|
||||
match: vi.fn().mockReturnThis(),
|
||||
single: vi.fn().mockResolvedValue({ data: { id: 'member-id' }, status: 200 }),
|
||||
} as any)
|
||||
|
||||
const result = await apiAuthenticate(reqWithProject, mockRes)
|
||||
expect(result).toStrictEqual({
|
||||
id: 'test-user-id',
|
||||
primary_email: 'test@example.com',
|
||||
})
|
||||
})
|
||||
|
||||
it('should use organization_id from projectRef in member check', async () => {
|
||||
const reqWithProject = {
|
||||
...mockReq,
|
||||
query: { ref: 'project-xyz' },
|
||||
}
|
||||
|
||||
// Mock user query
|
||||
vi.mocked(readOnly.from).mockReturnValueOnce({
|
||||
select: vi.fn().mockReturnThis(),
|
||||
eq: vi.fn().mockReturnThis(),
|
||||
single: vi
|
||||
.fn()
|
||||
.mockResolvedValue({ data: { id: 'user-123', primary_email: 'user@example.com' } }),
|
||||
} as any)
|
||||
|
||||
// Mock project query to return a specific organization_id
|
||||
vi.mocked(readOnly.from).mockReturnValueOnce({
|
||||
select: vi.fn().mockReturnThis(),
|
||||
match: vi.fn().mockReturnThis(),
|
||||
single: vi.fn().mockResolvedValue({ data: { organization_id: 'org-abc' } }),
|
||||
} as any)
|
||||
|
||||
// Spy on the match function for the member check
|
||||
const matchSpy = vi.fn().mockReturnThis()
|
||||
vi.mocked(readOnly.from).mockReturnValueOnce({
|
||||
select: vi.fn().mockReturnThis(),
|
||||
match: matchSpy,
|
||||
single: vi.fn().mockResolvedValue({ data: { id: 'member-1' }, status: 200 }),
|
||||
} as any)
|
||||
|
||||
const result = await apiAuthenticate(reqWithProject, mockRes)
|
||||
expect(result).toStrictEqual({
|
||||
id: 'user-123',
|
||||
primary_email: 'user@example.com',
|
||||
})
|
||||
// Ensure the member check used the org id from the project lookup
|
||||
expect(matchSpy).toHaveBeenCalledWith({ organization_id: 'org-abc', user_id: 'user-123' })
|
||||
})
|
||||
})
|
||||
@@ -19,18 +19,18 @@ export async function apiAuthenticate(
|
||||
res: NextApiResponse
|
||||
): Promise<SupaResponse<User>> {
|
||||
if (!req) {
|
||||
return { error: new Error('Request is not available') } as unknown as SupaResponse<User>
|
||||
return { error: new Error('Request is not available') }
|
||||
}
|
||||
|
||||
if (!res) {
|
||||
return { error: new Error('Response is not available') } as unknown as SupaResponse<User>
|
||||
return { error: new Error('Response is not available') }
|
||||
}
|
||||
|
||||
const { slug: orgSlug, ref: projectRef } = req.query
|
||||
try {
|
||||
const user = await fetchUser(req, res)
|
||||
if (!user) {
|
||||
return { error: new Error('The user does not exist') } as unknown as SupaResponse<User>
|
||||
return { error: new Error('The user does not exist') }
|
||||
}
|
||||
|
||||
if (orgSlug || projectRef) await checkMemberPermission(req, user)
|
||||
@@ -38,7 +38,7 @@ export async function apiAuthenticate(
|
||||
return user
|
||||
} catch (error: any) {
|
||||
console.error('Error at apiAuthenticate', error)
|
||||
return { error: { message: error.message ?? 'unknown' } } as unknown as SupaResponse<User>
|
||||
return { error: { name: 'Error', message: error.message ?? 'unknown' } }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
132
apps/studio/lib/api/apiHelpers.test.ts
Normal file
132
apps/studio/lib/api/apiHelpers.test.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { constructHeaders, toSnakeCase } from './apiHelpers'
|
||||
|
||||
vi.mock('lib/constants', () => ({
|
||||
IS_PLATFORM: false,
|
||||
}))
|
||||
|
||||
describe('apiHelpers', () => {
|
||||
describe('constructHeaders', () => {
|
||||
beforeEach(() => {
|
||||
process.env.READ_ONLY_API_KEY = 'test-readonly-key'
|
||||
process.env.SUPABASE_SERVICE_KEY = 'test-service-key'
|
||||
})
|
||||
|
||||
it('should return default headers when no headers are provided', () => {
|
||||
const result = constructHeaders(null as any)
|
||||
expect(result).toEqual({
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
})
|
||||
})
|
||||
|
||||
it('should clean and include only allowed headers', () => {
|
||||
const inputHeaders = {
|
||||
Accept: 'application/json',
|
||||
Authorization: 'Bearer token',
|
||||
'Content-Type': 'application/json',
|
||||
'x-connection-encrypted': 'true',
|
||||
cookie: 'test-cookie',
|
||||
'User-Agent': 'test-agent',
|
||||
Referer: 'test-referer',
|
||||
}
|
||||
|
||||
const result = constructHeaders(inputHeaders)
|
||||
expect(result).toEqual({
|
||||
Accept: 'application/json',
|
||||
Authorization: 'Bearer token',
|
||||
'Content-Type': 'application/json',
|
||||
'x-connection-encrypted': 'true',
|
||||
cookie: 'test-cookie',
|
||||
apiKey: 'test-service-key',
|
||||
})
|
||||
})
|
||||
|
||||
it('should remove undefined values from headers', () => {
|
||||
const inputHeaders = {
|
||||
Accept: undefined,
|
||||
Authorization: 'Bearer token',
|
||||
'Content-Type': 'application/json',
|
||||
cookie: undefined,
|
||||
}
|
||||
|
||||
const result = constructHeaders(inputHeaders)
|
||||
expect(result).toEqual({
|
||||
Authorization: 'Bearer token',
|
||||
'Content-Type': 'application/json',
|
||||
apiKey: 'test-service-key',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('toSnakeCase', () => {
|
||||
it('should return null for null input', () => {
|
||||
expect(toSnakeCase(null)).toBeNull()
|
||||
})
|
||||
|
||||
it('should convert object keys to snake case', () => {
|
||||
const input = {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
contactInfo: {
|
||||
emailAddress: 'john@example.com',
|
||||
phoneNumber: '1234567890',
|
||||
},
|
||||
}
|
||||
|
||||
const expected = {
|
||||
first_name: 'John',
|
||||
last_name: 'Doe',
|
||||
contact_info: {
|
||||
email_address: 'john@example.com',
|
||||
phone_number: '1234567890',
|
||||
},
|
||||
}
|
||||
|
||||
expect(toSnakeCase(input)).toEqual(expected)
|
||||
})
|
||||
|
||||
it('should handle arrays of objects', () => {
|
||||
const input = [
|
||||
{ firstName: 'John', lastName: 'Doe' },
|
||||
{ firstName: 'Jane', lastName: 'Smith' },
|
||||
]
|
||||
|
||||
const expected = [
|
||||
{ first_name: 'John', last_name: 'Doe' },
|
||||
{ first_name: 'Jane', last_name: 'Smith' },
|
||||
]
|
||||
|
||||
expect(toSnakeCase(input)).toEqual(expected)
|
||||
})
|
||||
|
||||
it('should handle arrays of primitive values', () => {
|
||||
const input = [1, 'test', true]
|
||||
expect(toSnakeCase(input)).toEqual([1, 'test', true])
|
||||
})
|
||||
|
||||
it('should handle nested arrays', () => {
|
||||
const input = {
|
||||
users: [
|
||||
{ firstName: 'John', contactInfo: { emailAddress: 'john@example.com' } },
|
||||
{ firstName: 'Jane', contactInfo: { emailAddress: 'jane@example.com' } },
|
||||
],
|
||||
}
|
||||
|
||||
const expected = {
|
||||
users: [
|
||||
{ first_name: 'John', contact_info: { email_address: 'john@example.com' } },
|
||||
{ first_name: 'Jane', contact_info: { email_address: 'jane@example.com' } },
|
||||
],
|
||||
}
|
||||
|
||||
expect(toSnakeCase(input)).toEqual(expected)
|
||||
})
|
||||
|
||||
it('should handle primitive values', () => {
|
||||
expect(toSnakeCase('test')).toBe('test')
|
||||
expect(toSnakeCase(123)).toBe(123)
|
||||
expect(toSnakeCase(true)).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
41
apps/studio/lib/api/apiWrappers.test.ts
Normal file
41
apps/studio/lib/api/apiWrappers.test.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import apiWrapper from './apiWrapper'
|
||||
import { apiAuthenticate } from './apiAuthenticate'
|
||||
|
||||
vi.mock('lib/constants', () => ({
|
||||
IS_PLATFORM: true,
|
||||
API_URL: 'https://api.example.com',
|
||||
}))
|
||||
|
||||
vi.mock('./apiAuthenticate', () => ({
|
||||
apiAuthenticate: vi.fn(),
|
||||
}))
|
||||
|
||||
describe('apiWrapper', () => {
|
||||
const mockReq = {} as any
|
||||
const mockRes = {
|
||||
status: vi.fn().mockReturnThis(),
|
||||
json: vi.fn().mockReturnThis(),
|
||||
} as any
|
||||
const mockHandler = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should call handler directly when withAuth is false', async () => {
|
||||
await apiWrapper(mockReq, mockRes, mockHandler, { withAuth: false })
|
||||
expect(mockHandler).toHaveBeenCalledWith(mockReq, mockRes)
|
||||
expect(apiAuthenticate).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should attach user to request and call handler when authentication succeeds', async () => {
|
||||
const mockUser = { id: '123', email: 'test@example.com' } as any as any
|
||||
vi.mocked(apiAuthenticate).mockResolvedValue(mockUser)
|
||||
|
||||
await apiWrapper(mockReq, mockRes, mockHandler, { withAuth: true })
|
||||
|
||||
expect(mockReq.user).toEqual(mockUser)
|
||||
expect(mockHandler).toHaveBeenCalledWith(mockReq, mockRes)
|
||||
})
|
||||
})
|
||||
49
apps/studio/lib/api/supabaseClient.test.ts
Normal file
49
apps/studio/lib/api/supabaseClient.test.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { readOnly } from './supabaseClient'
|
||||
|
||||
vi.mock('lib/constants', () => ({
|
||||
IS_PLATFORM: true,
|
||||
}))
|
||||
|
||||
const readOnlyErrMessage = 'Read only error'
|
||||
|
||||
vi.mock('@supabase/supabase-js', () => ({
|
||||
createClient: vi.fn(() => ({
|
||||
from: vi.fn(() => ({
|
||||
insert: () => {
|
||||
throw readOnlyErrMessage
|
||||
},
|
||||
delete: () => {
|
||||
throw readOnlyErrMessage
|
||||
},
|
||||
update: () => {
|
||||
throw readOnlyErrMessage
|
||||
},
|
||||
})),
|
||||
rpc: () => {
|
||||
throw readOnlyErrMessage
|
||||
},
|
||||
})),
|
||||
}))
|
||||
|
||||
describe('supabaseClient', () => {
|
||||
it('should be defined', () => {
|
||||
expect(readOnly).toBeDefined()
|
||||
})
|
||||
|
||||
it('should throw on inserts', () => {
|
||||
expect(() => readOnly.from('').insert({})).toThrowError()
|
||||
})
|
||||
|
||||
it('should throw on deletes', () => {
|
||||
expect(() => readOnly.from('').delete({})).toThrowError()
|
||||
})
|
||||
|
||||
it('should throw on updates', () => {
|
||||
expect(() => readOnly.from('').update({})).toThrowError()
|
||||
})
|
||||
|
||||
it('should throw on rpc', () => {
|
||||
expect(() => readOnly.rpc({})).toThrowError()
|
||||
})
|
||||
})
|
||||
22
apps/studio/lib/cloudprovider-utils.test.ts
Normal file
22
apps/studio/lib/cloudprovider-utils.test.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { getCloudProviderArchitecture } from './cloudprovider-utils'
|
||||
import { PROVIDERS } from './constants'
|
||||
describe('getCloudProviderArchitecture', () => {
|
||||
it('should return the correct architecture', () => {
|
||||
const result = getCloudProviderArchitecture(PROVIDERS.AWS.id)
|
||||
|
||||
expect(result).toBe('ARM')
|
||||
})
|
||||
|
||||
it('should return the correct architecture for fly', () => {
|
||||
const result = getCloudProviderArchitecture(PROVIDERS.FLY.id)
|
||||
|
||||
expect(result).toBe('x86 64-bit')
|
||||
})
|
||||
|
||||
it('should return an empty string if the cloud provider is not supported', () => {
|
||||
const result = getCloudProviderArchitecture('unknown')
|
||||
|
||||
expect(result).toBe('')
|
||||
})
|
||||
})
|
||||
39
apps/studio/lib/configcat.test.ts
Normal file
39
apps/studio/lib/configcat.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import * as configcat from 'configcat-js'
|
||||
import { getFlags } from './configcat'
|
||||
|
||||
vi.mock('configcat-js', () => ({
|
||||
getClient: vi.fn(),
|
||||
PollingMode: {
|
||||
AutoPoll: 'AutoPoll',
|
||||
},
|
||||
User: vi.fn(),
|
||||
}))
|
||||
|
||||
describe('configcat', () => {
|
||||
const mockClient = {
|
||||
getAllValuesAsync: vi.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
;(configcat.getClient as any).mockReturnValue(mockClient)
|
||||
})
|
||||
|
||||
it('should return empty array when no email is provided', async () => {
|
||||
const result = await getFlags()
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
it('should call getAllValuesAsync with user when email is provided', async () => {
|
||||
const email = 'test@example.com'
|
||||
const mockValues = { flag1: true, flag2: false }
|
||||
mockClient.getAllValuesAsync.mockResolvedValue(mockValues)
|
||||
|
||||
const result = await getFlags(email)
|
||||
|
||||
expect(configcat.User).toHaveBeenCalledWith(email)
|
||||
expect(mockClient.getAllValuesAsync).toHaveBeenCalled()
|
||||
expect(result).toEqual(mockValues)
|
||||
})
|
||||
})
|
||||
19
apps/studio/lib/formatSql.test.ts
Normal file
19
apps/studio/lib/formatSql.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { formatSql } from './formatSql'
|
||||
import { describe, it, expect } from 'vitest'
|
||||
|
||||
describe('formatSql', () => {
|
||||
it('should format SQL', () => {
|
||||
const result = formatSql('SELECT * FROM users')
|
||||
|
||||
expect(result).toBe(`select
|
||||
*
|
||||
from
|
||||
users`)
|
||||
})
|
||||
|
||||
it('should return the original argument if it is not valid, not throw', () => {
|
||||
const result = formatSql('123')
|
||||
|
||||
expect(result).toBe('123')
|
||||
})
|
||||
})
|
||||
21
apps/studio/lib/github.test.ts
Normal file
21
apps/studio/lib/github.test.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { openInstallGitHubIntegrationWindow, getGitHubProfileImgUrl } from './github'
|
||||
|
||||
// mock window.open
|
||||
vi.stubGlobal('open', vi.fn())
|
||||
|
||||
describe('openInstallGitHubIntegrationWindow', () => {
|
||||
it('should open the install window', () => {
|
||||
openInstallGitHubIntegrationWindow('install')
|
||||
|
||||
expect(window.open).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('getGitHubProfileImgUrl', () => {
|
||||
it('should return the correct URL', () => {
|
||||
const result = getGitHubProfileImgUrl('test')
|
||||
|
||||
expect(result).toBe('https://github.com/test.png?size=96')
|
||||
})
|
||||
})
|
||||
346
apps/studio/lib/helpers.test.ts
Normal file
346
apps/studio/lib/helpers.test.ts
Normal file
@@ -0,0 +1,346 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import {
|
||||
tryParseJson,
|
||||
minifyJSON,
|
||||
prettifyJSON,
|
||||
removeJSONTrailingComma,
|
||||
timeout,
|
||||
getURL,
|
||||
makeRandomString,
|
||||
pluckObjectFields,
|
||||
tryParseInt,
|
||||
propsAreEqual,
|
||||
formatBytes,
|
||||
snakeToCamel,
|
||||
copyToClipboard,
|
||||
detectBrowser,
|
||||
detectOS,
|
||||
pluralize,
|
||||
isValidHttpUrl,
|
||||
removeCommentsFromSql,
|
||||
getSemanticVersion,
|
||||
getDatabaseMajorVersion,
|
||||
getDistanceLatLonKM,
|
||||
formatCurrency,
|
||||
} from './helpers'
|
||||
|
||||
describe('tryParseJson', () => {
|
||||
it('should return the parsed JSON', () => {
|
||||
const result = tryParseJson('{"test": "test"}')
|
||||
|
||||
expect(result).toEqual({ test: 'test' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('minifyJSON', () => {
|
||||
it('should return the minified JSON', () => {
|
||||
const result = minifyJSON('{"test": "test"}')
|
||||
|
||||
expect(result).toEqual(`{"test":"test"}`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('prettifyJSON', () => {
|
||||
it('should return the prettified JSON', () => {
|
||||
const result = prettifyJSON('{"test": "test"}')
|
||||
|
||||
expect(result).toEqual(`{
|
||||
"test": "test"
|
||||
}`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('removeJSONTrailingComma', () => {
|
||||
it('should return the JSON without a trailing comma', () => {
|
||||
const result = removeJSONTrailingComma('{"test":"test",}')
|
||||
|
||||
expect(result).toEqual('{"test":"test"}')
|
||||
})
|
||||
})
|
||||
|
||||
describe('timeout', () => {
|
||||
it('resolves after given ms', async () => {
|
||||
vi.useFakeTimers()
|
||||
const spy = vi.fn()
|
||||
|
||||
timeout(1000).then(spy)
|
||||
|
||||
expect(spy).not.toHaveBeenCalled()
|
||||
|
||||
vi.advanceTimersByTime(1000)
|
||||
await vi.runAllTimersAsync()
|
||||
|
||||
expect(spy).toHaveBeenCalled()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
})
|
||||
|
||||
describe('getURL', () => {
|
||||
it('should return prod url by default', () => {
|
||||
const result = getURL()
|
||||
|
||||
expect(result).toEqual('https://supabase.com/dashboard')
|
||||
})
|
||||
})
|
||||
|
||||
describe('makeRandomString', () => {
|
||||
it('should return a random string of the given length', () => {
|
||||
const result = makeRandomString(10)
|
||||
|
||||
expect(result).toHaveLength(10)
|
||||
})
|
||||
})
|
||||
|
||||
describe('pluckObjectFields', () => {
|
||||
it('should return a new object with the specified fields', () => {
|
||||
const result = pluckObjectFields({ a: 1, b: 2, c: 3 }, ['a', 'c'])
|
||||
|
||||
expect(result).toEqual({ a: 1, c: 3 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('tryParseInt', () => {
|
||||
it('should return the parsed integer', () => {
|
||||
const result = tryParseInt('123')
|
||||
|
||||
expect(result).toEqual(123)
|
||||
})
|
||||
|
||||
it('should return undefined if the string is not a number', () => {
|
||||
const result = tryParseInt('not a number')
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('propsAreEqual', () => {
|
||||
it('should return true if the props are equal', () => {
|
||||
const result = propsAreEqual({ a: 1, b: 2 }, { a: 1, b: 2 })
|
||||
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false if the props are not equal', () => {
|
||||
const result = propsAreEqual({ a: 1, b: 2 }, { a: 1, b: 3 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatBytes', () => {
|
||||
it('should return the formatted bytes', () => {
|
||||
const result = formatBytes(1024)
|
||||
|
||||
expect(result).toEqual('1 KB')
|
||||
})
|
||||
|
||||
it('should return the formatted bytes in MB', () => {
|
||||
const result = formatBytes(1024 * 1024)
|
||||
|
||||
expect(result).toEqual('1 MB')
|
||||
})
|
||||
})
|
||||
|
||||
describe('snakeToCamel', () => {
|
||||
it('should convert snake_case to camelCase', () => {
|
||||
const result = snakeToCamel('snake_case')
|
||||
|
||||
expect(result).toEqual('snakeCase')
|
||||
})
|
||||
})
|
||||
|
||||
describe('copyToClipboard', () => {
|
||||
let writeMock: any
|
||||
let writeTextMock: any
|
||||
let hasFocusMock: any
|
||||
|
||||
beforeEach(() => {
|
||||
writeMock = vi.fn().mockResolvedValue(undefined)
|
||||
writeTextMock = vi.fn().mockResolvedValue(undefined)
|
||||
hasFocusMock = vi.fn().mockReturnValue(true)
|
||||
|
||||
vi.stubGlobal('navigator', {
|
||||
clipboard: {
|
||||
write: writeMock,
|
||||
writeText: writeTextMock,
|
||||
},
|
||||
})
|
||||
|
||||
vi.stubGlobal('window', {
|
||||
document: {
|
||||
hasFocus: hasFocusMock,
|
||||
},
|
||||
})
|
||||
|
||||
// If ClipboardItem is used
|
||||
vi.stubGlobal('ClipboardItem', function (items: any) {
|
||||
return items
|
||||
})
|
||||
|
||||
// Prevent toast errors
|
||||
vi.stubGlobal('toast', { error: vi.fn() })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it('uses clipboard.write if available', async () => {
|
||||
await copyToClipboard('hello')
|
||||
expect(writeMock).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('falls back to writeText if clipboard.write not available', async () => {
|
||||
;(navigator.clipboard as any).write = undefined
|
||||
await copyToClipboard('hello')
|
||||
expect(writeTextMock).toHaveBeenCalledWith('hello')
|
||||
})
|
||||
})
|
||||
|
||||
describe('detectBrowser', () => {
|
||||
const originalNavigator = global.navigator
|
||||
|
||||
const setUserAgent = (ua: string) => {
|
||||
vi.stubGlobal('navigator', { userAgent: ua })
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
global.navigator = originalNavigator
|
||||
})
|
||||
|
||||
it('detects Chrome', () => {
|
||||
setUserAgent('Mozilla/5.0 Chrome/90.0.0.0 Safari/537.36')
|
||||
expect(detectBrowser()).toBe('Chrome')
|
||||
})
|
||||
|
||||
it('detects Firefox', () => {
|
||||
setUserAgent('Mozilla/5.0 Firefox/88.0')
|
||||
expect(detectBrowser()).toBe('Firefox')
|
||||
})
|
||||
|
||||
it('detects Safari', () => {
|
||||
setUserAgent('Mozilla/5.0 Version/14.0 Safari/605.1.15')
|
||||
expect(detectBrowser()).toBe('Safari')
|
||||
})
|
||||
|
||||
it('returns undefined when navigator is not defined', () => {
|
||||
vi.stubGlobal('navigator', undefined)
|
||||
expect(detectBrowser()).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('detectOS', () => {
|
||||
const mockUserAgent = (ua: string) => {
|
||||
vi.stubGlobal('window', {
|
||||
navigator: { userAgent: ua },
|
||||
})
|
||||
vi.stubGlobal('navigator', { userAgent: ua }) // some code may use both
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it('detects macOS', () => {
|
||||
mockUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)')
|
||||
expect(detectOS()).toBe('macos')
|
||||
})
|
||||
|
||||
it('detects Windows', () => {
|
||||
mockUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64)')
|
||||
expect(detectOS()).toBe('windows')
|
||||
})
|
||||
|
||||
it('returns undefined for unknown OS', () => {
|
||||
mockUserAgent('Mozilla/5.0 (X11; Linux x86_64)')
|
||||
expect(detectOS()).toBeUndefined()
|
||||
})
|
||||
|
||||
it('returns undefined if window is undefined', () => {
|
||||
vi.stubGlobal('window', undefined)
|
||||
expect(detectOS()).toBeUndefined()
|
||||
})
|
||||
|
||||
it('returns undefined if navigator is undefined', () => {
|
||||
vi.stubGlobal('window', {})
|
||||
vi.stubGlobal('navigator', undefined)
|
||||
expect(detectOS()).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('pluralize', () => {
|
||||
it('should return the pluralized word', () => {
|
||||
const result = pluralize(2, 'test', 'tests')
|
||||
|
||||
expect(result).toEqual('tests')
|
||||
})
|
||||
})
|
||||
|
||||
describe('isValidHttpUrl', () => {
|
||||
it('should return true if the URL is valid', () => {
|
||||
const result = isValidHttpUrl('https://supabase.com')
|
||||
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false if the URL is not valid', () => {
|
||||
const result = isValidHttpUrl('not a url')
|
||||
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('removeCommentsFromSql', () => {
|
||||
it('should remove comments from SQL', () => {
|
||||
const result = removeCommentsFromSql(`-- This is a comment
|
||||
SELECT * FROM users
|
||||
`)
|
||||
|
||||
expect(result).toEqual(`
|
||||
SELECT * FROM users
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getSemanticVersion', () => {
|
||||
it('should return the semantic version', () => {
|
||||
const result = getSemanticVersion('supabase-postgres-14.1.0.88')
|
||||
|
||||
expect(result).toEqual(141088)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getDatabaseMajorVersion', () => {
|
||||
it('should return the database major version', () => {
|
||||
const result = getDatabaseMajorVersion('supabase-postgres-14.1.0.88')
|
||||
|
||||
expect(result).toEqual(14)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getDistanceLatLonKM', () => {
|
||||
it('should return the distance in kilometers', () => {
|
||||
const result = getDistanceLatLonKM(37.774929, -122.419418, 37.774929, -122.419418)
|
||||
|
||||
expect(result).toEqual(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatCurrency', () => {
|
||||
it('should return the formatted currency', () => {
|
||||
const result = formatCurrency(1000)
|
||||
|
||||
expect(result).toEqual('$1,000.00')
|
||||
})
|
||||
|
||||
it('should return the formatted currency with small values', () => {
|
||||
const result = formatCurrency(0.001)
|
||||
|
||||
expect(result).toEqual('$0')
|
||||
})
|
||||
|
||||
it('should return null if the value is undefined', () => {
|
||||
const result = formatCurrency(undefined)
|
||||
|
||||
expect(result).toEqual(null)
|
||||
})
|
||||
})
|
||||
@@ -97,7 +97,8 @@ export const pluckObjectFields = (model: any, fields: any[]) => {
|
||||
*/
|
||||
export const tryParseInt = (str: string) => {
|
||||
try {
|
||||
return parseInt(str, 10)
|
||||
const int = parseInt(str, 10)
|
||||
return isNaN(int) ? undefined : int
|
||||
} catch (error) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -10,11 +10,12 @@
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"clean": "rimraf node_modules tsconfig.tsbuildinfo .next .turbo",
|
||||
"test": "vitest --run",
|
||||
"test": "vitest --run --coverage",
|
||||
"test:watch": "vitest watch",
|
||||
"test:ui": "vitest --ui",
|
||||
"test:update": "vitest --run --update",
|
||||
"test:update:watch": "vitest --watch --update",
|
||||
"test:ci": "vitest --run --coverage",
|
||||
"test:report": "open coverage/lcov-report/index.html",
|
||||
"deploy:staging": "VERCEL_ORG_ID=team_E6KJ1W561hMTjon1QSwOh0WO VERCEL_PROJECT_ID=QmcmhbiAtCMFTAHCuGgQscNbke4TzgWULECctNcKmxWCoT vercel --prod -A .vercel/staging.json",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"prettier:check": "prettier --check .",
|
||||
@@ -160,6 +161,7 @@
|
||||
"@types/sqlstring": "^2.3.0",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@types/zxcvbn": "^4.4.1",
|
||||
"@vitest/coverage-v8": "^3.0.9",
|
||||
"@vitest/ui": "^3.0.0",
|
||||
"api-types": "workspace:*",
|
||||
"autoprefixer": "^10.4.14",
|
||||
|
||||
@@ -31,5 +31,11 @@ export default defineConfig({
|
||||
resolve(dirname, './tests/setup/polyfills.js'),
|
||||
resolve(dirname, './tests/setup/radix.js'),
|
||||
],
|
||||
reporters: [['default']],
|
||||
coverage: {
|
||||
reporter: ['lcov'],
|
||||
exclude: ['**/*.test.ts', '**/*.test.tsx'],
|
||||
include: ['lib/**/*.ts'],
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -14,7 +14,9 @@
|
||||
"extract-design-tokens": "node internals/tokens/extract-design-tokens.js",
|
||||
"generate-styles": "pnpm run extract-design-tokens && pnpm run transform-tokens && pnpm run cleanse-css-for-tailwind && pnpm run generate-demo-tailwind-classes",
|
||||
"clean": "rimraf node_modules",
|
||||
"test": "vitest"
|
||||
"test": "vitest",
|
||||
"test:ci": "vitest --run --coverage",
|
||||
"test:report": "open coverage/lcov-report/index.html"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.7.17",
|
||||
@@ -88,6 +90,7 @@
|
||||
"@types/react-copy-to-clipboard": "^5.0.4",
|
||||
"@types/react-dom": "catalog:",
|
||||
"@types/react-syntax-highlighter": "^15.5.6",
|
||||
"@vitest/coverage-v8": "^3.0.9",
|
||||
"common": "workspace:*",
|
||||
"config": "workspace:*",
|
||||
"glob": "^8.1.0",
|
||||
|
||||
@@ -4,5 +4,11 @@ export default defineConfig({
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
setupFiles: ['./vitest.setup.ts'],
|
||||
reporters: [['default']],
|
||||
coverage: {
|
||||
reporter: ['lcov'],
|
||||
exclude: ['**/*.test.ts', '**/*.test.tsx'],
|
||||
include: ['src/**/*.ts', 'src/**/*.tsx'],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
45
pnpm-lock.yaml
generated
45
pnpm-lock.yaml
generated
@@ -980,6 +980,9 @@ importers:
|
||||
'@types/zxcvbn':
|
||||
specifier: ^4.4.1
|
||||
version: 4.4.2
|
||||
'@vitest/coverage-v8':
|
||||
specifier: ^3.0.9
|
||||
version: 3.0.9(supports-color@8.1.1)(vitest@3.0.9)
|
||||
'@vitest/ui':
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.4(vitest@3.0.9)
|
||||
@@ -1854,7 +1857,7 @@ importers:
|
||||
version: 0.5.10(tailwindcss@3.4.1(ts-node@10.9.2(@types/node@20.12.11)(typescript@5.5.2)))
|
||||
autoprefixer:
|
||||
specifier: ^10.4.14
|
||||
version: 10.4.16(postcss@8.5.3)
|
||||
version: 10.4.16(postcss@8.4.38)
|
||||
class-variance-authority:
|
||||
specifier: ^0.6.1
|
||||
version: 0.6.1
|
||||
@@ -1973,6 +1976,9 @@ importers:
|
||||
'@types/react-syntax-highlighter':
|
||||
specifier: ^15.5.6
|
||||
version: 15.5.7
|
||||
'@vitest/coverage-v8':
|
||||
specifier: ^3.0.9
|
||||
version: 3.0.9(supports-color@8.1.1)(vitest@3.0.9(@types/node@20.12.11)(jiti@2.4.2)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.7.3(@types/node@20.12.11)(typescript@5.5.2))(sass@1.72.0)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.4.5))
|
||||
common:
|
||||
specifier: workspace:*
|
||||
version: link:../common
|
||||
@@ -9495,7 +9501,6 @@ packages:
|
||||
resolution: {integrity: sha512-t0q23FIpvHDTtnORW+bDJziGsal5uh9RJTJ1fyH8drd4lICOoXhJ5pLMUZ5C0VQei6dNmwTzzoTRgMkO9JgHEQ==}
|
||||
peerDependencies:
|
||||
eslint: '>= 5'
|
||||
bundledDependencies: []
|
||||
|
||||
eslint-plugin-import@2.29.1:
|
||||
resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==}
|
||||
@@ -22959,6 +22964,24 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@vitest/coverage-v8@3.0.9(supports-color@8.1.1)(vitest@3.0.9(@types/node@20.12.11)(jiti@2.4.2)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.7.3(@types/node@20.12.11)(typescript@5.5.2))(sass@1.72.0)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.4.5))':
|
||||
dependencies:
|
||||
'@ampproject/remapping': 2.3.0
|
||||
'@bcoe/v8-coverage': 1.0.2
|
||||
debug: 4.4.0(supports-color@8.1.1)
|
||||
istanbul-lib-coverage: 3.2.2
|
||||
istanbul-lib-report: 3.0.1
|
||||
istanbul-lib-source-maps: 5.0.6(supports-color@8.1.1)
|
||||
istanbul-reports: 3.1.7
|
||||
magic-string: 0.30.17
|
||||
magicast: 0.3.5
|
||||
std-env: 3.8.1
|
||||
test-exclude: 7.0.1
|
||||
tinyrainbow: 2.0.0
|
||||
vitest: 3.0.9(@types/node@20.12.11)(jiti@2.4.2)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.7.3(@types/node@20.12.11)(typescript@5.5.2))(sass@1.72.0)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.4.5)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@vitest/coverage-v8@3.0.9(supports-color@8.1.1)(vitest@3.0.9(@types/node@22.13.14)(jiti@2.4.2)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.7.3(@types/node@22.13.14)(typescript@5.5.2))(sass@1.72.0)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.4.5))':
|
||||
dependencies:
|
||||
'@ampproject/remapping': 2.3.0
|
||||
@@ -22977,6 +23000,24 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@vitest/coverage-v8@3.0.9(supports-color@8.1.1)(vitest@3.0.9)':
|
||||
dependencies:
|
||||
'@ampproject/remapping': 2.3.0
|
||||
'@bcoe/v8-coverage': 1.0.2
|
||||
debug: 4.4.0(supports-color@8.1.1)
|
||||
istanbul-lib-coverage: 3.2.2
|
||||
istanbul-lib-report: 3.0.1
|
||||
istanbul-lib-source-maps: 5.0.6(supports-color@8.1.1)
|
||||
istanbul-reports: 3.1.7
|
||||
magic-string: 0.30.17
|
||||
magicast: 0.3.5
|
||||
std-env: 3.8.1
|
||||
test-exclude: 7.0.1
|
||||
tinyrainbow: 2.0.0
|
||||
vitest: 3.0.9(@types/node@20.12.11)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.4.11(typescript@5.5.2))(sass@1.72.0)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.4.5)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@vitest/expect@3.0.9':
|
||||
dependencies:
|
||||
'@vitest/spy': 3.0.9
|
||||
|
||||
Reference in New Issue
Block a user