perf(fast-browser): batch value-free observation metadata
This commit is contained in:
@@ -103,6 +103,25 @@ class FakeBackend:
|
||||
pass
|
||||
|
||||
|
||||
class BatchFakeBackend(FakeBackend):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.metadata_many_calls = []
|
||||
self.metadata_calls = []
|
||||
self.batch_result = None
|
||||
|
||||
def metadata(self, backend_id):
|
||||
self.metadata_calls.append(backend_id)
|
||||
return super().metadata(backend_id)
|
||||
|
||||
def metadata_many(self, backend_ids):
|
||||
ids = list(backend_ids)
|
||||
self.metadata_many_calls.append(ids)
|
||||
if self.batch_result is not None:
|
||||
return self.batch_result
|
||||
return {backend_id: self.elements[backend_id] for backend_id in ids if backend_id in self.elements}
|
||||
|
||||
|
||||
class PopupFakeBackend(FakeBackend):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
@@ -550,6 +569,82 @@ class WorkerTests(unittest.TestCase):
|
||||
self.assertNotIn("values", json.dumps(observation))
|
||||
self.assertNotIn("Disabled", json.dumps(observation))
|
||||
|
||||
def test_observe_batches_dom_metadata_but_keeps_full_ax_roles_and_names(self):
|
||||
backend = BatchFakeBackend()
|
||||
backend.elements[1] = replace(backend.elements[1], name="DOM Email", ax_role="textbox", ax_name="AX Email")
|
||||
backend.elements[3] = replace(backend.elements[3], name="DOM Submit", ax_role="button", ax_name="AX Submit")
|
||||
backend.nodes[0]["name"] = {"value": "AX Email old-test-secret"}
|
||||
backend.nodes[1]["name"] = {"value": "AX Submit"}
|
||||
worker = BrowserWorker(backend, ["http://fixture.test"])
|
||||
|
||||
observation = worker.observe()
|
||||
|
||||
self.assertEqual(backend.metadata_many_calls, [[1, 3, 4, 2]])
|
||||
self.assertEqual(backend.metadata_calls, [])
|
||||
self.assertEqual([candidate["name"] for candidate in observation["candidates"]],
|
||||
["DOM Email", "AX Submit", "Password"])
|
||||
self.assertEqual([candidate["role"] for candidate in observation["candidates"]],
|
||||
["textbox", "button", "textbox"])
|
||||
self.assertNotIn("old-test-secret", json.dumps(observation))
|
||||
|
||||
result = worker.act({
|
||||
"observationId": observation["observationId"], "candidateId": "c-1", "action": "click",
|
||||
"postcondition": {"kind": "url", "value": "http://fixture.test/done"},
|
||||
})
|
||||
self.assertTrue(result["verified"])
|
||||
self.assertEqual(backend.metadata_calls, [3])
|
||||
|
||||
def test_observe_falls_back_for_missing_batch_metadata(self):
|
||||
backend = BatchFakeBackend()
|
||||
backend.batch_result = {1: backend.elements[1]}
|
||||
worker = BrowserWorker(backend, ["http://fixture.test"])
|
||||
|
||||
observation = worker.observe()
|
||||
|
||||
self.assertEqual([candidate["name"] for candidate in observation["candidates"]],
|
||||
["Email", "Submit", "Password"])
|
||||
self.assertEqual(backend.metadata_calls, [3, 4, 2])
|
||||
|
||||
def test_observe_batches_in_bounded_chunks_and_preserves_order_and_deduplication(self):
|
||||
backend = BatchFakeBackend()
|
||||
backend.elements = {
|
||||
index: ElementMetadata(index, "button", "", "button", f"Button {index}", True, False,
|
||||
ax_role="button", ax_name=f"Button {index}")
|
||||
for index in range(1, 34)
|
||||
}
|
||||
backend.nodes = [
|
||||
{"backendDOMNodeId": index, "role": {"value": "button"}, "name": {"value": f"AX {index}"}}
|
||||
for index in range(1, 34)
|
||||
]
|
||||
backend.nodes.insert(10, dict(backend.nodes[9]))
|
||||
worker = BrowserWorker(backend, ["http://fixture.test"])
|
||||
|
||||
observation = worker.observe()
|
||||
|
||||
self.assertEqual([len(chunk) for chunk in backend.metadata_many_calls], [16, 16, 1])
|
||||
self.assertEqual([candidate["name"] for candidate in observation["candidates"]],
|
||||
[f"AX {index}" for index in range(1, 34)])
|
||||
self.assertEqual(len(observation["candidates"]), 33)
|
||||
|
||||
def test_observe_skips_detached_nodes_and_keeps_candidate_limit(self):
|
||||
backend = BatchFakeBackend()
|
||||
backend.elements = {
|
||||
index: ElementMetadata(index, "button", "", "button", f"Button {index}", True, False,
|
||||
connected=index != 10, ax_role="button", ax_name=f"Button {index}")
|
||||
for index in range(1, 63)
|
||||
}
|
||||
backend.nodes = [
|
||||
{"backendDOMNodeId": index, "role": {"value": "button"}, "name": {"value": f"AX {index}"}}
|
||||
for index in range(1, 63)
|
||||
]
|
||||
worker = BrowserWorker(backend, ["http://fixture.test"])
|
||||
|
||||
observation = worker.observe()
|
||||
|
||||
self.assertEqual(len(observation["candidates"]), 60)
|
||||
self.assertNotIn("AX 10", json.dumps(observation))
|
||||
self.assertTrue(all(len(chunk) <= 16 for chunk in backend.metadata_many_calls))
|
||||
|
||||
def test_password_fill_uses_literal_without_exposing_value_in_metadata_or_result(self):
|
||||
backend = FakeBackend()
|
||||
backend.values[2] = "old-test-secret"
|
||||
@@ -907,6 +1002,55 @@ class BridgeRegressionTests(unittest.TestCase):
|
||||
self.assertEqual(keydowns[0].kwargs["modifiers"], 4)
|
||||
backend._helpers.press_key.assert_called_once_with("Backspace")
|
||||
|
||||
def test_metadata_many_uses_one_call_and_releases_temporary_object_group(self):
|
||||
backend = RealBrowserBackend.__new__(RealBrowserBackend)
|
||||
helper = Mock()
|
||||
backend._helpers = helper
|
||||
|
||||
def cdp(method, **kwargs):
|
||||
if method == "DOM.resolveNode":
|
||||
return {"object": {"objectId": f"object-{kwargs['backendNodeId']}"}}
|
||||
if method == "Runtime.callFunctionOn":
|
||||
return {"result": {"value": [
|
||||
{"tag": "input", "type": "text", "role": "", "name": "Email",
|
||||
"connected": True, "mainFrame": True, "contenteditable": False,
|
||||
"visible": True, "disabled": False},
|
||||
{"tag": "button", "type": "", "role": "button", "name": "Submit",
|
||||
"connected": True, "mainFrame": True, "contenteditable": False,
|
||||
"visible": True, "disabled": False},
|
||||
]}}
|
||||
return {}
|
||||
helper.cdp.side_effect = cdp
|
||||
|
||||
result = backend.metadata_many([1, 2])
|
||||
|
||||
self.assertEqual([metadata.name for metadata in result.values()], ["Email", "Submit"])
|
||||
calls = [call.args[0] for call in helper.cdp.call_args_list]
|
||||
self.assertEqual(calls.count("DOM.resolveNode"), 2)
|
||||
self.assertEqual(calls.count("Runtime.callFunctionOn"), 1)
|
||||
self.assertEqual(calls.count("Runtime.releaseObjectGroup"), 1)
|
||||
call = next(call for call in helper.cdp.call_args_list if call.args[0] == "Runtime.callFunctionOn")
|
||||
self.assertEqual([argument.get("objectId") for argument in call.kwargs["arguments"]],
|
||||
["object-1", "object-2"])
|
||||
|
||||
def test_metadata_many_releases_object_group_when_batch_call_fails(self):
|
||||
backend = RealBrowserBackend.__new__(RealBrowserBackend)
|
||||
helper = Mock()
|
||||
backend._helpers = helper
|
||||
|
||||
def cdp(method, **kwargs):
|
||||
if method == "DOM.resolveNode":
|
||||
return {"object": {"objectId": f"object-{kwargs['backendNodeId']}"}}
|
||||
if method == "Runtime.callFunctionOn":
|
||||
raise RuntimeError("detached")
|
||||
return {}
|
||||
helper.cdp.side_effect = cdp
|
||||
|
||||
self.assertEqual(backend.metadata_many([1, 2]), {})
|
||||
self.assertEqual(
|
||||
[call.args[0] for call in helper.cdp.call_args_list].count("Runtime.releaseObjectGroup"), 1
|
||||
)
|
||||
|
||||
def test_call_releases_remote_object_on_browser_exception(self):
|
||||
backend = RealBrowserBackend.__new__(RealBrowserBackend)
|
||||
backend._helpers = Mock()
|
||||
|
||||
Reference in New Issue
Block a user