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).
[](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;