Replace test-runner with vitest for storybook (#17187)
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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]);
|
||||
@@ -11,7 +11,7 @@ const modulesCoverage = {
|
||||
branches: 25,
|
||||
statements: 43,
|
||||
lines: 44,
|
||||
functions: 37,
|
||||
functions: 30,
|
||||
include: ['src/modules/**/*'],
|
||||
exclude: ['src/**/*.ts'],
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
+3
-7
@@ -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);
|
||||
|
||||
+3
-6
@@ -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);
|
||||
|
||||
+2
@@ -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,
|
||||
],
|
||||
};
|
||||
|
||||
+14
-14
@@ -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);
|
||||
|
||||
+2
-2
@@ -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();
|
||||
},
|
||||
|
||||
+18
@@ -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,
|
||||
|
||||
+8
-9
@@ -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);
|
||||
|
||||
+2
-3
@@ -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);
|
||||
|
||||
+2
-3
@@ -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();
|
||||
|
||||
+5
-6
@@ -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();
|
||||
},
|
||||
|
||||
+2
-3
@@ -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();
|
||||
|
||||
+4
-5
@@ -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 });
|
||||
|
||||
+13
-12
@@ -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',
|
||||
);
|
||||
|
||||
|
||||
+1
-2
@@ -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};
|
||||
}
|
||||
|
||||
|
||||
+2
-4
@@ -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%
|
||||
|
||||
-2
@@ -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;
|
||||
|
||||
+6
-6
@@ -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',
|
||||
|
||||
+18
-16
@@ -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('');
|
||||
|
||||
+2
-2
@@ -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');
|
||||
|
||||
|
||||
+33
-33
@@ -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();
|
||||
},
|
||||
|
||||
+6
-6
@@ -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');
|
||||
|
||||
|
||||
+10
-10
@@ -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');
|
||||
|
||||
|
||||
+4
-4
@@ -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();
|
||||
},
|
||||
|
||||
+17
-21
@@ -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);
|
||||
},
|
||||
};
|
||||
|
||||
+1
-2
@@ -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%
|
||||
|
||||
+2
-1
@@ -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,
|
||||
|
||||
+2
-1
@@ -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,
|
||||
],
|
||||
};
|
||||
|
||||
+1
-1
@@ -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');
|
||||
|
||||
|
||||
+2
-2
@@ -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',
|
||||
|
||||
+2
-2
@@ -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');
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"lingui.config.ts",
|
||||
"jest.config.mjs",
|
||||
"vite.config.ts",
|
||||
"vitest.config.ts",
|
||||
"setupTests.ts"
|
||||
],
|
||||
"exclude": [
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
@@ -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]);
|
||||
@@ -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": {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user