Skip to content

Commit 819a19b

Browse files
committed
02/03: add exercise texts
1 parent 70524b6 commit 819a19b

File tree

16 files changed

+326
-8
lines changed

16 files changed

+326
-8
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Global context
2+
3+
Let's follow up on the previous `createMockDatabase()` fixture and make it better.
4+
5+
By default, when you extend your test context via `test.extend()`, you get a _new `test()` function back_ that you should now explicitly use in tests:
6+
7+
```ts nonumber highlight=1
8+
import { test } from '../test-extend'
9+
10+
test('...', ({ yourFixture }) => {}) //
11+
```
12+
13+
Although this explicitness is nice, consistency would be even better. Forgetting to swap the default `test()` with your own can lead to unexpected behaviors, and if you're exposing `test()` globally via `globals: true`, TypeScript won't catch such mistakes either.
14+
15+
But there's another way.
16+
17+
You can apply your custom `test()` _globally_. This will save you one extra line of import in each test file and also guarantee consistent test context everywhere across the board.
18+
19+
## Your task
20+
21+
👨‍💼 That is precisely what you're going to do in this exercise. Follow the instructions in <InlineFile file="text-extend.ts" /> and <InlineFile file="vitest.config.ts" /> to expose your custom test context globally. Once you've got that covered, refactor the existing tests to rely on the global `test()` function instead.
22+
23+
You got this!
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"type": "module",
3+
"name": "exercises_02.context_03.problem.global-context",
4+
"scripts": {
5+
"test": "vitest"
6+
},
7+
"devDependencies": {
8+
"vite": "^6.2.6",
9+
"vitest": "^3.1.1"
10+
},
11+
"dependencies": {
12+
"sqlite3": "^5.1.7"
13+
}
14+
}
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: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// 💣 Remove this explicit `test` import.
2+
import { test } from '../test-extend'
3+
import { queryUser } from './query-user'
4+
5+
test('throws if the user is not found', async () => {
6+
await expect(queryUser('abc-123')).resolves.toBeUndefined()
7+
})
8+
9+
test('returns the user by id', async ({ createMockDatabase }) => {
10+
await createMockDatabase((db, done) => {
11+
db.run(
12+
'INSERT INTO users (id, name) VALUES (?, ?)',
13+
['abc-123', 'John Doe'],
14+
done,
15+
)
16+
})
17+
18+
await expect(queryUser('abc-123')).resolves.toEqual({
19+
id: 'abc-123',
20+
name: 'John Doe',
21+
})
22+
})
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: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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+
// 🐨 Augment the module declaration for "vitest".
14+
// 💰 declare module 'vitest' {}
15+
16+
// 🐨 In the module declaration, declare an interface called `TestContext`
17+
// and make it extend your custom `Fixtures` interface. Then, export it.
18+
// This will annotate the custom test content of the global `test()` function.
19+
// 💰 export interface A extends B {}
20+
21+
// 🐨 Declare a namespace for `globalThis`.
22+
// Inside that namespace, declare a variable called `test`
23+
// and assign it the type of `testBase` function.
24+
// This annotate the `globalThis.test` function so your extension below
25+
// would be type-safe.
26+
// 💰 declare namespace globalThis {}
27+
// 💰 var test: typeof testBase
28+
29+
type CallbackHandle = (error?: Error | null, ...args: Array<any>) => void
30+
31+
function toPromise<T>(init: (handle: CallbackHandle) => T): Promise<T> {
32+
return new Promise<T>((resolve, reject) => {
33+
const result = init((error) => {
34+
if (error) {
35+
return reject(error)
36+
}
37+
resolve(result)
38+
})
39+
})
40+
}
41+
42+
// 💣 Finally, delete the `test` variable declaration
43+
// and replace it with assigning to `globalThis.test`.
44+
// 💰 globalThis.test = testBase.extend<T>({})
45+
export const test = testBase.extend<Fixtures>({
46+
createMockDatabase: [
47+
async ({ task, onTestFinished }, use) => {
48+
const databasePath = `${task.file.filepath}-${task.id}.sqlite`
49+
50+
if (fs.existsSync(databasePath)) {
51+
await fs.promises.rm(databasePath)
52+
}
53+
54+
const mockDatabase = await toPromise((handle) => {
55+
return new sqlite3.Database(databasePath, handle)
56+
})
57+
58+
onTestFinished(async ({ task }) => {
59+
await toPromise((handle) => mockDatabase.close(handle))
60+
61+
if (task.type !== 'test') {
62+
return
63+
}
64+
65+
if (task.result?.state === 'pass') {
66+
await fs.promises.rm(databasePath)
67+
} else {
68+
task.result?.errors?.push({
69+
name: 'Mock database',
70+
message: 'See the database state:',
71+
codeFrame: path.relative(process.cwd(), databasePath),
72+
})
73+
}
74+
})
75+
76+
const clientSpy = vi
77+
.spyOn(databaseModule, 'client', 'get')
78+
.mockReturnValue(new databaseModule.DatabaseClient(mockDatabase))
79+
80+
await toPromise((handle) => {
81+
mockDatabase.run('CREATE TABLE users (id TEXT, name TEXT)', handle)
82+
})
83+
84+
await use((seed) => {
85+
return toPromise((handle) => {
86+
seed(mockDatabase, handle)
87+
})
88+
})
89+
90+
clientSpy.mockRestore()
91+
},
92+
{
93+
auto: true,
94+
},
95+
],
96+
})
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"extends": "./tsconfig.base.json",
3+
"include": ["src/**/*"],
4+
"exclude": ["src/**/*.test.ts*"],
5+
"compilerOptions": {
6+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
7+
"jsx": "react-jsx"
8+
}
9+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ESNext",
4+
"module": "ESNext",
5+
"useDefineForClassFields": true,
6+
"skipLibCheck": true,
7+
8+
/* Bundler mode */
9+
"moduleResolution": "bundler",
10+
"allowImportingTsExtensions": true,
11+
"isolatedModules": true,
12+
"moduleDetection": "force",
13+
"noEmit": true,
14+
"verbatimModuleSyntax": true,
15+
16+
/* Linting */
17+
"strict": true,
18+
"noUnusedLocals": false,
19+
"noUnusedParameters": false,
20+
"noFallthroughCasesInSwitch": true,
21+
"noUncheckedSideEffectImports": true
22+
}
23+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"files": [],
3+
"references": [
4+
{ "path": "./tsconfig.app.json" },
5+
{ "path": "./tsconfig.node.json" },
6+
{ "path": "./tsconfig.test.json" }
7+
]
8+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"extends": "./tsconfig.base.json",
3+
"compilerOptions": {
4+
"lib": ["ES2023"]
5+
},
6+
"include": ["vitest.config.ts"]
7+
}

0 commit comments

Comments
 (0)