From ecc7b38b75bf7cfb8ce7980817108ba3f83e4102 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?= <71827178+bosiraphael@users.noreply.github.com> Date: Mon, 15 Jun 2026 13:37:19 +0200 Subject: [PATCH] Remove randomness from flaky twenty-front Storybook stories (#21594) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Several twenty-front Storybook stories were flagged flaky by Argos because they render different pixels across runs. This removes the non-determinism behind them. **What changed** - **Images** — replaced random `picsum.photos` URLs in the Logo and TabList stories with the existing `AVATAR_URL_MOCK`, and added global MSW handlers in `.storybook/preview.tsx` that serve a deterministic image for every remote host (picsum, twenty-icons.com, twentyhq.github.io, etc.) so no story depends on a network image load. - **Numbers** — the line-chart story built its data with `Math.random()`; now uses a deterministic formula. - **Dates** — the terminal "long output" story stamped its lines with `new Date()`; now uses a fixed base timestamp. The calendar-channel date/time format previews and example event used render-time `Date.now()`/`new Date()` in shared components; they now use a fixed reference date (`DateTimeSettingsPreviewDate`). - **Lazy-load timing** — the date-picker story now waits for the lazily-loaded calendar before the snapshot. Review in cubic --- packages/twenty-front/.storybook/preview.tsx | 71 ++++++++++++------- .../__stories__/TerminalOutput.stories.tsx | 10 ++- .../components/__stories__/Logo.stories.tsx | 3 +- .../constants/DateTimeSettingsPreviewDate.ts | 3 + .../GraphWidgetLineChart.stories.tsx | 2 +- ...ettingsAccountsCalendarChannelsGeneral.tsx | 3 +- .../DateTimeSettingsDateFormatSelect.tsx | 9 +-- .../DateTimeSettingsTimeFormatSelect.tsx | 7 +- .../InternalDatePicker.stories.tsx | 11 ++- .../__stories__/Tablist.stories.tsx | 9 ++- 10 files changed, 88 insertions(+), 40 deletions(-) create mode 100644 packages/twenty-front/src/modules/localization/constants/DateTimeSettingsPreviewDate.ts diff --git a/packages/twenty-front/.storybook/preview.tsx b/packages/twenty-front/.storybook/preview.tsx index 64b164858c..c9648d9fb3 100644 --- a/packages/twenty-front/.storybook/preview.tsx +++ b/packages/twenty-front/.storybook/preview.tsx @@ -1,6 +1,7 @@ import { i18n } from '@lingui/core'; import { I18nProvider } from '@lingui/react'; import { type Preview } from '@storybook/react-vite'; +import { http, HttpResponse } from 'msw'; import { initialize, mswLoader } from 'msw-storybook-addon'; import { SOURCE_LOCALE } from 'twenty-shared/translations'; @@ -32,38 +33,60 @@ import { mockedUserJWT } from '~/testing/mock-data/jwt'; // oxlint-disable-next-line no-restricted-imports import { ClickOutsideListenerContext } from '../src/modules/ui/utilities/pointer-event/contexts/ClickOutsideListenerContext'; -initialize({ - onUnhandledRequest: async (request: Request) => { - const fileExtensionsToIgnore = - /\.(ts|tsx|js|jsx|svg|css|png|woff2)(\?v=[a-zA-Z0-9]+)?/; +const MOCK_IMAGE_SVG = + ''; - if (fileExtensionsToIgnore.test(request.url)) { - return; - } +const respondWithMockImage = () => + new HttpResponse(MOCK_IMAGE_SVG, { + headers: { 'Content-Type': 'image/svg+xml' }, + }); - if (request.url.startsWith('http://localhost:3000/files/')) { - return; - } +const remoteImageMockHandlers = [ + http.get('https://picsum.photos/*', respondWithMockImage), + http.get('https://twenty-icons.com/*', respondWithMockImage), + http.get('https://twentyhq.github.io/*', respondWithMockImage), + http.get('https://via.placeholder.com/*', respondWithMockImage), + http.get( + 'https://twenty-front-screenshots.s3.eu-west-3.amazonaws.com/*', + respondWithMockImage, + ), +]; - try { - const requestBody = await request.json(); +initialize( + { + onUnhandledRequest: async (request: Request) => { + const fileExtensionsToIgnore = + /\.(ts|tsx|js|jsx|svg|css|png|woff2)(\?v=[a-zA-Z0-9]+)?/; - // oxlint-disable-next-line no-console - console.warn(`Unhandled ${request.method} request to ${request.url} + if (fileExtensionsToIgnore.test(request.url)) { + return; + } + + if (request.url.startsWith('http://localhost:3000/files/')) { + return; + } + + try { + const requestBody = await request.json(); + + // oxlint-disable-next-line no-console + console.warn(`Unhandled ${request.method} request to ${request.url} with payload ${JSON.stringify(requestBody)}\n This request should be mocked with MSW`); - } catch (error) { - // oxlint-disable-next-line no-console - console.error(`Cannot parse msw request body : ${error}`); - } + } catch (error) { + // oxlint-disable-next-line no-console + console.error(`Cannot parse msw request body : ${error}`); + } - // oxlint-disable-next-line no-console - console.warn( - `Unhandled ${request.method} request to ${request.url} \n This request should be mocked with MSW`, - ); + // oxlint-disable-next-line no-console + console.warn( + `Unhandled ${request.method} request to ${request.url} \n This request should be mocked with MSW`, + ); + }, + quiet: true, }, - quiet: true, -}); + remoteImageMockHandlers, +); // Mirrors production's MinimalMetadataGater so any story rendering a // date-aware component (DateTimeDisplay, etc.) sees a real IANA timeZone diff --git a/packages/twenty-front/src/modules/ai/components/__stories__/TerminalOutput.stories.tsx b/packages/twenty-front/src/modules/ai/components/__stories__/TerminalOutput.stories.tsx index a5cde844b3..00e84e11be 100644 --- a/packages/twenty-front/src/modules/ai/components/__stories__/TerminalOutput.stories.tsx +++ b/packages/twenty-front/src/modules/ai/components/__stories__/TerminalOutput.stories.tsx @@ -112,12 +112,18 @@ export const Empty: Story = { }, }; +const LONG_OUTPUT_BASE_TIMESTAMP_MS = new Date( + '2024-03-12T09:30:00.000Z', +).getTime(); + export const LongOutput: Story = { args: { stdout: Array.from( { length: 100 }, - (_, i) => - `[${new Date().toISOString()}] Processing batch ${i + 1}/100...`, + (_, index) => + `[${new Date( + LONG_OUTPUT_BASE_TIMESTAMP_MS + index * 1000, + ).toISOString()}] Processing batch ${index + 1}/100...`, ).join('\n'), stderr: '', isRunning: false, diff --git a/packages/twenty-front/src/modules/auth/components/__stories__/Logo.stories.tsx b/packages/twenty-front/src/modules/auth/components/__stories__/Logo.stories.tsx index 870c9eda1c..38d97b0d43 100644 --- a/packages/twenty-front/src/modules/auth/components/__stories__/Logo.stories.tsx +++ b/packages/twenty-front/src/modules/auth/components/__stories__/Logo.stories.tsx @@ -2,11 +2,12 @@ import { type Meta, type StoryObj } from '@storybook/react-vite'; import { Logo } from '@/auth/components/Logo'; import { + AVATAR_URL_MOCK, ComponentDecorator, RouterDecorator, } from 'twenty-ui-deprecated/testing'; -const logoUrl = 'https://picsum.photos/192/192'; +const logoUrl = AVATAR_URL_MOCK; const meta: Meta = { title: 'Modules/Auth/Logo', diff --git a/packages/twenty-front/src/modules/localization/constants/DateTimeSettingsPreviewDate.ts b/packages/twenty-front/src/modules/localization/constants/DateTimeSettingsPreviewDate.ts new file mode 100644 index 0000000000..e49b9592cb --- /dev/null +++ b/packages/twenty-front/src/modules/localization/constants/DateTimeSettingsPreviewDate.ts @@ -0,0 +1,3 @@ +export const DATE_TIME_SETTINGS_PREVIEW_DATE = new Date( + '2024-03-12T09:30:00.000Z', +); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/components/__stories__/GraphWidgetLineChart.stories.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/components/__stories__/GraphWidgetLineChart.stories.tsx index d9425b4f0b..18e395dbb2 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/components/__stories__/GraphWidgetLineChart.stories.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/components/__stories__/GraphWidgetLineChart.stories.tsx @@ -115,7 +115,7 @@ const renderChart = (args: ChartArgs) => ( const generateLinearData = (points: number = 10) => { return Array.from({ length: points }, (_, i) => ({ x: i, - y: Math.floor(Math.random() * 100) + 20, + y: 20 + ((i * 37 + 13) % 80), })); }; diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarChannelsGeneral.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarChannelsGeneral.tsx index 88e9315eef..cf177d5ad8 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarChannelsGeneral.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarChannelsGeneral.tsx @@ -1,6 +1,7 @@ import { CalendarMonthCard } from '@/activities/calendar/components/CalendarMonthCard'; import { CalendarContext } from '@/activities/calendar/contexts/CalendarContext'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; +import { DATE_TIME_SETTINGS_PREVIEW_DATE } from '@/localization/constants/DateTimeSettingsPreviewDate'; import { SettingsAccountsCalendarDisplaySettings } from '@/settings/accounts/components/SettingsAccountsCalendarDisplaySettings'; import { styled } from '@linaria/react'; import { t } from '@lingui/core/macro'; @@ -24,7 +25,7 @@ const StyledGeneralContainer = styled.div` export const SettingsAccountsCalendarChannelsGeneral = () => { const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState); - const exampleStartDate = new Date(); + const exampleStartDate = DATE_TIME_SETTINGS_PREVIEW_DATE; const exampleEndDate = min([ addMinutes(exampleStartDate, 30), endOfDay(exampleStartDate), diff --git a/packages/twenty-front/src/modules/settings/experience/components/DateTimeSettingsDateFormatSelect.tsx b/packages/twenty-front/src/modules/settings/experience/components/DateTimeSettingsDateFormatSelect.tsx index 5f2ced128c..55c779266f 100644 --- a/packages/twenty-front/src/modules/settings/experience/components/DateTimeSettingsDateFormatSelect.tsx +++ b/packages/twenty-front/src/modules/settings/experience/components/DateTimeSettingsDateFormatSelect.tsx @@ -1,6 +1,7 @@ import { formatInTimeZone } from 'date-fns-tz'; import { DateFormat } from '@/localization/constants/DateFormat'; +import { DATE_TIME_SETTINGS_PREVIEW_DATE } from '@/localization/constants/DateTimeSettingsPreviewDate'; import { detectDateFormat } from '@/localization/utils/detection/detectDateFormat'; import { detectTimeZone } from '@/localization/utils/detection/detectTimeZone'; import { Select } from '@/ui/input/components/Select'; @@ -26,7 +27,7 @@ export const DateTimeSettingsDateFormatSelect = ({ const systemDateFormat = DateFormat[detectDateFormat()]; const systemDateFormatLabel = formatInTimeZone( - Date.now(), + DATE_TIME_SETTINGS_PREVIEW_DATE, usedTimeZone, systemDateFormat, ); @@ -46,7 +47,7 @@ export const DateTimeSettingsDateFormatSelect = ({ options={[ { label: formatInTimeZone( - Date.now(), + DATE_TIME_SETTINGS_PREVIEW_DATE, usedTimeZone, DateFormat.MONTH_FIRST, ), @@ -54,7 +55,7 @@ export const DateTimeSettingsDateFormatSelect = ({ }, { label: formatInTimeZone( - Date.now(), + DATE_TIME_SETTINGS_PREVIEW_DATE, usedTimeZone, DateFormat.DAY_FIRST, ), @@ -62,7 +63,7 @@ export const DateTimeSettingsDateFormatSelect = ({ }, { label: formatInTimeZone( - Date.now(), + DATE_TIME_SETTINGS_PREVIEW_DATE, usedTimeZone, DateFormat.YEAR_FIRST, ), diff --git a/packages/twenty-front/src/modules/settings/experience/components/DateTimeSettingsTimeFormatSelect.tsx b/packages/twenty-front/src/modules/settings/experience/components/DateTimeSettingsTimeFormatSelect.tsx index 9214496d2b..19990296d6 100644 --- a/packages/twenty-front/src/modules/settings/experience/components/DateTimeSettingsTimeFormatSelect.tsx +++ b/packages/twenty-front/src/modules/settings/experience/components/DateTimeSettingsTimeFormatSelect.tsx @@ -1,5 +1,6 @@ import { formatInTimeZone } from 'date-fns-tz'; +import { DATE_TIME_SETTINGS_PREVIEW_DATE } from '@/localization/constants/DateTimeSettingsPreviewDate'; import { TimeFormat } from '@/localization/constants/TimeFormat'; import { detectTimeFormat } from '@/localization/utils/detection/detectTimeFormat'; import { detectTimeZone } from '@/localization/utils/detection/detectTimeZone'; @@ -25,19 +26,19 @@ export const DateTimeSettingsTimeFormatSelect = ({ const systemTimeFormat = TimeFormat[detectTimeFormat()]; const systemTimeFormatLabel = formatInTimeZone( - Date.now(), + DATE_TIME_SETTINGS_PREVIEW_DATE, usedTimeZone, systemTimeFormat, ); const hour24Label = formatInTimeZone( - Date.now(), + DATE_TIME_SETTINGS_PREVIEW_DATE, usedTimeZone, TimeFormat.HOUR_24, ); const hour12Label = formatInTimeZone( - Date.now(), + DATE_TIME_SETTINGS_PREVIEW_DATE, usedTimeZone, TimeFormat.HOUR_12, ); diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/components/__stories__/InternalDatePicker.stories.tsx b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/__stories__/InternalDatePicker.stories.tsx index e147e235b6..c0d53e4f96 100644 --- a/packages/twenty-front/src/modules/ui/input/components/internal/date/components/__stories__/InternalDatePicker.stories.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/__stories__/InternalDatePicker.stories.tsx @@ -31,7 +31,16 @@ const meta: Meta = { export default meta; type Story = StoryObj; -export const Default: Story = {}; +export const Default: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await canvas.findByRole( + 'button', + { name: 'Select month and year' }, + { timeout: 10000 }, + ); + }, +}; export const WithOpenMonthSelect: Story = { play: async ({ canvasElement }) => { diff --git a/packages/twenty-front/src/modules/ui/layout/tab-list/components/__stories__/Tablist.stories.tsx b/packages/twenty-front/src/modules/ui/layout/tab-list/components/__stories__/Tablist.stories.tsx index 65ec215730..a249001d73 100644 --- a/packages/twenty-front/src/modules/ui/layout/tab-list/components/__stories__/Tablist.stories.tsx +++ b/packages/twenty-front/src/modules/ui/layout/tab-list/components/__stories__/Tablist.stories.tsx @@ -10,10 +10,13 @@ import { IconPhone, IconUser, } from 'twenty-ui-deprecated/display'; -import { ComponentWithRouterDecorator } from 'twenty-ui-deprecated/testing'; +import { + AVATAR_URL_MOCK, + ComponentWithRouterDecorator, +} from 'twenty-ui-deprecated/testing'; const tabs = [ - { id: 'general', title: 'General', logo: 'https://picsum.photos/200' }, + { id: 'general', title: 'General', logo: AVATAR_URL_MOCK }, { id: 'contacts', title: 'Contacts', Icon: IconUser }, { id: 'messages', title: 'Messages', Icon: IconMail }, { id: 'calls', title: 'Calls', Icon: IconPhone }, @@ -23,7 +26,7 @@ const tabs = [ { id: 'time', title: 'Time Tracking', - logo: 'https://picsum.photos/192/192', + logo: AVATAR_URL_MOCK, }, { id: 'activity',