Skip to content

Commit

Permalink
Merge pull request #28353 from storybookjs/kasper/context-prop
Browse files Browse the repository at this point in the history
Core: Add context as a property of the context (self-referencing)
  • Loading branch information
kasperpeulen authored Jun 28, 2024
2 parents 5e33c61 + ccabc98 commit 6791856
Show file tree
Hide file tree
Showing 37 changed files with 350 additions and 233 deletions.
10 changes: 7 additions & 3 deletions code/addons/interactions/src/preview.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import type { PlayFunction, PlayFunctionContext, StepLabel } from '@storybook/types';
import type { PlayFunction, StepLabel, StoryContext } from '@storybook/types';
import { instrument } from '@storybook/instrumenter';

export const { step: runStep } = instrument(
{
step: (label: StepLabel, play: PlayFunction, context: PlayFunctionContext<any>) =>
play(context),
// It seems like the label is unused, but the instrumenter has access to it
// The context will be bounded later in StoryRender, so that the user can write just:
// await step("label", (context) => {
// // labeled step
// });
step: (label: StepLabel, play: PlayFunction, context: StoryContext) => play(context),
},
{ intercept: true }
);
Expand Down
2 changes: 1 addition & 1 deletion code/addons/links/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
"prep": "node --loader ../../../scripts/node_modules/esbuild-register/loader.js -r ../../../scripts/node_modules/esbuild-register/register.js ../../../scripts/prepare/addon-bundle.ts"
},
"dependencies": {
"@storybook/csf": "^0.1.8",
"@storybook/csf": "0.1.10--canary.d841bb4.0",
"@storybook/global": "^5.0.0",
"ts-dedent": "^2.0.0"
},
Expand Down
4 changes: 4 additions & 0 deletions code/frameworks/angular/src/client/decorators.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,12 @@ const defaultContext: Addon_StoryContext<AngularRenderer> = {
viewMode: 'story',
abortSignal: undefined,
canvasElement: undefined,
step: undefined,
context: undefined,
};

defaultContext.context = defaultContext;

class MockModule {}
class MockModuleTwo {}
class MockService {}
Expand Down
2 changes: 1 addition & 1 deletion code/lib/codemod/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
"@babel/core": "^7.24.4",
"@babel/preset-env": "^7.24.4",
"@babel/types": "^7.24.0",
"@storybook/csf": "^0.1.8",
"@storybook/csf": "0.1.10--canary.d841bb4.0",
"@storybook/csf-tools": "workspace:*",
"@storybook/node-logger": "workspace:*",
"@storybook/types": "workspace:*",
Expand Down
2 changes: 1 addition & 1 deletion code/lib/core-events/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
"prep": "node --loader ../../../scripts/node_modules/esbuild-register/loader.js -r ../../../scripts/node_modules/esbuild-register/register.js ../../../scripts/prepare/bundle.ts"
},
"dependencies": {
"@storybook/csf": "^0.1.8",
"@storybook/csf": "0.1.10--canary.d841bb4.0",
"ts-dedent": "^2.0.0"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion code/lib/core-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
"@storybook/channels": "workspace:*",
"@storybook/core-common": "workspace:*",
"@storybook/core-events": "workspace:*",
"@storybook/csf": "^0.1.8",
"@storybook/csf": "0.1.10--canary.d841bb4.0",
"@storybook/csf-tools": "workspace:*",
"@storybook/docs-mdx": "3.1.0-next.0",
"@storybook/global": "^5.0.0",
Expand Down
2 changes: 1 addition & 1 deletion code/lib/csf-tools/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"@babel/parser": "^7.24.4",
"@babel/traverse": "^7.24.1",
"@babel/types": "^7.24.0",
"@storybook/csf": "^0.1.8",
"@storybook/csf": "0.1.10--canary.d841bb4.0",
"@storybook/types": "workspace:*",
"fs-extra": "^11.1.0",
"recast": "^0.23.5",
Expand Down
21 changes: 21 additions & 0 deletions code/lib/instrumenter/src/instrumenter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,27 @@ describe('Instrumenter', () => {
);
});

