Add test helpers
This makes waiting for text robust. script.addPreloadScript() doesn't export the declared functions, so we need to attach them to `window`. We also don't need all of them.
This commit is contained in:
parent
ecb9420e8e
commit
91be31b648
2 changed files with 102 additions and 12 deletions
39
bidi.py
39
bidi.py
|
|
@ -69,6 +69,10 @@ def pick_ports(count: int) -> list[int]:
|
||||||
return ports
|
return ports
|
||||||
|
|
||||||
|
|
||||||
|
def jsquote(js: object) -> str:
|
||||||
|
return json.dumps(js)
|
||||||
|
|
||||||
|
|
||||||
class WebdriverBidi:
|
class WebdriverBidi:
|
||||||
def __init__(self, headless=False) -> None:
|
def __init__(self, headless=False) -> None:
|
||||||
self.headless = headless
|
self.headless = headless
|
||||||
|
|
@ -117,6 +121,9 @@ class WebdriverBidi:
|
||||||
"log.entryAdded", "browsingContext.domContentLoaded",
|
"log.entryAdded", "browsingContext.domContentLoaded",
|
||||||
])
|
])
|
||||||
|
|
||||||
|
test_functions = Path("test-functions.js").read_text()
|
||||||
|
await self.bidi("script.addPreloadScript", functionDeclaration=f"() => {{ {test_functions} }}")
|
||||||
|
|
||||||
# wait for browser to initialize default context
|
# wait for browser to initialize default context
|
||||||
for _ in range(10):
|
for _ in range(10):
|
||||||
realms = (await self.bidi("script.getRealms"))["realms"]
|
realms = (await self.bidi("script.getRealms"))["realms"]
|
||||||
|
|
@ -147,7 +154,11 @@ class WebdriverBidi:
|
||||||
if "id" in data and data["id"] in self.pending_commands:
|
if "id" in data and data["id"] in self.pending_commands:
|
||||||
log_proto.debug("ws_reader: resolving pending command %i", data["id"])
|
log_proto.debug("ws_reader: resolving pending command %i", data["id"])
|
||||||
if data["type"] == "success":
|
if data["type"] == "success":
|
||||||
self.pending_commands[data["id"]].set_result(data["result"])
|
result = data["result"]
|
||||||
|
if result.get("type") == "exception":
|
||||||
|
self.pending_commands[data["id"]].set_exception(RuntimeError(result["exceptionDetails"]))
|
||||||
|
else:
|
||||||
|
self.pending_commands[data["id"]].set_result(result)
|
||||||
else:
|
else:
|
||||||
self.pending_commands[data["id"]].set_exception(
|
self.pending_commands[data["id"]].set_exception(
|
||||||
WebdriverError(f"{data['type']}: {data['message']}"))
|
WebdriverError(f"{data['type']}: {data['message']}"))
|
||||||
|
|
@ -185,8 +196,9 @@ class WebdriverBidi:
|
||||||
future = asyncio.get_event_loop().create_future()
|
future = asyncio.get_event_loop().create_future()
|
||||||
self.pending_commands[self.last_id] = future
|
self.pending_commands[self.last_id] = future
|
||||||
self.last_id += 1
|
self.last_id += 1
|
||||||
# we really expect this to be fast, otherwise the browser crashed; in particular, TIMEOUT is too long
|
# some calls can take very long (wait for condition);
|
||||||
return await asyncio.wait_for(future, timeout=5)
|
# safe-guard timeout for avoiding eternally hanging tests
|
||||||
|
return await asyncio.wait_for(future, timeout=60)
|
||||||
|
|
||||||
#
|
#
|
||||||
# BiDi state tracking
|
# BiDi state tracking
|
||||||
|
|
@ -249,6 +261,12 @@ class WebdriverBidi:
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"timed out waiting for {selector}: {last_error}")
|
raise ValueError(f"timed out waiting for {selector}: {last_error}")
|
||||||
|
|
||||||
|
async def wait_js_cond(self, cond: str, error_description: str = "null") -> None:
|
||||||
|
log_command.debug("wait_js_cond(%s)", cond)
|
||||||
|
await self.bidi("script.evaluate",
|
||||||
|
expression=f"window.ph_wait_cond(() => {cond}, {TIMEOUT * 1000}, {error_description})",
|
||||||
|
awaitPromise=True, target={"context": self.context})
|
||||||
|
|
||||||
async def text(self, selector: str) -> str:
|
async def text(self, selector: str) -> str:
|
||||||
# there is no BiDi way of evaluating a script on a particular element or getting
|
# there is no BiDi way of evaluating a script on a particular element or getting
|
||||||
# the text of an element, so just use the standard `.textContent` web platform property
|
# the text of an element, so just use the standard `.textContent` web platform property
|
||||||
|
|
@ -258,6 +276,11 @@ class WebdriverBidi:
|
||||||
awaitPromise=False, target={"context": self.context})
|
awaitPromise=False, target={"context": self.context})
|
||||||
return r['result']['value']
|
return r['result']['value']
|
||||||
|
|
||||||
|
async def wait_text(self, selector: str, text: str) -> None:
|
||||||
|
await self.wait(selector)
|
||||||
|
await self.wait_js_cond(f"window.ph_text({jsquote(selector)}) === {jsquote(text)}",
|
||||||
|
error_description=f"() => 'actual text: ' + window.ph_text({jsquote(selector)})")
|
||||||
|
|
||||||
async def mouse(self, selector: str, button: int = 0, click_count: int = 1) -> None:
|
async def mouse(self, selector: str, button: int = 0, click_count: int = 1) -> None:
|
||||||
element = await self.locate(selector)
|
element = await self.locate(selector)
|
||||||
|
|
||||||
|
|
@ -438,19 +461,11 @@ async def main():
|
||||||
|
|
||||||
print("\n\nSTEP: super-user-indicator")
|
print("\n\nSTEP: super-user-indicator")
|
||||||
try:
|
try:
|
||||||
await d.wait("#super-user-indicator")
|
await d.wait_text("#super-user-indicator", "Limited access")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
s = await d.bidi("browsingContext.captureScreenshot", context=d.top_context, origin="document")
|
s = await d.bidi("browsingContext.captureScreenshot", context=d.top_context, origin="document")
|
||||||
Path("screenshot.png").write_bytes(base64.b64decode(s["data"]))
|
Path("screenshot.png").write_bytes(base64.b64decode(s["data"]))
|
||||||
raise
|
raise
|
||||||
# FIXME: wait for text helper
|
|
||||||
for _ in range(5):
|
|
||||||
t = await d.text("#super-user-indicator")
|
|
||||||
if t == "Limited access":
|
|
||||||
break
|
|
||||||
await asyncio.sleep(0.5)
|
|
||||||
else:
|
|
||||||
raise ValueError("timed out waiting for #super-user-indicator text")
|
|
||||||
|
|
||||||
print("\n\nSTEP: wait/switch frame")
|
print("\n\nSTEP: wait/switch frame")
|
||||||
await d.switch_to_frame('cockpit1:localhost/system')
|
await d.switch_to_frame('cockpit1:localhost/system')
|
||||||
|
|
|
||||||
75
test-functions.js
Normal file
75
test-functions.js
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
window.ph_select = function(sel) {
|
||||||
|
if (!window.Sizzle) {
|
||||||
|
return Array.from(document.querySelectorAll(sel));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sel.includes(":contains(")) {
|
||||||
|
if (!window.Sizzle) {
|
||||||
|
throw new Error("Using ':contains' when window.Sizzle is not available.");
|
||||||
|
}
|
||||||
|
return window.Sizzle(sel);
|
||||||
|
} else {
|
||||||
|
return Array.from(document.querySelectorAll(sel));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.ph_find = function(sel) {
|
||||||
|
const els = window.ph_select(sel);
|
||||||
|
if (els.length === 0)
|
||||||
|
throw new Error(sel + " not found");
|
||||||
|
if (els.length > 1)
|
||||||
|
throw new Error(sel + " is ambiguous");
|
||||||
|
return els[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
window.ph_text = function(sel) {
|
||||||
|
const el = window.ph_find(sel);
|
||||||
|
if (el.textContent === undefined)
|
||||||
|
throw new Error(sel + " can not have text");
|
||||||
|
// 0xa0 is a non-breakable space, which is a rendering detail of Chromium
|
||||||
|
// and awkward to handle in tests; turn it into normal spaces
|
||||||
|
return el.textContent.replaceAll("\xa0", " ");
|
||||||
|
};
|
||||||
|
|
||||||
|
window.ph_is_visible = function(sel) {
|
||||||
|
const el = window.ph_find(sel);
|
||||||
|
return el.tagName === "svg" || ((el.offsetWidth > 0 || el.offsetHeight > 0) && !(el.style.visibility === "hidden" || el.style.display === "none"));
|
||||||
|
};
|
||||||
|
|
||||||
|
class PhWaitCondTimeout extends Error {
|
||||||
|
constructor(description) {
|
||||||
|
if (description && description.apply)
|
||||||
|
description = description.apply();
|
||||||
|
if (description)
|
||||||
|
super(description);
|
||||||
|
else
|
||||||
|
super("condition did not become true");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.ph_wait_cond = function (cond, timeout, error_description) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// poll every 100 ms for now; FIXME: poll less often and re-check on mutations using
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver
|
||||||
|
let stepTimer = null;
|
||||||
|
let last_err = null;
|
||||||
|
const tm = window.setTimeout(() => {
|
||||||
|
if (stepTimer)
|
||||||
|
window.clearTimeout(stepTimer);
|
||||||
|
reject(last_err || new PhWaitCondTimeout(error_description));
|
||||||
|
}, timeout);
|
||||||
|
function step() {
|
||||||
|
try {
|
||||||
|
if (cond()) {
|
||||||
|
window.clearTimeout(tm);
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
last_err = err;
|
||||||
|
}
|
||||||
|
stepTimer = window.setTimeout(step, 100);
|
||||||
|
}
|
||||||
|
step();
|
||||||
|
});
|
||||||
|
};
|
||||||
Loading…
Add table
Add a link
Reference in a new issue