mirror of
https://github.com/storybookjs/storybook.git
synced 2025-04-08 05:11:49 +08:00
Addon Test: Improve unhandled errors message
This commit is contained in:
parent
724be33c9c
commit
2c1c317efb
@ -13,6 +13,7 @@ import type { API_StatusUpdate } from 'storybook/internal/types';
|
||||
import type { Suite } from '@vitest/runner';
|
||||
import { throttle } from 'es-toolkit';
|
||||
import { satisfies } from 'semver';
|
||||
import { dedent } from 'ts-dedent';
|
||||
|
||||
import { TEST_PROVIDER_ID } from '../constants';
|
||||
import type { TestManager } from './test-manager';
|
||||
@ -60,6 +61,36 @@ const isVitest3OrLater = vitestVersion
|
||||
? satisfies(vitestVersion, '>=3.0.0-beta.3', { includePrerelease: true })
|
||||
: false;
|
||||
|
||||
interface VitestError extends Error {
|
||||
VITEST_TEST_PATH?: string;
|
||||
VITEST_TEST_NAME?: string;
|
||||
}
|
||||
|
||||
const getErrorOrigin = (error: VitestError): string => {
|
||||
const parts: string[] = [];
|
||||
|
||||
if (error.VITEST_TEST_PATH) {
|
||||
parts.push(
|
||||
dedent`
|
||||
\nThis error originated in "${error.VITEST_TEST_PATH}". It doesn't mean the error was thrown inside the file itself, but while it was running.
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
if (error.VITEST_TEST_NAME) {
|
||||
parts.push(
|
||||
dedent`
|
||||
The latest test that might've caused the error is "${error.VITEST_TEST_NAME}".
|
||||
It might mean one of the following:
|
||||
- The error was thrown, while Vitest was running this test.
|
||||
- If the error occurred after the test had been completed, this was the last documented test before it was thrown.
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
return parts.join('\n');
|
||||
};
|
||||
|
||||
export class StorybookReporter implements Reporter {
|
||||
testStatusData: API_StatusUpdate = {};
|
||||
|
||||
@ -219,6 +250,14 @@ export class StorybookReporter implements Reporter {
|
||||
|
||||
async onFinished() {
|
||||
const unhandledErrors = this.ctx.state.getUnhandledErrors();
|
||||
unhandledErrors.forEach((e: unknown) => {
|
||||
const error = e as VitestError;
|
||||
const origin = getErrorOrigin(error);
|
||||
if (origin) {
|
||||
error.message = `${error.message}\n${origin}`;
|
||||
error.stack = `${error.stack}\n${origin}`;
|
||||
}
|
||||
});
|
||||
|
||||
const isCancelled = isVitest3OrLater
|
||||
? this.testManager.vitestManager.isCancelling
|
||||
@ -256,7 +295,7 @@ export class StorybookReporter implements Reporter {
|
||||
name: `${unhandledErrors.length} unhandled error${unhandledErrors?.length > 1 ? 's' : ''}`,
|
||||
message: unhandledErrors
|
||||
.map((e, index) => `[${index}]: ${(e as any).stack || (e as any).message}`)
|
||||
.join('\n----------\n'),
|
||||
.join('\n\n----------\n\n'),
|
||||
};
|
||||
|
||||
this.sendReport({
|
||||
|
@ -8,10 +8,11 @@ import { SbPage } from "../../../../code/e2e-tests/util";
|
||||
|
||||
const STORYBOOK_URL = "http://localhost:6006";
|
||||
const TEST_STORY_PATH = path.resolve(__dirname, "..", "stories", "AddonTest.stories.tsx");
|
||||
const UNHANDLED_ERRORS_STORY_PATH = path.resolve(__dirname, "..", "stories", "UnhandledErrors.stories.tsx");
|
||||
|
||||
const setForceFailureFlag = async (value: boolean) => {
|
||||
const setForceFailureFlag = async (storyPath: string, value: boolean) => {
|
||||
// Read the story file content asynchronously
|
||||
const storyContent = (await fs.readFile(TEST_STORY_PATH)).toString();
|
||||
const storyContent = (await fs.readFile(storyPath)).toString();
|
||||
|
||||
// Create a regex to match 'forceFailure: true' or 'forceFailure: false'
|
||||
const forceFailureRegex = /forceFailure:\s*(true|false)/;
|
||||
@ -23,7 +24,7 @@ const setForceFailureFlag = async (value: boolean) => {
|
||||
);
|
||||
|
||||
// Write the updated content back to the file asynchronously
|
||||
await fs.writeFile(TEST_STORY_PATH, updatedContent);
|
||||
await fs.writeFile(storyPath, updatedContent);
|
||||
|
||||
// the file change causes a HMR event, which causes a browser reload,and that can take a few seconds
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
@ -129,7 +130,7 @@ test.describe("component testing", () => {
|
||||
browserName,
|
||||
}) => {
|
||||
test.skip(browserName !== "chromium", `Skipping tests for ${browserName}`);
|
||||
await setForceFailureFlag(true);
|
||||
await setForceFailureFlag(TEST_STORY_PATH, true);
|
||||
|
||||
const sbPage = new SbPage(page, expect);
|
||||
await sbPage.navigateToStory("addons/group/test", "Expected Failure");
|
||||
@ -197,7 +198,7 @@ test.describe("component testing", () => {
|
||||
browserName,
|
||||
}) => {
|
||||
test.skip(browserName !== "chromium", `Skipping tests for ${browserName}`);
|
||||
await setForceFailureFlag(false);
|
||||
await setForceFailureFlag(TEST_STORY_PATH, false);
|
||||
|
||||
const sbPage = new SbPage(page, expect);
|
||||
await sbPage.navigateToStory("addons/group/test", "Expected Failure");
|
||||
@ -212,7 +213,7 @@ test.describe("component testing", () => {
|
||||
|
||||
// We shouldn't have to do an arbitrary wait, but because there is no UI for loading state yet, we have to
|
||||
await page.waitForTimeout(8000);
|
||||
await setForceFailureFlag(true);
|
||||
await setForceFailureFlag(TEST_STORY_PATH, true);
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Wait for test results to appear
|
||||
@ -252,7 +253,7 @@ test.describe("component testing", () => {
|
||||
}) => {
|
||||
test.skip(browserName !== "chromium", `Skipping tests for ${browserName}`);
|
||||
// Arrange - Prepare Storybook
|
||||
await setForceFailureFlag(false);
|
||||
await setForceFailureFlag(TEST_STORY_PATH, false);
|
||||
|
||||
const sbPage = new SbPage(page, expect);
|
||||
await sbPage.navigateToStory("addons/group/test", "Expected Failure");
|
||||
@ -302,7 +303,7 @@ test.describe("component testing", () => {
|
||||
}) => {
|
||||
test.skip(browserName !== "chromium", `Skipping tests for ${browserName}`);
|
||||
// Arrange - Prepare Storybook
|
||||
await setForceFailureFlag(false);
|
||||
await setForceFailureFlag(TEST_STORY_PATH, false);
|
||||
|
||||
const sbPage = new SbPage(page, expect);
|
||||
await sbPage.navigateToStory("addons/group/test", "Expected Failure");
|
||||
@ -325,13 +326,50 @@ test.describe("component testing", () => {
|
||||
await expect(page.locator('#storybook-explorer-menu').getByRole('status', { name: 'Test status: success' })).toHaveCount(1);
|
||||
});
|
||||
|
||||
test("should show unhandled errors in the testing module", async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
|
||||
test.skip(browserName !== "chromium", `Skipping tests for ${browserName}`);
|
||||
// Arrange - Prepare Storybook
|
||||
await setForceFailureFlag(UNHANDLED_ERRORS_STORY_PATH, true);
|
||||
|
||||
const sbPage = new SbPage(page, expect);
|
||||
await sbPage.navigateToStory("example/unhandlederrors", "Success");
|
||||
|
||||
const storyElement = sbPage
|
||||
.getCanvasBodyElement()
|
||||
.getByText("Hello world");
|
||||
await expect(storyElement).toBeVisible({ timeout: 30000 });
|
||||
|
||||
// Act - Open sidebar context menu and start focused test
|
||||
await page.locator('[data-item-id="example-unhandlederrors"]').hover();
|
||||
await page.locator('[data-item-id="example-unhandlederrors"] div[data-testid="context-menu"] button').click();
|
||||
const sidebarContextMenu = page.getByTestId('tooltip');
|
||||
await sidebarContextMenu.getByLabel('Start test run').click();
|
||||
|
||||
// Assert - Tests are running and errors are reported
|
||||
const errorLink = page.locator('#testing-module-description a');
|
||||
await expect(errorLink).toContainText('2 unhandled errors', { timeout: 30000 });
|
||||
await errorLink.click();
|
||||
|
||||
await expect(page.locator('pre')).toContainText('I THREW AN UNHANDLED ERROR!');
|
||||
await expect(page.locator('pre')).toContainText('This error originated in');
|
||||
await expect(page.locator('pre')).toContainText("The latest test that might've caused the error is");
|
||||
await page.locator('body').click();
|
||||
|
||||
// Cleanup
|
||||
await setForceFailureFlag(UNHANDLED_ERRORS_STORY_PATH, false);
|
||||
});
|
||||
|
||||
test("should run focused test for a component", async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
test.skip(browserName !== "chromium", `Skipping tests for ${browserName}`);
|
||||
// Arrange - Prepare Storybook
|
||||
await setForceFailureFlag(false);
|
||||
await setForceFailureFlag(TEST_STORY_PATH, false);
|
||||
|
||||
const sbPage = new SbPage(page, expect);
|
||||
await sbPage.navigateToStory("addons/group/test", "Expected Failure");
|
||||
@ -364,7 +402,7 @@ test.describe("component testing", () => {
|
||||
}) => {
|
||||
test.skip(browserName !== "chromium", `Skipping tests for ${browserName}`);
|
||||
// Arrange - Prepare Storybook
|
||||
await setForceFailureFlag(false);
|
||||
await setForceFailureFlag(TEST_STORY_PATH, false);
|
||||
|
||||
const sbPage = new SbPage(page, expect);
|
||||
await sbPage.navigateToStory("addons/group/test", "Expected Failure");
|
||||
@ -397,7 +435,7 @@ test.describe("component testing", () => {
|
||||
}) => {
|
||||
test.skip(browserName !== "chromium", `Skipping tests for ${browserName}`);
|
||||
// Arrange - Prepare Storybook
|
||||
await setForceFailureFlag(false);
|
||||
await setForceFailureFlag(TEST_STORY_PATH, false);
|
||||
|
||||
const sbPage = new SbPage(page, expect);
|
||||
await sbPage.navigateToStory("example/button", "CSF 3 Primary");
|
||||
@ -441,5 +479,4 @@ test.describe("component testing", () => {
|
||||
expect(sbPercentage).toBeGreaterThanOrEqual(0);
|
||||
expect(sbPercentage).toBeLessThanOrEqual(100);
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -0,0 +1,46 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
async function unhandledRejection() {
|
||||
throwError('I THREW AN UNHANDLED REJECTION!');
|
||||
}
|
||||
|
||||
function unhandledError() {
|
||||
throwError('I THREW AN UNHANDLED ERROR!');
|
||||
}
|
||||
|
||||
function throwError(message: string) {
|
||||
throw new Error(message);
|
||||
}
|
||||
const meta = {
|
||||
title: 'Example/UnhandledErrors',
|
||||
args: {
|
||||
errorType: null,
|
||||
forceFailure: false,
|
||||
},
|
||||
component: ({ errorType, forceFailure }) => {
|
||||
if (forceFailure) {
|
||||
if (errorType === 'rejection') {
|
||||
setTimeout(unhandledRejection, 0);
|
||||
} else if (errorType === 'error') {
|
||||
setTimeout(unhandledError, 0);
|
||||
}
|
||||
}
|
||||
return 'Hello world';
|
||||
},
|
||||
} as Meta<{ errorType: 'rejection' | 'error' | null; forceFailure?: boolean }>;
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const UnhandledError: Story = {
|
||||
args: {
|
||||
errorType: 'error',
|
||||
},
|
||||
};
|
||||
|
||||
export const UnhandledRejection: Story = {
|
||||
args: {
|
||||
errorType: 'rejection',
|
||||
},
|
||||
};
|
||||
|
||||
export const Success: Story = {};
|
@ -30,7 +30,7 @@ export default defineWorkspace([
|
||||
}]
|
||||
},
|
||||
setupFiles: ["./.storybook/vitest.setup.ts"],
|
||||
environment: "happy-dom",
|
||||
environment: "jsdom",
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
Loading…
x
Reference in New Issue
Block a user