From 278698a8c5f8ee9693195bcf791cba2d8e652873 Mon Sep 17 00:00:00 2001 From: Artem Derevnjuk Date: Mon, 23 Jan 2023 00:28:44 +0400 Subject: [PATCH] refactor(plugin): extract loader and exporter to separate classes closes #163 --- src/Plugin.spec.ts | 135 ++++++++---------- src/Plugin.ts | 103 +++---------- src/index.ts | 5 +- src/network/DefaultHarExporter.spec.ts | 129 +++++++++++++++++ src/network/DefaultHarExporter.ts | 48 +++++++ src/network/DefaultHarExporterFactory.spec.ts | 82 +++++++++++ src/network/DefaultHarExporterFactory.ts | 31 ++++ src/network/HarExporter.ts | 9 ++ src/network/HarExporterFactory.ts | 10 ++ src/network/index.ts | 19 ++- src/utils/Loader.spec.ts | 41 ++++++ src/utils/Loader.ts | 17 +++ 12 files changed, 468 insertions(+), 161 deletions(-) create mode 100644 src/network/DefaultHarExporter.spec.ts create mode 100644 src/network/DefaultHarExporter.ts create mode 100644 src/network/DefaultHarExporterFactory.spec.ts create mode 100644 src/network/DefaultHarExporterFactory.ts create mode 100644 src/network/HarExporter.ts create mode 100644 src/network/HarExporterFactory.ts create mode 100644 src/utils/Loader.spec.ts create mode 100644 src/utils/Loader.ts diff --git a/src/Plugin.spec.ts b/src/Plugin.spec.ts index 15d02d2..6c515b8 100644 --- a/src/Plugin.spec.ts +++ b/src/Plugin.spec.ts @@ -1,10 +1,15 @@ +import type { RecordOptions, SaveOptions } from './Plugin'; import { Plugin } from './Plugin'; import { Logger } from './utils/Logger'; import { FileManager } from './utils/FileManager'; +import type { + Observer, + ObserverFactory, + HarExporter, + HarExporterFactory +} from './network'; import { NetworkRequest } from './network'; -import type { RecordOptions, SaveOptions } from './Plugin'; import type { Connection, ConnectionFactory } from './cdp'; -import type { Observer, ObserverFactory } from './network'; import { anyFunction, anyString, @@ -20,8 +25,7 @@ import { } from 'ts-mockito'; import { beforeEach, describe, expect, it, jest } from '@jest/globals'; import type { Entry } from 'har-format'; -import { WriteStream } from 'fs'; -import { EOL, tmpdir } from 'os'; +import { tmpdir } from 'os'; const resolvableInstance = (m: T): T => new Proxy(instance(m), { @@ -110,8 +114,9 @@ describe('Plugin', () => { const observerFactoryMock = mock(); const networkObserverMock = mock>(); const connectionFactoryMock = mock(); + const harExporterFactoryMock = mock(); + const harExporterMock = mock(); const connectionMock = mock(); - const writableStreamMock = mock(); const processEnv = process.env; let plugin!: Plugin; @@ -131,7 +136,8 @@ describe('Plugin', () => { instance(loggerMock), instance(fileManagerMock), instance(connectionFactoryMock), - instance(observerFactoryMock) + instance(observerFactoryMock), + instance(harExporterFactoryMock) ); }); @@ -142,10 +148,11 @@ describe('Plugin', () => { | Logger | FileManager | Connection - | WriteStream | ConnectionFactory | ObserverFactory | Observer + | HarExporter + | HarExporterFactory >( processSpy, loggerMock, @@ -154,7 +161,8 @@ describe('Plugin', () => { connectionFactoryMock, observerFactoryMock, networkObserverMock, - writableStreamMock + harExporterFactoryMock, + harExporterMock ); }); @@ -227,13 +235,6 @@ describe('Plugin', () => { outDir: tmpdir() } as SaveOptions; - it(`should return undefined to satisfy Cypress's contract when connection is not established yet`, async () => { - // act - const result = await plugin.saveHar(options); - // assert - expect(result).toBeUndefined(); - }); - it('should log an error message when the connection is corrupted', async () => { // act await plugin.saveHar(options); @@ -271,12 +272,10 @@ describe('Plugin', () => { observerFactoryMock.createNetworkObserver(anything(), anything()) ).thenReturn(instance(networkObserverMock)); when(networkObserverMock.empty).thenReturn(false, false, true); - when(fileManagerMock.createTmpWriteStream()).thenResolve( - resolvableInstance(writableStreamMock) + when(harExporterFactoryMock.create(anything())).thenResolve( + resolvableInstance(harExporterMock) ); - // @ts-expect-error type mismatch - when(writableStreamMock.closed).thenReturn(true); - when(writableStreamMock.path).thenReturn('temp-file.txt'); + when(harExporterMock.path).thenReturn('temp-file.txt'); plugin.ensureBrowserFlags(chrome, []); await plugin.recordHar({ rootDir: '/' @@ -302,12 +301,10 @@ describe('Plugin', () => { when( observerFactoryMock.createNetworkObserver(anything(), anything()) ).thenReturn(instance(networkObserverMock)); - when(fileManagerMock.createTmpWriteStream()).thenResolve( - resolvableInstance(writableStreamMock) + when(harExporterFactoryMock.create(anything())).thenResolve( + resolvableInstance(harExporterMock) ); - // @ts-expect-error type mismatch - when(writableStreamMock.closed).thenReturn(true); - when(writableStreamMock.path).thenReturn('temp-file.txt'); + when(harExporterMock.path).thenReturn('temp-file.txt'); plugin.ensureBrowserFlags(chrome, []); await plugin.recordHar({ rootDir: '/' @@ -335,12 +332,10 @@ describe('Plugin', () => { when( observerFactoryMock.createNetworkObserver(anything(), anything()) ).thenReturn(instance(networkObserverMock)); - when(fileManagerMock.createTmpWriteStream()).thenResolve( - resolvableInstance(writableStreamMock) + when(harExporterFactoryMock.create(anything())).thenResolve( + resolvableInstance(harExporterMock) ); - // @ts-expect-error type mismatch - when(writableStreamMock.closed).thenReturn(true); - when(writableStreamMock.path).thenReturn('temp-file.txt'); + when(harExporterMock.path).thenReturn('temp-file.txt'); plugin.ensureBrowserFlags(chrome, []); await plugin.recordHar({ rootDir: '/' @@ -378,7 +373,7 @@ describe('Plugin', () => { verify(networkObserverMock.unsubscribe()).once(); }); - it('should dispose a stream', async () => { + it('should dispose a exporter', async () => { // arrange when(connectionFactoryMock.create(anything())).thenReturn( instance(connectionMock) @@ -386,13 +381,11 @@ describe('Plugin', () => { when( observerFactoryMock.createNetworkObserver(anything(), anything()) ).thenReturn(instance(networkObserverMock)); - when(fileManagerMock.createTmpWriteStream()).thenResolve( - resolvableInstance(writableStreamMock) + when(harExporterFactoryMock.create(anything())).thenResolve( + resolvableInstance(harExporterMock) ); const tempFilePath = 'temp-file.txt'; - // @ts-expect-error type mismatch - when(writableStreamMock.closed).thenReturn(true); - when(writableStreamMock.path).thenReturn(tempFilePath); + when(harExporterMock.path).thenReturn(tempFilePath); plugin.ensureBrowserFlags(chrome, []); await plugin.recordHar({ rootDir: '/' @@ -404,7 +397,7 @@ describe('Plugin', () => { // act await plugin.saveHar(options); // assert - verify(writableStreamMock.end()).once(); + verify(harExporterMock.end()).once(); verify(fileManagerMock.removeFile(tempFilePath)).once(); }); }); @@ -442,6 +435,15 @@ describe('Plugin', () => { verify(networkObserverMock.subscribe(anyFunction())).once(); }); + it('should throw an error when the addr is not defined', async () => { + // act + const act = () => plugin.recordHar(options); + // assert + await expect(act).rejects.toThrow( + "Please call the 'ensureBrowserFlags' before attempting to start the recording." + ); + }); + it('should close connection when it is already opened', async () => { // arrange plugin.ensureBrowserFlags(chrome, []); @@ -453,52 +455,43 @@ describe('Plugin', () => { verify(connectionMock.open()).twice(); }); - it('should write an entry to a stream', async () => { + it('should pass an entry to a exporter', async () => { // arrange - when(fileManagerMock.createTmpWriteStream()).thenResolve( - resolvableInstance(writableStreamMock) + const request = new NetworkRequest( + '1', + 'https://example.com', + 'https://example.com', + '1' + ); + when(harExporterFactoryMock.create(anything())).thenResolve( + resolvableInstance(harExporterMock) ); - // @ts-expect-error type mismatch - when(writableStreamMock.closed).thenReturn(false); when(networkObserverMock.subscribe(anyFunction())).thenCall(callback => - callback( - new NetworkRequest( - '1', - 'https://example.com', - 'https://example.com', - '1' - ) - ) + callback(request) ); plugin.ensureBrowserFlags(chrome, []); // act await plugin.recordHar(options); // assert - verify(writableStreamMock.write(match(`${EOL}`))).once(); + verify(harExporterMock.write(request)).once(); }); - it('should do nothing when a stream is closed', async () => { + it('should do nothing when a exporter is not defined', async () => { // arrange - when(fileManagerMock.createTmpWriteStream()).thenResolve( - resolvableInstance(writableStreamMock) + const request = new NetworkRequest( + '1', + 'https://example.com', + 'https://example.com', + '1' ); - // @ts-expect-error type mismatch - when(writableStreamMock.closed).thenReturn(true); when(networkObserverMock.subscribe(anyFunction())).thenCall(callback => - callback( - new NetworkRequest( - '1', - 'https://example.com', - 'https://example.com', - '1' - ) - ) + callback(request) ); plugin.ensureBrowserFlags(chrome, []); // act await plugin.recordHar(options); // assert - verify(writableStreamMock.write(match(`${EOL}`))).never(); + verify(harExporterMock.write(request)).never(); }); }); @@ -510,15 +503,13 @@ describe('Plugin', () => { when( observerFactoryMock.createNetworkObserver(anything(), anything()) ).thenReturn(instance(networkObserverMock)); - when(fileManagerMock.createTmpWriteStream()).thenResolve( - resolvableInstance(writableStreamMock) + when(harExporterFactoryMock.create(anything())).thenResolve( + resolvableInstance(harExporterMock) ); - // @ts-expect-error type mismatch - when(writableStreamMock.closed).thenReturn(true); - when(writableStreamMock.path).thenReturn('temp-file.txt'); + when(harExporterMock.path).thenReturn('temp-file.txt'); }); - it('should dispose of a stream', async () => { + it('should dispose of a exporter', async () => { // arrange plugin.ensureBrowserFlags(chrome, []); await plugin.recordHar({ @@ -527,7 +518,7 @@ describe('Plugin', () => { // act await plugin.disposeOfHar(); // assert - verify(writableStreamMock.end()).once(); + verify(harExporterMock.end()).once(); }); it('should unsubscribe from the network events', async () => { diff --git a/src/Plugin.ts b/src/Plugin.ts index 33aeeee..487b1a1 100644 --- a/src/Plugin.ts +++ b/src/Plugin.ts @@ -1,21 +1,16 @@ import { Logger } from './utils/Logger'; import { FileManager } from './utils/FileManager'; -import { - EntryBuilder, - HarBuilder, - NetworkIdleMonitor, - NetworkRequest -} from './network'; -import { ErrorUtils } from './utils/ErrorUtils'; -import type { Connection, ConnectionFactory } from './cdp'; import type { + HarExporter, + HarExporterFactory, NetworkObserverOptions, Observer, ObserverFactory } from './network'; -import type { Entry } from 'har-format'; -import { join, resolve } from 'path'; -import { WriteStream } from 'fs'; +import { HarBuilder, NetworkIdleMonitor, NetworkRequest } from './network'; +import { ErrorUtils } from './utils/ErrorUtils'; +import type { Connection, ConnectionFactory } from './cdp'; +import { join } from 'path'; import { EOL } from 'os'; import { promisify } from 'util'; @@ -36,18 +31,7 @@ interface Addr { } export class Plugin { - private buffer?: WriteStream; - - private get tmpPath() { - if (this.buffer) { - const { path } = this.buffer; - - return Buffer.isBuffer(path) ? path.toString('utf-8') : path; - } - - return undefined; - } - + private exporter?: HarExporter; private networkObservable?: Observer; private addr?: Addr; private _connection?: Connection; @@ -58,7 +42,8 @@ export class Plugin { private readonly logger: Logger, private readonly fileManager: FileManager, private readonly connectionFactory: ConnectionFactory, - private readonly observerFactory: ObserverFactory + private readonly observerFactory: ObserverFactory, + private readonly exporterFactory: HarExporterFactory ) {} public ensureBrowserFlags( @@ -93,6 +78,10 @@ export class Plugin { ); } + this.exporter = await this.exporterFactory.create({ + predicatePath: options.filter, + rootDir: options.rootDir + }); this._connection = this.connectionFactory.create({ ...this.addr, maxRetries: 20, @@ -140,15 +129,11 @@ export class Plugin { await this.networkObservable?.unsubscribe(); delete this.networkObservable; - if (this.buffer) { - this.buffer.end(); + if (this.exporter) { + this.exporter.end(); + await this.fileManager.removeFile(this.exporter.path); + delete this.exporter; } - - if (this.tmpPath) { - await this.fileManager.removeFile(this.tmpPath); - } - - delete this.buffer; } private parseElectronSwitches(browser: Cypress.Browser): string[] { @@ -171,8 +156,8 @@ Please refer to the documentation: } private async buildHar(): Promise { - if (this.tmpPath) { - const content = await this.fileManager.readFile(this.tmpPath); + if (this.exporter) { + const content = await this.fileManager.readFile(this.exporter.path); if (content) { const entries = content @@ -203,8 +188,6 @@ Please refer to the documentation: } private async listenNetworkEvents(options: RecordOptions): Promise { - this.buffer = await this.fileManager.createTmpWriteStream(); - const network = this._connection?.discoverNetwork(); this.networkObservable = this.observerFactory.createNetworkObserver( @@ -213,51 +196,9 @@ Please refer to the documentation: options ); - let filter: ((request: Entry) => unknown) | undefined; - - if (options.filter) { - const modulePath = resolve(options.rootDir, options.filter); - const module = this.interopRequireDefault( - // eslint-disable-next-line @typescript-eslint/no-var-requires -- `ts-node` does not handle the dynamic imports like `import(modulePath)` - require(/* webpackIgnore: true */ modulePath) - ); - filter = module?.default; - } - - return this.networkObservable.subscribe(async (request: NetworkRequest) => { - // TODO: extract this logic to a separate class - const entry = await new EntryBuilder(request).build(); - - if (await this.applyPredicate(filter, entry)) { - return; - } - - const entryStr = JSON.stringify(entry); - - // @ts-expect-error type mismatch - if (this.buffer && !this.buffer.closed) { - this.buffer.write(`${entryStr}${EOL}`); - } - }); - } - - // TODO: extract to the util/helper class - private interopRequireDefault(obj: unknown): { - default: (request: Entry) => unknown; - } { - // @ts-expect-error unknown is not assignable to the module type - return (obj as any)?.__esModule ? obj : { default: obj }; - } - - private async applyPredicate( - predicate: ((request: Entry) => unknown) | undefined, - entry: Entry - ) { - try { - return typeof predicate === 'function' && (await predicate?.(entry)); - } catch { - return false; - } + return this.networkObservable.subscribe((request: NetworkRequest) => + this.exporter?.write(request) + ); } private async closeConnection(): Promise { diff --git a/src/index.ts b/src/index.ts index d7d6500..60c5d18 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,7 @@ import { Plugin } from './Plugin'; import { Logger } from './utils/Logger'; import { FileManager } from './utils/FileManager'; import { DefaultConnectionFactory } from './cdp'; -import { DefaultObserverFactory } from './network'; +import { DefaultHarExporterFactory, DefaultObserverFactory } from './network'; import { StringUtils } from './utils/StringUtils'; import type { RecordOptions, SaveOptions } from './Plugin'; @@ -21,7 +21,8 @@ const plugin = new Plugin( Logger.Instance, FileManager.Instance, new DefaultConnectionFactory(Logger.Instance), - new DefaultObserverFactory(Logger.Instance) + new DefaultObserverFactory(Logger.Instance), + new DefaultHarExporterFactory(FileManager.Instance) ); export const install = (on: Cypress.PluginEvents): void => { diff --git a/src/network/DefaultHarExporter.spec.ts b/src/network/DefaultHarExporter.spec.ts new file mode 100644 index 0000000..af8c9b4 --- /dev/null +++ b/src/network/DefaultHarExporter.spec.ts @@ -0,0 +1,129 @@ +import { NetworkRequest } from './NetworkRequest'; +import { DefaultHarExporter } from './DefaultHarExporter'; +import { + anyString, + instance, + match, + mock, + reset, + verify, + when +} from 'ts-mockito'; +import type { Entry } from 'har-format'; +import { beforeEach, describe, it, jest, expect } from '@jest/globals'; +import type { WriteStream } from 'fs'; +import { EOL } from 'os'; + +describe('DefaultHarExporter', () => { + const buffer = mock(); + const networkRequest = new NetworkRequest( + '1', + 'https://example.com', + 'https://example.com', + '1' + ); + const predicate: jest.Mock<(entry: Entry) => Promise | unknown> = + jest.fn<(entry: Entry) => Promise | unknown>(); + let harExporter!: DefaultHarExporter; + + beforeEach(() => { + harExporter = new DefaultHarExporter(instance(buffer), predicate); + }); + + afterEach(() => { + predicate.mockRestore(); + reset(buffer); + }); + + describe('path', () => { + it('should return the path serializing buffer', () => { + // arrange + const expected = '/path/file'; + when(buffer.path).thenReturn(Buffer.from(expected)); + + // act + const result = harExporter.path; + + // assert + expect(result).toBe(expected); + }); + + it('should return the path', () => { + // arrange + const expected = '/path/file'; + when(buffer.path).thenReturn(expected); + + // act + const result = harExporter.path; + + // assert + expect(result).toBe(expected); + }); + }); + + describe('end', () => { + it('should close the stream', () => { + // act + harExporter.end(); + + // assert + verify(buffer.end()).once(); + }); + }); + + describe('write', () => { + it('should write the entry to the buffer', async () => { + // arrange + // @ts-expect-error type mismatch + when(buffer.closed).thenReturn(false); + predicate.mockReturnValue(false); + + // act + await harExporter.write(networkRequest); + + // assert + verify(buffer.write(match(`${EOL}`))).once(); + }); + + it('should write the entry to the buffer if the predicate returns throws an error', async () => { + // arrange + // @ts-expect-error type mismatch + when(buffer.closed).thenReturn(false); + predicate.mockReturnValue( + Promise.reject(new Error('something went wrong')) + ); + + // act + await harExporter.write(networkRequest); + + // assert + verify(buffer.write(match(`${EOL}`))).once(); + }); + + it('should not write the entry to the buffer if the predicate returns true', async () => { + // arrange + // @ts-expect-error type mismatch + when(buffer.closed).thenReturn(false); + predicate.mockReturnValue(true); + + // act + await harExporter.write(networkRequest); + + // assert + verify(buffer.write(anyString())).never(); + }); + + it('should not write the entry to the buffer if the buffer is closed', async () => { + // arrange + // @ts-expect-error type mismatch + when(buffer.closed).thenReturn(true); + predicate.mockReturnValue(false); + + // act + await harExporter.write(networkRequest); + + // assert + verify(buffer.write(anyString())).never(); + }); + }); +}); diff --git a/src/network/DefaultHarExporter.ts b/src/network/DefaultHarExporter.ts new file mode 100644 index 0000000..7293977 --- /dev/null +++ b/src/network/DefaultHarExporter.ts @@ -0,0 +1,48 @@ +import { EntryBuilder } from './EntryBuilder'; +import type { NetworkRequest } from './NetworkRequest'; +import type { HarExporter } from './HarExporter'; +import type { Entry } from 'har-format'; +import type { WriteStream } from 'fs'; +import { EOL } from 'os'; + +export class DefaultHarExporter implements HarExporter { + get path(): string { + const { path } = this.buffer; + + return Buffer.isBuffer(path) ? path.toString('utf-8') : path; + } + + constructor( + private readonly buffer: WriteStream, + private readonly predicate?: (entry: Entry) => Promise | unknown + ) {} + + public async write(networkRequest: NetworkRequest): Promise { + const entry = await new EntryBuilder(networkRequest).build(); + + if (await this.applyPredicate(entry)) { + return; + } + + const json = JSON.stringify(entry); + + // @ts-expect-error type mismatch + if (!this.buffer.closed) { + this.buffer.write(`${json}${EOL}`); + } + } + + public end(): void { + this.buffer.end(); + } + + private async applyPredicate(entry: Entry) { + try { + return ( + typeof this.predicate === 'function' && (await this.predicate?.(entry)) + ); + } catch { + return false; + } + } +} diff --git a/src/network/DefaultHarExporterFactory.spec.ts b/src/network/DefaultHarExporterFactory.spec.ts new file mode 100644 index 0000000..1e87c0e --- /dev/null +++ b/src/network/DefaultHarExporterFactory.spec.ts @@ -0,0 +1,82 @@ +import type { FileManager } from '../utils/FileManager'; +import { DefaultHarExporterFactory } from './DefaultHarExporterFactory'; +import { DefaultHarExporter } from './DefaultHarExporter'; +import { Loader } from '../utils/Loader'; +import { instance, mock, reset, spy, verify, when } from 'ts-mockito'; +import { + jest, + describe, + it, + expect, + beforeEach, + afterEach +} from '@jest/globals'; +import type { WriteStream } from 'fs'; + +const resolvableInstance = (m: T): T => + new Proxy(instance(m), { + get(target, prop, receiver) { + if ( + ['Symbol(Symbol.toPrimitive)', 'then', 'catch'].includes( + prop.toString() + ) + ) { + return undefined; + } + + return Reflect.get(target, prop, receiver); + } + }); + +describe('DefaultHarExporterFactory', () => { + const fileManagerMock = mock(); + const writeStreamMock = mock(); + const loaderSpy = spy(Loader); + const predicate = jest.fn(); + + let factory!: DefaultHarExporterFactory; + + beforeEach(() => { + factory = new DefaultHarExporterFactory(instance(fileManagerMock)); + }); + + afterEach(() => + reset( + fileManagerMock, + writeStreamMock, + loaderSpy + ) + ); + + describe('create', () => { + it('should create a HarExporter instance', async () => { + // arrange + const options = { rootDir: '/root', predicatePath: 'predicate.js' }; + when(fileManagerMock.createTmpWriteStream()).thenResolve( + resolvableInstance(writeStreamMock) + ); + when(loaderSpy.load('/root/predicate.js')).thenReturn(predicate); + + // act + const result = await factory.create(options); + + // assert + expect(result).toBeInstanceOf(DefaultHarExporter); + verify(loaderSpy.load('/root/predicate.js')).once(); + }); + + it('should create a HarExporter instance without predicate', async () => { + // arrange + const options = { rootDir: '/root' }; + when(fileManagerMock.createTmpWriteStream()).thenResolve( + resolvableInstance(writeStreamMock) + ); + + // act + const result = await factory.create(options); + + // assert + expect(result).toBeInstanceOf(DefaultHarExporter); + }); + }); +}); diff --git a/src/network/DefaultHarExporterFactory.ts b/src/network/DefaultHarExporterFactory.ts new file mode 100644 index 0000000..4af60cf --- /dev/null +++ b/src/network/DefaultHarExporterFactory.ts @@ -0,0 +1,31 @@ +import type { + HarExporterFactory, + HarExporterOptions +} from './HarExporterFactory'; +import type { HarExporter } from './HarExporter'; +import { DefaultHarExporter } from './DefaultHarExporter'; +import { Loader } from '../utils/Loader'; +import { FileManager } from '../utils/FileManager'; +import type { Entry } from 'har-format'; +import { resolve } from 'path'; + +export class DefaultHarExporterFactory implements HarExporterFactory { + constructor(private readonly fileManager: FileManager) {} + + public async create({ + rootDir, + predicatePath + }: HarExporterOptions): Promise { + let predicate: ((request: Entry) => unknown) | undefined; + + if (predicatePath) { + const absolutePath = resolve(rootDir, predicatePath); + predicate = Loader.load(absolutePath); + } + + return new DefaultHarExporter( + await this.fileManager.createTmpWriteStream(), + predicate + ); + } +} diff --git a/src/network/HarExporter.ts b/src/network/HarExporter.ts new file mode 100644 index 0000000..689b455 --- /dev/null +++ b/src/network/HarExporter.ts @@ -0,0 +1,9 @@ +import type { NetworkRequest } from './NetworkRequest'; + +export interface HarExporter { + readonly path: string; + + write(networkRequest: NetworkRequest): Promise; + + end(): void; +} diff --git a/src/network/HarExporterFactory.ts b/src/network/HarExporterFactory.ts new file mode 100644 index 0000000..168080b --- /dev/null +++ b/src/network/HarExporterFactory.ts @@ -0,0 +1,10 @@ +import type { HarExporter } from './HarExporter'; + +export interface HarExporterOptions { + rootDir: string; + predicatePath?: string; +} + +export interface HarExporterFactory { + create(options: HarExporterOptions): Promise; +} diff --git a/src/network/index.ts b/src/network/index.ts index 5fb6653..a78e53f 100644 --- a/src/network/index.ts +++ b/src/network/index.ts @@ -1,10 +1,9 @@ -export { NetworkRequest, WebSocketFrameType } from './NetworkRequest'; -export { DefaultObserverFactory } from './DefaultObserverFactory'; -export { EntryBuilder } from './EntryBuilder'; -export { HarBuilder } from './HarBuilder'; -export { NetworkIdleMonitor } from './NetworkIdleMonitor'; -export { NetworkObserver } from './NetworkObserver'; export type { ContentData } from './NetworkRequest'; +export type { HarExporter } from './HarExporter'; +export type { + HarExporterFactory, + HarExporterOptions +} from './HarExporterFactory'; export type { Network, NetworkEvents, @@ -14,3 +13,11 @@ export type { export type { NetworkObserverOptions } from './NetworkObserverOptions'; export type { Observer } from './Observer'; export type { ObserverFactory } from './ObserverFactory'; +export { DefaultHarExporter } from './DefaultHarExporter'; +export { DefaultHarExporterFactory } from './DefaultHarExporterFactory'; +export { DefaultObserverFactory } from './DefaultObserverFactory'; +export { EntryBuilder } from './EntryBuilder'; +export { HarBuilder } from './HarBuilder'; +export { NetworkIdleMonitor } from './NetworkIdleMonitor'; +export { NetworkObserver } from './NetworkObserver'; +export { NetworkRequest, WebSocketFrameType } from './NetworkRequest'; diff --git a/src/utils/Loader.spec.ts b/src/utils/Loader.spec.ts new file mode 100644 index 0000000..3205311 --- /dev/null +++ b/src/utils/Loader.spec.ts @@ -0,0 +1,41 @@ +import { Loader } from './Loader'; +import { jest, describe, it, expect, afterEach } from '@jest/globals'; + +describe('Loader', () => { + const modulePath = './exampleModule'; + const module = 'example export'; + + afterEach(() => { + jest.resetModules(); + jest.resetAllMocks(); + }); + + describe('load', () => { + it('should load the module from the given path using the default export', () => { + // arrange + jest.mock( + modulePath, + // eslint-disable-next-line @typescript-eslint/naming-convention + () => ({ __esModule: true, default: module }), + { virtual: true } + ); + + // act + const result = Loader.load(modulePath); + + // assert + expect(result).toBe(module); + }); + + it('should load the module from the given path using the default module.exports', () => { + // arrange + jest.mock(modulePath, () => module, { virtual: true }); + + // act + const result = Loader.load(modulePath); + + // assert + expect(result).toBe(module); + }); + }); +}); diff --git a/src/utils/Loader.ts b/src/utils/Loader.ts new file mode 100644 index 0000000..2ec6f70 --- /dev/null +++ b/src/utils/Loader.ts @@ -0,0 +1,17 @@ +export class Loader { + public static load(path: string): T | undefined { + const module = this.interopRequireDefault( + // eslint-disable-next-line @typescript-eslint/no-var-requires -- `ts-node` does not handle the dynamic imports like `import(path)` + require(/* webpackIgnore: true */ path) + ); + + return module?.default as T | undefined; + } + + private static interopRequireDefault(obj: unknown): { + default: unknown; + } { + // @ts-expect-error unknown is not assignable to the module type + return obj?.__esModule ? obj : { default: obj }; + } +}