From d1e9f2d0de042eb854b913c8232ec273e7a624ed Mon Sep 17 00:00:00 2001 From: Michael Shilman Date: Mon, 9 Jan 2023 12:18:55 +0800 Subject: [PATCH] Telemetry: Count play function usage --- .../src/utils/StoryIndexGenerator.ts | 1 + .../core-server/src/utils/summarizeIndex.ts | 11 +- code/lib/csf-tools/src/CsfFile.test.ts | 100 ++++++++++++++++++ code/lib/csf-tools/src/CsfFile.ts | 8 +- .../stories/component-play.stories.ts | 23 ++++ 5 files changed, 139 insertions(+), 4 deletions(-) create mode 100644 code/lib/store/template/stories/component-play.stories.ts diff --git a/code/lib/core-server/src/utils/StoryIndexGenerator.ts b/code/lib/core-server/src/utils/StoryIndexGenerator.ts index 9beeae4e9809..abc9ce18a85e 100644 --- a/code/lib/core-server/src/utils/StoryIndexGenerator.ts +++ b/code/lib/core-server/src/utils/StoryIndexGenerator.ts @@ -38,6 +38,7 @@ type SpecifierStoriesCache = Record; export const AUTODOCS_TAG = 'autodocs'; export const STORIES_MDX_TAG = 'stories-mdx'; +export const PLAY_FN_TAG = 'play-fn'; /** Was this docs entry generated by a .mdx file? (see discussion below) */ export function isMdxEntry({ tags }: DocsIndexEntry) { diff --git a/code/lib/core-server/src/utils/summarizeIndex.ts b/code/lib/core-server/src/utils/summarizeIndex.ts index 8cbed54f59b8..98eb65ba655b 100644 --- a/code/lib/core-server/src/utils/summarizeIndex.ts +++ b/code/lib/core-server/src/utils/summarizeIndex.ts @@ -1,6 +1,6 @@ import type { StoryIndex } from '@storybook/types'; -import { STORIES_MDX_TAG, isMdxEntry, AUTODOCS_TAG } from './StoryIndexGenerator'; +import { STORIES_MDX_TAG, isMdxEntry, AUTODOCS_TAG, PLAY_FN_TAG } from './StoryIndexGenerator'; const PAGE_REGEX = /(page|screen)/i; @@ -9,6 +9,7 @@ export const isPageStory = (storyId: string) => PAGE_REGEX.test(storyId); export function summarizeIndex(storyIndex: StoryIndex) { let storyCount = 0; let pageStoryCount = 0; + let playStoryCount = 0; let autodocsCount = 0; let storiesMdxCount = 0; let mdxCount = 0; @@ -18,12 +19,15 @@ export function summarizeIndex(storyIndex: StoryIndex) { if (isPageStory(entry.title)) { pageStoryCount += 1; } + if (entry.tags?.includes(PLAY_FN_TAG)) { + playStoryCount += 1; + } } else if (entry.type === 'docs') { if (isMdxEntry(entry)) { mdxCount += 1; - } else if (entry.tags.includes(STORIES_MDX_TAG)) { + } else if (entry.tags?.includes(STORIES_MDX_TAG)) { storiesMdxCount += 1; - } else if (entry.tags.includes(AUTODOCS_TAG)) { + } else if (entry.tags?.includes(AUTODOCS_TAG)) { autodocsCount += 1; } } @@ -31,6 +35,7 @@ export function summarizeIndex(storyIndex: StoryIndex) { return { storyCount, pageStoryCount, + playStoryCount, autodocsCount, storiesMdxCount, mdxCount, diff --git a/code/lib/csf-tools/src/CsfFile.test.ts b/code/lib/csf-tools/src/CsfFile.test.ts index 7667f4f62471..bc6b319ebcc8 100644 --- a/code/lib/csf-tools/src/CsfFile.test.ts +++ b/code/lib/csf-tools/src/CsfFile.test.ts @@ -866,4 +866,104 @@ describe('CsfFile', () => { ).toThrow('CSF: Expected tag to be string literal'); }); }); + + describe('play functions', () => { + it('story csf2', () => { + expect( + parse( + dedent` + export default { title: 'foo/bar', tags: ['X'] }; + export const A = () => {}; + A.play = () => {}; + A.tags = ['Y']; + ` + ) + ).toMatchInlineSnapshot(` + meta: + title: foo/bar + tags: + - X + stories: + - id: foo-bar--a + name: A + tags: + - 'Y' + - play-fn + `); + }); + + it('story csf3', () => { + expect( + parse( + dedent` + export default { title: 'foo/bar', tags: ['X'] }; + export const A = { + render: () => {}, + play: () => {}, + tags: ['Y'], + }; + ` + ) + ).toMatchInlineSnapshot(` + meta: + title: foo/bar + tags: + - X + stories: + - id: foo-bar--a + name: A + tags: + - 'Y' + - play-fn + `); + }); + + it('meta csf2', () => { + expect( + parse( + dedent` + export default { title: 'foo/bar', play: () => {}, tags: ['X'] }; + export const A = { + render: () => {}, + tags: ['Y'], + }; + ` + ) + ).toMatchInlineSnapshot(` + meta: + title: foo/bar + tags: + - X + - play-fn + stories: + - id: foo-bar--a + name: A + tags: + - 'Y' + `); + }); + + it('meta csf3', () => { + expect( + parse( + dedent` + export default { title: 'foo/bar', play: () => {}, tags: ['X'] }; + export const A = () => {}; + A.tags = ['Y']; + ` + ) + ).toMatchInlineSnapshot(` + meta: + title: foo/bar + tags: + - X + - play-fn + stories: + - id: foo-bar--a + name: A + tags: + - 'Y' + `); + }); + }); }); diff --git a/code/lib/csf-tools/src/CsfFile.ts b/code/lib/csf-tools/src/CsfFile.ts index 8478d7174d68..075aa83588b5 100644 --- a/code/lib/csf-tools/src/CsfFile.ts +++ b/code/lib/csf-tools/src/CsfFile.ts @@ -454,6 +454,9 @@ export class CsfFile { // default export can come at any point in the file, so we do this post processing last const entries = Object.entries(self._stories); self._meta.title = this._makeTitle(self._meta.title); + if (self._metaAnnotations.play) { + self._meta.tags = [...(self._meta.tags || []), 'play-fn']; + } self._stories = entries.reduce((acc, [key, story]) => { if (isExportStory(key, self._meta)) { const id = toId(self._meta.id || self._meta.title, storyNameFromExport(key)); @@ -466,13 +469,16 @@ export class CsfFile { parameters.docsOnly = true; } acc[key] = { ...story, id, parameters }; - const { tags } = self._storyAnnotations[key]; + const { tags, play } = self._storyAnnotations[key]; if (tags) { const node = t.isIdentifier(tags) ? findVarInitialization(tags.name, this._ast.program) : tags; acc[key].tags = parseTags(node); } + if (play) { + acc[key].tags = [...(acc[key].tags || []), 'play-fn']; + } } return acc; }, {} as Record); diff --git a/code/lib/store/template/stories/component-play.stories.ts b/code/lib/store/template/stories/component-play.stories.ts new file mode 100644 index 000000000000..f238e7b34f3f --- /dev/null +++ b/code/lib/store/template/stories/component-play.stories.ts @@ -0,0 +1,23 @@ +import { global as globalThis } from '@storybook/global'; +import type { PartialStoryFn, PlayFunctionContext, StoryContext } from '@storybook/types'; +import { within } from '@storybook/testing-library'; +import { expect } from '@storybook/jest'; + +export default { + component: globalThis.Components.Pre, + play: async ({ canvasElement, name }: PlayFunctionContext) => { + await expect(JSON.parse(within(canvasElement).getByTestId('pre').innerText)).toEqual({ + name, + }); + }, + // Render the story name into the Pre + decorators: [ + (storyFn: PartialStoryFn, context: StoryContext) => { + const { name } = context; + return storyFn({ args: { object: { name } } }); + }, + ], +}; + +export const StoryOne = {}; +export const StoryTwo = {};