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('
') + var s = '