├── .editorconfig ├── .gitignore ├── .nvmrc ├── .prettierrc ├── LICENSE ├── README.md ├── bin └── hot-perf ├── docker └── docker-compose.yml ├── lib ├── cli.js ├── commands │ ├── local-server │ │ ├── index.js │ │ ├── public │ │ │ └── js │ │ │ │ ├── bar-item.js │ │ │ │ ├── bar-manager.js │ │ │ │ ├── chart-item.js │ │ │ │ ├── observer.js │ │ │ │ └── test-runner.js │ │ └── templates │ │ │ ├── benchmark-viewer.html │ │ │ ├── test-runner-a.html │ │ │ ├── test-runner-b.html │ │ │ ├── test-runner-c.html │ │ │ ├── test-runner-d.html │ │ │ └── test-runner.html │ └── protractor │ │ └── index.js ├── config.js └── storage │ ├── index.js │ └── mongo.js ├── package-lock.json ├── package.json ├── protractor.conf.js └── test ├── config.js ├── runner.js ├── spec ├── altering.spec.js ├── arrow-keys-navigation.spec.js ├── editing.spec.js └── view-scrolling.spec.js └── utils.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | node_modules 4 | temp/ 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2012-2014 Marcin Warpechowski 4 | Copyright (c) 2019 Handsoncode sp. z o.o. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining 7 | a copy of this software and associated documentation files (the 8 | 'Software'), to deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, merge, publish, 10 | distribute, sublicense, and/or sell copies of the Software, and to 11 | permit persons to whom the Software is furnished to do so, subject to 12 | the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 21 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 22 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Performance Lab 2 | 3 | JavaScript performance tests for Handsontable 4 | 5 | ## Install 6 | 7 | The minimal Node version which this project can run on is 14. Make sure that your version meets that criteria before 8 | you continue with the installation. 9 | 10 | Install dependencies via [NPM](https://npmjs.com/) 11 | 12 | ```sh 13 | $ npm install 14 | ``` 15 | 16 | Test results are stored in MongoDB instance so it is necessary to set up the DB before you run the script. If you have 17 | [docker](https://www.docker.com/) installed you can run services using [docker-compose](https://github.com/docker/compose). 18 | 19 | ```sh 20 | docker-compose -f docker/docker-compose.yml up 21 | ``` 22 | 23 | ## Run It 24 | 25 | To run performance tests and save the results to the DB execute 26 | 27 | ```sh 28 | $ ./bin/hot-perf run 29 | ``` 30 | 31 | or 32 | 33 | ```sh 34 | $ npm run start 35 | ``` 36 | 37 | Performance tests are defined in the `test/spec` directory. Each test contains code which prepares Handsontable for tests 38 | and block of code which then executes several times (defined as SAMPLE_SIZE in the `lib/config.js` file). After each 39 | call, stats are collected and after the amount of iteration hit the SAMPLE_SIZE the result is saved to the database. 40 | 41 | Once completed you can view generated test reports by running `./bin/hot-perf local-server benchmark-viewer`. 42 | It serves a page where you can compare your generated results between different Handsontable versions and different test cases. 43 | 44 | ## Usage 45 | 46 | ##### ```> ./bin/hot-perf run``` (or ```> ./bin/hot-perf r```) 47 | 48 | It runs a benchmark by running all spec files defined in the `test/spec` directory. Once completed results are saved to the database. 49 | 50 | ##### ```> ./bin/hot-perf local-server benchmark-viewer``` (or ```> ./bin/hot-perf ls bv```) 51 | 52 | Runs a local server where you can see the test results. 53 | 54 | Arguments: 55 | - ```test-runner``` (or ```tr```) - It serves a test runner page which is used by protractor to test the Handsontable. 56 | - ```benchmark-viewer``` (or ```bv```) - It serves a page which is used to view results generated by the `run` command. 57 | 58 | ### Global options: 59 | - ```--hot-version``` - Selects version of the Handsontable to test (it has to be a version which is accessible through [jsdelivr](https://www.jsdelivr.com/)). If not specified the `latest` tag is used. For example `--hot-version=6.2.2`. 60 | - ```--hot-server``` - Selects a server to be used to serve the Handsontable assets. For example `--hot-server=http://localhost:8082`. If 61 | used the assets are loaded from `dist` directory, such as `http://localhost:8082/dist/handsontable.full.css`. 62 | - ```--test-name``` - The name under which the test will be saved. For example `--test-name=my-feature`. If a test by that name is already stored, it will be replaced with the new test results. 63 | - ```--cpu-throttle-rate``` - The argument sets the CPU throttle rate for the browser. Adjusting the clock speed of the CPU slows down the computer. This can be useful for detecting slight deviations in performance that normally cannot be seen on a fast computer. It's advisable to perform the tests with rate sets as 4, for example, `--cpu-throttle-rate=4`. 64 | 65 | ## License 66 | 67 | [MIT License](https://opensource.org/licenses/MIT) 68 | -------------------------------------------------------------------------------- /bin/hot-perf: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../lib/cli.js'); -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Docker services prepared for development purposes. 3 | # 4 | version: "3.7" 5 | services: 6 | mongo: 7 | container_name: performance-lab-mongodb 8 | image: mongo:4 9 | ports: 10 | - 27017:27017 11 | environment: 12 | MONGO_INITDB_ROOT_USERNAME: root 13 | MONGO_INITDB_ROOT_PASSWORD: root 14 | volumes: 15 | - ./../temp/mongodb:/data/db 16 | 17 | mongo-express: 18 | container_name: performance-lab-dbviewer 19 | image: mongo-express 20 | ports: 21 | - 8081:8081 22 | environment: 23 | ME_CONFIG_MONGODB_SERVER: mongo 24 | ME_CONFIG_MONGODB_ADMINUSERNAME: root 25 | ME_CONFIG_MONGODB_ADMINPASSWORD: root 26 | depends_on: 27 | - mongo 28 | -------------------------------------------------------------------------------- /lib/cli.js: -------------------------------------------------------------------------------- 1 | const program = require("caporal"); 2 | const semver = require("semver"); 3 | const config = require("./config"); 4 | const { version: packageVersion, engines } = require("./../package"); 5 | 6 | const appToServeMap = new Map([ 7 | ["tr", "test-runner"], 8 | ["bv", "benchmark-viewer"], 9 | ]); 10 | const hotVersionRegExp = /^((\d{1,3}\.\d{1,3}\.\d{1,3}|latest)(\-next\-([a-z0-9]){7}\-[0-9]{8})?)$/; 11 | 12 | function parseArgs() { 13 | program 14 | .version(packageVersion) 15 | .description("JavaScript performance tests for Handsontable") 16 | .command( 17 | "local-server", 18 | 'Run a local server ("test-runner", "tr" or "benchmark-viewer", "bv").', 19 | ) 20 | .alias("ls") 21 | .argument( 22 | "", 23 | 'Type of the application to serve ("test-runner" or "benchmark-viewer")', 24 | /^(test\-runner|tr|benchmark\-viewer|bv)$/, 25 | "test-runner", 26 | ) 27 | .option( 28 | "--hot-version ", 29 | "The Handsontable which will be used for running a benchmark.", 30 | hotVersionRegExp, 31 | ) 32 | .option( 33 | "--hot-server ", 34 | "The server which will be used to serve Handsontable assets from.", 35 | ) 36 | .action((args, options) => { 37 | require("./commands/local-server")( 38 | appToServeMap.get(args.appToServe) ?? args.appToServe, 39 | parseInt(config.SERVER_PORT, 10) + 1, 40 | options, 41 | ); 42 | }) 43 | .command("run", "Run a benchmark.") 44 | .alias("r") 45 | .option( 46 | "--hot-version ", 47 | "The Handsontable which will be used for running a benchmark.", 48 | hotVersionRegExp, 49 | ) 50 | .option( 51 | "--hot-server ", 52 | "The server which will be used to serve Handsontable assets from.", 53 | ) 54 | .option( 55 | "--test-name ", 56 | "The name under which the test will be saved.", 57 | ) 58 | .option("--cpu-throttle-rate ", "The CPU throttle rate.") 59 | .action(async (args, options) => { 60 | await require("./commands/local-server")( 61 | "test-runner", 62 | config.SERVER_PORT, 63 | options, 64 | ); 65 | 66 | const statsGenerator = require("./commands/protractor")(options); 67 | 68 | await require("./storage").saveByReplace(statsGenerator); 69 | 70 | process.exit(0); 71 | }); 72 | 73 | program.parse(process.argv); 74 | } 75 | 76 | (function main() { 77 | try { 78 | if (!semver.satisfies(process.versions.node, engines.node)) { 79 | throw Error( 80 | `The project requires Node.js${engines.node} for running. You've currently installed version ${process.versions.node}.`, 81 | ); 82 | } 83 | 84 | parseArgs(); 85 | } catch (ex) { 86 | /* eslint-disable no-console */ 87 | console.log(ex.message); 88 | console.log(""); 89 | process.exit(2); 90 | } 91 | })(); 92 | -------------------------------------------------------------------------------- /lib/commands/local-server/index.js: -------------------------------------------------------------------------------- 1 | const Hapi = require("@hapi/hapi"); 2 | const path = require("path"); 3 | const config = require("./../../config"); 4 | const { loadAll } = require("./../../storage"); 5 | 6 | module.exports = async function ( 7 | appName, 8 | customPort = config.SERVER_PORT, 9 | { hotVersion = "latest", hotServer, testName } = {}, 10 | ) { 11 | const server = Hapi.server({ 12 | port: customPort, 13 | host: config.SERVER_HOST, 14 | routes: { 15 | files: { 16 | relativeTo: __dirname + path.sep + "public", 17 | }, 18 | }, 19 | }); 20 | 21 | await server.register(require("@hapi/vision")); 22 | await server.register(require("@hapi/inert")); 23 | 24 | server.views({ 25 | engines: { 26 | html: require("handlebars"), 27 | }, 28 | relativeTo: __dirname, 29 | path: "templates", 30 | }); 31 | 32 | server.route({ 33 | method: "GET", 34 | path: "/{param*}", 35 | handler: { 36 | directory: { 37 | path: ".", 38 | redirectToSlash: true, 39 | index: true, 40 | }, 41 | }, 42 | }); 43 | 44 | server.route({ 45 | method: "GET", 46 | path: "/", 47 | handler: async function (request, h) { 48 | const stats = await loadAll(); 49 | 50 | return h.view(appName, { 51 | sampleSize: config.SAMPLE_SIZE, 52 | stats: JSON.stringify(Array.from(stats.entries())), 53 | urls: getHotUrl({ hotVersion, hotServer }), 54 | }); 55 | }, 56 | }); 57 | 58 | await server.start(); 59 | 60 | console.log(`Server running at: ${server.info.uri}`); 61 | }; 62 | 63 | function getHotUrl({ hotVersion, hotServer } = {}) { 64 | const urls = { 65 | script: "", 66 | style_core: "", 67 | style_theme_main: "", 68 | style_theme_horizon: "", 69 | }; 70 | 71 | if (hotServer) { 72 | urls.script = `${hotServer}/dist/handsontable.full.js`; 73 | // urls.style_core = `${hotServer}/dist/handsontable.full.css`; 74 | urls.style_core = `${hotServer}/styles/handsontable.css`; 75 | urls.style_theme_main = `${hotServer}/styles/ht-theme-main.css`; 76 | // urls.style_core = `${hotServer}/styles_processed/handsontable.css`; 77 | // urls.style_theme_main = `${hotServer}/styles_processed/ht-theme-main.css`; 78 | // urls.style_theme_horizon = `${hotServer}/styles/ht-theme-horizon.css`; 79 | } else { 80 | urls.script = `https://cdn.jsdelivr.net/npm/handsontable@${hotVersion}/dist/handsontable.full.js`; 81 | // urls.style_core = `https://cdn.jsdelivr.net/npm/handsontable@${hotVersion}/dist/handsontable.full.css`; 82 | urls.style_core = `https://cdn.jsdelivr.net/npm/handsontable@${hotVersion}/styles/handsontable.css`; 83 | urls.style_theme_main = `https://cdn.jsdelivr.net/npm/handsontable@${hotVersion}/styles/ht-theme-main.css`; 84 | // urls.style_theme_horizon = `https://cdn.jsdelivr.net/npm/handsontable@${hotVersion}/styles/ht-theme-horizon.css`; 85 | } 86 | 87 | return urls; 88 | } 89 | -------------------------------------------------------------------------------- /lib/commands/local-server/public/js/bar-item.js: -------------------------------------------------------------------------------- 1 | (function (w, d) { 2 | var colorIndex = -1; 3 | 4 | /** 5 | * @param barManager 6 | * @constructor 7 | */ 8 | function BarItem(barManager) { 9 | Observer.call(this); 10 | this.barManager = barManager; 11 | this.template = null; 12 | this.selectedHotVersion = null; 13 | this.selectedTest = null; 14 | this.uniqColor = this.getFillColor(); 15 | this.chartData = {}; 16 | } 17 | 18 | BarItem.prototype = Object.create(Observer.prototype, { 19 | constructor: { 20 | value: BarItem, 21 | configurable: true, 22 | }, 23 | }); 24 | 25 | /** 26 | * Render bar UI to DOM 27 | * 28 | * @returns {HTMLElement} 29 | */ 30 | BarItem.prototype.renderDOM = function () { 31 | if (this.template) { 32 | return this.template; 33 | } 34 | var _this = this; 35 | var t; 36 | var selHotVersion; 37 | var selTest; 38 | var btnClean; 39 | var form; 40 | 41 | this.template = t = d.importNode(d.querySelector("#bar").content, true); 42 | form = t.querySelector("form"); 43 | selHotVersion = t.querySelector("select[name=hot-version]"); 44 | selTest = t.querySelector("select[name=test]"); 45 | btnClean = t.querySelector(".btn-clean"); 46 | 47 | var hotVersions = Array.from(this.barManager.getStatsMap().keys()); 48 | 49 | hotVersions = hotVersions 50 | .map(function (version) { 51 | return version.replace(/_/g, "."); 52 | }) 53 | .sort(function (a, b) { 54 | return a.localeCompare(b); 55 | }); 56 | 57 | selHotVersion.appendChild(this._buildOptionsList(hotVersions)); 58 | 59 | selHotVersion.addEventListener("change", function () { 60 | selTest.textContent = ""; 61 | selTest.appendChild( 62 | _this._buildOptionsList( 63 | _this.barManager.getStatsMap(selHotVersion.value), 64 | ), 65 | ); 66 | }); 67 | 68 | selTest.addEventListener("change", function () { 69 | _this.selectedHotVersion = selHotVersion.value; 70 | _this.selectedTest = selTest.value; 71 | 72 | _this.emit("change", _this); 73 | }); 74 | btnClean.addEventListener("click", function () { 75 | selHotVersion.value = ""; 76 | selTest.value = ""; 77 | 78 | _this.clearChartData(); 79 | _this.emit("reset", _this); 80 | }); 81 | 82 | return this.template; 83 | }; 84 | 85 | /** 86 | * Parse and save chart data 87 | * 88 | * @param {Object} data 89 | * @returns {Object} 90 | */ 91 | BarItem.prototype.parseChartData = function (data) { 92 | Object.keys(data.metrics).forEach(function (metric) { 93 | var samples = data.samples.map(function (sample) { 94 | return sample.values[metric]; 95 | }); 96 | 97 | this.chartData[metric] = { 98 | fillColor: this.uniqColor(0.7), 99 | strokeColor: this.uniqColor(1), 100 | highlightFill: this.uniqColor(1), 101 | highlightStroke: this.uniqColor(1), 102 | data: samples, 103 | }; 104 | }, this); 105 | 106 | this.chartData.id = data.id; 107 | }; 108 | 109 | /** 110 | * @returns {Object} 111 | */ 112 | BarItem.prototype.hasChartData = function () { 113 | return this.chartData.id ? true : false; 114 | }; 115 | 116 | /** 117 | * @returns {Object} 118 | */ 119 | BarItem.prototype.getChartData = function () { 120 | return this.chartData; 121 | }; 122 | 123 | /** 124 | * 125 | */ 126 | BarItem.prototype.clearChartData = function () { 127 | this.chartData = {}; 128 | }; 129 | 130 | /** 131 | * @returns {Function} 132 | */ 133 | BarItem.prototype.getFillColor = function () { 134 | var color = function (alpha) { 135 | // yellow #F9CC01 136 | return "rgba(249, 204, 1, " + alpha + ")"; 137 | }; 138 | 139 | if (colorIndex === 0) { 140 | color = function (alpha) { 141 | // red #F36247 142 | return "rgba(243, 98, 71, " + alpha + ")"; 143 | }; 144 | } else if (colorIndex === 1) { 145 | color = function (alpha) { 146 | // blue 147 | return "rgba(27, 149, 200, " + alpha + ")"; 148 | }; 149 | } else if (colorIndex === 2) { 150 | color = function (alpha) { 151 | // purple #A35DBD 152 | return "rgba(163, 93, 189, " + alpha + ")"; 153 | }; 154 | } else if (colorIndex === 3) { 155 | color = function (alpha) { 156 | // green #00B972 157 | return "rgba(0, 185, 114, " + alpha + ")"; 158 | }; 159 | } 160 | colorIndex++; 161 | 162 | return color; 163 | }; 164 | 165 | /** 166 | * Build options for select element 167 | * 168 | * @param {Array|Object} list 169 | * @returns {DocumentFragment} 170 | * @private 171 | */ 172 | BarItem.prototype._buildOptionsList = function (list) { 173 | var node = d.createDocumentFragment(), 174 | option; 175 | 176 | option = d.createElement("option"); 177 | option.value = ""; 178 | option.textContent = "..."; 179 | 180 | node.appendChild(option); 181 | 182 | var listOptions = [].concat(list); 183 | 184 | listOptions.forEach(function (item) { 185 | var option = d.createElement("option"); 186 | 187 | if (typeof item === "string") { 188 | option.value = item; 189 | option.textContent = item; 190 | } else { 191 | option.value = item.id; 192 | option.textContent = item.id; 193 | } 194 | 195 | node.appendChild(option); 196 | }); 197 | 198 | return node; 199 | }; 200 | 201 | w.BarItem = BarItem; 202 | })(window, document); 203 | -------------------------------------------------------------------------------- /lib/commands/local-server/public/js/bar-manager.js: -------------------------------------------------------------------------------- 1 | (function (w, d) { 2 | /** 3 | * @constructor 4 | */ 5 | function BarManager(statsMap, sampleSize) { 6 | this.barsContainer = d.querySelector(".bars"); 7 | this.bars = []; 8 | this.charts = []; 9 | this.statsMap = statsMap; 10 | this.sampleSize = sampleSize || 100; 11 | 12 | this.registerChart("scriptTime"); 13 | this.registerChart("renderTime"); 14 | this.registerChart("pureScriptTime"); 15 | this.registerChart("gcTime"); 16 | this.registerChart("gcAmount"); 17 | this.registerChart("majorGcTime"); 18 | 19 | this._createDefaultBars(); 20 | this.renderDOM(); 21 | } 22 | 23 | /** 24 | * Get benchmark results. 25 | * 26 | * @param {String} [hotVersion] Optionally narrow data to test for specify Handsontable version 27 | * @returns {Object} 28 | */ 29 | BarManager.prototype.getStatsMap = function (hotVersion) { 30 | if (hotVersion) { 31 | return this.statsMap.get(hotVersion.replace(/\./g, "_")); 32 | } 33 | 34 | return this.statsMap; 35 | }; 36 | 37 | /** 38 | * @param {String} metric 39 | */ 40 | BarManager.prototype.registerChart = function (metric) { 41 | this.charts.push(new ChartItem(metric, this.sampleSize)); 42 | }; 43 | 44 | /** 45 | * @param {String} metric 46 | * @returns ChartItem 47 | */ 48 | BarManager.prototype.getChart = function (metricName) { 49 | var result; 50 | 51 | this.charts.forEach(function (chart) { 52 | if (chart.metric === metricName) { 53 | result = chart; 54 | } 55 | }); 56 | 57 | return result; 58 | }; 59 | 60 | /** 61 | * Render UI to DOM 62 | */ 63 | BarManager.prototype.renderDOM = function () { 64 | this.bars.forEach(function (bar) { 65 | this.barsContainer.appendChild(bar.renderDOM()); 66 | }, this); 67 | }; 68 | 69 | /** 70 | * Render charts 71 | */ 72 | BarManager.prototype.renderCharts = function () { 73 | var chartData = {}; 74 | 75 | this.bars.forEach(function (bar) { 76 | if (!bar.hasChartData()) { 77 | return; 78 | } 79 | var barData = bar.getChartData(); 80 | 81 | Object.keys(ChartItem.METRICS).forEach(function (metricType) { 82 | var data = barData[metricType]; 83 | 84 | if (!chartData[metricType]) { 85 | chartData[metricType] = []; 86 | } 87 | data.selectedHotVersion = bar.selectedHotVersion; 88 | data.selectedTest = bar.selectedTest; 89 | chartData[metricType].push(data); 90 | }); 91 | }); 92 | 93 | Object.keys(ChartItem.METRICS).forEach(function (metricType) { 94 | var chart = this.getChart(metricType); 95 | 96 | if (!chart) { 97 | return; 98 | } 99 | chart.render(chartData[metricType]); 100 | }, this); 101 | }; 102 | 103 | /** 104 | * @param {BarItem} bar 105 | */ 106 | BarManager.prototype.onBarChanged = function (bar) { 107 | var _this = this; 108 | 109 | var chartData = this.getStatsMap(bar.selectedHotVersion).filter( 110 | function (sample) { 111 | return sample.id === bar.selectedTest; 112 | }, 113 | )[0]; 114 | 115 | bar.parseChartData(chartData); 116 | this.renderCharts(); 117 | }; 118 | 119 | /** 120 | * @param {BarItem} bar 121 | */ 122 | BarManager.prototype.onBarReset = function (bar) { 123 | this.renderCharts(); 124 | }; 125 | 126 | /** 127 | * Create bar empty slots 128 | * @private 129 | */ 130 | BarManager.prototype._createDefaultBars = function () { 131 | this.bars.push(new BarItem(this)); 132 | this.bars.push(new BarItem(this)); 133 | this.bars.push(new BarItem(this)); 134 | this.bars.push(new BarItem(this)); 135 | this.bars.push(new BarItem(this)); 136 | 137 | this.bars.forEach(function (bar) { 138 | bar.on("change", this.onBarChanged.bind(this)); 139 | bar.on("reset", this.onBarReset.bind(this)); 140 | }, this); 141 | }; 142 | 143 | w.BarManager = BarManager; 144 | })(window, document); 145 | -------------------------------------------------------------------------------- /lib/commands/local-server/public/js/chart-item.js: -------------------------------------------------------------------------------- 1 | (function (w, d) { 2 | var METRICS = { 3 | scriptTime: 4 | "Script execution time in ms, including garbage collection and render (lower is better). " + 5 | "To keep 60FPS execution time should not exceed 16.6ms.", 6 | pureScriptTime: 7 | "Script execution time in ms, without garbage collection nor render (lower is better).", 8 | renderTime: "Render time in and outside of script in ms (lower is better).", 9 | gcTime: 10 | "Garbage collection time in and outside of script in ms (lower is better).", 11 | gcAmount: "Garbage collection amount in kilobytes (lower is better).", 12 | majorGcTime: "Time of major garbage collections in ms (lower is better).", 13 | }; 14 | 15 | function parseTooltipMetrics(metric, value) { 16 | var fps = ""; 17 | 18 | if ( 19 | metric === "scriptTime" || 20 | metric === "pureScriptTime" || 21 | metric === "renderTime" 22 | ) { 23 | fps = " (" + ((1000 / parseFloat(value, 10)) >> 0) + "FPS)"; 24 | } 25 | 26 | return parseFloat(value, 10).toFixed(2) + "ms" + fps; 27 | } 28 | 29 | /** 30 | * @constructor 31 | */ 32 | function ChartItem(metric, sampleSize) { 33 | Observer.call(this); 34 | 35 | this.sampleSize = sampleSize; 36 | this.chartOptions = { 37 | barValueSpacing: 1, 38 | barDatasetSpacing: 0, 39 | barStrokeWidth: 1, 40 | barShowStroke: false, 41 | customTooltips: function (tooltip) { 42 | if (!tooltip) { 43 | return; 44 | } 45 | if (tooltip.labels) { 46 | tooltip.title = metric; 47 | tooltip.labels.forEach(function (value, index) { 48 | tooltip.labels[index] = parseTooltipMetrics(metric, value); 49 | }); 50 | } else { 51 | tooltip.text = parseTooltipMetrics(metric, tooltip.text); 52 | } 53 | tooltip.custom = null; 54 | tooltip.draw(); 55 | }, 56 | }; 57 | this.chart = null; 58 | this.metric = metric; 59 | this.container = d.querySelector(".charts"); 60 | this.template = d.importNode(d.querySelector("#chart").content, true); 61 | this.ctx = this.template.querySelector("canvas").getContext("2d"); 62 | 63 | this.template.querySelector(".header").textContent = metric; 64 | this.template.querySelector(".description").textContent = 65 | " - " + METRICS[metric]; 66 | 67 | this.legendEl = this.template.querySelector(".legend"); 68 | this.container.appendChild(this.template); 69 | this.createEmptyCharts(); 70 | } 71 | 72 | ChartItem.prototype = Object.create(Observer.prototype, { 73 | constructor: { 74 | value: Chart, 75 | configurable: true, 76 | }, 77 | }); 78 | 79 | ChartItem.METRICS = METRICS; 80 | 81 | /** 82 | * Render chart data 83 | * 84 | * @param {Object|Array|null} data 85 | */ 86 | ChartItem.prototype.render = function (data) { 87 | data = data ? (Array.isArray(data) ? data : [data]) : null; 88 | 89 | if (this.chart) { 90 | this.chart.destroy(); 91 | } 92 | this.legendEl.textContent = ""; 93 | 94 | if (data) { 95 | this.chart = new Chart(this.ctx).Bar( 96 | { 97 | labels: this._generateFixedArray(), 98 | datasets: data, 99 | }, 100 | this.chartOptions, 101 | ); 102 | 103 | data.forEach(function (d) { 104 | this.legendEl.appendChild(this._buildLegendTemplate(d.strokeColor, d)); 105 | }, this); 106 | } else { 107 | this.createEmptyCharts(); 108 | } 109 | }; 110 | 111 | /** 112 | * Create empty charts 113 | */ 114 | ChartItem.prototype.createEmptyCharts = function () { 115 | var data = { 116 | labels: this._generateFixedArray(), 117 | datasets: [ 118 | { 119 | data: this._generateFixedArray(), 120 | }, 121 | ], 122 | }; 123 | 124 | this.chart = new Chart(this.ctx).Bar(data, this.chartOptions); 125 | }; 126 | 127 | /** 128 | * Build template for chart legend 129 | * 130 | * @param {String} color 131 | * @param {Object} data 132 | * @returns {Node} 133 | * @private 134 | */ 135 | ChartItem.prototype._buildLegendTemplate = function (color, data) { 136 | var template = d.importNode(d.querySelector("#legend").content, true); 137 | 138 | template.querySelector(".color").style.backgroundColor = color; 139 | template.querySelector(".description").textContent = 140 | data.selectedHotVersion + ", " + data.selectedTest; 141 | 142 | return template; 143 | }; 144 | 145 | /** 146 | * Generate array with fixed size filled by empty strings 147 | * 148 | * @returns {Array} 149 | * @private 150 | */ 151 | ChartItem.prototype._generateFixedArray = function () { 152 | return new Array(this.sampleSize).join(",").split(","); 153 | }; 154 | 155 | w.ChartItem = ChartItem; 156 | })(window, document); 157 | -------------------------------------------------------------------------------- /lib/commands/local-server/public/js/observer.js: -------------------------------------------------------------------------------- 1 | (function (w, d) { 2 | /** 3 | * Simple implementation event observer pattern 4 | */ 5 | function Observer() { 6 | this.listeners = {}; 7 | } 8 | 9 | Observer.prototype.emit = function (name, args) { 10 | if (!this.listeners[name]) { 11 | return this; 12 | } 13 | var items = this.listeners[name]; 14 | var itemsLength = this.listeners[name].length; 15 | var i = 0; 16 | 17 | while (i < itemsLength) { 18 | items[i].apply(this, Array.isArray(args) ? args : [args]); 19 | i++; 20 | } 21 | }; 22 | 23 | Observer.prototype.on = function (name, callback) { 24 | if (!this.listeners[name]) { 25 | this.listeners[name] = []; 26 | } 27 | if (this.listeners[name].indexOf(callback) === -1) { 28 | this.listeners[name].push(callback); 29 | } 30 | 31 | return this; 32 | }; 33 | 34 | w.Observer = Observer; 35 | })(window, document); 36 | -------------------------------------------------------------------------------- /lib/commands/local-server/public/js/test-runner.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Parse Handsontable settings from query string 3 | * 4 | * @returns {Object} 5 | */ 6 | function getHotSettings() { 7 | var settings = parseQueryString(location.search.substr(1)); 8 | 9 | for (var i in settings) { 10 | if (!settings.hasOwnProperty(i)) { 11 | continue; 12 | } 13 | if (i === "data") { 14 | settings[i] = Handsontable.helper.createSpreadsheetData( 15 | settings[i].split(",")[0], 16 | settings[i].split(",")[1], 17 | ); 18 | } else { 19 | settings[i] = parseSetting(settings[i]); 20 | } 21 | } 22 | if (!settings.data) { 23 | settings.data = Handsontable.helper.createSpreadsheetData(1000, 1000); 24 | } 25 | 26 | return settings; 27 | } 28 | 29 | /** 30 | * Parse query string to object 31 | * 32 | * @param {String} queryString 33 | * @returns {Object} 34 | */ 35 | function parseQueryString(queryString) { 36 | var params = {}, 37 | queries, 38 | temp, 39 | i, 40 | l; 41 | 42 | queries = queryString.split("&"); 43 | l = queries.length; 44 | 45 | for (i = 0; i < l; i++) { 46 | temp = queries[i].split("="); 47 | params[temp[0]] = temp[1]; 48 | } 49 | 50 | return params; 51 | } 52 | 53 | /** 54 | * Parse single Handsontable setting 55 | * 56 | * @param {*} value 57 | * @returns {*} 58 | */ 59 | function parseSetting(value) { 60 | try { 61 | value = JSON.parse(value); 62 | } catch (ex) {} 63 | 64 | return value; 65 | } 66 | -------------------------------------------------------------------------------- /lib/commands/local-server/templates/benchmark-viewer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 27 | 28 | 29 | 30 |
31 |
Handsontable benchmark viewer
32 |
33 | 34 |
35 |
36 |
Select input data to compare (max 5)
37 |
38 | 39 |
40 |
41 |
42 | 43 | 44 | 64 | 65 | 66 | 78 | 79 | 80 | 84 | 85 | 86 | 87 | 88 | 89 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /lib/commands/local-server/templates/test-runner-a.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 17 | 18 | 19 | 20 | 21 |
22 | 161 | 162 | 167 | 168 | 169 | -------------------------------------------------------------------------------- /lib/commands/local-server/templates/test-runner-b.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 17 | 18 | 19 | 20 | 21 |
22 | 161 | 162 | 167 | 168 | 169 | -------------------------------------------------------------------------------- /lib/commands/local-server/templates/test-runner-c.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 17 | 18 | 19 | 20 | 21 |
22 | 148 | 149 | 154 | 155 | 156 | -------------------------------------------------------------------------------- /lib/commands/local-server/templates/test-runner-d.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 17 | 18 | 19 | 20 | 21 |
22 | 101 | 102 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /lib/commands/local-server/templates/test-runner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 16 | 17 | 18 | 19 |
20 | 41 | 42 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /lib/commands/protractor/index.js: -------------------------------------------------------------------------------- 1 | const { fork } = require("child_process"); 2 | 3 | module.exports = function ({ 4 | hotVersion = "latest", 5 | cpuThrottleRate = 0, 6 | hotServer, 7 | testName, 8 | } = {}) { 9 | console.log("Running protractor..."); 10 | 11 | const env = process.env; 12 | 13 | env.HOT_VERSION = testName ? testName : hotServer ? "develop" : hotVersion; 14 | env.CPU_THROTTLE_RATE = cpuThrottleRate; 15 | const childProcess = fork( 16 | "./node_modules/.bin/protractor", 17 | ["protractor.conf.js"], 18 | { env }, 19 | ); 20 | 21 | let isDone = false; 22 | let promiseResolver = null; 23 | let pendingPromise = new Promise((resolve) => { 24 | promiseResolver = resolve; 25 | }); 26 | 27 | childProcess.on("exit", function (code) { 28 | promiseResolver(null); 29 | isDone = true; 30 | }); 31 | childProcess.on("message", function (sampleResults) { 32 | const samples = JSON.parse(sampleResults); 33 | 34 | promiseResolver(samples); 35 | pendingPromise = new Promise((resolve) => { 36 | promiseResolver = resolve; 37 | }); 38 | }); 39 | 40 | return async function* _resultsGenerator() { 41 | while (true) { 42 | if (isDone) { 43 | break; 44 | } 45 | 46 | yield pendingPromise; 47 | } 48 | }; 49 | }; 50 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | SAMPLE_SIZE: 100, 3 | SERVER_HOST: "localhost", 4 | SERVER_PORT: "8082", 5 | DB_URL: "mongodb://root:root@localhost:27017", 6 | DB_NAME: "performance_lab", 7 | }; 8 | -------------------------------------------------------------------------------- /lib/storage/index.js: -------------------------------------------------------------------------------- 1 | const { 2 | connect, 3 | close, 4 | insertMany, 5 | findAll, 6 | dropIfExists, 7 | } = require("./mongo"); 8 | const axios = require("axios"); 9 | 10 | module.exports.saveByReplace = async function (statsGenerator) { 11 | const stats = statsGenerator(); 12 | const collections = new Map(); 13 | let hotVersion; 14 | 15 | for await (const stat of stats) { 16 | if (stat === null) { 17 | continue; 18 | } 19 | 20 | const { id, metrics, description } = stat.description; 21 | const samples = stat.completeSample; 22 | 23 | hotVersion = description.hotVersion; 24 | 25 | // Convert "latest" tag to specific semver value 26 | if (hotVersion === "latest") { 27 | const response = await axios.get( 28 | "https://api.cdnjs.com/libraries/handsontable?fields=version", 29 | ); 30 | const data = response.data; 31 | 32 | hotVersion = data.version; 33 | } 34 | 35 | hotVersion = hotVersion.replace(/\./g, "_"); 36 | 37 | if (!collections.has(hotVersion)) { 38 | collections.set(hotVersion, []); 39 | } 40 | 41 | collections.get(hotVersion).push({ 42 | id, 43 | metrics, 44 | samples, 45 | }); 46 | } 47 | 48 | await connect(); 49 | await dropIfExists(hotVersion); 50 | 51 | for (const [name, items] of collections) { 52 | await insertMany(name, items); 53 | } 54 | 55 | await close(); 56 | }; 57 | 58 | module.exports.loadAll = async function () { 59 | await connect(); 60 | const results = await findAll(); 61 | await close(); 62 | 63 | return results; 64 | }; 65 | -------------------------------------------------------------------------------- /lib/storage/mongo.js: -------------------------------------------------------------------------------- 1 | const MongoClient = require("mongodb").MongoClient; 2 | const config = require("./../config"); 3 | 4 | const dbName = config.DB_NAME; 5 | 6 | let currentConnection = null; 7 | 8 | async function connect() { 9 | if (currentConnection === null) { 10 | currentConnection = await MongoClient.connect(config.DB_URL, { 11 | useNewUrlParser: true, 12 | useUnifiedTopology: true, 13 | }); 14 | } 15 | 16 | return currentConnection; 17 | } 18 | 19 | async function close() { 20 | if (currentConnection !== null) { 21 | currentConnection.close(); 22 | currentConnection = null; 23 | } 24 | } 25 | 26 | async function insertMany(collectionName, items) { 27 | const collection = currentConnection.db(dbName).collection(collectionName); 28 | 29 | const queryResult = await collection.insertMany(items); 30 | 31 | return queryResult; 32 | } 33 | 34 | async function findAll(collectionName) { 35 | const collections = await currentConnection.db(dbName).collections(); 36 | const queryResult = new Map(); 37 | 38 | for (collection of collections) { 39 | const collectionName = collection.collectionName; 40 | 41 | if (!queryResult.has(collectionName)) { 42 | queryResult.set(collectionName, []); 43 | } 44 | 45 | const collectionResults = await collection.find().toArray(); 46 | 47 | queryResult.get(collectionName).push(...collectionResults); 48 | } 49 | 50 | return queryResult; 51 | } 52 | 53 | async function dropIfExists(collectionName) { 54 | let queryResult = null; 55 | 56 | if (await isCollectionExists(collectionName)) { 57 | const collection = currentConnection.db(dbName).collection(collectionName); 58 | 59 | queryResult = await collection.drop(); 60 | } 61 | 62 | return queryResult; 63 | } 64 | 65 | async function isCollectionExists(collectionName) { 66 | const collections = await currentConnection 67 | .db(dbName) 68 | .listCollections() 69 | .toArray(); 70 | 71 | return collections.some((collection) => collection.name === collectionName); 72 | } 73 | 74 | module.exports = { 75 | isCollectionExists, 76 | dropIfExists, 77 | findAll, 78 | insertMany, 79 | close, 80 | connect, 81 | }; 82 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "performance-lab", 3 | "version": "2.4.0", 4 | "description": "Performance tests for Handsontable", 5 | "main": "./lib/cli.js", 6 | "bin": "./bin/hot-perf", 7 | "private": true, 8 | "devDependencies": { 9 | "@angular/benchpress": "^0.3.0", 10 | "@hapi/hapi": "^19.2.0", 11 | "@hapi/inert": "^6.0.1", 12 | "@hapi/vision": "^6.0.0", 13 | "axios": "^0.19.2", 14 | "caporal": "^1.4.0", 15 | "chromedriver": "^134.0.0", 16 | "fs-extra": "^9.0.1", 17 | "handlebars": "^4.7.6", 18 | "mongodb": "^3.6.0", 19 | "prettier": "3.0.3", 20 | "protractor": "^7.0.0", 21 | "rxjs": "^7.8.1", 22 | "semver": "^7.3.2" 23 | }, 24 | "scripts": { 25 | "start": "./bin/hot-perf run", 26 | "serve": "./bin/hot-perf local-server benchmark-viewer", 27 | "postinstall": "chmod +x bin/hot-perf" 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "https://github.com/handsontable/performance-lab.git" 32 | }, 33 | "keywords": [ 34 | "performance", 35 | "test", 36 | "handsontable" 37 | ], 38 | "author": "Handsoncode ", 39 | "license": "MIT", 40 | "bugs": { 41 | "url": "https://github.com/handsontable/performance-lab/issues" 42 | }, 43 | "engines": { 44 | "node": ">=11" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /protractor.conf.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | const USE_HEADLESS_MODE = false; 4 | const CPU_THROTTLE_RATE = process.env.CPU_THROTTLE_RATE; 5 | 6 | exports.config = { 7 | directConnect: true, 8 | chromeDriver: path.resolve("./node_modules/chromedriver/bin/chromedriver"), 9 | // SELENIUM_PROMISE_MANAGER: false, 10 | capabilities: { 11 | browserName: "chrome", 12 | chromeOptions: { 13 | // binary: path.resolve("./temp/chrome_125/Google.app/Contents/MacOS/Google"), 14 | args: [ 15 | "--js-flags=--expose-gc", 16 | "--window-size=1300,1000", 17 | "--disable-dev-shm-usage", 18 | "--no-sandbox", 19 | "--disable-extensions", 20 | "--disable-infobars", 21 | "--disable-notifications", 22 | "--disable-popup-blocking", 23 | "--disable-default-apps", 24 | "--enable-automation", 25 | "--log-level=3", 26 | `user-data-dir=${path.resolve(`./temp/browser_profile/${Math.random()}`)}`, 27 | ...(USE_HEADLESS_MODE ? ["--headless", "--disable-gpu"] : []), 28 | ], 29 | perfLoggingPrefs: { 30 | traceCategories: 31 | "v8,blink.console,devtools.timeline,devtools.timeline.frame,blink.user_timing", 32 | }, 33 | // 'mobileEmulation': { 34 | // 'deviceMetrics': { 35 | // 'width': 600, 36 | // 'height': 960, 37 | // 'pixelRatio': 1 38 | // } 39 | // } 40 | }, 41 | loggingPrefs: { 42 | performance: "ALL", 43 | browser: "ALL", 44 | driver: "ALL", 45 | }, 46 | }, 47 | 48 | // specs: ["test/spec/**/*.spec.js"], 49 | // specs: ['test/config.js', 'test/spec/arrow-keys-navigation.spec.js', 'test/spec/editing.spec.js', 'test/spec/view-scrolling.spec.js'], 50 | specs: ['test/config.js', 'test/spec/arrow-keys-navigation.spec.js'], 51 | // specs: ['test/config.js', 'test/spec/editing.spec.js'], 52 | // specs: ['test/config.js', 'test/spec/altering.spec.js'], 53 | // specs: ['test/config.js', 'test/spec/view-scrolling.spec.js'], 54 | framework: "jasmine2", 55 | 56 | onPrepare: function () { 57 | patchProtractorWait(browser); 58 | 59 | beforeEach(function () { 60 | patchProtractorWait(browser); 61 | }); 62 | }, 63 | 64 | restartBrowserBetweenTests: true, 65 | skipSourceMapSupport: true, 66 | 67 | jasmineNodeOpts: { 68 | showColors: true, 69 | // 5 minute timeout 70 | defaultTimeoutInterval: 300000, 71 | }, 72 | }; 73 | 74 | function patchProtractorWait(browser) { 75 | // Tells protractor this isn't an Angular application 76 | browser.ignoreSynchronization = true; 77 | 78 | if (CPU_THROTTLE_RATE) { 79 | browser.driver.sendChromiumCommand("Emulation.setCPUThrottlingRate", { 80 | rate: parseInt(CPU_THROTTLE_RATE, 10), 81 | }); 82 | } 83 | 84 | // const _get = browser.get; 85 | // const sleepInterval = process.env.TRAVIS || process.env.JENKINS_URL ? 7000 : 3000; 86 | // 87 | // browser.get = function() { 88 | // const result = _get.apply(this, arguments); 89 | // 90 | // browser.sleep(sleepInterval); 91 | // 92 | // return result; 93 | // } 94 | } 95 | -------------------------------------------------------------------------------- /test/config.js: -------------------------------------------------------------------------------- 1 | const origIt = global.it; 2 | 3 | global.it = function (name, cb) { 4 | origIt.call(this, name, function (done) { 5 | cb().then(done, done.fail); 6 | }); 7 | }; 8 | -------------------------------------------------------------------------------- /test/runner.js: -------------------------------------------------------------------------------- 1 | const config = require("./../lib/config"); 2 | 3 | const HOT_VERSION = process.env.HOT_VERSION; 4 | 5 | exports.SAMPLE_SIZE = config.SAMPLE_SIZE; 6 | 7 | exports.runSample = async function (benchpressConfig) { 8 | const benchpress = await import("@angular/benchpress"); 9 | 10 | const bindings = [ 11 | benchpress.SeleniumWebDriverAdapter.PROTRACTOR_PROVIDERS, 12 | // { provide: benchpress.Options.FORCE_GC, useValue: true }, 13 | { 14 | provide: benchpress.RegressionSlopeValidator.SAMPLE_SIZE, 15 | useValue: benchpressConfig.SAMPLE_SIZE ?? config.SAMPLE_SIZE, 16 | }, 17 | benchpress.JsonFileReporter.PROVIDERS, 18 | benchpress.MultiReporter.provideWith([ 19 | benchpress.ConsoleReporter, 20 | benchpress.JsonFileReporter, 21 | ]), 22 | ]; 23 | 24 | benchpress.Options.DEFAULT_PROVIDERS.push({ 25 | provide: benchpress.Options.WRITE_FILE, 26 | useValue: (filename, content) => void process.send(content), 27 | }); 28 | 29 | const runner = new benchpress.Runner(bindings); 30 | 31 | benchpressConfig.providers = [ 32 | { 33 | provide: benchpress.Options.SAMPLE_DESCRIPTION, 34 | useValue: { hotVersion: HOT_VERSION }, 35 | }, 36 | ]; 37 | 38 | return new Promise((resolve) => { 39 | setTimeout(() => resolve(runner.sample(benchpressConfig)), 50); 40 | }); 41 | }; 42 | 43 | exports.openPage = async function (hotSettings) { 44 | const urlParams = []; 45 | 46 | hotSettings = hotSettings || {}; 47 | 48 | Object.keys(hotSettings).forEach((paramName) => { 49 | urlParams.push(paramName + "=" + hotSettings[paramName]); 50 | }); 51 | 52 | const url = `http://${config.SERVER_HOST}:${ 53 | config.SERVER_PORT 54 | }?${urlParams.join("&")}`; 55 | 56 | return new Promise((resolve) => { 57 | browser.get(encodeURI(url)); 58 | 59 | // setTimeout(() => resolve(url), 500); 60 | resolve(url); 61 | }); 62 | }; 63 | -------------------------------------------------------------------------------- /test/spec/altering.spec.js: -------------------------------------------------------------------------------- 1 | const { runSample, openPage } = require("./../runner"); 2 | const { waitUntilHotIsInitialized, sleep } = require("./../utils"); 3 | 4 | describe("altering a table", () => { 5 | it("started creating row", async () => { 6 | await openPage(); 7 | await waitUntilHotIsInitialized(); 8 | 9 | await runSample({ 10 | id: "altering.creating-row-top", 11 | execute: () => { 12 | browser.executeScript(`hot.alter('insert_row_above', 1, 5)`); 13 | }, 14 | }); 15 | }); 16 | 17 | it("started creating column", async () => { 18 | await openPage(); 19 | await waitUntilHotIsInitialized(); 20 | 21 | await runSample({ 22 | id: "altering.creating-column-top", 23 | execute: () => { 24 | browser.executeScript(`hot.alter('insert_col_start', 1, 5)`); 25 | }, 26 | }); 27 | }); 28 | 29 | it("started loading new data", async () => { 30 | await openPage(); 31 | await waitUntilHotIsInitialized(); 32 | 33 | await runSample({ 34 | id: "altering.loading-data", 35 | execute: () => { 36 | browser.executeScript( 37 | `hot.loadData(Handsontable.helper.createSpreadsheetData(400, 100))`, 38 | ); 39 | }, 40 | }); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /test/spec/arrow-keys-navigation.spec.js: -------------------------------------------------------------------------------- 1 | const { runSample, openPage, SAMPLE_SIZE } = require("./../runner"); 2 | const { waitUntilHotIsInitialized, sleep } = require("./../utils"); 3 | 4 | describe("navigating by arrow key", () => { 5 | describe("arrow down", () => { 6 | it("started from the most top-left position", async () => { 7 | await openPage(); 8 | await waitUntilHotIsInitialized(); 9 | 10 | browser.executeScript("hot.selectCell(25, 0)"); 11 | 12 | await runSample({ 13 | id: "arrow-down.most-top-left", 14 | execute: () => { 15 | browser.actions().sendKeys(protractor.Key.ARROW_DOWN).perform(); 16 | }, 17 | }); 18 | }); 19 | 20 | it("started from the middle position", async () => { 21 | await openPage(); 22 | await waitUntilHotIsInitialized(); 23 | 24 | browser.executeScript(` 25 | hot.selectCell(parseInt(hot.countRows() / 2, 10), parseInt(hot.countCols() / 2, 10)) 26 | `); 27 | 28 | await runSample({ 29 | id: "arrow-down.middle", 30 | execute: () => { 31 | browser.actions().sendKeys(protractor.Key.ARROW_DOWN).perform(); 32 | }, 33 | }); 34 | }); 35 | }); 36 | 37 | describe("arrow up", () => { 38 | it("started from the most bottom-right position", async () => { 39 | await openPage(); 40 | await waitUntilHotIsInitialized(); 41 | 42 | browser.executeScript(` 43 | var __rows = hot.countRows() - 1; 44 | var __cols = hot.countCols() - 1; 45 | 46 | hot.selectCell(__rows, __cols); 47 | hot.scrollViewportTo(__rows, __cols, false, true); 48 | `); 49 | 50 | await runSample({ 51 | id: "arrow-up.most-bottom-right", 52 | execute: () => { 53 | browser.actions().sendKeys(protractor.Key.ARROW_UP).perform(); 54 | }, 55 | }); 56 | }); 57 | }); 58 | 59 | describe("arrow right", () => { 60 | it("started from the most top-left position", async () => { 61 | await openPage(); 62 | await waitUntilHotIsInitialized(); 63 | 64 | browser.executeScript(` 65 | hot.selectCell(20, 20); 66 | `); 67 | 68 | await runSample({ 69 | id: "arrow-right.most-top-left", 70 | execute: () => { 71 | browser.actions().sendKeys(protractor.Key.ARROW_RIGHT).perform(); 72 | }, 73 | }); 74 | }); 75 | 76 | it("started from the middle position", async () => { 77 | await openPage(); 78 | await waitUntilHotIsInitialized(); 79 | 80 | browser.executeScript(` 81 | hot.selectCell(parseInt(hot.countRows() / 2, 10), parseInt(hot.countCols() / 2, 10)) 82 | `); 83 | 84 | await runSample({ 85 | id: "arrow-right.middle", 86 | execute: () => { 87 | browser.actions().sendKeys(protractor.Key.ARROW_RIGHT).perform(); 88 | }, 89 | }); 90 | }); 91 | }); 92 | 93 | describe("arrow left", () => { 94 | it("started from the most bottom-right position", async () => { 95 | await openPage(); 96 | await waitUntilHotIsInitialized(); 97 | 98 | browser.executeScript(` 99 | var __rows = hot.countRows() - 1; 100 | var __cols = hot.countCols() - 1; 101 | 102 | hot.selectCell(__rows, __cols); 103 | hot.scrollViewportTo(__rows, __cols, true, false); 104 | `); 105 | 106 | await runSample({ 107 | id: "arrow-left.most-bottom-right", 108 | execute: () => { 109 | browser.actions().sendKeys(protractor.Key.ARROW_LEFT).perform(); 110 | }, 111 | }); 112 | }); 113 | }); 114 | 115 | describe("arrow down and arrow up", () => { 116 | it("started from the middle position and back to the initial position", async () => { 117 | await openPage(); 118 | await waitUntilHotIsInitialized(); 119 | 120 | browser.executeScript(` 121 | hot.selectCell(parseInt(hot.countRows() / 2, 10), parseInt(hot.countCols() / 2, 10)) 122 | `); 123 | 124 | let sampleSize = SAMPLE_SIZE; 125 | let currentSampleSize = 0; 126 | 127 | await runSample({ 128 | id: "arrow-down-up.middle", 129 | execute: () => { 130 | if (currentSampleSize > sampleSize / 2) { 131 | browser.actions().sendKeys(protractor.Key.ARROW_UP).perform(); 132 | browser.actions().sendKeys(protractor.Key.ARROW_UP).perform(); 133 | } else { 134 | browser.actions().sendKeys(protractor.Key.ARROW_DOWN).perform(); 135 | browser.actions().sendKeys(protractor.Key.ARROW_DOWN).perform(); 136 | } 137 | 138 | currentSampleSize++; 139 | }, 140 | }); 141 | }); 142 | }); 143 | 144 | describe("page down", () => { 145 | it("started from the most top-left position", async () => { 146 | await openPage(); 147 | await waitUntilHotIsInitialized(); 148 | 149 | browser.executeScript("hot.selectCell(0, 0)"); 150 | 151 | await runSample({ 152 | id: "page-down.most-top-left", 153 | execute: () => { 154 | browser.actions().sendKeys(protractor.Key.PAGE_DOWN).perform(); 155 | }, 156 | }); 157 | }); 158 | }); 159 | 160 | describe("page down and page up", () => { 161 | it("started from the middle position and back to the initial position", async () => { 162 | await openPage(); 163 | await waitUntilHotIsInitialized(); 164 | 165 | browser.executeScript(`hot.selectCell(0, 0)`); 166 | 167 | let sampleSize = SAMPLE_SIZE; 168 | let currentSampleSize = 0; 169 | 170 | await runSample({ 171 | id: "page-down-up.most-top-left", 172 | execute: () => { 173 | if (currentSampleSize > sampleSize / 2) { 174 | browser.actions().sendKeys(protractor.Key.PAGE_UP).perform(); 175 | } else { 176 | browser.actions().sendKeys(protractor.Key.PAGE_DOWN).perform(); 177 | } 178 | 179 | currentSampleSize++; 180 | }, 181 | }); 182 | }); 183 | }); 184 | }); 185 | -------------------------------------------------------------------------------- /test/spec/editing.spec.js: -------------------------------------------------------------------------------- 1 | const { runSample, openPage } = require("./../runner"); 2 | const { waitUntilHotIsInitialized, sleep } = require("./../utils"); 3 | 4 | describe("editing a cell", () => { 5 | it("started from the most top-left position", async () => { 6 | await openPage(); 7 | await waitUntilHotIsInitialized(); 8 | 9 | browser.executeScript(` 10 | hot.selectCell(2, 2); 11 | hot.scrollViewportTo(20, 20, false, true); 12 | `); 13 | 14 | await runSample({ 15 | id: "editing-cell.most-top-left", 16 | execute: () => { 17 | browser.actions().sendKeys(protractor.Key.ENTER).perform(); 18 | }, 19 | }); 20 | }); 21 | 22 | it("started from the middle position", async () => { 23 | await openPage(); 24 | await waitUntilHotIsInitialized(); 25 | 26 | browser.executeScript(` 27 | var __rows = parseInt(hot.countRows() / 2, 10); 28 | var __cols = parseInt(hot.countCols() / 2, 10); 29 | 30 | hot.selectCell(__rows, __cols); 31 | hot.scrollViewportTo(__rows, __cols, false, true); 32 | `); 33 | 34 | await runSample({ 35 | id: "editing-cell.middle", 36 | execute: () => { 37 | browser.actions().sendKeys(protractor.Key.ENTER).perform(); 38 | }, 39 | }); 40 | }); 41 | 42 | it("started from the bottom-right position", async () => { 43 | await openPage(); 44 | await waitUntilHotIsInitialized(); 45 | 46 | browser.executeScript(` 47 | var __rows = hot.countRows() - 1; 48 | var __cols = hot.countCols() - 1; 49 | 50 | hot.selectCell(__rows, __cols); 51 | hot.scrollViewportTo(__rows, __cols, true, true); 52 | `); 53 | 54 | await runSample({ 55 | id: "editing-cell.bottom-right", 56 | execute: () => { 57 | browser 58 | .actions() 59 | .sendKeys(protractor.Key.ENTER) 60 | .sendKeys(protractor.Key.SHIFT) 61 | .perform(); 62 | }, 63 | }); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /test/spec/view-scrolling.spec.js: -------------------------------------------------------------------------------- 1 | const { runSample, openPage } = require("./../runner"); 2 | const { waitUntilHotIsInitialized, sleep } = require("./../utils"); 3 | const SCROLL_STEP = 50; 4 | 5 | const wtHolderInjection = (overlay = "master", scrollType) => ` 6 | window.wtHolder = document.querySelector('.ht_${overlay} .wtHolder'); 7 | window.step = document.querySelector('.ht_master .wtHolder').${scrollType}; 8 | `; 9 | 10 | describe("navigating by scroll", () => { 11 | describe("master table", () => { 12 | it("scroll down starting from the most top-left position", async () => { 13 | await openPage(); 14 | await waitUntilHotIsInitialized(); 15 | 16 | browser.executeScript(` 17 | ${wtHolderInjection("master", "scrollTop")} 18 | `); 19 | 20 | await runSample({ 21 | id: "scroll-down.master.most-top-left", 22 | execute: () => { 23 | browser.executeScript( 24 | `wtHolder.scrollTop = (step = step + ${SCROLL_STEP});`, 25 | ); 26 | }, 27 | }); 28 | }); 29 | 30 | xit("scroll down starting from the middle position", async () => { 31 | await openPage(); 32 | await waitUntilHotIsInitialized(); 33 | 34 | browser.executeScript(` 35 | var __rows = parseInt(hot.countRows() / 2, 10); 36 | var __cols = parseInt(hot.countCols() / 2, 10); 37 | 38 | hot.selectCell(__rows, __cols); 39 | ${wtHolderInjection("master", "scrollTop")} 40 | `); 41 | 42 | await runSample({ 43 | id: "scroll-down.master.middle", 44 | execute: () => { 45 | browser.executeScript( 46 | `wtHolder.scrollTop = (step = step + ${SCROLL_STEP});`, 47 | ); 48 | }, 49 | }); 50 | }); 51 | 52 | it("scroll right starting from the top-left position", async () => { 53 | await openPage(); 54 | await waitUntilHotIsInitialized(); 55 | 56 | browser.executeScript(` 57 | ${wtHolderInjection("master", "scrollLeft")} 58 | `); 59 | 60 | await runSample({ 61 | id: "scroll-right.master.top-left", 62 | execute: () => { 63 | browser.executeScript( 64 | `wtHolder.scrollLeft = (step = step + ${SCROLL_STEP});`, 65 | ); 66 | }, 67 | }); 68 | }); 69 | 70 | xit("scroll right starting from the middle position", async () => { 71 | await openPage(); 72 | await waitUntilHotIsInitialized(); 73 | 74 | browser.executeScript(` 75 | var __rows = parseInt(hot.countRows() / 2, 10); 76 | var __cols = parseInt(hot.countCols() / 2, 10); 77 | 78 | hot.selectCell(__rows, __cols); 79 | ${wtHolderInjection("master", "scrollLeft")} 80 | `); 81 | 82 | await runSample({ 83 | id: "scroll-right.master.middle", 84 | execute: () => { 85 | browser.executeScript( 86 | `wtHolder.scrollLeft = (step = step + ${SCROLL_STEP});`, 87 | ); 88 | }, 89 | }); 90 | }); 91 | }); 92 | 93 | xdescribe("top overlay", () => { 94 | it("scroll down starting from the most top-left position", async () => { 95 | await openPage(); 96 | await waitUntilHotIsInitialized(); 97 | 98 | browser.executeScript(` 99 | hot.selectCell(20, 20); 100 | ${wtHolderInjection("clone_top", "scrollTop")} 101 | `); 102 | 103 | await runSample({ 104 | id: "scroll-down.top-overlay.most-top-left", 105 | execute: () => { 106 | browser.executeScript( 107 | `wtHolder.dispatchEvent(new WheelEvent('wheel', {'deltaY': ${ 108 | SCROLL_STEP / 2.5 109 | }, 'deltaMode': 0}));`, 110 | ); 111 | }, 112 | }); 113 | }); 114 | 115 | it("scroll down starting from the middle position", async () => { 116 | await openPage(); 117 | await waitUntilHotIsInitialized(); 118 | 119 | browser.executeScript(` 120 | var __rows = parseInt(hot.countRows() / 2, 10); 121 | var __cols = parseInt(hot.countCols() / 2, 10); 122 | 123 | hot.selectCell(__rows, __cols); 124 | ${wtHolderInjection("clone_top", "scrollTop")} 125 | `); 126 | 127 | await runSample({ 128 | id: "scroll-down.top-overlay.middle", 129 | execute: () => { 130 | browser.executeScript( 131 | `wtHolder.dispatchEvent(new WheelEvent('wheel', {'deltaY': ${ 132 | SCROLL_STEP / 2.5 133 | }, 'deltaMode': 0}));`, 134 | ); 135 | }, 136 | }); 137 | }); 138 | 139 | it("scroll right starting from the top-left position", async () => { 140 | await openPage(); 141 | await waitUntilHotIsInitialized(); 142 | 143 | browser.executeScript(` 144 | hot.selectCell(20, 20); 145 | ${wtHolderInjection("clone_top", "scrollLeft")} 146 | `); 147 | 148 | await runSample({ 149 | id: "scroll-right.top-overlay.top-left", 150 | execute: () => { 151 | browser.executeScript( 152 | `wtHolder.dispatchEvent(new WheelEvent('wheel', {'deltaX': ${ 153 | SCROLL_STEP / 2.5 154 | }, 'deltaMode': 0}));`, 155 | ); 156 | }, 157 | }); 158 | }); 159 | 160 | it("scroll right starting from the middle position", async () => { 161 | await openPage(); 162 | await waitUntilHotIsInitialized(); 163 | 164 | browser.executeScript(` 165 | var __rows = parseInt(hot.countRows() / 2, 10); 166 | var __cols = parseInt(hot.countCols() / 2, 10); 167 | 168 | hot.selectCell(__rows, __cols); 169 | ${wtHolderInjection("clone_top", "scrollLeft")} 170 | `); 171 | 172 | await runSample({ 173 | id: "scroll-right.top-overlay.top-left", 174 | execute: () => { 175 | browser.executeScript( 176 | `wtHolder.dispatchEvent(new WheelEvent('wheel', {'deltaX': ${ 177 | SCROLL_STEP / 2.5 178 | }, 'deltaMode': 0}));`, 179 | ); 180 | }, 181 | }); 182 | }); 183 | }); 184 | 185 | xdescribe("left overlay", () => { 186 | it("scroll down starting from the most top-left position", async () => { 187 | await openPage(); 188 | await waitUntilHotIsInitialized(); 189 | 190 | browser.executeScript(` 191 | hot.selectCell(20, 20); 192 | ${wtHolderInjection("clone_left", "scrollTop")} 193 | `); 194 | 195 | await runSample({ 196 | id: "scroll-down.left-overlay.most-top-left", 197 | execute: () => { 198 | browser.executeScript( 199 | `wtHolder.dispatchEvent(new WheelEvent('wheel', {'deltaY': ${ 200 | SCROLL_STEP / 2.5 201 | }, 'deltaMode': 0}));`, 202 | ); 203 | }, 204 | }); 205 | }); 206 | 207 | it("scroll down starting from the middle position", async () => { 208 | await openPage(); 209 | await waitUntilHotIsInitialized(); 210 | 211 | browser.executeScript(` 212 | var __rows = parseInt(hot.countRows() / 2, 10); 213 | var __cols = parseInt(hot.countCols() / 2, 10); 214 | 215 | hot.selectCell(__rows, __cols); 216 | ${wtHolderInjection("clone_left", "scrollTop")} 217 | `); 218 | 219 | await runSample({ 220 | id: "scroll-down.left-overlay.most-top-left", 221 | execute: () => { 222 | browser.executeScript( 223 | `wtHolder.dispatchEvent(new WheelEvent('wheel', {'deltaY': ${ 224 | SCROLL_STEP / 2.5 225 | }, 'deltaMode': 0}));`, 226 | ); 227 | }, 228 | }); 229 | }); 230 | 231 | it("scroll right starting from the top-left position", async () => { 232 | await openPage(); 233 | await waitUntilHotIsInitialized(); 234 | 235 | browser.executeScript(` 236 | hot.selectCell(20, 20); 237 | ${wtHolderInjection("clone_left", "scrollLeft")} 238 | `); 239 | 240 | await runSample({ 241 | id: "scroll-right.left-overlay.most-top-left", 242 | execute: () => { 243 | browser.executeScript( 244 | `wtHolder.dispatchEvent(new WheelEvent('wheel', {'deltaX': ${ 245 | SCROLL_STEP / 2.5 246 | }, 'deltaMode': 0}));`, 247 | ); 248 | }, 249 | }); 250 | }); 251 | 252 | it("scroll right starting from the middle position", async () => { 253 | await openPage(); 254 | await waitUntilHotIsInitialized(); 255 | 256 | browser.executeScript(` 257 | var __rows = parseInt(hot.countRows() / 2, 10); 258 | var __cols = parseInt(hot.countCols() / 2, 10); 259 | 260 | hot.selectCell(__rows, __cols); 261 | ${wtHolderInjection("clone_left", "scrollLeft")} 262 | `); 263 | 264 | await runSample({ 265 | id: "scroll-right.left-overlay.middle", 266 | execute: () => { 267 | browser.executeScript( 268 | `wtHolder.dispatchEvent(new WheelEvent('wheel', {'deltaX': ${ 269 | SCROLL_STEP / 2.5 270 | }, 'deltaMode': 0}));`, 271 | ); 272 | }, 273 | }); 274 | }); 275 | }); 276 | }); 277 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | module.exports.waitUntilHotIsInitialized = async function () { 2 | return await browser.controlFlow().wait(async () => { 3 | const title = await browser.driver.getTitle(); 4 | 5 | return title === "ready"; 6 | }, 5000); 7 | }; 8 | 9 | module.exports.sleep = async function (delay = 1000) { 10 | return new Promise((r) => { 11 | setTimeout(() => r(), delay); 12 | }); 13 | }; 14 | --------------------------------------------------------------------------------