snapshot of https://github.com/twentyhq/twenty.git (main, 2026-09-17; blobs > 131072B omitted)
Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 2s

This commit is contained in:
upstream-mirror
2026-09-17 23:28:13 +05:00
commit e7da97194a
32688 changed files with 4208681 additions and 0 deletions

View File

@@ -0,0 +1,213 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import bundleSizes from '@/__stories__/example-sources-built/bundle-sizes.json';
type BundleSizeEntry = {
name: string;
reactBytes: number;
preactBytes: number;
};
const formatBytes = (bytes: number): string => {
if (bytes === 0) return '0 B';
const kb = bytes / 1024;
if (kb < 1024) {
return `${kb.toFixed(1)} KB`;
}
return `${(kb / 1024).toFixed(2)} MB`;
};
const formatLabel = (name: string): string =>
name
.replace('.front-component', '')
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
const BAR_MAX_WIDTH = 400;
const BundleSizesTable = () => {
const entries = (bundleSizes as BundleSizeEntry[]).sort(
(a, b) => b.reactBytes - a.reactBytes,
);
const maxBytes = Math.max(...entries.map((entry) => entry.reactBytes));
return (
<div style={{ fontFamily: 'Inter, system-ui, sans-serif', padding: 24 }}>
<h2 style={{ margin: '0 0 4px', fontSize: 18, fontWeight: 600 }}>
Front Component Bundle Sizes
</h2>
<p style={{ margin: '0 0 24px', fontSize: 13, color: '#666' }}>
Each bar shows the minified .mjs size (React vs Preact runtime)
</p>
<table
style={{
width: '100%',
borderCollapse: 'collapse',
fontSize: 13,
}}
>
<thead>
<tr
style={{
textAlign: 'left',
borderBottom: '2px solid #e4e4e7',
}}
>
<th style={{ padding: '8px 12px', width: 180 }}>Component</th>
<th style={{ padding: '8px 12px' }}>Size</th>
<th style={{ padding: '8px 12px', width: 100, textAlign: 'right' }}>
React
</th>
<th style={{ padding: '8px 12px', width: 100, textAlign: 'right' }}>
Preact
</th>
<th style={{ padding: '8px 12px', width: 80, textAlign: 'right' }}>
Saving
</th>
</tr>
</thead>
<tbody>
{entries.map((entry) => {
const reactWidth = (entry.reactBytes / maxBytes) * BAR_MAX_WIDTH;
const preactWidth = (entry.preactBytes / maxBytes) * BAR_MAX_WIDTH;
const saving =
entry.reactBytes > 0
? (
((entry.reactBytes - entry.preactBytes) /
entry.reactBytes) *
100
).toFixed(0)
: '0';
return (
<tr
key={entry.name}
style={{ borderBottom: '1px solid #f0f0f0' }}
>
<td
style={{
padding: '10px 12px',
fontWeight: 500,
whiteSpace: 'nowrap',
}}
>
{formatLabel(entry.name)}
</td>
<td style={{ padding: '10px 12px' }}>
<div
style={{ display: 'flex', flexDirection: 'column', gap: 3 }}
>
<div
style={{
height: 14,
width: reactWidth,
backgroundColor: '#3b82f6',
borderRadius: 3,
minWidth: 4,
}}
/>
<div
style={{
height: 14,
width: preactWidth,
backgroundColor: '#8b5cf6',
borderRadius: 3,
minWidth: 4,
}}
/>
</div>
</td>
<td
style={{
padding: '10px 12px',
textAlign: 'right',
fontVariantNumeric: 'tabular-nums',
color: '#3b82f6',
}}
>
{formatBytes(entry.reactBytes)}
</td>
<td
style={{
padding: '10px 12px',
textAlign: 'right',
fontVariantNumeric: 'tabular-nums',
color: '#8b5cf6',
}}
>
{formatBytes(entry.preactBytes)}
</td>
<td
style={{
padding: '10px 12px',
textAlign: 'right',
fontVariantNumeric: 'tabular-nums',
color: '#16a34a',
}}
>
{saving}%
</td>
</tr>
);
})}
</tbody>
</table>
<div
style={{
display: 'flex',
gap: 16,
marginTop: 16,
fontSize: 12,
color: '#888',
}}
>
<span>
<span
style={{
display: 'inline-block',
width: 10,
height: 10,
backgroundColor: '#3b82f6',
borderRadius: 2,
marginRight: 4,
}}
/>
React
</span>
<span>
<span
style={{
display: 'inline-block',
width: 10,
height: 10,
backgroundColor: '#8b5cf6',
borderRadius: 2,
marginRight: 4,
}}
/>
Preact
</span>
</div>
</div>
);
};
const meta: Meta = {
title: 'FrontComponent/BundleSizes',
component: BundleSizesTable,
parameters: {
layout: 'padded',
},
};
export default meta;
type Story = StoryObj;
export const Overview: Story = {};

View File

@@ -0,0 +1,152 @@
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 '@/__stories__/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,
recordId: null,
selectedRecordIds: [],
timelineActivityId: null,
colorScheme: 'light',
},
},
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();
expect(
await canvas.findByText(/Ticks: [1-9]\d*/, {}, { timeout: 10000 }),
).toBeVisible();
},
};
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',
recordId: null,
selectedRecordIds: [],
timelineActivityId: null,
colorScheme: 'light',
},
},
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');
},
};

View File

@@ -0,0 +1,25 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { expect } from 'storybook/test';
import { runFrontComponentSandboxIsolationProbe } from '@/__stories__/utils/runFrontComponentSandboxIsolationProbe';
const meta: Meta = {
title: 'FrontComponent/Security',
render: () => <div data-testid="front-component-sandbox-isolation" />,
};
export default meta;
type Story = StoryObj;
export const WorkerRunsInOpaqueOriginWithoutStorageAccess: Story = {
play: async () => {
const report = await runFrontComponentSandboxIsolationProbe();
expect(report.iframeOrigin).toBe('null');
expect(report.workerOrigin).toBe('null');
expect(report.localStorageDenied).toBe(true);
expect(report.cookiesDenied).toBe(true);
expect(report.indexedDbDenied).toBe(true);
expect(report.workerIndexedDbDenied).toBe(true);
},
};

View File

@@ -0,0 +1,95 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { expect, fn, waitFor, within } from 'storybook/test';
import { getBuiltStoryComponentPathForRender } from '@/__stories__/utils/getBuiltStoryComponentPathForRender';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
const GEOMETRY_TIMEOUT = 30000;
const errorHandler = fn();
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/Geometry',
component: FrontComponentRenderer,
parameters: {
layout: 'centered',
},
args: {
onError: errorHandler,
applicationAccessToken: 'fake-token',
executionContext: {
frontComponentId: 'storybook-test',
userId: null,
recordId: null,
selectedRecordIds: [],
timelineActivityId: null,
colorScheme: 'light',
},
},
beforeEach: () => {
errorHandler.mockClear();
},
};
export default meta;
type Story = StoryObj<typeof FrontComponentRenderer>;
const createGeometryStory = (name: string, play: Story['play']): Story => ({
args: {
componentUrl: getBuiltStoryComponentPathForRender(
`${name}.front-component`,
),
},
play,
});
export const MirrorsElementGeometryIntoTheWorker: Story = createGeometryStory(
'geometry-measure',
async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByTestId(
'geometry-measure-component',
{},
{ timeout: GEOMETRY_TIMEOUT },
);
await waitFor(
() => {
expect(canvas.getByTestId('geometry-measure-width')).toHaveTextContent(
'width: 240',
);
expect(canvas.getByTestId('geometry-measure-height')).toHaveTextContent(
'height: 80',
);
expect(
canvas.getByTestId('geometry-measure-offset-width'),
).toHaveTextContent('offsetWidth: 240');
expect(
canvas.getByTestId('geometry-measure-inner-width'),
).not.toHaveTextContent('innerWidth: 0');
},
{ timeout: GEOMETRY_TIMEOUT },
);
expect(errorHandler).not.toHaveBeenCalled();
},
);
export const RendersRechartsAreaChart: Story = createGeometryStory(
'recharts-example',
async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByTestId(
'recharts-component',
{},
{ timeout: GEOMETRY_TIMEOUT },
);
expect(await canvas.findByText('Jan')).toBeVisible();
expect(await canvas.findByText('Jun')).toBeVisible();
expect(errorHandler).not.toHaveBeenCalled();
},
);

View File

@@ -0,0 +1,437 @@
import { buttonControlsTest } from '@/__stories__/twenty-ui-gallery/utils/buttonControlsTest';
import { type Meta } from '@storybook/react-vite';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { type TwentyUiGalleryStory as Story } from '@/__stories__/twenty-ui-gallery/types/TwentyUiGalleryStory';
import {
createCheckboxTest,
createFieldControlsTest,
createRadioGroupPreactTest,
listItemTest,
sliderTest,
toastTest,
} from '@/__stories__/twenty-ui-gallery/utils/componentInteractionTests';
import {
statusControlsTest,
tagControlsTest,
avatarControlsTest,
chipControlsTest,
} from '@/__stories__/twenty-ui-gallery/utils/displayControlTests';
import { createGalleryStory } from '@/__stories__/twenty-ui-gallery/utils/createGalleryStory';
import { typographyTest } from '@/__stories__/twenty-ui-gallery/utils/typographyTest';
import {
codeEditorTest,
dataDisplayTest,
displayHelpersTest,
galleryRenderTest,
inputPreactTest,
inputReactTest,
modalOpenHangTest,
navigationTest,
themeTokenTest,
} from '@/__stories__/twenty-ui-gallery/utils/galleryRenderTests';
import {
alertDialogTest,
menuTest,
popoverTest,
radioGroupReactTest,
selectTest,
sliderRangeTest,
switchTest,
tabsPreactTest,
tabsReactTest,
} from '@/__stories__/twenty-ui-gallery/utils/sandboxFailureTests';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import { createTooltipSandboxFailureTest } from '@/__stories__/twenty-ui-gallery/utils/createTooltipSandboxFailureTest';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/Twenty UI Gallery',
component: FrontComponentRenderer,
parameters: {
layout: 'centered',
},
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
export const DataDisplayReact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-data-display-gallery',
runtime: 'react',
play: dataDisplayTest,
});
export const DataDisplayPreact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-data-display-gallery',
runtime: 'preact',
play: dataDisplayTest,
});
export const FeedbackReact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-feedback-gallery',
runtime: 'react',
play: galleryRenderTest,
});
export const FeedbackPreact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-feedback-gallery',
runtime: 'preact',
play: galleryRenderTest,
});
export const IconReact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-icon-gallery',
runtime: 'react',
play: galleryRenderTest,
});
export const IconPreact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-icon-gallery',
runtime: 'preact',
play: galleryRenderTest,
});
export const InputReact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-input-gallery',
runtime: 'react',
play: inputReactTest,
});
export const InputPreact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-input-gallery',
runtime: 'preact',
play: inputPreactTest,
});
export const JsonVisualizerReact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-json-visualizer-gallery',
runtime: 'react',
play: galleryRenderTest,
});
export const JsonVisualizerPreact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-json-visualizer-gallery',
runtime: 'preact',
play: galleryRenderTest,
});
export const LayoutReact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-layout-gallery',
runtime: 'react',
play: galleryRenderTest,
});
export const LayoutPreact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-layout-gallery',
runtime: 'preact',
play: galleryRenderTest,
});
export const NavigationReact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-navigation-gallery',
runtime: 'react',
play: navigationTest,
});
export const NavigationPreact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-navigation-gallery',
runtime: 'preact',
play: navigationTest,
});
export const SurfacesReact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-surfaces-gallery',
runtime: 'react',
play: galleryRenderTest,
});
export const SurfacesPreact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-surfaces-gallery',
runtime: 'preact',
play: galleryRenderTest,
});
export const ModalOpenReact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-modal-open-gallery',
runtime: 'react',
play: modalOpenHangTest,
});
export const ModalOpenPreact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-modal-open-gallery',
runtime: 'preact',
play: galleryRenderTest,
});
export const CodeEditorReact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-code-editor-gallery',
runtime: 'react',
play: codeEditorTest,
});
export const CodeEditorPreact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-code-editor-gallery',
runtime: 'preact',
play: codeEditorTest,
});
export const TypographyReact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-typography-gallery',
runtime: 'react',
play: typographyTest,
});
export const TypographyPreact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-typography-gallery',
runtime: 'preact',
play: typographyTest,
});
export const ThemeTokensReact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-theme-tokens',
runtime: 'react',
play: themeTokenTest,
});
export const ThemeTokensPreact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-theme-tokens',
runtime: 'preact',
play: themeTokenTest,
});
export const FieldControlsReact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-field-controls',
runtime: 'react',
// React serializes boolean ARIA as empty strings and loses Textarea's
// change handler.
play: createFieldControlsTest({
expectedAriaInvalid: '',
expectedReportedValues: /^Email: alice; Notes:$/,
}),
});
export const FieldControlsPreact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-field-controls',
runtime: 'preact',
play: createFieldControlsTest({
expectedAriaInvalid: 'true',
expectedReportedValues: 'Email: alice; Notes: Follow up',
}),
});
export const DisplayHelpersReact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-display-helpers',
runtime: 'react',
play: displayHelpersTest,
});
export const DisplayHelpersPreact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-display-helpers',
runtime: 'preact',
play: displayHelpersTest,
});
export const ListItemReact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-list-item',
runtime: 'react',
play: listItemTest,
});
export const ListItemPreact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-list-item',
runtime: 'preact',
play: listItemTest,
});
export const TabsReact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-tabs',
runtime: 'react',
play: tabsReactTest,
});
export const TabsPreact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-tabs',
runtime: 'preact',
play: tabsPreactTest,
});
export const PopoverReact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-popover',
runtime: 'react',
play: popoverTest,
});
export const PopoverPreact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-popover',
runtime: 'preact',
play: popoverTest,
});
export const TooltipReact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-tooltip',
runtime: 'react',
play: createTooltipSandboxFailureTest('react'),
});
export const TooltipPreact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-tooltip',
runtime: 'preact',
play: createTooltipSandboxFailureTest('preact'),
});
export const MenuReact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-menu',
runtime: 'react',
play: menuTest,
});
export const MenuPreact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-menu',
runtime: 'preact',
play: menuTest,
});
export const SelectReact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-select',
runtime: 'react',
play: selectTest,
});
export const SelectPreact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-select',
runtime: 'preact',
play: selectTest,
});
export const ToastReact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-toast',
runtime: 'react',
play: toastTest,
});
export const ToastPreact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-toast',
runtime: 'preact',
play: toastTest,
});
export const AlertDialogReact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-alert-dialog',
runtime: 'react',
play: alertDialogTest,
});
export const AlertDialogPreact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-alert-dialog',
runtime: 'preact',
play: alertDialogTest,
});
export const SwitchReact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-switch',
runtime: 'react',
play: switchTest,
});
export const SwitchPreact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-switch',
runtime: 'preact',
play: switchTest,
});
export const CheckboxReact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-checkbox',
runtime: 'react',
play: createCheckboxTest({ expectedAriaTrue: '' }),
});
export const CheckboxPreact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-checkbox',
runtime: 'preact',
play: createCheckboxTest({ expectedAriaTrue: 'true' }),
});
export const SliderReact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-slider',
runtime: 'react',
play: sliderTest,
});
export const SliderPreact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-slider',
runtime: 'preact',
play: sliderTest,
});
export const SliderRangeReact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-slider-range',
runtime: 'react',
play: sliderRangeTest,
});
export const SliderRangePreact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-slider-range',
runtime: 'preact',
play: sliderRangeTest,
});
export const RadioGroupReact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-radio-group',
runtime: 'react',
play: radioGroupReactTest,
});
export const RadioGroupPreact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-radio-group',
runtime: 'preact',
play: createRadioGroupPreactTest({ optionName: 'Daily' }),
});
export const CardPickerReact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-radio-group',
runtime: 'react',
play: radioGroupReactTest,
});
export const CardPickerPreact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-radio-group',
runtime: 'preact',
play: createRadioGroupPreactTest({ optionName: 'Pro plan' }),
});
export const StatusControlsReact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-status-controls',
runtime: 'react',
play: statusControlsTest,
});
export const StatusControlsPreact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-status-controls',
runtime: 'preact',
play: statusControlsTest,
});
export const TagControlsReact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-tag-controls',
runtime: 'react',
play: tagControlsTest,
});
export const TagControlsPreact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-tag-controls',
runtime: 'preact',
play: tagControlsTest,
});
export const ButtonControlsReact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-button-controls',
runtime: 'react',
play: buttonControlsTest,
});
export const ButtonControlsPreact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-button-controls',
runtime: 'preact',
play: buttonControlsTest,
});
export const AvatarControlsReact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-avatar-controls',
runtime: 'react',
play: avatarControlsTest,
});
export const AvatarControlsPreact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-avatar-controls',
runtime: 'preact',
play: avatarControlsTest,
});
export const ChipControlsReact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-chip-controls',
runtime: 'react',
play: chipControlsTest,
});
export const ChipControlsPreact: Story = createGalleryStory({
frontComponentBundleName: 'twenty-ui-chip-controls',
runtime: 'preact',
play: chipControlsTest,
});

