diff --git a/.travis.yml b/.travis.yml index e020ba7d15..409fcd75d4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,10 @@ +# we need trusty for the chrome addon +dist: trusty + +# we don't need sudo, so can run in a container, which makes startup much +# quicker. +sudo: false + language: node_js node_js: # make sure we work with a range of node versions. @@ -16,6 +23,8 @@ node_js: - 6.3 - 6 - 7 +addons: + chrome: stable install: # clone the deps with depth 1: we know we will only ever need that one # commit. diff --git a/README.md b/README.md index 89f2148f5e..d4b778b91e 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ to build. npm run build ``` However, we recommend setting up a proper development environment (see "Setting - up a development environment" below) if you want to run your own copy of the + up a dev environment" below) if you want to run your own copy of the `develop` branch, as it makes it much easier to keep these dependencies up-to-date. Or just use https://riot.im/develop - the continuous integration release of the develop branch. @@ -253,7 +253,6 @@ Finally, build and start Riot itself: 1. `rm -r node_modules/matrix-react-sdk; ln -s ../../matrix-react-sdk node_modules/` 1. `npm start` 1. Wait a few seconds for the initial build to finish; you should see something like: - ``` Hash: b0af76309dd56d7275c8 Version: webpack 1.12.14 @@ -282,19 +281,34 @@ If any of these steps error with, `file table overflow`, you are probably on a m which has a very low limit on max open files. Run `ulimit -Sn 1024` and try again. You'll need to do this in each new terminal you open before building Riot. -How to add a new translation? -============================= +Running the tests +----------------- + +There are a number of application-level tests in the `tests` directory; these +are designed to run in a browser instance under the control of +[karma](https://karma-runner.github.io). To run them: + +* Make sure you have Chrome installed (a recent version, like 59) +* Make sure you have `matrix-js-sdk` and `matrix-react-sdk` installed and + built, as above +* `npm run test` + +The above will run the tests under Chrome in a `headless` mode. + +You can also tell karma to run the tests in a loop (every time the source +changes), in an instance of Chrome on your desktop, with `npm run +test-multi`. This also gives you the option of running the tests in 'debug' +mode, which is useful for stepping through the tests in the developer tools. + +Translations +============ + +To add a new translation, head to the [translating doc](docs/translating.md). + +For a developer guide, see the [translating dev doc](docs/translating-dev.md). [translationsstatus](https://translate.riot.im/engage/riot-web/?utm_source=widget) - -Head to the [translating doc](docs/translating.md) - -Adding Strings to the translations (Developer Guide) -==================================================== - -Head to the [translating dev doc](docs/translating-dev.md) - Triaging issues =============== diff --git a/karma.conf.js b/karma.conf.js index 1e04366313..d834987e83 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -113,8 +113,23 @@ module.exports = function (config) { browsers: [ 'Chrome', //'PhantomJS', + //'ChromeHeadless' ], + customLaunchers: { + 'ChromeHeadless': { + base: 'Chrome', + flags: [ + // '--no-sandbox', + // See https://chromium.googlesource.com/chromium/src/+/lkgr/headless/README.md + '--headless', + '--disable-gpu', + // Without a remote debugging port, Google Chrome exits immediately. + '--remote-debugging-port=9222', + ], + } + }, + // Continuous Integration mode // if true, Karma captures browsers, runs the tests and exits // singleRun: false, diff --git a/package.json b/package.json index f51290061d..83d9bd16a8 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "lintall": "eslint src/ test/", "clean": "rimraf lib webapp electron_app/dist", "prepublish": "npm run build:compile", - "test": "karma start --single-run=true --autoWatch=false --browsers PhantomJS --colors=false", + "test": "karma start --single-run=true --autoWatch=false --browsers ChromeHeadless --colors=false", "test-multi": "karma start" }, "dependencies": { @@ -119,13 +119,12 @@ "karma-cli": "^0.1.2", "karma-junit-reporter": "^0.4.1", "karma-mocha": "^0.2.2", - "karma-phantomjs-launcher": "^1.0.0", "karma-webpack": "^1.7.0", + "matrix-mock-request": "^1.0.0", "minimist": "^1.2.0", "mkdirp": "^0.5.1", "mocha": "^2.4.5", "parallelshell": "^1.2.0", - "phantomjs-prebuilt": "^2.1.7", "postcss-extend": "^1.0.5", "postcss-import": "^9.0.0", "postcss-loader": "^1.2.2", diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js index 64a53d336d..e7d68c39f6 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.js @@ -73,6 +73,7 @@ module.exports = React.createClass({ this.protocols = response; this.setState({protocolsLoading: false}); }, (err) => { + console.warn(`error loading thirdparty protocols: ${err}`); this.setState({protocolsLoading: false}); if (MatrixClientPeg.get().isGuest()) { // Guests currently aren't allowed to use this API, so diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index bc82778aae..1c2877382c 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -67,7 +67,7 @@ module.exports = React.createClass({ onResendClick: function() { Resend.resend(this.props.mxEvent); - if (this.props.onFinished) this.props.onFinished(); + this.closeMenu(); }, onViewSourceClick: function() { @@ -75,7 +75,7 @@ module.exports = React.createClass({ Modal.createDialog(ViewSource, { content: this.props.mxEvent.event, }, 'mx_Dialog_viewsource'); - if (this.props.onFinished) this.props.onFinished(); + this.closeMenu(); }, onViewClearSourceClick: function() { @@ -84,7 +84,7 @@ module.exports = React.createClass({ // FIXME: _clearEvent is private content: this.props.mxEvent._clearEvent, }, 'mx_Dialog_viewsource'); - if (this.props.onFinished) this.props.onFinished(); + this.closeMenu(); }, onRedactClick: function() { @@ -106,12 +106,12 @@ module.exports = React.createClass({ }).done(); }, }, 'mx_Dialog_confirmredact'); - if (this.props.onFinished) this.props.onFinished(); + this.closeMenu(); }, onCancelSendClick: function() { Resend.removeFromQueue(this.props.mxEvent); - if (this.props.onFinished) this.props.onFinished(); + this.closeMenu(); }, onForwardClick: function() { @@ -130,7 +130,7 @@ module.exports = React.createClass({ if (this.props.eventTileOps) { this.props.eventTileOps.unhideWidget(); } - if (this.props.onFinished) this.props.onFinished(); + this.closeMenu(); }, onQuoteClick: function() { @@ -139,6 +139,7 @@ module.exports = React.createClass({ action: 'quote', event: this.props.mxEvent, }); + this.closeMenu(); }, render: function() { diff --git a/src/skins/vector/css/_components.scss b/src/skins/vector/css/_components.scss index 6680b282a1..a8ac18781b 100644 --- a/src/skins/vector/css/_components.scss +++ b/src/skins/vector/css/_components.scss @@ -58,6 +58,7 @@ @import "./matrix-react-sdk/views/rooms/_SearchableEntityList.scss"; @import "./matrix-react-sdk/views/rooms/_TabCompleteBar.scss"; @import "./matrix-react-sdk/views/rooms/_TopUnreadMessagesBar.scss"; +@import "./matrix-react-sdk/views/rooms/_AppsDrawer.scss"; @import "./matrix-react-sdk/views/settings/_DevicesPanel.scss"; @import "./matrix-react-sdk/views/settings/_IntegrationsManager.scss"; @import "./matrix-react-sdk/views/voip/_CallView.scss"; diff --git a/src/skins/vector/css/matrix-react-sdk/views/messages/_MEmoteBody.scss b/src/skins/vector/css/matrix-react-sdk/views/messages/_MEmoteBody.scss index e614aca7d8..cf722e5ae8 100644 --- a/src/skins/vector/css/matrix-react-sdk/views/messages/_MEmoteBody.scss +++ b/src/skins/vector/css/matrix-react-sdk/views/messages/_MEmoteBody.scss @@ -14,6 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ +.mx_MEmoteBody { + white-space: pre-wrap; +} + .mx_MEmoteBody_sender { cursor: pointer; } diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/_AppsDrawer.scss b/src/skins/vector/css/matrix-react-sdk/views/rooms/_AppsDrawer.scss new file mode 100644 index 0000000000..0fcabac1f4 --- /dev/null +++ b/src/skins/vector/css/matrix-react-sdk/views/rooms/_AppsDrawer.scss @@ -0,0 +1,159 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_AppsDrawer { +} + +.mx_AppsContainer { + display: flex; + flex-direction: row; + align-items: center; +} + +.mx_AddWidget_button { + order: 2; + cursor: pointer; + padding-right: 12px; + padding: 0; + margin: 0 0 5px 0; + color: $accent-color; + font-size: 12px; +} + +.mx_SetAppURLDialog_input { + border-radius: 3px; + border: 1px solid $input-border-color; + padding: 9px; + color: $primary-hairline-color; + background-color: $primary-bg-color; + font-size: 15px; +} + +.mx_AppTile { + width: 50%; + margin: 0 5px 2px 0; + border: 1px solid $primary-hairline-color; + border-radius: 2px; + // height: 350px; + // display: inline-block; +} + +.mx_AppTileFullWidth { + width: 100%; + margin: 0; + padding: 0; + border: 1px solid $primary-hairline-color; + border-radius: 2px; + // height: 350px; + // display: inline-block; +} + +.mx_AppTileMenuBar { + // height: 15px; + margin: 0; + padding: 2px 10px; + // background-color: $e2e-verified-color; + border-bottom: 1px solid $primary-hairline-color; + font-size: 10px; +} + +.mx_AppTileMenuBarWidgets { + float: right; +} +.mx_AppTileMenuBarWidget { + // pointer-events: none; + cursor: pointer; +} + +.mx_AppTileBody iframe { + width: 100%; + height: 350px; + overflow: hidden; + border: none; + padding: 0; + margin: 0; + display: block; +} + +.mx_CloseAppWidget { +} + +.mx_AppTileMenuBarWidgetPadding { + margin-right: 5px; +} + +.mx_AppIconTile { + background-color: $lightbox-bg-color; + border: 1px solid rgba(0, 0, 0, 0); + width: 200px; + box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2); + transition: 0.3s; + border-radius: 3px; + margin: 5px; + display: inline-block; +} + +.mx_AppIconTile.mx_AppIconTile_active { + color: $accent-color; + border-color: $accent-color; +} + +.mx_AppIconTile:hover { + border: 1px solid $accent-color; + box-shadow: 0 0 10px 5px rgba(200,200,200,0.5); +} + +.mx_AppIconTile_content { + padding: 2px 16px; + height: 60px; + overflow: hidden; +} + +.mx_AppIconTile_content h4 { + margin-top: 5px; + margin-bottom: 2px; +} + +.mx_AppIconTile_content p { + margin-top: 0; + margin-bottom: 5px; + font-size: smaller; +} + +.mx_AppIconTile_image { + padding: 10px; + width: 75%; + max-width:100px; + max-height:100px; + width: auto; + height: auto; +} + +.mx_AppIconTile_imageContainer { + text-align: center; + width: 100%; + background-color: white; + border-radius: 3px 3px 0 0; + height: 155px; + display: flex; + justify-content: center; + align-items: center; +} + +form.mx_Custom_Widget_Form div { + margin-top: 10px; + margin-bottom: 10px; +} diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/_Autocomplete.scss b/src/skins/vector/css/matrix-react-sdk/views/rooms/_Autocomplete.scss index 062dd0bade..6bf3f3b60a 100644 --- a/src/skins/vector/css/matrix-react-sdk/views/rooms/_Autocomplete.scss +++ b/src/skins/vector/css/matrix-react-sdk/views/rooms/_Autocomplete.scss @@ -38,6 +38,7 @@ .mx_Autocomplete_Completion_pill { border-radius: 17px; height: 34px; + padding: 0px 5px; display: flex; user-select: none; cursor: pointer; @@ -45,10 +46,22 @@ color: $primary-fg-color; } -.mx_Autocomplete_Completion_pill * { +.mx_Autocomplete_Completion_pill > * { margin: 0 3px; } +.mx_Autocomplete_Completion_container_truncate { + .mx_Autocomplete_Completion_title, + .mx_Autocomplete_Completion_subtitle, + .mx_Autocomplete_Completion_description { + /* Ellipsis for long names/subtitles/descriptions*/ + max-width: 150px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } +} + /* container for pill-style completions */ .mx_Autocomplete_Completion_container_pill { margin: 12px; diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/_MessageComposer.scss b/src/skins/vector/css/matrix-react-sdk/views/rooms/_MessageComposer.scss index 85c0e2c762..6c2216ddc6 100644 --- a/src/skins/vector/css/matrix-react-sdk/views/rooms/_MessageComposer.scss +++ b/src/skins/vector/css/matrix-react-sdk/views/rooms/_MessageComposer.scss @@ -78,6 +78,16 @@ limitations under the License. margin-right: 6px; } +@keyframes visualbell +{ + from { background-color: #faa } + to { background-color: $primary-bg-color } +} + +.mx_MessageComposer_input_error { + animation: 0.2s visualbell; +} + .mx_MessageComposer_input_empty .public-DraftEditorPlaceholder-root { display: none; } @@ -87,6 +97,7 @@ limitations under the License. flex: 1; max-height: 120px; overflow: auto; + word-break: break-word; } .mx_MessageComposer_input blockquote { @@ -128,7 +139,8 @@ limitations under the License. .mx_MessageComposer_upload, .mx_MessageComposer_hangup, .mx_MessageComposer_voicecall, -.mx_MessageComposer_videocall { +.mx_MessageComposer_videocall, +.mx_MessageComposer_apps { /*display: table-cell;*/ /*vertical-align: middle;*/ /*padding-left: 10px;*/ @@ -140,7 +152,8 @@ limitations under the License. .mx_MessageComposer_upload object, .mx_MessageComposer_hangup object, .mx_MessageComposer_voicecall object, -.mx_MessageComposer_videocall object { +.mx_MessageComposer_videocall object, +.mx_MessageComposer_apps object { pointer-events: none; } @@ -181,11 +194,6 @@ limitations under the License. cursor: pointer; } -.mx_MessageComposer_format_button_disabled { - cursor: not-allowed; - opacity: 0.5; -} - .mx_MessageComposer_formatbar_cancel { margin-right: 22px; } diff --git a/src/skins/vector/img/edit.svg b/src/skins/vector/img/edit.svg new file mode 100644 index 0000000000..a0be3454e1 --- /dev/null +++ b/src/skins/vector/img/edit.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/src/skins/vector/img/icons-apps-active.svg b/src/skins/vector/img/icons-apps-active.svg new file mode 100644 index 0000000000..ea222d0511 --- /dev/null +++ b/src/skins/vector/img/icons-apps-active.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/skins/vector/img/icons-apps.svg b/src/skins/vector/img/icons-apps.svg new file mode 100644 index 0000000000..affd8e6408 --- /dev/null +++ b/src/skins/vector/img/icons-apps.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/test/app-tests/joining.js b/test/app-tests/joining.js index d967012567..11fd3d4810 100644 --- a/test/app-tests/joining.js +++ b/test/app-tests/joining.js @@ -36,7 +36,7 @@ var expect = require('expect'); var q = require('q'); var test_utils = require('../test-utils'); -var MockHttpBackend = require('../mock-request'); +var MockHttpBackend = require('matrix-mock-request'); var HS_URL='http://localhost'; var IS_URL='http://localhost'; diff --git a/test/app-tests/loading.js b/test/app-tests/loading.js index 54e98760e9..d01836a36a 100644 --- a/test/app-tests/loading.js +++ b/test/app-tests/loading.js @@ -33,7 +33,7 @@ import {VIEWS} from 'matrix-react-sdk/lib/components/structures/MatrixChat'; import dis from 'matrix-react-sdk/lib/dispatcher'; import * as test_utils from '../test-utils'; -import MockHttpBackend from '../mock-request'; +import MockHttpBackend from 'matrix-mock-request'; import {parseQs, parseQsFromFragment} from '../../src/vector/url_utils'; var DEFAULT_HS_URL='http://my_server'; diff --git a/test/mock-request.js b/test/mock-request.js deleted file mode 100644 index 64ac6c06d5..0000000000 --- a/test/mock-request.js +++ /dev/null @@ -1,336 +0,0 @@ -"use strict"; -const q = require("q"); -import expect from 'expect'; - -/** - * Construct a mock HTTP backend, heavily inspired by Angular.js. - * @constructor - */ -function HttpBackend() { - this.requests = []; - this.expectedRequests = []; - const self = this; - // the request function dependency that the SDK needs. - this.requestFn = function(opts, callback) { - const req = new Request(opts, callback); - console.log(`${Date.now()} HTTP backend received request: ${req}`); - self.requests.push(req); - - const abort = function() { - const idx = self.requests.indexOf(req); - if (idx >= 0) { - console.log("Aborting HTTP request: %s %s", opts.method, - opts.uri); - self.requests.splice(idx, 1); - req.callback("aborted"); - } - }; - - return { - abort: abort, - }; - }; - - // very simplistic mapping from the whatwg fetch interface onto the request - // interface, so we can use the same mock backend for both. - this.fetchFn = function(input, init) { - init = init || {}; - const requestOpts = { - uri: input, - method: init.method || 'GET', - body: init.body, - }; - - return new Promise((resolve, reject) => { - function callback(err, response, body) { - if (err) { - reject(err); - } - resolve({ - ok: response.statusCode >= 200 && response.statusCode < 300, - json: () => body, - }); - }; - - const req = new Request(requestOpts, callback); - console.log(`HTTP backend received request: ${req}`); - self.requests.push(req); - }); - }; -} -HttpBackend.prototype = { - /** - * Respond to all of the requests (flush the queue). - * @param {string} path The path to flush (optional) default: all. - * @param {integer} numToFlush The number of things to flush (optional), default: all. - * @param {integer=} waitTime The time (in ms) to wait for a request to happen. - * default: 100 - * - * @return {Promise} resolves when there is nothing left to flush, with the - * number of requests flushed - */ - flush: function(path, numToFlush, waitTime) { - const defer = q.defer(); - const self = this; - let flushed = 0; - if (waitTime === undefined) { - waitTime = 100; - } - - function log(msg) { - console.log(`${Date.now()} flush[${path || ''}]: ${msg}`); - } - - log("HTTP backend flushing... (path=" + path - + " numToFlush=" + numToFlush - + " waitTime=" + waitTime - + ")", - ); - const endTime = waitTime + Date.now(); - - const tryFlush = function() { - // if there's more real requests and more expected requests, flush 'em. - log(` trying to flush => reqs=[${self.requests}] ` + - `expected=[${self.expectedRequests}]`, - ); - if (self._takeFromQueue(path)) { - // try again on the next tick. - flushed += 1; - if (numToFlush && flushed === numToFlush) { - log(`Flushed assigned amount: ${numToFlush}`); - defer.resolve(flushed); - } else { - log(` flushed. Trying for more.`); - setTimeout(tryFlush, 0); - } - } else if (flushed === 0 && Date.now() < endTime) { - // we may not have made the request yet, wait a generous amount of - // time before giving up. - log(` nothing to flush yet; waiting for requests.`); - setTimeout(tryFlush, 5); - } else { - if (flushed === 0) { - log("nothing to flush; giving up"); - } else { - log(`no more flushes after flushing ${flushed} requests`); - } - defer.resolve(flushed); - } - }; - - setTimeout(tryFlush, 0); - - return defer.promise; - }, - - /** - * Attempts to resolve requests/expected requests. - * @param {string} path The path to flush (optional) default: all. - * @return {boolean} true if something was resolved. - */ - _takeFromQueue: function(path) { - let req = null; - let i; - let j; - let matchingReq = null; - let expectedReq = null; - let testResponse = null; - for (i = 0; i < this.requests.length; i++) { - req = this.requests[i]; - for (j = 0; j < this.expectedRequests.length; j++) { - expectedReq = this.expectedRequests[j]; - if (path && path !== expectedReq.path) { - continue; - } - if (expectedReq.method === req.method && - req.path.indexOf(expectedReq.path) !== -1) { - if (!expectedReq.data || (JSON.stringify(expectedReq.data) === - JSON.stringify(req.data))) { - matchingReq = expectedReq; - this.expectedRequests.splice(j, 1); - break; - } - } - } - - if (matchingReq) { - // remove from request queue - this.requests.splice(i, 1); - i--; - - for (j = 0; j < matchingReq.checks.length; j++) { - matchingReq.checks[j](req); - } - testResponse = matchingReq.response; - console.log(`${Date.now()} responding to ${matchingReq.path}`); - let body = testResponse.body; - if (Object.prototype.toString.call(body) == "[object Function]") { - body = body(req.path, req.data); - } - req.callback( - testResponse.err, testResponse.response, body, - ); - matchingReq = null; - } - } - if (testResponse) { // flushed something - return true; - } - return false; - }, - - /** - * Makes sure that the SDK hasn't sent any more requests to the backend. - */ - verifyNoOutstandingRequests: function() { - const firstOutstandingReq = this.requests[0] || {}; - expect(this.requests.length).toEqual(0, - "Expected no more HTTP requests but received request to " + - firstOutstandingReq.path, - ); - }, - - /** - * Makes sure that the test doesn't have any unresolved requests. - */ - verifyNoOutstandingExpectation: function() { - const firstOutstandingExpectation = this.expectedRequests[0] || {}; - expect(this.expectedRequests.length).toEqual(0, - "Expected to see HTTP request for " + firstOutstandingExpectation.path, - ); - }, - - /** - * Create an expected request. - * @param {string} method The HTTP method - * @param {string} path The path (which can be partial) - * @param {Object} data The expected data. - * @return {Request} An expected request. - */ - when: function(method, path, data) { - const pendingReq = new ExpectedRequest(method, path, data); - this.expectedRequests.push(pendingReq); - return pendingReq; - }, -}; - -/** - * Represents the expectation of a request. - * - *

