Cypress and Playwright hand you a framework. Selenium hands you a browser control library and expects you to build the rest. Every serious Selenium project therefore contains roughly the same hand-built structure, and knowing it is what "Selenium experience" means on a CV.
The shape.
tests/
smoke/
login.test.js
contacts/
create.test.js
delete.test.js
pages/
BasePage.js
LoginPage.js
ContactsPage.js
utils/
driver-factory.js one place that builds drivers
waits.js shared explicit-wait helpers
data.js unique test data builders
config.js
Four pieces do the work, and each replaces something the other tools give free.
The driver factory replaces the config file's browser settings. One function builds every driver, so options stay consistent.
Page objects replace scattered selectors. A class per screen, holding locators and the actions on that screen. The rule that keeps them useful: page objects contain locators and actions, never assertions. A page object that asserts is a page object you cannot reuse in a test wanting the opposite outcome.
class ContactsPage {
constructor(driver) { this.driver = driver; }
async open() { await this.driver.get(`${config.baseUrl}/contacts`); }
async delete(name) {
const row = await waitForElement(this.driver, By.xpath(`//tr[contains(., '${name}')]`));
await row.findElement(By.css('[data-testid="delete"]')).click();
}
rowFor(name) { return By.xpath(`//tr[contains(., '${name}')]`); }
}
Shared wait helpers replace auto-waiting. Since you write every wait, write them once:
async function waitForElement(driver, locator, timeout = config.timeout) {
const el = await driver.wait(until.elementLocated(locator), timeout);
return driver.wait(until.elementIsVisible(el), timeout);
}
Note it waits for visible, not merely located. That single helper prevents a large share of Selenium flakiness, because it encodes the correct wait once instead of hoping ninety call sites got it right.
Data builders replace nothing, they are just necessary:
const uniqueContact = () => ({ name: `Dana ${Date.now()}`, company: 'FoxLabs' });
What goes in the suite. Not everything. Protect the journeys that cost money: sign in, the main create-and-view flow, checkout, permissions. Selenium is the slowest of the three tools, so the discipline about what earns a browser test matters more here than anywhere.
When it gets slow, in order:
- Reuse the browser per file rather than per test, with cookies cleared between tests.
- Set up through the API where the UI is not the point. Selenium has no HTTP client, so use
axiosorfetchalongside it. - Run in parallel, through your runner's workers or through Grid. Only possible if tests are independent.
- Delete tests. A test that has never failed in two years on a screen nobody uses costs you time on every run.
Agree the flaky rule early. A test that fails intermittently gets fixed within a set time or deleted. The middle option, where it stays red-ish for months, is what teaches a team to ignore red and kills the suite's value entirely.
Remember this: Selenium gives you parts. The factory, the page objects, the wait helpers and the data builders are the framework, and you own all four.
Reference: Page object models.