View File

@@ -0,0 +1,151 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { expect, fn, userEvent, within } from 'storybook/test';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import { getBuiltStoryComponentPathForRender } from '@/__stories__/utils/getBuiltStoryComponentPathForRender';
const errorHandler = fn();
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/UI Libraries',
component: FrontComponentRenderer,
parameters: {
layout: 'centered',
},
args: {
onError: errorHandler,
applicationAccessToken: 'fake-token',
executionContext: {
frontComponentId: 'storybook-test',
userId: null,
recordId: null,
selectedRecordIds: [],
timelineActivityId: null,
colorScheme: 'light',
},
},
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 } : {}),
});
const createCounterTest =
(testIdPrefix: string): Story['play'] =>
async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByTestId(
`${testIdPrefix}-component`,
{},
{ timeout: 30000 },
);
expect(await canvas.findByText('Count: 0')).toBeVisible();
const button = await canvas.findByTestId(`${testIdPrefix}-button`);
await userEvent.click(button);
expect(await canvas.findByText('Count: 1')).toBeVisible();
await userEvent.click(button);
expect(await canvas.findByText('Count: 2')).toBeVisible();
};
const chakraTest = createCounterTest('chakra');
export const ChakraReact: Story = createComponentStory('chakra-example', {
play: chakraTest,
});
export const ChakraPreact: Story = createComponentStory('chakra-example', {
runtime: 'preact',
play: chakraTest,
});
const tailwindTest = createCounterTest('tailwind');
export const TailwindReact: Story = createComponentStory('tailwind-example', {
play: tailwindTest,
});
export const TailwindPreact: Story = createComponentStory('tailwind-example', {
runtime: 'preact',
play: tailwindTest,
});
const emotionTest = createCounterTest('emotion');
export const EmotionReact: Story = createComponentStory('emotion-example', {
play: emotionTest,
});
export const EmotionPreact: Story = createComponentStory('emotion-example', {
runtime: 'preact',
play: emotionTest,
});
const styledComponentsTest = createCounterTest('styled-components');
export const StyledComponentsReact: Story = createComponentStory(
'styled-components-example',
{ play: styledComponentsTest },
);
export const StyledComponentsPreact: Story = createComponentStory(
'styled-components-example',
{ runtime: 'preact', play: styledComponentsTest },
);
const shadcnTest = createCounterTest('shadcn');
export const ShadcnReact: Story = createComponentStory('shadcn-example', {
play: shadcnTest,
});
export const ShadcnPreact: Story = createComponentStory('shadcn-example', {
runtime: 'preact',
play: shadcnTest,
});
const muiTest = createCounterTest('mui');
export const MuiReact: Story = createComponentStory('mui-example', {
play: muiTest,
});
export const MuiPreact: Story = createComponentStory('mui-example', {
runtime: 'preact',
play: muiTest,
});
const twentyUiTest: Story['play'] = async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByTestId('twenty-ui-component', {}, { timeout: 30000 });
expect(await canvas.findByText('Count: 0')).toBeVisible();
const button = await canvas.findByText('Increment');
await userEvent.click(button);
expect(await canvas.findByText('Count: 1')).toBeVisible();
await userEvent.click(button);
expect(await canvas.findByText('Count: 2')).toBeVisible();
};
export const TwentyUiReact: Story = createComponentStory('twenty-ui-example', {
play: twentyUiTest,
});
export const TwentyUiPreact: Story = createComponentStory('twenty-ui-example', {
runtime: 'preact',
play: twentyUiTest,
});

View File

@@ -0,0 +1,139 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { expect, userEvent, waitFor, within } from 'storybook/test';
import {
errorHandler,
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { expectJsonDataAttribute } from '@/__stories__/shared/test-utils/matchers/expectJsonDataAttribute';
import {
INTERACTION_TIMEOUT,
MOUNT_TIMEOUT,
} from '@/__stories__/shared/test-utils/timeouts';
import { getBuiltStoryComponentPathForRender } from '@/__stories__/utils/getBuiltStoryComponentPathForRender';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/Worker Platform APIs',
component: FrontComponentRenderer,
parameters: {
layout: 'centered',
},
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
type Story = StoryObj<typeof FrontComponentRenderer>;
const EXPECTED_OBSERVED_MUTATIONS = [
{
type: 'childList',
addedItems: ['item-0'],
removedItems: [],
hasPreviousSibling: false,
hasNextSibling: false,
},
{
type: 'childList',
addedItems: ['item-1'],
removedItems: [],
hasPreviousSibling: true,
hasNextSibling: false,
},
];
const mutationObserverTest: Story['play'] = async ({ canvasElement }) => {
const canvas = within(canvasElement);
const addItemButton = await canvas.findByTestId(
'mutation-observer-add',
{},
{ timeout: MOUNT_TIMEOUT },
);
await userEvent.click(addItemButton);
await userEvent.click(addItemButton);
await expectJsonDataAttribute({
canvas,
testId: 'mutation-observer-status',
attributeName: 'data-observed-records',
expectedValue: EXPECTED_OBSERVED_MUTATIONS,
});
expect(errorHandler).not.toHaveBeenCalled();
};
const EXPECTED_CLASS_LIST_REPORT = {
tokens: ['initial-class', 'mapboxgl-map', 'replaced', 'toggled-on'],
value: 'initial-class mapboxgl-map replaced toggled-on',
};
const classListTest: Story['play'] = async ({ canvasElement }) => {
const canvas = within(canvasElement);
const runButton = await canvas.findByTestId(
'class-list-run',
{},
{ timeout: MOUNT_TIMEOUT },
);
await userEvent.click(runButton);
await expectJsonDataAttribute({
canvas,
testId: 'class-list-status',
attributeName: 'data-class-list-report',
expectedValue: EXPECTED_CLASS_LIST_REPORT,
});
await waitFor(
() => {
expect(canvas.getByTestId('class-list-container').className).toBe(
EXPECTED_CLASS_LIST_REPORT.value,
);
},
{ timeout: INTERACTION_TIMEOUT },
);
expect(errorHandler).not.toHaveBeenCalled();
};
const createStory = ({
name,
play,
runtime,
}: {
name: string;
play: Story['play'];
runtime?: 'preact';
}): Story => ({
args: {
componentUrl: getBuiltStoryComponentPathForRender(
`${name}.front-component`,
runtime,
),
},
play,
});
export const MutationObserverReact: Story = createStory({
name: 'mutation-observer-example',
play: mutationObserverTest,
});
export const MutationObserverPreact: Story = createStory({
name: 'mutation-observer-example',
play: mutationObserverTest,
runtime: 'preact',
});
export const ClassListReact: Story = createStory({
name: 'class-list-example',
play: classListTest,
});
export const ClassListPreact: Story = createStory({
name: 'class-list-example',
play: classListTest,
runtime: 'preact',
});

View File

@@ -0,0 +1,7 @@
declare const bundleSizes: {
name: string;
reactBytes: number;
preactBytes: number;
}[];
export default bundleSizes;

View File

@@ -0,0 +1,60 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { expect, userEvent, waitFor, within } from 'storybook/test';
import { isDefined } from 'twenty-shared/utils';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { expectFrontComponentMounted } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentMounted';
import { runFrontComponentStory } from '@/__stories__/shared/test-utils/runFrontComponentStory';
import {
HOST_API_TIMEOUT,
INTERACTION_TIMEOUT,
} from '@/__stories__/shared/test-utils/timeouts';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HostApi/ClosePanel',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
type Story = StoryObj<typeof FrontComponentRenderer>;
export const ClosePanel: Story = runFrontComponentStory({
frontComponentBundleName: 'host-api-side-panel-close',
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const api = args.frontComponentHostCommunicationApi;
if (!isDefined(api)) {
throw new Error('frontComponentHostCommunicationApi is required');
}
await expectFrontComponentMounted(canvas);
const subject = await canvas.findByTestId('subject');
await userEvent.click(subject);
await waitFor(
() => {
expect(api.closeSidePanel).toHaveBeenCalled();
},
{ timeout: HOST_API_TIMEOUT },
);
expect(
await canvas.findByText(
'closePanel:success',
{},
{ timeout: INTERACTION_TIMEOUT },
),
).toBeVisible();
},
});

View File

@@ -0,0 +1,60 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { expect, userEvent, waitFor, within } from 'storybook/test';
import { isDefined } from 'twenty-shared/utils';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { expectFrontComponentMounted } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentMounted';
import { runFrontComponentStory } from '@/__stories__/shared/test-utils/runFrontComponentStory';
import {
HOST_API_TIMEOUT,
INTERACTION_TIMEOUT,
} from '@/__stories__/shared/test-utils/timeouts';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HostApi/CopyToClipboard',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
type Story = StoryObj<typeof FrontComponentRenderer>;
export const CopyToClipboard: Story = runFrontComponentStory({
frontComponentBundleName: 'host-api-copy-to-clipboard',
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const api = args.frontComponentHostCommunicationApi;
if (!isDefined(api)) {
throw new Error('frontComponentHostCommunicationApi is required');
}
await expectFrontComponentMounted(canvas);
const subject = await canvas.findByTestId('subject');
await userEvent.click(subject);
await waitFor(
() => {
expect(api.copyToClipboard).toHaveBeenCalledWith('Hello clipboard');
},
{ timeout: HOST_API_TIMEOUT },
);
expect(
await canvas.findByText(
'clipboard:success',
{},
{ timeout: INTERACTION_TIMEOUT },
),
).toBeVisible();
},
});

View File

