diff --git a/lib/commands/entities/PlaybackRequestMap.js b/lib/commands/entities/PlaybackRequestMap.js index a6d4365..c98526b 100644 --- a/lib/commands/entities/PlaybackRequestMap.js +++ b/lib/commands/entities/PlaybackRequestMap.js @@ -1,7 +1,6 @@ const { SERIALIZE_VERSION } = require('../constants'); const { getInterceptedResponseId } = require('../functions/getInterceptedResponseId'); const { getMatcherAsString } = require('../functions/getMatcherAsString'); - const { PlaybackRequestMatcher } = require('./PlaybackRequestMatcher'); class PlaybackRequestMap { @@ -90,11 +89,13 @@ class PlaybackRequestMap { if (!matcher) { throw new Error(`No request matcher found with id: ${matcherId}`); } - return matcher.getResponse(getInterceptedResponseId( + const responseCollection = matcher.getResponseCollection(getInterceptedResponseId( interceptedRequest, /* c8 ignore next*/ options.matching?.ignores ?? [] - )); + ), interceptedRequest.url); + + return responseCollection.getNextResponse(); } notifyRequestStarted(matcherId) { diff --git a/lib/commands/entities/PlaybackRequestMatcher.js b/lib/commands/entities/PlaybackRequestMatcher.js index b025cb0..9c521e3 100644 --- a/lib/commands/entities/PlaybackRequestMatcher.js +++ b/lib/commands/entities/PlaybackRequestMatcher.js @@ -1,6 +1,6 @@ const { getRequestMatcherId } = require('../functions/getRequestMatcherId'); const { getMatcherAsString } = require('../functions/getMatcherAsString'); -const { PlaybackResponse } = require('./PlaybackResponse'); +const { PlaybackResponseCollection } = require('./PlaybackResponseCollection'); class PlaybackRequestMatcher { /** @@ -27,9 +27,9 @@ class PlaybackRequestMatcher { */ #ignores = []; /** - * @type {Map} + * @type {Map} */ - #responses = new Map(); + #responseCollections = new Map(); /** * When true, any response matching is ignored if only a single response is * recorded for this request matcher. @@ -123,39 +123,34 @@ class PlaybackRequestMatcher { headers, interceptedRequest ) { - let response = new PlaybackResponse( - statusCode, - statusMessage, - body, - headers, + let responseCollection = new PlaybackResponseCollection( interceptedRequest, this.#ignores ); - if (this.#responses.has(response.id)) { - response = this.#responses.get(response.id); + if (this.#responseCollections.has(responseCollection.id)) { + responseCollection = this.#responseCollections.get(responseCollection.id); } else { - this.#responses.set(response.id, response); + this.#responseCollections.set(responseCollection.id, responseCollection); } - response.addHit(); - if (response.hits > 1 && this.#anyOnce) { + + responseCollection.addResponse(statusCode, statusMessage, body, headers); + if (responseCollection.hits > 1 && this.#anyOnce) { throw new Error('Request matcher has "matching.anyOnce" set on it, but has recorded more than one response.'); } } - getResponse(responseId) { - let response = this.#responses.get(responseId); - if (!response && this.#anyOnce && this.#responses.size === 1) { - // Get the 1st and only response in the map. - response = this.#responses.values().next().value; - } - if (response) { - response.addHit(); + getResponseCollection(responseCollectionId, interceptedRequestUrl) { + let responseCollection = this.#responseCollections.get(responseCollectionId); + + if (!responseCollection) { + throw new Error(`No response collection found with ID: ${responseCollectionId} and URL: ${interceptedRequestUrl}`); } - return response; + + return responseCollection; } - getAllResponses() { - return Array.from(this.#responses.values()); + getAllResponseCollections() { + return Array.from(this.#responseCollections.values()); } isPending() { @@ -166,8 +161,8 @@ class PlaybackRequestMatcher { if (this.#inflight > 0) { return true; } - for (const response of this.#responses.values()) { - hits += response.hits; + for (const responseCollection of this.#responseCollections.values()) { + hits += responseCollection.hits; } return hits < this.#atLeast; } @@ -180,9 +175,9 @@ class PlaybackRequestMatcher { atLeast: this.#atLeast, anyOnce: this.#anyOnce, ignores: this.#ignores, - responses: Array.from(this.#responses.values()) - .filter(response => response.hits > 0) - .map(response => response.serialize()) + responseCollections: Array.from(this.#responseCollections.values()) + .filter(responseCollection => responseCollection.hits > 0) + .map(responseCollection => responseCollection.serialize()) }; } @@ -195,10 +190,20 @@ class PlaybackRequestMatcher { this.#ignores = data.ignores; // Assume that a request being deserialized is stale. this.stale = true; - data.responses.forEach(entry => { - const response = new PlaybackResponse(entry); - this.#responses.set(response.id, response); - }); + + if (data.responses) { + data.responses.forEach(resp => { + const { id, url, ...rest } = resp; + // This should match the shape that the response collection constructor/deserialize method expects + const responseCollection = new PlaybackResponseCollection({ id, url, responses: [rest] }); + this.#responseCollections.set(responseCollection.id, responseCollection); + }); + } else { + data.responseCollections.forEach(entry => { + const responseCollection = new PlaybackResponseCollection(entry); + this.#responseCollections.set(responseCollection.id, responseCollection); + }); + } } } diff --git a/lib/commands/entities/PlaybackResponse.js b/lib/commands/entities/PlaybackResponse.js index 80e58c3..60be319 100644 --- a/lib/commands/entities/PlaybackResponse.js +++ b/lib/commands/entities/PlaybackResponse.js @@ -1,4 +1,3 @@ -const { getInterceptedResponseId } = require('../functions/getInterceptedResponseId'); const { arrayBufferToBase64, base64ToArrayBuffer } = require('../functions/arrayBufferFns'); const jsonStringableTypes = ['string', 'number', 'boolean']; @@ -50,10 +49,6 @@ function deserializeResponseBody(body, bodyType) { } class PlaybackResponse { - /** - * @type {string} - */ - #id = null; /** * @type {number} */ @@ -77,9 +72,6 @@ class PlaybackResponse { /** * @type {number} */ - #hits = 0; - - get id() { return this.#id; } get statusCode() { return this.#statusCode; } @@ -91,15 +83,11 @@ class PlaybackResponse { /* c8 ignore next */ set headers(value) { this.#headers = value; } - get hits() { return this.#hits; } - /** * @param {number} statusCode * @param {string} statusMessage * @param {any} body * @param {{[key: string]: string}} headers - * @param {CypressInterceptedRequest} interceptedRequest - * @param {ResponseMatchingIgnores} ignores */ constructor(...args) { switch (args.length) { @@ -107,8 +95,8 @@ class PlaybackResponse { this.deserialize(args[0]); break; } - case 6: { - const [statusCode, statusMessage, body, headers, interceptedRequest, ignores] = args; + case 4: { + const [statusCode, statusMessage, body, headers] = args; this.#statusCode = statusCode; this.#statusMessage = statusMessage; this.#body = body; @@ -116,9 +104,6 @@ class PlaybackResponse { if (body) { this.#bodyType = getResponseBodyType(body); } - if (interceptedRequest) { - this.#id = getInterceptedResponseId(interceptedRequest, ignores); - } break; } default: { @@ -127,13 +112,8 @@ class PlaybackResponse { } } - addHit() { - this.#hits += 1; - } - serialize() { return { - id: this.#id, statusCode: this.#statusCode, statusMessage: this.#statusMessage, body: serializeResponseBody(this.#body, this.#bodyType), @@ -143,14 +123,12 @@ class PlaybackResponse { } deserialize({ - id, statusCode, statusMessage, body, bodyType, headers }) { - this.#id = id; this.#statusCode = statusCode; this.#statusMessage = statusMessage; this.#body = deserializeResponseBody(body, bodyType); diff --git a/lib/commands/entities/PlaybackResponseCollection.js b/lib/commands/entities/PlaybackResponseCollection.js new file mode 100644 index 0000000..7f8d67b --- /dev/null +++ b/lib/commands/entities/PlaybackResponseCollection.js @@ -0,0 +1,102 @@ +const { getInterceptedResponseId } = require('../functions/getInterceptedResponseId'); +const { PlaybackResponse } = require('./PlaybackResponse'); + +class PlaybackResponseCollection { + /** + * @type {string} + */ + #id = null; + /** + * @type {number} + */ + #hits = 0; + /** + * @type {Array} + */ + #responses = []; + /** + * @type {string} + */ + #url = ''; + + get id() { return this.#id; } + + get hits() { return this.#hits; } + + get url() { return this.#url; } + + get responses() { return this.#responses; } + + /** + * @param {CypressInterceptedRequest} interceptedRequest + * @param {ResponseMatchingIgnores} ignores + */ + constructor(...args) { + switch (args.length) { + case 1: { + this.deserialize(args[0]); + break; + } + case 2: { + const [interceptedRequest, ignores] = args; + this.#id = getInterceptedResponseId(interceptedRequest, ignores); + this.#url = interceptedRequest.url; + break; + } + default: { + throw new RangeError(`Invalid number of arguments: ${args.length}`); + } + } + } + + /** + * @param {number} statusCode + * @param {string} statusMessage + * @param {any} body + * @param {{[key: string]: string}} headers + */ + addResponse(statusCode, statusMessage, body, headers) { + this.#responses.push(new PlaybackResponse(statusCode, statusMessage, body, headers)); + this.#hits += 1; + } + + /** + * @returns {PlaybackResponse} + */ + getNextResponse() { + if (this.#hits > this.#responses.length - 1) { + throw new Error(`No more recorded responses found for request with URL: ${this.#url}. Hit Count: ${this.#hits + 1}`); + } + + const response = this.#responses[this.#hits]; + + this.#hits += 1; + + return response; + } + + serialize() { + return { + id: this.#id, + url: this.#url, + responses: this.#responses.map((response) => response.serialize()), + }; + } + + deserialize({ + id, + url, + responses + }) { + this.#id = id; + this.#url = url; + responses.forEach((responseData) => { + const response = new PlaybackResponse(responseData); + this.#responses.push(response); + }); + } +} + +module.exports = { + PlaybackResponseCollection +}; diff --git a/lib/commands/entities/tests/PlaybackRequestMap.spec.js b/lib/commands/entities/tests/PlaybackRequestMap.spec.js index 27e73f9..dab8f8b 100644 --- a/lib/commands/entities/tests/PlaybackRequestMap.spec.js +++ b/lib/commands/entities/tests/PlaybackRequestMap.spec.js @@ -11,31 +11,36 @@ let PlaybackRequestMap; function createSerializedMap(matcherCount = 1, responseCount = 1) { const matchers = []; for (let i = 1; i <= matcherCount; i++) { - const responses = []; + const responseCollections = []; for (let j = 1; j <= responseCount; j++) { - responses.push({ + responseCollections.push({ id: `mock-intercepted-response-id-${j}`, - statusCode: 200, - statusMessage: 'OK', - body: 'body-string', - bodyType: 'string', - headers: { 'mock-header': 'yes' }, + hits: 1, + url: 'http://mockurl.com/api/todo', + responses: [{ + statusCode: 200, + statusMessage: 'OK', + body: 'body-string', + bodyType: 'string', + headers: { 'mock-header': 'yes' }, + }] }); } matchers.push({ id: `mock-request-id-${i}`, atLeast: responseCount, - responses + responseCollections }); } return { version: SERIALIZE_VERSION, matchers }; } describe('PlaybackRequestMap', () => { - before(() => { + beforeEach(() => { // Tests for PlaybackResponse may have already run and replaced a dependency // we are mocking below. Resetting the module will ensure it gets our mock. resetModule(/entities\/PlaybackResponse.js$/); + resetModule(/entities\/PlaybackResponseCollection.js$/); td.replace('../../functions/getRequestMatcherId', { getRequestMatcherId }); td.replace('../../functions/getInterceptedResponseId', { getInterceptedResponseId }); @@ -76,9 +81,9 @@ describe('PlaybackRequestMap', () => { // Setup mocks to return ids from the mock responses. td.when(getInterceptedResponseId({ stuff: 1 }, td.matchers.anything())) - .thenReturn(serializedMap.matchers[0].responses[0].id); + .thenReturn(serializedMap.matchers[0].responseCollections[0].id); td.when(getInterceptedResponseId({ stuff: 2 }, td.matchers.anything())) - .thenReturn(serializedMap.matchers[0].responses[1].id); + .thenReturn(serializedMap.matchers[0].responseCollections[1].id); // Act const map = new PlaybackRequestMap('file-1', 'title-1', serializedMap); @@ -156,9 +161,9 @@ describe('PlaybackRequestMap', () => { .thenReturn('mock-request-matcher-id'); // Setup mocks to return ids from the mock responses. - td.when(getInterceptedResponseId({ stuff: 1 }, td.matchers.anything())) + td.when(getInterceptedResponseId({ stuff: 1, url: '/todo/1' }, td.matchers.anything())) .thenReturn('mock-intercepted-response-id-1'); - td.when(getInterceptedResponseId({ stuff: 2 }, td.matchers.anything())) + td.when(getInterceptedResponseId({ stuff: 2, url: '/todo/2' }, td.matchers.anything())) .thenReturn('mock-intercepted-response-id-2'); const map = new PlaybackRequestMap('file-1', 'title-1'); @@ -174,8 +179,8 @@ describe('PlaybackRequestMap', () => { // Act - Add the response twice, as we set toBeCalledAtLeast to 2. const response = { statusCode: 200, statusMessage: 'OK', headers: { 'mock-header': 'yes' } }; - map.addResponse(id, { stuff: 1 }, { ...response, body: 'body-string-1' }); - map.addResponse(id, { stuff: 2 }, { ...response, body: 'body-string-2' }); + map.addResponse(id, { stuff: 1, url: '/todo/1' }, { ...response, body: 'body-string-1' }); + map.addResponse(id, { stuff: 2, url: '/todo/2' }, { ...response, body: 'body-string-2' }); // Assert expect(map.hasPendingRequests()).to.be.false; @@ -189,22 +194,28 @@ describe('PlaybackRequestMap', () => { method: 'GET', matcher: '/example', atLeast: 2, - responses: [ + responseCollections: [ { id: 'mock-intercepted-response-id-1', - statusCode: 200, - statusMessage: 'OK', - body: 'body-string-1', - bodyType: 'string', - headers: { 'mock-header': 'yes' }, + url: '/todo/1', + responses: [{ + statusCode: 200, + statusMessage: 'OK', + body: 'body-string-1', + bodyType: 'string', + headers: { 'mock-header': 'yes' }, + }] }, { id: 'mock-intercepted-response-id-2', - statusCode: 200, - statusMessage: 'OK', - body: 'body-string-2', - bodyType: 'string', - headers: { 'mock-header': 'yes' }, + url: '/todo/2', + responses: [{ + statusCode: 200, + statusMessage: 'OK', + body: 'body-string-2', + bodyType: 'string', + headers: { 'mock-header': 'yes' }, + }] }, ] }] @@ -231,7 +242,7 @@ describe('PlaybackRequestMap', () => { // Setup mocks to return ids from the mock responses. td.when(getInterceptedResponseId({ stuff: 1 }, td.matchers.anything())) - .thenReturn(serializedMap.matchers[0].responses[0].id); + .thenReturn(serializedMap.matchers[0].responseCollections[0].id); const map = new PlaybackRequestMap('file-1', 'title-1', serializedMap); diff --git a/lib/commands/entities/tests/PlaybackRequestMatcher.spec.js b/lib/commands/entities/tests/PlaybackRequestMatcher.spec.js index 02ba0a6..1dbd78f 100644 --- a/lib/commands/entities/tests/PlaybackRequestMatcher.spec.js +++ b/lib/commands/entities/tests/PlaybackRequestMatcher.spec.js @@ -3,6 +3,7 @@ const { resetModule } = require('../../../tests/utils'); // Mocked dependencies. const getRequestMatcherId = td.func(); const getInterceptedResponseId = td.func(); +let PlaybackResponseCollection; // Subject-under-test let PlaybackRequestMatcher; @@ -17,6 +18,31 @@ function createSerializedRequestMatcher( responseCount, anyOnce = false, ignores = [] +) { + const responseCollections = []; + for (let i = 0; i < responseCount; i++) { + responseCollections.push({ + id: `mock-intercepted-response-id-${i + 1}`, + method: 'GET', + matcher: '/foo/bar', + responses: [{ + statusCode: 200, + statusMessage: 'OK', + body: 'body-string', + bodyType: 'string', + headers: { 'mock-header': 'yes' }, + }] + }); + } + return { id, atLeast, anyOnce, ignores, responseCollections }; +} + +function createOldSerializedRequestMatcher( + id, + atLeast, + responseCount, + anyOnce = false, + ignores = [] ) { const responses = []; for (let i = 0; i < responseCount; i++) { @@ -35,13 +61,14 @@ function createSerializedRequestMatcher( } describe('PlaybackRequestMatcher', () => { - before(() => { + beforeEach(() => { // Tests for PlaybackResponse may have already run and replaced a dependency // we are mocking below. Resetting the module will ensure it gets our mock. resetModule(/entities\/PlaybackResponse.js$/); td.replace('../../functions/getRequestMatcherId', { getRequestMatcherId }); td.replace('../../functions/getInterceptedResponseId', { getInterceptedResponseId }); + PlaybackResponseCollection = td.replace('../PlaybackResponseCollection').PlaybackResponseCollection; ({ PlaybackRequestMatcher } = require('../PlaybackRequestMatcher')); }); @@ -101,6 +128,44 @@ describe('PlaybackRequestMatcher', () => { it('works', () => { // Arrange const serialized = createSerializedRequestMatcher('mock-id', 2, 3); + + for (let i = 0; i < serialized.responseCollections.length; i++) { + const collection = serialized.responseCollections[i]; + td.when(new PlaybackResponseCollection(collection)).thenReturn(collection); + } + + // Act + const matcher = new PlaybackRequestMatcher(serialized); + + // Assert + expect(matcher.id).to.equal('mock-id'); + expect(matcher.toBeCalledAtLeast).to.equal(2); + expect(matcher.method).to.equal(serialized.method); + expect(matcher.matcher).to.equal(serialized.matcher); + expect(matcher.stale).to.be.true; + for (let i = 1; i <= 3; i++) { + const responseCollection = matcher.getResponseCollection(`mock-intercepted-response-id-${i}`); + expect(responseCollection).property('id').to.equal(`mock-intercepted-response-id-${i}`); + } + // Assert that it restored exactly the number of responses it should have. + expect(() => matcher.getResponseCollection('mock-intercepted-response-id-4')).to.throw().property('message').to.match(/No response collection found with ID: mock-intercepted-response-id-4/i); + }); + + it('works with old responses', () => { + // Arrange + const serialized = createOldSerializedRequestMatcher('mock-id', 2, 3); + + for (let i = 0; i < serialized.responses.length; i++) { + const response = serialized.responses[i]; + const {id, url, ...rest} = response; + const collection = { + id, + url, + responses: [rest] + }; + td.when(new PlaybackResponseCollection(collection)).thenReturn(collection); + } + // Act const matcher = new PlaybackRequestMatcher(serialized); @@ -111,24 +176,51 @@ describe('PlaybackRequestMatcher', () => { expect(matcher.matcher).to.equal(serialized.matcher); expect(matcher.stale).to.be.true; for (let i = 1; i <= 3; i++) { - expect(matcher.getResponse(`mock-intercepted-response-id-${i}`)).property('hits').to.equal(1); + const responseCollection = matcher.getResponseCollection(`mock-intercepted-response-id-${i}`); + expect(responseCollection).property('id').to.equal(`mock-intercepted-response-id-${i}`); } // Assert that it restored exactly the number of responses it should have. - expect(matcher.getResponse('mock-intercepted-response-id-4')).to.be.undefined; + expect(() => matcher.getResponseCollection('mock-intercepted-response-id-4')).to.throw().property('message').to.match(/No response collection found with ID: mock-intercepted-response-id-4/i); }); }); describe('serialization', () => { it('works', () => { // Arrange - const args = createConstructorArgs('GET', 'http://example.com/', { toBeCalledAtLeast: 5 }); + const interceptedUrl = 'http://example.com/'; + const mockInterceptedResponseId= 'mock-intercepted-response-id'; + const mockResponse = { + statusCode: 200, + statusMessage: 'OK', + body: 'body-string', + bodyType: 'string', + headers: { 'mock-header': 'yes' }, + }; + const interceptedRequest = { stuff: 1, url: interceptedUrl }; + const args = createConstructorArgs('GET', interceptedUrl, { toBeCalledAtLeast: 5 }); td.when(getRequestMatcherId(...args)) .thenReturn('mock-request-id'); - td.when(getInterceptedResponseId(td.matchers.anything(), td.matchers.anything())) - .thenReturn('mock-intercepted-response-id'); + + // Fake ResponseCollection + const addResponseFake = td.func(); + const serializeFake = td.func(); + td.when(serializeFake()).thenReturn({ + id: mockInterceptedResponseId, + url:interceptedUrl, + responses: [mockResponse] + }); + td.when(new PlaybackResponseCollection(interceptedRequest, [])).thenReturn( + { + id: mockInterceptedResponseId, + hits: 1, + serialize: serializeFake, + addResponse: addResponseFake, + }); + const matcher = new PlaybackRequestMatcher(...args); - matcher.addResponse(200, 'OK', 'body-string', { 'mock-header': 'yes' }, { stuff: true }); + matcher.addResponse(200, 'OK', 'body-string', { 'mock-header': 'yes' }, interceptedRequest); + td.verify(addResponseFake(200, 'OK', 'body-string', {'mock-header': 'yes'})); // Act const serialized = matcher.serialize(); @@ -138,16 +230,13 @@ describe('PlaybackRequestMatcher', () => { id: 'mock-request-id', ignores: [], method: 'GET', - matcher: 'http://example.com/', + matcher: interceptedUrl, atLeast: 5, anyOnce: false, - responses: [{ - id: 'mock-intercepted-response-id', - statusCode: 200, - statusMessage: 'OK', - body: 'body-string', - bodyType: 'string', - headers: { 'mock-header': 'yes' }, + responseCollections: [{ + id: mockInterceptedResponseId, + url: interceptedUrl, + responses: [mockResponse] }] }); }); @@ -168,30 +257,42 @@ describe('PlaybackRequestMatcher', () => { it('returns a single response if "matching.anyOnce" set.', () => { // Arrange const serialized = createSerializedRequestMatcher('mock-id', 1, 1, true); - + for (let i = 0; i < serialized.responseCollections.length; i++) { + const collection = serialized.responseCollections[i]; + td.when(new PlaybackResponseCollection(collection)).thenReturn(collection); + } // Act const matcher = new PlaybackRequestMatcher(serialized); - const allResponses = matcher.getAllResponses(); + const allResponseCollections = matcher.getAllResponseCollections(); - // Asesrt - expect(allResponses).to.have.length(1); + // Assert + expect(allResponseCollections).to.have.length(1); // Act - const response = matcher.getResponse('junk'); + const responseCollection = matcher.getResponseCollection('mock-intercepted-response-id-1'); // Assert - expect(allResponses[0]).to.equal(response); + expect(allResponseCollections[0]).to.equal(responseCollection); }); it('throws an error if more than one response is recorded when "matching.anyOnce" is set.', () => { // Arrange + const mockInterceptedResponseId = 'mock-intercepted-response-id-1'; td.when(getInterceptedResponseId(td.matchers.anything(), td.matchers.anything())) - .thenReturn('mock-intercepted-response-id-1'); + .thenReturn(mockInterceptedResponseId); const args = createConstructorArgs('GET', 'http://example.com/', { toBeCalledAtLeast: 1, matching: { anyOnce: true } }); + // Fake ResponseCollection + td.when(new PlaybackResponseCollection({ stuff: true }, [])).thenReturn( + { + id: mockInterceptedResponseId, + hits: 2, + addResponse: td.func(), + }); + + // Act const matcher = new PlaybackRequestMatcher(...args); - matcher.addResponse(200, 'OK', 'body-string', { 'mock-header': 'yes' }, { stuff: true }); // Assert expect( @@ -207,6 +308,14 @@ describe('PlaybackRequestMatcher', () => { td.when(getInterceptedResponseId(td.matchers.anything(), td.matchers.anything())) .thenReturn('mock-intercepted-response-id'); + // Fake ResponseCollection + const playbackResponseCollectionFake = { + id: 'mock-intercepted-response-id', + hits: 1, + addResponse: td.func(), + }; + td.when(new PlaybackResponseCollection({ stuff: true }, [])).thenReturn(playbackResponseCollectionFake); + // Act const matcher = new PlaybackRequestMatcher(...args); matcher.addResponse(200, 'OK', 'body-string', { 'mock-header': 'yes' }, { stuff: true }); @@ -216,57 +325,12 @@ describe('PlaybackRequestMatcher', () => { expect(matcher.isPending()).to.be.true; // Act - matcher.getResponse('mock-intercepted-response-id'); + // add a second request + playbackResponseCollectionFake.hits = 2; - // Assert - We pulled out the response, so its hit count went up by one. // Since the hit count now matches the toBeCalledAtLeast count, the matcher is no // longer pending. expect(matcher.isPending()).to.be.false; }); - - it('returns the expected "isPending" value for deserialized requests.', () => { - td.reset(); - // Arrange - Allow us to add a response with the same id as one that was - // deserialized. - td.when(getInterceptedResponseId(td.matchers.anything(), td.matchers.anything())) - .thenReturn('mock-intercepted-response-id-1'); - - // Act - Create a matcher that expects to be called 3 times and has 2 mock - // responses. - const matcher = new PlaybackRequestMatcher(createSerializedRequestMatcher('mock-id', 3, 2)); - - // Assert - expect(matcher.isPending()).to.be.false; - - // Act - // Calling notify started 3 times, as that is how many requests we will - // mock resolve below. - matcher.notifyRequestStarted(); - matcher.notifyRequestStarted(); - matcher.notifyRequestStarted(); - // Mock resolve one of the requests. - matcher.notifyRequestCompleted(); - matcher.getResponse('mock-intercepted-response-id-1'); - - // Assert - Total hit count should be 1. - expect(matcher.isPending()).to.be.true; - - // Act - Mock resolve another request. - matcher.notifyRequestCompleted(); - matcher.getResponse('mock-intercepted-response-id-2'); - - // Assert - Total hit count should be 2. - expect(matcher.isPending()).to.be.true; - - // Act - Mock resolve the final request. The testdouble above will cause - // this response to have the `mock-intercepted-response-id-1`, which is - // the same id as an existing response. - matcher.notifyRequestCompleted(); - matcher.addResponse(200, 'OK', 'body-string', { 'mock-header': 'yes' }, { stuff: true }); - - // Assert - At this point, the total hit count across the two possible - // responses for the request matchers is 3. - expect(matcher.isPending()).to.be.false; - }); }); }); diff --git a/lib/commands/entities/tests/PlaybackResponse.spec.js b/lib/commands/entities/tests/PlaybackResponse.spec.js index 5190a2c..11612c5 100644 --- a/lib/commands/entities/tests/PlaybackResponse.spec.js +++ b/lib/commands/entities/tests/PlaybackResponse.spec.js @@ -1,8 +1,5 @@ const { resetModule } = require('../../../tests/utils'); -// Mocked dependencies. -const getInterceptedResponseId = td.func(); - // Subject-under-test /** @type {import('../PlaybackResponse.js').PlaybackResponse} */ let PlaybackResponse; @@ -17,10 +14,6 @@ function createConstructorArgs(body) { body, // headers { 'mock-header': 'yes' }, - // Cypress intercepted request - { stuff: 'things' }, - // ignoredAttributes - [] ]; } @@ -41,8 +34,6 @@ describe('PlaybackResponse', () => { // below. Resetting the module will ensure it gets our mock. resetModule(/entities\/PlaybackResponse.js$/); - td.replace('../../functions/getInterceptedResponseId', { getInterceptedResponseId }); - ({ PlaybackResponse } = require('../PlaybackResponse')); }); @@ -53,14 +44,10 @@ describe('PlaybackResponse', () => { describe('constructor', () => { it('should create a new instance from a Cypress request', () => { // Arrange - td.when(getInterceptedResponseId(td.matchers.anything(), td.matchers.anything())) - .thenReturn('mock-intercepted-response-id'); - // Act const playbackResponse = new PlaybackResponse(...createConstructorArgs({ foo: 'bar' })); // Assert - expect(playbackResponse.id).to.equal('mock-intercepted-response-id'); expect(playbackResponse.statusCode).to.equal(200); expect(playbackResponse.statusMessage).to.equal('OK'); expect(playbackResponse.body).to.deep.equal({ foo: 'bar' }); @@ -110,10 +97,6 @@ describe('PlaybackResponse', () => { }); describe('serialization', () => { - beforeEach(() => { - td.when(getInterceptedResponseId(td.matchers.anything(), td.matchers.anything())) - .thenReturn('mock-intercepted-response-id'); - }); const cases = [ { body: { foo: 'bar' }, expectedBodyType: 'json', expected: '{"foo":"bar"}' }, @@ -130,7 +113,6 @@ describe('PlaybackResponse', () => { const serialized = playbackResponse.serialize(); // Assert - expect(serialized.id).to.equal('mock-intercepted-response-id'); expect(serialized.statusCode).to.equal(200); expect(serialized.statusMessage).to.equal('OK'); expect(serialized.body).to.equal(expected); @@ -139,17 +121,4 @@ describe('PlaybackResponse', () => { }); } }); - - describe('hits', () => { - it('keeps track of hits.', () => { - // Arrange - const playbackResponse = new PlaybackResponse(...createConstructorArgs({ foo: 'bar' })); - - // Act - playbackResponse.addHit(); - - // Assert - expect(playbackResponse.hits).to.equal(1); - }); - }); }); \ No newline at end of file diff --git a/lib/commands/entities/tests/PlaybackResponseCollection.spec.js b/lib/commands/entities/tests/PlaybackResponseCollection.spec.js new file mode 100644 index 0000000..b6538e1 --- /dev/null +++ b/lib/commands/entities/tests/PlaybackResponseCollection.spec.js @@ -0,0 +1,195 @@ +const getInterceptedResponseId = td.func(); +let PlaybackResponse; + +// Subject-under-test +let PlaybackResponseCollection; + +function createConstructorArgs(interceptedRequest, ignores = []) { + return [interceptedRequest, ignores]; +} + +function createSerializedResponseCollection( + id, + url, + responseCount +) { + const responses = []; + for (let i = 0; i < responseCount; i++) { + responses.push({ + statusCode: 200, + statusMessage: 'OK', + body: {'value': i + 1}, + bodyType: 'json', + headers: { 'mock-header': 'yes' }, + }); + } + return { + id, + url, + responses + }; +} + +describe('PlaybackResponseCollection', () => { + beforeEach(() => { + td.replace('../../functions/getInterceptedResponseId', { getInterceptedResponseId }); + PlaybackResponse = td.replace('../PlaybackResponse').PlaybackResponse; + + ({ PlaybackResponseCollection } = require('../PlaybackResponseCollection')); + }); + + afterEach(() => { + td.reset(); + }); + + describe('constructor', () => { + it('works', () => { + // Arrange + const mockUrl = '/todo/5'; + const interceptedRequest = {stuff: 5, url: mockUrl}; + const mockRequestId = 'mock-request-id-1'; + const args = createConstructorArgs(interceptedRequest); + + td.when(getInterceptedResponseId(interceptedRequest, [])).thenReturn(mockRequestId); + // Act + const collection = new PlaybackResponseCollection(...args); + + // Assert + expect(collection.url).to.equal(mockUrl); + expect(collection.id).to.equal(mockRequestId); + expect(collection.hits).to.equal(0); + expect(collection.responses).to.have.length(0); + }); + + it('throws if an incorrect number of arguments are provided.', () => { + expect(() => new PlaybackResponseCollection()).to.throw() + .property('message').to.match(/Invalid number of arguments/i); + }); + + describe('deserialization', () => { + it('works', () => { + // Arrange + const mockId = 'mock-id'; + const mockUrl = '/todo/6'; + const responseCount = 2; + const serialized = createSerializedResponseCollection(mockId, mockUrl, responseCount); + + for (let i = 0; i < serialized.responses.length; i++) { + const response = serialized.responses[i]; + td.when(new PlaybackResponse(response)).thenReturn(response); + } + + // Act + const collection = new PlaybackResponseCollection(serialized); + + // Assert + expect(collection.url).to.equal(mockUrl); + expect(collection.id).to.equal(mockId); + expect(collection.hits).to.equal(0); + expect(collection.responses).to.have.length(responseCount); + }); + }); + + describe('serialization', () => { + it('works', () => { + // Arrange + const mockUrl = '/todo/10'; + const interceptedRequest = {stuff: 10, url: mockUrl}; + const mockRequestId = 'mock-request-id-1'; + const args = createConstructorArgs(interceptedRequest); + + td.when(getInterceptedResponseId(interceptedRequest, [])).thenReturn(mockRequestId); + + // Fake PlaybackResponse + const mockStatusCode = 200; + const mockStatusMessage = 'OK'; + const mockBody = {stuff: 10}; + const mockHeaders = {'mock-header': 'yes'}; + const mockResponseSerialize = td.func(); + + td.when(mockResponseSerialize()).thenReturn({ + statusCode: mockStatusCode, + statusMessage: mockStatusMessage, + body: mockBody, + headers: mockHeaders, + }); + td.when(new PlaybackResponse(mockStatusCode, mockStatusMessage, mockBody, mockHeaders)).thenReturn({ + statusCode: mockStatusCode, + statusMessage: mockStatusMessage, + body: mockBody, + headers: mockHeaders, + serialize: mockResponseSerialize + }); + + const collection = new PlaybackResponseCollection(...args); + + // Act + collection.addResponse(mockStatusCode, mockStatusMessage, mockBody, mockHeaders); + const serialized = collection.serialize(); + + // Assert + expect(serialized).to.deep.equal({ + id: mockRequestId, + url: mockUrl, + responses: [{ + statusCode: mockStatusCode, + statusMessage: mockStatusMessage, + body: mockBody, + headers: mockHeaders, + }] + }); + }); + }); + + describe('responses', () => { + it('increments #hits for each added response', () => { + // Arrange + const mockUrl = '/todo/11'; + const interceptedRequest = {stuff: 11, url: mockUrl}; + const mockRequestId = 'mock-request-id-1'; + const args = createConstructorArgs(interceptedRequest); + + td.when(getInterceptedResponseId(interceptedRequest, [])).thenReturn(mockRequestId); + + td.when(new PlaybackResponse()).thenReturn({}); + + const collection = new PlaybackResponseCollection(...args); + + // Act + collection.addResponse(); + collection.addResponse(); + collection.addResponse(); + + // Assert + expect(collection.hits).to.equal(3); + }); + + it('gets the next response', () => { + // Arrange + const mockId = 'mock-id'; + const mockUrl = '/todo/6'; + const responseCount = 3; + const serialized = createSerializedResponseCollection(mockId, mockUrl, responseCount); + + for (let i = 0; i < serialized.responses.length; i++) { + const response = serialized.responses[i]; + td.when(new PlaybackResponse(response)).thenReturn(response); + } + + // Act + const collection = new PlaybackResponseCollection(serialized); + + // Assert + const response1 = collection.getNextResponse(); + const response2 = collection.getNextResponse(); + const response3 = collection.getNextResponse(); + + expect(response1.body.value).to.equal(1); + expect(response2.body.value).to.equal(2); + expect(response3.body.value).to.equal(3); + + expect(() => collection.getNextResponse()).to.throw().property('message').to.match(/No more recorded responses found for request with URL: \/todo\/6. Hit Count: 4/i); + }); + }); + }); +}); diff --git a/lib/commands/functions/tests/getInterceptedResponseId.spec.js b/lib/commands/functions/tests/getInterceptedResponseId.spec.js index 11dda75..89f87fc 100644 --- a/lib/commands/functions/tests/getInterceptedResponseId.spec.js +++ b/lib/commands/functions/tests/getInterceptedResponseId.spec.js @@ -1,8 +1,7 @@ // Mocked dependencies. const md5 = td.func(); -// Subject-under-test -let getInterceptedResponseId; + function createMockRequest(body, search = 'foo=bar') { return { @@ -13,7 +12,10 @@ function createMockRequest(body, search = 'foo=bar') { } describe('getInterceptedResponseId', () => { - before(() => { + // Subject-under-test + let getInterceptedResponseId; + + beforeEach(() => { td.replace('blueimp-md5', md5); ({ getInterceptedResponseId } = require('../getInterceptedResponseId.js')); }); diff --git a/lib/commands/functions/tests/isPlaybackMode.spec.js b/lib/commands/functions/tests/isPlaybackMode.spec.js index 2cc494c..4a92935 100644 --- a/lib/commands/functions/tests/isPlaybackMode.spec.js +++ b/lib/commands/functions/tests/isPlaybackMode.spec.js @@ -5,7 +5,7 @@ const getPlaybackMode = td.func(); let isPlaybackMode; describe('isPlaybackMode', () => { - before(() => { + beforeEach(() => { td.replace('../getPlaybackMode.js', { getPlaybackMode }); ({ isPlaybackMode } = require('../isPlaybackMode')); }); diff --git a/lib/commands/index.js b/lib/commands/index.js index 58fd02b..0ceabd7 100644 --- a/lib/commands/index.js +++ b/lib/commands/index.js @@ -81,7 +81,7 @@ afterEach(function playbackAfterEach() { const allRequests = map.getAll().map(request => { return { matcher: `${request.method} ${request.matcher}`, - responses: request.getAllResponses().map(response => response.serialize()) + responseCollections: request.getAllResponseCollections().map(responseCollection => responseCollection.serialize()) }; }); diff --git a/lib/tasks/functions/tests/writeRequestsToDisk.spec.js b/lib/tasks/functions/tests/writeRequestsToDisk.spec.js index 0ab6784..516b24a 100644 --- a/lib/tasks/functions/tests/writeRequestsToDisk.spec.js +++ b/lib/tasks/functions/tests/writeRequestsToDisk.spec.js @@ -14,7 +14,7 @@ let writeRequestsToDisk; // x\x9C«V*.)MKS²*)*M\x05\x00'6\x05_ describe('writeRequestsToDisk', () => { - before(() => { + beforeEach(() => { td.replace(fs.promises, 'writeFile', writeFile); td.replace(fs.promises, 'mkdir', mkdir); ({ writeRequestsToDisk } = require('../writeRequestsToDisk.js')); diff --git a/sandbox/cypress/e2e/app.cy.js b/sandbox/cypress/e2e/app.cy.js index 45126ea..b00b4bc 100644 --- a/sandbox/cypress/e2e/app.cy.js +++ b/sandbox/cypress/e2e/app.cy.js @@ -1,6 +1,6 @@ describe('local to-do app', () => { let baseUrl; - before(() => { + beforeEach(() => { cy.isPlayingBackRequests().then(isPlayingBack => { cy.isRecordingRequests().then(isRecording => { // The scripts defined in the `package.json` cause the server to be run @@ -39,4 +39,27 @@ describe('local to-do app', () => { cy.wait('@static'); cy.get('h1').should('be.visible'); }); + + it("can read an old playback file", () => { + cy.playback('GET', new RegExp('./assets/static-image.png'), + { + matching: { ignores: ['port'] } + } + ).as('static'); + + cy.playback('GET', new RegExp('learning.oreilly')).as('image'); + + cy.playback('GET', new RegExp('/todos/'), + { + toBeCalledAtLeast: 2 + } + ).as('todos'); + + cy.visit(baseUrl); + + cy.wait('@todos'); + cy.wait('@image'); + cy.wait('@static'); + cy.get('h1').should('be.visible'); + }) }); diff --git a/sandbox/cypress/fixtures/e2e/app-cy/local-to-do-app-can-read-an-old-playback-file.cy-playback b/sandbox/cypress/fixtures/e2e/app-cy/local-to-do-app-can-read-an-old-playback-file.cy-playback new file mode 100644 index 0000000..4102822 Binary files /dev/null and b/sandbox/cypress/fixtures/e2e/app-cy/local-to-do-app-can-read-an-old-playback-file.cy-playback differ diff --git a/sandbox/cypress/fixtures/e2e/app-cy/local-to-do-app-does-something.cy-playback b/sandbox/cypress/fixtures/e2e/app-cy/local-to-do-app-does-something.cy-playback index 4102822..5a1a965 100644 Binary files a/sandbox/cypress/fixtures/e2e/app-cy/local-to-do-app-does-something.cy-playback and b/sandbox/cypress/fixtures/e2e/app-cy/local-to-do-app-does-something.cy-playback differ