Files
twenty/packages/twenty-front-component-renderer/src/__stories__/FrontComponentRenderer.stories.tsx
T
Paul Rastoin 37908114fc [SDK] Extract twenty-front-component-renderer outside of twenty-sdk ( 2.8MB ) (#19021)
Followup https://github.com/twentyhq/twenty/pull/19010

## Dependency diagram

```
┌─────────────────────┐
│     twenty-front    │
│   (React frontend)  │
└─────────┬───────────┘
          │ imports runtime:
          │   FrontComponentRenderer
          │   FrontComponentRendererWithSdkClient
          │   useFrontComponentExecutionContext
          ▼
┌──────────────────────────────────┐         ┌─────────────────────────┐
│ twenty-front-component-renderer  │────────▶│       twenty-sdk        │
│   (remote-dom host + worker)     │         │  (app developer SDK)    │
│                                  │         │                         │
│  imports from twenty-sdk:        │         │  Public API:            │
│   • types only:                  │         │   defineFrontComponent  │
│     FrontComponentExecutionContext│         │   navigate, closeSide…  │
│     NavigateFunction             │         │   useFrontComponent…    │
│     CloseSidePanelFunction       │         │   Command components    │
│     CommandConfirmation…         │         │   conditional avail.    │
│     OpenCommandConfirmation…     │         │                         │
│     EnqueueSnackbarFunction      │         │  Internal only:         │
│     etc.                         │         │   frontComponentHost…   │
│                                  │         │   front-component-build │
│  owns locally:                   │         │   esbuild plugins       │
│   • ALLOWED_HTML_ELEMENTS        │         │                         │
│   • EVENT_TO_REACT               │         └────────────┬────────────┘
│   • HTML_TAG_TO_CUSTOM_ELEMENT…  │                      │
│   • SerializedEventData          │                      │ types
│   • PropertySchema               │                      ▼
│   • frontComponentHostComm…      │         ┌─────────────────────────┐
│     (local ref to globalThis)    │         │     twenty-shared       │
│   • setFrontComponentExecution…  │         │  (common types/utils)   │
│     (local impl, same keys)      │         │   AppPath, SidePanelP…  │
│                                  │         │   EnqueueSnackbarParams │
└──────────────────────────────────┘         │   isDefined, …          │
          │                                  └─────────────────────────┘
          │ also depends on
          ▼
    twenty-shared (types)
    @remote-dom/* (runtime)
    @quilted/threads (runtime)
    react (runtime)
```

**Key points:**

- **`twenty-front`** depends on the renderer, **not** on `twenty-sdk`
directly (for rendering)
- **`twenty-front-component-renderer`** depends on `twenty-sdk` for
**types only** (function signatures, `FrontComponentExecutionContext`).
The runtime bridge (`frontComponentHostCommunicationApi`) is shared via
`globalThis` keys, not module imports
- **`twenty-sdk`** has no dependency on the renderer — clean one-way
dependency
- The renderer owns all remote-dom infrastructure (element schemas,
event mappings, custom element tags) that was previously leaking through
the SDK's public API
- The SDK's `./build` entry point was removed entirely (unused)
2026-03-30 17:06:06 +00:00

147 lines
4.2 KiB
TypeScript

import { type Meta, type StoryObj } from '@storybook/react-vite';
import { expect, fn, userEvent, waitFor, within } from 'storybook/test';
import { FrontComponentRenderer } from '../host/components/FrontComponentRenderer';
import { getBuiltStoryComponentPathForRender } from './utils/getBuiltStoryComponentPathForRender';
const errorHandler = fn();
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/Feature',
component: FrontComponentRenderer,
parameters: {
layout: 'centered',
},
args: {
onError: errorHandler,
applicationAccessToken: 'fake-token',
executionContext: { frontComponentId: 'storybook-test', userId: null },
},
beforeEach: () => {
errorHandler.mockClear();
},
};
export default meta;
type Story = StoryObj<typeof FrontComponentRenderer>;
const createComponentStory = (
name: string,
options?: { runtime?: 'preact'; play?: Story['play'] },
): Story => ({
args: {
componentUrl: getBuiltStoryComponentPathForRender(
`${name}.front-component`,
options?.runtime,
),
},
...(options?.play ? { play: options.play } : {}),
});
export const Static: Story = {
...createComponentStory('static'),
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const container = await canvas.findByTestId(
'static-component',
{},
{ timeout: 30000 },
);
expect(container).toBeVisible();
expect(container).toHaveStyle({
backgroundColor: '#f0f4f8',
borderRadius: '8px',
});
const heading = await canvas.findByText('Static Component');
expect(heading).toBeVisible();
expect(heading).toHaveStyle({ fontWeight: '700' });
const badge = await canvas.findByTestId('styled-badge');
expect(badge).toBeVisible();
expect(badge).toHaveStyle({ backgroundColor: '#48bb78' });
},
};
export const Interactive: Story = {
...createComponentStory('interactive'),
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByTestId('interactive-component', {}, { timeout: 10000 });
expect(await canvas.findByText('Count: 0')).toBeVisible();
const button = await canvas.findByTestId('increment-button');
await userEvent.click(button);
expect(await canvas.findByText('Count: 1')).toBeVisible();
await userEvent.click(button);
expect(await canvas.findByText('Count: 2')).toBeVisible();
},
};
export const Lifecycle: Story = {
...createComponentStory('lifecycle'),
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByTestId('lifecycle-component', {}, { timeout: 10000 });
expect(await canvas.findByText('Mounted')).toBeVisible();
await waitFor(
() => {
const tickElement = canvas.getByTestId('tick-count');
expect(tickElement.textContent).toMatch(/Ticks: [1-9]\d*/);
},
{ timeout: 10000 },
);
},
};
export const ErrorHandling: Story = {
...createComponentStory('nonexistent'),
play: async () => {
await waitFor(
() => {
expect(errorHandler).toHaveBeenCalled();
},
{ timeout: 10000 },
);
},
};
export const SdkContext: Story = {
...createComponentStory('sdk-context-example'),
args: {
...createComponentStory('sdk-context-example').args,
executionContext: {
frontComponentId: 'sdk-context-test',
userId: 'test-user-abc-123',
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByTestId('sdk-context-component', {}, { timeout: 30000 });
const userIdElement = await canvas.findByTestId('sdk-context-user-id');
expect(userIdElement).toBeVisible();
expect(userIdElement).toHaveTextContent('test-user-abc-123');
const jsonElement = await canvas.findByTestId('sdk-context-json');
expect(jsonElement).toHaveTextContent('"userId": "test-user-abc-123"');
const button = await canvas.findByTestId('sdk-context-button');
await userEvent.click(button);
const renderCount = await canvas.findByTestId('sdk-context-render-count');
expect(renderCount).toHaveTextContent('Renders: 1');
expect(userIdElement).toHaveTextContent('test-user-abc-123');
},
};