Chapter 12Lesson 02~210 minutes

JavaScript Execution, Browser Capabilities, Profiles, and Preferences: Guided Hands-On Workflow

Build one disposable loopback workflow that demonstrates script return conversion, an application-owned readiness event, native user input versus a deliberately scripted state helper, and one isolated Chromium preference with requested/returned capability evidence.

Local labexecuteScriptexecuteAsyncScriptChromium prefsEvidence

Learning objectives

  • Create a pinned Selenium 4.47.0 project and loopback fixture with a deterministic application-ready signal.
  • Use synchronous JavaScript for read-only page state and asynchronous JavaScript for a bounded app-owned event contract.
  • Compare native WebDriver text entry with a small scripted value helper and explain the lost user-event semantics.
  • Configure one Chromium preference in an isolated temporary profile and verify its observable effect.
  • Record requested capabilities, returned capabilities, DOM state, event counts, and cleanup evidence.
  • Choose the correct control surface for a follow-up challenge rather than copying a sequence.

1. Scenario and safety boundary

The local fixture exposes a nickname field, Apply button, event counter, and a synthetic academy:ready event after boot. It also displays navigator.language. The workflow never reaches an external site and contains no accounts, credentials, downloads, or sensitive data.

Lab rule: JavaScript may read page state and demonstrate one controlled value-setting helper. The business action remains a normal WebDriver button click; JavaScript is not used to force an obstructed/disabled control.

2. Create the isolated project and fixture

The following example makes the Create the isolated project and fixture behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.

mkdir selenium-ch12-workflow
cd selenium-ch12-workflow
python -m venv .venv
# Windows PowerShell: .\.venv\Scripts\Activate.ps1
# Linux/macOS: source .venv/bin/activate
python -m pip install "selenium==4.47.0"

The following example makes the Create the isolated project and fixture behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.

from pathlib import Path

root = Path("site")
root.mkdir(exist_ok=True)
(root / "index.html").write_text("""<!doctype html>
<html lang="en"><head><meta charset="utf-8"><title>Browser Configuration Lab</title>
<style>body{font-family:system-ui,sans-serif;max-width:850px;margin:2rem auto;padding:0 1rem}.panel{border:1px solid #8886;border-radius:.6rem;padding:1rem;margin:1rem 0}button,input{padding:.45rem .65rem;margin:.25rem}code{font-family:ui-monospace,monospace}</style></head>
<body>
<h1>Browser Configuration Lab</h1>
<p id="ready" data-ready="false">app:booting</p>
<div class="panel">
  <label>Nickname <input id="nickname" data-testid="nickname" autocomplete="off"></label>
  <button id="apply" type="button">Apply</button>
  <p id="draft">draft:</p>
  <p id="status">saved:none</p>
</div>
<div class="panel">
  <p id="language">language:unknown</p>
  <p id="events">events:0</p>
</div>
<script>
const input = document.querySelector('#nickname');
const apply = document.querySelector('#apply');
const draft = document.querySelector('#draft');
const status = document.querySelector('#status');
const ready = document.querySelector('#ready');
const language = document.querySelector('#language');
const events = document.querySelector('#events');
let eventCount = 0;

function noteEvent(){ eventCount += 1; events.textContent = `events:${eventCount}`; }
input.addEventListener('focus', noteEvent);
input.addEventListener('keydown', noteEvent);
input.addEventListener('input', () => { noteEvent(); draft.textContent = `draft:${input.value}`; });
input.addEventListener('change', noteEvent);
apply.addEventListener('click', () => { noteEvent(); status.textContent = `saved:${input.value}`; });

language.textContent = `language:${navigator.language}`;
window.setTimeout(() => {
  window.__academyReady = {state:'ready', version:1};
  ready.dataset.ready = 'true';
  ready.textContent = 'app:ready';
  window.dispatchEvent(new CustomEvent('academy:ready', {detail: window.__academyReady}));
}, 250);
</script></body></html>""", encoding="utf-8")
print(root.resolve())

Save the Python block as make_fixture.py, then:

python make_fixture.py
python -m http.server 8777 --bind 127.0.0.1 --directory site

3. Preflight and predictions

  1. Before app readiness, #ready starts as app:booting; the async callback should return the app-owned {state:"ready", version:1} object within the script timeout.
  2. Native send_keys() should increase the fixture event counter through focus/key/input events; direct value assignment plus one synthetic input event should produce fewer user-like events.
  3. A Chromium preference requesting intl.accept_languages=en-US should be visible in the requested vendor options and should normally produce a page-level language beginning with en-US in an unmanaged local Chrome/Chromium session.

4. Run the coherent workflow

The following example makes the Run the coherent workflow behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.

from pathlib import Path
import json
import shutil
import tempfile

import selenium
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait

URL = "http://127.0.0.1:8777/"
root = Path(tempfile.mkdtemp(prefix="selenium-ch12-"))
profile = root / "profile"
evidence = root / "evidence"
profile.mkdir()
evidence.mkdir()

options = webdriver.ChromeOptions()
options.page_load_strategy = "eager"
options.add_argument(f"--user-data-dir={profile.resolve()}")
options.add_experimental_option("prefs", {"intl.accept_languages": "en-US"})
requested = options.to_capabilities()

