diff --git a/changedetectionio/blueprint/browser_steps/__init__.py b/changedetectionio/blueprint/browser_steps/__init__.py index b223d9a9..44dfd701 100644 --- a/changedetectionio/blueprint/browser_steps/__init__.py +++ b/changedetectionio/blueprint/browser_steps/__init__.py @@ -131,21 +131,11 @@ def construct_blueprint(datastore: ChangeDetectionStore): this_session.call_action(action_name=step_operation, selector=step_selector, optional_value=step_optional_value) - except playwright._impl._api_types.TimeoutError as e: - print("Element wasnt found :-(", step_operation) - return make_response("Element was not found on page", 401) - - except playwright._impl._api_types.Error as e: - # Browser/playwright level error - print("Browser error - got playwright._impl._api_types.Error, try reloading the session/browser") - print (str(e)) + except Exception as e: + print("Exception when calling step operation", step_operation, str(e)) # Try to find something of value to give back to the user - for l in str(e).splitlines(): - if 'DOMException' in l: - return make_response(l, 401) - - return make_response('Browser session ran out of time :( Please reload this page.', 401) + return make_response(str(e).splitlines()[0], 401) # Get visual selector ready/update its data (also use the current filter info from the page?) # When the last 'apply' button was pressed @@ -205,32 +195,43 @@ def construct_blueprint(datastore: ChangeDetectionStore): cleanup_playwright_session() return make_response('Browser session ran out of time :( Please reload this page.', 401) - try: - state = this_session.get_current_state() - except playwright._impl._api_types.Error as e: - return make_response("Browser session ran out of time :( Please reload this page."+str(e), 401) + response = None - # Use send_file() which is way faster than read/write loop on bytes - import json - from tempfile import mkstemp - from flask import send_file - tmp_fd, tmp_file = mkstemp(text=True, suffix=".json", prefix="changedetectionio-") + if request.method == 'POST': + # Screenshots and other info only needed on requesting a step (POST) + try: + state = this_session.get_current_state() + except playwright._impl._api_types.Error as e: + return make_response("Browser session ran out of time :( Please reload this page."+str(e), 401) - output = json.dumps({'screenshot': "data:image/jpeg;base64,{}".format( - base64.b64encode(state[0]).decode('ascii')), - 'xpath_data': state[1], - 'session_age_start': this_session.age_start, - 'browser_time_remaining': round(remaining) - }) + # Use send_file() which is way faster than read/write loop on bytes + import json + from tempfile import mkstemp + from flask import send_file + tmp_fd, tmp_file = mkstemp(text=True, suffix=".json", prefix="changedetectionio-") - with os.fdopen(tmp_fd, 'w') as f: - f.write(output) + output = json.dumps({'screenshot': "data:image/jpeg;base64,{}".format( + base64.b64encode(state[0]).decode('ascii')), + 'xpath_data': state[1], + 'session_age_start': this_session.age_start, + 'browser_time_remaining': round(remaining) + }) - response = make_response(send_file(path_or_file=tmp_file, - mimetype='application/json; charset=UTF-8', - etag=True)) - # No longer needed - os.unlink(tmp_file) + with os.fdopen(tmp_fd, 'w') as f: + f.write(output) + + response = make_response(send_file(path_or_file=tmp_file, + mimetype='application/json; charset=UTF-8', + etag=True)) + # No longer needed + os.unlink(tmp_file) + + elif request.method == 'GET': + # Just enough to get the session rolling, it will call for goto-site via POST next + response = make_response({ + 'session_age_start': this_session.age_start, + 'browser_time_remaining': round(remaining) + }) return response diff --git a/changedetectionio/blueprint/browser_steps/browser_steps.py b/changedetectionio/blueprint/browser_steps/browser_steps.py index a44c830e..7fc7ca3b 100644 --- a/changedetectionio/blueprint/browser_steps/browser_steps.py +++ b/changedetectionio/blueprint/browser_steps/browser_steps.py @@ -90,7 +90,7 @@ class steppable_browser_interface(): return elem = self.page.get_by_text(value) if elem.count(): - elem.first.click(delay=randint(200, 500)) + elem.first.click(delay=randint(200, 500), timeout=3000) def action_enter_text_in_field(self, selector, value): if not len(selector.strip()): @@ -146,10 +146,10 @@ class steppable_browser_interface(): self.page.keyboard.press("PageDown", delay=randint(200, 500)) def action_check_checkbox(self, selector, value): - self.page.locator(selector).check() + self.page.locator(selector).check(timeout=1000) def action_uncheck_checkbox(self, selector, value): - self.page.locator(selector).uncheck() + self.page.locator(selector, timeout=1000).uncheck(timeout=1000) # Responsible for maintaining a live 'context' with browserless @@ -211,7 +211,7 @@ class browsersteps_live_ui(steppable_browser_interface): # Listen for all console events and handle errors self.page.on("console", lambda msg: print(f"Browser steps console - {msg.type}: {msg.text} {msg.args}")) - print("time to browser setup", time.time() - now) + print("Time to browser setup", time.time() - now) self.page.wait_for_timeout(1 * 1000) def mark_as_closed(self): diff --git a/changedetectionio/static/js/browser-steps.js b/changedetectionio/static/js/browser-steps.js index efd6b71f..37aca4e9 100644 --- a/changedetectionio/static/js/browser-steps.js +++ b/changedetectionio/static/js/browser-steps.js @@ -10,10 +10,10 @@ $(document).ready(function () { } }) var browsersteps_session_id; - var browserless_seconds_remaining=0; + var browserless_seconds_remaining = 0; var apply_buttons_disabled = false; var include_text_elements = $("#include_text_elements"); - var xpath_data; + var xpath_data = false; var current_selected_i; var state_clicked = false; var c; @@ -25,11 +25,42 @@ $(document).ready(function () { $(window).resize(function () { set_scale(); }); + // Should always be disabled + $('#browser_steps >li:first-child select').val('Goto site').attr('disabled', 'disabled'); - $('a#browsersteps-tab').click(function () { + $('#browsersteps-click-start').click(function () { + $("#browsersteps-click-start").fadeOut(); + $("#browsersteps-selector-wrapper .spinner").fadeIn(); start(); }); + $('a#browsersteps-tab').click(function () { + reset(); + }); + + window.addEventListener('hashchange', function () { + if (window.location.hash == '#browser-steps') { + reset(); + } + }); + + function reset() { + xpath_data = false; + $('#browsersteps-img').removeAttr('src'); + $("#browsersteps-click-start").show(); + $("#browsersteps-selector-wrapper .spinner").hide(); + browserless_seconds_remaining = 0; + browsersteps_session_id = false; + apply_buttons_disabled = false; + ctx.clearRect(0, 0, c.width, c.height); + set_first_gotosite_disabled(); + } + + function set_first_gotosite_disabled() { + $('#browser_steps >li:first-child select').val('Goto site').attr('disabled', 'disabled'); + $('#browser_steps >li:first-child').css('opacity', '0.5'); + } + // Show seconds remaining until playwright/browserless needs to restart the session // (See comment at the top of changedetectionio/blueprint/browser_steps/__init__.py ) setInterval(() => { @@ -40,21 +71,6 @@ $(document).ready(function () { }, "1000") - if (window.location.hash == '#browser-steps') { - start(); - } - - window.addEventListener('hashchange', function () { - if (window.location.hash == '#browser-steps') { - start(); - } - // For when the page loads - if (!window.location.hash || window.location.hash != '#browser-steps') { - $("img#browsersteps-img").attr('src', ''); - return; - } - }); - function set_scale() { // some things to check if the scaling doesnt work @@ -87,7 +103,6 @@ $(document).ready(function () { // @todo is click better? $('#browsersteps-selector-canvas').off("mousemove mousedown click"); // Undo disable_browsersteps_ui - $("#browser_steps select,input").removeAttr('disabled').css('opacity', '1.0'); $("#browser-steps-ui").css('opacity', '1.0'); // init @@ -103,7 +118,7 @@ $(document).ready(function () { // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent e.preventDefault() console.log(e); - console.log("current xpath in index is "+current_selected_i); + console.log("current xpath in index is " + current_selected_i); last_click_xy = {'x': parseInt((1 / x_scale) * e.offsetX), 'y': parseInt((1 / y_scale) * e.offsetY)} process_selected(current_selected_i); current_selected_i = false; @@ -118,6 +133,10 @@ $(document).ready(function () { }); $('#browsersteps-selector-canvas').bind('mousemove', function (e) { + if (!xpath_data) { + return; + } + // checkbox if find elements is enabled ctx.clearRect(0, 0, c.width, c.height); ctx.fillStyle = 'rgba(255,0,0, 0.1)'; @@ -153,8 +172,8 @@ $(document).ready(function () { // does it mean sort the xpath list by size (w*h) i think so! } else { - if ( include_text_elements[0].checked === true) { - // blue one with background instead? + if (include_text_elements[0].checked === true) { + // blue one with background instead? ctx.fillStyle = 'rgba(0,0,255, 0.1)'; ctx.strokeStyle = 'rgba(0,0,200, 0.7)'; $('#browsersteps-selector-canvas').css('cursor', 'grab'); @@ -175,7 +194,6 @@ $(document).ready(function () { // }); - // callback for clicking on an xpath on the canvas function process_selected(xpath_data_index) { found_something = false; @@ -190,23 +208,23 @@ $(document).ready(function () { console.log(x); if (x && first_available.length) { // @todo will it let you click shit that has a layer ontop? probably not. - if (x['tagtype'] === 'text' || x['tagtype'] === 'email' || x['tagName'] === 'textarea' || x['tagtype'] === 'password' || x['tagtype'] === 'search' ) { + if (x['tagtype'] === 'text' || x['tagtype'] === 'email' || x['tagName'] === 'textarea' || x['tagtype'] === 'password' || x['tagtype'] === 'search') { $('select', first_available).val('Enter text in field').change(); $('input[type=text]', first_available).first().val(x['xpath']); $('input[placeholder="Value"]', first_available).addClass('ok').click().focus(); found_something = true; } else { - if (x['isClickable'] || x['tagName'].startsWith('h')|| x['tagName'] === 'a' || x['tagName'] === 'button' || x['tagtype'] === 'submit'|| x['tagtype'] === 'checkbox'|| x['tagtype'] === 'radio'|| x['tagtype'] === 'li') { + if (x['isClickable'] || x['tagName'].startsWith('h') || x['tagName'] === 'a' || x['tagName'] === 'button' || x['tagtype'] === 'submit' || x['tagtype'] === 'checkbox' || x['tagtype'] === 'radio' || x['tagtype'] === 'li') { $('select', first_available).val('Click element').change(); $('input[type=text]', first_available).first().val(x['xpath']); found_something = true; } } - first_available.xpath_data_index=xpath_data_index; + first_available.xpath_data_index = xpath_data_index; if (!found_something) { - if ( include_text_elements[0].checked === true) { + if (include_text_elements[0].checked === true) { // Suggest that we use as filter? // @todo filters should always be in the last steps, nothing non-filter after it found_something = true; @@ -230,15 +248,15 @@ $(document).ready(function () { function start() { console.log("Starting browser-steps UI"); - browsersteps_session_id=Date.now(); + browsersteps_session_id = Date.now(); // @todo This setting of the first one should be done at the datalayer but wtforms doesnt wanna play nice $('#browser_steps >li:first-child').removeClass('empty'); - $('#browser_steps >li:first-child select').val('Goto site').attr('disabled', 'disabled'); - $('#browser-steps-ui .loader').show(); + set_first_gotosite_disabled(); + $('#browser-steps-ui .loader .spinner').show(); $('.clear,.remove', $('#browser_steps >li:first-child')).hide(); $.ajax({ type: "GET", - url: browser_steps_sync_url+"&browsersteps_session_id="+browsersteps_session_id, + url: browser_steps_sync_url + "&browsersteps_session_id=" + browsersteps_session_id, statusCode: { 400: function () { // More than likely the CSRF token was lost when the server restarted @@ -247,11 +265,12 @@ $(document).ready(function () { } }).done(function (data) { xpath_data = data.xpath_data; - $('#browsersteps-img').attr('src', data.screenshot); $("#loading-status-text").fadeIn(); // This should trigger 'Goto site' + console.log("Got startup response, requesting Goto-Site (first) step fake click"); $('#browser_steps >li:first-child .apply').click(); browserless_seconds_remaining = data.browser_time_remaining; + set_first_gotosite_disabled(); }).fail(function (data) { console.log(data); alert('There was an error communicating with the server.'); @@ -260,7 +279,7 @@ $(document).ready(function () { } function disable_browsersteps_ui() { - $("#browser_steps select,input").attr('disabled', 'disabled').css('opacity', '0.5'); + set_first_gotosite_disabled(); $("#browser-steps-ui").css('opacity', '0.3'); $('#browsersteps-selector-canvas').off("mousemove mousedown click"); } @@ -307,11 +326,14 @@ $(document).ready(function () { // Add the extra buttons to the steps $('ul#browser_steps li').each(function (i) { - $(this).append('
' + - 'Apply ' + - 'Clear ' + - 'Remove' + - '
') + var s = '
' + 'Apply '; + if (i > 0) { + // The first step never gets these (Goto-site) + s += 'Clear ' + + 'Remove'; + } + s += '
'; + $(this).append(s) } ); @@ -348,15 +370,15 @@ $(document).ready(function () { $('ul#browser_steps li .control .apply').click(function (event) { // sequential requests @todo refactor - if(apply_buttons_disabled) { + if (apply_buttons_disabled) { return; } var current_data = $(event.currentTarget).closest('li'); - $('#browser-steps-ui .loader').fadeIn(); - apply_buttons_disabled=true; - $('ul#browser_steps li .control .apply').css('opacity',0.5); - $("#browsersteps-img").css('opacity',0.65); + $('#browser-steps-ui .loader .spinner').fadeIn(); + apply_buttons_disabled = true; + $('ul#browser_steps li .control .apply').css('opacity', 0.5); + $("#browsersteps-img").css('opacity', 0.65); var is_last_step = 0; var step_n = $(event.currentTarget).data('step-index'); @@ -368,17 +390,17 @@ $(document).ready(function () { } }); - if (is_last_step == (step_n+1)) { + if (is_last_step == (step_n + 1)) { is_last_step = true; } else { is_last_step = false; } - + console.log("Requesting step via POST " + $("select[id$='operation']", current_data).first().val()); // POST the currently clicked step form widget back and await response, redraw $.ajax({ method: "POST", - url: browser_steps_sync_url+"&browsersteps_session_id="+browsersteps_session_id, + url: browser_steps_sync_url + "&browsersteps_session_id=" + browsersteps_session_id, data: { 'operation': $("select[id$='operation']", current_data).first().val(), 'selector': $("input[id$='selector']", current_data).first().val(), @@ -391,28 +413,35 @@ $(document).ready(function () { // More than likely the CSRF token was lost when the server restarted alert("There was a problem processing the request, please reload the page."); $("#loading-status-text").hide(); + $('#browser-steps-ui .loader .spinner').fadeOut(); + }, + 401: function (data) { + // More than likely the CSRF token was lost when the server restarted + alert(data.responseText); + $("#loading-status-text").hide(); + $('#browser-steps-ui .loader .spinner').fadeOut(); } } }).done(function (data) { // it should return the new state (selectors available and screenshot) xpath_data = data.xpath_data; $('#browsersteps-img').attr('src', data.screenshot); - $('#browser-steps-ui .loader').fadeOut(); - apply_buttons_disabled=false; - $("#browsersteps-img").css('opacity',1); - $('ul#browser_steps li .control .apply').css('opacity',1); + $('#browser-steps-ui .loader .spinner').fadeOut(); + apply_buttons_disabled = false; + $("#browsersteps-img").css('opacity', 1); + $('ul#browser_steps li .control .apply').css('opacity', 1); browserless_seconds_remaining = data.browser_time_remaining; $("#loading-status-text").hide(); + set_first_gotosite_disabled(); }).fail(function (data) { console.log(data); if (data.responseText.includes("Browser session expired")) { disable_browsersteps_ui(); } - apply_buttons_disabled=false; + apply_buttons_disabled = false; $("#loading-status-text").hide(); - $('ul#browser_steps li .control .apply').css('opacity',1); - $("#browsersteps-img").css('opacity',1); - //$('#browsersteps-selector-wrapper .loader').fadeOut(2500); + $('ul#browser_steps li .control .apply').css('opacity', 1); + $("#browsersteps-img").css('opacity', 1); }); }); diff --git a/changedetectionio/static/styles/parts/browser-steps.scss b/changedetectionio/static/styles/parts/browser-steps.scss index cf9b4401..fda60bbe 100644 --- a/changedetectionio/static/styles/parts/browser-steps.scss +++ b/changedetectionio/static/styles/parts/browser-steps.scss @@ -6,6 +6,11 @@ } li { + &:not(:first-child) { + &:hover { + opacity: 1.0; + } + } list-style: decimal; padding: 5px; .control { @@ -70,6 +75,8 @@ transform: translate(-50%, -50%); margin-left: -40px; z-index: 100; + max-width: 350px; + text-align: center; } /* nice tall skinny one */ @@ -78,4 +85,10 @@ height: 80px; font-size: 3px; } + + #browsersteps-click-start { + &:hover { + cursor: pointer; + } + } } \ No newline at end of file diff --git a/changedetectionio/static/styles/styles.css b/changedetectionio/static/styles/styles.css index 16d7a7da..2f2bb625 100644 --- a/changedetectionio/static/styles/styles.css +++ b/changedetectionio/static/styles/styles.css @@ -50,6 +50,8 @@ nvm use v14.18.1 && npm install && npm run build #browser_steps li { list-style: decimal; padding: 5px; } + #browser_steps li:not(:first-child):hover { + opacity: 1.0; } #browser_steps li .control { padding-left: 5px; padding-right: 5px; } @@ -96,11 +98,15 @@ nvm use v14.18.1 && npm install && npm run build top: 50%; transform: translate(-50%, -50%); margin-left: -40px; - z-index: 100; } + z-index: 100; + max-width: 350px; + text-align: center; } #browsersteps-selector-wrapper .spinner, #browsersteps-selector-wrapper .spinner:after { width: 80px; height: 80px; font-size: 3px; } + #browsersteps-selector-wrapper #browsersteps-click-start:hover { + cursor: pointer; } .arrow { border: solid #1b98f8; diff --git a/changedetectionio/templates/edit.html b/changedetectionio/templates/edit.html index f78127b1..83eaf2a9 100644 --- a/changedetectionio/templates/edit.html +++ b/changedetectionio/templates/edit.html @@ -166,8 +166,12 @@ User-Agent: wonderbra 1.0") }}
- -
+ + +

Click here to Start

+ Please allow 10-15 seconds for the browser to connect. +
+