diff --git a/web/.eslintrc.js b/web/.eslintrc.js index c60569db..98486dcd 100644 --- a/web/.eslintrc.js +++ b/web/.eslintrc.js @@ -5,7 +5,10 @@ module.exports = { 'amd': true, 'jasmine': true, }, - 'extends': 'eslint:recommended', + 'extends': [ + 'eslint:recommended', + "plugin:react/recommended", + ], 'parserOptions': { 'ecmaFeatures': { 'experimentalObjectRestSpread': true, @@ -40,6 +43,6 @@ module.exports = { 'comma-dangle': [ 'error', 'always-multiline' - ] + ], } }; \ No newline at end of file diff --git a/web/karma.conf.js b/web/karma.conf.js index 5b0ab57b..a194bd16 100644 --- a/web/karma.conf.js +++ b/web/karma.conf.js @@ -27,7 +27,7 @@ module.exports = function (config) { // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor preprocessors: { 'regression/javascript/**/*.js': ['webpack'], - // 'regression/javascript/**/*.jsx': ['webpack'], + 'regression/javascript/**/*.jsx': ['webpack'], }, webpack: webpackConfig, diff --git a/web/package.json b/web/package.json index b5d2e0cb..e72138c2 100644 --- a/web/package.json +++ b/web/package.json @@ -5,9 +5,11 @@ "babel-preset-es2015": "~6.24.0", "babel-preset-react": "~6.23.0", "enzyme": "~2.8.2", + "enzyme-matchers": "^3.1.0", "eslint": "^3.19.0", "eslint-plugin-react": "^6.10.3", "jasmine-core": "~2.5.2", + "jasmine-enzyme": "^3.1.0", "karma": "~1.5.0", "karma-babel-preprocessor": "^6.0.1", "karma-browserify": "~5.1.1", @@ -28,7 +30,9 @@ "babelify": "~7.3.0", "browserify": "~14.1.0", "exports-loader": "~0.6.4", + "immutability-helper": "^2.2.0", "imports-loader": "git+https://github.com/webpack-contrib/imports-loader.git#44d6f48463b256a17c1ba6fd9b5cc1449b4e379d", + "moment": "^2.18.1", "react": "~15.4.2", "react-dom": "~15.4.2", "requirejs": "~2.3.3", diff --git a/web/pgadmin/feature_tests/query_tool_journey_test.py b/web/pgadmin/feature_tests/query_tool_journey_test.py new file mode 100644 index 00000000..0766193e --- /dev/null +++ b/web/pgadmin/feature_tests/query_tool_journey_test.py @@ -0,0 +1,111 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2017, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +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 QueryToolJourneyTest(BaseFeatureTest): + """ + Tests the path through the query tool + """ + + scenarios = [ + ("Tests the path through the query tool", dict()) + ] + + 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._navigate_to_query_tool() + self._execute_query("SELECT * FROM test_table ORDER BY value") + + self._test_copies_rows() + self._test_copies_columns() + self._test_history_tab() + + def _test_copies_rows(self): + pyperclip.copy("old clipboard contents") + time.sleep(5) + self.page.driver.switch_to.default_content() + self.page.driver.switch_to_frame(self.page.driver.find_element_by_tag_name("iframe")) + self.page.find_by_xpath("//*[contains(@class, 'slick-row')]/*[1]").click() + self.page.find_by_xpath("//*[@id='btn-copy-row']").click() + + self.assertEqual("'Some-Name','6','some info'", + pyperclip.paste()) + + def _test_copies_columns(self): + pyperclip.copy("old clipboard contents") + + self.page.driver.switch_to.default_content() + self.page.driver.switch_to_frame(self.page.driver.find_element_by_tag_name("iframe")) + self.page.find_by_xpath("//*[@data-test='output-column-header' and contains(., 'some_column')]").click() + self.page.find_by_xpath("//*[@id='btn-copy-row']").click() + + self.assertTrue("'Some-Name'" in pyperclip.paste()) + self.assertTrue("'Some-Other-Name'" in pyperclip.paste()) + self.assertTrue("'Yet-Another-Name'" in pyperclip.paste()) + + def _test_history_tab(self): + self.__clear_query_tool() + + editor_input = self.page.find_by_id("output-panel") + self.page.click_element(editor_input) + self._execute_query("SELECT * FROM shoes") + + self.page.click_tab("History") + history_element = self.page.find_by_id("history_grid") + self.assertIn("SELECT * FROM test_table", history_element.text) + self.assertIn("SELECT * FROM shoes", history_element.text) + + def __clear_query_tool(self): + self.page.click_element(self.page.find_by_xpath("//*[@id='btn-edit']")) + self.page.click_modal('Yes') + + def _navigate_to_query_tool(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) + + def _execute_query(self, query): + ActionChains(self.page.driver).send_keys(query).perform() + self.page.driver.switch_to.default_content() + self.page.driver.switch_to_frame(self.page.driver.find_element_by_tag_name("iframe")) + self.page.find_by_id("btn-flash").click() + + 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/feature_tests/xss_checks_pgadmin_debugger_test.py b/web/pgadmin/feature_tests/xss_checks_pgadmin_debugger_test.py index 959b2c19..094dfed6 100644 --- a/web/pgadmin/feature_tests/xss_checks_pgadmin_debugger_test.py +++ b/web/pgadmin/feature_tests/xss_checks_pgadmin_debugger_test.py @@ -83,7 +83,7 @@ class CheckDebuggerForXssFeatureTest(BaseFeatureTest): # If debugger plugin is not found if is_error and is_error == "Debugger Error": - self.page.click_modal_ok() + self.page.click_modal('OK') self.skipTest("Please make sure that debugger plugin is properly configured") else: time.sleep(2) diff --git a/web/pgadmin/static/js/history/history_collection.js b/web/pgadmin/static/js/history/history_collection.js new file mode 100644 index 00000000..f7b6acd7 --- /dev/null +++ b/web/pgadmin/static/js/history/history_collection.js @@ -0,0 +1,34 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2017, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +export default class HistoryCollection { + + constructor(history_model) { + this.historyList = history_model; + this.onChange(() => {}); + } + + length() { + return this.historyList.length; + } + + add(object) { + this.historyList.push(object); + this.onChangeHandler(this.historyList); + } + + reset() { + this.historyList = []; + this.onChangeHandler(this.historyList); + } + + onChange(onChangeHandler) { + this.onChangeHandler = onChangeHandler; + } +} \ No newline at end of file diff --git a/web/pgadmin/static/js/history/index.js b/web/pgadmin/static/js/history/index.js new file mode 100644 index 00000000..834878a8 --- /dev/null +++ b/web/pgadmin/static/js/history/index.js @@ -0,0 +1,14 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2017, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import historyCollection from './history_collection'; + +export { + historyCollection, +}; diff --git a/web/pgadmin/static/jsx/components.jsx b/web/pgadmin/static/jsx/components.jsx index 5bcb5208..6ff34e4c 100644 --- a/web/pgadmin/static/jsx/components.jsx +++ b/web/pgadmin/static/jsx/components.jsx @@ -1,8 +1,10 @@ import React from 'react'; import {render} from 'react-dom'; +import QueryHistory from './history/query_history'; export { render, React, + QueryHistory, }; \ No newline at end of file diff --git a/web/pgadmin/static/jsx/history/query_history.jsx b/web/pgadmin/static/jsx/history/query_history.jsx new file mode 100644 index 00000000..d36f5ce9 --- /dev/null +++ b/web/pgadmin/static/jsx/history/query_history.jsx @@ -0,0 +1,49 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2017, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import React from 'react'; +import QueryHistoryEntry from './query_history_entry'; + +const liStyle = { + borderBottom: '1px solid #cccccc', +}; + +export default class QueryHistory extends React.Component { + + constructor(props) { + super(props); + + this.state = { + history: [], + }; + } + + componentWillMount() { + this.setState({history: this.props.historyCollection.historyList}); + this.props.historyCollection.onChange((historyList) => this.setState({history: historyList})); + } + + render() { + return ; + } +} + +QueryHistory.propTypes = { + historyCollection: React.PropTypes.object.isRequired, +}; \ No newline at end of file diff --git a/web/pgadmin/static/jsx/history/query_history_entry.jsx b/web/pgadmin/static/jsx/history/query_history_entry.jsx new file mode 100644 index 00000000..d66cb3a7 --- /dev/null +++ b/web/pgadmin/static/jsx/history/query_history_entry.jsx @@ -0,0 +1,93 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2017, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import React from 'react'; +import update from 'immutability-helper'; +import moment from 'moment'; + +const outerDivStyle = { + paddingLeft: '10px', + fontFamily: 'monospace', + paddingRight: '20px', + fontSize: '14px', + backgroundColor: '#FFF', +}; +const sqlStyle = { + textOverflow: 'ellipsis', + overflow: 'hidden', + whiteSpace: 'nowrap', + userSelect: 'auto', +}; +const secondLineStyle = { + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + fontSize: '13px', + color: '#888888', +}; +const timestampStyle = { + alignSelf: 'flex-start', +}; +const rowsAffectedStyle = { + alignSelf: 'flex-end', +}; +const errorMessageStyle = { + textOverflow: 'ellipsis', + overflow: 'hidden', + whiteSpace: 'nowrap', + userSelect: 'auto', + fontSize: '13px', + color: '#888888', +}; + +export default class QueryHistoryEntry extends React.Component { + formatDate(date) { + return (moment(date).format('MMM D YYYY [–] HH:mm:ss')); + } + + render() { + return ( +
+
+ {this.props.historyEntry.query} +
+
+
+ {this.formatDate(this.props.historyEntry.start_time)} / + total time: {this.props.historyEntry.total_time} +
+
+ {this.props.historyEntry.row_affected} rows affected +
+
+
+ {this.props.historyEntry.message} +
+
+ ); + } + + queryEntryBackgroundColor() { + if (!this.props.historyEntry.status) { + return update(outerDivStyle, {$merge: {backgroundColor: '#F7D0D5'}}); + } + return outerDivStyle; + } +} + +QueryHistoryEntry.propTypes = { + historyEntry: React.PropTypes.shape({ + query: React.PropTypes.string, + start_time: React.PropTypes.instanceOf(Date), + status: React.PropTypes.bool, + total_time: React.PropTypes.string, + row_affected: React.PropTypes.int, + message: React.PropTypes.string, + }), +}; \ No newline at end of file diff --git a/web/pgadmin/tools/sqleditor/templates/sqleditor/js/sqleditor.js b/web/pgadmin/tools/sqleditor/templates/sqleditor/js/sqleditor.js index da24f771..c6e02e6e 100644 --- a/web/pgadmin/tools/sqleditor/templates/sqleditor/js/sqleditor.js +++ b/web/pgadmin/tools/sqleditor/templates/sqleditor/js/sqleditor.js @@ -9,7 +9,10 @@ define([ 'sources/slickgrid/event_handlers/handle_query_output_keyboard_event', 'sources/selection/xcell_selection_model', 'sources/selection/set_staged_rows', - 'sources/gettext', 'sources/sqleditor_utils', + 'sources/gettext', + 'sources/sqleditor_utils', + 'sources/generated/history', + 'sources/generated/reactComponents', 'slickgrid', 'bootstrap', 'pgadmin.browser', 'wcdocker', 'codemirror/mode/sql/sql', 'codemirror/addon/selection/mark-selection', @@ -32,9 +35,9 @@ define([ 'slickgrid/plugins/slick.rowselectionmodel', 'slickgrid/slick.grid' ], function( - $, _, S, alertify, pgAdmin, Backbone, Backgrid, CodeMirror, pgExplain, GridSelector, - ActiveCellCapture, clipboard, copyData, RangeSelectionHelper, handleQueryOutputKeyboardEvent, - XCellSelectionModel, setStagedRows, gettext, SqlEditorUtils + $, _, S, alertify, pgAdmin, Backbone, Backgrid, CodeMirror, + pgExplain, GridSelector, ActiveCellCapture, clipboard, copyData, RangeSelectionHelper, handleQueryOutputKeyboardEvent, + XCellSelectionModel, setStagedRows, gettext, SqlEditorUtils, HistoryBundle, reactComponents ) { /* Return back, this has been called more than once */ if (pgAdmin.SqlEditor) @@ -864,147 +867,14 @@ define([ // Remove any existing grid first if (self.history_grid) { - self.history_grid.remove(); + self.history_grid.remove(); } - var history_model = Backbone.Model.extend({ - defaults: { - status: undefined, - start_time: undefined, - query: undefined, - row_affected: 0, - row_retrieved: 0, - total_time: undefined, - message: '' - } - }); - - var history_collection = self.history_collection = new (Backbone.Collection.extend({ - model: history_model, - // comparator to sort the history in reverse order of the start_time - comparator: function(a, b) { - return -a.get('start_time').localeCompare(b.get('start_time')); - } - })); - var columns = [{ - name: "status", - label: "", - cell: Backgrid.Cell.extend({ - class: 'sql-status-cell', - render: function() { - this.$el.empty(); - var $btn = $('', { - class: 'btn btn-circle' - }).appendTo(this.$el); - var $circleDiv = $('', {class: 'fa'}).appendTo($btn); - if (this.model.get('status')) { - $btn.addClass('btn-success'); - $circleDiv.addClass('fa-check'); - } else { - $btn.addClass('btn-danger'); - $circleDiv.addClass('fa-times'); - } - - return this; - }, - editable: false - }), - editable: false - }, { - name: "start_time", - label: "Date", - cell: "string", - editable: false, - resizeable: true - }, { - name: "query", - label: "Query", - cell: "string", - editable: false, - resizeable: true - }, { - name: "row_affected", - label: "Rows affected", - cell: "integer", - editable: false, - resizeable: true - }, { - name: "total_time", - label: "Total Time", - cell: "string", - editable: false, - resizeable: true - }, { - name: "message", - label: "Message", - cell: "string", - editable: false, - resizeable: true - }]; - - - // Create Collection of Backgrid columns - var columnsColl = new Backgrid.Columns(columns); - var $history_grid = self.$el.find('#history_grid'); - - var grid = self.history_grid = new Backgrid.Grid({ - columns: columnsColl, - collection: history_collection, - className: "backgrid table-bordered presentation table backgrid-striped" - }); - - // Render the grid - $history_grid.append(grid.render().$el); - - var sizeAbleCol = new Backgrid.Extension.SizeAbleColumns({ - collection: history_collection, - columns: columnsColl, - grid: self.history_grid - }); - - $history_grid.find('thead').before(sizeAbleCol.render().el); + self.history_collection = new HistoryBundle.historyCollection([]); - // Add resize handlers - var sizeHandler = new Backgrid.Extension.SizeAbleColumnsHandlers({ - sizeAbleColumns: sizeAbleCol, - grid: self.history_grid, - saveColumnWidth: true - }); - - // sizeHandler should render only when table grid loaded completely. - setTimeout(function() { - $history_grid.find('thead').before(sizeHandler.render().el); - }, 1000); - - // re render sizeHandler whenever history panel tab becomes visible - self.history_panel.on(wcDocker.EVENT.VISIBILITY_CHANGED, function(ev) { - $history_grid.find('thead').before(sizeHandler.render().el); - }); - - // Initialized table width 0 still not calculated - var table_width = 0; - // Listen to resize events - columnsColl.on('resize', - function(columnModel, newWidth, oldWidth, offset) { - var $grid_el = $history_grid.find('table'), - tbl_orig_width = $grid_el.width(), - offset = oldWidth - newWidth, - tbl_new_width = tbl_orig_width - offset; - - if (table_width == 0) { - table_width = tbl_orig_width - } - // Table new width cannot be less than original width - if (tbl_new_width >= table_width) { - $($grid_el).css('width', tbl_new_width + 'px'); - } - else { - // reset if calculated tbl_new_width is less than original - // table width - tbl_new_width = table_width; - $($grid_el).css('width', tbl_new_width + 'px'); - } - }); + let queryHistoryElement = reactComponents.React.createElement( + reactComponents.QueryHistory, {historyCollection: self.history_collection}); + reactComponents.render(queryHistoryElement, $('#history_grid')[0]); }, // Callback function for Add New Row button click. @@ -1307,7 +1177,7 @@ define([ this._stopEventPropogation(ev); this._closeDropDown(ev); // ask for confirmation only if anything to clear - if(!self.history_collection.length) { return; } + if(!self.history_collection.length()) { return; } alertify.confirm(gettext("Clear history"), gettext("Are you sure you wish to clear the history?"), @@ -2130,11 +2000,13 @@ define([ $("#btn-flash").prop('disabled', false); self.trigger('pgadmin-sqleditor:loading-icon:hide'); self.gridView.history_collection.add({ - 'status' : status, 'start_time': self.query_start_time.toString(), - 'query': self.query, 'row_affected': self.rows_affected, - 'total_time': self.total_time, 'message':msg + 'status' : status, + 'start_time': self.query_start_time, + 'query': self.query, + 'row_affected': self.rows_affected, + 'total_time': self.total_time, + 'message':msg, }); - self.gridView.history_collection.sort(); } }, @@ -2407,10 +2279,13 @@ define([ // Update the sql results in history tab _.each(res.data.query_result, function(r) { - self.gridView.history_collection.add( - {'status' : r.status, 'start_time': self.query_start_time.toString(), - 'query': r.sql, 'row_affected': r.rows_affected, - 'total_time': self.total_time, 'message': r.result + self.gridView.history_collection.add({ + 'status': r.status, + 'start_time': self.query_start_time, + 'query': r.sql, + 'row_affected': r.rows_affected, + 'total_time': self.total_time, + 'message': r.result, }); }); self.trigger('pgadmin-sqleditor:loading-icon:hide'); @@ -3356,7 +3231,7 @@ define([ var msg = e.responseText; if (e.responseJSON != undefined && - e.responseJSON.errormsg != undefined) + e.responseJSON.errormsg != undefined) msg = e.responseJSON.errormsg; alertify.alert('Get Object Name Error', msg); diff --git a/web/regression/feature_utils/app_starter.py b/web/regression/feature_utils/app_starter.py index 96cc516b..f40d6921 100644 --- a/web/regression/feature_utils/app_starter.py +++ b/web/regression/feature_utils/app_starter.py @@ -11,6 +11,7 @@ import subprocess import signal import random +import time class AppStarter: """ Helper for starting the full pgadmin4 app and loading the page via @@ -40,6 +41,7 @@ class AppStarter: ) self.driver.set_window_size(1024, 1024) + time.sleep(10) self.driver.get( "http://" + self.app_config.DEFAULT_SERVER + ":" + random_server_port) diff --git a/web/regression/feature_utils/pgadmin_page.py b/web/regression/feature_utils/pgadmin_page.py index 46d50156..a1cb7147 100644 --- a/web/regression/feature_utils/pgadmin_page.py +++ b/web/regression/feature_utils/pgadmin_page.py @@ -33,15 +33,16 @@ class PgadminPage: def reset_layout(self): self.click_element(self.find_by_partial_link_text("File")) self.find_by_partial_link_text("Reset Layout").click() - self.click_modal_ok() + self.click_modal('OK') self.wait_for_reloading_indicator_to_disappear() - def click_modal_ok(self): + def click_modal(self, button_text): time.sleep(0.5) # Find active alertify dialog in case of multiple alertify dialog & click on that dialog - self.click_element( - self.find_by_xpath("//div[contains(@class, 'alertify') and not(contains(@class, 'ajs-hidden'))]//button[.='OK']") - ) + modal_button = self.find_by_xpath( + "//div[contains(@class, 'alertify') and not(contains(@class, 'ajs-hidden'))]//button[.='%s']" + % button_text) + self.click_element(modal_button) def add_server(self, server_config): self.find_by_xpath("//*[@class='aciTreeText' and contains(.,'Servers')]").click() @@ -78,10 +79,13 @@ class PgadminPage: 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() - self.click_modal_ok() + server_to_remove = self.find_by_xpath("//*[@id='tree']//*[.='" + server_config['name'] + "' and @class='aciTreeItem']") + self.click_element(server_to_remove) + object_menu_item = self.find_by_partial_link_text("Object") + self.click_element(object_menu_item) + delete_menu_item = self.find_by_partial_link_text("Delete/Drop") + self.click_element(delete_menu_item) + self.click_modal('OK') def select_tree_item(self, tree_item_text): self.find_by_xpath("//*[@id='tree']//*[.='" + tree_item_text + "' and @class='aciTreeItem']").click() @@ -130,6 +134,7 @@ class PgadminPage: ) def click_element(self, element): + # driver must be here to adhere to the method contract in selenium.webdriver.support.wait.WebDriverWait.until() def click_succeeded(driver): try: element.click() @@ -175,8 +180,9 @@ class PgadminPage: time.sleep(sleep_time) def click_tab(self, tab_name): - self.find_by_xpath("//*[contains(@class,'wcTabTop')]//*[contains(@class,'wcPanelTab') " - "and contains(.,'" + tab_name + "')]").click() + tab = self.find_by_xpath("//*[contains(@class,'wcTabTop')]//*[contains(@class,'wcPanelTab') " + "and contains(.,'" + tab_name + "')]") + self.click_element(tab) def wait_for_input_field_content(self, field_name, content): def input_field_has_content(driver): diff --git a/web/regression/javascript/check_node_visiblity_spec.js b/web/regression/javascript/check_node_visibility_spec.js similarity index 100% rename from web/regression/javascript/check_node_visiblity_spec.js rename to web/regression/javascript/check_node_visibility_spec.js diff --git a/web/regression/javascript/history/history_collection_spec.js b/web/regression/javascript/history/history_collection_spec.js new file mode 100644 index 00000000..e1baa554 --- /dev/null +++ b/web/regression/javascript/history/history_collection_spec.js @@ -0,0 +1,83 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2017, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import HistoryCollection from '../../../pgadmin/static/js/history/history_collection'; + +describe('historyCollection', function () { + let historyCollection, historyModel, onChangeSpy; + beforeEach(() => { + historyModel = [{some: 'thing', someOther: ['array element']}]; + historyCollection = new HistoryCollection(historyModel); + onChangeSpy = jasmine.createSpy('onChangeHandler'); + + historyCollection.onChange(onChangeSpy); + }); + + describe('length', function () { + it('returns 0 when underlying history model has no elements', function () { + historyCollection = new HistoryCollection([]); + + expect(historyCollection.length()).toBe(0); + }); + + it('returns the length of the underlying history model', function () { + expect(historyCollection.length()).toBe(1); + }); + }); + + describe('add', function () { + let expectedHistory; + beforeEach(() => { + historyCollection.add({some: 'new thing', someOther: ['value1', 'value2']}); + + expectedHistory = [ + {some: 'thing', someOther: ['array element']}, + {some: 'new thing', someOther: ['value1', 'value2']}, + ]; + }); + + it('adds a passed entry', function () { + expect(historyCollection.historyList).toEqual(expectedHistory); + }); + + it('calls the onChange function', function () { + expect(onChangeSpy).toHaveBeenCalledWith(expectedHistory); + }); + }); + + describe('reset', function () { + beforeEach(() => { + historyCollection.reset(); + }); + + it('drops the history', function () { + expect(historyCollection.historyList).toEqual([]); + expect(historyCollection.length()).toBe(0); + }); + + it('calls the onChange function', function () { + expect(onChangeSpy).toHaveBeenCalledWith([]); + }); + }); + + describe('sort', function () { + it('doesn\'t sort'); + }); + + describe('when instantiated', function () { + describe('from a history model', function () { + it('has the historyModel', () => { + let content = historyCollection.historyList; + + expect(content).toEqual(historyModel); + }); + + }); + }); +}); \ No newline at end of file diff --git a/web/regression/javascript/history/query_history_entry_spec.jsx b/web/regression/javascript/history/query_history_entry_spec.jsx new file mode 100644 index 00000000..c86a1cfc --- /dev/null +++ b/web/regression/javascript/history/query_history_entry_spec.jsx @@ -0,0 +1,50 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2017, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import React from 'react'; + +import QueryHistoryEntry from '../../../pgadmin/static/jsx/history/query_history_entry'; + +import {mount} from 'enzyme'; +import jasmineEnzyme from 'jasmine-enzyme'; + +describe('QueryHistoryEntry', () => { + let historyWrapper; + beforeEach(() => { + jasmineEnzyme(); + }); + + describe('for a failed query', () => { + beforeEach(() => { + const historyEntry = { + query: 'second sql statement', + start_time: new Date(2016, 11, 11, 1, 33, 5, 99), + status: false, + }; + historyWrapper = mount(); + }); + it('displays a pink background color', () => { + expect(historyWrapper.find('div').first()).toHaveStyle('backgroundColor', '#F7D0D5'); + }); + }); + + describe('for a successful query', () => { + beforeEach(() => { + const historyEntry = { + query: 'second sql statement', + start_time: new Date(2016, 11, 11, 1, 33, 5, 99), + status: true, + }; + historyWrapper = mount(); + }); + it('does not display a pink background color', () => { + expect(historyWrapper.find('div').first()).toHaveStyle('backgroundColor', '#FFF'); + }); + }); +}); diff --git a/web/regression/javascript/history/query_history_spec.jsx b/web/regression/javascript/history/query_history_spec.jsx new file mode 100644 index 00000000..e36988a8 --- /dev/null +++ b/web/regression/javascript/history/query_history_spec.jsx @@ -0,0 +1,103 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2017, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import React from 'react'; +import QueryHistory from '../../../pgadmin/static/jsx/history/query_history'; +import QueryHistoryEntry from '../../../pgadmin/static/jsx/history/query_history_entry'; +import HistoryCollection from '../../../pgadmin/static/js/history/history_collection'; +import jasmineEnzyme from 'jasmine-enzyme'; + +import {mount, shallow} from 'enzyme'; + +describe('QueryHistory', () => { + let historyWrapper; + beforeEach(() => { + jasmineEnzyme(); + const historyCollection = new HistoryCollection([]); + historyWrapper = shallow(); + }); + + describe('on construction', () => { + it('has no entries', (done) => { + let foundChildren = historyWrapper.find(QueryHistoryEntry); + expect(foundChildren.length).toBe(0); + done(); + }); + }); + + describe('when it has history', () => { + describe('when two SQL queries were executed', () => { + let foundChildren; + + beforeEach(() => { + const historyObjects = [ + { + query: 'second sql statement', + start_time: new Date(2016, 11, 11, 1, 33, 5, 99), + status: false, + row_affected: 1, + total_time: '234 msec', + message: 'some other message', + }, + { + query: 'first sql statement', + start_time: new Date(2017, 5, 3, 14, 3, 15, 150), + status: true, + row_affected: 2, + total_time: '14 msec', + message: 'a very important message', + }, + ]; + const historyCollection = new HistoryCollection(historyObjects); + + historyWrapper = mount(); + + foundChildren = historyWrapper.find(QueryHistoryEntry); + }); + + it('has two query history entries', () => { + expect(foundChildren.length).toBe(2); + }); + + it('displays the SQL of the queries in order', () => { + expect(foundChildren.at(0).text()).toContain('first sql statement'); + expect(foundChildren.at(1).text()).toContain('second sql statement'); + }); + + it('displays the formatted timestamp of the queries in chronological order by most recent first', () => { + expect(foundChildren.at(0).text()).toContain('Jun 3 2017 – 14:03:15'); + expect(foundChildren.at(1).text()).toContain('Dec 11 2016 – 01:33:05'); + }); + + it('displays the number of rows affected', () => { + expect(foundChildren.at(1).text()).toContain('1 rows affected'); + expect(foundChildren.at(0).text()).toContain('2 rows affected'); + }); + + it('displays the total time', () => { + expect(foundChildren.at(0).text()).toContain('total time: 14 msec'); + expect(foundChildren.at(1).text()).toContain('total time: 234 msec'); + }); + + it('displays the truncated message', () => { + expect(foundChildren.at(0).text()).toContain('a very important message'); + expect(foundChildren.at(1).text()).toContain('some other message'); + }); + + describe('when there are one failing and one successful query each', () => { + it('adds a white background color for the successful query', () => { + expect(foundChildren.at(0).find('div').first()).toHaveStyle('backgroundColor', '#FFF'); + }); + it('adds a red background color for the failed query', () => { + expect(foundChildren.at(1).find('div').first()).toHaveStyle('backgroundColor', '#F7D0D5'); + }); + }); + }); + }); +}); \ 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 cf559c44..f3e7ed01 100644 --- a/web/regression/python_test_utils/test_utils.py +++ b/web/regression/python_test_utils/test_utils.py @@ -227,6 +227,7 @@ def create_constraint( except Exception: traceback.print_exc(file=sys.stderr) + def create_debug_function(server, db_name, function_name="test_func"): try: connection = get_db_connection(db_name, @@ -305,6 +306,7 @@ def drop_database(connection, database_name): connection.commit() connection.close() + def drop_tablespace(connection): """This function used to drop the tablespace""" pg_cursor = connection.cursor() diff --git a/web/webpack.config.js b/web/webpack.config.js index 91586592..fc6d29e0 100644 --- a/web/webpack.config.js +++ b/web/webpack.config.js @@ -1,18 +1,21 @@ /* eslint-env node */ module.exports = { - context: __dirname + '/pgadmin/static/jsx', - entry: './components.jsx', + context: __dirname + '/pgadmin/static', + entry: { + reactComponents: './jsx/components.jsx', + history: './js/history/index.js', + }, output: { libraryTarget: 'amd', path: __dirname + '/pgadmin/static/js/generated', - filename: 'reactComponents.js', + filename: '[name].js', }, module: { rules: [{ test: /\.jsx?$/, - exclude: /node_modules/, + exclude: [/node_modules/, /vendor/], use: { loader: 'babel-loader', options: { diff --git a/web/webpack.test.config.js b/web/webpack.test.config.js index b156f147..4909f4a7 100644 --- a/web/webpack.test.config.js +++ b/web/webpack.test.config.js @@ -22,7 +22,7 @@ module.exports = { use: { loader: 'babel-loader', options: { - presets: ['es2015'], + presets: ['es2015', 'react'], }, }, }, @@ -51,6 +51,7 @@ module.exports = { }, resolve: { + extensions: ['.js', '.jsx'], alias: { 'alertify': sourcesDir + '/vendor/alertifyjs/alertify', 'jquery': sourcesDir + '/vendor/jquery/jquery-1.11.2', @@ -66,4 +67,11 @@ module.exports = { 'pgadmin': sourcesDir + '/js/pgadmin', }, }, + externals: { + 'react/addons': true, + 'react/lib/ReactContext': true, + 'react/lib/ExecutionEnvironment': true, + 'react-dom/test-utils': true, + 'react-test-renderer/shallow': true, + }, }; diff --git a/web/yarn.lock b/web/yarn.lock index b04caa5c..ce72d41b 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -1371,6 +1371,12 @@ decamelize@^1.0.0, decamelize@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" +deep-equal-ident@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/deep-equal-ident/-/deep-equal-ident-1.1.1.tgz#06f4b89e53710cd6cea4a7781c7a956642de8dc9" + dependencies: + lodash.isequal "^3.0" + deep-extend@~0.4.0: version "0.4.2" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f" @@ -1608,6 +1614,12 @@ entities@^1.1.1, entities@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0" +enzyme-matchers@^3.1.0, enzyme-matchers@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/enzyme-matchers/-/enzyme-matchers-3.2.0.tgz#4718779a3b9eb5e8ebad46804f8d3e66045d0181" + dependencies: + deep-equal-ident "^1.1.1" + enzyme@~2.8.2: version "2.8.2" resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-2.8.2.tgz#6c8bcb05012abc4aa4bc3213fb23780b9b5b1714" @@ -2299,6 +2311,12 @@ ignore@^3.2.0: version "3.3.3" resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.3.tgz#432352e57accd87ab3110e82d3fea0e47812156d" +immutability-helper@^2.2.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/immutability-helper/-/immutability-helper-2.2.2.tgz#e7e9da728b3de2fad34a216f4157b326dbccc892" + dependencies: + invariant "^2.2.0" + "imports-loader@git+https://github.com/webpack-contrib/imports-loader.git#44d6f48463b256a17c1ba6fd9b5cc1449b4e379d": version "0.7.1" resolved "git+https://github.com/webpack-contrib/imports-loader.git#44d6f48463b256a17c1ba6fd9b5cc1449b4e379d" @@ -2568,6 +2586,12 @@ jasmine-core@~2.5.2: version "2.5.2" resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-2.5.2.tgz#6f61bd79061e27f43e6f9355e44b3c6cab6ff297" +jasmine-enzyme@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/jasmine-enzyme/-/jasmine-enzyme-3.2.0.tgz#0eeb370d4fa965db03e04347ca9c4ed5a60fadc2" + dependencies: + enzyme-matchers "^3.2.0" + jodid25519@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/jodid25519/-/jodid25519-1.0.2.tgz#06d4912255093419477d425633606e0e90782967" @@ -2828,6 +2852,22 @@ loader-utils@^1.0.2: emojis-list "^2.0.0" json5 "^0.5.0" +lodash._baseisequal@^3.0.0: + version "3.0.7" + resolved "https://registry.yarnpkg.com/lodash._baseisequal/-/lodash._baseisequal-3.0.7.tgz#d8025f76339d29342767dcc887ce5cb95a5b51f1" + dependencies: + lodash.isarray "^3.0.0" + lodash.istypedarray "^3.0.0" + lodash.keys "^3.0.0" + +lodash._bindcallback@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e" + +lodash._getnative@^3.0.0: + version "3.9.1" + resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" + lodash.assignin@^4.0.9: version "4.2.0" resolved "https://registry.yarnpkg.com/lodash.assignin/-/lodash.assignin-4.2.0.tgz#ba8df5fb841eb0a3e8044232b0e263a8dc6a28a2" @@ -2852,6 +2892,33 @@ lodash.foreach@^4.3.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz#1a6a35eace401280c7f06dddec35165ab27e3e53" +lodash.isarguments@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" + +lodash.isarray@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" + +lodash.isequal@^3.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-3.0.4.tgz#1c35eb3b6ef0cd1ff51743e3ea3cf7fdffdacb64" + dependencies: + lodash._baseisequal "^3.0.0" + lodash._bindcallback "^3.0.0" + +lodash.istypedarray@^3.0.0: + version "3.0.6" + resolved "https://registry.yarnpkg.com/lodash.istypedarray/-/lodash.istypedarray-3.0.6.tgz#c9a477498607501d8e8494d283b87c39281cef62" + +lodash.keys@^3.0.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a" + dependencies: + lodash._getnative "^3.0.0" + lodash.isarguments "^3.0.0" + lodash.isarray "^3.0.0" + lodash.map@^4.4.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.map/-/lodash.map-4.6.0.tgz#771ec7839e3473d9c4cde28b19394c3562f4f6d3" @@ -3013,6 +3080,10 @@ module-deps@^4.0.8: through2 "^2.0.0" xtend "^4.0.0" +moment@^2.18.1: + version "2.18.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f" + ms@0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" @@ -3499,10 +3570,10 @@ randomatic@^1.1.3: kind-of "^3.0.2" randombytes@^2.0.0, randombytes@^2.0.1: - version "2.0.4" - resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.4.tgz#9551df208422c8f80eb58e2326dd0b840ff22efd" + version "2.0.5" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.5.tgz#dc009a246b8d09a177b4b7a0ae77bc570f4b1b79" dependencies: - safe-buffer "^5.0.1" + safe-buffer "^5.1.0" range-parser@^1.0.3, range-parser@^1.2.0: version "1.2.0" @@ -3818,7 +3889,7 @@ rx-lite@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102" -safe-buffer@^5.0.1: +safe-buffer@^5.0.1, safe-buffer@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.0.tgz#fe4c8460397f9eaaaa58e73be46273408a45e223"