Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions plugins/interface/components/primitives.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { renderToString } from 'hono/jsx/dom/server'
import { describe, expect, it } from 'vitest'

import { Avatar } from './avatar'
import { Card } from './card'
import { Input } from './input/Input'
import { Label } from './label/Label'
import { Loader } from './loader/Loader'
import { Toggle } from './toggle'

describe('interface primitive components', () => {
it('renders avatar links with fallback initials and custom classes', () => {
const html = renderToString(
<Avatar as="a" href="/profile" username="outerbase" class="extra" />
)

expect(html).toContain('<a')
expect(html).toContain('href="/profile"')
expect(html).toContain('extra')
expect(html).toContain('>O</p>')
})

it('renders avatar images with accessible alt text and selected state', () => {
const html = renderToString(
<Avatar
image="/avatar.png"
toggled
username="Ada"
data-testid="avatar"
/>
)

expect(html).toContain('<button')
expect(html).toContain('after:opacity-100')
expect(html).toContain('src="/avatar.png"')
expect(html).toContain('alt="Ada"')
expect(html).toContain('data-testid="avatar"')
})

it('renders cards as links or divs with variant classes', () => {
const link = renderToString(
<Card as="a" href="/docs" variant="primary">
Docs
</Card>
)
const panel = renderToString(
<Card variant="secondary" data-testid="card">
Panel
</Card>
)

expect(link).toContain('<a')
expect(link).toContain('href="/docs"')
expect(link).toContain('btn-primary')
expect(link).toContain('Docs')

expect(panel).toContain('<div')
expect(panel).toContain('btn-secondary')
expect(panel).toContain('data-testid="card"')
})

it('renders labels with validation messaging only when invalid', () => {
const invalid = renderToString(
<Label
title="Database"
required
requiredDescription="Required"
isValid={false}
>
<input />
</Label>
)
const valid = renderToString(
<Label
title="Database"
required
requiredDescription="Required"
isValid
/>
)

expect(invalid).toContain('Database')
expect(invalid).toContain('*')
expect(invalid).toContain('Required')
expect(valid).not.toContain('Required')
})

it('renders loader and toggle sizing/state classes', () => {
const loader = renderToString(<Loader size={18} class="spin" />)
const toggle = renderToString(
<Toggle onClick={() => undefined} size="lg" toggled />
)

expect(loader).toContain('class="spin"')
expect(loader).toContain('style="height: 18px; width: 18px"')
expect(toggle).toContain('h-7.5 w-12.5')
expect(toggle).toContain('translate-x-full')
})

it('renders input wrappers with prefix, suffix, and invalid state', () => {
const wrapped = renderToString(
<Input
initialValue="abc"
isValid={false}
onValueChange={() => undefined}
placeholder="Filter"
preText="$"
postText="USD"
size="sm"
/>
)
const plain = renderToString(
<Input
className="extra-input"
initialValue="plain"
onValueChange={() => undefined}
size="lg"
/>
)

expect(wrapped).toContain('<div')
expect(wrapped).toContain('ob-size-sm')
expect(wrapped).toContain('>$</span>')
expect(wrapped).toContain('>USD</span>')
expect(wrapped).toContain('placeholder="Filter"')
expect(wrapped).toContain('text-ob-destructive')

expect(plain).toContain('<input')
expect(plain).toContain('extra-input')
expect(plain).toContain('ob-size-lg')
expect(plain).toContain('value="plain"')
})
})
47 changes: 47 additions & 0 deletions plugins/interface/pages/template/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'

const hydrateRoot = vi.hoisted(() => vi.fn())

vi.mock('hono/jsx/dom/client', () => ({
hydrateRoot,
}))

vi.mock('../../public/global.css', () => ({}))

describe('template page entrypoint', () => {
beforeEach(() => {
vi.resetModules()
hydrateRoot.mockClear()
})

afterEach(() => {
vi.unstubAllGlobals()
})

it('does not hydrate when the template root is missing', async () => {
const querySelector = vi.fn(() => null)
vi.stubGlobal('document', { querySelector })

await import('./index')

expect(querySelector).toHaveBeenCalledWith(
'#root[data-client="template"]'
)
expect(hydrateRoot).not.toHaveBeenCalled()
})

it('hydrates the template page when the server root is present', async () => {
const root = {
dataset: {
serverProps: '{}',
},
}
const querySelector = vi.fn(() => root)
vi.stubGlobal('document', { querySelector })

await import('./index')

expect(hydrateRoot).toHaveBeenCalledTimes(1)
expect(hydrateRoot).toHaveBeenCalledWith(root, expect.any(Object))
})
})
139 changes: 139 additions & 0 deletions src/import/json.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,38 @@ describe('JSON Import Module', () => {
expect(jsonResponse.error).toContain('Invalid JSON format')
})

it.each([
['missing data', {}],
['null data', { data: null }],
['object data', { data: { id: 1, name: 'Alice' } }],
])(
'should return 400 without inserts for application/json with %s',
async (_caseName, payload) => {
const request = new Request('http://localhost', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})

const response = await importTableFromJsonRoute(
'users',
request,
mockDataSource,
mockConfig
)

expect(response.status).toBe(400)
expect(executeOperation).not.toHaveBeenCalled()
const jsonResponse = (await response.json()) as {
error?: string
result?: any
}
expect(jsonResponse.error).toBe(
'Invalid JSON format. Expected an object with "data" array and optional "columnMapping".'
)
}
)

