@wdio/devtools-script 1.4.1 → 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.
- package/dist/script.js +852 -803
- package/package.json +5 -5
- package/src/collectors/consoleLogs.ts +1 -1
- package/src/collectors/networkRequests.ts +127 -148
- package/src/index.ts +42 -53
- package/src/logger.ts +1 -1
- package/src/utils.ts +13 -9
- package/tests/logger.test.ts +36 -0
- package/tests/networkRequests.test.ts +244 -25
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wdio/devtools-script",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Script to be injected into a page to trace the page",
|
|
5
5
|
"author": "Christian Bromann <mail@bromann.dev>",
|
|
6
6
|
"repository": {
|
|
@@ -14,12 +14,12 @@
|
|
|
14
14
|
"typeScriptVersion": "^5.0.0",
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"htm": "^3.1.1",
|
|
17
|
-
"parse5": "^8.0.
|
|
18
|
-
"preact": "^10.
|
|
19
|
-
"vite-plugin-singlefile": "^2.3.
|
|
17
|
+
"parse5": "^8.0.1",
|
|
18
|
+
"preact": "^10.29.2",
|
|
19
|
+
"vite-plugin-singlefile": "^2.3.3"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
|
-
"vite": "8.0.
|
|
22
|
+
"vite": "^8.0.16"
|
|
23
23
|
},
|
|
24
24
|
"license": "MIT",
|
|
25
25
|
"scripts": {
|
|
@@ -3,7 +3,7 @@ import type { Collector } from './collector.js'
|
|
|
3
3
|
const consoleMethods = ['log', 'info', 'warn', 'error'] as const
|
|
4
4
|
export interface ConsoleLogs {
|
|
5
5
|
type: 'log' | 'info' | 'warn' | 'error'
|
|
6
|
-
args:
|
|
6
|
+
args: unknown[]
|
|
7
7
|
timestamp: number
|
|
8
8
|
source?: 'browser' | 'test' | 'terminal'
|
|
9
9
|
}
|
|
@@ -68,94 +68,94 @@ export class NetworkRequestCollector implements Collector<NetworkRequest> {
|
|
|
68
68
|
return false
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
#extractFetchUrl(input: RequestInfo | URL): string {
|
|
72
|
+
if (typeof input === 'string') {
|
|
73
|
+
return input
|
|
74
|
+
}
|
|
75
|
+
return input instanceof URL ? input.href : input.url
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async #readResponseBody(
|
|
79
|
+
response: Response,
|
|
80
|
+
contentType: string
|
|
81
|
+
): Promise<string | undefined> {
|
|
82
|
+
try {
|
|
83
|
+
if (
|
|
84
|
+
contentType.includes('application/json') ||
|
|
85
|
+
contentType.includes('text/')
|
|
86
|
+
) {
|
|
87
|
+
return await response.clone().text()
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
/* ignore body read errors */
|
|
91
|
+
}
|
|
92
|
+
return undefined
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async #recordFetchResponse(
|
|
96
|
+
id: string,
|
|
97
|
+
request: Partial<NetworkRequest>,
|
|
98
|
+
response: Response,
|
|
99
|
+
startTime: number
|
|
100
|
+
): Promise<void> {
|
|
101
|
+
const endTime = performance.now()
|
|
102
|
+
const responseHeaders = this.#extractHeaders(response.headers)
|
|
103
|
+
const contentType = responseHeaders['content-type']?.trim()
|
|
104
|
+
if (!contentType || contentType === '-') {
|
|
105
|
+
this.#pendingRequests.delete(id)
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
const responseBody = await this.#readResponseBody(response, contentType)
|
|
109
|
+
this.#requests.push({
|
|
110
|
+
id,
|
|
111
|
+
url: request.url!,
|
|
112
|
+
method: request.method!,
|
|
113
|
+
status: response.status,
|
|
114
|
+
statusText: response.statusText,
|
|
115
|
+
type: 'fetch',
|
|
116
|
+
timestamp: request.timestamp!,
|
|
117
|
+
startTime,
|
|
118
|
+
endTime,
|
|
119
|
+
time: endTime - startTime,
|
|
120
|
+
requestHeaders: request.requestHeaders,
|
|
121
|
+
responseHeaders,
|
|
122
|
+
requestBody: request.requestBody,
|
|
123
|
+
responseBody,
|
|
124
|
+
size: this.#estimateSize(responseBody)
|
|
125
|
+
})
|
|
126
|
+
this.#pendingRequests.delete(id)
|
|
127
|
+
}
|
|
128
|
+
|
|
71
129
|
#patchFetch() {
|
|
72
130
|
if (typeof window.fetch !== 'function') {
|
|
73
131
|
return
|
|
74
132
|
}
|
|
75
|
-
|
|
76
133
|
this.#originalFetch = window.fetch
|
|
77
134
|
const self = this
|
|
78
|
-
|
|
79
135
|
window.fetch = async function (
|
|
80
136
|
input: RequestInfo | URL,
|
|
81
137
|
init?: RequestInit
|
|
82
138
|
): Promise<Response> {
|
|
83
|
-
const
|
|
84
|
-
const url =
|
|
85
|
-
typeof input === 'string'
|
|
86
|
-
? input
|
|
87
|
-
: input instanceof URL
|
|
88
|
-
? input.href
|
|
89
|
-
: input.url
|
|
90
|
-
const method = init?.method?.toUpperCase() || 'GET'
|
|
91
|
-
|
|
92
|
-
// Skip internal/non-HTTP requests
|
|
139
|
+
const url = self.#extractFetchUrl(input)
|
|
93
140
|
if (self.#shouldIgnoreRequest(url)) {
|
|
94
141
|
return self.#originalFetch!.apply(this, [input, init])
|
|
95
142
|
}
|
|
96
|
-
|
|
143
|
+
const id = self.#generateId()
|
|
97
144
|
const startTime = performance.now()
|
|
98
|
-
const timestamp = Date.now()
|
|
99
|
-
|
|
100
145
|
const request: Partial<NetworkRequest> = {
|
|
101
146
|
id,
|
|
102
147
|
url,
|
|
103
|
-
method,
|
|
148
|
+
method: init?.method?.toUpperCase() || 'GET',
|
|
104
149
|
type: 'fetch',
|
|
105
|
-
timestamp,
|
|
150
|
+
timestamp: Date.now(),
|
|
106
151
|
startTime,
|
|
107
152
|
requestHeaders: init?.headers ? self.#extractHeaders(init.headers) : {},
|
|
108
153
|
requestBody: init?.body ? String(init.body) : undefined
|
|
109
154
|
}
|
|
110
|
-
|
|
111
155
|
self.#pendingRequests.set(id, request)
|
|
112
|
-
|
|
113
156
|
try {
|
|
114
157
|
const response = await self.#originalFetch!.apply(this, [input, init])
|
|
115
|
-
|
|
116
|
-
const time = endTime - startTime
|
|
117
|
-
|
|
118
|
-
const responseHeaders = self.#extractHeaders(response.headers)
|
|
119
|
-
const contentType = responseHeaders['content-type']?.trim()
|
|
120
|
-
|
|
121
|
-
if (!contentType || contentType === '-') {
|
|
122
|
-
self.#pendingRequests.delete(id)
|
|
123
|
-
return response
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
let responseBody: string | undefined
|
|
127
|
-
try {
|
|
128
|
-
if (
|
|
129
|
-
contentType.includes('application/json') ||
|
|
130
|
-
contentType.includes('text/')
|
|
131
|
-
) {
|
|
132
|
-
responseBody = await response.clone().text()
|
|
133
|
-
}
|
|
134
|
-
} catch {
|
|
135
|
-
// Ignore body read errors
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const networkRequest: NetworkRequest = {
|
|
139
|
-
id,
|
|
140
|
-
url,
|
|
141
|
-
method,
|
|
142
|
-
status: response.status,
|
|
143
|
-
statusText: response.statusText,
|
|
144
|
-
type: 'fetch',
|
|
145
|
-
timestamp,
|
|
146
|
-
startTime,
|
|
147
|
-
endTime,
|
|
148
|
-
time,
|
|
149
|
-
requestHeaders: request.requestHeaders,
|
|
150
|
-
responseHeaders,
|
|
151
|
-
requestBody: request.requestBody,
|
|
152
|
-
responseBody,
|
|
153
|
-
size: self.#estimateSize(responseBody)
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
self.#requests.push(networkRequest)
|
|
157
|
-
self.#pendingRequests.delete(id)
|
|
158
|
-
|
|
158
|
+
await self.#recordFetchResponse(id, request, response, startTime)
|
|
159
159
|
return response
|
|
160
160
|
} catch (error) {
|
|
161
161
|
self.#pendingRequests.delete(id)
|
|
@@ -164,15 +164,48 @@ export class NetworkRequestCollector implements Collector<NetworkRequest> {
|
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
#
|
|
168
|
-
|
|
167
|
+
#recordXHRResponse(
|
|
168
|
+
xhr: XMLHttpRequest,
|
|
169
|
+
requestData: Partial<NetworkRequest>,
|
|
170
|
+
startTime: number
|
|
171
|
+
): void {
|
|
172
|
+
const endTime = performance.now()
|
|
173
|
+
const responseHeaders = this.#extractXHRHeaders(xhr)
|
|
174
|
+
const contentType = responseHeaders['content-type']?.trim()
|
|
175
|
+
if (!contentType || contentType === '-') {
|
|
169
176
|
return
|
|
170
177
|
}
|
|
178
|
+
let responseBody: string | undefined
|
|
179
|
+
try {
|
|
180
|
+
if (
|
|
181
|
+
contentType.includes('application/json') ||
|
|
182
|
+
contentType.includes('text/')
|
|
183
|
+
) {
|
|
184
|
+
responseBody = xhr.responseText
|
|
185
|
+
}
|
|
186
|
+
} catch {
|
|
187
|
+
/* ignore body read errors */
|
|
188
|
+
}
|
|
189
|
+
this.#requests.push({
|
|
190
|
+
id: requestData.id!,
|
|
191
|
+
url: requestData.url!,
|
|
192
|
+
method: requestData.method!,
|
|
193
|
+
status: xhr.status,
|
|
194
|
+
statusText: xhr.statusText,
|
|
195
|
+
type: 'xhr',
|
|
196
|
+
timestamp: requestData.timestamp!,
|
|
197
|
+
startTime,
|
|
198
|
+
endTime,
|
|
199
|
+
time: endTime - startTime,
|
|
200
|
+
requestHeaders: requestData.requestHeaders,
|
|
201
|
+
responseHeaders,
|
|
202
|
+
requestBody: requestData.requestBody,
|
|
203
|
+
responseBody,
|
|
204
|
+
size: this.#estimateSize(responseBody)
|
|
205
|
+
})
|
|
206
|
+
}
|
|
171
207
|
|
|
172
|
-
|
|
173
|
-
this.#originalXhrOpen = XMLHttpRequest.prototype.open
|
|
174
|
-
this.#originalXhrSend = XMLHttpRequest.prototype.send
|
|
175
|
-
|
|
208
|
+
#patchXHROpen(self: this): void {
|
|
176
209
|
XMLHttpRequest.prototype.open = function (
|
|
177
210
|
method: string,
|
|
178
211
|
url: string | URL,
|
|
@@ -180,31 +213,18 @@ export class NetworkRequestCollector implements Collector<NetworkRequest> {
|
|
|
180
213
|
username?: string | null,
|
|
181
214
|
password?: string | null
|
|
182
215
|
) {
|
|
183
|
-
const id = self.#generateId()
|
|
184
216
|
const urlString = typeof url === 'string' ? url : url.href
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
)
|
|
217
|
+
if (!self.#shouldIgnoreRequest(urlString)) {
|
|
218
|
+
self.#pendingXHRRequests.set(this, {
|
|
219
|
+
id: self.#generateId(),
|
|
220
|
+
url: urlString,
|
|
221
|
+
method: method.toUpperCase(),
|
|
222
|
+
type: 'xhr',
|
|
223
|
+
timestamp: Date.now(),
|
|
224
|
+
startTime: performance.now(),
|
|
225
|
+
requestHeaders: {}
|
|
226
|
+
})
|
|
196
227
|
}
|
|
197
|
-
|
|
198
|
-
self.#pendingXHRRequests.set(this, {
|
|
199
|
-
id,
|
|
200
|
-
url: urlString,
|
|
201
|
-
method: method.toUpperCase(),
|
|
202
|
-
type: 'xhr',
|
|
203
|
-
timestamp: Date.now(),
|
|
204
|
-
startTime: performance.now(),
|
|
205
|
-
requestHeaders: {}
|
|
206
|
-
})
|
|
207
|
-
|
|
208
228
|
return self.#originalXhrOpen!.call(
|
|
209
229
|
this,
|
|
210
230
|
method,
|
|
@@ -214,78 +234,37 @@ export class NetworkRequestCollector implements Collector<NetworkRequest> {
|
|
|
214
234
|
password
|
|
215
235
|
)
|
|
216
236
|
}
|
|
237
|
+
}
|
|
217
238
|
|
|
239
|
+
#patchXHRSend(self: this): void {
|
|
218
240
|
XMLHttpRequest.prototype.send = function (
|
|
219
241
|
body?: Document | XMLHttpRequestBodyInit | null
|
|
220
242
|
) {
|
|
221
243
|
const requestData = self.#pendingXHRRequests.get(this)
|
|
222
|
-
|
|
223
|
-
// If no request data, this request was filtered out - just send it
|
|
224
244
|
if (!requestData) {
|
|
225
245
|
return self.#originalXhrSend!.call(this, body)
|
|
226
246
|
}
|
|
227
|
-
|
|
228
247
|
if (body) {
|
|
229
248
|
requestData.requestBody = String(body)
|
|
230
249
|
}
|
|
231
|
-
|
|
232
250
|
const startTime = requestData.startTime || performance.now()
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const time = endTime - startTime
|
|
237
|
-
|
|
238
|
-
const responseHeaders = self.#extractXHRHeaders(this)
|
|
239
|
-
const contentType = responseHeaders['content-type']?.trim()
|
|
240
|
-
|
|
241
|
-
if (!contentType || contentType === '-') {
|
|
242
|
-
return
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
let responseBody: string | undefined
|
|
246
|
-
try {
|
|
247
|
-
if (
|
|
248
|
-
contentType.includes('application/json') ||
|
|
249
|
-
contentType.includes('text/')
|
|
250
|
-
) {
|
|
251
|
-
responseBody = this.responseText
|
|
252
|
-
}
|
|
253
|
-
} catch {
|
|
254
|
-
// Ignore
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
const networkRequest: NetworkRequest = {
|
|
258
|
-
id: requestData.id!,
|
|
259
|
-
url: requestData.url!,
|
|
260
|
-
method: requestData.method!,
|
|
261
|
-
status: this.status,
|
|
262
|
-
statusText: this.statusText,
|
|
263
|
-
type: 'xhr',
|
|
264
|
-
timestamp: requestData.timestamp!,
|
|
265
|
-
startTime,
|
|
266
|
-
endTime,
|
|
267
|
-
time,
|
|
268
|
-
requestHeaders: requestData.requestHeaders,
|
|
269
|
-
responseHeaders,
|
|
270
|
-
requestBody: requestData.requestBody,
|
|
271
|
-
responseBody,
|
|
272
|
-
size: self.#estimateSize(responseBody)
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
self.#requests.push(networkRequest)
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
const errorHandler = function (this: XMLHttpRequest) {
|
|
279
|
-
// Skip errors
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
this.addEventListener('load', loadHandler)
|
|
283
|
-
this.addEventListener('error', errorHandler)
|
|
284
|
-
|
|
251
|
+
this.addEventListener('load', function (this: XMLHttpRequest) {
|
|
252
|
+
self.#recordXHRResponse(this, requestData, startTime)
|
|
253
|
+
})
|
|
285
254
|
return self.#originalXhrSend!.call(this, body)
|
|
286
255
|
}
|
|
287
256
|
}
|
|
288
257
|
|
|
258
|
+
#patchXHR() {
|
|
259
|
+
if (typeof XMLHttpRequest === 'undefined') {
|
|
260
|
+
return
|
|
261
|
+
}
|
|
262
|
+
this.#originalXhrOpen = XMLHttpRequest.prototype.open
|
|
263
|
+
this.#originalXhrSend = XMLHttpRequest.prototype.send
|
|
264
|
+
this.#patchXHROpen(this)
|
|
265
|
+
this.#patchXHRSend(this)
|
|
266
|
+
}
|
|
267
|
+
|
|
289
268
|
#extractHeaders(headers: HeadersInit | Headers): Record<string, string> {
|
|
290
269
|
const result: Record<string, string> = {}
|
|
291
270
|
|
package/src/index.ts
CHANGED
|
@@ -8,6 +8,43 @@ import {
|
|
|
8
8
|
import { log } from './logger.js'
|
|
9
9
|
import { collector } from './collector.js'
|
|
10
10
|
|
|
11
|
+
function serializeMutation(
|
|
12
|
+
m: MutationRecord,
|
|
13
|
+
timestamp: number
|
|
14
|
+
): TraceMutation {
|
|
15
|
+
const addedNodes = Array.from(m.addedNodes).map((node) => {
|
|
16
|
+
assignRef(node as Element)
|
|
17
|
+
return parseFragment(node as Element)
|
|
18
|
+
})
|
|
19
|
+
const removedNodes = Array.from(m.removedNodes).map((node) => getRef(node))
|
|
20
|
+
const target = getRef(m.target)
|
|
21
|
+
const previousSibling = m.previousSibling ? getRef(m.previousSibling) : null
|
|
22
|
+
const nextSibling = m.nextSibling ? getRef(m.nextSibling) : null
|
|
23
|
+
let attributeValue: string | undefined
|
|
24
|
+
if (m.type === 'attributes') {
|
|
25
|
+
attributeValue = (m.target as Element).getAttribute(m.attributeName!) || ''
|
|
26
|
+
}
|
|
27
|
+
let newTextContent: string | undefined
|
|
28
|
+
if (m.type === 'characterData') {
|
|
29
|
+
newTextContent = (m.target as Element).textContent || ''
|
|
30
|
+
}
|
|
31
|
+
log(`added mutation: ${m.type}`)
|
|
32
|
+
return {
|
|
33
|
+
type: m.type,
|
|
34
|
+
attributeName: m.attributeName,
|
|
35
|
+
attributeNamespace: m.attributeNamespace,
|
|
36
|
+
oldValue: m.oldValue,
|
|
37
|
+
addedNodes,
|
|
38
|
+
target,
|
|
39
|
+
removedNodes,
|
|
40
|
+
previousSibling,
|
|
41
|
+
nextSibling,
|
|
42
|
+
timestamp,
|
|
43
|
+
attributeValue,
|
|
44
|
+
newTextContent
|
|
45
|
+
} as TraceMutation
|
|
46
|
+
}
|
|
47
|
+
|
|
11
48
|
try {
|
|
12
49
|
log('waiting for body to render')
|
|
13
50
|
await waitForBody()
|
|
@@ -32,66 +69,18 @@ try {
|
|
|
32
69
|
const observer = new MutationObserver((ml) => {
|
|
33
70
|
const timestamp = Date.now()
|
|
34
71
|
const mutationList = ml.filter((m) => m.attributeName !== 'data-wdio-ref')
|
|
35
|
-
|
|
36
72
|
log(`observed ${mutationList.length} mutations`)
|
|
37
73
|
try {
|
|
38
74
|
collector.captureMutation(
|
|
39
|
-
mutationList.map(
|
|
40
|
-
({
|
|
41
|
-
target: t,
|
|
42
|
-
addedNodes: an,
|
|
43
|
-
removedNodes: rn,
|
|
44
|
-
type,
|
|
45
|
-
attributeName,
|
|
46
|
-
attributeNamespace,
|
|
47
|
-
previousSibling: ps,
|
|
48
|
-
nextSibling: ns,
|
|
49
|
-
oldValue
|
|
50
|
-
}) => {
|
|
51
|
-
const addedNodes = Array.from(an).map((node) => {
|
|
52
|
-
assignRef(node as Element)
|
|
53
|
-
return parseFragment(node as Element)
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
const removedNodes = Array.from(rn).map((node) => getRef(node))
|
|
57
|
-
const target = getRef(t)
|
|
58
|
-
const previousSibling = ps ? getRef(ps) : null
|
|
59
|
-
const nextSibling = ns ? getRef(ns) : null
|
|
60
|
-
|
|
61
|
-
let attributeValue: string | undefined
|
|
62
|
-
if (type === 'attributes') {
|
|
63
|
-
attributeValue = (t as Element).getAttribute(attributeName!) || ''
|
|
64
|
-
}
|
|
65
|
-
let newTextContent: string | undefined
|
|
66
|
-
if (type === 'characterData') {
|
|
67
|
-
newTextContent = (t as Element).textContent || ''
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
log(`added mutation: ${type}`)
|
|
71
|
-
return {
|
|
72
|
-
type,
|
|
73
|
-
attributeName,
|
|
74
|
-
attributeNamespace,
|
|
75
|
-
oldValue,
|
|
76
|
-
addedNodes,
|
|
77
|
-
target,
|
|
78
|
-
removedNodes,
|
|
79
|
-
previousSibling,
|
|
80
|
-
nextSibling,
|
|
81
|
-
timestamp,
|
|
82
|
-
attributeValue,
|
|
83
|
-
newTextContent
|
|
84
|
-
} as TraceMutation
|
|
85
|
-
}
|
|
86
|
-
)
|
|
75
|
+
mutationList.map((m) => serializeMutation(m, timestamp))
|
|
87
76
|
)
|
|
88
|
-
} catch (err
|
|
89
|
-
collector.captureError(err)
|
|
77
|
+
} catch (err) {
|
|
78
|
+
collector.captureError(err as Error)
|
|
90
79
|
}
|
|
91
80
|
})
|
|
92
81
|
observer.observe(document.body, config)
|
|
93
|
-
} catch (err
|
|
94
|
-
collector.captureError(err)
|
|
82
|
+
} catch (err) {
|
|
83
|
+
collector.captureError(err as Error)
|
|
95
84
|
}
|
|
96
85
|
|
|
97
86
|
log('Finished program')
|
package/src/logger.ts
CHANGED
package/src/utils.ts
CHANGED
|
@@ -14,7 +14,7 @@ export type vElement = DefaultTreeAdapterMap['element']
|
|
|
14
14
|
export type vText = DefaultTreeAdapterMap['textNode']
|
|
15
15
|
export type vChildNode = DefaultTreeAdapterMap['childNode']
|
|
16
16
|
|
|
17
|
-
function createVNode(elem:
|
|
17
|
+
function createVNode(elem: { type: unknown; props: unknown }) {
|
|
18
18
|
const { type, props } = elem
|
|
19
19
|
return { type, props } as SimplifiedVNode
|
|
20
20
|
}
|
|
@@ -22,7 +22,7 @@ function createVNode(elem: any) {
|
|
|
22
22
|
export function parseNode(
|
|
23
23
|
fragment: vFragment | vComment | vText | vChildNode
|
|
24
24
|
): SimplifiedVNode | string {
|
|
25
|
-
const props: Record<string,
|
|
25
|
+
const props: Record<string, unknown> = {}
|
|
26
26
|
|
|
27
27
|
if (fragment.nodeName === '#comment') {
|
|
28
28
|
return (fragment as vComment).data
|
|
@@ -38,10 +38,10 @@ export function parseNode(
|
|
|
38
38
|
|
|
39
39
|
try {
|
|
40
40
|
return createVNode(
|
|
41
|
-
h(tagName, props, ...(childNodes || []).map((cn) => parseNode(cn)))
|
|
41
|
+
h(tagName, props, ...(childNodes || []).map((cn) => parseNode(cn)))
|
|
42
42
|
)
|
|
43
|
-
} catch (err
|
|
44
|
-
return createVNode(h('div', { class: 'parseNode' }, err.stack))
|
|
43
|
+
} catch (err) {
|
|
44
|
+
return createVNode(h('div', { class: 'parseNode' }, (err as Error).stack))
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
@@ -49,8 +49,10 @@ export function parseDocument(node: HTMLElement) {
|
|
|
49
49
|
try {
|
|
50
50
|
const fragment = parse(node.outerHTML)
|
|
51
51
|
return parseNode(fragment.childNodes[0])
|
|
52
|
-
} catch (err
|
|
53
|
-
return createVNode(
|
|
52
|
+
} catch (err) {
|
|
53
|
+
return createVNode(
|
|
54
|
+
h('div', { class: 'parseDocument' }, (err as Error).stack)
|
|
55
|
+
)
|
|
54
56
|
}
|
|
55
57
|
}
|
|
56
58
|
|
|
@@ -58,8 +60,10 @@ export function parseFragment(node: Element) {
|
|
|
58
60
|
try {
|
|
59
61
|
const fragment = parseFragmentImport(node.outerHTML)
|
|
60
62
|
return parseNode(fragment)
|
|
61
|
-
} catch (err
|
|
62
|
-
return createVNode(
|
|
63
|
+
} catch (err) {
|
|
64
|
+
return createVNode(
|
|
65
|
+
h('div', { class: 'parseFragmentWrapper' }, (err as Error).stack)
|
|
66
|
+
)
|
|
63
67
|
}
|
|
64
68
|
}
|
|
65
69
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest'
|
|
2
|
+
import { clearLogs, getLogs, log } from '../src/logger.js'
|
|
3
|
+
|
|
4
|
+
describe('script/logger', () => {
|
|
5
|
+
beforeEach(() => clearLogs())
|
|
6
|
+
|
|
7
|
+
it('appends a JSON-serialized line per call', () => {
|
|
8
|
+
log('hello', 42)
|
|
9
|
+
log({ a: 1 })
|
|
10
|
+
expect(getLogs()).toEqual(['"hello" 42', '{"a":1}'])
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('joins multiple args with a single space', () => {
|
|
14
|
+
log('a', 'b', 'c')
|
|
15
|
+
expect(getLogs()).toEqual(['"a" "b" "c"'])
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('clearLogs wipes the buffer', () => {
|
|
19
|
+
log('x')
|
|
20
|
+
clearLogs()
|
|
21
|
+
expect(getLogs()).toEqual([])
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('getLogs returns the live buffer (callers must not mutate)', () => {
|
|
25
|
+
log('one')
|
|
26
|
+
const snap = getLogs()
|
|
27
|
+
log('two')
|
|
28
|
+
expect(snap).toEqual(['"one"', '"two"'])
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('renders undefined args as an empty slot (JSON.stringify(undefined) → undefined)', () => {
|
|
32
|
+
log('a', undefined, 'b')
|
|
33
|
+
// JSON.stringify(undefined) returns undefined, and Array#join coerces it to ''
|
|
34
|
+
expect(getLogs()).toEqual(['"a" "b"'])
|
|
35
|
+
})
|
|
36
|
+
})
|