diff --git a/__tests__/plugins/plugin-manager.test.ts b/__tests__/plugins/plugin-manager.test.ts index 2d68363d..ec69e4e7 100644 --- a/__tests__/plugins/plugin-manager.test.ts +++ b/__tests__/plugins/plugin-manager.test.ts @@ -29,7 +29,7 @@ import { PluginManager } from '../../src/plugins'; jest.mock('../../src/plugins/network'); describe('plugin manager', () => { - const pluginManager = new PluginManager({} as any); + const pluginManager = new PluginManager({ context: {} } as any); it('register plugin by type', async () => { await pluginManager.register('network', {}); diff --git a/src/common_types.ts b/src/common_types.ts index 476ccb8e..86dbc03e 100644 --- a/src/common_types.ts +++ b/src/common_types.ts @@ -64,6 +64,10 @@ export type NetworkConditions = { latency: number; }; +export type APIDriver = { + request: APIRequestContext; +}; + export type Driver = { browser: ChromiumBrowser; context: ChromiumBrowserContext; @@ -252,6 +256,13 @@ export type RunOptions = BaseArgs & { grepOpts?: GrepOptions; }; +export type APIRunOptions = BaseArgs & { + network?: boolean; + environment?: string; + reporter?: BuiltInReporterName | ReporterInstance; + grepOpts?: GrepOptions; +}; + export type PushOptions = Partial & Partial & { auth: string; @@ -285,6 +296,12 @@ export type SyntheticsConfig = { project?: ProjectSettings; }; +/** Runner Payload types */ +export type APIJourneyResult = Partial & { + networkinfo?: PluginOutput['networkinfo']; + stepsresults?: Array; +}; + /** Runner Payload types */ export type JourneyResult = Partial & { networkinfo?: PluginOutput['networkinfo']; @@ -324,4 +341,10 @@ export type JourneyEndResult = JourneyStartResult & options: RunOptions; }; +export type APIJourneyEndResult = JourneyStartResult & + APIJourneyResult & { + browserDelay: number; + options: RunOptions; + }; + export type StepEndResult = StepResult; diff --git a/src/core/gatherer.ts b/src/core/gatherer.ts index 1b12f96f..7166b3bf 100644 --- a/src/core/gatherer.ts +++ b/src/core/gatherer.ts @@ -32,7 +32,12 @@ import { } from 'playwright-core'; import { PluginManager } from '../plugins'; import { log } from './logger'; -import { Driver, NetworkConditions, RunOptions } from '../common_types'; +import { + APIDriver, + Driver, + NetworkConditions, + RunOptions, +} from '../common_types'; // Default timeout for Playwright actions and Navigations const DEFAULT_TIMEOUT = 50000; @@ -71,32 +76,48 @@ export class Gatherer { }); } - static async setupDriver(options: RunOptions): Promise { - await Gatherer.launchBrowser(options); + static async setupDriver( + options: RunOptions, + journeyType: T = 'browser' as T + ): Promise { const { playwrightOptions } = options; - const context = await Gatherer.browser.newContext({ - ...playwrightOptions, - userAgent: await Gatherer.getUserAgent(playwrightOptions?.userAgent), - }); - // Set timeouts for actions and navigations - context.setDefaultTimeout( - playwrightOptions?.actionTimeout ?? DEFAULT_TIMEOUT - ); - context.setDefaultNavigationTimeout( - playwrightOptions?.navigationTimeout ?? DEFAULT_TIMEOUT - ); - - // TODO: Network throttling via chrome devtools emulation is disabled for now. - // See docs/throttling.md for more details. - // Gatherer.setNetworkConditions(context, networkConditions); - if (playwrightOptions?.testIdAttribute) { - selectors.setTestIdAttribute(playwrightOptions.testIdAttribute); - } - const page = await context.newPage(); - const client = await context.newCDPSession(page); - const request = await apiRequest.newContext({ ...playwrightOptions }); - return { browser: Gatherer.browser, context, page, client, request }; + if (journeyType === 'browser') { + await Gatherer.launchBrowser(options); + + const context = await Gatherer.browser.newContext({ + ...playwrightOptions, + userAgent: await Gatherer.getUserAgent(playwrightOptions?.userAgent), + }); + // Set timeouts for actions and navigations + context.setDefaultTimeout( + playwrightOptions?.actionTimeout ?? DEFAULT_TIMEOUT + ); + context.setDefaultNavigationTimeout( + playwrightOptions?.navigationTimeout ?? DEFAULT_TIMEOUT + ); + + // TODO: Network throttling via chrome devtools emulation is disabled for now. + // See docs/throttling.md for more details. + // Gatherer.setNetworkConditions(context, networkConditions); + if (playwrightOptions?.testIdAttribute) { + selectors.setTestIdAttribute(playwrightOptions.testIdAttribute); + } + + const page = await context.newPage(); + const client = await context.newCDPSession(page); + const request = await apiRequest.newContext({ ...playwrightOptions }); + return { + browser: Gatherer.browser, + context, + page, + client, + request, + } as Driver; + } else { + const request = await apiRequest.newContext({ ...playwrightOptions }); + return { request } as T extends 'browser' ? Driver : APIDriver; + } } static async getUserAgent(userAgent?: string) { @@ -136,14 +157,20 @@ export class Gatherer { * Starts recording all events related to the v8 devtools protocol * https://chromedevtools.github.io/devtools-protocol/v8/ */ - static async beginRecording(driver: Driver, options: RunOptions) { + static async beginRecording(driver: Driver | APIDriver, options: RunOptions) { log('Gatherer: started recording'); const { network, metrics } = options; Gatherer.pluginManager = new PluginManager(driver); Gatherer.pluginManager.registerAll(options); - const plugins = [await Gatherer.pluginManager.start('browserconsole')]; - network && plugins.push(await Gatherer.pluginManager.start('network')); - metrics && plugins.push(await Gatherer.pluginManager.start('performance')); + const plugins = []; + if ('browser' in driver) { + plugins.push(await Gatherer.pluginManager.start('browserconsole')); + network && plugins.push(await Gatherer.pluginManager.start('network')); + metrics && + plugins.push(await Gatherer.pluginManager.start('performance')); + } else { + plugins.push(await Gatherer.pluginManager.start('network')); + } await Promise.all(plugins); return Gatherer.pluginManager; } @@ -153,10 +180,12 @@ export class Gatherer { await Gatherer.pluginManager.unregisterAll(); } - static async dispose(driver: Driver) { + static async dispose(driver: Driver | APIDriver) { log(`Gatherer: closing all contexts`); await driver.request.dispose(); - await driver.context.close(); + if ('context' in driver) { + await driver.context.close(); + } } static async stop() { diff --git a/src/core/globals.ts b/src/core/globals.ts index 77f80b6d..0c823f86 100644 --- a/src/core/globals.ts +++ b/src/core/globals.ts @@ -26,7 +26,7 @@ import Runner from './runner'; /** - * Use a gloabl Runner which would be accessed by the runtime and + * Use a global Runner which would be accessed by the runtime and * required to handle the local vs global invocation through CLI */ const SYNTHETICS_RUNNER = Symbol.for('SYNTHETICS_RUNNER'); diff --git a/src/core/index.ts b/src/core/index.ts index 2dafa5ce..26f1b1ad 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -24,6 +24,10 @@ */ import { + APIJourney, + APIJourneyCallback, + APIJourneyOptions, + APIJourneyWithAnnotations, Journey, JourneyCallback, JourneyOptions, @@ -60,6 +64,28 @@ export const journey = createJourney() as JourneyWithAnnotations; journey.skip = createJourney('skip'); journey.only = createJourney('only'); +const createAPIJourney = (type?: 'skip' | 'only') => + wrapFnWithLocation( + ( + location: Location, + options: APIJourneyOptions | string, + callback: APIJourneyCallback + ) => { + log(`API Journey register: ${JSON.stringify(options)}`); + if (typeof options === 'string') { + options = { name: options, id: options }; + } + const j = new APIJourney({ ...options, type: 'api' }, callback, location); + if (type) { + j[type] = true; + } + runner._addJourney(j); + return j; + } + ); + +export const apiJourney = createAPIJourney() as APIJourneyWithAnnotations; + const createStep = (type?: 'skip' | 'soft' | 'only') => wrapFnWithLocation( (location: Location, name: string, callback: VoidCallback) => { diff --git a/src/core/runner.ts b/src/core/runner.ts index d6b2273c..721c66ea 100644 --- a/src/core/runner.ts +++ b/src/core/runner.ts @@ -25,7 +25,7 @@ import { join } from 'path'; import { mkdir, rm, writeFile } from 'fs/promises'; -import { Journey } from '../dsl/journey'; +import { Journey, JourneyCallbackOpts } from '../dsl/journey'; import { Step } from '../dsl/step'; import { reporters, Reporter } from '../reporters'; import { @@ -44,6 +44,7 @@ import { JourneyResult, StepResult, PushOptions, + APIDriver, } from '../common_types'; import { PerformanceManager, filterBrowserMessages } from '../plugins'; import { Gatherer } from './gatherer'; @@ -65,7 +66,7 @@ export interface RunnerInfo { */ readonly currentJourney: Journey | undefined; /** - * All registerd journeys + * All registered journeys */ readonly journeys: Journey[]; } @@ -77,7 +78,7 @@ export default class Runner implements RunnerInfo { #journeys: Journey[] = []; #hooks: SuiteHooks = { beforeAll: [], afterAll: [] }; #screenshotPath = join(CACHE_PATH, 'screenshots'); - #driver?: Driver; + #driver?: Driver | APIDriver; #browserDelay = -1; #hookError: Error | undefined; #monitor?: Monitor; @@ -209,18 +210,22 @@ export default class Runner implements RunnerInfo { async #runStep(step: Step, options: RunOptions): Promise { log(`Runner: start step (${step.name})`); const { metrics, screenshots, filmstrips, trace } = options; - /** - * URL needs to be the first navigation request of any step - * Listening for request solves the case where `about:blank` would be - * reported for failed navigations - */ - const captureUrl = req => { - if (!step.url && req.isNavigationRequest()) { - step.url = req.url(); - } - this.#driver.context.off('request', captureUrl); - }; - this.#driver.context.on('request', captureUrl); + + if ('context' in this.#driver) { + /** + * URL needs to be the first navigation request of any step + * Listening for request solves the case where `about:blank` would be + * reported for failed navigations + */ + const captureUrl = req => { + if (!step.url && req.isNavigationRequest()) { + step.url = req.url(); + } + 'context' in this.#driver && + this.#driver.context.off('request', captureUrl); + }; + this.#driver.context.on('request', captureUrl); + } const data: StepResult = {}; const traceEnabled = trace || filmstrips; @@ -239,30 +244,32 @@ export default class Runner implements RunnerInfo { step.status = 'failed'; step.error = error; } finally { - /** - * Collect all step level metrics and trace events - */ - if (metrics) { - data.pagemetrics = await ( - Gatherer.pluginManager.get('performance') as PerformanceManager - ).getMetrics(); - } - if (traceEnabled) { - const traceOutput = await Gatherer.pluginManager.stop('trace'); - Object.assign(data, traceOutput); - } - /** - * Capture screenshot for the newly created pages - * via popup or new windows/tabs - * - * Last open page will get us the correct screenshot - */ - const pages = this.#driver.context.pages(); - const page = pages[pages.length - 1]; - if (page) { - step.url ??= page.url(); - if (screenshots && screenshots !== 'off') { - await this.captureScreenshot(page, step); + if ('context' in this.#driver) { + /** + * Collect all step level metrics and trace events + */ + if (metrics) { + data.pagemetrics = await ( + Gatherer.pluginManager.get('performance') as PerformanceManager + ).getMetrics(); + } + if (traceEnabled) { + const traceOutput = await Gatherer.pluginManager.stop('trace'); + Object.assign(data, traceOutput); + } + /** + * Capture screenshot for the newly created pages + * via popup or new windows/tabs + * + * Last open page will get us the correct screenshot + */ + const pages = this.#driver.context.pages(); + const page = pages[pages.length - 1]; + if (page) { + step.url ??= page.url(); + if (screenshots && screenshots !== 'off') { + await this.captureScreenshot(page, step); + } } } } @@ -309,22 +316,24 @@ export default class Runner implements RunnerInfo { async #startJourney(journey: Journey, options: RunOptions) { journey._startTime = monotonicTimeInSeconds(); - this.#driver = await Gatherer.setupDriver(options); + this.#driver = await Gatherer.setupDriver(options, journey.type); await Gatherer.beginRecording(this.#driver, options); - /** - * For each journey we create the screenshots folder for - * caching all screenshots and clear them at end of each journey - */ - await mkdir(this.#screenshotPath, { recursive: true }); + if (journey.type === 'browser') { + /** + * For each journey we create the screenshots folder for + * caching all screenshots and clear them at end of each journey + */ + await mkdir(this.#screenshotPath, { recursive: true }); + } const params = options.params; this.#reporter?.onJourneyStart?.(journey, { timestamp: getTimestamp(), params, }); /** - * Exeucute the journey callback which registers the steps for current journey + * Execute the journey callback which registers the steps for current journey */ - journey.cb({ ...this.#driver, params, info: this }); + journey.cb({ ...this.#driver, params, info: this } as JourneyCallbackOpts); } async #endJourney( @@ -508,9 +517,9 @@ export default class Runner implements RunnerInfo { const { dryRun, grepOpts } = options; // collect all journeys with `.only` annotation and skip the rest - const onlyJournerys = this.#journeys.filter(j => j.only); - if (onlyJournerys.length > 0) { - this.#journeys = onlyJournerys; + const onlyJourneys = this.#journeys.filter(j => j.only); + if (onlyJourneys.length > 0) { + this.#journeys = onlyJourneys; } else { // filter journeys based on tags and skip annotations this.#journeys = this.#journeys.filter( @@ -570,7 +579,7 @@ export default class Runner implements RunnerInfo { this.#journeys = []; this.#active = false; /** - * Clear all cache data stored for post processing by + * Clear all cache data stored for post-processing by * the current synthetic agent run */ await rm(CACHE_PATH, { recursive: true, force: true }); diff --git a/src/dsl/api-journey.ts b/src/dsl/api-journey.ts new file mode 100644 index 00000000..f6fa58e5 --- /dev/null +++ b/src/dsl/api-journey.ts @@ -0,0 +1,72 @@ +/** + * MIT License + * + * Copyright (c) 2020-present, Elastic NV + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +import { APIRequestContext } from 'playwright-core'; +import { APIDriver, Driver, Location, Params } from '../common_types'; + +import { Journey, JourneyCallback, JourneyOptions } from './journey'; +import { RunnerInfo } from '../core/runner'; + +export type APIJourneyOptions = { + name: string; + id?: string; + tags?: string[]; +}; + +export type APIJourneyCallbackOpts = { + params: Params; + request: APIRequestContext; + info: RunnerInfo; +}; +export type APIJourneyCallback = (options: APIJourneyCallbackOpts) => void; + +type APIJourneyType = ( + options: string | APIJourneyOptions, + callback: APIJourneyCallback +) => APIJourney; + +export class APIJourney extends Journey { + #cb: APIJourneyCallback; + #driver?: Driver | APIDriver; + constructor( + options: JourneyOptions & { type?: 'browser' | 'api' }, + cb: JourneyCallback, + location?: Location + ) { + super(options, cb, location); + this.#cb = cb; + } +} + +export type APIJourneyWithAnnotations = APIJourneyType & { + /** + * Skip this journey and all its steps + */ + skip: APIJourneyType; + /** + * Run only this journey and skip rest of the journeys + */ + only: APIJourneyType; +}; diff --git a/src/dsl/index.ts b/src/dsl/index.ts index a4faaea7..ab7f1aff 100644 --- a/src/dsl/index.ts +++ b/src/dsl/index.ts @@ -24,4 +24,5 @@ */ export * from './journey'; +export * from './api-journey'; export * from './step'; diff --git a/src/dsl/journey.ts b/src/dsl/journey.ts index e7ccac6f..7a9d85a7 100644 --- a/src/dsl/journey.ts +++ b/src/dsl/journey.ts @@ -50,7 +50,7 @@ export type JourneyOptions = { type HookType = 'before' | 'after'; export type Hooks = Record>; -type JourneyCallbackOpts = { +export type JourneyCallbackOpts = { page: Page; context: BrowserContext; browser: Browser; @@ -62,6 +62,7 @@ type JourneyCallbackOpts = { export type JourneyCallback = (options: JourneyCallbackOpts) => void; export class Journey { + readonly type: 'browser' | 'api' = 'browser'; readonly name: string; readonly id?: string; readonly tags?: string[]; @@ -78,10 +79,11 @@ export class Journey { error?: Error; constructor( - options: JourneyOptions, + options: JourneyOptions & { type?: 'browser' | 'api' }, cb: JourneyCallback, location?: Location ) { + this.type = options.type ?? 'browser'; this.name = options.name; this.id = options.id || options.name; this.tags = options.tags; diff --git a/src/index.ts b/src/index.ts index 0e2f9767..1d1e278b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -35,6 +35,7 @@ export async function run(options: RunOptions) { */ export { journey, + apiJourney, step, monitor, beforeAll, diff --git a/src/plugins/api-network.ts b/src/plugins/api-network.ts new file mode 100644 index 00000000..970cb3ef --- /dev/null +++ b/src/plugins/api-network.ts @@ -0,0 +1,156 @@ +/** + * MIT License + * + * Copyright (c) 2020-present, Elastic NV + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +import { Frame, Page, Request } from 'playwright-core'; +import { NetworkInfo, APIDriver } from '../common_types'; +import { log } from '../core/logger'; +import { Step } from '../dsl'; +import { getTimestamp } from '../helpers'; + +/** + * Kibana UI expects the requestStartTime and loadEndTime to be baseline + * in seconds as they have the logic to convert it to milliseconds before + * using for offset calculation + */ +function epochTimeInSeconds() { + return getTimestamp() / 1e6; +} + +const roundMilliSecs = (value: number): number => { + return Math.floor(value * 1000) / 1000; +}; + +export class APINetworkManager { + private _barrierPromises = new Set>(); + results: Array = []; + _currentStep: Partial = null; + _originalMethods: any; + + constructor(private driver: APIDriver) { + this._originalMethods = {}; + } + + /** + * Adds a protection barrier aganist all asynchronous extract operations from + * request/response object that are happening during page lifecycle, If the + * page is closed during the extraction, the barrier enforces those operations + * to not result in exception + */ + private _addBarrier(page: Page, promise: Promise) { + if (!page) return; + const race = Promise.race([ + new Promise(resolve => + page.on('close', () => { + this._barrierPromises.delete(race); + resolve(); + }) + ), + promise, + ]); + this._barrierPromises.add(race); + race.then(() => this._barrierPromises.delete(race)); + } + + private _nullableFrameBarrier(req: Request): Frame | null { + try { + return req.frame(); + } catch (_) { + // frame might be unavailable for certain requests if they are issued + // before the frame is created - true for navigation requests + // https://playwright.dev/docs/api/class-request#request-frame + } + return null; + } + + async start() { + log(`Plugins: started collecting network events`); + const { request } = this.driver; + + // Intercept API requests + ['fetch', 'get', 'post', 'put', 'delete', 'patch', 'head'].forEach( + method => { + this._originalMethods[method] = request[method].bind(request); + request[method] = this._interceptRequest.bind(this, method); + } + ); + } + + async _interceptRequest(method, url, options: any) { + const timestamp = getTimestamp(); + const requestStartTime = epochTimeInSeconds(); + + log(`Intercepting request: ${url}`); + + const requestEntry = { + step: this._currentStep, + timestamp, + url, + type: 'fetch', + request: { + url, + method: method.toUpperCase(), + headers: options?.headers || {}, + body: options?.postData || options?.data || null, + }, + response: { + status: -1, + headers: {}, + mimeType: 'x-unknown', + }, + requestSentTime: requestStartTime, + loadEndTime: -1, + responseReceivedTime: -1, + timings: { + wait: -1, + receive: -1, + total: -1, + }, + }; + + this.results.push(requestEntry as any); + + const start = Date.now(); + const response = await this._originalMethods[method](url, options); + const end = Date.now(); + + requestEntry.responseReceivedTime = epochTimeInSeconds(); + requestEntry.loadEndTime = requestEntry.responseReceivedTime; + requestEntry.response = { + // url: response.url() as any, + status: response.status(), + headers: await response.headers(), + mimeType: response.headers()['content-type'] || 'x-unknown', + }; + requestEntry.timings.wait = roundMilliSecs((end - start) / 1000); + requestEntry.timings.receive = requestEntry.timings.wait; + requestEntry.timings.total = requestEntry.timings.wait; + + return response; + } + + async stop() { + return this.results; + } +} diff --git a/src/plugins/plugin-manager.ts b/src/plugins/plugin-manager.ts index 740bcb0b..d4fb9b37 100644 --- a/src/plugins/plugin-manager.ts +++ b/src/plugins/plugin-manager.ts @@ -23,7 +23,7 @@ * */ -import { PluginOutput, Driver } from '../common_types'; +import { PluginOutput, Driver, APIDriver } from '../common_types'; import { BrowserConsole, NetworkManager, @@ -32,9 +32,15 @@ import { TraceOptions, } from './'; import { Step } from '../dsl'; +import { APINetworkManager } from './api-network'; type PluginType = 'network' | 'trace' | 'performance' | 'browserconsole'; -type Plugin = NetworkManager | Tracing | PerformanceManager | BrowserConsole; +type Plugin = + | NetworkManager + | Tracing + | PerformanceManager + | BrowserConsole + | APINetworkManager; type PluginOptions = TraceOptions; export class PluginManager { @@ -45,23 +51,32 @@ export class PluginManager { 'performance', 'browserconsole', ]; - constructor(private driver: Driver) {} + constructor(private driver: Driver | APIDriver) {} register(type: PluginType, options: PluginOptions) { let instance: Plugin; - switch (type) { - case 'network': - instance = new NetworkManager(this.driver); - break; - case 'trace': - instance = new Tracing(this.driver, options); - break; - case 'performance': - instance = new PerformanceManager(this.driver); - break; - case 'browserconsole': - instance = new BrowserConsole(this.driver); - break; + + if ('context' in this.driver) { + switch (type) { + case 'network': + instance = new NetworkManager(this.driver); + break; + case 'trace': + instance = new Tracing(this.driver, options); + break; + case 'performance': + instance = new PerformanceManager(this.driver); + break; + case 'browserconsole': + instance = new BrowserConsole(this.driver); + break; + } + } else { + switch (type) { + case 'network': + instance = new APINetworkManager(this.driver); + break; + } } instance && this.plugins.set(type, instance); return instance; @@ -98,7 +113,9 @@ export class PluginManager { } onStep(step: Step) { - (this.get('browserconsole') as BrowserConsole)._currentStep = step; + if (this.get('browserconsole') as BrowserConsole) { + (this.get('browserconsole') as BrowserConsole)._currentStep = step; + } (this.get('network') as NetworkManager)._currentStep = step; } diff --git a/src/reporters/base.ts b/src/reporters/base.ts index 69aa8d14..b9230143 100644 --- a/src/reporters/base.ts +++ b/src/reporters/base.ts @@ -30,6 +30,7 @@ import { symbols, indent, now } from '../helpers'; import { Reporter, ReporterOptions } from '.'; import { Journey, Step } from '../dsl'; import { + APIJourneyEndResult, JourneyEndResult, JourneyStartResult, StepResult, @@ -84,7 +85,7 @@ export default class BaseReporter implements Reporter { } /* eslint-disable @typescript-eslint/no-unused-vars */ - onJourneyEnd(journey: Journey, {}: JourneyEndResult) { + onJourneyEnd(journey: Journey, {}: JourneyEndResult | APIJourneyEndResult) { const { failed, succeeded, skipped } = this.metrics; const total = failed + succeeded + skipped; /** diff --git a/src/reporters/index.ts b/src/reporters/index.ts index 0cd42ae8..8649ed5d 100644 --- a/src/reporters/index.ts +++ b/src/reporters/index.ts @@ -32,6 +32,7 @@ import { JourneyEndResult, JourneyStartResult, StepEndResult, + APIJourneyEndResult, } from '../common_types'; import BuildKiteCLIReporter from './build_kite_cli'; @@ -52,6 +53,7 @@ export const reporters: { default: BaseReporter, json: JSONReporter, junit: JUnitReporter, + // 'api-default': APIReporter, 'buildkite-cli': BuildKiteCLIReporter, }; @@ -63,7 +65,7 @@ export interface Reporter { onStepEnd?(journey: Journey, step: Step, result: StepEndResult): void; onJourneyEnd?( journey: Journey, - result: JourneyEndResult + result: JourneyEndResult | APIJourneyEndResult ): void | Promise; onEnd?(): void | Promise; }