Level 1 · Junior Automation · Practice 03

Same Test, Different Tool

Rewrite the Playwright search test using Selenium and Python. See the syntax differences, feel the waiting-model differences, and decide for yourself which you prefer.

1 Goal

By the end of this exercise you will have a Python project with Selenium installed, a test script that opens DuckDuckGo, fills the search box, submits the form, and asserts the URL plus a visible result link — the same scenario as Practice 02, rewritten in a different tool. You will understand why Selenium needs explicit waits, how webdriver-manager eliminates the driver-download headache, and how the assertion model differs from Playwright's web-first expectations.

Why learn both? Selenium is still the dominant tool in enterprise Java shops and has been around for nearly two decades. Playwright is newer and nicer but you will absolutely see Selenium in paid work. Knowing one makes you fluent; knowing both makes you hireable.

2 Install the tool (Python + Selenium)

  1. Install Python 3.12 or newer Download from python.org/downloads.

    Run the installer. Critical: tick "Add python.exe to PATH" on the first screen before clicking Install — otherwise python will not work from PowerShell.

    Run the .pkg installer, or install via Homebrew:

    brew install python@3.12

    Most distros have 3.10+ pre-installed. On Debian/Ubuntu if you need the newer version:

    sudo apt update
    sudo apt install -y python3.12 python3.12-venv
  2. Verify the install Open a fresh terminal and run:
    python --version
    pip --version
    python3 --version
    pip3 --version

    Expect Python 3.12.x or newer. On macOS/Linux you will use python3 and pip3 throughout this exercise — substitute in the commands below.

  3. Create a project folder and virtual environment A venv keeps this project's dependencies isolated from the system Python.
    cd $HOME\Desktop
    mkdir selenium-practice-03
    cd selenium-practice-03
    python -m venv .venv
    .\.venv\Scripts\Activate.ps1

    If PowerShell refuses with an execution-policy error, run Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned once, then try the activate command again.

    cd ~/Desktop
    mkdir selenium-practice-03
    cd selenium-practice-03
    python3 -m venv .venv
    source .venv/bin/activate

    Your prompt should now show (.venv) at the start — that confirms the venv is active.

  4. Install Selenium and webdriver-manager
    pip install selenium webdriver-manager

    selenium is the automation library itself. webdriver-manager downloads the matching ChromeDriver binary at runtime so you don't have to match versions manually — historically the single most painful part of using Selenium.

  5. Confirm versions
    pip show selenium webdriver-manager

    You should see selenium 4.x and webdriver-manager 4.x. Selenium 4 uses the W3C WebDriver protocol and has the modern locator API — anything older is not worth learning.

3 Project setup

Create one script file at the project root:

New-Item test_search.py
touch test_search.py

Final layout:

selenium-practice-03/
├── .venv/
└── test_search.py

4 Write the script

Paste this exact content into test_search.py:

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from webdriver_manager.chrome import ChromeDriverManager


def test_duckduckgo_returns_results_for_an_nz_query():
    service = Service(ChromeDriverManager().install())
    driver = webdriver.Chrome(service=service)
    try:
        driver.get("https://duckduckgo.com/")

        search_box = WebDriverWait(driver, 10).until(
            EC.element_to_be_clickable((By.NAME, "q"))
        )
        search_box.send_keys("Resync Consulting New Zealand")
        search_box.send_keys(Keys.RETURN)

        WebDriverWait(driver, 10).until(
            EC.url_contains("q=Resync+Consulting+New+Zealand")
        )
        assert "q=Resync+Consulting+New+Zealand" in driver.current_url

        first_result = WebDriverWait(driver, 10).until(
            EC.visibility_of_element_located(
                (By.XPATH, "//a[contains(translate(., 'RESYNC', 'resync'), 'resync')]")
            )
        )
        assert first_result.is_displayed()

        driver.save_screenshot("results.png")
    finally:
        driver.quit()


if __name__ == "__main__":
    test_duckduckgo_returns_results_for_an_nz_query()
    print("PASS")