@@ -0,0 +1,47 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import { copyToClipboard } from 'twenty-sdk/front-component';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import {
BUTTON_STYLE,
STATUS_STYLE,
} from '@/__stories__/shared/front-components/styles';
const HostApiCopyToClipboardFrontComponent = () => {
const [status, setStatus] = useState('idle');
const handleClick = async () => {
try {
await copyToClipboard('Hello clipboard');
setStatus('clipboard:success');
} catch (error) {
setStatus(
`clipboard:error:${error instanceof Error ? error.message : String(error)}`,
);
}
};
return (
<FrontComponentCard title="host-api:copy-to-clipboard">
<button
data-testid="subject"
type="button"
onClick={handleClick}
style={BUTTON_STYLE}
>
Copy
</button>
<span data-testid="api-status" style={STATUS_STYLE}>
{status}
</span>
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-host-clipboard-00000000-0000-0000-0000-000000000020',
name: 'host-api-copy-to-clipboard-front-component',
description: 'Front component covering copyToClipboard host API',
component: HostApiCopyToClipboardFrontComponent,
});

View File

@@ -0,0 +1,49 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import { AppPath, navigate } from 'twenty-sdk/front-component';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import {
BUTTON_STYLE,
STATUS_STYLE,
} from '@/__stories__/shared/front-components/styles';
const HostApiNavigateFrontComponent = () => {
const [status, setStatus] = useState('idle');
const handleClick = async () => {
try {
await navigate(AppPath.RecordIndexPage, {
objectNamePlural: 'companies',
});
setStatus('navigate:success');
} catch (error) {
setStatus(
`navigate:error:${error instanceof Error ? error.message : String(error)}`,
);
}
};
return (
<FrontComponentCard title="host-api:navigate">
<button
data-testid="subject"
type="button"
onClick={handleClick}
style={BUTTON_STYLE}
>
Navigate
</button>
<span data-testid="api-status" style={STATUS_STYLE}>
{status}
</span>
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-host-navigate-00000000-0000-0000-0000-000000000020',
name: 'host-api-navigate-front-component',
description: 'Front component covering navigate host API',
component: HostApiNavigateFrontComponent,
});

View File

@@ -0,0 +1,55 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import {
BUTTON_STYLE,
STATUS_STYLE,
} from '@/__stories__/shared/front-components/styles';
// Deliberately uses the standard web API rather than the SDK helper: this is
// what a third-party library calls, and it only works if the worker polyfill
// installed navigator.clipboard in the real sandbox realm.
const HostApiNavigatorClipboardFrontComponent = () => {
const [status, setStatus] = useState('idle');
const handleClick = async () => {
try {
if (typeof navigator.clipboard?.writeText !== 'function') {
setStatus('clipboard:missing');
return;
}
await navigator.clipboard.writeText('Hello standard clipboard');
setStatus('clipboard:success');
} catch (error) {
setStatus(
`clipboard:error:${error instanceof Error ? error.message : String(error)}`,
);
}
};
return (
<FrontComponentCard title="host-api:navigator-clipboard">
<button
data-testid="subject"
type="button"
onClick={handleClick}
style={BUTTON_STYLE}
>
Copy
</button>
<span data-testid="api-status" style={STATUS_STYLE}>
{status}
</span>
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-host-clipboard-00000000-0000-0000-0000-000000000021',
name: 'host-api-navigator-clipboard-front-component',
description: 'Front component covering the navigator.clipboard polyfill',
component: HostApiNavigatorClipboardFrontComponent,
});

View File

@@ -0,0 +1,47 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import { updateProgress } from 'twenty-sdk/front-component';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import {
BUTTON_STYLE,
STATUS_STYLE,
} from '@/__stories__/shared/front-components/styles';
const HostApiProgressFrontComponent = () => {
const [status, setStatus] = useState('idle');
const handleClick = async () => {
try {
await updateProgress(50);
setStatus('progress:success');
} catch (error) {
setStatus(
`progress:error:${error instanceof Error ? error.message : String(error)}`,
);
}
};
return (
<FrontComponentCard title="host-api:progress">
<button
data-testid="subject"
type="button"
onClick={handleClick}
style={BUTTON_STYLE}
>
Update Progress
</button>
<span data-testid="api-status" style={STATUS_STYLE}>
{status}
</span>
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-host-progress-00000000-0000-0000-0000-000000000020',
name: 'host-api-progress-front-component',
description: 'Front component covering updateProgress host API',
component: HostApiProgressFrontComponent,
});

View File

@@ -0,0 +1,30 @@
import { defineFrontComponent } from 'twenty-sdk/define';
import { UndecoratedLink } from 'twenty-ui/primitives/navigation';
import { ThemeProvider } from 'twenty-ui/theme-constants';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
// KNOWN ISSUE (TDD): a react-router Link click must route through the host
// navigate API. Today the link cannot even render (no router context in the
// sandbox); once it renders, the click must reach hostApi.navigate — and the
// host must guard native anchor clicks, which otherwise navigate the host
// page before the async worker round-trip can preventDefault.
const HostApiRouterLinkFrontComponent = () => (
<ThemeProvider colorScheme="light">
<FrontComponentCard title="host-api:router-link">
<span data-testid="router-link">
<UndecoratedLink to="/objects/companies">
Go to companies
</UndecoratedLink>
</span>
</FrontComponentCard>
</ThemeProvider>
);
export default defineFrontComponent({
universalIdentifier: 'test-20ui0-0000-0000-0000-000000000113',
name: 'host-api-router-link',
description:
'A front component whose react-router link navigates through the host API',
component: HostApiRouterLinkFrontComponent,
});

View File

@@ -0,0 +1,47 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import { closeSidePanel } from 'twenty-sdk/front-component';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import {
BUTTON_STYLE,
STATUS_STYLE,
} from '@/__stories__/shared/front-components/styles';
const HostApiSidePanelCloseFrontComponent = () => {
const [status, setStatus] = useState('idle');
const handleClick = async () => {
try {
await closeSidePanel();
setStatus('closePanel:success');
} catch (error) {
setStatus(
`closePanel:error:${error instanceof Error ? error.message : String(error)}`,
);
}
};
return (
<FrontComponentCard title="host-api:side-panel:close">
<button
data-testid="subject"
type="button"
onClick={handleClick}
style={BUTTON_STYLE}
>
Close Side Panel
</button>
<span data-testid="api-status" style={STATUS_STYLE}>
{status}
</span>
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-host-spc-00000000-0000-0000-0000-000000000020',
name: 'host-api-side-panel-close-front-component',
description: 'Front component covering closeSidePanel host API',
component: HostApiSidePanelCloseFrontComponent,
});

View File

@@ -0,0 +1,53 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import { AppPath, openSidePanelPage } from 'twenty-sdk/front-component';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import {
BUTTON_STYLE,
STATUS_STYLE,
} from '@/__stories__/shared/front-components/styles';
const HostApiSidePanelOpenFrontComponent = () => {
const [status, setStatus] = useState('idle');
const handleClick = async () => {
try {
await openSidePanelPage({
to: AppPath.RecordShowPage,
params: {
objectNameSingular: 'company',
objectRecordId: 'test-record-id',
},
});
setStatus('sidePanel:success');
} catch (error) {
setStatus(
`sidePanel:error:${error instanceof Error ? error.message : String(error)}`,
);
}
};
return (
<FrontComponentCard title="host-api:side-panel:open">
<button
data-testid="subject"
type="button"
onClick={handleClick}
style={BUTTON_STYLE}
>
Open Side Panel
</button>
<span data-testid="api-status" style={STATUS_STYLE}>
{status}
</span>
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-host-spo-00000000-0000-0000-0000-000000000020',
name: 'host-api-side-panel-open-front-component',
description: 'Front component covering openSidePanelPage host API',
component: HostApiSidePanelOpenFrontComponent,
});

View File

@@ -0,0 +1,50 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import { enqueueSnackbar } from 'twenty-sdk/front-component';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import {
BUTTON_STYLE,
STATUS_STYLE,
} from '@/__stories__/shared/front-components/styles';
const HostApiSnackbarFrontComponent = () => {
const [status, setStatus] = useState('idle');
const handleClick = async () => {
try {
await enqueueSnackbar({
message: 'Test notification',
variant: 'success',
});
setStatus('snackbar:success');
} catch (error) {
setStatus(
`snackbar:error:${error instanceof Error ? error.message : String(error)}`,
);
}
};
return (
<FrontComponentCard title="host-api:snackbar">
<button
data-testid="subject"
type="button"
onClick={handleClick}
style={BUTTON_STYLE}
>
Snackbar
</button>
<span data-testid="api-status" style={STATUS_STYLE}>
{status}
</span>
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-host-snackbar-00000000-0000-0000-0000-000000000020',
name: 'host-api-snackbar-front-component',
description: 'Front component covering enqueueSnackbar host API',
component: HostApiSnackbarFrontComponent,
});

View File

@@ -0,0 +1,112 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import {
BUTTON_STYLE,
STATUS_STYLE,
} from '@/__stories__/shared/front-components/styles';
const OVERSIZED_VALUE = 'x'.repeat(300 * 1024);
const getErrorName = (error: unknown): string =>
error instanceof Error ? error.name : String(error);
const HostApiStorageFrontComponent = () => {
const [status, setStatus] = useState('idle');
const runLocalStorageRoundTrip = () => {
try {
window.localStorage.setItem('greeting', 'hello');
const storedValue = window.localStorage.getItem('greeting');
const storedKey = window.localStorage.key(0);
const storedLength = window.localStorage.length;
window.localStorage.removeItem('greeting');
setStatus(
`storage:local:${storedValue}:${storedKey}:${storedLength}:${window.localStorage.length}`,
);
} catch (error) {
setStatus(`storage:error:${getErrorName(error)}`);
}
};
const runLocalStorageWrite = () => {
try {
window.localStorage.setItem('theme', 'dark');
setStatus(`storage:local-write:${window.localStorage.getItem('theme')}`);
} catch (error) {
setStatus(`storage:error:${getErrorName(error)}`);
}
};
const runSessionStorageRoundTrip = () => {
try {
window.sessionStorage.setItem('visits', '2');
setStatus(`storage:session:${window.sessionStorage.getItem('visits')}`);
} catch (error) {
setStatus(`storage:error:${getErrorName(error)}`);
}
};
const runOversizedWrite = () => {
try {
window.localStorage.setItem('oversized', OVERSIZED_VALUE);
setStatus('storage:local:oversized');
} catch (error) {
setStatus(`storage:error:${getErrorName(error)}`);
}
};
return (
<FrontComponentCard title="host-api:storage">
<button
data-testid="subject"
type="button"
onClick={runLocalStorageRoundTrip}
style={BUTTON_STYLE}
>
Local storage round trip
</button>
<button
data-testid="local-storage-write"
type="button"
onClick={runLocalStorageWrite}
style={BUTTON_STYLE}
>
Local storage write
</button>
<button
data-testid="session-storage"
type="button"
onClick={runSessionStorageRoundTrip}
style={BUTTON_STYLE}
>
Session storage round trip
</button>
<button
data-testid="oversized"
type="button"
onClick={runOversizedWrite}
style={BUTTON_STYLE}
>
Write oversized value
</button>
<span data-testid="api-status" style={STATUS_STYLE}>
{status}
</span>
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-host-storage-00000000-0000-0000-0000-000000000021',
name: 'host-api-storage-front-component',
description: 'Front component covering the window storage shims',
component: HostApiStorageFrontComponent,
});

View File

@@ -0,0 +1,47 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import { unmountFrontComponent } from 'twenty-sdk/front-component';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import {
BUTTON_STYLE,
STATUS_STYLE,
} from '@/__stories__/shared/front-components/styles';
const HostApiUnmountFrontComponent = () => {
const [status, setStatus] = useState('idle');
const handleClick = async () => {
try {
await unmountFrontComponent();
setStatus('unmount:success');
} catch (error) {
setStatus(
`unmount:error:${error instanceof Error ? error.message : String(error)}`,
);
}
};
return (
<FrontComponentCard title="host-api:unmount">
<button
data-testid="subject"
type="button"
onClick={handleClick}
style={BUTTON_STYLE}
>
Unmount
</button>
<span data-testid="api-status" style={STATUS_STYLE}>
{status}
</span>
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-host-unmount-00000000-0000-0000-0000-000000000020',
name: 'host-api-unmount-front-component',
description: 'Front component covering unmountFrontComponent host API',
component: HostApiUnmountFrontComponent,
});

View File

@@ -0,0 +1,60 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { expect, userEvent, waitFor, within } from 'storybook/test';
import { isDefined } from 'twenty-shared/utils';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { expectFrontComponentMounted } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentMounted';
import { runFrontComponentStory } from '@/__stories__/shared/test-utils/runFrontComponentStory';
import {
HOST_API_TIMEOUT,
INTERACTION_TIMEOUT,
} from '@/__stories__/shared/test-utils/timeouts';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HostApi/Navigate',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
type Story = StoryObj<typeof FrontComponentRenderer>;
export const Navigate: Story = runFrontComponentStory({
frontComponentBundleName: 'host-api-navigate',
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const api = args.frontComponentHostCommunicationApi;
if (!isDefined(api)) {
throw new Error('frontComponentHostCommunicationApi is required');
}
await expectFrontComponentMounted(canvas);
const subject = await canvas.findByTestId('subject');
await userEvent.click(subject);
await waitFor(
() => {
expect(api.navigate).toHaveBeenCalled();
},
{ timeout: HOST_API_TIMEOUT },
);
expect(
await canvas.findByText(
'navigate:success',
{},
{ timeout: INTERACTION_TIMEOUT },
),
).toBeVisible();
},
});

View File

