Skip to content

Commit d537671

Browse files
propagate session data to devtools
1 parent f396d43 commit d537671

File tree

13 files changed

+217
-20
lines changed

13 files changed

+217
-20
lines changed

example/wdio.conf.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,8 +221,9 @@ export const config: Options.Testrunner = {
221221
* @param {Array.<String>} specs List of spec file paths that are to be run
222222
* @param {object} browser instance of created browser/device session
223223
*/
224-
// before: function (capabilities, specs) {
225-
// },
224+
before: async function () {
225+
await browser.pause(5000)
226+
},
226227
/**
227228
* Runs before a WebdriverIO command gets executed.
228229
* @param {string} commandName hook command name

packages/app/src/components/placeholder.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export class DevtoolsPlaceholder extends Element {
1616
.ph-item {
1717
border: 0;
1818
height: 100%;
19+
background-color: transparent;
1920
}
2021
2122
.ph-item div {

packages/app/src/context.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ export { type TraceLog }
77

88
const CACHE_ID = 'wdio-trace-cache'
99

10+
interface SocketMessage<T extends keyof TraceLog = keyof TraceLog> {
11+
scope: T
12+
data: TraceLog[T]
13+
}
14+
1015
async function fetchData () {
1116
const hasSocketConnection = await connectSocket()
1217
if (hasSocketConnection) {
@@ -23,14 +28,16 @@ async function connectSocket () {
2328
/**
2429
* expect application to be served from backend
2530
*/
26-
console.log(`Connecting to ws://${window.location}`)
27-
const ws = new WebSocket(`ws://${window.location}`)
31+
const wsUrl = `ws://${window.location.host}/client`
32+
console.log(`Connecting to ${wsUrl}`)
33+
const ws = new WebSocket(wsUrl)
2834

2935
if (ws.readyState === WebSocket.CLOSED) {
3036
return undefined
3137
}
3238

3339
return new Promise((resolve, reject) => {
40+
ws.addEventListener('message', handleSocketMessage)
3441
ws.onopen = () => resolve({})
3542
ws.onerror = reject
3643
})
@@ -45,6 +52,22 @@ function loadCachedTraceData () {
4552
}
4653
}
4754

55+
function handleSocketMessage (event: MessageEvent) {
56+
try {
57+
const { scope, data } = JSON.parse(event.data) as SocketMessage
58+
if (scope === 'mutations') {
59+
context.__context__.mutations = [
60+
...(context.__context__.mutations || []),
61+
...data as TraceMutation[]
62+
]
63+
} else {
64+
context.__context__[scope] = data as any
65+
}
66+
} catch (e: unknown) {
67+
console.warn(`Failed to parse socket message: ${(e as Error).message}`)
68+
}
69+
}
70+
4871
export function cacheTraceData (traceLog: TraceLog) {
4972
localStorage.setItem(CACHE_ID, JSON.stringify(traceLog))
5073
context.__context__ = traceLog

packages/backend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
},
2020
"dependencies": {
2121
"@fastify/static": "^6.12.0",
22+
"@fastify/websocket": "^8.2.0",
2223
"@wdio/devtools-app": "workspace:^",
2324
"@wdio/logger": "^8.24.12",
2425
"fastify": "^4.24.3",

packages/backend/src/index.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import url from 'node:url'
22

33
import Fastify, { type FastifyInstance } from 'fastify'
44
import staticServer from '@fastify/static'
5+
import websocket from '@fastify/websocket'
56
import getPort from 'get-port'
67
import logger from '@wdio/logger'
78

@@ -12,21 +13,41 @@ let server: FastifyInstance | undefined
1213

1314
interface DevtoolsBackendOptions {
1415
port?: number
16+
hostname?: string
1517
}
1618

1719
const log = logger('@wdio/devtools-backend')
20+
const clients = new Set<websocket.SocketStream>()
1821

1922
export async function start (opts: DevtoolsBackendOptions = {}) {
23+
const host = opts.hostname || 'localhost'
2024
const port = opts.port || await getPort({ port: DEFAULT_PORT })
2125
const appPath = await getDevtoolsApp()
2226

2327
server = Fastify({ logger: true })
28+
server.register(websocket)
2429
server.register(staticServer, {
2530
root: appPath
2631
})
2732

33+
server.register(async function (f) {
34+
f.get('/client', { websocket: true }, (connection) => {
35+
log.info('client connected')
36+
clients.add(connection)
37+
})
38+
f.get('/worker', { websocket: true }, (connection) => {
39+
/**
40+
* forward messages to all connected clients
41+
*/
42+
connection.socket.on('message', (message) => {
43+
log.info(`received ${message.toLocaleString().length} byte message from worker to ${clients.size} clients`, )
44+
clients.forEach((client) => client.socket.send(message.toString()))
45+
})
46+
})
47+
})
48+
2849
log.info(`Starting WebdriverIO Devtools application on port ${port}`)
29-
await server.listen({ port })
50+
await server.listen({ port, host })
3051
return server
3152
}
3253

@@ -35,7 +56,9 @@ export async function stop () {
3556
return
3657
}
3758

59+
log.info('Shutting down WebdriverIO Devtools application')
3860
await server.close()
61+
clients.clear()
3962
}
4063

