Skip to content

Commit 95bf657

Browse files
authoredDec 18, 2024··
feat!: replace ava with vitest (#554)
BREAKING CHANGE: Requires Node.js >=18.0.0
1 parent 53b7bd0 commit 95bf657

9 files changed

+7076
-12568
lines changed
 

‎.github/workflows/workflow.yml

+3-3
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ jobs:
1313
strategy:
1414
matrix:
1515
os: [ubuntu-latest, macOS-latest, windows-latest]
16-
node-version: [14.0.0, '*']
16+
node-version: [18.0.0, '*']
1717
exclude:
1818
- os: macOS-latest
19-
node-version: 14.0.0
19+
node-version: 18.0.0
2020
- os: windows-latest
21-
node-version: 14.0.0
21+
node-version: 18.0.0
2222
fail-fast: false
2323
steps:
2424
- name: Git checkout

‎package-lock.json

+6,967-12,472
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

+7-12
Original file line numberDiff line numberDiff line change
@@ -50,21 +50,15 @@
5050
"format:fix:prettier": "cross-env-shell prettier --write $npm_package_config_prettier",
5151
"test:dev": "run-s build test:dev:*",
5252
"test:ci": "run-s test:ci:*",
53-
"test:dev:ava": "ava",
53+
"test:dev:vitest": "vitest",
5454
"test:dev:tsd": "tsd",
5555
"test:publish": "publint && attw --pack",
56-
"test:ci:ava": "nyc -r lcovonly -r text -r json ava"
56+
"test:ci:vitest": "vitest run --coverage"
5757
},
5858
"config": {
5959
"eslint": "--ignore-pattern README.md --ignore-path .gitignore --cache --format=codeframe --max-warnings=0 \"{src,scripts,.github,test}/**/*.{ts,js,md,html}\" \"*.{ts,js,md,html}\" \".*.{ts,js,md,html}\"",
6060
"prettier": "--ignore-path .gitignore --loglevel=warn \"{src,scripts,.github}/**/*.{ts,js,md,yml,json,html}\" \"*.{ts,js,yml,json,html}\" \".*.{ts,js,yml,json,html}\" \"!**/package-lock.json\" \"!package-lock.json\""
6161
},
62-
"ava": {
63-
"files": [
64-
"test/unit/*.js"
65-
],
66-
"verbose": true
67-
},
6862
"tsd": {
6963
"directory": "test/types/"
7064
},
@@ -86,17 +80,18 @@
8680
"@commitlint/cli": "^17.0.0",
8781
"@commitlint/config-conventional": "^17.0.0",
8882
"@netlify/eslint-config-node": "^7.0.1",
89-
"ava": "^2.4.0",
83+
"@types/semver": "^7.5.8",
84+
"@vitest/coverage-v8": "^2.1.8",
9085
"husky": "^7.0.4",
9186
"npm-run-all2": "^5.0.0",
92-
"nyc": "^15.0.0",
9387
"publint": "^0.2.7",
9488
"semver": "^7.5.4",
9589
"tsd": "^0.31.0",
9690
"tsup": "^8.0.2",
97-
"typescript": "^4.4.4"
91+
"typescript": "^4.4.4",
92+
"vitest": "^2.1.8"
9893
},
9994
"engines": {
100-
"node": ">=14.0.0"
95+
"node": ">=18.0.0"
10196
}
10297
}

‎test/unit/builder.js ‎src/lib/builder.test.ts

+39-27
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
1-
const test = require('ava')
1+
import { expect, test } from 'vitest'
22

3-
const { builder } = require('../../dist/lib/builder')
4-
const { invokeLambda } = require('../helpers/main')
3+
import { invokeLambda } from '../../test/helpers/main.mjs'
4+
import { BaseHandler } from '../function/handler.js'
5+
import { HandlerEvent } from '../main.js'
6+
7+
import { builder } from './builder.js'
58

69
const METADATA_OBJECT = { metadata: { version: 1, builder_function: true, ttl: 0 } }
710