@@ -0,0 +1,65 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { expect, userEvent, waitFor, within } from 'storybook/test';
import { isDefined } from 'twenty-shared/utils';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { expectFrontComponentMounted } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentMounted';
import { runFrontComponentStory } from '@/__stories__/shared/test-utils/runFrontComponentStory';
import {
HOST_API_TIMEOUT,
INTERACTION_TIMEOUT,
} from '@/__stories__/shared/test-utils/timeouts';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HostApi/NavigatorClipboard',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
type Story = StoryObj<typeof FrontComponentRenderer>;
// Runs the real sandbox: iframe, worker, and bridge in an actual browser, so
// it fails if the polyfill never reaches the worker realm — which the unit
// tests of the install function cannot detect.
export const NavigatorClipboard: Story = runFrontComponentStory({
frontComponentBundleName: 'host-api-navigator-clipboard',
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const api = args.frontComponentHostCommunicationApi;
if (!isDefined(api)) {
throw new Error('frontComponentHostCommunicationApi is required');
}
await expectFrontComponentMounted(canvas);
const subject = await canvas.findByTestId('subject');
await userEvent.click(subject);
await waitFor(
() => {
expect(api.copyToClipboard).toHaveBeenCalledWith(
'Hello standard clipboard',
);
},
{ timeout: HOST_API_TIMEOUT },
);
expect(
await canvas.findByText(
'clipboard:success',
{},
{ timeout: INTERACTION_TIMEOUT },
),
).toBeVisible();
},
});

View File

@@ -0,0 +1,60 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { expect, userEvent, waitFor, within } from 'storybook/test';
import { isDefined } from 'twenty-shared/utils';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { expectFrontComponentMounted } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentMounted';
import { runFrontComponentStory } from '@/__stories__/shared/test-utils/runFrontComponentStory';
import {
HOST_API_TIMEOUT,
INTERACTION_TIMEOUT,
} from '@/__stories__/shared/test-utils/timeouts';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HostApi/Progress',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
type Story = StoryObj<typeof FrontComponentRenderer>;
export const Progress: Story = runFrontComponentStory({
frontComponentBundleName: 'host-api-progress',
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const api = args.frontComponentHostCommunicationApi;
if (!isDefined(api)) {
throw new Error('frontComponentHostCommunicationApi is required');
}
await expectFrontComponentMounted(canvas);
const subject = await canvas.findByTestId('subject');
await userEvent.click(subject);
await waitFor(
() => {
expect(api.updateProgress).toHaveBeenCalledWith(50);
},
{ timeout: HOST_API_TIMEOUT },
);
expect(
await canvas.findByText(
'progress:success',
{},
{ timeout: INTERACTION_TIMEOUT },
),
).toBeVisible();
},
});

View File

@@ -0,0 +1,53 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { expect, waitFor } from 'storybook/test';
import { isDefined } from 'twenty-shared/utils';
import {
errorHandler,
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { HOST_API_TIMEOUT } from '@/__stories__/shared/test-utils/timeouts';
import { runFrontComponentStory } from '@/__stories__/shared/test-utils/runFrontComponentStory';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HostApi/RouterLink',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
type Story = StoryObj<typeof FrontComponentRenderer>;
// KNOWN ISSUE (TDD) golden test: the fixture's react-router Link crashes at
// render (no router context in the sandbox), so the component errors and the
// host navigate API is never reached. When a router fix lands, flip this play
// to the acceptance assertions: find the link by role, click it, and expect
// hostApi.navigate to receive '/objects/companies' — a MemoryRouter cannot
// pass that (it renders the link but swallows the click into in-memory
// history), and clicking also requires the host to guard native anchor clicks,
// which otherwise navigate the host page away before the async worker
// round-trip can preventDefault.
export const RouterLink: Story = runFrontComponentStory({
frontComponentBundleName: 'host-api-router-link',
play: async ({ args }) => {
const api = args.frontComponentHostCommunicationApi;
if (!isDefined(api)) {
throw new Error('frontComponentHostCommunicationApi is required');
}
await waitFor(
() => {
expect(errorHandler).toHaveBeenCalled();
},
{ timeout: HOST_API_TIMEOUT },
);
expect(api.navigate).not.toHaveBeenCalled();
},
});

View File

@@ -0,0 +1,63 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { expect, userEvent, waitFor, within } from 'storybook/test';
import { isDefined } from 'twenty-shared/utils';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { expectFrontComponentMounted } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentMounted';
import { runFrontComponentStory } from '@/__stories__/shared/test-utils/runFrontComponentStory';
import {
HOST_API_TIMEOUT,
INTERACTION_TIMEOUT,
} from '@/__stories__/shared/test-utils/timeouts';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HostApi/Snackbar',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
type Story = StoryObj<typeof FrontComponentRenderer>;
export const Snackbar: Story = runFrontComponentStory({
frontComponentBundleName: 'host-api-snackbar',
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const api = args.frontComponentHostCommunicationApi;
if (!isDefined(api)) {
throw new Error('frontComponentHostCommunicationApi is required');
}
await expectFrontComponentMounted(canvas);
const subject = await canvas.findByTestId('subject');
await userEvent.click(subject);
await waitFor(
() => {
expect(api.enqueueSnackbar).toHaveBeenCalledWith({
message: 'Test notification',
variant: 'success',
});
},
{ timeout: HOST_API_TIMEOUT },
);
expect(
await canvas.findByText(
'snackbar:success',
{},
{ timeout: INTERACTION_TIMEOUT },
),
).toBeVisible();
},
});

View File

@@ -0,0 +1,138 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { expect, userEvent, waitFor, within } from 'storybook/test';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import { buildFrontComponentStorageNamespace } from '@/host/storage/utils/buildFrontComponentStorageNamespace';
import { clearFrontComponentStorage } from '@/host/storage/utils/clearFrontComponentStorage';
import { deleteFrontComponentStorageItem } from '@/host/storage/utils/deleteFrontComponentStorageItem';
import { setFrontComponentStorageItem } from '@/host/storage/utils/setFrontComponentStorageItem';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
hostApiMocks,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { expectFrontComponentMounted } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentMounted';
import { runFrontComponentStory } from '@/__stories__/shared/test-utils/runFrontComponentStory';
import {
HOST_API_TIMEOUT,
INTERACTION_TIMEOUT,
} from '@/__stories__/shared/test-utils/timeouts';
const PERSISTED_STORAGE_NAMESPACE = buildFrontComponentStorageNamespace({
applicationId: 'story-application-id',
userId: 'story-user-id',
});
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HostApi/Storage',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
type Story = StoryObj<typeof FrontComponentRenderer>;
const roundTripStory = runFrontComponentStory({
frontComponentBundleName: 'host-api-storage',
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expectFrontComponentMounted(canvas);
await userEvent.click(await canvas.findByTestId('subject'));
expect(
await canvas.findByText(
'storage:local:hello:greeting:1:0',
{},
{ timeout: INTERACTION_TIMEOUT },
),
).toBeVisible();
await userEvent.click(await canvas.findByTestId('local-storage-write'));
await userEvent.click(await canvas.findByTestId('session-storage'));
await waitFor(
() => {
expect(
window.localStorage.getItem(`${PERSISTED_STORAGE_NAMESPACE}greeting`),
).toBeNull();
expect(
window.localStorage.getItem(`${PERSISTED_STORAGE_NAMESPACE}theme`),
).toBe('dark');
expect(
window.sessionStorage.getItem(`${PERSISTED_STORAGE_NAMESPACE}visits`),
).toBe('2');
},
{ timeout: HOST_API_TIMEOUT },
);
},
});
const clearPersistedStorageNamespace = () => {
for (const storageType of ['localStorage', 'sessionStorage'] as const) {
clearFrontComponentStorage({
namespace: PERSISTED_STORAGE_NAMESPACE,
storageType,
});
}
};
export const StorageRoundTrip: Story = {
...roundTripStory,
beforeEach: () => {
clearPersistedStorageNamespace();
return clearPersistedStorageNamespace;
},
args: {
...roundTripStory.args,
storageNamespace: PERSISTED_STORAGE_NAMESPACE,
frontComponentHostCommunicationApi: {
...hostApiMocks,
storageSet: async ({ storageType, key, serializedValue }) => {
setFrontComponentStorageItem({
namespace: PERSISTED_STORAGE_NAMESPACE,
storageType,
key,
serializedValue,
});
},
storageDelete: async ({ storageType, key }) => {
deleteFrontComponentStorageItem({
namespace: PERSISTED_STORAGE_NAMESPACE,
storageType,
key,
});
},
storageClear: async ({ storageType }) => {
clearFrontComponentStorage({
namespace: PERSISTED_STORAGE_NAMESPACE,
storageType,
});
},
},
},
};
export const StorageValueTooLarge: Story = runFrontComponentStory({
frontComponentBundleName: 'host-api-storage',
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expectFrontComponentMounted(canvas);
await userEvent.click(await canvas.findByTestId('oversized'));
expect(
await canvas.findByText(
'storage:error:QuotaExceededError',
{},
{ timeout: INTERACTION_TIMEOUT },
),
).toBeVisible();
},
});

View File

@@ -0,0 +1,37 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { FILL_RECT_STYLE } from '@/__stories__/shared/front-components/styles';
const IframeClickFrontComponent = () => {
const [interactionCount, setInteractionCount] = useState(0);
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="iframe:click">
<iframe
data-testid="subject"
title="probe"
onClick={(event) => {
setInteractionCount((previous) => previous + 1);
pushEvent(event);
}}
tabIndex={0}
style={{ ...FILL_RECT_STYLE, height: 80 }}
/>
<span data-testid="front-component-value">{interactionCount}</span>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-iframe-c-click-00000000-0000-0000-0000-000000000020',
name: 'iframe-click-front-component',
description: 'Front component covering click on <iframe>',
component: IframeClickFrontComponent,
});

View File

@@ -0,0 +1,29 @@
import { type Meta } from '@storybook/react-vite';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import {
createHtmlTagClickStory,
createHtmlTagFocusStory,
} from '@/__stories__/shared/test-utils/createHtmlElementStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Embedded/Iframe/Events',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
export const Click = createHtmlTagClickStory({
frontComponentBundleName: 'iframe-click',
});
export const FocusBlur = createHtmlTagFocusStory({
frontComponentBundleName: 'iframe-focus-blur',
});

View File

@@ -0,0 +1,38 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { FILL_RECT_STYLE } from '@/__stories__/shared/front-components/styles';
const IframeFocusBlurFrontComponent = () => {
const [interactionCount, setInteractionCount] = useState(0);
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="iframe:focus-blur">
<iframe
data-testid="subject"
title="probe"
onFocus={(event) => {
setInteractionCount((previous) => previous + 1);
pushEvent(event);
}}
onBlur={(event) => pushEvent(event)}
tabIndex={0}
style={{ ...FILL_RECT_STYLE, height: 80 }}
/>
<span data-testid="front-component-value">{interactionCount}</span>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-iframe-fb-fb-00000000-0000-0000-0000-000000000020',
name: 'iframe-focus-blur-front-component',
description: 'Front component covering focus-blur on <iframe>',
component: IframeFocusBlurFrontComponent,
});

View File

@@ -0,0 +1,21 @@
import { defineFrontComponent } from 'twenty-sdk/define';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { FILL_RECT_STYLE } from '@/__stories__/shared/front-components/styles';
const IframeSandboxDangerousFrontComponent = () => (
<FrontComponentCard title="iframe:sandbox-dangerous">
<iframe
data-testid="subject"
title="probe"
sandbox="allow-scripts allow-same-origin allow-top-navigation allow-popups-to-escape-sandbox"
style={{ ...FILL_RECT_STYLE, height: 80 }}
/>
</FrontComponentCard>
);
export default defineFrontComponent({
universalIdentifier: 'fc-iframe-sb-dgr-00000000-0000-0000-0000-000000000020',
name: 'iframe-sandbox-dangerous-front-component',
description: 'Front component declaring an <iframe> with a dangerous sandbox',
component: IframeSandboxDangerousFrontComponent,
});

View File

@@ -0,0 +1,20 @@
import { defineFrontComponent } from 'twenty-sdk/define';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { FILL_RECT_STYLE } from '@/__stories__/shared/front-components/styles';
const IframeSandboxDefaultFrontComponent = () => (
<FrontComponentCard title="iframe:sandbox-default">
<iframe
data-testid="subject"
title="probe"
style={{ ...FILL_RECT_STYLE, height: 80 }}
/>
</FrontComponentCard>
);
export default defineFrontComponent({
universalIdentifier: 'fc-iframe-sb-def-00000000-0000-0000-0000-000000000020',
name: 'iframe-sandbox-default-front-component',
description: 'Front component declaring an <iframe> without a sandbox value',
component: IframeSandboxDefaultFrontComponent,
});

View File

@@ -0,0 +1,43 @@
import { type Meta } from '@storybook/react-vite';
import { within } from 'storybook/test';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { expectFrontComponentMounted } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentMounted';
import { expectStorybookIframeSandboxSanitized } from '@/__stories__/shared/test-utils/matchers/expectStorybookIframeSandboxSanitized';
import { runFrontComponentStory } from '@/__stories__/shared/test-utils/runFrontComponentStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Embedded/Iframe/Sandbox',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
export const Dangerous = runFrontComponentStory({
frontComponentBundleName: 'iframe-sandbox-dangerous',
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expectFrontComponentMounted(canvas);
await expectStorybookIframeSandboxSanitized({ canvas });
},
});
export const Default = runFrontComponentStory({
frontComponentBundleName: 'iframe-sandbox-default',
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expectFrontComponentMounted(canvas);
await expectStorybookIframeSandboxSanitized({ canvas });
},
});

View File

