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

Introduce logLevel configuration #406

Merged
merged 1 commit into from
Nov 27, 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .storybook/test-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [],
Expand Down
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions src/playwright/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
79 changes: 61 additions & 18 deletions src/setup-page-script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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}`;
}
}
Expand Down Expand Up @@ -266,35 +280,64 @@ async function __test(storyId: string): Promise<any> {
);
}

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]) => {
Expand All @@ -309,12 +352,12 @@ async function __test(storyId: string): Promise<any> {

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: () => {
Expand Down Expand Up @@ -355,7 +398,7 @@ async function __test(storyId: string): Promise<any> {
channel.on(eventName, listener);
});

channel.emit('setCurrentStory', { storyId, viewMode });
channel.emit('setCurrentStory', { storyId, viewMode: TEST_RUNNER_VIEW_MODE });
});
}

Expand Down
1 change: 1 addition & 0 deletions src/setup-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down