├── .gitignore ├── .travis.yml ├── README.md ├── bower.json ├── package.json └── src ├── main ├── java │ └── TestController.java ├── js │ └── jsbandwidth.js └── webapp │ ├── post.aspx │ ├── post.jsp │ ├── post.php │ ├── post.pl │ └── test.bin └── test ├── karma.conf.js └── spec └── JsBandwidthSpec.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .project 4 | .settings 5 | build 6 | index.js 7 | 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | before_script: 5 | - export DISPLAY=:99.0 6 | - sh -e /etc/init.d/xvfb start 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Bower](https://img.shields.io/bower/v/jsbandwidth.svg) [![NPM](https://img.shields.io/npm/v/jsbandwidth.svg)](https://www.npmjs.com/package/jsbandwidth) ![License](https://img.shields.io/npm/l/jsbandwidth.svg) 2 | [![Build Status](https://travis-ci.org/beradrian/jsbandwidth.png)](https://travis-ci.org/beradrian/jsbandwidth) 3 | 4 | [![NPM](https://nodei.co/npm/jsbandwidth.png)](https://nodei.co/npm/jsbandwidth/) 5 | 6 | # JSBandwidth 7 | 8 | To test inside a browser the bandwidth and latency, there's no easy way. This is what JsBandwidth tries to achieve. 9 | 10 | This project was initially forked from https://code.google.com/p/jsbandwidth/. At this moment it became a total rewrite. 11 | 12 | ## License 13 | I decided to keep the same license as the initial project, [MIT](http://opensource.org/licenses/mit-license.php). 14 | 15 | ## Installation 16 | 17 | npm install jsbandwidth 18 | 19 | ## Browser support 20 | This library is using `XMLHttpRequest` and `Promise`. XMLHttpRequest is supported starting with IE7. For older browsers (are you serious?) you can use this [shim](https://gist.github.com/beradrian/5d30bcbf7e8e0d9b5090). For browsers that do not feature [Promise support](http://caniuse.com/#search=promise) you can use any of the following shims: 21 | - https://github.com/jakearchibald/es6-promise 22 | - https://github.com/taylorhakes/promise-polyfill 23 | - https://github.com/getify/native-promise-only 24 | 25 | **If your shim is not here, please drop me an email or enter a new issue to include it.** 26 | 27 | ## Server-side set-up 28 | 1. Set up a web server of your choice. 29 | 2. Depending on your web server, drop the corresponding project files in your web server's document root (or a sub-directory, if you wish). What `src/main/webapp/post.*` file you should choose depends on your web server. The upload test needs to be able to send a POST request to the server. The receiving page doesn't have to do anything with the data. However, some servers will not allow you to send a POST request to a .htm file. Therefore, the project includes several blank server side script files (post.aspx, post.php, post.pl). `src/main/webapp/test.bin` is mandatory, but it's nothing more than random bytes. 30 | 31 | ### Spring Controller 32 | 33 | If you want to use a Spring Controller to post test data you can define a controller method like this 34 | 35 | @RequestMapping("/test-post") 36 | public @ResponseBody String testPost() { 37 | return "true"; 38 | } 39 | 40 | and then specify `options.uploadUrl='/test-post'`. [TestController](src/main/java/TestController.java) is a fully working Spring controller. 41 | 42 | Please be aware that some servers, like Tomcat, by their default setup can impose a limit on the upload data size to avoid DoS attacks. You either modify that setup or specify `options.uploadDataMaxSize`. 43 | 44 | ## JavaScript API 45 | The JavaScript API works with both Angular and jQuery, depending on what library is included (if both, Angular is preferred). 46 | 47 | First you need to get hold of the `jsBandwidth` object. 48 | 49 | - In Angular 50 | 51 |

 52 | myApp.controller('JsBandwidthTestController', ["$scope", "jsBandwidth", function ($scope, jsBandwidth) {
 53 | 	$scope.test = function(options, callback, errorCallback) {
 54 | 		jsBandwidth.testSpeed(options)
 55 | 				.then(function(result) {
 56 | 						$scope.result = result;
 57 | 						callback();
 58 | 					}
 59 | 					, function(error) {
 60 | 						$scope.error = error;
 61 | 						errorCallback();
 62 | 					});
 63 | 	};
 64 | }]);
 65 | 
66 | 67 | - With require 68 | 69 |

 70 | 	var jsBandwidth = require("jsbandwidth");
 71 | 
72 | 73 | The `jsBandwidth` object has 3 methods with a similar signature: 74 | - `testLatency(options)` 75 | - `testDownloadSpeed(options)` 76 | - `testUploadSpeed(options)` 77 | - `testSpeed(options)` which combines all the above into one 78 | 79 | The `options` parameter is an object and it has the following fields 80 | - `latencyTestUrl` the URL used for latency testing. Usually a big binary content is expected to be downloaded. If not specified, it is considered to be the same as `downloadUrl`, but requested with `HEAD` method. 81 | - `downloadUrl` the URL used for download speed testing. Usually a big binary content is expected to be downloaded. 82 | - `uploadUrl` the URL used for upload speed testing. It should accept a POST method. 83 | - `uploadData` the data that is sent to the server to test the upload 84 | - `uploadDataMaxSize` if specified `uploadData` is going to be truncated to this maximum length. Some servers, like Tomcat, by their default setup can impose a limit on the upload data size to avoid DoS attacks. You either modify that setting or use `options.uploadDataMaxSize`. The usual limit is 2Mb. 85 | - `uploadDataSize` if `uploadData` is not specified, then a chunk of this size is randomly generated instead 86 | - 'ajax' the AJAX service, either from jQuery or $http from Angular. If not specified, it will be automatically detected depending whether jQuery or Angular is included. 87 | 88 | All three methods return a [promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) and you can use the `then` method. That promise is also augmented with a `cancel()` method. 89 | 90 | ### Example 91 | 92 | var jsBandwidth = require("jsbandwidth"); 93 | jsBandwidth.testSpeed(options) 94 | .then(function (result) { 95 | console.log("Latency is " + result.latency + " ms, download speed is " + result.downloadSpeed + "bps and upload speed is " result.uploadSpeed + "bps"); 96 | }, 97 | function(error) { 98 | console.log("An error occured during net speed test."); 99 | }); 100 | 101 | ### Angular controller 102 | An Angular controller, called `JsBandwidthController`, is provided for your convenience. The controller uses the service and it defines the following fields/methods in the scope 103 | - `test` this is the service running the speed test. If null or undefined, there's no test currently running, so it can be used for checking if a speed test is currently running. 104 | - `options` the options used to run the speed test 105 | - `result.latency` the estimated latency in ms. If `result` is null or undefined, the test is in progress or ended with an error. 106 | - `result.downloadSpeed` the estimated download speed in bps. 107 | - `result.uploadSpeed` the estimated upload speed in bps. 108 | - `error` if null or undefined, then a test is in progress or completed successfully. If not null, then an error occured during the last speed test. 109 | - `error.status` the error status 110 | 111 | 'complete` event is emitted when the test is completed or 'error' if an error occured. 112 | 113 | Below is an example on how to use it in your page: 114 | 115 |
117 | 118 | Error: 119 | 120 | 121 | Latency: 122 | ms 123 | Download speed: 124 | Mbps 125 | Upload speed 126 | Mbps 127 | 128 | 129 | 130 |
131 | 132 | 133 | ### Formatting 134 | The speed is calculated in bps (bits per second). In the Angular controller you have the method `convertToMbps` for your convenience. If you want to format it differently, you can use [js-quantities](https://github.com/gentooboontoo/js-quantities). 135 | 136 | ## How to get support 137 | * Ask a question on StackOverflow 138 | * [Fill in](https://github.com/beradrian/jsbandwidth/issues/new) an issue. 139 | 140 | 141 | ## Development 142 | 143 | ### How to make a new release 144 | 1. Change the version number in `bower.json` and `package.json` 145 | 2. Create a new [release](https://github.com/beradrian/jsbandwidth/releases) in github with the same name for tag and title as the version number (e.g. `1.0.0`). Do not forget to include the changelog in the release description. 146 | 3. Run `npm publish` to publish the new version to npm 147 | 148 | ### Testing 149 | To run the tests do `npm run test`. 150 | 151 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsbandwidth", 3 | "version": "1.1.2", 4 | "description": "JsBandwidth - the JavaScript net speed test", 5 | "author": "Adrian Ber", 6 | "license": "MIT", 7 | "homepage": "https://github.com/beradrian/jsbandwidth", 8 | "main": "jsbandwidth.js", 9 | "ignore": 10 | [ 11 | "**/.*", 12 | "node_modules", 13 | "docs", 14 | "Gruntfile.js", 15 | "index.html", 16 | "package.json" 17 | ] 18 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsbandwidth", 3 | "license": "MIT", 4 | "version": "1.1.2", 5 | "description": "JsBandwidth - the JavaScript net speed test", 6 | "author": 7 | { 8 | "name": "Adrian Ber", 9 | "email": "beradrian@yahoo.com" 10 | }, 11 | 12 | "repository": 13 | { 14 | "type": "git", 15 | "url": "https://github.com/beradrian/jsbandwidth.git" 16 | }, 17 | 18 | "main": "index.js", 19 | "dependencies": 20 | { 21 | "extend": "latest", 22 | "xhrpromise": "latest", 23 | "babel-cli": "latest", 24 | "babel-plugin-add-module-exports": "latest", 25 | "babel-preset-es2015": "latest" 26 | }, 27 | 28 | "devDependencies": 29 | { 30 | "angular": ">=1.4.7", 31 | "angular-mocks": "1.4.7", 32 | "jasmine": "latest", 33 | "jasmine-ajax": "latest", 34 | "jasmine-core": "latest", 35 | "js-quantities": "latest", 36 | "karma": "latest", 37 | "karma-babel-preprocessor": "latest", 38 | "karma-browserify": "latest", 39 | "karma-chrome-launcher": "latest", 40 | "karma-commonjs": "latest", 41 | "karma-firefox-launcher": "latest", 42 | "karma-jasmine": "latest", 43 | "karma-jasmine-ajax": "latest", 44 | "uglifyify": "latest" 45 | }, 46 | 47 | "scripts": 48 | { 49 | "browserify": "node_modules/.bin/browserify src/main/js/jsbandwidth.js -o jsbandwidth.js -t [ babelify --presets [ es2015 ] --plugins [ babel-plugin-add-module-exports ] uglifyify]", 50 | "babel": "node_modules/.bin/babel src/main/js/jsbandwidth.js -o index.js --presets es2015 --plugins babel-plugin-add-module-exports", 51 | "postinstall": "npm run babel", 52 | "test": "node_modules/.bin/karma start src/test/karma.conf.js" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/TestController.java: -------------------------------------------------------------------------------- 1 | import java.util.Locale; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.RequestMapping; 5 | import org.springframework.web.bind.annotation.ResponseBody; 6 | 7 | @Controller 8 | public class TestController { 9 | 10 | /** This is used for uploading a chunk of data and test the upload speed. It does nothing, it just returns "true". */ 11 | @RequestMapping("/test-post") 12 | public @ResponseBody String testPost() { 13 | return "true"; 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/js/jsbandwidth.js: -------------------------------------------------------------------------------- 1 | if (typeof angular != "undefined") { 2 | angular.module("jsBandwidth", ["ng"]) 3 | .factory("jsBandwidth", ["$http", function($http) { 4 | return new JsBandwidth(); 5 | }]) 6 | .controller("JsBandwidthController", [ '$scope', "jsBandwidth", function($scope, jsBandwidth) { 7 | var MEGABIT = 1000000; 8 | $scope.convertToMbps = function(x) { 9 | return x < 0 || isNaN(x) ? x : Math.floor((x / MEGABIT) * 100) / 100; 10 | }; 11 | 12 | $scope.start = function() { 13 | $scope.result = $scope.error = null; 14 | $scope.isRunning = true; 15 | $scope.test = jsBandwidth.testSpeed($scope.options); 16 | $scope.test.then(function(result) { 17 | $scope.result = result; 18 | $scope.isRunning = false; 19 | $scope.$emit("complete", result); 20 | } 21 | , function(error) { 22 | $scope.error = error; 23 | $scope.isRunning = false; 24 | $scope.$emit("error", error); 25 | }); 26 | }; 27 | 28 | $scope.cancel = function() { 29 | $scope.test.cancel(); 30 | }; 31 | }]); 32 | } 33 | 34 | import extend from "extend"; 35 | import XhrPromise from "xhrpromise"; 36 | 37 | export default class JsBandwidth { 38 | 39 | /** 40 | * The default options 41 | */ 42 | static get DEFAULT_OPTIONS() { 43 | return { 44 | latencyTestUrl: "/test" 45 | , downloadUrl: "/test.bin" 46 | , uploadUrl: "/post" 47 | , uploadDataSize: 5 * 1024 * 1024 48 | , uploadDataMaxSize: Number.MAX_VALUE 49 | }; 50 | } 51 | 52 | /** 53 | * Calculates the bandwidth in bps (bits per second) 54 | * @param size the size in bytes to be transfered 55 | * @param startTime the time when the transfer started. The end time is 56 | * considered to be now. 57 | */ 58 | static calculateBandwidth(size, start) { 59 | return (size * 8) / ((new Date().getTime() - start) / 1000); 60 | } 61 | 62 | static truncate(data, maxSize) { 63 | if (maxSize === undefined) { 64 | return; 65 | } 66 | if (data.length > maxSize) { 67 | if (data.substring) { 68 | data = data.substring(0, maxSize); 69 | } else { 70 | data.length = maxSize; 71 | } 72 | } 73 | return data; 74 | } 75 | 76 | /** 77 | * Creates a new js bandwidth tester. 78 | * @param options the options 79 | */ 80 | constructor(options) { 81 | var self = this; 82 | this.options = extend({}, JsBandwidth.DEFAULT_OPTIONS, options); 83 | } 84 | 85 | testDownloadSpeed(options) { 86 | var self = this; 87 | options = extend({}, this.options, options); 88 | var start = new Date().getTime(); 89 | var r = XhrPromise.create({ 90 | method: "GET", 91 | url: options.downloadUrl + "?id=" + start, 92 | dataType: 'application/octet-stream', 93 | headers: {'Content-type': 'application/octet-stream'}}); 94 | var r1 = r.then( 95 | function(response) { 96 | return {downloadSpeed: JsBandwidth.calculateBandwidth((response.data || response).length, start), data: response.data || response}; 97 | }); 98 | r1.cancel = r.cancel; 99 | return r1; 100 | } 101 | 102 | testUploadSpeed(options) { 103 | var self = this; 104 | options = extend({}, this.options, options); 105 | // generate randomly the upload data 106 | if (!options.uploadData) { 107 | options.uploadData = new Array(Math.min(options.uploadDataSize, options.uploadDataMaxSize)); 108 | for (var i = 0; i < options.uploadData.length; i++) { 109 | options.uploadData[i] = Math.floor(Math.random() * 256); 110 | } 111 | } else { 112 | options.uploadData = JsBandwidth.truncate(options.uploadData, options.uploadDataMaxSize); 113 | } 114 | var start = new Date().getTime(); 115 | var r = XhrPromise.create({ 116 | method: "POST", 117 | url: options.uploadUrl + "?id=" + start, 118 | data: options.uploadData, 119 | dataType: 'application/octet-stream', 120 | headers: {'Content-type': 'application/octet-stream'}}); 121 | var r1 = r.then( 122 | function(response) { 123 | return {uploadSpeed: JsBandwidth.calculateBandwidth(options.uploadData.length, start)}; 124 | }); 125 | r1.cancel = r.cancel; 126 | return r1; 127 | } 128 | 129 | testLatency(options) { 130 | var self = this; 131 | options = extend({}, this.options, options); 132 | options.latencyTestUrl = options.latencyTestUrl || options.downloadUrl; 133 | var start = new Date().getTime(); 134 | var r = XhrPromise.create({ 135 | method: "HEAD", 136 | url: options.latencyTestUrl + "?id=" + start, 137 | dataType: 'application/octet-stream', 138 | headers: {'Content-type': 'application/octet-stream'}}); 139 | var r1 = r.then( 140 | function(response) { 141 | // time divided by 2 because of 3-way TCP handshake 142 | return {latency: (new Date().getTime() - start) / 2}; 143 | }); 144 | r1.cancel = r.cancel; 145 | return r1; 146 | } 147 | 148 | testSpeed(options) { 149 | var self = this; 150 | var r; 151 | r = self.testLatency(options); 152 | var r1 = r.then( 153 | function(latencyResult) { 154 | r = self.testDownloadSpeed(options); 155 | var r1 = r.then( 156 | function(downloadResult) { 157 | options.uploadData = downloadResult.data; 158 | r = self.testUploadSpeed(options); 159 | var r1 = r.then( 160 | function(uploadResult) { 161 | return {latency: latencyResult.latency, 162 | downloadSpeed: downloadResult.downloadSpeed, 163 | uploadSpeed: uploadResult.uploadSpeed}; 164 | } 165 | ); 166 | r1.cancel = r.cancel; 167 | return r1; 168 | } 169 | ); 170 | r1.cancel = r.cancel; 171 | return r1; 172 | }); 173 | r1.cancel = r.cancel; 174 | return r1; 175 | } 176 | 177 | } 178 | 179 | -------------------------------------------------------------------------------- /src/main/webapp/post.aspx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beradrian/jsbandwidth/cefa177c4c8076dc75a27f64fcd072da622d0c19/src/main/webapp/post.aspx -------------------------------------------------------------------------------- /src/main/webapp/post.jsp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beradrian/jsbandwidth/cefa177c4c8076dc75a27f64fcd072da622d0c19/src/main/webapp/post.jsp -------------------------------------------------------------------------------- /src/main/webapp/post.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beradrian/jsbandwidth/cefa177c4c8076dc75a27f64fcd072da622d0c19/src/main/webapp/post.php -------------------------------------------------------------------------------- /src/main/webapp/post.pl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beradrian/jsbandwidth/cefa177c4c8076dc75a27f64fcd072da622d0c19/src/main/webapp/post.pl -------------------------------------------------------------------------------- /src/test/karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | var deps = "@(extend|xhrpromise)"; 3 | var cfg = { 4 | basePath: '../..' 5 | , logLevel: 'DEBUG' 6 | , frameworks: ["jasmine-ajax", "jasmine", "commonjs"] 7 | , files: ["src/test/spec/*Spec.js", {pattern: "src/main/js/*.js", included: true}, 8 | {pattern: "node_modules/" + deps + "/*.js", included: true, watched: false}, 9 | {pattern: "node_modules/" + deps + "/**/*", included: false, watched: false}] 10 | , preprocessors: { 11 | "**/*Spec.js": ["commonjs"] 12 | , "src/main/js/*.js": ["babel", "commonjs"] 13 | } 14 | , browsers: ["Firefox"] 15 | , singleRun: true 16 | , browserify: { 17 | debug: true, 18 | transform: [["babelify", { "presets": ["es2015"], "plugins": ["babel-plugin-add-module-exports"] }]] 19 | } 20 | , babelPreprocessor: { 21 | options: { 22 | presets: ['es2015'] 23 | , plugins: ["babel-plugin-add-module-exports"] 24 | } 25 | } 26 | }; 27 | cfg.preprocessors["node_modules/" + deps + "/**/*.js"] = ["commonjs"]; 28 | config.set(cfg); 29 | }; 30 | -------------------------------------------------------------------------------- /src/test/spec/JsBandwidthSpec.js: -------------------------------------------------------------------------------- 1 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 11000; 2 | 3 | var JsBandwidth = require("../../main/js/jsbandwidth.js"); 4 | 5 | var MEGABIT = 1000000; 6 | var getRandomData = function(size) { 7 | var dataSize = size ? size : MEGABIT; 8 | var data = new Array(dataSize); 9 | for (var i = 0; i < data.length; i++) { 10 | data[i] = Math.floor(Math.random() * 256); 11 | } 12 | return data; 13 | }; 14 | 15 | describe("JsBandwidthSpec", function() { 16 | var jsbandwidth; 17 | 18 | beforeEach(function() { 19 | jsbandwidth = new JsBandwidth(); 20 | jasmine.Ajax.install(); 21 | }); 22 | 23 | afterEach(function() { 24 | jasmine.Ajax.uninstall(); 25 | }); 26 | 27 | it('should get latency and net speed', function(done) { 28 | var data = getRandomData(); 29 | var n = data.length; 30 | jsbandwidth.testSpeed({latencyTestUrl: "/test.bin", downloadUrl: "/test.bin", uploadUrl: "/post"}).then( 31 | function(result) { 32 | console.log("Test result: " + JSON.stringify(result)); 33 | expect(result.latency).toBeGreaterThan(500); 34 | expect(result.latency).toBeLessThan(600); 35 | expect(result.downloadSpeed).toBeLessThan(2 * MEGABIT); 36 | expect(result.downloadSpeed).toBeGreaterThan(MEGABIT); 37 | done(); 38 | } 39 | ); 40 | 41 | console.log("Wait 500ms for latency ..."); 42 | setTimeout(function() { 43 | var request = jasmine.Ajax.requests.mostRecent(); 44 | expect(request.method).toBe('HEAD'); 45 | expect(request.url).toMatch(/\/test.bin\?.*/); 46 | request.respondWith({ 47 | "status": 200, 48 | "responseText": "", 49 | "responseHeaders": {"Access-Control-Allow-Origin": "*"} 50 | }); 51 | var timeout = (n / MEGABIT * 8) * 1000 / 2 + 100; 52 | console.log("Wait " + timeout + "ms for " + n + " bytes ..."); 53 | setTimeout(function() { 54 | var request = jasmine.Ajax.requests.mostRecent(); 55 | expect(request.method).toBe('GET'); 56 | expect(request.url).toMatch(/\/test.bin\?.*/); 57 | request.respondWith({ 58 | "status": 200, 59 | "responseText": data, 60 | "responseHeaders": {"Access-Control-Allow-Origin": "*"} 61 | }); 62 | setTimeout(function() { 63 | var request = jasmine.Ajax.requests.mostRecent(); 64 | expect(request.method).toBe('POST'); 65 | expect(request.url).toMatch(/\/post?.*/); 66 | request.respondWith({ 67 | "status": 200, 68 | "responseText": "", 69 | "responseHeaders": {"Access-Control-Allow-Origin": "*"} 70 | }); 71 | }, 100); 72 | }, timeout); 73 | }, 500 * 2); 74 | }); 75 | 76 | it('should get error', function(done) { 77 | jsbandwidth.testSpeed({latencyTestUrl: "/xtest.bin", downloadUrl: "/xtest.bin", uploadUrl: "/post"}) 78 | .then(function(result) {}, function(error) { 79 | expect(error.status).toEqual(404); 80 | done(); 81 | }); 82 | var request = jasmine.Ajax.requests.mostRecent(); 83 | expect(request.method).toBe('HEAD'); 84 | expect(request.url).toMatch(/\/xtest.bin\?.*/); 85 | request.respondWith({ 86 | "status": 404, 87 | "responseText": "", 88 | "responseHeaders": {"Access-Control-Allow-Origin": "*"} 89 | }); 90 | }); 91 | 92 | it('should cancel speed test', function(done) { 93 | var data = getRandomData(); 94 | var p = jsbandwidth.testSpeed({latencyTestUrl: "/test.bin", downloadUrl: "/test.bin", uploadUrl: "/post"}); 95 | p.then(function(result) {}, function(error) { 96 | expect(error.status).toEqual(-1); 97 | done(); 98 | }); 99 | var request = jasmine.Ajax.requests.mostRecent(); 100 | expect(request.method).toBe('HEAD'); 101 | expect(request.url).toMatch(/\/test.bin\?.*/); 102 | p.cancel(); 103 | }); 104 | 105 | }); --------------------------------------------------------------------------------