Skip to content

Commit 1086453

Browse files
author
Jiří Fencl
committed
fix: 🐛 fix stale widget leak, prop-only re-render, and test expectation
- Unmount and clear lastStory when args.args.widget is falsy for the same story, preventing stale DOM/state leaks between Storybook loader calls - Fix hasSetState && !hasSetProps branch to trigger a lifecycle update/render when only props change (else if), so Storybook controls re-render correctly - Update test 'should not force widget.state to {}...' to reflect actual componentPlugin default (state is {} not undefined)
1 parent e098c92 commit 1086453

2 files changed

Lines changed: 59 additions & 1 deletion

File tree

packages/tool-storybook/src/__tests__/indexSpec.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,53 @@ describe('Merkur tool storybook', () => {
324324
expect(first.state).toEqual({ fromPreviousLoad: true });
325325
});
326326

327+
it('should call lifeCycle.update when only props change (hasSetState && !hasSetProps)', async () => {
328+
let loader = createWidgetLoader({ widgetProperties, render });
329+
330+
let { widget: first } = await loader(storyArgs);
331+
first.setProps = undefined; // force hasSetState && !hasSetProps branch
332+
const lifeCycleUpdate = jest.fn().mockResolvedValue(undefined);
333+
first.$in.component.lifeCycle.update = lifeCycleUpdate;
334+
render.mockClear();
335+
336+
storyArgs.args.widget = { props: { title: 'New' } }; // no 'state' key
337+
await loader(storyArgs);
338+
339+
expect(lifeCycleUpdate).toHaveBeenCalledWith(first);
340+
expect(first.props).toEqual({ title: 'New' });
341+
});
342+
343+
it('should call render when only props change and lifeCycle.update is unavailable (hasSetState && !hasSetProps)', async () => {
344+
let loader = createWidgetLoader({ widgetProperties, render });
345+
346+
let { widget: first } = await loader(storyArgs);
347+
first.setProps = undefined; // force hasSetState && !hasSetProps branch
348+
first.$in.component.lifeCycle.update = undefined;
349+
render.mockClear();
350+
351+
storyArgs.args.widget = { props: { title: 'New' } }; // no 'state' key
352+
await loader(storyArgs);
353+
354+
expect(render).toHaveBeenCalledWith(first);
355+
expect(first.props).toEqual({ title: 'New' });
356+
});
357+
358+
it('should not trigger a render when neither props nor state key is present (hasSetState && !hasSetProps)', async () => {
359+
let loader = createWidgetLoader({ widgetProperties, render });
360+
361+
let { widget: first } = await loader(storyArgs);
362+
first.setProps = undefined; // force hasSetState && !hasSetProps branch
363+
const lifeCycleUpdate = jest.fn().mockResolvedValue(undefined);
364+
first.$in.component.lifeCycle.update = lifeCycleUpdate;
365+
render.mockClear();
366+
367+
storyArgs.args.widget = {}; // no 'state' key, no 'props' key
368+
await loader(storyArgs);
369+
370+
expect(lifeCycleUpdate).not.toHaveBeenCalled();
371+
expect(render).not.toHaveBeenCalled();
372+
});
373+
327374
it('should not update props when props key is absent (hasSetState && !hasSetProps)', async () => {
328375
let loader = createWidgetLoader({ widgetProperties, render });
329376

packages/tool-storybook/src/index.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ function createWidgetLoader({ render, widgetProperties }) {
2222
}
2323

2424
if (!args?.args?.widget) {
25+
if (lastStory.widget && lastStory.name === args.story) {
26+
await lastStory.widget.unmount();
27+
lastStory = {};
28+
}
2529
return { widget: null };
2630
}
2731

@@ -43,13 +47,20 @@ function createWidgetLoader({ render, widgetProperties }) {
4347
const hasSetState = typeof widget.setState === 'function';
4448
const hasSetProps = typeof widget.setProps === 'function';
4549
if (hasSetState && !hasSetProps) {
46-
// Use setState as the single source of truth for triggering an update.
4750
if (hasNextProps) {
4851
widget.props = nextProps;
4952
}
5053
if (hasNextState) {
5154
widget.state = {};
5255
await widget.setState(nextState);
56+
} else if (hasNextProps) {
57+
// When only props change, manually trigger a lifecycle update/render so
58+
// that Storybook controls that modify props still cause a re-render.
59+
if (lifeCycle && typeof lifeCycle.update === 'function') {
60+
await lifeCycle.update(widget);
61+
} else {
62+
await render(widget);
63+
}
5364
}
5465
return { widget };
5566
}

0 commit comments

Comments
 (0)