CodeceptJS Page Exploration
Authoring a test, debugging a failure, and refactoring a stale locator share one task: open a page, find the right element, pick a stable locator. This is that playbook.
Tools
run_code — runs CodeceptJS code, returns produced values, captures console.*, saves a final-state snapshot. For do something and look at the result.
snapshot — captures state without acting (URL, cookies, localStorage, HTML, ARIA, screenshot, console). For "what's on the page right now".
Artifact sources, in preference order:
- ARIA snapshot — structured, no styling noise, easy duplicate/accessibility-name scanning
- Screenshot — visual confirmation; catches layout breaks ARIA can't show
- HTML — only when ARIA lacks context (custom widgets without accessible names, attribute-driven behaviour)
Inspect an element
I.grabWebElement(locator) → one WebElement; I.grabWebElements(locator) → array. Same cross-helper API on Playwright / Puppeteer / WebDriver.
| You want to … |
Method |
| Confirm rendered / visible / enabled |
exists(), isVisible(), isEnabled() |
| Read text / value / attribute / property |
getText(), getValue(), getAttribute(n), getProperty(n) |
| Position on page |
getBoundingBox() — flags offscreen / zero-sized |
| Rendered markup |
toOuterHTML(), toSimplifiedHTML(300) (truncated, MCP-friendly) |
| Stable selector for a fix |
toAbsoluteXPath() |
| Inside an iframe |
inIframe(async (body) => { ... }) |
| Drill into children |
$(loc), $$(loc) |
| Browser-side function |
evaluate(fn, ...args) |
Discover candidates when the obvious locator misses
When Edit matches nothing, the control may say "Change", carry aria-label="Edit user", or live in .btn-edit. Cast a wide net with a permissive XPath via I.grabWebElements, then disambiguate.
OR together in the XPath:
- visible text —
text() (or . for descendants)
- attributes —
@class, @aria-label, @title, @data-action, @id
- synonyms — edit/change/modify; delete/remove/trash; submit/send/save
Case-insensitive via translate(...):
//*[contains(translate(., 'EDIT', 'edit'), 'edit')
or contains(translate(@class, 'EDIT', 'edit'), 'edit')
or contains(translate(@aria-label, 'EDIT', 'edit'), 'edit')
or contains(translate(., 'CHANGE', 'change'), 'change')]
Then iterate toSimplifiedHTML(150) over the results, pick the right candidate, commit a stable locator from its discriminating attribute or text.
Pick a stable locator
Two decisions in order: which region scopes the lookup (context), what identifies the element inside it. Region first keeps the identifier short and semantic — the discriminator found during disambiguation belongs in the context argument:
I.click('Edit user', '.user-row') // ✅ region + what the user sees
I.click('#user-row-42 button.edit') // ❌ same element, brittle, unreadable
Stable regions: landmarks (nav, main, { role: 'dialog' }), app-shell containers (.sidebar, .toolbar, .modal), rows/cards identified by data via locate(...). Identifier priority (full rationale: codeceptjs-fundamentals § Locators):
- Visible label / accessible name — plain string already matches
aria-label; don't expand to { css: '[aria-label="..."]' }
- ARIA role when ambiguous within context or role is part of the check
$name via customLocator when team test attributes exist
- Composed CSS, still scoped:
I.click('button.edit', '#user-row-42')
toAbsoluteXPath() — last resort; flag the team to add a data-testid
Never commit an unverified locator — confirm via run_code (I.seeElement(loc, context) or grabWebElement(loc)) that it matches exactly one element.
Common patterns
- Strict mode 2+ matches →
grabWebElements('Save') + toSimplifiedHTML(200) each, find discriminator, pass as context: I.click('Save', '.modal')
- Button rendered but doesn't act →
grabWebElement('Submit') + isEnabled() + getBoundingBox() — disabled? offscreen? zero-sized?
- Wrong row in a list →
grabWebElements('.user-row'), getText() per row to identify, getAttribute('data-id') for stable hook
- Inside iframe →
(await I.grabWebElement('iframe.editor')).inIframe(async (body) => body.$('button'))
Things to avoid
- Choosing a locator without seeing candidates first.
- Committing
toAbsoluteXPath() when a semantic locator is available.
- Committing unscoped locators where a context keeps them short.
- Ignoring the screenshot — "exists in HTML" ≠ "user can see it".
usePlaywrightTo / useWebDriverTo when WebElement methods cover it.
Related skills
codeceptjs-fundamentals — locator priority, await rule
writing-codeceptjs-tests — invokes this during Mode B exploration
debugging-codeceptjs-tests — invokes this for live inspection; offline variant via codeceptq
1---2name: codeceptjs-exploration3description: Use when an agent needs to learn what's on a page in CodeceptJS — read the ARIA tree, inspect candidate elements, pick or disambiguate a stable locator. Drives the live browser via MCP `run_code` / `snapshot`. Invoked by writing-codeceptjs-tests, debugging-codeceptjs-tests, and refactoring-codeceptjs-tests whenever page inspection is needed.4---56# CodeceptJS Page Exploration78Authoring a test, debugging a failure, and refactoring a stale locator share one task: open a page, find the right element, pick a stable locator. This is that playbook.910## Tools1112- **`run_code`** — runs CodeceptJS code, returns produced values, captures `console.*`, saves a final-state snapshot. For *do something and look at the result*.13- **`snapshot`** — captures state without acting (URL, cookies, localStorage, HTML, ARIA, screenshot, console). For "what's on the page right now".1415Artifact sources, in preference order:16171. **ARIA snapshot** — structured, no styling noise, easy duplicate/accessibility-name scanning182. **Screenshot** — visual confirmation; catches layout breaks ARIA can't show193. **HTML** — only when ARIA lacks context (custom widgets without accessible names, attribute-driven behaviour)2021## Inspect an element2223`I.grabWebElement(locator)` → one WebElement; `I.grabWebElements(locator)` → array. Same cross-helper API on Playwright / Puppeteer / WebDriver.2425| You want to … | Method |26|---|---|27| Confirm rendered / visible / enabled | `exists()`, `isVisible()`, `isEnabled()` |28| Read text / value / attribute / property | `getText()`, `getValue()`, `getAttribute(n)`, `getProperty(n)` |29| Position on page | `getBoundingBox()` — flags offscreen / zero-sized |30| Rendered markup | `toOuterHTML()`, `toSimplifiedHTML(300)` (truncated, MCP-friendly) |31| Stable selector for a fix | `toAbsoluteXPath()` |32| Inside an iframe | `inIframe(async (body) => { ... })` |33| Drill into children | `$(loc)`, `$$(loc)` |34| Browser-side function | `evaluate(fn, ...args)` |3536## Discover candidates when the obvious locator misses3738When `Edit` matches nothing, the control may say "Change", carry `aria-label="Edit user"`, or live in `.btn-edit`. Cast a wide net with a permissive XPath via `I.grabWebElements`, then disambiguate.3940OR together in the XPath:4142- visible text — `text()` (or `.` for descendants)43- attributes — `@class`, `@aria-label`, `@title`, `@data-action`, `@id`44- **synonyms** — edit/change/modify; delete/remove/trash; submit/send/save4546Case-insensitive via `translate(...)`:4748```49//*[contains(translate(., 'EDIT', 'edit'), 'edit')50 or contains(translate(@class, 'EDIT', 'edit'), 'edit')51 or contains(translate(@aria-label, 'EDIT', 'edit'), 'edit')52 or contains(translate(., 'CHANGE', 'change'), 'change')]53```5455Then iterate `toSimplifiedHTML(150)` over the results, pick the right candidate, commit a stable locator from its discriminating attribute or text.5657## Pick a stable locator5859Two decisions in order: **which region scopes the lookup** (context), **what identifies the element inside it**. Region first keeps the identifier short and semantic — the discriminator found during disambiguation belongs in the context argument:6061```js62I.click('Edit user', '.user-row') // ✅ region + what the user sees63I.click('#user-row-42 button.edit') // ❌ same element, brittle, unreadable64```6566Stable regions: landmarks (`nav`, `main`, `{ role: 'dialog' }`), app-shell containers (`.sidebar`, `.toolbar`, `.modal`), rows/cards identified by data via `locate(...)`. Identifier priority (full rationale: `codeceptjs-fundamentals` § Locators):67681. Visible label / accessible name — plain string already matches `aria-label`; don't expand to `{ css: '[aria-label="..."]' }`692. ARIA role when ambiguous within context or role is part of the check703. `$name` via `customLocator` when team test attributes exist714. Composed CSS, still scoped: `I.click('button.edit', '#user-row-42')`725. `toAbsoluteXPath()` — last resort; flag the team to add a `data-testid`7374**Never commit an unverified locator** — confirm via `run_code` (`I.seeElement(loc, context)` or `grabWebElement(loc)`) that it matches exactly one element.7576## Common patterns7778- Strict mode 2+ matches → `grabWebElements('Save')` + `toSimplifiedHTML(200)` each, find discriminator, pass as context: `I.click('Save', '.modal')`79- Button rendered but doesn't act → `grabWebElement('Submit')` + `isEnabled()` + `getBoundingBox()` — disabled? offscreen? zero-sized?80- Wrong row in a list → `grabWebElements('.user-row')`, `getText()` per row to identify, `getAttribute('data-id')` for stable hook81- Inside iframe → `(await I.grabWebElement('iframe.editor')).inIframe(async (body) => body.$('button'))`8283## Things to avoid8485- Choosing a locator without seeing candidates first.86- Committing `toAbsoluteXPath()` when a semantic locator is available.87- Committing unscoped locators where a context keeps them short.88- Ignoring the screenshot — "exists in HTML" ≠ "user can see it".89- `usePlaywrightTo` / `useWebDriverTo` when WebElement methods cover it.9091## Related skills9293- `codeceptjs-fundamentals` — locator priority, await rule94- `writing-codeceptjs-tests` — invokes this during Mode B exploration95- `debugging-codeceptjs-tests` — invokes this for live inspection; offline variant via `codeceptq`