Merge remote-tracking branch 'origin/develop' into dbkr/groups_better_groupview

pull/4530/head
David Baker 2017-07-07 12:02:38 +01:00
commit 052f9effc1
17 changed files with 306 additions and 367 deletions

View File

@ -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 language: node_js
node_js: node_js:
# make sure we work with a range of node versions. # make sure we work with a range of node versions.
@ -16,6 +23,8 @@ node_js:
- 6.3 - 6.3
- 6 - 6
- 7 - 7
addons:
chrome: stable
install: install:
# clone the deps with depth 1: we know we will only ever need that one # clone the deps with depth 1: we know we will only ever need that one
# commit. # commit.

View File

@ -81,7 +81,7 @@ to build.
npm run build npm run build
``` ```
However, we recommend setting up a proper development environment (see "Setting 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 `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 up-to-date. Or just use https://riot.im/develop - the continuous integration
release of the develop branch. 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. `rm -r node_modules/matrix-react-sdk; ln -s ../../matrix-react-sdk node_modules/`
1. `npm start` 1. `npm start`
1. Wait a few seconds for the initial build to finish; you should see something like: 1. Wait a few seconds for the initial build to finish; you should see something like:
``` ```
Hash: b0af76309dd56d7275c8 Hash: b0af76309dd56d7275c8
Version: webpack 1.12.14 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. 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. 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).
[<img src="https://translate.riot.im/widgets/riot-web/-/multi-auto.svg" alt="translationsstatus" width="340">](https://translate.riot.im/engage/riot-web/?utm_source=widget) [<img src="https://translate.riot.im/widgets/riot-web/-/multi-auto.svg" alt="translationsstatus" width="340">](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 Triaging issues
=============== ===============

View File

@ -113,8 +113,23 @@ module.exports = function (config) {
browsers: [ browsers: [
'Chrome', 'Chrome',
//'PhantomJS', //'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 // Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits // if true, Karma captures browsers, runs the tests and exits
// singleRun: false, // singleRun: false,

View File

@ -48,7 +48,7 @@
"lintall": "eslint src/ test/", "lintall": "eslint src/ test/",
"clean": "rimraf lib webapp electron_app/dist", "clean": "rimraf lib webapp electron_app/dist",
"prepublish": "npm run build:compile", "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" "test-multi": "karma start"
}, },
"dependencies": { "dependencies": {
@ -119,13 +119,12 @@
"karma-cli": "^0.1.2", "karma-cli": "^0.1.2",
"karma-junit-reporter": "^0.4.1", "karma-junit-reporter": "^0.4.1",
"karma-mocha": "^0.2.2", "karma-mocha": "^0.2.2",
"karma-phantomjs-launcher": "^1.0.0",
"karma-webpack": "^1.7.0", "karma-webpack": "^1.7.0",
"matrix-mock-request": "^1.0.0",
"minimist": "^1.2.0", "minimist": "^1.2.0",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"mocha": "^2.4.5", "mocha": "^2.4.5",
"parallelshell": "^1.2.0", "parallelshell": "^1.2.0",
"phantomjs-prebuilt": "^2.1.7",
"postcss-extend": "^1.0.5", "postcss-extend": "^1.0.5",
"postcss-import": "^9.0.0", "postcss-import": "^9.0.0",
"postcss-loader": "^1.2.2", "postcss-loader": "^1.2.2",

View File

@ -73,6 +73,7 @@ module.exports = React.createClass({
this.protocols = response; this.protocols = response;
this.setState({protocolsLoading: false}); this.setState({protocolsLoading: false});
}, (err) => { }, (err) => {
console.warn(`error loading thirdparty protocols: ${err}`);
this.setState({protocolsLoading: false}); this.setState({protocolsLoading: false});
if (MatrixClientPeg.get().isGuest()) { if (MatrixClientPeg.get().isGuest()) {
// Guests currently aren't allowed to use this API, so // Guests currently aren't allowed to use this API, so

View File

@ -67,7 +67,7 @@ module.exports = React.createClass({
onResendClick: function() { onResendClick: function() {
Resend.resend(this.props.mxEvent); Resend.resend(this.props.mxEvent);
if (this.props.onFinished) this.props.onFinished(); this.closeMenu();
}, },
onViewSourceClick: function() { onViewSourceClick: function() {
@ -75,7 +75,7 @@ module.exports = React.createClass({
Modal.createDialog(ViewSource, { Modal.createDialog(ViewSource, {
content: this.props.mxEvent.event, content: this.props.mxEvent.event,
}, 'mx_Dialog_viewsource'); }, 'mx_Dialog_viewsource');
if (this.props.onFinished) this.props.onFinished(); this.closeMenu();
}, },
onViewClearSourceClick: function() { onViewClearSourceClick: function() {
@ -84,7 +84,7 @@ module.exports = React.createClass({
// FIXME: _clearEvent is private // FIXME: _clearEvent is private
content: this.props.mxEvent._clearEvent, content: this.props.mxEvent._clearEvent,
}, 'mx_Dialog_viewsource'); }, 'mx_Dialog_viewsource');
if (this.props.onFinished) this.props.onFinished(); this.closeMenu();
}, },
onRedactClick: function() { onRedactClick: function() {
@ -106,12 +106,12 @@ module.exports = React.createClass({
}).done(); }).done();
}, },
}, 'mx_Dialog_confirmredact'); }, 'mx_Dialog_confirmredact');
if (this.props.onFinished) this.props.onFinished(); this.closeMenu();
}, },
onCancelSendClick: function() { onCancelSendClick: function() {
Resend.removeFromQueue(this.props.mxEvent); Resend.removeFromQueue(this.props.mxEvent);
if (this.props.onFinished) this.props.onFinished(); this.closeMenu();
}, },
onForwardClick: function() { onForwardClick: function() {
@ -130,7 +130,7 @@ module.exports = React.createClass({
if (this.props.eventTileOps) { if (this.props.eventTileOps) {
this.props.eventTileOps.unhideWidget(); this.props.eventTileOps.unhideWidget();
} }
if (this.props.onFinished) this.props.onFinished(); this.closeMenu();
}, },
onQuoteClick: function() { onQuoteClick: function() {
@ -139,6 +139,7 @@ module.exports = React.createClass({
action: 'quote', action: 'quote',
event: this.props.mxEvent, event: this.props.mxEvent,
}); });
this.closeMenu();
}, },
render: function() { render: function() {

View File

@ -58,6 +58,7 @@
@import "./matrix-react-sdk/views/rooms/_SearchableEntityList.scss"; @import "./matrix-react-sdk/views/rooms/_SearchableEntityList.scss";
@import "./matrix-react-sdk/views/rooms/_TabCompleteBar.scss"; @import "./matrix-react-sdk/views/rooms/_TabCompleteBar.scss";
@import "./matrix-react-sdk/views/rooms/_TopUnreadMessagesBar.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/_DevicesPanel.scss";
@import "./matrix-react-sdk/views/settings/_IntegrationsManager.scss"; @import "./matrix-react-sdk/views/settings/_IntegrationsManager.scss";
@import "./matrix-react-sdk/views/voip/_CallView.scss"; @import "./matrix-react-sdk/views/voip/_CallView.scss";

View File

@ -14,6 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_MEmoteBody {
white-space: pre-wrap;
}
.mx_MEmoteBody_sender { .mx_MEmoteBody_sender {
cursor: pointer; cursor: pointer;
} }

View File

@ -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;
}

View File

@ -38,6 +38,7 @@
.mx_Autocomplete_Completion_pill { .mx_Autocomplete_Completion_pill {
border-radius: 17px; border-radius: 17px;
height: 34px; height: 34px;
padding: 0px 5px;
display: flex; display: flex;
user-select: none; user-select: none;
cursor: pointer; cursor: pointer;
@ -45,10 +46,22 @@
color: $primary-fg-color; color: $primary-fg-color;
} }
.mx_Autocomplete_Completion_pill * { .mx_Autocomplete_Completion_pill > * {
margin: 0 3px; 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 */ /* container for pill-style completions */
.mx_Autocomplete_Completion_container_pill { .mx_Autocomplete_Completion_container_pill {
margin: 12px; margin: 12px;

View File

@ -78,6 +78,16 @@ limitations under the License.
margin-right: 6px; 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 { .mx_MessageComposer_input_empty .public-DraftEditorPlaceholder-root {
display: none; display: none;
} }
@ -87,6 +97,7 @@ limitations under the License.
flex: 1; flex: 1;
max-height: 120px; max-height: 120px;
overflow: auto; overflow: auto;
word-break: break-word;
} }
.mx_MessageComposer_input blockquote { .mx_MessageComposer_input blockquote {
@ -128,7 +139,8 @@ limitations under the License.
.mx_MessageComposer_upload, .mx_MessageComposer_upload,
.mx_MessageComposer_hangup, .mx_MessageComposer_hangup,
.mx_MessageComposer_voicecall, .mx_MessageComposer_voicecall,
.mx_MessageComposer_videocall { .mx_MessageComposer_videocall,
.mx_MessageComposer_apps {
/*display: table-cell;*/ /*display: table-cell;*/
/*vertical-align: middle;*/ /*vertical-align: middle;*/
/*padding-left: 10px;*/ /*padding-left: 10px;*/
@ -140,7 +152,8 @@ limitations under the License.
.mx_MessageComposer_upload object, .mx_MessageComposer_upload object,
.mx_MessageComposer_hangup object, .mx_MessageComposer_hangup object,
.mx_MessageComposer_voicecall object, .mx_MessageComposer_voicecall object,
.mx_MessageComposer_videocall object { .mx_MessageComposer_videocall object,
.mx_MessageComposer_apps object {
pointer-events: none; pointer-events: none;
} }
@ -181,11 +194,6 @@ limitations under the License.
cursor: pointer; cursor: pointer;
} }
.mx_MessageComposer_format_button_disabled {
cursor: not-allowed;
opacity: 0.5;
}
.mx_MessageComposer_formatbar_cancel { .mx_MessageComposer_formatbar_cancel {
margin-right: 22px; margin-right: 22px;
} }

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="512px" height="512px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
<g>
<rect x="178.846" y="92.087" transform="matrix(-0.7071 -0.7071 0.7071 -0.7071 224.3476 631.1498)" width="128.085" height="354.049"/>
<path d="M471.723,88.393l-48.115-48.114c-11.723-11.724-31.558-10.896-44.304,1.85l-45.202,45.203l90.569,90.568l45.202-45.202
C482.616,119.952,483.445,100.116,471.723,88.393z"/>
<polygon points="64.021,363.252 32,480 148.737,447.979 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 876 B

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 15.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="35px"
height="35px" viewBox="0 0 35 35" enable-background="new 0 0 35 35" xml:space="preserve">
<g id="Layer_1">
<path id="Oval-109-Copy" fill="#76CFA6" enable-background="new " d="M17.5,35C27.165,35,35,27.165,35,17.5S27.165,0,17.5,0
S0,7.835,0,17.5S7.835,35,17.5,35z"/>
<g id="Icon">
<g>
<path fill="none" stroke="#FFFFFF" d="M7.5,12.5h5v-5h-5V12.5z M15,27.5h5v-5h-5V27.5z M7.5,27.5h5v-5h-5V27.5z M7.5,20h5v-5h-5
V20z M15,20h5v-5h-5V20z M22.5,7.5v5h5v-5H22.5z M15,12.5h5v-5h-5V12.5z M22.5,20h5v-5h-5V20z M22.5,27.5h5v-5h-5V27.5z"/>
</g>
</g>
</g>
<g id="Layer_2">
<g id="Icon_1_" opacity="0.15">
<g>
<path fill="none" stroke="#76CFA6" d="M7.5,12.5h5v-5h-5V12.5z M15,27.5h5v-5h-5V27.5z M7.5,27.5h5v-5h-5V27.5z M7.5,20h5v-5h-5
V20z M15,20h5v-5h-5V20z M22.5,7.5v5h5v-5H22.5z M15,12.5h5v-5h-5V12.5z M22.5,20h5v-5h-5V20z M22.5,27.5h5v-5h-5V27.5z"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 15.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="35px" height="35px" viewBox="0 0 35 35" enable-background="new 0 0 35 35" xml:space="preserve">
<path id="Oval-109-Copy" opacity="0.15" fill="#76CFA6" enable-background="new " d="M17.5,35C27.165,35,35,27.165,35,17.5
S27.165,0,17.5,0S0,7.835,0,17.5S7.835,35,17.5,35z"/>
<g id="Icon">
<g>
<path fill="none" stroke="#76CFA6" d="M7.5,12.5h5v-5h-5V12.5z M15,27.5h5v-5h-5V27.5z M7.5,27.5h5v-5h-5V27.5z M7.5,20h5v-5h-5
V20z M15,20h5v-5h-5V20z M22.5,7.5v5h5v-5H22.5z M15,12.5h5v-5h-5V12.5z M22.5,20h5v-5h-5V20z M22.5,27.5h5v-5h-5V27.5z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 941 B

View File

@ -36,7 +36,7 @@ var expect = require('expect');
var q = require('q'); var q = require('q');
var test_utils = require('../test-utils'); var test_utils = require('../test-utils');
var MockHttpBackend = require('../mock-request'); var MockHttpBackend = require('matrix-mock-request');
var HS_URL='http://localhost'; var HS_URL='http://localhost';
var IS_URL='http://localhost'; var IS_URL='http://localhost';

View File

@ -33,7 +33,7 @@ import {VIEWS} from 'matrix-react-sdk/lib/components/structures/MatrixChat';
import dis from 'matrix-react-sdk/lib/dispatcher'; import dis from 'matrix-react-sdk/lib/dispatcher';
import * as test_utils from '../test-utils'; 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'; import {parseQs, parseQsFromFragment} from '../../src/vector/url_utils';
var DEFAULT_HS_URL='http://my_server'; var DEFAULT_HS_URL='http://my_server';

View File

@ -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.
*
* <p>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;