From dceace62b6ed14e757190d20fc6499baf4b32c65 Mon Sep 17 00:00:00 2001 From: Taka Date: Wed, 15 Aug 2018 03:26:01 +0000 Subject: [PATCH] Check for a dirty scenario before destructive operations. --- conftest.py | 10 +- vasl_templates/main_window.py | 44 ++++- vasl_templates/webapp/static/css/main.css | 3 + vasl_templates/webapp/static/main.js | 18 ++- vasl_templates/webapp/static/snippets.js | 130 ++++++++++----- vasl_templates/webapp/static/utils.js | 1 + .../tests/test_dirty_scenario_checks.py | 153 ++++++++++++++++++ .../webapp/tests/test_scenario_persistence.py | 82 +++++++--- .../webapp/tests/test_template_packs.py | 6 +- .../webapp/tests/test_vo_reports.py | 4 +- vasl_templates/webapp/tests/utils.py | 23 ++- 11 files changed, 387 insertions(+), 87 deletions(-) create mode 100644 vasl_templates/webapp/tests/test_dirty_scenario_checks.py diff --git a/conftest.py b/conftest.py index a75404e..72039ee 100644 --- a/conftest.py +++ b/conftest.py @@ -39,12 +39,12 @@ def pytest_addoption( parser ): # add test options parser.addoption( - "--no-clipboard", action="store_true", dest="no_clipboard", default=False, - help="Don't use the clipboard to get snippets." + "--short-tests", action="store_true", dest="short_tests", default=False, + help="Run a shorter version of the test suite." ) parser.addoption( - "--vo-reports", action="store_true", dest="check_vo_reports", default=False, - help="Check the vehicle/ordnance reports." + "--no-clipboard", action="store_true", dest="no_clipboard", default=False, + help="Don't use the clipboard to get snippets." ) # --------------------------------------------------------------------- @@ -62,6 +62,8 @@ def webapp(): # very quickly, causing problems by obscuring other elements and making them non-clickable :-/ # We used to explicitly dismiss them, but it's simpler to just always disable them. kwargs["store_msgs"] = 1 + # stop the browser from checking for a dirty scenario when leaving the page + kwargs["disable_close_window_check"] = 1 # check if the tests are being run headless if pytest.config.option.headless: #pylint: disable=no-member # yup - there is no clipboard support :-/ diff --git a/vasl_templates/main_window.py b/vasl_templates/main_window.py index 44deab3..6c3dea4 100644 --- a/vasl_templates/main_window.py +++ b/vasl_templates/main_window.py @@ -2,7 +2,7 @@ import os -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QFileDialog +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QFileDialog, QMessageBox from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineProfile, QWebEnginePage from PyQt5.QtGui import QDesktopServices from PyQt5.QtCore import QUrl @@ -23,6 +23,8 @@ class MainWindow( QWidget ): # initialize assert MainWindow._main_window is None MainWindow._main_window = self + self.view = None + self._is_closing = False # initialize super().__init__() @@ -38,19 +40,47 @@ class MainWindow( QWidget ): if not webapp.config.get( "DISABLE_WEBENGINEVIEW" ): # load the webapp # NOTE: We create an off-the-record profile to stop the view from using cached JS files :-/ - view = QWebEngineView() - layout.addWidget( view ) - profile = QWebEngineProfile( None, view ) + self.view = QWebEngineView() + layout.addWidget( self.view ) + profile = QWebEngineProfile( None, self.view ) profile.downloadRequested.connect( self.onDownloadRequested ) - page = QWebEnginePage( profile, view ) - view.setPage( page ) - view.load( QUrl(url) ) + page = QWebEnginePage( profile, self.view ) + self.view.setPage( page ) + self.view.load( QUrl(url) ) else: label = QLabel() label.setText( "Running the {} application.\n\nClose this window when you're done.".format( APP_NAME ) ) layout.addWidget( label ) QDesktopServices.openUrl( QUrl(url) ) + def closeEvent( self, evt ) : + """Handle requests to close the window (i.e. exit the application).""" + + # check if we need to check for a dirty scenario + if self.view is None or self._is_closing: + return + + # check if the scenario is dirty + def callback( is_dirty ): + """Callback for PyQt to return the result of running the Javascript.""" + if not is_dirty: + # nope - just close the window + self._is_closing = True + self.close() + return + # yup - ask the user to confirm the close + rc = QMessageBox.question( self, "Close program", + "This scenario has been changed\n\nDo you want to close the program, and lose your changes?", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No + ) + if rc == QMessageBox.Yes: + # confirmed - close the window + self._is_closing = True + self.close() + self.view.page().runJavaScript( "is_scenario_dirty()", callback ) + evt.ignore() # nb: we wait until the Javascript finishes to process the event + @staticmethod def onDownloadRequested( item ): """Handle download requests.""" diff --git a/vasl_templates/webapp/static/css/main.css b/vasl_templates/webapp/static/css/main.css index 9fb2242..efa0b63 100644 --- a/vasl_templates/webapp/static/css/main.css +++ b/vasl_templates/webapp/static/css/main.css @@ -109,6 +109,7 @@ button.edit-template img { height: 18px ; vertical-align: middle ; margin-right: .ui-dialog-titlebar { padding: 0.2em 0.5em 0.2em 0.5em !important ; } .ui-dialog-titlebar-close { margin-top: -10px !important ; } +.ui-dialog-content p { margin-bottom: 0.5em ; } #edit-template { overflow: hidden ; padding: 2px ; } .ui-dialog.edit-template .ui-dialog-titlebar { background: #f8d868 ; } @@ -122,6 +123,8 @@ button.edit-template img { height: 18px ; vertical-align: middle ; margin-right: .ui-dialog.edit-simple_note .ui-dialog-buttonpane { border: none ; padding: 0 ; font-size: 75% ; } .ui-dialog.edit-simple_note button { margin: 0 0 0 5px ; padding: 0.1em 0.2em ; } +.ui-dialog.ask .ui-dialog-titlebar { background: #ffa878 ; } + #select-vo { overflow: hidden ; padding: 2px ; } #select-vo .header { height: 1.75em ; margin-top: 0.25em ; font-size: 80% ; } #select-vo select { width: 100% ; top: 2em ; height: calc(100% - 1.5em) ; } diff --git a/vasl_templates/webapp/static/main.js b/vasl_templates/webapp/static/main.js index be25c4e..4982461 100644 --- a/vasl_templates/webapp/static/main.js +++ b/vasl_templates/webapp/static/main.js @@ -1,7 +1,7 @@ -var gTemplatePack = {} ; -var gDefaultNationalities = {} ; -var gValidTemplateIds = [] ; -var gVehicleOrdnanceListings = {} ; +gTemplatePack = {} ; +gDefaultNationalities = {} ; +gValidTemplateIds = [] ; +gVehicleOrdnanceListings = {} ; var _NATIONALITY_SPECIFIC_BUTTONS = { "russian": [ "mol", "mol-p" ], @@ -288,6 +288,16 @@ $(document).ready( function () { // initialize hotkeys init_hotkeys() ; + // check for a dirty scenario before leaving the page + if ( ! getUrlParam( "disable_close_window_check" ) ) { + window.addEventListener( "beforeunload", function(evt) { + if ( is_scenario_dirty() ) { + evt.returnValue = "This scenario has been changed. Do you want to leave the page, and lose your changes?" ; + return evt.returnValue ; + } + } ) ; + } + // add some dummy links for the test suite to edit templates if ( getUrlParam( "edit_template_links" ) ) { $("button.generate").each( function() { diff --git a/vasl_templates/webapp/static/snippets.js b/vasl_templates/webapp/static/snippets.js index 3557b17..33d107d 100644 --- a/vasl_templates/webapp/static/snippets.js +++ b/vasl_templates/webapp/static/snippets.js @@ -15,6 +15,7 @@ var _DAY_OF_MONTH_POSTFIXES = { // nb: we assume English :-/ } ; var gDefaultScenario = null ; +var gLastSavedScenario = null ; // -------------------------------------------------------------------- @@ -38,7 +39,7 @@ function generate_snippet( $btn, extra_params ) // unload the template parameters var template_id = $btn.data( "id" ) ; - unload_params( params, true ) ; + unload_snippet_params( params, true ) ; // set player-specific parameters var nationalities = gTemplatePack.nationalities ; @@ -191,7 +192,7 @@ function generate_snippet( $btn, extra_params ) // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -function unload_params( params, check_date_capabilities ) +function unload_snippet_params( params, check_date_capabilities ) { // collect all the template parameters add_param = function($elem) { params[ $elem.attr("name") ] = $elem.val() ; } ; @@ -439,19 +440,30 @@ function edit_template( template_id ) function on_load_scenario() { - // FIXME! confirm this operation if the scenario is dirty - - // FOR TESTING PORPOISES! We can't control a file upload from Selenium (since - // the browser will use native controls), so we store the result in a
). - if ( getUrlParam( "scenario_persistence" ) ) { - var $elem = $( "#_scenario-persistence_" ) ; - do_load_scenario( JSON.parse( $elem.val() ) ) ; - showInfoMsg( "The scenario was loaded." ) ; // nb: the tests are looking for this - return ; + // check if the scenario is dirty + if ( ! is_scenario_dirty() ) + do_on_load_scenario() ; + else { + // yup - confirm the operation + ask( "Load scenario", "

This scenario has been changed.

Do you want load another scenario, and lose your changes?", { + ok: do_on_load_scenario, + cancel: function() {}, + } ) ; } - // ask the user to upload the scenario file - $("#load-scenario").trigger( "click" ) ; + function do_on_load_scenario() { + // FOR TESTING PORPOISES! We can't control a file upload from Selenium (since + // the browser will use native controls), so we get the data from a