.
+ */
+ get name() {
+ const paddedCode = String(this.code).padStart(4, '0');
+ return `SB_${this.category}_${paddedCode}` as `SB_${this['category']}_${string}`;
+ }
+
+ /**
+ * Generates the error message along with additional documentation link (if applicable).
+ */
+ get message() {
+ let page: string | undefined;
+
+ if (this.documentation === true) {
+ page = `https://storybook.js.org/error/${this.name}`;
+ } else if (typeof this.documentation === 'string') {
+ page = this.documentation;
+ }
+
+ return this.template() + (page != null ? `\n\nMore info: ${page}` : '');
+ }
+}
diff --git a/code/lib/core-events/src/index.ts b/code/lib/core-events/src/index.ts
index beec768961de..d8d6c41c01d3 100644
--- a/code/lib/core-events/src/index.ts
+++ b/code/lib/core-events/src/index.ts
@@ -69,6 +69,7 @@ enum events {
RESULT_WHATS_NEW_DATA = 'resultWhatsNewData',
SET_WHATS_NEW_CACHE = 'setWhatsNewCache',
TOGGLE_WHATS_NEW_NOTIFICATIONS = 'toggleWhatsNewNotifications',
+ TELEMETRY_ERROR = 'telemetryError',
}
// Enables: `import Events from ...`
@@ -120,9 +121,11 @@ export const {
RESULT_WHATS_NEW_DATA,
SET_WHATS_NEW_CACHE,
TOGGLE_WHATS_NEW_NOTIFICATIONS,
+ TELEMETRY_ERROR,
} = events;
// Used to break out of the current render without showing a redbox
+// eslint-disable-next-line local-rules/no-uncategorized-errors
export const IGNORED_EXCEPTION = new Error('ignoredException');
export interface WhatsNewCache {
diff --git a/code/lib/core-server/package.json b/code/lib/core-server/package.json
index 571923980407..305d010bb28f 100644
--- a/code/lib/core-server/package.json
+++ b/code/lib/core-server/package.json
@@ -1,6 +1,6 @@
{
"name": "@storybook/core-server",
- "version": "7.3.0-alpha.0",
+ "version": "7.4.0-alpha.0",
"description": "Storybook framework-agnostic API",
"keywords": [
"storybook"
@@ -47,7 +47,8 @@
"public/**/*",
"README.md",
"*.js",
- "*.d.ts"
+ "*.d.ts",
+ "!src/**/*"
],
"scripts": {
"check": "../../../scripts/prepare/check.ts",
@@ -89,7 +90,7 @@
"read-pkg-up": "^7.0.1",
"semver": "^7.3.7",
"serve-favicon": "^2.5.0",
- "telejson": "^7.0.3",
+ "telejson": "^7.2.0",
"tiny-invariant": "^1.3.1",
"ts-dedent": "^2.0.0",
"util": "^0.12.4",
diff --git a/code/lib/core-server/src/presets/common-preset.ts b/code/lib/core-server/src/presets/common-preset.ts
index fcb4b5681571..36816316ccbf 100644
--- a/code/lib/core-server/src/presets/common-preset.ts
+++ b/code/lib/core-server/src/presets/common-preset.ts
@@ -25,6 +25,7 @@ import type { WhatsNewCache, WhatsNewData } from '@storybook/core-events';
import {
REQUEST_WHATS_NEW_DATA,
RESULT_WHATS_NEW_DATA,
+ TELEMETRY_ERROR,
SET_WHATS_NEW_CACHE,
TOGGLE_WHATS_NEW_NOTIFICATIONS,
} from '@storybook/core-events';
@@ -195,7 +196,7 @@ export const features = async (
});
export const csfIndexer: Indexer = {
- test: /\.stories\.(m?js|ts)x?$/,
+ test: /\.(stories|story)\.(m?js|ts)x?$/,
index: async (fileName, options) => (await readCsf(fileName, options)).parse().indexInputs,
};
@@ -329,5 +330,17 @@ export const experimental_serverChannel = async (
}
);
+ channel.on(TELEMETRY_ERROR, async (error) => {
+ const isTelemetryEnabled = coreOptions.disableTelemetry !== true;
+
+ if (isTelemetryEnabled) {
+ await sendTelemetryError(error, 'browser', {
+ cliOptions: options,
+ presetOptions: { ...options, corePresets: [], overridePresets: [] },
+ skipPrompt: true,
+ });
+ }
+ });
+
return channel;
};
diff --git a/code/lib/core-server/src/utils/StoryIndexGenerator.test.ts b/code/lib/core-server/src/utils/StoryIndexGenerator.test.ts
index 7110743b7003..b1097c168a86 100644
--- a/code/lib/core-server/src/utils/StoryIndexGenerator.test.ts
+++ b/code/lib/core-server/src/utils/StoryIndexGenerator.test.ts
@@ -99,6 +99,36 @@ describe('StoryIndexGenerator', () => {
`);
});
});
+ describe('single file .story specifier', () => {
+ it('extracts stories from the right files', async () => {
+ const specifier: NormalizedStoriesSpecifier = normalizeStoriesEntry(
+ './src/F.story.ts',
+ options
+ );
+
+ const generator = new StoryIndexGenerator([specifier], options);
+ await generator.initialize();
+
+ expect(await generator.getIndex()).toMatchInlineSnapshot(`
+ Object {
+ "entries": Object {
+ "f--story-one": Object {
+ "id": "f--story-one",
+ "importPath": "./src/F.story.ts",
+ "name": "Story One",
+ "tags": Array [
+ "autodocs",
+ "story",
+ ],
+ "title": "F",
+ "type": "story",
+ },
+ },
+ "v": 4,
+ }
+ `);
+ });
+ });
describe('non-recursive specifier', () => {
it('extracts stories from the right files', async () => {
const specifier: NormalizedStoriesSpecifier = normalizeStoriesEntry(
diff --git a/code/lib/core-server/src/utils/StoryIndexGenerator.ts b/code/lib/core-server/src/utils/StoryIndexGenerator.ts
index 2f95114113ad..d2a195a31463 100644
--- a/code/lib/core-server/src/utils/StoryIndexGenerator.ts
+++ b/code/lib/core-server/src/utils/StoryIndexGenerator.ts
@@ -338,8 +338,7 @@ export class StoryIndexGenerator {
autodocs === true || (autodocs === 'tag' && hasAutodocsTag) || isStoriesMdx;
if (createDocEntry) {
- const name = this.options.docs.defaultName;
- invariant(name, 'expected a defaultName property in options.docs');
+ const name = this.options.docs.defaultName ?? 'Docs';
const { metaId } = indexInputs[0];
const { title } = entries[0];
const tags = indexInputs[0].tags || [];
@@ -407,8 +406,7 @@ export class StoryIndexGenerator {
// a) it is a stories.mdx transpiled to CSF, OR
// b) we have docs page enabled for this file
if (componentTags.includes(STORIES_MDX_TAG) || autodocsOptedIn) {
- const name = this.options.docs.defaultName;
- invariant(name, 'expected a defaultName property in options.docs');
+ const name = this.options.docs.defaultName ?? 'Docs';
invariant(csf.meta.title, 'expected a title property in csf.meta');
const id = toId(csf.meta.id || csf.meta.title, name);
entries.unshift({
@@ -511,8 +509,7 @@ export class StoryIndexGenerator {
title,
"makeTitle created an undefined title. This happens when a specifier's doesn't have any matches in its fileName"
);
- const { defaultName } = this.options.docs;
- invariant(defaultName, 'expected a defaultName property in options.docs');
+ const defaultName = this.options.docs.defaultName ?? 'Docs';
const name =
result.name ||
diff --git a/code/lib/core-server/src/utils/__mockdata__/src/F.story.ts b/code/lib/core-server/src/utils/__mockdata__/src/F.story.ts
new file mode 100644
index 000000000000..bb14d42c7112
--- /dev/null
+++ b/code/lib/core-server/src/utils/__mockdata__/src/F.story.ts
@@ -0,0 +1,7 @@
+const component = {};
+export default {
+ component,
+ tags: ['autodocs'],
+};
+
+export const StoryOne = {};
diff --git a/code/lib/core-server/src/withTelemetry.ts b/code/lib/core-server/src/withTelemetry.ts
index 706439b4bfd7..49b8c6c79699 100644
--- a/code/lib/core-server/src/withTelemetry.ts
+++ b/code/lib/core-server/src/withTelemetry.ts
@@ -85,6 +85,25 @@ export async function sendTelemetryError(
error instanceof Error,
'The error passed to sendTelemetryError was not an Error, please only send Errors'
);
+
+ let storybookErrorProperties = {};
+ // if it's an UNCATEGORIZED error, it won't have a coded name, so we just pass the category and source
+ if ((error as any).category) {
+ const { category } = error as any;
+ storybookErrorProperties = {
+ category,
+ };
+ }
+
+ if ((error as any).fromStorybook) {
+ const { code, name } = error as any;
+ storybookErrorProperties = {
+ ...storybookErrorProperties,
+ code,
+ name,
+ };
+ }
+
await telemetry(
'error',
{
@@ -92,6 +111,7 @@ export async function sendTelemetryError(
precedingUpgrade,
error: errorLevel === 'full' ? error : undefined,
errorHash: oneWayHash(error.message),
+ ...storybookErrorProperties,
},
{
immediate: true,
diff --git a/code/lib/core-webpack/package.json b/code/lib/core-webpack/package.json
index 0e0a5eaadc29..3f1ddffc3a66 100644
--- a/code/lib/core-webpack/package.json
+++ b/code/lib/core-webpack/package.json
@@ -1,6 +1,6 @@
{
"name": "@storybook/core-webpack",
- "version": "7.3.0-alpha.0",
+ "version": "7.4.0-alpha.0",
"description": "Storybook framework-agnostic API",
"keywords": [
"storybook"
@@ -36,7 +36,8 @@
"templates/**/*",
"README.md",
"*.js",
- "*.d.ts"
+ "*.d.ts",
+ "!src/**/*"
],
"scripts": {
"check": "../../../scripts/prepare/check.ts",
diff --git a/code/lib/csf-plugin/package.json b/code/lib/csf-plugin/package.json
index b1dad9ee786e..bb2cbd8f9c37 100644
--- a/code/lib/csf-plugin/package.json
+++ b/code/lib/csf-plugin/package.json
@@ -1,6 +1,6 @@
{
"name": "@storybook/csf-plugin",
- "version": "7.3.0-alpha.0",
+ "version": "7.4.0-alpha.0",
"description": "Enrich CSF files via static analysis",
"keywords": [
"storybook"
@@ -36,7 +36,8 @@
"dist/**/*",
"README.md",
"*.js",
- "*.d.ts"
+ "*.d.ts",
+ "!src/**/*"
],
"scripts": {
"check": "../../../scripts/prepare/check.ts",
diff --git a/code/lib/csf-tools/package.json b/code/lib/csf-tools/package.json
index de1e835e7360..ed12dbfb8c70 100644
--- a/code/lib/csf-tools/package.json
+++ b/code/lib/csf-tools/package.json
@@ -1,6 +1,6 @@
{
"name": "@storybook/csf-tools",
- "version": "7.3.0-alpha.0",
+ "version": "7.4.0-alpha.0",
"description": "Parse and manipulate CSF and Storybook config files",
"keywords": [
"storybook"
@@ -34,7 +34,8 @@
"dist/**/*",
"README.md",
"*.js",
- "*.d.ts"
+ "*.d.ts",
+ "!src/**/*"
],
"scripts": {
"check": "../../../scripts/prepare/check.ts",
diff --git a/code/lib/docs-tools/package.json b/code/lib/docs-tools/package.json
index 1bf9432eadee..49d36403f753 100644
--- a/code/lib/docs-tools/package.json
+++ b/code/lib/docs-tools/package.json
@@ -1,6 +1,6 @@
{
"name": "@storybook/docs-tools",
- "version": "7.3.0-alpha.0",
+ "version": "7.4.0-alpha.0",
"description": "Shared utility functions for frameworks to implement docs",
"keywords": [
"storybook"
@@ -36,7 +36,8 @@
"dist/**/*",
"README.md",
"*.js",
- "*.d.ts"
+ "*.d.ts",
+ "!src/**/*"
],
"scripts": {
"check": "../../../scripts/prepare/check.ts",
diff --git a/code/lib/instrumenter/package.json b/code/lib/instrumenter/package.json
index e72d73f10729..22f218c85f73 100644
--- a/code/lib/instrumenter/package.json
+++ b/code/lib/instrumenter/package.json
@@ -1,6 +1,6 @@
{
"name": "@storybook/instrumenter",
- "version": "7.3.0-alpha.0",
+ "version": "7.4.0-alpha.0",
"description": "",
"keywords": [
"storybook"
@@ -36,7 +36,8 @@
"dist/**/*",
"README.md",
"*.js",
- "*.d.ts"
+ "*.d.ts",
+ "!src/**/*"
],
"scripts": {
"check": "../../../scripts/prepare/check.ts",
diff --git a/code/lib/manager-api/package.json b/code/lib/manager-api/package.json
index 40b6bd44ea96..95ac20b8a0bb 100644
--- a/code/lib/manager-api/package.json
+++ b/code/lib/manager-api/package.json
@@ -1,6 +1,6 @@
{
"name": "@storybook/manager-api",
- "version": "7.3.0-alpha.0",
+ "version": "7.4.0-alpha.0",
"description": "Core Storybook Manager API & Context",
"keywords": [
"storybook"
@@ -35,7 +35,8 @@
"dist/**/*",
"README.md",
"*.js",
- "*.d.ts"
+ "*.d.ts",
+ "!src/**/*"
],
"scripts": {
"check": "../../../scripts/prepare/check.ts",
@@ -55,7 +56,7 @@
"memoizerific": "^1.11.3",
"semver": "^7.3.7",
"store2": "^2.14.2",
- "telejson": "^7.0.3",
+ "telejson": "^7.2.0",
"ts-dedent": "^2.0.0"
},
"devDependencies": {
diff --git a/code/lib/manager-api/src/index.tsx b/code/lib/manager-api/src/index.tsx
index 52c2f3000986..688b4a2d6837 100644
--- a/code/lib/manager-api/src/index.tsx
+++ b/code/lib/manager-api/src/index.tsx
@@ -65,6 +65,7 @@ import * as version from './modules/versions';
import * as whatsnew from './modules/whatsnew';
import * as globals from './modules/globals';
+import type { ModuleFn } from './lib/types';
export * from './lib/shortcut';
@@ -76,14 +77,6 @@ export { ActiveTabs };
export const ManagerContext = createContext({ api: undefined, state: getInitialState({}) });
-export type ModuleArgs = RouterData &
- API_ProviderData & {
- mode?: 'production' | 'development';
- state: State;
- fullAPI: API;
- store: Store;
- };
-
export type State = layout.SubState &
stories.SubState &
refs.SubState &
@@ -152,28 +145,10 @@ export const combineParameters = (...parameterSets: Parameters[]) =>
return undefined;
});
-interface ModuleWithInit {
- init: () => void | Promise;
- api: APIType;
- state: StateType;
-}
-
-type ModuleWithoutInit = Omit<
- ModuleWithInit,
- 'init'
->;
-
-export type ModuleFn = (
- m: ModuleArgs,
- options?: any
-) => HasInit extends true
- ? ModuleWithInit
- : ModuleWithoutInit;
-
class ManagerProvider extends Component {
api: API = {} as API;
- modules: (ModuleWithInit | ModuleWithoutInit)[];
+ modules: ReturnType[];
static displayName = 'Manager';
diff --git a/code/lib/manager-api/src/lib/types.tsx b/code/lib/manager-api/src/lib/types.tsx
new file mode 100644
index 000000000000..a195f514999e
--- /dev/null
+++ b/code/lib/manager-api/src/lib/types.tsx
@@ -0,0 +1,22 @@
+import type { API_ProviderData } from '@storybook/types';
+import type { RouterData } from '@storybook/router';
+
+import type { API, State } from '../index';
+import type Store from '../store';
+
+export type ModuleFn = (
+ m: ModuleArgs,
+ options?: any
+) => {
+ init?: () => void | Promise;
+ api: APIType;
+ state: StateType;
+};
+
+export type ModuleArgs = RouterData &
+ API_ProviderData & {
+ mode?: 'production' | 'development';
+ state: State;
+ fullAPI: API;
+ store: Store;
+ };
diff --git a/code/lib/manager-api/src/modules/addons.ts b/code/lib/manager-api/src/modules/addons.ts
index 87027262d631..84fd51d7f206 100644
--- a/code/lib/manager-api/src/modules/addons.ts
+++ b/code/lib/manager-api/src/modules/addons.ts
@@ -7,7 +7,7 @@ import type {
API_StateMerger,
} from '@storybook/types';
import { Addon_TypesEnum } from '@storybook/types';
-import type { ModuleFn } from '../index';
+import type { ModuleFn } from '../lib/types';
import type { Options } from '../store';
export interface SubState {
diff --git a/code/lib/manager-api/src/modules/channel.ts b/code/lib/manager-api/src/modules/channel.ts
index e6c178ae32b4..c83c342c5253 100644
--- a/code/lib/manager-api/src/modules/channel.ts
+++ b/code/lib/manager-api/src/modules/channel.ts
@@ -3,7 +3,8 @@ import { STORIES_COLLAPSE_ALL, STORIES_EXPAND_ALL } from '@storybook/core-events
import type { Listener } from '@storybook/channels';
import type { API_Provider } from '@storybook/types';
-import type { API, ModuleFn } from '../index';
+import type { API } from '../index';
+import type { ModuleFn } from '../lib/types';
export interface SubAPI {
/**
diff --git a/code/lib/manager-api/src/modules/globals.ts b/code/lib/manager-api/src/modules/globals.ts
index 9b8d47069564..393deb58c4a2 100644
--- a/code/lib/manager-api/src/modules/globals.ts
+++ b/code/lib/manager-api/src/modules/globals.ts
@@ -3,7 +3,7 @@ import { logger } from '@storybook/client-logger';
import { dequal as deepEqual } from 'dequal';
import type { SetGlobalsPayload, Globals, GlobalTypes } from '@storybook/types';
-import type { ModuleFn } from '../index';
+import type { ModuleFn } from '../lib/types';
// eslint-disable-next-line import/no-cycle
import { getEventMetadata } from '../lib/events';
@@ -32,7 +32,7 @@ export interface SubAPI {
updateGlobals: (newGlobals: Globals) => void;
}
-export const init: ModuleFn = ({ store, fullAPI }) => {
+export const init: ModuleFn = ({ store, fullAPI, provider }) => {
const api: SubAPI = {
getGlobals() {
return store.getState().globals;
@@ -42,7 +42,7 @@ export const init: ModuleFn = ({ store, fullAPI }) => {
},
updateGlobals(newGlobals) {
// Only emit the message to the local ref
- fullAPI.emit(UPDATE_GLOBALS, {
+ provider.channel.emit(UPDATE_GLOBALS, {
globals: newGlobals,
options: {
target: 'storybook-preview-iframe',
@@ -62,8 +62,9 @@ export const init: ModuleFn = ({ store, fullAPI }) => {
}
};
- const initModule = () => {
- fullAPI.on(GLOBALS_UPDATED, function handleGlobalsUpdated({ globals }: { globals: Globals }) {
+ provider.channel.on(
+ GLOBALS_UPDATED,
+ function handleGlobalsUpdated({ globals }: { globals: Globals }) {
const { ref } = getEventMetadata(this, fullAPI);
if (!ref) {
@@ -73,10 +74,13 @@ export const init: ModuleFn = ({ store, fullAPI }) => {
'received a GLOBALS_UPDATED from a non-local ref. This is not currently supported.'
);
}
- });
+ }
+ );
- // Emitted by the preview on initialization
- fullAPI.on(SET_GLOBALS, function handleSetStories({ globals, globalTypes }: SetGlobalsPayload) {
+ // Emitted by the preview on initialization
+ provider.channel.on(
+ SET_GLOBALS,
+ function handleSetStories({ globals, globalTypes }: SetGlobalsPayload) {
const { ref } = getEventMetadata(this, fullAPI);
const currentGlobals = store.getState()?.globals;
@@ -93,12 +97,11 @@ export const init: ModuleFn = ({ store, fullAPI }) => {
) {
api.updateGlobals(currentGlobals);
}
- });
- };
+ }
+ );
return {
api,
state,
- init: initModule,
};
};
diff --git a/code/lib/manager-api/src/modules/layout.ts b/code/lib/manager-api/src/modules/layout.ts
index bbe43af9863d..f37e51e19942 100644
--- a/code/lib/manager-api/src/modules/layout.ts
+++ b/code/lib/manager-api/src/modules/layout.ts
@@ -7,7 +7,8 @@ import type { ThemeVars } from '@storybook/theming';
import type { API_Layout, API_PanelPositions, API_UI } from '@storybook/types';
import merge from '../lib/merge';
-import type { State, ModuleFn } from '../index';
+import type { State } from '../index';
+import type { ModuleFn } from '../lib/types';
const { document } = global;
@@ -284,7 +285,7 @@ export const init: ModuleFn = ({ store, provider, singleStory, fullAPI }) => {
state: merge(api.getInitialOptions(), persisted),
init: () => {
api.setOptions(merge(api.getInitialOptions(), persisted));
- fullAPI.on(SET_CONFIG, () => {
+ provider.channel.on(SET_CONFIG, () => {
api.setOptions(merge(api.getInitialOptions(), persisted));
});
},
diff --git a/code/lib/manager-api/src/modules/notifications.ts b/code/lib/manager-api/src/modules/notifications.ts
index 1f1059dc1939..83e95d3928ca 100644
--- a/code/lib/manager-api/src/modules/notifications.ts
+++ b/code/lib/manager-api/src/modules/notifications.ts
@@ -1,5 +1,5 @@
import type { API_Notification } from '@storybook/types';
-import type { ModuleFn } from '../index';
+import type { ModuleFn } from '../lib/types';
export interface SubState {
notifications: API_Notification[];
diff --git a/code/lib/manager-api/src/modules/provider.ts b/code/lib/manager-api/src/modules/provider.ts
index 272fc0d1839c..c150bf90bf8b 100644
--- a/code/lib/manager-api/src/modules/provider.ts
+++ b/code/lib/manager-api/src/modules/provider.ts
@@ -1,11 +1,11 @@
import type { API_IframeRenderer } from '@storybook/types';
-import type { ModuleFn } from '../index';
+import type { ModuleFn } from '../lib/types';
export interface SubAPI {
renderPreview?: API_IframeRenderer;
}
-export const init: ModuleFn = ({ provider, fullAPI }) => {
+export const init: ModuleFn = ({ provider, fullAPI }) => {
return {
api: provider.renderPreview ? { renderPreview: provider.renderPreview } : {},
state: {},
diff --git a/code/lib/manager-api/src/modules/refs.ts b/code/lib/manager-api/src/modules/refs.ts
index 5fe2462dfbb3..3ca68f09be89 100644
--- a/code/lib/manager-api/src/modules/refs.ts
+++ b/code/lib/manager-api/src/modules/refs.ts
@@ -15,7 +15,7 @@ import {
transformStoryIndexToStoriesHash,
} from '../lib/stories';
-import type { ModuleFn } from '../index';
+import type { ModuleFn } from '../lib/types';
const { location, fetch } = global;
@@ -154,7 +154,7 @@ const map = (
return input;
};
-export const init: ModuleFn = (
+export const init: ModuleFn = (
{ store, provider, singleStory, docsOptions = {} },
{ runCheck = true } = {}
) => {
diff --git a/code/lib/manager-api/src/modules/settings.ts b/code/lib/manager-api/src/modules/settings.ts
index 2439a5954bcc..4c850f9aca1a 100644
--- a/code/lib/manager-api/src/modules/settings.ts
+++ b/code/lib/manager-api/src/modules/settings.ts
@@ -1,5 +1,5 @@
import type { API_Settings, StoryId } from '@storybook/types';
-import type { ModuleFn } from '../index';
+import type { ModuleFn } from '../lib/types';
export interface SubAPI {
storeSelection: () => void;
@@ -78,5 +78,8 @@ export const init: ModuleFn = ({ store, navigate, fullAPI }) =
},
};
- return { state: { settings: { lastTrackedStoryId: null } }, api };
+ return {
+ state: { settings: { lastTrackedStoryId: null } },
+ api,
+ };
};
diff --git a/code/lib/manager-api/src/modules/shortcuts.ts b/code/lib/manager-api/src/modules/shortcuts.ts
index 8dcf942f4bc7..be5592e98118 100644
--- a/code/lib/manager-api/src/modules/shortcuts.ts
+++ b/code/lib/manager-api/src/modules/shortcuts.ts
@@ -2,7 +2,7 @@
import { global } from '@storybook/global';
import { FORCE_REMOUNT, PREVIEW_KEYDOWN } from '@storybook/core-events';
-import type { ModuleFn } from '../index';
+import type { ModuleFn } from '../lib/types';
import type { KeyboardEventLike } from '../lib/shortcut';
import { shortcutMatchesShortcut, eventToShortcut } from '../lib/shortcut';
@@ -152,7 +152,7 @@ function focusInInput(event: KeyboardEvent) {
return /input|textarea/i.test(target.tagName) || target.getAttribute('contenteditable') !== null;
}
-export const init: ModuleFn = ({ store, fullAPI }) => {
+export const init: ModuleFn = ({ store, fullAPI, provider }) => {
const api: SubAPI = {
// Getting and setting shortcuts
getShortcutKeys(): API_Shortcuts {
@@ -397,13 +397,13 @@ export const init: ModuleFn = ({ store, fullAPI }) => {
// Listen for keydown events in the manager
document.addEventListener('keydown', (event: KeyboardEvent) => {
if (!focusInInput(event)) {
- fullAPI.handleKeydownEvent(event);
+ api.handleKeydownEvent(event);
}
});
// Also listen to keydown events sent over the channel
- fullAPI.on(PREVIEW_KEYDOWN, (data: { event: KeyboardEventLike }) => {
- fullAPI.handleKeydownEvent(data.event);
+ provider.channel.on(PREVIEW_KEYDOWN, (data: { event: KeyboardEventLike }) => {
+ api.handleKeydownEvent(data.event);
});
};
diff --git a/code/lib/manager-api/src/modules/stories.ts b/code/lib/manager-api/src/modules/stories.ts
index 78b8b086e106..b295af6730db 100644
--- a/code/lib/manager-api/src/modules/stories.ts
+++ b/code/lib/manager-api/src/modules/stories.ts
@@ -21,6 +21,7 @@ import type {
API_ViewMode,
API_StatusState,
API_StatusUpdate,
+ API_FilterFunction,
} from '@storybook/types';
import {
PRELOAD_ENTRIES,
@@ -39,6 +40,7 @@ import {
STORY_MISSING,
DOCS_PREPARED,
SET_CURRENT_STORY,
+ SET_CONFIG,
} from '@storybook/core-events';
import { logger } from '@storybook/client-logger';
@@ -53,7 +55,8 @@ import {
addPreparedStories,
} from '../lib/stories';
-import type { ComposedRef, ModuleFn } from '../index';
+import type { ComposedRef } from '../index';
+import type { ModuleFn } from '../lib/types';
const { FEATURES, fetch } = global;
const STORY_INDEX_PATH = './index.json';
@@ -71,6 +74,7 @@ export interface SubState extends API_LoadedRefData {
storyId: StoryId;
viewMode: API_ViewMode;
status: API_StatusState;
+ filters: Record;
}
export interface SubAPI {
@@ -259,6 +263,14 @@ export interface SubAPI {
* @returns {Promise} A promise that resolves when the status has been updated.
*/
experimental_updateStatus: (addonId: string, update: API_StatusUpdate) => Promise;
+ /**
+ * Updates the filtering of the index.
+ *
+ * @param {string} addonId - The ID of the addon to update.
+ * @param {API_FilterFunction} filterFunction - A function that returns a boolean based on the story, index and status.
+ * @returns {Promise} A promise that resolves when the state has been updated.
+ */
+ experimental_setFilter: (addonId: string, filterFunction: API_FilterFunction) => Promise;
}
const removedOptions = ['enableShortcuts', 'theme', 'showRoots'];
@@ -278,7 +290,7 @@ function removeRemovedOptions = Record = ({
+export const init: ModuleFn = ({
fullAPI,
store,
navigate,
@@ -468,7 +480,7 @@ export const init: ModuleFn = ({
},
updateStoryArgs: (story, updatedArgs) => {
const { id: storyId, refId } = story;
- fullAPI.emit(UPDATE_STORY_ARGS, {
+ provider.channel.emit(UPDATE_STORY_ARGS, {
storyId,
updatedArgs,
options: { target: refId },
@@ -476,7 +488,7 @@ export const init: ModuleFn = ({
},
resetStoryArgs: (story, argNames?: [string]) => {
const { id: storyId, refId } = story;
- fullAPI.emit(RESET_STORY_ARGS, {
+ provider.channel.emit(RESET_STORY_ARGS, {
storyId,
argNames,
options: { target: refId },
@@ -495,7 +507,7 @@ export const init: ModuleFn = ({
return;
}
- await fullAPI.setIndex(storyIndex);
+ await api.setIndex(storyIndex);
} catch (err) {
await store.setState({ indexError: err });
}
@@ -503,7 +515,7 @@ export const init: ModuleFn = ({
// The story index we receive on SET_INDEX is "prepared" in that it has parameters
// The story index we receive on fetchStoryIndex is not, but all the prepared fields are optional
// so we can cast one to the other easily enough
- setIndex: async (storyIndex: API_PreparedStoryIndex) => {
+ setIndex: async (storyIndex) => {
const newHash = transformStoryIndexToStoriesHash(storyIndex, {
provider,
docsOptions,
@@ -556,7 +568,7 @@ export const init: ModuleFn = ({
await fullAPI.updateRef(refId, { index });
}
},
- setPreviewInitialized: async (ref?: ComposedRef): Promise => {
+ setPreviewInitialized: async (ref) => {
if (!ref) {
store.setState({ previewInitialized: true });
} else {
@@ -576,180 +588,193 @@ export const init: ModuleFn = ({
await store.setState({ status: newStatus }, { persistence: 'session' });
},
+ experimental_setFilter: async (id, filterFunction) => {
+ await store.setState({ filters: { ...store.getState().filters, [id]: filterFunction } });
+ },
};
- const initModule = async () => {
- // On initial load, the local iframe will select the first story (or other "selection specifier")
- // and emit STORY_SPECIFIED with the id. We need to ensure we respond to this change.
- fullAPI.on(
- STORY_SPECIFIED,
- function handler({
- storyId,
- viewMode,
- }: {
- storyId: string;
- viewMode: API_ViewMode;
- [k: string]: any;
- }) {
- const { sourceType } = getEventMetadata(this, fullAPI);
-
- if (sourceType === 'local') {
- const state = store.getState();
- const isCanvasRoute =
- state.path === '/' || state.viewMode === 'story' || state.viewMode === 'docs';
- const stateHasSelection = state.viewMode && state.storyId;
- const stateSelectionDifferent = state.viewMode !== viewMode || state.storyId !== storyId;
- /**
- * When storybook starts, we want to navigate to the first story.
- * But there are a few exceptions:
- * - If the current storyId and viewMode are already set/correct.
- * - If the user has navigated away already.
- * - If the user started storybook with a specific page-URL like "/settings/about"
- */
- if (isCanvasRoute) {
- if (stateHasSelection && stateSelectionDifferent) {
- // The manager state is correct, the preview state is lagging behind
- fullAPI.emit(SET_CURRENT_STORY, { storyId: state.storyId, viewMode: state.viewMode });
- } else if (stateSelectionDifferent) {
- // The preview state is correct, the manager state is lagging behind
- navigate(`/${viewMode}/${storyId}`);
- }
+ // On initial load, the local iframe will select the first story (or other "selection specifier")
+ // and emit STORY_SPECIFIED with the id. We need to ensure we respond to this change.
+ provider.channel.on(
+ STORY_SPECIFIED,
+ function handler({
+ storyId,
+ viewMode,
+ }: {
+ storyId: string;
+ viewMode: API_ViewMode;
+ [k: string]: any;
+ }) {
+ const { sourceType } = getEventMetadata(this, fullAPI);
+
+ if (sourceType === 'local') {
+ const state = store.getState();
+ const isCanvasRoute =
+ state.path === '/' || state.viewMode === 'story' || state.viewMode === 'docs';
+ const stateHasSelection = state.viewMode && state.storyId;
+ const stateSelectionDifferent = state.viewMode !== viewMode || state.storyId !== storyId;
+ /**
+ * When storybook starts, we want to navigate to the first story.
+ * But there are a few exceptions:
+ * - If the current storyId and viewMode are already set/correct.
+ * - If the user has navigated away already.
+ * - If the user started storybook with a specific page-URL like "/settings/about"
+ */
+ if (isCanvasRoute) {
+ if (stateHasSelection && stateSelectionDifferent) {
+ // The manager state is correct, the preview state is lagging behind
+ provider.channel.emit(SET_CURRENT_STORY, {
+ storyId: state.storyId,
+ viewMode: state.viewMode,
+ });
+ } else if (stateSelectionDifferent) {
+ // The preview state is correct, the manager state is lagging behind
+ navigate(`/${viewMode}/${storyId}`);
}
}
}
- );
-
- // The CURRENT_STORY_WAS_SET event is the best event to use to tell if a ref is ready.
- // Until the ref has a selection, it will not render anything (e.g. while waiting for
- // the preview.js file or the index to load). Once it has a selection, it will render its own
- // preparing spinner.
- fullAPI.on(CURRENT_STORY_WAS_SET, function handler() {
- const { ref } = getEventMetadata(this, fullAPI);
- fullAPI.setPreviewInitialized(ref);
- });
+ }
+ );
+
+ // The CURRENT_STORY_WAS_SET event is the best event to use to tell if a ref is ready.
+ // Until the ref has a selection, it will not render anything (e.g. while waiting for
+ // the preview.js file or the index to load). Once it has a selection, it will render its own
+ // preparing spinner.
+ provider.channel.on(CURRENT_STORY_WAS_SET, function handler() {
+ const { ref } = getEventMetadata(this, fullAPI);
+ api.setPreviewInitialized(ref);
+ });
- fullAPI.on(STORY_CHANGED, function handler() {
- const { sourceType } = getEventMetadata(this, fullAPI);
+ provider.channel.on(STORY_CHANGED, function handler() {
+ const { sourceType } = getEventMetadata(this, fullAPI);
- if (sourceType === 'local') {
- const options = fullAPI.getCurrentParameter('options');
+ if (sourceType === 'local') {
+ const options = api.getCurrentParameter('options');
- if (options) {
- fullAPI.setOptions(removeRemovedOptions(options));
- }
+ if (options) {
+ fullAPI.setOptions(removeRemovedOptions(options));
}
- });
+ }
+ });
- fullAPI.on(STORY_PREPARED, function handler({ id, ...update }: StoryPreparedPayload) {
- const { ref, sourceType } = getEventMetadata(this, fullAPI);
- fullAPI.updateStory(id, { ...update, prepared: true }, ref);
+ provider.channel.on(STORY_PREPARED, function handler({ id, ...update }: StoryPreparedPayload) {
+ const { ref, sourceType } = getEventMetadata(this, fullAPI);
+ api.updateStory(id, { ...update, prepared: true }, ref);
- if (!ref) {
- if (!store.getState().hasCalledSetOptions) {
- const { options } = update.parameters;
- fullAPI.setOptions(removeRemovedOptions(options));
- store.setState({ hasCalledSetOptions: true });
- }
+ if (!ref) {
+ if (!store.getState().hasCalledSetOptions) {
+ const { options } = update.parameters;
+ fullAPI.setOptions(removeRemovedOptions(options));
+ store.setState({ hasCalledSetOptions: true });
}
+ }
- if (sourceType === 'local') {
- const { storyId, index, refId } = store.getState();
-
- // create a list of related stories to be preloaded
- const toBePreloaded = Array.from(
- new Set([
- api.findSiblingStoryId(storyId, index, 1, true),
- api.findSiblingStoryId(storyId, index, -1, true),
- ])
- ).filter(Boolean);
-
- fullAPI.emit(PRELOAD_ENTRIES, {
- ids: toBePreloaded,
- options: { target: refId },
- });
- }
- });
+ if (sourceType === 'local') {
+ const { storyId, index, refId } = store.getState();
- fullAPI.on(DOCS_PREPARED, function handler({ id, ...update }: DocsPreparedPayload) {
- const { ref } = getEventMetadata(this, fullAPI);
- fullAPI.updateStory(id, { ...update, prepared: true }, ref);
- });
+ // create a list of related stories to be preloaded
+ const toBePreloaded = Array.from(
+ new Set([
+ api.findSiblingStoryId(storyId, index, 1, true),
+ api.findSiblingStoryId(storyId, index, -1, true),
+ ])
+ ).filter(Boolean);
- fullAPI.on(SET_INDEX, function handler(index: API_PreparedStoryIndex) {
- const { ref } = getEventMetadata(this, fullAPI);
+ provider.channel.emit(PRELOAD_ENTRIES, {
+ ids: toBePreloaded,
+ options: { target: refId },
+ });
+ }
+ });
- if (!ref) {
- fullAPI.setIndex(index);
- const options = fullAPI.getCurrentParameter('options');
- fullAPI.setOptions(removeRemovedOptions(options));
- } else {
- fullAPI.setRef(ref.id, { ...ref, storyIndex: index }, true);
- }
- });
+ provider.channel.on(DOCS_PREPARED, function handler({ id, ...update }: DocsPreparedPayload) {
+ const { ref } = getEventMetadata(this, fullAPI);
+ api.updateStory(id, { ...update, prepared: true }, ref);
+ });
+
+ provider.channel.on(SET_INDEX, function handler(index: API_PreparedStoryIndex) {
+ const { ref } = getEventMetadata(this, fullAPI);
+
+ if (!ref) {
+ api.setIndex(index);
+ const options = api.getCurrentParameter('options');
+ fullAPI.setOptions(removeRemovedOptions(options));
+ } else {
+ fullAPI.setRef(ref.id, { ...ref, storyIndex: index }, true);
+ }
+ });
+
+ // For composition back-compatibilty
+ provider.channel.on(SET_STORIES, function handler(data: SetStoriesPayload) {
+ const { ref } = getEventMetadata(this, fullAPI);
+ const setStoriesData = data.v ? denormalizeStoryParameters(data) : data.stories;
+
+ if (!ref) {
+ throw new Error('Cannot call SET_STORIES for local frame');
+ } else {
+ fullAPI.setRef(ref.id, { ...ref, setStoriesData }, true);
+ }
+ });
- // For composition back-compatibilty
- fullAPI.on(SET_STORIES, function handler(data: SetStoriesPayload) {
+ provider.channel.on(
+ SELECT_STORY,
+ function handler({
+ kind,
+ title = kind,
+ story,
+ name = story,
+ storyId,
+ ...rest
+ }: {
+ kind?: StoryKind;
+ title?: ComponentTitle;
+ story?: StoryName;
+ name?: StoryName;
+ storyId: string;
+ viewMode: API_ViewMode;
+ }) {
const { ref } = getEventMetadata(this, fullAPI);
- const setStoriesData = data.v ? denormalizeStoryParameters(data) : data.stories;
if (!ref) {
- throw new Error('Cannot call SET_STORIES for local frame');
+ fullAPI.selectStory(storyId || title, name, rest);
} else {
- fullAPI.setRef(ref.id, { ...ref, setStoriesData }, true);
+ fullAPI.selectStory(storyId || title, name, { ...rest, ref: ref.id });
}
- });
-
- fullAPI.on(
- SELECT_STORY,
- function handler({
- kind,
- title = kind,
- story,
- name = story,
- storyId,
- ...rest
- }: {
- kind?: StoryKind;
- title?: ComponentTitle;
- story?: StoryName;
- name?: StoryName;
- storyId: string;
- viewMode: API_ViewMode;
- }) {
- const { ref } = getEventMetadata(this, fullAPI);
-
- if (!ref) {
- fullAPI.selectStory(storyId || title, name, rest);
- } else {
- fullAPI.selectStory(storyId || title, name, { ...rest, ref: ref.id });
- }
- }
- );
-
- fullAPI.on(
- STORY_ARGS_UPDATED,
- function handleStoryArgsUpdated({ storyId, args }: { storyId: StoryId; args: Args }) {
- const { ref } = getEventMetadata(this, fullAPI);
- fullAPI.updateStory(storyId, { args }, ref);
- }
- );
+ }
+ );
- // When there's a preview error, we don't show it in the manager, but simply
- fullAPI.on(CONFIG_ERROR, function handleConfigError(err) {
+ provider.channel.on(
+ STORY_ARGS_UPDATED,
+ function handleStoryArgsUpdated({ storyId, args }: { storyId: StoryId; args: Args }) {
const { ref } = getEventMetadata(this, fullAPI);
- fullAPI.setPreviewInitialized(ref);
- });
+ api.updateStory(storyId, { args }, ref);
+ }
+ );
- fullAPI.on(STORY_MISSING, function handleConfigError(err) {
- const { ref } = getEventMetadata(this, fullAPI);
- fullAPI.setPreviewInitialized(ref);
- });
+ // When there's a preview error, we don't show it in the manager, but simply
+ provider.channel.on(CONFIG_ERROR, function handleConfigError(err) {
+ const { ref } = getEventMetadata(this, fullAPI);
+ api.setPreviewInitialized(ref);
+ });
- if (FEATURES?.storyStoreV7) {
- fullAPI.on(STORY_INDEX_INVALIDATED, () => fullAPI.fetchIndex());
- await fullAPI.fetchIndex();
+ provider.channel.on(STORY_MISSING, function handleConfigError(err) {
+ const { ref } = getEventMetadata(this, fullAPI);
+ api.setPreviewInitialized(ref);
+ });
+
+ provider.channel.on(SET_CONFIG, () => {
+ const config = provider.getConfig();
+ if (config?.sidebar?.filters) {
+ store.setState({
+ filters: {
+ ...store.getState().filters,
+ ...config?.sidebar?.filters,
+ },
+ });
}
- };
+ });
+
+ const config = provider.getConfig();
return {
api,
@@ -759,7 +784,13 @@ export const init: ModuleFn = ({
hasCalledSetOptions: false,
previewInitialized: false,
status: {},
+ filters: config?.sidebar?.filters || {},
+ },
+ init: async () => {
+ if (FEATURES?.storyStoreV7) {
+ provider.channel.on(STORY_INDEX_INVALIDATED, () => api.fetchIndex());
+ await api.fetchIndex();
+ }
},
- init: initModule,
};
};
diff --git a/code/lib/manager-api/src/modules/url.ts b/code/lib/manager-api/src/modules/url.ts
index b8e4dcf6ebf1..c6cfc5abbd80 100644
--- a/code/lib/manager-api/src/modules/url.ts
+++ b/code/lib/manager-api/src/modules/url.ts
@@ -11,7 +11,7 @@ import { dequal as deepEqual } from 'dequal';
import { global } from '@storybook/global';
import type { API_Layout, API_UI } from '@storybook/types';
-import type { ModuleArgs, ModuleFn } from '../index';
+import type { ModuleArgs, ModuleFn } from '../lib/types';
const { window: globalWindow } = global;
@@ -116,7 +116,9 @@ export interface SubAPI {
setQueryParams: (input: QueryParams) => void;
}
-export const init: ModuleFn = ({ store, navigate, state, provider, fullAPI, ...rest }) => {
+export const init: ModuleFn = (moduleArgs) => {
+ const { store, navigate, provider, fullAPI } = moduleArgs;
+
const navigateTo = (
path: string,
queryParams: Record = {},
@@ -153,7 +155,7 @@ export const init: ModuleFn = ({ store, navigate, state, provider, fullAPI, ...r
};
if (!deepEqual(customQueryParams, update)) {
store.setState({ customQueryParams: update });
- fullAPI.emit(UPDATE_QUERY_PARAMS, update);
+ provider.channel.emit(UPDATE_QUERY_PARAMS, update);
}
},
navigateUrl(url, options) {
@@ -161,49 +163,48 @@ export const init: ModuleFn = ({ store, navigate, state, provider, fullAPI, ...r
},
};
- const initModule = () => {
- // Sets `args` parameter in URL, omitting any args that have their initial value or cannot be unserialized safely.
- const updateArgsParam = () => {
- const { path, queryParams, viewMode } = fullAPI.getUrlState();
- if (viewMode !== 'story') return;
-
- const currentStory = fullAPI.getCurrentStoryData();
- if (currentStory?.type !== 'story') return;
-
- const { args, initialArgs } = currentStory;
- const argsString = buildArgsParam(initialArgs, args);
- navigateTo(path, { ...queryParams, args: argsString }, { replace: true });
- api.setQueryParams({ args: argsString });
- };
-
- fullAPI.on(SET_CURRENT_STORY, () => updateArgsParam());
-
- let handleOrId: any;
- fullAPI.on(STORY_ARGS_UPDATED, () => {
- if ('requestIdleCallback' in globalWindow) {
- if (handleOrId) globalWindow.cancelIdleCallback(handleOrId);
- handleOrId = globalWindow.requestIdleCallback(updateArgsParam, { timeout: 1000 });
- } else {
- if (handleOrId) clearTimeout(handleOrId);
- setTimeout(updateArgsParam, 100);
- }
- });
-
- fullAPI.on(GLOBALS_UPDATED, ({ globals, initialGlobals }) => {
- const { path, queryParams } = fullAPI.getUrlState();
- const globalsString = buildArgsParam(initialGlobals, globals);
- navigateTo(path, { ...queryParams, globals: globalsString }, { replace: true });
- api.setQueryParams({ globals: globalsString });
- });
-
- fullAPI.on(NAVIGATE_URL, (url: string, options: NavigateOptions) => {
- fullAPI.navigateUrl(url, options);
- });
+ /**
+ * Sets `args` parameter in URL, omitting any args that have their initial value or cannot be unserialized safely.
+ */
+ const updateArgsParam = () => {
+ const { path, queryParams, viewMode } = api.getUrlState();
+ if (viewMode !== 'story') return;
+
+ const currentStory = fullAPI.getCurrentStoryData();
+ if (currentStory?.type !== 'story') return;
+
+ const { args, initialArgs } = currentStory;
+ const argsString = buildArgsParam(initialArgs, args);
+ navigateTo(path, { ...queryParams, args: argsString }, { replace: true });
+ api.setQueryParams({ args: argsString });
};
+ provider.channel.on(SET_CURRENT_STORY, () => updateArgsParam());
+
+ let handleOrId: any;
+ provider.channel.on(STORY_ARGS_UPDATED, () => {
+ if ('requestIdleCallback' in globalWindow) {
+ if (handleOrId) globalWindow.cancelIdleCallback(handleOrId);
+ handleOrId = globalWindow.requestIdleCallback(updateArgsParam, { timeout: 1000 });
+ } else {
+ if (handleOrId) clearTimeout(handleOrId);
+ setTimeout(updateArgsParam, 100);
+ }
+ });
+
+ provider.channel.on(GLOBALS_UPDATED, ({ globals, initialGlobals }) => {
+ const { path, queryParams } = api.getUrlState();
+ const globalsString = buildArgsParam(initialGlobals, globals);
+ navigateTo(path, { ...queryParams, globals: globalsString }, { replace: true });
+ api.setQueryParams({ globals: globalsString });
+ });
+
+ provider.channel.on(NAVIGATE_URL, (url: string, options: NavigateOptions) => {
+ api.navigateUrl(url, options);
+ });
+
return {
api,
- state: initialUrlSupport({ store, navigate, state, provider, fullAPI, ...rest }),
- init: initModule,
+ state: initialUrlSupport(moduleArgs),
};
};
diff --git a/code/lib/manager-api/src/modules/versions.ts b/code/lib/manager-api/src/modules/versions.ts
index 49ff24be9b1f..2d90a14fcd69 100644
--- a/code/lib/manager-api/src/modules/versions.ts
+++ b/code/lib/manager-api/src/modules/versions.ts
@@ -5,7 +5,7 @@ import memoize from 'memoizerific';
import type { API_UnknownEntries, API_Version, API_Versions } from '@storybook/types';
import { version as currentVersion } from '../version';
-import type { ModuleFn } from '../index';
+import type { ModuleFn } from '../lib/types';
const { VERSIONCHECK } = global;
diff --git a/code/lib/manager-api/src/modules/whatsnew.ts b/code/lib/manager-api/src/modules/whatsnew.ts
index 5890eaae6fc7..6ee90558bc7c 100644
--- a/code/lib/manager-api/src/modules/whatsnew.ts
+++ b/code/lib/manager-api/src/modules/whatsnew.ts
@@ -6,7 +6,7 @@ import {
SET_WHATS_NEW_CACHE,
TOGGLE_WHATS_NEW_NOTIFICATIONS,
} from '@storybook/core-events';
-import type { ModuleFn } from '../index';
+import type { ModuleFn } from '../lib/types';
export type SubState = {
whatsNewData?: WhatsNewData;
@@ -20,7 +20,7 @@ export type SubAPI = {
const WHATS_NEW_NOTIFICATION_ID = 'whats-new';
-export const init: ModuleFn = ({ fullAPI, store }) => {
+export const init: ModuleFn = ({ fullAPI, store, provider }) => {
const state: SubState = {
whatsNewData: undefined,
};
@@ -47,7 +47,7 @@ export const init: ModuleFn = ({ fullAPI, store }) => {
...state.whatsNewData,
disableWhatsNewNotifications: !state.whatsNewData.disableWhatsNewNotifications,
});
- fullAPI.emit(TOGGLE_WHATS_NEW_NOTIFICATIONS, {
+ provider.channel.emit(TOGGLE_WHATS_NEW_NOTIFICATIONS, {
disableWhatsNewNotifications: state.whatsNewData.disableWhatsNewNotifications,
});
}
@@ -55,20 +55,24 @@ export const init: ModuleFn = ({ fullAPI, store }) => {
};
function getLatestWhatsNewPost(): Promise {
- fullAPI.emit(REQUEST_WHATS_NEW_DATA);
+ provider.channel.emit(REQUEST_WHATS_NEW_DATA);
return new Promise((resolve) =>
- fullAPI.once(RESULT_WHATS_NEW_DATA, ({ data }: { data: WhatsNewData }) => resolve(data))
+ provider.channel.once(RESULT_WHATS_NEW_DATA, ({ data }: { data: WhatsNewData }) =>
+ resolve(data)
+ )
);
}
function setWhatsNewCache(cache: WhatsNewCache): void {
- fullAPI.emit(SET_WHATS_NEW_CACHE, cache);
+ provider.channel.emit(SET_WHATS_NEW_CACHE, cache);
}
const initModule = async () => {
// The server channel doesn't exist in production, and we don't want to show what's new in production storybooks.
- if (global.CONFIG_TYPE !== 'DEVELOPMENT') return;
+ if (global.CONFIG_TYPE !== 'DEVELOPMENT') {
+ return;
+ }
const whatsNewData = await getLatestWhatsNewPost();
setWhatsNewState(whatsNewData);
@@ -92,7 +96,9 @@ export const init: ModuleFn = ({ fullAPI, store }) => {
},
icon: { name: 'hearthollow' },
onClear({ dismissed }) {
- if (dismissed) setWhatsNewCache({ lastDismissedPost: whatsNewData.url });
+ if (dismissed) {
+ setWhatsNewCache({ lastDismissedPost: whatsNewData.url });
+ }
},
});
}
diff --git a/code/lib/manager-api/src/tests/globals.test.ts b/code/lib/manager-api/src/tests/globals.test.ts
index babd449131b2..1f51a113935a 100644
--- a/code/lib/manager-api/src/tests/globals.test.ts
+++ b/code/lib/manager-api/src/tests/globals.test.ts
@@ -1,9 +1,10 @@
import { EventEmitter } from 'events';
import { SET_STORIES, SET_GLOBALS, UPDATE_GLOBALS, GLOBALS_UPDATED } from '@storybook/core-events';
-import type { ModuleArgs, API } from '../index';
+import type { API } from '../index';
import type { SubAPI } from '../modules/globals';
import { init as initModule } from '../modules/globals';
+import type { ModuleArgs } from '../lib/types';
const { logger } = require('@storybook/client-logger');
const { getEventMetadata } = require('../lib/events');
@@ -27,7 +28,8 @@ function createMockStore() {
describe('globals API', () => {
it('sets a sensible initialState', () => {
const store = createMockStore();
- const { state } = initModule({ store } as unknown as ModuleArgs);
+ const channel = new EventEmitter();
+ const { state } = initModule({ store, provider: { channel } } as unknown as ModuleArgs);
expect(state).toEqual({
globals: {},
@@ -36,13 +38,15 @@ describe('globals API', () => {
});
it('set global args on SET_GLOBALS', () => {
- const api = Object.assign(new EventEmitter(), { findRef: jest.fn() });
+ const channel = new EventEmitter();
const store = createMockStore();
- const { state, init } = initModule({ store, fullAPI: api } as unknown as ModuleArgs);
+ const { state } = initModule({
+ store,
+ provider: { channel },
+ } as unknown as ModuleArgs);
store.setState(state);
- init();
- api.emit(SET_GLOBALS, {
+ channel.emit(SET_GLOBALS, {
globals: { a: 'b' },
globalTypes: { a: { type: { name: 'string' } } },
});
@@ -53,26 +57,34 @@ describe('globals API', () => {
});
it('ignores SET_STORIES from other refs', () => {
- const api = Object.assign(new EventEmitter(), { findRef: jest.fn() });
+ const channel = new EventEmitter();
+ const api = { findRef: jest.fn() };
const store = createMockStore();
- const { state, init } = initModule({ store, fullAPI: api } as unknown as ModuleArgs);
+ const { state } = initModule({
+ store,
+ fullAPI: api,
+ provider: { channel },
+ } as unknown as ModuleArgs);
store.setState(state);
- init();
getEventMetadata.mockReturnValueOnce({ sourceType: 'external', ref: { id: 'ref' } });
- api.emit(SET_STORIES, { globals: { a: 'b' } });
+ channel.emit(SET_STORIES, { globals: { a: 'b' } });
expect(store.getState()).toEqual({ globals: {}, globalTypes: {} });
});
it('ignores SET_GLOBALS from other refs', () => {
- const api = Object.assign(new EventEmitter(), { findRef: jest.fn() });
+ const api = { findRef: jest.fn() };
+ const channel = new EventEmitter();
const store = createMockStore();
- const { state, init } = initModule({ store, fullAPI: api } as unknown as ModuleArgs);
+ const { state } = initModule({
+ store,
+ fullAPI: api,
+ provider: { channel },
+ } as unknown as ModuleArgs);
store.setState(state);
- init();
getEventMetadata.mockReturnValueOnce({ sourceType: 'external', ref: { id: 'ref' } });
- api.emit(SET_GLOBALS, {
+ channel.emit(SET_GLOBALS, {
globals: { a: 'b' },
globalTypes: { a: { type: { name: 'string' } } },
});
@@ -80,48 +92,56 @@ describe('globals API', () => {
});
it('updates the state when the preview emits GLOBALS_UPDATED', () => {
- const api = Object.assign(new EventEmitter(), { findRef: jest.fn() });
+ const channel = new EventEmitter();
+ const api = { findRef: jest.fn() };
const store = createMockStore();
- const { state, init } = initModule({ store, fullAPI: api } as unknown as ModuleArgs);
+ const { state } = initModule({
+ store,
+ fullAPI: api,
+ provider: { channel },
+ } as unknown as ModuleArgs);
store.setState(state);
- init();
-
- api.emit(GLOBALS_UPDATED, { globals: { a: 'b' } });
+ channel.emit(GLOBALS_UPDATED, { globals: { a: 'b' } });
expect(store.getState()).toEqual({ globals: { a: 'b' }, globalTypes: {} });
- api.emit(GLOBALS_UPDATED, { globals: { a: 'c' } });
+ channel.emit(GLOBALS_UPDATED, { globals: { a: 'c' } });
expect(store.getState()).toEqual({ globals: { a: 'c' }, globalTypes: {} });
// SHOULD NOT merge global args
- api.emit(GLOBALS_UPDATED, { globals: { d: 'e' } });
+ channel.emit(GLOBALS_UPDATED, { globals: { d: 'e' } });
expect(store.getState()).toEqual({ globals: { d: 'e' }, globalTypes: {} });
});
it('ignores GLOBALS_UPDATED from other refs', () => {
- const api = Object.assign(new EventEmitter(), { findRef: jest.fn() });
+ const channel = new EventEmitter();
+ const api = { findRef: jest.fn() };
const store = createMockStore();
- const { state, init } = initModule({ store, fullAPI: api } as unknown as ModuleArgs);
+ const { state } = initModule({
+ store,
+ fullAPI: api,
+ provider: { channel },
+ } as unknown as ModuleArgs);
store.setState(state);
- init();
-
getEventMetadata.mockReturnValueOnce({ sourceType: 'external', ref: { id: 'ref' } });
logger.warn.mockClear();
- api.emit(GLOBALS_UPDATED, { globals: { a: 'b' } });
+ channel.emit(GLOBALS_UPDATED, { globals: { a: 'b' } });
expect(store.getState()).toEqual({ globals: {}, globalTypes: {} });
expect(logger.warn).toHaveBeenCalled();
});
it('emits UPDATE_GLOBALS when updateGlobals is called', () => {
- const fullAPI = { emit: jest.fn(), on: jest.fn() } as unknown as API;
+ const channel = new EventEmitter();
+ const fullAPI = {} as unknown as API;
const store = createMockStore();
- const { init, api } = initModule({ store, fullAPI } as unknown as ModuleArgs);
-
- init();
+ const listener = jest.fn();
+ channel.on(UPDATE_GLOBALS, listener);
+ const { api } = initModule({ store, fullAPI, provider: { channel } } as unknown as ModuleArgs);
(api as SubAPI).updateGlobals({ a: 'b' });
- expect(fullAPI.emit).toHaveBeenCalledWith(UPDATE_GLOBALS, {
+
+ expect(listener).toHaveBeenCalledWith({
globals: { a: 'b' },
options: { target: 'storybook-preview-iframe' },
});
diff --git a/code/lib/manager-api/src/tests/mockStoriesEntries.ts b/code/lib/manager-api/src/tests/mockStoriesEntries.ts
new file mode 100644
index 000000000000..703b6e6efb76
--- /dev/null
+++ b/code/lib/manager-api/src/tests/mockStoriesEntries.ts
@@ -0,0 +1,129 @@
+import type { StoryIndex, API_PreparedStoryIndex } from '@storybook/types';
+
+export const mockEntries: StoryIndex['entries'] = {
+ 'component-a--docs': {
+ type: 'docs',
+ id: 'component-a--docs',
+ title: 'Component A',
+ name: 'Docs',
+ importPath: './path/to/component-a.ts',
+ storiesImports: [],
+ },
+ 'component-a--story-1': {
+ type: 'story',
+ id: 'component-a--story-1',
+ title: 'Component A',
+ name: 'Story 1',
+ importPath: './path/to/component-a.ts',
+ },
+ 'component-a--story-2': {
+ type: 'story',
+ id: 'component-a--story-2',
+ title: 'Component A',
+ name: 'Story 2',
+ importPath: './path/to/component-a.ts',
+ },
+ 'component-b--story-3': {
+ type: 'story',
+ id: 'component-b--story-3',
+ title: 'Component B',
+ name: 'Story 3',
+ importPath: './path/to/component-b.ts',
+ },
+};
+export const docsEntries: StoryIndex['entries'] = {
+ 'component-a--page': {
+ type: 'story',
+ id: 'component-a--page',
+ title: 'Component A',
+ name: 'Page',
+ importPath: './path/to/component-a.ts',
+ },
+ 'component-a--story-2': {
+ type: 'story',
+ id: 'component-a--story-2',
+ title: 'Component A',
+ name: 'Story 2',
+ importPath: './path/to/component-a.ts',
+ },
+ 'component-b-docs': {
+ type: 'docs',
+ id: 'component-b--docs',
+ title: 'Component B',
+ name: 'Docs',
+ importPath: './path/to/component-b.ts',
+ storiesImports: [],
+ tags: ['stories-mdx'],
+ },
+ 'component-c--story-4': {
+ type: 'story',
+ id: 'component-c--story-4',
+ title: 'Component c',
+ name: 'Story 4',
+ importPath: './path/to/component-c.ts',
+ },
+};
+export const navigationEntries: StoryIndex['entries'] = {
+ 'a--1': {
+ type: 'story',
+ title: 'a',
+ name: '1',
+ id: 'a--1',
+ importPath: './a.ts',
+ },
+ 'a--2': {
+ type: 'story',
+ title: 'a',
+ name: '2',
+ id: 'a--2',
+ importPath: './a.ts',
+ },
+ 'b-c--1': {
+ type: 'story',
+ title: 'b/c',
+ name: '1',
+ id: 'b-c--1',
+ importPath: './b/c.ts',
+ },
+ 'b-d--1': {
+ type: 'story',
+ title: 'b/d',
+ name: '1',
+ id: 'b-d--1',
+ importPath: './b/d.ts',
+ },
+ 'b-d--2': {
+ type: 'story',
+ title: 'b/d',
+ name: '2',
+ id: 'b-d--2',
+ importPath: './b/d.ts',
+ },
+ 'custom-id--1': {
+ type: 'story',
+ title: 'b/e',
+ name: '1',
+ id: 'custom-id--1',
+ importPath: './b/.ts',
+ },
+};
+export const preparedEntries: API_PreparedStoryIndex['entries'] = {
+ 'a--1': {
+ type: 'story',
+ title: 'a',
+ name: '1',
+ parameters: {},
+ id: 'a--1',
+ args: { a: 'b' },
+ importPath: './a.ts',
+ },
+ 'b--1': {
+ type: 'story',
+ title: 'b',
+ name: '1',
+ parameters: {},
+ id: 'b--1',
+ args: { x: 'y' },
+ importPath: './b.ts',
+ },
+};
diff --git a/code/lib/manager-api/src/tests/stories.test.ts b/code/lib/manager-api/src/tests/stories.test.ts
index 5427226865e5..b9f7687f4526 100644
--- a/code/lib/manager-api/src/tests/stories.test.ts
+++ b/code/lib/manager-api/src/tests/stories.test.ts
@@ -16,28 +16,28 @@ import {
import { EventEmitter } from 'events';
import { global } from '@storybook/global';
-import { Channel } from '@storybook/channels';
+import type { API_IndexHash, API_StoryEntry } from '@storybook/types';
+import { getEventMetadata as getEventMetadataOriginal } from '../lib/events';
-import type { API_StoryEntry, StoryIndex, API_PreparedStoryIndex } from '@storybook/types';
-import { getEventMetadata } from '../lib/events';
-
-import type { SubAPI } from '../modules/stories';
import { init as initStories } from '../modules/stories';
import type Store from '../store';
-import type { ModuleArgs } from '..';
-
-function mockChannel() {
- const transport = {
- setHandler: () => {},
- send: () => {},
- };
+import type { API, State } from '..';
+import { mockEntries, docsEntries, preparedEntries, navigationEntries } from './mockStoriesEntries';
+import type { ModuleArgs } from '../lib/types';
- return new Channel({ transport });
-}
+import { getAncestorIds } from '../../../../ui/manager/src/utils/tree';
const mockGetEntries = jest.fn();
+const fetch = global.fetch as jest.Mock>;
+const getEventMetadata = getEventMetadataOriginal as unknown as jest.Mock<
+ ReturnType
+>;
-jest.mock('../lib/events');
+const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
+
+jest.mock('../lib/events', () => ({
+ getEventMetadata: jest.fn(() => ({ sourceType: 'local' })),
+}));
jest.mock('@storybook/global', () => ({
global: {
...globalThis,
@@ -47,41 +47,7 @@ jest.mock('@storybook/global', () => ({
},
}));
-const getEventMetadataMock = getEventMetadata as ReturnType;
-
-const mockEntries: StoryIndex['entries'] = {
- 'component-a--docs': {
- type: 'docs',
- id: 'component-a--docs',
- title: 'Component A',
- name: 'Docs',
- importPath: './path/to/component-a.ts',
- storiesImports: [],
- },
- 'component-a--story-1': {
- type: 'story',
- id: 'component-a--story-1',
- title: 'Component A',
- name: 'Story 1',
- importPath: './path/to/component-a.ts',
- },
- 'component-a--story-2': {
- type: 'story',
- id: 'component-a--story-2',
- title: 'Component A',
- name: 'Story 2',
- importPath: './path/to/component-a.ts',
- },
- 'component-b--story-3': {
- type: 'story',
- id: 'component-b--story-3',
- title: 'Component B',
- name: 'Story 3',
- importPath: './path/to/component-b.ts',
- },
-};
-
-function createMockStore(initialState = {}) {
+function createMockStore(initialState: Partial = {}) {
let state = initialState;
return {
getState: jest.fn(() => state),
@@ -91,40 +57,34 @@ function createMockStore(initialState = {}) {
}),
} as any as Store;
}
-
-function initStoriesAndSetState({ store, ...options }: any) {
- const { state, ...result } = initStories({ store, ...options } as any);
-
- store?.setState(state);
-
- return { state, ...result };
+function createMockProvider() {
+ return {
+ getConfig: jest.fn().mockReturnValue({}),
+ channel: new EventEmitter(),
+ };
+}
+function createMockModuleArgs({
+ fullAPI = {},
+ initialState = {},
+}: {
+ fullAPI?: Partial>;
+ initialState?: Partial;
+}) {
+ const navigate = jest.fn();
+ const store = createMockStore(initialState);
+ const provider = createMockProvider();
+
+ return { navigate, store, provider, fullAPI };
}
-
-const provider = { getConfig: jest.fn().mockReturnValue({}), serverChannel: mockChannel() };
-
-beforeEach(() => {
- provider.getConfig.mockReset().mockReturnValue({});
- provider.serverChannel = mockChannel();
- mockGetEntries.mockReset().mockReturnValue(mockEntries);
-
- (global.fetch as jest.Mock>).mockReset().mockReturnValue(
- Promise.resolve({
- status: 200,
- ok: true,
- json: () => ({ v: 4, entries: mockGetEntries() }),
- } as any as Response)
- );
-
- getEventMetadataMock.mockReturnValue({ sourceType: 'local' } as any);
- getEventMetadataMock.mockReturnValue({ sourceType: 'local' } as any);
-});
describe('stories API', () => {
it('sets a sensible initialState', () => {
- const { state } = initStoriesAndSetState({
+ const moduleArgs = createMockModuleArgs({});
+ const { state } = initStories({
+ ...(moduleArgs as unknown as ModuleArgs),
storyId: 'id',
viewMode: 'story',
- } as ModuleArgs);
+ });
expect(state).toEqual(
expect.objectContaining({
@@ -138,16 +98,11 @@ describe('stories API', () => {
describe('setIndex', () => {
it('sets the initial set of stories in the stories hash', async () => {
- const navigate = jest.fn();
- const store = createMockStore();
- const fullAPI = Object.assign(new EventEmitter());
-
- const { api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any);
- Object.assign(fullAPI, api);
-
+ const moduleArgs = createMockModuleArgs({});
+ const { api } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { store } = moduleArgs;
api.setIndex({ v: 4, entries: mockEntries });
const { index } = store.getState();
-
// We need exact key ordering, even if in theory JS doesn't guarantee it
expect(Object.keys(index)).toEqual([
'component-a',
@@ -162,7 +117,6 @@ describe('stories API', () => {
id: 'component-a',
children: ['component-a--docs', 'component-a--story-1', 'component-a--story-2'],
});
-
expect(index['component-a--docs']).toMatchObject({
type: 'docs',
id: 'component-a--docs',
@@ -172,7 +126,6 @@ describe('stories API', () => {
storiesImports: [],
prepared: false,
});
-
expect(index['component-a--story-1']).toMatchObject({
type: 'story',
id: 'component-a--story-1',
@@ -185,15 +138,10 @@ describe('stories API', () => {
(index['component-a--story-1'] as API_StoryEntry as API_StoryEntry).args
).toBeUndefined();
});
-
it('trims whitespace of group/component names (which originate from the kind)', () => {
- const navigate = jest.fn();
- const store = createMockStore();
- const fullAPI = Object.assign(new EventEmitter());
-
- const { api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any);
- Object.assign(fullAPI, api);
-
+ const moduleArgs = createMockModuleArgs({});
+ const { api } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { store } = moduleArgs;
api.setIndex({
v: 4,
entries: {
@@ -207,7 +155,6 @@ describe('stories API', () => {
},
});
const { index } = store.getState();
-
// We need exact key ordering, even if in theory JS doesn't guarantee it
expect(Object.keys(index)).toEqual([
'design-system',
@@ -228,15 +175,10 @@ describe('stories API', () => {
name: ' My Story ', // story name is kept as-is, because it's set directly on the story
});
});
-
it('moves rootless stories to the front of the list', async () => {
- const navigate = jest.fn();
- const store = createMockStore();
- const fullAPI = Object.assign(new EventEmitter());
-
- const { api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any);
- Object.assign(fullAPI, api);
-
+ const moduleArgs = createMockModuleArgs({});
+ const { api } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { store } = moduleArgs;
api.setIndex({
v: 4,
entries: {
@@ -251,7 +193,6 @@ describe('stories API', () => {
},
});
const { index } = store.getState();
-
// We need exact key ordering, even if in theory JS doesn't guarantee it
expect(Object.keys(index)).toEqual([
'component-a',
@@ -270,15 +211,10 @@ describe('stories API', () => {
children: ['root-first'],
});
});
-
it('sets roots when showRoots = true', () => {
- const navigate = jest.fn();
- const store = createMockStore();
- const fullAPI = Object.assign(new EventEmitter());
-
- const { api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any);
- Object.assign(fullAPI, api);
-
+ const moduleArgs = createMockModuleArgs({});
+ const { api } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { store, provider } = moduleArgs;
provider.getConfig.mockReturnValue({ sidebar: { showRoots: true } });
api.setIndex({
v: 4,
@@ -292,9 +228,7 @@ describe('stories API', () => {
},
},
});
-
const { index } = store.getState();
-
// We need exact key ordering, even if in theory JS doens't guarantee it
expect(Object.keys(index)).toEqual(['a', 'a-b', 'a-b--1']);
expect(index.a).toMatchObject({
@@ -316,15 +250,10 @@ describe('stories API', () => {
title: 'a/b',
});
});
-
it('does not put bare stories into a root when showRoots = true', () => {
- const navigate = jest.fn();
- const store = createMockStore();
- const fullAPI = Object.assign(new EventEmitter());
-
- const { api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any);
- Object.assign(fullAPI, api);
-
+ const moduleArgs = createMockModuleArgs({});
+ const { api } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { store, provider } = moduleArgs;
provider.getConfig.mockReturnValue({ sidebar: { showRoots: true } });
api.setIndex({
v: 4,
@@ -338,9 +267,7 @@ describe('stories API', () => {
},
},
});
-
const { index } = store.getState();
-
// We need exact key ordering, even if in theory JS doens't guarantee it
expect(Object.keys(index)).toEqual(['a', 'a--1']);
expect(index.a).toMatchObject({
@@ -356,17 +283,12 @@ describe('stories API', () => {
name: '1',
});
});
-
// Stories can get out of order for a few reasons -- see reproductions on
// https://github.com/storybookjs/storybook/issues/5518
it('does the right thing for out of order stories', async () => {
- const navigate = jest.fn();
- const store = createMockStore();
- const fullAPI = Object.assign(new EventEmitter());
-
- const { api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any);
- Object.assign(fullAPI, api);
-
+ const moduleArgs = createMockModuleArgs({});
+ const { api } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { store, provider } = moduleArgs;
provider.getConfig.mockReturnValue({ sidebar: { showRoots: true } });
api.setIndex({
v: 4,
@@ -376,9 +298,7 @@ describe('stories API', () => {
'a--2': { type: 'story', title: 'a', name: '2', id: 'a--2', importPath: './a.ts' },
},
});
-
const { index } = store.getState();
-
// We need exact key ordering, even if in theory JS doens't guarantee it
expect(Object.keys(index)).toEqual(['a', 'a--1', 'a--2', 'b', 'b--1']);
expect(index.a).toMatchObject({
@@ -386,23 +306,17 @@ describe('stories API', () => {
id: 'a',
children: ['a--1', 'a--2'],
});
-
expect(index.b).toMatchObject({
type: 'component',
id: 'b',
children: ['b--1'],
});
});
-
// Entries on the SET_STORIES event will be prepared
it('handles properly prepared stories', async () => {
- const navigate = jest.fn();
- const store = createMockStore();
- const fullAPI = Object.assign(new EventEmitter(), {});
-
- const { api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any);
- Object.assign(fullAPI, api);
-
+ const moduleArgs = createMockModuleArgs({});
+ const { api } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { store } = moduleArgs;
api.setIndex({
v: 4,
entries: {
@@ -417,9 +331,7 @@ describe('stories API', () => {
},
},
});
-
const { index } = store.getState();
-
expect(index['prepared--story']).toMatchObject({
type: 'story',
id: 'prepared--story',
@@ -431,21 +343,13 @@ describe('stories API', () => {
args: { arg: 'exists' },
});
});
-
it('retains prepared-ness of stories', async () => {
- const navigate = jest.fn();
- const store = createMockStore();
- const fullAPI = Object.assign(new EventEmitter(), {
- setOptions: jest.fn(),
- });
-
- const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any);
- Object.assign(fullAPI, api);
- init();
-
+ const fullAPI = { setOptions: jest.fn() };
+ const moduleArgs = createMockModuleArgs({ fullAPI });
+ const { api } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { store, provider } = moduleArgs;
api.setIndex({ v: 4, entries: mockEntries });
-
- fullAPI.emit(STORY_PREPARED, {
+ provider.channel.emit(STORY_PREPARED, {
id: 'component-a--story-1',
parameters: { a: 'b' },
args: { c: 'd' },
@@ -457,9 +361,7 @@ describe('stories API', () => {
parameters: { a: 'b' },
args: { c: 'd' },
});
-
api.setIndex({ v: 4, entries: mockEntries });
-
// Let the promise/await chain resolve
await new Promise((r) => setTimeout(r, 0));
expect(store.getState().index['component-a--story-1'] as API_StoryEntry).toMatchObject({
@@ -470,51 +372,13 @@ describe('stories API', () => {
});
describe('docs entries', () => {
- const docsEntries: StoryIndex['entries'] = {
- 'component-a--page': {
- type: 'story',
- id: 'component-a--page',
- title: 'Component A',
- name: 'Page',
- importPath: './path/to/component-a.ts',
- },
- 'component-a--story-2': {
- type: 'story',
- id: 'component-a--story-2',
- title: 'Component A',
- name: 'Story 2',
- importPath: './path/to/component-a.ts',
- },
- 'component-b-docs': {
- type: 'docs',
- id: 'component-b--docs',
- title: 'Component B',
- name: 'Docs',
- importPath: './path/to/component-b.ts',
- storiesImports: [],
- tags: ['stories-mdx'],
- },
- 'component-c--story-4': {
- type: 'story',
- id: 'component-c--story-4',
- title: 'Component c',
- name: 'Story 4',
- importPath: './path/to/component-c.ts',
- },
- };
-
it('handles docs entries', async () => {
- const navigate = jest.fn();
- const store = createMockStore();
- const fullAPI = Object.assign(new EventEmitter());
-
- const { api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any);
- Object.assign(fullAPI, api);
+ const moduleArgs = createMockModuleArgs({});
+ const { api } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { store } = moduleArgs;
api.setIndex({ v: 4, entries: docsEntries });
-
const { index } = store.getState();
-
// We need exact key ordering, even if in theory JS doesn't guarantee it
expect(Object.keys(index)).toEqual([
'component-a',
@@ -530,26 +394,16 @@ describe('stories API', () => {
expect(index['component-b--docs'].type).toBe('docs');
expect(index['component-c--story-4'].type).toBe('story');
});
-
describe('when DOCS_MODE = true', () => {
it('strips out story entries', async () => {
- const navigate = jest.fn();
- const store = createMockStore();
- const fullAPI = Object.assign(new EventEmitter());
-
- const { api } = initStoriesAndSetState({
- store,
- navigate,
- provider,
- fullAPI,
+ const moduleArgs = createMockModuleArgs({});
+ const { api } = initStories({
+ ...(moduleArgs as unknown as ModuleArgs),
docsOptions: { docsMode: true },
- } as any);
- Object.assign(fullAPI, api);
-
+ });
+ const { store } = moduleArgs;
api.setIndex({ v: 4, entries: docsEntries });
-
const { index } = store.getState();
-
expect(Object.keys(index)).toEqual(['component-b', 'component-b--docs']);
});
});
@@ -558,269 +412,197 @@ describe('stories API', () => {
describe('SET_INDEX event', () => {
it('calls setIndex w/ the data', () => {
- const fullAPI = Object.assign(new EventEmitter());
- const navigate = jest.fn();
- const store = createMockStore();
-
- const { init, api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any);
- Object.assign(fullAPI, api, {
- setIndex: jest.fn(),
- setOptions: jest.fn(),
- });
- init();
+ const fullAPI = { setOptions: jest.fn() };
+ const moduleArgs = createMockModuleArgs({ fullAPI });
+ initStories(moduleArgs as unknown as ModuleArgs);
+ const { store, provider } = moduleArgs;
- fullAPI.emit(SET_INDEX, { v: 4, entries: mockEntries });
-
- expect(fullAPI.setIndex).toHaveBeenCalled();
+ provider.channel.emit(SET_INDEX, { v: 4, entries: mockEntries });
+ expect(store.getState().index).toEqual(
+ expect.objectContaining({
+ 'component-a': expect.any(Object),
+ 'component-a--docs': expect.any(Object),
+ 'component-a--story-1': expect.any(Object),
+ })
+ );
});
-
it('calls setOptions w/ first story parameter', () => {
- const fullAPI = Object.assign(new EventEmitter());
- const navigate = jest.fn();
- const store = createMockStore();
-
- const { init, api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any);
- Object.assign(fullAPI, api, {
- setIndex: jest.fn(),
- setOptions: jest.fn(),
+ const fullAPI = { setOptions: jest.fn() };
+ const moduleArgs = createMockModuleArgs({ fullAPI });
+ const { api } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { provider } = moduleArgs;
+
+ // HACK api to effectively mock getCurrentParameter
+ Object.assign(api, {
getCurrentParameter: jest.fn().mockReturnValue('options'),
});
- init();
-
- fullAPI.emit(SET_INDEX, { v: 4, entries: mockEntries });
+ provider.channel.emit(SET_INDEX, { v: 4, entries: mockEntries });
expect(fullAPI.setOptions).toHaveBeenCalledWith('options');
});
});
describe('fetchIndex', () => {
it('deals with 500 errors', async () => {
- const navigate = jest.fn();
- const store = createMockStore({});
- const fullAPI = Object.assign(new EventEmitter(), {}, {});
-
- (global.fetch as jest.Mock>).mockReturnValue(
+ fetch.mockReturnValue(
Promise.resolve({
status: 500,
text: async () => new Error('sorting error'),
} as any as Response)
);
- const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any);
- Object.assign(fullAPI, api);
+ const moduleArgs = createMockModuleArgs({});
+ const { init } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { store } = moduleArgs;
await init();
const { indexError } = store.getState();
expect(indexError).toBeDefined();
});
-
it('watches for the INVALIDATE event and re-fetches -- and resets the hash', async () => {
- const navigate = jest.fn();
- const store = createMockStore();
- const fullAPI = Object.assign(new EventEmitter(), {
- setIndex: jest.fn(),
- });
+ fetch.mockReturnValue(
+ Promise.resolve({
+ status: 200,
+ ok: true,
+ json: () => ({
+ v: 4,
+ entries: {
+ 'component-a--story-1': {
+ type: 'story',
+ id: 'component-a--story-1',
+ title: 'Component A',
+ name: 'Story 1',
+ importPath: './path/to/component-a.ts',
+ },
+ },
+ }),
+ } as any as Response)
+ );
- const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any);
- Object.assign(fullAPI, api);
+ const moduleArgs = createMockModuleArgs({});
+ const { init } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { store, provider } = moduleArgs;
- (global.fetch as jest.Mock>).mockClear();
await init();
- expect(global.fetch as jest.Mock>).toHaveBeenCalledTimes(1);
-
- (global.fetch as jest.Mock>).mockClear();
- mockGetEntries.mockReturnValueOnce({
- 'component-a--story-1': {
- type: 'story',
- id: 'component-a--story-1',
- title: 'Component A',
- name: 'Story 1',
- importPath: './path/to/component-a.ts',
- },
- });
- fullAPI.emit(STORY_INDEX_INVALIDATED);
- expect(global.fetch).toHaveBeenCalledTimes(1);
- // Let the promise/await chain resolve
- await new Promise((r) => setTimeout(r, 0));
- const { index } = store.getState();
+ expect(fetch).toHaveBeenCalledTimes(1);
+ provider.channel.emit(STORY_INDEX_INVALIDATED);
+ expect(global.fetch).toHaveBeenCalledTimes(2);
+
+ // this side-effect is in an un-awaited promise.
+ await wait(16);
+ const { index } = store.getState();
expect(Object.keys(index)).toEqual(['component-a', 'component-a--story-1']);
});
-
it('clears 500 errors when invalidated', async () => {
- const navigate = jest.fn();
- const store = createMockStore();
- const fullAPI = Object.assign(new EventEmitter(), {
- setIndex: jest.fn(),
- });
-
- (global.fetch as jest.Mock>).mockReturnValueOnce(
+ fetch.mockReturnValueOnce(
Promise.resolve({
status: 500,
text: async () => new Error('sorting error'),
} as any as Response)
);
- const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any);
- Object.assign(fullAPI, api);
+ const moduleArgs = createMockModuleArgs({});
+ const { init } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { store, provider } = moduleArgs;
await init();
const { indexError } = store.getState();
expect(indexError).toBeDefined();
- (global.fetch as jest.Mock>).mockClear();
- mockGetEntries.mockReturnValueOnce({
- 'component-a--story-1': {
- type: 'story',
- id: 'component-a--story-1',
- title: 'Component A',
- name: 'Story 1',
- importPath: './path/to/component-a.ts',
- },
- });
- fullAPI.emit(STORY_INDEX_INVALIDATED);
- expect(global.fetch).toHaveBeenCalledTimes(1);
+ fetch.mockReturnValueOnce(
+ Promise.resolve({
+ status: 200,
+ ok: true,
+ json: () => ({
+ v: 4,
+ entries: {
+ 'component-a--story-1': {
+ type: 'story',
+ id: 'component-a--story-1',
+ title: 'Component A',
+ name: 'Story 1',
+ importPath: './path/to/component-a.ts',
+ },
+ },
+ }),
+ } as any as Response)
+ );
+
+ provider.channel.emit(STORY_INDEX_INVALIDATED);
+ expect(global.fetch).toHaveBeenCalledTimes(2);
+
+ // this side-effect is in an un-awaited promise.
+ await wait(16);
- // Let the promise/await chain resolve
- await new Promise((r) => setTimeout(r, 0));
const { index, indexError: newIndexError } = store.getState();
expect(newIndexError).not.toBeDefined();
-
expect(Object.keys(index)).toEqual(['component-a', 'component-a--story-1']);
});
});
describe('STORY_SPECIFIED event', () => {
it('navigates to the story', async () => {
- const navigate = jest.fn();
- const fullAPI = Object.assign(new EventEmitter(), {
- isSettingsScreenActive() {
- return false;
- },
- });
- const store = createMockStore({ viewMode: 'story' });
- const { init, api } = initStoriesAndSetState({
- store,
- navigate,
- provider,
- fullAPI,
- viewMode: 'story',
- } as any);
-
- Object.assign(fullAPI, api);
- init();
- fullAPI.emit(STORY_SPECIFIED, { storyId: 'a--1', viewMode: 'story' });
+ const moduleArgs = createMockModuleArgs({ initialState: { path: '/' } });
+ initStories(moduleArgs as unknown as ModuleArgs);
+ const { navigate, provider } = moduleArgs;
+ provider.channel.emit(STORY_SPECIFIED, { storyId: 'a--1', viewMode: 'story' });
expect(navigate).toHaveBeenCalledWith('/story/a--1');
});
-
it('DOES not navigate if the story was already selected', async () => {
- const navigate = jest.fn();
- const fullAPI = Object.assign(new EventEmitter(), {
- isSettingsScreenActive() {
- return true;
- },
- });
- const store = createMockStore({ viewMode: 'story', storyId: 'a--1' });
- const { api, init } = initStoriesAndSetState({
- store,
- navigate,
- provider,
- fullAPI,
- viewMode: 'story',
- storyId: 'a--1',
- } as any);
- Object.assign(fullAPI, api);
- init();
-
- fullAPI.emit(STORY_SPECIFIED, { storyId: 'a--1', viewMode: 'story' });
+ const moduleArgs = createMockModuleArgs({ initialState: { path: '/story/a--1' } });
+ initStories(moduleArgs as unknown as ModuleArgs);
+ const { navigate, provider } = moduleArgs;
+ provider.channel.emit(STORY_SPECIFIED, { storyId: 'a--1', viewMode: 'story' });
expect(navigate).not.toHaveBeenCalled();
});
-
it('DOES not navigate if a settings page was selected', async () => {
- const navigate = jest.fn();
- const fullAPI = Object.assign(new EventEmitter(), {
- isSettingsScreenActive() {
- return true;
- },
- });
- const store = createMockStore({ viewMode: 'settings', storyId: 'about' });
- const { api, init } = initStoriesAndSetState({
- store,
- navigate,
- provider,
- fullAPI,
- viewMode: 'settings',
- storyId: 'about',
- } as any);
- Object.assign(fullAPI, api);
- init();
-
- fullAPI.emit(STORY_SPECIFIED, { storyId: 'a--1', viewMode: 'story' });
+ const moduleArgs = createMockModuleArgs({ initialState: { path: '/settings/about' } });
+ initStories(moduleArgs as unknown as ModuleArgs);
+ const { navigate, provider } = moduleArgs;
+ provider.channel.emit(STORY_SPECIFIED, { storyId: 'a--1', viewMode: 'story' });
expect(navigate).not.toHaveBeenCalled();
});
-
it('DOES not navigate if a custom page was selected', async () => {
- const navigate = jest.fn();
- const fullAPI = Object.assign(new EventEmitter(), {
- isSettingsScreenActive() {
- return true;
- },
- });
- const store = createMockStore({ viewMode: 'custom', storyId: undefined });
- const { api, init } = initStoriesAndSetState({
- store,
- navigate,
- provider,
- fullAPI,
- viewMode: 'custom',
- storyId: undefined,
- } as any);
- Object.assign(fullAPI, api);
- init();
-
- fullAPI.emit(STORY_SPECIFIED, { storyId: 'a--1', viewMode: 'story' });
+ const moduleArgs = createMockModuleArgs({ initialState: { path: '/custom/page' } });
+ initStories(moduleArgs as unknown as ModuleArgs);
+ const { navigate, provider } = moduleArgs;
+ provider.channel.emit(STORY_SPECIFIED, { storyId: 'a--1', viewMode: 'story' });
expect(navigate).not.toHaveBeenCalled();
});
});
describe('CURRENT_STORY_WAS_SET event', () => {
it('sets previewInitialized', async () => {
- const navigate = jest.fn();
- const fullAPI = Object.assign(new EventEmitter(), {});
- const store = createMockStore({});
- const { init, api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any);
-
- Object.assign(fullAPI, api);
- await init();
- fullAPI.emit(CURRENT_STORY_WAS_SET, { id: 'a--1' });
+ const moduleArgs = createMockModuleArgs({});
+ initStories(moduleArgs as unknown as ModuleArgs);
+ const { store, provider } = moduleArgs;
+ provider.channel.emit(CURRENT_STORY_WAS_SET, { id: 'a--1' });
expect(store.getState().previewInitialized).toBe(true);
});
-
it('sets a ref to previewInitialized', async () => {
- const navigate = jest.fn();
- const fullAPI = Object.assign(new EventEmitter(), {
- updateRef: jest.fn(),
- });
- const store = createMockStore();
- const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any);
-
- Object.assign(fullAPI, api);
+ const fullAPI = { updateRef: jest.fn() };
+ const moduleArgs = createMockModuleArgs({ fullAPI });
+ initStories(moduleArgs as unknown as ModuleArgs);
+ const { provider } = moduleArgs;
+ provider.channel.emit(CURRENT_STORY_WAS_SET, { id: 'a--1' });
- getEventMetadataMock.mockReturnValueOnce({
+ getEventMetadata.mockReturnValueOnce({
sourceType: 'external',
- ref: { id: 'refId', index: { 'a--1': { args: { a: 'b' } } } },
- } as any);
- await init();
- fullAPI.emit(CURRENT_STORY_WAS_SET, { id: 'a--1' });
-
+ refId: 'refId',
+ source: '',
+ sourceLocation: '',
+ type: '',
+ ref: { id: 'refId', index: { 'a--1': { args: { a: 'b' } } } } as any,
+ });
+ provider.channel.emit(CURRENT_STORY_WAS_SET, { id: 'a--1' });
expect(fullAPI.updateRef.mock.calls.length).toBe(1);
-
expect(fullAPI.updateRef.mock.calls[0][1]).toEqual({
previewInitialized: true,
});
@@ -828,88 +610,53 @@ describe('stories API', () => {
});
describe('args handling', () => {
- const parameters = {};
- const preparedEntries: API_PreparedStoryIndex['entries'] = {
- 'a--1': {
- type: 'story',
- title: 'a',
- name: '1',
- parameters,
- id: 'a--1',
- args: { a: 'b' },
- importPath: './a.ts',
- },
- 'b--1': {
- type: 'story',
- title: 'b',
- name: '1',
- parameters,
- id: 'b--1',
- args: { x: 'y' },
- importPath: './b.ts',
- },
- };
-
it('changes args properly, per story when receiving STORY_ARGS_UPDATED', () => {
- const navigate = jest.fn();
- const store = createMockStore();
- const fullAPI = Object.assign(new EventEmitter(), {
- updateRef: jest.fn(),
- });
-
- const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any);
-
- const { setIndex } = Object.assign(fullAPI, api);
- setIndex({ v: 4, entries: preparedEntries });
+ const fullAPI = { updateRef: jest.fn() };
+ const moduleArgs = createMockModuleArgs({ fullAPI });
+ const { api } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { provider, store } = moduleArgs;
+ api.setIndex({ v: 4, entries: preparedEntries });
const { index } = store.getState();
expect((index['a--1'] as API_StoryEntry).args).toEqual({ a: 'b' });
expect((index['b--1'] as API_StoryEntry).args).toEqual({ x: 'y' });
-
- init();
- fullAPI.emit(STORY_ARGS_UPDATED, { storyId: 'a--1', args: { foo: 'bar' } });
-
+ provider.channel.emit(STORY_ARGS_UPDATED, { storyId: 'a--1', args: { foo: 'bar' } });
const { index: changedIndex } = store.getState();
expect((changedIndex['a--1'] as API_StoryEntry).args).toEqual({ foo: 'bar' });
expect((changedIndex['b--1'] as API_StoryEntry).args).toEqual({ x: 'y' });
});
-
it('changes reffed args properly, per story when receiving STORY_ARGS_UPDATED', () => {
- const navigate = jest.fn();
- const store = createMockStore();
- const fullAPI = new EventEmitter();
-
- const { init, api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any);
- Object.assign(fullAPI, api, {
- updateRef: jest.fn(),
- });
+ const fullAPI = { updateRef: jest.fn() };
+ const moduleArgs = createMockModuleArgs({ fullAPI });
+ initStories(moduleArgs as unknown as ModuleArgs);
+ const { provider } = moduleArgs;
- init();
- getEventMetadataMock.mockReturnValueOnce({
+ getEventMetadata.mockReturnValueOnce({
sourceType: 'external',
- ref: { id: 'refId', index: { 'a--1': { args: { a: 'b' } } } },
- } as any);
- fullAPI.emit(STORY_ARGS_UPDATED, { storyId: 'a--1', args: { foo: 'bar' } });
- expect((fullAPI as any).updateRef).toHaveBeenCalledWith('refId', {
+ refId: 'refId',
+ source: '',
+ sourceLocation: '',
+ type: '',
+ ref: { id: 'refId', index: { 'a--1': { args: { a: 'b' } } } } as any,
+ });
+ provider.channel.emit(STORY_ARGS_UPDATED, { storyId: 'a--1', args: { foo: 'bar' } });
+ expect(fullAPI.updateRef).toHaveBeenCalledWith('refId', {
index: { 'a--1': { args: { foo: 'bar' } } },
});
});
-
it('updateStoryArgs emits UPDATE_STORY_ARGS to the local frame and does not change anything', () => {
- const navigate = jest.fn();
- const emit = jest.fn();
- const on = jest.fn();
- const fullAPI = { emit, on };
- const store = createMockStore();
+ const fullAPI = { updateRef: jest.fn() };
+ const moduleArgs = createMockModuleArgs({ fullAPI });
+ const { api } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { provider, store } = moduleArgs;
- const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any);
- const { setIndex } = Object.assign(fullAPI, api);
- setIndex({ v: 4, entries: preparedEntries });
-
- init();
+ const listener = jest.fn();
+ provider.channel.on(UPDATE_STORY_ARGS, listener);
+ api.setIndex({ v: 4, entries: preparedEntries });
api.updateStoryArgs({ id: 'a--1' } as API_StoryEntry, { foo: 'bar' });
- expect(emit).toHaveBeenCalledWith(UPDATE_STORY_ARGS, {
+
+ expect(listener).toHaveBeenCalledWith({
storyId: 'a--1',
updatedArgs: { foo: 'bar' },
options: {
@@ -921,23 +668,18 @@ describe('stories API', () => {
expect((index['a--1'] as API_StoryEntry).args).toEqual({ a: 'b' });
expect((index['b--1'] as API_StoryEntry).args).toEqual({ x: 'y' });
});
-
it('updateStoryArgs emits UPDATE_STORY_ARGS to the right frame', () => {
- const navigate = jest.fn();
- const emit = jest.fn();
- const on = jest.fn();
- const fullAPI = { emit, on };
- const store = createMockStore();
+ const fullAPI = { updateRef: jest.fn() };
+ const moduleArgs = createMockModuleArgs({ fullAPI });
+ const { api } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { provider } = moduleArgs;
- const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any);
-
- const { setIndex } = Object.assign(fullAPI, api);
- setIndex({ v: 4, entries: preparedEntries });
-
- init();
+ const listener = jest.fn();
+ provider.channel.on(UPDATE_STORY_ARGS, listener);
+ api.setIndex({ v: 4, entries: preparedEntries });
api.updateStoryArgs({ id: 'a--1', refId: 'refId' } as API_StoryEntry, { foo: 'bar' });
- expect(emit).toHaveBeenCalledWith(UPDATE_STORY_ARGS, {
+ expect(listener).toHaveBeenCalledWith({
storyId: 'a--1',
updatedArgs: { foo: 'bar' },
options: {
@@ -945,22 +687,18 @@ describe('stories API', () => {
},
});
});
-
it('refId to the local frame and does not change anything', () => {
- const navigate = jest.fn();
- const emit = jest.fn();
- const on = jest.fn();
- const fullAPI = { emit, on };
- const store = createMockStore();
-
- const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any);
-
- const { setIndex } = Object.assign(fullAPI, api);
- setIndex({ v: 4, entries: preparedEntries });
- init();
-
+ const fullAPI = { updateRef: jest.fn() };
+ const moduleArgs = createMockModuleArgs({ fullAPI });
+ const { api } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { provider, store } = moduleArgs;
+ const listener = jest.fn();
+ provider.channel.on(RESET_STORY_ARGS, listener);
+
+ api.setIndex({ v: 4, entries: preparedEntries });
api.resetStoryArgs({ id: 'a--1' } as API_StoryEntry, ['foo']);
- expect(emit).toHaveBeenCalledWith(RESET_STORY_ARGS, {
+
+ expect(listener).toHaveBeenCalledWith({
storyId: 'a--1',
argNames: ['foo'],
options: {
@@ -972,22 +710,18 @@ describe('stories API', () => {
expect((index['a--1'] as API_StoryEntry).args).toEqual({ a: 'b' });
expect((index['b--1'] as API_StoryEntry).args).toEqual({ x: 'y' });
});
-
it('resetStoryArgs emits RESET_STORY_ARGS to the right frame', () => {
- const navigate = jest.fn();
- const emit = jest.fn();
- const on = jest.fn();
- const fullAPI = { emit, on };
- const store = createMockStore();
-
- const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any);
+ const fullAPI = { updateRef: jest.fn() };
+ const moduleArgs = createMockModuleArgs({ fullAPI });
+ const { api } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { provider } = moduleArgs;
- const { setIndex } = Object.assign(fullAPI, api);
- setIndex({ v: 4, entries: preparedEntries });
- init();
+ const listener = jest.fn();
+ provider.channel.on(RESET_STORY_ARGS, listener);
+ api.setIndex({ v: 4, entries: preparedEntries });
api.resetStoryArgs({ id: 'a--1', refId: 'refId' } as API_StoryEntry, ['foo']);
- expect(emit).toHaveBeenCalledWith(RESET_STORY_ARGS, {
+ expect(listener).toHaveBeenCalledWith({
storyId: 'a--1',
argNames: ['foo'],
options: {
@@ -997,268 +731,156 @@ describe('stories API', () => {
});
});
- const navigationEntries: StoryIndex['entries'] = {
- 'a--1': {
- type: 'story',
- title: 'a',
- name: '1',
- id: 'a--1',
- importPath: './a.ts',
- },
- 'a--2': {
- type: 'story',
- title: 'a',
- name: '2',
- id: 'a--2',
- importPath: './a.ts',
- },
- 'b-c--1': {
- type: 'story',
- title: 'b/c',
- name: '1',
- id: 'b-c--1',
- importPath: './b/c.ts',
- },
- 'b-d--1': {
- type: 'story',
- title: 'b/d',
- name: '1',
- id: 'b-d--1',
- importPath: './b/d.ts',
- },
- 'b-d--2': {
- type: 'story',
- title: 'b/d',
- name: '2',
- id: 'b-d--2',
- importPath: './b/d.ts',
- },
- 'custom-id--1': {
- type: 'story',
- title: 'b/e',
- name: '1',
- id: 'custom-id--1',
- importPath: './b/.ts',
- },
- };
-
describe('jumpToStory', () => {
it('works forward', () => {
- const navigate = jest.fn();
- const store = createMockStore();
+ const initialState = { path: '/story/a--1', storyId: 'a--1', viewMode: 'story' };
+ const moduleArgs = createMockModuleArgs({ initialState });
+ const { api } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { navigate } = moduleArgs;
- const {
- api: { setIndex, jumpToStory },
- } = initStoriesAndSetState({
- store,
- storyId: 'a--1',
- viewMode: 'story',
- navigate,
- provider,
- } as any);
- setIndex({ v: 4, entries: navigationEntries });
+ api.setIndex({ v: 4, entries: navigationEntries });
+ api.jumpToStory(1);
- jumpToStory(1);
expect(navigate).toHaveBeenCalledWith('/story/a--2');
});
-
it('works backwards', () => {
- const navigate = jest.fn();
- const store = createMockStore();
+ const initialState = { path: '/story/a--2', storyId: 'a--2', viewMode: 'story' };
+ const moduleArgs = createMockModuleArgs({ initialState });
+ const { api } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { navigate } = moduleArgs;
- const {
- api: { setIndex, jumpToStory },
- } = initStoriesAndSetState({
- store,
- storyId: 'a--2',
- viewMode: 'story',
- navigate,
- provider,
- } as any);
- setIndex({ v: 4, entries: navigationEntries });
+ api.setIndex({ v: 4, entries: navigationEntries });
+ api.jumpToStory(-1);
- jumpToStory(-1);
expect(navigate).toHaveBeenCalledWith('/story/a--1');
});
-
it('does nothing if you are at the last story and go forward', () => {
- const navigate = jest.fn();
- const store = createMockStore();
-
- const {
- api: { setIndex, jumpToStory },
- } = initStoriesAndSetState({
- store,
+ const initialState = {
+ path: '/story/custom-id--1',
storyId: 'custom-id--1',
viewMode: 'story',
- navigate,
- provider,
- } as any);
- setIndex({ v: 4, entries: navigationEntries });
+ };
+ const moduleArgs = createMockModuleArgs({ initialState });
+ const { api } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { navigate } = moduleArgs;
- jumpToStory(1);
+ api.setIndex({ v: 4, entries: navigationEntries });
+ api.jumpToStory(1);
expect(navigate).not.toHaveBeenCalled();
});
-
it('does nothing if you are at the first story and go backward', () => {
- const navigate = jest.fn();
- const store = createMockStore();
-
- const {
- api: { setIndex, jumpToStory },
- } = initStoriesAndSetState({
- store,
- storyId: 'a--1',
- viewMode: 'story',
- navigate,
- provider,
- } as any);
- setIndex({ v: 4, entries: navigationEntries });
+ const initialState = { path: '/story/a--1', storyId: 'a--1', viewMode: 'story' };
+ const moduleArgs = createMockModuleArgs({ initialState });
+ const { api } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { navigate } = moduleArgs;
- jumpToStory(-1);
+ api.setIndex({ v: 4, entries: navigationEntries });
+ api.jumpToStory(-1);
expect(navigate).not.toHaveBeenCalled();
});
-
it('does nothing if you have not selected a story', () => {
- const navigate = jest.fn();
- const store = createMockStore();
-
- const {
- api: { setIndex, jumpToStory },
- } = initStoriesAndSetState({ store, navigate, provider } as any);
- setIndex({ v: 4, entries: navigationEntries });
-
- jumpToStory(1);
+ // @ts-expect-error (storyId type is maybe wrong?)
+ const initialState = { path: '/story', storyId: undefined, viewMode: 'story' };
+ const moduleArgs = createMockModuleArgs({ initialState });
+ const { api } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { navigate } = moduleArgs;
+
+ api.setIndex({ v: 4, entries: navigationEntries });
+ api.jumpToStory(1);
expect(navigate).not.toHaveBeenCalled();
});
});
describe('findSiblingStoryId', () => {
it('works forward', () => {
- const navigate = jest.fn();
- const store = createMockStore();
+ const initialState = { path: '/story/a--1', storyId: 'a--1', viewMode: 'story' };
+ const moduleArgs = createMockModuleArgs({ initialState });
+ const { api } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { store } = moduleArgs;
- const storyId = 'a--1';
- const {
- api: { setIndex, findSiblingStoryId },
- } = initStoriesAndSetState({ store, navigate, storyId, viewMode: 'story', provider } as any);
- setIndex({ v: 4, entries: navigationEntries });
-
- const result = findSiblingStoryId(storyId, store.getState().index, 1, false);
+ api.setIndex({ v: 4, entries: navigationEntries });
+ const result = api.findSiblingStoryId('a--1', store.getState().index, 1, false);
expect(result).toBe('a--2');
});
it('works forward toSiblingGroup', () => {
- const navigate = jest.fn();
- const store = createMockStore();
-
- const storyId = 'a--1';
- const {
- api: { setIndex, findSiblingStoryId },
- } = initStoriesAndSetState({ store, navigate, storyId, viewMode: 'story', provider } as any);
- setIndex({ v: 4, entries: navigationEntries });
+ const initialState = { path: '/story/a--1', storyId: 'a--1', viewMode: 'story' };
+ const moduleArgs = createMockModuleArgs({ initialState });
+ const { api } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { store } = moduleArgs;
- const result = findSiblingStoryId(storyId, store.getState().index, 1, true);
+ api.setIndex({ v: 4, entries: navigationEntries });
+ const result = api.findSiblingStoryId('a--1', store.getState().index, 1, true);
expect(result).toBe('b-c--1');
});
});
describe('jumpToComponent', () => {
it('works forward', () => {
- const navigate = jest.fn();
- const store = createMockStore();
+ const initialState = { path: '/story/a--1', storyId: 'a--1', viewMode: 'story' };
+ const moduleArgs = createMockModuleArgs({ initialState });
+ const { api } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { navigate } = moduleArgs;
- const {
- api: { setIndex, jumpToComponent },
- } = initStoriesAndSetState({
- store,
- navigate,
- storyId: 'a--1',
- viewMode: 'story',
- provider,
- } as any);
- setIndex({ v: 4, entries: navigationEntries });
-
- jumpToComponent(1);
+ api.setIndex({ v: 4, entries: navigationEntries });
+ api.jumpToComponent(1);
expect(navigate).toHaveBeenCalledWith('/story/b-c--1');
});
-
it('works backwards', () => {
- const navigate = jest.fn();
- const store = createMockStore();
-
- const {
- api: { setIndex, jumpToComponent },
- } = initStoriesAndSetState({
- store,
- navigate,
+ const initialState = {
+ path: '/story/b-c--1',
storyId: 'b-c--1',
viewMode: 'story',
- provider,
- } as any);
- setIndex({ v: 4, entries: navigationEntries });
+ };
+ const moduleArgs = createMockModuleArgs({ initialState });
+ const { api } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { navigate } = moduleArgs;
- jumpToComponent(-1);
+ api.setIndex({ v: 4, entries: navigationEntries });
+ api.jumpToComponent(-1);
expect(navigate).toHaveBeenCalledWith('/story/a--1');
});
-
it('does nothing if you are in the last component and go forward', () => {
- const navigate = jest.fn();
- const store = createMockStore();
-
- const {
- api: { setIndex, jumpToComponent },
- } = initStoriesAndSetState({
- store,
- navigate,
+ const initialState = {
+ path: '/story/custom-id--1',
storyId: 'custom-id--1',
viewMode: 'story',
- provider,
- } as any);
- setIndex({ v: 4, entries: navigationEntries });
+ };
+ const moduleArgs = createMockModuleArgs({ initialState });
+ const { api } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { navigate } = moduleArgs;
- jumpToComponent(1);
+ api.setIndex({ v: 4, entries: navigationEntries });
+ api.jumpToComponent(1);
expect(navigate).not.toHaveBeenCalled();
});
-
it('does nothing if you are at the first component and go backward', () => {
- const navigate = jest.fn();
- const store = createMockStore();
-
- const {
- api: { setIndex, jumpToComponent },
- } = initStoriesAndSetState({
- store,
- navigate,
- storyId: 'a--2',
- viewMode: 'story',
- provider,
- } as any);
- setIndex({ v: 4, entries: navigationEntries });
+ const initialState = { path: '/story/a--2', storyId: 'a--2', viewMode: 'story' };
+ const moduleArgs = createMockModuleArgs({ initialState });
+ const { api } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { navigate } = moduleArgs;
- jumpToComponent(-1);
+ api.setIndex({ v: 4, entries: navigationEntries });
+ api.jumpToComponent(-1);
expect(navigate).not.toHaveBeenCalled();
});
});
-
describe('selectStory', () => {
it('navigates', () => {
- const navigate = jest.fn();
- const store = createMockStore({ storyId: 'a--1', viewMode: 'story' });
- const {
- api: { setIndex, selectStory },
- } = initStoriesAndSetState({ store, navigate, provider } as any);
- setIndex({ v: 4, entries: navigationEntries });
+ const initialState = { path: '/story/a--1', storyId: 'a--1', viewMode: 'story' };
+ const moduleArgs = createMockModuleArgs({ initialState });
+ const { api } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { navigate } = moduleArgs;
- selectStory('a--2');
+ api.setIndex({ v: 4, entries: navigationEntries });
+ api.selectStory('a--2');
expect(navigate).toHaveBeenCalledWith('/story/a--2');
});
-
it('sets view mode to docs if doc-level component is selected', () => {
- const navigate = jest.fn();
- const store = createMockStore({ storyId: 'a--1', viewMode: 'docs' });
- const {
- api: { setIndex, selectStory },
- } = initStoriesAndSetState({ store, navigate, provider } as any);
- setIndex({
+ const initialState = { path: '/docs/a--1', storyId: 'a--1', viewMode: 'docs' };
+ const moduleArgs = createMockModuleArgs({ initialState });
+ const { api } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { navigate } = moduleArgs;
+
+ api.setIndex({
v: 4,
entries: {
...navigationEntries,
@@ -1272,194 +894,129 @@ describe('stories API', () => {
},
},
});
-
- selectStory('intro');
+ api.selectStory('intro');
expect(navigate).toHaveBeenCalledWith('/docs/intro--docs');
});
-
- describe('legacy api', () => {
+ describe('deprecated api', () => {
it('allows navigating to a combination of title + name', () => {
- const navigate = jest.fn();
- const store = createMockStore();
- const {
- api: { setIndex, selectStory },
- } = initStoriesAndSetState({
- store,
- storyId: 'a--1',
- viewMode: 'story',
- navigate,
- provider,
- } as any);
- setIndex({ v: 4, entries: navigationEntries });
-
- selectStory('a', '2');
+ const initialState = { path: '/story/a--1', storyId: 'a--1', viewMode: 'story' };
+ const moduleArgs = createMockModuleArgs({ initialState });
+ const { api } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { navigate } = moduleArgs;
+
+ api.setIndex({ v: 4, entries: navigationEntries });
+ api.selectStory('a', '2');
expect(navigate).toHaveBeenCalledWith('/story/a--2');
});
-
it('allows navigating to a given name (in the current component)', () => {
- const navigate = jest.fn();
- const store = createMockStore();
- const {
- api: { setIndex, selectStory },
- } = initStoriesAndSetState({
- store,
- storyId: 'a--1',
- viewMode: 'story',
- navigate,
- provider,
- } as any);
- setIndex({ v: 4, entries: navigationEntries });
-
- selectStory(undefined, '2');
+ const initialState = { path: '/story/a--1', storyId: 'a--1', viewMode: 'story' };
+ const moduleArgs = createMockModuleArgs({ initialState });
+ const { api } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { navigate } = moduleArgs;
+
+ api.setIndex({ v: 4, entries: navigationEntries });
+ api.selectStory(undefined, '2');
expect(navigate).toHaveBeenCalledWith('/story/a--2');
});
});
-
it('allows navigating away from the settings pages', () => {
- const navigate = jest.fn();
- const store = createMockStore({ storyId: 'a--1', viewMode: 'settings' });
- const {
- api: { setIndex, selectStory },
- } = initStoriesAndSetState({ store, navigate, provider } as any);
- setIndex({ v: 4, entries: navigationEntries });
+ const initialState = { path: '/settings/a--1', storyId: 'a--1', viewMode: 'settings' };
+ const moduleArgs = createMockModuleArgs({ initialState });
+ const { api } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { navigate } = moduleArgs;
- selectStory('a--2');
+ api.setIndex({ v: 4, entries: navigationEntries });
+ api.selectStory('a--2');
expect(navigate).toHaveBeenCalledWith('/story/a--2');
});
-
it('allows navigating to first story in component on call by component id', () => {
- const navigate = jest.fn();
- const store = createMockStore();
- const {
- api: { setIndex, selectStory },
- } = initStoriesAndSetState({
- store,
- storyId: 'a--1',
- viewMode: 'story',
- navigate,
- provider,
- } as any);
- setIndex({ v: 4, entries: navigationEntries });
+ const initialState = { path: '/story/a--1', storyId: 'a--1', viewMode: 'story' };
+ const moduleArgs = createMockModuleArgs({ initialState });
+ const { api } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { navigate } = moduleArgs;
- selectStory('a');
+ api.setIndex({ v: 4, entries: navigationEntries });
+ api.selectStory('a');
expect(navigate).toHaveBeenCalledWith('/story/a--1');
});
-
it('allows navigating to first story in group on call by group id', () => {
- const navigate = jest.fn();
- const store = createMockStore();
- const {
- api: { setIndex, selectStory },
- } = initStoriesAndSetState({
- store,
- storyId: 'a--1',
- viewMode: 'story',
- navigate,
- provider,
- } as any);
- setIndex({ v: 4, entries: navigationEntries });
+ const initialState = { path: '/story/a--1', storyId: 'a--1', viewMode: 'story' };
+ const moduleArgs = createMockModuleArgs({ initialState });
+ const { api } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { navigate } = moduleArgs;
- selectStory('b');
+ api.setIndex({ v: 4, entries: navigationEntries });
+ api.selectStory('b');
expect(navigate).toHaveBeenCalledWith('/story/b-c--1');
});
-
it('allows navigating to first story in component on call by title', () => {
- const navigate = jest.fn();
- const store = createMockStore();
- const {
- api: { setIndex, selectStory },
- } = initStoriesAndSetState({
- store,
- storyId: 'a--1',
- viewMode: 'story',
- navigate,
- provider,
- } as any);
- setIndex({ v: 4, entries: navigationEntries });
+ const initialState = { path: '/story/a--1', storyId: 'a--1', viewMode: 'story' };
+ const moduleArgs = createMockModuleArgs({ initialState });
+ const { api } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { navigate } = moduleArgs;
- selectStory('A');
+ api.setIndex({ v: 4, entries: navigationEntries });
+ api.selectStory('A');
expect(navigate).toHaveBeenCalledWith('/story/a--1');
});
-
it('allows navigating to the first story of the current component if passed nothing', () => {
- const navigate = jest.fn();
- const store = createMockStore();
- const {
- api: { setIndex, selectStory },
- } = initStoriesAndSetState({
- store,
- storyId: 'a--2',
- viewMode: 'story',
- navigate,
- provider,
- } as any);
- setIndex({ v: 4, entries: navigationEntries });
+ const initialState = { path: '/story/a--1', storyId: 'a--1', viewMode: 'story' };
+ const moduleArgs = createMockModuleArgs({ initialState });
+ const { api } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { navigate } = moduleArgs;
- selectStory();
+ api.setIndex({ v: 4, entries: navigationEntries });
+ api.selectStory();
expect(navigate).toHaveBeenCalledWith('/story/a--1');
});
-
describe('component permalinks', () => {
it('allows navigating to kind/storyname (legacy api)', () => {
- const navigate = jest.fn();
- const store = createMockStore();
+ const initialState = { path: '/story/a--1', storyId: 'a--1', viewMode: 'story' };
+ const moduleArgs = createMockModuleArgs({ initialState });
+ const { api } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { navigate } = moduleArgs;
- const {
- api: { selectStory, setIndex },
- } = initStoriesAndSetState({ store, navigate, provider } as any);
- setIndex({ v: 4, entries: navigationEntries });
-
- selectStory('b/e', '1');
+ api.setIndex({ v: 4, entries: navigationEntries });
+ api.selectStory('b/e', '1');
expect(navigate).toHaveBeenCalledWith('/story/custom-id--1');
});
-
it('allows navigating to component permalink/storyname (legacy api)', () => {
- const navigate = jest.fn();
- const store = createMockStore();
-
- const {
- api: { selectStory, setIndex },
- } = initStoriesAndSetState({ store, navigate, provider } as any);
- setIndex({ v: 4, entries: navigationEntries });
+ const initialState = { path: '/story/a--1', storyId: 'a--1', viewMode: 'story' };
+ const moduleArgs = createMockModuleArgs({ initialState });
+ const { api } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { navigate } = moduleArgs;
- selectStory('custom-id', '1');
+ api.setIndex({ v: 4, entries: navigationEntries });
+ api.selectStory('custom-id', '1');
expect(navigate).toHaveBeenCalledWith('/story/custom-id--1');
});
-
it('allows navigating to first story in kind on call by kind', () => {
- const navigate = jest.fn();
- const store = createMockStore();
-
- const {
- api: { selectStory, setIndex },
- } = initStoriesAndSetState({ store, navigate, provider } as any);
- setIndex({ v: 4, entries: navigationEntries });
+ const initialState = { path: '/story/a--1', storyId: 'a--1', viewMode: 'story' };
+ const moduleArgs = createMockModuleArgs({ initialState });
+ const { api } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { navigate } = moduleArgs;
- selectStory('b/e');
+ api.setIndex({ v: 4, entries: navigationEntries });
+ api.selectStory('b/e');
expect(navigate).toHaveBeenCalledWith('/story/custom-id--1');
});
});
});
-
describe('STORY_PREPARED', () => {
it('prepares the story', async () => {
- const navigate = jest.fn();
- const store = createMockStore();
- const fullAPI = Object.assign(new EventEmitter(), {
- setStories: jest.fn(),
- setOptions: jest.fn(),
- });
+ const fullAPI = { setOptions: jest.fn() };
+ const initialState = { path: '/story/a--1', storyId: 'a--1', viewMode: 'story' };
+ const moduleArgs = createMockModuleArgs({ initialState, fullAPI });
+ const { api } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { provider, store } = moduleArgs;
- const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any);
- Object.assign(fullAPI, api);
+ api.setIndex({ v: 4, entries: mockEntries });
- await init();
- fullAPI.emit(STORY_PREPARED, {
+ provider.channel.emit(STORY_PREPARED, {
id: 'component-a--story-1',
parameters: { a: 'b' },
args: { c: 'd' },
});
-
const { index } = store.getState();
expect(index['component-a--story-1']).toMatchObject({
type: 'story',
@@ -1472,54 +1029,42 @@ describe('stories API', () => {
args: { c: 'd' },
});
});
-
it('sets options the first time it is called', async () => {
- const navigate = jest.fn();
- const store = createMockStore();
- const fullAPI = Object.assign(new EventEmitter(), {
- setStories: jest.fn(),
- setOptions: jest.fn(),
- });
+ const fullAPI = { setOptions: jest.fn() };
+ const initialState = { path: '/story/a--1', storyId: 'a--1', viewMode: 'story' };
+ const moduleArgs = createMockModuleArgs({ initialState, fullAPI });
+ const { api } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { provider } = moduleArgs;
- const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any);
- Object.assign(fullAPI, api);
+ api.setIndex({ v: 4, entries: mockEntries });
- await init();
- fullAPI.emit(STORY_PREPARED, {
+ provider.channel.emit(STORY_PREPARED, {
id: 'component-a--story-1',
parameters: { options: 'options' },
});
-
expect(fullAPI.setOptions).toHaveBeenCalledWith('options');
fullAPI.setOptions.mockClear();
- fullAPI.emit(STORY_PREPARED, {
+
+ provider.channel.emit(STORY_PREPARED, {
id: 'component-a--story-1',
parameters: { options: 'options2' },
});
-
expect(fullAPI.setOptions).not.toHaveBeenCalled();
});
});
-
describe('DOCS_PREPARED', () => {
it('prepares the docs entry', async () => {
- const navigate = jest.fn();
- const store = createMockStore();
- const fullAPI = Object.assign(new EventEmitter(), {
- setStories: jest.fn(),
- setOptions: jest.fn(),
- });
+ const moduleArgs = createMockModuleArgs({});
+ const { api } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { provider, store } = moduleArgs;
- const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any);
- Object.assign(fullAPI, api);
+ api.setIndex({ v: 4, entries: mockEntries });
- await init();
- fullAPI.emit(DOCS_PREPARED, {
+ provider.channel.emit(DOCS_PREPARED, {
id: 'component-a--docs',
parameters: { a: 'b' },
});
-
const { index } = store.getState();
expect(index['component-a--docs']).toMatchObject({
type: 'docs',
@@ -1532,104 +1077,75 @@ describe('stories API', () => {
});
});
});
-
describe('CONFIG_ERROR', () => {
it('sets previewInitialized to true, local', async () => {
- const navigate = jest.fn();
- const store = createMockStore();
- const fullAPI = Object.assign(new EventEmitter(), {});
-
- const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any);
- Object.assign(fullAPI, api);
+ const moduleArgs = createMockModuleArgs({});
+ const { api } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { provider, store } = moduleArgs;
- await init();
-
- fullAPI.emit(CONFIG_ERROR, { message: 'Failed to run configure' });
+ api.setIndex({ v: 4, entries: mockEntries });
+ provider.channel.emit(CONFIG_ERROR, { message: 'Failed to run configure' });
const { previewInitialized } = store.getState();
expect(previewInitialized).toBe(true);
});
-
it('sets previewInitialized to true, ref', async () => {
- const navigate = jest.fn();
- const fullAPI = Object.assign(new EventEmitter(), {
- updateRef: jest.fn(),
- });
- const store = createMockStore();
- const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any);
+ const fullAPI = { updateRef: jest.fn() };
+ const moduleArgs = createMockModuleArgs({ fullAPI });
+ const { api } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { provider } = moduleArgs;
- Object.assign(fullAPI, api);
+ api.setIndex({ v: 4, entries: mockEntries });
- getEventMetadataMock.mockReturnValueOnce({
+ getEventMetadata.mockReturnValueOnce({
sourceType: 'external',
ref: { id: 'refId', stories: { 'a--1': { args: { a: 'b' } } } },
} as any);
- await init();
- fullAPI.emit(CONFIG_ERROR, { message: 'Failed to run configure' });
-
+ provider.channel.emit(CONFIG_ERROR, { message: 'Failed to run configure' });
expect(fullAPI.updateRef.mock.calls.length).toBe(1);
expect(fullAPI.updateRef.mock.calls[0][1]).toEqual({
previewInitialized: true,
});
});
});
-
describe('STORY_MISSING', () => {
it('sets previewInitialized to true, local', async () => {
- const navigate = jest.fn();
- const store = createMockStore();
- const fullAPI = Object.assign(new EventEmitter(), {});
-
- const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any);
- Object.assign(fullAPI, api);
-
- await init();
-
- fullAPI.emit(STORY_MISSING, { message: 'Failed to run configure' });
+ const moduleArgs = createMockModuleArgs({});
+ initStories(moduleArgs as unknown as ModuleArgs);
+ const { provider, store } = moduleArgs;
+ provider.channel.emit(STORY_MISSING, { message: 'Failed to run configure' });
const { previewInitialized } = store.getState();
expect(previewInitialized).toBe(true);
});
-
it('sets previewInitialized to true, ref', async () => {
- const navigate = jest.fn();
- const fullAPI = Object.assign(new EventEmitter(), {
- updateRef: jest.fn(),
- });
- const store = createMockStore();
- const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any);
+ const fullAPI = { updateRef: jest.fn() };
+ const moduleArgs = createMockModuleArgs({ fullAPI });
+ initStories(moduleArgs as unknown as ModuleArgs);
+ const { provider } = moduleArgs;
- Object.assign(fullAPI, api);
-
- getEventMetadataMock.mockReturnValueOnce({
+ getEventMetadata.mockReturnValueOnce({
sourceType: 'external',
ref: { id: 'refId', stories: { 'a--1': { args: { a: 'b' } } } },
} as any);
- await init();
- fullAPI.emit(STORY_MISSING, { message: 'Failed to run configure' });
-
+ provider.channel.emit(STORY_MISSING, { message: 'Failed to run configure' });
expect(fullAPI.updateRef.mock.calls.length).toBe(1);
expect(fullAPI.updateRef.mock.calls[0][1]).toEqual({
previewInitialized: true,
});
});
});
-
describe('v2 SET_STORIES event', () => {
it('normalizes parameters and calls setRef for external stories', () => {
- const fullAPI = Object.assign(new EventEmitter(), {});
- const navigate = jest.fn();
- const store = createMockStore();
-
- const { init, api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any);
- const finalAPI = Object.assign(fullAPI, api, {
- setIndex: jest.fn(),
+ const fullAPI = {
findRef: jest.fn(),
setRef: jest.fn(),
- });
- init();
+ };
+ const moduleArgs = createMockModuleArgs({ fullAPI });
+ initStories(moduleArgs as unknown as ModuleArgs);
+ const { provider, store } = moduleArgs;
- getEventMetadataMock.mockReturnValueOnce({
+ getEventMetadata.mockReturnValueOnce({
sourceType: 'external',
ref: { id: 'ref' },
} as any);
@@ -1639,10 +1155,9 @@ describe('stories API', () => {
kindParameters: { a: { kind: 'kind' } },
stories: { 'a--1': { kind: 'a', parameters: { story: 'story' } } },
};
- finalAPI.emit(SET_STORIES, setStoriesPayload);
-
- expect(finalAPI.setIndex).not.toHaveBeenCalled();
- expect(finalAPI.setRef).toHaveBeenCalledWith(
+ provider.channel.emit(SET_STORIES, setStoriesPayload);
+ expect(store.getState().index).toBeUndefined();
+ expect(fullAPI.setRef).toHaveBeenCalledWith(
'ref',
{
id: 'ref',
@@ -1656,28 +1171,23 @@ describe('stories API', () => {
});
describe('legacy (v1) SET_STORIES event', () => {
it('calls setRef with stories', () => {
- const fullAPI = Object.assign(new EventEmitter());
- const navigate = jest.fn();
- const store = createMockStore();
-
- const { init, api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any);
- Object.assign(fullAPI, api, {
- setIndex: jest.fn(),
+ const fullAPI = {
findRef: jest.fn(),
setRef: jest.fn(),
- });
- init();
+ };
+ const moduleArgs = createMockModuleArgs({ fullAPI });
+ initStories(moduleArgs as unknown as ModuleArgs);
+ const { provider, store } = moduleArgs;
- getEventMetadataMock.mockReturnValueOnce({
+ getEventMetadata.mockReturnValueOnce({
sourceType: 'external',
ref: { id: 'ref' },
} as any);
const setStoriesPayload = {
stories: { 'a--1': {} },
};
- fullAPI.emit(SET_STORIES, setStoriesPayload);
-
- expect(fullAPI.setIndex).not.toHaveBeenCalled();
+ provider.channel.emit(SET_STORIES, setStoriesPayload);
+ expect(store.getState().index).toBeUndefined();
expect(fullAPI.setRef).toHaveBeenCalledWith(
'ref',
{
@@ -1690,47 +1200,33 @@ describe('stories API', () => {
);
});
});
+});
+describe('experimental_updateStatus', () => {
+ it('is included in the initial state', () => {
+ const moduleArgs = createMockModuleArgs({});
+ const { state } = initStories(moduleArgs as unknown as ModuleArgs);
- describe('experimental_updateStatus', () => {
- it('is included in the initial state', () => {
- const { state } = initStoriesAndSetState({
- storyId: 'id',
- viewMode: 'story',
- } as ModuleArgs);
-
- expect(state).toEqual(
- expect.objectContaining({
- status: {},
- })
- );
- });
-
- it('updates a story', async () => {
- const fullAPI = Object.assign(new EventEmitter());
- const navigate = jest.fn();
- const store = createMockStore();
-
- const { init, api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any);
-
- const API: SubAPI = Object.assign(fullAPI, api, {
- setIndex: jest.fn(),
- findRef: jest.fn(),
- setRef: jest.fn(),
- });
-
- await init();
-
- await expect(
- API.experimental_updateStatus('a-addon-id', {
- 'a-story-id': {
- status: 'pending',
- title: 'an addon title',
- description: 'an addon description',
- },
- })
- ).resolves.not.toThrow();
-
- expect(store.getState().status).toMatchInlineSnapshot(`
+ expect(state).toEqual(
+ expect.objectContaining({
+ status: {},
+ })
+ );
+ });
+ it('updates a story', async () => {
+ const moduleArgs = createMockModuleArgs({});
+ const { api } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { store } = moduleArgs;
+
+ await expect(
+ api.experimental_updateStatus('a-addon-id', {
+ 'a-story-id': {
+ status: 'pending',
+ title: 'an addon title',
+ description: 'an addon description',
+ },
+ })
+ ).resolves.not.toThrow();
+ expect(store.getState().status).toMatchInlineSnapshot(`
Object {
"a-story-id": Object {
"a-addon-id": Object {
@@ -1741,35 +1237,23 @@ describe('stories API', () => {
},
}
`);
- });
-
- it('updates multiple stories', async () => {
- const fullAPI = Object.assign(new EventEmitter());
- const navigate = jest.fn();
- const store = createMockStore();
-
- const { init, api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any);
-
- const API: SubAPI = Object.assign(fullAPI, api, {
- setIndex: jest.fn(),
- findRef: jest.fn(),
- setRef: jest.fn(),
- });
-
- await init();
-
- await expect(
- API.experimental_updateStatus('a-addon-id', {
- 'a-story-id': {
- status: 'pending',
- title: 'an addon title',
- description: 'an addon description',
- },
- 'another-story-id': { status: 'success', title: 'a addon title', description: '' },
- })
- ).resolves.not.toThrow();
-
- expect(store.getState().status).toMatchInlineSnapshot(`
+ });
+ it('updates multiple stories', async () => {
+ const moduleArgs = createMockModuleArgs({});
+ const { api } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { store } = moduleArgs;
+
+ await expect(
+ api.experimental_updateStatus('a-addon-id', {
+ 'a-story-id': {
+ status: 'pending',
+ title: 'an addon title',
+ description: 'an addon description',
+ },
+ 'another-story-id': { status: 'success', title: 'a addon title', description: '' },
+ })
+ ).resolves.not.toThrow();
+ expect(store.getState().status).toMatchInlineSnapshot(`
Object {
"a-story-id": Object {
"a-addon-id": Object {
@@ -1787,6 +1271,132 @@ describe('stories API', () => {
},
}
`);
+ });
+ describe('experimental_setFilter', () => {
+ it('is included in the initial state', () => {
+ const moduleArgs = createMockModuleArgs({});
+ const { state } = initStories(moduleArgs as unknown as ModuleArgs);
+
+ expect(state).toEqual(
+ expect.objectContaining({
+ filters: {},
+ })
+ );
+ });
+ it('updates state', () => {
+ const moduleArgs = createMockModuleArgs({});
+ const { api } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { store } = moduleArgs;
+
+ api.experimental_setFilter('myCustomFilter', () => true);
+
+ expect(store.getState()).toEqual(
+ expect.objectContaining({
+ filters: {
+ myCustomFilter: expect.any(Function),
+ },
+ })
+ );
+ });
+
+ it('can filter', () => {
+ const moduleArgs = createMockModuleArgs({});
+ const {
+ api,
+ state: { status },
+ } = initStories(moduleArgs as unknown as ModuleArgs);
+ const { store } = moduleArgs;
+
+ /**
+ * This function is a copy of the one in the containers/sidebar.ts file inside of ui/manager
+ * I'm hoping we can eventually merge this 2 packages so there's no odd looking import and no re-implementation.
+ */
+ const applyFilters = (originalIndex: API_IndexHash) => {
+ if (!originalIndex) {
+ return originalIndex;
+ }
+
+ const filtered = new Set();
+ Object.values(originalIndex).forEach((item) => {
+ if (item.type === 'story' || item.type === 'docs') {
+ let result = true;
+
+ Object.values(filters).forEach((filter) => {
+ if (result === true) {
+ result = filter({ ...item, status: status[item.id] });
+ }
+ });
+
+ if (result) {
+ filtered.add(item.id);
+ getAncestorIds(originalIndex, item.id).forEach((id) => {
+ filtered.add(id);
+ });
+ }
+ }
+ });
+
+ return Object.fromEntries(
+ Object.entries(originalIndex).filter(([key]) => filtered.has(key))
+ );
+ };
+
+ api.experimental_setFilter('myCustomFilter', (item) => item.id.startsWith('a'));
+ api.setIndex({ v: 4, entries: navigationEntries });
+
+ const { index, filters } = store.getState();
+
+ const filtered = applyFilters(index);
+
+ expect(filtered).toMatchInlineSnapshot(`
+ Object {
+ "a": Object {
+ "children": Array [
+ "a--1",
+ "a--2",
+ ],
+ "depth": 0,
+ "id": "a",
+ "isComponent": true,
+ "isLeaf": false,
+ "isRoot": false,
+ "name": "a",
+ "parent": undefined,
+ "renderLabel": undefined,
+ "type": "component",
+ },
+ "a--1": Object {
+ "depth": 1,
+ "id": "a--1",
+ "importPath": "./a.ts",
+ "isComponent": false,
+ "isLeaf": true,
+ "isRoot": false,
+ "kind": "a",
+ "name": "1",
+ "parent": "a",
+ "prepared": false,
+ "renderLabel": undefined,
+ "title": "a",
+ "type": "story",
+ },
+ "a--2": Object {
+ "depth": 1,
+ "id": "a--2",
+ "importPath": "./a.ts",
+ "isComponent": false,
+ "isLeaf": true,
+ "isRoot": false,
+ "kind": "a",
+ "name": "2",
+ "parent": "a",
+ "prepared": false,
+ "renderLabel": undefined,
+ "title": "a",
+ "type": "story",
+ },
+ }
+ `);
});
});
});
diff --git a/code/lib/manager-api/src/tests/url.test.js b/code/lib/manager-api/src/tests/url.test.js
index c269331b1c77..33cf4a1872c1 100644
--- a/code/lib/manager-api/src/tests/url.test.js
+++ b/code/lib/manager-api/src/tests/url.test.js
@@ -2,6 +2,7 @@ import qs from 'qs';
import { SET_CURRENT_STORY, GLOBALS_UPDATED, UPDATE_QUERY_PARAMS } from '@storybook/core-events';
+import EventEmitter from 'events';
import { init as initURL } from '../modules/url';
jest.mock('@storybook/client-logger');
@@ -17,7 +18,7 @@ describe('initial state', () => {
const {
state: { layout },
- } = initURL({ navigate, state: { location } });
+ } = initURL({ navigate, state: { location }, provider: { channel: new EventEmitter() } });
expect(layout).toEqual({ isFullscreen: true });
});
@@ -28,7 +29,7 @@ describe('initial state', () => {
const {
state: { layout },
- } = initURL({ navigate, state: { location } });
+ } = initURL({ navigate, state: { location }, provider: { channel: new EventEmitter() } });
expect(layout).toEqual({ showNav: false });
});
@@ -39,7 +40,7 @@ describe('initial state', () => {
const {
state: { ui },
- } = initURL({ navigate, state: { location } });
+ } = initURL({ navigate, state: { location }, provider: { channel: new EventEmitter() } });
expect(ui).toEqual({ enableShortcuts: false });
});
@@ -50,7 +51,7 @@ describe('initial state', () => {
const {
state: { layout },
- } = initURL({ navigate, state: { location } });
+ } = initURL({ navigate, state: { location }, provider: { channel: new EventEmitter() } });
expect(layout).toEqual({ panelPosition: 'bottom' });
});
@@ -61,7 +62,7 @@ describe('initial state', () => {
const {
state: { layout },
- } = initURL({ navigate, state: { location } });
+ } = initURL({ navigate, state: { location }, provider: { channel: new EventEmitter() } });
expect(layout).toEqual({ panelPosition: 'right' });
});
@@ -72,7 +73,7 @@ describe('initial state', () => {
const {
state: { layout },
- } = initURL({ navigate, state: { location } });
+ } = initURL({ navigate, state: { location }, provider: { channel: new EventEmitter() } });
expect(layout).toEqual({ showPanel: false });
});
@@ -88,18 +89,23 @@ describe('queryParams', () => {
},
getState: () => state,
};
- const fullAPI = { emit: jest.fn() };
+ const channel = new EventEmitter();
const { api } = initURL({
state: { location: { search: '' } },
navigate: jest.fn(),
store,
- fullAPI,
+ provider: { channel },
});
+ const listener = jest.fn();
+
+ channel.on(UPDATE_QUERY_PARAMS, listener);
+
api.setQueryParams({ foo: 'bar' });
expect(api.getQueryParam('foo')).toEqual('bar');
- expect(fullAPI.emit).toHaveBeenCalledWith(UPDATE_QUERY_PARAMS, { foo: 'bar' });
+
+ expect(listener).toHaveBeenCalledWith({ foo: 'bar' });
});
});
@@ -120,14 +126,6 @@ describe('initModule', () => {
});
const fullAPI = {
- callbacks: {},
- on(event, fn) {
- this.callbacks[event] = this.callbacks[event] || [];
- this.callbacks[event].push(fn);
- },
- emit(event, ...args) {
- this.callbacks[event]?.forEach((cb) => cb(...args));
- },
showReleaseNotesOnLaunch: jest.fn(),
};
@@ -140,19 +138,22 @@ describe('initModule', () => {
store.setState(storyState('test--story'));
const navigate = jest.fn();
-
- const { api, init } = initURL({ store, state: { location: {} }, navigate, fullAPI });
- Object.assign(fullAPI, api, {
- getCurrentStoryData: () => ({
- type: 'story',
- args: { a: 1, b: 2 },
- initialArgs: { a: 1, b: 1 },
- isLeaf: true,
+ const channel = new EventEmitter();
+ initURL({
+ store,
+ provider: { channel },
+ state: { location: {} },
+ navigate,
+ fullAPI: Object.assign(fullAPI, {
+ getCurrentStoryData: () => ({
+ type: 'story',
+ args: { a: 1, b: 2 },
+ initialArgs: { a: 1, b: 1 },
+ isLeaf: true,
+ }),
}),
});
- init();
-
- fullAPI.emit(SET_CURRENT_STORY);
+ channel.emit(SET_CURRENT_STORY);
expect(navigate).toHaveBeenCalledWith(
'/story/test--story&args=b:2',
expect.objectContaining({ replace: true })
@@ -164,12 +165,10 @@ describe('initModule', () => {
store.setState(storyState('test--story'));
const navigate = jest.fn();
+ const channel = new EventEmitter();
+ initURL({ store, provider: { channel }, state: { location: {} }, navigate, fullAPI });
- const { api, init } = initURL({ store, state: { location: {} }, navigate, fullAPI });
- Object.assign(fullAPI, api);
- init();
-
- fullAPI.emit(GLOBALS_UPDATED, { globals: { a: 2 }, initialGlobals: { a: 1, b: 1 } });
+ channel.emit(GLOBALS_UPDATED, { globals: { a: 2 }, initialGlobals: { a: 1, b: 1 } });
expect(navigate).toHaveBeenCalledWith(
'/story/test--story&globals=a:2;b:!undefined',
expect.objectContaining({ replace: true })
@@ -180,20 +179,24 @@ describe('initModule', () => {
it('adds url params alphabetically', async () => {
store.setState({ ...storyState('test--story'), customQueryParams: { full: 1 } });
const navigate = jest.fn();
-
- const { api, init } = initURL({ store, state: { location: {} }, navigate, fullAPI });
- Object.assign(fullAPI, api, {
- getCurrentStoryData: () => ({ type: 'story', args: { a: 1 }, isLeaf: true }),
+ const channel = new EventEmitter();
+ const { api } = initURL({
+ store,
+ provider: { channel },
+ state: { location: {} },
+ navigate,
+ fullAPI: Object.assign(fullAPI, {
+ getCurrentStoryData: () => ({ type: 'story', args: { a: 1 }, isLeaf: true }),
+ }),
});
- init();
- fullAPI.emit(GLOBALS_UPDATED, { globals: { g: 2 } });
+ channel.emit(GLOBALS_UPDATED, { globals: { g: 2 } });
expect(navigate).toHaveBeenCalledWith(
'/story/test--story&full=1&globals=g:2',
expect.objectContaining({ replace: true })
);
- fullAPI.emit(SET_CURRENT_STORY);
+ channel.emit(SET_CURRENT_STORY);
expect(navigate).toHaveBeenCalledWith(
'/story/test--story&args=a:1&full=1&globals=g:2',
expect.objectContaining({ replace: true })
diff --git a/code/lib/manager-api/src/version.ts b/code/lib/manager-api/src/version.ts
index a5a65afa751e..70107dd59789 100644
--- a/code/lib/manager-api/src/version.ts
+++ b/code/lib/manager-api/src/version.ts
@@ -1 +1 @@
-export const version = '7.3.0-alpha.0';
+export const version = '7.4.0-alpha.0';
diff --git a/code/lib/node-logger/package.json b/code/lib/node-logger/package.json
index 497b2a521760..0870a33674a3 100644
--- a/code/lib/node-logger/package.json
+++ b/code/lib/node-logger/package.json
@@ -1,6 +1,6 @@
{
"name": "@storybook/node-logger",
- "version": "7.3.0-alpha.0",
+ "version": "7.4.0-alpha.0",
"description": "",
"keywords": [
"storybook"
@@ -34,7 +34,8 @@
"dist/**/*",
"README.md",
"*.js",
- "*.d.ts"
+ "*.d.ts",
+ "!src/**/*"
],
"scripts": {
"check": "../../../scripts/prepare/check.ts",
diff --git a/code/lib/postinstall/package.json b/code/lib/postinstall/package.json
index dc58f48394ac..49c70de632b4 100644
--- a/code/lib/postinstall/package.json
+++ b/code/lib/postinstall/package.json
@@ -1,6 +1,6 @@
{
"name": "@storybook/postinstall",
- "version": "7.3.0-alpha.0",
+ "version": "7.4.0-alpha.0",
"description": "Storybook addons postinstall utilities",
"keywords": [
"api",
@@ -37,7 +37,8 @@
"dist/**/*",
"README.md",
"*.js",
- "*.d.ts"
+ "*.d.ts",
+ "!src/**/*"
],
"scripts": {
"check": "../../../scripts/prepare/check.ts",
diff --git a/code/lib/preview-api/package.json b/code/lib/preview-api/package.json
index 966ea6360c45..b7e9fa5add43 100644
--- a/code/lib/preview-api/package.json
+++ b/code/lib/preview-api/package.json
@@ -1,6 +1,6 @@
{
"name": "@storybook/preview-api",
- "version": "7.3.0-alpha.0",
+ "version": "7.4.0-alpha.0",
"description": "",
"keywords": [
"storybook"
@@ -60,7 +60,8 @@
"dist/**/*",
"README.md",
"*.js",
- "*.d.ts"
+ "*.d.ts",
+ "!src/**/*"
],
"scripts": {
"check": "../../../scripts/prepare/check.ts",
diff --git a/code/lib/preview-api/src/modules/store/StoryIndexStore.test.ts b/code/lib/preview-api/src/modules/store/StoryIndexStore.test.ts
index a898ecd1b06c..c906d7a21a75 100644
--- a/code/lib/preview-api/src/modules/store/StoryIndexStore.test.ts
+++ b/code/lib/preview-api/src/modules/store/StoryIndexStore.test.ts
@@ -154,7 +154,7 @@ describe('StoryIndexStore', () => {
const store = new StoryIndexStore(storyIndex);
expect(() => store.storyIdToEntry('random')).toThrow(
- /Couldn't find story matching 'random'/
+ /Couldn't find story matching id 'random'/
);
});
});
diff --git a/code/lib/preview-api/src/modules/store/StoryIndexStore.ts b/code/lib/preview-api/src/modules/store/StoryIndexStore.ts
index 8731f3d495e1..03e8a129b9bf 100644
--- a/code/lib/preview-api/src/modules/store/StoryIndexStore.ts
+++ b/code/lib/preview-api/src/modules/store/StoryIndexStore.ts
@@ -1,4 +1,3 @@
-import { dedent } from 'ts-dedent';
import type {
IndexEntry,
Path,
@@ -8,6 +7,7 @@ import type {
ComponentTitle,
} from '@storybook/types';
import memoize from 'memoizerific';
+import { MissingStoryAfterHmrError } from '@storybook/core-events/preview-errors';
export type StorySpecifier = StoryId | { name: StoryName; title: ComponentTitle } | '*';
@@ -49,11 +49,7 @@ export class StoryIndexStore {
storyIdToEntry(storyId: StoryId): IndexEntry {
const storyEntry = this.entries[storyId];
if (!storyEntry) {
- throw new Error(dedent`Couldn't find story matching '${storyId}' after HMR.
- - Did you remove it from your CSF file?
- - Are you sure a story with that id exists?
- - Please check your entries field of your main.js config.
- - Also check the browser console and terminal for error messages.`);
+ throw new MissingStoryAfterHmrError({ storyId });
}
return storyEntry;
diff --git a/code/lib/preview-api/src/modules/store/csf/testing-utils/index.ts b/code/lib/preview-api/src/modules/store/csf/testing-utils/index.ts
index 40fcddcb6e35..10468737e6dc 100644
--- a/code/lib/preview-api/src/modules/store/csf/testing-utils/index.ts
+++ b/code/lib/preview-api/src/modules/store/csf/testing-utils/index.ts
@@ -10,7 +10,7 @@ import type {
Store_CSFExports,
StoryContext,
Parameters,
- PreparedStoryFn,
+ ComposedStoryFn,
} from '@storybook/types';
import { HooksContext } from '../../../addons';
@@ -36,7 +36,7 @@ export function composeStory = GLOBAL_STORYBOOK_PROJECT_ANNOTATIONS as ProjectAnnotations,
defaultConfig: ProjectAnnotations = {},
exportsName?: string
-): PreparedStoryFn> {
+): ComposedStoryFn> {
if (storyAnnotations === undefined) {
throw new Error('Expected a story but received undefined.');
}
@@ -73,22 +73,25 @@ export function composeStory) => {
- const context: Partial = {
- ...story,
- hooks: new HooksContext(),
- globals: defaultGlobals,
- args: { ...story.initialArgs, ...extraArgs },
- };
-
- return story.unboundStoryFn(prepareContext(context as StoryContext));
- };
-
- composedStory.storyName = storyName;
- composedStory.args = story.initialArgs as Partial;
- composedStory.play = story.playFunction as ComposedStoryPlayFn>;
- composedStory.parameters = story.parameters as Parameters;
- composedStory.id = story.id;
+ const composedStory: ComposedStoryFn> = Object.assign(
+ (extraArgs?: Partial) => {
+ const context: Partial = {
+ ...story,
+ hooks: new HooksContext(),
+ globals: defaultGlobals,
+ args: { ...story.initialArgs, ...extraArgs },
+ };
+
+ return story.unboundStoryFn(prepareContext(context as StoryContext));
+ },
+ {
+ storyName,
+ args: story.initialArgs as Partial,
+ play: story.playFunction as ComposedStoryPlayFn>,
+ parameters: story.parameters as Parameters,
+ id: story.id,
+ }
+ );
return composedStory;
}
diff --git a/code/lib/preview-api/src/typings.d.ts b/code/lib/preview-api/src/typings.d.ts
index fb9194834b96..bedefed4b9a8 100644
--- a/code/lib/preview-api/src/typings.d.ts
+++ b/code/lib/preview-api/src/typings.d.ts
@@ -24,6 +24,7 @@ declare var __STORYBOOK_PREVIEW__: import('./modules/preview-web/PreviewWeb').Pr
declare var __STORYBOOK_STORY_STORE__: any;
declare var STORYBOOK_HOOKS_CONTEXT: any;
declare var LOGLEVEL: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'silent' | undefined;
+declare var sendTelemetryError: (error: any) => void;
declare module 'ansi-to-html';
declare class AnsiToHtml {
diff --git a/code/lib/preview/package.json b/code/lib/preview/package.json
index 0445b0d24547..7d386ea80aae 100644
--- a/code/lib/preview/package.json
+++ b/code/lib/preview/package.json
@@ -1,6 +1,6 @@
{
"name": "@storybook/preview",
- "version": "7.3.0-alpha.0",
+ "version": "7.4.0-alpha.0",
"description": "",
"keywords": [
"storybook"
@@ -48,7 +48,8 @@
"dist/**/*",
"README.md",
"*.js",
- "*.d.ts"
+ "*.d.ts",
+ "!src/**/*"
],
"scripts": {
"check": "../../../scripts/prepare/check.ts",
@@ -58,6 +59,7 @@
"@storybook/channels": "workspace:*",
"@storybook/client-logger": "workspace:*",
"@storybook/core-events": "workspace:*",
+ "@storybook/global": "^5.0.0",
"@storybook/preview-api": "workspace:*",
"typescript": "~4.9.3"
},
diff --git a/code/lib/preview/src/runtime.ts b/code/lib/preview/src/runtime.ts
index 5e890a133a55..7785e42df8c9 100644
--- a/code/lib/preview/src/runtime.ts
+++ b/code/lib/preview/src/runtime.ts
@@ -1,3 +1,6 @@
+import { TELEMETRY_ERROR } from '@storybook/core-events';
+import { global } from '@storybook/global';
+
import { values } from './globals/runtime';
import { globals } from './globals/types';
@@ -5,5 +8,23 @@ const getKeys = Object.keys as (obj: T) => Array;
// Apply all the globals
getKeys(globals).forEach((key) => {
- (globalThis as any)[globals[key]] = values[key];
+ (global as any)[globals[key]] = values[key];
+});
+
+global.sendTelemetryError = (error: any) => {
+ const channel = global.__STORYBOOK_ADDONS_CHANNEL__;
+ channel.emit(TELEMETRY_ERROR, error);
+};
+
+// handle all uncaught StorybookError at the root of the application and log to telemetry if applicable
+global.addEventListener('error', (args: any) => {
+ const error = args.error || args;
+ if (error.fromStorybook) {
+ global.sendTelemetryError(error);
+ }
+});
+global.addEventListener('unhandledrejection', ({ reason }: any) => {
+ if (reason.fromStorybook) {
+ global.sendTelemetryError(reason);
+ }
});
diff --git a/code/lib/preview/src/typings.d.ts b/code/lib/preview/src/typings.d.ts
index bfd9e55123ff..a816c261fa72 100644
--- a/code/lib/preview/src/typings.d.ts
+++ b/code/lib/preview/src/typings.d.ts
@@ -1 +1,5 @@
+/* eslint-disable @typescript-eslint/naming-convention */
declare var LOGLEVEL: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'silent' | undefined;
+
+declare var __STORYBOOK_ADDONS_CHANNEL__: any;
+declare var sendTelemetryError: (error: any) => void;
diff --git a/code/lib/react-dom-shim/package.json b/code/lib/react-dom-shim/package.json
index 6db51a1302a0..d2ae7e06a70e 100644
--- a/code/lib/react-dom-shim/package.json
+++ b/code/lib/react-dom-shim/package.json
@@ -1,6 +1,6 @@
{
"name": "@storybook/react-dom-shim",
- "version": "7.3.0-alpha.0",
+ "version": "7.4.0-alpha.0",
"description": "",
"keywords": [
"storybook"
@@ -46,7 +46,8 @@
"dist/**/*",
"README.md",
"*.js",
- "*.d.ts"
+ "*.d.ts",
+ "!src/**/*"
],
"scripts": {
"check": "../../../scripts/prepare/check.ts",
diff --git a/code/lib/router/package.json b/code/lib/router/package.json
index 175b765ac2be..742df75b4d4a 100644
--- a/code/lib/router/package.json
+++ b/code/lib/router/package.json
@@ -1,6 +1,6 @@
{
"name": "@storybook/router",
- "version": "7.3.0-alpha.0",
+ "version": "7.4.0-alpha.0",
"description": "Core Storybook Router",
"keywords": [
"storybook"
@@ -41,7 +41,8 @@
"dist/**/*",
"README.md",
"*.js",
- "*.d.ts"
+ "*.d.ts",
+ "!src/**/*"
],
"scripts": {
"check": "../../../scripts/prepare/check.ts",
diff --git a/code/lib/source-loader/package.json b/code/lib/source-loader/package.json
index e86bb6e5c358..0f53d7cd35e3 100644
--- a/code/lib/source-loader/package.json
+++ b/code/lib/source-loader/package.json
@@ -1,6 +1,6 @@
{
"name": "@storybook/source-loader",
- "version": "7.3.0-alpha.0",
+ "version": "7.4.0-alpha.0",
"description": "Source loader",
"keywords": [
"lib",
@@ -37,7 +37,8 @@
"dist/**/*",
"README.md",
"*.js",
- "*.d.ts"
+ "*.d.ts",
+ "!src/**/*"
],
"scripts": {
"check": "../../../scripts/prepare/check.ts",
diff --git a/code/lib/telemetry/package.json b/code/lib/telemetry/package.json
index b0941ef96519..8f85a012b16a 100644
--- a/code/lib/telemetry/package.json
+++ b/code/lib/telemetry/package.json
@@ -1,6 +1,6 @@
{
"name": "@storybook/telemetry",
- "version": "7.3.0-alpha.0",
+ "version": "7.4.0-alpha.0",
"description": "Telemetry logging for crash reports and usage statistics",
"keywords": [
"storybook"
@@ -36,7 +36,8 @@
"dist/**/*",
"README.md",
"*.js",
- "*.d.ts"
+ "*.d.ts",
+ "!src/**/*"
],
"scripts": {
"check": "../../../scripts/prepare/check.ts",
diff --git a/code/lib/telemetry/src/types.ts b/code/lib/telemetry/src/types.ts
index ba3af37719c0..35266814dff7 100644
--- a/code/lib/telemetry/src/types.ts
+++ b/code/lib/telemetry/src/types.ts
@@ -9,6 +9,7 @@ export type EventType =
| 'build'
| 'upgrade'
| 'init'
+ | 'browser'
| 'canceled'
| 'error'
| 'error-metadata'
diff --git a/code/lib/theming/package.json b/code/lib/theming/package.json
index 1ca54183a75b..1f44927fdf1d 100644
--- a/code/lib/theming/package.json
+++ b/code/lib/theming/package.json
@@ -1,6 +1,6 @@
{
"name": "@storybook/theming",
- "version": "7.3.0-alpha.0",
+ "version": "7.4.0-alpha.0",
"description": "Core Storybook Components",
"keywords": [
"storybook"
@@ -41,7 +41,8 @@
"dist/**/*",
"README.md",
"*.js",
- "*.d.ts"
+ "*.d.ts",
+ "!src/**/*"
],
"scripts": {
"check": "../../../scripts/prepare/check.ts",
diff --git a/code/lib/types/package.json b/code/lib/types/package.json
index e64f21ee68b7..9ee90eea32ee 100644
--- a/code/lib/types/package.json
+++ b/code/lib/types/package.json
@@ -1,6 +1,6 @@
{
"name": "@storybook/types",
- "version": "7.3.0-alpha.0",
+ "version": "7.4.0-alpha.0",
"description": "Core Storybook TS Types",
"keywords": [
"storybook"
@@ -36,7 +36,8 @@
"dist/**/*",
"README.md",
"*.js",
- "*.d.ts"
+ "*.d.ts",
+ "!src/**/*"
],
"scripts": {
"check": "../../../scripts/prepare/check.ts",
diff --git a/code/lib/types/src/modules/addons.ts b/code/lib/types/src/modules/addons.ts
index f55efcd29983..7c6a7987a2e3 100644
--- a/code/lib/types/src/modules/addons.ts
+++ b/code/lib/types/src/modules/addons.ts
@@ -10,6 +10,7 @@ import type {
} from 'react';
import type { RenderData as RouterData } from '../../../router/src/types';
import type { ThemeVars } from '../../../theming/src/types';
+import type { API_SidebarOptions } from './api';
import type {
Args,
ArgsStoryFn as ArgsStoryFnForFramework,
@@ -477,6 +478,7 @@ export interface Addon_Config {
toolbar?: {
[id: string]: Addon_ToolbarConfig;
};
+ sidebar?: API_SidebarOptions;
[key: string]: any;
}
diff --git a/code/lib/types/src/modules/api-stories.ts b/code/lib/types/src/modules/api-stories.ts
index 414f1384d761..fd0f3ca31d04 100644
--- a/code/lib/types/src/modules/api-stories.ts
+++ b/code/lib/types/src/modules/api-stories.ts
@@ -130,7 +130,7 @@ export interface API_IndexHash {
}
// We used to received a bit more data over the channel on the SET_STORIES event, including
// the full parameters for each story.
-type API_PreparedIndexEntry = IndexEntry & {
+export type API_PreparedIndexEntry = IndexEntry & {
parameters?: Parameters;
argTypes?: ArgTypes;
args?: Args;
@@ -184,3 +184,7 @@ export interface API_StatusObject {
export type API_StatusState = Record>;
export type API_StatusUpdate = Record;
+
+export type API_FilterFunction = (
+ item: API_IndexHash[keyof API_IndexHash] & { status: Record }
+) => boolean;
diff --git a/code/lib/types/src/modules/api.ts b/code/lib/types/src/modules/api.ts
index 762166b48fee..1fbaf0bba9bd 100644
--- a/code/lib/types/src/modules/api.ts
+++ b/code/lib/types/src/modules/api.ts
@@ -4,7 +4,7 @@ import type { RenderData } from '../../../router/src/types';
import type { Channel } from '../../../channels/src';
import type { ThemeVars } from '../../../theming/src/types';
import type { DocsOptions } from './core-common';
-import type { API_HashEntry, API_IndexHash } from './api-stories';
+import type { API_FilterFunction, API_HashEntry, API_IndexHash } from './api-stories';
import type { SetStoriesStory, SetStoriesStoryData } from './channelApi';
import type { Addon_BaseType, Addon_Collection, Addon_RenderOptions, Addon_Type } from './addons';
import type { StoryIndex } from './indexer';
@@ -112,6 +112,7 @@ export type API_ActiveTabsType = 'sidebar' | 'canvas' | 'addons';
export interface API_SidebarOptions {
showRoots?: boolean;
+ filters?: Record;
collapsedRoots?: string[];
renderLabel?: (item: API_HashEntry) => any;
}
diff --git a/code/lib/types/src/modules/composedStory.ts b/code/lib/types/src/modules/composedStory.ts
index ce31138bac20..f02a30187a38 100644
--- a/code/lib/types/src/modules/composedStory.ts
+++ b/code/lib/types/src/modules/composedStory.ts
@@ -8,8 +8,8 @@ import type {
ComponentAnnotations,
Parameters,
StoryAnnotations,
+ StoryAnnotationsOrFn,
StoryContext,
- StoryFn,
} from './csf';
import type { ProjectAnnotations } from './story';
@@ -22,6 +22,15 @@ export type Store_CSFExports ) // or PrimaryButton()
+ * PrimaryButton.play({ canvasElement: container })
+ */
export type ComposedStoryPlayContext = Partial<
StoryContext & Pick, 'canvasElement'>
>;
@@ -30,40 +39,52 @@ export type ComposedStoryPlayFn
) => Promise | void;
-export type PreparedStoryFn = AnnotatedStoryFn<
- TRenderer,
- TArgs
-> & { play: ComposedStoryPlayFn; args: TArgs; id: StoryId };
-
-export type ComposedStory =
- | StoryFn
- | StoryAnnotations;
+/**
+ * A story function with partial args, used internally by composeStory
+ */
+export type PartialArgsStoryFn = (
+ args?: TArgs
+) => (TRenderer & {
+ T: TArgs;
+})['storyResult'];
/**
- * T represents the whole ES module of a stories file. K of T means named exports (basically the Story type)
- * 1. pick the keys K of T that have properties that are Story
- * 2. infer the actual prop type for each Story
- * 3. reconstruct Story with Partial. Story -> Story>
+ * A story that got recomposed for portable stories, containing all the necessary data to be rendered in external environments
+ */
+export type ComposedStoryFn<
+ TRenderer extends Renderer = Renderer,
+ TArgs = Args
+> = PartialArgsStoryFn & {
+ play: ComposedStoryPlayFn;
+ args: TArgs;
+ id: StoryId;
+ storyName: string;
+ parameters: Parameters;
+};
+/**
+ * Based on a module of stories, it returns all stories within it, filtering non-stories
+ * Each story will have partial props, as their props should be handled when composing stories
*/
export type StoriesWithPartialProps = {
- // @TODO once we can use Typescript 4.0 do this to exclude nonStory exports:
- // replace [K in keyof TModule] with [K in keyof TModule as TModule[K] extends ComposedStory ? K : never]
- [K in keyof TModule]: TModule[K] extends ComposedStory
- ? PreparedStoryFn>
+ // T represents the whole ES module of a stories file. K of T means named exports (basically the Story type)
+ // 1. pick the keys K of T that have properties that are Story
+ // 2. infer the actual prop type for each Story
+ // 3. reconstruct Story with Partial. Story -> Story>
+ [K in keyof TModule as TModule[K] extends StoryAnnotationsOrFn
+ ? K
+ : never]: TModule[K] extends StoryAnnotationsOrFn
+ ? ComposedStoryFn>
: unknown;
};
+/**
+ * Type used for integrators of portable stories, as reference when creating their own composeStory function
+ */
export interface ComposeStoryFn {
(
storyAnnotations: AnnotatedStoryFn | StoryAnnotations,
componentAnnotations: ComponentAnnotations,
projectAnnotations: ProjectAnnotations,
exportsName?: string
- ): {
- (extraArgs: Partial): TRenderer['storyResult'];
- storyName: string;
- args: Args;
- play: ComposedStoryPlayFn;
- parameters: Parameters;
- };
+ ): ComposedStoryFn;
}
diff --git a/code/package.json b/code/package.json
index 86f31753abae..3ae4a217a371 100644
--- a/code/package.json
+++ b/code/package.json
@@ -1,6 +1,6 @@
{
"name": "@storybook/root",
- "version": "7.3.0-alpha.0",
+ "version": "7.4.0-alpha.0",
"private": true,
"description": "Storybook root",
"homepage": "https://storybook.js.org/",
@@ -225,6 +225,7 @@
"eslint": "^8.28.0",
"eslint-import-resolver-typescript": "^3.5.2",
"eslint-plugin-import": "^2.26.0",
+ "eslint-plugin-local-rules": "portal:../scripts/eslint-plugin-local-rules",
"eslint-plugin-react": "^7.31.10",
"eslint-plugin-storybook": "^0.6.6",
"fs-extra": "^11.1.0",
diff --git a/code/presets/create-react-app/package.json b/code/presets/create-react-app/package.json
index 55886997a704..e55935c0173e 100644
--- a/code/presets/create-react-app/package.json
+++ b/code/presets/create-react-app/package.json
@@ -1,6 +1,6 @@
{
"name": "@storybook/preset-create-react-app",
- "version": "7.3.0-alpha.0",
+ "version": "7.4.0-alpha.0",
"description": "Storybook for Create React App preset",
"keywords": [
"storybook"
@@ -41,7 +41,8 @@
"dist/**/*",
"README.md",
"*.js",
- "*.d.ts"
+ "*.d.ts",
+ "!src/**/*"
],
"scripts": {
"check": "../../../scripts/prepare/check.ts",
diff --git a/code/presets/html-webpack/package.json b/code/presets/html-webpack/package.json
index e682ca0f08bf..02b3a3a1fb75 100644
--- a/code/presets/html-webpack/package.json
+++ b/code/presets/html-webpack/package.json
@@ -1,6 +1,6 @@
{
"name": "@storybook/preset-html-webpack",
- "version": "7.3.0-alpha.0",
+ "version": "7.4.0-alpha.0",
"description": "Storybook for HTML: View HTML snippets in isolation with Hot Reloading.",
"keywords": [
"storybook"
@@ -41,7 +41,8 @@
"dist/**/*",
"README.md",
"*.js",
- "*.d.ts"
+ "*.d.ts",
+ "!src/**/*"
],
"scripts": {
"check": "../../../scripts/prepare/check.ts",
diff --git a/code/presets/preact-webpack/package.json b/code/presets/preact-webpack/package.json
index 6806ab325e0f..59c0a2c2b7a2 100644
--- a/code/presets/preact-webpack/package.json
+++ b/code/presets/preact-webpack/package.json
@@ -1,6 +1,6 @@
{
"name": "@storybook/preset-preact-webpack",
- "version": "7.3.0-alpha.0",
+ "version": "7.4.0-alpha.0",
"description": "Storybook for Preact: Develop Preact Component in isolation.",
"keywords": [
"storybook"
@@ -41,7 +41,8 @@
"dist/**/*",
"README.md",
"*.js",
- "*.d.ts"
+ "*.d.ts",
+ "!src/**/*"
],
"scripts": {
"check": "../../../scripts/prepare/check.ts",
diff --git a/code/presets/react-webpack/package.json b/code/presets/react-webpack/package.json
index 208dee7257bb..80128c0a7697 100644
--- a/code/presets/react-webpack/package.json
+++ b/code/presets/react-webpack/package.json
@@ -1,6 +1,6 @@
{
"name": "@storybook/preset-react-webpack",
- "version": "7.3.0-alpha.0",
+ "version": "7.4.0-alpha.0",
"description": "Storybook for React: Develop React Component in isolation with Hot Reloading",
"keywords": [
"storybook"
@@ -56,7 +56,8 @@
"dist/**/*",
"README.md",
"*.js",
- "*.d.ts"
+ "*.d.ts",
+ "!src/**/*"
],
"scripts": {
"check": "../../../scripts/prepare/check.ts",
diff --git a/code/presets/server-webpack/package.json b/code/presets/server-webpack/package.json
index a7fb771923a9..b7d8b5b1ba5f 100644
--- a/code/presets/server-webpack/package.json
+++ b/code/presets/server-webpack/package.json
@@ -1,6 +1,6 @@
{
"name": "@storybook/preset-server-webpack",
- "version": "7.3.0-alpha.0",
+ "version": "7.4.0-alpha.0",
"description": "Storybook for Server: View HTML snippets from a server in isolation with Hot Reloading.",
"keywords": [
"storybook"
@@ -46,7 +46,8 @@
"dist/**/*",
"README.md",
"*.js",
- "*.d.ts"
+ "*.d.ts",
+ "!src/**/*"
],
"scripts": {
"check": "../../../scripts/prepare/check.ts",
diff --git a/code/presets/svelte-webpack/package.json b/code/presets/svelte-webpack/package.json
index 3a91d6c27fe3..2e01104e0e1b 100644
--- a/code/presets/svelte-webpack/package.json
+++ b/code/presets/svelte-webpack/package.json
@@ -1,6 +1,6 @@
{
"name": "@storybook/preset-svelte-webpack",
- "version": "7.3.0-alpha.0",
+ "version": "7.4.0-alpha.0",
"description": "Storybook for Svelte: Develop Svelte Component in isolation with Hot Reloading.",
"keywords": [
"storybook"
@@ -56,7 +56,8 @@
"dist/**/*",
"README.md",
"*.js",
- "*.d.ts"
+ "*.d.ts",
+ "!src/**/*"
],
"scripts": {
"check": "../../../scripts/prepare/check.ts",
diff --git a/code/presets/vue-webpack/package.json b/code/presets/vue-webpack/package.json
index c8a5f6cc40e5..0d416acc071f 100644
--- a/code/presets/vue-webpack/package.json
+++ b/code/presets/vue-webpack/package.json
@@ -1,6 +1,6 @@
{
"name": "@storybook/preset-vue-webpack",
- "version": "7.3.0-alpha.0",
+ "version": "7.4.0-alpha.0",
"description": "Storybook for Vue: Develop Vue Component in isolation with Hot Reloading.",
"keywords": [
"storybook"
@@ -51,7 +51,8 @@
"dist/**/*",
"README.md",
"*.js",
- "*.d.ts"
+ "*.d.ts",
+ "!src/**/*"
],
"scripts": {
"check": "../../../scripts/prepare/check.ts",
diff --git a/code/presets/vue3-webpack/package.json b/code/presets/vue3-webpack/package.json
index e3c87d969fc8..b0698c46efa0 100644
--- a/code/presets/vue3-webpack/package.json
+++ b/code/presets/vue3-webpack/package.json
@@ -1,6 +1,6 @@
{
"name": "@storybook/preset-vue3-webpack",
- "version": "7.3.0-alpha.0",
+ "version": "7.4.0-alpha.0",
"description": "Storybook for Vue 3: Develop Vue 3 Components in isolation with Hot Reloading.",
"keywords": [
"storybook"
@@ -51,7 +51,8 @@
"dist/**/*",
"README.md",
"*.js",
- "*.d.ts"
+ "*.d.ts",
+ "!src/**/*"
],
"scripts": {
"check": "../../../scripts/prepare/check.ts",
diff --git a/code/presets/web-components-webpack/package.json b/code/presets/web-components-webpack/package.json
index 08378a729ca4..ff0e9ac7d830 100644
--- a/code/presets/web-components-webpack/package.json
+++ b/code/presets/web-components-webpack/package.json
@@ -1,6 +1,6 @@
{
"name": "@storybook/preset-web-components-webpack",
- "version": "7.3.0-alpha.0",
+ "version": "7.4.0-alpha.0",
"description": "Storybook for web-components: View web components snippets in isolation with Hot Reloading.",
"keywords": [
"lit",
@@ -44,7 +44,8 @@
"dist/**/*",
"README.md",
"*.js",
- "*.d.ts"
+ "*.d.ts",
+ "!src/**/*"
],
"scripts": {
"check": "../../../scripts/prepare/check.ts",
diff --git a/code/renderers/html/package.json b/code/renderers/html/package.json
index e1e0ed5e4563..cb58d3272450 100644
--- a/code/renderers/html/package.json
+++ b/code/renderers/html/package.json
@@ -1,6 +1,6 @@
{
"name": "@storybook/html",
- "version": "7.3.0-alpha.0",
+ "version": "7.4.0-alpha.0",
"description": "Storybook HTML renderer",
"keywords": [
"storybook"
@@ -38,10 +38,11 @@
"types": "dist/index.d.ts",
"files": [
"dist/**/*",
- "template/**/*",
+ "template/cli/**/*",
"README.md",
"*.js",
- "*.d.ts"
+ "*.d.ts",
+ "!src/**/*"
],
"scripts": {
"check": "../../../scripts/prepare/check.ts",
diff --git a/code/renderers/preact/package.json b/code/renderers/preact/package.json
index 63e0a26a508e..8493c73e9792 100644
--- a/code/renderers/preact/package.json
+++ b/code/renderers/preact/package.json
@@ -1,6 +1,6 @@
{
"name": "@storybook/preact",
- "version": "7.3.0-alpha.0",
+ "version": "7.4.0-alpha.0",
"description": "Storybook Preact renderer",
"keywords": [
"storybook"
@@ -38,10 +38,11 @@
"types": "dist/index.d.ts",
"files": [
"dist/**/*",
- "template/**/*",
+ "template/cli/**/*",
"README.md",
"*.js",
- "*.d.ts"
+ "*.d.ts",
+ "!src/**/*"
],
"scripts": {
"check": "../../../scripts/prepare/check.ts",
diff --git a/code/renderers/react/package.json b/code/renderers/react/package.json
index f2e06a9cdd20..df67f8ea3845 100644
--- a/code/renderers/react/package.json
+++ b/code/renderers/react/package.json
@@ -1,6 +1,6 @@
{
"name": "@storybook/react",
- "version": "7.3.0-alpha.0",
+ "version": "7.4.0-alpha.0",
"description": "Storybook React renderer",
"keywords": [
"storybook"
@@ -42,10 +42,11 @@
"types": "dist/index.d.ts",
"files": [
"dist/**/*",
- "template/**/*",
+ "template/cli/**/*",
"README.md",
"*.js",
- "*.d.ts"
+ "*.d.ts",
+ "!src/**/*"
],
"scripts": {
"check": "../../../scripts/prepare/check.ts",
diff --git a/code/renderers/react/src/testing-api.ts b/code/renderers/react/src/testing-api.ts
index da061e64ef04..545147fab2db 100644
--- a/code/renderers/react/src/testing-api.ts
+++ b/code/renderers/react/src/testing-api.ts
@@ -6,7 +6,7 @@ import {
import type {
Args,
ProjectAnnotations,
- ComposedStory,
+ StoryAnnotationsOrFn,
Store_CSFExports,
StoriesWithPartialProps,
} from '@storybook/types';
@@ -81,13 +81,13 @@ const defaultProjectAnnotations: ProjectAnnotations = {
* @param [exportsName] - in case your story does not contain a name and you want it to have a name.
*/
export function composeStory(
- story: ComposedStory,
+ story: StoryAnnotationsOrFn,
componentAnnotations: Meta,
projectAnnotations?: ProjectAnnotations,
exportsName?: string
) {
return originalComposeStory(
- story as ComposedStory,
+ story as StoryAnnotationsOrFn,
componentAnnotations,
projectAnnotations,
defaultProjectAnnotations,
diff --git a/code/renderers/server/package.json b/code/renderers/server/package.json
index 4eea18ccfef7..74df9790f49b 100644
--- a/code/renderers/server/package.json
+++ b/code/renderers/server/package.json
@@ -1,6 +1,6 @@
{
"name": "@storybook/server",
- "version": "7.3.0-alpha.0",
+ "version": "7.4.0-alpha.0",
"description": "Storybook Server renderer",
"keywords": [
"storybook"
@@ -43,10 +43,11 @@
"types": "dist/index.d.ts",
"files": [
"dist/**/*",
- "template/**/*",
+ "template/cli/**/*",
"README.md",
"*.js",
- "*.d.ts"
+ "*.d.ts",
+ "!src/**/*"
],
"scripts": {
"check": "../../../scripts/prepare/check.ts",
diff --git a/code/renderers/svelte/package.json b/code/renderers/svelte/package.json
index a17bbe33040e..cb9d4df6509d 100644
--- a/code/renderers/svelte/package.json
+++ b/code/renderers/svelte/package.json
@@ -1,6 +1,6 @@
{
"name": "@storybook/svelte",
- "version": "7.3.0-alpha.0",
+ "version": "7.4.0-alpha.0",
"description": "Storybook Svelte renderer",
"keywords": [
"storybook"
@@ -42,10 +42,11 @@
"files": [
"dist/**/*",
"templates/**/*",
- "template/**/*",
+ "template/cli/**/*",
"README.md",
"*.js",
- "*.d.ts"
+ "*.d.ts",
+ "!src/**/*"
],
"scripts": {
"check": "svelte-check",
diff --git a/code/renderers/vue/package.json b/code/renderers/vue/package.json
index a9275946b25c..4333bcc45055 100644
--- a/code/renderers/vue/package.json
+++ b/code/renderers/vue/package.json
@@ -1,6 +1,6 @@
{
"name": "@storybook/vue",
- "version": "7.3.0-alpha.0",
+ "version": "7.4.0-alpha.0",
"description": "Storybook Vue renderer",
"keywords": [
"storybook"
@@ -38,10 +38,11 @@
"types": "dist/index.d.ts",
"files": [
"dist/**/*",
- "template/**/*",
+ "template/cli/**/*",
"README.md",
"*.js",
- "*.d.ts"
+ "*.d.ts",
+ "!src/**/*"
],
"scripts": {
"check": "vue-tsc --noEmit",
diff --git a/code/renderers/vue3/package.json b/code/renderers/vue3/package.json
index b8905045274d..df621810ebc2 100644
--- a/code/renderers/vue3/package.json
+++ b/code/renderers/vue3/package.json
@@ -1,6 +1,6 @@
{
"name": "@storybook/vue3",
- "version": "7.3.0-alpha.0",
+ "version": "7.4.0-alpha.0",
"description": "Storybook Vue 3 renderer",
"keywords": [
"storybook"
@@ -38,10 +38,11 @@
"types": "dist/index.d.ts",
"files": [
"dist/**/*",
- "template/**/*",
+ "template/cli/**/*",
"README.md",
"*.js",
- "*.d.ts"
+ "*.d.ts",
+ "!src/**/*"
],
"scripts": {
"check": "vue-tsc --noEmit",
diff --git a/code/renderers/web-components/package.json b/code/renderers/web-components/package.json
index e84d54f7cb4d..c0659a9c57fd 100644
--- a/code/renderers/web-components/package.json
+++ b/code/renderers/web-components/package.json
@@ -1,6 +1,6 @@
{
"name": "@storybook/web-components",
- "version": "7.3.0-alpha.0",
+ "version": "7.4.0-alpha.0",
"description": "Storybook web-components renderer",
"keywords": [
"lit",
@@ -41,10 +41,11 @@
"types": "dist/index.d.ts",
"files": [
"dist/**/*",
- "template/**/*",
+ "template/cli/**/*",
"README.md",
"*.js",
- "*.d.ts"
+ "*.d.ts",
+ "!src/**/*"
],
"scripts": {
"check": "../../../scripts/prepare/check.ts",
diff --git a/code/ui/.storybook/manager.tsx b/code/ui/.storybook/manager.tsx
index 775a1f63c8ed..64691f64f4a3 100644
--- a/code/ui/.storybook/manager.tsx
+++ b/code/ui/.storybook/manager.tsx
@@ -1,7 +1,5 @@
import { addons, types } from '@storybook/manager-api';
-import { IconButton, Icons } from '@storybook/components';
import startCase from 'lodash/startCase.js';
-import React, { Fragment } from 'react';
addons.setConfig({
sidebar: {
diff --git a/code/ui/blocks/package.json b/code/ui/blocks/package.json
index f8086dff4690..e077d15ec27c 100644
--- a/code/ui/blocks/package.json
+++ b/code/ui/blocks/package.json
@@ -1,6 +1,6 @@
{
"name": "@storybook/blocks",
- "version": "7.3.0-alpha.0",
+ "version": "7.4.0-alpha.0",
"description": "Storybook Doc Blocks",
"keywords": [
"storybook"
@@ -36,7 +36,8 @@
"dist/**/*",
"README.md",
"*.js",
- "*.d.ts"
+ "*.d.ts",
+ "!src/**/*"
],
"scripts": {
"check": "../../../scripts/prepare/check.ts",
@@ -62,7 +63,7 @@
"memoizerific": "^1.11.3",
"polished": "^4.2.2",
"react-colorful": "^5.1.2",
- "telejson": "^7.0.3",
+ "telejson": "^7.2.0",
"tocbot": "^4.20.1",
"ts-dedent": "^2.0.0",
"util-deprecate": "^1.0.2"
diff --git a/code/ui/components/package.json b/code/ui/components/package.json
index e819b577a0e5..65b38724e688 100644
--- a/code/ui/components/package.json
+++ b/code/ui/components/package.json
@@ -1,6 +1,6 @@
{
"name": "@storybook/components",
- "version": "7.3.0-alpha.0",
+ "version": "7.4.0-alpha.0",
"description": "Core Storybook Components",
"keywords": [
"storybook"
@@ -60,7 +60,8 @@
"dist/**/*",
"README.md",
"*.js",
- "*.d.ts"
+ "*.d.ts",
+ "!src/**/*"
],
"scripts": {
"check": "../../../scripts/prepare/check.ts",
diff --git a/code/ui/manager/package.json b/code/ui/manager/package.json
index 89bded8e03d2..96191e437764 100644
--- a/code/ui/manager/package.json
+++ b/code/ui/manager/package.json
@@ -1,6 +1,6 @@
{
"name": "@storybook/manager",
- "version": "7.3.0-alpha.0",
+ "version": "7.4.0-alpha.0",
"description": "Core Storybook UI",
"keywords": [
"storybook"
@@ -43,7 +43,8 @@
"static/**/*",
"README.md",
"*.js",
- "*.d.ts"
+ "*.d.ts",
+ "!src/**/*"
],
"scripts": {
"check": "../../../scripts/prepare/check.ts",
diff --git a/code/ui/manager/src/containers/sidebar.tsx b/code/ui/manager/src/containers/sidebar.tsx
index c4beb5bda4c4..a97f929bf246 100755
--- a/code/ui/manager/src/containers/sidebar.tsx
+++ b/code/ui/manager/src/containers/sidebar.tsx
@@ -1,10 +1,11 @@
-import React from 'react';
+import React, { useMemo } from 'react';
import type { Combo, StoriesHash } from '@storybook/manager-api';
import { Consumer } from '@storybook/manager-api';
import { Sidebar as SidebarComponent } from '../components/sidebar/Sidebar';
import { useMenu } from './menu';
+import { getAncestorIds } from '../utils/tree';
export type Item = StoriesHash[keyof StoriesHash];
@@ -16,11 +17,12 @@ const Sidebar = React.memo(function Sideber() {
storyId,
refId,
layout: { showToolbar, isFullscreen, showPanel, showNav },
- index,
+ index: originalIndex,
status,
indexError,
previewInitialized,
refs,
+ filters,
} = state;
const menu = useMenu(
@@ -36,6 +38,34 @@ const Sidebar = React.memo(function Sideber() {
const whatsNewNotificationsEnabled =
state.whatsNewData?.status === 'SUCCESS' && !state.disableWhatsNewNotifications;
+ const index = useMemo(() => {
+ if (!originalIndex) {
+ return originalIndex;
+ }
+
+ const filtered = new Set();
+ Object.values(originalIndex).forEach((item) => {
+ if (item.type === 'story' || item.type === 'docs') {
+ let result = true;
+
+ Object.values(filters).forEach((filter) => {
+ if (result === true) {
+ result = filter({ ...item, status: status[item.id] });
+ }
+ });
+
+ if (result) {
+ filtered.add(item.id);
+ getAncestorIds(originalIndex, item.id).forEach((id) => {
+ filtered.add(id);
+ });
+ }
+ }
+ });
+
+ return Object.fromEntries(Object.entries(originalIndex).filter(([key]) => filtered.has(key)));
+ }, [originalIndex, filters, status]);
+
return {
title: name,
url,
diff --git a/code/ui/manager/src/globals/exports.ts b/code/ui/manager/src/globals/exports.ts
index 793165aaf103..8eb900860c6e 100644
--- a/code/ui/manager/src/globals/exports.ts
+++ b/code/ui/manager/src/globals/exports.ts
@@ -172,6 +172,7 @@ export default {
'STORY_SPECIFIED',
'STORY_THREW_EXCEPTION',
'STORY_UNCHANGED',
+ 'TELEMETRY_ERROR',
'TOGGLE_WHATS_NEW_NOTIFICATIONS',
'UPDATE_GLOBALS',
'UPDATE_QUERY_PARAMS',
diff --git a/code/ui/manager/src/index.tsx b/code/ui/manager/src/index.tsx
index 8bf921c005da..2836846be5b1 100644
--- a/code/ui/manager/src/index.tsx
+++ b/code/ui/manager/src/index.tsx
@@ -7,6 +7,7 @@ import { Location, LocationProvider, useNavigate } from '@storybook/router';
import { Provider as ManagerProvider, types } from '@storybook/manager-api';
import type { Combo } from '@storybook/manager-api';
import { ThemeProvider, ensure as ensureTheme } from '@storybook/theming';
+import { ProviderDoesNotExtendBaseProviderError } from '@storybook/core-events/manager-errors';
import { HelmetProvider } from 'react-helmet-async';
@@ -83,7 +84,7 @@ const Main: FC<{ provider: Provider }> = ({ provider }) => {
export function renderStorybookUI(domNode: HTMLElement, provider: Provider) {
if (!(provider instanceof Provider)) {
- throw new Error('provider is not extended from the base Provider');
+ throw new ProviderDoesNotExtendBaseProviderError();
}
ReactDOM.render( , domNode);
diff --git a/code/ui/manager/src/runtime.ts b/code/ui/manager/src/runtime.ts
index bb1691be334c..dd29b9a45223 100644
--- a/code/ui/manager/src/runtime.ts
+++ b/code/ui/manager/src/runtime.ts
@@ -1,3 +1,5 @@
+/* eslint-disable local-rules/no-uncategorized-errors */
+
import { global } from '@storybook/global';
import type { Channel } from '@storybook/channels';
@@ -5,7 +7,8 @@ import type { AddonStore } from '@storybook/manager-api';
import { addons } from '@storybook/manager-api';
import type { Addon_Types, Addon_Config } from '@storybook/types';
import { createBrowserChannel } from '@storybook/channels';
-import { CHANNEL_CREATED } from '@storybook/core-events';
+import { CHANNEL_CREATED, TELEMETRY_ERROR } from '@storybook/core-events';
+import { UncaughtManagerError } from '@storybook/core-events/manager-errors';
import Provider from './provider';
import { renderStorybookUI } from './index';
@@ -35,6 +38,7 @@ class ReactProvider extends Provider {
this.addons = addons;
this.channel = channel;
+ global.__STORYBOOK_ADDONS_CHANNEL__ = channel;
if (FEATURES?.storyStoreV7 && CONFIG_TYPE === 'DEVELOPMENT') {
this.serverChannel = this.channel;
@@ -55,12 +59,51 @@ class ReactProvider extends Provider {
}
}
-const { document } = global;
-
-const rootEl = document.getElementById('root');
-renderStorybookUI(rootEl, new ReactProvider());
-
// Apply all the globals
Object.keys(Keys).forEach((key: keyof typeof Keys) => {
global[Keys[key]] = values[key];
});
+
+function preprocessError(
+ originalError: Error & {
+ fromStorybook?: boolean;
+ category?: string;
+ target?: any;
+ currentTarget?: any;
+ srcElement?: any;
+ }
+) {
+ let error = originalError;
+
+ if (!originalError.fromStorybook) {
+ error = new UncaughtManagerError(originalError);
+ }
+
+ // DOM manipulation errors and other similar errors are not serializable as they contain
+ // circular references to the window object. If that's the case, we make a simplified copy
+ if (error.target === window || error.currentTarget === window || error.srcElement === window) {
+ error = new Error(originalError.message);
+ error.name = originalError.name || error.name;
+ error.category = originalError.category;
+ }
+
+ return error;
+}
+
+global.sendTelemetryError = (error) => {
+ const channel = global.__STORYBOOK_ADDONS_CHANNEL__;
+ channel.emit(TELEMETRY_ERROR, preprocessError(error));
+};
+
+// handle all uncaught errors at the root of the application and log to telemetry
+global.addEventListener('error', (args) => {
+ const error = args.error || args;
+ global.sendTelemetryError(error);
+});
+global.addEventListener('unhandledrejection', ({ reason }) => {
+ global.sendTelemetryError(reason);
+});
+
+const { document } = global;
+const rootEl = document.getElementById('root');
+renderStorybookUI(rootEl, new ReactProvider());
diff --git a/code/ui/manager/src/typings.d.ts b/code/ui/manager/src/typings.d.ts
index f46c49b91852..2ff3df07e63e 100644
--- a/code/ui/manager/src/typings.d.ts
+++ b/code/ui/manager/src/typings.d.ts
@@ -25,3 +25,5 @@ declare var __STORYBOOKTHEMING__: any;
declare var __STORYBOOKAPI__: any;
declare var __STORYBOOKADDONS__: any;
declare var __STORYBOOKCLIENTLOGGER__: any;
+declare var __STORYBOOK_ADDONS_CHANNEL__: any;
+declare var sendTelemetryError: (error: any) => void;
diff --git a/code/yarn.lock b/code/yarn.lock
index 05b7c110550b..545d48a58ab0 100644
--- a/code/yarn.lock
+++ b/code/yarn.lock
@@ -463,7 +463,7 @@ __metadata:
languageName: node
linkType: hard
-"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.0, @babel/core@npm:^7.12.3, @babel/core@npm:^7.13.16, @babel/core@npm:^7.19.6, @babel/core@npm:^7.20.12, @babel/core@npm:^7.22.0, @babel/core@npm:^7.22.1, @babel/core@npm:^7.22.9, @babel/core@npm:^7.3.4, @babel/core@npm:^7.7.5":
+"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.0, @babel/core@npm:^7.12.3, @babel/core@npm:^7.13.16, @babel/core@npm:^7.19.6, @babel/core@npm:^7.20.12, @babel/core@npm:^7.22.1, @babel/core@npm:^7.22.9, @babel/core@npm:^7.3.4, @babel/core@npm:^7.7.5":
version: 7.22.9
resolution: "@babel/core@npm:7.22.9"
dependencies:
@@ -5822,7 +5822,7 @@ __metadata:
polished: ^4.2.2
prop-types: ^15.7.2
react-inspector: ^6.0.0
- telejson: ^7.0.3
+ telejson: ^7.2.0
ts-dedent: ^2.0.0
typescript: ~4.9.3
uuid: ^9.0.0
@@ -6395,7 +6395,7 @@ __metadata:
jest-specific-snapshot: ^8.0.0
read-pkg-up: ^7.0.1
semver: ^7.3.7
- telejson: ^7.0.3
+ telejson: ^7.2.0
tmp: ^0.2.1
ts-dedent: ^2.0.0
tsconfig-paths-webpack-plugin: ^4.0.1
@@ -6501,7 +6501,7 @@ __metadata:
memoizerific: ^1.11.3
polished: ^4.2.2
react-colorful: ^5.1.2
- telejson: ^7.0.3
+ telejson: ^7.2.0
tocbot: ^4.20.1
ts-dedent: ^2.0.0
util-deprecate: ^1.0.2
@@ -6584,15 +6584,23 @@ __metadata:
version: 0.0.0-use.local
resolution: "@storybook/builder-webpack5@workspace:builders/builder-webpack5"
dependencies:
- "@babel/core": ^7.22.0
+ "@babel/core": ^7.22.9
+ "@storybook/addons": "workspace:*"
"@storybook/channels": "workspace:*"
+ "@storybook/client-api": "workspace:*"
"@storybook/client-logger": "workspace:*"
+ "@storybook/components": "workspace:*"
"@storybook/core-common": "workspace:*"
"@storybook/core-events": "workspace:*"
"@storybook/core-webpack": "workspace:*"
+ "@storybook/global": ^5.0.0
+ "@storybook/manager-api": "workspace:*"
"@storybook/node-logger": "workspace:*"
"@storybook/preview": "workspace:*"
"@storybook/preview-api": "workspace:*"
+ "@storybook/router": "workspace:*"
+ "@storybook/store": "workspace:*"
+ "@storybook/theming": "workspace:*"
"@swc/core": ^1.3.49
"@types/node": ^16.0.0
"@types/pretty-hrtime": ^1.0.0
@@ -6627,6 +6635,9 @@ __metadata:
webpack-dev-middleware: ^6.1.1
webpack-hot-middleware: ^2.25.1
webpack-virtual-modules: ^0.5.0
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
+ react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
typescript:
optional: true
@@ -6661,7 +6672,7 @@ __metadata:
"@storybook/core-events": "workspace:*"
"@storybook/global": ^5.0.0
qs: ^6.10.0
- telejson: ^7.0.3
+ telejson: ^7.2.0
tiny-invariant: ^1.3.1
typescript: ~4.9.3
languageName: unknown
@@ -6861,6 +6872,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "@storybook/core-events@workspace:lib/core-events"
dependencies:
+ ts-dedent: ^2.0.0
typescript: ~4.9.3
languageName: unknown
linkType: soft
@@ -6915,7 +6927,7 @@ __metadata:
semver: ^7.3.7
serve-favicon: ^2.5.0
slash: ^5.0.0
- telejson: ^7.0.3
+ telejson: ^7.2.0
tiny-invariant: ^1.3.1
ts-dedent: ^2.0.0
typescript: ~4.9.3
@@ -7195,7 +7207,7 @@ __metadata:
qs: ^6.10.0
semver: ^7.3.7
store2: ^2.14.2
- telejson: ^7.0.3
+ telejson: ^7.2.0
ts-dedent: ^2.0.0
typescript: ~4.9.3
peerDependencies:
@@ -7621,6 +7633,7 @@ __metadata:
"@storybook/channels": "workspace:*"
"@storybook/client-logger": "workspace:*"
"@storybook/core-events": "workspace:*"
+ "@storybook/global": ^5.0.0
"@storybook/preview-api": "workspace:*"
typescript: ~4.9.3
languageName: unknown
@@ -7877,6 +7890,7 @@ __metadata:
eslint: ^8.28.0
eslint-import-resolver-typescript: ^3.5.2
eslint-plugin-import: ^2.26.0
+ eslint-plugin-local-rules: "portal:../scripts/eslint-plugin-local-rules"
eslint-plugin-react: ^7.31.10
eslint-plugin-storybook: ^0.6.6
fs-extra: ^11.1.0
@@ -15912,6 +15926,12 @@ __metadata:
languageName: node
linkType: hard
+"eslint-plugin-local-rules@portal:../scripts/eslint-plugin-local-rules::locator=%40storybook%2Froot%40workspace%3A.":
+ version: 0.0.0-use.local
+ resolution: "eslint-plugin-local-rules@portal:../scripts/eslint-plugin-local-rules::locator=%40storybook%2Froot%40workspace%3A."
+ languageName: node
+ linkType: soft
+
"eslint-plugin-prettier@npm:^3.4.0":
version: 3.4.1
resolution: "eslint-plugin-prettier@npm:3.4.1"
@@ -29608,12 +29628,12 @@ __metadata:
languageName: node
linkType: hard
-"telejson@npm:^7.0.3":
- version: 7.1.0
- resolution: "telejson@npm:7.1.0"
+"telejson@npm:^7.2.0":
+ version: 7.2.0
+ resolution: "telejson@npm:7.2.0"
dependencies:
memoizerific: ^1.11.3
- checksum: dc9a185d0e00d947c0eaa229bfb993aab61a3ba79282ae409768fc8ae66d236e89a64ebe291f9ea6ed5e05396e0be52a7542ea32b6c1321b20440f28c7828edc
+ checksum: d26e6cc93e54bfdcdb207b49905508c5db45862e811a2e2193a735409e47b14530e1c19351618a3e03ad2fd4ffc3759364fcd72851aba2df0300fab574b6151c
languageName: node
linkType: hard
diff --git a/docs/essentials/controls.md b/docs/essentials/controls.md
index 5266c0760f02..cc0f937ec97b 100644
--- a/docs/essentials/controls.md
+++ b/docs/essentials/controls.md
@@ -123,26 +123,26 @@ If you haven't used the CLI to setup the configuration, or if you want to define
## Fully custom args
-Until now, we only used auto-generated controls based on the component we're writing stories for. If we are writing [complex stories](../writing-stories/stories-for-multiple-components.md), we may want to add controls for args that arenβt part of the component.
+Until now, we only used auto-generated controls based on the component we're writing stories for. If we are writing [complex stories](../writing-stories/stories-for-multiple-components.md), we may want to add controls for args that arenβt part of the component. For example, here's how you could use a `footer` arg to populate a child component:
diff --git a/docs/snippets/angular/page-story-slots.ts.mdx b/docs/snippets/angular/page-story-slots.ts.mdx
index 2b79a6caa3e3..63ac7f3309cc 100644
--- a/docs/snippets/angular/page-story-slots.ts.mdx
+++ b/docs/snippets/angular/page-story-slots.ts.mdx
@@ -5,26 +5,23 @@ import type { Meta, StoryObj } from '@storybook/angular';
import { Page } from './page.component';
-const meta: Meta = {
- component: Page,
-};
-
-export default meta;
-type Story = StoryObj;
+type PagePropsAndCustomArgs = Page & { footer?: string };
-/*
- *π Render functions are a framework specific feature to allow you control on how the component renders.
- * See https://storybook.js.org/docs/angular/api/csf
- * to learn how to use render functions.
- */
-export const CustomFooter: Story = {
- render: (args) => ({
+const meta: Meta = {
+ component: Page,
+ render: ({ footer, ...args }) => ({
props: args,
template: `
- ${args.footer}
+ ${footer}
`,
}),
+};
+export default meta;
+
+type Story = StoryObj;
+
+export const CustomFooter: Story = {
args: {
footer: 'Built with Storybook',
},
diff --git a/docs/snippets/angular/table-story-fully-customize-controls.ts.mdx b/docs/snippets/angular/table-story-fully-customize-controls.ts.mdx
deleted file mode 100644
index 387ca9506c93..000000000000
--- a/docs/snippets/angular/table-story-fully-customize-controls.ts.mdx
+++ /dev/null
@@ -1,39 +0,0 @@
-```ts
-// Table.stories.ts
-
-import type { Meta, StoryObj } from '@storybook/angular';
-
-import { Table } from './Table.component';
-
-const meta: Meta = {
- component: Table,
-};
-
-export default meta;
-type Story = StoryObj;
-
-export const Numeric: Story = {
- render: (args) => ({
- props: args,
- template: `
-
-
-
-
- {{data[i][j]}}
-
-
-
-
- `,
- }),
- args: {
- data: [
- [1, 2, 3],
- [4, 5, 6],
- ],
- //π The remaining args get passed to the `Table` component
- size: 'large',
- },
-};
-```
diff --git a/docs/snippets/angular/typed-csf-file.ts.mdx b/docs/snippets/angular/typed-csf-file.ts.mdx
new file mode 100644
index 000000000000..6ac8b473a93b
--- /dev/null
+++ b/docs/snippets/angular/typed-csf-file.ts.mdx
@@ -0,0 +1,22 @@
+```ts
+// Button.stories.ts
+
+import type { Meta, StoryObj } from '@storybook/angular';
+
+import { Button } from './button.component';
+
+const meta: Meta