robotframework-lynqa 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ """Robot Framework library for executing Lynqa test runs."""
2
+
3
+ from robotframework_lynqa.library import BASE_URL, LynqaLibrary
4
+
5
+ __all__ = ["BASE_URL", "LynqaLibrary"]
@@ -0,0 +1,19 @@
1
+ *** Settings ***
2
+ Documentation Acceptance test executed against the real Lynqa service.
3
+ ... Requires the ``LYNQA_API_KEY`` environment variable to be set to a valid Lynqa API key.
4
+ ... Runs a Google search for "lynqa" and checks that the first result mentions "Smartesting".
5
+
6
+ Library robotframework_lynqa.LynqaLibrary api_key=%{LYNQA_API_KEY}
7
+
8
+
9
+ *** Variables ***
10
+ ${LYNQA_URL} https://www.google.com/
11
+
12
+
13
+ *** Test Cases ***
14
+ Search Engine
15
+ Given go to the website
16
+ When I look at the search input
17
+ Then the search input exists
18
+ When I search for 'lynqa'
19
+ Then several results are displayed and the first results display 'Smartesting'
@@ -0,0 +1,338 @@
1
+ """Robot Framework test library and listener for the Lynqa execution agent.
2
+
3
+ This module exposes :class:`LynqaLibrary`, a Robot Framework library that lets you write scenarios in plain Gherkin and
4
+ have them executed by the Lynqa AI agent instead of by locally defined keywords.
5
+
6
+ Scenarios are written with the ``Given``/``When``/``Then`` keywords, each taking a single natural-language step as its
7
+ argument:
8
+
9
+ ::
10
+
11
+ *** Settings ***
12
+ Library robotframework_lynqa.LynqaLibrary api_key=%{LYNQA_API_KEY}
13
+
14
+ *** Variables ***
15
+ ${LYNQA_URL} https://example.com/
16
+
17
+ *** Test Cases ***
18
+ Search For An Item
19
+ Given the home page is open
20
+ When the user searches for an item
21
+ Then matching results are displayed
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import time
27
+ from datetime import datetime
28
+ from typing import Any
29
+
30
+ import pylynqa
31
+ from pylynqa import LynqaClient, TestData, TestRunContext
32
+ from robot.api import SuiteVisitor, logger
33
+ from robot.api.deco import keyword
34
+ from robot.libraries.BuiltIn import BuiltIn
35
+
36
+ from robotframework_lynqa.reporter import StepReporter
37
+
38
+ BASE_URL = "https://api.lynqa.smartesting.com"
39
+
40
+ GHERKIN_KEYWORDS = ("Given", "When", "Then")
41
+
42
+ # Statuses that mean the run is not finished yet.
43
+ PENDING_STATUSES = ("waiting", "running", "not_run")
44
+
45
+ # How often to poll the run status, and how long to wait before giving up.
46
+ POLL_INTERVAL_SECONDS = 10
47
+ RUN_TIMEOUT_SECONDS = 3600
48
+
49
+ # Variable names
50
+ URL_VAR = "${LYNQA_URL}"
51
+ LANGUAGE_VAR = "${LYNQA_LANGUAGE}"
52
+ DATETIME_VAR = "${LYNQA_DATETIME}"
53
+ SECRETS_VAR = "&{LYNQA_SECRETS}"
54
+
55
+
56
+ class _GherkinStepCollector(SuiteVisitor):
57
+ """Collect the Gherkin steps of a running test case.
58
+
59
+ Walks a Robot Framework test case and records every keyword call whose name is one of :data:`GHERKIN_KEYWORDS`,
60
+ rebuilding the original Gherkin step (keyword name followed by its argument text).
61
+ """
62
+
63
+ def __init__(self) -> None:
64
+ """Initialize the collector with an empty list of steps."""
65
+ self.steps: list[str] = []
66
+
67
+ def start_keyword(self, keyword) -> None:
68
+ """Record the keyword when it is one of the Gherkin step keywords.
69
+
70
+ :param keyword: Running keyword visited by Robot Framework.
71
+ """
72
+ if keyword.name in GHERKIN_KEYWORDS:
73
+ text_step = ""
74
+ if len(keyword.args) > 0:
75
+ text_step = keyword.args[0]
76
+ self.steps.append(f"{keyword.name} {text_step}")
77
+
78
+
79
+ class LynqaLibrary:
80
+ """Robot Framework library that runs Gherkin scenarios via Lynqa.
81
+
82
+ Acts both as a test library exposing the Gherkin step keywords (``Given``/``When``/``Then``) and as a listener that
83
+ captures each scenario and submits it to the Lynqa execution agent.
84
+
85
+ **Execution timeout**:
86
+
87
+ Once a scenario is submitted, the library polls Lynqa until the run reaches a final status, waiting up to **1
88
+ hour** before failing the test with a timeout error. To change this behaviour, use Robot Framework's timeout
89
+ feature (the ``Test Timeout`` suite setting or a ``[Timeout]`` on the test case), which caps how long a test is
90
+ allowed to run.
91
+ """
92
+
93
+ ROBOT_LIBRARY_SCOPE = "TEST"
94
+ ROBOT_LIBRARY_DOC_FORMAT = "reST"
95
+ ROBOT_AUTO_KEYWORDS = False
96
+
97
+ def __init__(self, api_key: str, base_url: str = BASE_URL) -> None:
98
+ """Initialize the Lynqa library.
99
+
100
+ :param api_key: API key used to authenticate against the Lynqa service.
101
+ :param base_url: Base URL of the Lynqa API.
102
+ """
103
+ self.ROBOT_LIBRARY_LISTENER = self
104
+ self._init_client(api_key, base_url)
105
+ self._run_id: str = ""
106
+ self._start_error: Exception | None = None
107
+ self._step_index: int = 0
108
+ self._reporter: StepReporter | None = None
109
+
110
+ self.scenario: str = ""
111
+ self.url: str | None = ""
112
+ self.context: TestRunContext = TestRunContext()
113
+ self.results: dict[str, Any] = {}
114
+
115
+ def _init_client(self, api_key: str, base_url: str):
116
+ """Initialize the Lynqa client.
117
+
118
+ :param api_key: API key used to authenticate against the Lynqa service.
119
+ :param base_url: Base URL of the Lynqa API.
120
+ """
121
+ pylynqa.client._PACKAGE_CONSUMER_NAME = "petit-robot:robotframework-lynqa" # ty: ignore[invalid-assignment] # ruff: ignore[private-member-access]
122
+ self._client = LynqaClient(api_key=api_key, base_url=base_url)
123
+
124
+ # ------------------------------------------------------------------
125
+ # Listener interface
126
+ # ------------------------------------------------------------------
127
+
128
+ def start_test(self, data, result) -> None:
129
+ """Collect the scenario's Gherkin steps when a test starts.
130
+
131
+ Reads the running test case with a :class:`_GherkinStepCollector` visitor and stores its
132
+ ``Given``/``When``/``Then`` steps as the current scenario.
133
+
134
+ Any exception raised while preparing the run is captured and turned into a test failure in :meth:`end_test` (a
135
+ status set here would otherwise be overwritten when the test body runs).
136
+
137
+ :param data: Running test case provided by Robot Framework.
138
+ :param result: Test result object provided by Robot Framework.
139
+ """
140
+
141
+ def collect_steps(data):
142
+ """Collect the Gherkin steps from the running test case."""
143
+ collector = _GherkinStepCollector()
144
+ data.visit(collector)
145
+ return collector.steps
146
+
147
+ self._start_error = None
148
+ self._step_index = 0
149
+ try:
150
+ self.scenario = "\n".join(collect_steps(data))
151
+ url = self._search_variable(URL_VAR)
152
+ self.url = None if url is None else str(url)
153
+ self.context = self._search_context()
154
+ self._execute_lynqa_testrun(name=data.name)
155
+ except Exception as error:
156
+ logger.error(f"Lynqa run preparation failed: {error}")
157
+ self._start_error = error
158
+
159
+ def end_test(self, data, result) -> None:
160
+ """Fail the current test if :meth:`start_test` caught an exception.
161
+
162
+ :param data: Running test case provided by Robot Framework.
163
+ :param result: Test result object provided by Robot Framework.
164
+ """
165
+ if self._start_error is not None:
166
+ result.status = "FAIL"
167
+ result.message = f"Lynqa execution failed: {self._start_error}"
168
+ return
169
+
170
+ self._report_test_results(result)
171
+
172
+ def start_keyword(self, data, result) -> None:
173
+ """Log the commands and assertions of a Gherkin keyword's Lynqa step.
174
+
175
+ Logging happens here, when the keyword starts, rather than in the keyword body: Robot Framework stops executing
176
+ the bodies of the keywords that follow a failed one, but :meth:`start_keyword` still fires for them, so every
177
+ Gherkin keyword gets its logs. The step is matched in execution order via :attr:`_step_index`, which is only
178
+ incremented later in :meth:`end_keyword`.
179
+
180
+ :param data: Running keyword provided by Robot Framework.
181
+ :param result: Keyword result object provided by Robot Framework.
182
+ """
183
+ if data.name not in GHERKIN_KEYWORDS:
184
+ return
185
+ if self._reporter is not None:
186
+ self._reporter.log_step(self._step_index, self.results)
187
+
188
+ def end_keyword(self, data, result) -> None:
189
+ """Set each Gherkin keyword's status from its matching Lynqa step.
190
+
191
+ Only the Gherkin step keywords are reported; other keywords (logging, setup, ...) keep the status Robot
192
+ Framework gave them.
193
+
194
+ :param data: Running keyword provided by Robot Framework.
195
+ :param result: Keyword result object provided by Robot Framework.
196
+ """
197
+ if data.name not in GHERKIN_KEYWORDS:
198
+ return
199
+ steps = self.results.get("stepStatuses", [])
200
+ if self._step_index < len(steps):
201
+ step_status = steps[self._step_index].get("status")
202
+ else:
203
+ # No per-step detail: fall back to the overall run status.
204
+ step_status = self.results.get("status")
205
+ result.status = "PASS" if step_status == "success" else "FAIL"
206
+ self._step_index += 1
207
+
208
+ def _execute_lynqa_testrun(self, name: str, timeout: float = RUN_TIMEOUT_SECONDS):
209
+ """Start a Lynqa test run and wait for it to reach a final status.
210
+
211
+ Polls the run status every :data:`POLL_INTERVAL_SECONDS` seconds until it leaves the :data:`PENDING_STATUSES`,
212
+ giving up after ``timeout`` seconds so the run cannot hang forever when a final status is never reached.
213
+
214
+ :param name: Name of the test run.
215
+ :param timeout: Maximum time, in seconds, to wait for the run to finish.
216
+
217
+ :raises TimeoutError: If the run is still pending once ``timeout`` elapses.
218
+ """
219
+ if not self.url:
220
+ raise ValueError(f"No URL configured. Please set the {URL_VAR} variable.") # ruff: ignore[raise-vanilla-args]
221
+
222
+ self._run_id = self._client.add_gherkin_test_run(
223
+ url=self.url,
224
+ name=name,
225
+ scenario=self.scenario,
226
+ )
227
+ logger.info(f"Testrun ID is: {self._run_id}")
228
+ self._wait_until_testrun_end(timeout)
229
+ self.results = self._client.get_test_run_full_status(self._run_id)
230
+ self._reporter = StepReporter(self._client, self._run_id, self.url)
231
+
232
+ def _wait_until_testrun_end(self, timeout: float):
233
+ """Wait until the test run reaches a final status.
234
+
235
+ Polls the run status every :data:`POLL_INTERVAL_SECONDS` seconds until it leaves the :data:`PENDING_STATUSES`.
236
+
237
+ :param timeout: Maximum time, in seconds, to wait for the run to finish.
238
+
239
+ :raises TimeoutError: If the run is still pending once ``timeout`` elapses.
240
+ """
241
+ deadline = time.monotonic() + timeout
242
+ while True:
243
+ status = self._client.get_test_run_status(self._run_id).get("status")
244
+ logger.debug(f"Current run status: {status}")
245
+ if status not in PENDING_STATUSES:
246
+ return
247
+ if time.monotonic() >= deadline:
248
+ msg = f"Lynqa test run {self._run_id} did not complete within {timeout}s"
249
+ raise TimeoutError(msg)
250
+ time.sleep(POLL_INTERVAL_SECONDS)
251
+
252
+ def _report_test_results(self, result):
253
+ """Report the test results from the Lynqa API.
254
+
255
+ Sets the test status to ``PASS`` when the run succeeded and ``FAIL`` otherwise, and copies the run's status
256
+ message onto the result.
257
+
258
+ :param result: Test result object provided by Robot Framework.
259
+ """
260
+ if self.results.get("status") == "success":
261
+ result.status = "PASS"
262
+ else:
263
+ result.status = "FAIL"
264
+
265
+ result.message = self.results.get("statusMessage")
266
+
267
+ # ------------------------------------------------------------------
268
+ # Keywords exposed to Robot Framework
269
+ # ------------------------------------------------------------------
270
+
271
+ @keyword
272
+ def given(self, text: str) -> None:
273
+ """Create a ``Given`` step in the current scenario.
274
+
275
+ :param text: Natural-language description of the step.
276
+ """
277
+
278
+ @keyword
279
+ def when(self, text: str) -> None:
280
+ """Create a ``When`` step in the current scenario.
281
+
282
+ :param text: Natural-language description of the step.
283
+ """
284
+
285
+ @keyword
286
+ def then(self, text: str) -> None:
287
+ """Create a ``Then`` step in the current scenario.
288
+
289
+ :param text: Natural-language description of the step.
290
+ """
291
+
292
+ # ------------------------------------------------------------------
293
+ # Helpers
294
+ # ------------------------------------------------------------------
295
+
296
+ @staticmethod
297
+ def _search_variable(variable_name) -> object:
298
+ """Return the value of a Robot Framework variable.
299
+
300
+ :param variable_name: Name of the variable to look up, e.g. ``${LYNQA_URL}``.
301
+
302
+ :returns: The variable's value, or ``None`` when it is not set.
303
+ """
304
+ return BuiltIn().get_variable_value(variable_name)
305
+
306
+ def _search_secrets_variables(self) -> list[TestData]:
307
+ """Convert the secrets variable into a list of :class:`TestData`.
308
+
309
+ The ``&{LYNQA_SECRETS}`` variable is expected to be a mapping of secret name to value, e.g. ``login=superu
310
+ password=TrèsS3cr3t``. Each entry is turned into a :class:`TestData` name/value pair.
311
+
312
+ :returns: The secrets as :class:`TestData` items, or an empty list if unset.
313
+
314
+ :raises TypeError: If the secrets variable is not a name/value mapping.
315
+ """
316
+ raw_secrets = self._search_variable(SECRETS_VAR)
317
+ if not raw_secrets:
318
+ return []
319
+ if not isinstance(raw_secrets, dict):
320
+ raise TypeError("Invalid secrets variable, expected a dict") # ruff: ignore[raise-vanilla-args]
321
+ return [TestData(name=str(name), value=str(value)) for name, value in raw_secrets.items()]
322
+
323
+ def _search_context(self) -> TestRunContext:
324
+ """Build the Lynqa run context from the ``LYNQA_*`` Robot Framework variables.
325
+
326
+ Reads the optional ``${LYNQA_LANGUAGE}`` and ``${LYNQA_DATETIME}`` variables and the ``&{LYNQA_SECRETS}``
327
+ mapping. When ``${LYNQA_DATETIME}`` is not set, the current local date and time is used.
328
+
329
+ :returns: The context passed to the Lynqa test run.
330
+ """
331
+ client_language = self._search_variable(LANGUAGE_VAR)
332
+ client_datetime = self._search_variable(DATETIME_VAR)
333
+ if client_datetime is None:
334
+ client_datetime = datetime.now().astimezone().strftime("%a %b %d %Y %H:%M:%S GMT%z")
335
+ secrets = self._search_secrets_variables()
336
+ return TestRunContext(
337
+ client_language=str(client_language), client_datetime=str(client_datetime), secrets=secrets
338
+ )
@@ -0,0 +1,150 @@
1
+ """Robot Framework log presentation for Lynqa test runs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from robot.api import logger
8
+
9
+ if TYPE_CHECKING:
10
+ from pylynqa import LynqaClient
11
+
12
+
13
+ class StepReporter:
14
+ """Render the Robot log for one Lynqa test run step.
15
+
16
+ Holds the run data needed to embed screenshots and caption commands and assertions, and exposes :meth:`log_step` to
17
+ log a single step's details under the current keyword.
18
+ """
19
+
20
+ def __init__(self, client: LynqaClient, run_id: str, url: str | None) -> None:
21
+ """Initialize the reporter for a single run.
22
+
23
+ :param client: Lynqa client used to fetch screenshots.
24
+ :param run_id: Identifier of the Lynqa test run.
25
+ :param url: URL the run was started against, shown with the initial screenshot.
26
+ """
27
+ self._client = client
28
+ self._run_id = run_id
29
+ self._url = url
30
+
31
+ def log_step(self, step_index: int, results: dict) -> None:
32
+ """Log the commands and assertions of a Gherkin step.
33
+
34
+ Logs a ``Command`` list with one log per command and an ``Assertion`` list with one log per assertion, each
35
+ followed by its screenshots if any. The initial screenshot captured before the run is embedded once, ahead of
36
+ the first step's commands. The step is the one at ``step_index`` in the run's ``stepStatuses``.
37
+
38
+ :param step_index: Index of the step to log, in execution order.
39
+ :param results: Full status of the run, as returned by the Lynqa API.
40
+ """
41
+ steps = results.get("stepStatuses", [])
42
+ if step_index >= len(steps):
43
+ return
44
+ step = steps[step_index]
45
+
46
+ if step_index == 0:
47
+ self._log_initial_report(results)
48
+ self._log_step_commands(step)
49
+ self._log_step_assertions(step)
50
+
51
+ def _log_initial_report(self, results) -> None:
52
+ """Log the initial report (global info).
53
+
54
+ :param results: Full status of the run, as returned by the Lynqa API.
55
+ """
56
+ initial_report = results.get("initialReport", {})
57
+ logger.info(self._embed_screenshot(initial_report.get("screenshot"), f"Open URL {self._url}"), html=True)
58
+
59
+ def _log_step_commands(self, step: dict) -> None:
60
+ """Log the current step's commands and embed their screenshots.
61
+
62
+ :param step: The step from the run's ``stepStatuses`` to log.
63
+ """
64
+
65
+ def _is_response_success(command):
66
+ return "success" in command.get("response", {})
67
+
68
+ for command in step.get("commands", []):
69
+ level = "INFO" if _is_response_success(command) else "ERROR"
70
+ logger.write(
71
+ self._embed_screenshot(command.get("screenshot"), self._command_caption(command)),
72
+ level=level,
73
+ html=True,
74
+ )
75
+
76
+ @staticmethod
77
+ def _command_caption(command: dict) -> str:
78
+ """Build a human-readable caption for a command's screenshot.
79
+
80
+ :param command: A command entry from a step's ``commands`` list.
81
+
82
+ :returns: The command name enriched with available information.
83
+ """
84
+ caption = f"Command: {command.get('name', '')}"
85
+ if "value" in command:
86
+ caption += f"(value: {command['value']})"
87
+ elif "button" in command:
88
+ caption += f"(button: {command['button']})"
89
+ if command.get("htmlElement"):
90
+ caption += f' on element "{command["htmlElement"]}"'
91
+ return str(caption)
92
+
93
+ def _log_step_assertions(self, step: dict) -> None:
94
+ """Log the current step's assertions and embed the assertions report screenshot.
95
+
96
+ :param step: The step from the run's ``stepStatuses`` to log.
97
+ """
98
+
99
+ def _is_assertion_checked(assertion):
100
+ return assertion.get("checked", False)
101
+
102
+ report = step.get("assertionsReport", {})
103
+ assertions = report.get("assertions", [])
104
+
105
+ for assertion in assertions:
106
+ level = "INFO" if _is_assertion_checked(assertion) else "ERROR"
107
+ logger.write(
108
+ self._assertion_caption(assertion),
109
+ level=level,
110
+ html=True,
111
+ )
112
+ if "testVerdictCause" in step:
113
+ logger.error(f"Verdict: {step['testVerdictCause']}")
114
+
115
+ logger.info(self._embed_screenshot(report.get("screenshot"), ""), html=True)
116
+
117
+ @staticmethod
118
+ def _assertion_caption(assertion):
119
+ """Build a human-readable caption for an assertion.
120
+
121
+ :param assertion: An assertion entry from a step's ``assertionsReport`` assertions list.
122
+
123
+ :returns: The assertion text enriched with available information.
124
+ """
125
+ caption = f'Assertion: "{assertion.get("assertion", "")}"'
126
+ return str(caption)
127
+
128
+ def _embed_screenshot(self, screenshot_id: str | None, caption: str) -> str:
129
+ """Fetch a Lynqa screenshot and return it embedded inline in an HTML string.
130
+
131
+ Downloads the screenshot as base64-encoded PNG data and wraps it in an ``<img>`` inside a collapsible
132
+ ``<details>`` block. A download failure is logged and caught, so it never fails the test run.
133
+
134
+ :param screenshot_id: UUID of the screenshot to fetch, or ``None`` when the step has none.
135
+ :param caption: Text shown above the embedded image.
136
+
137
+ :returns: The caption and embedded image as an HTML string, or an empty string when there is no screenshot or
138
+ the download failed.
139
+ """
140
+ if not screenshot_id:
141
+ return ""
142
+ try:
143
+ data = self._client.get_screenshot(self._run_id, screenshot_id)
144
+ except Exception as error:
145
+ logger.error(f"Failed to fetch Lynqa screenshot {screenshot_id}: {error}")
146
+ return ""
147
+ return (
148
+ f'{caption}<details><summary>screenshot</summary><img src="data:image/png;base64,{data}"'
149
+ + 'style="max-width: 100%;"/></details>'
150
+ )
@@ -0,0 +1,28 @@
1
+ """Unit tests for robotframework_lynqa."""
2
+
3
+ from robot.api.interfaces import ListenerV3
4
+
5
+ # ruff: file-ignore[non-empty-init-module]
6
+ from robot.libraries.BuiltIn import BuiltIn
7
+
8
+ API_KEY = "fake_api_key"
9
+
10
+
11
+ class _CaptureLibInstance(ListenerV3):
12
+ """Listener that captures the live LynqaLibrary instance during the run.
13
+
14
+ The ``.robot`` file owns the :class:`LynqaLibrary` instance (it is created by the ``Library`` import), so the test
15
+ cannot reference it directly. This listener grabs that instance from the running context before the test ends.
16
+ """
17
+
18
+ def __init__(self) -> None:
19
+ """Initialize the capture listener."""
20
+ self.instance = None
21
+
22
+ def end_test(self, data, result) -> None:
23
+ """Capture the library instance while the execution context is alive.
24
+
25
+ :param data: Running test data passed by Robot Framework.
26
+ :param result: Test result object passed by Robot Framework.
27
+ """
28
+ self.instance = BuiltIn().get_library_instance("robotframework_lynqa.LynqaLibrary")
@@ -0,0 +1,29 @@
1
+ """Pytest configuration for tests."""
2
+
3
+ import base64
4
+ from pathlib import Path
5
+
6
+ import pytest
7
+ from pylynqa.models import TestData, TestRunContext
8
+
9
+ SCREENSHOT_PNG_FILE = Path(__file__).parent / "data_set" / "screenshot.png"
10
+
11
+
12
+ def pytest_configure(config):
13
+ """Pytest configuration tests."""
14
+ # Prevent pytest from collecting these model classes as test suites
15
+ # (their names start with "Test" but they are domain objects, not test cases).
16
+ TestData.__test__ = False # ty: ignore[unresolved-attribute]
17
+ TestRunContext.__test__ = False # ty: ignore[unresolved-attribute]
18
+
19
+
20
+ @pytest.fixture
21
+ def screenshot_base64():
22
+ """Return the sample screenshot as base64-encoded PNG data.
23
+
24
+ Shared by the library integration tests and the reporter unit tests as the fake payload returned by the mocked
25
+ ``get_screenshot`` client call.
26
+
27
+ :returns: Base64-encoded contents of the sample PNG fixture.
28
+ """
29
+ return base64.b64encode(SCREENSHOT_PNG_FILE.read_bytes()).decode()
@@ -0,0 +1,17 @@
1
+ *** Settings ***
2
+ Library robotframework_lynqa.LynqaLibrary api_key=%{LYNQA_API_KEY}
3
+
4
+
5
+ *** Variables ***
6
+ ${LYNQA_URL} https://www.super-u.ai
7
+ ${LYNQA_LANGUAGE} rf-RF
8
+ &{LYNQA_SECRETS} login=superu password=TrèsS3cr3t
9
+
10
+
11
+ *** Test Cases ***
12
+ Browsing And Buying An AI Agent
13
+ Given the storefront is open at the home page
14
+ When the user searches for an autonomous testing agent
15
+ When the user filters the results to top-rated agents under 50 credits
16
+ Then a list of matching AI agents is displayed with their prices
17
+ Then the premium "Bobcat" agent is sold out because its success
@@ -0,0 +1,17 @@
1
+ *** Settings ***
2
+ Library DateTime
3
+ Library robotframework_lynqa.LynqaLibrary api_key=%{LYNQA_API_KEY}
4
+
5
+
6
+ *** Variables ***
7
+ ${LYNQA_URL} https://www.super-u.ai
8
+ ${LYNQA_LANGUAGE} rf-RF
9
+ ${LYNQA_DATETIME} Wed May 8 2026 09:00:00 GMT+0200
10
+ &{LYNQA_SECRETS} login=superu password=TrèsS3cr3t
11
+
12
+
13
+ *** Test Cases ***
14
+ Browsing And Buying An AI Agent With Date
15
+ Given the storefront is open at the home page
16
+ When the user searches for an autonomous testing agent
17
+ Then a list of matching AI agents is displayed with their prices
@@ -0,0 +1,15 @@
1
+ *** Settings ***
2
+ Library robotframework_lynqa.LynqaLibrary api_key=%{LYNQA_API_KEY}
3
+
4
+
5
+ *** Variables ***
6
+ ${LYNQA_URL} https://www.google.com/
7
+
8
+
9
+ *** Test Cases ***
10
+ Search Engine
11
+ Given go to the website
12
+ When I look at the search input
13
+ Then the search input exists
14
+ When I search for 'lynqa'
15
+ Then several results are displayed and the first results display 'Smartesting'
@@ -0,0 +1,16 @@
1
+ *** Settings ***
2
+ Library DateTime
3
+ Library robotframework_lynqa.LynqaLibrary api_key=%{LYNQA_API_KEY}
4
+
5
+
6
+ *** Variables ***
7
+ ${LYNQA_LANGUAGE} rf-RF
8
+ ${LYNQA_DATETIME} Wed May 8 2026 09:00:00 GMT+0200
9
+ &{LYNQA_SECRETS} login=superu password=TrèsS3cr3t
10
+
11
+
12
+ *** Test Cases ***
13
+ Browsing And Buying An AI Agent Without URL
14
+ Given the storefront is open at the home page
15
+ When the user searches for an autonomous testing agent
16
+ Then a list of matching AI agents is displayed with their prices