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,129 @@
1
+ """Dataset: Test run full status response."""
2
+
3
+ TEST_RUN_FULL_STATUS_SUCCESS_RESPONSE = {
4
+ "createdAt": "2026-06-12T13:58:24.944Z",
5
+ "start": "2026-06-12T13:58:28.693Z",
6
+ "end": "2026-06-12T14:00:36.029Z",
7
+ "status": "success",
8
+ "statusMessage": "OK",
9
+ "stepStatuses": [
10
+ {
11
+ "end": "2026-06-12T13:59:07.365Z",
12
+ "start": "2026-06-12T13:58:53.483Z",
13
+ "status": "success",
14
+ "commands": [
15
+ {
16
+ "name": "scroll",
17
+ "response": {"success": {"delta": 500, "direction": "down"}},
18
+ "screenshot": "3f85e362-d50e-4521-8447-27427110cf5f",
19
+ },
20
+ {
21
+ "name": "click",
22
+ "button": "left",
23
+ "response": {"success": {"newUrl": None}},
24
+ "screenshot": "4e63b6a6-b337-4642-a872-91f20a4ab919",
25
+ "htmlElement": '"Tout refuser button"',
26
+ },
27
+ ],
28
+ "assertionsReport": {
29
+ "assertions": [
30
+ {"checked": True, "assertion": "The browser is navigated to https://www.google.com/"},
31
+ {"checked": True, "assertion": "The Google homepage is fully loaded with the search input visible"},
32
+ {"checked": True, "assertion": "The cookie consent popup is dismissed"},
33
+ ],
34
+ "screenshot": "1be40c8c-dbb2-466e-ab1b-a5155a531fc4",
35
+ },
36
+ },
37
+ {
38
+ "end": "2026-06-12T13:59:28.493Z",
39
+ "start": "2026-06-12T13:59:28.493Z",
40
+ "status": "success",
41
+ "commands": [],
42
+ "assertionsReport": {
43
+ "assertions": [
44
+ {"checked": True, "assertion": "The browser is navigated to https://www.google.com/"},
45
+ {
46
+ "checked": True,
47
+ "assertion": "The cookie consent popup is dismissed and the Google homepage is fully accessible", # ruff: ignore[line-too-long]
48
+ },
49
+ {"checked": True, "assertion": "The search input is visible and accessible on the Google homepage"},
50
+ ],
51
+ "screenshot": "cce0c8f8-e96a-48f2-8b35-ffd41ec979dd",
52
+ },
53
+ },
54
+ {
55
+ "end": "2026-06-12T13:59:40.504Z",
56
+ "start": "2026-06-12T13:59:40.504Z",
57
+ "status": "success",
58
+ "commands": [],
59
+ "assertionsReport": {
60
+ "assertions": [
61
+ {"checked": True, "assertion": "The search input exists and is visible on the Google homepage"}
62
+ ],
63
+ "screenshot": "296ad2a5-4cea-44a8-8af6-fbf62ce1aeaf",
64
+ },
65
+ },
66
+ {
67
+ "end": "2026-06-12T14:00:16.258Z",
68
+ "start": "2026-06-12T14:00:00.401Z",
69
+ "status": "success",
70
+ "commands": [
71
+ {
72
+ "name": "click",
73
+ "button": "left",
74
+ "response": {"success": {"newUrl": None}},
75
+ "screenshot": "62a28ce4-f106-4b70-94f7-ea640d55fcdc",
76
+ "htmlElement": "Search input field",
77
+ },
78
+ {
79
+ "name": "type",
80
+ "value": "lynqa",
81
+ "response": {"success": True},
82
+ "screenshot": "caeaa3c5-e37d-4460-a1ed-c53d81406617",
83
+ },
84
+ {
85
+ "name": "pressKey",
86
+ "value": "Return",
87
+ "response": {
88
+ "success": {
89
+ "newUrl": "https://www.google.com/search?q=lynqa&sca_esv=2aa300c80f7be1f4&source=hp&ei=gxAsaqTHMqq4kdUP1ciGmAY&iflsig=AFdpzrgAAAAAaiwek3wNXsJiALMoqmxWYThTRkSuPVDQ&ved=0ahUKEwjklP737YGVAxUqXKQEHVWkAWMQ4dUDCCE&uact=5&oq=lynqa&gs_lp=Egdnd3Mtd2l6IgVseW5xYTIMEC4YgAQYChgLGLEDMg8QLhiABBgKGAsYsQMYgwEyDBAuGAoYCxixAxiABDIMEC4YChgLGLEDGIAEMgkQLhiABBgKGAsyCRAuGIAEGAoYCzIJEAAYgAQYChgLMgkQABiABBgKGAsyCRAuGIAEGAoYCzIJEC4YgAQYChgLSJtnUMRAWJlBcAF4AJABAJgBM6AB1gGqAQE1uAEDyAEA-AEBmAIGoAKIAqgCCsICChAuGAMYjwEY6gLCAgoQABgDGI8BGOoCwgILEAAYgAQYsQMYgwHCAggQABiABBixA8ICCBAuGIAEGLEDwgIREC4YgAQYsQMYgwEYxwEY0QPCAg4QLhiABBixAxjHARjRA8ICBRAuGIAEwgIOEC4YgAQYigUYsQMYgwHCAg4QABiABBiKBRixAxiDAcICBhAAGAMYCsICBBAAGAPCAgUQABiABMICBxAAGIAEGAqYAw3xBaK8RTujFJwPkgcBNqAH50ayBwE1uAf7AcIHBTItNC4yyAcogAgB&sclient=gws-wiz&sei=3hAsauOqNoqbkdUP-YX-qAw"
90
+ }
91
+ },
92
+ "screenshot": "d7a51561-7ac9-4a55-bdf6-b809c5287a69",
93
+ "keysSequence": ["Return"],
94
+ },
95
+ ],
96
+ "assertionsReport": {
97
+ "assertions": [
98
+ {"checked": True, "assertion": "Search for 'lynqa' is executed and results page is displayed"},
99
+ {"checked": True, "assertion": "Search results include 'Smartesting' in the first results"},
100
+ ],
101
+ "screenshot": "3b78845d-41e8-43ba-abe2-b006e8735be2",
102
+ },
103
+ },
104
+ {
105
+ "end": "2026-06-12T14:00:35.981Z",
106
+ "start": "2026-06-12T14:00:35.981Z",
107
+ "status": "success",
108
+ "commands": [],
109
+ "assertionsReport": {
110
+ "assertions": [
111
+ {"checked": True, "assertion": "The search query 'lynqa' is entered and results are displayed"},
112
+ {"checked": True, "assertion": "Several search results are displayed for the query 'lynqa'"},
113
+ {"checked": True, "assertion": "The first result displays 'Smartesting' as the source"},
114
+ {"checked": True, "assertion": "The first results (plural) display 'Smartesting'"},
115
+ ],
116
+ "screenshot": "d9b611e0-955d-4b69-a51b-58ed40d88fb3",
117
+ },
118
+ },
119
+ ],
120
+ "initialReport": {"screenshot": "b276c303-d612-4c25-a2cd-18d42b8b4930", "architecture": "omeron"},
121
+ "type": "gherkin",
122
+ "steps": [
123
+ {"keyword": "Given", "text": "go to the website"},
124
+ {"keyword": "When", "text": "I look at the search input"},
125
+ {"keyword": "Then", "text": "the search input exists"},
126
+ {"keyword": "When", "text": "I search for 'lynqa'"},
127
+ {"keyword": "Then", "text": "several results are displayed and the first results display 'Smartesting'"},
128
+ ],
129
+ }
@@ -0,0 +1,130 @@
1
+ """Dataset: Test run full status response."""
2
+
3
+ TEST_RUN_FULL_STATUS_FAILURE_RESPONSE = {
4
+ "createdAt": "2026-06-12T13:58:24.944Z",
5
+ "start": "2026-06-12T13:58:28.693Z",
6
+ "end": "2026-06-12T14:00:36.029Z",
7
+ "status": "failed",
8
+ "statusMessage": "KO",
9
+ "stepStatuses": [
10
+ {
11
+ "end": "2026-06-12T13:59:07.365Z",
12
+ "start": "2026-06-12T13:58:53.483Z",
13
+ "status": "success",
14
+ "commands": [
15
+ {
16
+ "name": "scroll",
17
+ "response": {"success": {"delta": 500, "direction": "down"}},
18
+ "screenshot": "3f85e362-d50e-4521-8447-27427110cf5f",
19
+ },
20
+ {
21
+ "name": "click",
22
+ "button": "left",
23
+ "response": {"success": {"newUrl": None}},
24
+ "screenshot": "4e63b6a6-b337-4642-a872-91f20a4ab919",
25
+ "htmlElement": '"Tout refuser button"',
26
+ },
27
+ ],
28
+ "assertionsReport": {
29
+ "assertions": [
30
+ {"checked": True, "assertion": "The browser is navigated to https://www.google.com/"},
31
+ {"checked": True, "assertion": "The Google homepage is fully loaded with the search input visible"},
32
+ {"checked": True, "assertion": "The cookie consent popup is dismissed"},
33
+ ],
34
+ "screenshot": "1be40c8c-dbb2-466e-ab1b-a5155a531fc4",
35
+ },
36
+ },
37
+ {
38
+ "end": "2026-06-12T13:59:28.493Z",
39
+ "start": "2026-06-12T13:59:28.493Z",
40
+ "status": "success",
41
+ "commands": [],
42
+ "assertionsReport": {
43
+ "assertions": [
44
+ {"checked": True, "assertion": "The browser is navigated to https://www.google.com/"},
45
+ {
46
+ "checked": True,
47
+ "assertion": "The cookie consent popup is dismissed and the Google homepage is fully accessible", # ruff: ignore[line-too-long]
48
+ },
49
+ {"checked": True, "assertion": "The search input is visible and accessible on the Google homepage"},
50
+ ],
51
+ "screenshot": "cce0c8f8-e96a-48f2-8b35-ffd41ec979dd",
52
+ },
53
+ },
54
+ {
55
+ "end": "2026-06-12T13:59:40.504Z",
56
+ "start": "2026-06-12T13:59:40.504Z",
57
+ "status": "failed",
58
+ "commands": [],
59
+ "assertionsReport": {
60
+ "assertions": [
61
+ {"checked": True, "assertion": "The search input exists and is visible on the Google homepage"}
62
+ ],
63
+ "screenshot": "296ad2a5-4cea-44a8-8af6-fbf62ce1aeaf",
64
+ },
65
+ },
66
+ {
67
+ "end": "2026-06-12T14:00:16.258Z",
68
+ "start": "2026-06-12T14:00:00.401Z",
69
+ "status": "success",
70
+ "commands": [
71
+ {
72
+ "name": "click",
73
+ "button": "left",
74
+ "response": {"success": {"newUrl": None}},
75
+ "screenshot": "62a28ce4-f106-4b70-94f7-ea640d55fcdc",
76
+ "htmlElement": "Search input field",
77
+ },
78
+ {
79
+ "name": "type",
80
+ "value": "lynqa",
81
+ "response": {"success": True},
82
+ "screenshot": "caeaa3c5-e37d-4460-a1ed-c53d81406617",
83
+ },
84
+ {
85
+ "name": "pressKey",
86
+ "value": "Return",
87
+ "response": {
88
+ "success": {
89
+ "newUrl": "https://www.google.com/search?q=lynqa&sca_esv=2aa300c80f7be1f4&source=hp&ei=gxAsaqTHMqq4kdUP1ciGmAY&iflsig=AFdpzrgAAAAAaiwek3wNXsJiALMoqmxWYThTRkSuPVDQ&ved=0ahUKEwjklP737YGVAxUqXKQEHVWkAWMQ4dUDCCE&uact=5&oq=lynqa&gs_lp=Egdnd3Mtd2l6IgVseW5xYTIMEC4YgAQYChgLGLEDMg8QLhiABBgKGAsYsQMYgwEyDBAuGAoYCxixAxiABDIMEC4YChgLGLEDGIAEMgkQLhiABBgKGAsyCRAuGIAEGAoYCzIJEAAYgAQYChgLMgkQABiABBgKGAsyCRAuGIAEGAoYCzIJEC4YgAQYChgLSJtnUMRAWJlBcAF4AJABAJgBM6AB1gGqAQE1uAEDyAEA-AEBmAIGoAKIAqgCCsICChAuGAMYjwEY6gLCAgoQABgDGI8BGOoCwgILEAAYgAQYsQMYgwHCAggQABiABBixA8ICCBAuGIAEGLEDwgIREC4YgAQYsQMYgwEYxwEY0QPCAg4QLhiABBixAxjHARjRA8ICBRAuGIAEwgIOEC4YgAQYigUYsQMYgwHCAg4QABiABBiKBRixAxiDAcICBhAAGAMYCsICBBAAGAPCAgUQABiABMICBxAAGIAEGAqYAw3xBaK8RTujFJwPkgcBNqAH50ayBwE1uAf7AcIHBTItNC4yyAcogAgB&sclient=gws-wiz&sei=3hAsauOqNoqbkdUP-YX-qAw"
90
+ }
91
+ },
92
+ "screenshot": "d7a51561-7ac9-4a55-bdf6-b809c5287a69",
93
+ "keysSequence": ["Return"],
94
+ },
95
+ ],
96
+ "assertionsReport": {
97
+ "assertions": [
98
+ {"checked": True, "assertion": "Search for 'lynqa' is executed and results page is displayed"},
99
+ {"checked": True, "assertion": "Search results include 'Smartesting' in the first results"},
100
+ ],
101
+ "screenshot": "3b78845d-41e8-43ba-abe2-b006e8735be2",
102
+ },
103
+ },
104
+ {
105
+ "end": "2026-06-12T14:00:35.981Z",
106
+ "start": "2026-06-12T14:00:35.981Z",
107
+ "status": "failed",
108
+ "commands": [],
109
+ "assertionsReport": {
110
+ "assertions": [
111
+ {"checked": True, "assertion": "The search query 'lynqa' is entered and results are displayed"},
112
+ {"checked": True, "assertion": "Several search results are displayed for the query 'lynqa'"},
113
+ {"checked": True, "assertion": "The first result displays 'Smartesting' as the source"},
114
+ {"checked": True, "assertion": "The first results (plural) display 'Smartesting'"},
115
+ ],
116
+ "screenshot": "d9b611e0-955d-4b69-a51b-58ed40d88fb3",
117
+ },
118
+ "testVerdictCause": "An error occurred while executing the test",
119
+ },
120
+ ],
121
+ "initialReport": {"screenshot": "b276c303-d612-4c25-a2cd-18d42b8b4930", "architecture": "omeron"},
122
+ "type": "gherkin",
123
+ "steps": [
124
+ {"keyword": "Given", "text": "go to the website"},
125
+ {"keyword": "When", "text": "I look at the search input"},
126
+ {"keyword": "Then", "text": "the search input exists"},
127
+ {"keyword": "When", "text": "I search for 'lynqa'"},
128
+ {"keyword": "Then", "text": "several results are displayed and the first results display 'Smartesting'"},
129
+ ],
130
+ }
@@ -0,0 +1,203 @@
1
+ """Unit tests for robotframework_lynqa."""
2
+
3
+ from datetime import datetime, timedelta, timezone
4
+ from pathlib import Path
5
+
6
+ import pytest
7
+ import robot
8
+ from pylynqa import TestData, TestRunContext
9
+ from robot.result import ExecutionResult, Keyword
10
+
11
+ from robotframework_lynqa.library import LynqaLibrary
12
+ from robotframework_lynqa.test import API_KEY, _CaptureLibInstance
13
+ from robotframework_lynqa.test.data_set.testrun_full_status import TEST_RUN_FULL_STATUS_SUCCESS_RESPONSE
14
+ from robotframework_lynqa.test.data_set.testrun_full_status_failure import TEST_RUN_FULL_STATUS_FAILURE_RESPONSE
15
+ from robotframework_lynqa.test.test_reporter import EXPECTED_SCREENSHOT_COUNT
16
+
17
+ DATA_SET_FOLDER = Path(__file__).parent / "data_set"
18
+ TEST_LYNQA_ROBOT_FILE = DATA_SET_FOLDER / "test_lynqa_nominal.robot"
19
+ TEST_LYNQA_WITH_DATE_ROBOT_FILE = DATA_SET_FOLDER / "test_lynqa_with_date.robot"
20
+ TEST_LYNQA_WITHOUT_URL_ROBOT_FILE = DATA_SET_FOLDER / "test_lynqa_without_url.robot"
21
+ TEST_LYNQA_WITH_RESULTS_ROBOT_FILE = DATA_SET_FOLDER / "test_lynqa_with_results.robot"
22
+
23
+ EXPECTED_SCENARIO = (
24
+ "Given the storefront is open at the home page\n"
25
+ "When the user searches for an autonomous testing agent\n"
26
+ "When the user filters the results to top-rated agents under 50 credits\n"
27
+ "Then a list of matching AI agents is displayed with their prices\n"
28
+ 'Then the premium "Bobcat" agent is sold out because its success'
29
+ )
30
+
31
+
32
+ @pytest.fixture
33
+ def client(mocker, screenshot_base64):
34
+ """Mock the Lynqa HTTP client so no real request is sent.
35
+
36
+ Patches :class:`LynqaClient` in the library module: every ``LynqaLibrary`` created during the run gets this mock as
37
+ its ``_client``. The run is reported as an immediately successful one.
38
+
39
+ :returns: The mock client instance shared by every ``LynqaLibrary``.
40
+ """
41
+ client = mocker.patch("robotframework_lynqa.library.LynqaClient").return_value
42
+ client.add_gherkin_test_run.return_value = 123456
43
+ client.get_test_run_status.return_value = {"status": "success"}
44
+ client.get_test_run_full_status.return_value = {"status": "success", "statusMessage": "OK"}
45
+ client.get_screenshot.return_value = screenshot_base64
46
+ return client
47
+
48
+
49
+ @pytest.fixture
50
+ def listener_probe(monkeypatch, mocker, client):
51
+ """Initialize the listener probe used to capture the library instance."""
52
+ monkeypatch.setenv("LYNQA_API_KEY", API_KEY)
53
+ mock_datetime = mocker.patch("robotframework_lynqa.library.datetime")
54
+ fixed_now = datetime(2026, 6, 24, 9, 52, 0, tzinfo=timezone(timedelta(hours=2)))
55
+ mock_datetime.now.return_value.astimezone.return_value = fixed_now
56
+ return _CaptureLibInstance()
57
+
58
+
59
+ @pytest.fixture
60
+ def library(client):
61
+ """Build a ``LynqaLibrary`` instance wired to the mocked client for unit tests.
62
+
63
+ :returns: A library whose ``_client`` is the shared mock from ``client``.
64
+ """
65
+ return LynqaLibrary(api_key=API_KEY)
66
+
67
+
68
+ def test_nominal_scenario(tmp_path, listener_probe):
69
+ """Test that the scenario is executed."""
70
+ # Act
71
+ rc = robot.run(str(TEST_LYNQA_ROBOT_FILE), outputdir=str(tmp_path), listener=listener_probe)
72
+
73
+ # Assert
74
+ listener_inst = listener_probe.instance
75
+ assert rc == 0
76
+ assert listener_inst is not None
77
+ assert listener_inst.scenario == EXPECTED_SCENARIO
78
+ assert listener_inst.url == "https://www.super-u.ai"
79
+ assert listener_inst.context == TestRunContext(
80
+ client_language="rf-RF",
81
+ client_datetime="Wed Jun 24 2026 09:52:00 GMT+0200",
82
+ secrets=[TestData(name="login", value="superu"), TestData(name="password", value="TrèsS3cr3t")],
83
+ )
84
+
85
+
86
+ def test_variable_with_date(tmp_path, listener_probe):
87
+ """Test that the scenario is executed with a provided date."""
88
+ # Act
89
+ rc = robot.run(str(TEST_LYNQA_WITH_DATE_ROBOT_FILE), outputdir=str(tmp_path), listener=listener_probe)
90
+
91
+ # Assert
92
+ listener_inst = listener_probe.instance
93
+ assert rc == 0
94
+ assert listener_inst.context == TestRunContext(
95
+ client_language="rf-RF",
96
+ client_datetime="Wed May 8 2026 09:00:00 GMT+0200",
97
+ secrets=[TestData(name="login", value="superu"), TestData(name="password", value="TrèsS3cr3t")],
98
+ )
99
+
100
+
101
+ def test_variable_without_url(tmp_path, listener_probe):
102
+ """Test that the execution fails if URL is missing."""
103
+ # Act
104
+ rc = robot.run(str(TEST_LYNQA_WITHOUT_URL_ROBOT_FILE), outputdir=str(tmp_path), listener=listener_probe)
105
+
106
+ # Assert
107
+ assert rc > 0
108
+
109
+
110
+ def test_scenario_result(tmp_path, listener_probe, client):
111
+ """Test that the scenario result is returned."""
112
+ # Arrange
113
+ client.get_test_run_full_status.return_value = TEST_RUN_FULL_STATUS_SUCCESS_RESPONSE
114
+ # Act
115
+ rc = robot.run(str(TEST_LYNQA_WITH_RESULTS_ROBOT_FILE), outputdir=str(tmp_path), listener=listener_probe)
116
+
117
+ # Assert
118
+ assert rc == 0
119
+ robot_result = ExecutionResult(str(tmp_path / "output.xml"))
120
+ # Assert Test status
121
+ test = robot_result.suite.tests[0]
122
+ assert test.status == "PASS"
123
+ assert test.message == "OK"
124
+ # Assert Keywords status
125
+ keywords = [elt for elt in test.body if isinstance(elt, Keyword)]
126
+ for keyword in keywords:
127
+ assert keyword.status == "PASS"
128
+ assert len(keywords[0].messages) > 1
129
+ # Assert Screenshots presence
130
+ embedded_images = [
131
+ message for keyword in keywords for message in keyword.messages if message.html and "<img" in message.message
132
+ ]
133
+ assert len(embedded_images) == EXPECTED_SCREENSHOT_COUNT
134
+
135
+
136
+ def test_scenario_result_failure(tmp_path, listener_probe, client):
137
+ """Test that the scenario result is failed."""
138
+ # Arrange
139
+ client.get_test_run_full_status.return_value = TEST_RUN_FULL_STATUS_FAILURE_RESPONSE
140
+
141
+ # Act
142
+ rc = robot.run(str(TEST_LYNQA_WITH_RESULTS_ROBOT_FILE), outputdir=str(tmp_path), listener=listener_probe)
143
+
144
+ # Assert
145
+ assert rc > 0
146
+ robot_result = ExecutionResult(str(tmp_path / "output.xml"))
147
+ # Assert Test status
148
+ test = robot_result.suite.tests[0]
149
+ assert test.status == "FAIL"
150
+ assert test.message == "KO"
151
+ # Assert Keywords status
152
+ keywords = [elt for elt in test.body if isinstance(elt, Keyword)]
153
+ keywords_status = [keyword.status for keyword in keywords]
154
+ assert keywords_status == ["PASS", "PASS", "FAIL", "PASS", "FAIL"]
155
+ assert len(keywords[0].messages) > 1
156
+
157
+
158
+ def test_wait_until_testrun_end_polls_until_complete(library, client, mocker):
159
+ """Polling continues while the run is pending, then stops on a final status."""
160
+ # Arrange
161
+ mocker.patch("robotframework_lynqa.library.time.sleep")
162
+ statuses = [
163
+ {"status": "running"},
164
+ {"status": "running"},
165
+ {"status": "success"},
166
+ ]
167
+ client.get_test_run_status.side_effect = statuses
168
+
169
+ # Act
170
+ library._wait_until_testrun_end(timeout=100)
171
+
172
+ # Assert
173
+ assert client.get_test_run_status.call_count == len(statuses)
174
+
175
+
176
+ def test_wait_until_testrun_end_raises_on_timeout(library, client, mocker):
177
+ """A run that never leaves a pending status raises ``TimeoutError``."""
178
+ # Arrange
179
+ mocker.patch("robotframework_lynqa.library.time.sleep")
180
+ client.get_test_run_status.return_value = {"status": "running"}
181
+
182
+ # Act / Assert
183
+ with pytest.raises(TimeoutError):
184
+ library._wait_until_testrun_end(timeout=-1)
185
+
186
+
187
+ def test_search_secrets_variables_unset(library, mocker):
188
+ """An unset secrets variable yields an empty list."""
189
+ # Arrange
190
+ mocker.patch.object(library, "_search_variable", return_value=None)
191
+
192
+ # Act / Assert
193
+ assert library._search_secrets_variables() == []
194
+
195
+
196
+ def test_search_secrets_variables_invalid_type(library, mocker):
197
+ """A non-mapping secrets variable raises ``TypeError``."""
198
+ # Arrange
199
+ mocker.patch.object(library, "_search_variable", return_value="not-a-dict")
200
+
201
+ # Act / Assert
202
+ with pytest.raises(TypeError):
203
+ library._search_secrets_variables()
@@ -0,0 +1,61 @@
1
+ """Unit tests for :class:`robotframework_lynqa.reporter.StepReporter`.
2
+
3
+ These tests exercise the reporter in isolation from Robot Framework: the Robot ``logger`` is patched to capture the
4
+ messages, and the Lynqa client is a mock, so only the reporter's own log-presentation logic is under test.
5
+ """
6
+
7
+ import pytest
8
+
9
+ from robotframework_lynqa.reporter import StepReporter
10
+ from robotframework_lynqa.test.data_set.testrun_full_status import TEST_RUN_FULL_STATUS_SUCCESS_RESPONSE
11
+ from robotframework_lynqa.test.data_set.testrun_full_status_failure import TEST_RUN_FULL_STATUS_FAILURE_RESPONSE
12
+
13
+ # Screenshots embedded for the sample runs: 1 initial + 5 command + 5 assertion screenshots.
14
+ EXPECTED_SCREENSHOT_COUNT = 11
15
+
16
+
17
+ @pytest.fixture
18
+ def reporter(mocker, screenshot_base64):
19
+ """Build a :class:`StepReporter` wired to the given client and run data."""
20
+ client = mocker.patch("pylynqa.LynqaClient")
21
+ client.get_screenshot.return_value = screenshot_base64
22
+ return StepReporter(client, run_id="ur35vkfr7k5rm321dsuvllab", url="https://petit-robot.bzh")
23
+
24
+
25
+ @pytest.fixture
26
+ def mock_logger(mocker):
27
+ """Fixture to intercept logger calls."""
28
+ return mocker.patch("robotframework_lynqa.reporter.logger")
29
+
30
+
31
+ def _embedded_image_count(mock_logger):
32
+ """Count the log messages that embedded an inline screenshot."""
33
+ messages = [call.args[0] for call in mock_logger.info.call_args_list]
34
+ messages += [call.args[0] for call in mock_logger.write.call_args_list]
35
+ return sum("<img" in message for message in messages)
36
+
37
+
38
+ @pytest.mark.parametrize(
39
+ "results",
40
+ [TEST_RUN_FULL_STATUS_SUCCESS_RESPONSE, TEST_RUN_FULL_STATUS_FAILURE_RESPONSE],
41
+ ids=["success", "failure"],
42
+ )
43
+ def test_log_step_embeds_every_screenshot(reporter, mock_logger, results):
44
+ """Every screenshot (initial + commands + assertions) is fetched and embedded as an inline image."""
45
+ # Act
46
+ for index, _ in enumerate(results["stepStatuses"]):
47
+ reporter.log_step(index, results)
48
+
49
+ # Assert
50
+ assert reporter._client.get_screenshot.call_count == EXPECTED_SCREENSHOT_COUNT
51
+ assert _embedded_image_count(mock_logger) == EXPECTED_SCREENSHOT_COUNT
52
+
53
+
54
+ def test_embed_screenshot_catches_fetch_errors(reporter, mock_logger):
55
+ """A screenshot download error is caught and logged, returning an empty string instead of raising."""
56
+ # Arrange
57
+ reporter._client.get_screenshot.side_effect = Exception("Kenavo")
58
+
59
+ # Act / Assert
60
+ assert not reporter._embed_screenshot("415bceb6-9328-4c50-80b3-d0139f8bcafe", "caption")
61
+ mock_logger.error.assert_called_once()
@@ -0,0 +1,109 @@
1
+ Metadata-Version: 2.4
2
+ Name: robotframework-lynqa
3
+ Version: 0.1.0
4
+ Summary: Robot Framework library for the Lynqa test execution agent
5
+ Author-email: Alexis Pallier <alexis.pallier@petit-robot.bzh>
6
+ License-Expression: Apache-2.0
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: pylynqa>=0.1
11
+ Requires-Dist: robotframework>=7
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest>=7; extra == "dev"
14
+ Requires-Dist: pytest-mock>=3; extra == "dev"
15
+ Requires-Dist: pytest-cov>=5; extra == "dev"
16
+ Requires-Dist: ruff>=0.15; extra == "dev"
17
+ Requires-Dist: ty>=0.0; extra == "dev"
18
+ Requires-Dist: coverage>=7.10; extra == "dev"
19
+ Requires-Dist: robotframework-robocop>=6; extra == "dev"
20
+ Requires-Dist: docstrfmt>=1.11; extra == "dev"
21
+ Requires-Dist: sphinx-lint>=0.9; extra == "dev"
22
+ Provides-Extra: doc
23
+ Requires-Dist: docutils>=0.22; extra == "doc"
24
+ Requires-Dist: zensical>=0.0; extra == "doc"
25
+ Dynamic: license-file
26
+
27
+ # robotframework-lynqa
28
+
29
+ [![CI](https://github.com/petit-robot/robotframework-lynqa/actions/workflows/ci.yml/badge.svg)](https://github.com/petit-robot/robotframework-lynqa/actions/workflows/ci.yml)
30
+ [![Acceptance test](https://github.com/petit-robot/robotframework-lynqa/actions/workflows/acceptance.yml/badge.svg)](https://github.com/petit-robot/robotframework-lynqa/actions/workflows/acceptance.yml)
31
+ [![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
32
+ [![Documentation](https://img.shields.io/badge/docs-Full%20documentation-blue.svg)](https://petit-robot.github.io/robotframework-lynqa/)
33
+
34
+ **robotframework-lynqa** is a [Robot Framework](https://robotframework.org) library for the
35
+ [Lynqa](https://lynqa.smartesting.com) test execution AI Agent.
36
+
37
+ <picture>
38
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/petit-robot/robotframework-lynqa/main/docs/robotframework-lynqa-dark.png">
39
+ <img alt="Robot Framework/Lynqa connector" src="https://raw.githubusercontent.com/petit-robot/robotframework-lynqa/main/docs/robotframework-lynqa.png">
40
+ </picture>
41
+
42
+ Write your scenarios in plain Gherkin and let Lynqa, [Smartesting](https://www.smartesting.com)'s AI test-execution agent, execute them autonomously.
43
+ No need to implement local keywords, simply provide your web application's URL.
44
+ Lynqa analyses your test steps and application UI, then runs your test cases as a user would, producing comprehensive execution evidence.
45
+ UI changes are handled automatically, eliminating the need for test automation maintenance.
46
+
47
+ It builds on [pylynqa](https://github.com/petit-robot/pylynqa), the Python client for the Lynqa REST API.
48
+
49
+ ## Disclaimers
50
+
51
+ > [!IMPORTANT]
52
+ > **This is an unofficial project.** robotframework-lynqa is **not affiliated with, endorsed by, or maintained by
53
+ > [Smartesting](https://www.smartesting.com)**, the company that develops Lynqa. "Lynqa" and "Smartesting" are the
54
+ > property of their respective owners.
55
+
56
+ > [!NOTE]
57
+ > **Lynqa is a commercial product.** Using this library requires a Lynqa account and an API key, and running tests
58
+ > consumes paid credits. See <https://my.lynqa.smartesting.com/integration> to create an API key.
59
+
60
+ ## Requirements
61
+
62
+ - Python 3.9+
63
+ - Robot Framework 7+
64
+ - A Lynqa account and API key (test executions consume credits)
65
+
66
+ ## Installation
67
+
68
+ ```bash
69
+ pip install robotframework-lynqa
70
+ ```
71
+
72
+ ## Quick start
73
+
74
+ Import the library and write your test cases with the `Given`/`When`/`Then` keywords, each taking a single
75
+ natural-language step:
76
+
77
+ ```robotframework
78
+ *** Settings ***
79
+ Library robotframework_lynqa.LynqaLibrary api_key=%{LYNQA_API_KEY}
80
+
81
+ *** Variables ***
82
+ ${LYNQA_URL} https://lemonde.fr
83
+
84
+ *** Test Cases ***
85
+ Search An Article
86
+ Given the website is open
87
+ When I search an article on "france ia"
88
+ Then there is an article on "IA agentique"
89
+ ```
90
+
91
+ The scenario is captured and submitted to Lynqa, which runs it against the site at `${LYNQA_URL}` and reports each step's
92
+ result back to Robot Framework.
93
+
94
+ ## Documentation
95
+
96
+ Full documentation: installation, configuration variables and the complete keyword reference — is available at
97
+ <https://petit-robot.github.io/robotframework-lynqa/>.
98
+
99
+ ## Collaboration
100
+
101
+ This project is at an early stage, so **external contributions are limited for now**. This may be opened up more broadly
102
+ later as the project matures.
103
+
104
+ In the meantime, **you are very welcome to open an
105
+ [issue](https://github.com/petit-robot/robotframework-lynqa/issues)** to report a bug or ask any question.
106
+
107
+ ## License
108
+
109
+ Licensed under the [Apache License 2.0](LICENSE).
@@ -0,0 +1,22 @@
1
+ robotframework_lynqa/__init__.py,sha256=cfyw4h9FAPx6XZqEbBiNIFykosoLjyKQE-O-E7beANg,166
2
+ robotframework_lynqa/library.py,sha256=O9YrSQ-cgO1DYbttFwOiCExBmN21BQ9oj-FAgpetYfU,13810
3
+ robotframework_lynqa/reporter.py,sha256=4RheUnEubzBr_ZYRPd1TV1dbmYcf0iucLTYXDrz4kO8,5950
4
+ robotframework_lynqa/atest/lynqa_search.robot,sha256=_eZXZzjhKhZ6EAjAo0NnoRNUKqdGKXWOA5EVTqLISrw,715
5
+ robotframework_lynqa/test/__init__.py,sha256=PKTiZ9hxApp32cXzuhG1kWKEuR2oNJJFowiTe2ohWkg,1037
6
+ robotframework_lynqa/test/conftest.py,sha256=K6l5557bSPAd6wbpTy2m5kmuqV-XQFn73_iZAQVhBsk,989
7
+ robotframework_lynqa/test/test_library.py,sha256=9SO52kjZKEtXbksvjVtCFADQoAlKjOds7DpqDt-LkGw,7837
8
+ robotframework_lynqa/test/test_reporter.py,sha256=Q-D45CCi4W9jrWnBiG0O8U44G5adzw5YMaS-t1AYGEo,2569
9
+ robotframework_lynqa/test/data_set/screenshot.png,sha256=TWkyp04NyIeOUCP6Md0nOVp33ItE3AHmnIA1K5MyetY,16612
10
+ robotframework_lynqa/test/data_set/test_lynqa_nominal.robot,sha256=DH0Wdd0Q_4JbTCeQnIdNX84zcPTiftJTkBFFFI5Srrg,638
11
+ robotframework_lynqa/test/data_set/test_lynqa_with_date.robot,sha256=wOEkTmxa5Q9gmd4T9cAQ-LuJqS5P2_Xa_eCyI7h9C98,577
12
+ robotframework_lynqa/test/data_set/test_lynqa_with_results.robot,sha256=Tc4kzTkKOUfTIUG6zfR9mGrA5MrS3NW6WPllzeR9H1k,411
13
+ robotframework_lynqa/test/data_set/test_lynqa_without_url.robot,sha256=PZ_LCeAOajiBDTiDyfFyh1qmOyuvIz3NdZAiNCwQWvo,532
14
+ robotframework_lynqa/test/data_set/testrun_full_status.py,sha256=nynK-Vtg8te_1N-s9nt7vg2AAQaH_NrSJWXsRwJ-a7E,6437
15
+ robotframework_lynqa/test/data_set/testrun_full_status_failure.py,sha256=7u28mBECxzjWyQY5W9WCXc-1aLOYbEIIUqYZlOvEGC4,6512
16
+ robotframework_lynqa-0.1.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
17
+ robotframework_lynqa-0.1.0.dist-info/METADATA,sha256=jTHg2qsYRu0nzANLXfnRy7dbb8O1CYdSk7p795DbR18,4542
18
+ robotframework_lynqa-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
19
+ robotframework_lynqa-0.1.0.dist-info/scm_file_list.json,sha256=n7wEzh7hU-uQa4XGobXRCHT-ooJu3PNS3G-cwVl5V2I,1428
20
+ robotframework_lynqa-0.1.0.dist-info/scm_version.json,sha256=1zocZrvkPySUGA-DaIkk-Nfowm3bbZdbBkTDECNxwGk,160
21
+ robotframework_lynqa-0.1.0.dist-info/top_level.txt,sha256=9wtppVSjlZ9nniUMeFezx6h1SKSk9YCvrzFQmAdpoXE,21
22
+ robotframework_lynqa-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+