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.
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.
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
-
Before app readiness,
#readystarts asapp:booting; the async callback should return the app-owned{state:"ready", version:1}object within the script timeout. -
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. -
A Chromium preference requesting
intl.accept_languages=en-USshould be visible in the requested vendor options and should normally produce a page-level language beginning withen-USin 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?
It directly changes the DOM value property and emits only the event the script asks for; it does not perform real focus/key sequences.
Why inspect options.to_capabilities() before
starting the browser?
It records what the test requested before the remote end/browser normalize or augment the session response.
Why verify navigator.language in addition to
inspecting vendor options?
A configuration request is not proof that the browser honored it; runtime behavior is independent evidence.
What is the correct control for a keyboard-shortcut requirement?
WebDriver keyboard input, because user input semantics are part of the requirement.
Why remove the profile directory only after
quit()?
The browser may still hold files/locks while the process is alive; quitting releases ownership before filesystem cleanup.
Official references and version notes
- Selenium 4.47 release notes — stable baseline pinned for this chapter.
- Selenium downloads — stable bindings/Grid versus 4.48 snapshot artifacts.
- Python WebDriver API — synchronous/asynchronous JavaScript execution and session capabilities.
- Browser options — standard capabilities including page-load strategy and insecure-certificate behavior.
- Chrome-specific functionality — Chromium option/configuration boundary.
-
Python Chrome Options API
— arguments, experimental options, capabilities, and
to_capabilities(). - Python common Options API — page-load strategy and cross-browser option properties.
- Avoid sharing state — fresh-session isolation guidance.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.