[FRONT COMPONENT] Stories in twenty-sdk for front component generation and upon render interactivity (#17675)

closes https://github.com/twentyhq/core-team-issues/issues/2179
This commit is contained in:
nitin
2026-02-04 18:09:54 +05:30
committed by GitHub
parent 59c36736d4
commit 660a15b721
26 changed files with 720 additions and 34 deletions
+4 -1
View File
@@ -25,7 +25,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
task: [lint, typecheck, test:unit]
task: [lint, typecheck, test:unit, storybook:build, storybook:test]
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0
@@ -39,6 +39,9 @@ jobs:
uses: ./.github/actions/yarn-install
- name: Build
run: npx nx build twenty-sdk
- name: Install Playwright
if: contains(matrix.task, 'storybook')
run: npx playwright install chromium
- name: Run ${{ matrix.task }} task
uses: ./.github/actions/nx-affected
with:
+2
View File
@@ -1,2 +1,4 @@
node_modules
.twenty
storybook-static
src/front-component/__stories__/built
+38
View File
@@ -0,0 +1,38 @@
import type { StorybookConfig } from '@storybook/react-vite';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const dirname =
typeof __dirname !== 'undefined'
? __dirname
: path.dirname(fileURLToPath(import.meta.url));
const config: StorybookConfig = {
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: ['@storybook/addon-vitest'],
framework: '@storybook/react-vite',
staticDirs: [
{
from: '../src/front-component/__stories__/built',
to: '/built',
},
],
viteFinal: async (viteConfig) => {
return {
...viteConfig,
resolve: {
...viteConfig.resolve,
alias: {
...viteConfig.resolve?.alias,
'@': path.resolve(dirname, '../src'),
},
},
};
},
};
export default config;
@@ -0,0 +1,25 @@
import { ThemeProvider } from '@emotion/react';
import { type Preview } from '@storybook/react-vite';
import { THEME_LIGHT, ThemeContextProvider } from 'twenty-ui/theme';
const preview: Preview = {
tags: ['autodocs'],
decorators: [
(Story) => {
const theme = THEME_LIGHT;
return (
<ThemeProvider theme={theme}>
<ThemeContextProvider theme={theme}>
<Story />
</ThemeContextProvider>
</ThemeProvider>
);
},
],
args: {
theme: THEME_LIGHT,
},
};
export default preview;
@@ -0,0 +1,6 @@
import { setProjectAnnotations } from '@storybook/react-vite';
import * as projectAnnotations from './preview';
// This is an important step to apply the right configuration when testing your stories.
// More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations
setProjectAnnotations([projectAnnotations]);
+5
View File
@@ -81,6 +81,8 @@
},
"devDependencies": {
"@prettier/sync": "^0.5.2",
"@storybook/addon-vitest": "^10.1.11",
"@storybook/react-vite": "^10.1.11",
"@types/archiver": "^6.0.0",
"@types/fs-extra": "^11.0.0",
"@types/inquirer": "^9.0.0",
@@ -88,6 +90,9 @@
"@types/node": "^24.0.0",
"@types/react": "18.2.66",
"@types/react-dom": "18.2.22",
"@vitest/browser-playwright": "^4.0.17",
"playwright": "^1.56.1",
"storybook": "^10.1.11",
"ts-morph": "^25.0.0",
"tsx": "^4.7.0",
"twenty-ui": "workspace:*",
+66 -1
View File
@@ -107,6 +107,71 @@
"projects": "twenty-server"
}
]
}
},
"generateRemoteDomElements": {
"executor": "nx:run-commands",
"cache": true,
"inputs": [
"{projectRoot}/scripts/remote-dom/**/*",
"{projectRoot}/src/front-component-constants/**/*"
],
"outputs": [
"{projectRoot}/src/front-component/host/generated/*",
"{projectRoot}/src/front-component/remote/generated/*"
],
"options": {
"command": "tsx {projectRoot}/scripts/remote-dom/generateRemoteDomElements.ts"
}
},
"storybook:prebuild": {
"executor": "nx:run-commands",
"cache": true,
"dependsOn": ["generateRemoteDomElements"],
"inputs": [
"{projectRoot}/src/front-component/__stories__/mocks/**/*",
"{projectRoot}/src/front-component/__stories__/utils/**/*",
"{projectRoot}/src/cli/utilities/build/common/front-component-build/**/*",
"{projectRoot}/src/front-component-constants/**/*",
"{projectRoot}/src/sdk/**/*"
],
"outputs": ["{projectRoot}/src/front-component/__stories__/built/*"],
"options": {
"command": "tsx {projectRoot}/src/front-component/__stories__/utils/buildMockComponents.ts"
}
},
"storybook:build": {
"dependsOn": ["storybook:prebuild"],
"configurations": {
"test": {}
}
},
"storybook:serve:dev": {
"dependsOn": ["storybook:prebuild"],
"options": {
"port": 6008
}
},
"storybook:serve:static": {
"options": {
"buildTarget": "twenty-sdk:storybook:build",
"port": 6008
},
"configurations": {
"test": {}
}
},
"storybook:test": {
"dependsOn": ["storybook:prebuild"],
"options": {
"command": "vitest run --coverage --config vitest.storybook.config.ts --shard={args.shard}"
}
},
"storybook:test:no-coverage": {
"dependsOn": ["storybook:prebuild"],
"options": {
"command": "vitest run --config vitest.storybook.config.ts --shard={args.shard}"
}
},
"storybook:coverage": {}
}
}
@@ -66,7 +66,33 @@ const filterProps = (props: Record<string, unknown>) => {
};`;
};
// HTML void elements cannot have children
// https://developer.mozilla.org/en-US/docs/Glossary/Void_element
const VOID_ELEMENTS = new Set([
'input',
'br',
'hr',
'img',
'area',
'base',
'col',
'embed',
'link',
'meta',
'source',
'track',
'wbr',
]);
const generateHtmlWrapperComponent = (component: ComponentSchema): string => {
const isVoidElement = VOID_ELEMENTS.has(component.htmlTag ?? '');
if (isVoidElement) {
return `const ${component.name}Wrapper = ({ children: _children, ...props }: { children?: React.ReactNode } & Record<string, unknown>) => {
return React.createElement('${component.htmlTag}', filterProps(props));
};`;
}
return `const ${component.name}Wrapper = ({ children, ...props }: { children?: React.ReactNode } & Record<string, unknown>) => {
return React.createElement('${component.htmlTag}', filterProps(props), children);
};`;
@@ -1,7 +1,7 @@
import { cleanupRemovedFiles } from '@/cli/utilities/build/common/cleanup-removed-files';
import { processEsbuildResult } from '@/cli/utilities/build/common/esbuild-result-processor';
import { jsxTransformToRemoteDomWorkerFormatPlugin } from '@/cli/utilities/build/common/front-component-build/jsx-transform-to-remote-dom-worker-format-plugin';
import { reactGlobalsPlugin } from '@/cli/utilities/build/common/front-component-build/react-globals-plugin';
import { FRONT_COMPONENT_EXTERNAL_MODULES } from '@/cli/utilities/build/common/front-component-build/constants/front-component-external-modules';
import { getFrontComponentBuildPlugins } from '@/cli/utilities/build/common/front-component-build/utils/get-front-component-build-plugins';
import {
type OnBuildErrorCallback,
type OnFileBuiltCallback,
@@ -38,14 +38,6 @@ export const LOGIC_FUNCTION_EXTERNAL_MODULES: string[] = [
'twenty-shared/*',
];
export const FRONT_COMPONENT_EXTERNAL_MODULES: string[] = [
'react-dom',
'twenty-sdk',
'twenty-sdk/*',
'twenty-shared',
'twenty-shared/*',
];
export type EsbuildWatcherConfig = {
externalModules: string[];
fileFolder: FileFolder;
@@ -226,9 +218,6 @@ export const createFrontComponentsWatcher = (
externalModules: FRONT_COMPONENT_EXTERNAL_MODULES,
fileFolder: FileFolder.BuiltFrontComponent,
jsx: 'automatic',
extraPlugins: [
reactGlobalsPlugin,
jsxTransformToRemoteDomWorkerFormatPlugin,
],
extraPlugins: getFrontComponentBuildPlugins(),
},
});
@@ -202,4 +202,65 @@ describe('reactGlobalsPlugin', () => {
expect(result).not.toContain('globalThis.React.useEffect');
});
});
describe('multiple entry points', () => {
it('should handle multiple files with different React imports', async () => {
const fileA = path.join(tempDir, 'component-a.tsx');
const fileB = path.join(tempDir, 'component-b.tsx');
fs.writeFileSync(
fileA,
`
import { useState } from 'react';
export const ComponentA = () => {
const [state] = useState(0);
return state;
};
`,
'utf-8',
);
fs.writeFileSync(
fileB,
`
import { useEffect } from 'react';
export const ComponentB = () => {
useEffect(() => {}, []);
return null;
};
`,
'utf-8',
);
const result = await esbuild.build({
entryPoints: [fileA, fileB],
bundle: true,
write: false,
format: 'esm',
jsx: 'automatic',
outdir: tempDir,
plugins: [reactGlobalsPlugin],
});
const outputA = result.outputFiles.find(
(f) => path.basename(f.path) === 'component-a.js',
)?.text;
const outputB = result.outputFiles.find(
(f) => path.basename(f.path) === 'component-b.js',
)?.text;
expect(outputA).toBeDefined();
expect(outputB).toBeDefined();
// Each file should only include the React exports it needs
expect(outputA).toContain('globalThis.React.useState');
expect(outputB).toContain('globalThis.React.useEffect');
expect(outputA).not.toContain('globalThis.React.useEffect');
expect(outputB).not.toContain('globalThis.React.useState');
// No raw react imports should remain
expect(outputA).not.toContain('from "react"');
expect(outputB).not.toContain('from "react"');
});
});
});
@@ -0,0 +1,7 @@
export const FRONT_COMPONENT_EXTERNAL_MODULES: string[] = [
'react-dom',
'twenty-sdk',
'twenty-sdk/*',
'twenty-shared',
'twenty-shared/*',
];
@@ -78,18 +78,20 @@ export const reactGlobalsPlugin: esbuild.Plugin = {
if (importer && !reactImportsByFilePath.has(importer)) {
try {
const sourceFileContent = await fs.readFile(importer, 'utf-8');
reactImportsByFilePath.set(
importer,
collectReactImports(sourceFileContent),
);
} catch {
reactImportsByFilePath.set(importer, new Set());
reactImportsByFilePath.set(importer, new Set<string>());
}
}
return {
path,
path:
path === 'react' && importer
? `react?importer=${encodeURIComponent(importer)}`
: path,
namespace: 'react-globals',
pluginData: { importer },
};
@@ -98,11 +100,7 @@ export const reactGlobalsPlugin: esbuild.Plugin = {
build.onLoad(
{ filter: /.*/, namespace: 'react-globals' },
({ pluginData, path }) => {
const importerFilePath = pluginData?.importer || '';
const collectedReactImports =
reactImportsByFilePath.get(importerFilePath) || new Set<string>();
({ path, pluginData }) => {
if (path === 'react/jsx-runtime') {
return {
contents: JSX_RUNTIME_EXPORTS,
@@ -110,7 +108,13 @@ export const reactGlobalsPlugin: esbuild.Plugin = {
};
}
if (path === 'react') {
if (path === 'react' || path.startsWith('react?importer=')) {
const importerFilePath =
pluginData?.importer ||
decodeURIComponent(path.split('react?importer=')[1] || '');
const collectedReactImports =
reactImportsByFilePath.get(importerFilePath) || new Set<string>();
return {
contents: generateReactExports(collectedReactImports),
loader: 'js',
@@ -0,0 +1,46 @@
import type * as esbuild from 'esbuild';
import { FRONT_COMPONENT_EXTERNAL_MODULES } from '../constants/front-component-external-modules';
import { getFrontComponentBuildPlugins } from './get-front-component-build-plugins';
type FrontComponentBuildOptions = {
entryPoints: esbuild.BuildOptions['entryPoints'];
outdir: string;
tsconfigPath?: string;
externalModules?: string[];
logLevel?: esbuild.LogLevel;
platform?: esbuild.Platform;
minify?: boolean;
metafile?: boolean;
sourcemap?: boolean;
};
export const createFrontComponentBuildOptions = ({
entryPoints,
outdir,
tsconfigPath,
externalModules = FRONT_COMPONENT_EXTERNAL_MODULES,
logLevel = 'silent',
platform,
minify,
metafile = true,
sourcemap = true,
}: FrontComponentBuildOptions): esbuild.BuildOptions => {
return {
entryPoints,
bundle: true,
splitting: false,
format: 'esm',
platform,
outdir,
outExtension: { '.js': '.mjs' },
external: externalModules,
tsconfig: tsconfigPath,
jsx: 'automatic',
sourcemap,
metafile,
logLevel,
minify,
plugins: getFrontComponentBuildPlugins(),
};
};
@@ -0,0 +1,9 @@
import type * as esbuild from 'esbuild';
import { jsxTransformToRemoteDomWorkerFormatPlugin } from '../jsx-transform-to-remote-dom-worker-format-plugin';
import { reactGlobalsPlugin } from '../react-globals-plugin';
export const getFrontComponentBuildPlugins = (): esbuild.Plugin[] => [
reactGlobalsPlugin,
jsxTransformToRemoteDomWorkerFormatPlugin,
];
@@ -1,6 +1,6 @@
import { isDefined } from 'twenty-shared/utils';
import { HTML_TAG_TO_REMOTE_COMPONENT } from '@/front-component-constants';
import { HTML_TAG_TO_REMOTE_COMPONENT } from '../../../../../../front-component-constants';
const REMOTE_COMPONENTS_GLOBAL_NAMESPACE = 'RemoteComponents';
@@ -0,0 +1,108 @@
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 { getBuiltComponentPath } from './utils/loadBuiltComponent';
const errorHandler = fn();
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/FrontComponentRenderer',
component: FrontComponentRenderer,
parameters: {
layout: 'centered',
},
args: {
onError: errorHandler,
},
beforeEach: () => {
errorHandler.mockClear();
},
};
export default meta;
type Story = StoryObj<typeof FrontComponentRenderer>;
export const Static: Story = {
args: {
componentUrl: getBuiltComponentPath('static.front-component'),
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const container = await canvas.findByTestId(
'static-component',
{},
{ timeout: 5000 },
);
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 = {
args: {
componentUrl: getBuiltComponentPath('interactive.front-component'),
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByTestId('interactive-component', {}, { timeout: 5000 });
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 = {
args: {
componentUrl: getBuiltComponentPath('lifecycle.front-component'),
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByTestId('lifecycle-component', {}, { timeout: 5000 });
expect(await canvas.findByText('Mounted')).toBeVisible();
await waitFor(
() => {
const tickElement = canvas.getByTestId('tick-count');
expect(tickElement.textContent).toMatch(/Ticks: [1-9]\d*/);
},
{ timeout: 5000 },
);
},
};
export const ErrorHandling: Story = {
args: {
componentUrl: '/built/nonexistent.front-component.mjs',
},
play: async () => {
await waitFor(
() => {
expect(errorHandler).toHaveBeenCalled();
},
{ timeout: 5000 },
);
},
};
@@ -0,0 +1,62 @@
import { defineFrontComponent } from '@/sdk';
import { useState } from 'react';
const InteractiveComponent = () => {
const [count, setCount] = useState(0);
return (
<div
data-testid="interactive-component"
style={{
padding: 24,
backgroundColor: '#faf5ff',
border: '2px solid #9f7aea',
borderRadius: 12,
}}
>
<h2
style={{
color: '#553c9a',
fontWeight: 700,
fontSize: 18,
marginBottom: 16,
}}
>
Interactive Component
</h2>
<p
data-testid="count-display"
style={{
fontSize: 32,
fontWeight: 800,
color: '#6b46c1',
marginBottom: 16,
}}
>
Count: {count}
</p>
<button
data-testid="increment-button"
onClick={() => setCount((c) => c + 1)}
style={{
padding: '10px 20px',
backgroundColor: '#805ad5',
color: 'white',
border: 'none',
borderRadius: 6,
fontWeight: 600,
cursor: 'pointer',
}}
>
Increment
</button>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'test-int0-00000000-0000-0000-0000-000000000002',
name: 'interactive-component',
description: 'Component with click interactions',
component: InteractiveComponent,
});
@@ -0,0 +1,73 @@
import { defineFrontComponent } from '@/sdk';
import { useEffect, useState } from 'react';
const LifecycleComponent = () => {
const [mounted, setMounted] = useState(false);
const [ticks, setTicks] = useState(0);
useEffect(() => {
setMounted(true);
const interval = setInterval(() => {
setTicks((t) => t + 1);
}, 1000);
return () => clearInterval(interval);
}, []);
return (
<div
data-testid="lifecycle-component"
style={{
padding: 20,
backgroundColor: '#fffaf0',
borderLeft: '4px solid #ed8936',
borderRadius: '0 8px 8px 0',
}}
>
<h2
style={{
color: '#c05621',
fontWeight: 700,
fontSize: 18,
marginBottom: 16,
}}
>
Lifecycle Component
</h2>
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
<p
data-testid="mounted-status"
style={{
padding: '8px 16px',
borderRadius: 20,
fontWeight: 600,
fontSize: 14,
backgroundColor: mounted ? '#c6f6d5' : '#fed7d7',
color: mounted ? '#276749' : '#c53030',
}}
>
{mounted ? 'Mounted' : 'Not mounted'}
</p>
<p
data-testid="tick-count"
style={{
padding: '8px 16px',
backgroundColor: '#bee3f8',
color: '#2b6cb0',
borderRadius: 20,
fontWeight: 600,
fontSize: 14,
}}
>
Ticks: {ticks}
</p>
</div>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'test-lif0-00000000-0000-0000-0000-000000000003',
name: 'lifecycle-component',
description: 'Component with useEffect lifecycle',
component: LifecycleComponent,
});
@@ -0,0 +1,41 @@
import { defineFrontComponent } from '@/sdk';
const StaticComponent = () => (
<div
data-testid="static-component"
style={{
padding: 20,
backgroundColor: '#f0f4f8',
borderRadius: 8,
fontFamily: 'system-ui, sans-serif',
}}
>
<h2 style={{ color: '#1a365d', fontWeight: 700, marginBottom: 12 }}>
Static Component
</h2>
<p style={{ color: '#4a5568', fontSize: 14, lineHeight: 1.5 }}>
This is a simple static component.
</p>
<span
data-testid="styled-badge"
style={{
display: 'inline-block',
padding: '4px 8px',
backgroundColor: '#48bb78',
color: 'white',
borderRadius: 4,
fontSize: 12,
fontWeight: 600,
}}
>
Styled Badge
</span>
</div>
);
export default defineFrontComponent({
universalIdentifier: 'test-static-00000000-0000-0000-0000-000000000001',
name: 'static-component',
description: 'A simple static component for testing',
component: StaticComponent,
});
@@ -0,0 +1,51 @@
import * as esbuild from 'esbuild';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
import { createFrontComponentBuildOptions } from '../../../cli/utilities/build/common/front-component-build/utils/create-front-component-build-options';
const dirname = path.dirname(fileURLToPath(import.meta.url));
const mocksDir = path.resolve(dirname, '../mocks');
const outputDir = path.resolve(dirname, '../built');
const sdkRoot = path.resolve(dirname, '../../../..');
const STORY_COMPONENTS = [
'static.front-component',
'interactive.front-component',
'lifecycle.front-component',
];
export const buildMockComponents = async (): Promise<void> => {
fs.mkdirSync(outputDir, { recursive: true });
const entryPoints: Record<string, string> = {};
for (const name of STORY_COMPONENTS) {
const filePath = path.join(mocksDir, `${name}.tsx`);
if (!fs.existsSync(filePath)) {
throw new Error(
`Story component source file not found: ${filePath}\n` +
`Ensure the file exists in ${mocksDir} and the name in STORY_COMPONENTS is correct.`,
);
}
entryPoints[name] = filePath;
}
const buildOptions = createFrontComponentBuildOptions({
entryPoints,
outdir: outputDir,
tsconfigPath: path.join(sdkRoot, 'tsconfig.json'),
});
await esbuild.build(buildOptions);
console.log(
`Built ${STORY_COMPONENTS.length} story components to ${outputDir}`,
);
};
buildMockComponents().catch((error) => {
console.error('Failed to build mock components:', error);
process.exit(1);
});
@@ -0,0 +1,3 @@
export const getBuiltComponentPath = (componentName: string): string => {
return `/built/${componentName}.mjs`;
};
@@ -227,10 +227,10 @@ const HtmlAWrapper = ({
return React.createElement('a', filterProps(props), children);
};
const HtmlImgWrapper = ({
children,
children: _children,
...props
}: { children?: React.ReactNode } & Record<string, unknown>) => {
return React.createElement('img', filterProps(props), children);
return React.createElement('img', filterProps(props));
};
const HtmlUlWrapper = ({
children,
@@ -263,10 +263,10 @@ const HtmlLabelWrapper = ({
return React.createElement('label', filterProps(props), children);
};
const HtmlInputWrapper = ({
children,
children: _children,
...props
}: { children?: React.ReactNode } & Record<string, unknown>) => {
return React.createElement('input', filterProps(props), children);
return React.createElement('input', filterProps(props));
};
const HtmlTextareaWrapper = ({
children,
@@ -335,16 +335,16 @@ const HtmlTdWrapper = ({
return React.createElement('td', filterProps(props), children);
};
const HtmlBrWrapper = ({
children,
children: _children,
...props
}: { children?: React.ReactNode } & Record<string, unknown>) => {
return React.createElement('br', filterProps(props), children);
return React.createElement('br', filterProps(props));
};
const HtmlHrWrapper = ({
children,
children: _children,
...props
}: { children?: React.ReactNode } & Record<string, unknown>) => {
return React.createElement('hr', filterProps(props), children);
return React.createElement('hr', filterProps(props));
};
const TwentyUiButtonWrapper = ({
children,
+2
View File
@@ -20,6 +20,8 @@
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.d.ts",
".storybook/*.ts",
".storybook/*.tsx",
"**/__mocks__/**/*",
"**/__tests__/**/*",
"vite.config.ts",
+11
View File
@@ -156,5 +156,16 @@ export default defineConfig(() => {
},
},
logLevel: 'warn',
optimizeDeps: {
include: [
'@remote-dom/core/polyfill',
'@remote-dom/react/polyfill',
'@remote-dom/core/elements',
'@remote-dom/react',
'@remote-dom/react/host',
'react-dom/client',
'react/jsx-runtime',
],
},
};
});
@@ -0,0 +1,44 @@
import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
import { playwright } from '@vitest/browser-playwright';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { defineConfig } from 'vitest/config';
const MINUTES_IN_MS = 60 * 1000;
const dirname =
typeof __dirname !== 'undefined'
? __dirname
: path.dirname(fileURLToPath(import.meta.url));
export default defineConfig({
test: {
coverage: {
provider: 'istanbul',
reporter: ['json', 'text'],
reportsDirectory: './coverage/storybook',
},
projects: [
{
extends: './vite.config.ts',
plugins: [
storybookTest({
configDir: path.join(dirname, '.storybook'),
storybookScript: 'yarn storybook --no-open --port 6008',
}),
],
test: {
name: 'storybook',
browser: {
enabled: true,
headless: true,
provider: playwright({}),
instances: [{ browser: 'chromium' }],
},
setupFiles: ['./.storybook/vitest.setup.ts'],
testTimeout: 5 * MINUTES_IN_MS,
},
},
],
},
});
+5
View File
@@ -57682,6 +57682,8 @@ __metadata:
"@remote-dom/core": "npm:^1.10.1"
"@remote-dom/react": "npm:^1.2.2"
"@sniptt/guards": "npm:^0.2.0"
"@storybook/addon-vitest": "npm:^10.1.11"
"@storybook/react-vite": "npm:^10.1.11"
"@types/archiver": "npm:^6.0.0"
"@types/fs-extra": "npm:^11.0.0"
"@types/inquirer": "npm:^9.0.0"
@@ -57689,6 +57691,7 @@ __metadata:
"@types/node": "npm:^24.0.0"
"@types/react": "npm:18.2.66"
"@types/react-dom": "npm:18.2.22"
"@vitest/browser-playwright": "npm:^4.0.17"
archiver: "npm:^7.0.1"
axios: "npm:^1.6.0"
chalk: "npm:^5.3.0"
@@ -57704,8 +57707,10 @@ __metadata:
inquirer: "npm:^10.0.0"
jsonc-parser: "npm:^3.2.0"
lodash.camelcase: "npm:^4.3.0"
playwright: "npm:^1.56.1"
react: "npm:^18.2.0"
react-dom: "npm:^18.2.0"
storybook: "npm:^10.1.11"
ts-morph: "npm:^25.0.0"
tsx: "npm:^4.7.0"
twenty-shared: "workspace:*"