@@ -0,0 +1,38 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { FILL_RECT_STYLE } from '@/__stories__/shared/front-components/styles';
const ImgClickFrontComponent = () => {
const [interactionCount, setInteractionCount] = useState(0);
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="img:click">
<img
data-testid="subject"
src="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='80' height='40'/>"
alt="probe"
onClick={(event) => {
setInteractionCount((previous) => previous + 1);
pushEvent(event);
}}
tabIndex={0}
style={{ ...FILL_RECT_STYLE, height: 40 }}
/>
<span data-testid="front-component-value">{interactionCount}</span>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-img-c-click-00000000-0000-0000-0000-000000000020',
name: 'img-click-front-component',
description: 'Front component covering click on <img>',
component: ImgClickFrontComponent,
});

View File

@@ -0,0 +1,29 @@
import { type Meta } from '@storybook/react-vite';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import {
createHtmlTagClickStory,
createHtmlTagFocusStory,
} from '@/__stories__/shared/test-utils/createHtmlElementStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Embedded/Img/Events',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
export const Click = createHtmlTagClickStory({
frontComponentBundleName: 'img-click',
});
export const FocusBlur = createHtmlTagFocusStory({
frontComponentBundleName: 'img-focus-blur',
});

View File

@@ -0,0 +1,39 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { FILL_RECT_STYLE } from '@/__stories__/shared/front-components/styles';
const ImgFocusBlurFrontComponent = () => {
const [interactionCount, setInteractionCount] = useState(0);
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="img:focus-blur">
<img
data-testid="subject"
src="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='80' height='40'/>"
alt="probe"
onFocus={(event) => {
setInteractionCount((previous) => previous + 1);
pushEvent(event);
}}
onBlur={(event) => pushEvent(event)}
tabIndex={0}
style={{ ...FILL_RECT_STYLE, height: 40 }}
/>
<span data-testid="front-component-value">{interactionCount}</span>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-img-fb-fb-00000000-0000-0000-0000-000000000020',
name: 'img-focus-blur-front-component',
description: 'Front component covering focus-blur on <img>',
component: ImgFocusBlurFrontComponent,
});

View File

@@ -0,0 +1,27 @@
import { defineFrontComponent } from 'twenty-sdk/define';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { PROPERTY_FIXTURE } from '@/__stories__/shared/front-components/property-fixture';
const ImgPropertiesFrontComponent = () => (
<FrontComponentCard title="img:properties">
<img
data-testid="subject"
id={PROPERTY_FIXTURE.id}
className={PROPERTY_FIXTURE.className}
title={PROPERTY_FIXTURE.title}
role={PROPERTY_FIXTURE.role}
aria-label={PROPERTY_FIXTURE.ariaLabel}
tabIndex={PROPERTY_FIXTURE.tabIndex}
src={PROPERTY_FIXTURE.src}
alt={PROPERTY_FIXTURE.alt}
/>
</FrontComponentCard>
);
export default defineFrontComponent({
universalIdentifier: 'fc-img-props-00000000-0000-0000-0000-000000000020',
name: 'img-properties-front-component',
description: 'Front component covering property reflection on <img>',
component: ImgPropertiesFrontComponent,
});

View File

@@ -0,0 +1,26 @@
import { type Meta } from '@storybook/react-vite';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { createPropertyReflectionStory } from '@/__stories__/shared/test-utils/createPropertyReflectionStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Embedded/Img/Properties',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
export const Properties = createPropertyReflectionStory({
frontComponentBundleName: 'img-properties',
extraAttributes: {
src: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="80" height="40"/>',
alt: 'subject-alt',
},
});

View File

@@ -0,0 +1,42 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { FILL_RECT_STYLE } from '@/__stories__/shared/front-components/styles';
const PictureClickFrontComponent = () => {
const [interactionCount, setInteractionCount] = useState(0);
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="picture:click">
<picture
data-testid="subject"
onClick={(event) => {
setInteractionCount((previous) => previous + 1);
pushEvent(event);
}}
tabIndex={0}
style={FILL_RECT_STYLE}
>
<img
src="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='80' height='40'/>"
alt="probe"
/>
</picture>
<span data-testid="front-component-value">{interactionCount}</span>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier:
'fc-picture-c-click-00000000-0000-0000-0000-000000000020',
name: 'picture-click-front-component',
description: 'Front component covering click on <picture>',
component: PictureClickFrontComponent,
});

View File

@@ -0,0 +1,29 @@
import { type Meta } from '@storybook/react-vite';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import {
createHtmlTagClickStory,
createHtmlTagFocusStory,
} from '@/__stories__/shared/test-utils/createHtmlElementStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Embedded/Picture/Events',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
export const Click = createHtmlTagClickStory({
frontComponentBundleName: 'picture-click',
});
export const FocusBlur = createHtmlTagFocusStory({
frontComponentBundleName: 'picture-focus-blur',
});

View File

@@ -0,0 +1,42 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { FILL_RECT_STYLE } from '@/__stories__/shared/front-components/styles';
const PictureFocusBlurFrontComponent = () => {
const [interactionCount, setInteractionCount] = useState(0);
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="picture:focus-blur">
<picture
data-testid="subject"
onFocus={(event) => {
setInteractionCount((previous) => previous + 1);
pushEvent(event);
}}
onBlur={(event) => pushEvent(event)}
tabIndex={0}
style={FILL_RECT_STYLE}
>
<img
src="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='80' height='40'/>"
alt="probe"
/>
</picture>
<span data-testid="front-component-value">{interactionCount}</span>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-picture-fb-fb-00000000-0000-0000-0000-000000000020',
name: 'picture-focus-blur-front-component',
description: 'Front component covering focus-blur on <picture>',
component: PictureFocusBlurFrontComponent,
});

View File

@@ -0,0 +1,39 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { BUTTON_STYLE } from '@/__stories__/shared/front-components/styles';
const ButtonClickFrontComponent = () => {
const [clickCount, setClickCount] = useState(0);
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="button:click">
<button
data-testid="subject"
type="button"
onClick={(event) => {
setClickCount((previous) => previous + 1);
pushEvent(event);
}}
style={BUTTON_STYLE}
>
Click me
</button>
<span data-testid="front-component-value">{clickCount}</span>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-btn-click-00000000-0000-0000-0000-000000000020',
name: 'button-click-front-component',
description: 'Front component covering click event on <button>',
component: ButtonClickFrontComponent,
});

View File

@@ -0,0 +1,43 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { userEvent, within } from 'storybook/test';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { expectEventLogged } from '@/__stories__/shared/test-utils/matchers/expectEventLogged';
import { expectFrontComponentMounted } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentMounted';
import { expectFrontComponentValue } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentValue';
import { runFrontComponentStory } from '@/__stories__/shared/test-utils/runFrontComponentStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Button/Events',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
type Story = StoryObj<typeof FrontComponentRenderer>;
export const ClickEvent: Story = runFrontComponentStory({
frontComponentBundleName: 'button-click',
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expectFrontComponentMounted(canvas);
const subject = await canvas.findByTestId('subject');
await userEvent.click(subject);
await expectFrontComponentValue({ canvas, expected: '1' });
await userEvent.click(subject);
await expectFrontComponentValue({ canvas, expected: '2' });
await expectEventLogged({ canvas, matcher: { type: 'click' } });
},
});

View File

@@ -0,0 +1,28 @@
import { defineFrontComponent } from 'twenty-sdk/define';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { PROPERTY_FIXTURE } from '@/__stories__/shared/front-components/property-fixture';
const ButtonPropertiesFrontComponent = () => (
<FrontComponentCard title="button:properties">
<button
data-testid="subject"
type="button"
id={PROPERTY_FIXTURE.id}
className={PROPERTY_FIXTURE.className}
title={PROPERTY_FIXTURE.title}
role={PROPERTY_FIXTURE.role}
aria-label={PROPERTY_FIXTURE.ariaLabel}
tabIndex={PROPERTY_FIXTURE.tabIndex}
>
button
</button>
</FrontComponentCard>
);
export default defineFrontComponent({
universalIdentifier: 'fc-btn-props-00000000-0000-0000-0000-000000000020',
name: 'button-properties-front-component',
description: 'Front component covering property reflection on <button>',
component: ButtonPropertiesFrontComponent,
});

View File

@@ -0,0 +1,22 @@
import { type Meta } from '@storybook/react-vite';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { createPropertyReflectionStory } from '@/__stories__/shared/test-utils/createPropertyReflectionStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Button/Properties',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
export const Properties = createPropertyReflectionStory({
frontComponentBundleName: 'button-properties',
});

View File

@@ -0,0 +1,41 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
const DatalistClickFrontComponent = () => {
const [interactionCount, setInteractionCount] = useState(0);
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="datalist:click">
<>
<input list="probe-list" />
<datalist
id="probe-list"
data-testid="subject"
onClick={(event) => {
setInteractionCount((previous) => previous + 1);
pushEvent(event);
}}
>
<option value="alpha" />
<option value="beta" />
</datalist>
</>
<span data-testid="front-component-value">{interactionCount}</span>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier:
'fc-datalist-c-click-00000000-0000-0000-0000-000000000020',
name: 'datalist-click-front-component',
description: 'Front component covering click on <datalist>',
component: DatalistClickFrontComponent,
});

View File

@@ -0,0 +1,22 @@
import { type Meta } from '@storybook/react-vite';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { createHtmlTagClickStory } from '@/__stories__/shared/test-utils/createHtmlElementStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Datalist/Events',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
export const Click = createHtmlTagClickStory({
frontComponentBundleName: 'datalist-click',
});

View File

@@ -0,0 +1,39 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { FILL_RECT_STYLE } from '@/__stories__/shared/front-components/styles';
const FieldsetClickFrontComponent = () => {
const [interactionCount, setInteractionCount] = useState(0);
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="fieldset:click">
<fieldset
data-testid="subject"
onClick={(event) => {
setInteractionCount((previous) => previous + 1);
pushEvent(event);
}}
tabIndex={0}
style={FILL_RECT_STYLE}
>
fieldset
</fieldset>
<span data-testid="front-component-value">{interactionCount}</span>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier:
'fc-fieldset-c-click-00000000-0000-0000-0000-000000000020',
name: 'fieldset-click-front-component',
description: 'Front component covering click on <fieldset>',
component: FieldsetClickFrontComponent,
});

View File

@@ -0,0 +1,29 @@
import { type Meta } from '@storybook/react-vite';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import {
createHtmlTagClickStory,
createHtmlTagFocusStory,
} from '@/__stories__/shared/test-utils/createHtmlElementStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Fieldset/Events',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
export const Click = createHtmlTagClickStory({
frontComponentBundleName: 'fieldset-click',
});
export const FocusBlur = createHtmlTagFocusStory({
frontComponentBundleName: 'fieldset-focus-blur',
});

View File

@@ -0,0 +1,39 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { FILL_RECT_STYLE } from '@/__stories__/shared/front-components/styles';
const FieldsetFocusBlurFrontComponent = () => {
const [interactionCount, setInteractionCount] = useState(0);
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="fieldset:focus-blur">
<fieldset
data-testid="subject"
onFocus={(event) => {
setInteractionCount((previous) => previous + 1);
pushEvent(event);
}}
onBlur={(event) => pushEvent(event)}
tabIndex={0}
style={FILL_RECT_STYLE}
>
fieldset
</fieldset>
<span data-testid="front-component-value">{interactionCount}</span>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-fieldset-fb-fb-00000000-0000-0000-0000-000000000020',
name: 'fieldset-focus-blur-front-component',
description: 'Front component covering focus-blur on <fieldset>',
component: FieldsetFocusBlurFrontComponent,
});

View File

@@ -0,0 +1,27 @@
import { defineFrontComponent } from 'twenty-sdk/define';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { PROPERTY_FIXTURE } from '@/__stories__/shared/front-components/property-fixture';
const FieldsetPropertiesFrontComponent = () => (
<FrontComponentCard title="fieldset:properties">
<fieldset
data-testid="subject"
id={PROPERTY_FIXTURE.id}
className={PROPERTY_FIXTURE.className}
title={PROPERTY_FIXTURE.title}
role={PROPERTY_FIXTURE.role}
aria-label={PROPERTY_FIXTURE.ariaLabel}
tabIndex={PROPERTY_FIXTURE.tabIndex}
>
field
</fieldset>
</FrontComponentCard>
);
export default defineFrontComponent({
universalIdentifier: 'fc-fieldset-props-00000000-0000-0000-0000-000000000020',
name: 'fieldset-properties-front-component',
description: 'Front component covering property reflection on <fieldset>',
component: FieldsetPropertiesFrontComponent,
});

View File

@@ -0,0 +1,22 @@
import { type Meta } from '@storybook/react-vite';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { createPropertyReflectionStory } from '@/__stories__/shared/test-utils/createPropertyReflectionStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Fieldset/Properties',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
export const Properties = createPropertyReflectionStory({
frontComponentBundleName: 'fieldset-properties',
});

View File

@@ -0,0 +1,40 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { userEvent, within } from 'storybook/test';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { expectEventLogged } from '@/__stories__/shared/test-utils/matchers/expectEventLogged';
import { expectFrontComponentMounted } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentMounted';
import { expectFrontComponentValue } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentValue';
import { runFrontComponentStory } from '@/__stories__/shared/test-utils/runFrontComponentStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Form/Events',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
type Story = StoryObj<typeof FrontComponentRenderer>;
export const SubmitEvent: Story = runFrontComponentStory({
frontComponentBundleName: 'form-submit',
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expectFrontComponentMounted(canvas);
const submitButton = await canvas.findByTestId('submit-button');
await userEvent.click(submitButton);
await expectFrontComponentValue({ canvas, expected: 'value-from-form' });
await expectEventLogged({ canvas, matcher: { type: 'submit' } });
},
});

