Skip to content

Commit b370e75

Browse files
committed
03/03: add solution
1 parent 6800b71 commit b370e75

File tree

8 files changed

+139
-3
lines changed

8 files changed

+139
-3
lines changed
Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1 @@
11
# Global context
2-
3-
- Show how to expose the custom `test` function globally instead of importing it everywhere.

exercises/03.context/03.solution.global-context/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,8 @@
99
"devDependencies": {
1010
"vite": "^6.0.7",
1111
"vitest": "^3.0.5"
12+
},
13+
"dependencies": {
14+
"sqlite3": "^5.1.7"
1215
}
1316
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import sqlite3, { type Database } from 'sqlite3'
2+
3+
export class DatabaseClient {
4+
constructor(private readonly db: Database) {}
5+
6+
public async query(query: string, params: Array<unknown>): Promise<any> {
7+
return new Promise((resolve, reject) => {
8+
this.db.get(query, params, function (error, row) {
9+
if (error) {
10+
return reject(error)
11+
}
12+
13+
resolve(row)
14+
})
15+
})
16+
}
17+
}
18+
19+
export const client = new DatabaseClient(new sqlite3.Database(':memory:'))
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { queryUser } from './query-user'
2+
3+
test('throws if the user is not found', async () => {
4+
await expect(queryUser('abc-123')).resolves.toBeUndefined()
5+
})
6+
7+
test('returns the user by id', async ({ createMockDatabase }) => {
8+
await createMockDatabase((db, done) => {
9+
db.run(
10+
'INSERT INTO users (id, name) VALUES (?, ?)',
11+
['abc-123', 'John Doe'],
12+
done,
13+
)
14+
})
15+
16+
await expect(queryUser('abc-123')).resolves.toEqual({
17+
id: 'abc-123',
18+
name: 'John Doe',
19+
})
20+
})
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { client } from './database'
2+
3+
export interface User {
4+
id: string
5+
name: string
6+
}
7+
8+
export async function queryUser(id: string): Promise<User> {
9+
return await client.query(`SELECT * FROM users WHERE id = ?`, [id])
10+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import * as fs from 'node:fs'
2+
import * as path from 'node:path'
3+
import { test as testBase } from 'vitest'
4+
import sqlite3, { type Database } from 'sqlite3'
5+
import * as databaseModule from './src/database'
6+
7+
interface Fixtures {
8+
createMockDatabase: (
9+
seed: (database: Database, handle: CallbackHandle) => void,
10+
) => Promise<void>
11+
}
12+
13+
declare module 'vitest' {
14+
export interface TestContext extends Fixtures {}
15+
}
16+
17+
declare namespace globalThis {
18+
var test: typeof testBase
19+
}
20+
21+
type CallbackHandle = (error?: Error | null, ...args: Array<any>) => void
22+
23+
function toPromise<T>(init: (handle: CallbackHandle) => T): Promise<T> {
24+
return new Promise<T>((resolve, reject) => {
25+
const result = init((error) => {
26+
if (error) {
27+
return reject(error)
28+
}
29+
resolve(result)
30+
})
31+
})
32+
}
33+
34+
globalThis.test = testBase.extend<Fixtures>({
35+
createMockDatabase: [
36+
async ({ task, onTestFinished }, use) => {
37+
const dbFile = `${task.file.filepath}-${task.id}.sqlite`
38+
39+
if (fs.existsSync(dbFile)) {
40+
await fs.promises.rm(dbFile)
41+
}
42+
43+
const mockDatabase = await toPromise((handle) => {
44+
return new sqlite3.Database(dbFile, handle)
45+
})
46+
47+
onTestFinished(async ({ task }) => {
48+
await toPromise((handle) => mockDatabase.close(handle))
49+
50+
if (task.type !== 'test') {
51+
return
52+
}
53+
54+
if (task.result?.state === 'pass') {
55+
await fs.promises.rm(dbFile)
56+
} else {
57+
task.result?.errors?.push({
58+
name: 'Mock database',
59+
message: 'See the database state:',
60+
codeFrame: path.relative(process.cwd(), dbFile),
61+
})
62+
}
63+
})
64+
65+
const clientSpy = vi
66+
.spyOn(databaseModule, 'client', 'get')
67+
.mockReturnValue(new databaseModule.DatabaseClient(mockDatabase))
68+
69+
await toPromise((handle) => {
70+
mockDatabase.run('CREATE TABLE users (id TEXT, name TEXT)', handle)
71+
})
72+
73+
await use((seed) => {
74+
return toPromise((handle) => {
75+
seed(mockDatabase, handle)
76+
})
77+
})
78+
79+
clientSpy.mockRestore()
80+
},
81+
{
82+
auto: true,
83+
},
84+
],
85+
})

exercises/03.context/03.solution.global-context/tsconfig.test.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"extends": "./tsconfig.app.json",
3-
"include": ["src/**/*", "src/**/*.test.ts*"],
3+
"include": ["test-extend.ts", "src/**/*", "src/**/*.test.ts*"],
44
"exclude": [],
55
"compilerOptions": {
66
"types": ["vitest/globals"]

exercises/03.context/03.solution.global-context/vite.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ export default defineConfig({
77
},
88
test: {
99
globals: true,
10+
setupFiles: ['./test-extend.ts'],
1011
},
1112
})

0 commit comments

Comments
 (0)