Replace test-runner with vitest for storybook (#17187)

This commit is contained in:
Abdullah.
2026-01-18 08:25:45 +05:00
committed by GitHub
parent c737028dd6
commit 92eff62523
56 changed files with 4999 additions and 715 deletions
+1 -1
View File
@@ -55,7 +55,7 @@ You can manually reference any rule using the `@ruleName` syntax:
# Testing
npx nx test twenty-front # Run unit tests
npx nx storybook:build twenty-front # Build Storybook
npx nx storybook:serve-and-test:static # Run Storybook tests
npx nx storybook:test # Run Storybook tests
# Development
npx nx lint:diff-with-main twenty-front # Lint files changed vs main (fastest)
+16 -9
View File
@@ -56,8 +56,6 @@ jobs:
run: df -h
- name: Front / Write .env
run: npx nx reset:env twenty-front
- name: Front / Clean storybook-static
run: rm -rf packages/twenty-front/storybook-static
- name: Front / Build storybook
run: npx nx storybook:build twenty-front
- name: Save storybook build cache
@@ -83,18 +81,27 @@ jobs:
fetch-depth: 0
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build dependencies
run: |
npx nx build twenty-shared
npx nx build twenty-ui
- name: Install Playwright
run: cd packages/twenty-front && npx playwright install
- name: Restore storybook build cache
uses: ./.github/actions/restore-cache
with:
key: ${{ env.STORYBOOK_BUILD_CACHE_KEY_FOR_RESTORE_ACTION }}
run: |
cd packages/twenty-front
npx playwright install
- name: Front / Write .env
run: npx nx reset:env twenty-front
- name: Run storybook tests
run: npx nx storybook:serve-and-test:static twenty-front --configuration=${{ matrix.storybook_scope }} --shard=${{ matrix.shard }}/${{ env.SHARD_COUNTER }} --checkCoverage=false
run: npx nx storybook:test twenty-front --configuration=${{ matrix.storybook_scope }} --shard=${{ matrix.shard }}/${{ env.SHARD_COUNTER }}
- name: Rename coverage file
run: mv packages/twenty-front/coverage/storybook/coverage-storybook.json packages/twenty-front/coverage/storybook/coverage-shard-${{matrix.shard}}.json
run: |
if [ -f "packages/twenty-front/coverage/storybook/coverage-final.json" ]; then
mv packages/twenty-front/coverage/storybook/coverage-final.json packages/twenty-front/coverage/storybook/coverage-shard-${{matrix.shard}}.json
else
echo "Error: coverage-final.json not found"
ls -la packages/twenty-front/coverage/storybook/ || echo "Coverage directory does not exist"
exit 1
fi
- name: Upload coverage artifact
uses: actions/upload-artifact@v4
with:
+1 -1
View File
@@ -28,7 +28,7 @@ npx nx run twenty-server:test:integration:with-db-reset # Integration tests wit
# Storybook
npx nx storybook:build twenty-front # Build Storybook
npx nx storybook:serve-and-test:static twenty-front # Run Storybook tests
npx nx storybook:test twenty-front # Run Storybook tests
When testing the UI end to end, click on "Continue with Email" and use the prefilled credentials.
+4 -24
View File
@@ -181,15 +181,8 @@
"outputs": ["{projectRoot}/coverage/storybook"],
"options": {
"cwd": "{projectRoot}",
"commands": [
"test-storybook --url http://localhost:{args.port} --maxWorkers=3 --coverage --coverageDirectory={args.coverageDir} --shard={args.shard}",
"nx storybook:coverage {projectName} --coverageDir={args.coverageDir} --checkCoverage={args.checkCoverage}"
],
"shard": "1/1",
"parallel": false,
"coverageDir": "coverage/storybook",
"port": 6006,
"checkCoverage": true
"command": "vitest run --coverage --shard={args.shard}",
"shard": "1/1"
}
},
"storybook:test:no-coverage": {
@@ -197,10 +190,8 @@
"inputs": ["^default", "excludeTests"],
"options": {
"cwd": "{projectRoot}",
"commands": [
"test-storybook --url http://localhost:{args.port} --maxWorkers=2"
],
"port": 6006
"command": "vitest run --shard={args.shard}",
"shard": "1/1"
}
},
"storybook:coverage": {
@@ -227,17 +218,6 @@
}
}
},
"storybook:serve-and-test:static": {
"executor": "nx:run-commands",
"options": {
"commands": [
"npx concurrently --kill-others --success=first -n SB,TEST 'nx storybook:serve:static {projectName} --port={args.port}' 'npx wait-on tcp:{args.port} && nx storybook:test {projectName} --shard={args.shard} --checkCoverage={args.checkCoverage} --port={args.port} --configuration={args.scope}'"
],
"shard": "1/1",
"checkCoverage": true,
"port": 6006
}
},
"chromatic": {
"executor": "nx:run-commands",
"options": {
+8 -3
View File
@@ -85,6 +85,7 @@
"@storybook/addon-coverage": "^3.0.0",
"@storybook/addon-docs": "^10.1.11",
"@storybook/addon-links": "^10.1.11",
"@storybook/addon-vitest": "^10.1.11",
"@storybook/icons": "^2.0.1",
"@storybook/react-vite": "^10.1.11",
"@storybook/test-runner": "^0.24.2",
@@ -135,6 +136,9 @@
"@typescript-eslint/parser": "^8.39.0",
"@typescript-eslint/utils": "^8.39.0",
"@vitejs/plugin-react-swc": "3.11.0",
"@vitest/browser-playwright": "^4.0.17",
"@vitest/coverage-istanbul": "^4.0.17",
"@vitest/coverage-v8": "^4.0.17",
"@yarnpkg/types": "^4.0.0",
"chromatic": "^6.18.0",
"concurrently": "^8.2.2",
@@ -163,8 +167,8 @@
"jest-environment-node": "^29.4.1",
"jest-fetch-mock": "^3.0.3",
"jsdom": "~22.1.0",
"msw": "^2.0.11",
"msw-storybook-addon": "^2.0.5",
"msw": "^2.12.7",
"msw-storybook-addon": "^2.0.6",
"nx": "22.3.3",
"prettier": "^3.1.1",
"raw-loader": "^4.0.2",
@@ -179,7 +183,8 @@
"ts-node": "10.9.1",
"tsconfig-paths": "^4.2.0",
"tsx": "^4.17.0",
"vite": "^7.0.0"
"vite": "^7.0.0",
"vitest": "^4.0.17"
},
"engines": {
"node": "^24.5.0",
+2 -8
View File
@@ -31,8 +31,6 @@ const computeStoriesGlob = () => {
const config: StorybookConfig = {
stories: computeStoriesGlob(),
staticDirs: ['../public'],
build: {
test: {
disabledAddons: [
@@ -48,8 +46,9 @@ const config: StorybookConfig = {
'@storybook/addon-links',
'@storybook/addon-coverage',
'storybook-addon-pseudo-states',
'storybook-addon-mock-date',
// 'storybook-dark-mode',
'storybook-addon-mock-date',
'@storybook/addon-vitest',
],
framework: '@storybook/react-vite',
@@ -58,11 +57,6 @@ const config: StorybookConfig = {
const { mergeConfig } = await import('vite');
return mergeConfig(viteConfig, {
resolve: {
alias: {
'react-dom/client': 'react-dom/profiling',
},
},
logLevel: 'warn',
});
},
@@ -1,21 +0,0 @@
import { getJestConfig } from '@storybook/test-runner';
const MINUTES_IN_MS = 60 * 1000;
/**
* @type {import('@jest/types').Config.InitialOptions}
*/
export default {
// The default configuration comes from @storybook/test-runner
...getJestConfig(),
/** Add your own overrides below
* @see https://jestjs.io/docs/configuration
*/
testTimeout: 5 * MINUTES_IN_MS,
modulePathIgnorePatterns: [
"<rootDir>/.nx/cache/*",
"<rootDir>/packages/twenty-server/dist/*",
"<rootDir>/packages/twenty-front/dist/*",
"<rootDir>/packages/twenty-ui/dist/*"
],
};
@@ -1,9 +0,0 @@
import type { TestRunnerConfig } from '@storybook/test-runner';
const config: TestRunnerConfig = {
tags: {
exclude: ['no-tests'],
},
};
export default config;
@@ -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]);
+1 -1
View File
@@ -11,7 +11,7 @@ const modulesCoverage = {
branches: 25,
statements: 43,
lines: 44,
functions: 37,
functions: 30,
include: ['src/modules/**/*'],
exclude: ['src/**/*.ts'],
};
+13 -11
View File
@@ -192,9 +192,6 @@
}
},
"storybook:test": {
"options": {
"port": 6006
},
"configurations": {
"docs": {
"env": {
@@ -218,22 +215,27 @@
}
}
},
"storybook:serve-and-test:static": {
"options": {
"port": 6006
},
"storybook:test:no-coverage": {
"configurations": {
"docs": {
"scope": "ui-docs"
"env": {
"STORYBOOK_SCOPE": "ui-docs"
}
},
"modules": {
"scope": "modules"
"env": {
"STORYBOOK_SCOPE": "modules"
}
},
"pages": {
"scope": "pages"
"env": {
"STORYBOOK_SCOPE": "pages"
}
},
"performance": {
"scope": "performance"
"env": {
"STORYBOOK_SCOPE": "performance"
}
}
}
},
+133 -68
View File
@@ -5,24 +5,23 @@
* Mock Service Worker.
* @see https://github.com/mswjs/msw
* - Please do NOT modify this file.
* - Please do NOT serve this file on production.
*/
const PACKAGE_VERSION = '2.3.5'
const INTEGRITY_CHECKSUM = '26357c79639bfa20d64c0efca2a87423'
const PACKAGE_VERSION = '2.12.7'
const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82'
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
const activeClientIds = new Set()
self.addEventListener('install', function () {
addEventListener('install', function () {
self.skipWaiting()
})
self.addEventListener('activate', function (event) {
addEventListener('activate', function (event) {
event.waitUntil(self.clients.claim())
})
self.addEventListener('message', async function (event) {
const clientId = event.source.id
addEventListener('message', async function (event) {
const clientId = Reflect.get(event.source || {}, 'id')
if (!clientId || !self.clients) {
return
@@ -62,16 +61,16 @@ self.addEventListener('message', async function (event) {
sendToClient(client, {
type: 'MOCKING_ENABLED',
payload: true,
payload: {
client: {
id: client.id,
frameType: client.frameType,
},
},
})
break
}
case 'MOCK_DEACTIVATE': {
activeClientIds.delete(clientId)
break
}
case 'CLIENT_CLOSED': {
activeClientIds.delete(clientId)
@@ -89,72 +88,99 @@ self.addEventListener('message', async function (event) {
}
})
self.addEventListener('fetch', function (event) {
const { request } = event
addEventListener('fetch', function (event) {
const requestInterceptedAt = Date.now()
// Bypass navigation requests.
if (request.mode === 'navigate') {
if (event.request.mode === 'navigate') {
return
}
// Opening the DevTools triggers the "only-if-cached" request
// that cannot be handled by the worker. Bypass such requests.
if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
if (
event.request.cache === 'only-if-cached' &&
event.request.mode !== 'same-origin'
) {
return
}
// Bypass all requests when there are no active clients.
// Prevents the self-unregistered worked from handling requests
// after it's been deleted (still remains active until the next reload).
// after it's been terminated (still remains active until the next reload).
if (activeClientIds.size === 0) {
return
}
// Generate unique request ID.
const requestId = crypto.randomUUID()
event.respondWith(handleRequest(event, requestId))
event.respondWith(handleRequest(event, requestId, requestInterceptedAt))
})
async function handleRequest(event, requestId) {
/**
* @param {FetchEvent} event
* @param {string} requestId
* @param {number} requestInterceptedAt
*/
async function handleRequest(event, requestId, requestInterceptedAt) {
const client = await resolveMainClient(event)
const response = await getResponse(event, client, requestId)
const requestCloneForEvents = event.request.clone()
const response = await getResponse(
event,
client,
requestId,
requestInterceptedAt,
)
// Send back the response clone for the "response:*" life-cycle events.
// Ensure MSW is active and ready to handle the message, otherwise
// this message will pend indefinitely.
if (client && activeClientIds.has(client.id)) {
;(async function () {
const responseClone = response.clone()
const serializedRequest = await serializeRequest(requestCloneForEvents)
sendToClient(
client,
{
type: 'RESPONSE',
payload: {
requestId,
isMockedResponse: IS_MOCKED_RESPONSE in response,
// Clone the response so both the client and the library could consume it.
const responseClone = response.clone()
sendToClient(
client,
{
type: 'RESPONSE',
payload: {
isMockedResponse: IS_MOCKED_RESPONSE in response,
request: {
id: requestId,
...serializedRequest,
},
response: {
type: responseClone.type,
status: responseClone.status,
statusText: responseClone.statusText,
body: responseClone.body,
headers: Object.fromEntries(responseClone.headers.entries()),
body: responseClone.body,
},
},
[responseClone.body],
)
})()
},
responseClone.body ? [serializedRequest.body, responseClone.body] : [],
)
}
return response
}
// Resolve the main client for the given event.
// Client that issues a request doesn't necessarily equal the client
// that registered the worker. It's with the latter the worker should
// communicate with during the response resolving phase.
/**
* Resolve the main client for the given event.
* Client that issues a request doesn't necessarily equal the client
* that registered the worker. It's with the latter the worker should
* communicate with during the response resolving phase.
* @param {FetchEvent} event
* @returns {Promise<Client | undefined>}
*/
async function resolveMainClient(event) {
const client = await self.clients.get(event.clientId)
if (activeClientIds.has(event.clientId)) {
return client
}
if (client?.frameType === 'top-level') {
return client
}
@@ -175,20 +201,39 @@ async function resolveMainClient(event) {
})
}
async function getResponse(event, client, requestId) {
const { request } = event
/**
* @param {FetchEvent} event
* @param {Client | undefined} client
* @param {string} requestId
* @param {number} requestInterceptedAt
* @returns {Promise<Response>}
*/
async function getResponse(event, client, requestId, requestInterceptedAt) {
// Clone the request because it might've been already used
// (i.e. its body has been read and sent to the client).
const requestClone = request.clone()
const requestClone = event.request.clone()
function passthrough() {
const headers = Object.fromEntries(requestClone.headers.entries())
// Cast the request headers to a new Headers instance
// so the headers can be manipulated with.
const headers = new Headers(requestClone.headers)
// Remove internal MSW request header so the passthrough request
// complies with any potential CORS preflight checks on the server.
// Some servers forbid unknown request headers.
delete headers['x-msw-intention']
// Remove the "accept" header value that marked this request as passthrough.
// This prevents request alteration and also keeps it compliant with the
// user-defined CORS policies.
const acceptHeader = headers.get('accept')
if (acceptHeader) {
const values = acceptHeader.split(',').map((value) => value.trim())
const filteredValues = values.filter(
(value) => value !== 'msw/passthrough',
)
if (filteredValues.length > 0) {
headers.set('accept', filteredValues.join(', '))
} else {
headers.delete('accept')
}
}
return fetch(requestClone, { headers })
}
@@ -207,29 +252,18 @@ async function getResponse(event, client, requestId) {
}
// Notify the client that a request has been intercepted.
const requestBuffer = await request.arrayBuffer()
const serializedRequest = await serializeRequest(event.request)
const clientMessage = await sendToClient(
client,
{
type: 'REQUEST',
payload: {
id: requestId,
url: request.url,
mode: request.mode,
method: request.method,
headers: Object.fromEntries(request.headers.entries()),
cache: request.cache,
credentials: request.credentials,
destination: request.destination,
integrity: request.integrity,
redirect: request.redirect,
referrer: request.referrer,
referrerPolicy: request.referrerPolicy,
body: requestBuffer,
keepalive: request.keepalive,
interceptedAt: requestInterceptedAt,
...serializedRequest,
},
},
[requestBuffer],
[serializedRequest.body],
)
switch (clientMessage.type) {
@@ -245,6 +279,12 @@ async function getResponse(event, client, requestId) {
return passthrough()
}
/**
* @param {Client} client
* @param {any} message
* @param {Array<Transferable>} transferrables
* @returns {Promise<any>}
*/
function sendToClient(client, message, transferrables = []) {
return new Promise((resolve, reject) => {
const channel = new MessageChannel()
@@ -257,14 +297,18 @@ function sendToClient(client, message, transferrables = []) {
resolve(event.data)
}
client.postMessage(
message,
[channel.port2].concat(transferrables.filter(Boolean)),
)
client.postMessage(message, [
channel.port2,
...transferrables.filter(Boolean),
])
})
}
async function respondWithMock(response) {
/**
* @param {Response} response
* @returns {Response}
*/
function respondWithMock(response) {
// Setting response status code to 0 is a no-op.
// However, when responding with a "Response.error()", the produced Response
// instance will have status code set to 0. Since it's not possible to create
@@ -282,3 +326,24 @@ async function respondWithMock(response) {
return mockedResponse
}
/**
* @param {Request} request
*/
async function serializeRequest(request) {
return {
url: request.url,
mode: request.mode,
method: request.method,
headers: Object.fromEntries(request.headers.entries()),
cache: request.cache,
credentials: request.credentials,
destination: request.destination,
integrity: request.integrity,
redirect: request.redirect,
referrer: request.referrer,
referrerPolicy: request.referrerPolicy,
body: await request.arrayBuffer(),
keepalive: request.keepalive,
}
}
@@ -9,11 +9,7 @@ import { createMockActionMenuActions } from '@/action-menu/mock/action-menu-acti
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import {
ComponentDecorator,
RouterDecorator,
getCanvasElementForDropdownTesting,
} from 'twenty-ui/testing';
import { ComponentDecorator, RouterDecorator } from 'twenty-ui/testing';
import { ContextStoreDecorator } from '~/testing/decorators/ContextStoreDecorator';
const deleteMock = test.fn();
const addToFavoritesMock = test.fn();
@@ -86,8 +82,8 @@ export const WithButtonClicks: Story = {
args: {
actionMenuId: 'story-action-menu',
},
play: async () => {
const canvas = within(getCanvasElementForDropdownTesting());
play: async ({ canvasElement }) => {
const canvas = within(canvasElement.ownerDocument.body);
let actionButton = await canvas.findByText('Options');
await userEvent.click(actionButton);
@@ -10,10 +10,7 @@ import { ActionMenuComponentInstanceContext } from '@/action-menu/states/context
import { recordIndexActionMenuDropdownPositionComponentState } from '@/action-menu/states/recordIndexActionMenuDropdownPositionComponentState';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import {
RouterDecorator,
getCanvasElementForDropdownTesting,
} from 'twenty-ui/testing';
import { RouterDecorator } from 'twenty-ui/testing';
import { ContextStoreDecorator } from '~/testing/decorators/ContextStoreDecorator';
const deleteMock = test.fn();
@@ -81,8 +78,8 @@ export const WithInteractions: Story = {
args: {
actionMenuId: 'story',
},
play: async () => {
const canvas = within(getCanvasElementForDropdownTesting());
play: async ({ canvasElement }) => {
const canvas = within(canvasElement.ownerDocument.body);
const deleteButton = await canvas.findByText('Delete');
await userEvent.click(deleteButton);
@@ -6,6 +6,7 @@ import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/ob
import { TabListComponentInstanceContext } from '@/ui/layout/tab-list/states/contexts/TabListComponentInstanceContext';
import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { RightDrawerDecorator } from '~/testing/decorators/RightDrawerDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockedTasks } from '~/testing/mock-data/tasks';
@@ -27,6 +28,7 @@ const meta: Meta<typeof TaskGroups> = {
),
ComponentWithRouterDecorator,
ObjectMetadataItemsDecorator,
RightDrawerDecorator,
SnackBarDecorator,
],
};
@@ -116,8 +116,8 @@ export default meta;
type Story = StoryObj<typeof CommandMenu>;
export const DefaultWithoutSearch: Story = {
play: async () => {
const canvas = within(document.body);
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(await canvas.findByText('Go to People')).toBeVisible();
expect(await canvas.findByText('Go to Opportunities')).toBeVisible();
@@ -128,8 +128,8 @@ export const DefaultWithoutSearch: Story = {
};
export const LimitedPermissions: Story = {
play: async () => {
const canvas = within(document.body);
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(await canvas.findByText('Go to People')).toBeVisible();
expect(canvas.queryByText('Go to Opportunities')).not.toBeInTheDocument();
expect(canvas.queryByText('Go to Tasks')).not.toBeInTheDocument();
@@ -151,8 +151,8 @@ export const LimitedPermissions: Story = {
};
export const MatchingNavigate: Story = {
play: async () => {
const canvas = within(document.body);
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const searchInput = await canvas.findByTestId(
COMMAND_MENU_SEARCH_INPUT_FOCUS_ID,
);
@@ -163,8 +163,8 @@ export const MatchingNavigate: Story = {
};
export const MatchingNavigateShortcuts: Story = {
play: async () => {
const canvas = within(document.body);
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const searchInput = await canvas.findByTestId(
COMMAND_MENU_SEARCH_INPUT_FOCUS_ID,
);
@@ -176,8 +176,8 @@ export const MatchingNavigateShortcuts: Story = {
// TEMP_DISABLED_TEST: Temporarily commented out due to test failure
// export const SearchRecordsAction: Story = {
// play: async () => {
// const canvas = within(document.body);
// play: async ({ canvasElement }) => {
// const canvas = within(canvasElement.ownerDocument.body);
// const searchRecordsButton = await canvas.findByText('Search records');
// await userEvent.click(searchRecordsButton);
// const searchInput = await canvas.findByPlaceholderText('Type anything...');
@@ -190,8 +190,8 @@ export const MatchingNavigateShortcuts: Story = {
// };
export const NoResultsSearchFallback: Story = {
play: async () => {
const canvas = within(document.body);
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const searchInput = await canvas.findByTestId(
COMMAND_MENU_SEARCH_INPUT_FOCUS_ID,
);
@@ -225,8 +225,8 @@ export const NoResultsSearchFallback: Story = {
// TEMP_DISABLED_TEST: Temporarily commented out due to test failure
// export const ClickOnSearchRecordsAndGoBack: Story = {
// play: async () => {
// const canvas = within(document.body);
// play: async ({ canvasElement }) => {
// const canvas = within(canvasElement.ownerDocument.body);
// const searchRecordsButton = await canvas.findByText('Search records');
// await userEvent.click(searchRecordsButton);
// await sleep(openTimeout);
@@ -28,8 +28,8 @@ export default meta;
type Story = StoryObj<typeof KeyboardShortcutMenu>;
export const Default: Story = {
play: async () => {
const canvas = within(document.body);
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(await canvas.findByText('Keyboard shortcuts')).toBeInTheDocument();
},
@@ -1,6 +1,7 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { LayoutRenderingProvider } from '@/ui/layout/contexts/LayoutRenderingContext';
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
@@ -9,6 +10,7 @@ import { getCompaniesMock } from '~/testing/mock-data/companies';
import { RecordDetailDuplicatesSection } from '@/object-record/record-field-list/record-detail-section/duplicate/components/RecordDetailDuplicatesSection';
import { ComponentDecorator } from 'twenty-ui/testing';
import { PageLayoutType } from '~/generated/graphql';
const companiesMock = getCompaniesMock();
@@ -17,6 +19,22 @@ const meta: Meta<typeof RecordDetailDuplicatesSection> = {
'Modules/ObjectRecord/RecordShow/RecordDetailSection/RecordDetailDuplicatesSection',
component: RecordDetailDuplicatesSection,
decorators: [
(Story) => (
<LayoutRenderingProvider
value={{
targetRecordIdentifier: {
id: companiesMock[0].id,
targetObjectNameSingular: 'company',
},
layoutType: PageLayoutType.RECORD_PAGE,
isInRightDrawer: false,
// TODO: Remove once the traditional record show page is removed.
isLegacyRecordShowPage: true,
}}
>
<Story />
</LayoutRenderingProvider>
),
ComponentDecorator,
ObjectMetadataItemsDecorator,
SnackBarDecorator,
@@ -2,7 +2,6 @@ import { FormArrayFieldInput } from '@/object-record/record-field/ui/form-types/
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { expect, fn, userEvent, waitFor, within } from 'storybook/test';
import { isDefined } from 'twenty-shared/utils';
import { getCanvasElementForDropdownTesting } from 'twenty-ui/testing';
import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorator';
import { MOCKED_STEP_ID } from '~/testing/mock-data/workflow';
@@ -44,7 +43,7 @@ export const AddTwoItems: Story = {
});
const addItemButton = await within(
getCanvasElementForDropdownTesting(),
canvasElement.ownerDocument.body,
).findByText('Add item');
await userEvent.click(addItemButton);
@@ -54,7 +53,7 @@ export const AddTwoItems: Story = {
});
const newItemInput = await within(
getCanvasElementForDropdownTesting(),
canvasElement.ownerDocument.body,
).findByRole('textbox');
await userEvent.type(newItemInput, 'Second item{enter}');
@@ -69,7 +68,7 @@ export const AddTwoItems: Story = {
const secondItemMenuItem = await waitFor(() => {
const allSecondItems = within(
getCanvasElementForDropdownTesting(),
canvasElement.ownerDocument.body,
).getAllByText('Second item');
expect(allSecondItems).toHaveLength(2);
@@ -95,7 +94,7 @@ export const EditExistingItem: Story = {
await userEvent.click(firstItemChip);
const openSecondItemMenuButton = await waitFor(() => {
const button = getCanvasElementForDropdownTesting().querySelector(
const button = canvasElement.ownerDocument.body.querySelector(
'[aria-controls$="-1-options"] > button',
);
@@ -109,13 +108,13 @@ export const EditExistingItem: Story = {
await userEvent.click(openSecondItemMenuButton);
const editSecondItemButton = await within(
getCanvasElementForDropdownTesting(),
canvasElement.ownerDocument.body,
).findByText('Edit');
await userEvent.click(editSecondItemButton);
const editSecondItemInput = await within(
getCanvasElementForDropdownTesting(),
canvasElement.ownerDocument.body,
).findByRole('textbox');
expect(editSecondItemInput).toHaveValue('Second item');
@@ -152,7 +151,7 @@ export const DeleteExistingItem: Story = {
await userEvent.click(firstItemChip);
const openSecondItemMenuButton = await waitFor(() => {
const button = getCanvasElementForDropdownTesting().querySelector(
const button = canvasElement.ownerDocument.body.querySelector(
'[aria-controls$="-1-options"] > button',
);
@@ -166,7 +165,7 @@ export const DeleteExistingItem: Story = {
await userEvent.click(openSecondItemMenuButton);
const deleteSecondItemButton = await within(
getCanvasElementForDropdownTesting(),
canvasElement.ownerDocument.body,
).findByText('Delete');
await userEvent.click(deleteSecondItemButton);
@@ -1,7 +1,6 @@
import { FormBooleanFieldInput } from '@/object-record/record-field/ui/form-types/components/FormBooleanFieldInput';
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { expect, fn, userEvent, waitFor, within } from 'storybook/test';
import { getCanvasElementForDropdownTesting } from 'twenty-ui/testing';
import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorator';
import { MOCKED_STEP_ID } from '~/testing/mock-data/workflow';
@@ -118,7 +117,7 @@ export const ChangesValueToTrue: Story = {
await userEvent.click(select);
const trueOption = await within(
getCanvasElementForDropdownTesting(),
canvasElement.ownerDocument.body,
).findByText('True');
await userEvent.click(trueOption);
@@ -143,7 +142,7 @@ export const ChangesValueToFalse: Story = {
await userEvent.click(select);
const falseOption = await within(
getCanvasElementForDropdownTesting(),
canvasElement.ownerDocument.body,
).findByText('False');
await userEvent.click(falseOption);
@@ -1,7 +1,6 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { useEffect } from 'react';
import { expect, fn, userEvent, within } from 'storybook/test';
import { getCanvasElementForDropdownTesting } from 'twenty-ui/testing';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { useEmailsField } from '@/object-record/record-field/ui/meta-types/hooks/useEmailsField';
@@ -170,13 +169,13 @@ export const CanNotSetPrimaryLinkAsPrimaryLink: Story = {
await userEvent.click(openDropdownButtons[0]);
const editOption = await within(
getCanvasElementForDropdownTesting(),
canvasElement.ownerDocument.body,
).findByText('Edit');
expect(editOption).toBeVisible();
const setPrimaryOption = within(
getCanvasElementForDropdownTesting(),
canvasElement.ownerDocument.body,
).queryByText('Set as Primary');
expect(setPrimaryOption).not.toBeInTheDocument();
@@ -11,7 +11,6 @@ import { RECORD_TABLE_CELL_INPUT_ID_PREFIX } from '@/object-record/record-table/
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { getCanvasElementForDropdownTesting } from 'twenty-ui/testing';
import { FieldMetadataType } from '~/generated-metadata/graphql';
const updateRecord = fn();
@@ -296,7 +295,7 @@ export const DeletePrimaryLink: Story = {
await userEvent.click(openDropdownButton);
const deleteOption = await within(
getCanvasElementForDropdownTesting(),
canvasElement.ownerDocument.body,
).findByText('Delete');
await userEvent.click(deleteOption);
@@ -335,7 +334,7 @@ export const DeletePrimaryLinkAndUseSecondaryLinkAsTheNewPrimaryLink: Story = {
await userEvent.click(openDropdownButtons[0]);
const deleteOption = await within(
getCanvasElementForDropdownTesting(),
canvasElement.ownerDocument.body,
).findByText('Delete');
await userEvent.click(deleteOption);
@@ -377,7 +376,7 @@ export const DeleteSecondaryLink: Story = {
await userEvent.click(openDropdownButtons[1]);
const deleteOption = await within(
getCanvasElementForDropdownTesting(),
canvasElement.ownerDocument.body,
).findByText('Delete');
await userEvent.click(deleteOption);
@@ -485,7 +484,7 @@ export const MakeSecondaryLinkPrimary: Story = {
await userEvent.click(openDropdownButtons[1]); // Click the secondary link's dropdown
const setPrimaryOption = await within(
getCanvasElementForDropdownTesting(),
canvasElement.ownerDocument.body,
).findByText('Set as Primary');
await userEvent.click(setPrimaryOption);
},
@@ -514,7 +513,7 @@ export const CanNotSetPrimaryLinkAsPrimaryLink: Story = {
// Should not see "Set as Primary" option for primary link
const setPrimaryOption = within(
getCanvasElementForDropdownTesting(),
canvasElement.ownerDocument.body,
).queryByText('Set as Primary');
expect(setPrimaryOption).not.toBeInTheDocument();
},
@@ -1,7 +1,6 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { useEffect } from 'react';
import { expect, userEvent, within } from 'storybook/test';
import { getCanvasElementForDropdownTesting } from 'twenty-ui/testing';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { usePhonesField } from '@/object-record/record-field/ui/meta-types/hooks/usePhonesField';
@@ -185,13 +184,13 @@ export const CanNotSetPrimaryLinkAsPrimaryLink: Story = {
await userEvent.click(openDropdownButtons[0]);
const editOption = await within(
getCanvasElementForDropdownTesting(),
canvasElement.ownerDocument.body,
).findByText('Edit');
expect(editOption).toBeVisible();
const setPrimaryOption = within(
getCanvasElementForDropdownTesting(),
canvasElement.ownerDocument.body,
).queryByText('Set as Primary');
expect(setPrimaryOption).not.toBeInTheDocument();
@@ -25,7 +25,6 @@ import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePush
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { FieldMetadataType } from 'twenty-shared/types';
import { getCanvasElementForDropdownTesting } from 'twenty-ui/testing';
import { RelationManyToOneFieldInput } from '@/object-record/record-field/ui/meta-types/input/components/RelationManyToOneFieldInput';
import { getFieldInputEventContextProviderWithJestMocks } from './utils/getFieldInputEventContextProviderWithJestMocks';
@@ -158,8 +157,8 @@ type Story = StoryObj<typeof RelationManyToOneFieldInputWithContext>;
export const Default: Story = {};
export const Submit: Story = {
play: async () => {
const canvas = within(getCanvasElementForDropdownTesting());
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(handleSubmitMocked).toHaveBeenCalledTimes(0);
@@ -176,8 +175,8 @@ export const Submit: Story = {
};
export const Cancel: Story = {
play: async () => {
const canvas = within(getCanvasElementForDropdownTesting());
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(handleCancelMocked).toHaveBeenCalledTimes(0);
await canvas.findByText('Linkedin', undefined, { timeout: 3000 });
@@ -47,15 +47,16 @@ export const Default: Story = {
};
export const HeaderMenuOpen: Story = {
play: async () => {
const canvas = within(document.body);
await canvas.findByText('Linkedin', {}, { timeout: 3000 });
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const body = within(canvasElement.ownerDocument.body);
await canvas.findAllByText('Linkedin', {}, { timeout: 3000 });
const headerMenuButton = await canvas.findByText('Domain Name');
await userEvent.click(headerMenuButton);
await canvas.findByText('Move right');
await body.findByText('Move right');
},
};
@@ -65,11 +66,11 @@ export const ScrolledLeft: Story = {
width: 1000,
},
},
play: async () => {
const canvas = within(document.body);
await canvas.findByText('Linkedin', {}, { timeout: 3000 });
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findAllByText('Linkedin', {}, { timeout: 3000 });
const scrollWrapper = document.body.querySelector(
const scrollWrapper = canvasElement.ownerDocument.body.querySelector(
'.scroll-wrapper-x-enabled',
);
@@ -95,11 +96,11 @@ export const ScrolledBottom: Story = {
height: 300,
},
},
play: async () => {
const canvas = within(document.body);
await canvas.findByText('Linkedin', {}, { timeout: 3000 });
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findAllByText('Linkedin', {}, { timeout: 3000 });
const scrollWrapper = document.body.querySelector(
const scrollWrapper = canvasElement.ownerDocument.body.querySelector(
'.scroll-wrapper-y-enabled',
);
@@ -44,8 +44,7 @@ const StyledDragHandleContainer = styled.div`
background: ${({ theme }) => theme.background.transparent.secondary};
backdrop-filter: ${({ theme }) => theme.blur.medium};
color: ${({ theme }) => theme.font.color.primary};
box-shadow:
${({ theme }) => theme.boxShadow.light},
box-shadow: ${({ theme }) => theme.boxShadow.light},
${({ theme }) => theme.boxShadow.strong};
}
@@ -67,8 +67,7 @@ const StyledWidgetCard = styled.div<{
${isDragging &&
css`
background:
linear-gradient(
background: linear-gradient(
0deg,
${theme.background.transparent.lighter} 0%,
${theme.background.transparent.lighter} 100%
@@ -120,8 +119,7 @@ const StyledWidgetCard = styled.div<{
${isDragging &&
css`
background:
linear-gradient(
background: linear-gradient(
0deg,
${theme.background.transparent.lighter} 0%,
${theme.background.transparent.lighter} 100%
@@ -34,6 +34,4 @@ export const Default: Story = {
hasRightElement: false,
onChange: fn(),
},
decorators: [ObjectMetadataItemsDecorator],
};
@@ -1 +1,2 @@
export const SETTINGS_OBJECT_MODEL_IS_LABEL_SYNCED_WITH_NAME_LABEL_DEFAULT_VALUE = true;
export const SETTINGS_OBJECT_MODEL_IS_LABEL_SYNCED_WITH_NAME_LABEL_DEFAULT_VALUE =
true;
@@ -39,8 +39,8 @@ type Story = StoryObj<typeof SettingsObjectInactiveMenuDropDown>;
export const Default: Story = {};
export const Open: Story = {
play: async () => {
const canvas = within(document.body);
play: async ({ canvasElement }) => {
const canvas = within(canvasElement.ownerDocument.body);
const dropdownButton = await canvas.findByRole('button', {
name: 'Inactive Object Options',
@@ -51,8 +51,8 @@ export const Open: Story = {
};
export const WithActivate: Story = {
play: async () => {
const canvas = within(document.body);
play: async ({ canvasElement }) => {
const canvas = within(canvasElement.ownerDocument.body);
const dropdownButton = await canvas.findByRole('button', {
name: 'Inactive Object Options',
@@ -74,8 +74,8 @@ export const WithActivate: Story = {
export const WithDelete: Story = {
args: { isCustomObject: true },
play: async () => {
const canvas = within(document.body);
play: async ({ canvasElement }) => {
const canvas = within(canvasElement.ownerDocument.body);
const dropdownButton = await canvas.findByRole('button', {
name: 'Inactive Object Options',
@@ -1,5 +1,5 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { useArgs } from 'storybook/preview-api';
import { useState } from 'react';
import { expect, userEvent, within } from 'storybook/test';
import { IconsProviderDecorator } from '~/testing/decorators/IconsProviderDecorator';
@@ -11,16 +11,17 @@ import {
} from '@/ui/input/components/IconPicker';
import { ComponentDecorator } from 'twenty-ui/testing';
type RenderProps = IconPickerProps;
const Render = (args: RenderProps) => {
const [{ selectedIconKey }, updateArgs] = useArgs();
type IconPickerStoryProps = IconPickerProps;
const IconPickerStory = (args: IconPickerStoryProps) => {
const [selectedIconKey, setSelectedIconKey] = useState(args.selectedIconKey);
return (
<IconPicker
// eslint-disable-next-line react/jsx-props-no-spreading
{...args}
onChange={({ iconKey }) => {
updateArgs({ selectedIconKey: iconKey });
setSelectedIconKey(iconKey);
}}
selectedIconKey={selectedIconKey}
/>
@@ -31,7 +32,10 @@ const meta: Meta<typeof IconPicker> = {
title: 'UI/Input/IconPicker/IconPicker',
component: IconPicker,
decorators: [IconsProviderDecorator, ComponentDecorator],
render: Render,
render: (args: IconPickerProps) => (
// eslint-disable-next-line react/jsx-props-no-spreading
<IconPickerStory key={args.selectedIconKey ?? 'no-selection'} {...args} />
),
};
export default meta;
@@ -40,8 +44,8 @@ type Story = StoryObj<typeof IconPicker>;
export const Default: Story = {};
export const WithOpen: Story = {
play: async () => {
const canvas = within(document.body);
play: async ({ canvasElement }) => {
const canvas = within(canvasElement.ownerDocument.body);
const iconPickerButton = await canvas.findByRole('button', {
name: 'Click to select icon (no icon selected)',
@@ -57,8 +61,8 @@ export const WithSelectedIcon: Story = {
export const WithOpenAndSelectedIcon: Story = {
...WithSelectedIcon,
play: async () => {
const canvas = within(document.body);
play: async ({ canvasElement }) => {
const canvas = within(canvasElement.ownerDocument.body);
const iconPickerButton = await canvas.findByRole('button', {
name: 'Click to select icon (selected: IconCalendarEvent)',
@@ -70,8 +74,8 @@ export const WithOpenAndSelectedIcon: Story = {
export const WithSearch: Story = {
...WithSelectedIcon,
play: async () => {
const canvas = within(document.body);
play: async ({ canvasElement }) => {
const canvas = within(canvasElement.ownerDocument.body);
const iconPickerButton = await canvas.findByRole('button', {
name: 'Click to select icon (selected: IconCalendarEvent)',
@@ -95,8 +99,8 @@ export const WithSearch: Story = {
export const WithSearchAndClose: Story = {
...WithSelectedIcon,
play: async () => {
const canvas = within(document.body);
play: async ({ canvasElement }) => {
const canvas = within(canvasElement.ownerDocument.body);
let iconPickerButton = await canvas.findByRole('button', {
name: 'Click to select icon (selected: IconCalendarEvent)',
@@ -128,8 +132,6 @@ export const WithSearchAndClose: Story = {
await userEvent.click(iconPickerButton);
await sleep(500);
searchInput = await canvas.findByRole('textbox');
expect(searchInput).toHaveValue('');
@@ -41,8 +41,8 @@ export const Disabled: Story = {
export const WithLabel: Story = {
args: { label: 'My Textarea' },
play: async () => {
const canvas = within(document.body);
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const label = await canvas.findByText('My Textarea');
@@ -1,33 +1,31 @@
import { DateTimePicker } from '@/ui/input/components/internal/date/components/DateTimePicker';
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { useArgs } from 'storybook/preview-api';
import { useState } from 'react';
import { expect, userEvent, within } from 'storybook/test';
import { Temporal } from 'temporal-polyfill';
import { ComponentDecorator } from 'twenty-ui/testing';
const INITIAL_DATE = Temporal.ZonedDateTime.from(
'2023-01-01T02:00:00+00:00[UTC]',
);
const DateTimePickerStory = () => {
const [date, setDate] = useState<Temporal.ZonedDateTime | null>(INITIAL_DATE);
return (
<DateTimePicker
instanceId="story-date-time-picker"
date={date}
onChange={setDate}
/>
);
};
const meta: Meta<typeof DateTimePicker> = {
title: 'UI/Input/Internal/InternalDatePicker',
component: DateTimePicker,
decorators: [ComponentDecorator],
argTypes: {
date: { control: 'date' },
},
render: ({ date }) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const [, updateArgs] = useArgs();
return (
<DateTimePicker
instanceId="story-date-time-picker"
date={date}
onChange={(newDate) => updateArgs({ date: newDate })}
/>
);
},
args: {
date: Temporal.Instant.from('2023-01-01T02:00:00Z').toZonedDateTimeISO(
'UTC',
),
},
render: () => <DateTimePickerStory />,
};
export default meta;
@@ -36,14 +34,15 @@ type Story = StoryObj<typeof DateTimePicker>;
export const Default: Story = {};
export const WithOpenMonthSelect: Story = {
play: async () => {
const canvas = within(document.body);
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const body = within(canvasElement.ownerDocument.body);
const monthSelect = await canvas.findByText('January');
await userEvent.click(monthSelect);
[
for (const monthLabel of [
'February',
'March',
'April',
@@ -55,29 +54,30 @@ export const WithOpenMonthSelect: Story = {
'October',
'November',
'December',
].forEach(async (monthLabel) =>
expect(await canvas.findByText(monthLabel)).toBeInTheDocument(),
);
]) {
expect(await body.findByText(monthLabel)).toBeInTheDocument();
}
await userEvent.click(await canvas.findByText('February'));
await userEvent.click(await body.findByText('February'));
expect(await canvas.findByText('February')).toBeInTheDocument();
},
};
export const WithOpenYearSelect: Story = {
play: async () => {
const canvas = within(document.body);
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const body = within(canvasElement.ownerDocument.body);
const yearSelect = await canvas.findByText('2023');
await userEvent.click(yearSelect);
['2024', '2025', '2026'].forEach(async (yearLabel) =>
expect(await canvas.findByText(yearLabel)).toBeInTheDocument(),
);
for (const yearLabel of ['2024', '2025', '2026']) {
expect(await body.findByText(yearLabel)).toBeInTheDocument();
}
await userEvent.click(await canvas.findByText('2024'));
await userEvent.click(await body.findByText('2024'));
expect(await canvas.findByText('2024')).toBeInTheDocument();
},
@@ -91,8 +91,8 @@ export const Empty: Story = {
</DropdownContent>
),
},
play: async () => {
const canvas = within(document.body);
play: async ({ canvasElement }) => {
const canvas = within(canvasElement.ownerDocument.body);
const buttons = await canvas.findAllByRole('button');
await userEvent.click(buttons[0]);
@@ -222,8 +222,8 @@ const optionsMock = [
// );
// };
const playInteraction: PlayFunction<any, any> = async () => {
const canvas = within(document.body);
const playInteraction: PlayFunction<any, any> = async ({ canvasElement }) => {
const canvas = within(canvasElement.ownerDocument.body);
const buttons = await canvas.findAllByRole('button');
await userEvent.click(buttons[0]);
@@ -279,8 +279,8 @@ export const SearchWithLoadingMenu: Story = {
</DropdownContent>
),
},
play: async () => {
const canvas = within(document.body);
play: async ({ canvasElement }) => {
const canvas = within(canvasElement.ownerDocument.body);
const buttons = await canvas.findAllByRole('button');
@@ -75,8 +75,8 @@ export const CloseOnEscape: Story = {
confirmButtonText: 'Confirm',
onClose: closeMock,
},
play: async () => {
const body = within(document.body);
play: async ({ canvasElement }) => {
const body = within(canvasElement.ownerDocument.body);
await body.findByText('Escape Key Test');
@@ -98,8 +98,8 @@ export const CloseOnClickOutside: Story = {
confirmButtonText: 'Confirm',
onClose: closeMock,
},
play: async () => {
const body = within(document.body);
play: async ({ canvasElement }) => {
const body = within(canvasElement.ownerDocument.body);
await body.findByText('Click Outside Test');
@@ -124,8 +124,8 @@ export const ConfirmWithEnterKey: Story = {
confirmButtonText: 'Confirm',
onConfirmClick: confirmMock,
},
play: async () => {
const body = within(document.body);
play: async ({ canvasElement }) => {
const body = within(canvasElement.ownerDocument.body);
await body.findByText('Enter Key Test');
@@ -145,8 +145,8 @@ export const CancelButtonClick: Story = {
confirmButtonText: 'Confirm',
onClose: closeMock,
},
play: async () => {
const body = within(document.body);
play: async ({ canvasElement }) => {
const body = within(canvasElement.ownerDocument.body);
await body.findByText('Cancel Button Test');
@@ -169,8 +169,8 @@ export const ConfirmButtonClick: Story = {
confirmButtonText: 'Confirm',
onConfirmClick: confirmMock,
},
play: async () => {
const body = within(document.body);
play: async ({ canvasElement }) => {
const body = within(canvasElement.ownerDocument.body);
await body.findByText('Confirm Button Test');
@@ -121,8 +121,8 @@ export const Default: Story = {
</>
),
},
play: async () => {
const canvas = within(document.body);
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(await canvas.findByText('Workspace')).toBeInTheDocument();
},
@@ -200,8 +200,8 @@ export const Settings: Story = {
</>
),
},
play: async () => {
const canvas = within(document.body);
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(await canvas.findByText('User')).toBeInTheDocument();
},
@@ -1,6 +1,5 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { type TaskGroups } from '@/activities/tasks/components/TaskGroups';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
@@ -19,11 +18,8 @@ import { useRecordIndexFieldMetadataDerivedStates } from '@/object-record/record
import { ViewBarFilterDropdownIds } from '@/views/constants/ViewBarFilterDropdownIds';
import { coreViewsState } from '@/views/states/coreViewState';
import { useSetRecoilState } from 'recoil';
import { within } from 'storybook/test';
import {
ComponentDecorator,
getCanvasElementForDropdownTesting,
} from 'twenty-ui/testing';
import { userEvent, within } from 'storybook/test';
import { ComponentDecorator } from 'twenty-ui/testing';
import { ContextStoreDecorator } from '~/testing/decorators/ContextStoreDecorator';
import { IconsProviderDecorator } from '~/testing/decorators/IconsProviderDecorator';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
@@ -128,54 +124,54 @@ const meta: Meta<typeof ViewBarFilterDropdown> = {
};
export default meta;
type Story = StoryObj<typeof TaskGroups>;
type Story = StoryObj<typeof ViewBarFilterDropdown>;
export const Default: Story = {
play: async () => {
const canvas = within(getCanvasElementForDropdownTesting());
play: async ({ canvasElement }) => {
const canvas = within(canvasElement.ownerDocument.body);
const filterButton = await canvas.findByText('Filter');
filterButton.click();
await userEvent.click(filterButton);
const textFilter = await canvas.findByText('Tagline');
textFilter.click();
await userEvent.click(textFilter);
const operatorDropdown = await canvas.findByText('Contains');
operatorDropdown.click();
await userEvent.click(operatorDropdown);
const containsOption = await canvas.findByText("Doesn't contain");
containsOption.click();
await userEvent.click(containsOption);
},
};
export const Date: Story = {
play: async () => {
const canvas = within(getCanvasElementForDropdownTesting());
play: async ({ canvasElement }) => {
const canvas = within(canvasElement.ownerDocument.body);
const filterButton = await canvas.findByText('Filter');
filterButton.click();
await userEvent.click(filterButton);
const dateFilter = await canvas.findByText('Last update');
dateFilter.click();
await userEvent.click(dateFilter);
},
};
export const Number: Story = {
play: async () => {
const canvas = within(getCanvasElementForDropdownTesting());
play: async ({ canvasElement }) => {
const canvas = within(canvasElement.ownerDocument.body);
const filterButton = await canvas.findByText('Filter');
filterButton.click();
await userEvent.click(filterButton);
const dateFilter = await canvas.findByText('Employees');
dateFilter.click();
await userEvent.click(dateFilter);
},
};
@@ -36,8 +36,7 @@ const StyledNodeContainer = styled.div<{
: colors.unselected.borderColor};
&:hover {
background:
linear-gradient(
background: linear-gradient(
0deg,
${theme.background.transparent.lighter} 0%,
${theme.background.transparent.lighter} 100%
@@ -1,7 +1,7 @@
import { WorkflowEditActionCreateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionCreateRecord';
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { expect, fn, userEvent, within } from 'storybook/test';
import { ComponentDecorator } from 'twenty-ui/testing';
import { ComponentDecorator, RouterDecorator } from 'twenty-ui/testing';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { WorkflowStepActionDrawerDecorator } from '~/testing/decorators/WorkflowStepActionDrawerDecorator';
@@ -43,6 +43,7 @@ const meta: Meta<typeof WorkflowEditActionCreateRecord> = {
WorkflowStepActionDrawerDecorator,
WorkflowStepDecorator,
ComponentDecorator,
RouterDecorator,
ObjectMetadataItemsDecorator,
SnackBarDecorator,
WorkspaceDecorator,
@@ -3,7 +3,7 @@ import { WorkflowEditActionFilter } from '@/workflow/workflow-steps/workflow-act
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { fn } from 'storybook/test';
import { StepLogicalOperator, ViewFilterOperand } from 'twenty-shared/types';
import { ComponentDecorator } from 'twenty-ui/testing';
import { ComponentDecorator, RouterDecorator } from 'twenty-ui/testing';
import { WorkflowStepActionDrawerDecorator } from '~/testing/decorators/WorkflowStepActionDrawerDecorator';
import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorator';
import { WorkspaceDecorator } from '~/testing/decorators/WorkspaceDecorator';
@@ -85,6 +85,7 @@ const meta: Meta<typeof WorkflowEditActionFilter> = {
WorkflowStepActionDrawerDecorator,
WorkflowStepDecorator,
ComponentDecorator,
RouterDecorator,
WorkspaceDecorator,
],
};
@@ -192,7 +192,7 @@ export const ReadonlyMode: Story = {
// await waitFor(() => {
// expect(
// within(getCanvasElementForDropdownTesting()).getByText('Louis Duss'),
// within(document.body).getByText('Louis Duss'),
// ).toBeVisible();
// });
@@ -7,7 +7,6 @@ import {
import { graphqlMocks } from '~/testing/graphqlMocks';
import { within } from 'storybook/test';
import { getCanvasElementForDropdownTesting } from 'twenty-ui/testing';
import { SettingsExperience } from '~/pages/settings/profile/appearance/components/SettingsExperience';
const meta: Meta<PageDecoratorArgs> = {
@@ -26,8 +25,8 @@ export default meta;
export type Story = StoryObj<typeof SettingsExperience>;
export const Default: Story = {
play: async () => {
const canvas = within(getCanvasElementForDropdownTesting());
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findAllByText('Experience', undefined, {
timeout: 3000,
@@ -39,8 +38,8 @@ export const Default: Story = {
// TEMP_DISABLED_TEST: Temporarily commented out due to test failure
// export const DateTimeSettingsTimeFormat: Story = {
// play: async () => {
// const canvas = within(getCanvasElementForDropdownTesting());
// play: async ({ canvasElement }) => {
// const canvas = within(canvasElement.ownerDocument.body);
// await canvas.findByText('Formats');
@@ -58,8 +57,8 @@ export const Default: Story = {
// TEMP_DISABLED_TEST: Temporarily commented out due to test failure
// export const DateTimeSettingsTimezone: Story = {
// play: async () => {
// const canvas = within(getCanvasElementForDropdownTesting());
// play: async ({ canvasElement }) => {
// const canvas = within(canvasElement.ownerDocument.body);
// await canvas.findByText('Formats');
@@ -81,8 +80,8 @@ export const Default: Story = {
// TEMP_DISABLED_TEST: Temporarily commented out due to test failure
// export const DateTimeSettingsDateFormat: Story = {
// play: async () => {
// const canvas = within(getCanvasElementForDropdownTesting());
// play: async ({ canvasElement }) => {
// const canvas = within(canvasElement.ownerDocument.body);
// await canvas.findByText('Formats');
@@ -26,8 +26,8 @@ export default meta;
export type Story = StoryObj<typeof SettingsNewObject>;
export const WithStandardSelected: Story = {
play: async () => {
const canvas = within(document.body);
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByRole(
'heading',
@@ -40,8 +40,8 @@ export const CustomObject: Story = {
};
export const ObjectTabs: Story = {
play: async () => {
const canvas = within(document.body);
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const fieldsTab = await canvas.findByTestId('tab-fields');
const settingsTab = await canvas.findByTestId('tab-settings');
@@ -54,8 +54,8 @@ export type Story = StoryObj<typeof SettingsDevelopersApiKeyDetail>;
// TEMP_DISABLED_TEST: Temporarily commented out due to test failure
// export const RegenerateApiKey: Story = {
// play: async ({ step }) => {
// const canvas = within(document.body);
// play: async ({ canvasElement, step }) => {
// const canvas = within(canvasElement.ownerDocument.body);
// await canvas.findByText('Role', undefined, { timeout: 3000 });
@@ -1,5 +1,6 @@
import { getOperationName } from '@apollo/client/utilities';
import { graphql, type GraphQLQuery, http, HttpResponse } from 'msw';
import { parse, type FieldNode } from 'graphql';
import { graphql, http, HttpResponse, type GraphQLQuery } from 'msw';
import { TRACK_ANALYTICS } from '@/analytics/graphql/queries/track';
import { FIND_MANY_OBJECT_METADATA_ITEMS } from '@/object-metadata/graphql/queries';
@@ -40,6 +41,39 @@ const peopleMock = getPeopleRecordConnectionMock();
const companiesMock = getCompaniesRecordConnectionMock();
const duplicateCompanyMock = getCompanyDuplicateMock();
const getRootFieldNamesFromQuery = (query: string) => {
try {
const document = parse(query);
const operationDefinition = document.definitions.find(
(definition) => definition.kind === 'OperationDefinition',
);
if (
!operationDefinition ||
operationDefinition.kind !== 'OperationDefinition'
) {
return [];
}
return operationDefinition.selectionSet.selections
.filter((selection): selection is FieldNode => selection.kind === 'Field')
.map((selection) => selection.name.value);
} catch {
return [];
}
};
const createEmptyRecordConnection = () => ({
edges: [],
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
endCursor: null,
},
totalCount: 0,
});
export const metadataGraphql = graphql.link(
`${REACT_APP_SERVER_BASE_URL}/metadata`,
);
@@ -228,6 +262,17 @@ export const graphqlMocks = {
},
});
}),
graphql.query('CombinedFindManyRecords', ({ query }) => {
const rootFieldNames = getRootFieldNamesFromQuery(query ?? '');
const data = Object.fromEntries(
rootFieldNames.map((fieldName) => [
fieldName,
createEmptyRecordConnection(),
]),
);
return HttpResponse.json({ data });
}),
graphql.query('FindManyViews', ({ variables }) => {
const objectMetadataId = variables.filter?.objectMetadataId?.eq;
const viewType = variables.filter?.type?.eq;
+1
View File
@@ -12,6 +12,7 @@
"lingui.config.ts",
"jest.config.mjs",
"vite.config.ts",
"vitest.config.ts",
"setupTests.ts"
],
"exclude": [
+44
View File
@@ -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',
}),
],
test: {
name: 'storybook',
browser: {
enabled: true,
headless: true,
provider: playwright({}),
instances: [{ browser: 'chromium' }],
},
setupFiles: ['./.storybook/vitest.setup.ts'],
testTimeout: 5 * MINUTES_IN_MS,
},
},
],
},
});
+28 -12
View File
@@ -1,7 +1,15 @@
import type { StorybookConfig } from '@storybook/react-vite';
import path from 'path';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import checker from 'vite-plugin-checker';
const dirname =
typeof __dirname !== 'undefined'
? __dirname
: path.dirname(fileURLToPath(import.meta.url));
const isVitest = Boolean(process.env.VITEST);
const config: StorybookConfig = {
stories: ['../src/**/*.@(mdx|stories.@(js|jsx|ts|tsx))'],
@@ -10,21 +18,29 @@ const config: StorybookConfig = {
'@storybook/addon-links',
'@storybook/addon-coverage',
'storybook-addon-pseudo-states',
'@storybook/addon-vitest',
],
framework: '@storybook/react-vite',
viteFinal: async (viteConfig) => ({
...viteConfig,
plugins: [
...(viteConfig.plugins ?? []),
checker({
typescript: {
tsconfigPath: path.resolve(__dirname, '../tsconfig.dev.json'),
},
}),
],
}),
viteFinal: async (viteConfig) => {
const plugins = [...(viteConfig.plugins ?? [])];
if (!isVitest) {
plugins.push(
checker({
typescript: {
tsconfigPath: path.resolve(dirname, '../tsconfig.dev.json'),
},
}),
);
}
return {
...viteConfig,
plugins,
};
},
};
export default config;
@@ -1,21 +0,0 @@
import { getJestConfig } from '@storybook/test-runner';
const MINUTES_IN_MS = 60 * 1000;
/**
* @type {import('@jest/types').Config.InitialOptions}
*/
export default {
// The default configuration comes from @storybook/test-runner
...getJestConfig(),
/** Add your own overrides below
* @see https://jestjs.io/docs/configuration
*/
testTimeout: 5 * MINUTES_IN_MS,
modulePathIgnorePatterns: [
'<rootDir>/.nx/cache/*',
'<rootDir>/packages/twenty-server/dist/*',
'<rootDir>/packages/twenty-front/dist/*',
'<rootDir>/packages/twenty-ui/dist/*',
],
};
@@ -0,0 +1,5 @@
import { setProjectAnnotations } from '@storybook/react-vite';
import * as projectAnnotations from './preview';
// Apply Storybook's preview configuration to Vitest runs.
setProjectAnnotations([projectAnnotations]);
+3 -11
View File
@@ -101,16 +101,8 @@
"test": {}
}
},
"storybook:coverage": {},
"storybook:test": {
"options": {
"port": 6007
}
},
"storybook:serve-and-test:static": {
"options": {
"port": 6007
}
}
"storybook:test": {},
"storybook:test:no-coverage": {},
"storybook:coverage": {}
}
}
-1
View File
@@ -24,4 +24,3 @@ export { RecoilRootDecorator } from './decorators/RecoilRootDecorator';
export { RouterDecorator } from './decorators/RouterDecorator';
export { AVATAR_URL_MOCK } from './mocks/avatarUrlMock';
export type { CatalogStory } from './types/CatalogStory';
export { getCanvasElementForDropdownTesting } from './utils/getCanvasElementForDropdownTesting';
@@ -1,14 +0,0 @@
import { isDefined } from 'twenty-shared/utils';
export const getCanvasElementForDropdownTesting = () => {
const canvasElement = document.getElementsByClassName(
'sb-show-main',
)[0] as HTMLElement | null;
if (!isDefined(canvasElement)) {
throw new Error(
`Canvas element not found for dropdown testing in storybook, verify that storybook still uses the class name "sb-show-main" for its body that displays stories.`,
);
}
return canvasElement;
};
+39
View File
@@ -0,0 +1,39 @@
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: {
projects: [
{
extends: './vite.config.ts',
plugins: [
storybookTest({
configDir: path.join(dirname, '.storybook'),
storybookScript: 'yarn storybook --no-open --port 6007',
}),
],
test: {
name: 'storybook',
browser: {
enabled: true,
headless: true,
provider: playwright({}),
instances: [{ browser: 'chromium' }],
},
setupFiles: ['./.storybook/vitest.setup.ts'],
testTimeout: 5 * MINUTES_IN_MS,
},
},
],
},
});
+4449 -302
View File
File diff suppressed because it is too large Load Diff