driver = webdriver.Chrome(options=options)
try:
    driver.set_script_timeout(2)
    driver.get(URL)

    # Read-only synchronous script: simple serializable values.
    initial = driver.execute_script("""
    return {
      title: document.title,
      ready: document.querySelector('#ready').dataset.ready,
      language: navigator.language
    };
    """)

    # App-owned asynchronous readiness contract.
    ready = driver.execute_async_script("""
    const done = arguments[arguments.length - 1];
    if (window.__academyReady) {
      done(window.__academyReady);
      return;
    }
    window.addEventListener('academy:ready', event => done(event.detail), {once:true});
    """)

    field = driver.find_element(By.ID, "nickname")
    status = driver.find_element(By.ID, "status")

    # Path A: native user-like text entry.
    field.clear()
    field.send_keys("webdriver-native")
    native_events = int(driver.find_element(By.ID, "events").text.split(":")[1])
    driver.find_element(By.ID, "apply").click()
    WebDriverWait(driver, 2).until(lambda d: status.text == "saved:webdriver-native")

    # Reset page to compare the second path from a clean DOM state.
    driver.refresh()
    WebDriverWait(driver, 2).until(lambda d: d.find_element(By.ID, "ready").text == "app:ready")
    field = driver.find_element(By.ID, "nickname")

    # Path B: deliberately small script helper. This bypasses typing semantics.
    driver.execute_script("""
    arguments[0].value = arguments[1];
    arguments[0].dispatchEvent(new Event('input', {bubbles:true}));
    """, field, "script-helper")
    scripted_events = int(driver.find_element(By.ID, "events").text.split(":")[1])
    driver.find_element(By.ID, "apply").click()  # business action remains WebDriver-native
    WebDriverWait(driver, 2).until(
        lambda d: d.find_element(By.ID, "status").text == "saved:script-helper"
    )

    returned = driver.capabilities
    packet = {
        "selenium": selenium.__version__,
        "session_id": driver.session_id,
        "requested_page_load_strategy": requested.get("pageLoadStrategy"),
        "requested_vendor_options": requested.get("goog:chromeOptions", {}),
        "returned_browser": returned.get("browserName"),
        "returned_browser_version": returned.get("browserVersion"),
        "returned_page_load_strategy": returned.get("pageLoadStrategy"),
        "initial": initial,
        "app_ready": ready,
        "native_event_count_before_apply": native_events,
        "script_event_count_before_apply": scripted_events,
        "runtime_language": driver.execute_script("return navigator.language"),
        "final_status": driver.find_element(By.ID, "status").text,
    }
    (evidence / "run.json").write_text(json.dumps(packet, indent=2), encoding="utf-8")
    driver.save_screenshot(str(evidence / "final.png"))
    print(json.dumps(packet, indent=2))
finally:
    driver.quit()
    shutil.rmtree(root)

5. Interpret the evidence, not just PASS

The native path should produce more focus/key/input activity because WebDriver models user input. The script helper changes a DOM property and emits only the event you explicitly dispatch. Both can reach the same final application status, but they do not test the same browser behavior.

Observation What it proves What it does not prove
native event count user-like focus/key/input path occurred server-side business correctness by itself
scripted event count helper set value + dispatched input event typing, keyboard layout, focus, interactability
requested goog:chromeOptions prefs test asked Chromium for a vendor preference browser necessarily honored every preference
navigator.language runtime page-level effect cross-browser portability of the preference mechanism
returned pageLoadStrategy standard capability negotiation SPA readiness

6. Return WebElements safely through the script bridge

The following example makes the Return WebElements safely through the script bridge behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.

field = driver.execute_script("return document.querySelector('#nickname')")
print(field.tag_name, field.get_attribute("id"))

Selenium converts the returned DOM node into a WebElement reference. That does not make script lookup preferable to ordinary locators; it simply demonstrates the transport semantics.

7. Challenge: choose the control surface

You need to test that pressing Enter in the nickname field triggers a keyboard-specific shortcut. Should you use execute_script() to call the handler directly, set the value property, or use WebDriver keyboard input?

Answer before revealing: use WebDriver keyboard input. The requirement is specifically about user keyboard behavior, so bypassing input semantics would invalidate the test. Use JavaScript only for evidence/readiness helpers if needed.

8. Cleanup and rollback

The browser is quit before the disposable profile directory is removed. No system browser settings are changed: all preferences exist only in the temporary profile/session. Stop the loopback HTTP server and delete the project when finished.

9. Summary and next step

The workflow showed that equal final DOM state does not imply equal test semantics. WebDriver-native input provides richer user-behavior evidence; a JavaScript helper can be legitimate only when the bypass is the point and its lost semantics are documented. Browser preferences likewise require both requested-configuration and runtime evidence.

Knowledge check

Why does the scripted path generate fewer interaction events?

Why inspect options.to_capabilities() before starting the browser?

Why verify navigator.language in addition to inspecting vendor options?

What is the correct control for a keyboard-shortcut requirement?

Why remove the profile directory only after quit()?

Next lesson

Choose configuration deliberately

Lesson 3 turns these observations into design rules for portability, profiles, headless execution, page-load strategy, vendor options, and realistic trust.

Official references and version notes

Version and compatibility note

Version-sensitive behavior was rechecked against current Selenium primary documentation on 2026-08-28. Mandatory examples pin Selenium Python 4.47.0 and Python 3.10+, use a supported locally installed Chromium-family browser with Selenium Manager, and target only 127.0.0.1. Selenium 4.48 material currently exposed in generated API pages/download snapshots is treated as development/nightly, not the stable lesson baseline. Browser-specific preferences are labeled as such. JavaScript state mutation is used only in explicit comparison/safety examples; user-facing business interactions remain WebDriver-native. No mandatory example enables insecure certificates or changes system/enterprise browser policy.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.