From 7bd99f2afe9eee6acbfd65a67568acf03c4da4c9 Mon Sep 17 00:00:00 2001 From: Artem Derevnjuk Date: Tue, 10 Jan 2023 23:51:15 +0400 Subject: [PATCH] feat(plugin): allow to dispose of a HAR file (#135) To stop the ongoing recording of network requests and disposes of the recorded logs, which will be not saved to a HAR file, new `disposeOfHar` command has been introduced. You may use this command if you have started recording network logs with `recordHar` command, but you decide to refuse to save the recorded logs to a HAR file. Here's an example of how you might use this command to dispose of the HAR: ```js describe('my tests', () => { before(() => { // start recording cy.recordHar(); }); after(() => { // decide not to save the recorded logs cy.disposeOfHar(); }); }); ``` closes #103 --- README.md | 52 ++++++++++---- src/Plugin.spec.ts | 124 +++++++++++++++++++++++++++++++++ src/Plugin.ts | 14 ++-- src/commands.ts | 5 ++ src/index.ts | 5 +- src/network/NetworkObserver.ts | 1 + 6 files changed, 175 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index b7141d2..4367926 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Generate [HTTP Archive (HAR)](http://www.softwareishard.com/blog/har-12-spec/) f - [Commands](#commands) - [recordHar](#recordhar) - [saveHar](#savehar) + - [disposeOfHar](#disposeofhar) - [Example Usage](#example-usage) - [License](#license) @@ -201,6 +202,27 @@ You can customize a destination folder overriding any previous settings: cy.saveHar({ outDir: './hars' }); ``` +### disposeOfHar + +Stops the ongoing recording of network requests and disposes of the recorded logs, which will be not saved to a HAR file. + +You may use this command if you have started recording network logs with `recordHar` command, but you decide to refuse to save the recorded logs to a HAR file. +Here's an example of how you might use this command to dispose of the HAR: + +```js +describe('my tests', () => { + before(() => { + // start recording + cy.recordHar(); + }); + + after(() => { + // decide not to save the recorded logs + cy.disposeOfHar(); + }); +}); +``` + ## Example Usage To generate a HAR file during your tests, you'll need to include the `recordHar` and `saveHar` commands in your test file(s). Here's an example of how you might use these commands in a test: @@ -222,21 +244,23 @@ describe('my tests', () => { You can also generate a HAR file only for Chrome browser sessions, if it is not interactive run, and only if the test has failed: ```js -beforeEach(() => { - const isInteractive = Cypress.config('isInteractive'); - const isChrome = Cypress.browser.name === 'chrome'; - if (!isInteractive && isChrome) { - cy.recordHar(); - } -}); +describe('my tests', () => { + beforeEach(() => { + const isInteractive = Cypress.config('isInteractive'); + const isChrome = Cypress.browser.name === 'chrome'; + if (!isInteractive && isChrome) { + cy.recordHar(); + } + }); -afterEach(() => { - const { state } = this.currentTest; - const isInteractive = Cypress.config('isInteractive'); - const isChrome = Cypress.browser.name === 'chrome'; - if (!isInteractive && isChrome && state !== 'passed') { - cy.saveHar(); - } + afterEach(function () { + const { state } = this.currentTest; + const isInteractive = Cypress.config('isInteractive'); + const isChrome = Cypress.browser.name === 'chrome'; + if (!isInteractive && isChrome && state !== 'passed') { + cy.saveHar(); + } + }); }); ``` diff --git a/src/Plugin.spec.ts b/src/Plugin.spec.ts index e64fc50..88b8b4d 100644 --- a/src/Plugin.spec.ts +++ b/src/Plugin.spec.ts @@ -148,6 +148,37 @@ describe('Plugin', () => { }); describe('saveHar', () => { + it(`should return null to satisfy Cypress's contract when connection is not established yet`, async () => { + // arrange + const options = { + fileName: 'file.har', + outDir: tmpdir() + } as SaveOptions; + // act + const result = await plugin.saveHar(options); + // assert + expect(result).toBe(null); + }); + + it(`should return null to satisfy Cypress's contract by default`, async () => { + // arrange + when(connectionFactoryMock.create(anything())).thenReturn( + instance(connectionMock) + ); + when( + observerFactoryMock.createNetworkObserver(anything(), anything()) + ).thenReturn(instance(networkObserverMock)); + await plugin.recordHar({}); + const options = { + fileName: 'file.har', + outDir: tmpdir() + } as SaveOptions; + // act + const result = await plugin.saveHar(options); + // assert + expect(result).toBe(null); + }); + it('should log an error message when the connection is corrupted', async () => { // arrange const options = { @@ -216,6 +247,37 @@ describe('Plugin', () => { ).once(); }); + it('should log an error message when failing to build a HAR file', async () => { + // arrange + when(connectionFactoryMock.create(anything())).thenReturn( + instance(connectionMock) + ); + when( + observerFactoryMock.createNetworkObserver(anything(), anything()) + ).thenReturn(instance(networkObserverMock)); + when(fileManagerMock.createTmpWriteStream()).thenResolve( + resolvableInstance(writableStreamMock) + ); + // @ts-expect-error type mismatch + when(writableStreamMock.closed).thenReturn(true); + when(writableStreamMock.path).thenReturn('temp-file.txt'); + await plugin.recordHar({}); + + const outDir = tmpdir(); + const fileName = 'file.har'; + + when(fileManagerMock.readFile(anyString())).thenThrow( + new Error('something went wrong') + ); + // act + await plugin.saveHar({ + outDir, + fileName + }); + // assert + verify(loggerMock.err(match(/^Failed to save HAR/))).once(); + }); + it('should unsubscribe from the network events', async () => { // arrange when(connectionFactoryMock.create(anything())).thenReturn( @@ -282,6 +344,19 @@ describe('Plugin', () => { ).thenReturn(instance(networkObserverMock)); }); + it(`should return null to satisfy Cypress's contract`, async () => { + // arrange + const options = { + content: true, + excludePaths: [], + includeHosts: [] + } as RecordOptions; + // act + const result = await plugin.recordHar(options); + // assert + expect(result).toBe(null); + }); + it('should open connection and listen to network events', async () => { // arrange const options = { @@ -365,4 +440,53 @@ describe('Plugin', () => { verify(writableStreamMock.write(match(`${EOL}`))).never(); }); }); + + describe('disposeOfHar', () => { + beforeEach(() => { + when(connectionFactoryMock.create(anything())).thenReturn( + instance(connectionMock) + ); + when( + observerFactoryMock.createNetworkObserver(anything(), anything()) + ).thenReturn(instance(networkObserverMock)); + when(fileManagerMock.createTmpWriteStream()).thenResolve( + resolvableInstance(writableStreamMock) + ); + // @ts-expect-error type mismatch + when(writableStreamMock.closed).thenReturn(true); + when(writableStreamMock.path).thenReturn('temp-file.txt'); + }); + + it(`should return null to satisfy Cypress's contract`, async () => { + // act + const result = await plugin.disposeOfHar(); + // assert + expect(result).toBe(null); + }); + + it('should dispose of a stream', async () => { + // arrange + await plugin.recordHar({}); + // act + await plugin.disposeOfHar(); + // assert + verify(writableStreamMock.end()).once(); + }); + + it('should unsubscribe from the network events', async () => { + // arrange + await plugin.recordHar({}); + // act + await plugin.disposeOfHar(); + // assert + verify(networkObserverMock.unsubscribe()).once(); + }); + + it('should do nothing when the recording is not started yet', async () => { + // act + await plugin.disposeOfHar(); + // assert + verify(networkObserverMock.unsubscribe()).never(); + }); + }); }); diff --git a/src/Plugin.ts b/src/Plugin.ts index e55141a..5e75216 100644 --- a/src/Plugin.ts +++ b/src/Plugin.ts @@ -83,8 +83,6 @@ export class Plugin { public async saveHar(options: SaveOptions): Promise { const filePath = join(options.outDir, options.fileName); - this.assertFilePath(filePath); - if (!this.connection) { this.logger.err(`Failed to save HAR. First you should start recording.`); @@ -101,13 +99,13 @@ export class Plugin { } catch (e) { this.logger.err(`Failed to save HAR: ${e.message}`); } finally { - await this.dispose(); + await this.disposeOfHar(); } return null; } - public async dispose(): Promise { + public async disposeOfHar(): Promise { await this.networkObservable?.unsubscribe(); delete this.networkObservable; @@ -120,6 +118,8 @@ export class Plugin { } delete this.buffer; + + return null; } private async buildHar(): Promise { @@ -163,12 +163,6 @@ export class Plugin { } } - private assertFilePath(path: string | undefined): asserts path is string { - if (typeof path !== 'string') { - throw new Error('File path must be a string.'); - } - } - private isSupportedBrowser(browser: Cypress.Browser): boolean { return ['chromium'].includes(browser?.family); } diff --git a/src/commands.ts b/src/commands.ts index dab0232..707d4e8 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -23,3 +23,8 @@ Cypress.Commands.add( return cy.task('saveHar', options); } ); + +Cypress.Commands.add( + 'disposeOfHar', + (): Cypress.Chainable => cy.task('disposeOfHar') +); diff --git a/src/index.ts b/src/index.ts index bbe3648..3edb39e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,8 +8,8 @@ declare global { namespace Cypress { interface Chainable { saveHar(options?: Partial): Chainable; - recordHar(options?: RecordOptions): Chainable; + disposeOfHar(): Chainable; } } } @@ -25,7 +25,8 @@ export const install = (on: Cypress.PluginEvents): void => { on('task', { saveHar: (options: SaveOptions): Promise => plugin.saveHar(options), recordHar: (options: RecordOptions): Promise => - plugin.recordHar(options) + plugin.recordHar(options), + disposeOfHar: (): Promise => plugin.disposeOfHar() }); on( diff --git a/src/network/NetworkObserver.ts b/src/network/NetworkObserver.ts index 3274165..aa8cdd9 100644 --- a/src/network/NetworkObserver.ts +++ b/src/network/NetworkObserver.ts @@ -65,6 +65,7 @@ export class NetworkObserver implements Observer { await Promise.all([this.security?.disable(), this.network?.disable()]); delete this.destination; this._entries.clear(); + this._extraInfoBuilders.clear(); } public signedExchangeReceived(