View File

@@ -0,0 +1,27 @@
import { defineFrontComponent } from 'twenty-sdk/define';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { PROPERTY_FIXTURE } from '@/__stories__/shared/front-components/property-fixture';
const FormPropertiesFrontComponent = () => (
<FrontComponentCard title="form:properties">
<form
data-testid="subject"
id={PROPERTY_FIXTURE.id}
className={PROPERTY_FIXTURE.className}
title={PROPERTY_FIXTURE.title}
role={PROPERTY_FIXTURE.role}
aria-label={PROPERTY_FIXTURE.ariaLabel}
tabIndex={PROPERTY_FIXTURE.tabIndex}
>
form
</form>
</FrontComponentCard>
);
export default defineFrontComponent({
universalIdentifier: 'fc-form-props-00000000-0000-0000-0000-000000000020',
name: 'form-properties-front-component',
description: 'Front component covering property reflection on <form>',
component: FormPropertiesFrontComponent,
});

View File

@@ -0,0 +1,22 @@
import { type Meta } from '@storybook/react-vite';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { createPropertyReflectionStory } from '@/__stories__/shared/test-utils/createPropertyReflectionStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Form/Properties',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
export const Properties = createPropertyReflectionStory({
frontComponentBundleName: 'form-properties',
});

View File

@@ -0,0 +1,53 @@
import { type FormEvent, useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import {
BUTTON_STYLE,
INPUT_STYLE,
} from '@/__stories__/shared/front-components/styles';
const FormSubmitFrontComponent = () => {
const [fieldValue, setFieldValue] = useState('value-from-form');
const [submittedValue, setSubmittedValue] = useState<string | null>(null);
const { entries, pushEvent } = useEventLog();
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
setSubmittedValue(fieldValue);
pushEvent(event);
};
return (
<FrontComponentCard title="form:submit">
<form data-testid="subject" onSubmit={handleSubmit}>
<input
data-testid="form-field"
type="text"
name="field"
value={fieldValue}
onChange={(event) => setFieldValue(event.target.value)}
style={INPUT_STYLE}
/>
<button data-testid="submit-button" type="submit" style={BUTTON_STYLE}>
Submit
</button>
</form>
<span data-testid="front-component-value">
{submittedValue ?? 'none'}
</span>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-form-submit-00000000-0000-0000-0000-000000000020',
name: 'form-submit-front-component',
description: 'Front component covering submit event on <form>',
component: FormSubmitFrontComponent,
});

View File

@@ -0,0 +1,38 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import {
INPUT_STYLE,
LABEL_STYLE,
SUBJECT_WRAPPER_STYLE,
} from '@/__stories__/shared/front-components/styles';
const CARET_INITIAL_VALUE = 'Hello world';
const InputCaretFrontComponent = () => {
const [value, setValue] = useState(CARET_INITIAL_VALUE);
return (
<FrontComponentCard title="input:text:caret">
<div style={SUBJECT_WRAPPER_STYLE}>
<label style={LABEL_STYLE}>Text input (pre-filled)</label>
<input
data-testid="subject"
type="text"
value={value}
onChange={(event) => setValue(event.target.value)}
style={INPUT_STYLE}
/>
<span data-testid="front-component-value">{value}</span>
</div>
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-input-caret-00000000-0000-0000-0000-000000000020',
name: 'input-caret-front-component',
description: 'Front component covering caret behavior on <input type="text">',
component: InputCaretFrontComponent,
});

View File

@@ -0,0 +1,61 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { expect, userEvent, waitFor, within } from 'storybook/test';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { expectFrontComponentMounted } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentMounted';
import { runFrontComponentStory } from '@/__stories__/shared/test-utils/runFrontComponentStory';
import {
INTERACTION_TIMEOUT,
TYPING_TIMEOUT,
} from '@/__stories__/shared/test-utils/timeouts';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Input/Caret',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
type Story = StoryObj<typeof FrontComponentRenderer>;
export const CaretPreservedMidString: Story = runFrontComponentStory({
frontComponentBundleName: 'input-caret',
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expectFrontComponentMounted(canvas);
const subject = (await canvas.findByTestId('subject')) as HTMLInputElement;
await waitFor(
() => {
expect(subject.value).toBe('Hello world');
},
{ timeout: INTERACTION_TIMEOUT },
);
subject.focus();
subject.setSelectionRange(4, 4);
await userEvent.keyboard('X');
await waitFor(
() => {
expect(subject.value).toBe('HellXo world');
expect(canvas.getByTestId('front-component-value').textContent).toBe(
'HellXo world',
);
expect(subject.selectionStart).toBe(5);
expect(subject.selectionEnd).toBe(5);
},
{ timeout: TYPING_TIMEOUT },
);
},
});

View File

@@ -0,0 +1,43 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import {
LABEL_STYLE,
ROW_STYLE,
} from '@/__stories__/shared/front-components/styles';
const InputCheckboxFrontComponent = () => {
const [checked, setChecked] = useState(false);
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="input:checkbox:checked">
<div style={ROW_STYLE}>
<input
data-testid="subject"
type="checkbox"
checked={checked}
onChange={(event) => {
setChecked(event.target.checked);
pushEvent(event);
}}
/>
<label style={LABEL_STYLE}>Check me</label>
<span data-testid="front-component-value">{String(checked)}</span>
</div>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-input-checkbox-00000000-0000-0000-0000-000000000020',
name: 'input-checkbox-front-component',
description: 'Front component covering <input type="checkbox">',
component: InputCheckboxFrontComponent,
});

View File

@@ -0,0 +1,51 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { userEvent, within } from 'storybook/test';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { expectEventLogged } from '@/__stories__/shared/test-utils/matchers/expectEventLogged';
import { expectFrontComponentMounted } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentMounted';
import { expectFrontComponentValue } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentValue';
import { runFrontComponentStory } from '@/__stories__/shared/test-utils/runFrontComponentStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Input/Checkbox',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
type Story = StoryObj<typeof FrontComponentRenderer>;
export const CheckedRoundTrip: Story = runFrontComponentStory({
frontComponentBundleName: 'input-checkbox',
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expectFrontComponentMounted(canvas);
const subject = await canvas.findByTestId('subject');
await userEvent.click(subject);
await expectFrontComponentValue({ canvas, expected: 'true' });
await expectEventLogged({
canvas,
matcher: { type: 'change', checked: true },
});
await userEvent.click(subject);
await expectFrontComponentValue({ canvas, expected: 'false' });
await expectEventLogged({
canvas,
matcher: { type: 'change', checked: false },
});
},
});

View File

@@ -0,0 +1,41 @@
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import {
INPUT_STYLE,
LABEL_STYLE,
SUBJECT_WRAPPER_STYLE,
} from '@/__stories__/shared/front-components/styles';
const InputClipboardFrontComponent = () => {
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="input:text:clipboard">
<div style={SUBJECT_WRAPPER_STYLE}>
<label style={LABEL_STYLE}>Clipboard input</label>
<input
data-testid="subject"
type="text"
onPaste={pushEvent}
onCopy={pushEvent}
onCut={pushEvent}
style={INPUT_STYLE}
/>
</div>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier:
'fc-input-clipboard-00000000-0000-0000-0000-000000000020',
name: 'input-clipboard-front-component',
description: 'Front component covering clipboard events on <input>',
component: InputClipboardFrontComponent,
});

View File

@@ -0,0 +1,69 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { userEvent, within } from 'storybook/test';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { expectEventLogged } from '@/__stories__/shared/test-utils/matchers/expectEventLogged';
import { expectFrontComponentMounted } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentMounted';
import { runFrontComponentStory } from '@/__stories__/shared/test-utils/runFrontComponentStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Input/Clipboard',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
type Story = StoryObj<typeof FrontComponentRenderer>;
export const Paste: Story = runFrontComponentStory({
frontComponentBundleName: 'input-clipboard',
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expectFrontComponentMounted(canvas);
const subject = await canvas.findByTestId('subject');
await userEvent.click(subject);
await userEvent.paste('hello-clipboard');
await expectEventLogged({
canvas,
matcher: { type: 'paste', clipboardText: 'hello-clipboard' },
});
},
});
export const CopyAndCutStayPrivate: Story = runFrontComponentStory({
frontComponentBundleName: 'input-clipboard',
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expectFrontComponentMounted(canvas);
const subject = await canvas.findByTestId('subject');
await userEvent.click(subject);
await userEvent.type(subject, 'secret');
(subject as HTMLInputElement).select();
await userEvent.copy();
await expectEventLogged({
canvas,
matcher: { type: 'copy', clipboardText: undefined },
});
await userEvent.cut();
await expectEventLogged({
canvas,
matcher: { type: 'cut', clipboardText: undefined },
});
},
});

View File

@@ -0,0 +1,42 @@
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import {
INPUT_STYLE,
LABEL_STYLE,
SUBJECT_WRAPPER_STYLE,
} from '@/__stories__/shared/front-components/styles';
const InputEditingFrontComponent = () => {
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="input:text:editing">
<div style={SUBJECT_WRAPPER_STYLE}>
<label style={LABEL_STYLE}>Editing input</label>
<input
data-testid="subject"
type="text"
onBeforeInput={pushEvent}
onCompositionStart={pushEvent}
onCompositionUpdate={pushEvent}
onCompositionEnd={pushEvent}
style={INPUT_STYLE}
/>
</div>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-input-editing-00000000-0000-0000-0000-000000000020',
name: 'input-editing-front-component',
description:
'Front component covering beforeinput and composition events on <input>',
component: InputEditingFrontComponent,
});

View File

@@ -0,0 +1,90 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { userEvent, within } from 'storybook/test';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { expectEventLogged } from '@/__stories__/shared/test-utils/matchers/expectEventLogged';
import { expectFrontComponentMounted } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentMounted';
import { runFrontComponentStory } from '@/__stories__/shared/test-utils/runFrontComponentStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Input/Editing',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
type Story = StoryObj<typeof FrontComponentRenderer>;
export const BeforeInputInsert: Story = runFrontComponentStory({
frontComponentBundleName: 'input-editing',
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expectFrontComponentMounted(canvas);
const subject = await canvas.findByTestId('subject');
await userEvent.type(subject, 'a');
await expectEventLogged({
canvas,
matcher: { type: 'beforeinput', inputType: 'insertText', data: 'a' },
});
},
});
export const BeforeInputDelete: Story = runFrontComponentStory({
frontComponentBundleName: 'input-editing',
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expectFrontComponentMounted(canvas);
const subject = await canvas.findByTestId('subject');
await userEvent.type(subject, 'ab{backspace}');
await expectEventLogged({
canvas,
matcher: { type: 'beforeinput', inputType: 'deleteContentBackward' },
});
},
});
export const Composition: Story = runFrontComponentStory({
frontComponentBundleName: 'input-editing',
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expectFrontComponentMounted(canvas);
const subject = await canvas.findByTestId('subject');
subject.dispatchEvent(
new CompositionEvent('compositionstart', { bubbles: true }),
);
subject.dispatchEvent(
new CompositionEvent('compositionupdate', { data: 'か', bubbles: true }),
);
subject.dispatchEvent(
new CompositionEvent('compositionend', { data: 'か', bubbles: true }),
);
await expectEventLogged({ canvas, matcher: { type: 'compositionstart' } });
await expectEventLogged({
canvas,
matcher: { type: 'compositionupdate', data: 'か' },
});
await expectEventLogged({
canvas,
matcher: { type: 'compositionend', data: 'か' },
});
},
});

View File

@@ -0,0 +1,63 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { userEvent, within } from 'storybook/test';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { expectEventLogged } from '@/__stories__/shared/test-utils/matchers/expectEventLogged';
import { expectFrontComponentMounted } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentMounted';
import { expectFrontComponentValue } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentValue';
import { runFrontComponentStory } from '@/__stories__/shared/test-utils/runFrontComponentStory';
import { TYPING_DELAY } from '@/__stories__/shared/test-utils/timeouts';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Input/Events',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
type Story = StoryObj<typeof FrontComponentRenderer>;
export const TextValueRoundTrip: Story = runFrontComponentStory({
frontComponentBundleName: 'input-text-value',
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expectFrontComponentMounted(canvas);
const subject = await canvas.findByTestId('subject');
await userEvent.type(subject, 'hello', { delay: TYPING_DELAY });
await expectFrontComponentValue({ canvas, expected: 'hello' });
await expectEventLogged({
canvas,
matcher: { type: 'change', value: 'hello' },
});
},
});
export const FocusBlurEvents: Story = runFrontComponentStory({
frontComponentBundleName: 'input-text-focus-blur',
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expectFrontComponentMounted(canvas);
const subject = await canvas.findByTestId('subject');
await userEvent.click(subject);
await expectEventLogged({ canvas, matcher: { type: 'focus' } });
const blurTarget = await canvas.findByTestId('blur-target');
await userEvent.click(blurTarget);
await expectEventLogged({ canvas, matcher: { type: 'blur' } });
},
});

View File

@@ -0,0 +1,39 @@
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import {
LABEL_STYLE,
SUBJECT_WRAPPER_STYLE,
} from '@/__stories__/shared/front-components/styles';
const InputFileMultipleFrontComponent = () => {
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="input:file:multiple">
<div style={SUBJECT_WRAPPER_STYLE}>
<label style={LABEL_STYLE}>Multiple files</label>
<input
data-testid="subject"
type="file"
multiple
accept="image/*"
onChange={(event) => pushEvent(event)}
/>
</div>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier:
'fc-input-file-multiple-00000000-0000-0000-0000-000000000020',
name: 'input-file-multiple-front-component',
description: 'Front component covering multi-file <input type="file">',
component: InputFileMultipleFrontComponent,
});

