skip to content

How would you move a large Selenium 4 suite off getAttribute onto getDomAttribute and getDomProperty?

level: principalimportance: must knowfreq 52%

answer

  1. It looks like a rename and is not
  2. Nothing throws when you pick wrong
  3. Group by the name, not the file
  4. Boolean flags become predicates
  5. The empty form hides the mistake

basics

~20 s

Treat it as a semantic audit, not a rename. Group call sites by the name being read, move markup-only names first, turn boolean flags into isSelected and isEnabled, change live-value reads last, then ban the old call.

solid answer

~40 s

The risk is that it looks like a rename and is not. In Selenium 4 `getAttribute` returns the property first and the attribute as a fallback, so `getAttribute("value")` maps to `getDomProperty("value")`, while the similarly named `getDomAttribute("value")` silently switches the read to the markup default. Nothing throws when you choose wrong; on an empty form both reads return the same string, so the change looks green. I would group call sites by the name being read rather than by file, move the pure-markup names such as `data-*` first because they are true drop-ins, convert boolean flags to `isSelected()` and `isEnabled()` rather than to string reads, and change live-value names last with the page's markup open. Once clean, ban `getAttribute` in new code so the ambiguity cannot return.

go deeper

for a junior

Recall that Selenium 4 offers two explicit reads where older code used one, and that changing a call to the wrong one of the pair changes what the test reads rather than causing an error.

for a middle

Explain what each of the three calls returns for a typed field and a ticked box, so you can say for any one call site which replacement preserves its current meaning.

for a senior

Show how you would sequence and verify the change: convert one name at a time, exercise a filled form as well as a freshly loaded one, and treat a batch that changes nothing as suspicious rather than as success.

for a principal

Own the tradeoff. Decide how much churn is worth buying, which names get which read as a standing convention, how the constraint is enforced after the migration, and whether the change lands in batches a reviewer can actually check.

## The change that is not a rename Selenium 4 split the old attribute getter in two: `getDomAttribute(String)` reads the markup attribute, `getDomProperty(String)` reads the live DOM property, and each maps onto its own W3C WebDriver command. `getAttribute(String)` remains — it is not deprecated in Selenium 4 — and is implemented by shipping the bundled `getAttribute.js` atom to the execute-script endpoint, where it returns the property when one exists and the attribute otherwise. The migration is dangerous precisely because it is not a rename. A wrong choice still returns a `String`, so nothing throws. On a courier route planner's freshly loaded stop form, the markup default and the live value are the same text, so a read converted the wrong way passes on an empty form and lies on a filled one. ## What actually differs, call site by call site | what the call reads today | `getAttribute` returns | faithful Selenium 4 replacement | trap | |---|---|---|---| | `value` on a typed-in field | the live typed text | `getDomProperty("value")` | `getDomAttribute` gives the markup default | | `checked` on a ticked box | `"true"` or `null`, live | `isSelected()` | `getDomAttribute` reads the markup only | | `disabled` on a button | `"true"` or `null`, live | `isEnabled()`, inverted | `getDomProperty` gives `"true"`/`"false"`, never `null` | | `class` on a styled row | the `className` property | `getDomAttribute("class")` | `getDomProperty("class")` is `null` | | `data-stop-id` | the attribute | `getDomAttribute("data-stop-id")` | none; a true drop-in | | `href` on an anchor | the resolved absolute URL | `getDomProperty("href")` | `getDomAttribute` gives the raw markup path | ## A staged migration 1. **Inventory, then group by name, not by file.** The unit of decision is the name being read (`value`, `checked`, `class`, `data-*`), because every call site reading the same name usually wants the same store. 2. **Convert the pure-markup names first.** `data-*` attributes, `id`, authored `href`, `aria-*` written by the template: these move to `getDomAttribute` with no change in meaning, and they are usually the bulk of the count. 3. **Convert the boolean flags next, to predicates rather than to reads.** `checked` becomes `isSelected()`, `disabled` becomes `isEnabled()`. This is where the silent bugs live, and swapping to a `boolean` removes the string comparison entirely. 4. **Leave the live-state names for last**, and change them with the page's markup open. `value` on the route planner's capacity field is the archetype: only the markup tells you whether the old read was ever seeing the default. 5. **Close the door.** Once the suite is clean, ban `getAttribute` in new code — a lint rule or a review convention — so the ambiguity cannot creep back. ## The judgment calls the plan cannot make for you - **Which store the old call was actually hitting.** For `value` the atom returns the property, so the drop-in is `getDomProperty`, not the similarly-named `getDomAttribute`. Reading the pair by name alone is how teams invert the meaning of half their reads. - **Whether the mis-capitalised names are load-bearing.** `getAttribute("class")` and `getAttribute("readonly")` are silently rewritten to `className` and `readOnly`. Those call sites need `getDomAttribute`, not the property. - **How much churn is worth buying.** A read whose two stores can never disagree — an `id`, a `data-` attribute the app never rewrites — is safe either way, so the migration's value there is only clarity for the next reader. - **Whether to migrate at all in one move.** A big-bang rewrite of hundreds of reads produces a diff nobody can review; per-name batches produce diffs a reviewer can actually check against the markup. ## Verifying without trusting the green run - Convert one name at a time and read the failures. A batch that changes nothing anywhere is suspicious: it usually means those reads only ever ran against pages where the two stores agreed. - Exercise a filled state, not only a freshly loaded one. On the route planner that means a form a step has typed into and a stop a step has ticked, because that is the only state in which the two reads differ. - Prefer the predicates where they exist. `isSelected()` and `isEnabled()` return a `boolean` from their own commands, and a `boolean` cannot be compared against the wrong string. ## What you gain Two named calls, each with one meaning, one W3C command apiece, and no bundled script in the request body. The cost is a one-off audit; the benefit is that every future reader of a page object can tell, without opening the page's HTML, which store the test meant to read.

  • Which single name causes the most damage in this migration, and why?
    `value` on an editable field. The old call returns the live property, so the faithful replacement is `getDomProperty("value")`; picking `getDomAttribute("value")` because the names look alike switches the read to the markup default. It fails silently, because on a form nobody has typed into yet the default and the live value are the same string, so the converted read only lies on the states a test actually exercises.
  • How do you keep the old call from creeping back after the migration?
    Make the constraint mechanical rather than cultural: a lint or static-analysis rule that fails the build on `getAttribute` in the test sources, plus a short note in the page-object convention saying which read each kind of name gets. A review convention alone decays as the team changes; a rule that fails the build states the decision once and enforces it on every branch.
  • Is there any reason to keep getAttribute at some call sites?
    Only where its compatibility layer is doing real work and the team knowingly wants it: it rewrites `class` to the `className` property, `readonly` to `readOnly`, flattens `style` into the declaration's text, and resolves an anchor's `href` to an absolute URL. Each of those has an explicit equivalent, so the honest choice is to convert deliberately and record why, rather than leave the ambiguous call in place.

saying these in an interview costs you the question

  • Calls it a find-and-replace across the test sources
  • Maps getAttribute on value to getDomAttribute because the names match
  • Trusts a green suite as proof the conversion was faithful
  • Converts boolean flags to string reads instead of predicates
  • Claims the old call is deprecated and must be removed at once