ace-test 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.ace-defaults/nav/protocols/agent-sources/ace-test.yml +19 -0
- data/.ace-defaults/nav/protocols/guide-sources/ace-test.yml +19 -0
- data/.ace-defaults/nav/protocols/tmpl-sources/ace-test.yml +11 -0
- data/.ace-defaults/nav/protocols/wfi-sources/ace-test.yml +19 -0
- data/CHANGELOG.md +169 -0
- data/LICENSE +21 -0
- data/README.md +40 -0
- data/Rakefile +12 -0
- data/handbook/agents/mock.ag.md +164 -0
- data/handbook/agents/profile-tests.ag.md +132 -0
- data/handbook/agents/test.ag.md +99 -0
- data/handbook/guides/SUMMARY.md +95 -0
- data/handbook/guides/embedded-testing-guide.g.md +261 -0
- data/handbook/guides/mocking-patterns.g.md +464 -0
- data/handbook/guides/quick-reference.g.md +46 -0
- data/handbook/guides/test-driven-development-cycle/meta-documentation.md +26 -0
- data/handbook/guides/test-driven-development-cycle/ruby-application.md +18 -0
- data/handbook/guides/test-driven-development-cycle/ruby-gem.md +19 -0
- data/handbook/guides/test-driven-development-cycle/rust-cli.md +18 -0
- data/handbook/guides/test-driven-development-cycle/rust-wasm-zed.md +19 -0
- data/handbook/guides/test-driven-development-cycle/typescript-nuxt.md +18 -0
- data/handbook/guides/test-driven-development-cycle/typescript-vue.md +19 -0
- data/handbook/guides/test-layer-decision.g.md +261 -0
- data/handbook/guides/test-mocking-patterns.g.md +414 -0
- data/handbook/guides/test-organization.g.md +140 -0
- data/handbook/guides/test-performance.g.md +353 -0
- data/handbook/guides/test-responsibility-map.g.md +220 -0
- data/handbook/guides/test-review-checklist.g.md +231 -0
- data/handbook/guides/test-suite-health.g.md +337 -0
- data/handbook/guides/testable-code-patterns.g.md +315 -0
- data/handbook/guides/testing/ruby-rspec-config-examples.md +120 -0
- data/handbook/guides/testing/ruby-rspec.md +87 -0
- data/handbook/guides/testing/rust.md +52 -0
- data/handbook/guides/testing/test-maintenance.md +364 -0
- data/handbook/guides/testing/typescript-bun.md +47 -0
- data/handbook/guides/testing/vue-firebase-auth.md +546 -0
- data/handbook/guides/testing/vue-vitest.md +236 -0
- data/handbook/guides/testing-philosophy.g.md +82 -0
- data/handbook/guides/testing-strategy.g.md +151 -0
- data/handbook/guides/testing-tdd-cycle.g.md +146 -0
- data/handbook/guides/testing.g.md +170 -0
- data/handbook/skills/as-test-create-cases/SKILL.md +24 -0
- data/handbook/skills/as-test-fix/SKILL.md +26 -0
- data/handbook/skills/as-test-improve-coverage/SKILL.md +22 -0
- data/handbook/skills/as-test-optimize/SKILL.md +34 -0
- data/handbook/skills/as-test-performance-audit/SKILL.md +34 -0
- data/handbook/skills/as-test-plan/SKILL.md +34 -0
- data/handbook/skills/as-test-review/SKILL.md +34 -0
- data/handbook/skills/as-test-verify-suite/SKILL.md +45 -0
- data/handbook/templates/e2e-sandbox-checklist.template.md +289 -0
- data/handbook/templates/test-case.template.md +56 -0
- data/handbook/templates/test-performance-audit.template.md +132 -0
- data/handbook/templates/test-responsibility-map.template.md +92 -0
- data/handbook/templates/test-review-checklist.template.md +163 -0
- data/handbook/workflow-instructions/test/analyze-failures.wf.md +120 -0
- data/handbook/workflow-instructions/test/create-cases.wf.md +675 -0
- data/handbook/workflow-instructions/test/fix.wf.md +120 -0
- data/handbook/workflow-instructions/test/improve-coverage.wf.md +370 -0
- data/handbook/workflow-instructions/test/optimize.wf.md +368 -0
- data/handbook/workflow-instructions/test/performance-audit.wf.md +17 -0
- data/handbook/workflow-instructions/test/plan.wf.md +323 -0
- data/handbook/workflow-instructions/test/review.wf.md +16 -0
- data/handbook/workflow-instructions/test/verify-suite.wf.md +343 -0
- data/lib/ace/test/version.rb +7 -0
- data/lib/ace/test.rb +10 -0
- metadata +152 -0
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
---
|
|
2
|
+
doc-type: guide
|
|
3
|
+
title: Vue.js 3 + Firebase Authentication Testing Guide
|
|
4
|
+
purpose: Vue Firebase authentication testing
|
|
5
|
+
ace-docs:
|
|
6
|
+
last-updated: 2026-01-23
|
|
7
|
+
last-checked: 2026-03-21
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Vue.js 3 + Firebase Authentication Testing Guide
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
|
|
14
|
+
This guide provides comprehensive testing strategies for Vue.js 3 applications using Firebase Authentication with Vitest, covering both mock-based unit tests and integration tests with Firebase Emulator Suite.
|
|
15
|
+
|
|
16
|
+
## Testing Philosophy
|
|
17
|
+
|
|
18
|
+
Firebase Authentication testing follows a two-layer approach:
|
|
19
|
+
|
|
20
|
+
1. **Unit/Component Tests (Mocked)** - Fast, offline tests focusing on application logic
|
|
21
|
+
2. **Integration Tests (Emulator)** - Slower but comprehensive tests using Firebase Emulator Suite
|
|
22
|
+
|
|
23
|
+
## Mock-Based Testing Setup
|
|
24
|
+
|
|
25
|
+
### Firebase Auth Mock Module
|
|
26
|
+
|
|
27
|
+
Create a dedicated mock module for Firebase Authentication:
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
// tests/__mocks__/firebase-auth.ts
|
|
31
|
+
import { vi } from 'vitest'
|
|
32
|
+
|
|
33
|
+
export const fakeUser = {
|
|
34
|
+
uid: 'test-user-123',
|
|
35
|
+
email: 'test@example.com',
|
|
36
|
+
displayName: 'Test User',
|
|
37
|
+
emailVerified: true
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const getAuth = vi.fn(() => ({
|
|
41
|
+
currentUser: fakeUser,
|
|
42
|
+
}))
|
|
43
|
+
|
|
44
|
+
export const signInWithEmailAndPassword = vi.fn(async () => ({
|
|
45
|
+
user: fakeUser,
|
|
46
|
+
}))
|
|
47
|
+
|
|
48
|
+
export const signOut = vi.fn(async () => {})
|
|
49
|
+
|
|
50
|
+
export const onAuthStateChanged = vi.fn((auth, callback) => {
|
|
51
|
+
callback(fakeUser)
|
|
52
|
+
return vi.fn() // Unsubscribe function
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
export const signInWithPopup = vi.fn(async () => ({
|
|
56
|
+
user: fakeUser,
|
|
57
|
+
}))
|
|
58
|
+
|
|
59
|
+
export const GoogleAuthProvider = vi.fn()
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Vitest Configuration
|
|
63
|
+
|
|
64
|
+
Configure Vitest to use the mock:
|
|
65
|
+
|
|
66
|
+
```javascript
|
|
67
|
+
// vitest.config.js
|
|
68
|
+
import { defineConfig } from 'vitest/config'
|
|
69
|
+
|
|
70
|
+
export default defineConfig({
|
|
71
|
+
test: {
|
|
72
|
+
environment: 'jsdom',
|
|
73
|
+
setupFiles: ['./tests/setup.ts']
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
// tests/setup.ts
|
|
80
|
+
import { vi } from 'vitest'
|
|
81
|
+
|
|
82
|
+
// Mock Firebase Auth before any imports
|
|
83
|
+
vi.mock('firebase/auth', () => import('./__mocks__/firebase-auth'))
|
|
84
|
+
|
|
85
|
+
// Reset mocks between tests
|
|
86
|
+
afterEach(() => {
|
|
87
|
+
vi.resetAllMocks()
|
|
88
|
+
})
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Testing Vue Components with Authentication
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
// tests/components/LoginForm.test.ts
|
|
95
|
+
import { mount } from '@vue/test-utils'
|
|
96
|
+
import { createTestingPinia } from '@pinia/testing'
|
|
97
|
+
import LoginForm from '@/components/auth/LoginForm.vue'
|
|
98
|
+
import { signInWithEmailAndPassword } from 'firebase/auth'
|
|
99
|
+
|
|
100
|
+
describe('LoginForm', () => {
|
|
101
|
+
it('handles login successfully', async () => {
|
|
102
|
+
const wrapper = mount(LoginForm, {
|
|
103
|
+
global: {
|
|
104
|
+
plugins: [createTestingPinia()]
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
await wrapper.find('[data-testid="email"]').setValue('test@example.com')
|
|
109
|
+
await wrapper.find('[data-testid="password"]').setValue('password')
|
|
110
|
+
await wrapper.find('[data-testid="login-form"]').trigger('submit')
|
|
111
|
+
|
|
112
|
+
expect(signInWithEmailAndPassword).toHaveBeenCalledWith(
|
|
113
|
+
expect.anything(),
|
|
114
|
+
'test@example.com',
|
|
115
|
+
'password'
|
|
116
|
+
)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('displays error message on failed login', async () => {
|
|
120
|
+
signInWithEmailAndPassword.mockRejectedValueOnce({
|
|
121
|
+
code: 'auth/invalid-password',
|
|
122
|
+
message: 'Invalid password'
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
const wrapper = mount(LoginForm, {
|
|
126
|
+
global: {
|
|
127
|
+
plugins: [createTestingPinia()]
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
await wrapper.find('[data-testid="login-form"]').trigger('submit')
|
|
132
|
+
await wrapper.vm.$nextTick()
|
|
133
|
+
|
|
134
|
+
expect(wrapper.find('[data-testid="error-message"]').text())
|
|
135
|
+
.toContain('Invalid password')
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Testing Pinia Stores
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
// tests/stores/userStore.test.ts
|
|
144
|
+
import { setActivePinia, createPinia } from 'pinia'
|
|
145
|
+
import { useUserStore } from '@/stores/userStore'
|
|
146
|
+
import { signInWithEmailAndPassword, signOut } from 'firebase/auth'
|
|
147
|
+
|
|
148
|
+
describe('userStore', () => {
|
|
149
|
+
beforeEach(() => {
|
|
150
|
+
setActivePinia(createPinia())
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('logs in user successfully', async () => {
|
|
154
|
+
const store = useUserStore()
|
|
155
|
+
|
|
156
|
+
await store.login('test@example.com', 'password')
|
|
157
|
+
|
|
158
|
+
expect(signInWithEmailAndPassword).toHaveBeenCalledWith(
|
|
159
|
+
expect.anything(),
|
|
160
|
+
'test@example.com',
|
|
161
|
+
'password'
|
|
162
|
+
)
|
|
163
|
+
expect(store.user).toEqual(expect.objectContaining({
|
|
164
|
+
email: 'test@example.com'
|
|
165
|
+
}))
|
|
166
|
+
expect(store.isAuthenticated).toBe(true)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('handles logout correctly', async () => {
|
|
170
|
+
const store = useUserStore()
|
|
171
|
+
store.user = { uid: '123', email: 'test@example.com' }
|
|
172
|
+
|
|
173
|
+
await store.logout()
|
|
174
|
+
|
|
175
|
+
expect(signOut).toHaveBeenCalled()
|
|
176
|
+
expect(store.user).toBeNull()
|
|
177
|
+
expect(store.isAuthenticated).toBe(false)
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Testing Vue Router Guards
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
// tests/router/authGuards.test.ts
|
|
186
|
+
import { createRouter, createWebHistory } from 'vue-router'
|
|
187
|
+
import { useUserStore } from '@/stores/userStore'
|
|
188
|
+
import { createTestingPinia } from '@pinia/testing'
|
|
189
|
+
|
|
190
|
+
describe('Auth Guards', () => {
|
|
191
|
+
it('redirects unauthenticated users to login', async () => {
|
|
192
|
+
const pinia = createTestingPinia()
|
|
193
|
+
const userStore = useUserStore(pinia)
|
|
194
|
+
userStore.isAuthenticated = false
|
|
195
|
+
|
|
196
|
+
const router = createRouter({
|
|
197
|
+
history: createWebHistory(),
|
|
198
|
+
routes: [
|
|
199
|
+
{ path: '/login', component: { template: '<div>Login</div>' } },
|
|
200
|
+
{
|
|
201
|
+
path: '/dashboard',
|
|
202
|
+
component: { template: '<div>Dashboard</div>' },
|
|
203
|
+
beforeEnter: requireAuth
|
|
204
|
+
}
|
|
205
|
+
]
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
await router.push('/dashboard')
|
|
209
|
+
|
|
210
|
+
expect(router.currentRoute.value.path).toBe('/login')
|
|
211
|
+
})
|
|
212
|
+
})
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Firebase Emulator Integration Testing
|
|
216
|
+
|
|
217
|
+
### Emulator Setup
|
|
218
|
+
|
|
219
|
+
```json
|
|
220
|
+
{
|
|
221
|
+
"scripts": {
|
|
222
|
+
"test:integration": "firebase emulators:exec --only auth,firestore \"vitest run --config vitest.integration.config.js\"",
|
|
223
|
+
"test:emulator": "firebase emulators:start --only auth,firestore"
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Integration Test Configuration
|
|
229
|
+
|
|
230
|
+
```javascript
|
|
231
|
+
// vitest.integration.config.js
|
|
232
|
+
import { defineConfig } from 'vitest/config'
|
|
233
|
+
|
|
234
|
+
export default defineConfig({
|
|
235
|
+
test: {
|
|
236
|
+
environment: 'jsdom',
|
|
237
|
+
setupFiles: ['./tests/integration/setup.ts'],
|
|
238
|
+
testTimeout: 10000
|
|
239
|
+
}
|
|
240
|
+
})
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
// tests/integration/setup.ts
|
|
245
|
+
import { initializeApp } from 'firebase/app'
|
|
246
|
+
import { getAuth, connectAuthEmulator } from 'firebase/auth'
|
|
247
|
+
import { getFirestore, connectFirestoreEmulator } from 'firebase/firestore'
|
|
248
|
+
|
|
249
|
+
// Initialize Firebase for testing
|
|
250
|
+
const app = initializeApp({
|
|
251
|
+
projectId: 'demo-test-project',
|
|
252
|
+
apiKey: 'fake-api-key'
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
const auth = getAuth(app)
|
|
256
|
+
const db = getFirestore(app)
|
|
257
|
+
|
|
258
|
+
// Connect to emulators
|
|
259
|
+
connectAuthEmulator(auth, 'http://localhost:9099', { disableWarnings: true })
|
|
260
|
+
connectFirestoreEmulator(db, 'localhost', 8080)
|
|
261
|
+
|
|
262
|
+
export { auth, db }
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Integration Test Examples
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
// tests/integration/auth.test.ts
|
|
269
|
+
import { createUserWithEmailAndPassword, signInWithEmailAndPassword } from 'firebase/auth'
|
|
270
|
+
import { auth } from './setup'
|
|
271
|
+
|
|
272
|
+
describe('Firebase Auth Integration', () => {
|
|
273
|
+
beforeEach(async () => {
|
|
274
|
+
// Clear auth state before each test
|
|
275
|
+
await fetch('http://localhost:9099/emulator/v1/projects/demo-test-project/accounts', {
|
|
276
|
+
method: 'DELETE'
|
|
277
|
+
})
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
it('creates and authenticates user', async () => {
|
|
281
|
+
// Create user
|
|
282
|
+
const userCredential = await createUserWithEmailAndPassword(
|
|
283
|
+
auth,
|
|
284
|
+
'test@example.com',
|
|
285
|
+
'password123'
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
expect(userCredential.user.email).toBe('test@example.com')
|
|
289
|
+
expect(userCredential.user.emailVerified).toBe(false)
|
|
290
|
+
|
|
291
|
+
// Sign out
|
|
292
|
+
await auth.signOut()
|
|
293
|
+
|
|
294
|
+
// Sign in again
|
|
295
|
+
const signInCredential = await signInWithEmailAndPassword(
|
|
296
|
+
auth,
|
|
297
|
+
'test@example.com',
|
|
298
|
+
'password123'
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
expect(signInCredential.user.uid).toBe(userCredential.user.uid)
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
it('handles invalid credentials correctly', async () => {
|
|
305
|
+
await expect(
|
|
306
|
+
signInWithEmailAndPassword(auth, 'nonexistent@example.com', 'wrongpassword')
|
|
307
|
+
).rejects.toThrow()
|
|
308
|
+
})
|
|
309
|
+
})
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Security Rules Testing
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
// tests/integration/securityRules.test.ts
|
|
316
|
+
import { doc, getDoc, setDoc } from 'firebase/firestore'
|
|
317
|
+
import { db, auth } from './setup'
|
|
318
|
+
import { signInWithEmailAndPassword } from 'firebase/auth'
|
|
319
|
+
|
|
320
|
+
describe('Firestore Security Rules', () => {
|
|
321
|
+
it('allows users to read their own profile', async () => {
|
|
322
|
+
// Create and sign in user
|
|
323
|
+
const user = await createTestUser('user@example.com', 'password')
|
|
324
|
+
|
|
325
|
+
// Try to read user's own profile
|
|
326
|
+
const profileRef = doc(db, 'users', user.uid)
|
|
327
|
+
const profileSnap = await getDoc(profileRef)
|
|
328
|
+
|
|
329
|
+
expect(profileSnap.exists()).toBe(true)
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
it('denies access to other users profiles', async () => {
|
|
333
|
+
const user1 = await createTestUser('user1@example.com', 'password')
|
|
334
|
+
const user2 = await createTestUser('user2@example.com', 'password')
|
|
335
|
+
|
|
336
|
+
// Sign in as user1
|
|
337
|
+
await signInWithEmailAndPassword(auth, 'user1@example.com', 'password')
|
|
338
|
+
|
|
339
|
+
// Try to read user2's profile
|
|
340
|
+
const otherProfileRef = doc(db, 'users', user2.uid)
|
|
341
|
+
|
|
342
|
+
await expect(getDoc(otherProfileRef)).rejects.toThrow()
|
|
343
|
+
})
|
|
344
|
+
})
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
## Test Helpers and Utilities
|
|
348
|
+
|
|
349
|
+
### Authentication Test Helpers
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
// tests/helpers/authHelpers.ts
|
|
353
|
+
import { createUserWithEmailAndPassword, signInWithEmailAndPassword } from 'firebase/auth'
|
|
354
|
+
import { auth } from '../integration/setup'
|
|
355
|
+
|
|
356
|
+
export async function createTestUser(email: string, password: string) {
|
|
357
|
+
const userCredential = await createUserWithEmailAndPassword(auth, email, password)
|
|
358
|
+
return userCredential.user
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export async function signInTestUser(email: string, password: string) {
|
|
362
|
+
const userCredential = await signInWithEmailAndPassword(auth, email, password)
|
|
363
|
+
return userCredential.user
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export async function clearAuthEmulator() {
|
|
367
|
+
await fetch('http://localhost:9099/emulator/v1/projects/demo-test-project/accounts', {
|
|
368
|
+
method: 'DELETE'
|
|
369
|
+
})
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### Vue Component Test Helpers
|
|
374
|
+
|
|
375
|
+
```typescript
|
|
376
|
+
// tests/helpers/vueHelpers.ts
|
|
377
|
+
import { mount } from '@vue/test-utils'
|
|
378
|
+
import { createTestingPinia } from '@pinia/testing'
|
|
379
|
+
import { createRouter, createWebHistory } from 'vue-router'
|
|
380
|
+
|
|
381
|
+
export function createTestWrapper(component: any, options: any = {}) {
|
|
382
|
+
const router = createRouter({
|
|
383
|
+
history: createWebHistory(),
|
|
384
|
+
routes: [
|
|
385
|
+
{ path: '/', component: { template: '<div>Home</div>' } },
|
|
386
|
+
{ path: '/login', component: { template: '<div>Login</div>' } }
|
|
387
|
+
]
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
return mount(component, {
|
|
391
|
+
global: {
|
|
392
|
+
plugins: [
|
|
393
|
+
createTestingPinia({
|
|
394
|
+
stubActions: false,
|
|
395
|
+
...options.pinia
|
|
396
|
+
}),
|
|
397
|
+
router
|
|
398
|
+
],
|
|
399
|
+
...options.global
|
|
400
|
+
},
|
|
401
|
+
...options
|
|
402
|
+
})
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
## CI/CD Integration
|
|
407
|
+
|
|
408
|
+
### GitHub Actions Workflow
|
|
409
|
+
|
|
410
|
+
```yaml
|
|
411
|
+
# .github/workflows/test.yml
|
|
412
|
+
name: Test
|
|
413
|
+
on: [push, pull_request]
|
|
414
|
+
|
|
415
|
+
jobs:
|
|
416
|
+
test:
|
|
417
|
+
runs-on: ubuntu-latest
|
|
418
|
+
steps:
|
|
419
|
+
- uses: actions/checkout@v4
|
|
420
|
+
|
|
421
|
+
- uses: actions/setup-node@v4
|
|
422
|
+
with:
|
|
423
|
+
node-version: '20'
|
|
424
|
+
cache: 'npm'
|
|
425
|
+
|
|
426
|
+
- uses: actions/setup-java@v4
|
|
427
|
+
with:
|
|
428
|
+
distribution: 'temurin'
|
|
429
|
+
java-version: '17'
|
|
430
|
+
|
|
431
|
+
- run: npm ci
|
|
432
|
+
|
|
433
|
+
- name: Run unit tests
|
|
434
|
+
run: npm run test:unit
|
|
435
|
+
|
|
436
|
+
- name: Run integration tests
|
|
437
|
+
run: npm run test:integration
|
|
438
|
+
|
|
439
|
+
- name: Upload coverage
|
|
440
|
+
uses: codecov/codecov-action@v3
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
### NPM Scripts
|
|
444
|
+
|
|
445
|
+
```json
|
|
446
|
+
{
|
|
447
|
+
"scripts": {
|
|
448
|
+
"test": "vitest",
|
|
449
|
+
"test:unit": "vitest run",
|
|
450
|
+
"test:integration": "firebase emulators:exec --only auth,firestore \"vitest run --config vitest.integration.config.js\"",
|
|
451
|
+
"test:coverage": "vitest run --coverage",
|
|
452
|
+
"test:watch": "vitest watch"
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
## Best Practices
|
|
458
|
+
|
|
459
|
+
### Test Organization
|
|
460
|
+
|
|
461
|
+
1. **Separate Unit and Integration Tests**
|
|
462
|
+
- Unit tests in `tests/unit/`
|
|
463
|
+
- Integration tests in `tests/integration/`
|
|
464
|
+
- Shared helpers in `tests/helpers/`
|
|
465
|
+
|
|
466
|
+
2. **Mock Strategy**
|
|
467
|
+
- Mock Firebase SDK for unit tests
|
|
468
|
+
- Use real Firebase Emulator for integration tests
|
|
469
|
+
- Mock external APIs and services
|
|
470
|
+
|
|
471
|
+
3. **Test Data Management**
|
|
472
|
+
- Use factories for test data creation
|
|
473
|
+
- Clear emulator state between tests
|
|
474
|
+
- Use meaningful test data that reflects real usage
|
|
475
|
+
|
|
476
|
+
### Error Handling Testing
|
|
477
|
+
|
|
478
|
+
```typescript
|
|
479
|
+
describe('Error Handling', () => {
|
|
480
|
+
it('handles network errors gracefully', async () => {
|
|
481
|
+
signInWithEmailAndPassword.mockRejectedValueOnce(
|
|
482
|
+
new Error('Network error')
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
const store = useUserStore()
|
|
486
|
+
await store.login('test@example.com', 'password')
|
|
487
|
+
|
|
488
|
+
expect(store.error).toContain('Network error')
|
|
489
|
+
expect(store.isLoading).toBe(false)
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
it('handles Firebase auth errors', async () => {
|
|
493
|
+
signInWithEmailAndPassword.mockRejectedValueOnce({
|
|
494
|
+
code: 'auth/user-not-found',
|
|
495
|
+
message: 'User not found'
|
|
496
|
+
})
|
|
497
|
+
|
|
498
|
+
const store = useUserStore()
|
|
499
|
+
await store.login('nonexistent@example.com', 'password')
|
|
500
|
+
|
|
501
|
+
expect(store.error).toContain('User not found')
|
|
502
|
+
})
|
|
503
|
+
})
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
### Performance Testing
|
|
507
|
+
|
|
508
|
+
```typescript
|
|
509
|
+
describe('Authentication Performance', () => {
|
|
510
|
+
it('completes login within acceptable time', async () => {
|
|
511
|
+
const startTime = Date.now()
|
|
512
|
+
|
|
513
|
+
await store.login('test@example.com', 'password')
|
|
514
|
+
|
|
515
|
+
const duration = Date.now() - startTime
|
|
516
|
+
expect(duration).toBeLessThan(1000) // 1 second max
|
|
517
|
+
})
|
|
518
|
+
})
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
## Common Pitfalls and Solutions
|
|
522
|
+
|
|
523
|
+
### Mock Issues
|
|
524
|
+
|
|
525
|
+
- **ESM Side Effects**: Always mock before importing your application code
|
|
526
|
+
- **TypeScript Errors**: Create proper type declarations for mocked modules
|
|
527
|
+
- **Async Callbacks**: Use `vi.fn()` with proper callback simulation
|
|
528
|
+
|
|
529
|
+
### Emulator Issues
|
|
530
|
+
|
|
531
|
+
- **Connection Timing**: Ensure emulator is ready before running tests
|
|
532
|
+
- **State Pollution**: Clear emulator state between test suites
|
|
533
|
+
- **Port Conflicts**: Use dedicated ports for CI environments
|
|
534
|
+
|
|
535
|
+
### Vue-Specific Issues
|
|
536
|
+
|
|
537
|
+
- **Pinia State**: Reset store state between tests
|
|
538
|
+
- **Router Navigation**: Mock or stub router in unit tests
|
|
539
|
+
- **Component Lifecycle**: Use proper Vue Test Utils methods for async operations
|
|
540
|
+
|
|
541
|
+
## Resources
|
|
542
|
+
|
|
543
|
+
- [Vitest Documentation](https://vitest.dev/)
|
|
544
|
+
- [Firebase Emulator Suite](https://firebase.google.com/docs/emulator-suite)
|
|
545
|
+
- [Vue Test Utils](https://test-utils.vuejs.org/)
|
|
546
|
+
- [Pinia Testing](https://pinia.vuejs.org/cookbook/testing.html)
|