Skip to content

Commit 3bec5ba

Browse files
authored
Merge pull request #482 from objectstack-ai/copilot/fix-storybook-errors-again
2 parents 7404210 + 23015d1 commit 3bec5ba

3 files changed

Lines changed: 176 additions & 2 deletions

File tree

.storybook/preview.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
import type { Preview, LoaderFunction } from '@storybook/react-vite'
1+
import React from 'react';
2+
import type { Preview, LoaderFunction, Decorator } from '@storybook/react-vite'
23
import { initialize, mswLoader } from 'msw-storybook-addon';
34
import { handlers } from './mocks';
45
import { startMockServer, getHandlers } from './msw-browser';
56
import '../packages/components/src/index.css';
67

78
import { ComponentRegistry } from '@object-ui/core';
89
import * as components from '../packages/components/src/index';
10+
import { SchemaRendererProvider } from '@object-ui/react';
911

1012
// Initialize MSW
1113
initialize({
@@ -74,7 +76,19 @@ import '@object-ui/plugin-view';
7476
import '@object-ui/layout';
7577
import '@object-ui/fields';
7678

79+
// Global decorator: wrap every story in SchemaRendererProvider so that
80+
// plugin components calling useSchemaContext() never throw.
81+
// Stories that need a specific dataSource can still wrap with their own provider
82+
// (the innermost provider wins via React context).
83+
const withSchemaProvider: Decorator = (Story) =>
84+
React.createElement(
85+
SchemaRendererProvider,
86+
{ dataSource: {} },
87+
React.createElement(Story)
88+
);
89+
7790
const preview: Preview = {
91+
decorators: [withSchemaProvider],
7892
loaders: [objectStackLoader],
7993
parameters: {
8094
msw: {

.storybook/test-runner.cjs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
* Storybook test-runner configuration.
33
*
44
* Includes:
5-
* - preVisit: injects __test global for test-runner compatibility
5+
* - preVisit: injects __test global and console error collector
66
* - postVisit: captures DOM snapshots for visual regression testing (§1.4)
7+
* and asserts no React/provider errors were logged
78
*
89
* @type {import('@storybook/test-runner').TestRunnerConfig}
910
*/
@@ -17,9 +18,36 @@ module.exports = {
1718
await page.evaluate(() => {
1819
window.__test = () => {};
1920
});
21+
22+
// Collect console.error calls so postVisit can assert none were critical.
23+
// This detects missing-provider errors such as
24+
// "useSchemaContext must be used within a SchemaRendererProvider".
25+
await page.evaluate(() => {
26+
window.__collectedErrors = [];
27+
const origError = console.error;
28+
console.error = (...args) => {
29+
window.__collectedErrors.push(args.map(String).join(' '));
30+
origError.apply(console, args);
31+
};
32+
});
2033
},
2134

2235
async postVisit(page, context) {
36+
// Assert no critical React/provider errors were logged during render
37+
const errors = await page.evaluate(() => window.__collectedErrors || []);
38+
const critical = errors.filter(
39+
(msg) =>
40+
msg.includes('must be used within') ||
41+
msg.includes('Uncaught Error') ||
42+
msg.includes('The above error occurred in')
43+
);
44+
if (critical.length > 0) {
45+
throw new Error(
46+
`Story "${context.id}" logged ${critical.length} critical error(s):\n` +
47+
critical.map((e) => ` • ${e.slice(0, 200)}`).join('\n')
48+
);
49+
}
50+
2351
// Capture a DOM snapshot for each story as a lightweight visual regression check.
2452
// This detects structural changes (added/removed elements, changed text, class changes)
2553
// without the overhead of pixel-based screenshot comparison.
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
/**
10+
* Smoke tests for SchemaRendererProvider.
11+
*
12+
* These tests ensure that every registered plugin component that calls
13+
* useSchemaContext() can render without throwing when wrapped in a
14+
* SchemaRendererProvider. This catches the class of errors reported in
15+
* Storybook ("useSchemaContext must be used within a SchemaRendererProvider").
16+
*/
17+
18+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
19+
import { render, screen } from '@testing-library/react';
20+
import React from 'react';
21+
import { ComponentRegistry } from '@object-ui/core';
22+
import { SchemaRenderer } from '../SchemaRenderer';
23+
import { SchemaRendererProvider, useSchemaContext } from '../context/SchemaRendererContext';
24+
25+
// Suppress console.error from React error boundary during tests
26+
const originalConsoleError = console.error;
27+
beforeEach(() => {
28+
console.error = vi.fn();
29+
});
30+
afterEach(() => {
31+
console.error = originalConsoleError;
32+
});
33+
34+
// Helper: a component that calls useSchemaContext (mimics plugin pattern)
35+
const ContextConsumer: React.FC<any> = ({ schema }) => {
36+
const { dataSource } = useSchemaContext();
37+
return <div data-testid="ctx-consumer">dataSource: {JSON.stringify(dataSource)}</div>;
38+
};
39+
40+
describe('useSchemaContext provider requirement', () => {
41+
it('should throw when used outside SchemaRendererProvider', () => {
42+
expect(() => render(<ContextConsumer schema={{}} />)).toThrow(
43+
'useSchemaContext must be used within a SchemaRendererProvider'
44+
);
45+
});
46+
47+
it('should not throw when used inside SchemaRendererProvider', () => {
48+
render(
49+
<SchemaRendererProvider dataSource={{ test: true }}>
50+
<ContextConsumer schema={{}} />
51+
</SchemaRendererProvider>
52+
);
53+
expect(screen.getByTestId('ctx-consumer')).toHaveTextContent('dataSource: {"test":true}');
54+
});
55+
56+
it('should fall back to empty dataSource when provider has empty object', () => {
57+
render(
58+
<SchemaRendererProvider dataSource={{}}>
59+
<ContextConsumer schema={{}} />
60+
</SchemaRendererProvider>
61+
);
62+
expect(screen.getByTestId('ctx-consumer')).toHaveTextContent('dataSource: {}');
63+
});
64+
});
65+
66+
describe('SchemaRenderer + SchemaRendererProvider integration', () => {
67+
beforeEach(() => {
68+
ComponentRegistry.register('test-ctx-consumer', ContextConsumer);
69+
});
70+
71+
afterEach(() => {
72+
ComponentRegistry.unregister?.('test-ctx-consumer');
73+
});
74+
75+
it('should render a component that calls useSchemaContext without error when provider wraps the tree', () => {
76+
render(
77+
<SchemaRendererProvider dataSource={{ foo: 'bar' }}>
78+
<SchemaRenderer schema={{ type: 'test-ctx-consumer' }} />
79+
</SchemaRendererProvider>
80+
);
81+
expect(screen.getByTestId('ctx-consumer')).toHaveTextContent('dataSource: {"foo":"bar"}');
82+
});
83+
84+
it('should show error boundary fallback (not crash) when provider is missing', () => {
85+
// Without a provider, the SchemaErrorBoundary catches the throw
86+
render(
87+
<SchemaRenderer schema={{ type: 'test-ctx-consumer' }} />
88+
);
89+
expect(screen.getByRole('alert')).toBeInTheDocument();
90+
expect(screen.getByText(/failed to render/i)).toBeInTheDocument();
91+
});
92+
});
93+
94+
describe('Plugin component types render inside provider', () => {
95+
// This test ensures all registered plugin types that use useSchemaContext
96+
// can at least mount without throwing inside a SchemaRendererProvider.
97+
const pluginTypes = [
98+
'kanban',
99+
'object-kanban',
100+
'timeline',
101+
'object-timeline',
102+
'object-grid',
103+
'object-calendar',
104+
'object-map',
105+
'chart',
106+
'object-gantt',
107+
];
108+
109+
for (const type of pluginTypes) {
110+
it(`type="${type}" should not throw inside SchemaRendererProvider`, () => {
111+
const component = ComponentRegistry.get(type);
112+
if (!component) {
113+
// Component not registered in test environment — skip
114+
return;
115+
}
116+
117+
// Render via SchemaRenderer inside provider
118+
const { container } = render(
119+
<SchemaRendererProvider dataSource={{}}>
120+
<SchemaRenderer schema={{ type }} />
121+
</SchemaRendererProvider>
122+
);
123+
124+
// Should NOT show the error boundary alert
125+
const alerts = container.querySelectorAll('[role="alert"]');
126+
const providerErrors = Array.from(alerts).filter((el) =>
127+
el.textContent?.includes('useSchemaContext must be used within')
128+
);
129+
expect(providerErrors).toHaveLength(0);
130+
});
131+
}
132+
});

0 commit comments

Comments
 (0)