A text/plain or text/markdown response was parsed as HTML, so every
<word> in the body became an element and its text was dropped from
textContent, --dump text and --dump markdown. document.contentType also
reported text/html for these documents because it was inferred from the
URL.
Plain-text responses now build the document Chrome builds: a body
holding one <pre> with the whole text. document.contentType reports the
main resource's own MIME type, and _isXMLDocument no longer treats a
plain-text document as XML.
Web Storage was a plain object inside the V8 realm, and `Page::init_js` builds a
new realm on every navigation, so every entry was dropped by the next document.
localStorage now lives on the BrowserContext and sessionStorage on the Page, both
in Rust keyed by origin, with per-origin and total quotas.
Under `--storage-dir`, localStorage is written per origin after a document's
scripts run and on shutdown, and read back when a context is created. Only
origins that changed since the last successful write are touched, a clear or a
last-key removal unlinks the file instead of letting the next load resurrect it,
and a failed write leaves the store dirty so it is retried. sessionStorage stays
in memory: it is per tab, like a real browser, but it now survives navigation
and CDP target switching within the process.
`obscura mcp` never passed `--storage-dir` to its BrowserContext, so that path
persisted nothing, cookies included. It reads the profile from
OBSCURA_STORAGE_DIR, set by the CLI from the global flag, and flushes on exit.
IndexedDB was a shim that handed every object store its own Map and never fired
onupgradeneeded, so a schema-created database was unreachable and the first read
threw. Records and the schema live in an origin-keyed store on the context, and
the shim implements the spec shape: versionchange transactions, real
objectStoreNames, NotFoundError, asynchronous request settlement, transactions
that complete on a macrotask so chained requests work, key paths, generators,
indexes and cursors. Values round-trip through JSON.
Fixes#678.
`browser_evaluate` is synchronous, so an agent that wrote an async IIFE or
`await fetch(...)` got `{}` back, with no way to tell that from an empty object.
The expression is now evaluated once and, if the result is a thenable, its
settlement is polled until it resolves, fails, or the timeout runs out. The poll
drives the page's own task queue, because a promise that settles in a network
callback never gets a chance otherwise, which is what made the first version of
this look like a hang. A page with no realm falls back to the old synchronous
reading, so the tool can only ever return more than before.
`browser_wait_for_function` polls a JavaScript predicate with a timeout and a
poll interval. Agents otherwise express "wait until the app finished loading" as
a loop of evaluate plus a sleep, which costs a round trip per iteration and is
where flaky automation comes from.
`expression_wrapper` is separate from the polling loop so its shape can be
asserted in a test: a wrapper that dereferences its slot before creating it turns
every synchronous expression into "did not settle", which is exactly what the
first version did.