Skip to content

Commit 9e82ccc

Browse files
rewrite hook into class
1 parent 08b0339 commit 9e82ccc

File tree

6 files changed

+158
-110
lines changed

6 files changed

+158
-110
lines changed

packages/hook/src/index.ts

Lines changed: 14 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,13 @@
11
/// <reference types="../../script/types.d.ts" />
22

3-
import fs from 'node:fs/promises'
4-
import url from 'node:url'
5-
import path from 'node:path'
6-
7-
import { resolve } from 'import-meta-resolve'
8-
import { SevereServiceError } from 'webdriverio'
93
import type { Capabilities, Options } from '@wdio/types'
104
import type { WebDriverCommands } from '@wdio/protocols'
115

12-
import { PAGE_TRANSITION_COMMANDS } from './constants.js'
13-
import { type CommandLog, type TraceLog, TraceType } from './types.js'
14-
15-
let commandsLog: CommandLog[] = []
16-
let currentTraceId: string | undefined
17-
let sources = new Map<string, string>()
18-
19-
function getBrowserObject (elem: WebdriverIO.Element | WebdriverIO.Browser): WebdriverIO.Browser {
20-
const elemObject = elem as WebdriverIO.Element
21-
return (elemObject as WebdriverIO.Element).parent ? getBrowserObject(elemObject.parent) : elem as WebdriverIO.Browser
22-
}
6+
import { SessionCapturer } from './session.js'
237

