1
1
import './App.less' ;
2
2
3
3
import { Alert , ConfigProvider , Empty } from 'antd' ;
4
- import { useCallback , useEffect , useRef , useState } from 'react' ;
4
+ import { useEffect , useRef , useState } from 'react' ;
5
5
import { Panel , PanelGroup , PanelResizeHandle } from 'react-resizable-panels' ;
6
6
7
7
import { antiEscapeScriptTag } from '@midscene/shared/utils' ;
@@ -10,11 +10,11 @@ import DetailPanel from './components/detail-panel';
10
10
import DetailSide from './components/detail-side' ;
11
11
import GlobalHoverPreview from './components/global-hover-preview' ;
12
12
import Sidebar from './components/sidebar' ;
13
- import { useExecutionDump } from './components/store' ;
13
+ import { type DumpStoreType , useExecutionDump } from './components/store' ;
14
14
import Timeline from './components/timeline' ;
15
15
import type {
16
- ExecutionDumpWithPlaywrightAttributes ,
17
- StoreState ,
16
+ PlaywrightTaskAttributes ,
17
+ PlaywrightTasks ,
18
18
VisualizerProps ,
19
19
} from './types' ;
20
20
@@ -23,41 +23,31 @@ let globalRenderCount = 1;
23
23
function Visualizer ( props : VisualizerProps ) : JSX . Element {
24
24
const { dumps } = props ;
25
25
26
- const executionDump = useExecutionDump ( ( store : StoreState ) => store . dump ) ;
26
+ const executionDump = useExecutionDump ( ( store : DumpStoreType ) => store . dump ) ;
27
27
const executionDumpLoadId = useExecutionDump (
28
- ( store : StoreState ) => store . _executionDumpLoadId ,
28
+ ( store ) => store . _executionDumpLoadId ,
29
29
) ;
30
30
31
- const setReplayAllMode = useExecutionDump (
32
- ( store : StoreState ) => store . setReplayAllMode ,
33
- ) ;
31
+ const setReplayAllMode = useExecutionDump ( ( store ) => store . setReplayAllMode ) ;
34
32
const replayAllScripts = useExecutionDump (
35
- ( store : StoreState ) => store . allExecutionAnimation ,
36
- ) ;
37
- const insightWidth = useExecutionDump (
38
- ( store : StoreState ) => store . insightWidth ,
39
- ) ;
40
- const insightHeight = useExecutionDump (
41
- ( store : StoreState ) => store . insightHeight ,
42
- ) ;
43
- const replayAllMode = useExecutionDump (
44
- ( store : StoreState ) => store . replayAllMode ,
45
- ) ;
46
- const setGroupedDump = useExecutionDump (
47
- ( store : StoreState ) => store . setGroupedDump ,
33
+ ( store ) => store . allExecutionAnimation ,
48
34
) ;
35
+ const insightWidth = useExecutionDump ( ( store ) => store . insightWidth ) ;
36
+ const insightHeight = useExecutionDump ( ( store ) => store . insightHeight ) ;
37
+ const replayAllMode = useExecutionDump ( ( store ) => store . replayAllMode ) ;
38
+ const setGroupedDump = useExecutionDump ( ( store ) => store . setGroupedDump ) ;
49
39
const sdkVersion = useExecutionDump ( ( store ) => store . sdkVersion ) ;
50
40
const modelName = useExecutionDump ( ( store ) => store . modelName ) ;
51
41
const modelDescription = useExecutionDump ( ( store ) => store . modelDescription ) ;
52
- const reset = useExecutionDump ( ( store : StoreState ) => store . reset ) ;
42
+ const reset = useExecutionDump ( ( store ) => store . reset ) ;
53
43
const [ mainLayoutChangeFlag , setMainLayoutChangeFlag ] = useState ( 0 ) ;
54
44
const mainLayoutChangedRef = useRef ( false ) ;
55
- const dump = useExecutionDump ( ( store : StoreState ) => store . dump ) ;
45
+ const dump = useExecutionDump ( ( store ) => store . dump ) ;
56
46
const [ proModeEnabled , setProModeEnabled ] = useState ( false ) ;
57
47
58
48
useEffect ( ( ) => {
59
- if ( dumps ) {
60
- setGroupedDump ( dumps [ 0 ] ) ;
49
+ if ( dumps ?. [ 0 ] ) {
50
+ setGroupedDump ( dumps [ 0 ] . get ( ) , dumps [ 0 ] . attributes ) ;
61
51
}
62
52
return ( ) => {
63
53
reset ( ) ;
@@ -150,10 +140,6 @@ function Visualizer(props: VisualizerProps): JSX.Element {
150
140
< div className = "page-side" >
151
141
< Sidebar
152
142
dumps = { dumps }
153
- selectedDump = { executionDump }
154
- onDumpSelect = { ( dump ) => {
155
- setGroupedDump ( dump ) ;
156
- } }
157
143
proModeEnabled = { proModeEnabled }
158
144
onProModeChange = { setProModeEnabled }
159
145
replayAllScripts = { replayAllScripts }
@@ -243,11 +229,11 @@ function Visualizer(props: VisualizerProps): JSX.Element {
243
229
}
244
230
245
231
export function App ( ) {
246
- function getDumpElements ( ) : ExecutionDumpWithPlaywrightAttributes [ ] {
232
+ function getDumpElements ( ) : PlaywrightTasks [ ] {
247
233
const dumpElements = document . querySelectorAll (
248
234
'script[type="midscene_web_dump"]' ,
249
235
) ;
250
- const reportDump : ExecutionDumpWithPlaywrightAttributes [ ] = [ ] ;
236
+ const reportDump : PlaywrightTasks [ ] = [ ] ;
251
237
Array . from ( dumpElements )
252
238
. filter ( ( el ) => {
253
239
const textContent = el . textContent ;
@@ -257,65 +243,95 @@ export function App() {
257
243
return ! ! textContent ;
258
244
} )
259
245
. forEach ( ( el ) => {
260
- const attributes : Record < string , any > = { } ;
246
+ const attributes : PlaywrightTaskAttributes = {
247
+ playwright_test_name : '' ,
248
+ playwright_test_description : '' ,
249
+ playwright_test_id : '' ,
250
+ playwright_test_title : '' ,
251
+ playwright_test_status : '' ,
252
+ playwright_test_duration : '' ,
253
+ } ;
261
254
Array . from ( el . attributes ) . forEach ( ( attr ) => {
262
255
const { name, value } = attr ;
263
256
const valueDecoded = decodeURIComponent ( value ) ;
264
257
if ( name . startsWith ( 'playwright_' ) ) {
265
- attributes [ attr . name ] = valueDecoded ;
258
+ attributes [ attr . name as keyof PlaywrightTaskAttributes ] =
259
+ valueDecoded ;
266
260
}
267
261
} ) ;
268
- const content = antiEscapeScriptTag ( el . textContent || '' ) ;
269
- try {
270
- const jsonContent = JSON . parse ( content ) ;
271
- jsonContent . attributes = attributes ;
272
- reportDump . push ( jsonContent ) ;
273
- } catch ( e ) {
274
- console . error ( el ) ;
275
- console . error ( 'failed to parse json content' , e ) ;
276
- }
262
+
263
+ // Lazy loading: Store raw content and parse only when get() is called
264
+ let cachedJsonContent : any = null ;
265
+ let isParsed = false ;
266
+
267
+ reportDump . push ( {
268
+ get : ( ) => {
269
+ if ( ! isParsed ) {
270
+ try {
271
+ console . time ( 'parse_dump' ) ;
272
+ const content = antiEscapeScriptTag ( el . textContent || '' ) ;
273
+ cachedJsonContent = JSON . parse ( content ) ;
274
+ console . timeEnd ( 'parse_dump' ) ;
275
+ cachedJsonContent . attributes = attributes ;
276
+ isParsed = true ;
277
+ } catch ( e ) {
278
+ console . error ( el ) ;
279
+ console . error ( 'failed to parse json content' , e ) ;
280
+ // Return a fallback object to prevent crashes
281
+ cachedJsonContent = {
282
+ attributes,
283
+ error : 'Failed to parse JSON content' ,
284
+ } ;
285
+ isParsed = true ;
286
+ }
287
+ }
288
+ return cachedJsonContent ;
289
+ } ,
290
+ attributes : attributes ,
291
+ } ) ;
277
292
} ) ;
278
293
return reportDump ;
279
294
}
280
295
281
- const [ reportDump , setReportDump ] = useState <
282
- ExecutionDumpWithPlaywrightAttributes [ ]
283
- > ( [ ] ) ;
296
+ const [ reportDump , setReportDump ] = useState < PlaywrightTasks [ ] > ( [ ] ) ;
284
297
const [ error , setError ] = useState < string | null > ( null ) ;
285
298
286
299
const dumpsLoadedRef = useRef ( false ) ;
287
300
288
- const loadDumpElements = useCallback ( ( ) => {
289
- const currentElements = document . querySelectorAll (
290
- 'script[type="midscene_web_dump"]' ,
291
- ) ;
301
+ useEffect ( ( ) => {
302
+ // Check if document is already loaded
292
303
293
- // If it has been loaded and the number of elements has not changed, skip it.
294
- if (
295
- dumpsLoadedRef . current &&
296
- currentElements . length === reportDump . length
297
- ) {
298
- return ;
299
- }
304
+ const loadDumpElements = ( ) => {
305
+ const currentElements = document . querySelectorAll (
306
+ 'script[type="midscene_web_dump"]' ,
307
+ ) ;
300
308
301
- dumpsLoadedRef . current = true ;
302
- if (
303
- currentElements . length === 1 &&
304
- currentElements [ 0 ] . textContent ?. trim ( ) === ''
305
- ) {
306
- setError ( 'There is no dump data to display.' ) ;
307
- setReportDump ( [ ] ) ;
308
- return ;
309
- }
310
- setError ( null ) ;
311
- setReportDump ( getDumpElements ( ) ) ;
312
- } , [ reportDump . length ] ) ;
309
+ // If it has been loaded and the number of elements has not changed, skip it.
310
+ if (
311
+ dumpsLoadedRef . current &&
312
+ currentElements . length === reportDump . length
313
+ ) {
314
+ return ;
315
+ }
316
+
317
+ dumpsLoadedRef . current = true ;
318
+ if (
319
+ currentElements . length === 1 &&
320
+ currentElements [ 0 ] . textContent ?. trim ( ) === ''
321
+ ) {
322
+ setError ( 'There is no dump data to display.' ) ;
323
+ setReportDump ( [ ] ) ;
324
+ return ;
325
+ }
326
+ setError ( null ) ;
327
+ const dumpElements = getDumpElements ( ) ;
328
+ setReportDump ( dumpElements ) ;
329
+ } ;
313
330
314
- useEffect ( ( ) => {
315
- // Check if document is already loaded
316
331
const loadDumps = ( ) => {
317
- console . log ( 'Loading dump elements... ') ;
332
+ console . time ( 'loading_dump ') ;
318
333
loadDumpElements ( ) ;
334
+ console . timeEnd ( 'loading_dump' ) ;
319
335
} ;
320
336
321
337
// If DOM is already loaded (React mounts after DOMContentLoaded in most cases)
@@ -330,39 +346,10 @@ export function App() {
330
346
document . addEventListener ( 'DOMContentLoaded' , loadDumps ) ;
331
347
}
332
348
333
- // Set up a MutationObserver to detect if dump scripts are added after initial load
334
- const observer = new MutationObserver ( ( mutations ) => {
335
- for ( const mutation of mutations ) {
336
- if ( mutation . type === 'childList' ) {
337
- const addedNodes = Array . from ( mutation . addedNodes ) ;
338
- const hasDumpScripts = addedNodes . some (
339
- ( node ) =>
340
- node . nodeType === Node . ELEMENT_NODE &&
341
- node . nodeName === 'SCRIPT' &&
342
- ( node as HTMLElement ) . getAttribute ( 'type' ) ===
343
- 'midscene_web_dump' ,
344
- ) ;
345
-
346
- if ( hasDumpScripts ) {
347
- loadDumps ( ) ;
348
- break ;
349
- }
350
- }
351
- }
352
- } ) ;
353
-
354
- // Start observing the document with the configured parameters
355
- observer . observe ( document . body , { childList : true , subtree : true } ) ;
356
-
357
- // Safety fallback in case other methods fail
358
- const fallbackTimer = setTimeout ( loadDumps , 3000 ) ;
359
-
360
349
return ( ) => {
361
350
document . removeEventListener ( 'DOMContentLoaded' , loadDumps ) ;
362
- observer . disconnect ( ) ;
363
- clearTimeout ( fallbackTimer ) ;
364
351
} ;
365
- } , [ loadDumpElements ] ) ;
352
+ } , [ ] ) ;
366
353
367
354
if ( error ) {
368
355
return (
0 commit comments