diff --git a/lib/helper/CDPBrowser.js b/lib/helper/CDPBrowser.js index 9edf74eb7..48d229a1f 100644 --- a/lib/helper/CDPBrowser.js +++ b/lib/helper/CDPBrowser.js @@ -7,7 +7,9 @@ import Helper from '@codeceptjs/helper' import CDPConnection from './extras/CDPConnection.js' import installCodeceptClient from './clientscripts/cdpBrowserClient.js' import xpathPolyfillSource from './clientscripts/xpathPolyfill.js' +import layoutProbe from './clientscripts/layoutProbe.js' import Locator from '../locator.js' +import output from '../output.js' import store from '../store.js' import { xpathLocator, normalizePath, resolveUrl, toCamelCase, convertCssPropertiesToCamelCase, normalizeSpacesInString, fileExists, base64EncodeFile, getMimeType, isFile, isWindows } from '../utils.js' import ElementNotFound from './errors/ElementNotFound.js' @@ -595,8 +597,8 @@ class CDPBrowser extends Helper { async _probeCapabilities() { if (this.capabilities.layout === null) { - const display = await this._evaluate(`getComputedStyle(document.documentElement).display`) - this.capabilities.layout = display === '' ? 'none' : 'real' + const display = await this._evaluate(`(${layoutProbe.toString()})()`) + this.capabilities.layout = display === 'none' ? 'real' : 'none' } if (this.capabilities.screenshot === null) { this.capabilities.screenshot = this.capabilities.layout === 'real' @@ -610,6 +612,13 @@ class CDPBrowser extends Helper { if (this.capabilities.innerText === null) { await this._needsVisibleTextFallback() } + if (!this._engineReported) { + this._engineReported = true + let engine = 'render (layout and screenshots)' + if (this.capabilities.layout === 'none') engine = 'no-render (DOM only: no layout, visibility checks or screenshots)' + else if (!this.capabilities.screenshot) engine = 'layout without screenshots' + output.print(`${this.constructor.name} engine: ${engine}`) + } } async _run(candidates, action, payload, context = null) { @@ -807,7 +816,8 @@ class CDPBrowser extends Helper { const useWalker = await this._needsVisibleTextFallback() const res = await this._runTextCheck(text, { ignoreCase, useWalker }) if (negate ? !res.found : res.found) return - let source = await this._textSource(null) + let source = res.text + if (source === undefined) source = await this._textSource(null) let needle = text if (ignoreCase) { needle = needle.toLowerCase() diff --git a/lib/helper/clientscripts/cdpBrowserClient.js b/lib/helper/clientscripts/cdpBrowserClient.js index 80a31a268..63e086c0d 100644 --- a/lib/helper/clientscripts/cdpBrowserClient.js +++ b/lib/helper/clientscripts/cdpBrowserClient.js @@ -76,7 +76,7 @@ export default function installCodeceptClient(xpathNeedsPolyfill) { const hayCmp = opts.ignoreCase ? hay.toLowerCase() : hay const needleCmp = opts.ignoreCase ? needleNorm.toLowerCase() : needleNorm const idx = hayCmp.indexOf(needleCmp) - if (idx === -1) return { found: false, snippet: null } + if (idx === -1) return { found: false, snippet: null, text: hay } const from = Math.max(0, idx - 60) return { found: true, snippet: hay.slice(from, idx + needleNorm.length + 60) } } diff --git a/lib/helper/clientscripts/layoutProbe.js b/lib/helper/clientscripts/layoutProbe.js new file mode 100644 index 000000000..5b93633c3 --- /dev/null +++ b/lib/helper/clientscripts/layoutProbe.js @@ -0,0 +1,12 @@ +export default function layoutProbe() { + const style = document.createElement('style') + style.textContent = '.__codecept-layout-probe{display:none}' + document.documentElement.appendChild(style) + const probe = document.createElement('div') + probe.className = '__codecept-layout-probe' + ;(document.body || document.documentElement).appendChild(probe) + const display = getComputedStyle(probe).display + style.remove() + probe.remove() + return display +} diff --git a/lib/helper/extras/CDPConnection.js b/lib/helper/extras/CDPConnection.js index 0254afb53..9b0346ae1 100644 --- a/lib/helper/extras/CDPConnection.js +++ b/lib/helper/extras/CDPConnection.js @@ -1,4 +1,7 @@ import { WebSocket } from 'ws' +import debugModule from 'debug' + +const debug = debugModule('codeceptjs:cdp') class CDPConnection { constructor(endpoint, options = {}) { @@ -9,6 +12,7 @@ class CDPConnection { this.lastId = 0 this.pending = new Map() this.listeners = new Map() + this.totals = {} } async connect() { @@ -49,7 +53,7 @@ class CDPConnection { this.pending.delete(id) reject(new Error(`CDP command ${method} timed out after ${this.timeout}ms`)) }, this.timeout) - this.pending.set(id, { resolve, reject, timer }) + this.pending.set(id, { resolve, reject, timer, method, started: Date.now() }) this.ws.send(JSON.stringify(message)) }) } @@ -61,9 +65,12 @@ class CDPConnection { _onMessage(msg) { if (msg.id && this.pending.has(msg.id)) { - const { resolve, reject, timer } = this.pending.get(msg.id) + const { resolve, reject, timer, method, started } = this.pending.get(msg.id) clearTimeout(timer) this.pending.delete(msg.id) + const ms = Date.now() - started + this.totals[method] = (this.totals[method] || 0) + ms + debug(`${method} ${ms}ms`) if (msg.error) reject(new Error(msg.error.message)) else resolve(msg.result) return @@ -80,6 +87,7 @@ class CDPConnection { async close() { if (!this.ws) return + debug('total time per method', this.totals) await new Promise(resolve => { if (this.ws.readyState === WebSocket.CLOSED) return resolve() this.ws.once('close', resolve) diff --git a/test/helper/webapi.js b/test/helper/webapi.js index a28cf8a82..226701ae7 100644 --- a/test/helper/webapi.js +++ b/test/helper/webapi.js @@ -2618,7 +2618,8 @@ export function tests() { }) describe('role locators', () => { - it('should locate elements by role', async () => { + it('should locate elements by role', async function () { + if (I.capabilities?.layout === 'none') this.skip() // visibility requires a real layout engine await I.amOnPage('/form/role_elements') // Test basic role locators @@ -2634,7 +2635,8 @@ export function tests() { await I.seeNumberOfVisibleElements({ role: 'checkbox' }, 2) }) - it('should locate elements by role with text filter', async () => { + it('should locate elements by role with text filter', async function () { + if (I.capabilities?.layout === 'none') this.skip() // visibility requires a real layout engine await I.amOnPage('/form/role_elements') // Test role with text (exact match)