Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
57c0103
feat: add SnapshotAgent for HTTP request recording and playback
mcollina Jun 8, 2025
a90fe15
Merge branch 'main' into feature/snapshot-testing
mcollina Jul 19, 2025
8084eac
feat: implement Phase 1 of SnapshotAgent enhancements
mcollina Jul 19, 2025
4401261
docs: update PLAN.md to reflect Phase 1 completion
mcollina Jul 19, 2025
65e53a1
feat: implement Phase 2 - Enhanced Request Matching
mcollina Jul 19, 2025
0e8046e
feat: implement Phase 3 - Advanced Playback Features for SnapshotAgent
mcollina Jul 19, 2025
0a50c9b
docs: update PLAN.md to reflect completion of all primary objectives
mcollina Jul 19, 2025
f379bab
feat: update TypeScript definitions and add comprehensive tsd tests
mcollina Jul 19, 2025
0372422
Merge remote-tracking branch 'origin/main' into feature/snapshot-testing
mcollina Jul 20, 2025
ffaaa48
feat: implement Phase 4 optional enhancements for SnapshotAgent
mcollina Jul 20, 2025
ac43299
fix: resolve flaky sequential response test
mcollina Jul 20, 2025
d40eaf0
chore: remove PLAN.md file
mcollina Jul 20, 2025
83c4f7e
removed PR_DESCRIPTION.md
mcollina Jul 24, 2025
4c1d77c
docs: update snapshot agent documentation and implementation
mcollina Jul 28, 2025
559d936
fixup
mcollina Jul 29, 2025
01fdcc0
test: add redirect interceptor integration test and fix race condition
mcollina Jul 29, 2025
fe03c68
fix: make SnapshotAgent work properly with redirect interceptor
mcollina Jul 29, 2025
d7d9024
fix: complete SnapshotAgent redirect interceptor integration
mcollina Jul 29, 2025
27e1c49
fixup
mcollina Jul 29, 2025
2664559
fixup
mcollina Jul 29, 2025
a9d7cb7
fix: clean up console.logs and improve SnapshotAgent experimental war…
mcollina Jul 29, 2025
c968233
test: add test case for SnapshotAgent with pre-existing array responses
mcollina Jul 29, 2025
5db08e8
docs: simplify snapshot testing example to single working demo
mcollina Jul 29, 2025
14a70bb
remove spurious console.error
mcollina Jul 30, 2025
f61bb5a
clean: remove phase mentions and fix t.after() placement
mcollina Jul 30, 2025
3b460af
refactor: convert snapshot tests to use describe blocks and top-level…
mcollina Jul 30, 2025
4edfca0
fix: ensure agent.close() method is always awaited in tests
mcollina Jul 31, 2025
4cc196c
feat: add async close() method to SnapshotRecorder that saves recordings
mcollina Jul 31, 2025
f3945d4
fixup
mcollina Jul 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
616 changes: 616 additions & 0 deletions docs/docs/api/SnapshotAgent.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/docsify/sidebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
* [MockClient](/docs/api/MockClient.md "Undici API - MockClient")
* [MockPool](/docs/api/MockPool.md "Undici API - MockPool")
* [MockAgent](/docs/api/MockAgent.md "Undici API - MockAgent")
* [SnapshotAgent](/docs/api/SnapshotAgent.md "Undici API - SnapshotAgent")
* [MockCallHistory](/docs/api/MockCallHistory.md "Undici API - MockCallHistory")
* [MockCallHistoryLog](/docs/api/MockCallHistoryLog.md "Undici API - MockCallHistoryLog")
* [MockErrors](/docs/api/MockErrors.md "Undici API - MockErrors")
Expand Down
109 changes: 109 additions & 0 deletions docs/examples/snapshot-testing.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
const { SnapshotAgent, setGlobalDispatcher, getGlobalDispatcher, request } = require('../../index.js')
const { createServer } = require('node:http')
const { promisify } = require('node:util')
const { tmpdir } = require('node:os')
const { join } = require('node:path')

/**
* Example: Basic Snapshot Testing
*
* This example demonstrates how to use SnapshotAgent to record API
* interactions and replay them in tests for consistent, offline testing.
*/

async function basicSnapshotExample () {
console.log('🚀 Basic Snapshot Testing Example\n')

// Create a temporary snapshot file path
const snapshotPath = join(tmpdir(), `snapshot-example-${Date.now()}.json`)
console.log(`📁 Using temporary snapshot file: ${snapshotPath}\n`)

// Create a local test server
const server = createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({
message: 'Hello from test server!',
timestamp: new Date().toISOString(),
path: req.url
}))
})

await promisify(server.listen.bind(server))(0)
const { port } = server.address()
const origin = `http://localhost:${port}`

try {
// Step 1: Record mode - capture API responses
console.log('📹 Step 1: Recording API response...')

const recordingAgent = new SnapshotAgent({
mode: 'record',
snapshotPath
})

const originalDispatcher = getGlobalDispatcher()
setGlobalDispatcher(recordingAgent)

try {
// Make an API call that will be recorded
const response = await request(`${origin}/api/test`)
const data = await response.body.json()

console.log(`✅ Recorded response: ${data.message}`)

// Save the recorded snapshots
await recordingAgent.saveSnapshots()
console.log('💾 Snapshot saved to temporary file\n')
} finally {
setGlobalDispatcher(originalDispatcher)
recordingAgent.close()
}

// Step 2: Playback mode - use recorded responses (server can be down)
console.log('🎬 Step 2: Playing back recorded response...')
server.close() // Close server to prove we're using snapshots

const playbackAgent = new SnapshotAgent({
mode: 'playback',
snapshotPath
})

setGlobalDispatcher(playbackAgent)

try {
// This will use the recorded response instead of making a real request
const response = await request(`${origin}/api/test`)
const data = await response.body.json()

console.log(`✅ Playback response: ${data.message}`)
console.log('🎉 Successfully used recorded data instead of live server!')
} finally {
setGlobalDispatcher(originalDispatcher)
playbackAgent.close()
}
} finally {
// Ensure server is closed
if (server.listening) {
server.close()
}

// Clean up temporary file
try {
const { unlink } = require('node:fs/promises')
await unlink(snapshotPath)
console.log('\n🗑️ Cleaned up temporary snapshot file')
} catch (error) {
// File might not exist or already be deleted
}
}
}

// Main execution
async function main () {
await basicSnapshotExample()
}

// Run if called directly
if (require.main === module) {
main().catch(console.error)
}
2 changes: 2 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const MockClient = require('./lib/mock/mock-client')
const { MockCallHistory, MockCallHistoryLog } = require('./lib/mock/mock-call-history')
const MockAgent = require('./lib/mock/mock-agent')
const MockPool = require('./lib/mock/mock-pool')
const SnapshotAgent = require('./lib/mock/snapshot-agent')
const mockErrors = require('./lib/mock/mock-errors')
const RetryHandler = require('./lib/handler/retry-handler')
const { getGlobalDispatcher, setGlobalDispatcher } = require('./lib/global')
Expand Down Expand Up @@ -178,6 +179,7 @@ module.exports.MockCallHistory = MockCallHistory
module.exports.MockCallHistoryLog = MockCallHistoryLog
module.exports.MockPool = MockPool
module.exports.MockAgent = MockAgent
module.exports.SnapshotAgent = SnapshotAgent
module.exports.mockErrors = mockErrors

const { EventSource } = require('./lib/web/eventsource/eventsource')
Expand Down
Loading
Loading