248
export function setupForDevtools (opts: Options.WebdriverIO) {
9+
const session = new SessionCapturer()
10+
2511
/**
2612
* make sure to run with Bidi enabled by setting `webSocketUrl` to `true`
2713
*/
@@ -34,105 +20,26 @@ export function setupForDevtools (opts: Options.WebdriverIO) {
3420
: opts.capabilities as WebdriverIO.Capabilities
3521
caps.webSocketUrl = true
3622

23+
/**
24+
* register before command hook
25+
*/
3726
opts.beforeCommand = Array.isArray(opts.beforeCommand)
3827
? opts.beforeCommand
3928
: opts.beforeCommand ? [opts.beforeCommand] : []
40-
opts.beforeCommand.push(async function (this: WebdriverIO.Browser, command) {
41-
await injectScript(this)
42-
43-
/**
44-
* capture trace on `deleteSession` before command is called
45-
*/
46-
if (command === 'deleteSession') {
47-
await this.pause(1000)
48-
const browser = getBrowserObject(this)
49-
await captureTrace(browser)
50-
}
29+
opts.beforeCommand.push(async function (this: WebdriverIO.Browser, command: keyof WebDriverCommands) {
30+
return session.beforeCommand(this, command)
5131
})
5232

33+
/**
34+
* register after command hook
35+
*/
5336
opts.afterCommand = Array.isArray(opts.afterCommand)
5437
? opts.afterCommand
5538
: opts.afterCommand ? [opts.afterCommand] : []
56-
opts.afterCommand.push(async function(this: WebdriverIO.Browser, command: keyof WebDriverCommands, args, result, error) {
57-
const timestamp = Date.now()
58-
const callSource = (new Error('')).stack?.split('\n').pop()?.split(' ').pop()!
59-
const sourceFile = callSource.split(':').slice(0, -2).join(':')
60-
const absPath = sourceFile.startsWith('file://')
61-
? url.fileURLToPath(sourceFile)
62-
: sourceFile
63-
if (sourceFile && !sources.has(sourceFile)) {
64-
const sourceCode = await fs.readFile(absPath, 'utf-8')
65-
sources.set(absPath, sourceCode.toString())
66-
}
67-
commandsLog.push({ command, args, result, error, timestamp, callSource: absPath })
68-
69-
if (PAGE_TRANSITION_COMMANDS.includes(command)) {
70-
const browser = getBrowserObject(this)
71-
await captureTrace(browser)
72-
}
73-
})
74-
75-
return opts
76-
}
77-
78-
let isInjected = false
79-
async function injectScript (browser: WebdriverIO.Browser) {
80-
if (isInjected) {
81-
return
82-
}
83-
84-
if (!browser.isBidi) {
85-
throw new SevereServiceError(`Can not set up devtools for session with id "${browser.sessionId}" because it doesn't support WebDriver Bidi`)
86-
}
39+
opts.afterCommand.push(session.afterCommand.bind(session))
8740

88-
isInjected = true
89-
const script = await resolve('@devtools/script', import.meta.url)
90-
const source = (await fs.readFile(url.fileURLToPath(script))).toString()
91-
const functionDeclaration = `async () => { ${source} }`
92-
93-
await browser.scriptAddPreloadScriptCommand({
94-
functionDeclaration
95-
})
96-
}
97-
98-
async function captureTrace (browser: WebdriverIO.Browser) {
9941
/**
100-
* only capture trace if script was injected and command is a page transition command
42+
* return modified session configuration
10143
*/
102-
if (!isInjected) {
103-
return
104-
}
105-
106-
const [mutations, logs, pageMetadata, consoleLogs] = await browser.execute(() => [
107-
window.wdioDOMChanges,
108-
window.wdioTraceLogs,
109-
window.wdioMetadata,
110-
window.wdioConsoleLogs
111-
])
112-
113-
if (!currentTraceId) {
114-
currentTraceId = pageMetadata.id
115-
}
116-
117-
if (currentTraceId !== pageMetadata.id) {
118-
commandsLog = []
119-
sources = new Map()
120-
}
121-
122-
const outputDir = browser.options.outputDir || process.cwd()
123-
const { capabilities, ...options } = browser.options as Options.WebdriverIO
124-
const traceLog: TraceLog = {
125-
mutations,
126-
logs,
127-
consoleLogs,
128-
metadata: {
129-
type: TraceType.Standalone,
130-
...pageMetadata,
131-
options,
132-
capabilities
133-
},
134-
commands: commandsLog,
135-
sources: Object.fromEntries(sources)
136-
}
137-
await fs.writeFile(path.join(outputDir, `${pageMetadata.id}.json`), JSON.stringify(traceLog))
44+
return opts
13845
}

packages/hook/src/session.ts

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import fs from 'node:fs/promises'
2+
import path from 'node:path'
3+
import url from 'node:url'
4+
5+
import { resolve } from 'import-meta-resolve'
6+
import { SevereServiceError } from 'webdriverio'
7+
import type { Options } from '@wdio/types'
8+
import type { WebDriverCommands } from '@wdio/protocols'
9+
10+
import { getBrowserObject } from './utils.js'
11+
import { PAGE_TRANSITION_COMMANDS } from './constants.js'
12+
import { type CommandLog, type TraceLog, TraceType } from './types.js'
13+
14+
export class SessionCapturer {
15+
#isInjected = false
16+
#commandsLog: CommandLog[] = []
17+
#sources = new Map<string, string>()
18+
#browser: WebdriverIO.Browser | undefined
19+
20+
/**
21+
* before command hook
22+
*
23+
* Used to
24+
* - capture the browser object if not existing
25+
* - inject script for capturing application states
26+
* - capture trace on `deleteSession` command
27+
*
28+
* @param {object} browser browser object
29+
* @param {string} command command name
30+
*/
31+
async beforeCommand (browser: WebdriverIO.Browser | WebdriverIO.Element, command: keyof WebDriverCommands) {
32+
if (!this.#browser) {
33+
this.#browser = getBrowserObject(browser)
34+
}
35+
await this.#injectScript()
36+
37+
/**
38+
* capture trace on `deleteSession` since we can't do it in `afterCommand` as the session
39+
* would be terminated by then
40+
*/
41+
if (command === 'deleteSession') {
42+
await this.#browser.pause(1000)
43+
await this.#captureTrace()
44+
}
45+
}
46+
47+
/**
48+
* after command hook
49+
*
50+
* Used to
51+
* - capture command logs
52+
* - capture trace data from the application under test
53+
*
54+
*
55+
* @param {string} command command name
56+
* @param {Array} args command arguments
57+
* @param {object} result command result
58+
* @param {Error} error command error
59+
*/
60+
async afterCommand (command: keyof WebDriverCommands, args: any[], result: any, error: Error) {
61+
const timestamp = Date.now()
62+
const callSource = (new Error('')).stack?.split('\n').pop()?.split(' ').pop()!
63+
const sourceFile = callSource.split(':').slice(0, -2).join(':')
64+
const absPath = sourceFile.startsWith('file://')
65+
? url.fileURLToPath(sourceFile)
66+
: sourceFile
67+
if (sourceFile && !this.#sources.has(sourceFile)) {
68+
const sourceCode = await fs.readFile(absPath, 'utf-8')
69+
this.#sources.set(absPath, sourceCode.toString())
70+
}
71+
this.#commandsLog.push({ command, args, result, error, timestamp, callSource: absPath })
72+
73+
/**
74+
* capture trace and write to file on commands that could trigger a page transition
75+
*/
76+
if (PAGE_TRANSITION_COMMANDS.includes(command)) {
77+
await this.#captureTrace()
78+
}
79+
}
80+
81+
async #injectScript () {
82+
if (this.#isInjected || !this.#browser) {
83+
return
84+
}
85+
86+
if (!this.#browser.isBidi) {
87+
throw new SevereServiceError(`Can not set up devtools for session with id "${browser.sessionId}" because it doesn't support WebDriver Bidi`)
88+
}
89+
90+
this.#isInjected = true
91+
const script = await resolve('@devtools/script', import.meta.url)
92+
const source = (await fs.readFile(url.fileURLToPath(script))).toString()
93+
const functionDeclaration = `async () => { ${source} }`
94+
95+
await this.#browser.scriptAddPreloadScriptCommand({
96+
functionDeclaration
97+
})
98+
}
99+
100+
async #captureTrace () {
101+
/**
102+
* only capture trace if script was injected
103+
*/
104+
if (!this.#isInjected || !this.#browser) {
105+
return
106+
}
107+
108+
const [mutations, logs, pageMetadata, consoleLogs] = await this.#browser.execute(() => [
109+
window.wdioDOMChanges,
110+
window.wdioTraceLogs,
111+
window.wdioMetadata,
112+
window.wdioConsoleLogs
113+
])
114+
115+
const outputDir = this.#browser.options.outputDir || process.cwd()
116+
const { capabilities, ...options } = this.#browser.options as Options.WebdriverIO
117+
const traceLog: TraceLog = {
118+
mutations,
119+
logs,
120+
consoleLogs,
121+
metadata: {
122+
type: TraceType.Standalone,
123+
...pageMetadata,
124+
options,
125+
capabilities
126+
},
127+
commands: this.#commandsLog,
128+
sources: Object.fromEntries(this.#sources)
129+
}
130+
131+
/**
132+
* ToDo(Christian): we are writing the trace to file after every command, we should find smarter ways
133+
* to do this less often
134+
*/
135+
await fs.writeFile(path.join(outputDir, `wdio-trace-${this.#browser.sessionId}.json`), JSON.stringify(traceLog))
136+
}
137+
}

packages/hook/src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export interface TraceLog {
2121
consoleLogs: ConsoleLogs[]
2222
metadata: {
2323
type: TraceType
24-
id: string
24+
pageLoadId: string
2525
url: string
2626
options: Omit<Options.WebdriverIO, 'capabilities'>
2727
capabilities: Capabilities.RemoteCapability

packages/hook/src/utils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export function getBrowserObject (elem: WebdriverIO.Element | WebdriverIO.Browser): WebdriverIO.Browser {
2+
const elemObject = elem as WebdriverIO.Element
3+
return (elemObject as WebdriverIO.Element).parent ? getBrowserObject(elemObject.parent) : elem as WebdriverIO.Browser
4+
}

packages/script/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ window.wdioDOMChanges = []
55
window.wdioConsoleLogs = []
66
window.wdioMetadata = {
77
url: window.location.href,
8-
id: `wdio-trace-${Math.random().toString().slice(2)}`,
8+
pageLoadId: Math.random().toString().slice(2),
99
viewport: window.visualViewport!
1010
}
1111

packages/script/types.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export interface TraceMetadata {
2-
id: string
2+
pageLoadId: string
33
url: string
44
viewport: VisualViewport
55
}

0 commit comments

Comments
 (0)