8-
test('Injects the metadata object into an asynchronous handler', async (t) => {
11+
test('Injects the metadata object into an asynchronous handler', async () => {
12+
const ttl = 3600
913
const originalResponse = {
1014
body: ':thumbsup:',
1115
statusCode: 200,
12-
ttl: 3600,
16+
ttl,
1317
}
1418
const myHandler = async () => {
1519
const asyncTask = new Promise((resolve) => {
@@ -22,23 +26,25 @@ test('Injects the metadata object into an asynchronous handler', async (t) => {
2226
}
2327
const response = await invokeLambda(builder(myHandler))
2428

25-
t.deepEqual(response, { ...originalResponse, metadata: { version: 1, builder_function: true, ttl: 3600 } })
29+
expect(response).toStrictEqual({ ...originalResponse, metadata: { version: 1, builder_function: true, ttl } })
2630
})
2731

28-
test('Injects the metadata object into a synchronous handler', async (t) => {
32+
test('Injects the metadata object into a synchronous handler', async () => {
2933
const originalResponse = {
3034
body: ':thumbsup:',
3135
statusCode: 200,
3236
}
33-
const myHandler = (event, context, callback) => {
34-
callback(null, originalResponse)
37+
// eslint-disable-next-line promise/prefer-await-to-callbacks
38+
const myHandler: BaseHandler = (event, context, callback) => {
39+
// eslint-disable-next-line n/callback-return, promise/prefer-await-to-callbacks
40+
callback?.(null, originalResponse)
3541
}
3642
const response = await invokeLambda(builder(myHandler))
3743

38-
t.deepEqual(response, { ...originalResponse, ...METADATA_OBJECT })
44+
expect(response).toStrictEqual({ ...originalResponse, ...METADATA_OBJECT })
3945
})
4046

41-
test('Injects the metadata object for non-200 responses', async (t) => {
47+
test('Injects the metadata object for non-200 responses', async () => {
4248
const originalResponse = {
4349
body: ':thumbsdown:',
4450
statusCode: 404,
@@ -54,10 +60,10 @@ test('Injects the metadata object for non-200 responses', async (t) => {
5460
}
5561
const response = await invokeLambda(builder(myHandler))
5662

57-
t.deepEqual(response, { ...originalResponse, ...METADATA_OBJECT })
63+
expect(response).toStrictEqual({ ...originalResponse, ...METADATA_OBJECT })
5864
})
5965

60-
test('Returns a 405 error for requests using the POST method', async (t) => {
66+
test('Returns a 405 error for requests using the POST method', async () => {
6167
const originalResponse = {
6268
body: ':thumbsup:',
6369
statusCode: 200,
@@ -73,10 +79,10 @@ test('Returns a 405 error for requests using the POST method', async (t) => {
7379
}
7480
const response = await invokeLambda(builder(myHandler), { method: 'POST' })
7581

76-
t.deepEqual(response, { body: 'Method Not Allowed', statusCode: 405 })
82+
expect(response).toStrictEqual({ body: 'Method Not Allowed', statusCode: 405 })
7783
})
7884

79-
test('Returns a 405 error for requests using the PUT method', async (t) => {
85+
test('Returns a 405 error for requests using the PUT method', async () => {
8086
const originalResponse = {
8187
body: ':thumbsup:',
8288
statusCode: 200,
@@ -92,10 +98,10 @@ test('Returns a 405 error for requests using the PUT method', async (t) => {
9298
}
9399
const response = await invokeLambda(builder(myHandler), { method: 'PUT' })
94100

95-
t.deepEqual(response, { body: 'Method Not Allowed', statusCode: 405 })
101+
expect(response).toStrictEqual({ body: 'Method Not Allowed', statusCode: 405 })
96102
})
97103

98-
test('Returns a 405 error for requests using the DELETE method', async (t) => {
104+
test('Returns a 405 error for requests using the DELETE method', async () => {
99105
const originalResponse = {
100106
body: ':thumbsup:',
101107
statusCode: 200,
@@ -111,10 +117,10 @@ test('Returns a 405 error for requests using the DELETE method', async (t) => {
111117
}
112118
const response = await invokeLambda(builder(myHandler), { method: 'DELETE' })
113119

114-
t.deepEqual(response, { body: 'Method Not Allowed', statusCode: 405 })
120+
expect(response).toStrictEqual({ body: 'Method Not Allowed', statusCode: 405 })
115121
})
116122

117-
test('Returns a 405 error for requests using the PATCH method', async (t) => {
123+
test('Returns a 405 error for requests using the PATCH method', async () => {
118124
const originalResponse = {
119125
body: ':thumbsup:',
120126
statusCode: 200,
@@ -130,12 +136,13 @@ test('Returns a 405 error for requests using the PATCH method', async (t) => {
130136
}
131137
const response = await invokeLambda(builder(myHandler), { method: 'PATCH' })
132138

133-
t.deepEqual(response, { body: 'Method Not Allowed', statusCode: 405 })
139+
expect(response).toStrictEqual({ body: 'Method Not Allowed', statusCode: 405 })
134140
})
135141

136-
test('Preserves errors thrown inside the wrapped handler', async (t) => {
142+
test('Preserves errors thrown inside the wrapped handler', async () => {
137143
const error = new Error('Uh-oh!')
138144

145+
// @ts-expect-error There's no type for this custom property.
139146
error.someProperty = ':thumbsdown:'
140147

141148
const myHandler = async () => {
@@ -148,27 +155,32 @@ test('Preserves errors thrown inside the wrapped handler', async (t) => {
148155
throw error
149156
}
150157

151-
await t.throwsAsync(invokeLambda(builder(myHandler)), { is: error })
158+
try {
159+
await invokeLambda(builder(myHandler))
160+
161+
throw new Error('Invocation should have failed')
162+
} catch {}
152163
})
153164

154-
test('Does not pass query parameters to the wrapped handler', async (t) => {
165+
test('Does not pass query parameters to the wrapped handler', async () => {
155166
const originalResponse = {
156167
body: ':thumbsup:',
157168
statusCode: 200,
158169
}
159170
// eslint-disable-next-line require-await
160-
const myHandler = async (event) => {
161-
t.deepEqual(event.multiValueQueryStringParameters, {})
162-
t.deepEqual(event.queryStringParameters, {})
171+
const myHandler = async (event: HandlerEvent) => {
172+
expect(event.multiValueQueryStringParameters).toStrictEqual({})
173+
expect(event.queryStringParameters).toStrictEqual({})
163174

164175
return originalResponse
165176
}
166177
const multiValueQueryStringParameters = { foo: ['bar'], bar: ['baz'] }
167178
const queryStringParameters = { foo: 'bar', bar: 'baz' }
168179
const response = await invokeLambda(builder(myHandler), {
180+
// @ts-expect-error TODO: Fic types.
169181
multiValueQueryStringParameters,
170182
queryStringParameters,
171183
})
172184

173-
t.deepEqual(response, { ...originalResponse, ...METADATA_OBJECT })
185+
expect(response).toStrictEqual({ ...originalResponse, ...METADATA_OBJECT })
174186
})
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,31 @@
1-
const process = require('process')
1+
import process from 'node:process'
22

3-
const test = require('ava')
4-
const semver = require('semver')
3+
import semver from 'semver'
4+
import { beforeEach, afterEach, expect, test } from 'vitest'
55

6-
const { purgeCache } = require('../../dist/lib/purge_cache')
7-
const { invokeLambda } = require('../helpers/main')
8-
const MockFetch = require('../helpers/mock_fetch')
6+
import { invokeLambda } from '../../test/helpers/main.mjs'
7+
import { MockFetch } from '../../test/helpers/mock_fetch.mjs'
8+
9+
import { purgeCache } from './purge_cache.js'
910

1011
const globalFetch = globalThis.fetch
1112
const hasFetchAPI = semver.gte(process.version, '18.0.0')
1213

13-
test.beforeEach(() => {
14+
beforeEach(() => {
1415
delete process.env.NETLIFY_PURGE_API_TOKEN
1516
delete process.env.SITE_ID
1617
delete process.env.NETLIFY_LOCAL
1718
})
1819

19-
test.afterEach(() => {
20+
afterEach(() => {
2021
globalThis.fetch = globalFetch
2122
})
2223

23-
test.serial('Calls the purge API endpoint and returns `undefined` if the operation was successful', async (t) => {
24+
test('Calls the purge API endpoint and returns `undefined` if the operation was successful', async () => {
2425
if (!hasFetchAPI) {
2526
console.warn('Skipping test requires the fetch API')
2627

27-
return t.pass()
28+
return
2829
}
2930

3031
const mockSiteID = '123456789'
@@ -34,16 +35,17 @@ test.serial('Calls the purge API endpoint and returns `undefined` if the operati
3435
process.env.SITE_ID = mockSiteID
3536

3637
const mockAPI = new MockFetch().post({
37-
body: (payload) => {
38+
body: (payload: string) => {
3839
const data = JSON.parse(payload)
3940

40-
t.is(data.site_id, mockSiteID)
41+
expect(data.site_id).toBe(mockSiteID)
4142
},
4243
headers: { Authorization: `Bearer ${mockToken}` },
4344
method: 'post',
4445
response: new Response(null, { status: 202 }),
4546
url: `https://api.netlify.com/api/v1/purge`,
4647
})
48+
// eslint-disable-next-line unicorn/consistent-function-scoping
4749
const myFunction = async () => {
4850
await purgeCache()
4951
}
@@ -52,15 +54,13 @@ test.serial('Calls the purge API endpoint and returns `undefined` if the operati
5254

5355
const response = await invokeLambda(myFunction)
5456

55-
t.is(response, undefined)
56-
t.true(mockAPI.fulfilled)
57+
expect(response).toBeUndefined()
58+
expect(mockAPI.fulfilled).toBeTruthy()
5759
})
5860

59-
test.serial('Throws if the API response does not have a successful status code', async (t) => {
61+
test('Throws if the API response does not have a successful status code', async () => {
6062
if (!hasFetchAPI) {
6163
console.warn('Skipping test requires the fetch API')
62-
63-
return t.pass()
6464
}
6565

6666
const mockSiteID = '123456789'
@@ -70,42 +70,49 @@ test.serial('Throws if the API response does not have a successful status code',
7070
process.env.SITE_ID = mockSiteID
7171

7272
const mockAPI = new MockFetch().post({
73-
body: (payload) => {
73+
body: (payload: string) => {
7474
const data = JSON.parse(payload)
7575

76-
t.is(data.site_id, mockSiteID)
76+
expect(data.site_id).toBe(mockSiteID)
7777
},
7878
headers: { Authorization: `Bearer ${mockToken}` },
7979
method: 'post',
8080
response: new Response(null, { status: 500 }),
8181
url: `https://api.netlify.com/api/v1/purge`,
8282
})
83+
// eslint-disable-next-line unicorn/consistent-function-scoping
8384
const myFunction = async () => {
8485
await purgeCache()
8586
}
8687

8788
globalThis.fetch = mockAPI.fetcher
8889

89-
await t.throwsAsync(
90-
async () => await invokeLambda(myFunction),
91-
'Cache purge API call returned an unexpected status code: 500',
92-
)
90+
try {
91+
await invokeLambda(myFunction)
92+
93+
throw new Error('Invocation should have failed')
94+
} catch (error) {
95+
expect((error as NodeJS.ErrnoException).message).toBe(
96+
'Cache purge API call returned an unexpected status code: 500',
97+
)
98+
}
9399
})
94100

95-
test.serial('Ignores purgeCache if in local dev with no token or site', async (t) => {
101+
test('Ignores purgeCache if in local dev with no token or site', async () => {
96102
if (!hasFetchAPI) {
97103
console.warn('Skipping test requires the fetch API')
98104

99-
return t.pass()
105+
return
100106
}
101107

102108
process.env.NETLIFY_LOCAL = '1'
103109

104110
const mockAPI = new MockFetch().post({
105111
body: () => {
106-
t.fail()
107-
}
112+
throw new Error('Unexpected request')
113+
},
108114
})
115+
// eslint-disable-next-line unicorn/consistent-function-scoping
109116
const myFunction = async () => {
110117
await purgeCache()
111118
}
@@ -114,5 +121,5 @@ test.serial('Ignores purgeCache if in local dev with no token or site', async (t
114121

115122
const response = await invokeLambda(myFunction)
116123

117-
t.is(response, undefined)
124+
expect(response).toBeUndefined()
118125
})

‎test/unit/system_logger.js ‎src/lib/system_logger.test.ts

+21-21
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,57 @@
1-
const process = require("process")
1+
import process from 'node:process'
22

3-
const test = require('ava')
3+
import { expect, test } from 'vitest'
44

5-
const { systemLogger, LogLevel } = require('../../dist/internal')
5+
import { LogLevel, systemLogger } from '../internal.js'
66

7-
test('Log Level', (t) => {
7+
test('Log Level', () => {
88
const originalDebug = console.debug
99

1010
const debugLogs = []
1111
console.debug = (...message) => debugLogs.push(message)
1212

1313
systemLogger.debug('hello!')
14-
t.is(debugLogs.length, 0)
14+
expect(debugLogs.length).toBe(0)
1515

1616
systemLogger.withLogLevel(LogLevel.Debug).debug('hello!')
17-
t.is(debugLogs.length, 1)
17+
expect(debugLogs.length).toBe(1)
1818

1919
systemLogger.withLogLevel(LogLevel.Log).debug('hello!')
20-
t.is(debugLogs.length, 1)
20+
expect(debugLogs.length).toBe(1)
2121

2222
console.debug = originalDebug
2323
})
2424

25-
test('Fields', (t) => {
25+
test('Fields', () => {
2626
const originalLog = console.log
27-
const logs = []
27+
const logs: string[][] = []
2828
console.log = (...message) => logs.push(message)
2929
systemLogger.withError(new Error('boom')).withFields({ foo: 'bar' }).log('hello!')
30-
t.is(logs.length, 1)
31-
t.is(logs[0][0], '__nfSystemLog')
30+
expect(logs.length).toBe(1)
31+
expect(logs[0][0]).toBe('__nfSystemLog')
3232
const log = JSON.parse(logs[0][1])
33-
t.is(log.msg, 'hello!')
34-
t.is(log.fields.foo, 'bar')
35-
t.is(log.fields.error, 'boom')
36-
t.is(log.fields.error_stack.split('\n').length > 2, true)
33+
expect(log.msg).toBe('hello!')
34+
expect(log.fields.foo).toBe('bar')
35+
expect(log.fields.error).toBe('boom')
36+
expect(log.fields.error_stack.split('\n').length > 2).toBe(true)
3737

3838
console.log = originalLog
3939
})
4040

41-
test('Local Dev', (t) => {
41+
test('Local Dev', () => {
4242
const originalLog = console.log
4343
const logs = []
4444
console.log = (...message) => logs.push(message)
4545
systemLogger.log('hello!')
46-
t.is(logs.length, 1)
46+
expect(logs.length).toBe(1)
4747

48-
process.env.NETLIFY_DEV= "true"
48+
process.env.NETLIFY_DEV = 'true'
4949
systemLogger.log('hello!')
50-
t.is(logs.length, 1)
50+
expect(logs.length).toBe(1)
5151

52-
process.env.NETLIFY_ENABLE_SYSTEM_LOGGING= "true"
52+
process.env.NETLIFY_ENABLE_SYSTEM_LOGGING = 'true'
5353
systemLogger.log('hello!')
54-
t.is(logs.length, 2)
54+
expect(logs.length).toBe(2)
5555

5656
delete process.env.NETLIFY_DEV
5757
delete process.env.NETLIFY_ENABLE_SYSTEM_LOGGING

‎test/helpers/main.js ‎test/helpers/main.mjs

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const invokeLambda = (handler, { method = 'GET', ...options } = {}) => {
1+
export const invokeLambda = (handler, { method = 'GET', ...options } = {}) => {
22
const event = {
33
...options,
44
httpMethod: method,
@@ -17,4 +17,3 @@ const invokeLambda = (handler, { method = 'GET', ...options } = {}) => {
1717
})
1818
}
1919

20-
module.exports = { invokeLambda }

‎test/helpers/mock_fetch.js ‎test/helpers/mock_fetch.mjs

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
const assert = require('assert')
1+
import assert from "node:assert"
22

3-
module.exports = class MockFetch {
3+
export class MockFetch {
44
constructor() {
55
this.requests = []
66
}

‎tsconfig.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626
/* Modules */
2727
"module": "commonjs" /* Specify what module code is generated. */,
28-
"rootDir": "./src" /* Specify the root folder within your source files. */,
28+
"rootDir": "./" /* Specify the root folder within your source files. */,
2929
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
3030
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
3131
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */

0 commit comments

Comments
 (0)
Please sign in to comment.