From da8a9a45913fe675ed27b2390f9be20c7d86f80e Mon Sep 17 00:00:00 2001 From: Sarah McAlear and Tira Odhner Date: Tue, 4 Apr 2017 11:24:44 -0400 Subject: [PATCH] Allow selecting columns in query output - Add row, column, grid selector to SQLEditor - Update README with xsel package to enable testing with Pyperclip on linux - Refactor copying of data - Deselect rows when clicking on column header - Write a new row selection plugin to replace the problematic checkboxselectcolumn plugin - the entire cell is clickable - Extract the generic selection methods to rangeSelectionHelper - just business logic and rendering logic are in row/column selectors. --- .../copy_selected_query_results_feature_test.py | 67 ++++++ web/pgadmin/static/js/selection/column_selector.js | 92 ++++++++ web/pgadmin/static/js/selection/copy_data.js | 42 ++++ web/pgadmin/static/js/selection/grid_selector.js | 79 +++++++ .../js/selection/range_boundary_navigator.js | 97 +++++++++ .../static/js/selection/range_selection_helper.js | 60 ++++++ web/pgadmin/static/js/selection/row_selector.js | 85 ++++++++ .../tools/sqleditor/static/css/sqleditor.css | 9 + .../sqleditor/templates/sqleditor/js/sqleditor.js | 61 +----- web/regression/README | 6 + web/regression/feature_utils/base_feature_test.py | 1 + web/regression/feature_utils/pgadmin_page.py | 11 + .../javascript/selection/column_selector_spec.js | 235 +++++++++++++++++++++ .../javascript/selection/copy_data_spec.js | 95 +++++++++ .../javascript/selection/grid_selector_spec.js | 126 +++++++++++ .../selection/range_boundary_navigator_spec.js | 139 ++++++++++++ .../javascript/selection/row_selector_spec.js | 174 +++++++++++++++ web/regression/python_test_utils/test_utils.py | 1 + web/regression/requirements.txt | 1 + 19 files changed, 1329 insertions(+), 52 deletions(-) create mode 100644 web/pgadmin/feature_tests/copy_selected_query_results_feature_test.py create mode 100644 web/pgadmin/static/js/selection/column_selector.js create mode 100644 web/pgadmin/static/js/selection/copy_data.js create mode 100644 web/pgadmin/static/js/selection/grid_selector.js create mode 100644 web/pgadmin/static/js/selection/range_boundary_navigator.js create mode 100644 web/pgadmin/static/js/selection/range_selection_helper.js create mode 100644 web/pgadmin/static/js/selection/row_selector.js create mode 100644 web/regression/javascript/selection/column_selector_spec.js create mode 100644 web/regression/javascript/selection/copy_data_spec.js create mode 100644 web/regression/javascript/selection/grid_selector_spec.js create mode 100644 web/regression/javascript/selection/range_boundary_navigator_spec.js create mode 100644 web/regression/javascript/selection/row_selector_spec.js diff --git a/web/pgadmin/feature_tests/copy_selected_query_results_feature_test.py b/web/pgadmin/feature_tests/copy_selected_query_results_feature_test.py new file mode 100644 index 00000000..2f0bd336 --- /dev/null +++ b/web/pgadmin/feature_tests/copy_selected_query_results_feature_test.py @@ -0,0 +1,67 @@ +import pyperclip +import time + +from selenium.webdriver import ActionChains + +from regression.python_test_utils import test_utils +from regression.feature_utils.base_feature_test import BaseFeatureTest + + +class CopySelectedQueryResultsFeatureTest(BaseFeatureTest): + def before(self): + connection = test_utils.get_db_connection(self.server['db'], + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port']) + test_utils.drop_database(connection, "acceptance_test_db") + test_utils.create_database(self.server, "acceptance_test_db") + test_utils.create_table(self.server, "acceptance_test_db", "test_table") + self.page.add_server(self.server) + + def runTest(self): + self.page.toggle_open_tree_item(self.server['name']) + self.page.toggle_open_tree_item('Databases') + self.page.toggle_open_tree_item('acceptance_test_db') + time.sleep(5) + self.page.find_by_partial_link_text("Tools").click() + self.page.find_by_partial_link_text("Query Tool").click() + self.page.click_tab('Query-1') + time.sleep(5) + ActionChains(self.page.driver).send_keys("SELECT * FROM test_table").perform() + self.page.driver.switch_to_frame(self.page.driver.find_element_by_tag_name("iframe")) + self.page.find_by_id("btn-flash").click() + + self._copies_rows() + self._copies_columns() + + def _copies_rows(self): + pyperclip.copy("old clipboard contents") + time.sleep(5) + self.page.find_by_xpath("//*[contains(@class, 'sr')]/*[1]/input[@type='checkbox']").click() + self.page.find_by_xpath("//*[@id='btn-copy-row']").click() + + self.assertEqual("'Some-Name','6'", + pyperclip.paste()) + + def _copies_columns(self): + pyperclip.copy("old clipboard contents") + + self.page.find_by_xpath("//*[@data-test='output-column-header' and contains(., 'some_column')]/input").click() + self.page.find_by_xpath("//*[@id='btn-copy-row']").click() + + self.assertEqual( + """'Some-Name' +'Some-Other-Name'""", + pyperclip.paste()) + + def after(self): + self.page.close_query_tool() + self.page.remove_server(self.server) + + connection = test_utils.get_db_connection(self.server['db'], + self.server['username'], + self.server['db_password'], + self.server['host'], + self.server['port']) + test_utils.drop_database(connection, "acceptance_test_db") diff --git a/web/pgadmin/static/js/selection/column_selector.js b/web/pgadmin/static/js/selection/column_selector.js new file mode 100644 index 00000000..c89b3fa8 --- /dev/null +++ b/web/pgadmin/static/js/selection/column_selector.js @@ -0,0 +1,92 @@ +define(['jquery', 'sources/selection/range_selection_helper', 'slickgrid'], function ($, rangeSelectionHelper) { + var ColumnSelector = function () { + var init = function (grid) { + grid.onHeaderClick.subscribe(function (event, eventArgument) { + var column = eventArgument.column; + + if (column.selectable !== false) { + + if (!clickedCheckbox(event)) { + var $checkbox = $("[data-id='checkbox-" + column.id + "']"); + toggleCheckbox($checkbox); + } + + updateRanges(grid, column.id); + } + } + ); + grid.getSelectionModel().onSelectedRangesChanged + .subscribe(handleSelectedRangesChanged.bind(null, grid)); + }; + + var handleSelectedRangesChanged = function (grid, event, ranges) { + $('[data-cell-type="column-header-row"] input:checked') + .each(function (index, checkbox) { + var $checkbox = $(checkbox); + var columnIndex = grid.getColumnIndex($checkbox.data('column-id')); + var isStillSelected = rangeSelectionHelper.isRangeSelected(ranges, rangeSelectionHelper.rangeForColumn(grid, columnIndex)); + if (!isStillSelected) { + toggleCheckbox($checkbox); + } + }); + }; + + var updateRanges = function (grid, columnId) { + var selectionModel = grid.getSelectionModel(); + var ranges = selectionModel.getSelectedRanges(); + + var columnIndex = grid.getColumnIndex(columnId); + + var columnRange = rangeSelectionHelper.rangeForColumn(grid, columnIndex); + var newRanges; + if (rangeSelectionHelper.isRangeSelected(ranges, columnRange)) { + newRanges = rangeSelectionHelper.removeRange(ranges, columnRange); + } else { + if (rangeSelectionHelper.areAllRangesColumns(ranges, grid)) { + newRanges = rangeSelectionHelper.addRange(ranges, columnRange); + } else { + newRanges = [columnRange]; + } + } + selectionModel.setSelectedRanges(newRanges); + }; + + var clickedCheckbox = function (e) { + return e.target.type == "checkbox" + }; + + var toggleCheckbox = function (checkbox) { + if (checkbox.prop("checked")) { + checkbox.prop("checked", false) + } else { + checkbox.prop("checked", true) + } + }; + + var getColumnDefinitionsWithCheckboxes = function (columnDefinitions) { + return _.map(columnDefinitions, function (columnDefinition) { + if (columnDefinition.selectable !== false) { + var name = + "" + + " " + + " " + columnDefinition.name + "" + + ""; + return _.extend(columnDefinition, { + name: name + }); + } else { + return columnDefinition; + } + }); + }; + + $.extend(this, { + "init": init, + "getColumnDefinitionsWithCheckboxes": getColumnDefinitionsWithCheckboxes + }); + }; + return ColumnSelector; +}); diff --git a/web/pgadmin/static/js/selection/copy_data.js b/web/pgadmin/static/js/selection/copy_data.js new file mode 100644 index 00000000..b640235e --- /dev/null +++ b/web/pgadmin/static/js/selection/copy_data.js @@ -0,0 +1,42 @@ +define(['jquery', 'underscore', 'sources/selection/clipboard', 'sources/selection/range_boundary_navigator'], function ($, _, clipboard, rangeBoundaryNavigator) { + var copyData = function () { + var self = this; + + // Disable copy button + $("#btn-copy-row").prop('disabled', true); + // Enable paste button + if (self.can_edit) { + $("#btn-paste-row").prop('disabled', false); + } + + var grid = self.slickgrid; + var columnDefinitions = grid.getColumns(); + var selectedRanges = grid.getSelectionModel().getSelectedRanges(); + var data = grid.getData(); + var rows = grid.getSelectedRows(); + + + if (allTheRangesAreFullRows(selectedRanges, columnDefinitions)) { + self.copied_rows = rows.map(function (rowIndex) { + return data[rowIndex]; + }); + } else { + self.copied_rows = []; + } + + var csvText = rangeBoundaryNavigator.rangesToCsv(data, columnDefinitions, selectedRanges); + + // If there is something to set into clipboard + if (csvText) + clipboard.copyTextToClipboard(csvText); + }; + + var allTheRangesAreFullRows = function (ranges, columnDefinitions){ + var colRangeBounds = ranges.map(function (range) { + return [range.fromCell, range.toCell]; + }); + return _.isEqual(_.union.apply(null, colRangeBounds), [0, columnDefinitions.length - 1]); + }; + + return copyData +}); \ No newline at end of file diff --git a/web/pgadmin/static/js/selection/grid_selector.js b/web/pgadmin/static/js/selection/grid_selector.js new file mode 100644 index 00000000..31aee69f --- /dev/null +++ b/web/pgadmin/static/js/selection/grid_selector.js @@ -0,0 +1,79 @@ +define(['jquery', 'sources/selection/column_selector', 'sources/selection/row_selector'], + function ($, ColumnSelector, RowSelector) { + var Slick = window.Slick; + + var GridSelector = function (columnDefinitions) { + var rowSelector = new RowSelector(columnDefinitions); + var columnSelector = new ColumnSelector(columnDefinitions); + + var init = function (grid) { + this.grid = grid; + grid.onHeaderClick.subscribe(function (event, eventArguments) { + if (eventArguments.column.selectAllOnClick) { + toggleSelectAll(grid); + } + }); + + grid.getSelectionModel().onSelectedRangesChanged + .subscribe(handleSelectedRangesChanged.bind(null, grid)); + grid.registerPlugin(rowSelector); + grid.registerPlugin(columnSelector); + }; + + var getColumnDefinitionsWithCheckboxes = function (columnDefinitions) { + columnDefinitions = columnSelector.getColumnDefinitionsWithCheckboxes(columnDefinitions); + columnDefinitions = rowSelector.getColumnDefinitionsWithCheckboxes(columnDefinitions); + + columnDefinitions[0].selectAllOnClick = true; + columnDefinitions[0].name = '' + columnDefinitions[0].name; + return columnDefinitions; + }; + + function handleSelectedRangesChanged(grid) { + $("[data-id='checkbox-select-all']").prop("checked", isEntireGridSelected(grid)); + } + + function isEntireGridSelected(grid) { + var selectionModel = grid.getSelectionModel(); + var selectedRanges = selectionModel.getSelectedRanges(); + return selectedRanges.length == 1 && isSameRange(selectedRanges[0], getRangeOfWholeGrid(grid)); + } + + function toggleSelectAll(grid) { + if (isEntireGridSelected(grid)) { + deselect(grid); + } else { + selectAll(grid) + } + } + + var isSameRange = function (range, otherRange) { + return range.fromCell == otherRange.fromCell && range.toCell == otherRange.toCell && + range.fromRow == otherRange.fromRow && range.toRow == otherRange.toRow; + }; + + function getRangeOfWholeGrid(grid) { + return new Slick.Range(0, 1, grid.getDataLength() - 1, grid.getColumns().length - 1); + } + + function deselect(grid) { + var selectionModel = grid.getSelectionModel(); + selectionModel.setSelectedRanges([]); + } + + function selectAll(grid) { + var range = getRangeOfWholeGrid(grid); + var selectionModel = grid.getSelectionModel(); + + selectionModel.setSelectedRanges([range]); + } + + $.extend(this, { + "init": init, + "getColumnDefinitionsWithCheckboxes": getColumnDefinitionsWithCheckboxes + }); + }; + + return GridSelector; + }); diff --git a/web/pgadmin/static/js/selection/range_boundary_navigator.js b/web/pgadmin/static/js/selection/range_boundary_navigator.js new file mode 100644 index 00000000..f7d64b97 --- /dev/null +++ b/web/pgadmin/static/js/selection/range_boundary_navigator.js @@ -0,0 +1,97 @@ +define(function () { + return { + getUnion: function (dimensionBounds) { + dimensionBounds.sort(); + var boundsUnion = []; + if (!_.isEmpty(dimensionBounds)) { + boundsUnion = [dimensionBounds[0]]; + } + + dimensionBounds.forEach(function (bound) { + var previousBound = _.last(boundsUnion); + if (bound[0] <= previousBound[1] + 1) { + if (bound[1] > previousBound[1]) { + previousBound[1] = bound[1]; + } + } else { + boundsUnion.push(bound); + } + }); + return boundsUnion; + }, + + mapDimensionBoundaryUnion: function (unionedDimensionBoundaries, iteratee) { + var mapResult = []; + unionedDimensionBoundaries.forEach(function (subrange) { + for (var index = subrange[0]; index <= subrange[1]; index += 1) { + mapResult.push(iteratee(index)); + } + }); + return mapResult; + }, + + mapOver2DArray: function (rowRangeBounds, colRangeBounds, processCell, rowCollector) { + var unionedRowRanges = this.getUnion(rowRangeBounds); + var unionedColRanges = this.getUnion(colRangeBounds); + + return this.mapDimensionBoundaryUnion(unionedRowRanges, function (rowId) { + var rowData = this.mapDimensionBoundaryUnion(unionedColRanges, function (colId) { + return processCell(rowId, colId); + }); + return rowCollector(rowData); + }.bind(this)); + }, + + rangesToCsv: function (data, columnDefinitions, selectedRanges) { + var rowRangeBounds = selectedRanges.map(function (range) { + return [range.fromRow, range.toRow]; + }); + var colRangeBounds = selectedRanges.map(function (range) { + return [range.fromCell, range.toCell]; + }); + + if(_.isUndefined(columnDefinitions[0].pos)){ + colRangeBounds = this.removeFirstColumn(colRangeBounds); + } + + var csvRows = this.mapOver2DArray(rowRangeBounds, colRangeBounds, this.csvCell.bind(this, data, columnDefinitions), function (rowData) { + return rowData.join(','); + }); + return csvRows.join('\n'); + }, + + removeFirstColumn: function (colRangeBounds) { + var unionedColRanges = this.getUnion(colRangeBounds); + + var firstSubrangeStartsAt0 = function () { + return unionedColRanges[0][0] == 0; + }; + + function firstSubrangeIsJustFirstColumn() { + return unionedColRanges[0][1] == 0; + } + + if(firstSubrangeStartsAt0()){ + if(firstSubrangeIsJustFirstColumn()){ + unionedColRanges.shift(); + } else { + unionedColRanges[0][0] = 1; + } + } + return unionedColRanges; + }, + + csvCell: function (data, columnDefinitions, rowId, colId) { + var val = data[rowId][columnDefinitions[colId].pos]; + + if (val && _.isObject(val)) { + val = "'" + JSON.stringify(val) + "'"; + } else if (val && typeof val != "number" && typeof val != "boolean") { + val = "'" + val.toString() + "'"; + } else if (_.isNull(val) || _.isUndefined(val)) { + val = ''; + } + return val; + } + }; +}); \ No newline at end of file diff --git a/web/pgadmin/static/js/selection/range_selection_helper.js b/web/pgadmin/static/js/selection/range_selection_helper.js new file mode 100644 index 00000000..c219fcf1 --- /dev/null +++ b/web/pgadmin/static/js/selection/range_selection_helper.js @@ -0,0 +1,60 @@ +define(['slickgrid'], function () { + var Slick = window.Slick; + + var isSameRange = function (range, otherRange) { + + return range.fromCell == otherRange.fromCell && range.toCell == otherRange.toCell && + range.fromRow == otherRange.fromRow && range.toRow == otherRange.toRow; + }; + + var isRangeSelected = function (selectedRanges, range) { + return _.any(selectedRanges, function (selectedRange) { + return isSameRange(selectedRange, range) + }) + }; + + + var removeRange = function (selectedRanges, range) { + return _.filter(selectedRanges, function (selectedRange) { + return !(isSameRange(selectedRange, range)) + }) + }; + + var addRange = function (ranges, range) { + ranges.push(range); + return ranges; + }; + + + var areAllRangesRows = function (ranges, grid) { + return _.every(ranges, function (range) { + return range.fromRow == range.toRow && + range.fromCell == 1 && range.toCell == grid.getColumns().length - 1 + }) + }; + + var areAllRangesColumns = function (ranges, grid) { + return _.every(ranges, function (range) { + return range.fromCell == range.toCell && + range.fromRow == 0 && range.toRow == grid.getDataLength() - 1 + }) + }; + + var rangeForRow = function (grid, rowId) { + return new Slick.Range(rowId, 1, rowId, grid.getColumns().length - 1); + }; + + function rangeForColumn(grid, columnIndex) { + return new Slick.Range(0, columnIndex, grid.getDataLength() - 1, columnIndex) + } + + return { + addRange: addRange, + removeRange: removeRange, + isRangeSelected: isRangeSelected, + areAllRangesRows: areAllRangesRows, + areAllRangesColumns: areAllRangesColumns, + rangeForRow: rangeForRow, + rangeForColumn: rangeForColumn + } +}); \ No newline at end of file diff --git a/web/pgadmin/static/js/selection/row_selector.js b/web/pgadmin/static/js/selection/row_selector.js new file mode 100644 index 00000000..76a8c1a7 --- /dev/null +++ b/web/pgadmin/static/js/selection/row_selector.js @@ -0,0 +1,85 @@ +define(['jquery', 'sources/selection/range_selection_helper', 'slickgrid'], function ($, rangeSelectionHelper) { + var RowSelector = function () { + var Slick = window.Slick; + + var gridEventBus = new Slick.EventHandler(); + + var init = function (grid) { + grid.getSelectionModel() + .onSelectedRangesChanged.subscribe(handleSelectedRangesChanged.bind(null, grid)); + gridEventBus + .subscribe(grid.onClick, handleClick.bind(null, grid)) + }; + + var handleClick = function (grid, event, args) { + if (grid.getColumns()[args.cell].id === 'row-header-column') { + if (event.target.type != "checkbox") { + var checkbox = $(event.target).find('input[type="checkbox"]'); + toggleCheckbox($(checkbox)); + } + updateRanges(grid, args.row); + } + } + + var handleSelectedRangesChanged = function (grid, event, ranges) { + $('[data-cell-type="row-header-checkbox"]:checked') + .each(function (index, checkbox) { + var $checkbox = $(checkbox); + var row = parseInt($checkbox.data('row')); + var isStillSelected = rangeSelectionHelper.isRangeSelected(ranges, + rangeSelectionHelper.rangeForRow(grid, row)); + if (!isStillSelected) { + toggleCheckbox($checkbox); + } + }); + } + + var updateRanges = function (grid, rowId) { + var selectionModel = grid.getSelectionModel(); + var ranges = selectionModel.getSelectedRanges(); + + var rowRange = rangeSelectionHelper.rangeForRow(grid, rowId); + + var newRanges; + if (rangeSelectionHelper.isRangeSelected(ranges, rowRange)) { + newRanges = rangeSelectionHelper.removeRange(ranges, rowRange); + } else { + if (rangeSelectionHelper.areAllRangesRows(ranges, grid)) { + newRanges = rangeSelectionHelper.addRange(ranges, rowRange); + } else { + newRanges = [rowRange]; + } + } + selectionModel.setSelectedRanges(newRanges); + } + + var toggleCheckbox = function (checkbox) { + if (checkbox.prop("checked")) { + checkbox.prop("checked", false) + } else { + checkbox.prop("checked", true) + } + }; + + var getColumnDefinitionsWithCheckboxes = function (columnDefinitions) { + columnDefinitions.unshift({ + id: 'row-header-column', + name: '', + selectable: false, + focusable: false, + formatter: function (rowIndex) { + return '' + } + }); + return columnDefinitions; + }; + + $.extend(this, { + "init": init, + "getColumnDefinitionsWithCheckboxes": getColumnDefinitionsWithCheckboxes + }); + }; + return RowSelector; +}); diff --git a/web/pgadmin/tools/sqleditor/static/css/sqleditor.css b/web/pgadmin/tools/sqleditor/static/css/sqleditor.css index 6a6f1f8f..d71fc8cf 100644 --- a/web/pgadmin/tools/sqleditor/static/css/sqleditor.css +++ b/web/pgadmin/tools/sqleditor/static/css/sqleditor.css @@ -358,6 +358,10 @@ li { padding: 4px 0 4px 6px; } +.column-description { + display: table-cell; +} + .long_text_editor { margin-left: 5px; font-size: 12px !important; @@ -419,6 +423,11 @@ input.editor-checkbox:focus { background: #e46b6b; } +/* color the first column */ +.sr .sc:first-child { + background-color: #2c76b4; +} + #datagrid div.slick-header.ui-state-default { background: #2c76b4; } diff --git a/web/pgadmin/tools/sqleditor/templates/sqleditor/js/sqleditor.js b/web/pgadmin/tools/sqleditor/templates/sqleditor/js/sqleditor.js index 1bda0679..70412a5f 100644 --- a/web/pgadmin/tools/sqleditor/templates/sqleditor/js/sqleditor.js +++ b/web/pgadmin/tools/sqleditor/templates/sqleditor/js/sqleditor.js @@ -2,7 +2,8 @@ define( [ 'jquery', 'underscore', 'underscore.string', 'alertify', 'pgadmin', 'backbone', 'backgrid', 'codemirror', 'pgadmin.misc.explain', - 'sources/selection/clipboard', + 'sources/selection/grid_selector', 'sources/selection/clipboard', + 'sources/selection/copy_data', 'slickgrid', 'bootstrap', 'pgadmin.browser', 'wcdocker', 'codemirror/mode/sql/sql', 'codemirror/addon/selection/mark-selection', @@ -21,13 +22,12 @@ define( 'slickgrid/plugins/slick.cellrangedecorator', 'slickgrid/plugins/slick.cellrangeselector', 'slickgrid/plugins/slick.cellselectionmodel', - 'slickgrid/plugins/slick.checkboxselectcolumn', 'slickgrid/plugins/slick.cellcopymanager', 'slickgrid/plugins/slick.rowselectionmodel', 'slickgrid/slick.grid' ], function( - $, _, S, alertify, pgAdmin, Backbone, Backgrid, CodeMirror, pgExplain, clipboard + $, _, S, alertify, pgAdmin, Backbone, Backgrid, CodeMirror, pgExplain, GridSelector, clipboard, copyData ) { /* Return back, this has been called more than once */ if (pgAdmin.SqlEditor) @@ -549,14 +549,7 @@ define( collection = []; } - var grid_columns = new Array(), - checkboxSelector; - - checkboxSelector = new Slick.CheckboxSelectColumn({ - cssClass: "sc-cb" - }); - - grid_columns.push(checkboxSelector.getColumnDefinition()); + var grid_columns = []; var grid_width = $($('#editor-panel').find('.wcFrame')[1]).width() _.each(columns, function(c) { @@ -592,6 +585,9 @@ define( grid_columns.push(options) }); + var gridSelector = new GridSelector(); + grid_columns = gridSelector.getColumnDefinitionsWithCheckboxes(grid_columns); + var grid_options = { editable: true, enableAddRow: is_editable, @@ -635,7 +631,7 @@ define( var grid = new Slick.Grid($data_grid, collection, grid_columns, grid_options); grid.registerPlugin( new Slick.AutoTooltips({ enableForHeaderCells: false }) ); grid.setSelectionModel(new Slick.RowSelectionModel({selectActiveRow: false})); - grid.registerPlugin(checkboxSelector); + grid.registerPlugin(gridSelector); var editor_data = { keys: self.handler.primary_keys, @@ -2947,46 +2943,7 @@ define( }, // This function will copy the selected row. - _copy_row: function() { - var self = this, grid, data, rows, copied_text = ''; - - self.copied_rows = []; - - // Disable copy button - $("#btn-copy-row").prop('disabled', true); - // Enable paste button - if(self.can_edit) { - $("#btn-paste-row").prop('disabled', false); - } - - grid = self.slickgrid; - data = grid.getData(); - rows = grid.getSelectedRows(); - // Iterate over all the selected rows & fetch data - for (var i = 0; i < rows.length; i += 1) { - var idx = rows[i], - _rowData = data[idx], - _values = []; - self.copied_rows.push(_rowData); - // Convert it as CSV for clipboard - for (var j = 0; j < self.columns.length; j += 1) { - var val = _rowData[self.columns[j].pos]; - if(val && _.isObject(val)) - val = "'" + JSON.stringify(val) + "'"; - else if(val && typeof val != "number" && typeof true != "boolean") - val = "'" + val.toString() + "'"; - else if (_.isNull(val) || _.isUndefined(val)) - val = ''; - _values.push(val); - } - // Append to main text string - if(_values.length > 0) - copied_text += _values.toString() + "\n"; - } - // If there is something to set into clipboard - if(copied_text) - clipboard.copyTextToClipboard(copied_text); - }, + _copy_row: copyData, // This function will paste the selected row. _paste_row: function() { diff --git a/web/regression/README b/web/regression/README index 2eb5c65a..72edaaf8 100644 --- a/web/regression/README +++ b/web/regression/README @@ -22,6 +22,12 @@ installed with: (pgadmin4) $ pip install -r $PGADMIN4_SRC/web/regression/requirements.txt +While running in Linux environments install: +sudo apt-get install xsel + +Otherwise the following error happens: +"Pyperclip could not find a copy/paste mechanism for your system" + General Information ------------------- diff --git a/web/regression/feature_utils/base_feature_test.py b/web/regression/feature_utils/base_feature_test.py index 8bb6bc00..dc704e8e 100644 --- a/web/regression/feature_utils/base_feature_test.py +++ b/web/regression/feature_utils/base_feature_test.py @@ -28,6 +28,7 @@ class BaseFeatureTest(BaseTestGenerator): self.page = PgadminPage(self.driver, app_config) try: + self.page.driver.switch_to.default_content() self.page.wait_for_app() self.page.wait_for_spinner_to_disappear() self.page.reset_layout() diff --git a/web/regression/feature_utils/pgadmin_page.py b/web/regression/feature_utils/pgadmin_page.py index aa31bc04..c1995966 100644 --- a/web/regression/feature_utils/pgadmin_page.py +++ b/web/regression/feature_utils/pgadmin_page.py @@ -55,7 +55,18 @@ class PgadminPage: self.find_by_xpath("//*[@id='tree']//*[.='" + server_config['name'] + "']") + def close_query_tool(self): + self.driver.switch_to.default_content() + tab = self.find_by_xpath("//*[contains(@class,'wcPanelTab') and contains(.,'" + "Query" + "')]") + ActionChains(self.driver).context_click(tab).perform() + self.find_by_xpath("//li[contains(@class, 'context-menu-item')]/span[contains(text(), 'Remove Panel')]").click() + self.driver.switch_to.frame(self.driver.find_elements_by_tag_name("iframe")[0]) + time.sleep(.5) + self.click_element(self.find_by_xpath('//button[contains(@class, "ajs-button") and contains(.,"Yes")]')) + self.driver.switch_to.default_content() + def remove_server(self, server_config): + self.driver.switch_to.default_content() self.find_by_xpath("//*[@id='tree']//*[.='" + server_config['name'] + "' and @class='aciTreeItem']").click() self.find_by_partial_link_text("Object").click() self.find_by_partial_link_text("Delete/Drop").click() diff --git a/web/regression/javascript/selection/column_selector_spec.js b/web/regression/javascript/selection/column_selector_spec.js new file mode 100644 index 00000000..947f4852 --- /dev/null +++ b/web/regression/javascript/selection/column_selector_spec.js @@ -0,0 +1,235 @@ +define( + ["jquery", + "underscore", + "slickgrid/slick.grid", + "sources/selection/column_selector", + "slickgrid/slick.rowselectionmodel", + "slickgrid" + ], + function ($, _, SlickGrid, ColumnSelector, RowSelectionModel, Slick) { + describe("ColumnSelector", function () { + var container, data, columns, options; + beforeEach(function () { + container = $("
"); + container.height(9999); + + data = [{'some-column-name': 'first value', 'second column': 'second value'}]; + + columns = [ + { + id: '1', + name: 'some-column-name', + }, + { + id: '2', + name: 'second column', + }, + { + name: 'some-non-selectable-column', + selectable: false + } + ] + }); + + describe("when a column is not selectable", function () { + it("does not create a checkbox for selecting the column", function () { + var checkboxColumn = { + name: 'some-column-name-4', + selectable: false + }; + columns.push(checkboxColumn); + + setupGrid(columns); + + expect(container.find('.slick-header-columns input').length).toBe(2) + }); + }); + + it("renders a checkbox in the column header", function () { + setupGrid(columns); + + expect(container.find('.slick-header-columns input').length).toBe(2) + }); + + it("displays the name of the column", function () { + setupGrid(columns); + + expect($(container.find('.slick-header-columns .slick-column-name')[0]).text()) + .toContain('some-column-name'); + expect($(container.find('.slick-header-columns .slick-column-name')[1]).text()) + .toContain('second column'); + }); + + it("preserves the other attributes of column definitions", function () { + var columnSelector = new ColumnSelector(); + var selectableColumns = columnSelector.getColumnDefinitionsWithCheckboxes(columns); + + expect(selectableColumns[0].id).toBe('1'); + }); + + describe("selecting columns", function () { + var grid, rowSelectionModel; + beforeEach(function () { + var columnSelector = new ColumnSelector(); + columns = columnSelector.getColumnDefinitionsWithCheckboxes(columns); + data = []; + for (var i = 0; i < 10; i++) { + data.push({'some-column-name': 'some-value-' + i, 'second column': 'second value ' + i}); + } + grid = new SlickGrid(container, data, columns, options); + + rowSelectionModel = new RowSelectionModel(); + grid.setSelectionModel(rowSelectionModel); + + grid.registerPlugin(columnSelector); + grid.invalidate(); + $("body").append(container); + }); + + afterEach(function () { + $("body").find(container).remove(); + }); + + describe("when the user clicks a column header", function () { + it("selects the column", function () { + container.find('.slick-header-column')[0].click(); + var selectedRanges = rowSelectionModel.getSelectedRanges(); + expectOnlyTheFirstColumnToBeSelected(selectedRanges); + }); + }); + + describe("when the user clicks additional column headers", function () { + beforeEach(function () { + container.find('.slick-header-column')[1].click(); + }); + + it("selects additional columns", function () { + container.find('.slick-header-column')[0].click(); + + var selectedRanges = rowSelectionModel.getSelectedRanges(); + var column1 = selectedRanges[0]; + + expect(selectedRanges.length).toEqual(2); + expect(column1.fromCell).toBe(1); + expect(column1.toCell).toBe(1); + + var column2 = selectedRanges[1]; + + expect(column2.fromCell).toBe(0); + expect(column2.toCell).toBe(0); + }); + }); + + describe("when the user clicks a column header checkbox", function () { + it("selects the column", function () { + container.find('.slick-header-columns input')[0].click(); + + var selectedRanges = rowSelectionModel.getSelectedRanges(); + expectOnlyTheFirstColumnToBeSelected(selectedRanges); + }); + + it("checks the checkbox", function () { + container.find('.slick-header-column')[1].click(); + expect($(container.find('.slick-header-columns input')[1]).is(':checked')).toBeTruthy(); + }); + }); + + describe("when a row is selected", function () { + beforeEach(function () { + var selectedRanges = [new Slick.Range(0, 0, 0, 1)]; + rowSelectionModel.setSelectedRanges(selectedRanges); + }); + + it("deselects the row", function () { + container.find('.slick-header-column')[1].click(); + var selectedRanges = rowSelectionModel.getSelectedRanges(); + + expect(selectedRanges.length).toBe(1); + + var column = selectedRanges[0]; + + expect(column.fromCell).toBe(1); + expect(column.toCell).toBe(1); + expect(column.fromRow).toBe(0); + expect(column.toRow).toBe(9); + }) + }); + + describe("clicking a second time", function () { + beforeEach(function () { + container.find('.slick-header-column')[1].click(); + }); + + it("unchecks checkbox", function () { + container.find('.slick-header-column')[1].click(); + expect($(container.find('.slick-header-columns input')[1]).is(':checked')).toBeFalsy(); + }); + + it("deselects the column", function () { + container.find('.slick-header-column')[1].click(); + var selectedRanges = rowSelectionModel.getSelectedRanges(); + + expect(selectedRanges.length).toEqual(0); + }) + }); + + describe("when the column is not selectable", function () { + it("does not select the column", function () { + $(container.find('.slick-header-column:contains(some-non-selectable-column)')).click(); + var selectedRanges = rowSelectionModel.getSelectedRanges(); + + expect(selectedRanges.length).toEqual(0); + }); + }); + + describe("when the column is deselected through setSelectedRanges", function () { + beforeEach(function () { + container.find('.slick-header-column')[1].click(); + }); + + it("unchecks the checkbox", function () { + rowSelectionModel.setSelectedRanges([]); + + expect($(container.find('.slick-header-columns input')[1]) + .is(':checked')).toBeFalsy(); + }); + }); + + describe("when a non-column range was already selected", function () { + beforeEach(function () { + var selectedRanges = [new Slick.Range(0, 0, 1, 0)]; + rowSelectionModel.setSelectedRanges(selectedRanges); + }); + + it("deselects the non-column range", function () { + container.find('.slick-header-column')[0].click(); + + var selectedRanges = rowSelectionModel.getSelectedRanges(); + expectOnlyTheFirstColumnToBeSelected(selectedRanges); + }) + }); + }); + + var setupGrid = function (columns) { + var columnSelector = new ColumnSelector(); + columns = columnSelector.getColumnDefinitionsWithCheckboxes(columns); + var grid = new SlickGrid(container, data, columns, options); + + var rowSelectionModel = new RowSelectionModel(); + grid.setSelectionModel(rowSelectionModel); + + grid.registerPlugin(columnSelector); + grid.invalidate(); + }; + + function expectOnlyTheFirstColumnToBeSelected(selectedRanges) { + var row = selectedRanges[0]; + + expect(selectedRanges.length).toEqual(1); + expect(row.fromCell).toBe(0); + expect(row.toCell).toBe(0); + expect(row.fromRow).toBe(0); + expect(row.toRow).toBe(9); + } + }); + }); diff --git a/web/regression/javascript/selection/copy_data_spec.js b/web/regression/javascript/selection/copy_data_spec.js new file mode 100644 index 00000000..5102512e --- /dev/null +++ b/web/regression/javascript/selection/copy_data_spec.js @@ -0,0 +1,95 @@ +define( + ["jquery", + "slickgrid/slick.grid", + "slickgrid/slick.rowselectionmodel", + "sources/selection/copy_data", + "sources/selection/clipboard" + ], + function ($, SlickGrid, RowSelectionModel, copyData, clipboard) { + describe('copyData', function () { + var grid, sqlEditor; + + beforeEach(function () { + var data = [[1, "leopord", "12"], + [2, "lion", "13"], + [3, "puma", "9"]]; + + var columns = [ + { + name: "id", + pos: 0, + label: "id
numeric", + cell: "number", + can_edit: false, + type: "numeric" + }, + { + name: "brand", + pos: 1, + label: "flavor
character varying", + cell: "string", + can_edit: false, + type: "character varying" + }, + { + name: "size", + pos: 2, + label: "size
numeric", + cell: "number", + can_edit: false, + type: "numeric" + }]; + var gridContainer = $("
"); + $("body").append(gridContainer); + grid = new Slick.Grid("#grid", data, columns, {}); + grid.setSelectionModel(new Slick.RowSelectionModel({selectActiveRow: false})); + sqlEditor = {slickgrid: grid}; + }); + + describe("when rows are selected", function () { + beforeEach(function () { + grid.getSelectionModel().setSelectedRows([0, 2]) + }); + + it("copies them", function () { + spyOn(clipboard, 'copyTextToClipboard'); + + copyData.apply(sqlEditor); + + expect(sqlEditor.copied_rows.length).toBe(2); + + expect(clipboard.copyTextToClipboard).toHaveBeenCalled(); + expect(clipboard.copyTextToClipboard.calls.mostRecent().args[0]).toContain("1,'leopord','12'"); + expect(clipboard.copyTextToClipboard.calls.mostRecent().args[0]).toContain("3,'puma','9'"); + }); + }); + + describe("when a column is selected", function () { + beforeEach(function () { + var firstColumn = new Slick.Range(0, 0, 2, 0); + grid.getSelectionModel().setSelectedRanges([firstColumn]) + }); + + it("copies text to the clipboard", function () { + spyOn(clipboard, 'copyTextToClipboard'); + + copyData.apply(sqlEditor); + + expect(clipboard.copyTextToClipboard).toHaveBeenCalled(); + + var copyArg = clipboard.copyTextToClipboard.calls.mostRecent().args[0]; + var rowStrings = copyArg.split('\n'); + expect(rowStrings[0]).toBe("1"); + expect(rowStrings[1]).toBe("2"); + expect(rowStrings[2]).toBe("3"); + }); + + it("sets copied_rows to empty", function () { + copyData.apply(sqlEditor); + + expect(sqlEditor.copied_rows.length).toBe(0); + }); + }); + }); + } +); \ No newline at end of file diff --git a/web/regression/javascript/selection/grid_selector_spec.js b/web/regression/javascript/selection/grid_selector_spec.js new file mode 100644 index 00000000..a74a66f9 --- /dev/null +++ b/web/regression/javascript/selection/grid_selector_spec.js @@ -0,0 +1,126 @@ +define(["jquery", + "underscore", + "slickgrid/slick.grid", + "slickgrid/slick.rowselectionmodel", + "sources/selection/grid_selector" + ], + function ($, _, SlickGrid, RowSelectionModel, GridSelector) { + describe("GridSelector", function () { + var container, data, columns, gridSelector, rowSelectionModel; + + beforeEach(function () { + container = $("
"); + container.height(9999); + columns = [{ + id: '1', + name: 'some-column-name', + }, { + id: '2', + name: 'second column', + }]; + + gridSelector = new GridSelector(); + columns = gridSelector.getColumnDefinitionsWithCheckboxes(columns); + + data = []; + for (var i = 0; i < 10; i++) { + data.push({'some-column-name': 'some-value-' + i, 'second column': 'second value ' + i}); + } + var grid = new SlickGrid(container, data, columns); + + rowSelectionModel = new RowSelectionModel(); + grid.setSelectionModel(rowSelectionModel); + + grid.registerPlugin(gridSelector); + grid.invalidate(); + + $("body").append(container); + }); + + afterEach(function () { + $("body").find(container).remove(); + }); + + it("renders an additional column on the left for selecting rows", function () { + expect(columns.length).toBe(3); + + var leftmostColumn = columns[0]; + expect(leftmostColumn.id).toBe('row-header-column'); + }); + + it("renders checkboxes for selecting columns", function () { + expect(container.find('[data-test="output-column-header"] input').length).toBe(2) + }); + + it("renders a checkbox for selecting all the cells", function () { + expect(container.find("[title='Select/Deselect All']").length).toBe(1); + }); + + describe("when the cell for the select/deselect all is clicked", function () { + it("selects the whole grid", function () { + container.find("[title='Select/Deselect All']").parent().click(); + + var selectedRanges = rowSelectionModel.getSelectedRanges(); + expect(selectedRanges.length).toBe(1); + var selectedRange = selectedRanges[0]; + expect(selectedRange.fromCell).toBe(1); + expect(selectedRange.toCell).toBe(2); + expect(selectedRange.fromRow).toBe(0); + expect(selectedRange.toRow).toBe(9); + }); + + it("checks the checkbox", function () { + container.find("[title='Select/Deselect All']").parent().click(); + + expect($(container.find("[data-id='checkbox-select-all']")).is(':checked')).toBeTruthy(); + }) + }); + + describe("when the main checkbox in the corner gets selected", function () { + it("unchecks all the columns", function () { + container.find("[title='Select/Deselect All']").click(); + + expect($(container.find('.slick-header-columns input')[1]).is(':checked')).toBeFalsy(); + expect($(container.find('.slick-header-columns input')[2]).is(':checked')).toBeFalsy(); + }); + + it("selects all the cells", function () { + container.find("[title='Select/Deselect All']").click(); + + var selectedRanges = rowSelectionModel.getSelectedRanges(); + expect(selectedRanges.length).toBe(1); + var selectedRange = selectedRanges[0]; + expect(selectedRange.fromCell).toBe(1); + expect(selectedRange.toCell).toBe(2); + expect(selectedRange.fromRow).toBe(0); + expect(selectedRange.toRow).toBe(9); + }); + + describe("when the main checkbox in the corner gets deselected", function () { + beforeEach(function () { + container.find("[title='Select/Deselect All']").click(); + }); + + it("deselects all the cells", function () { + container.find("[title='Select/Deselect All']").click(); + + var selectedRanges = rowSelectionModel.getSelectedRanges(); + expect(selectedRanges.length).toBe(0); + }); + }); + + describe("and then the underlying selection changes", function () { + beforeEach(function () { + container.find("[title='Select/Deselect All']").click(); + }); + + it("unchecks the main checkbox", function () { + var ranges = [new Slick.Range(0, 0, 0, 1)]; + rowSelectionModel.setSelectedRanges(ranges); + + expect($(container.find("[title='Select/Deselect All']")).is(':checked')).toBeFalsy(); + }); + }); + }); + }); + }); diff --git a/web/regression/javascript/selection/range_boundary_navigator_spec.js b/web/regression/javascript/selection/range_boundary_navigator_spec.js new file mode 100644 index 00000000..70fad25f --- /dev/null +++ b/web/regression/javascript/selection/range_boundary_navigator_spec.js @@ -0,0 +1,139 @@ +define(['sources/selection/range_boundary_navigator'], function (rangeBoundaryNavigator) { + + describe("#getUnion", function () { + describe("when the ranges completely overlap", function () { + it("returns a list with that range", function () { + var ranges = [[1, 4], [1, 4], [1, 4]]; + + var union = rangeBoundaryNavigator.getUnion(ranges); + + expect(union).toEqual([[1, 4]]); + }); + }); + + describe("when the ranges all overlap partially or touch", function () { + it("returns one long range", function () { + var rangeBounds = [[3, 6], [1, 4], [7, 14]]; + + var union = rangeBoundaryNavigator.getUnion(rangeBounds); + + expect(union).toEqual([[1, 14]]); + }); + + describe("when one range is a subset of another", function () { + it("returns the larger range", function () { + var rangeBounds = [[2, 6], [1, 14], [8, 10]]; + + var union = rangeBoundaryNavigator.getUnion(rangeBounds); + + expect(union).toEqual([[1, 14]]); + }) + }) + }); + + describe("when the ranges do not touch", function () { + it("returns them in order from lowest to highest", function () { + var rangeBounds = [[3, 6], [1, 1], [8, 10]]; + + var union = rangeBoundaryNavigator.getUnion(rangeBounds); + + expect(union).toEqual([[1, 1], [3, 6], [8, 10]]); + }); + }); + }); + + + describe("#mapDimensionBoundaryUnion", function () { + it("returns a list of the results of the callback", function () { + var rangeBounds = [[0, 1], [3, 3]]; + var callback = function () { + return 'hello'; + }; + var result = rangeBoundaryNavigator.mapDimensionBoundaryUnion(rangeBounds, callback); + expect(result).toEqual(['hello', 'hello', 'hello']); + }); + + it("calls the callback with each index in the dimension", function () { + var rangeBounds = [[0, 1], [3, 3]]; + var callback = jasmine.createSpy('callbackSpy'); + rangeBoundaryNavigator.mapDimensionBoundaryUnion(rangeBounds, callback); + expect(callback.calls.allArgs()).toEqual([[0], [1], [3]]); + }); + }); + + describe("#mapOver2DArray", function () { + var data, rowCollector, processCell; + beforeEach(function () { + data = [[0, 1, 2, 3], [2, 2, 2, 2], [4, 5, 6, 7]]; + processCell = function (rowIndex, columnIndex) { + return data[rowIndex][columnIndex]; + }; + rowCollector = function (rowData) { + return JSON.stringify(rowData); + }; + }); + + it("calls the callback for each item in the ranges", function () { + var rowRanges = [[0, 0], [2, 2]]; + var colRanges = [[0, 3]]; + + var selectionResult = rangeBoundaryNavigator.mapOver2DArray(rowRanges, colRanges, processCell, rowCollector); + + expect(selectionResult).toEqual(["[0,1,2,3]", "[4,5,6,7]"]); + }); + + describe("when the ranges are out of order/duplicated", function () { + var rowRanges, colRanges; + beforeEach(function () { + rowRanges = [[2, 2], [2, 2], [0, 0]]; + colRanges = [[0, 3]]; + }); + + it("uses the union of the ranges", function () { + spyOn(rangeBoundaryNavigator, "getUnion").and.callThrough(); + + var selectionResult = rangeBoundaryNavigator.mapOver2DArray(rowRanges, colRanges, processCell, rowCollector); + + expect(rangeBoundaryNavigator.getUnion).toHaveBeenCalledWith(rowRanges); + expect(rangeBoundaryNavigator.getUnion).toHaveBeenCalledWith(colRanges); + expect(selectionResult).toEqual(["[0,1,2,3]", "[4,5,6,7]"]); + }); + }); + }); + + describe("#rangesToCsv", function () { + var data, columnDefinitions, ranges; + beforeEach(function () { + data = [[1, "leopard", "12"], + [2, "lion", "13"], + [3, "cougar", "9"], + [4, "tiger", "10"]]; + columnDefinitions = [{name: 'id', pos: 0}, {name: 'animal', pos: 1}, {name: 'size', pos: 2}]; + ranges = [new Slick.Range(0, 0, 0, 2), new Slick.Range(3, 0, 3, 2)]; + }); + + it("returns csv for the provided ranges", function () { + + var csvResult = rangeBoundaryNavigator.rangesToCsv(data, columnDefinitions, ranges); + + expect(csvResult).toEqual("1,'leopard','12'\n4,'tiger','10'"); + }); + + describe("when there is an extra column with checkboxes", function () { + beforeEach(function () { + columnDefinitions = [{name: 'not-a-data-column'}, {name: 'id', pos: 0}, {name: 'animal', pos: 1}, { + name: 'size', + pos: 2 + }]; + ranges = [new Slick.Range(0, 0, 0, 3), new Slick.Range(3, 0, 3, 3)]; + + }); + + it("returns csv for the columns with data", function () { + var csvResult = rangeBoundaryNavigator.rangesToCsv(data, columnDefinitions, ranges); + + expect(csvResult).toEqual("1,'leopard','12'\n4,'tiger','10'"); + }); + }); + }); +}); \ No newline at end of file diff --git a/web/regression/javascript/selection/row_selector_spec.js b/web/regression/javascript/selection/row_selector_spec.js new file mode 100644 index 00000000..10697e6a --- /dev/null +++ b/web/regression/javascript/selection/row_selector_spec.js @@ -0,0 +1,174 @@ +define( + ["jquery", + "underscore", + "slickgrid/slick.grid", + "sources/selection/row_selector", + "slickgrid/slick.rowselectionmodel", + "slickgrid", + ], + function ($, _, SlickGrid, RowSelector, RowSelectionModel, Slick) { + describe("RowSelector", function () { + var container, data, columnDefinitions, grid, rowSelectionModel; + + beforeEach(function () { + container = $("
"); + container.height(9999); + + columnDefinitions = [{ + id: '1', + name: 'some-column-name', + selectable: true + }, { + id: '2', + name: 'second column', + selectable: true + }]; + + var rowSelector = new RowSelector(); + data = []; + for (var i = 0; i < 10; i++) { + data.push(['some-value-' + i, 'second value ' + i]); + } + columnDefinitions = rowSelector.getColumnDefinitionsWithCheckboxes(columnDefinitions); + grid = new SlickGrid(container, data, columnDefinitions); + + rowSelectionModel = new RowSelectionModel(); + grid.setSelectionModel(rowSelectionModel); + grid.registerPlugin(rowSelector); + grid.invalidate(); + + $("body").append(container); + }); + + afterEach(function () { + $("body").find(container).remove(); + }); + + it("renders an additional column on the left", function () { + expect(columnDefinitions.length).toBe(3); + + var leftmostColumn = columnDefinitions[0]; + expect(leftmostColumn.id).toBe('row-header-column'); + expect(leftmostColumn.name).toBe(''); + expect(leftmostColumn.selectable).toBe(false); + }); + + it("renders a checkbox the leftmost column", function () { + expect(container.find('.sr').length).toBe(11); + expect(container.find('.sr .sc:first-child input[type="checkbox"]').length).toBe(10); + }); + + it("preserves the other attributes of column definitions", function () { + expect(columnDefinitions[1].id).toBe('1'); + expect(columnDefinitions[1].selectable).toBe(true); + }); + + describe("selecting rows", function () { + describe("when the user clicks a row header checkbox", function () { + it("selects the row", function () { + container.find('.sr .sc:first-child input[type="checkbox"]')[0].click(); + + var selectedRanges = rowSelectionModel.getSelectedRanges(); + expectOnlyTheFirstRowToBeSelected(selectedRanges); + }); + + it("checks the checkbox", function () { + container.find('.sr .sc:first-child input[type="checkbox"]')[5].click(); + + expect($(container.find('.sr .sc:first-child input[type="checkbox"]')[5]) + .is(':checked')).toBeTruthy(); + }); + }); + + describe("when the user clicks a row header", function () { + it("selects the row", function () { + container.find('.sr .sc:first-child')[0].click(); + + var selectedRanges = rowSelectionModel.getSelectedRanges(); + expectOnlyTheFirstRowToBeSelected(selectedRanges); + }); + + it("checks the checkbox", function () { + container.find('.sr .sc:first-child')[7].click(); + + expect($(container.find('.sr .sc:first-child input[type="checkbox"]')[7]) + .is(':checked')).toBeTruthy(); + }); + }); + + describe("when the user clicks multiple row headers", function () { + it("selects another row", function () { + container.find('.sr .sc:first-child')[4].click(); + container.find('.sr .sc:first-child')[0].click(); + + var selectedRanges = rowSelectionModel.getSelectedRanges(); + expect(selectedRanges.length).toEqual(2); + + var row1 = selectedRanges[0]; + expect(row1.fromRow).toBe(4); + expect(row1.toRow).toBe(4); + + var row2 = selectedRanges[1]; + expect(row2.fromRow).toBe(0); + expect(row2.toRow).toBe(0); + }); + }); + + describe("when a column was already selected", function () { + beforeEach(function () { + var selectedRanges = [new Slick.Range(0, 0, 0, 1)]; + rowSelectionModel.setSelectedRanges(selectedRanges); + }); + + it("deselects the column", function () { + container.find('.sr .sc:first-child')[0].click(); + var selectedRanges = rowSelectionModel.getSelectedRanges(); + + expectOnlyTheFirstRowToBeSelected(selectedRanges); + }) + }); + + describe("when the row is deselected through setSelectedRanges", function () { + beforeEach(function () { + container.find('.sr .sc:first-child')[4].click(); + }); + + it("should uncheck the checkbox", function () { + rowSelectionModel.setSelectedRanges([]); + + expect($(container.find('.sr .sc:first-child input[type="checkbox"]')[4]) + .is(':checked')).toBeFalsy(); + }); + }); + + describe("click a second time", function () { + beforeEach(function () { + container.find('.sr .sc:first-child')[1].click(); + }); + + it("unchecks checkbox", function () { + container.find('.sr .sc:first-child')[1].click(); + expect($(container.find('.sr .sc:first-child input[type="checkbox"]')[1]) + .is(':checked')).toBeFalsy(); + }); + + it("unselects the row", function () { + container.find('.sr .sc:first-child')[1].click(); + var selectedRanges = rowSelectionModel.getSelectedRanges(); + + expect(selectedRanges.length).toEqual(0); + }) + }); + }); + }); + + function expectOnlyTheFirstRowToBeSelected(selectedRanges) { + var row = selectedRanges[0]; + + expect(selectedRanges.length).toEqual(1); + expect(row.fromCell).toBe(1); + expect(row.toCell).toBe(2); + expect(row.fromRow).toBe(0); + expect(row.toRow).toBe(0); + } + }); \ No newline at end of file diff --git a/web/regression/python_test_utils/test_utils.py b/web/regression/python_test_utils/test_utils.py index ada3f829..c7808e92 100644 --- a/web/regression/python_test_utils/test_utils.py +++ b/web/regression/python_test_utils/test_utils.py @@ -166,6 +166,7 @@ def create_table(server, db_name, table_name): pg_cursor = connection.cursor() pg_cursor.execute('''CREATE TABLE "%s" (some_column VARCHAR, value NUMERIC)''' % table_name) pg_cursor.execute('''INSERT INTO "%s" VALUES ('Some-Name', 6)''' % table_name) + pg_cursor.execute('''INSERT INTO "%s" VALUES ('Some-Other-Name', 22)''' % table_name) connection.set_isolation_level(old_isolation_level) connection.commit() diff --git a/web/regression/requirements.txt b/web/regression/requirements.txt index f644c12a..693ea177 100644 --- a/web/regression/requirements.txt +++ b/web/regression/requirements.txt @@ -1,4 +1,5 @@ chromedriver_installer==0.0.6 +pyperclip~=1.5.27 selenium==3.3.1 testscenarios==0.5.0 testtools==2.0.0 -- 2.12.0