Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/selenium-configure-options.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@wdio/selenium-devtools': patch
---

`DevTools.configure()` now applies `port`, `hostname`, `captureScreenshots`, `filmstrip` and `emitArtifactsManifest`. They were documented as configure options but silently ignored, so the defaults always applied. `port`/`hostname` passed after the backend start has begun are ignored with a warning, and an invalid port, a `null` value or a value of the wrong type (for example the string `'false'` for a boolean option) is refused with a warning rather than applied. A rerun keeps its parent's backend whatever they say.
Comment thread
vishnuv688 marked this conversation as resolved.
2 changes: 2 additions & 0 deletions packages/selenium-devtools/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,8 @@ DevTools.configure({

Here's what every option does, in plain language. **All are optional** — the plugin runs fine with `DevTools.configure({})` or no configure call at all.

> `port` and `hostname` only take effect before the backend starts, which happens when the first driver is built, so call `DevTools.configure(...)` before that; a later call ignores them with a warning. A rerun keeps its parent's backend whatever they say.

> **For CI**, set both `headless: true` (hide the test browser) and `openUi: false` (don't try to open the dashboard window). The backend stays running on the configured port so you can still open the UI later.

#### `screencast` — record a video of the browser
Expand Down
118 changes: 118 additions & 0 deletions packages/selenium-devtools/src/helpers/configure-options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import type { DevToolsOptions, ServerLock } from '../types.js'

type OptionKind = 'boolean' | 'choice' | 'port' | 'host'

// A Record over every option, so a key added to DevToolsOptions fails to compile here instead of being silently ignored.
const OPTION_KIND = {
openUi: 'boolean',
headless: 'boolean',
captureScreenshots: 'boolean',
captureAssertions: 'boolean',
filmstrip: 'boolean',
emitArtifactsManifest: 'boolean',
mode: 'choice',
traceFormat: 'choice',
traceGranularity: 'choice',
tracePolicy: 'choice',
screenshot: 'choice',
video: 'choice',
port: 'port',
hostname: 'host'
} as const satisfies Record<
Exclude<keyof DevToolsOptions, 'screencast' | 'rerunCommand'>,
OptionKind
>

const EXPECTED: Record<OptionKind, string> = {
boolean: 'a boolean',
choice: 'a non-empty string',
port: 'an integer between 1 and 65535',
host: 'a non-empty string'
}

type ConfigurableKey = keyof typeof OPTION_KIND

export type ConfigurableOptions = Pick<DevToolsOptions, ConfigurableKey>

export function serverLock(state: {
isReuse: boolean
backendStarting: boolean
}): ServerLock | undefined {
if (state.isReuse) {
return 'reuse'
}
return state.backendStarting ? 'started' : undefined
}

const CONFIGURABLE_KEYS = Object.keys(OPTION_KIND) as ConfigurableKey[]

function isServerKey(key: ConfigurableKey): boolean {
const kind: OptionKind = OPTION_KIND[key]
return kind === 'port' || kind === 'host'
}

// JS callers bypass the types (e.g. `headless: process.env.HEADLESS`), and the string 'false' is truthy.
function hasExpectedShape(kind: OptionKind, value: unknown): boolean {
switch (kind) {
case 'boolean':
return typeof value === 'boolean'
case 'port':
return (
typeof value === 'number' &&
Number.isInteger(value) &&
value > 0 &&
value < 65536
)
case 'choice':
case 'host':
return typeof value === 'string' && value !== ''
}
}

function describeValue(value: unknown): string {
return typeof value === 'string' ? `'${value}'` : String(value)
}

function copyOption<K extends ConfigurableKey>(
target: ConfigurableOptions,
opts: DevToolsOptions,
key: K
): void {
target[key] = opts[key]
}

/** Returns one warning per refused option; a reused backend's port/hostname are skipped silently, as that is the rerun working. */
export function applyConfigureOptions(
target: ConfigurableOptions,
opts: DevToolsOptions,
lock?: ServerLock
): string[] {
const warnings: string[] = []
const accepted: ConfigurableKey[] = []
for (const key of CONFIGURABLE_KEYS) {
const value: unknown = opts[key]
if (value === undefined || value === null) {
continue
}
const kind: OptionKind = OPTION_KIND[key]
if (hasExpectedShape(kind, value)) {
accepted.push(key)
} else {
warnings.push(
`configure: ignoring ${key}: expected ${EXPECTED[kind]}, got ${describeValue(value)}`
)
}
}
const server = accepted.filter(isServerKey)
if (server.length > 0 && lock === 'started') {
warnings.push(
`configure: ignoring ${server.join('/')}: the backend start has already begun`
)
}
for (const key of accepted) {
if (!(lock && isServerKey(key))) {
copyOption(target, opts, key)
}
}
return warnings
}
75 changes: 12 additions & 63 deletions packages/selenium-devtools/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ import {
detectSeleniumVersion
} from './helpers/runtime.js'
import { findFreePort } from './helpers/utils.js'
import {
applyConfigureOptions,
serverLock
} from './helpers/configure-options.js'
import {
RetryTracker,
errorMessage,
Expand All @@ -60,15 +64,9 @@ import {
import {
type ActionSnapshot,
type CapturedCommand,
type DevToolsMode,
type DevToolsOptions,
type ScreencastFrame,
type ScreencastOptions,
type TraceFormat,
type TraceGranularity,
type TraceRetentionPolicy,
type TraceScreenshotPolicy,
type TraceVideoPolicy,
type SeleniumDriverLike,
type TestStats
} from './types.js'
Expand Down Expand Up @@ -299,54 +297,17 @@ class SeleniumDevToolsPlugin {
return this.#uiReadyPromise
}

// Declarative option-application mapper — one guarded assignment per option;
// splitting it purely to satisfy the line rule hurts readability.
// eslint-disable-next-line max-lines-per-function
configure(
opts: {
rerunCommand?: string
screencast?: ScreencastOptions
headless?: boolean
openUi?: boolean
captureAssertions?: boolean
mode?: DevToolsMode
traceFormat?: TraceFormat
traceGranularity?: TraceGranularity
tracePolicy?: TraceRetentionPolicy
screenshot?: TraceScreenshotPolicy
video?: TraceVideoPolicy
} = {}
) {
configure(opts: DevToolsOptions = {}) {
if ('rerunCommand' in opts) {
this.#rerunManager.configure(opts.rerunCommand)
this.#options.rerunCommand = opts.rerunCommand
}
if (typeof opts.headless === 'boolean') {
this.#options.headless = opts.headless
}
if (typeof opts.captureAssertions === 'boolean') {
this.#options.captureAssertions = opts.captureAssertions
}
if (typeof opts.openUi === 'boolean') {
this.#options.openUi = opts.openUi
}
if (opts.mode) {
this.#options.mode = opts.mode
}
if (opts.traceFormat) {
this.#options.traceFormat = opts.traceFormat
}
if (opts.traceGranularity) {
this.#options.traceGranularity = opts.traceGranularity
}
if (opts.tracePolicy) {
this.#options.tracePolicy = opts.tracePolicy
}
if (opts.screenshot) {
this.#options.screenshot = opts.screenshot
}
if (opts.video) {
this.#options.video = opts.video
const lock = serverLock({
isReuse: this.#isReuse,
backendStarting: this.#backendStartPromise !== undefined
})
for (const warning of applyConfigureOptions(this.#options, opts, lock)) {
log.warn(warning)
}
if (opts.screencast) {
if (this.#options.mode === 'trace' && opts.screencast.enabled) {
Expand Down Expand Up @@ -734,19 +695,7 @@ if (!registerHooks()) {
registerProcessHooks(plugin)

export const DevTools = {
configure: (opts: {
rerunCommand?: string
screencast?: ScreencastOptions
headless?: boolean
openUi?: boolean
captureAssertions?: boolean
mode?: DevToolsMode
traceFormat?: TraceFormat
traceGranularity?: TraceGranularity
tracePolicy?: TraceRetentionPolicy
screenshot?: TraceScreenshotPolicy
video?: TraceVideoPolicy
}) => plugin.configure(opts),
configure: (opts: DevToolsOptions) => plugin.configure(opts),
startTest: (name: string, meta?: { file?: string }) =>
plugin.startTest(name, meta),
endTest: (state: TestStats['state'] = 'passed') => plugin.endTest(state)
Expand Down
3 changes: 3 additions & 0 deletions packages/selenium-devtools/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ export {
type TraceVideoPolicy
} from '@wdio/devtools-shared'

/** Why `configure()` can no longer change port/hostname: a rerun reusing its parent's backend, or a backend start already under way. */
export type ServerLock = 'reuse' | 'started'

export interface DevToolsOptions extends BaseDevToolsOptions {
/** Open a Chrome window pointing at the UI. Default true. */
openUi?: boolean
Expand Down
Loading
Loading