Skip to content

Commit 111a443

Browse files
DavertMikclaude
andcommitted
feat(CDPBrowser): detect render/no-render engines, add CDP timing metrics
- Detect layout support with a stylesheet probe: Obscura 0.2.x no-render builds ignore stylesheet rules and were misdetected as layout+screenshot capable. Print the detected engine once per run. - Time every CDP command; DEBUG=codeceptjs:cdp prints per-call durations and a per-method summary when the connection closes. - Reuse the page text from a failed see() for the error message instead of walking the whole page a second time. - Skip role-locator visibility tests on engines without layout. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent ab1d8e8 commit 111a443

4 files changed

Lines changed: 55 additions & 7 deletions

File tree

‎lib/helper/CDPBrowser.js‎

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import CDPConnection from './extras/CDPConnection.js'
88
import installCodeceptClient from './clientscripts/cdpBrowserClient.js'
99
import xpathPolyfillSource from './clientscripts/xpathPolyfill.js'
1010
import Locator from '../locator.js'
11+
import output from '../output.js'
1112
import store from '../store.js'
1213
import { xpathLocator, normalizePath, resolveUrl, toCamelCase, convertCssPropertiesToCamelCase, normalizeSpacesInString, fileExists, base64EncodeFile, getMimeType, isFile, isWindows } from '../utils.js'
1314
import ElementNotFound from './errors/ElementNotFound.js'
@@ -595,8 +596,19 @@ class CDPBrowser extends Helper {
595596

596597
async _probeCapabilities() {
597598
if (this.capabilities.layout === null) {
598-
const display = await this._evaluate(`getComputedStyle(document.documentElement).display`)
599-
this.capabilities.layout = display === '' ? 'none' : 'real'
599+
const display = await this._evaluate(`(function(){
600+
var style = document.createElement('style')
601+
style.textContent = '.__codecept-layout-probe{display:none}'
602+
document.documentElement.appendChild(style)
603+
var probe = document.createElement('div')
604+
probe.className = '__codecept-layout-probe'
605+
;(document.body || document.documentElement).appendChild(probe)
606+
var display = getComputedStyle(probe).display
607+
style.remove()
608+
probe.remove()
609+
return display
610+
})()`)
611+
this.capabilities.layout = display === 'none' ? 'real' : 'none'
600612
}
601613
if (this.capabilities.screenshot === null) {
602614
this.capabilities.screenshot = this.capabilities.layout === 'real'
@@ -610,6 +622,13 @@ class CDPBrowser extends Helper {
610622
if (this.capabilities.innerText === null) {
611623
await this._needsVisibleTextFallback()
612624
}
625+
if (!this._engineReported) {
626+
this._engineReported = true
627+
let engine = 'render (layout and screenshots)'
628+
if (this.capabilities.layout === 'none') engine = 'no-render (DOM only: no layout, visibility checks or screenshots)'
629+
else if (!this.capabilities.screenshot) engine = 'layout without screenshots'
630+
output.print(`${this.constructor.name} engine: ${engine}`)
631+
}
613632
}
614633

615634
async _run(candidates, action, payload, context = null) {
@@ -807,7 +826,8 @@ class CDPBrowser extends Helper {
807826
const useWalker = await this._needsVisibleTextFallback()
808827
const res = await this._runTextCheck(text, { ignoreCase, useWalker })
809828
if (negate ? !res.found : res.found) return
810-
let source = await this._textSource(null)
829+
let source = res.text
830+
if (source === undefined) source = await this._textSource(null)
811831
let needle = text
812832
if (ignoreCase) {
813833
needle = needle.toLowerCase()

‎lib/helper/clientscripts/cdpBrowserClient.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export default function installCodeceptClient(xpathNeedsPolyfill) {
7676
const hayCmp = opts.ignoreCase ? hay.toLowerCase() : hay
7777
const needleCmp = opts.ignoreCase ? needleNorm.toLowerCase() : needleNorm
7878
const idx = hayCmp.indexOf(needleCmp)
79-
if (idx === -1) return { found: false, snippet: null }
79+
if (idx === -1) return { found: false, snippet: null, text: hay }
8080
const from = Math.max(0, idx - 60)
8181
return { found: true, snippet: hay.slice(from, idx + needleNorm.length + 60) }
8282
}

‎lib/helper/extras/CDPConnection.js‎

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import { WebSocket } from 'ws'
2+
import debugModule from 'debug'
3+
4+
const debug = debugModule('codeceptjs:cdp')
25

36
class CDPConnection {
47
constructor(endpoint, options = {}) {
@@ -9,6 +12,7 @@ class CDPConnection {
912
this.lastId = 0
1013
this.pending = new Map()
1114
this.listeners = new Map()
15+
this.stats = new Map()
1216
}
1317

1418
async connect() {
@@ -44,14 +48,35 @@ class CDPConnection {
4448
const id = ++this.lastId
4549
const message = { id, method, params }
4650
if (sessionId) message.sessionId = sessionId
47-
return new Promise((resolve, reject) => {
51+
const started = Date.now()
52+
const promise = new Promise((resolve, reject) => {
4853
const timer = setTimeout(() => {
4954
this.pending.delete(id)
5055
reject(new Error(`CDP command ${method} timed out after ${this.timeout}ms`))
5156
}, this.timeout)
5257
this.pending.set(id, { resolve, reject, timer })
5358
this.ws.send(JSON.stringify(message))
5459
})
60+
const record = () => {
61+
const ms = Date.now() - started
62+
const stat = this.stats.get(method) || { count: 0, total: 0, max: 0 }
63+
stat.count++
64+
stat.total += ms
65+
if (ms > stat.max) stat.max = ms
66+
this.stats.set(method, stat)
67+
let detail = ''
68+
if (params.expression) detail = params.expression.slice(0, 80).replace(/\s+/g, ' ')
69+
debug(`${method} ${ms}ms`, detail)
70+
}
71+
promise.then(record, record)
72+
return promise
73+
}
74+
75+
printStats() {
76+
const rows = [...this.stats.entries()].sort((a, b) => b[1].total - a[1].total)
77+
for (const [method, { count, total, max }] of rows) {
78+
debug(`${method}: ${count} calls, total ${total}ms, avg ${Math.round(total / count)}ms, max ${max}ms`)
79+
}
5580
}
5681

5782
on(method, fn) {
@@ -80,6 +105,7 @@ class CDPConnection {
80105

81106
async close() {
82107
if (!this.ws) return
108+
this.printStats()
83109
await new Promise(resolve => {
84110
if (this.ws.readyState === WebSocket.CLOSED) return resolve()
85111
this.ws.once('close', resolve)

‎test/helper/webapi.js‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2618,7 +2618,8 @@ export function tests() {
26182618
})
26192619

26202620
describe('role locators', () => {
2621-
it('should locate elements by role', async () => {
2621+
it('should locate elements by role', async function () {
2622+
if (I.capabilities?.layout === 'none') this.skip() // visibility requires a real layout engine
26222623
await I.amOnPage('/form/role_elements')
26232624

26242625
// Test basic role locators
@@ -2634,7 +2635,8 @@ export function tests() {
26342635
await I.seeNumberOfVisibleElements({ role: 'checkbox' }, 2)
26352636
})
26362637

2637-
it('should locate elements by role with text filter', async () => {
2638+
it('should locate elements by role with text filter', async function () {
2639+
if (I.capabilities?.layout === 'none') this.skip() // visibility requires a real layout engine
26382640
await I.amOnPage('/form/role_elements')
26392641

26402642
// Test role with text (exact match)

0 commit comments

Comments
 (0)