UI tests for the Sauce Labs demo shop written with Selenium WebDriver 4 and TypeScript, in the page-object style, run by Mocha and asserted with Chai.
Written as a realistic input for a Selenium → Playwright conversion demo. The tests are real and they pass against the live site; the suite exists so the conversion has something honest to work on.
src/pages/BasePage.ts waiting, clicking, typing, reading — inherited by every page
src/pages/LoginPage.ts the sign-in form
src/pages/InventoryPage.ts the product list, the sort control, the cart badge
src/support/driver.ts one Chrome per test, headless unless HEADLESS=false
src/data/users.ts the accounts the demo site publishes on its own login page
tests/login.spec.ts 5 tests
tests/inventory.spec.ts 7 tests
npm install
npm test # headless, writes reports/selenium-report.html
npm run test:headed # watch it drive a real Chrome
npm run typecheckSelenium Manager fetches a matching chromedriver on first run, so there is nothing else to install.
Login
✓ shows the Swag Labs branding on the sign-in form
✓ signs a standard user in and lands on the products page
✓ refuses an unknown username with an explanatory message
✓ refuses a locked-out account
✓ refuses an empty form
Inventory
✓ lists all six products
✓ starts with an empty cart
✓ adds a product to the cart and counts it
✓ adds three products and counts all of them
✓ removes a product again and empties the badge
✓ sorts products by price, low to high
✓ sorts products by name, Z to A
12 passing (16s)
Sauce Labs' demo shop is a React app, and a Selenium suite written against it
naively is flaky. BasePage answers each of those in one place:
It waits for the destination, not the address bar. The URL changes before
the app paints — measured here, #checkout and .cart_item are both absent at
the instant /cart.html appears. So every navigation waits for the URL and
for an element that exists only on the page being navigated to.
It re-reads elements that went stale. The product list re-renders after a
navigation, so a handle taken a moment earlier is no longer attached. Every read
retries once on StaleElementReferenceError.
It types, then checks the text arrived. sendKeys on an input the app has
not finished wiring up is accepted and discarded, and shows up much later as a
validation error on a form that looks filled in. And clear() is only called on
a field that actually has a value, because emptying a React-controlled input
without firing its events leaves the app's state disagreeing with the DOM.
Each add-to-cart button carries its own id — add-to-cart-sauce-labs-backpack —
so InventoryPage builds that id from the product name rather than searching
for a button inside a product card. A card contains three elements with the
button role (the image link, the title link, and the real button), so "the
button in this card" is ambiguous, and a locator that matches three elements is
a bug waiting for a strict-mode runner to find it.
MIT.