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(
+ undefined}
+ placeholder="Filter"
+ preText="$"
+ postText="USD"
+ size="sm"
+ />
+ )
+ const plain = renderToString(
+ undefined}
+ size="lg"
+ />
+ )
+
+ expect(wrapped).toContain('$')
+ expect(wrapped).toContain('>USD')
+ expect(wrapped).toContain('placeholder="Filter"')
+ expect(wrapped).toContain('text-ob-destructive')
+
+ expect(plain).toContain(' 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))
+ })
+})
diff --git a/src/import/json.test.ts b/src/import/json.test.ts
index 04b4ed1..053dcf6 100644
--- a/src/import/json.test.ts
+++ b/src/import/json.test.ts
@@ -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()
@@ -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([])
@@ -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([])
diff --git a/src/public-entrypoints.test.ts b/src/public-entrypoints.test.ts
new file mode 100644
index 0000000..cdd5b46
--- /dev/null
+++ b/src/public-entrypoints.test.ts
@@ -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',
+ ])
+ })
+})