Implementation:MarketSquare Robotframework browser Create Lib Component From Jsextension
Document Type
API Doc -- Documents the Python APIs responsible for loading JavaScript extension modules, introspecting their exports via gRPC, and dynamically generating Python wrapper methods that become Robot Framework keywords.
API Summary
Three methods in the Browser class collaborate to register JavaScript extension keywords:
_create_lib_component_from_jsextension-- Orchestrates the full pipeline from file path to registered LibraryComponentinit_js_extension-- Sends the module path to the Node.js gRPC server and receives keyword metadata_jskeyword_call-- Generates a Python wrapper method for a single JavaScript function and binds it to a component
Source References
| File | Lines | Description |
|---|---|---|
Browser/browser.py |
L888-893 | jsextension handling in __init__
|
Browser/browser.py |
L997-1009 | _create_lib_component_from_jsextension
|
Browser/browser.py |
L1011-1017 | init_js_extension
|
Browser/browser.py |
L1019-1028 | _js_value_to_python_value
|
Browser/browser.py |
L1030-1084 | _jskeyword_call
|
Entry Point: jsextension in __init__
During library initialization, if the jsextension parameter is provided, it is split and each extension is processed:
if jsextension:
jsextensions = (
jsextension.split(",") if isinstance(jsextension, str) else jsextension
)
for js_ext in jsextensions:
libraries.append(self._create_lib_component_from_jsextension(js_ext))
Parameters:
jsextension-- A string (single path or comma-separated paths) or a list of paths to JavaScript module files
Behavior:
- Splits comma-separated strings into individual paths
- Creates a
LibraryComponentfor each extension - Appends each component to the
librarieslist that is passed toDynamicCore.__init__
API: _create_lib_component_from_jsextension
def _create_lib_component_from_jsextension(
self, jsextension: str
) -> LibraryComponent:
component = LibraryComponent(self)
response = self.init_js_extension(Path(jsextension))
for name, args, doc in zip(
response.keywords,
response.keywordArguments,
response.keywordDocumentations,
strict=False,
):
self._jskeyword_call(component, name, args, doc)
return component
Parameters:
jsextension(str) -- Path to the JavaScript extension module file
Returns:
LibraryComponent-- A component instance with all JavaScript functions registered as bound methods
Behavior:
- Creates a new empty
LibraryComponentinstance withself(the Browser library) as its parent - Calls
init_js_extensionto load the module on the Node.js side and receive metadata - Iterates over the parallel arrays in the response (
keywords,keywordArguments,keywordDocumentations) - For each function, calls
_jskeyword_callto generate and bind a Python wrapper method
API: init_js_extension
def init_js_extension(self, js_extension_path: Path | str) -> Response.Keywords:
with self.playwright.grpc_channel() as stub:
return stub.InitializeExtension(
Request().FilePath(
path=str(Path(js_extension_path).resolve().absolute())
)
)
Parameters:
js_extension_path(Path | str) -- Path to the JavaScript file, resolved to an absolute path before sending
Returns:
Response.Keywords-- A protobuf message containing:keywords-- List of function names (strings)keywordArguments-- List of argument specification strings (comma-separated name=default pairs)keywordDocumentations-- List of documentation strings (from.rfdocor empty)
Behavior:
- Opens a gRPC channel to the Node.js Playwright wrapper
- Sends an
InitializeExtensionrequest with the resolved absolute file path - The Node.js side calls
require()on the file, introspects exports, and returns metadata
Helper: _js_value_to_python_value
def _js_value_to_python_value(self, value: str) -> str:
return {
"true": "True",
"false": "False",
"null": "None",
"undefined": "None",
"NaN": "float('nan')",
"Infinity": "float('inf')",
"-Infinity": "float('-inf')",
}.get(value, value)
Parameters:
value(str) -- A JavaScript default value as a string
Returns:
- The Python equivalent string representation, or the original value if no mapping exists
API: _jskeyword_call
This is the most complex method, responsible for dynamic code generation:
def _jskeyword_call(
self,
component: LibraryComponent,
name: str,
argument_names_and_default_values: str,
doc: str,
):
argument_names_and_vals = [
[a.strip() for a in arg.split("=")]
for arg in (argument_names_and_default_values or "").split(",")
if arg
]
argument_names_and_default_values_texts = []
arg_set_texts = []
for item in argument_names_and_vals:
arg_name = item[0]
if arg_name in ["logger", "playwright", "page", "context", "browser"]:
arg_set_texts.append(f'("{arg_name}", "RESERVED")')
else:
arg_set_texts.append(f'("{arg_name}", {arg_name})')
if arg_name == "args":
argument_names_and_default_values_texts.append("*args")
elif len(item) > 1:
argument_names_and_default_values_texts.append(
f"{arg_name}={self._js_value_to_python_value(item[1])}"
)
else:
argument_names_and_default_values_texts.append(f"{arg_name}")
text = f"""
@keyword
def {name}(self, {", ".join(argument_names_and_default_values_texts)}):
\"\"\"{doc}\"\"\"
_args_browser_internal = dict()
_args_browser_internal["arguments"] = [{", ".join(arg_set_texts)}]
with self.playwright.grpc_channel() as stub:
responses = stub.CallExtensionKeyword(
Request().KeywordCall(name="{name}", arguments=json.dumps(_args_browser_internal))
)
for response in responses:
logger.info(response.log)
if response.json == "":
return
return json.loads(response.json)
"""
try:
exec(
text,
{**globals(), "keyword": keyword, "json": json},
component.__dict__,
)
setattr(
component, name, types.MethodType(component.__dict__[name], component)
)
except SyntaxError as e:
raise DataError(f"{e.msg} in {name}")
Parameters:
component(LibraryComponent) -- The component to bind the generated method toname(str) -- The JavaScript function name (becomes the Python method name)argument_names_and_default_values(str) -- Comma-separated argument specifications (e.g.,"selector,page,logger"or"a=default")doc(str) -- Documentation string from the.rfdocproperty
Behavior:
The method performs these steps:
- Parse arguments: Splits the argument specification string by commas, then each argument by
=to separate name from default value
- Classify arguments: For each argument:
- If the name is a reserved name (
logger,playwright,page,context,browser): adds("name", "RESERVED")to the argument tuple list but does not add it to the Python function's parameter list - If the name is
args: maps it to*args(variadic positional arguments) - If there is a default value: maps JavaScript defaults to Python equivalents and adds
name=default - Otherwise: adds the name as a required positional argument
- If the name is a reserved name (
- Generate Python code: Constructs a Python function definition string decorated with
@keywordthat:- Accepts
selfplus the user-facing arguments - Builds an arguments dictionary with reserved names set to
"RESERVED" - Calls
stub.CallExtensionKeywordvia gRPC - Logs messages from the response stream
- Returns the JSON-parsed result or
None
- Accepts
- Execute and bind: Uses
exec()to compile the function definition, then binds it to the component usingtypes.MethodType
- Error handling: Catches
SyntaxError(e.g., if a parameter namedselfwas used) and raisesDataError
Generated Code Example
For a JavaScript function:
async function withDefaultValue(a = "default") { ... }
withDefaultValue.rfdoc = "Returns the uppercased value";
The generated Python method would be equivalent to:
@keyword
def withDefaultValue(self, a="default"):
"""Returns the uppercased value"""
_args_browser_internal = dict()
_args_browser_internal["arguments"] = [("a", a)]
with self.playwright.grpc_channel() as stub:
responses = stub.CallExtensionKeyword(
Request().KeywordCall(name="withDefaultValue", arguments=json.dumps(_args_browser_internal))
)
for response in responses:
logger.info(response.log)
if response.json == "":
return
return json.loads(response.json)
Related
- MarketSquare_Robotframework_browser_Extension_Import_and_Registration -- Principle document for the registration process
- MarketSquare_Robotframework_browser_Call_Js_Keyword -- The programmatic API for calling JS keywords without code generation
- MarketSquare_Robotframework_browser_Js_Module_Exports_Pattern -- The expected module structure