Includes the conditions to be matched against, the checks to be made, - * and the response to be returned. - * - * @constructor - * @param {string} method - * @param {string} path - * @param {object?} data - */ -function ExpectedRequest(method, path, data) { - this.method = method; - this.path = path; - this.data = data; - this.response = null; - this.checks = []; -} - -ExpectedRequest.prototype = { - toString: function() { - return this.method + " " + this.path - }, - - /** - * Execute a check when this request has been satisfied. - * @param {Function} fn The function to execute. - * @return {Request} for chaining calls. - */ - check: function(fn) { - this.checks.push(fn); - return this; - }, - - /** - * Respond with the given data when this request is satisfied. - * @param {Number} code The HTTP status code. - * @param {Object|Function} data The HTTP JSON body. If this is a function, - * it will be invoked when the JSON body is required (which should be returned). - */ - respond: function(code, data) { - this.response = { - response: { - statusCode: code, - headers: {}, - }, - body: data, - err: null, - }; - }, - - /** - * Fail with an Error when this request is satisfied. - * @param {Number} code The HTTP status code. - * @param {Error} err The error to throw (e.g. Network Error) - */ - fail: function(code, err) { - this.response = { - response: { - statusCode: code, - headers: {}, - }, - body: null, - err: err, - }; - }, -}; - -/** - * Represents a request made by the app. - * - * @constructor - * @param {object} opts opts passed to request() - * @param {function} callback - */ -function Request(opts, callback) { - this.opts = opts; - this.callback = callback; - - Object.defineProperty(this, 'method', { - get: function() { - return opts.method; - }, - }); - - Object.defineProperty(this, 'path', { - get: function() { - return opts.uri; - }, - }); - - Object.defineProperty(this, 'data', { - get: function() { - return opts.body; - }, - }); - - Object.defineProperty(this, 'queryParams', { - get: function() { - return opts.qs; - }, - }); - - Object.defineProperty(this, 'headers', { - get: function() { - return opts.headers || {}; - }, - }); -} - -Request.prototype = { - toString: function() { - return this.method + " " + this.path; - }, -}; - -/** - * The HttpBackend class. - */ -module.exports = HttpBackend;