What each piece does:

  • Service(ChromeDriverManager().install()) — downloads the correct ChromeDriver for your installed Chrome and returns the path. Selenium 4 requires you to pass this as a Service object — passing the path as a string directly is deprecated.
  • webdriver.Chrome(service=service) — launches Chrome. No Firefox/WebKit flag here — Selenium needs a separate driver for each browser.
  • try / finally with driver.quit() — Selenium will leave a zombie Chrome process running if the test errors without calling quit(). The finally block guarantees cleanup. Playwright does this for you automatically — Selenium does not.
  • WebDriverWait(driver, 10).until(EC.element_to_be_clickable(...)) — Selenium's explicit wait. Unlike Playwright's auto-wait, you must tell Selenium to wait for the element to be interactable. Skipping this is the #1 cause of flaky Selenium tests.
  • (By.NAME, "q") — the locator tuple. Selenium's locator options are ID, NAME, CLASS_NAME, CSS_SELECTOR, XPATH, LINK_TEXT, TAG_NAME. No built-in role-based locator — you fall back to attributes.
  • send_keys(Keys.RETURN) — submits the form by pressing Enter. Equivalent to Playwright's .press('Enter').
  • EC.url_contains(...) — the closest Selenium equivalent to Playwright's expect(page).toHaveURL(). You wrap it in WebDriverWait because there is no auto-retry on a plain assert.
  • XPATH with translate() — Selenium's XPath 1.0 has no case-insensitive contains, so translate() is the standard workaround to make the match case-insensitive.
  • driver.save_screenshot("results.png") — writes a viewport-only PNG. Selenium does not have Playwright's fullPage: true equivalent — full-page capture requires a CDP call or a third-party lib.
Reminder: this test targets DuckDuckGo. Do not retarget it at Google (it will captcha you) or any live client site (you'll hammer their prod). playwright.dev, duckduckgo.com, and example.com are the approved practice targets.

Playwright vs Selenium at a glance (same test, key differences):

ConcernPlaywright (P02)Selenium (P03)
Installnpm init playwright@latest — browsers includedpip install selenium webdriver-manager — then WDM downloads driver at runtime
WaitsAutomatic — expect(...) polls up to 5 s by defaultManual — wrap every lookup in WebDriverWait(...).until(...)
Cross-browserChromium + Firefox + WebKit by default in one runOne browser per run; each needs its own driver binary
LocatorsgetByRole, getByLabel, getByText built inBy.NAME, By.XPATH, By.CSS_SELECTOR — lower level
CleanupHandled by the test runnerYou write try/finally with driver.quit()
ReportingHTML report, trace viewer, UI mode out of the boxYou add pytest-html / allure yourself

5 Run & verify

With the venv active, from the project root:

python test_search.py
python3 test_search.py

Expected output (first run will also download ChromeDriver — 10-20 MB):

====== WebDriver manager ======
Get LATEST chromedriver version for google-chrome
Driver [~/.wdm/drivers/chromedriver/.../chromedriver] found in cache
PASS

A Chrome window will pop open, perform the search, and close. Check the project folder — results.png is there.

Want to run it as a proper test with pytest? Install pytest and re-run:

pip install pytest
pytest test_search.py -v

pytest discovers the test_-prefixed function automatically and gives you a nicer report. This is how you would structure a real project.

Pass criteria: script prints PASS (or pytest shows 1 passed), no uncaught exception, and results.png exists. If Chrome doesn't close automatically when the script ends, the finally block didn't fire — investigate before proceeding.

6 Troubleshooting

Error: session not created: This version of ChromeDriver only supports Chrome version X

Your Chrome auto-updated and the cached ChromeDriver is now older than your browser. webdriver-manager usually handles this, but its cache can go stale.

Fix: clear the cache and let it re-download.

Remove-Item $HOME\.wdm -Recurse -Force
rm -rf ~/.wdm
Error: NoSuchElementException: Unable to locate element: {"method":"name","selector":"q"}

You didn't wrap the lookup in a wait, and the page wasn't done loading. This is the Selenium classic — Playwright auto-waits, Selenium does not.

Fix: always use WebDriverWait(...).until(EC.presence_of_element_located(...)) or element_to_be_clickable when looking up elements. Never use a bare driver.find_element(By.NAME, 'q') for anything that might not be instant.

Error: ModuleNotFoundError: No module named 'selenium'

Your venv isn't active, or you installed into a different Python. Check the prompt shows (.venv) and run:

pip list | grep -i selenium

If empty, re-activate the venv and pip install selenium webdriver-manager again.

Chrome opens but the test times out on the result link

DuckDuckGo may show a cookie consent banner based on your region, or the results layout may have shifted.

Fix: open the browser in headed mode (it already is by default), watch what happens, and either dismiss the consent with an extra click, or loosen the XPath to match any link containing the word "resync" — even one with a broader class list.

7 Challenge

Stretch yourself

  1. Run Chrome headless. Add the option before launching:
    from selenium.webdriver.chrome.options import Options
    options = Options()
    options.add_argument("--headless=new")
    driver = webdriver.Chrome(service=service, options=options)
    This is how you will run in CI — no visible browser window. Confirm the test still passes.
  2. Convert the assertion into a proper pytest test with a @pytest.fixture that yields the driver and handles driver.quit() in teardown — so you can drop the try/finally from the test body. This is the standard Selenium-plus-pytest pattern you will see in real codebases.

Bonus: write a short reflection in a README — which tool felt cleaner for this test, and which would you reach for if a client asked you to stand up a new automation project tomorrow?