4164
/**

packages/script/src/collector.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,17 @@ class DataCollector {
2626
clearLogs()
2727
}
2828

29+
getMetadata () {
30+
return this.#metadata
31+
}
32+
2933
getTraceData () {
3034
const data = {
3135
errors: this.#errors,
3236
mutations: this.#mutations,
3337
consoleLogs: this.#consoleLogs.getArtifacts(),
3438
traceLogs: getLogs(),
35-
metadata: this.#metadata
39+
metadata: this.getMetadata(),
3640
} as const
3741
this.reset()
3842
return data

packages/service/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@
3232
"@wdio/types": "^8.23.0",
3333
"import-meta-resolve": "^4.0.0",
3434
"stack-trace": "1.0.0-pre2",
35-
"webdriverio": "^8.23.0"
35+
"webdriverio": "^8.23.0",
36+
"ws": "^8.15.1"
3637
},
3738
"license": "MIT",
3839
"devDependencies": {

packages/service/src/index.ts

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,14 @@ import path from 'node:path'
44

55
import logger from '@wdio/logger'
66
import { SevereServiceError } from 'webdriverio'
7-
import type { Capabilities, Options } from '@wdio/types'
7+
import type { Services, Reporters, Capabilities, Options } from '@wdio/types'
88
import type { WebDriverCommands } from '@wdio/protocols'
9-
import type { Services, Reporters } from '@wdio/types'
109

1110
import { SessionCapturer } from './session.js'
1211
import { TestReporter } from './reporter.js'
1312
import { DevToolsAppLauncher } from './launcher.js'
1413
import { getBrowserObject } from './utils.ts'
15-
import { type TraceLog, TraceType } from './types.ts'
14+
import { type TraceLog, type ExtendedCapabilities, TraceType } from './types.ts'
1615

1716
export const launcher = DevToolsAppLauncher
1817

@@ -24,6 +23,7 @@ const log = logger('@wdio/devtools-service')
2423
export function setupForDevtools (opts: Options.WebdriverIO) {
2524
let browserCaptured = false
2625
const service = new DevToolsHookService()
26+
service.captureType = TraceType.Standalone
2727
service.beforeSession(null as never, opts.capabilities as Capabilities.RemoteCapability)
2828

2929
/**
@@ -69,8 +69,21 @@ export default class DevToolsHookService implements Services.ServiceInstance {
6969
#sessionCapturer = new SessionCapturer()
7070
#browser: WebdriverIO.Browser | undefined
7171

72-
before (_: never, __: never, browser: WebdriverIO.Browser) {
72+
/**
73+
* allows to define the type of data being captured to hint the
74+
* devtools app which data to expect
75+
*/
76+
captureType = TraceType.Testrunner
77+
78+
before (caps: Capabilities.RemoteCapability, __: never, browser: WebdriverIO.Browser) {
7379
this.#browser = browser
80+
81+
console.log('\n\n\nWUTTT', caps)
82+
const w3cCaps = caps as Capabilities.W3CCapabilities
83+
const c = w3cCaps.alwaysMatch
84+
? w3cCaps.alwaysMatch as ExtendedCapabilities
85+
: caps as ExtendedCapabilities
86+
this.#sessionCapturer = new SessionCapturer(c['wdio:devtoolsOptions'])
7487
}
7588

