Skip to content

Commit c4ae164

Browse files
danwritecodeclaude
andcommitted
(feat) add Vitest and ESLint to frontend
Set up test infrastructure with Vitest, @vue/test-utils, and happy-dom, plus ESLint via @nuxt/eslint flat config. Includes a sample test for the landing page and documents the new dev commands in the README. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 9b7492f commit c4ae164

8 files changed

Lines changed: 124 additions & 3 deletions

File tree

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,34 @@ bun run dev
6666

6767
The backend runs on `http://localhost:50060` (gRPC) and the frontend on `http://localhost:3000`. No Clerk account is needed -- auth is disabled by default (see [Enabling Auth](#enabling-auth)).
6868

69+
## Development
70+
71+
### Testing
72+
73+
Bufstack uses [Vitest](https://vitest.dev/) for frontend unit tests:
74+
75+
```bash
76+
cd frontend
77+
bun run test # Watch mode
78+
bun run test:run # Single run (CI)
79+
```
80+
81+
Test files live next to their source in `__tests__/` directories:
82+
```
83+
app/pages/__tests__/index.test.ts # tests for app/pages/index.vue
84+
app/components/__tests__/Foo.test.ts # tests for app/components/Foo.vue
85+
```
86+
87+
### Linting
88+
89+
[ESLint](https://eslint.org/) with [`@nuxt/eslint`](https://eslint.nuxt.com/) flat config:
90+
91+
```bash
92+
cd frontend
93+
bun run lint # Check for issues
94+
bun run lint:fix # Auto-fix issues
95+
```
96+
6997
## Project Structure
7098

7199
```

frontend/app/composables/useGrpc.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export async function useGrpcClient<T extends DescService>(
1919
const headers = useRequestHeaders(['cookie', 'authorization']);
2020
const { createGrpcTransport } = await import("@connectrpc/connect-node");
2121
const transport = createGrpcTransport({
22-
// @ts-ignore
22+
// @ts-expect-error -- backendUrl comes from runtimeConfig
2323
baseUrl: process.env.NUXT_BACKEND_URL,
2424
interceptors: [
2525
(next) => async (req) => {
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest'
2+
import { mount } from '@vue/test-utils'
3+
import IndexPage from '../index.vue'
4+
5+
const mockSayHello = vi.fn()
6+
7+
// useGrpcClient is auto-imported by Nuxt, so stub it as a global
8+
vi.stubGlobal('useGrpcClient', vi.fn(() => Promise.resolve({ sayHello: mockSayHello })))
9+
10+
const stubs = {
11+
Card: { template: '<div><slot /></div>' },
12+
CardHeader: { template: '<div><slot /></div>' },
13+
CardTitle: { template: '<h1><slot /></h1>' },
14+
CardDescription: { template: '<p><slot /></p>' },
15+
CardContent: { template: '<div><slot /></div>' },
16+
CardFooter: { template: '<div><slot /></div>' },
17+
Button: { template: '<button @click="$emit(\'click\')"><slot /></button>' },
18+
Badge: { template: '<span><slot /></span>' },
19+
Alert: { template: '<div><slot /></div>' },
20+
AlertTitle: { template: '<div><slot /></div>' },
21+
AlertDescription: { template: '<div><slot /></div>' },
22+
}
23+
24+
beforeEach(() => {
25+
mockSayHello.mockReset()
26+
})
27+
28+
describe('Index Page', () => {
29+
it('renders the title and description', () => {
30+
const wrapper = mount(IndexPage, { global: { stubs } })
31+
32+
expect(wrapper.text()).toContain('Bufstack')
33+
expect(wrapper.text()).toContain('Rust gRPC + Nuxt 4 + Protocol Buffers')
34+
})
35+
36+
it('calls sayHello and displays the response', async () => {
37+
mockSayHello.mockResolvedValue({ message: 'Hello World!' })
38+
39+
const wrapper = mount(IndexPage, { global: { stubs } })
40+
41+
await wrapper.find('button').trigger('click')
42+
43+
// Wait for the async gRPC call to resolve and DOM to update
44+
await vi.waitFor(() => {
45+
expect(wrapper.text()).toContain('Hello World!')
46+
})
47+
})
48+
})

frontend/bun.lockb

92.3 KB
Binary file not shown.

frontend/eslint.config.mjs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// @ts-check
2+
import withNuxt from './.nuxt/eslint.config.mjs'
3+
4+
export default withNuxt(
5+
{
6+
ignores: ['app/gen/**'],
7+
},
8+
)

frontend/nuxt.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export default defineNuxtConfig({
1717
modules: [
1818
...(clerkEnabled ? ['@clerk/nuxt'] : []),
1919
'@nuxtjs/color-mode',
20+
'@nuxt/eslint',
2021
'shadcn-nuxt',
2122
],
2223

@@ -33,6 +34,7 @@ export default defineNuxtConfig({
3334

3435
vite: {
3536
plugins: [
37+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
3638
tailwindcss() as any
3739
],
3840
},

frontend/package.json

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@
88
"generate": "buf generate",
99
"preview": "nuxt preview",
1010
"postinstall": "nuxt prepare",
11-
"check": "nuxi typecheck"
11+
"check": "nuxi typecheck",
12+
"test": "vitest",
13+
"test:run": "vitest run",
14+
"lint": "eslint .",
15+
"lint:fix": "eslint . --fix"
1216
},
1317
"dependencies": {
1418
"@bufbuild/buf": "^1.59.0",
@@ -33,6 +37,14 @@
3337
"vue-router": "^4.6.3"
3438
},
3539
"devDependencies": {
36-
"tailwindcss": "^4.1.18"
40+
"@nuxt/eslint": "^1.14.0",
41+
"@nuxt/test-utils": "^4.0.0",
42+
"@vitejs/plugin-vue": "^6.0.4",
43+
"@vue/test-utils": "^2.4.6",
44+
"eslint": "^10.0.0",
45+
"happy-dom": "^20.5.1",
46+
"tailwindcss": "^4.1.18",
47+
"unplugin-auto-import": "^21.0.0",
48+
"vitest": "^4.0.18"
3749
}
3850
}

frontend/vitest.config.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { defineConfig } from 'vitest/config'
2+
import vue from '@vitejs/plugin-vue'
3+
import AutoImport from 'unplugin-auto-import/vite'
4+
import { resolve } from 'path'
5+
6+
export default defineConfig({
7+
plugins: [
8+
vue(),
9+
AutoImport({
10+
imports: ['vue'],
11+
}),
12+
],
13+
resolve: {
14+
alias: {
15+
'@': resolve(__dirname, 'app'),
16+
'~': resolve(__dirname, 'app'),
17+
},
18+
},
19+
test: {
20+
environment: 'happy-dom',
21+
include: ['**/__tests__/**/*.test.ts'],
22+
},
23+
})

0 commit comments

Comments
 (0)