Skip to content

Commit c5825fc

Browse files
DavertMikclaude
andcommitted
perf(codeceptq): pre-resolve //*[@id]/@id subqueries
Locator.clickable.wide and field.labelContains emit predicates of form [@aria-labelledby = //*[@id][normalize-space(string(.)) = 'X']/@id ]. xpath@0.0.34 re-runs the inner //* scan once per outer element match — O(N²) on non-trivial docs. The 2k-line github fixture spent 8.5s in that single branch out of 12. Pre-resolve the inner subquery once, splice the resulting id (or a sentinel for no-match) back as a literal so the engine sees a flat attribute compare. Github 'Sign up' --click: 9026ms → 276ms (~33×). Full runner suite: 14s → 6s. Reverts the 30s describe-level timeout from the previous commit since the underlying perf issue is now fixed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent db7c7e8 commit c5825fc

2 files changed

Lines changed: 20 additions & 5 deletions

File tree

lib/command/query.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ export default async function query(locator, context, options = {}) {
2626
}
2727

2828
const { doc, source } = htmlToDoc(html)
29+
xpathExpr = preresolveIdLookups(xpathExpr, doc)
30+
if (contextExpr) contextExpr = preresolveIdLookups(contextExpr, doc)
2931

3032
let nodes
3133
try {
@@ -118,6 +120,23 @@ function buildXPath(input, options) {
118120
return loc.toXPath()
119121
}
120122

123+
// `Locator.clickable.wide` and `Locator.field.labelContains` emit
124+
// `[@aria-labelledby = //*[@id][normalize-space(string(.)) = '...']/@id]`.
125+
// xpath@0.0.34 re-runs the inner //* scan once per outer match — O(N²) on
126+
// any non-trivial document. Pre-resolve the inner subquery to a literal so
127+
// the engine sees a flat attribute compare. Dropped a 2k-line github fixture
128+
// from 8500ms to ~50ms.
129+
function preresolveIdLookups(expr, doc) {
130+
return expr.replace(
131+
/\/\/\*\[@id\]\[normalize-space\(string\(\.\)\)\s*=\s*('[^']*'|"[^"]*")\]\/@id/g,
132+
sub => {
133+
const ids = toArray(xpath.select(sub, doc)).map(a => a.value || '')
134+
if (ids.length === 0) return "'__codeceptq_no_match__'"
135+
return `'${ids[0].replace(/'/g, '&apos;')}'`
136+
}
137+
)
138+
}
139+
121140
function htmlToDoc(html) {
122141
const p5doc = parse5.parse(html, { sourceCodeLocationInfo: true })
123142
const impl = new DOMImplementation()

test/runner/codeceptq_test.js

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,7 @@ const runWithStdin = (args, html) => {
4545
// For long snippets, assert first 50 chars only.
4646
const head = matches => matches.map(m => ({ line: m.line, snippet: m.snippet.slice(0, 50) }))
4747

48-
describe('codeceptq', function () {
49-
// Each test spawns a node process; the github.html semantic-click case
50-
// chews ~8s locally, so give the suite headroom over the default 10s.
51-
this.timeout(30000)
52-
48+
describe('codeceptq', () => {
5349
describe('XPath locators', () => {
5450
it('.//input — finds every input on checkout.html', async () => {
5551
const { parsed, code } = await runJson(`'.//input' --file ${checkoutHtml}`)

0 commit comments

Comments
 (0)