diff --git a/lib/mock/http_backend.dart b/lib/mock/http_backend.dart index 2d472ccd8..47662f949 100644 --- a/lib/mock/http_backend.dart +++ b/lib/mock/http_backend.dart @@ -75,37 +75,37 @@ class MockHttpExpectation { MockHttpExpectation(this.method, this.url, [this.data, this.headers, withCredentials]) : this.withCredentials = withCredentials == true; - bool match(method, url, [data, headers, withCredentials]) { - if (method != method) return false; - if (!matchUrl(url)) return false; - if (data != null && !matchData(data)) return false; - if (headers != null && !matchHeaders(headers)) return false; - if (withCredentials != null && !matchWithCredentials(withCredentials)) return false; - return true; - } + bool match(req) => + matchMethodAndUrl(req) && matchData(req) && matchHeaders(req) && matchWithCredentials(req); + + bool matchMethodAndUrl(req) => + method == req.method && matchUrl(req); - bool matchUrl(u) { + bool matchUrl(req) { if (url == null) return true; - if (url is RegExp) return url.hasMatch(u); - return url == u; + return _matchUrl(url, req.url); } - bool matchHeaders(h) { + bool matchHeaders(req) { if (headers == null) return true; - if (headers is Function) return headers(h); - return "$headers" == "$h"; + if (headers is Function) return headers(req.headers); + return "$headers" == "${req.headers}"; } - bool matchData(d) { + bool matchData(req) { if (data == null) return true; - if (d == null) return false; - if (data is File) return data == d; - assert(d is String); - if (data is RegExp) return data.hasMatch(d); - return JSON.encode(data) == JSON.encode(d); + if (req.data == null) return false; + if (data is File) return data == req.data; + assert(req.data is String); + if (data is RegExp) return data.hasMatch(req.data); + return JSON.encode(data) == JSON.encode(req.data); } - bool matchWithCredentials(withCredentials) => this.withCredentials == withCredentials; + bool matchWithCredentials(req) { + if (withCredentials == null) return true; + if (req.withCredentials == null) return true; + return withCredentials == req.withCredentials; + } String toString() => "$method $url"; } @@ -117,13 +117,32 @@ class _Chain { respond([x,y,z]) => _respondFn(x,y,z); } + +/** + * An internal class used by [MockHttpBackend]. + */ +class RecordedRequest { + final String method; + final url, callback, data, headers, timeout; + final bool withCredentials; + + RecordedRequest({this.method, this.url, this.callback, this.data, + this.headers, this.timeout, this.withCredentials}); + + bool matchMethodAndUrl(method, url) => + this.method == method && _matchUrl(url, this.url); +} + +bool _matchUrl(expected, actual) => + (expected is RegExp) ? expected.hasMatch(actual) : expected == actual; + /** * A mock implementation of [HttpBackend], used in tests. */ class MockHttpBackend implements HttpBackend { var definitions = [], expectations = [], - responses = []; + requests = []; /** * This function is called from [Http] and designed to mimic the Dart APIs. @@ -164,81 +183,16 @@ class MockHttpBackend implements HttpBackend { } - /** - * A callback oriented API. This function takes a callback with - * will be called with (status, data, headers) - */ void call(method, url, callback, {data, headers, timeout, withCredentials: false}) { - var xhr = new _MockXhr(), - expectation = expectations.isEmpty ? null : expectations[0], - wasExpected = false; - - var prettyPrint = (data) { - return (data is String || data is Function || data is RegExp) - ? data - : JSON.encode(data); - }; - - var wrapResponse = (wrapped) { - var handleResponse = () { - var response = wrapped.response(method, url, data, headers); - xhr.respHeaders = response[2]; - utils.relaxFnApply(callback, [response[0], response[1], - xhr.getAllResponseHeaders()]); - }; - - var handleTimeout = () { - for (var i = 0; i < responses.length; i++) { - if (identical(responses[i], handleResponse)) { - responses.removeAt(i); - callback(-1, null, ''); - break; - } - } - }; - - if (timeout != null) timeout.then(handleTimeout); - return handleResponse; - }; - - if (expectation != null && expectation.match(method, url)) { - if (!expectation.matchData(data)) - throw ['Expected $expectation with different data\n' + - 'EXPECTED: ${prettyPrint(expectation.data)}\nGOT: $data']; - - if (!expectation.matchHeaders(headers)) - throw ['Expected $expectation with different headers\n' - 'EXPECTED: ${prettyPrint(expectation.headers)}\n' - 'GOT: ${prettyPrint(headers)}']; - - if (!expectation.matchWithCredentials(withCredentials)) - throw ['Expected $expectation with different withCredentials\n' - 'EXPECTED: ${prettyPrint(expectation.withCredentials)}\n' - 'GOT: ${prettyPrint(withCredentials)}']; - - expectations.removeAt(0); - - if (expectation.response != null) { - responses.add(wrapResponse(expectation)); - return; - } - wasExpected = true; - } - - for (var definition in definitions) { - if (definition.match(method, url, data, headers != null ? headers : {}, withCredentials)) { - if (definition.response != null) { - // if $browser specified, we do auto flush all requests - responses.add(wrapResponse(definition)); - } else throw ['No response defined !']; - return; - } - } - throw wasExpected ? - ['No response defined !'] : - ['Unexpected request: $method $url\n' + (expectation != null ? - 'Expected $expectation' : - 'No more requests expected')]; + requests.add(new RecordedRequest( + method: method, + url: url, + callback: callback, + data: data, + headers: headers, + timeout: timeout, + withCredentials: withCredentials + )); } /** @@ -475,19 +429,6 @@ class MockHttpBackend implements HttpBackend { _Chain expectPATCH(url, [data, headers, withCredentials = false]) => expect('PATCH', url, data, headers, withCredentials); - /** - * @ngdoc method - * @name ngMock.httpBackend#expectHEAD - * @methodOf ngMock.httpBackend - * @description - * Creates a new request expectation for HEAD requests. For more info see `expect()`. - * - * @param {string|RegExp} url HTTP url. - * @param {Object=} headers HTTP headers. - * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. - */ - /** * @ngdoc method * @name ngMock.httpBackend#flush @@ -500,21 +441,147 @@ class MockHttpBackend implements HttpBackend { * is called an exception is thrown (as this typically a sign of programming error). */ void flush([count]) { - if (responses.isEmpty) throw ['No pending request to flush !']; + if (requests.isEmpty) throw ['No pending request to flush !']; if (count != null) { while (count-- > 0) { - if (responses.isEmpty) throw ['No more pending request to flush !']; - responses.removeAt(0)(); + if (requests.isEmpty) throw ['No more pending request to flush !']; + _processRequest(requests.removeAt(0)); } } else { - while (!responses.isEmpty) { - responses.removeAt(0)(); + while (!requests.isEmpty) { + _processRequest(requests.removeAt(0)); } } verifyNoOutstandingExpectation(); } + /** + * Creates a new expectation and flushes all pending requests until the one matching the expectation. + * + * @param {string} method HTTP method. + * @param {string|RegExp} url HTTP url. + * @param {(string|RegExp)=} data HTTP request body. + * @param {(Object|function(Object))=} headers HTTP headers or function that + * receives http header object and returns true if the headers match the + * current definition. + * @returns {requestHandler} Returns an object with `respond` method that + * control how a matched request is handled. + * + * - respond – `{function([status,] data[, headers])|function(function(method, url, data, headers)}` + * – The respond method takes a set of static data to be returned or a function that can return + * an array containing response status (number), response data (string) and response headers + * (Object). + */ + _Chain flushExpected(String method, url, [data, headers, withCredentials = false]) { + var expectation = new MockHttpExpectation(method, url, data, headers, withCredentials); + expectations.add(expectation); + + flushUntilMethodAndUrlMatch () { + while (requests.isNotEmpty) { + final r = requests.removeAt(0); + _processRequest(r); + if (r.matchMethodAndUrl(method, url)) return; + } + throw ['No more pending requests matching $method $url']; + } + + return new _Chain(respond: (status, data, headers) { + expectation.response = _createResponse(status, data, headers); + flushUntilMethodAndUrlMatch(); + }); + } + + /** + * @ngdoc method + * @name ngMock.httpBackend#flushGET + * @methodOf ngMock.httpBackend + * @description + * Creates a new request expectation for GET requests. For more info see `flushExpected()`. + * + * @param {string|RegExp} url HTTP url. + * @param {Object=} headers HTTP headers. + * @returns {requestHandler} Returns an object with `respond` method that control how a matched + * request is handled. + */ + _Chain flushGET(url, [headers, withCredentials = false]) => + flushExpected("GET", url, null, headers, withCredentials); + + /** + * @ngdoc method + * @name ngMock.httpBackend#flushPOST + * @methodOf ngMock.httpBackend + * @description + * Creates a new request expectation for POST requests. For more info see `flushExpected()`. + * + * @param {string|RegExp} url HTTP url. + * @param {Object=} headers HTTP headers. + * @returns {requestHandler} Returns an object with `respond` method that control how a matched + * request is handled. + */ + _Chain flushPOST(url, [data, headers, withCredentials = false]) => + flushExpected("POST", url, data, headers, withCredentials); + + /** + * @ngdoc method + * @name ngMock.httpBackend#flushPUT + * @methodOf ngMock.httpBackend + * @description + * Creates a new request expectation for PUT requests. For more info see `flushExpected()`. + * + * @param {string|RegExp} url HTTP url. + * @param {Object=} headers HTTP headers. + * @returns {requestHandler} Returns an object with `respond` method that control how a matched + * request is handled. + */ + _Chain flushPUT(url, [data, headers, withCredentials = false]) => + flushExpected("PUT", url, data, headers, withCredentials); + + /** + * @ngdoc method + * @name ngMock.httpBackend#flushPATCH + * @methodOf ngMock.httpBackend + * @description + * Creates a new request expectation for PATCH requests. For more info see `flushExpected()`. + * + * @param {string|RegExp} url HTTP url. + * @param {Object=} headers HTTP headers. + * @returns {requestHandler} Returns an object with `respond` method that control how a matched + * request is handled. + */ + _Chain flushPATCH(url, [data, headers, withCredentials = false]) => + flushExpected("PATCH", url, data, headers, withCredentials); + + /** + * @ngdoc method + * @name ngMock.httpBackend#flushDELETE + * @methodOf ngMock.httpBackend + * @description + * Creates a new request expectation for DELETE requests. For more info see `flushExpected()`. + * + * @param {string|RegExp} url HTTP url. + * @param {Object=} headers HTTP headers. + * @returns {requestHandler} Returns an object with `respond` method that control how a matched + * request is handled. + */ + _Chain flushDELETE(url, [headers, withCredentials = false]) => + flushExpected("DELETE", url, null, headers, withCredentials); + + /** + * @ngdoc method + * @name ngMock.httpBackend#flushJSONP + * @methodOf ngMock.httpBackend + * @description + * Creates a new request expectation for JSONP requests. For more info see `flushExpected()`. + * + * @param {string|RegExp} url HTTP url. + * @param {Object=} headers HTTP headers. + * @returns {requestHandler} Returns an object with `respond` method that control how a matched + * request is handled. + */ + _Chain flushJSONP(url, [headers, withCredentials = false]) => + flushExpected("JSONP", url, null, headers, withCredentials); + /** * @ngdoc method @@ -553,7 +620,7 @@ class MockHttpBackend implements HttpBackend { * */ void verifyNoOutstandingRequest() { - if (!responses.isEmpty) throw ['Unflushed requests: ${responses.length}']; + if (!requests.isEmpty) throw ['Unflushed requests: ${requests.length}']; } @@ -568,7 +635,96 @@ class MockHttpBackend implements HttpBackend { */ void resetExpectations() { expectations.length = 0; - responses.length = 0; + requests.length = 0; + } + + + void _processRequest(RecordedRequest req) { + prettyPrint(data) { + return (data is String || data is Function || data is RegExp) + ? data + : JSON.encode(data); + } + + handleResponse(expectation) { + final xhr = new _MockXhr(); + final response = expectation.response(req.method, req.url, req.data, req.headers); + + final status = response[0]; + final data = response[1]; + xhr.respHeaders = response[2]; + + utils.relaxFnApply(req.callback, [status, data, xhr.getAllResponseHeaders()]); + } + + handleResponseAndTimeout(expectation) { + if (req.timeout != null) { + req.timeout.then(() => req.callback(-1, null, '')); + } else { + handleResponse(expectation); + } + } + + checkExpectation(expectation) { + if (!expectation.matchData(req)) { + throw ['Expected $expectation with different data\n' + 'EXPECTED: ${prettyPrint(expectation.data)}\nGOT: ${req.data}']; + } + + if (!expectation.matchHeaders(req)) { + throw ['Expected $expectation with different headers\n' + 'EXPECTED: ${prettyPrint(expectation.headers)}\n' + 'GOT: ${prettyPrint(req.headers)}']; + } + + if (!expectation.matchWithCredentials(req)) { + throw ['Expected $expectation with different withCredentials\n' + 'EXPECTED: ${prettyPrint(expectation.withCredentials)}\n' + 'GOT: ${prettyPrint(req.withCredentials)}']; + } + } + + hasResponse(d) => d != null && d.response != null; + + executeFirstExpectation(e, d) { + checkExpectation(e); + expectations.remove(e); + + if (hasResponse(e)) { + handleResponseAndTimeout(e); + } else if (hasResponse(d)) { + handleResponseAndTimeout(d); + } else { + throw ['No response defined !']; + } + } + + executeMatchingDefinition(e, d) { + if (hasResponse(d)) { + handleResponseAndTimeout(d); + } else if (d != null) { + throw ['No response defined !'];; + } else if (e != null) { + throw ['Unexpected request: ${req.method} ${req.url}\nExpected $e']; + } else { + throw ['Unexpected request: ${req.method} ${req.url}\nNo more requests expected']; + } + } + + firstExpectation() => + expectations.isEmpty ? null : expectations.first; + + matchingDefinition() => + definitions.firstWhere((d) => d.match(req), orElse: () => null); + + final e = firstExpectation(); + final d = matchingDefinition(); + + if (e != null && e.matchMethodAndUrl(req)) { + executeFirstExpectation(e, d); + } else { + executeMatchingDefinition(e, d); + } } } diff --git a/test/core/templateurl_spec.dart b/test/core/templateurl_spec.dart index fca4105fa..21d4ee984 100644 --- a/test/core/templateurl_spec.dart +++ b/test/core/templateurl_spec.dart @@ -98,16 +98,15 @@ _run({resolveUrls, staticMode}) { Injector injector, VmTurnZone zone, MockHttpBackend backend, DirectiveMap directives) { - backend - ..whenGET('PREFIX:${prefix}simple.html').respond('