Skip to content

Commit

Permalink
Progress reporting for Testing Module
Browse files Browse the repository at this point in the history
  • Loading branch information
ghengeveld committed Oct 9, 2024
1 parent 8c6e3f8 commit c4d3ed0
Show file tree
Hide file tree
Showing 13 changed files with 490 additions and 303 deletions.
82 changes: 74 additions & 8 deletions code/addons/test/src/manager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@ import React, { useCallback } from 'react';
import { AddonPanel, Badge, Spaced } from 'storybook/internal/components';
import type { Combo } from 'storybook/internal/manager-api';
import { Consumer, addons, types, useAddonState } from 'storybook/internal/manager-api';
import { Addon_TypesEnum } from 'storybook/internal/types';

import { PointerHandIcon } from '@storybook/icons';
import {
type API_StatusObject,
type API_StatusValue,
type Addon_TestProviderType,
Addon_TypesEnum,
} from 'storybook/internal/types';

import { Panel } from './Panel';
import { ADDON_ID, PANEL_ID, TEST_PROVIDER_ID } from './constants';
import type { TestResult } from './node/reporter';

function Title() {
const [addonState = {}] = useAddonState(ADDON_ID);
Expand All @@ -27,15 +31,77 @@ function Title() {
);
}

addons.register(ADDON_ID, (api) => {
const statusMap: Record<any['status'], API_StatusValue> = {
failed: 'error',
passed: 'success',
pending: 'pending',
};

export function getRelativeTimeString(date: Date): string {
const delta = Math.round((date.getTime() - Date.now()) / 1000);
const cutoffs = [60, 3600, 86400, 86400 * 7, 86400 * 30, 86400 * 365, Infinity];
const units: Intl.RelativeTimeFormatUnit[] = [
'second',
'minute',
'hour',
'day',
'week',
'month',
'year',
];

const unitIndex = cutoffs.findIndex((cutoff) => cutoff > Math.abs(delta));
const divisor = unitIndex ? cutoffs[unitIndex - 1] : 1;
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
return rtf.format(Math.floor(delta / divisor), units[unitIndex]);
}

addons.register(ADDON_ID, () => {
addons.add(TEST_PROVIDER_ID, {
type: Addon_TypesEnum.experimental_TEST_PROVIDER,
icon: <PointerHandIcon />,
title: 'Component Tests',
description: () => 'Not yet run',
runnable: true,
watchable: true,
});

title: ({ failed }) => (failed ? "Component tests didn't complete" : 'Component tests'),
description: ({ failed, running, watching, progress }) => {
if (running) {
return progress
? `Testing... ${progress.numPassedTests}/${progress.numTotalTests}`
: 'Starting...';
}
if (failed) {
return 'Component tests failed';
}
if (watching) {
return 'Watching for file changes';
}
if (progress?.finishedAt) {
return `Ran ${getRelativeTimeString(progress.finishedAt).replace(/^now$/, 'just now')}`;
}
return 'Not run';
},

mapStatusUpdate: (state) =>
Object.fromEntries(
(state.details.testResults || []).flatMap((testResult) =>
testResult.results
.map(({ storyId, status, ...rest }) => {
if (storyId) {
const statusObject: API_StatusObject = {
title: 'Vitest',
status: statusMap[status],
description:
'failureMessages' in rest && rest.failureMessages?.length
? rest.failureMessages.join('\n')
: '',
};
return [storyId, statusObject];
}
})
.filter(Boolean)
)
),
} as Addon_TestProviderType<{ testResults: TestResult[] }>);

addons.add(PANEL_ID, {
type: types.PANEL,
Expand Down
6 changes: 3 additions & 3 deletions code/addons/test/src/node/boot-test-runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { Channel, type ChannelTransport } from '@storybook/core/channels';

import {
TESTING_MODULE_CANCEL_TEST_RUN_REQUEST,
TESTING_MODULE_PROGRESS_REPORT,
TESTING_MODULE_RUN_ALL_REQUEST,
TESTING_MODULE_RUN_PROGRESS_RESPONSE,
TESTING_MODULE_RUN_REQUEST,
TESTING_MODULE_WATCH_MODE_REQUEST,
} from '@storybook/core/core-events';
Expand Down Expand Up @@ -101,8 +101,8 @@ describe('bootTestRunner', () => {
bootTestRunner(mockChannel);
message({ type: 'ready' });

message({ type: TESTING_MODULE_RUN_PROGRESS_RESPONSE, args: ['foo'] });
expect(mockChannel.last(TESTING_MODULE_RUN_PROGRESS_RESPONSE)).toEqual(['foo']);
message({ type: TESTING_MODULE_PROGRESS_REPORT, args: ['foo'] });
expect(mockChannel.last(TESTING_MODULE_PROGRESS_REPORT)).toEqual(['foo']);

mockChannel.emit(TESTING_MODULE_RUN_REQUEST, 'foo');
expect(child.send).toHaveBeenCalledWith({
Expand Down
28 changes: 14 additions & 14 deletions code/addons/test/src/node/boot-test-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import {
TESTING_MODULE_RUN_ALL_REQUEST,
TESTING_MODULE_RUN_REQUEST,
TESTING_MODULE_WATCH_MODE_REQUEST,
type TestingModuleCrashReportPayload,
} from 'storybook/internal/core-events';

import { execaNode } from 'execa';

import { TEST_PROVIDER_ID } from '../constants';
import { log } from '../logger';

const MAX_START_ATTEMPTS = 3;
Expand All @@ -24,6 +26,7 @@ const vitestModulePath = join(__dirname, 'node', 'vitest.mjs');
export const bootTestRunner = async (channel: Channel, initEvent?: string, initArgs?: any[]) => {
let aborted = false;
let child: null | ChildProcess;
let stderr: string[] = [];

const forwardRun = (...args: any[]) =>
child?.send({ args, from: 'server', type: TESTING_MODULE_RUN_REQUEST });
Expand Down Expand Up @@ -55,15 +58,15 @@ export const bootTestRunner = async (channel: Channel, initEvent?: string, initA
const startChildProcess = (attempt = 1) =>
new Promise<void>((resolve, reject) => {
child = execaNode(vitestModulePath);
stderr = [];

child.stdout?.on('data', log);
child.stderr?.on('data', (data) => {
const message = data.toString();
// TODO: improve this error handling. Example use case is Playwright is not installed
if (message.includes('Error: browserType.launch')) {
channel.emit(TESTING_MODULE_CRASH_REPORT, message);
// Ignore deprecation warnings which appear in yellow ANSI color
if (!data.toString().match(/^\u001B\[33m/)) {
log(data);
stderr.push(data.toString());
}

log(data);
});

child.on('message', (result: any) => {
Expand All @@ -82,13 +85,7 @@ export const bootTestRunner = async (channel: Channel, initEvent?: string, initA
resolve();
} else if (result.type === 'error') {
killChild();

if (result.message) {
log(result.message);
}
if (result.error) {
log(result.error);
}
log(`${result.message}: ${result.error}`);

if (attempt >= MAX_START_ATTEMPTS) {
log(`Aborting test runner process after ${attempt} restart attempts`);
Expand All @@ -108,8 +105,11 @@ export const bootTestRunner = async (channel: Channel, initEvent?: string, initA
);

await Promise.race([startChildProcess(), timeout]).catch((e) => {
log(e.message);
aborted = true;
channel.emit(TESTING_MODULE_CRASH_REPORT, {
providerId: TEST_PROVIDER_ID,
message: stderr.join('\n'),
} as TestingModuleCrashReportPayload);
throw e;
});
};
Loading

0 comments on commit c4d3ed0

Please sign in to comment.