it('should return 400 if no file is uploaded in multipart form-data', async () => {
const formData = new FormData()

Expand All @@ -106,6 +138,36 @@ describe('JSON Import Module', () => {
expect(jsonResponse.error).toBe('No file uploaded')
})

it('should return 400 if uploaded JSON file is invalid', async () => {
const formData = new FormData()
formData.set(
'file',
new File(['not json'], 'users.json', {
type: 'application/json',
})
)

const request = new Request('http://localhost', {
method: 'POST',
body: formData,
})

const response = await importTableFromJsonRoute(
'users',
request,
mockDataSource,
mockConfig
)

expect(response.status).toBe(400)
expect(executeOperation).not.toHaveBeenCalled()
const jsonResponse = (await response.json()) as {
error?: string
result?: any
}
expect(jsonResponse.error).toBe('Invalid file upload')
})

it('should successfully insert valid JSON data into the table', async () => {
vi.mocked(executeOperation).mockResolvedValue([])

Expand Down Expand Up @@ -136,6 +198,83 @@ describe('JSON Import Module', () => {
)
})

it('should apply column mapping when inserting JSON records', async () => {
vi.mocked(executeOperation).mockResolvedValue([])

const request = new Request('http://localhost', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
data: [{ fullName: 'Alice', emailAddress: 'alice@test.dev' }],
columnMapping: {
fullName: 'name',
emailAddress: 'email',
},
}),
})

const response = await importTableFromJsonRoute(
'users',
request,
mockDataSource,
mockConfig
)

expect(response.status).toBe(200)
expect(executeOperation).toHaveBeenCalledWith(
[
{
sql: 'INSERT INTO users (name, email) VALUES (?, ?)',
params: ['Alice', 'alice@test.dev'],
},
],
mockDataSource,
mockConfig
)
})

it('should insert valid JSON data from multipart file upload', async () => {
vi.mocked(executeOperation).mockResolvedValue([])

const formData = new FormData()
formData.set(
'file',
new File(
[
JSON.stringify({
data: [{ id: 1, name: 'Alice' }],
}),
],
'users.json',
{ type: 'application/json' }
)
)

const request = new Request('http://localhost', {
method: 'POST',
body: formData,
})

const response = await importTableFromJsonRoute(
'users',
request,
mockDataSource,
mockConfig
)

expect(response.status).toBe(200)
expect(executeOperation).toHaveBeenCalledWith(
[
{
sql: 'INSERT INTO users (id, name) VALUES (?, ?)',
params: [1, 'Alice'],
},
],
mockDataSource,
mockConfig
)
})

it('should return partial success if some inserts fail', async () => {
vi.mocked(executeOperation)
.mockResolvedValueOnce([])
Expand Down
54 changes: 54 additions & 0 deletions src/public-entrypoints.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { describe, expect, it, vi } from 'vitest'

import { ChangeDataCapturePlugin } from '../plugins/cdc'
import { ClerkPlugin } from '../plugins/clerk'
import { QueryLogPlugin } from '../plugins/query-log'
import { ResendPlugin } from '../plugins/resend'
import { SqlMacrosPlugin } from '../plugins/sql-macros'
import { StripeSubscriptionPlugin } from '../plugins/stripe'
import { StudioPlugin } from '../plugins/studio'
import { WebSocketPlugin } from '../plugins/websocket'
import * as publicApi from '../dist'
import * as pluginApi from '../dist/plugins'
import { StarbaseDBDurableObject } from './do'
import { StarbaseDB } from './handler'

vi.mock('cloudflare:workers', () => {
return {
DurableObject: class MockDurableObject {},
}
})

describe('public package entrypoints', () => {
it('exposes runtime APIs from the root package export', () => {
expect(publicApi.StarbaseDB).toBe(StarbaseDB)
expect(publicApi.StarbaseDBDurableObject).toBe(StarbaseDBDurableObject)
expect(Object.keys(publicApi).sort()).toEqual([
'StarbaseDB',
'StarbaseDBDurableObject',
])
})

it('exposes documented plugin constructors from the plugin export', () => {
expect(pluginApi.StudioPlugin).toBe(StudioPlugin)
expect(pluginApi.WebSocketPlugin).toBe(WebSocketPlugin)
expect(pluginApi.SqlMacrosPlugin).toBe(SqlMacrosPlugin)
expect(pluginApi.StripeSubscriptionPlugin).toBe(
StripeSubscriptionPlugin
)
expect(pluginApi.ChangeDataCapturePlugin).toBe(ChangeDataCapturePlugin)
expect(pluginApi.QueryLogPlugin).toBe(QueryLogPlugin)
expect(pluginApi.ResendPlugin).toBe(ResendPlugin)
expect(pluginApi.ClerkPlugin).toBe(ClerkPlugin)
expect(Object.keys(pluginApi).sort()).toEqual([
'ChangeDataCapturePlugin',
'ClerkPlugin',
'QueryLogPlugin',
'ResendPlugin',
'SqlMacrosPlugin',
'StripeSubscriptionPlugin',
'StudioPlugin',
'WebSocketPlugin',
])
})
})