it('handles circular references', () => {
const { fn } = instrument({ fn: (...args: any) => {} });
const obj = { key: 'value', obj: {}, array: [] as any[] };
obj.obj = obj;
obj.array = [obj];

expect(() => fn(obj)).not.toThrow();

expect(callSpy.mock.calls[0][0].args).toMatchInlineSnapshot(`
[
{
"array": [
"[Circular]",
],
"key": "value",
"obj": "[Circular]",
},
]
`);
});

it('provides metadata about the call in the event', () => {
const { obj } = instrument({ obj: { fn: () => {} } });
obj.fn();
Expand Down
21 changes: 16 additions & 5 deletions code/lib/instrumenter/src/instrumenter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -411,13 +411,21 @@ export class Instrumenter {
invoke(fn: Function, object: Record<string, unknown>, call: Call, options: Options) {
const { callRefsByResult, renderPhase } = this.getState(call.storyId);

// Map complex values to a JSON-serializable representation.
const serializeValues = (value: any): any => {
// TODO This function should not needed anymore, as the channel already serializes values with telejson
// Possibly we need to add HTMLElement support to telejson though
// Keeping this function here, as removing it means we need to refactor the deserializing that happens in addon-interactions
const maximumDepth = 25; // mimicks the max depth of telejson
const serializeValues = (value: any, depth: number, seen: unknown[]): any => {
if (seen.includes(value)) return '[Circular]';
seen = [...seen, value];

if (depth > maximumDepth) return '...';

if (callRefsByResult.has(value)) {
return callRefsByResult.get(value);
}
if (value instanceof Array) {
return value.map(serializeValues);
return value.map((it) => serializeValues(it, ++depth, seen));
}
if (value instanceof Date) {
return { __date__: { value: value.toISOString() } };
Expand Down Expand Up @@ -452,13 +460,16 @@ export class Instrumenter {
}
if (Object.prototype.toString.call(value) === '[object Object]') {
return Object.fromEntries(
Object.entries(value).map(([key, val]) => [key, serializeValues(val)])
Object.entries(value).map(([key, val]) => [key, serializeValues(val, ++depth, seen)])
);
}
return value;
};

const info: Call = { ...call, args: call.args.map(serializeValues) };
const info: Call = {
...call,
args: call.args.map((arg) => serializeValues(arg, 0, [])),
};

// Mark any ancestor calls as "chained upon" so we won't attempt to defer it later.
call.path.forEach((ref: any) => {
Expand Down
2 changes: 1 addition & 1 deletion code/lib/manager-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"@storybook/channels": "workspace:*",
"@storybook/client-logger": "workspace:*",
"@storybook/core-events": "workspace:*",
"@storybook/csf": "^0.1.8",
"@storybook/csf": "0.1.10--canary.d841bb4.0",
"@storybook/global": "^5.0.0",
"@storybook/icons": "^1.2.5",
"@storybook/router": "workspace:*",
Expand Down
2 changes: 1 addition & 1 deletion code/lib/preview-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"@storybook/channels": "workspace:*",
"@storybook/client-logger": "workspace:*",
"@storybook/core-events": "workspace:*",
"@storybook/csf": "^0.1.8",
"@storybook/csf": "0.1.10--canary.d841bb4.0",
"@storybook/global": "^5.0.0",
"@storybook/types": "workspace:*",
"@types/qs": "^6.9.5",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,15 @@ import {
STORY_THREW_EXCEPTION,
} from '@storybook/core-events';

import type { ModuleImportFn, StoryIndex, TeardownRenderToCanvas } from '@storybook/types';
import type {
ModuleImportFn,
ProjectAnnotations,
Renderer,
StoryIndex,
TeardownRenderToCanvas,
} from '@storybook/types';
import type { RenderPhase } from './render/StoryRender';
import { composeConfigs } from '../store';

export const componentOneExports = {
default: {
Expand Down Expand Up @@ -65,14 +72,18 @@ export const docsRenderer = {
unmount: vi.fn(),
};
export const teardownrenderToCanvas: Mock<[TeardownRenderToCanvas]> = vi.fn();
export const projectAnnotations = {
const rawProjectAnnotations = {
initialGlobals: { a: 'b' },
globalTypes: {},
decorators: [vi.fn((s) => s())],
render: vi.fn(),
renderToCanvas: vi.fn().mockReturnValue(teardownrenderToCanvas),
parameters: { docs: { renderer: () => docsRenderer } },
};
export const projectAnnotations = composeConfigs([
rawProjectAnnotations,
]) as ProjectAnnotations<Renderer> & typeof rawProjectAnnotations;

export const getProjectAnnotations = vi.fn(() => projectAnnotations as any);

export const storyIndex: StoryIndex = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -949,7 +949,7 @@ describe('PreviewWeb', () => {
forceRemount: true,
storyContext: expect.objectContaining({
loaded: { l: 8 }, // This is the value returned by the *first* loader call
args: { foo: 'a', new: 'arg', one: 'mapped-1' },
args: { foo: 'a', one: 'mapped-1' },
}),
}),
'story-element'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import type {
ModuleExport,
ModuleExports,
PreparedStory,
StoryContextForLoaders,
StoryId,
StoryName,
ResolvedModuleExportType,
Expand Down Expand Up @@ -232,8 +231,9 @@ export class DocsContext<TRenderer extends Renderer> implements DocsContextProps
getStoryContext = (story: PreparedStory<TRenderer>) => {
return {
...this.store.getStoryContext(story),
loaded: {},
viewMode: 'docs',
} as StoryContextForLoaders<TRenderer>;
};
};

loadStory = (id: StoryId) => {
Expand Down
53 changes: 24 additions & 29 deletions code/lib/preview-api/src/modules/preview-web/render/StoryRender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@ import type {
PreparedStory,
TeardownRenderToCanvas,
StoryContext,
StoryContextForLoaders,
StoryId,
StoryRenderOptions,
ViewMode,
} from '@storybook/types';
import type { Channel } from '@storybook/channels';
import {
Expand Down Expand Up @@ -71,7 +69,7 @@ export class StoryRender<TRenderer extends Renderer> implements Render<TRenderer
private renderToScreen: RenderToCanvas<TRenderer>,
private callbacks: RenderContextCallbacks<TRenderer>,
public id: StoryId,
public viewMode: ViewMode,
public viewMode: StoryContext['viewMode'],
public renderOptions: StoryRenderOptions = { autoplay: true, forceInitialArgs: false },
story?: PreparedStory<TRenderer>
) {
Expand Down Expand Up @@ -165,6 +163,7 @@ export class StoryRender<TRenderer extends Renderer> implements Render<TRenderer
applyBeforeEach,
unboundStoryFn,
playFunction,
runStep,
} = story;

if (forceRemount && !initial) {
Expand All @@ -180,33 +179,17 @@ export class StoryRender<TRenderer extends Renderer> implements Render<TRenderer
const abortSignal = (this.abortController as AbortController).signal;

try {
let loadedContext: Awaited<ReturnType<typeof applyLoaders>>;
await this.runPhase(abortSignal, 'loading', async () => {
loadedContext = await applyLoaders({
...this.storyContext(),
viewMode: this.viewMode,
// TODO add this to CSF
canvasElement,
} as unknown as StoryContextForLoaders<TRenderer>);
});
if (abortSignal.aborted) return;

const renderStoryContext: StoryContext<TRenderer> = {
...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
const context: StoryContext<TRenderer> = {
...this.storyContext(),
viewMode: this.viewMode,
abortSignal,
// We should consider parameterizing the story types with TRenderer['canvasElement'] in the future
canvasElement: canvasElement as any,
canvasElement,
loaded: {},
step: (label, play) => runStep(label, play, context),
context: null!,
};

await this.runPhase(abortSignal, 'beforeEach', async () => {
const cleanupCallbacks = await applyBeforeEach(renderStoryContext);
this.store.addCleanupCallbacks(story, cleanupCallbacks);
});

if (abortSignal.aborted) return;
context.context = context;

const renderContext: RenderContext<TRenderer> = {
componentId,
Expand All @@ -226,10 +209,22 @@ export class StoryRender<TRenderer extends Renderer> implements Render<TRenderer
return this.callbacks.showException(error);
},
forceRemount: forceRemount || this.notYetRendered,
storyContext: renderStoryContext,
storyFn: () => unboundStoryFn(renderStoryContext),
storyContext: context,
storyFn: () => unboundStoryFn(context),
unboundStoryFn,
};
await this.runPhase(abortSignal, 'loading', async () => {
context.loaded = await applyLoaders(context);
});

if (abortSignal.aborted) return;

await this.runPhase(abortSignal, 'beforeEach', async () => {
const cleanupCallbacks = await applyBeforeEach(context);
this.store.addCleanupCallbacks(story, cleanupCallbacks);
});

if (abortSignal.aborted) return;

await this.runPhase(abortSignal, 'rendering', async () => {
const teardown = await this.renderToScreen(renderContext, canvasElement);
Expand All @@ -253,7 +248,7 @@ export class StoryRender<TRenderer extends Renderer> implements Render<TRenderer
this.disableKeyListeners = true;
try {
await this.runPhase(abortSignal, 'playing', async () => {
await playFunction(renderContext.storyContext);
await playFunction(context);
});
if (!ignoreUnhandledErrors && unhandledErrors.size > 0) {
await this.runPhase(abortSignal, 'errored');
Expand Down
18 changes: 12 additions & 6 deletions code/lib/preview-api/src/modules/store/StoryStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { prepareStory } from './csf/prepareStory';
import { processCSFFile } from './csf/processCSFFile';
import { StoryStore } from './StoryStore';
import type { HooksContext } from './hooks';
import { composeConfigs } from './csf/composeConfigs';

// Spy on prepareStory/processCSFFile
vi.mock('./csf/prepareStory', async (importOriginal) => {
Expand Down Expand Up @@ -41,12 +42,14 @@ const importFn = vi.fn(async (path) => {
return path === './src/ComponentOne.stories.js' ? componentOneExports : componentTwoExports;
});

const projectAnnotations: ProjectAnnotations<any> = {
globals: { a: 'b' },
globalTypes: { a: { type: 'string' } },
argTypes: { a: { type: 'string' } },
render: vi.fn(),
};
const projectAnnotations: ProjectAnnotations<any> = composeConfigs([
{
globals: { a: 'b' },
globalTypes: { a: { type: 'string' } },
argTypes: { a: { type: 'string' } },
render: vi.fn(),
},
]);

const storyIndex: StoryIndex = {
v: 5,
Expand Down Expand Up @@ -660,6 +663,7 @@ describe('StoryStore', () => {
"fileName": "./src/ComponentOne.stories.js",
},
"playFunction": undefined,
"runStep": [Function],
"story": "A",
"storyFn": [Function],
"subcomponents": undefined,
Expand Down Expand Up @@ -707,6 +711,7 @@ describe('StoryStore', () => {
"fileName": "./src/ComponentOne.stories.js",
},
"playFunction": undefined,
"runStep": [Function],
"story": "B",
"storyFn": [Function],
"subcomponents": undefined,
Expand Down Expand Up @@ -754,6 +759,7 @@ describe('StoryStore', () => {
"fileName": "./src/ComponentTwo.stories.js",
},
"playFunction": undefined,
"runStep": [Function],
"story": "C",
"storyFn": [Function],
"subcomponents": undefined,
Expand Down
Loading

0 comments on commit 6791856

Please sign in to comment.