Skip to content

Commit

Permalink
feat(plugin): allow to dispose of a HAR file (#135)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
derevnjuk authored Jan 10, 2023
1 parent 93cb981 commit 7bd99f2
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 26 deletions.
52 changes: 38 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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:
Expand All @@ -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();
}
});
});
```

Expand Down
124 changes: 124 additions & 0 deletions src/Plugin.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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();
});
});
});
14 changes: 4 additions & 10 deletions src/Plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,6 @@ export class Plugin {
public async saveHar(options: SaveOptions): Promise<void> {
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.`);

Expand All @@ -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<void> {
public async disposeOfHar(): Promise<void> {
await this.networkObservable?.unsubscribe();
delete this.networkObservable;

Expand All @@ -120,6 +118,8 @@ export class Plugin {
}

delete this.buffer;

return null;
}

private async buildHar(): Promise<string | undefined> {
Expand Down Expand Up @@ -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);
}
Expand Down
5 changes: 5 additions & 0 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,8 @@ Cypress.Commands.add(
return cy.task('saveHar', options);
}
);

Cypress.Commands.add(
'disposeOfHar',
(): Cypress.Chainable => cy.task('disposeOfHar')
);
5 changes: 3 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ declare global {
namespace Cypress {
interface Chainable<Subject = any> {
saveHar(options?: Partial<SaveOptions>): Chainable<Subject>;

recordHar(options?: RecordOptions): Chainable<Subject>;
disposeOfHar(): Chainable<Subject>;
}
}
}
Expand All @@ -25,7 +25,8 @@ export const install = (on: Cypress.PluginEvents): void => {
on('task', {
saveHar: (options: SaveOptions): Promise<void> => plugin.saveHar(options),
recordHar: (options: RecordOptions): Promise<void> =>
plugin.recordHar(options)
plugin.recordHar(options),
disposeOfHar: (): Promise<void> => plugin.disposeOfHar()
});

on(
Expand Down
1 change: 1 addition & 0 deletions src/network/NetworkObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export class NetworkObserver implements Observer<NetworkRequest> {
await Promise.all([this.security?.disable(), this.network?.disable()]);
delete this.destination;
this._entries.clear();
this._extraInfoBuilders.clear();
}

public signedExchangeReceived(
Expand Down

0 comments on commit 7bd99f2

Please sign in to comment.