7689
beforeSession (config: Options.WebdriverIO | Options.Testrunner, capabilities: Capabilities.RemoteCapability) {
@@ -103,7 +116,7 @@ export default class DevToolsHookService implements Services.ServiceInstance {
103116
*/
104117
class DevToolsReporter extends TestReporter {
105118
constructor (options: Reporters.Options) {
106-
super(options)
119+
super(options, self.#sessionCapturer)
107120
self.#testReporters.push(this)
108121
}
109122
}
@@ -119,6 +132,20 @@ export default class DevToolsHookService implements Services.ServiceInstance {
119132
}
120133

121134
afterCommand(command: keyof WebDriverCommands, args: any[], result: any, error: Error) {
135+
if (this.#browser && command === 'navigateTo') {
136+
/**
137+
* propagate session metadata at the beginning of the session
138+
*/
139+
browser.execute(() => window.wdioTraceCollector.getMetadata())
140+
.then((metadata) => this.#sessionCapturer.sendUpstream('metadata', {
141+
...metadata,
142+
type: this.captureType,
143+
options: browser.options,
144+
capabilities: browser.capabilities
145+
})
146+
)
147+
}
148+
122149
return this.#sessionCapturer.afterCommand(browser, command, args, result, error)
123150
}
124151

@@ -127,7 +154,7 @@ export default class DevToolsHookService implements Services.ServiceInstance {
127154
* we can use it to write all trace information to a file
128155
*/
129156
async after () {
130-
if (!this.#browser) {
157+
if (!this.#browser || this.#sessionCapturer.isReportingUpstream) {
131158
return
132159
}
133160
const outputDir = this.#browser.options.outputDir || process.cwd()
@@ -137,7 +164,7 @@ export default class DevToolsHookService implements Services.ServiceInstance {
137164
logs: this.#sessionCapturer.traceLogs,
138165
consoleLogs: this.#sessionCapturer.consoleLogs,
139166
metadata: {
140-
type: TraceType.Standalone,
167+
type: this.captureType,
141168
...this.#sessionCapturer.metadata!,
142169
options,
143170
capabilities

packages/service/src/launcher.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { remote } from 'webdriverio'
22
import { start } from '@wdio/devtools-backend'
33

44
import { DEFAULT_LAUNCH_CAPS } from './constants.ts'
5-
import type { ServiceOptions } from './types.js'
5+
import type { ServiceOptions, ExtendedCapabilities } from './types.js'
66

77
export class DevToolsAppLauncher {
88
#options: ServiceOptions
@@ -12,18 +12,22 @@ export class DevToolsAppLauncher {
1212
this.#options = options
1313
}
1414

15-
async onPrepare () {
15+
async onPrepare (_: never, caps: ExtendedCapabilities[]) {
1616
try {
17-
const { server } = await start({ port: this.#options.port })
17+
const { server } = await start({
18+
port: this.#options.port,
19+
hostname: this.#options.hostname
20+
})
1821
const address = server.address()
1922
const port = address && typeof address === 'object'
2023
? address.port
2124
: undefined
2225

2326
if (!port) {
24-
console.log(`Failed to start server on port ${port}`)
27+
return console.log(`Failed to start server on port ${port}`)
2528
}
2629

30+
this.#updateCapabilities(caps, { port })
2731
this.#browser = await remote({
2832
capabilities: {
2933
...DEFAULT_LAUNCH_CAPS,
@@ -39,4 +43,20 @@ export class DevToolsAppLauncher {
3943
async onComplete () {
4044
await this.#browser?.deleteSession()
4145
}
46+
47+
#updateCapabilities (caps: ExtendedCapabilities[], devtoolsApp: { port: number }) {
48+
/**
49+
* we don't support multiremote yet
50+
*/
51+
if (!Array.isArray(caps)) {
52+
return
53+
}
54+
55+
for (const cap of caps) {
56+
cap['wdio:devtoolsOptions'] = {
57+
port: devtoolsApp.port,
58+
hostname: 'localhost'
59+
}
60+
}
61+
}
4262
}

packages/service/src/reporter.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,34 @@
1-
import WebdriverIOReporter from '@wdio/reporter'
1+
import WebdriverIOReporter, { type SuiteStats, type TestStats } from '@wdio/reporter'
2+
import type { SessionCapturer } from './session.ts'
23

34
export class TestReporter extends WebdriverIOReporter {
5+
#sessionCapturer: SessionCapturer
6+
7+
constructor (options: any, sessionCapturer: SessionCapturer) {
8+
super(options)
9+
this.#sessionCapturer = sessionCapturer
10+
}
11+
12+
onSuiteStart(suiteStats: SuiteStats): void {
13+
super.onSuiteStart(suiteStats)
14+
this.#sessionCapturer.sendUpstream('suites', [this.suites])
15+
}
16+
17+
onTestStart(testStats: TestStats): void {
18+
super.onTestStart(testStats)
19+
this.#sessionCapturer.sendUpstream('suites', [this.suites])
20+
}
21+
22+
onTestEnd(testStats: TestStats): void {
23+
super.onTestEnd(testStats)
24+
this.#sessionCapturer.sendUpstream('suites', [this.suites])
25+
}
26+
27+
onSuiteEnd(suiteStats: SuiteStats): void {
28+
super.onSuiteEnd(suiteStats)
29+
this.#sessionCapturer.sendUpstream('suites', [this.suites])
30+
}
31+
432
get report () {
533
return this.suites
634
}

0 commit comments

Comments
 (0)