From dd7eb64f0f685c611f4b1ca8b7cdd451bf68318c Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Mon, 27 Nov 2023 16:11:11 +0100 Subject: [PATCH] introduce logLevel configuration --- .storybook/test-runner.ts | 1 + README.md | 21 +++++++++++ src/playwright/hooks.ts | 5 +++ src/setup-page-script.ts | 79 ++++++++++++++++++++++++++++++--------- src/setup-page.ts | 1 + 5 files changed, 89 insertions(+), 18 deletions(-) diff --git a/.storybook/test-runner.ts b/.storybook/test-runner.ts index 460f4c9..caed9e2 100644 --- a/.storybook/test-runner.ts +++ b/.storybook/test-runner.ts @@ -7,6 +7,7 @@ const customSnapshotsDir = `${process.cwd()}/${snapshotsDir}`; const skipSnapshots = process.env.SKIP_SNAPSHOTS === 'true'; const config: TestRunnerConfig = { + logLevel: 'verbose', tags: { exclude: ['exclude'], include: [], diff --git a/README.md b/README.md index c291eae..3f70dc7 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ Storybook test runner turns all of your stories into executable tests. - [prepare](#prepare) - [getHttpHeaders](#gethttpheaders) - [tags (experimental)](#tags-experimental) + - [logLevel](#loglevel) - [Utility functions](#utility-functions) - [getStoryContext](#getstorycontext) - [waitForPageReady](#waitforpageready) @@ -705,6 +706,26 @@ export default config; `tags` are used for filtering your tests. Learn more [here](#filtering-tests-experimental). +#### logLevel + +When tests fail and there were browser logs during the rendering of a story, the test-runner provides the logs alongside the error message. The `logLevel` property defines what kind of logs should be displayed: + +- **`info` (default):** Shows console logs, warnings, and errors. +- **`warn`:** Shows only warnings and errors. +- **`error`:** Displays only error messages. +- **`verbose`:** Includes all console outputs, including debug information and stack traces. +- **`none`:** Suppresses all log output. + +```ts +// .storybook/test-runner.ts +import type { TestRunnerConfig } from '@storybook/test-runner'; + +const config: TestRunnerConfig = { + logLevel: 'verbose', +}; +export default config; +``` + ### Utility functions For more specific use cases, the test runner provides utility functions that could be useful to you. diff --git a/src/playwright/hooks.ts b/src/playwright/hooks.ts index f3c2bbd..f2032da 100644 --- a/src/playwright/hooks.ts +++ b/src/playwright/hooks.ts @@ -58,6 +58,11 @@ export interface TestRunnerConfig { exclude?: string[]; skip?: string[]; }; + /** + * Defines the log level of the test runner. Browser logs are printed to the console when reporting errors. + * @default 'info' + */ + logLevel?: 'info' | 'warn' | 'error' | 'verbose' | 'none'; } export const setPreVisit = (preVisit: TestHook) => { diff --git a/src/setup-page-script.ts b/src/setup-page-script.ts index 65c6b41..56b9c39 100644 --- a/src/setup-page-script.ts +++ b/src/setup-page-script.ts @@ -5,13 +5,27 @@ * setup-page.ts will read the contents of this file and replace values that use {{x}} pattern, and they should be put right below: */ +type ConsoleMethod = + | 'log' + | 'info' + | 'warn' + | 'error' + | 'trace' + | 'debug' + | 'group' + | 'groupCollapsed' + | 'table' + | 'dir'; +type LogLevel = 'none' | 'info' | 'warn' | 'error' | 'verbose'; + // All of these variables will be replaced once this file is processed. -const storybookUrl: string = '{{storybookUrl}}'; -const testRunnerVersion: string = '{{testRunnerVersion}}'; -const failOnConsole: string = '{{failOnConsole}}'; -const renderedEvent: string = '{{renderedEvent}}'; -const viewMode: string = '{{viewMode}}'; -const debugPrintLimit = parseInt('{{debugPrintLimit}}', 10); +const TEST_RUNNER_STORYBOOK_URL: string = '{{storybookUrl}}'; +const TEST_RUNNER_VERSION: string = '{{testRunnerVersion}}'; +const TEST_RUNNER_FAIL_ON_CONSOLE: string = '{{failOnConsole}}'; +const TEST_RUNNER_RENDERED_EVENT: string = '{{renderedEvent}}'; +const TEST_RUNNER_VIEW_MODE: string = '{{viewMode}}'; +const TEST_RUNNER_LOG_LEVEL = '{{logLevel}}' as LogLevel; +const TEST_RUNNER_DEBUG_PRINT_LIMIT = parseInt('{{debugPrintLimit}}', 10); // Type definitions for globals declare global { @@ -194,7 +208,7 @@ class StorybookTestRunnerError extends Error { constructor(storyId: string, errorMessage: string, logs: string[] = []) { super(errorMessage); this.name = 'StorybookTestRunnerError'; - const storyUrl = `${storybookUrl}?path=/story/${storyId}`; + const storyUrl = `${TEST_RUNNER_STORYBOOK_URL}?path=/story/${storyId}`; const finalStoryUrl = `${storyUrl}&addonPanel=storybook/interactions/panel`; const separator = '\n\n--------------------------------------------------'; // The original error message will also be collected in the logs, so we filter it to avoid duplication @@ -204,7 +218,7 @@ class StorybookTestRunnerError extends Error { this.message = `\nAn error occurred in the following story. Access the link for full output:\n${finalStoryUrl}\n\nMessage:\n ${truncate( errorMessage, - debugPrintLimit + TEST_RUNNER_DEBUG_PRINT_LIMIT )}\n${extraLogs}`; } } @@ -266,35 +280,64 @@ async function __test(storyId: string): Promise { ); } - addToUserAgent(`(StorybookTestRunner@${testRunnerVersion})`); + addToUserAgent(`(StorybookTestRunner@${TEST_RUNNER_VERSION})`); // Collect logs to show upon test error let logs: string[] = []; let hasErrors = false; - type ConsoleMethod = 'log' | 'group' | 'warn' | 'error' | 'trace' | 'groupCollapsed'; + const logLevelMapping: { [key in ConsoleMethod]: LogLevel[] } = { + log: ['info', 'verbose'], + warn: ['info', 'warn', 'verbose'], + error: ['info', 'warn', 'error', 'verbose'], + info: ['verbose'], + trace: ['verbose'], + debug: ['verbose'], + group: ['verbose'], + groupCollapsed: ['verbose'], + table: ['verbose'], + dir: ['verbose'], + }; const spyOnConsole = (method: ConsoleMethod, name: string): void => { const originalFn = console[method].bind(console); console[method] = function () { - if (failOnConsole === 'true' && method === 'error') { + const shouldCollectError = TEST_RUNNER_FAIL_ON_CONSOLE === 'true' && method === 'error'; + if (shouldCollectError) { hasErrors = true; } - const message = Array.from(arguments).map(composeMessage).join(', '); - const prefix = `${bold(name)}: `; - logs.push(prefix + message); + + let message = Array.from(arguments).map(composeMessage).join(', '); + if (method === 'trace') { + const stackTrace = new Error().stack; + message += `\n${stackTrace}\n`; + } + + if (logLevelMapping[method].includes(TEST_RUNNER_LOG_LEVEL) || shouldCollectError) { + const prefix = `${bold(name)}: `; + logs.push(prefix + message); + } + originalFn(...arguments); }; }; // Console methods + color function for their prefix const spiedMethods: { [key: string]: Colorizer } = { + // info log: blue, + info: blue, + // warn warn: yellow, + // error error: red, + // verbose + dir: magenta, trace: magenta, group: magenta, groupCollapsed: magenta, + table: magenta, + debug: magenta, }; Object.entries(spiedMethods).forEach(([method, color]) => { @@ -309,12 +352,12 @@ async function __test(storyId: string): Promise { return new Promise((resolve, reject) => { const listeners = { - [renderedEvent]: () => { + [TEST_RUNNER_RENDERED_EVENT]: () => { cleanup(listeners); if (hasErrors) { - return reject(new StorybookTestRunnerError(storyId, 'Browser console errors', logs)); + reject(new StorybookTestRunnerError(storyId, 'Browser console errors', logs)); } - return resolve(document.getElementById('root')); + resolve(document.getElementById('root')); }, storyUnchanged: () => { @@ -355,7 +398,7 @@ async function __test(storyId: string): Promise { channel.on(eventName, listener); }); - channel.emit('setCurrentStory', { storyId, viewMode }); + channel.emit('setCurrentStory', { storyId, viewMode: TEST_RUNNER_VIEW_MODE }); }); } diff --git a/src/setup-page.ts b/src/setup-page.ts index ea2bf64..3a137f0 100644 --- a/src/setup-page.ts +++ b/src/setup-page.ts @@ -69,6 +69,7 @@ export const setupPage = async (page: Page, browserContext: BrowserContext) => { .replaceAll('{{failOnConsole}}', failOnConsole ?? 'false') .replaceAll('{{renderedEvent}}', renderedEvent) .replaceAll('{{testRunnerVersion}}', testRunnerVersion) + .replaceAll('{{logLevel}}', testRunnerConfig.logLevel ?? 'info') .replaceAll('{{debugPrintLimit}}', debugPrintLimit.toString()); await page.addScriptTag({ content });