Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Forward console logs from the story context to the user's CLI #790

Merged
merged 2 commits into from
Jul 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,7 @@ Options:
--disableWaitAssets Disable waiting for requested assets [boolean] [default: false]
--silent [boolean] [default: false]
--verbose [boolean] [default: false]
--forwardConsoleLogs Forward in-page console logs to the user's console. [boolean] [default: false]
--serverCmd Command line to launch Storybook server. [string] [default: ""]
--serverTimeout Timeout [msec] for starting Storybook server. [number] [default: 60000]
--shard The sharding options for this run. In the format <shardNumber>/<totalShards>.
Expand Down
157 changes: 93 additions & 64 deletions packages/storycap/src/node/capturing-browser.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { EventEmitter } from 'events';
import path from 'path';
import type { Viewport } from 'puppeteer-core';
import type { ConsoleMessage, Viewport } from 'puppeteer-core';
import {
Story,
StorybookConnection,
Expand All @@ -21,6 +21,7 @@ import {
pickupWithVariantKey,
InvalidVariantKeysReason,
} from '../shared/screenshot-options-helper';
import { Logger } from './logger';

/**
*
Expand Down Expand Up @@ -361,87 +362,115 @@ export class CapturingBrowser extends StoryPreviewBrowser {
story: Story,
variantKey: VariantKey,
retryCount: number,
logger: Logger,
forwardConsoleLogs: boolean,
): Promise<ScreenshotResult> {
this.currentRequestId = requestId;
this.currentVariantKey = variantKey;
this.currentStoryRetryCount = retryCount;
let emittedScreenshotOptions: ScreenshotOptions | undefined;
this.resourceWatcher.clear();

await this.setCurrentStory(story, { forceRerender: true });
function onConsoleLog(msg: ConsoleMessage) {
const niceMessage = `From ${requestId} (${msg.type()}): ${msg.text()}`;

if (forwardConsoleLogs) {
switch (msg.type()) {
case 'warning':
logger.warn(niceMessage);
break;
case 'error':
logger.error(niceMessage);
break;
default:
logger.log(niceMessage);
break;
}
} else {
logger.debug(niceMessage);
}
}

this.page.on('console', onConsoleLog);

try {
await this.setCurrentStory(story, { forceRerender: true });

if (this.mode === 'managed') {
// Screenshot options are emitted form the browser process when managed mode.
emittedScreenshotOptions = await this.waitForOptionsFromBrowser();
if (!this.currentStory) {
throw new InvalidCurrentStoryStateError();
if (this.mode === 'managed') {
// Screenshot options are emitted form the browser process when managed mode.
emittedScreenshotOptions = await this.waitForOptionsFromBrowser();
if (!this.currentStory) {
throw new InvalidCurrentStoryStateError();
}
if (!emittedScreenshotOptions) {
// End this capturing process as failure of timeout if emitter don't resolve screenshot options.
return { buffer: null, succeeded: false, variantKeysToPush: [], defaultVariantSuffix: '' };
}
} else {
await sleep(this.opt.delay);
await this.waitBrowserMetricsStable('preEmit');
// Use only `baseScreenshotOptions` when simple mode.
emittedScreenshotOptions = pickupWithVariantKey(this.baseScreenshotOptions, this.currentVariantKey);
}
if (!emittedScreenshotOptions) {
// End this capturing process as failure of timeout if emitter don't resolve screenshot options.
return { buffer: null, succeeded: false, variantKeysToPush: [], defaultVariantSuffix: '' };

const mergedScreenshotOptions = mergeScreenshotOptions(this.baseScreenshotOptions, emittedScreenshotOptions);

// Get keys for variants included in the screenshot options in order to queue capturing them after this sequence.
const [invalidReason, keys] = extractVariantKeys(mergedScreenshotOptions);
const variantKeysToPush = this.currentVariantKey.isDefault ? keys : [];
this.logInvalidVariantKeysReason(invalidReason);

// End this capturing process as success if `skip` set true.
if (mergedScreenshotOptions.skip) {
await this.waitForDebugInput();
return { buffer: null, succeeded: true, variantKeysToPush, defaultVariantSuffix: '' };
}
} else {
await sleep(this.opt.delay);
await this.waitBrowserMetricsStable('preEmit');
// Use only `baseScreenshotOptions` when simple mode.
emittedScreenshotOptions = pickupWithVariantKey(this.baseScreenshotOptions, this.currentVariantKey);
}

const mergedScreenshotOptions = mergeScreenshotOptions(this.baseScreenshotOptions, emittedScreenshotOptions);
this.touched = false;

// Get keys for variants included in the screenshot options in order to queue capturing them after this sequence.
const [invalidReason, keys] = extractVariantKeys(mergedScreenshotOptions);
const variantKeysToPush = this.currentVariantKey.isDefault ? keys : [];
this.logInvalidVariantKeysReason(invalidReason);
// Change browser's viewport if needed.
const vpChanged = await this.setViewport(mergedScreenshotOptions);
// Skip to capture if the viewport option is invalid.
if (!vpChanged) return { buffer: null, succeeded: true, variantKeysToPush: [], defaultVariantSuffix: '' };

// End this capturing process as success if `skip` set true.
if (mergedScreenshotOptions.skip) {
await this.waitForDebugInput();
return { buffer: null, succeeded: true, variantKeysToPush, defaultVariantSuffix: '' };
}
// Modify elements state.
await this.setHover(mergedScreenshotOptions);
await this.setFocus(mergedScreenshotOptions);
await this.setClick(mergedScreenshotOptions);
await this.waitIfTouched();

this.touched = false;
// Wait until browser main thread gets stable.
await this.waitForResources(mergedScreenshotOptions);
await this.waitBrowserMetricsStable('postEmit');

// Change browser's viewport if needed.
const vpChanged = await this.setViewport(mergedScreenshotOptions);
// Skip to capture if the viewport option is invalid.
if (!vpChanged) return { buffer: null, succeeded: true, variantKeysToPush: [], defaultVariantSuffix: '' };

// Modify elements state.
await this.setHover(mergedScreenshotOptions);
await this.setFocus(mergedScreenshotOptions);
await this.setClick(mergedScreenshotOptions);
await this.waitIfTouched();

// Wait until browser main thread gets stable.
await this.waitForResources(mergedScreenshotOptions);
await this.waitBrowserMetricsStable('postEmit');

await this.page.evaluate(() => new Promise(res => (window as any).requestIdleCallback(res, { timeout: 3000 })));

// Get PNG image buffer
const rawBuffer = await this.page.screenshot({
fullPage: emittedScreenshotOptions.fullPage,
omitBackground: emittedScreenshotOptions.omitBackground,
captureBeyondViewport: emittedScreenshotOptions.captureBeyondViewport,
clip: emittedScreenshotOptions.clip ?? undefined,
});
await this.page.evaluate(() => new Promise(res => (window as any).requestIdleCallback(res, { timeout: 3000 })));

let buffer: Buffer | null = null;
if (Buffer.isBuffer(rawBuffer)) {
buffer = rawBuffer;
}
// Get PNG image buffer
const rawBuffer = await this.page.screenshot({
fullPage: emittedScreenshotOptions.fullPage,
omitBackground: emittedScreenshotOptions.omitBackground,
captureBeyondViewport: emittedScreenshotOptions.captureBeyondViewport,
clip: emittedScreenshotOptions.clip ?? undefined,
});

// We should reset elements state(e.g. focusing, hovering, clicking) for future screenshot for this story.
await this.resetIfTouched(mergedScreenshotOptions);
let buffer: Buffer | null = null;
if (Buffer.isBuffer(rawBuffer)) {
buffer = rawBuffer;
}

await this.waitForDebugInput();
// We should reset elements state(e.g. focusing, hovering, clicking) for future screenshot for this story.
await this.resetIfTouched(mergedScreenshotOptions);

return {
buffer,
succeeded: true,
variantKeysToPush,
defaultVariantSuffix: emittedScreenshotOptions.defaultVariantSuffix,
};
await this.waitForDebugInput();

return {
buffer,
succeeded: true,
variantKeysToPush,
defaultVariantSuffix: emittedScreenshotOptions.defaultVariantSuffix,
};
} finally {
this.page.off('console', onConsoleLog);
}
}
}
7 changes: 7 additions & 0 deletions packages/storycap/src/node/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ function createOptions(): MainOptions {
})
.option('silent', { boolean: true, default: false })
.option('verbose', { boolean: true, default: false })
.option('forwardConsoleLogs', {
boolean: true,
default: false,
description: "Forward in-page console logs to the user's console.",
})
.option('serverCmd', { string: true, default: '', description: 'Command line to launch Storybook server.' })
.option('serverTimeout', {
number: true,
Expand Down Expand Up @@ -114,6 +119,7 @@ function createOptions(): MainOptions {
parallel,
silent,
verbose,
forwardConsoleLogs,
serverTimeout,
serverCmd,
shard,
Expand Down Expand Up @@ -179,6 +185,7 @@ function createOptions(): MainOptions {
stateChangeDelay,
disableCssAnimation,
disableWaitAssets,
forwardConsoleLogs,
chromiumChannel: chromiumChannel as ChromeChannel,
chromiumPath,
launchOptions: puppeteerLaunchConfig,
Expand Down
8 changes: 7 additions & 1 deletion packages/storycap/src/node/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,13 @@ export async function main(mainOptions: MainOptions) {

try {
// Execution caputuring procedure.
const captured = await createScreenshotService({ workers, stories: shardedStories, fileSystem, logger }).execute();
const captured = await createScreenshotService({
workers,
stories: shardedStories,
fileSystem,
logger,
forwardConsoleLogs: mainOptions.forwardConsoleLogs,
}).execute();
logger.debug('Ended ScreenshotService execution.');
return captured;
} catch (error) {
Expand Down
6 changes: 5 additions & 1 deletion packages/storycap/src/node/screenshot-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export type ScreenshotServiceOptions = {
workers: CapturingBrowser[];
fileSystem: FileSystem;
stories: Story[];
forwardConsoleLogs: boolean;
};

/**
Expand All @@ -63,14 +64,17 @@ export function createScreenshotService({
logger,
stories,
workers,
forwardConsoleLogs,
}: ScreenshotServiceOptions): ScreenshotService {
const service = createExecutionService(
workers,
stories.map(story => createRequest({ story })),
({ rid, story, variantKey, count }, { push }) =>
async worker => {
// Delegate the request to the worker.
const [result, elapsedTime] = await time(worker.screenshot(rid, story, variantKey, count));
const [result, elapsedTime] = await time(
worker.screenshot(rid, story, variantKey, count, logger, forwardConsoleLogs),
);

const { succeeded, buffer, variantKeysToPush, defaultVariantSuffix } = result;

Expand Down
1 change: 1 addition & 0 deletions packages/storycap/src/node/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export interface MainOptions extends BaseBrowserOptions {
exclude: string[];
disableCssAnimation: boolean;
disableWaitAssets: boolean;
forwardConsoleLogs: boolean;
parallel: number;
shard: ShardOptions;
metricsWatchRetryCount: number;
Expand Down
1 change: 1 addition & 0 deletions packages/storycap/src/shared/screenshot-options-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const defaultScreenshotOptions = {
omitBackground: false,
captureBeyondViewport: true,
clip: null,
forwardConsoleLogs: false,
} as const;

/**
Expand Down