diff --git a/code/addons/test/src/components/Interaction.tsx b/code/addons/test/src/components/Interaction.tsx index 4ceef384d02a..75797c65c5a3 100644 --- a/code/addons/test/src/components/Interaction.tsx +++ b/code/addons/test/src/components/Interaction.tsx @@ -23,7 +23,7 @@ const MethodCallWrapper = styled.div(() => ({ const RowContainer = styled('div', { shouldForwardProp: (prop) => !['call', 'pausedAt'].includes(prop.toString()), -})<{ call: Call; pausedAt: Call['id'] }>( +})<{ call: Call; pausedAt: Call['id'] | undefined }>( ({ theme, call }) => ({ position: 'relative', display: 'flex', @@ -117,6 +117,9 @@ const RowMessage = styled('div')(({ theme }) => ({ export const Exception = ({ exception }: { exception: Call['exception'] }) => { const filter = useAnsiToHtmlFilter(); + if (!exception) { + return null; + } if (isJestError(exception)) { return ; } @@ -187,7 +190,7 @@ export const Interaction = ({ - {childCallIds?.length > 0 && ( + {(childCallIds?.length ?? 0) > 0 && ( } diff --git a/code/addons/test/src/components/MatcherResult.tsx b/code/addons/test/src/components/MatcherResult.tsx index 46b5e540ad8d..fa01b73548ec 100644 --- a/code/addons/test/src/components/MatcherResult.tsx +++ b/code/addons/test/src/components/MatcherResult.tsx @@ -74,10 +74,10 @@ export const MatcherResult = ({ {lines.flatMap((line: string, index: number) => { if (line.startsWith('expect(')) { const received = getParams(line, 7); - const remainderIndex = received && 7 + received.length; + const remainderIndex = received ? 7 + received.length : 0; const matcher = received && line.slice(remainderIndex).match(/\.(to|last|nth)[A-Z]\w+\(/); if (matcher) { - const expectedIndex = remainderIndex + matcher.index + matcher[0].length; + const expectedIndex = remainderIndex + (matcher.index ?? 0) + matcher[0].length; const expected = getParams(line, expectedIndex); if (expected) { return [ diff --git a/code/addons/test/src/components/MethodCall.tsx b/code/addons/test/src/components/MethodCall.tsx index 59b907d13daa..34d1e6bb6f58 100644 --- a/code/addons/test/src/components/MethodCall.tsx +++ b/code/addons/test/src/components/MethodCall.tsx @@ -139,7 +139,7 @@ export const Node = ({ case Object.prototype.hasOwnProperty.call(value, '__class__'): return ; case Object.prototype.hasOwnProperty.call(value, '__callId__'): - return ; + return ; /* eslint-enable no-underscore-dangle */ case Object.prototype.toString.call(value) === '[object Object]': @@ -418,7 +418,7 @@ export const MethodCall = ({ callsById, }: { call?: Call; - callsById: Map; + callsById?: Map; }) => { // Call might be undefined during initial render, can be safely ignored. if (!call) { @@ -434,7 +434,7 @@ export const MethodCall = ({ const callId = (elem as CallRef).__callId__; return [ callId ? ( - + ) : ( {elem as any} ), diff --git a/code/addons/test/src/components/Panel.tsx b/code/addons/test/src/components/Panel.tsx index cc2eaf233356..d6ea74843151 100644 --- a/code/addons/test/src/components/Panel.tsx +++ b/code/addons/test/src/components/Panel.tsx @@ -22,14 +22,6 @@ import type { API_StatusValue } from '@storybook/types'; import { ADDON_ID, STORYBOOK_ADDON_TEST_CHANNEL, TEST_PROVIDER_ID } from '../constants'; import { InteractionsPanel } from './InteractionsPanel'; -interface Interaction extends Call { - status: Call['status']; - childCallIds: Call['id'][]; - isHidden: boolean; - isCollapsed: boolean; - toggleCollapsed: () => void; -} - const INITIAL_CONTROL_STATES = { start: false, back: false, @@ -60,7 +52,7 @@ export const getInteractions = ({ const childCallMap = new Map(); return log - .map(({ callId, ancestors, status }) => { + .map(({ callId, ancestors, status }) => { let isHidden = false; ancestors.forEach((ancestor) => { if (collapsed.has(ancestor)) { @@ -68,11 +60,12 @@ export const getInteractions = ({ } childCallMap.set(ancestor, (childCallMap.get(ancestor) || []).concat(callId)); }); - return { ...calls.get(callId), status, isHidden }; + return { ...calls.get(callId)!, status, isHidden }; }) - .map((call) => { + .map((call) => { const status = call.status === CallStates.ERROR && + call.ancestors && callsById.get(call.ancestors.slice(-1)[0])?.status === CallStates.ACTIVE ? CallStates.ACTIVE : call.status; @@ -131,7 +124,7 @@ export const Panel = memo<{ storyId: string }>(function PanelMemoized({ storyId const calls = useRef>>(new Map()); const setCall = ({ status, ...call }: Call) => calls.current.set(call.id, call); - const endRef = useRef(); + const endRef = useRef(); useEffect(() => { let observer: IntersectionObserver; if (global.IntersectionObserver) { @@ -151,6 +144,7 @@ export const Panel = memo<{ storyId: string }>(function PanelMemoized({ storyId { [EVENTS.CALL]: setCall, [EVENTS.SYNC]: (payload) => { + // @ts-expect-error TODO set((s) => { const list = getInteractions({ log: payload.logItems, @@ -214,6 +208,7 @@ export const Panel = memo<{ storyId: string }>(function PanelMemoized({ storyId ); useEffect(() => { + // @ts-expect-error TODO set((s) => { const list = getInteractions({ log: log.current, @@ -250,16 +245,17 @@ export const Panel = memo<{ storyId: string }>(function PanelMemoized({ storyId const hasException = !!caughtException || !!unhandledErrors || + // @ts-expect-error TODO interactions.some((v) => v.status === CallStates.ERROR); const storyStatus = storyStatuses[storyId]?.[TEST_PROVIDER_ID]; const storyTestStatus = storyStatus?.status; - const browserTestStatus = useMemo(() => { + const browserTestStatus = useMemo(() => { if (!isPlaying && (interactions.length > 0 || hasException)) { return hasException ? CallStates.ERROR : CallStates.DONE; } - return isPlaying ? CallStates.ACTIVE : null; + return isPlaying ? CallStates.ACTIVE : undefined; }, [isPlaying, interactions, hasException]); const { testRunId } = storyStatus?.data || {}; @@ -315,6 +311,7 @@ export const Panel = memo<{ storyId: string }>(function PanelMemoized({ storyId unhandledErrors={unhandledErrors} isPlaying={isPlaying} pausedAt={pausedAt} + // @ts-expect-error TODO endRef={endRef} onScrollToEnd={scrollTarget && scrollToTarget} /> diff --git a/code/addons/test/src/components/RelativeTime.tsx b/code/addons/test/src/components/RelativeTime.tsx index 9cb1df1b1b66..4d4cf3c48693 100644 --- a/code/addons/test/src/components/RelativeTime.tsx +++ b/code/addons/test/src/components/RelativeTime.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; export const RelativeTime = ({ timestamp }: { timestamp?: number }) => { - const [timeAgo, setTimeAgo] = useState(null); + const [timeAgo, setTimeAgo] = useState(null); useEffect(() => { if (timestamp) { diff --git a/code/addons/test/src/components/StatusBadge.tsx b/code/addons/test/src/components/StatusBadge.tsx index d730b8ef985c..a906b501a939 100644 --- a/code/addons/test/src/components/StatusBadge.tsx +++ b/code/addons/test/src/components/StatusBadge.tsx @@ -14,7 +14,7 @@ const StyledBadge = styled.div(({ theme, status }) => { [CallStates.ERROR]: theme.color.negative, [CallStates.ACTIVE]: theme.color.warning, [CallStates.WAITING]: theme.color.warning, - }[status]; + }[status!]; return { padding: '4px 6px 4px 8px;', borderRadius: '4px', @@ -36,7 +36,7 @@ export const StatusBadge: React.FC = ({ status }) => { [CallStates.ERROR]: 'Fail', [CallStates.ACTIVE]: 'Runs', [CallStates.WAITING]: 'Runs', - }[status]; + }[status!]; return ( {badgeText} diff --git a/code/addons/test/src/components/Subnav.tsx b/code/addons/test/src/components/Subnav.tsx index bf9d8436cee0..88fcbd5c4522 100644 --- a/code/addons/test/src/components/Subnav.tsx +++ b/code/addons/test/src/components/Subnav.tsx @@ -109,7 +109,7 @@ const RerunButton = styled(StyledIconButton)< >(({ theme, animating, disabled }) => ({ opacity: disabled ? 0.5 : 1, svg: { - animation: animating && `${theme.animation.rotate360} 200ms ease-out`, + animation: animating ? `${theme.animation.rotate360} 200ms ease-out` : undefined, }, })); diff --git a/code/addons/test/src/components/TestDiscrepancyMessage.tsx b/code/addons/test/src/components/TestDiscrepancyMessage.tsx index b23af4a7be6a..2ff2e97c9f77 100644 --- a/code/addons/test/src/components/TestDiscrepancyMessage.tsx +++ b/code/addons/test/src/components/TestDiscrepancyMessage.tsx @@ -1,13 +1,12 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import { Link } from 'storybook/internal/components'; import { useStorybookApi } from 'storybook/internal/manager-api'; import { styled } from 'storybook/internal/theming'; -import type { StoryId } from 'storybook/internal/types'; import { CallStates } from '@storybook/instrumenter'; -import { DOCUMENTATION_DISCREPANCY_LINK, STORYBOOK_ADDON_TEST_CHANNEL } from '../constants'; +import { DOCUMENTATION_DISCREPANCY_LINK } from '../constants'; const Wrapper = styled.div(({ theme: { color, typography, background } }) => ({ textAlign: 'start', @@ -32,7 +31,7 @@ const Wrapper = styled.div(({ theme: { color, typography, background } }) => ({ })); interface TestDiscrepancyMessageProps { - browserTestStatus: CallStates; + browserTestStatus?: CallStates; } export const TestDiscrepancyMessage = ({ browserTestStatus }: TestDiscrepancyMessageProps) => { diff --git a/code/addons/test/src/components/TestProviderRender.stories.tsx b/code/addons/test/src/components/TestProviderRender.stories.tsx index 1189248f3d53..5111c1507a49 100644 --- a/code/addons/test/src/components/TestProviderRender.stories.tsx +++ b/code/addons/test/src/components/TestProviderRender.stories.tsx @@ -43,7 +43,7 @@ const baseState: TestProviderState = { cancellable: true, cancelling: false, crashed: false, - error: null, + error: undefined, failed: false, running: false, watching: false, diff --git a/code/addons/test/src/components/TestProviderRender.tsx b/code/addons/test/src/components/TestProviderRender.tsx index 29b0f950da8c..4b1f4c35bc4d 100644 --- a/code/addons/test/src/components/TestProviderRender.tsx +++ b/code/addons/test/src/components/TestProviderRender.tsx @@ -162,7 +162,7 @@ export const TestProviderRender: FC< : undefined; const a11ySkippedAmount = - state.running || !state?.details.config?.a11y || !state.config.a11y + state.running || !state?.details.config?.a11y || !state.config?.a11y ? null : a11yResults?.filter((result) => !result).length; @@ -304,9 +304,11 @@ export const TestProviderRender: FC< const firstNotPassed = results.find( (r) => r.status === 'failed' || r.status === 'warning' ); - openPanel(firstNotPassed.storyId, PANEL_ID); + if (firstNotPassed) { + openPanel(firstNotPassed.storyId, PANEL_ID); + } } - : null + : undefined } icon={ state.crashed ? ( @@ -359,9 +361,11 @@ export const TestProviderRender: FC< (report) => report.status === 'failed' || report.status === 'warning' ) ); - openPanel(firstNotPassed.storyId, A11y_ADDON_PANEL_ID); + if (firstNotPassed) { + openPanel(firstNotPassed.storyId, A11y_ADDON_PANEL_ID); + } } - : null + : undefined } icon={} right={isStoryEntry ? null : a11yNotPassedAmount || null} diff --git a/code/addons/test/src/manager.tsx b/code/addons/test/src/manager.tsx index 26a8c8f42cd7..7264e5fe49cc 100644 --- a/code/addons/test/src/manager.tsx +++ b/code/addons/test/src/manager.tsx @@ -38,6 +38,7 @@ addons.register(ADDON_ID, (api) => { runnable: true, watchable: true, name: 'Component tests', + // @ts-expect-error: TODO: Fix types render: (state) => { const [isModalOpen, setModalOpen] = useState(false); return ( @@ -55,6 +56,7 @@ addons.register(ADDON_ID, (api) => { ); }, + // @ts-expect-error: TODO: Fix types sidebarContextMenu: ({ context, state }) => { if (context.type === 'docs') { return null; @@ -72,6 +74,7 @@ addons.register(ADDON_ID, (api) => { ); }, + // @ts-expect-error: TODO: Fix types stateUpdater: (state, update) => { const updated = { ...state, @@ -89,6 +92,7 @@ addons.register(ADDON_ID, (api) => { await api.experimental_updateStatus( TEST_PROVIDER_ID, Object.fromEntries( + // @ts-expect-error: TODO: Fix types update.details.testResults.flatMap((testResult) => testResult.results .filter(({ storyId }) => storyId) @@ -113,6 +117,7 @@ addons.register(ADDON_ID, (api) => { await api.experimental_updateStatus( 'storybook/addon-a11y/test-provider', Object.fromEntries( + // @ts-expect-error: TODO: Fix types update.details.testResults.flatMap((testResult) => testResult.results .filter(({ storyId }) => storyId) @@ -143,7 +148,7 @@ addons.register(ADDON_ID, (api) => { return updated; }, - } as Addon_TestProviderType); + } satisfies Omit, 'id'>); } const filter = ({ state }: Combo) => { @@ -158,7 +163,7 @@ addons.register(ADDON_ID, (api) => { match: ({ viewMode }) => viewMode === 'story', render: ({ active }) => { return ( - + {({ storyId }) => } ); diff --git a/code/addons/test/src/node/boot-test-runner.ts b/code/addons/test/src/node/boot-test-runner.ts index 3f0329807e98..f1fe3ddc94f3 100644 --- a/code/addons/test/src/node/boot-test-runner.ts +++ b/code/addons/test/src/node/boot-test-runner.ts @@ -24,7 +24,7 @@ const MAX_START_TIME = 30000; const vitestModulePath = join(__dirname, 'node', 'vitest.mjs'); // Events that were triggered before Vitest was ready are queued up and resent once it's ready -const eventQueue: { type: string; args: any[] }[] = []; +const eventQueue: { type: string; args?: any[] }[] = []; let child: null | ChildProcess; let ready = false; @@ -87,7 +87,7 @@ const bootTestRunner = async (channel: Channel) => { if (result.type === 'ready') { // Resend events that triggered (during) the boot sequence, now that Vitest is ready while (eventQueue.length) { - const { type, args } = eventQueue.shift(); + const { type, args } = eventQueue.shift()!; child?.send({ type, args, from: 'server' }); } diff --git a/code/addons/test/src/node/reporter.ts b/code/addons/test/src/node/reporter.ts index ecda9ab4a672..1f71e1bf9670 100644 --- a/code/addons/test/src/node/reporter.ts +++ b/code/addons/test/src/node/reporter.ts @@ -220,7 +220,7 @@ export class StorybookReporter implements Reporter { (t) => t.status === 'failed' && t.results.length === 0 ); - const reducedTestSuiteFailures = new Set(); + const reducedTestSuiteFailures = new Set(); testSuiteFailures.forEach((t) => { reducedTestSuiteFailures.add(t.message); @@ -240,7 +240,7 @@ export class StorybookReporter implements Reporter { message: Array.from(reducedTestSuiteFailures).reduce( (acc, curr) => `${acc}\n${curr}`, '' - ), + )!, } : { name: `${unhandledErrors.length} unhandled error${unhandledErrors?.length > 1 ? 's' : ''}`, diff --git a/code/addons/test/src/node/test-manager.test.ts b/code/addons/test/src/node/test-manager.test.ts index 7056aee5666b..985f74c97595 100644 --- a/code/addons/test/src/node/test-manager.test.ts +++ b/code/addons/test/src/node/test-manager.test.ts @@ -22,10 +22,11 @@ const vitest = vi.hoisted(() => ({ configOverride: { actualTestNamePattern: undefined, get testNamePattern() { - return this.actualTestNamePattern; + return this.actualTestNamePattern!; }, set testNamePattern(value: string) { setTestNamePattern(value); + // @ts-expect-error Ignore for testing this.actualTestNamePattern = value; }, }, diff --git a/code/addons/test/src/node/vitest-manager.ts b/code/addons/test/src/node/vitest-manager.ts index 1e405ea9c344..4145acf18a3f 100644 --- a/code/addons/test/src/node/vitest-manager.ts +++ b/code/addons/test/src/node/vitest-manager.ts @@ -88,7 +88,7 @@ export class VitestManager { try { await this.vitest.init(); - } catch (e) { + } catch (e: any) { let message = 'Failed to initialize Vitest'; const isV8 = e.message?.includes('@vitest/coverage-v8'); const isIstanbul = e.message?.includes('@vitest/coverage-istanbul'); @@ -148,7 +148,7 @@ export class VitestManager { ])) as StoryIndex; const storyIds = requestStoryIds || Object.keys(index.entries); return storyIds.map((id) => index.entries[id]).filter((story) => story.type === 'story'); - } catch (e) { + } catch (e: any) { log('Failed to fetch story index: ' + e.message); return []; } @@ -330,7 +330,7 @@ export class VitestManager { async registerVitestConfigListener() { this.vitest?.server?.watcher.on('change', async (file) => { file = normalize(file); - const isConfig = file === this.vitest.server.config.configFile; + const isConfig = file === this.vitest?.server.config.configFile; if (isConfig) { log('Restarting Vitest due to config change'); await this.closeVitest(); diff --git a/code/addons/test/src/vitest-plugin/global-setup.ts b/code/addons/test/src/vitest-plugin/global-setup.ts index ca287c105e2b..8526c48e245e 100644 --- a/code/addons/test/src/vitest-plugin/global-setup.ts +++ b/code/addons/test/src/vitest-plugin/global-setup.ts @@ -74,13 +74,15 @@ export const teardown = async () => { logger.verbose('Stopping Storybook process'); await new Promise((resolve, reject) => { // Storybook starts multiple child processes, so we need to kill the whole tree - treeKill(storybookProcess.pid, 'SIGTERM', (error) => { - if (error) { - logger.error('Failed to stop Storybook process:'); - reject(error); - return; - } - resolve(); - }); + if (storybookProcess?.pid) { + treeKill(storybookProcess.pid, 'SIGTERM', (error) => { + if (error) { + logger.error('Failed to stop Storybook process:'); + reject(error); + return; + } + resolve(); + }); + } }); }; diff --git a/code/addons/test/src/vitest-plugin/index.ts b/code/addons/test/src/vitest-plugin/index.ts index e73549ded6d9..180075f9f891 100644 --- a/code/addons/test/src/vitest-plugin/index.ts +++ b/code/addons/test/src/vitest-plugin/index.ts @@ -193,6 +193,7 @@ export const storybookTest = async (options?: UserOptions): Promise => { } : {}), + // @ts-expect-error: TODO browser: { ...inputConfig_ONLY_MUTATE_WHEN_STRICTLY_NEEDED_OR_YOU_WILL_BE_FIRED.test?.browser, commands: { @@ -266,9 +267,11 @@ export const storybookTest = async (options?: UserOptions): Promise => { // alert the user of problems if ( - inputConfig_ONLY_MUTATE_WHEN_STRICTLY_NEEDED_OR_YOU_WILL_BE_FIRED.test.include?.length > 0 + (inputConfig_ONLY_MUTATE_WHEN_STRICTLY_NEEDED_OR_YOU_WILL_BE_FIRED.test?.include?.length ?? + 0) > 0 ) { // remove the user's existing include, because we're replacing it with our own heuristic based on main.ts#stories + // @ts-expect-error: Ignore inputConfig_ONLY_MUTATE_WHEN_STRICTLY_NEEDED_OR_YOU_WILL_BE_FIRED.test.include = []; console.log( picocolors.yellow(dedent` @@ -285,19 +288,21 @@ export const storybookTest = async (options?: UserOptions): Promise => { return config; }, async configureServer(server) { - for (const staticDir of staticDirs) { - try { - const { staticPath, targetEndpoint } = mapStaticDir(staticDir, directories.configDir); - server.middlewares.use( - targetEndpoint, - sirv(staticPath, { - dev: true, - etag: true, - extensions: [], - }) - ); - } catch (e) { - console.warn(e); + if (staticDirs) { + for (const staticDir of staticDirs) { + try { + const { staticPath, targetEndpoint } = mapStaticDir(staticDir, directories.configDir); + server.middlewares.use( + targetEndpoint, + sirv(staticPath, { + dev: true, + etag: true, + extensions: [], + }) + ); + } catch (e) { + console.warn(e); + } } } }, diff --git a/code/addons/test/src/vitest-plugin/viewports.ts b/code/addons/test/src/vitest-plugin/viewports.ts index a8bcc90bc408..905ee44fc937 100644 --- a/code/addons/test/src/vitest-plugin/viewports.ts +++ b/code/addons/test/src/vitest-plugin/viewports.ts @@ -85,7 +85,7 @@ export const setViewport = async (parameters: Parameters = {}, globals: Globals let viewportWidth = DEFAULT_VIEWPORT_DIMENSIONS.width; let viewportHeight = DEFAULT_VIEWPORT_DIMENSIONS.height; - if (defaultViewport in viewports) { + if (defaultViewport && defaultViewport in viewports) { const styles = viewports[defaultViewport].styles as ViewportStyles; if (styles?.width && styles?.height) { const { width, height } = styles; diff --git a/code/addons/test/tsconfig.json b/code/addons/test/tsconfig.json index 060b5d432fc7..e8a15eafa0bd 100644 --- a/code/addons/test/tsconfig.json +++ b/code/addons/test/tsconfig.json @@ -5,7 +5,7 @@ "module": "Preserve", "moduleResolution": "Bundler", "types": ["vitest"], - "strict": false + "strict": true }, "include": ["src/**/*", "./typings.d.ts"] }