Skip to content
Merged
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
6 changes: 3 additions & 3 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ jobs:

test:
runs-on: ubuntu-latest
name: ✅ Bun Tests
name: ✅ Unit Tests
if: >-
github.event_name != 'pull_request' || github.event.pull_request.draft ==
false
Expand All @@ -84,8 +84,8 @@ jobs:
- name: 📥 Install Dependencies
run: bun install --frozen-lockfile

- name: ✅ Run Bun Tests
run: bun test ./server ./mock-servers
- name: ✅ Run Unit Tests
run: bun run test:unit

e2e:
runs-on: ubuntu-latest
Expand Down
125 changes: 125 additions & 0 deletions bun.lock

Large diffs are not rendered by default.

89 changes: 62 additions & 27 deletions client/app-session-refresh.test.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,34 @@
/// <reference types="bun" />
import { expect, mock, test } from 'bun:test'
import { expect, test, vi } from 'vitest'
import { type Handle } from 'remix/component'

type QueueTask = Parameters<Handle['queueTask']>[0]

const navigationListeners: Array<() => void> = []
const queuedSessionResponses: Array<{ email: string } | null> = []
const fetchSessionInfoMock = mock(async () => {
const fetchSessionInfoMock = vi.fn(async () => {
return queuedSessionResponses.shift() ?? null
})

mock.module('./client-router.tsx', () => ({
routerEvents: new EventTarget(),
listenToRouterNavigation: (_handle: Handle, listener: () => void) => {
navigationListeners.push(listener)
},
getPathname: () => '/',
navigate: () => {
return
},
Router: () => () => null,
}))

mock.module('./session.ts', () => ({
fetchSessionInfo: fetchSessionInfoMock,
}))

const { App } = await import('./app.tsx')
async function loadApp() {
vi.resetModules()
vi.doMock('./client-router.tsx', () => ({
routerEvents: new EventTarget(),
listenToRouterNavigation: (_handle: Handle, listener: () => void) => {
navigationListeners.push(listener)
},
getPathname: () => '/',
navigate: () => {
return
},
Router: () => () => null,
}))
vi.doMock('./session.ts', () => ({
fetchSessionInfo: fetchSessionInfoMock,
}))
const { App } = await import('./app.tsx')
return App
}

async function runNextTask(tasks: Array<QueueTask>, aborted: boolean) {
const task = tasks.shift()
Expand All @@ -40,6 +42,7 @@ test('aborted refresh does not erase a ready authenticated session', async () =>
navigationListeners.length = 0
queuedSessionResponses.length = 0
queuedSessionResponses.push({ email: 'signed-in@example.com' }, null)
fetchSessionInfoMock.mockClear()

const queuedTasks: Array<QueueTask> = []
const handle = {
Expand All @@ -54,24 +57,56 @@ test('aborted refresh does not erase a ready authenticated session', async () =>
},
} as unknown as Handle

const App = await loadApp()
const render = App(handle)
expect(navigationListeners).toHaveLength(1)

// Initial bootstrap task enqueues the session fetch.
await runNextTask(queuedTasks, false)
await runNextTask(queuedTasks, false)

const authenticatedUi = Bun.inspect(render())
expect(authenticatedUi).toContain('signed-in@example.com')
expect(authenticatedUi).toContain('Log out')
const authenticatedUi = render()
const navChildren = (authenticatedUi.props as { children: Array<unknown> })
.children[0] as {
props: { children: Array<unknown> }
}
const navItems = navChildren.props.children as Array<unknown>
const accountLink = navItems[2] as { props?: { children?: Array<unknown> } }
const accountEntry = (accountLink.props?.children ?? [])[1] as {
props?: { children?: string }
}
const logoutEntry = (accountLink.props?.children ?? [])[2] as {
props?: { children?: { props?: { children?: string } } }
}
expect(accountEntry.props?.children).toBe('signed-in@example.com')
expect(logoutEntry.props?.children?.props?.children).toBe('Log out')

// Re-run refresh via navigation, then abort in-flight fetch.
navigationListeners[0]!()
await runNextTask(queuedTasks, true)

const uiAfterAbort = Bun.inspect(render())
expect(uiAfterAbort).toContain('signed-in@example.com')
expect(uiAfterAbort).toContain('Log out')
expect(uiAfterAbort).not.toContain('>Login<')
expect(uiAfterAbort).not.toContain('>Signup<')
const uiAfterAbort = render()
const navAfterAbort = (uiAfterAbort.props as { children: Array<unknown> })
.children[0] as {
props: { children: Array<unknown> }
}
const navAfterAbortItems = navAfterAbort.props.children as Array<unknown>
expect(navAfterAbortItems).toHaveLength(3)
const accountLinkAfterAbort = navAfterAbortItems[2] as {
props?: { children?: Array<unknown> }
}
const accountEntryAfterAbort = (accountLinkAfterAbort.props?.children ?? [])[1] as {
props?: { children?: string }
}
const logoutEntryAfterAbort = (accountLinkAfterAbort.props?.children ?? [])[2] as {
props?: { children?: { props?: { children?: string } } }
}
expect(accountEntryAfterAbort.props?.children).toBe('signed-in@example.com')
expect(logoutEntryAfterAbort.props?.children?.props?.children).toBe('Log out')
Comment thread
cursor[bot] marked this conversation as resolved.
const navAfterAbortText = JSON.stringify(navAfterAbort)
expect(navAfterAbortText).not.toContain('Login')
expect(navAfterAbortText).not.toContain('Signup')

vi.doUnmock('./client-router.tsx')
vi.doUnmock('./session.ts')
})
2 changes: 1 addition & 1 deletion client/mcp-apps/widget-host-bridge.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { afterEach, expect, test } from 'bun:test'
import { afterEach, expect, test } from 'vitest'
import { createWidgetHostBridge } from './widget-host-bridge.ts'

type HostRequestMessage = {
Expand Down
6 changes: 3 additions & 3 deletions docs/agents/testing-principles.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ magic.
- Write tests so they could run offline if necessary: avoid relying on the
public internet and third-party services; prefer local fakes/fixtures.
- Prefer fast unit tests for server logic; keep e2e tests focused on journeys.
- Run server/unit tests with `bun test ./server ./mock-servers` to avoid
- Run server/unit tests with `bun run test:unit` to avoid
Playwright spec discovery and accidental matches like `mcp-server-e2e`.

## Examples

### `Symbol.dispose` with `using`

```ts
import { test, expect } from 'bun:test'
import { test, expect } from 'vitest'

const createTempFile = () => {
const path = `/tmp/test-${crypto.randomUUID()}.txt`
Expand Down Expand Up @@ -53,7 +53,7 @@ test('reads a temp file', () => {
### `Symbol.asyncDispose` with `await using`

```ts
import { test, expect } from 'bun:test'
import { test, expect } from 'vitest'

const createDisposableServer = async () => {
const server = Bun.serve({
Expand Down
2 changes: 1 addition & 1 deletion mcp/context.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/// <reference types="bun" />
import { expect, test } from 'bun:test'
import { expect, test } from 'vitest'
import { createMcpCallerContext, parseMcpCallerContext } from './context.ts'

test('createMcpCallerContext normalizes missing user to null', () => {
Expand Down
Loading
Loading