mirror of
https://github.com/storybookjs/storybook.git
synced 2025-04-09 00:19:13 +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 type { Suite } from '@vitest/runner';
|
||||||
import { throttle } from 'es-toolkit';
|
import { throttle } from 'es-toolkit';
|
||||||
import { satisfies } from 'semver';
|
import { satisfies } from 'semver';
|
||||||
|
import { dedent } from 'ts-dedent';
|
||||||
|
|
||||||
import { TEST_PROVIDER_ID } from '../constants';
|
import { TEST_PROVIDER_ID } from '../constants';
|
||||||
import type { TestManager } from './test-manager';
|
import type { TestManager } from './test-manager';
|
||||||
@ -60,6 +61,36 @@ const isVitest3OrLater = vitestVersion
|
|||||||
? satisfies(vitestVersion, '>=3.0.0-beta.3', { includePrerelease: true })
|
? satisfies(vitestVersion, '>=3.0.0-beta.3', { includePrerelease: true })
|
||||||
: false;
|
: 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 {
|
export class StorybookReporter implements Reporter {
|
||||||
testStatusData: API_StatusUpdate = {};
|
testStatusData: API_StatusUpdate = {};
|
||||||
|
|
||||||
@ -219,6 +250,14 @@ export class StorybookReporter implements Reporter {
|
|||||||
|
|
||||||
async onFinished() {
|
async onFinished() {
|
||||||
const unhandledErrors = this.ctx.state.getUnhandledErrors();
|
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
|
const isCancelled = isVitest3OrLater
|
||||||
? this.testManager.vitestManager.isCancelling
|
? this.testManager.vitestManager.isCancelling
|
||||||
@ -256,7 +295,7 @@ export class StorybookReporter implements Reporter {
|
|||||||
name: `${unhandledErrors.length} unhandled error${unhandledErrors?.length > 1 ? 's' : ''}`,
|
name: `${unhandledErrors.length} unhandled error${unhandledErrors?.length > 1 ? 's' : ''}`,
|
||||||
message: unhandledErrors
|
message: unhandledErrors
|
||||||
.map((e, index) => `[${index}]: ${(e as any).stack || (e as any).message}`)
|
.map((e, index) => `[${index}]: ${(e as any).stack || (e as any).message}`)
|
||||||
.join('\n----------\n'),
|
.join('\n\n----------\n\n'),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.sendReport({
|
this.sendReport({
|
||||||
|
@ -8,10 +8,11 @@ import { SbPage } from "../../../../code/e2e-tests/util";
|
|||||||
|
|
||||||
const STORYBOOK_URL = "http://localhost:6006";
|
const STORYBOOK_URL = "http://localhost:6006";
|
||||||
const TEST_STORY_PATH = path.resolve(__dirname, "..", "stories", "AddonTest.stories.tsx");
|
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
|
// 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'
|
// Create a regex to match 'forceFailure: true' or 'forceFailure: false'
|
||||||
const forceFailureRegex = /forceFailure:\s*(true|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
|
// 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
|
// 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));
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
@ -129,7 +130,7 @@ test.describe("component testing", () => {
|
|||||||
browserName,
|
browserName,
|
||||||
}) => {
|
}) => {
|
||||||
test.skip(browserName !== "chromium", `Skipping tests for ${browserName}`);
|
test.skip(browserName !== "chromium", `Skipping tests for ${browserName}`);
|
||||||
await setForceFailureFlag(true);
|
await setForceFailureFlag(TEST_STORY_PATH, true);
|
||||||
|
|
||||||
const sbPage = new SbPage(page, expect);
|
const sbPage = new SbPage(page, expect);
|
||||||
await sbPage.navigateToStory("addons/group/test", "Expected Failure");
|
await sbPage.navigateToStory("addons/group/test", "Expected Failure");
|
||||||
@ -197,7 +198,7 @@ test.describe("component testing", () => {
|
|||||||
browserName,
|
browserName,
|
||||||
}) => {
|
}) => {
|
||||||
test.skip(browserName !== "chromium", `Skipping tests for ${browserName}`);
|
test.skip(browserName !== "chromium", `Skipping tests for ${browserName}`);
|
||||||
await setForceFailureFlag(false);
|
await setForceFailureFlag(TEST_STORY_PATH, false);
|
||||||
|
|
||||||
const sbPage = new SbPage(page, expect);
|
const sbPage = new SbPage(page, expect);
|
||||||
await sbPage.navigateToStory("addons/group/test", "Expected Failure");
|
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
|
// 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 page.waitForTimeout(8000);
|
||||||
await setForceFailureFlag(true);
|
await setForceFailureFlag(TEST_STORY_PATH, true);
|
||||||
await page.waitForTimeout(500);
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
// Wait for test results to appear
|
// Wait for test results to appear
|
||||||
@ -252,7 +253,7 @@ test.describe("component testing", () => {
|
|||||||
}) => {
|
}) => {
|
||||||
test.skip(browserName !== "chromium", `Skipping tests for ${browserName}`);
|
test.skip(browserName !== "chromium", `Skipping tests for ${browserName}`);
|
||||||
// Arrange - Prepare Storybook
|
// Arrange - Prepare Storybook
|
||||||
await setForceFailureFlag(false);
|
await setForceFailureFlag(TEST_STORY_PATH, false);
|
||||||
|
|
||||||
const sbPage = new SbPage(page, expect);
|
const sbPage = new SbPage(page, expect);
|
||||||
await sbPage.navigateToStory("addons/group/test", "Expected Failure");
|
await sbPage.navigateToStory("addons/group/test", "Expected Failure");
|
||||||
@ -302,7 +303,7 @@ test.describe("component testing", () => {
|
|||||||
}) => {
|
}) => {
|
||||||
test.skip(browserName !== "chromium", `Skipping tests for ${browserName}`);
|
test.skip(browserName !== "chromium", `Skipping tests for ${browserName}`);
|
||||||
// Arrange - Prepare Storybook
|
// Arrange - Prepare Storybook
|
||||||
await setForceFailureFlag(false);
|
await setForceFailureFlag(TEST_STORY_PATH, false);
|
||||||
|
|
||||||
const sbPage = new SbPage(page, expect);
|
const sbPage = new SbPage(page, expect);
|
||||||
await sbPage.navigateToStory("addons/group/test", "Expected Failure");
|
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);
|
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 ({
|
test("should run focused test for a component", async ({
|
||||||
page,
|
page,
|
||||||
browserName,
|
browserName,
|
||||||
}) => {
|
}) => {
|
||||||
test.skip(browserName !== "chromium", `Skipping tests for ${browserName}`);
|
test.skip(browserName !== "chromium", `Skipping tests for ${browserName}`);
|
||||||
// Arrange - Prepare Storybook
|
// Arrange - Prepare Storybook
|
||||||
await setForceFailureFlag(false);
|
await setForceFailureFlag(TEST_STORY_PATH, false);
|
||||||
|
|
||||||
const sbPage = new SbPage(page, expect);
|
const sbPage = new SbPage(page, expect);
|
||||||
await sbPage.navigateToStory("addons/group/test", "Expected Failure");
|
await sbPage.navigateToStory("addons/group/test", "Expected Failure");
|
||||||
@ -364,7 +402,7 @@ test.describe("component testing", () => {
|
|||||||
}) => {
|
}) => {
|
||||||
test.skip(browserName !== "chromium", `Skipping tests for ${browserName}`);
|
test.skip(browserName !== "chromium", `Skipping tests for ${browserName}`);
|
||||||
// Arrange - Prepare Storybook
|
// Arrange - Prepare Storybook
|
||||||
await setForceFailureFlag(false);
|
await setForceFailureFlag(TEST_STORY_PATH, false);
|
||||||
|
|
||||||
const sbPage = new SbPage(page, expect);
|
const sbPage = new SbPage(page, expect);
|
||||||
await sbPage.navigateToStory("addons/group/test", "Expected Failure");
|
await sbPage.navigateToStory("addons/group/test", "Expected Failure");
|
||||||
@ -397,7 +435,7 @@ test.describe("component testing", () => {
|
|||||||
}) => {
|
}) => {
|
||||||
test.skip(browserName !== "chromium", `Skipping tests for ${browserName}`);
|
test.skip(browserName !== "chromium", `Skipping tests for ${browserName}`);
|
||||||
// Arrange - Prepare Storybook
|
// Arrange - Prepare Storybook
|
||||||
await setForceFailureFlag(false);
|
await setForceFailureFlag(TEST_STORY_PATH, false);
|
||||||
|
|
||||||
const sbPage = new SbPage(page, expect);
|
const sbPage = new SbPage(page, expect);
|
||||||
await sbPage.navigateToStory("example/button", "CSF 3 Primary");
|
await sbPage.navigateToStory("example/button", "CSF 3 Primary");
|
||||||
@ -441,5 +479,4 @@ test.describe("component testing", () => {
|
|||||||
expect(sbPercentage).toBeGreaterThanOrEqual(0);
|
expect(sbPercentage).toBeGreaterThanOrEqual(0);
|
||||||
expect(sbPercentage).toBeLessThanOrEqual(100);
|
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"],
|
setupFiles: ["./.storybook/vitest.setup.ts"],
|
||||||
environment: "happy-dom",
|
environment: "jsdom",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user