Skip to content

Commit bd77e11

Browse files
Copilothotlong
andcommitted
fix: wrap InteractiveDemo with SchemaRendererContext, harden array guards in components, add docs e2e smoke tests
- InteractiveDemo now provides SchemaRendererContext so plugin demos (grid, kanban, view, etc.) no longer crash with "useSchemaContext must be used within a SchemaRendererProvider" - Add Array.isArray guards in resizable, list, tree-view, and table components to prevent ".map is not a function" errors during SSG - Add e2e/docs-smoke.spec.ts with Playwright tests covering 20 representative doc pages across components, fields, core, plugins, and blocks categories Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 883da28 commit bd77e11

6 files changed

Lines changed: 139 additions & 32 deletions

File tree

apps/site/app/components/InteractiveDemo.tsx

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client';
22

3-
import React from 'react';
4-
import { SchemaRenderer } from '@object-ui/react';
3+
import React, { useMemo } from 'react';
4+
import { SchemaRenderer, SchemaRendererContext } from '@object-ui/react';
55
import { SidebarProvider } from '@object-ui/components';
66
import type { SchemaNode } from '@object-ui/core';
77
import { Tabs, Tab } from 'fumadocs-ui/components/tabs';
@@ -10,6 +10,17 @@ import { CodeBlock, Pre } from 'fumadocs-ui/components/codeblock';
1010
// Re-export SchemaNode type for use in MDX files
1111
export type { SchemaNode } from '@object-ui/core';
1212

