From 9d2dca3269d3f45aed2aedbdda240e6b173dfafe Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Wed, 22 Jun 2022 17:02:27 +1000 Subject: [PATCH 1/7] Use strict types in store --- lib/addons/src/types.ts | 20 ++++-- lib/store/src/GlobalsStore.test.ts | 24 +++---- lib/store/src/GlobalsStore.ts | 17 ++++- lib/store/src/StoryIndexStore.ts | 2 - lib/store/src/StoryStore.ts | 67 ++++++++++++++----- lib/store/src/args.ts | 2 +- .../src/csf/normalizeProjectAnnotations.ts | 4 +- lib/store/src/csf/normalizeStory.test.ts | 40 +++++------ lib/store/src/csf/normalizeStory.ts | 22 +++--- lib/store/src/csf/prepareStory.ts | 7 +- lib/store/src/csf/processCSFFile.ts | 7 +- lib/store/src/csf/testing-utils/index.ts | 11 +-- lib/store/src/csf/testing-utils/types.ts | 6 +- lib/store/src/decorators.ts | 3 + lib/store/src/inferControls.ts | 4 +- lib/store/src/parameters.ts | 7 +- lib/store/src/sortStories.ts | 2 +- lib/store/src/types.ts | 10 ++- lib/store/tsconfig.json | 1 + 19 files changed, 159 insertions(+), 97 deletions(-) diff --git a/lib/addons/src/types.ts b/lib/addons/src/types.ts index 64a66273db0c..0b518cff7310 100644 --- a/lib/addons/src/types.ts +++ b/lib/addons/src/types.ts @@ -12,6 +12,7 @@ import type { StoryKind, StoryName, Args, + ComponentTitle, } from '@storybook/csf'; import { Addon } from './index'; @@ -50,18 +51,29 @@ export interface StorySortObjectParameter { includeNames?: boolean; } -interface StoryIndexEntry { +type Path = string; +interface BaseIndexEntry { id: StoryId; name: StoryName; - title: string; - importPath: string; + title: ComponentTitle; + importPath: Path; } +export type StoryIndexEntry = BaseIndexEntry & { + type: 'story'; +}; + +export type DocsIndexEntry = BaseIndexEntry & { + storiesImports: Path[]; + type: 'docs'; + legacy?: boolean; +}; +export type IndexEntry = StoryIndexEntry | DocsIndexEntry; // The `any` here is the story store's `StoreItem` record. Ideally we should probably only // pass a defined subset of that full data, but we pass it all so far :shrug: export type StorySortComparator = Comparator<[StoryId, any, Parameters, Parameters]>; export type StorySortParameter = StorySortComparator | StorySortObjectParameter; -export type StorySortComparatorV7 = Comparator; +export type StorySortComparatorV7 = Comparator; export type StorySortParameterV7 = StorySortComparatorV7 | StorySortObjectParameter; export interface OptionsParameter extends Object { diff --git a/lib/store/src/GlobalsStore.test.ts b/lib/store/src/GlobalsStore.test.ts index 40e4205f99f3..1f299157c107 100644 --- a/lib/store/src/GlobalsStore.test.ts +++ b/lib/store/src/GlobalsStore.test.ts @@ -2,8 +2,7 @@ import { GlobalsStore } from './GlobalsStore'; describe('GlobalsStore', () => { it('is initialized to the value in globals', () => { - const store = new GlobalsStore(); - store.set({ + const store = new GlobalsStore({ globals: { arg1: 'arg1', arg2: 2, @@ -20,8 +19,7 @@ describe('GlobalsStore', () => { }); it('is initialized to the default values from globalTypes if global is unset', () => { - const store = new GlobalsStore(); - store.set({ + const store = new GlobalsStore({ globals: { arg1: 'arg1', arg2: 2, @@ -42,8 +40,7 @@ describe('GlobalsStore', () => { describe('update', () => { it('changes the global args', () => { - const store = new GlobalsStore(); - store.set({ globals: { foo: 'old' }, globalTypes: { baz: {} } }); + const store = new GlobalsStore({ globals: { foo: 'old' }, globalTypes: { baz: {} } }); store.update({ foo: 'bar' }); expect(store.get()).toEqual({ foo: 'bar' }); @@ -57,8 +54,7 @@ describe('GlobalsStore', () => { }); it('does not merge objects', () => { - const store = new GlobalsStore(); - store.set({ globals: {}, globalTypes: {} }); + const store = new GlobalsStore({ globals: {}, globalTypes: {} }); store.update({ obj: { foo: 'bar' } }); expect(store.get()).toEqual({ obj: { foo: 'bar' } }); @@ -70,8 +66,7 @@ describe('GlobalsStore', () => { describe('updateFromPersisted', () => { it('only sets values for which globals or globalArgs exist', () => { - const store = new GlobalsStore(); - store.set({ + const store = new GlobalsStore({ globals: { arg1: 'arg1', }, @@ -92,8 +87,7 @@ describe('GlobalsStore', () => { describe('second call to set', () => { it('is initialized to the (new) default values from globalTypes if the (new) global is unset', () => { - const store = new GlobalsStore(); - store.set({ globals: {}, globalTypes: {} }); + const store = new GlobalsStore({ globals: {}, globalTypes: {} }); expect(store.get()).toEqual({}); @@ -118,8 +112,7 @@ describe('GlobalsStore', () => { describe('when underlying globals have not changed', () => { it('retains updated values, but not if they are undeclared', () => { - const store = new GlobalsStore(); - store.set({ + const store = new GlobalsStore({ globals: { arg1: 'arg1', }, @@ -152,8 +145,7 @@ describe('GlobalsStore', () => { describe('when underlying globals have changed', () => { it('retains a the same delta', () => { - const store = new GlobalsStore(); - store.set({ + const store = new GlobalsStore({ globals: { arg1: 'arg1', arg4: 'arg4', diff --git a/lib/store/src/GlobalsStore.ts b/lib/store/src/GlobalsStore.ts index f61aa1758754..219ba6832ae8 100644 --- a/lib/store/src/GlobalsStore.ts +++ b/lib/store/src/GlobalsStore.ts @@ -14,11 +14,22 @@ const setUndeclaredWarning = deprecate( ); export class GlobalsStore { - allowedGlobalNames: Set; + // We use ! here because TS doesn't analyse the .set() function to see if it actually get set + allowedGlobalNames!: Set; - initialGlobals: Globals; + initialGlobals!: Globals; - globals: Globals = {}; + globals!: Globals; + + constructor({ + globals = {}, + globalTypes = {}, + }: { + globals?: Globals; + globalTypes?: GlobalTypes; + }) { + this.set({ globals, globalTypes }); + } set({ globals = {}, globalTypes = {} }: { globals?: Globals; globalTypes?: GlobalTypes }) { const delta = this.initialGlobals && deepDiff(this.initialGlobals, this.globals); diff --git a/lib/store/src/StoryIndexStore.ts b/lib/store/src/StoryIndexStore.ts index 98292c2eca2b..bb6f2fa07da6 100644 --- a/lib/store/src/StoryIndexStore.ts +++ b/lib/store/src/StoryIndexStore.ts @@ -13,8 +13,6 @@ const getImportPathMap = memoize(1)((entries: StoryIndex['entries']) => ); export class StoryIndexStore { - channel: Channel; - entries: StoryIndex['entries']; constructor({ entries }: StoryIndex = { v: 4, entries: {} }) { diff --git a/lib/store/src/StoryStore.ts b/lib/store/src/StoryStore.ts index dc17a256e1e9..805e3f767aeb 100644 --- a/lib/store/src/StoryStore.ts +++ b/lib/store/src/StoryStore.ts @@ -38,13 +38,13 @@ const CSF_CACHE_SIZE = 1000; const STORY_CACHE_SIZE = 10000; export class StoryStore { - storyIndex: StoryIndexStore; + storyIndex?: StoryIndexStore; - importFn: ModuleImportFn; + importFn?: ModuleImportFn; - projectAnnotations: NormalizedProjectAnnotations; + projectAnnotations?: NormalizedProjectAnnotations; - globals: GlobalsStore; + globals?: GlobalsStore; args: ArgsStore; @@ -58,10 +58,10 @@ export class StoryStore { initializationPromise: SynchronousPromise; - resolveInitializationPromise: () => void; + // This *does* get set in the constructor but the semantics of `new SynchronousPromise` trip up TS + resolveInitializationPromise!: () => void; constructor() { - this.globals = new GlobalsStore(); this.args = new ArgsStore(); this.hooks = {}; @@ -82,7 +82,11 @@ export class StoryStore { this.projectAnnotations = normalizeProjectAnnotations(projectAnnotations); const { globals, globalTypes } = projectAnnotations; - this.globals.set({ globals, globalTypes }); + if (this.globals) { + this.globals.set({ globals, globalTypes }); + } else { + this.globals = new GlobalsStore({ globals, globalTypes }); + } } initialize({ @@ -114,6 +118,8 @@ export class StoryStore { importFn?: ModuleImportFn; storyIndex?: StoryIndex; }) { + if (!this.storyIndex) throw new Error(`onStoriesChanged called before initialization`); + if (importFn) this.importFn = importFn; if (storyIndex) this.storyIndex.entries = storyIndex.entries; if (this.cachedCSFFiles) await this.cacheAllCSFFiles(); @@ -122,11 +128,15 @@ export class StoryStore { // Get an entry from the index, waiting on initialization if necessary async storyIdToEntry(storyId: StoryId) { await this.initializationPromise; - return this.storyIndex.storyIdToEntry(storyId); + // The index will always be set before the initialization promise returns + return this.storyIndex!.storyIdToEntry(storyId); } // To load a single CSF file to service a story we need to look up the importPath in the index loadCSFFileByStoryId(storyId: StoryId): PromiseLike> { + if (!this.storyIndex || !this.importFn) + throw new Error(`loadCSFFileByStoryId called before initialization`); + const { importPath, title } = this.storyIndex.storyIdToEntry(storyId); return this.importFn(importPath).then((moduleExports) => // We pass the title in here as it may have been generated by autoTitle on the server. @@ -135,6 +145,8 @@ export class StoryStore { } loadAllCSFFiles(): PromiseLike['cachedCSFFiles']> { + if (!this.storyIndex) throw new Error(`loadAllCSFFiles called before initialization`); + const importPaths: Record = {}; Object.entries(this.storyIndex.entries).forEach(([storyId, { importPath }]) => { importPaths[importPath] = storyId; @@ -179,6 +191,8 @@ export class StoryStore { storyId: StoryId; csfFile: CSFFile; }): Story { + if (!this.projectAnnotations) throw new Error(`storyFromCSFFile called before initialization`); + const storyAnnotations = csfFile.stories[storyId]; if (!storyAnnotations) { throw new Error(`Didn't find '${storyId}' in CSF file, this is unexpected`); @@ -197,6 +211,9 @@ export class StoryStore { // If we have a CSF file we can get all the stories from it synchronously componentStoriesFromCSFFile({ csfFile }: { csfFile: CSFFile }): Story[] { + if (!this.storyIndex) + throw new Error(`componentStoriesFromCSFFile called before initialization`); + return Object.keys(this.storyIndex.entries) .filter((storyId: StoryId) => !!csfFile.stories[storyId]) .map((storyId: StoryId) => this.storyFromCSFFile({ storyId, csfFile })); @@ -205,6 +222,10 @@ export class StoryStore { async loadDocsFileById( docsId: StoryId ): Promise<{ docsExports: ModuleExports; csfFiles: CSFFile[] }> { + const { storyIndex } = this; + if (!storyIndex || !this.importFn) + throw new Error(`componentStoriesFromCSFFile called before initialization`); + const entry = await this.storyIdToEntry(docsId); if (entry.type !== 'docs') throw new Error(`Cannot load docs file for id ${docsId}`); @@ -213,7 +234,7 @@ export class StoryStore { const [docsExports, ...csfFiles] = (await Promise.all([ this.importFn(importPath), ...storiesImports.map((storyImportPath) => { - const firstStoryEntry = this.storyIndex.importPathToEntry(storyImportPath); + const firstStoryEntry = storyIndex.importPathToEntry(storyImportPath); return this.loadCSFFileByStoryId(firstStoryEntry.id); }), ])) as [ModuleExports, ...CSFFile[]]; @@ -232,6 +253,8 @@ export class StoryStore { // A prepared story does not include args, globals or hooks. These are stored in the story store // and updated separtely to the (immutable) story. getStoryContext(story: Story): Omit, 'viewMode'> { + if (!this.globals) throw new Error(`getStoryContext called before initialization`); + return { ...story, args: this.args.get(story.id), @@ -247,15 +270,17 @@ export class StoryStore { extract( options: ExtractOptions = { includeDocsOnly: false } ): Record> { - if (!this.cachedCSFFiles) { + if (!this.storyIndex) throw new Error(`extract called before initialization`); + + const { cachedCSFFiles } = this; + if (!cachedCSFFiles) throw new Error('Cannot call extract() unless you call cacheAllCSFFiles() first.'); - } return Object.entries(this.storyIndex.entries).reduce( (acc, [storyId, { type, importPath }]) => { if (type === 'docs') return acc; - const csfFile = this.cachedCSFFiles[importPath]; + const csfFile = cachedCSFFiles[importPath]; const story = this.storyFromCSFFile({ storyId, csfFile }); if (!options.includeDocsOnly && story.parameters.docsOnly) { @@ -282,6 +307,8 @@ export class StoryStore { } getSetStoriesPayload() { + if (!this.globals) throw new Error(`getSetStoriesPayload called before initialization`); + const stories = this.extract({ includeDocsOnly: true }); const kindParameters: Parameters = Object.values(stories).reduce( @@ -305,11 +332,14 @@ export class StoryStore { // It is used to allow v7 Storybooks to be composed in v6 Storybooks, which expect a // `stories.json` file with legacy fields (`kind` etc). getStoriesJsonData = (): StoryIndexV3 => { + const { storyIndex } = this; + if (!storyIndex) throw new Error(`getStoriesJsonData called before initialization`); + const value = this.getSetStoriesPayload(); const allowedParameters = ['fileName', 'docsOnly', 'framework', '__id', '__isArgsStory']; const stories: Record = mapValues(value.stories, (story) => { - const { importPath } = this.storyIndex.entries[story.id]; + const { importPath } = storyIndex.entries[story.id]; return { ...pick(story, ['id', 'name', 'title']), importPath, @@ -332,13 +362,16 @@ export class StoryStore { }; raw(): BoundStory[] { - return Object.values(this.extract()).map(({ id }: { id: StoryId }) => this.fromId(id)); + return Object.values(this.extract()) + .map(({ id }: { id: StoryId }) => this.fromId(id)) + .filter(Boolean) as BoundStory[]; } - fromId(storyId: StoryId): BoundStory { - if (!this.cachedCSFFiles) { + fromId(storyId: StoryId): BoundStory | null { + if (!this.storyIndex) throw new Error(`fromId called before initialization`); + + if (!this.cachedCSFFiles) throw new Error('Cannot call fromId/raw() unless you call cacheAllCSFFiles() first.'); - } let importPath; try { diff --git a/lib/store/src/args.ts b/lib/store/src/args.ts index 577c5587bcae..d635a04fdcec 100644 --- a/lib/store/src/args.ts +++ b/lib/store/src/args.ts @@ -151,7 +151,7 @@ export function groupArgsByTarget({ }: StoryContext) { const groupedArgs: Record> = {}; (Object.entries(args) as [keyof TArgs, any][]).forEach(([name, value]) => { - const { target = NO_TARGET_NAME } = (argTypes[name] || {}) as { target: string }; + const { target = NO_TARGET_NAME } = (argTypes[name] || {}) as { target?: string }; groupedArgs[target] = groupedArgs[target] || {}; groupedArgs[target][name] = value; diff --git a/lib/store/src/csf/normalizeProjectAnnotations.ts b/lib/store/src/csf/normalizeProjectAnnotations.ts index e6448ea2a16c..b443f2a75c17 100644 --- a/lib/store/src/csf/normalizeProjectAnnotations.ts +++ b/lib/store/src/csf/normalizeProjectAnnotations.ts @@ -1,4 +1,4 @@ -import type { AnyFramework, ProjectAnnotations } from '@storybook/csf'; +import type { AnyFramework, ArgTypes, ProjectAnnotations } from '@storybook/csf'; import { inferArgTypes } from '../inferArgTypes'; import { inferControls } from '../inferControls'; @@ -12,7 +12,7 @@ export function normalizeProjectAnnotations({ ...annotations }: ProjectAnnotations): NormalizedProjectAnnotations { return { - ...(argTypes && { argTypes: normalizeInputTypes(argTypes) }), + ...(argTypes && { argTypes: normalizeInputTypes(argTypes as ArgTypes) }), ...(globalTypes && { globalTypes: normalizeInputTypes(globalTypes) }), argTypesEnhancers: [ ...(argTypesEnhancers || []), diff --git a/lib/store/src/csf/normalizeStory.test.ts b/lib/store/src/csf/normalizeStory.test.ts index 3492178f87ff..52a3ffb2c599 100644 --- a/lib/store/src/csf/normalizeStory.test.ts +++ b/lib/store/src/csf/normalizeStory.test.ts @@ -3,10 +3,6 @@ import { normalizeStory } from './normalizeStory'; describe('normalizeStory', () => { describe('id generation', () => { - it('combines title and export name', () => { - expect(normalizeStory('name', {}, { title: 'title' }).id).toEqual('title--name'); - }); - it('respects component id', () => { expect(normalizeStory('name', {}, { title: 'title', id: 'component-id' }).id).toEqual( 'component-id--name' @@ -27,22 +23,28 @@ describe('normalizeStory', () => { describe('name', () => { it('preferences story.name over story.storyName', () => { expect( - normalizeStory('export', { name: 'name', storyName: 'storyName' }, { title: 'title' }).name + normalizeStory( + 'export', + { name: 'name', storyName: 'storyName' }, + { id: 'title', title: 'title' } + ).name ).toEqual('name'); - expect(normalizeStory('export', { storyName: 'storyName' }, { title: 'title' }).name).toEqual( - 'storyName' - ); + expect( + normalizeStory('export', { storyName: 'storyName' }, { id: 'title', title: 'title' }).name + ).toEqual('storyName'); }); it('falls back to capitalized export name', () => { - expect(normalizeStory('exportOne', {}, { title: 'title' }).name).toEqual('Export One'); + expect(normalizeStory('exportOne', {}, { id: 'title', title: 'title' }).name).toEqual( + 'Export One' + ); }); }); describe('user-provided story function', () => { it('should normalize into an object', () => { const storyFn = () => {}; - const meta = { title: 'title' }; + const meta = { id: 'title', title: 'title' }; expect(normalizeStory('storyExport', storyFn, meta)).toMatchInlineSnapshot(` Object { "argTypes": Object {}, @@ -63,21 +65,21 @@ describe('normalizeStory', () => { describe('render function', () => { it('implicit render function', () => { const storyObj = {}; - const meta = { title: 'title' }; + const meta = { id: 'title', title: 'title' }; const normalized = normalizeStory('storyExport', storyObj, meta); expect(normalized.render).toBeUndefined(); }); it('user-provided story render function', () => { const storyObj = { render: jest.fn() }; - const meta = { title: 'title', render: jest.fn() }; + const meta = { id: 'title', title: 'title', render: jest.fn() }; const normalized = normalizeStory('storyExport', storyObj, meta); expect(normalized.render).toBe(storyObj.render); }); it('user-provided meta render function', () => { const storyObj = {}; - const meta = { title: 'title', render: jest.fn() }; + const meta = { id: 'title', title: 'title', render: jest.fn() }; const normalized = normalizeStory('storyExport', storyObj, meta); expect(normalized.render).toBeUndefined(); }); @@ -86,21 +88,21 @@ describe('normalizeStory', () => { describe('play function', () => { it('no render function', () => { const storyObj = {}; - const meta = { title: 'title' }; + const meta = { id: 'title', title: 'title' }; const normalized = normalizeStory('storyExport', storyObj, meta); expect(normalized.play).toBeUndefined(); }); it('user-provided story render function', () => { const storyObj = { play: jest.fn() }; - const meta = { title: 'title', play: jest.fn() }; + const meta = { id: 'title', title: 'title', play: jest.fn() }; const normalized = normalizeStory('storyExport', storyObj, meta); expect(normalized.play).toBe(storyObj.play); }); it('user-provided meta render function', () => { const storyObj = {}; - const meta = { title: 'title', play: jest.fn() }; + const meta = { id: 'title', title: 'title', play: jest.fn() }; const normalized = normalizeStory('storyExport', storyObj, meta); expect(normalized.play).toBeUndefined(); }); @@ -109,7 +111,7 @@ describe('normalizeStory', () => { describe('annotations', () => { it('empty annotations', () => { const storyObj = {}; - const meta = { title: 'title' }; + const meta = { id: 'title', title: 'title' }; const normalized = normalizeStory('storyExport', storyObj, meta); expect(normalized).toMatchInlineSnapshot(` Object { @@ -135,7 +137,7 @@ describe('normalizeStory', () => { args: { storyArg: 'val' }, argTypes: { storyArgType: { type: 'string' } }, }; - const meta = { title: 'title' }; + const meta = { id: 'title', title: 'title' }; const { moduleExport, ...normalized } = normalizeStory('storyExport', storyObj, meta); expect(normalized).toMatchInlineSnapshot(` Object { @@ -182,7 +184,7 @@ describe('normalizeStory', () => { argTypes: { storyArgType2: { type: 'string' } }, }, }; - const meta = { title: 'title' }; + const meta = { id: 'title', title: 'title' }; const { moduleExport, ...normalized } = normalizeStory('storyExport', storyObj, meta); expect(normalized).toMatchInlineSnapshot(` Object { diff --git a/lib/store/src/csf/normalizeStory.ts b/lib/store/src/csf/normalizeStory.ts index a1635ab67daf..96ff55348e81 100644 --- a/lib/store/src/csf/normalizeStory.ts +++ b/lib/store/src/csf/normalizeStory.ts @@ -1,16 +1,17 @@ import type { - ComponentAnnotations, AnyFramework, LegacyStoryAnnotationsOrFn, StoryId, StoryAnnotations, StoryFn, + ArgTypes, + Args, } from '@storybook/csf'; import { storyNameFromExport, toId } from '@storybook/csf'; import dedent from 'ts-dedent'; import { logger } from '@storybook/client-logger'; import deprecate from 'util-deprecate'; -import type { NormalizedStoryAnnotations } from '../types'; +import type { NormalizedComponentAnnotations, NormalizedStoryAnnotations } from '../types'; import { normalizeInputTypes } from './normalizeInputTypes'; const deprecatedStoryAnnotation = dedent` @@ -25,16 +26,11 @@ const deprecatedStoryAnnotationWarning = deprecate(() => {}, deprecatedStoryAnno export function normalizeStory( key: StoryId, storyAnnotations: LegacyStoryAnnotationsOrFn, - meta: ComponentAnnotations + meta: NormalizedComponentAnnotations ): NormalizedStoryAnnotations { - let userStoryFn: StoryFn; - let storyObject: StoryAnnotations; - if (typeof storyAnnotations === 'function') { - userStoryFn = storyAnnotations; - storyObject = storyAnnotations; - } else { - storyObject = storyAnnotations; - } + const storyObject: StoryAnnotations = storyAnnotations; + const userStoryFn: StoryFn | null = + typeof storyAnnotations === 'function' ? storyAnnotations : null; const { story } = storyObject; if (story) { @@ -51,12 +47,12 @@ export function normalizeStory( const decorators = [...(storyObject.decorators || []), ...(story?.decorators || [])]; const parameters = { ...story?.parameters, ...storyObject.parameters }; const args = { ...story?.args, ...storyObject.args }; - const argTypes = { ...story?.argTypes, ...storyObject.argTypes }; + const argTypes = { ...(story?.argTypes as ArgTypes), ...(storyObject.argTypes as ArgTypes) }; const loaders = [...(storyObject.loaders || []), ...(story?.loaders || [])]; const { render, play } = storyObject; // eslint-disable-next-line no-underscore-dangle - const id = parameters.__id || toId(meta.id || meta.title, exportName); + const id = parameters.__id || toId(meta.id, exportName); return { moduleExport: storyAnnotations, id, diff --git a/lib/store/src/csf/prepareStory.ts b/lib/store/src/csf/prepareStory.ts index 6e82d761441a..8ff1e5d2ccc2 100644 --- a/lib/store/src/csf/prepareStory.ts +++ b/lib/store/src/csf/prepareStory.ts @@ -11,6 +11,7 @@ import type { StoryContext, AnyFramework, StrictArgTypes, + StoryContextForLoaders, } from '@storybook/csf'; import { includeConditionalArg } from '@storybook/csf'; @@ -84,6 +85,8 @@ export function prepareStory( componentAnnotations.render || projectAnnotations.render; + if (!render) throw new Error(`No render function available for storyId '${id}'`); + const passedArgTypes: StrictArgTypes = combineParameters( projectAnnotations.argTypes, componentAnnotations.argTypes, @@ -154,7 +157,7 @@ export function prepareStory( }; } - const applyLoaders = async (context: StoryContext) => { + const applyLoaders = async (context: StoryContextForLoaders) => { const loadResults = await Promise.all(loaders.map((loader) => loader(context))); const loaded = Object.assign({}, ...loadResults); return { ...context, loaded }; @@ -183,7 +186,7 @@ export function prepareStory( const unboundStoryFn = (context: StoryContext) => { let finalContext: StoryContext = context; if (global.FEATURES?.argTypeTargetsV7) { - const argsByTarget = groupArgsByTarget({ args: context.args, ...context }); + const argsByTarget = groupArgsByTarget(context); finalContext = { ...context, allArgs: context.args, diff --git a/lib/store/src/csf/processCSFFile.ts b/lib/store/src/csf/processCSFFile.ts index 02bf81c918cd..ff082349dcf1 100644 --- a/lib/store/src/csf/processCSFFile.ts +++ b/lib/store/src/csf/processCSFFile.ts @@ -24,10 +24,9 @@ const checkStorySort = (parameters: Parameters) => { if (options?.storySort) logger.error('The storySort option parameter can only be set globally'); }; -const checkDisallowedParameters = (parameters: Parameters) => { - if (!parameters) { - return; - } +const checkDisallowedParameters = (parameters?: Parameters) => { + if (!parameters) return; + checkGlobals(parameters); checkStorySort(parameters); }; diff --git a/lib/store/src/csf/testing-utils/index.ts b/lib/store/src/csf/testing-utils/index.ts index 04a7f93594bb..6426f15d80a6 100644 --- a/lib/store/src/csf/testing-utils/index.ts +++ b/lib/store/src/csf/testing-utils/index.ts @@ -8,6 +8,7 @@ import { Args, StoryContext, Parameters, + LegacyStoryAnnotationsOrFn, } from '@storybook/csf'; import { composeConfigs } from '../composeConfigs'; @@ -50,7 +51,7 @@ export function composeStory< TFramework extends AnyFramework = AnyFramework, TArgs extends Args = Args >( - storyAnnotations: AnnotatedStoryFn | StoryAnnotations, + storyAnnotations: LegacyStoryAnnotationsOrFn, componentAnnotations: ComponentAnnotations, projectAnnotations: ProjectAnnotations = GLOBAL_STORYBOOK_PROJECT_ANNOTATIONS, defaultConfig: ProjectAnnotations = {}, @@ -63,15 +64,17 @@ export function composeStory< // @TODO: Support auto title // eslint-disable-next-line no-param-reassign componentAnnotations.title = componentAnnotations.title ?? 'ComposedStory'; - const normalizedComponentAnnotations = normalizeComponentAnnotations(componentAnnotations); + const normalizedComponentAnnotations = + normalizeComponentAnnotations(componentAnnotations); const storyName = exportsName || storyAnnotations.storyName || storyAnnotations.story?.name || - storyAnnotations.name; + storyAnnotations.name || + 'unknown'; - const normalizedStory = normalizeStory( + const normalizedStory = normalizeStory( storyName, storyAnnotations, normalizedComponentAnnotations diff --git a/lib/store/src/csf/testing-utils/types.ts b/lib/store/src/csf/testing-utils/types.ts index 498e091aedac..747427ecb23f 100644 --- a/lib/store/src/csf/testing-utils/types.ts +++ b/lib/store/src/csf/testing-utils/types.ts @@ -5,9 +5,13 @@ import type { ComponentAnnotations, Args, StoryContext, + LegacyAnnotatedStoryFn, } from '@storybook/csf'; -export type CSFExports = { +export type CSFExports = Record< + string, + LegacyAnnotatedStoryFn +> & { default: ComponentAnnotations; __esModule?: boolean; __namedExportsOrder?: string[]; diff --git a/lib/store/src/decorators.ts b/lib/store/src/decorators.ts index 0b7a3a167f17..5c77411b1621 100644 --- a/lib/store/src/decorators.ts +++ b/lib/store/src/decorators.ts @@ -69,6 +69,9 @@ export function defaultDecorateStory( const bindWithContext = (decoratedStoryFn: LegacyStoryFn): PartialStoryFn => (update) => { + // This code path isn't possible because we always set `contextStore.value` before calling + // `decoratedWithContextStore`, but TS doesn't know that. + if (!contextStore.value) throw new Error('Decorated function called without init'); contextStore.value = { ...contextStore.value, ...sanitizeStoryContextUpdate(update), diff --git a/lib/store/src/inferControls.ts b/lib/store/src/inferControls.ts index 3dcbbbd1c30a..084b4c663793 100644 --- a/lib/store/src/inferControls.ts +++ b/lib/store/src/inferControls.ts @@ -11,13 +11,13 @@ type ControlsMatchers = { const inferControl = (argType: StrictInputType, name: string, matchers: ControlsMatchers): any => { const { type, options } = argType; - if (!type && !options) { + if (!type) { return undefined; } // args that end with background or color e.g. iconColor if (matchers.color && matchers.color.test(name)) { - const controlType = argType.type.name; + const controlType = type.name; if (controlType === 'string') { return { control: { type: 'color' } }; diff --git a/lib/store/src/parameters.ts b/lib/store/src/parameters.ts index 728aa1e693b2..a961ba0326eb 100644 --- a/lib/store/src/parameters.ts +++ b/lib/store/src/parameters.ts @@ -10,8 +10,9 @@ import isPlainObject from 'lodash/isPlainObject'; */ export const combineParameters = (...parameterSets: (Parameters | undefined)[]) => { const mergeKeys: Record = {}; - const combined = parameterSets.filter(Boolean).reduce((acc, p) => { - Object.entries(p).forEach(([key, value]) => { + const definedParametersSets = parameterSets.filter(Boolean) as Parameters[]; + const combined = definedParametersSets.reduce((acc, parameters) => { + Object.entries(parameters).forEach(([key, value]) => { const existing = acc[key]; if (Array.isArray(value) || typeof existing === 'undefined') { acc[key] = value; @@ -26,7 +27,7 @@ export const combineParameters = (...parameterSets: (Parameters | undefined)[]) }, {} as Parameters); Object.keys(mergeKeys).forEach((key) => { - const mergeValues = parameterSets + const mergeValues = definedParametersSets .filter(Boolean) .map((p) => p[key]) .filter((value) => typeof value !== 'undefined'); diff --git a/lib/store/src/sortStories.ts b/lib/store/src/sortStories.ts index a9c89e157fff..a574022dee21 100644 --- a/lib/store/src/sortStories.ts +++ b/lib/store/src/sortStories.ts @@ -37,7 +37,7 @@ export const sortStoriesV7 = ( throw new Error(dedent` Error sorting stories with sort parameter ${storySortParameter}: - > ${err.message} + > ${(err as Error).message} Are you using a V6-style sort function in V7 mode? diff --git a/lib/store/src/types.ts b/lib/store/src/types.ts index 88bf6bab8cc9..d0bcc4752f2a 100644 --- a/lib/store/src/types.ts +++ b/lib/store/src/types.ts @@ -51,8 +51,9 @@ export type NormalizedProjectAnnotations = ComponentAnnotations & { - // Useful to guarantee that id exists + // Useful to guarantee that id & title exists id: ComponentId; + title: ComponentTitle; argTypes?: StrictArgTypes; }; @@ -64,6 +65,7 @@ export type NormalizedStoryAnnotations; }; @@ -80,8 +82,10 @@ export type Story = unboundStoryFn: LegacyStoryFn; applyLoaders: ( context: StoryContextForLoaders - ) => Promise>; - playFunction: (context: StoryContext) => Promise | void; + ) => Promise< + StoryContextForLoaders & { loaded: StoryContext['loaded'] } + >; + playFunction?: (context: StoryContext) => Promise | void; }; export type BoundStory = Story & { diff --git a/lib/store/tsconfig.json b/lib/store/tsconfig.json index 5960cccb79a2..4f9534826dd7 100644 --- a/lib/store/tsconfig.json +++ b/lib/store/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../../tsconfig.json", "compilerOptions": { + "strict": true }, "include": [ "src/**/*" From 611e82d55ad6fbaeca8a8c47e66ca5ec75509a35 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Wed, 22 Jun 2022 17:04:03 +1000 Subject: [PATCH 2/7] Don't duplicate index types --- lib/store/src/types.ts | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/lib/store/src/types.ts b/lib/store/src/types.ts index d0bcc4752f2a..c84bdd451807 100644 --- a/lib/store/src/types.ts +++ b/lib/store/src/types.ts @@ -22,7 +22,9 @@ import type { PartialStoryFn, Parameters, } from '@storybook/csf'; +import type { StoryIndexEntry, DocsIndexEntry, IndexEntry } from '@storybook/addons'; +export type { StoryIndexEntry, DocsIndexEntry, IndexEntry }; export type { StoryId, Parameters }; export type Path = string; export type ModuleExport = any; @@ -103,23 +105,6 @@ export declare type RenderContext; }; -interface BaseIndexEntry { - id: StoryId; - name: StoryName; - title: ComponentTitle; - importPath: Path; -} -export type StoryIndexEntry = BaseIndexEntry & { - type: 'story'; -}; - -export type DocsIndexEntry = BaseIndexEntry & { - storiesImports: Path[]; - type: 'docs'; - legacy?: boolean; -}; - -export type IndexEntry = StoryIndexEntry | DocsIndexEntry; export interface V2CompatIndexEntry extends Omit { kind: StoryIndexEntry['title']; story: StoryIndexEntry['name']; From 766fe7fe3471303dfe5f1aaacec41cc37ee10d15 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Wed, 22 Jun 2022 21:18:50 +1000 Subject: [PATCH 3/7] Update preview web to use strict types --- lib/core-client/src/preview/start.test.ts | 11 +- lib/preview-web/src/DocsRender.ts | 33 ++++-- lib/preview-web/src/Preview.tsx | 16 ++- .../src/PreviewWeb.integration.test.ts | 6 ++ lib/preview-web/src/PreviewWeb.test.ts | 101 +++++++++--------- lib/preview-web/src/PreviewWeb.tsx | 67 +++++++----- lib/preview-web/src/StoryRender.ts | 23 ++-- lib/preview-web/src/UrlStore.ts | 9 +- lib/preview-web/src/WebView.ts | 2 +- lib/preview-web/src/parseArgsParam.ts | 3 +- lib/preview-web/tsconfig.json | 1 + 11 files changed, 166 insertions(+), 106 deletions(-) diff --git a/lib/core-client/src/preview/start.test.ts b/lib/core-client/src/preview/start.test.ts index 80672c2634d1..07e33406c06f 100644 --- a/lib/core-client/src/preview/start.test.ts +++ b/lib/core-client/src/preview/start.test.ts @@ -16,6 +16,7 @@ import { start } from './start'; jest.mock('@storybook/preview-web/dist/cjs/WebView'); jest.spyOn(WebView.prototype, 'prepareForDocs').mockReturnValue('docs-root'); +jest.spyOn(WebView.prototype, 'prepareForStory').mockReturnValue('story-root'); jest.mock('global', () => ({ // @ts-ignore @@ -156,7 +157,7 @@ describe('start', () => { expect.objectContaining({ id: 'component-a--story-one', }), - undefined + 'story-root' ); }); @@ -328,7 +329,7 @@ describe('start', () => { }), }), }), - undefined + 'story-root' ); }); @@ -365,7 +366,7 @@ describe('start', () => { }, }), }), - undefined + 'story-root' ); expect((window as any).IS_STORYBOOK).toBe(true); @@ -707,7 +708,7 @@ describe('start', () => { expect.objectContaining({ id: 'component-c--story-one', }), - undefined + 'story-root' ); }); @@ -1184,7 +1185,7 @@ describe('start', () => { expect.objectContaining({ id: 'component-a--story-one', }), - undefined + 'story-root' ); }); }); diff --git a/lib/preview-web/src/DocsRender.ts b/lib/preview-web/src/DocsRender.ts index 6e5ddee106d8..7ff7d5f690a7 100644 --- a/lib/preview-web/src/DocsRender.ts +++ b/lib/preview-web/src/DocsRender.ts @@ -34,7 +34,9 @@ export class DocsRender implements Render Promise; + public teardown?: (options: { viewModeChanged?: boolean }) => Promise; + + public torndown = false; constructor( private channel: Channel, @@ -42,7 +44,7 @@ export class DocsRender implements Render implements Render(); const storyIdToCSFFile = new Map>(); @@ -126,16 +130,18 @@ export class DocsRender implements Render { - if (exportToStoryId.has(moduleExport)) return exportToStoryId.get(moduleExport); + const storyId = exportToStoryId.get(moduleExport); + if (storyId) return storyId; throw new Error(`No story found with that export: ${moduleExport}`); }, storyById, componentStories: () => { - return Object.entries(metaCsfFile) - .map(([_, moduleExport]) => exportToStoryId.get(moduleExport)) - .filter(Boolean) - .map(storyById); + return ( + Object.entries(metaCsfFile) + .map(([_, moduleExport]) => exportToStoryId.get(moduleExport)) + .filter(Boolean) as StoryId[] + ).map(storyById); }, setMeta(m: ModuleExports) { metaCsfFile = m; @@ -154,10 +160,15 @@ export class DocsRender implements Render implements Render this.channel.emit(DOCS_RENDERED, this.id) @@ -178,6 +190,7 @@ export class DocsRender implements Render { if (!viewModeChanged || !this.canvasElement) return; renderer.unmount(this.canvasElement); + this.torndown = true; }; } diff --git a/lib/preview-web/src/Preview.tsx b/lib/preview-web/src/Preview.tsx index 224c3d5c1789..a179c0229399 100644 --- a/lib/preview-web/src/Preview.tsx +++ b/lib/preview-web/src/Preview.tsx @@ -46,7 +46,7 @@ export class Preview { importFn?: ModuleImportFn; - renderToDOM: RenderToDOM; + renderToDOM?: RenderToDOM; storyRenders: StoryRender[] = []; @@ -156,6 +156,8 @@ export class Preview { } emitGlobals() { + if (!this.storyStore.globals || !this.storyStore.projectAnnotations) + throw new Error(`Cannot emit before initialization`); this.channel.emit(SET_GLOBALS, { globals: this.storyStore.globals.get() || {}, globalTypes: this.storyStore.projectAnnotations.globalTypes || {}, @@ -171,6 +173,9 @@ export class Preview { // If initialization gets as far as the story index, this function runs. initializeWithStoryIndex(storyIndex: StoryIndex): PromiseLike { + if (!this.importFn) + throw new Error(`Cannot call initializeWithStoryIndex before initialization`); + return this.storyStore.initialize({ storyIndex, importFn: this.importFn, @@ -218,7 +223,7 @@ export class Preview { // Update the store with the new stories. await this.onStoriesChanged({ storyIndex }); } catch (err) { - this.renderPreviewEntryError('Error loading story index:', err); + this.renderPreviewEntryError('Error loading story index:', err as Error); throw err; } } @@ -235,6 +240,8 @@ export class Preview { } async onUpdateGlobals({ globals }: { globals: Globals }) { + if (!this.storyStore.globals) + throw new Error(`Cannot call onUpdateGlobals before initialization`); this.storyStore.globals.update(globals); await Promise.all(this.storyRenders.map((r) => r.rerender())); @@ -295,6 +302,9 @@ export class Preview { // we will change it to go ahead and load the story, which will end up being // "instant", although async. renderStoryToElement(story: Story, element: HTMLElement) { + if (!this.renderToDOM) + throw new Error(`Cannot call renderStoryToElement before initialization`); + const render = new StoryRender( this.channel, this.storyStore, @@ -318,7 +328,7 @@ export class Preview { { viewModeChanged }: { viewModeChanged?: boolean } = {} ) { this.storyRenders = this.storyRenders.filter((r) => r !== render); - await render?.teardown({ viewModeChanged }); + await render?.teardown?.({ viewModeChanged }); } // API diff --git a/lib/preview-web/src/PreviewWeb.integration.test.ts b/lib/preview-web/src/PreviewWeb.integration.test.ts index 426da9afad6f..c82fb8e96f4d 100644 --- a/lib/preview-web/src/PreviewWeb.integration.test.ts +++ b/lib/preview-web/src/PreviewWeb.integration.test.ts @@ -3,8 +3,11 @@ import global from 'global'; import { RenderContext } from '@storybook/store'; import addons, { mockChannel as createMockChannel } from '@storybook/addons'; import { DocsRenderer } from '@storybook/addon-docs'; +import { mocked } from 'ts-jest/utils'; +import { expect } from '@jest/globals'; import { PreviewWeb } from './PreviewWeb'; +import { WebView } from './WebView'; import { componentOneExports, importFn, @@ -56,6 +59,9 @@ beforeEach(() => { addons.setChannel(mockChannel as any); addons.setServerChannel(createMockChannel()); + + mocked(WebView.prototype).prepareForDocs.mockReturnValue('docs-element' as any); + mocked(WebView.prototype).prepareForStory.mockReturnValue('story-element' as any); }); describe('PreviewWeb', () => { diff --git a/lib/preview-web/src/PreviewWeb.test.ts b/lib/preview-web/src/PreviewWeb.test.ts index 050ee10a4879..bd6ecf9726bf 100644 --- a/lib/preview-web/src/PreviewWeb.test.ts +++ b/lib/preview-web/src/PreviewWeb.test.ts @@ -28,6 +28,8 @@ import { logger } from '@storybook/client-logger'; import { addons, mockChannel as createMockChannel } from '@storybook/addons'; import type { AnyFramework } from '@storybook/csf'; import type { ModuleImportFn, WebProjectAnnotations } from '@storybook/store'; +import { expect } from '@jest/globals'; +import { mocked } from 'ts-jest/utils'; import { PreviewWeb } from './PreviewWeb'; import { @@ -48,8 +50,8 @@ import { modernDocsExports, teardownRenderToDOM, } from './PreviewWeb.mockdata'; +import { WebView } from './WebView'; -jest.mock('./WebView'); const { history, document } = global; const mockStoryIndex = jest.fn(() => storyIndex); @@ -79,6 +81,7 @@ jest.mock('global', () => ({ jest.mock('@storybook/client-logger'); jest.mock('react-dom'); +jest.mock('./WebView'); const createGate = (): [Promise, (_?: any) => void] => { let openGate = (_?: any) => {}; @@ -104,9 +107,6 @@ async function createAndRenderPreview({ getProjectAnnotations?: () => WebProjectAnnotations; } = {}) { const preview = new PreviewWeb(); - ( - preview.view.prepareForDocs as jest.MockedFunction - ).mockReturnValue('docs-element' as any); await preview.initialize({ importFn: inputImportFn, getProjectAnnotations: inputGetProjectAnnotations, @@ -116,6 +116,7 @@ async function createAndRenderPreview({ return preview; } +const mockWebView = new WebView(); beforeEach(() => { document.location.search = ''; mockChannel.emit.mockClear(); @@ -134,6 +135,9 @@ beforeEach(() => { addons.setChannel(mockChannel as any); addons.setServerChannel(createMockChannel()); mockFetchResult = { status: 200, json: mockStoryIndex, text: () => 'error text' }; + + mocked(WebView.prototype).prepareForDocs.mockReturnValue('docs-element' as any); + mocked(WebView.prototype).prepareForStory.mockReturnValue('story-element' as any); }); describe('PreviewWeb', () => { @@ -172,7 +176,7 @@ describe('PreviewWeb', () => { const preview = await createAndRenderPreview(); - expect(preview.storyStore.globals.get()).toEqual({ a: 'c' }); + expect(preview.storyStore.globals!.get()).toEqual({ a: 'c' }); }); it('emits the SET_GLOBALS event', async () => { @@ -233,7 +237,7 @@ describe('PreviewWeb', () => { }, }); - expect(preview.storyStore.globals.get()).toEqual({ a: 'b' }); + expect(preview.storyStore.globals!.get()).toEqual({ a: 'b' }); }); }); @@ -456,7 +460,7 @@ describe('PreviewWeb', () => { loaded: { l: 7 }, }), }), - undefined // this is coming from view.prepareForStory, not super important + 'story-element' ); }); @@ -487,26 +491,27 @@ describe('PreviewWeb', () => { }); it('renders helpful message if renderToDOM is undefined', async () => { - const originalRenderToDOM = projectAnnotations.renderToDOM; - try { - projectAnnotations.renderToDOM = undefined; - - document.location.search = '?id=component-one--a'; - const preview = new PreviewWeb(); - await expect(preview.initialize({ importFn, getProjectAnnotations })).rejects.toThrow(); + document.location.search = '?id=component-one--a'; + const preview = new PreviewWeb(); + await expect( + preview.initialize({ + importFn, + getProjectAnnotations: () => ({ + ...getProjectAnnotations, + renderToDOM: undefined, + }), + }) + ).rejects.toThrow(); - expect(preview.view.showErrorDisplay).toHaveBeenCalled(); - expect((preview.view.showErrorDisplay as jest.Mock).mock.calls[0][0]) - .toMatchInlineSnapshot(` + expect(preview.view.showErrorDisplay).toHaveBeenCalled(); + expect((preview.view.showErrorDisplay as jest.Mock).mock.calls[0][0]) + .toMatchInlineSnapshot(` [Error: Expected your framework's preset to export a \`renderToDOM\` field. Perhaps it needs to be upgraded for Storybook 6.4? More info: https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#mainjs-framework-field ] `); - } finally { - projectAnnotations.renderToDOM = originalRenderToDOM; - } }); it('renders exception if the play function throws', async () => { @@ -705,7 +710,7 @@ describe('PreviewWeb', () => { emitter.emit(UPDATE_GLOBALS, { globals: { foo: 'bar' } }); - expect(preview.storyStore.globals.get()).toEqual({ a: 'b', foo: 'bar' }); + expect(preview.storyStore.globals!.get()).toEqual({ a: 'b', foo: 'bar' }); }); it('passes new globals in context to renderToDOM', async () => { @@ -724,7 +729,7 @@ describe('PreviewWeb', () => { globals: { a: 'b', foo: 'bar' }, }), }), - undefined // this is coming from view.prepareForStory, not super important + 'story-element' ); }); @@ -807,7 +812,7 @@ describe('PreviewWeb', () => { args: { foo: 'a', new: 'arg' }, }), }), - undefined // this is coming from view.prepareForStory, not super important + 'story-element' ); }); @@ -864,7 +869,7 @@ describe('PreviewWeb', () => { args: { foo: 'a', new: 'arg' }, }), }), - undefined // this is coming from view.prepareForStory, not super important + 'story-element' ); // Now let the first loader call resolve @@ -883,7 +888,7 @@ describe('PreviewWeb', () => { args: { foo: 'a', new: 'arg' }, }), }), - undefined // this is coming from view.prepareForStory, not super important + 'story-element' ); }); @@ -914,7 +919,7 @@ describe('PreviewWeb', () => { args: { foo: 'a' }, }), }), - undefined // this is coming from view.prepareForStory, not super important + 'story-element' ); expect(projectAnnotations.renderToDOM).toHaveBeenCalledWith( expect.objectContaining({ @@ -924,7 +929,7 @@ describe('PreviewWeb', () => { args: { foo: 'a', new: 'arg' }, }), }), - undefined // this is coming from view.prepareForStory, not super important + 'story-element' ); }); @@ -947,7 +952,7 @@ describe('PreviewWeb', () => { args: { foo: 'a' }, }), }), - undefined // this is coming from view.prepareForStory, not super important + 'story-element' ); expect(projectAnnotations.renderToDOM).toHaveBeenCalledWith( expect.objectContaining({ @@ -957,7 +962,7 @@ describe('PreviewWeb', () => { args: { foo: 'a', new: 'arg' }, }), }), - undefined // this is coming from view.prepareForStory, not super important + 'story-element' ); }); @@ -985,7 +990,7 @@ describe('PreviewWeb', () => { args: { foo: 'a' }, }), }), - undefined // this is coming from view.prepareForStory, not super important + 'story-element' ); emitter.emit(UPDATE_STORY_ARGS, { @@ -1005,7 +1010,7 @@ describe('PreviewWeb', () => { args: { foo: 'a', new: 'arg' }, }), }), - undefined // this is coming from view.prepareForStory, not super important + 'story-element' ); // Now let the playFunction call resolve @@ -1172,7 +1177,7 @@ describe('PreviewWeb', () => { args: { foo: 'a', new: 'value' }, }), }), - undefined // this is coming from view.prepareForStory, not super important + 'story-element' ); await waitForEvents([STORY_ARGS_UPDATED]); @@ -1213,7 +1218,7 @@ describe('PreviewWeb', () => { args: { foo: 'a' }, }), }), - undefined // this is coming from view.prepareForStory, not super important + 'story-element' ); await waitForEvents([STORY_ARGS_UPDATED]); @@ -1254,7 +1259,7 @@ describe('PreviewWeb', () => { args: { foo: 'a' }, }), }), - undefined // this is coming from view.prepareForStory, not super important + 'story-element' ); await waitForEvents([STORY_ARGS_UPDATED]); @@ -1295,7 +1300,7 @@ describe('PreviewWeb', () => { args: { foo: 'a' }, }), }), - undefined // this is coming from view.prepareForStory, not super important + 'story-element' ); await waitForEvents([STORY_ARGS_UPDATED]); @@ -1323,7 +1328,7 @@ describe('PreviewWeb', () => { expect(projectAnnotations.renderToDOM).toHaveBeenCalledWith( expect.objectContaining({ forceRemount: false }), - undefined // this is coming from view.prepareForStory, not super important + 'story-element' ); }); }); @@ -1347,7 +1352,7 @@ describe('PreviewWeb', () => { expect(projectAnnotations.renderToDOM).toHaveBeenCalledWith( expect.objectContaining({ forceRemount: true }), - undefined // this is coming from view.prepareForStory, not super important + 'story-element' ); }); @@ -1367,7 +1372,7 @@ describe('PreviewWeb', () => { loaded: { l: 7 }, }), }), - undefined // this is coming from view.prepareForStory, not super important + 'story-element' ); mockChannel.emit.mockClear(); @@ -1723,7 +1728,7 @@ describe('PreviewWeb', () => { loaded: { l: 7 }, }), }), - undefined // this is coming from view.prepareForStory, not super important + 'story-element' ); }); @@ -1888,7 +1893,7 @@ describe('PreviewWeb', () => { loaded: { l: 7 }, }), }), - undefined // this is coming from view.prepareForStory, not super important + 'story-element' ); }); @@ -1942,7 +1947,7 @@ describe('PreviewWeb', () => { loaded: { l: 7 }, }), }), - undefined // this is coming from view.prepareForStory, not super important + 'story-element' ); mockChannel.emit.mockClear(); @@ -1967,7 +1972,7 @@ describe('PreviewWeb', () => { loaded: { l: 7 }, }), }), - undefined // this is coming from view.prepareForStory, not super important + 'story-element' ); await waitForRenderPhase('playing'); @@ -1995,7 +2000,7 @@ describe('PreviewWeb', () => { loaded: { l: 7 }, }), }), - undefined // this is coming from view.prepareForStory, not super important + 'story-element' ); mockChannel.emit.mockClear(); @@ -2284,7 +2289,7 @@ describe('PreviewWeb', () => { loaded: { l: 7 }, }), }), - undefined // this is coming from view.prepareForStory, not super important + 'story-element' ); }); @@ -2557,7 +2562,7 @@ describe('PreviewWeb', () => { loaded: { l: 7 }, }), }), - undefined // this is coming from view.prepareForStory, not super important + 'story-element' ); }); @@ -2585,7 +2590,7 @@ describe('PreviewWeb', () => { args: { foo: 'updated' }, }), }), - undefined // this is coming from view.prepareForStory, not super important + 'story-element' ); }); @@ -2846,7 +2851,7 @@ describe('PreviewWeb', () => { args: { foo: 'updated', bar: 'edited' }, }), }), - undefined // this is coming from view.prepareForStory, not super important + 'story-element' ); }); }); @@ -3071,7 +3076,7 @@ describe('PreviewWeb', () => { globals: { a: 'edited' }, }), }), - undefined // this is coming from view.prepareForStory, not super important + 'story-element' ); }); }); diff --git a/lib/preview-web/src/PreviewWeb.tsx b/lib/preview-web/src/PreviewWeb.tsx index 7aea527f6dd2..d52e65fbb74d 100644 --- a/lib/preview-web/src/PreviewWeb.tsx +++ b/lib/preview-web/src/PreviewWeb.tsx @@ -20,7 +20,7 @@ import { UPDATE_QUERY_PARAMS, } from '@storybook/core-events'; import { logger } from '@storybook/client-logger'; -import { AnyFramework, StoryId, ProjectAnnotations, Args, Globals } from '@storybook/csf'; +import { AnyFramework, StoryId, ProjectAnnotations, Args, Globals, ViewMode } from '@storybook/csf'; import type { ModuleImportFn, Selection, @@ -54,9 +54,9 @@ export class PreviewWeb extends Preview | DocsRender; + currentRender?: StoryRender | DocsRender; constructor() { super(); @@ -93,6 +93,9 @@ export class PreviewWeb extends Preview extends Preview extends Preview extends Preview extends Preview extends Preview { // At the start of renderToDOM we make the story visible (see note in WebView) this.view.showStoryDuringRender(); - return this.renderToDOM(...args); + return renderToDOM(...args); }, this.mainStoryCallbacks(storyId), storyId, @@ -302,7 +305,10 @@ export class PreviewWeb extends Preview extends Preview extends Preview extends Preview extends Preview); (this.currentRender as StoryRender).renderToElement( this.view.prepareForStory(render.story) @@ -380,6 +396,9 @@ export class PreviewWeb extends Preview, element: HTMLElement) { + if (!this.renderToDOM) + throw new Error(`Cannot call renderStoryToElement before initialization`); + const render = new StoryRender( this.channel, this.storyStore, @@ -400,10 +419,10 @@ export class PreviewWeb extends Preview, - { viewModeChanged }: { viewModeChanged?: boolean } = {} + { viewModeChanged = false }: { viewModeChanged?: boolean } = {} ) { this.storyRenders = this.storyRenders.filter((r) => r !== render); - await render?.teardown({ viewModeChanged }); + await render?.teardown?.({ viewModeChanged }); } // API diff --git a/lib/preview-web/src/StoryRender.ts b/lib/preview-web/src/StoryRender.ts index 8f77b0f68781..01d8cecebfd0 100644 --- a/lib/preview-web/src/StoryRender.ts +++ b/lib/preview-web/src/StoryRender.ts @@ -54,7 +54,8 @@ export interface Render { story?: Story; isPreparing: () => boolean; disableKeyListeners: boolean; - teardown: (options: { viewModeChanged: boolean }) => Promise; + teardown?: (options: { viewModeChanged: boolean }) => Promise; + torndown: boolean; renderToElement: (canvasElement: HTMLElement, renderStoryToElement?: any) => Promise; } @@ -75,6 +76,8 @@ export class StoryRender implements Render {}; + public torndown = false; + constructor( public channel: Channel, public store: StoryStore, @@ -143,6 +146,7 @@ export class StoryRender implements Render implements Render implements Render; + let loadedContext: Awaited>; await this.runPhase(abortSignal, 'loading', async () => { loadedContext = await applyLoaders({ ...this.storyContext(), @@ -181,13 +188,12 @@ export class StoryRender implements Render = { - // @ts-ignore - ...loadedContext, + ...loadedContext!, // By this stage, it is possible that new args/globals have been received for this story // and we need to ensure we render it with the new values ...this.storyContext(), abortSignal, - canvasElement: this.canvasElement as HTMLElement, + canvasElement, }; const renderContext: RenderContext = { componentId, @@ -205,7 +211,7 @@ export class StoryRender implements Render { this.teardownRender = - (await this.renderToScreen(renderContext, this.canvasElement)) || (() => {}); + (await this.renderToScreen(renderContext, canvasElement)) || (() => {}); }); this.notYetRendered = false; if (abortSignal.aborted) return; @@ -242,12 +248,11 @@ export class StoryRender implements Render): string | void => { return getFirstString(v[0]); } if (isObject(v)) { - // @ts-ignore - return getFirstString(Object.values(v)); + return getFirstString(Object.values(v).filter(Boolean) as string[]); } return undefined; }; @@ -74,7 +73,7 @@ Use \`id=$storyId\` instead. See https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#new-url-structure` ); -export const getSelectionSpecifierFromPath: () => SelectionSpecifier = () => { +export const getSelectionSpecifierFromPath: () => SelectionSpecifier | null = () => { const query = qs.parse(document.location.search, { ignoreQueryPrefix: true }); const args = typeof query.args === 'string' ? parseArgsParam(query.args) : undefined; const globals = typeof query.globals === 'string' ? parseArgsParam(query.globals) : undefined; @@ -103,9 +102,9 @@ export const getSelectionSpecifierFromPath: () => SelectionSpecifier = () => { }; export class UrlStore { - selectionSpecifier: SelectionSpecifier; + selectionSpecifier: SelectionSpecifier | null; - selection: Selection; + selection?: Selection; constructor() { this.selectionSpecifier = getSelectionSpecifierFromPath(); diff --git a/lib/preview-web/src/WebView.ts b/lib/preview-web/src/WebView.ts index e710cb7fbc39..2d6988ad8a27 100644 --- a/lib/preview-web/src/WebView.ts +++ b/lib/preview-web/src/WebView.ts @@ -41,7 +41,7 @@ export class WebView { testing = false; - preparingTimeout: ReturnType = null; + preparingTimeout?: ReturnType; constructor() { // Special code for testing situations diff --git a/lib/preview-web/src/parseArgsParam.ts b/lib/preview-web/src/parseArgsParam.ts index c13a9c07c914..2aa9d53f2c85 100644 --- a/lib/preview-web/src/parseArgsParam.ts +++ b/lib/preview-web/src/parseArgsParam.ts @@ -25,7 +25,8 @@ const validateArgs = (key = '', value: unknown): boolean => { ); } if (Array.isArray(value)) return value.every((v) => validateArgs(key, v)); - if (isPlainObject(value)) return Object.entries(value).every(([k, v]) => validateArgs(k, v)); + if (isPlainObject(value)) + return Object.entries(value as object).every(([k, v]) => validateArgs(k, v)); return false; }; diff --git a/lib/preview-web/tsconfig.json b/lib/preview-web/tsconfig.json index 5960cccb79a2..4f9534826dd7 100644 --- a/lib/preview-web/tsconfig.json +++ b/lib/preview-web/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../../tsconfig.json", "compilerOptions": { + "strict": true }, "include": [ "src/**/*" From 1e259c597068f2051f528c54de151e8a81e35f84 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Wed, 22 Jun 2022 21:36:50 +1000 Subject: [PATCH 4/7] Got store tests typechecking --- lib/store/src/ArgsStore.test.ts | 2 + lib/store/src/GlobalsStore.test.ts | 1 + lib/store/src/StoryIndexStore.test.ts | 5 +++ lib/store/src/StoryStore.test.ts | 14 +++--- lib/store/src/args.test.ts | 17 ++++--- lib/store/src/autoTitle.test.ts | 1 + lib/store/src/csf/composeConfigs.test.ts | 2 + lib/store/src/csf/normalizeInputTypes.test.ts | 2 + lib/store/src/csf/normalizeStory.test.ts | 2 + lib/store/src/csf/prepareStory.test.ts | 8 ++-- lib/store/src/csf/processCSFFile.test.ts | 2 + lib/store/src/decorators.test.ts | 11 ++--- .../src/{hooks.test.js => hooks.test.ts} | 11 +++-- lib/store/src/inferArgTypes.test.ts | 1 + lib/store/src/inferControls.test.ts | 4 +- lib/store/src/parameters.test.ts | 1 + lib/store/src/storySort.test.ts | 44 +++++++++++-------- 17 files changed, 83 insertions(+), 45 deletions(-) rename lib/store/src/{hooks.test.js => hooks.test.ts} (97%) diff --git a/lib/store/src/ArgsStore.test.ts b/lib/store/src/ArgsStore.test.ts index 31b7880d1fdb..d4656ef71f3b 100644 --- a/lib/store/src/ArgsStore.test.ts +++ b/lib/store/src/ArgsStore.test.ts @@ -1,3 +1,5 @@ +import { expect } from '@jest/globals'; + import { ArgsStore } from './ArgsStore'; jest.mock('@storybook/client-logger'); diff --git a/lib/store/src/GlobalsStore.test.ts b/lib/store/src/GlobalsStore.test.ts index 1f299157c107..2ebb079c27b2 100644 --- a/lib/store/src/GlobalsStore.test.ts +++ b/lib/store/src/GlobalsStore.test.ts @@ -1,3 +1,4 @@ +import { expect } from '@jest/globals'; import { GlobalsStore } from './GlobalsStore'; describe('GlobalsStore', () => { diff --git a/lib/store/src/StoryIndexStore.test.ts b/lib/store/src/StoryIndexStore.test.ts index 6aaa1b0e218a..bf484991e3b8 100644 --- a/lib/store/src/StoryIndexStore.test.ts +++ b/lib/store/src/StoryIndexStore.test.ts @@ -1,3 +1,5 @@ +import { expect } from '@jest/globals'; + import { StoryIndexStore } from './StoryIndexStore'; import { StoryIndex } from './types'; @@ -7,18 +9,21 @@ const storyIndex: StoryIndex = { v: 4, entries: { 'component-one--a': { + type: 'story', id: 'component-one--a', title: 'Component One', name: 'A', importPath: './src/ComponentOne.stories.js', }, 'component-one--b': { + type: 'story', id: 'component-one--b', title: 'Component One', name: 'B', importPath: './src/ComponentOne.stories.js', }, 'component-two--c': { + type: 'story', id: 'component-one--c', title: 'Component Two', name: 'C', diff --git a/lib/store/src/StoryStore.test.ts b/lib/store/src/StoryStore.test.ts index afac624ff7c1..6ca4c653672a 100644 --- a/lib/store/src/StoryStore.test.ts +++ b/lib/store/src/StoryStore.test.ts @@ -1,5 +1,6 @@ import type { AnyFramework, ProjectAnnotations } from '@storybook/csf'; import global from 'global'; +import { expect } from '@jest/globals'; import { prepareStory } from './csf/prepareStory'; import { processCSFFile } from './csf/processCSFFile'; @@ -76,10 +77,10 @@ describe('StoryStore', () => { store.setProjectAnnotations(projectAnnotations); store.initialize({ storyIndex, importFn, cache: false }); - expect(store.projectAnnotations.globalTypes).toEqual({ + expect(store.projectAnnotations!.globalTypes).toEqual({ a: { name: 'a', type: { name: 'string' } }, }); - expect(store.projectAnnotations.argTypes).toEqual({ + expect(store.projectAnnotations!.argTypes).toEqual({ a: { name: 'a', type: { name: 'string' } }, }); }); @@ -90,10 +91,10 @@ describe('StoryStore', () => { store.initialize({ storyIndex, importFn, cache: false }); store.setProjectAnnotations(projectAnnotations); - expect(store.projectAnnotations.globalTypes).toEqual({ + expect(store.projectAnnotations!.globalTypes).toEqual({ a: { name: 'a', type: { name: 'string' } }, }); - expect(store.projectAnnotations.argTypes).toEqual({ + expect(store.projectAnnotations!.argTypes).toEqual({ a: { name: 'a', type: { name: 'string' } }, }); }); @@ -408,7 +409,7 @@ describe('StoryStore', () => { const story = await store.loadStory({ storyId: 'component-one--a' }); store.args.update(story.id, { foo: 'bar' }); - store.globals.update({ a: 'c' }); + store.globals!.update({ a: 'c' }); expect(store.getStoryContext(story)).toMatchObject({ args: { foo: 'bar' }, @@ -455,8 +456,9 @@ describe('StoryStore', () => { importFn.mockClear(); const csfFiles = await store.loadAllCSFFiles(); + expect(csfFiles).not.toBeUndefined(); - expect(Object.keys(csfFiles)).toEqual([ + expect(Object.keys(csfFiles!)).toEqual([ './src/ComponentOne.stories.js', './src/ComponentTwo.stories.js', ]); diff --git a/lib/store/src/args.test.ts b/lib/store/src/args.test.ts index aafd94f6ee99..c3adf885f3f2 100644 --- a/lib/store/src/args.test.ts +++ b/lib/store/src/args.test.ts @@ -1,4 +1,7 @@ import { once } from '@storybook/client-logger'; +import { expect } from '@jest/globals'; +import { SBType } from '@storybook/csf'; + import { combineArgs, groupArgsByTarget, @@ -7,13 +10,13 @@ import { validateOptions, } from './args'; -const stringType = { name: 'string' }; -const numberType = { name: 'number' }; -const booleanType = { name: 'boolean' }; -const enumType = { name: 'enum' }; -const functionType = { name: 'function' }; -const numArrayType = { name: 'array', value: numberType }; -const boolObjectType = { name: 'object', value: { bool: booleanType } }; +const stringType: SBType = { name: 'string' }; +const numberType: SBType = { name: 'number' }; +const booleanType: SBType = { name: 'boolean' }; +const enumType: SBType = { name: 'enum', value: [1, 2, 3] }; +const functionType: SBType = { name: 'function' }; +const numArrayType: SBType = { name: 'array', value: numberType }; +const boolObjectType: SBType = { name: 'object', value: { bool: booleanType } }; jest.mock('@storybook/client-logger'); diff --git a/lib/store/src/autoTitle.test.ts b/lib/store/src/autoTitle.test.ts index b7e95b6e7268..256b91d2d402 100644 --- a/lib/store/src/autoTitle.test.ts +++ b/lib/store/src/autoTitle.test.ts @@ -1,4 +1,5 @@ import { normalizeStoriesEntry } from '@storybook/core-common'; +import { expect } from '@jest/globals'; import { userOrAutoTitleFromSpecifier as userOrAuto } from './autoTitle'; diff --git a/lib/store/src/csf/composeConfigs.test.ts b/lib/store/src/csf/composeConfigs.test.ts index ef6d04c5ddd2..ad1aeea84e32 100644 --- a/lib/store/src/csf/composeConfigs.test.ts +++ b/lib/store/src/csf/composeConfigs.test.ts @@ -1,3 +1,5 @@ +import { expect } from '@jest/globals'; + import { composeConfigs } from './composeConfigs'; describe('composeConfigs', () => { diff --git a/lib/store/src/csf/normalizeInputTypes.test.ts b/lib/store/src/csf/normalizeInputTypes.test.ts index 180924c801f5..c06939e9cc59 100644 --- a/lib/store/src/csf/normalizeInputTypes.test.ts +++ b/lib/store/src/csf/normalizeInputTypes.test.ts @@ -1,3 +1,5 @@ +import { expect } from '@jest/globals'; + import { normalizeInputType, normalizeInputTypes } from './normalizeInputTypes'; describe('normalizeInputType', () => { diff --git a/lib/store/src/csf/normalizeStory.test.ts b/lib/store/src/csf/normalizeStory.test.ts index 52a3ffb2c599..a05675acae5e 100644 --- a/lib/store/src/csf/normalizeStory.test.ts +++ b/lib/store/src/csf/normalizeStory.test.ts @@ -1,4 +1,6 @@ +import { expect } from '@jest/globals'; import { AnyFramework, StoryAnnotationsOrFn } from '@storybook/csf'; + import { normalizeStory } from './normalizeStory'; describe('normalizeStory', () => { diff --git a/lib/store/src/csf/prepareStory.test.ts b/lib/store/src/csf/prepareStory.test.ts index a5005d87a339..c4243e52212b 100644 --- a/lib/store/src/csf/prepareStory.test.ts +++ b/lib/store/src/csf/prepareStory.test.ts @@ -1,4 +1,5 @@ import global from 'global'; +import { expect } from '@jest/globals'; import { addons, HooksContext } from '@storybook/addons'; import type { AnyFramework, @@ -7,6 +8,7 @@ import type { SBScalarType, StoryContext, } from '@storybook/csf'; + import { NO_TARGET_NAME } from '../args'; import { prepareStory } from './prepareStory'; @@ -193,7 +195,7 @@ describe('prepareStory', () => { describe('argsEnhancers', () => { it('are applied in the right order', () => { - const run = []; + const run: number[] = []; const enhancerOne: ArgsEnhancer = () => { run.push(1); return {}; @@ -351,7 +353,7 @@ describe('prepareStory', () => { it('awaits the result of a loader', async () => { const loader = jest.fn(async () => new Promise((r) => setTimeout(() => r({ foo: 7 }), 100))); const { applyLoaders } = prepareStory( - { id, name, loaders: [loader], moduleExport }, + { id, name, loaders: [loader as any], moduleExport }, { id, title }, { render } ); @@ -387,7 +389,7 @@ describe('prepareStory', () => { }); it('later loaders override earlier loaders', async () => { - const loaders = [ + const loaders: any[] = [ async () => new Promise((r) => setTimeout(() => r({ foo: 7 }), 100)), async () => new Promise((r) => setTimeout(() => r({ foo: 3 }), 50)), ]; diff --git a/lib/store/src/csf/processCSFFile.test.ts b/lib/store/src/csf/processCSFFile.test.ts index e5540f069901..6f04b2b7361f 100644 --- a/lib/store/src/csf/processCSFFile.test.ts +++ b/lib/store/src/csf/processCSFFile.test.ts @@ -1,3 +1,5 @@ +import { expect } from '@jest/globals'; + import { processCSFFile } from './processCSFFile'; describe('processCSFFile', () => { diff --git a/lib/store/src/decorators.test.ts b/lib/store/src/decorators.test.ts index 7ce226d00ad9..c576d03a548b 100644 --- a/lib/store/src/decorators.test.ts +++ b/lib/store/src/decorators.test.ts @@ -1,3 +1,4 @@ +import { expect } from '@jest/globals'; import type { AnyFramework, StoryContext } from '@storybook/csf'; import { defaultDecorateStory } from './decorators'; @@ -15,7 +16,7 @@ function makeContext(input: Record = {}): StoryContext { it('calls decorators in out to in order', () => { - const order = []; + const order: number[] = []; const decorators = [ (s) => order.push(1) && s(), (s) => order.push(2) && s(), @@ -29,7 +30,7 @@ describe('client-api.decorators', () => { }); it('passes context through to sub decorators', () => { - const contexts = []; + const contexts: StoryContext[] = []; const decorators = [ (s, c) => contexts.push(c) && s({ args: { k: 1 } }), (s, c) => contexts.push(c) && s({ args: { k: 2 } }), @@ -43,7 +44,7 @@ describe('client-api.decorators', () => { }); it('passes context through to sub decorators additively', () => { - const contexts = []; + const contexts: StoryContext[] = []; const decorators = [ (s, c) => contexts.push(c) && s({ args: { a: 1 } }), (s, c) => contexts.push(c) && s({ globals: { g: 2 } }), @@ -78,7 +79,7 @@ describe('client-api.decorators', () => { // both story functions would receive {story: 2}. The assumption here is that we'll never render // the same story twice at the same time. it('does not interleave contexts if two decorated stories are call simultaneously', async () => { - const contexts = []; + const contexts: StoryContext[] = []; let resolve; const fence = new Promise((r) => { resolve = r; @@ -104,7 +105,7 @@ describe('client-api.decorators', () => { }); it('DOES NOT merge core metadata or pass through core metadata keys in context', () => { - const contexts = []; + const contexts: StoryContext[] = []; const decorators = [ (s, c) => contexts.push(c) && diff --git a/lib/store/src/hooks.test.js b/lib/store/src/hooks.test.ts similarity index 97% rename from lib/store/src/hooks.test.js rename to lib/store/src/hooks.test.ts index 8ee8c590470d..1be7c41a9c61 100644 --- a/lib/store/src/hooks.test.js +++ b/lib/store/src/hooks.test.ts @@ -1,3 +1,4 @@ +import { expect } from '@jest/globals'; import { FORCE_RE_RENDER, STORY_RENDERED, @@ -6,6 +7,8 @@ import { UPDATE_GLOBALS, } from '@storybook/core-events'; import { addons } from '@storybook/addons'; +import { DecoratorFunction, StoryContext } from '@storybook/csf'; + import { defaultDecorateStory } from './decorators'; import { applyHooks, @@ -60,8 +63,8 @@ beforeEach(() => { const decorateStory = applyHooks(defaultDecorateStory); -const run = (storyFn, decorators = [], context) => - decorateStory(storyFn, decorators)({ ...context, hooks }); +const run = (storyFn, decorators: DecoratorFunction[] = [], context = {}) => + decorateStory(storyFn, decorators)({ ...context, hooks } as Partial); describe('Preview hooks', () => { describe('useEffect', () => { @@ -321,7 +324,7 @@ describe('Preview hooks', () => { expect(result).toBe(callback); }); it('returns the previous callback reference if deps are unchanged', () => { - const callbacks = []; + const callbacks: (() => void)[] = []; const storyFn = () => { const callback = useCallback(() => {}, []); callbacks.push(callback); @@ -331,7 +334,7 @@ describe('Preview hooks', () => { expect(callbacks[0]).toBe(callbacks[1]); }); it('creates new callback reference if deps are changed', () => { - const callbacks = []; + const callbacks: (() => void)[] = []; let counter = 0; const storyFn = () => { counter += 1; diff --git a/lib/store/src/inferArgTypes.test.ts b/lib/store/src/inferArgTypes.test.ts index b5b9762b338c..e2a51d347b2e 100644 --- a/lib/store/src/inferArgTypes.test.ts +++ b/lib/store/src/inferArgTypes.test.ts @@ -1,4 +1,5 @@ import { logger } from '@storybook/client-logger'; +import { expect } from '@jest/globals'; import { inferArgTypes } from './inferArgTypes'; diff --git a/lib/store/src/inferControls.test.ts b/lib/store/src/inferControls.test.ts index 15d99dfb23ba..79c916e3c208 100644 --- a/lib/store/src/inferControls.test.ts +++ b/lib/store/src/inferControls.test.ts @@ -1,5 +1,7 @@ -import { StoryContextForEnhancers } from '@storybook/store'; +import { expect } from '@jest/globals'; import { logger } from '@storybook/client-logger'; +import { StoryContextForEnhancers } from '@storybook/csf'; + import { argTypesEnhancers } from './inferControls'; const getStoryContext = (overrides: any = {}): StoryContextForEnhancers => ({ diff --git a/lib/store/src/parameters.test.ts b/lib/store/src/parameters.test.ts index e0ed1bc03b70..29a15b48a503 100644 --- a/lib/store/src/parameters.test.ts +++ b/lib/store/src/parameters.test.ts @@ -1,3 +1,4 @@ +import { expect } from '@jest/globals'; import { combineParameters } from './parameters'; describe('client-api.parameters', () => { diff --git a/lib/store/src/storySort.test.ts b/lib/store/src/storySort.test.ts index 50976f126bd5..72ea64091f3b 100644 --- a/lib/store/src/storySort.test.ts +++ b/lib/store/src/storySort.test.ts @@ -1,25 +1,31 @@ +import { expect } from '@jest/globals'; +import { StoryId } from '@storybook/csf'; +import { StoryIndexEntry } from '@storybook/store'; + import { storySort } from './storySort'; describe('preview.storySort', () => { - const fixture = { - a: { title: 'a' }, - á: { title: 'á' }, - A: { title: 'A' }, - b: { title: 'b' }, - a_a: { title: 'a / a' }, - a_b: { title: 'a / b' }, - a_c: { title: 'a / c' }, - b_a_a: { title: 'b / a / a' }, - b_b: { title: 'b / b' }, - c: { title: 'c' }, - locale1: { title: 'Б' }, - locale2: { title: 'Г' }, - c__a: { title: 'c', name: 'a' }, - c_b__a: { title: 'c / b', name: 'a' }, - c_b__b: { title: 'c / b', name: 'b' }, - c_b__c: { title: 'c / b', name: 'c' }, - c__c: { title: 'c', name: 'c' }, - }; + const fixture: Record = Object.fromEntries( + Object.entries({ + a: { title: 'a' }, + á: { title: 'á' }, + A: { title: 'A' }, + b: { title: 'b' }, + a_a: { title: 'a / a' }, + a_b: { title: 'a / b' }, + a_c: { title: 'a / c' }, + b_a_a: { title: 'b / a / a' }, + b_b: { title: 'b / b' }, + c: { title: 'c' }, + locale1: { title: 'Б' }, + locale2: { title: 'Г' }, + c__a: { title: 'c', name: 'a' }, + c_b__a: { title: 'c / b', name: 'a' }, + c_b__b: { title: 'c / b', name: 'b' }, + c_b__c: { title: 'c / b', name: 'c' }, + c__c: { title: 'c', name: 'c' }, + }).map(([id, entry]) => [id, { type: 'story', name: 'name', ...entry, id, importPath: id }]) + ); it('uses configure order by default', () => { const sortFn = storySort(); From 77aa709233fb80e27607b3f6a98a550a6240d917 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Thu, 23 Jun 2022 09:57:40 +1000 Subject: [PATCH 5/7] External types care about `TArgs`, internal types do not --- renderers/react/src/testing/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/renderers/react/src/testing/index.ts b/renderers/react/src/testing/index.ts index 2575028335ee..7e24d0807796 100644 --- a/renderers/react/src/testing/index.ts +++ b/renderers/react/src/testing/index.ts @@ -83,7 +83,7 @@ export function composeStory( exportsName?: string ) { return originalComposeStory( - story, + story as ComposedStory, componentAnnotations, projectAnnotations, defaultProjectAnnotations, From 4f315c43ccdac1bdaea83266cb81257f225726bf Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Thu, 23 Jun 2022 11:09:42 +1000 Subject: [PATCH 6/7] Deepscan --- lib/preview-web/src/PreviewWeb.test.ts | 1 - lib/store/src/StoryIndexStore.ts | 1 - lib/store/src/csf/normalizeStory.ts | 1 - 3 files changed, 3 deletions(-) diff --git a/lib/preview-web/src/PreviewWeb.test.ts b/lib/preview-web/src/PreviewWeb.test.ts index bd6ecf9726bf..3820eb9ddf44 100644 --- a/lib/preview-web/src/PreviewWeb.test.ts +++ b/lib/preview-web/src/PreviewWeb.test.ts @@ -116,7 +116,6 @@ async function createAndRenderPreview({ return preview; } -const mockWebView = new WebView(); beforeEach(() => { document.location.search = ''; mockChannel.emit.mockClear(); diff --git a/lib/store/src/StoryIndexStore.ts b/lib/store/src/StoryIndexStore.ts index bb6f2fa07da6..342b74fa76fb 100644 --- a/lib/store/src/StoryIndexStore.ts +++ b/lib/store/src/StoryIndexStore.ts @@ -1,5 +1,4 @@ import dedent from 'ts-dedent'; -import { Channel } from '@storybook/addons'; import type { StoryId } from '@storybook/csf'; import memoize from 'memoizerific'; diff --git a/lib/store/src/csf/normalizeStory.ts b/lib/store/src/csf/normalizeStory.ts index 96ff55348e81..8db9c7b7872a 100644 --- a/lib/store/src/csf/normalizeStory.ts +++ b/lib/store/src/csf/normalizeStory.ts @@ -5,7 +5,6 @@ import type { StoryAnnotations, StoryFn, ArgTypes, - Args, } from '@storybook/csf'; import { storyNameFromExport, toId } from '@storybook/csf'; import dedent from 'ts-dedent'; From fe9af61a1a1cfa90137c23d1f3b821fcd7d3f4e9 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Thu, 23 Jun 2022 13:48:48 +1000 Subject: [PATCH 7/7] Undo change to `CSFExports` type --- lib/store/src/csf/testing-utils/index.ts | 7 ++++++- lib/store/src/csf/testing-utils/types.ts | 6 +----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/store/src/csf/testing-utils/index.ts b/lib/store/src/csf/testing-utils/index.ts index 6426f15d80a6..6fddcbce6f5e 100644 --- a/lib/store/src/csf/testing-utils/index.ts +++ b/lib/store/src/csf/testing-utils/index.ts @@ -124,7 +124,12 @@ export function composeStories( } const result = Object.assign(storiesMap, { - [exportsName]: composeStoryFn(story, meta, globalConfig, exportsName), + [exportsName]: composeStoryFn( + story as LegacyStoryAnnotationsOrFn, + meta, + globalConfig, + exportsName + ), }); return result; }, {}); diff --git a/lib/store/src/csf/testing-utils/types.ts b/lib/store/src/csf/testing-utils/types.ts index 747427ecb23f..498e091aedac 100644 --- a/lib/store/src/csf/testing-utils/types.ts +++ b/lib/store/src/csf/testing-utils/types.ts @@ -5,13 +5,9 @@ import type { ComponentAnnotations, Args, StoryContext, - LegacyAnnotatedStoryFn, } from '@storybook/csf'; -export type CSFExports = Record< - string, - LegacyAnnotatedStoryFn -> & { +export type CSFExports = { default: ComponentAnnotations; __esModule?: boolean; __namedExportsOrder?: string[];