View File

@@ -0,0 +1,37 @@
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import {
LABEL_STYLE,
SUBJECT_WRAPPER_STYLE,
} from '@/__stories__/shared/front-components/styles';
const InputFileSingleFrontComponent = () => {
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="input:file:single">
<div style={SUBJECT_WRAPPER_STYLE}>
<label style={LABEL_STYLE}>Single file</label>
<input
data-testid="subject"
type="file"
onChange={(event) => pushEvent(event)}
/>
</div>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier:
'fc-input-file-single-00000000-0000-0000-0000-000000000020',
name: 'input-file-single-front-component',
description: 'Front component covering single-file <input type="file">',
component: InputFileSingleFrontComponent,
});

View File

@@ -0,0 +1,76 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { userEvent, within } from 'storybook/test';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { expectEventLogged } from '@/__stories__/shared/test-utils/matchers/expectEventLogged';
import { expectFrontComponentMounted } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentMounted';
import { runFrontComponentStory } from '@/__stories__/shared/test-utils/runFrontComponentStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Input/File',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
type Story = StoryObj<typeof FrontComponentRenderer>;
export const SingleFile: Story = runFrontComponentStory({
frontComponentBundleName: 'input-file-single',
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expectFrontComponentMounted(canvas);
const subject = (await canvas.findByTestId('subject')) as HTMLInputElement;
const file = new File(['hello world'], 'hello.txt', {
type: 'text/plain',
lastModified: 1700000000000,
});
await userEvent.upload(subject, file);
await expectEventLogged({
canvas,
matcher: {
type: 'change',
files: [{ name: 'hello.txt', type: 'text/plain' }],
},
});
},
});
export const MultipleFiles: Story = runFrontComponentStory({
frontComponentBundleName: 'input-file-multiple',
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expectFrontComponentMounted(canvas);
const subject = (await canvas.findByTestId('subject')) as HTMLInputElement;
const first = new File(['a'], 'one.png', { type: 'image/png' });
const second = new File(['bb'], 'two.png', { type: 'image/png' });
await userEvent.upload(subject, [first, second]);
await expectEventLogged({
canvas,
matcher: {
type: 'change',
files: [
{ name: 'one.png', type: 'image/png' },
{ name: 'two.png', type: 'image/png' },
],
},
});
},
});

View File

@@ -0,0 +1,45 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import {
INPUT_STYLE,
LABEL_STYLE,
SUBJECT_WRAPPER_STYLE,
} from '@/__stories__/shared/front-components/styles';
const InputKeyboardFrontComponent = () => {
const [lastKey, setLastKey] = useState('');
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="input:text:keyboard">
<div style={SUBJECT_WRAPPER_STYLE}>
<label style={LABEL_STYLE}>Keyboard input</label>
<input
data-testid="subject"
type="text"
onKeyDown={(event) => {
setLastKey(event.key);
pushEvent(event);
}}
onKeyUp={(event) => pushEvent(event)}
style={INPUT_STYLE}
/>
<span data-testid="front-component-value">{lastKey}</span>
</div>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-input-keyboard-00000000-0000-0000-0000-000000000020',
name: 'input-keyboard-front-component',
description: 'Front component covering keyboard events on <input>',
component: InputKeyboardFrontComponent,
});

View File

@@ -0,0 +1,65 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { userEvent, within } from 'storybook/test';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { expectEventLogged } from '@/__stories__/shared/test-utils/matchers/expectEventLogged';
import { expectFrontComponentMounted } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentMounted';
import { runFrontComponentStory } from '@/__stories__/shared/test-utils/runFrontComponentStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Input/Keyboard',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
type Story = StoryObj<typeof FrontComponentRenderer>;
export const BasicKey: Story = runFrontComponentStory({
frontComponentBundleName: 'input-keyboard',
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expectFrontComponentMounted(canvas);
const subject = await canvas.findByTestId('subject');
await userEvent.click(subject);
await userEvent.keyboard('a');
await expectEventLogged({
canvas,
matcher: { type: 'keydown', key: 'a', code: 'KeyA' },
});
await expectEventLogged({
canvas,
matcher: { type: 'keyup', key: 'a', code: 'KeyA' },
});
},
});
export const ShiftModifier: Story = runFrontComponentStory({
frontComponentBundleName: 'input-keyboard',
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expectFrontComponentMounted(canvas);
const subject = await canvas.findByTestId('subject');
await userEvent.click(subject);
await userEvent.keyboard('{Shift>}b{/Shift}');
await expectEventLogged({
canvas,
matcher: { type: 'keydown', shiftKey: true },
});
},
});

View File

@@ -0,0 +1,33 @@
import { defineFrontComponent } from 'twenty-sdk/define';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { PROPERTY_FIXTURE } from '@/__stories__/shared/front-components/property-fixture';
const noop = () => undefined;
const InputNumberPropertiesFrontComponent = () => (
<FrontComponentCard title="input:number:properties">
<input
data-testid="subject"
id={PROPERTY_FIXTURE.id}
className={PROPERTY_FIXTURE.className}
title={PROPERTY_FIXTURE.title}
role={PROPERTY_FIXTURE.role}
aria-label={PROPERTY_FIXTURE.ariaLabel}
tabIndex={PROPERTY_FIXTURE.tabIndex}
type="number"
name={PROPERTY_FIXTURE.name}
value={String(PROPERTY_FIXTURE.numberValue)}
onChange={noop}
/>
</FrontComponentCard>
);
export default defineFrontComponent({
universalIdentifier:
'fc-input-num-props-00000000-0000-0000-0000-000000000020',
name: 'input-number-properties-front-component',
description:
'Front component covering property reflection on <input type="number">',
component: InputNumberPropertiesFrontComponent,
});

View File

@@ -0,0 +1,38 @@
import { type Meta } from '@storybook/react-vite';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import { PROPERTY_FIXTURE } from '@/__stories__/shared/front-components/property-fixture';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { createPropertyReflectionStory } from '@/__stories__/shared/test-utils/createPropertyReflectionStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Input/Properties',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
export const Text = createPropertyReflectionStory({
frontComponentBundleName: 'input-text-properties',
extraAttributes: {
type: PROPERTY_FIXTURE.type,
name: PROPERTY_FIXTURE.name,
placeholder: PROPERTY_FIXTURE.placeholder,
},
extraProperties: { value: PROPERTY_FIXTURE.textValue },
});
export const Number = createPropertyReflectionStory({
frontComponentBundleName: 'input-number-properties',
extraAttributes: {
type: 'number',
name: PROPERTY_FIXTURE.name,
},
extraProperties: { value: String(PROPERTY_FIXTURE.numberValue) },
});

View File

@@ -0,0 +1,40 @@
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import {
INPUT_STYLE,
LABEL_STYLE,
SUBJECT_WRAPPER_STYLE,
} from '@/__stories__/shared/front-components/styles';
const InputTextFocusBlurFrontComponent = () => {
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="input:text:focus-blur">
<div style={SUBJECT_WRAPPER_STYLE}>
<label style={LABEL_STYLE}>Focus / Blur</label>
<input
data-testid="subject"
type="text"
onFocus={(event) => pushEvent(event)}
onBlur={(event) => pushEvent(event)}
style={INPUT_STYLE}
/>
</div>
<input data-testid="blur-target" type="text" style={INPUT_STYLE} />
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-input-text-fb-00000000-0000-0000-0000-000000000020',
name: 'input-text-focus-blur-front-component',
description: 'Front component covering focus/blur on text <input>',
component: InputTextFocusBlurFrontComponent,
});

View File

@@ -0,0 +1,34 @@
import { defineFrontComponent } from 'twenty-sdk/define';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { PROPERTY_FIXTURE } from '@/__stories__/shared/front-components/property-fixture';
const noop = () => undefined;
const InputTextPropertiesFrontComponent = () => (
<FrontComponentCard title="input:text:properties">
<input
data-testid="subject"
id={PROPERTY_FIXTURE.id}
className={PROPERTY_FIXTURE.className}
title={PROPERTY_FIXTURE.title}
role={PROPERTY_FIXTURE.role}
aria-label={PROPERTY_FIXTURE.ariaLabel}
tabIndex={PROPERTY_FIXTURE.tabIndex}
type={PROPERTY_FIXTURE.type}
name={PROPERTY_FIXTURE.name}
placeholder={PROPERTY_FIXTURE.placeholder}
value={PROPERTY_FIXTURE.textValue}
onChange={noop}
/>
</FrontComponentCard>
);
export default defineFrontComponent({
universalIdentifier:
'fc-input-text-props-00000000-0000-0000-0000-000000000020',
name: 'input-text-properties-front-component',
description:
'Front component covering property reflection on <input type="text">',
component: InputTextPropertiesFrontComponent,
});

View File

@@ -0,0 +1,47 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import {
INPUT_STYLE,
LABEL_STYLE,
SUBJECT_WRAPPER_STYLE,
} from '@/__stories__/shared/front-components/styles';
const InputTextValueFrontComponent = () => {
const [value, setValue] = useState('');
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="input:text:value">
<div style={SUBJECT_WRAPPER_STYLE}>
<label style={LABEL_STYLE}>Text input</label>
<input
data-testid="subject"
type="text"
placeholder="Type here..."
value={value}
onChange={(event) => {
setValue(event.target.value);
pushEvent(event);
}}
style={INPUT_STYLE}
/>
<span data-testid="front-component-value">{value}</span>
</div>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier:
'fc-input-text-value-00000000-0000-0000-0000-000000000020',
name: 'input-text-value-front-component',
description: 'Front component covering <input type="text"> value round-trip',
component: InputTextValueFrontComponent,
});

View File

@@ -0,0 +1,38 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { FILL_INLINE_STYLE } from '@/__stories__/shared/front-components/styles';
const LabelClickFrontComponent = () => {
const [interactionCount, setInteractionCount] = useState(0);
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="label:click">
<label
data-testid="subject"
onClick={(event) => {
setInteractionCount((previous) => previous + 1);
pushEvent(event);
}}
tabIndex={0}
style={FILL_INLINE_STYLE}
>
label
</label>
<span data-testid="front-component-value">{interactionCount}</span>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-label-c-click-00000000-0000-0000-0000-000000000020',
name: 'label-click-front-component',
description: 'Front component covering click on <label>',
component: LabelClickFrontComponent,
});

View File

@@ -0,0 +1,29 @@
import { type Meta } from '@storybook/react-vite';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import {
createHtmlTagClickStory,
createHtmlTagFocusStory,
} from '@/__stories__/shared/test-utils/createHtmlElementStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Label/Events',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
export const Click = createHtmlTagClickStory({
frontComponentBundleName: 'label-click',
});
export const FocusBlur = createHtmlTagFocusStory({
frontComponentBundleName: 'label-focus-blur',
});

View File

@@ -0,0 +1,39 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { FILL_INLINE_STYLE } from '@/__stories__/shared/front-components/styles';
const LabelFocusBlurFrontComponent = () => {
const [interactionCount, setInteractionCount] = useState(0);
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="label:focus-blur">
<label
data-testid="subject"
onFocus={(event) => {
setInteractionCount((previous) => previous + 1);
pushEvent(event);
}}
onBlur={(event) => pushEvent(event)}
tabIndex={0}
style={FILL_INLINE_STYLE}
>
label
</label>
<span data-testid="front-component-value">{interactionCount}</span>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-label-fb-fb-00000000-0000-0000-0000-000000000020',
name: 'label-focus-blur-front-component',
description: 'Front component covering focus-blur on <label>',
component: LabelFocusBlurFrontComponent,
});

View File

@@ -0,0 +1,27 @@
import { defineFrontComponent } from 'twenty-sdk/define';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { PROPERTY_FIXTURE } from '@/__stories__/shared/front-components/property-fixture';
const LabelPropertiesFrontComponent = () => (
<FrontComponentCard title="label:properties">
<label
data-testid="subject"
id={PROPERTY_FIXTURE.id}
className={PROPERTY_FIXTURE.className}
title={PROPERTY_FIXTURE.title}
role={PROPERTY_FIXTURE.role}
aria-label={PROPERTY_FIXTURE.ariaLabel}
tabIndex={PROPERTY_FIXTURE.tabIndex}
>
content
</label>
</FrontComponentCard>
);
export default defineFrontComponent({
universalIdentifier: 'fc-label-props-00000000-0000-0000-0000-000000000020',
name: 'label-properties-front-component',
description: 'Front component covering property reflection on <label>',
component: LabelPropertiesFrontComponent,
});

View File

@@ -0,0 +1,22 @@
import { type Meta } from '@storybook/react-vite';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { createPropertyReflectionStory } from '@/__stories__/shared/test-utils/createPropertyReflectionStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Label/Properties',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
export const Properties = createPropertyReflectionStory({
frontComponentBundleName: 'label-properties',
});

View File

@@ -0,0 +1,40 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { FILL_INLINE_STYLE } from '@/__stories__/shared/front-components/styles';
const LegendClickFrontComponent = () => {
const [interactionCount, setInteractionCount] = useState(0);
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="legend:click">
<fieldset>
<legend
data-testid="subject"
onClick={(event) => {
setInteractionCount((previous) => previous + 1);
pushEvent(event);
}}
tabIndex={0}
style={FILL_INLINE_STYLE}
>
legend
</legend>
</fieldset>
<span data-testid="front-component-value">{interactionCount}</span>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-legend-c-click-00000000-0000-0000-0000-000000000020',
name: 'legend-click-front-component',
description: 'Front component covering click on <legend>',
component: LegendClickFrontComponent,
});

