@wdio/devtools-script 1.4.0 → 1.5.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.
@@ -1,40 +1,259 @@
1
1
  /** @vitest-environment happy-dom */
2
- import { describe, it, expect, beforeEach, afterEach } from 'vitest'
2
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
3
3
  import { NetworkRequestCollector } from '../src/collectors/networkRequests.js'
4
4
 
5
- describe('NetworkRequestCollector', () => {
6
- let collector: NetworkRequestCollector
5
+ // happy-dom doesn't ship a Blob with reliable .size in some versions —
6
+ // fall back to a stub that returns the byte length so #estimateSize works
7
+ // without depending on the polyfill flavor we land on.
8
+ if (typeof globalThis.Blob === 'undefined') {
9
+ ;(globalThis as unknown as { Blob: unknown }).Blob = class {
10
+ size: number
11
+ constructor(parts: BlobPart[]) {
12
+ this.size = parts.reduce(
13
+ (n, p) => n + (typeof p === 'string' ? p.length : 0),
14
+ 0
15
+ )
16
+ }
17
+ }
18
+ }
7
19
 
8
- beforeEach(() => {
9
- collector = new NetworkRequestCollector()
10
- })
20
+ let collector: NetworkRequestCollector
21
+ const realFetch = window.fetch
22
+ let fetchMock: ReturnType<typeof vi.fn>
23
+
24
+ beforeEach(() => {
25
+ // Patch fetch BEFORE constructing the collector so the collector wraps
26
+ // OUR mock and we can drive captures deterministically without real I/O.
27
+ fetchMock = vi.fn()
28
+ window.fetch = fetchMock as unknown as typeof fetch
29
+ collector = new NetworkRequestCollector()
30
+ })
31
+
32
+ afterEach(() => {
33
+ collector.clear()
34
+ window.fetch = realFetch
35
+ })
11
36
 
12
- afterEach(() => {
37
+ describe('NetworkRequestCollector — lifecycle', () => {
38
+ it('starts empty and clear() resets the buffer', () => {
39
+ expect(collector.getArtifacts()).toEqual([])
13
40
  collector.clear()
41
+ expect(collector.getArtifacts()).toEqual([])
42
+ // Reference is stable across reads (callers can hold the array ref)
43
+ expect(collector.getArtifacts()).toBe(collector.getArtifacts())
14
44
  })
45
+ })
46
+
47
+ describe('NetworkRequestCollector — fetch capture', () => {
48
+ it('captures a successful JSON response with headers, body, timing, size', async () => {
49
+ fetchMock.mockResolvedValueOnce(
50
+ new Response('{"hello":"world"}', {
51
+ status: 200,
52
+ statusText: 'OK',
53
+ headers: { 'content-type': 'application/json' }
54
+ })
55
+ )
56
+
57
+ await window.fetch('https://api.example.com/data', {
58
+ method: 'POST',
59
+ headers: { 'x-trace': 'abc' },
60
+ body: JSON.stringify({ q: 1 })
61
+ })
15
62
 
16
- it('should initialize, clear, and return artifacts correctly', () => {
17
- // Test initialization
18
63
  const artifacts = collector.getArtifacts()
19
- expect(artifacts).toEqual([])
20
- expect(Array.isArray(artifacts)).toBe(true)
21
- expect(artifacts).toHaveLength(0)
64
+ expect(artifacts).toHaveLength(1)
65
+ const req = artifacts[0]
66
+ expect(req).toMatchObject({
67
+ url: 'https://api.example.com/data',
68
+ method: 'POST',
69
+ type: 'fetch',
70
+ status: 200,
71
+ statusText: 'OK',
72
+ requestBody: JSON.stringify({ q: 1 }),
73
+ responseBody: '{"hello":"world"}'
74
+ })
75
+ expect(req.requestHeaders).toMatchObject({ 'x-trace': 'abc' })
76
+ expect(req.responseHeaders).toMatchObject({
77
+ 'content-type': 'application/json'
78
+ })
79
+ expect(req.startTime).toBeTypeOf('number')
80
+ expect(req.endTime).toBeTypeOf('number')
81
+ expect(req.time).toBeGreaterThanOrEqual(0)
82
+ })
22
83
 
23
- // Test clear functionality
24
- collector.clear()
25
- const clearedArtifacts = collector.getArtifacts()
26
- expect(clearedArtifacts).toEqual([])
27
- expect(clearedArtifacts).toHaveLength(0)
84
+ it('skips internal protocols (data:, blob:, chrome:, about:, ws:)', async () => {
85
+ for (const url of [
86
+ 'data:text/plain,hello',
87
+ 'blob:https://example.com/abc',
88
+ 'chrome://settings',
89
+ 'chrome-extension://abc/page',
90
+ 'about:blank',
91
+ 'ws://example.com',
92
+ 'wss://example.com'
93
+ ]) {
94
+ fetchMock.mockResolvedValueOnce(new Response(''))
95
+ await window.fetch(url)
96
+ }
97
+ // Every call passed through to the original mock but NONE got captured
98
+ expect(collector.getArtifacts()).toEqual([])
99
+ expect(fetchMock).toHaveBeenCalledTimes(7)
100
+ })
28
101
 
29
- // Test multiple clears are safe
30
- collector.clear()
102
+ it('skips noise URLs (/favicon.ico, /.well-known/...)', async () => {
103
+ fetchMock.mockResolvedValue(new Response(''))
104
+ await window.fetch('https://example.com/favicon.ico')
105
+ await window.fetch('https://example.com/.well-known/something')
31
106
  expect(collector.getArtifacts()).toEqual([])
107
+ })
108
+
109
+ it('extracts the URL from URL objects (URL.href, not toString)', async () => {
110
+ fetchMock.mockResolvedValueOnce(
111
+ new Response('{"ok":1}', {
112
+ status: 200,
113
+ headers: { 'content-type': 'application/json' }
114
+ })
115
+ )
116
+ await window.fetch(new URL('https://example.com/x'))
117
+ expect(collector.getArtifacts()[0].url).toBe('https://example.com/x')
118
+ })
119
+
120
+ // Known limitation: the collector reads `init?.method` only — when a
121
+ // Request object is passed as the first arg with its own method, the
122
+ // collector reports 'GET' (the default) because it doesn't inspect
123
+ // Request.method. Pin the behavior here so a future change is intentional.
124
+ it('reports GET for Request-object inputs with a non-GET Request.method (known limitation)', async () => {
125
+ fetchMock.mockResolvedValueOnce(
126
+ new Response('{"ok":2}', {
127
+ status: 200,
128
+ headers: { 'content-type': 'application/json' }
129
+ })
130
+ )
131
+ await window.fetch(new Request('https://example.com/y', { method: 'PUT' }))
132
+ const captured = collector.getArtifacts()[0]
133
+ expect(captured.url).toBe('https://example.com/y')
134
+ expect(captured.method).toBe('GET') // not 'PUT' — see comment above
135
+ })
136
+
137
+ it('does not capture an entry when the underlying fetch rejects, and re-throws', async () => {
138
+ fetchMock.mockRejectedValueOnce(new Error('network down'))
139
+ await expect(window.fetch('https://example.com/will-fail')).rejects.toThrow(
140
+ 'network down'
141
+ )
142
+ expect(collector.getArtifacts()).toEqual([])
143
+ })
144
+
145
+ it('extracts headers from all three accepted shapes (Headers, Array, plain object) and lowercases keys', async () => {
146
+ // Headers instance
147
+ fetchMock.mockResolvedValueOnce(
148
+ new Response('a', {
149
+ status: 200,
150
+ headers: { 'content-type': 'text/plain' }
151
+ })
152
+ )
153
+ await window.fetch('https://example.com/h1', {
154
+ headers: new Headers({ 'X-One': '1' })
155
+ })
156
+ // [[k,v]] tuple array
157
+ fetchMock.mockResolvedValueOnce(
158
+ new Response('b', {
159
+ status: 200,
160
+ headers: { 'content-type': 'text/plain' }
161
+ })
162
+ )
163
+ await window.fetch('https://example.com/h2', {
164
+ headers: [['X-Two', '2']]
165
+ })
166
+ // Plain object
167
+ fetchMock.mockResolvedValueOnce(
168
+ new Response('c', {
169
+ status: 200,
170
+ headers: { 'content-type': 'text/plain' }
171
+ })
172
+ )
173
+ await window.fetch('https://example.com/h3', {
174
+ headers: { 'X-Three': '3' }
175
+ })
176
+
177
+ const reqs = collector.getArtifacts()
178
+ expect(reqs[0].requestHeaders).toMatchObject({ 'x-one': '1' })
179
+ expect(reqs[1].requestHeaders).toMatchObject({ 'x-two': '2' })
180
+ expect(reqs[2].requestHeaders).toMatchObject({ 'x-three': '3' })
181
+ })
182
+ })
183
+
184
+ describe('NetworkRequestCollector — XHR capture', () => {
185
+ it('captures a successful JSON XHR (status + body + content-type filter passes)', async () => {
186
+ const xhr = new XMLHttpRequest()
187
+ xhr.open('GET', 'https://api.example.com/xhr')
188
+
189
+ // Patch the just-opened xhr to fake a successful JSON response without
190
+ // hitting the network. happy-dom doesn't deliver `load` for a never-sent
191
+ // request, so we wire up the response shape manually + fire the event.
192
+ Object.defineProperty(xhr, 'status', { value: 200, configurable: true })
193
+ Object.defineProperty(xhr, 'statusText', {
194
+ value: 'OK',
195
+ configurable: true
196
+ })
197
+ Object.defineProperty(xhr, 'responseText', {
198
+ value: '{"id":42}',
199
+ configurable: true
200
+ })
201
+ Object.defineProperty(xhr, 'getAllResponseHeaders', {
202
+ value: () => 'content-type: application/json\r\nx-rate-limit: 99\r\n',
203
+ configurable: true
204
+ })
205
+
206
+ xhr.send()
207
+ xhr.dispatchEvent(new Event('load'))
208
+
209
+ const reqs = collector.getArtifacts()
210
+ expect(reqs).toHaveLength(1)
211
+ expect(reqs[0]).toMatchObject({
212
+ url: 'https://api.example.com/xhr',
213
+ method: 'GET',
214
+ type: 'xhr',
215
+ status: 200,
216
+ statusText: 'OK',
217
+ responseBody: '{"id":42}'
218
+ })
219
+ expect(reqs[0].responseHeaders).toMatchObject({
220
+ 'content-type': 'application/json',
221
+ 'x-rate-limit': '99'
222
+ })
223
+ })
224
+
225
+ it('skips ignored URLs in XHR open (does not record)', () => {
226
+ const xhr = new XMLHttpRequest()
227
+ xhr.open('GET', 'data:text/plain,hello')
228
+ // The send + load aren't even needed — `open` was filtered, nothing in
229
+ // the pending map, nothing to record on load.
230
+ expect(collector.getArtifacts()).toEqual([])
231
+ })
232
+
233
+ it('captures request body for XHR POST + lowercases response headers', async () => {
234
+ const xhr = new XMLHttpRequest()
235
+ xhr.open('POST', 'https://api.example.com/submit')
236
+ Object.defineProperty(xhr, 'status', { value: 201, configurable: true })
237
+ Object.defineProperty(xhr, 'statusText', {
238
+ value: 'Created',
239
+ configurable: true
240
+ })
241
+ Object.defineProperty(xhr, 'responseText', {
242
+ value: 'OK',
243
+ configurable: true
244
+ })
245
+ Object.defineProperty(xhr, 'getAllResponseHeaders', {
246
+ value: () => 'Content-Type: text/plain\r\n',
247
+ configurable: true
248
+ })
249
+
250
+ xhr.send('payload-data')
251
+ xhr.dispatchEvent(new Event('load'))
32
252
 
33
- // Test reference consistency
34
- const artifacts1 = collector.getArtifacts()
35
- const artifacts2 = collector.getArtifacts()
36
- expect(artifacts1).toBe(artifacts2)
37
- expect(artifacts1).toBeDefined()
38
- expect(artifacts1).not.toBeNull()
253
+ const req = collector.getArtifacts()[0]
254
+ expect(req.requestBody).toBe('payload-data')
255
+ // Header key was uppercase "Content-Type" on the wire; the parser
256
+ // lowercases for consistency with the fetch path.
257
+ expect(req.responseHeaders).toHaveProperty('content-type', 'text/plain')
39
258
  })
40
259
  })