13+
/** Minimal provider so plugins can find SchemaRendererContext */
14+
const defaultCtx = { dataSource: {} };
15+
function DemoProvider({ children }: { children: React.ReactNode }) {
16+
const value = useMemo(() => defaultCtx, []);
17+
return (
18+
<SchemaRendererContext.Provider value={value}>
19+
{children}
20+
</SchemaRendererContext.Provider>
21+
);
22+
}
23+
1324
interface InteractiveDemoProps {
1425
schema: SchemaNode;
1526
title?: string;
@@ -55,11 +66,13 @@ export function InteractiveDemo({
5566
</div>
5667
)}
5768
<div className="p-6 bg-background">
58-
<SidebarProvider className="min-h-0 w-full" defaultOpen={false}>
59-
<div className="w-full">
60-
<SchemaRenderer schema={example.schema} />
61-
</div>
62-
</SidebarProvider>
69+
<DemoProvider>
70+
<SidebarProvider className="min-h-0 w-full" defaultOpen={false}>
71+
<div className="w-full">
72+
<SchemaRenderer schema={example.schema} />
73+
</div>
74+
</SidebarProvider>
75+
</DemoProvider>
6376
</div>
6477
</div>
6578
))}
@@ -98,11 +111,13 @@ export function InteractiveDemo({
98111
<Tabs items={['Preview', 'Code']} defaultIndex={0}>
99112
<Tab value="Preview">
100113
<div className="border rounded-lg p-6 bg-background">
101-
<SidebarProvider className="min-h-0 w-full" defaultOpen={false}>
102-
<div className="w-full">
103-
<SchemaRenderer schema={schema} />
104-
</div>
105-
</SidebarProvider>
114+
<DemoProvider>
115+
<SidebarProvider className="min-h-0 w-full" defaultOpen={false}>
116+
<div className="w-full">
117+
<SchemaRenderer schema={schema} />
118+
</div>
119+
</SidebarProvider>
120+
</DemoProvider>
106121
</div>
107122
</Tab>
108123
<Tab value="Code">

e2e/docs-smoke.spec.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { test, expect } from '@playwright/test';
2+
3+
/**
4+
* Docs site smoke tests.
5+
*
6+
* These tests run against the production build of the docs site (Next.js)
7+
* and verify that documentation pages render without client-side errors.
8+
* This catches issues like:
9+
* - Missing React context providers (e.g. SchemaRendererProvider)
10+
* - Broken component demos that throw during hydration
11+
* - Non-array `.map()` crashes in component renderers
12+
* - Failed asset loading (JS/CSS bundles returning 404)
13+
*/
14+
15+
const DOCS_BASE = process.env.DOCS_BASE_URL || 'http://localhost:3000';
16+
17+
/** Representative pages across all documentation categories */
18+
const SMOKE_PAGES = [
19+
'/docs',
20+
// Components
21+
'/docs/components/form/button',
22+
'/docs/components/layout/card',
23+
'/docs/components/layout/tabs',
24+
'/docs/components/feedback/alert',
25+
'/docs/components/complex/data-table',
26+
'/docs/components/complex/resizable',
27+
'/docs/components/data-display/list',
28+
// Fields
29+
'/docs/fields/text',
30+
'/docs/fields/select',
31+
// Core
32+
'/docs/core/schema-renderer',
33+
'/docs/core/form-renderer',
34+
// Plugins (these require SchemaRendererProvider)
35+
'/docs/plugins/plugin-grid',
36+
'/docs/plugins/plugin-kanban',
37+
'/docs/plugins/plugin-view',
38+
'/docs/plugins/plugin-charts',
39+
'/docs/plugins/plugin-timeline',
40+
// Blocks
41+
'/docs/blocks/dashboard',
42+
'/docs/blocks/marketing',
43+
];
44+
45+
test.describe('Docs Site – Smoke', () => {
46+
for (const path of SMOKE_PAGES) {
47+
test(`${path} should load without client-side errors`, async ({ page }) => {
48+
const errors: string[] = [];
49+
50+
page.on('pageerror', (err) => {
51+
errors.push(err.message);
52+
});
53+
54+
const response = await page.goto(`${DOCS_BASE}${path}`, {
55+
waitUntil: 'domcontentloaded',
56+
timeout: 30_000,
57+
});
58+
59+
// Page must return a successful HTTP status
60+
expect(response?.status(), `${path} returned HTTP ${response?.status()}`).toBeLessThan(400);
61+
62+
// Wait for hydration
63+
await page.waitForTimeout(2000);
64+
65+
// Must not have thrown uncaught JS exceptions
66+
expect(errors, `Uncaught JS errors on ${path}:\n${errors.join('\n')}`).toEqual([]);
67+
68+
// Must not show the Next.js error overlay
69+
const bodyText = await page.locator('body').innerText();
70+
expect(bodyText).not.toContain('Application error');
71+
expect(bodyText).not.toContain('client-side exception');
72+
});
73+
}
74+
75+
test('should load all JS/CSS bundles without 404s', async ({ page }) => {
76+
const failedAssets: string[] = [];
77+
78+
page.on('response', (resp) => {
79+
const url = resp.url();
80+
if ((url.endsWith('.js') || url.endsWith('.css')) && resp.status() >= 400) {
81+
failedAssets.push(`${resp.status()} ${url}`);
82+
}
83+
});
84+
85+
await page.goto(`${DOCS_BASE}/docs`, { waitUntil: 'networkidle', timeout: 30_000 });
86+
expect(failedAssets, 'Critical assets returned HTTP errors').toEqual([]);
87+
});
88+
});

packages/components/src/renderers/complex/resizable.tsx

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,26 @@ import {
1717
import { renderChildren } from '../../lib/utils';
1818

1919
ComponentRegistry.register('resizable',
20-
({ schema, className, ...props }: { schema: ResizableSchema; className?: string; [key: string]: any }) => (
21-
<ResizablePanelGroup
22-
orientation={(schema.direction || 'horizontal') as "horizontal" | "vertical"}
23-
className={className}
24-
{...props}
25-
style={{ minHeight: schema.minHeight || '200px' }}
26-
>
27-
{schema.panels?.map((panel: any, index: number) => (
28-
<React.Fragment key={index}>
29-
<ResizablePanel defaultSize={panel.defaultSize} minSize={panel.minSize} maxSize={panel.maxSize}>
30-
{renderChildren(panel.content)}
31-
</ResizablePanel>
32-
{index < schema.panels.length - 1 && <ResizableHandle withHandle={schema.withHandle} />}
33-
</React.Fragment>
34-
))}
35-
</ResizablePanelGroup>
36-
),
20+
({ schema, className, ...props }: { schema: ResizableSchema; className?: string; [key: string]: any }) => {
21+
const panels = Array.isArray(schema.panels) ? schema.panels : [];
22+
return (
23+
<ResizablePanelGroup
24+
orientation={(schema.direction || 'horizontal') as "horizontal" | "vertical"}
25+
className={className}
26+
{...props}
27+
style={{ minHeight: schema.minHeight || '200px' }}
28+
>
29+
{panels.map((panel: any, index: number) => (
30+
<React.Fragment key={index}>
31+
<ResizablePanel defaultSize={panel.defaultSize} minSize={panel.minSize} maxSize={panel.maxSize}>
32+
{renderChildren(panel.content)}
33+
</ResizablePanel>
34+
{index < panels.length - 1 && <ResizableHandle withHandle={schema.withHandle} />}
35+
</React.Fragment>
36+
))}
37+
</ResizablePanelGroup>
38+
);
39+
},
3740
{
3841
namespace: 'ui',
3942
label: 'Resizable Panel Group',

packages/components/src/renderers/data-display/list.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ ComponentRegistry.register('list',
1515
({ schema, className, ...props }: { schema: ListSchema; className?: string; [key: string]: any }) => {
1616
// Support data binding
1717
const boundData = useDataScope(schema.bind);
18-
const items = boundData || schema.items || [];
18+
const items = Array.isArray(boundData) ? boundData : Array.isArray(schema.items) ? schema.items : [];
1919

2020
// We use 'ol' or 'ul' based on ordered prop
2121
const ListTag = schema.ordered ? 'ol' : 'ul';

packages/components/src/renderers/data-display/table.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export const SimpleTableRenderer = ({ schema, className }: any) => {
2323
// Try to get data from binding first, then fall back to inline data
2424
const boundData = useDataScope(schema.bind);
2525
const data = boundData || schema.data || schema.props?.data || [];
26-
const columns = schema.columns || schema.props?.columns || [];
26+
const columns = Array.isArray(schema.columns) ? schema.columns : Array.isArray(schema.props?.columns) ? schema.props.columns : [];
2727

2828
// If we have data but it's not an array, show error.
2929
// If data is undefined, we might just be loading or empty.

packages/components/src/renderers/data-display/tree-view.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,8 @@ ComponentRegistry.register('tree-view',
102102

103103
// Support data binding
104104
const boundData = useDataScope(schema.bind);
105-
const nodes = boundData || schema.nodes || schema.data || [];
105+
const rawNodes = boundData || schema.nodes || schema.data || [];
106+
const nodes = Array.isArray(rawNodes) ? rawNodes : [];
106107

107108
return (
108109
<div className={cn(

0 commit comments

Comments
 (0)