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

feat: add console logs on test failure #157

Merged
merged 4 commits into from
Aug 5, 2022
Merged
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
87 changes: 79 additions & 8 deletions src/setup-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,26 +50,70 @@ export const setupPage = async (page) => {

await page.addScriptTag({
content: `
// colorizes the console output
const bold = (message) => \`\\u001b[1m\${message}\\u001b[22m\`;
const magenta = (message) => \`\\u001b[35m\${message}\\u001b[39m\`;
const blue = (message) => \`\\u001b[34m\${message}\\u001b[39m\`;
const red = (message) => \`\\u001b[31m\${message}\\u001b[39m\`;
const yellow = (message) => \`\\u001b[33m\${message}\\u001b[39m\`;

// removes circular references from the object
function serializer(replacer, cycleReplacer) {
let stack = [],
keys = [];

if (cycleReplacer == null)
cycleReplacer = function (_key, value) {
if (stack[0] === value) return '[Circular]';
return '[Circular ~.' + keys.slice(0, stack.indexOf(value)).join('.') + ']';
};

return function (key, value) {
if (stack.length > 0) {
let thisPos = stack.indexOf(this);
~thisPos ? stack.splice(thisPos + 1) : stack.push(this);
~thisPos ? keys.splice(thisPos, Infinity, key) : keys.push(key);
if (~stack.indexOf(value)) value = cycleReplacer.call(this, key, value);
} else {
stack.push(value);
}

return replacer == null ? value : replacer.call(this, key, value);
};
}

function safeStringify(obj, replacer, spaces, cycleReplacer) {
return JSON.stringify(obj, serializer(replacer, cycleReplacer), spaces);
}

function composeMessage(args) {
if (typeof args === 'undefined') return "undefined";
if (typeof args === 'string') return args;
return safeStringify(args);
}

function truncate(input, limit) {
if (input.length > limit) {
return input.substring(0, limit) + '…';
}
return input;
}

class StorybookTestRunnerError extends Error {
constructor(storyId, errorMessage) {
constructor(storyId, errorMessage, logs) {
super(errorMessage);
this.name = 'StorybookTestRunnerError';
const storyUrl = \`${referenceURL || targetURL}?path=/story/\${storyId}\`;
const finalStoryUrl = \`\${storyUrl}&addonPanel=storybook/interactions/panel\`;
const separator = '\\n\\n--------------------------------------------------';
const extraLogs = logs.length > 0 ? separator + "\\n\\nBrowser logs:\\n\\n"+ logs.join('\\n\\n') : '';

this.message = \`\nAn error occurred in the following story. Access the link for full output:\n\${finalStoryUrl}\n\nMessage:\n \${truncate(errorMessage,${debugPrintLimit})}\`;
this.message = \`\nAn error occurred in the following story. Access the link for full output:\n\${finalStoryUrl}\n\nMessage:\n \${truncate(errorMessage,${debugPrintLimit})}\n\${extraLogs}\`;
}
}

async function __throwError(storyId, errorMessage) {
throw new StorybookTestRunnerError(storyId, errorMessage);
async function __throwError(storyId, errorMessage, logs) {
throw new StorybookTestRunnerError(storyId, errorMessage, logs);
}

async function __waitForElement(selector) {
Expand Down Expand Up @@ -118,18 +162,45 @@ export const setupPage = async (page) => {
'The test runner could not access the Storybook channel. Are you sure the Storybook is running correctly in that URL?'
);
}

// collect logs to show upon test error
let logs = [];

const spyOnConsole = (method, name) => {
const originalFn = console[method];
return function () {
const message = [...arguments].map(composeMessage).join(', ');
const prefix = \`\${bold(name)}: \`;
logs.push(prefix + message);
originalFn.apply(console, arguments);
};
};

// console methods + color function for their prefix
const spiedMethods = {
log: blue,
warn: yellow,
error: red,
trace: magenta,
group: magenta,
groupCollapsed: magenta,
}

Object.entries(spiedMethods).forEach(([method, color]) => {
console[method] = spyOnConsole(method, color(method))
})

return new Promise((resolve, reject) => {
channel.on('${renderedEvent}', () => resolve(document.getElementById('root')));
channel.on('storyUnchanged', () => resolve(document.getElementById('root')));
channel.on('storyErrored', ({ description }) => reject(
new StorybookTestRunnerError(storyId, description))
new StorybookTestRunnerError(storyId, description, logs))
);
channel.on('storyThrewException', (error) => reject(
new StorybookTestRunnerError(storyId, error.message))
new StorybookTestRunnerError(storyId, error.message, logs))
);
channel.on('storyMissing', (id) => id === storyId && reject(
new StorybookTestRunnerError(storyId, 'The story was missing when trying to access it.'))
new StorybookTestRunnerError(storyId, 'The story was missing when trying to access it.', logs))
);

channel.emit('setCurrentStory', { storyId, viewMode: '${viewMode}' });
Expand Down