View File

@@ -0,0 +1,29 @@
import { type Meta } from '@storybook/react-vite';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import {
createHtmlTagClickStory,
createHtmlTagFocusStory,
} from '@/__stories__/shared/test-utils/createHtmlElementStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Legend/Events',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
export const Click = createHtmlTagClickStory({
frontComponentBundleName: 'legend-click',
});
export const FocusBlur = createHtmlTagFocusStory({
frontComponentBundleName: 'legend-focus-blur',
});

View File

@@ -0,0 +1,41 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { FILL_INLINE_STYLE } from '@/__stories__/shared/front-components/styles';
const LegendFocusBlurFrontComponent = () => {
const [interactionCount, setInteractionCount] = useState(0);
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="legend:focus-blur">
<fieldset>
<legend
data-testid="subject"
onFocus={(event) => {
setInteractionCount((previous) => previous + 1);
pushEvent(event);
}}
onBlur={(event) => pushEvent(event)}
tabIndex={0}
style={FILL_INLINE_STYLE}
>
legend
</legend>
</fieldset>
<span data-testid="front-component-value">{interactionCount}</span>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-legend-fb-fb-00000000-0000-0000-0000-000000000020',
name: 'legend-focus-blur-front-component',
description: 'Front component covering focus-blur on <legend>',
component: LegendFocusBlurFrontComponent,
});

View File

@@ -0,0 +1,39 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { FILL_INLINE_STYLE } from '@/__stories__/shared/front-components/styles';
const MeterClickFrontComponent = () => {
const [interactionCount, setInteractionCount] = useState(0);
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="meter:click">
<meter
data-testid="subject"
value={0.5}
min={0}
max={1}
onClick={(event) => {
setInteractionCount((previous) => previous + 1);
pushEvent(event);
}}
tabIndex={0}
style={FILL_INLINE_STYLE}
/>
<span data-testid="front-component-value">{interactionCount}</span>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-meter-c-click-00000000-0000-0000-0000-000000000020',
name: 'meter-click-front-component',
description: 'Front component covering click on <meter>',
component: MeterClickFrontComponent,
});

View File

@@ -0,0 +1,29 @@
import { type Meta } from '@storybook/react-vite';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import {
createHtmlTagClickStory,
createHtmlTagFocusStory,
} from '@/__stories__/shared/test-utils/createHtmlElementStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Meter/Events',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
export const Click = createHtmlTagClickStory({
frontComponentBundleName: 'meter-click',
});
export const FocusBlur = createHtmlTagFocusStory({
frontComponentBundleName: 'meter-focus-blur',
});

View File

@@ -0,0 +1,40 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { FILL_INLINE_STYLE } from '@/__stories__/shared/front-components/styles';
const MeterFocusBlurFrontComponent = () => {
const [interactionCount, setInteractionCount] = useState(0);
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="meter:focus-blur">
<meter
data-testid="subject"
value={0.5}
min={0}
max={1}
onFocus={(event) => {
setInteractionCount((previous) => previous + 1);
pushEvent(event);
}}
onBlur={(event) => pushEvent(event)}
tabIndex={0}
style={FILL_INLINE_STYLE}
/>
<span data-testid="front-component-value">{interactionCount}</span>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-meter-fb-fb-00000000-0000-0000-0000-000000000020',
name: 'meter-focus-blur-front-component',
description: 'Front component covering focus-blur on <meter>',
component: MeterFocusBlurFrontComponent,
});

View File

@@ -0,0 +1,28 @@
import { defineFrontComponent } from 'twenty-sdk/define';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { PROPERTY_FIXTURE } from '@/__stories__/shared/front-components/property-fixture';
const MeterPropertiesFrontComponent = () => (
<FrontComponentCard title="meter:properties">
<meter
data-testid="subject"
id={PROPERTY_FIXTURE.id}
className={PROPERTY_FIXTURE.className}
title={PROPERTY_FIXTURE.title}
role={PROPERTY_FIXTURE.role}
aria-label={PROPERTY_FIXTURE.ariaLabel}
tabIndex={PROPERTY_FIXTURE.tabIndex}
value={0.5}
min={0}
max={1}
/>
</FrontComponentCard>
);
export default defineFrontComponent({
universalIdentifier: 'fc-meter-props-00000000-0000-0000-0000-000000000020',
name: 'meter-properties-front-component',
description: 'Front component covering property reflection on <meter>',
component: MeterPropertiesFrontComponent,
});

View File

@@ -0,0 +1,27 @@
import { type Meta } from '@storybook/react-vite';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { createPropertyReflectionStory } from '@/__stories__/shared/test-utils/createPropertyReflectionStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Meter/Properties',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
export const Properties = createPropertyReflectionStory({
frontComponentBundleName: 'meter-properties',
extraAttributes: {
value: '0.5',
min: '0',
max: '1',
},
});

View File

@@ -0,0 +1,39 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
const OptgroupClickFrontComponent = () => {
const [interactionCount, setInteractionCount] = useState(0);
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="optgroup:click">
<select defaultValue="x">
<optgroup
data-testid="subject"
label="group"
onClick={(event) => {
setInteractionCount((previous) => previous + 1);
pushEvent(event);
}}
>
<option value="x">inside</option>
</optgroup>
</select>
<span data-testid="front-component-value">{interactionCount}</span>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier:
'fc-optgroup-c-click-00000000-0000-0000-0000-000000000020',
name: 'optgroup-click-front-component',
description: 'Front component covering click on <optgroup>',
component: OptgroupClickFrontComponent,
});

View File

@@ -0,0 +1,22 @@
import { type Meta } from '@storybook/react-vite';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { createHtmlTagClickStory } from '@/__stories__/shared/test-utils/createHtmlElementStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Optgroup/Events',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
export const Click = createHtmlTagClickStory({
frontComponentBundleName: 'optgroup-click',
});

View File

@@ -0,0 +1,41 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
const OptionClickFrontComponent = () => {
const [interactionCount, setInteractionCount] = useState(0);
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="option:click">
<select defaultValue="alpha">
<>
<option
data-testid="subject"
value="alpha"
onClick={(event) => {
setInteractionCount((previous) => previous + 1);
pushEvent(event);
}}
>
alpha
</option>
<option value="beta">beta</option>
</>
</select>
<span data-testid="front-component-value">{interactionCount}</span>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-option-c-click-00000000-0000-0000-0000-000000000020',
name: 'option-click-front-component',
description: 'Front component covering click on <option>',
component: OptionClickFrontComponent,
});

View File

@@ -0,0 +1,22 @@
import { type Meta } from '@storybook/react-vite';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { createHtmlTagClickStory } from '@/__stories__/shared/test-utils/createHtmlElementStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Option/Events',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
export const Click = createHtmlTagClickStory({
frontComponentBundleName: 'option-click',
});

View File

@@ -0,0 +1,38 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { FILL_INLINE_STYLE } from '@/__stories__/shared/front-components/styles';
const OutputClickFrontComponent = () => {
const [interactionCount, setInteractionCount] = useState(0);
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="output:click">
<output
data-testid="subject"
onClick={(event) => {
setInteractionCount((previous) => previous + 1);
pushEvent(event);
}}
tabIndex={0}
style={FILL_INLINE_STYLE}
>
output
</output>
<span data-testid="front-component-value">{interactionCount}</span>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-output-c-click-00000000-0000-0000-0000-000000000020',
name: 'output-click-front-component',
description: 'Front component covering click on <output>',
component: OutputClickFrontComponent,
});

View File

@@ -0,0 +1,29 @@
import { type Meta } from '@storybook/react-vite';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import {
createHtmlTagClickStory,
createHtmlTagFocusStory,
} from '@/__stories__/shared/test-utils/createHtmlElementStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Output/Events',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
export const Click = createHtmlTagClickStory({
frontComponentBundleName: 'output-click',
});
export const FocusBlur = createHtmlTagFocusStory({
frontComponentBundleName: 'output-focus-blur',
});

View File

@@ -0,0 +1,39 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { FILL_INLINE_STYLE } from '@/__stories__/shared/front-components/styles';
const OutputFocusBlurFrontComponent = () => {
const [interactionCount, setInteractionCount] = useState(0);
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="output:focus-blur">
<output
data-testid="subject"
onFocus={(event) => {
setInteractionCount((previous) => previous + 1);
pushEvent(event);
}}
onBlur={(event) => pushEvent(event)}
tabIndex={0}
style={FILL_INLINE_STYLE}
>
output
</output>
<span data-testid="front-component-value">{interactionCount}</span>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-output-fb-fb-00000000-0000-0000-0000-000000000020',
name: 'output-focus-blur-front-component',
description: 'Front component covering focus-blur on <output>',
component: OutputFocusBlurFrontComponent,
});

View File

@@ -0,0 +1,39 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { FILL_INLINE_STYLE } from '@/__stories__/shared/front-components/styles';
const ProgressClickFrontComponent = () => {
const [interactionCount, setInteractionCount] = useState(0);
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="progress:click">
<progress
data-testid="subject"
value={30}
max={100}
onClick={(event) => {
setInteractionCount((previous) => previous + 1);
pushEvent(event);
}}
tabIndex={0}
style={FILL_INLINE_STYLE}
/>
<span data-testid="front-component-value">{interactionCount}</span>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier:
'fc-progress-c-click-00000000-0000-0000-0000-000000000020',
name: 'progress-click-front-component',
description: 'Front component covering click on <progress>',
component: ProgressClickFrontComponent,
});

View File

@@ -0,0 +1,29 @@
import { type Meta } from '@storybook/react-vite';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import {
createHtmlTagClickStory,
createHtmlTagFocusStory,
} from '@/__stories__/shared/test-utils/createHtmlElementStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Progress/Events',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
export const Click = createHtmlTagClickStory({
frontComponentBundleName: 'progress-click',
});
export const FocusBlur = createHtmlTagFocusStory({
frontComponentBundleName: 'progress-focus-blur',
});

View File

@@ -0,0 +1,39 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { FILL_INLINE_STYLE } from '@/__stories__/shared/front-components/styles';
const ProgressFocusBlurFrontComponent = () => {
const [interactionCount, setInteractionCount] = useState(0);
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="progress:focus-blur">
<progress
data-testid="subject"
value={30}
max={100}
onFocus={(event) => {
setInteractionCount((previous) => previous + 1);
pushEvent(event);
}}
onBlur={(event) => pushEvent(event)}
tabIndex={0}
style={FILL_INLINE_STYLE}
/>
<span data-testid="front-component-value">{interactionCount}</span>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-progress-fb-fb-00000000-0000-0000-0000-000000000020',
name: 'progress-focus-blur-front-component',
description: 'Front component covering focus-blur on <progress>',
component: ProgressFocusBlurFrontComponent,
});

View File

@@ -0,0 +1,27 @@
import { defineFrontComponent } from 'twenty-sdk/define';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { PROPERTY_FIXTURE } from '@/__stories__/shared/front-components/property-fixture';
const ProgressPropertiesFrontComponent = () => (
<FrontComponentCard title="progress:properties">
<progress
data-testid="subject"
id={PROPERTY_FIXTURE.id}
className={PROPERTY_FIXTURE.className}
title={PROPERTY_FIXTURE.title}
role={PROPERTY_FIXTURE.role}
aria-label={PROPERTY_FIXTURE.ariaLabel}
tabIndex={PROPERTY_FIXTURE.tabIndex}
value={30}
max={PROPERTY_FIXTURE.max}
/>
</FrontComponentCard>
);
export default defineFrontComponent({
universalIdentifier: 'fc-progress-props-00000000-0000-0000-0000-000000000020',
name: 'progress-properties-front-component',
description: 'Front component covering property reflection on <progress>',
component: ProgressPropertiesFrontComponent,
});

View File

@@ -0,0 +1,26 @@
import { type Meta } from '@storybook/react-vite';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { createPropertyReflectionStory } from '@/__stories__/shared/test-utils/createPropertyReflectionStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Progress/Properties',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
export const Properties = createPropertyReflectionStory({
frontComponentBundleName: 'progress-properties',
extraAttributes: {
value: '30',
max: '100',
},
});

View File

@@ -0,0 +1,51 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { userEvent, within } from 'storybook/test';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { expectEventLogged } from '@/__stories__/shared/test-utils/matchers/expectEventLogged';
import { expectFrontComponentMounted } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentMounted';
import { expectFrontComponentValue } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentValue';
import { runFrontComponentStory } from '@/__stories__/shared/test-utils/runFrontComponentStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Select/Events',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
type Story = StoryObj<typeof FrontComponentRenderer>;
export const ValueRoundTrip: Story = runFrontComponentStory({
frontComponentBundleName: 'select-value',
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expectFrontComponentMounted(canvas);
const subject = (await canvas.findByTestId('subject')) as HTMLSelectElement;
await userEvent.selectOptions(subject, 'beta');
await expectFrontComponentValue({ canvas, expected: 'beta' });
await expectEventLogged({
canvas,
matcher: { type: 'change', value: 'beta' },
});
await userEvent.selectOptions(subject, 'gamma');
await expectFrontComponentValue({ canvas, expected: 'gamma' });
await expectEventLogged({
canvas,
matcher: { type: 'change', value: 'gamma' },
});
},
});

Some files were not shown because too many files have changed in this diff Show More