Skip to content

Commit 9d9bb69

Browse files
author
Jiří Fencl
committed
fix: 🐛 Fix createVanillaRenderer ViewComponent function check
Fixes situation when ViewComponent is function then the args.components are never checked, add tests
1 parent 7f848e0 commit 9d9bb69

3 files changed

Lines changed: 205 additions & 6 deletions

File tree

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
const defaultConfig = require('../../jest.config.js');
22

3-
module.exports = { ...defaultConfig };
3+
module.exports = { ...defaultConfig, testEnvironment: 'jsdom' };

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

Lines changed: 198 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createWidgetLoader } from '../index';
1+
import { createWidgetLoader, createVanillaRenderer } from '../index';
22

33
import { getMerkur, createMerkurWidget } from '@merkur/core';
44
import { componentPlugin } from '@merkur/plugin-component';
@@ -79,4 +79,201 @@ describe('Merkur tool storybook', () => {
7979
expect(typeof widget.customFunction === 'function').toBeTruthy();
8080
});
8181
});
82+
83+
describe('createVanillaRenderer method', () => {
84+
let mockWidget;
85+
let mockViewFunction;
86+
let mockBindEvents;
87+
88+
beforeEach(() => {
89+
mockWidget = {
90+
state: { count: 0 },
91+
props: { title: 'Test' },
92+
setState: jest.fn(),
93+
};
94+
mockViewFunction = jest.fn(
95+
(widget) => `<div>Count: ${widget.state.count}</div>`,
96+
);
97+
mockBindEvents = jest.fn();
98+
});
99+
100+
it('should render empty widget when no widget is provided', () => {
101+
const renderer = createVanillaRenderer({
102+
ViewComponent: mockViewFunction,
103+
});
104+
105+
const container = renderer.render({}, { loaded: {} });
106+
107+
expect(container.tagName).toBe('DIV');
108+
expect(container.textContent).toBe('Loading widget...');
109+
});
110+
111+
it('should render widget with ViewComponent as function', () => {
112+
const renderer = createVanillaRenderer({
113+
ViewComponent: mockViewFunction,
114+
});
115+
116+
const container = renderer.render({}, { loaded: { widget: mockWidget } });
117+
118+
expect(container.tagName).toBe('DIV');
119+
expect(container.innerHTML).toBe('<div>Count: 0</div>');
120+
expect(mockViewFunction).toHaveBeenCalledWith(mockWidget);
121+
});
122+
123+
it('should render widget with ViewComponent as object with default', () => {
124+
const defaultView = jest.fn(() => '<div>Default view</div>');
125+
const renderer = createVanillaRenderer({
126+
ViewComponent: { default: defaultView },
127+
});
128+
129+
const container = renderer.render({}, { loaded: { widget: mockWidget } });
130+
131+
expect(container.innerHTML).toBe('<div>Default view</div>');
132+
expect(defaultView).toHaveBeenCalledWith(mockWidget);
133+
});
134+
135+
it('should use args.viewComponent to select from ViewComponent object', () => {
136+
const customView = jest.fn(() => '<div>Custom view</div>');
137+
const renderer = createVanillaRenderer({
138+
ViewComponent: {
139+
default: mockViewFunction,
140+
customView,
141+
},
142+
});
143+
144+
const container = renderer.render(
145+
{ viewComponent: 'customView' },
146+
{ loaded: { widget: mockWidget } },
147+
);
148+
149+
expect(container.innerHTML).toBe('<div>Custom view</div>');
150+
expect(customView).toHaveBeenCalledWith(mockWidget);
151+
expect(mockViewFunction).not.toHaveBeenCalled();
152+
});
153+
154+
it('should use args.component as string to select from ViewComponent object', () => {
155+
const componentView = jest.fn(() => '<div>Component view</div>');
156+
const renderer = createVanillaRenderer({
157+
ViewComponent: {
158+
default: mockViewFunction,
159+
componentView,
160+
},
161+
});
162+
163+
const container = renderer.render(
164+
{ component: 'componentView' },
165+
{ loaded: { widget: mockWidget } },
166+
);
167+
168+
expect(container.innerHTML).toBe('<div>Component view</div>');
169+
expect(componentView).toHaveBeenCalledWith(mockWidget);
170+
});
171+
172+
it('should use args.component as function', () => {
173+
const inlineComponent = jest.fn(() => '<div>Inline component</div>');
174+
const renderer = createVanillaRenderer({
175+
ViewComponent: mockViewFunction,
176+
});
177+
178+
const container = renderer.render(
179+
{ component: inlineComponent },
180+
{ loaded: { widget: mockWidget } },
181+
);
182+
183+
expect(container.innerHTML).toBe('<div>Inline component</div>');
184+
expect(inlineComponent).toHaveBeenCalledWith(mockWidget);
185+
expect(mockViewFunction).not.toHaveBeenCalled();
186+
});
187+
188+
it('should call bindEvents if provided in options', () => {
189+
const renderer = createVanillaRenderer({
190+
ViewComponent: mockViewFunction,
191+
bindEvents: mockBindEvents,
192+
});
193+
194+
const container = renderer.render({}, { loaded: { widget: mockWidget } });
195+
196+
expect(mockBindEvents).toHaveBeenCalledWith(container, mockWidget);
197+
});
198+
199+
it('should call widget.View.bindEvents if no bindEvents provided', () => {
200+
const widgetBindEvents = jest.fn();
201+
mockWidget.View = { bindEvents: widgetBindEvents };
202+
203+
const renderer = createVanillaRenderer({
204+
ViewComponent: mockViewFunction,
205+
});
206+
207+
const container = renderer.render({}, { loaded: { widget: mockWidget } });
208+
209+
expect(widgetBindEvents).toHaveBeenCalledWith(container, mockWidget);
210+
});
211+
212+
it('should prioritize options.bindEvents over widget.View.bindEvents', () => {
213+
const widgetBindEvents = jest.fn();
214+
mockWidget.View = { bindEvents: widgetBindEvents };
215+
216+
const renderer = createVanillaRenderer({
217+
ViewComponent: mockViewFunction,
218+
bindEvents: mockBindEvents,
219+
});
220+
221+
const container = renderer.render({}, { loaded: { widget: mockWidget } });
222+
223+
expect(mockBindEvents).toHaveBeenCalledWith(container, mockWidget);
224+
expect(widgetBindEvents).not.toHaveBeenCalled();
225+
});
226+
227+
it('should re-render widget when update is called', () => {
228+
const renderer = createVanillaRenderer({
229+
ViewComponent: mockViewFunction,
230+
bindEvents: mockBindEvents,
231+
});
232+
233+
const container = renderer.render({}, { loaded: { widget: mockWidget } });
234+
235+
// Clear previous calls
236+
mockViewFunction.mockClear();
237+
mockBindEvents.mockClear();
238+
239+
// Update widget state
240+
mockWidget.state.count = 5;
241+
242+
// Call update
243+
renderer.update();
244+
245+
expect(container.innerHTML).toBe('<div>Count: 5</div>');
246+
expect(mockViewFunction).toHaveBeenCalledWith(mockWidget);
247+
expect(mockBindEvents).toHaveBeenCalledWith(container, mockWidget);
248+
});
249+
250+
it('should handle update gracefully when called before render', () => {
251+
const renderer = createVanillaRenderer({
252+
ViewComponent: mockViewFunction,
253+
});
254+
255+
expect(() => renderer.update()).not.toThrow();
256+
expect(mockViewFunction).not.toHaveBeenCalled();
257+
});
258+
259+
it('should preserve references across multiple updates', () => {
260+
const renderer = createVanillaRenderer({
261+
ViewComponent: mockViewFunction,
262+
});
263+
264+
const container = renderer.render({}, { loaded: { widget: mockWidget } });
265+
266+
mockWidget.state.count = 1;
267+
renderer.update();
268+
expect(container.innerHTML).toBe('<div>Count: 1</div>');
269+
270+
mockWidget.state.count = 2;
271+
renderer.update();
272+
expect(container.innerHTML).toBe('<div>Count: 2</div>');
273+
274+
mockWidget.state.count = 3;
275+
renderer.update();
276+
expect(container.innerHTML).toBe('<div>Count: 3</div>');
277+
});
278+
});
82279
});

packages/tool-storybook/src/index.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,7 @@ function createVanillaRenderer(options) {
9999
let currentViewFunction = null;
100100

101101
function getViewFunction(args, ViewComponent) {
102-
if (typeof ViewComponent === 'function') {
103-
return ViewComponent;
104-
}
105-
102+
// Check args first to allow overriding
106103
if (args.viewComponent && ViewComponent[args.viewComponent]) {
107104
return ViewComponent[args.viewComponent];
108105
}
@@ -113,6 +110,11 @@ function createVanillaRenderer(options) {
113110
: ViewComponent[args.component];
114111
}
115112

113+
// Fall back to ViewComponent
114+
if (typeof ViewComponent === 'function') {
115+
return ViewComponent;
116+
}
117+
116118
return ViewComponent.default || ViewComponent;
117119
}
118120

0 commit comments

Comments
 (0)