From 1c2a30e87f924f8d95f17fea9964619a38d77ef9 Mon Sep 17 00:00:00 2001 From: Ika Date: Thu, 20 May 2021 13:10:19 +0400 Subject: [PATCH 1/3] add filter --- index.js | 133 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 85 insertions(+), 48 deletions(-) diff --git a/index.js b/index.js index 9e8192c..425cd11 100644 --- a/index.js +++ b/index.js @@ -1,35 +1,44 @@ -'use strict'; -const path = require('path'); -const util = require('./util'); +"use strict"; +const path = require("path"); +const util = require("./util"); const guidGenerator = util.guidGenerator; const sizeInMbytes = util.sizeInMbytes; const blobToPlain = util.blobToPlain; -const cypressConfig = Cypress.config('autorecord') || {}; +const cypressConfig = Cypress.config("autorecord") || {}; const isCleanMocks = cypressConfig.cleanMocks || false; const isForceRecord = cypressConfig.forceRecord || false; const recordTests = cypressConfig.recordTests || []; const blacklistRoutes = cypressConfig.blacklistRoutes || []; +const interceptPattern = cypressConfig.interceptPattern || "*"; const whitelistHeaders = cypressConfig.whitelistHeaders || []; -const supportedMethods = ['get', 'GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD']; +const supportedMethods = [ + "get", + "GET", + "POST", + "PUT", + "DELETE", + "PATCH", + "HEAD", +]; const fileName = path.basename( - Cypress.spec.name, - path.extname(Cypress.spec.name), + Cypress.spec.name, + path.extname(Cypress.spec.name) ); // The replace fixes Windows path handling -const fixturesFolder = Cypress.config('fixturesFolder').replace(/\\/g, '/'); -const fixturesFolderSubDirectory = fileName.replace(/\./, '-'); -const mocksFolder = path.join(fixturesFolder, '../mocks'); +const fixturesFolder = Cypress.config("fixturesFolder").replace(/\\/g, "/"); +const fixturesFolderSubDirectory = fileName.replace(/\./, "-"); +const mocksFolder = path.join(fixturesFolder, "../mocks"); -before(function() { +before(function () { if (isCleanMocks) { - cy.task('cleanMocks'); + cy.task("cleanMocks"); } if (isForceRecord) { - cy.task('removeAllMocks'); + cy.task("removeAllMocks"); } }); @@ -53,37 +62,39 @@ module.exports = function autoRecord() { // Timestamp for when this test was executed let timestamp = null; - before(function() { + before(function () { // Get mock data that relates to this spec file - cy.task('readFile', path.join(mocksFolder, `${fileName}.json`)).then((data) => { - routesByTestId = data === null ? {} : data; - }); + cy.task("readFile", path.join(mocksFolder, `${fileName}.json`)).then( + (data) => { + routesByTestId = data === null ? {} : data; + } + ); }); - beforeEach(function() { + beforeEach(function () { // Reset routes before each test case routes = []; - cy.intercept('*', (req) => { + cy.intercept(interceptPattern, (req) => { // This is cypress loading the page if ( - Object.keys(req.headers).some((k) => k === 'x-cypress-authorization') + Object.keys(req.headers).some((k) => k === "x-cypress-authorization") ) { return; } req.reply((res) => { const url = req.url; - const status = req.status; + const status = res.statusCode; const method = req.method; const data = - res.body.constructor.name === 'Blob' + res.body.constructor.name === "Blob" ? blobToPlain(res.body) : res.body; const body = req.body; const headers = Object.entries(res.headers) .filter(([key]) => - whitelistHeaderRegexes.some((regex) => regex.test(key)), + whitelistHeaderRegexes.some((regex) => regex.test(key)) ) .reduce((obj, [key, value]) => ({ ...obj, [key]: value }), {}); @@ -98,7 +109,7 @@ module.exports = function autoRecord() { // when the response has changed for an identical request signature // add this entry as well. This is useful for polling-oriented endpoints // that can have varying responses. - route.response === data, + route.response === data ) ) { routes.push({ url, method, status, data, body, headers }); @@ -108,17 +119,19 @@ module.exports = function autoRecord() { // check to see if test is being force recorded // TODO: change this to regex so it only reads from the beginning of the string - isTestForceRecord = this.currentTest.title.includes('[r]'); - this.currentTest.title = isTestForceRecord ? this.currentTest.title.split('[r]')[1].trim() : this.currentTest.title; + isTestForceRecord = this.currentTest.title.includes("[r]"); + this.currentTest.title = isTestForceRecord + ? this.currentTest.title.split("[r]")[1].trim() + : this.currentTest.title; // Load stubbed data from local JSON file // Do not stub if... // This test is being force recorded // there are no mock data for this test if ( - !recordTests.includes(this.currentTest.title) - && !isTestForceRecord - && routesByTestId[this.currentTest.title] + !recordTests.includes(this.currentTest.title) && + !isTestForceRecord && + routesByTestId[this.currentTest.title] ) { // This is used to group routes by method type and url (e.g. { GET: { '/api/messages': {...} }}) const sortedRoutes = {}; @@ -127,7 +140,7 @@ module.exports = function autoRecord() { }); // set the browser's Date to the timestamp at which this spec's endpoints were recorded. - cy.clock(routesByTestId[this.currentTest.title].timestamp, ['Date']); + cy.clock(routesByTestId[this.currentTest.title].timestamp, ["Date"]); routesByTestId[this.currentTest.title].routes.forEach((request) => { if (!sortedRoutes[request.method][request.url]) { @@ -157,27 +170,29 @@ module.exports = function autoRecord() { fixture: `${fixturesFolderSubDirectory}/${newResponse.fixtureId}.json`, } : newResponse.response, - newResponse.headers, + newResponse.headers ); if (sortedRoutes[method][url].length > index + 1) { index++; } }); - }, + } ); }; // Stub all recorded routes Object.keys(sortedRoutes).forEach((method) => { - Object.keys(sortedRoutes[method]).forEach((url) => createStubbedRoute(method, url)); + Object.keys(sortedRoutes[method]).forEach((url) => + createStubbedRoute(method, url) + ); }); } else { // lock the browser's timestamp in place so that there is no variation with the // timestamp REST APIs use as an argument due to undeterministic page load times // which will cause varying timestamps. `cy.clock` locks the timestamp. timestamp = Date.now(); - cy.clock(timestamp, ['Date']); + cy.clock(timestamp, ["Date"]); } // Store test name if isCleanMocks is true @@ -185,16 +200,16 @@ module.exports = function autoRecord() { testNames.push(this.currentTest.title); } - cy.clock().invoke('restore'); + cy.clock().invoke("restore"); }); - afterEach(function() { + afterEach(function () { // Check to see if the current test already has mock data or if forceRecord is on if ( - (!routesByTestId[this.currentTest.title] - || isTestForceRecord - || recordTests.includes(this.currentTest.title)) - && !isCleanMocks + (!routesByTestId[this.currentTest.title] || + isTestForceRecord || + recordTests.includes(this.currentTest.title)) && + !isCleanMocks ) { // Construct endpoint to be saved locally const endpoints = routes.map((request) => { @@ -205,7 +220,13 @@ module.exports = function autoRecord() { // If the mock data is too large, store it in a separate json if (isFileOversized) { fixtureId = guidGenerator(); - addFixture[path.join(fixturesFolder, fixturesFolderSubDirectory, `${fixtureId}.json`)] = request.data; + addFixture[ + path.join( + fixturesFolder, + fixturesFolderSubDirectory, + `${fixtureId}.json` + ) + ] = request.data; } return { @@ -215,7 +236,7 @@ module.exports = function autoRecord() { status: request.status, headers: request.headers, body: request.body, - response: isFileOversized ? undefined : request.data + response: isFileOversized ? undefined : request.data, }; }); @@ -224,7 +245,13 @@ module.exports = function autoRecord() { routesByTestId[this.currentTest.title].routes.forEach((route) => { // If fixtureId exist, delete the json if (route.fixtureId) { - removeFixture.push(path.join(fixturesFolder, fixturesFolderSubDirectory, `${route.fixtureId}.json`)); + removeFixture.push( + path.join( + fixturesFolder, + fixturesFolderSubDirectory, + `${route.fixtureId}.json` + ) + ); } }); } @@ -237,13 +264,13 @@ module.exports = function autoRecord() { // to that specific time so that the endpoints can be properly stubbed as the // the timestamp is part of many of the APIs' signature as well as POST body and uniquely identifies it. timestamp, - routes: endpoints + routes: endpoints, }; } } }); - after(function() { + after(function () { // Transfer used mock data to new object to be stored locally if (isCleanMocks) { Object.keys(routesByTestId).forEach((testName) => { @@ -252,15 +279,25 @@ module.exports = function autoRecord() { } else { routesByTestId[testName].routes.forEach((route) => { if (route.fixtureId) { - cy.task('deleteFile', path.join(fixturesFolder, fixturesFolderSubDirectory, `${route.fixtureId}.json`)); + cy.task( + "deleteFile", + path.join( + fixturesFolder, + fixturesFolderSubDirectory, + `${route.fixtureId}.json` + ) + ); } }); } }); } - removeFixture.forEach((fixtureName) => cy.task('deleteFile', fixtureName)); - cy.writeFile(path.join(mocksFolder, `${fileName}.json`), isCleanMocks ? cleanMockData : routesByTestId); + removeFixture.forEach((fixtureName) => cy.task("deleteFile", fixtureName)); + cy.writeFile( + path.join(mocksFolder, `${fileName}.json`), + isCleanMocks ? cleanMockData : routesByTestId + ); Object.keys(addFixture).forEach((fixtureName) => { cy.writeFile(fixtureName, addFixture[fixtureName]); }); From e780885d3e407a89ed2776c0f2b477c2ee429636 Mon Sep 17 00:00:00 2001 From: Ika Date: Fri, 21 May 2021 10:06:13 +0400 Subject: [PATCH 2/3] update readme --- README.md | 71 +++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 50 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 4ebdbe8..7add444 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ Cypress Autorecord is a plugin built to be used with Cypress.io. It simplifies mocking by auto-recording/stubbing HTTP interactions and automating the process of updating/deleting recordings. Spend more time writing integration tests instead of managing your mock data. Refer to the [changelog](https://github.com/Nanciee/cypress-autorecord/blob/master/CHANGELOG.md) for more details on all the changes. ## v3.0.0 is now live! + Version 3 is now compatible with Cypress 6 and 7 and includes a few fixes. If you are using an earlier cypress version, you will need to use cypress-autorecord v2.x. ## Getting Started @@ -16,24 +17,27 @@ npm install --save-dev cypress-autorecord Add this snippet in your project's `/cypress/plugins/index.js` ```js -const fs = require('fs'); -const autoRecord = require('cypress-autorecord/plugin'); +const fs = require("fs"); +const autoRecord = require("cypress-autorecord/plugin"); module.exports = (on, config) => { autoRecord(on, config, fs); }; ``` + To allow for auto-recording and stubbing to work, require cypress-autorecord in each of your test file and call the function at the beginning of your parent `describe` block. ```js -const autoRecord = require('cypress-autorecord'); // Require the autorecord function - -describe('Home Page', function() { // Do not use arrow functions +const autoRecord = require("cypress-autorecord"); // Require the autorecord function + +describe("Home Page", function () { + // Do not use arrow functions autoRecord(); // Call the autoRecord function at the beginning of your describe block - + // Your hooks (beforeEach, afterEach, etc) goes here - - it('...', function() { // Do not use arrow functions + + it("...", function () { + // Do not use arrow functions // Your test goes here }); }); @@ -46,17 +50,20 @@ That is it! Now, just run your tests and the auto-record will take care of the r ## Updating Mocks In the case you need to update your mocks for a particular test: + ```js -const autoRecord = require('cypress-autorecord'); - -describe('Home Page', function() { +const autoRecord = require("cypress-autorecord"); + +describe("Home Page", function () { autoRecord(); - - it('[r] my awesome test', function() { // Insert [r] at the beginning of your test name + + it("[r] my awesome test", function () { + // Insert [r] at the beginning of your test name // ... }); }); ``` + This will force the test to record over your existent mocks for **ONLY** this test on your next run. This can also be done through the configurations by adding the test name in the file `cypress.json`: @@ -91,39 +98,61 @@ Stale mocks that are no longer being used can be automatically removed when you } ``` +## Set Recording Pattern For Cypress Intercept + +By default autorecorder is recording all outgoing requests but if you want to record only specific calls based on pattern(Ex. just record api calls on backend), you can set `interceptPattern` in `cypress.json`. it can be string, regex or glob + +```json +{ + "autorecord": { + "interceptPattern": "http://localhost:3000/api/*" + } +} +``` + **_NOTE: Only mocks that are used during the run are considered "active". Make sure to only set `cleanMocks` to `true` when you are running ALL your tests. Remove any unintentional `.only` or `.skip`._** ## How It Works ### How does the recording and stubbing work? + Cypress Autorecord uses Cypress' built-in `cy.intercept` to hook into every request, including GET, POST, DELETE and PUT. If mocks doesn't exist for a test, the http calls (requests and responses) are captured and automatically written to a local file. If mocks exist for a test, each http call will be stubbed in the `beforeEach` hook. ### Where are the mocks saved? + The mocks will be automatically generated and saved in the `/cypress/mocks/` folder. Mocks are grouped by test name and test file name. You will find mock files matching the name of your test files. Within your mock files, mocks are organized by test names in the order that they were called. Changing the test file name or test name will result to a disconnection to the mocks and trigger a recording on your next run. ### Can I manually update the mocks? + Mocks are saved as a simple json object and can be updated manually. This is **not** recommended since any manual change you make will be overwritten when you automatically update the mocks. Leave the data management to cypress-autorecord. Make any modifications to the http calls inside your test so that it will be consistent across recordings. ```js -it('should display an error message when send message fails', function() { +it("should display an error message when send message fails", function () { cy.route({ - url: '/message', - method: 'POST', + url: "/message", + method: "POST", status: 404, - response: { error: 'It did not work' }, + response: { error: "It did not work" }, }); - cy.get('[data-cy="msgInput"]').type('Hello World!'); + cy.get('[data-cy="msgInput"]').type("Hello World!"); cy.get('[data-cy="msgSend"]').click(); - cy.get('[data-cy="errorMessage"]').should('contain', 'Looks like we ran into a problem. Please try again.'); + cy.get('[data-cy="errorMessage"]').should( + "contain", + "Looks like we ran into a problem. Please try again." + ); }); ``` ## Known Issues #### Only XMLHttpRequests will be recorded and stubbed + Cypress-autorecord leverages Cypress' built in `cy.route` to handle stubbing, which means that it inherits some limitations as well. This is the disclaimer on the `cy.route` documentation page with some potential workarounds: ->Please be aware that Cypress only currently supports intercepting XMLHttpRequests. Requests using the Fetch API and other types of network requests like page loads and