A fifteen-year-old internal application. Table-based layout, nested frames, IDs like ctl00$ContentPlaceHolder1$GridView1$ctl03$btnDelete, no test attributes, and a development team of two who are busy.
This is where Selenium lives. It is chosen for exactly these applications, so handling them is a core senior skill for this tool rather than an edge case.
Generated IDs that change between builds.
ASP.NET WebForms, older JSF and similar frameworks generate IDs containing the control hierarchy. Add a panel and every ID beneath it changes.
Do not match the whole ID. Match the stable part:
By.css('[id$="btnDelete"]') // ends with
By.css('[id*="GridView1"]') // contains
By.xpath("//input[contains(@id, 'btnDelete')]")
This is one of the few situations where XPath earns its place, because it can express relationships CSS cannot: "the delete button in the row containing this text".
By.xpath("//tr[td[contains(text(), 'Dana Fox')]]//input[contains(@id, 'btnDelete')]")
Verbose, and it targets a relationship rather than a position, which is what makes it survive.
Table-based layouts with no semantic markup.
Old applications use tables for layout, so <table> tells you nothing about meaning and there are no roles to target.
Anchor on text the user sees, then navigate relative to it. Text is the most stable thing such an application has, because changing it requires a deliberate decision by someone.
// the input in the cell next to the label "Customer name"
By.xpath("//td[normalize-space()='Customer name']/following-sibling::td//input")
normalize-space() handles the stray whitespace these applications are full of, and forgetting it causes matches to fail for no visible reason.
Nested frames.
Legacy applications often nest frames several deep. The rules from the beginner track still apply, with two additions at scale.
Write a helper that switches by path, because doing it by hand everywhere is where mistakes happen:
async function switchToFrames(driver, ...locators) {
await driver.switchTo().defaultContent();
for (const locator of locators) {
const frame = await waits.forVisible(driver, locator, `Frame not found: ${locator}`);
await driver.switchTo().frame(frame);
}
}
Always start from defaultContent(). Relative switching from an unknown current frame is the cause of most frame bugs, because a helper cannot know where the previous test step left the context.
Popup windows for everything. Old applications open windows constantly. Wrap the handle dance in a helper with a wait, as in the beginner track, and always switch back explicitly.
Now the part that matters more than any technique: improving testability.
You will be told the application cannot be changed. Often that is not quite true, and the negotiation is winnable with a narrow ask.
Ask for the smallest thing that helps most. Not "add test IDs everywhere". Ask for stable IDs on the specific elements in the critical flows, perhaps thirty elements. That is a small, boring change a developer can do in an afternoon, and it removes most of your fragility.
Quantify the cost. "Each of these selectors breaks roughly twice a year and costs half a day to repair. Thirty test attributes removes about a week of work per year." Time is the unit that gets budget.
Do the work yourself where you can. Submit the pull request adding the attributes. Reviewing a working change is far easier than acting on a request, and it removes the effort objection entirely.
Attach it to work already happening. When a screen is being modified for a feature, that is the cheapest moment to add attributes. A standing agreement that touched screens get test IDs costs almost nothing and compounds.
Accept a lower ceiling, and say so early. Some legacy applications will never support fast, fully reliable automation. Twenty solid tests on the critical paths is a real achievement there. Promising eighty percent coverage on a fifteen-year-old WebForms application is how automation projects get cancelled after six months, and a senior engineer sets that expectation at the start rather than discovering it later.
Remember this: anchor on visible text, use XPath for relationships, centralise frame switching, and negotiate a small testability change rather than a large one.
Reference: Locator strategies.