Files
camoufox/tests/async/test_headful.py
T
Jake WriterandClaude Opus 5 c3d57217a5 fix(juggler): evaluate in an isolated world, with an opt-in main-world hatch
Playwright routes page.evaluate() to the execution context juggler names '',
and upstream puts that context on the page window itself. Everything the
automation evaluates is then reachable by page script: a detection script can
hook Function.prototype.toString, window.eval or Object.defineProperty and
watch the automation work. That is the leak this fork exists to avoid, and it
regressed silently in 03c1230 ("migrate Juggler modules from JSM to ESM"),
which replaced the juggler sources with upstream's -- a two-line change,
invisible in a diff full of module-format churn, and no test noticed, because
page.evaluate() keeps working either way. It just stops being hidden.

FrameTree.js gives the '' world a Cu.Sandbox over the page window instead, so
the automation runs in its own compartment. tests/patches/isolated-evaluate.py
pins the property so it cannot regress the same way twice.

The cost is that Xray vision hides the page's own JS state, so
page.evaluate('window.pageVar') reads undefined. Runtime.js therefore carries a
`mw:` escape hatch: a standalone re-implementation of Playwright's
utilityScript.evaluate compiled inside the page's real global, gated on the
`allowMainWorld` config key and off by default. It mirrors the wire format
exactly, since the client would otherwise misread a returned object such as
{a: 1} as a serialized array. Handles are refused rather than silently
mistranslated.

forceScopeAccess now selects a system-principal sandbox for that world rather
than installing an accessor on the page's Element.prototype (#628), so the flag
no longer advertises itself to anything that probes for shadowRootUnl.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 14:32:42 -06:00

186 lines
6.7 KiB
Python

# Copyright (c) Microsoft Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from pathlib import Path
from typing import Dict
import pytest
from playwright.async_api import BrowserType
from tests.server import Server
async def test_should_have_default_url_when_launching_browser(
browser_type: BrowserType, launch_arguments: Dict, tmpdir: Path
) -> None:
browser_context = await browser_type.launch_persistent_context(
tmpdir, **{**launch_arguments, "headless": False}
)
urls = [page.url for page in browser_context.pages]
assert urls == ["about:blank"]
await browser_context.close()
async def test_should_close_browser_with_beforeunload_page(
browser_type: BrowserType, launch_arguments: Dict, server: Server, tmpdir: Path
) -> None:
browser_context = await browser_type.launch_persistent_context(
tmpdir, **{**launch_arguments, "headless": False}
)
page = await browser_context.new_page()
await page.goto(server.PREFIX + "/beforeunload.html")
# We have to interact with a page so that 'beforeunload' handlers
# fire.
await page.click("body")
await browser_context.close()
async def test_should_not_crash_when_creating_second_context(
browser_type: BrowserType, launch_arguments: Dict, server: Server
) -> None:
browser = await browser_type.launch(**{**launch_arguments, "headless": False})
browser_context = await browser.new_context()
await browser_context.new_page()
await browser_context.close()
browser_context = await browser.new_context()
await browser_context.new_page()
await browser_context.close()
await browser.close()
async def test_should_click_background_tab(
browser_type: BrowserType, launch_arguments: Dict, server: Server
) -> None:
browser = await browser_type.launch(**{**launch_arguments, "headless": False})
page = await browser.new_page()
await page.set_content(
f'<button>Hello</button><a target=_blank href="{server.EMPTY_PAGE}">empty.html</a>'
)
await page.click("a")
await page.click("button")
await browser.close()
async def test_should_close_browser_after_context_menu_was_triggered(
browser_type: BrowserType, launch_arguments: Dict, server: Server
) -> None:
browser = await browser_type.launch(**{**launch_arguments, "headless": False})
page = await browser.new_page()
await page.goto(server.PREFIX + "/grid.html")
await page.click("body", button="right")
await browser.close()
async def test_should_not_block_third_party_cookies(
browser_type: BrowserType,
launch_arguments: Dict,
server: Server,
is_chromium: bool,
is_firefox: bool,
) -> None:
browser = await browser_type.launch(**{**launch_arguments, "headless": False})
page = await browser.new_page()
await page.goto(server.EMPTY_PAGE)
await page.evaluate(
"""src => {
let fulfill;
const promise = new Promise(x => fulfill = x);
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
iframe.onload = fulfill;
iframe.src = src;
return promise;
}""",
server.CROSS_PROCESS_PREFIX + "/grid.html",
)
document_cookie = await page.frames[1].evaluate(
"""() => {
document.cookie = 'username=John Doe';
return document.cookie;
}"""
)
await page.wait_for_timeout(2000)
allows_third_party = is_firefox
assert document_cookie == ("username=John Doe" if allows_third_party else "")
cookies = await page.context.cookies(server.CROSS_PROCESS_PREFIX + "/grid.html")
if allows_third_party:
assert cookies == [
{
"domain": "127.0.0.1",
"expires": -1,
"httpOnly": False,
"name": "username",
"path": "/",
"sameSite": "Lax" if is_chromium else "None",
"secure": False,
"value": "John Doe",
}
]
else:
assert cookies == []
await browser.close()
# page.evaluate() runs in an isolated world whose sandbox holds an expanded
# principal, not the page's own (see additions/juggler/content/FrameTree.js).
# Firefox refuses cross-window property access from such a principal, so the
# popup's `resizeTo` is unreachable: "Permission denied to access property
# resizeTo on cross-origin object". Reaching another window from evaluate()
# requires being the page, which is exactly what Camoufox does not do.
@pytest.mark.skip(reason="Not supported by Camoufox")
async def test_should_not_override_viewport_size_when_passed_null(
browser_type: BrowserType, launch_arguments: Dict, server: Server
) -> None:
# Our WebKit embedder does not respect window features.
browser = await browser_type.launch(**{**launch_arguments, "headless": False})
context = await browser.new_context(no_viewport=True)
page = await context.new_page()
await page.goto(server.EMPTY_PAGE)
async with page.expect_popup() as popup_info:
await page.evaluate(
"""() => {
const win = window.open(window.location.href, 'Title', 'toolbar=no,location=no,directories=no,status=no,menubar=no,scrollbars=yes,resizable=yes,width=600,height=300,top=0,left=0');
win.resizeTo(500, 450);
}"""
)
popup = await popup_info.value
await popup.wait_for_load_state()
await popup.wait_for_function(
"""() => window.outerWidth === 500 && window.outerHeight === 450"""
)
await context.close()
await browser.close()
async def test_page_bring_to_front_should_work(
browser_type: BrowserType, launch_arguments: Dict
) -> None:
browser = await browser_type.launch(**{**launch_arguments, "headless": False})
page1 = await browser.new_page()
await page1.set_content("Page1")
page2 = await browser.new_page()
await page2.set_content("Page2")
await page1.bring_to_front()
assert await page1.evaluate("document.visibilityState") == "visible"
assert await page2.evaluate("document.visibilityState") == "visible"
await page2.bring_to_front()
assert await page1.evaluate("document.visibilityState") == "visible"
assert await page2.evaluate("document.visibilityState") == "visible"
await browser.close()