Skip to content

Commit eff3cd2

Browse files
committed
Make improvements to Recorder Mode assertions
1 parent 17fe3ad commit eff3cd2

File tree

3 files changed

+48
-64
lines changed

3 files changed

+48
-64
lines changed

help_docs/recorder_mode.md

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,13 @@ pytest TEST_NAME.py --rec -s
1818
import ipdb; ipdb.set_trace()
1919
```
2020

21-
🔴 The Recorder will capture browser actions on URLs that begin with ``https:``, ``http:``, and ``file:``. (The Recorder won't work on ``data:`` URLS.)
22-
23-
🔴 You can also activate Debug Mode at the start of your test by adding ``--trace`` as a ``pytest`` command-line option:
21+
🔴 You can also activate Debug Mode at the start of your test by adding ``--trace`` as a ``pytest`` command-line option: (This is useful when running Recorder Mode without any breakpoints.)
2422

2523
```bash
2624
pytest TEST_NAME.py --trace --rec -s
2725
```
2826

29-
🔴 Once you've reached the breakpoint, you can take control of the browser and add in any actions that you want recorded. When you are finished recording, type "``c``" on the command-line and press ``[Enter]`` to let the test continue from the breakpoint. After the test completes, a file called ``TEST_NAME_rec.py`` will be automatically created in the ``./recordings`` folder, which will include the actions performed by the test, and the manual actions that you added in. Below is a command-line notification:
27+
🔴 Once you've reached the breakpoint, you can take control of the browser and add in any actions that you want recorded. The Recorder will capture browser actions on URLs that begin with ``https:``, ``http:``, and ``file:``; (the Recorder won't work on ``data:`` URLS). When you are finished recording, type "``c``" on the command-line and press ``[Enter]`` to let the test continue from the breakpoint. After the test completes, a file called ``TEST_NAME_rec.py`` will be automatically created in the ``./recordings`` folder, which will include the actions performed by the test, and the manual actions that you added in. Below is a command-line notification:
3028

3129
```bash
3230
>>> RECORDING SAVED as: recordings/my_first_test_rec.py
@@ -52,7 +50,7 @@ class RecorderTest(BaseCase):
5250
import ipdb; ipdb.set_trace()
5351
```
5452

55-
<p>🔴 The above file gives you a basic SeleniumBase file with a breakpoint in it so that you can immediately start recording after you've opened a new web page in the browser.</p>
53+
<p>🔴 The above code gives you a basic SeleniumBase file with a breakpoint in it so that you can immediately start recording after you've opened a new web page in the browser.</p>
5654

5755
<p>🔴 Recorder Mode works by saving your recorded actions into the browser's <code>sessionStorage</code>. SeleniumBase then reads from the browser's <code>sessionStorage</code> to take the raw data and generate a full test from it. Keep in mind that <code>sessionStorage</code> is only present for a website while the browser tab remains on a web page of the same domain/origin. If you leave that domain/origin, the <code>sessionStorage</code> of that tab will no longer have the raw data that SeleniumBase needs to create a full recording. To compensate for this, all links to web pages of a different domain/origin will automatically open a new tab for you while in Recorder Mode. Additionally, the SeleniumBase <code>self.open(URL)</code> method will also open a new tab for you in Recorder Mode if the domain/origin is different from the current URL. When the recorded test completes, SeleniumBase will scan the <code>sessionStorage</code> of all open browser tabs for the data it needs to generate the complete SeleniumBase automation script.</p>
5856

@@ -66,26 +64,16 @@ class RecorderTest(BaseCase):
6664

6765
<p>🔴 SeleniumBase <code>1.66.2</code> adds the ability to save selectors using the <code>":contains(TEXT)"</code> selector. If a Python file being recorded has multiple tests being run, then all those tests will get saved to the generated <code>*_rec.py</code> file. The Recorder will now save common <code>self.assert_*</code> calls made during tests. In order to escape iframes when using <code>self.set_content_to_frame(frame)</code>, a new method was added: <code>self.set_content_to_default()</code>. The <code>self.set_content_to_*()</code> methods will be automatically used in place of <code>self.switch_to_*()</code> methods in Recorder Mode, unless a test explicitly calls <code>self._rec_overrides_switch = False</code> before the <code>self.switch_to_*()</code> methods are called. Additionally, if an iframe contains the <code>src</code> attribute, that page will get loaded in a new tab when switching to it in Recorder Mode.</p>
6866

69-
<p>🔴 SeleniumBase <code>1.66.3</code> improves the algorithm for generating efficient selectors. Additionally, single and double quotes in selectors will now be properly escaped with backslashes as needed.</p>
70-
71-
<p>🔴 SeleniumBase <code>1.66.4</code> adds better error-handling for the Recorder, and a few other improvements.</p>
72-
73-
<p>🔴 SeleniumBase <code>1.66.5</code> improves the algorithm for converting recorded actions into SeleniumBase code.</p>
74-
75-
<p>🔴 SeleniumBase <code>1.66.6</code> improves the algorithm for converting recorded actions into SeleniumBase code.</p>
67+
<p>🔴 SeleniumBase versions <code>1.66.3</code>, <code>1.66.4</code>, <code>1.66.5</code>, <code>1.66.6</code>, <code>1.66.7</code>, <code>1.66.8</code>, and <code>1.66.9</code> improve the algorithm for converting recorded actions into SeleniumBase code.</p>
7668

77-
<p>🔴 SeleniumBase <code>1.66.7</code> improves Recorder Mode post-processing and automatically adds <code>self.show_file_choosers()</code> if file-upload fields are hidden when calling <code>self.choose_file(selector, file_path)</code>.</p>
78-
79-
<p>🔴 SeleniumBase <code>1.66.8</code> generates better selectors in Recorder Mode and improves the algorithm for converting recorded actions into SeleniumBase code.</p>
80-
81-
<p>🔴 SeleniumBase <code>1.66.9</code> allows the Recorder to record methods related to file downloads. It also updates how the Recorder handles class-parsing and form submissions.</p>
82-
83-
<p>🔴 SeleniumBase <code>1.66.10</code> adds better error-handling for the Recorder. It also adds the console script option <code>-r</code> for <code>sbase mkfile</code> to generate a new test file with a breakpoint for Recorder Mode: <code>sbase mkfile NEW_FILE.py -r</code></p>
69+
<p>🔴 SeleniumBase <code>1.66.10</code> adds better error-handling to the Recorder. It also adds the console script option <code>-r</code> for <code>sbase mkfile</code> to generate a new test file with a breakpoint for Recorder Mode: <code>sbase mkfile NEW_FILE.py -r</code></p>
8470

8571
<p>🔴 SeleniumBase <code>1.66.12</code> adds the ability to instantly create a new test recording by running <code>sbase mkrec FILE.py</code>. Once the browser spins up, you can open a new web page and start performing actions that will get recorded and saved to the file you specified.</p>
8672

8773
<p>🔴 SeleniumBase <code>1.66.13</code> lets you add assertions for elements and text while making a recording. To add an element assertion, press the <code>[^]-key (SHIFT+6)</code>, (the border will become purple) then click on elements that you'd like to assert. To add a text assertion, press the <code>[&]-key (SHIFT+7)</code>, (the border will become orange) then click on text elements that you'd like to assert. To go back to the regular Record Mode, press any other key. While in the special assertion modes, certain actions such as clicking on links won't have any effect. This lets you make assertions on elements without certain actions getting in the way.</p>
8874

75+
<p>🔴 SeleniumBase <code>1.66.14</code> improves the algorithm for converting recorded assertions into SeleniumBase code. Text assertions that contain the newline character will now be handled correctly. If a text assertion has a <code>:contains</code> selector, then the text assertion will be changed to an element assertion. Asserted text from multi-line assertions with use <code>self.assert_text()</code> while asserted text from single-line assertions will use <code>self.assert_exact_text()</code>.</p>
76+
8977
--------
9078

9179
<div>To learn more about SeleniumBase, check out the Docs Site:</div>

seleniumbase/extensions/recorder.zip

57 Bytes
Binary file not shown.

seleniumbase/js_code/recorder_js.py

Lines changed: 41 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,7 @@
9999
var step = allowOverlapping ? 1 : subString.length;
100100
while (true) {
101101
pos = string.indexOf(subString, pos);
102-
if (pos >= 0) {
103-
++n;
104-
pos += step;
105-
}
102+
if (pos >= 0) { ++n; pos += step; }
106103
else break;
107104
}
108105
return n;
@@ -156,8 +153,9 @@
156153
child_count_by_attr = [];
157154
for (var i = 0; i < non_id_attributes.length; i++) {
158155
selector_by_attr[i] = null;
159-
if (non_id_attributes[i] == 'class')
156+
if (non_id_attributes[i] == 'class') {
160157
selector_by_attr[i] = selector_by_class;
158+
}
161159
else {
162160
selector_by_attr[i] = cssPathByAttribute(el, non_id_attributes[i]);
163161
}
@@ -174,6 +172,7 @@
174172
basic_tags.push('h1');
175173
basic_tags.push('h2');
176174
basic_tags.push('h3');
175+
basic_tags.push('center');
177176
basic_tags.push('input');
178177
basic_tags.push('textarea');
179178
for (var i = 0; i < basic_tags.length; i++) {
@@ -211,8 +210,8 @@
211210
t_count += 1;
212211
}
213212
if (t_count === 1 && !inner_text.includes('\n')) {
214-
inner_text = inner_text.replace("'", "\\'");
215-
inner_text = inner_text.replace('"', '\\"');
213+
inner_text = inner_text.replaceAll("'", "\\'");
214+
inner_text = inner_text.replaceAll('"', '\\"');
216215
return tag_name += ':contains("'+inner_text+'")';
217216
}
218217
}
@@ -227,16 +226,17 @@
227226
qsa_element = "span";
228227
if (parent_tag_name == "button")
229228
qsa_element = "button > span";
230-
else { qsa_element = "button > "+parent_tag_name+" > span" }
229+
else
230+
qsa_element = "button > "+parent_tag_name+" > span";
231231
t_count = 0;
232232
all_el_found = document.querySelectorAll(qsa_element);
233233
for (var j = 0; j < all_el_found.length; j++) {
234234
if (all_el_found[j].innerText.includes(inner_text))
235235
t_count += 1;
236236
}
237237
if (t_count === 1 && !inner_text.includes('\n')) {
238-
inner_text = inner_text.replace("'", "\\'");
239-
inner_text = inner_text.replace('"', '\\"');
238+
inner_text = inner_text.replaceAll("'", "\\'");
239+
inner_text = inner_text.replaceAll('"', '\\"');
240240
return qsa_element += ':contains("'+inner_text+'")';
241241
}
242242
}
@@ -299,30 +299,15 @@
299299
sessionStorage.setItem('pause_recorder', 'no');
300300
sessionStorage.setItem('recorder_mode', '1');
301301
const d_now = Date.now();
302+
w_orig = window.location.origin;
303+
w_href = window.location.href;
302304
if (sessionStorage.getItem('recorder_activated') === 'yes') {
303305
ss_ra = JSON.parse(sessionStorage.getItem('recorded_actions'));
304306
document.recorded_actions = ss_ra;
305-
w_orig = window.location.origin;
306-
w_href = window.location.href;
307-
ra_len = document.recorded_actions.length;
308-
if (ra_len > 0 && document.recorded_actions[ra_len-1][0] === 'begin') {
309-
document.recorded_actions.pop();
310-
document.recorded_actions.push(['begin', w_orig, w_href, d_now]);
311-
}
312-
else if (ra_len > 0 &&
313-
document.recorded_actions[ra_len-1][0] === '_url_')
314-
{
315-
document.recorded_actions.pop();
316-
document.recorded_actions.push(['_url_', w_orig, w_href, d_now]);
317-
}
318-
else {
319-
document.recorded_actions.push(['_url_', w_orig, w_href, d_now]);
320-
}
307+
document.recorded_actions.push(['_url_', w_orig, w_href, d_now]);
321308
}
322309
else {
323310
sessionStorage.setItem('recorder_activated', 'yes');
324-
w_orig = window.location.origin;
325-
w_href = window.location.href;
326311
document.recorded_actions.push(['begin', w_orig, w_href, d_now]);
327312
}
328313
json_rec_act = JSON.stringify(document.recorded_actions);
@@ -368,8 +353,9 @@
368353
{
369354
document.recorded_actions.pop();
370355
}
371-
if (element.draggable === true)
356+
if (element.draggable === true) {
372357
document.recorded_actions.push(['drags', selector, '', d_now]);
358+
}
373359
json_rec_act = JSON.stringify(document.recorded_actions);
374360
sessionStorage.setItem('recorded_actions', json_rec_act);
375361
});
@@ -450,7 +436,7 @@
450436
document.recorded_actions[ra_len-1][1] === selector &&
451437
tag_name === 'input' && e_type === 'checkbox')
452438
{
453-
// The checkbox state only needs to be set once. (Pop duplicates.)
439+
// Pop duplicate checkbox state changes.
454440
document.recorded_actions.pop();
455441
ra_len = document.recorded_actions.length;
456442
if (ra_len > 0 && document.recorded_actions[ra_len-1][1] === selector)
@@ -474,15 +460,13 @@
474460
const selector = getBestSelector(element);
475461
ra_len = document.recorded_actions.length;
476462
tag_name = element.tagName.toLowerCase();
477-
if (ra_len > 0 && document.recorded_actions[ra_len-1][0] === 'mo_dn') {
463+
if (ra_len > 0 && document.recorded_actions[ra_len-1][0] === 'mo_dn')
478464
document.recorded_actions.pop();
479-
}
480465
if (tag_name === 'select') {
481466
// Do Nothing. (Handle select in 'change' action.)
482467
}
483-
else {
468+
else
484469
document.recorded_actions.push(['mo_dn', selector, '', d_now]);
485-
}
486470
json_rec_act = JSON.stringify(document.recorded_actions);
487471
sessionStorage.setItem('recorded_actions', json_rec_act);
488472
});
@@ -506,17 +490,32 @@
506490
document.recorded_actions[ra_len-1][0] === 'mo_dn' &&
507491
document.recorded_actions[ra_len-1][1] === selector)
508492
{
509-
if (rec_mode === '2') {
493+
sel_has_contains = selector.includes(':contains(');
494+
if (rec_mode === '2' || (rec_mode === '3' && sel_has_contains)) {
510495
origin = window.location.origin;
511496
document.recorded_actions.push(['as_el', selector, origin, d_now]);
512497
return;
513498
}
514499
else if (rec_mode === '3') {
515500
origin = window.location.origin;
516-
text = element.textContent;
501+
text = element.innerText;
502+
action = 'as_et';
517503
if (!text) { text = ''; }
504+
else {
505+
text = text.trim();
506+
var match = /\r|\n/.exec(text);
507+
if (match) {
508+
lines = text.split(/\r\n|\r|\n/g);
509+
text = '';
510+
for (var i = 0; i < lines.length; i++) {
511+
if (lines[i].length > 0) {
512+
action = 'as_te'; text = lines[i]; break;
513+
}
514+
}
515+
}
516+
}
518517
tex_sel = [text, selector];
519-
document.recorded_actions.push(['as_te', tex_sel, origin, d_now]);
518+
document.recorded_actions.push([action, tex_sel, origin, d_now]);
520519
return;
521520
}
522521
}
@@ -566,16 +565,13 @@
566565
child_count > 2 && !grand_element.hasAttribute('onclick')))
567566
{
568567
w_orig = window.location.origin;
569-
if (origin === w_orig) {
568+
if (origin === w_orig)
570569
document.recorded_actions.push(['_url_', origin, href, d_now]);
571-
}
572-
else {
570+
else
573571
document.recorded_actions.push(['begin', origin, href, d_now]);
574-
}
575572
}
576-
else {
573+
else
577574
document.recorded_actions.push(['click', selector, href, d_now]);
578-
}
579575
// Switch to hover_click() if in a dropdown.
580576
if (element.parentElement.classList.contains('dropdown-content') &&
581577
element.parentElement.parentElement.classList.contains('dropdown'))
@@ -603,7 +599,7 @@
603599
else if (ra_len > 0 &&
604600
document.recorded_actions[ra_len-1][0] === 'mo_dn')
605601
{
606-
// Probably an accidental drag & drop action.
602+
// Probably an accidental drag & drop.
607603
document.recorded_actions.pop();
608604
}
609605
json_rec_act = JSON.stringify(document.recorded_actions);
@@ -644,7 +640,7 @@
644640
document.body.addEventListener('keyup', function (event) {
645641
if (typeof document.recorded_actions === 'undefined')
646642
reset_recorder_state();
647-
// Controls for Pausing and Resuming the Recorder.
643+
// Controls for Pausing & Resuming.
648644
pause_rec = sessionStorage.getItem('pause_recorder');
649645
if (event.key.toLowerCase() === 'escape' && pause_rec === 'no')
650646
{

0 commit comments

Comments
 (0)