From e5d31e83a52917397223a4066cae5feb2e9755df Mon Sep 17 00:00:00 2001 From: Mangala Sadhu Sangeet Singh Khalsa Date: Fri, 22 May 2020 16:36:23 -0700 Subject: [PATCH] Add Node.js REST server and client-side REST mock (#1470) dgrid's tests currently rely on a PHP script for REST testing. This change adds a Node.js REST server and client-side REST mocking with 'dojo/request/registry' --- package.json | 3 + test/Rest.html | 284 ++++++++++++++------------- test/data/rest-node.js | 66 +++++++ test/data/restHelpers.js | 99 ++++++++++ test/data/restMock.js | 51 +++++ test/extensions/Pagination_Tree.html | 112 +++++++---- test/widgets/SelectServer.html | 11 ++ test/widgets/SelectServer.js | 114 +++++++++++ 8 files changed, 566 insertions(+), 174 deletions(-) create mode 100644 test/data/rest-node.js create mode 100644 test/data/restHelpers.js create mode 100644 test/data/restMock.js create mode 100644 test/widgets/SelectServer.html create mode 100644 test/widgets/SelectServer.js diff --git a/package.json b/package.json index 20608c23b..e3f00bbfd 100644 --- a/package.json +++ b/package.json @@ -22,5 +22,8 @@ }, "main": "./OnDemandGrid", "icon": "http://packages.dojofoundation.org/images/dgrid.png", + "scripts": { + "test-server": "node ./test/data/rest-node.js" + }, "dojoBuild": "package.js" } diff --git a/test/Rest.html b/test/Rest.html index defd96588..261c290ba 100644 --- a/test/Rest.html +++ b/test/Rest.html @@ -9,6 +9,10 @@ @import "../css/dgrid.css"; @import "../css/skins/claro.css"; + body { + padding-left: 2rem; + } + h2 { margin: 12px; } @@ -22,149 +26,165 @@ margin: 10px; } - + + + +
+ +

A basic grid with Rest store

+
+ +

A basic grid with Rest store using range headers

+
+ + + - - -

A basic grid with Rest store

-
- -

A basic grid with Rest store using range headers

-
diff --git a/test/data/rest-node.js b/test/data/rest-node.js new file mode 100644 index 000000000..b33cb955d --- /dev/null +++ b/test/data/rest-node.js @@ -0,0 +1,66 @@ +/* +A simple test server that returns dynamic data. +Sort: not supported +Paging: supports both "limit(count,start)" in the querystring and Range/X-Range header: items=start-end +Hierarchical data: supports the "parent" parameter in the querystring +See: https://github.com/SitePen/dstore/blob/master/docs/Stores.md#request + +Launch command: node rest-node.js +Stop server with Ctrl+C +*/ + +var http = require('http'); +var process = require('process'); +var url = require('url'); +var restHelpers = require('./restHelpers'); + +var PORT_NUMBER = 8040; +var RESPONSE_DELAY_MIN = 20; +var RESPONSE_DELAY_MAX = 100; + +var server = http.createServer(function (request, response) { + var requestUrl = new url.URL(request.url, 'http://' + request.headers.host); + var searchParams = {}; + requestUrl.searchParams.forEach(function (value, name) { + searchParams[name] = value; + }); + var responseData = restHelpers.getResponseData(searchParams, request.headers); + var responseDelay = restHelpers.getDelay(RESPONSE_DELAY_MIN, RESPONSE_DELAY_MAX); + + response.setHeader('Access-Control-Allow-Headers', '*, Range, X-Range, X-Requested-With'); + response.setHeader('Access-Control-Allow-Origin', '*'); + response.setHeader('Access-Control-Expose-Headers', 'Content-Range'); + response.setHeader('Content-Type', 'application/json'); + response.setHeader('Content-Range', responseData.contentRange); + response.write(JSON.stringify(responseData.items)); + + if (responseDelay) { + setTimeout(function () { + response.end(); + }, responseDelay); + } + else { + response.end(); + } +}); + +server.listen(PORT_NUMBER); +console.log('server listening on port ' + PORT_NUMBER + '...'); + +if (process.platform === 'win32') { + var readline = require('readline'); + var readlineInterface = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + readlineInterface.on('SIGINT', function () { + process.emit('SIGINT'); + }); +} + +process.on('SIGINT', function () { + server.close(function () { + console.log('server stopped'); + process.exit(); + }); +}); diff --git a/test/data/restHelpers.js b/test/data/restHelpers.js new file mode 100644 index 000000000..edb6042fd --- /dev/null +++ b/test/data/restHelpers.js @@ -0,0 +1,99 @@ +// partial UMD - only supports AMD and CommonJS, no global +// for use with both client-side mocking (restMock.js) and Node.js server (rest-node.js) +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(factory); + } + else if (typeof exports === 'object') { + module.exports = factory(); + } +}(function () { + var TOTAL_ITEM_COUNT = 500; + + var limitRegex = /^limit\((\d+),*(\d+)*\)/; + var rangeHeaderRegex = /(\d+)-(\d+)/; + + function getDelay (min, max) { + return max ? + Math.floor(Math.random() * ((max - min) + 1)) + min : + 0; + } + + function getRangeFromHeaders (headers) { + var rangeString = headers && (headers.Range || headers.range || headers['X-Range'] || headers['x-range']); + var match; + var range = {}; + + if (rangeString) { + match = rangeHeaderRegex.exec(rangeString); + if (match && match[2]) { + range.start = parseInt(match[1], 10); + range.end = parseInt(match[2], 10) + 1; + } + } + + return range; + } + + function getRangeFromSearchParams (searchParams) { + var searchParam; + var match; + var range; + + for (searchParam in searchParams) { + match = limitRegex.exec(searchParam); + + if (match) { + range = { start: 0 }; + + if (match[2]) { + range.start = parseInt(match[2], 10); + } + range.end = range.start + parseInt(match[1], 10); + } + } + + return range; + } + + function getResponseData (searchParams, headers) { + var range = getRangeFromSearchParams(searchParams); + var idPrefix = 'parent' in searchParams ? + searchParams.parent === 'undefined' ? '' : (searchParams.parent + '-') : ''; + var data = []; + var i; + + if (!range) { + range = getRangeFromHeaders(headers); + + if (!('start' in range)) { + range.start = 0; + } + if (!('end' in range)) { + range.end = 40; + } + } + + range.end = Math.min(range.end, TOTAL_ITEM_COUNT); + + for (i = range.start; i < range.end; i++) { + data.push({ + id: idPrefix + i, + name: (idPrefix ? ('Child ' + idPrefix) : 'Item ') + i, + comment: 'hello' + }); + } + + return { + items: data, + contentRange: 'items ' + range.start + '-' + range.end + '/' + TOTAL_ITEM_COUNT + }; + } + + return { + getDelay: getDelay, + getRangeFromHeaders: getRangeFromHeaders, + getRangeFromSearchParams: getRangeFromSearchParams, + getResponseData: getResponseData + }; +})); diff --git a/test/data/restMock.js b/test/data/restMock.js new file mode 100644 index 000000000..20d15d800 --- /dev/null +++ b/test/data/restMock.js @@ -0,0 +1,51 @@ +/* +Client-side request mock for testing grids with dstore/Rest +Sort: not supported +Paging: supports both "limit(count,start)" in the querystring and Range/X-Range header: items=start-end +Hierarchical data: supports the "parent" parameter in the querystring +See: https://github.com/SitePen/dstore/blob/master/docs/Stores.md#request +*/ + +define([ + 'dojo/_base/lang', + 'dojo/Deferred', + 'dojo/io-query', + 'dojo/json', + './restHelpers' +], function (lang, Deferred, ioQuery, JSON, restHelpers) { + var RESPONSE_DELAY_MIN = 20; + var RESPONSE_DELAY_MAX = 100; + + return function restMock (url, options) { + var searchParams = ioQuery.queryToObject(url.match(/[^?]*(?:\?([^#]*))?/)[1] || ''); + var responseData = restHelpers.getResponseData(searchParams, options.headers); + var responseHeaders = { + 'content-type': 'application/json', + 'content-range': responseData.contentRange + }; + var responseDelay = restHelpers.getDelay(RESPONSE_DELAY_MIN, RESPONSE_DELAY_MAX); + var responseText = JSON.stringify(responseData.items); + var dfd = new Deferred(); + var responseDfd = new Deferred(); + + responseDfd.resolve({ + getHeader: function (name) { + return responseHeaders[name.toLowerCase()]; + }, + data: responseText + }); + + if (responseDelay) { + setTimeout(function () { + dfd.resolve(responseText); + }, responseDelay); + } + else { + dfd.resolve(responseText); + } + + return lang.delegate(dfd.promise, { + response: responseDfd + }); + }; +}); diff --git a/test/extensions/Pagination_Tree.html b/test/extensions/Pagination_Tree.html index 0d096ecd4..f29656503 100644 --- a/test/extensions/Pagination_Tree.html +++ b/test/extensions/Pagination_Tree.html @@ -8,6 +8,11 @@ @import "../../../dojo/resources/dojo.css"; @import "../../css/dgrid.css"; @import "../../css/skins/claro.css"; + + body { + padding-left: 2rem; + } + .heading { font-weight: bold; padding-bottom: 0.25em; @@ -25,22 +30,70 @@ white-space: nowrap; } - + + + +
+ +

A basic grid with the Pagination extension with tree plugin

+

Configuration

+
+
+
+
+
+
+
+ +
+
+
+
+

An autoheight grid with the Pagination extension, + with a rows-per-page drop-down

+
+ + + - - -

A basic grid with the Pagination extension with tree plugin

-

Configuration

-
-
-
-
-
-
-
- -
-
-
-
-

An autoheight grid with the Pagination extension, - with a rows-per-page drop-down

-
diff --git a/test/widgets/SelectServer.html b/test/widgets/SelectServer.html new file mode 100644 index 000000000..aac0bc9a8 --- /dev/null +++ b/test/widgets/SelectServer.html @@ -0,0 +1,11 @@ +
+

Choose a server for the dstore/Rest store:

+
+
+
+
+
+ +
+
diff --git a/test/widgets/SelectServer.js b/test/widgets/SelectServer.js new file mode 100644 index 000000000..08f60f8ab --- /dev/null +++ b/test/widgets/SelectServer.js @@ -0,0 +1,114 @@ +define([ + 'dojo/Deferred', + 'dojo/io-query', + 'dojo/on', + 'dojo/request/registry', + 'dijit/_WidgetBase', + 'dijit/_TemplatedMixin', + '../data/restMock', + 'dojo/text!./SelectServer.html' +], function (Deferred, ioQuery, on, requestRegistry, _WidgetBase, _TemplatedMixin, restMock, template) { + var NODEJS_TARGET_URL = window.location.origin + ':8040/data/rest'; + var PHP_TARGET_URL = '../data/rest.php'; + var TIMEOUT = 1500; // milliseconds; timeout for server up status check + + return _WidgetBase.createSubclass([ _TemplatedMixin ], { + templateString: template, + + postCreate: function () { + this.inherited(arguments); + + this.own(on(this.form, 'change', function (event) { + if (event.target.value === 'mock') { + window.location.assign(window.location.origin + window.location.pathname); + } + else { + window.location.assign('?server=' + event.target.value); + } + })); + }, + + startup: function () { + var startupDfd = new Deferred(); + this.startupPromise = startupDfd.promise; + + this.inherited(arguments); + + var self = this; + var searchParams = ioQuery.queryToObject(window.location.search.slice(1)); + var value = searchParams.server; + + if (value) { + this.set('value', value); + } + else { + value = 'mock'; + } + + if (value === 'mock') { + requestRegistry.register(/data\/rest/, restMock); + startupDfd.resolve(); + } + else { + requestRegistry.get(this.get('targetUrl') + '?limit(1)', { + timeout: TIMEOUT + }).response.then(function (response) { + var contentType = response.getHeader('Content-Type'); + // If PHP is not running or configured correctly `rest.php` may be served by a plain HTTP server + // as text or html. The PHP script is configured to set Content-Type to 'application/json' + if (contentType !== 'application/json') { + throw { response: response }; + } + startupDfd.resolve(response); + }).otherwise(function (error) { + var message = 'Failed to load data from URL: ' + error.response.url + '
'; + if (value === 'php') { + message += 'PHP must be running and configured to execute the script dgrid/test/data/rest.php'; + } + else { + message += 'The Node.js server must be running: npm run test-server'; + } + self.messageNode.innerHTML = message; + self.messageNode.style.display = 'inline-block'; + }); + } + }, + + _getTargetUrlAttr: function () { + var targetUrl = NODEJS_TARGET_URL; + var value = this.get('value'); + + if (value === 'php') { + targetUrl = PHP_TARGET_URL; + } + + return targetUrl; + }, + + _getValueAttr: function () { + var value = ''; + var radioInput; + var i; + + for (i = 0; i < this.form.serverType.length; i++) { + radioInput = this.form.serverType[i]; + if (radioInput.checked) { + value = radioInput.value; + break; + } + } + + return value; + }, + + _setValueAttr: function (newValue) { + var radioInput; + var i; + + for (i = 0; i < this.form.serverType.length; i++) { + radioInput = this.form.serverType[i]; + radioInput.checked = (radioInput.value === newValue); + } + } + }); +});