diff --git a/.dockerignore b/.dockerignore index 14f0bba5c33..d06d9902403 100644 --- a/.dockerignore +++ b/.dockerignore @@ -36,3 +36,5 @@ !unit.json !plugin-transpiler/src !plugin-transpiler/*.* +!test-runner-jest.config.js +!test-runner-jest-environment.js diff --git a/.github/workflows/storybook-chromatic.yml b/.github/workflows/storybook-chromatic.yml index ace0ae3e793..02397ca2586 100644 --- a/.github/workflows/storybook-chromatic.yml +++ b/.github/workflows/storybook-chromatic.yml @@ -64,7 +64,6 @@ jobs: SHARD_COUNT: '2' CYPRESS_INSTALL_BINARY: '0' NODE_OPTIONS: --max-old-space-size=6144 - JEST_IMAGE_SNAPSHOT_TRACK_OBSOLETE: '1' # Remove obsolete snapshots OPT_OUT_CAPTURE: 1 outputs: # The below have to be manually listed unfortunately, as GitHub Actions doesn't allow matrix-dependent outputs @@ -132,7 +131,7 @@ jobs: retries=3 while [ $retries -gt 0 ]; do pnpm exec http-server storybook-static --port 6006 --silent & - if pnpm wait-on http://127.0.0.1:6006 --timeout 60; then + if pnpm wait-on http://127.0.0.1:6006 --timeout 15; then break fi retries=$((retries-1)) @@ -146,14 +145,7 @@ jobs: # Update snapshots for PRs on the main repo, verify on forks, which don't have access to PostHog Bot VARIANT: ${{ github.event.pull_request.head.repo.full_name == github.repository && 'update' || 'verify' }} run: | - retries=3 - while [ $retries -gt 0 ]; do - if pnpm test:visual-regression:stories:ci:$VARIANT --browsers ${{ matrix.browser }} --shard ${{ matrix.shard }}/$SHARD_COUNT; then - break - fi - retries=$((retries-1)) - echo "Failed @storybook/test-runner, retrying... ($retries retries left)" - done + pnpm test:visual-regression:stories:ci:$VARIANT --browsers ${{ matrix.browser }} --shard ${{ matrix.shard }}/$SHARD_COUNT - name: Run @playwright/test (legacy, Chromium-only) if: matrix.browser == 'chromium' && matrix.shard == 1 @@ -163,6 +155,13 @@ jobs: run: | pnpm test:visual-regression:legacy:ci:$VARIANT + - name: Archive failure screenshots + if: ${{ failure() }} + uses: actions/upload-artifact@v3 + with: + name: failure-screenshots-${{ matrix.browser }} + path: frontend/__snapshots__/__failures__/ + - name: Count and optimize updated snapshots id: diff # Skip on forks diff --git a/.gitignore b/.gitignore index b3ee2cc9213..95a429f2d24 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ frontend/.cache/ frontend/dist/ frontend/types/ frontend/__snapshots__/__diff_output__/ +frontend/__snapshots__/__failures__/ *Type.ts frontend/pnpm-error.log frontend/tmp diff --git a/.storybook/test-runner.ts b/.storybook/test-runner.ts index fbb572e5382..cf02d028151 100644 --- a/.storybook/test-runner.ts +++ b/.storybook/test-runner.ts @@ -48,36 +48,35 @@ declare module '@storybook/types' { } } -const RETRY_TIMES = 5 +const RETRY_TIMES = 3 const LOADER_SELECTORS = [ '.ant-skeleton', '.Spinner', '.LemonSkeleton', '.LemonTableLoader', + '.Toastify__toast-container', '[aria-busy="true"]', - '[aria-label="Content is loading..."]', '.SessionRecordingPlayer--buffering', '.Lettermark--unknown', ] const customSnapshotsDir = `${process.cwd()}/frontend/__snapshots__` -const TEST_TIMEOUT_MS = 10000 -const BROWSER_DEFAULT_TIMEOUT_MS = 9000 // Reduce the default timeout down from 30s, to pre-empt Jest timeouts -const SCREENSHOT_TIMEOUT_MS = 9000 +const JEST_TIMEOUT_MS = 15000 +const PLAYWRIGHT_TIMEOUT_MS = 10000 // Must be shorter than JEST_TIMEOUT_MS module.exports = { setup() { expect.extend({ toMatchImageSnapshot }) jest.retryTimes(RETRY_TIMES, { logErrorsBeforeRetry: true }) - jest.setTimeout(TEST_TIMEOUT_MS) + jest.setTimeout(JEST_TIMEOUT_MS) }, async postRender(page, context) { const browserContext = page.context() const storyContext = (await getStoryContext(page, context)) as StoryContext const { skip = false, snapshotBrowsers = ['chromium'] } = storyContext.parameters?.testOptions ?? {} - browserContext.setDefaultTimeout(BROWSER_DEFAULT_TIMEOUT_MS) + browserContext.setDefaultTimeout(PLAYWRIGHT_TIMEOUT_MS) if (!skip) { const currentBrowser = browserContext.browser()!.browserType().name() as SupportedBrowserName if (snapshotBrowsers.includes(currentBrowser)) { @@ -202,7 +201,7 @@ async function expectLocatorToMatchStorySnapshot( browser: SupportedBrowserName, options?: LocatorScreenshotOptions ): Promise { - const image = await locator.screenshot({ timeout: SCREENSHOT_TIMEOUT_MS, ...options }) + const image = await locator.screenshot({ ...options }) let customSnapshotIdentifier = context.id if (browser !== 'chromium') { customSnapshotIdentifier += `--${browser}` diff --git a/Dockerfile.playwright b/Dockerfile.playwright index 303b3ad7ab2..7591a53141b 100644 --- a/Dockerfile.playwright +++ b/Dockerfile.playwright @@ -15,6 +15,6 @@ ENV CYPRESS_INSTALL_BINARY=0 RUN pnpm install --frozen-lockfile -COPY playwright.config.ts webpack.config.js babel.config.js tsconfig.json ./ +COPY playwright.config.ts webpack.config.js babel.config.js tsconfig.json test-runner-jest.config.js test-runner-jest-environment.js ./ COPY .storybook/ .storybook/ diff --git a/frontend/__snapshots__/scenes-app-surveys--survey-view.png b/frontend/__snapshots__/scenes-app-surveys--survey-view.png deleted file mode 100644 index 115efcf6e29..00000000000 Binary files a/frontend/__snapshots__/scenes-app-surveys--survey-view.png and /dev/null differ diff --git a/frontend/__snapshots__/scenes-other-login--self-hosted-with-saml.png b/frontend/__snapshots__/scenes-other-login--self-hosted-with-saml.png index 6b63cb0b7db..ee4ee6206d2 100644 Binary files a/frontend/__snapshots__/scenes-other-login--self-hosted-with-saml.png and b/frontend/__snapshots__/scenes-other-login--self-hosted-with-saml.png differ diff --git a/frontend/src/lib/components/ActivationSidebar/activationLogic.ts b/frontend/src/lib/components/ActivationSidebar/activationLogic.ts index 8ed9e776aef..b2579b889df 100644 --- a/frontend/src/lib/components/ActivationSidebar/activationLogic.ts +++ b/frontend/src/lib/components/ActivationSidebar/activationLogic.ts @@ -137,7 +137,7 @@ export const activationLogic = kea([ 0, { loadCustomEvents: async (_, breakpoint) => { - breakpoint(200) + await breakpoint(200) const url = api.eventDefinitions.determineListEndpoint({ event_type: EventDefinitionType.EventCustom, }) diff --git a/frontend/src/lib/components/Cards/InsightCard/InsightCard.scss b/frontend/src/lib/components/Cards/InsightCard/InsightCard.scss index b42ba8047e9..8176646fac6 100644 --- a/frontend/src/lib/components/Cards/InsightCard/InsightCard.scss +++ b/frontend/src/lib/components/Cards/InsightCard/InsightCard.scss @@ -127,6 +127,7 @@ .SeriesDisplay__raw-name { display: inline-flex; + align-items: center; padding: 0.125rem 0.25rem; margin: 0 0.25rem; background: var(--primary-bg-hover); @@ -135,12 +136,12 @@ font-size: 0.6875rem; font-weight: 600; line-height: 1rem; - vertical-align: -0.3em; &.SeriesDisplay__raw-name--action, &.SeriesDisplay__raw-name--event { padding: 0.25rem; &::before { display: inline-block; + flex-shrink: 0; text-align: center; width: 1rem; border-radius: 0.25rem; @@ -169,5 +170,4 @@ margin-right: 0.25rem; color: var(--border-bold); font-size: 1.25rem; - vertical-align: middle; } diff --git a/frontend/src/lib/components/Map/Map.stories.tsx b/frontend/src/lib/components/Map/Map.stories.tsx index 41807351e63..e7e120a8244 100644 --- a/frontend/src/lib/components/Map/Map.stories.tsx +++ b/frontend/src/lib/components/Map/Map.stories.tsx @@ -14,6 +14,11 @@ const meta: Meta = { center: coordinates, className: 'h-60', }, + parameters: { + testOptions: { + skip: true, + }, + }, } type Story = StoryObj diff --git a/frontend/src/lib/components/PropertyFilters/components/TaxonomicPropertyFilter.tsx b/frontend/src/lib/components/PropertyFilters/components/TaxonomicPropertyFilter.tsx index e99bcf464c9..1ee7d36a6d2 100644 --- a/frontend/src/lib/components/PropertyFilters/components/TaxonomicPropertyFilter.tsx +++ b/frontend/src/lib/components/PropertyFilters/components/TaxonomicPropertyFilter.tsx @@ -109,7 +109,7 @@ export function TaxonomicPropertyFilter({ const { ref: wrapperRef, size } = useResizeBreakpoints({ 0: 'tiny', - 400: 'small', + 300: 'small', 550: 'medium', }) diff --git a/frontend/src/lib/components/SceneDashboardChoice/sceneDashboardChoiceModalLogic.test.ts b/frontend/src/lib/components/SceneDashboardChoice/sceneDashboardChoiceModalLogic.test.ts index 7b870921051..88182d3131a 100644 --- a/frontend/src/lib/components/SceneDashboardChoice/sceneDashboardChoiceModalLogic.test.ts +++ b/frontend/src/lib/components/SceneDashboardChoice/sceneDashboardChoiceModalLogic.test.ts @@ -7,7 +7,7 @@ import { userLogic } from 'scenes/userLogic' import { useMocks } from '~/mocks/jest' import { Scene } from 'scenes/sceneTypes' -describe('sceneDashboardChoiceModalLogic ', () => { +describe('sceneDashboardChoiceModalLogic', () => { let logic: ReturnType beforeEach(async () => { diff --git a/frontend/src/scenes/authentication/Login.stories.tsx b/frontend/src/scenes/authentication/Login.stories.tsx index 909643db620..f6ffca27426 100644 --- a/frontend/src/scenes/authentication/Login.stories.tsx +++ b/frontend/src/scenes/authentication/Login.stories.tsx @@ -1,5 +1,5 @@ // Login.stories.tsx -import { Meta } from '@storybook/react' +import { Meta, StoryFn } from '@storybook/react' import { Login } from './Login' import { mswDecorator, useStorybookMocks } from '~/mocks/browser' import { useEffect } from 'react' @@ -27,7 +27,8 @@ const meta: Meta = { ], } export default meta -export const Cloud = (): JSX.Element => { + +export const Cloud: StoryFn = () => { useStorybookMocks({ get: { '/_preflight': { @@ -41,7 +42,8 @@ export const Cloud = (): JSX.Element => { }) return } -export const CloudEU = (): JSX.Element => { + +export const CloudEU: StoryFn = () => { useStorybookMocks({ get: { '/_preflight': { @@ -56,7 +58,8 @@ export const CloudEU = (): JSX.Element => { }) return } -export const CloudWithGoogleLoginEnforcement = (): JSX.Element => { + +export const CloudWithGoogleLoginEnforcement: StoryFn = () => { useStorybookMocks({ get: { '/_preflight': { @@ -78,7 +81,13 @@ export const CloudWithGoogleLoginEnforcement = (): JSX.Element => { }, []) return } -export const SelfHosted = (): JSX.Element => { +CloudWithGoogleLoginEnforcement.parameters = { + testOptions: { + waitForSelector: '[href^="/login/google-oauth2/"]', + }, +} + +export const SelfHosted: StoryFn = () => { useStorybookMocks({ get: { '/_preflight': { @@ -92,7 +101,7 @@ export const SelfHosted = (): JSX.Element => { return } -export const SelfHostedWithSAML = (): JSX.Element => { +export const SelfHostedWithSAML: StoryFn = () => { useStorybookMocks({ get: { '/_preflight': { @@ -105,8 +114,13 @@ export const SelfHostedWithSAML = (): JSX.Element => { }) return } +SelfHostedWithSAML.parameters = { + testOptions: { + waitForSelector: '[href^="/login/saml/"]', + }, +} -export const SSOError = (): JSX.Element => { +export const SSOError: StoryFn = () => { useStorybookMocks({ get: { '/_preflight': preflightJson, @@ -119,7 +133,7 @@ export const SSOError = (): JSX.Element => { return } -export const SecondFactor = (): JSX.Element => { +export const SecondFactor: StoryFn = () => { useEffect(() => { // Change the URL router.actions.push(urls.login2FA()) diff --git a/frontend/src/scenes/billing/BillingGauge.scss b/frontend/src/scenes/billing/BillingGauge.scss index 3a207844284..37ebdc11bbd 100644 --- a/frontend/src/scenes/billing/BillingGauge.scss +++ b/frontend/src/scenes/billing/BillingGauge.scss @@ -1,8 +1,5 @@ -.BillingGauge { -} - .BillingGaugeItem { - transition: all 1000ms cubic-bezier(0.15, 0.15, 0.2, 1); + animation: billing-gauge-item-expand 800ms cubic-bezier(0.15, 0.15, 0.2, 1) forwards; .BillingGaugeItem__info { position: absolute; @@ -28,3 +25,12 @@ } } } + +@keyframes billing-gauge-item-expand { + 0% { + width: 0%; + } + 100% { + width: var(--billing-gauge-item-width); + } +} diff --git a/frontend/src/scenes/billing/BillingGauge.tsx b/frontend/src/scenes/billing/BillingGauge.tsx index 27e41faf153..1d8e5649404 100644 --- a/frontend/src/scenes/billing/BillingGauge.tsx +++ b/frontend/src/scenes/billing/BillingGauge.tsx @@ -1,7 +1,7 @@ import clsx from 'clsx' import { Tooltip } from 'lib/lemon-ui/Tooltip' import { compactNumber } from 'lib/utils' -import { useEffect, useMemo, useState } from 'react' +import { useMemo } from 'react' import './BillingGauge.scss' type BillingGaugeItemProps = { @@ -15,7 +15,10 @@ type BillingGaugeItemProps = { const BillingGaugeItem = ({ width, className, tooltip, top, value }: BillingGaugeItemProps): JSX.Element => { return ( // eslint-disable-next-line react/forbid-dom-props -
+
{ return Math.max(100, ...items.map((item) => item.value)) * 1.3 }, [items]) - useEffect(() => { - // On mount, animate the gauge to full width - setExpanded(true) - }, []) - return (
{items.map((item, i) => ( {item.text}} top={item.top} diff --git a/frontend/src/scenes/billing/billingLogic.ts b/frontend/src/scenes/billing/billingLogic.ts index 920e524ced1..4e694d0d115 100644 --- a/frontend/src/scenes/billing/billingLogic.ts +++ b/frontend/src/scenes/billing/billingLogic.ts @@ -231,7 +231,7 @@ export const billingLogic = kea([ license: !license ? 'Please enter your license key' : undefined, }), submit: async ({ license }, breakpoint) => { - breakpoint(500) + await breakpoint(500) try { await api.update('api/billing-v2/license', { license, diff --git a/frontend/src/scenes/experiments/Experiment.stories.tsx b/frontend/src/scenes/experiments/Experiment.stories.tsx index f4befaccc04..a80c329c6e4 100644 --- a/frontend/src/scenes/experiments/Experiment.stories.tsx +++ b/frontend/src/scenes/experiments/Experiment.stories.tsx @@ -1,5 +1,5 @@ import { useEffect } from 'react' -import { Meta } from '@storybook/react' +import { Meta, StoryFn } from '@storybook/react' import { router } from 'kea-router' import { urls } from 'scenes/urls' import { App } from 'scenes/App' @@ -454,7 +454,7 @@ const MOCK_TREND_EXPERIMENT_RESULTS: TrendsExperimentResults = { }, aggregated_value: 0, label: '$pageview - control', - count: 11.421053, // eslint-disable-line no-loss-of-precision + count: 11.421053, data: [ 2.4210526315789473, 1.4210526315789473, 3.4210526315789473, 0.4210526315789473, 3.4210526315789473, ], @@ -598,7 +598,7 @@ const meta: Meta = { ], } export default meta -export function ExperimentsList(): JSX.Element { +export const ExperimentsList: StoryFn = () => { useAvailableFeatures([AvailableFeature.EXPERIMENTATION]) useEffect(() => { router.actions.push(urls.experiments()) @@ -606,15 +606,20 @@ export function ExperimentsList(): JSX.Element { return } -export function CompleteFunnelExperiment(): JSX.Element { +export const CompleteFunnelExperiment: StoryFn = () => { useAvailableFeatures([AvailableFeature.EXPERIMENTATION]) useEffect(() => { router.actions.push(urls.experiment(MOCK_FUNNEL_EXPERIMENT.id)) }, []) return } +CompleteFunnelExperiment.parameters = { + testOptions: { + waitForSelector: '.card-secondary', + }, +} -export function RunningTrendExperiment(): JSX.Element { +export const RunningTrendExperiment: StoryFn = () => { useAvailableFeatures([AvailableFeature.EXPERIMENTATION]) useEffect(() => { router.actions.push(urls.experiment(MOCK_TREND_EXPERIMENT.id)) @@ -622,22 +627,27 @@ export function RunningTrendExperiment(): JSX.Element { return } +RunningTrendExperiment.parameters = { + testOptions: { + waitForSelector: '.card-secondary', + }, +} -export function ExperimentsListPayGate(): JSX.Element { +export const ExperimentsListPayGate: StoryFn = () => { useEffect(() => { router.actions.push(urls.experiments()) }, []) return } -export function ViewExperimentPayGate(): JSX.Element { +export const ViewExperimentPayGate: StoryFn = () => { useEffect(() => { router.actions.push(urls.experiment(MOCK_FUNNEL_EXPERIMENT.id)) }, []) return } -export function ExperimentNotFound(): JSX.Element { +export const ExperimentNotFound: StoryFn = () => { useAvailableFeatures([AvailableFeature.EXPERIMENTATION]) useEffect(() => { router.actions.push(urls.experiment('1200000')) diff --git a/frontend/src/scenes/feature-flags/RecentFeatureFlagInsightsCard.tsx b/frontend/src/scenes/feature-flags/RecentFeatureFlagInsightsCard.tsx index 5145689f520..5fdf19a9a4c 100644 --- a/frontend/src/scenes/feature-flags/RecentFeatureFlagInsightsCard.tsx +++ b/frontend/src/scenes/feature-flags/RecentFeatureFlagInsightsCard.tsx @@ -6,11 +6,11 @@ import { InsightModel } from '~/types' import { featureFlagLogic } from './featureFlagLogic' export function RecentFeatureFlagInsights(): JSX.Element { - const { recentInsights, recentInsightsLoading, featureFlag } = useValues(featureFlagLogic) + const { relatedInsights, relatedInsightsLoading, featureFlag } = useValues(featureFlagLogic) return ( } /> ) diff --git a/frontend/src/scenes/feature-flags/featureFlagLogic.ts b/frontend/src/scenes/feature-flags/featureFlagLogic.ts index a9aee748e64..19544d46770 100644 --- a/frontend/src/scenes/feature-flags/featureFlagLogic.ts +++ b/frontend/src/scenes/feature-flags/featureFlagLogic.ts @@ -524,10 +524,10 @@ export const featureFlagLogic = kea([ } }, }, - recentInsights: [ + relatedInsights: [ [] as InsightModel[], { - loadRecentInsights: async () => { + loadRelatedInsights: async () => { if (props.id && props.id !== 'new' && values.featureFlag.key) { const response = await api.get( `api/projects/${values.currentTeamId}/insights/?feature_flag=${values.featureFlag.key}&order=-created_at` @@ -661,7 +661,7 @@ export const featureFlagLogic = kea([ } }, loadFeatureFlagSuccess: async () => { - actions.loadRecentInsights() + actions.loadRelatedInsights() actions.loadAllInsightsForFlag() }, loadInsightAtIndex: async ({ index, filters }) => { @@ -1010,7 +1010,7 @@ export const featureFlagLogic = kea([ if (foundFlag) { const formatPayloads = variantKeyToIndexFeatureFlagPayloads(foundFlag) actions.setFeatureFlag(formatPayloads) - actions.loadRecentInsights() + actions.loadRelatedInsights() actions.loadAllInsightsForFlag() } else if (props.id !== 'new') { actions.loadFeatureFlag() diff --git a/frontend/src/scenes/notebooks/Notebook/Notebook.stories.tsx b/frontend/src/scenes/notebooks/Notebook/Notebook.stories.tsx index 44242519298..6e00e37d265 100644 --- a/frontend/src/scenes/notebooks/Notebook/Notebook.stories.tsx +++ b/frontend/src/scenes/notebooks/Notebook/Notebook.stories.tsx @@ -1,4 +1,4 @@ -import { Meta } from '@storybook/react' +import { Meta, StoryFn } from '@storybook/react' import { useEffect } from 'react' import { mswDecorator } from '~/mocks/browser' import { router } from 'kea-router' @@ -347,63 +347,68 @@ const meta: Meta = { ], } export default meta -export function NotebooksList(): JSX.Element { +export const NotebooksList: StoryFn = () => { useEffect(() => { router.actions.push(urls.notebooks()) }, []) return } -export function Headings(): JSX.Element { +export const Headings: StoryFn = () => { useEffect(() => { router.actions.push(urls.notebook('headings')) }, []) return } -export function TextFormats(): JSX.Element { +export const TextFormats: StoryFn = () => { useEffect(() => { router.actions.push(urls.notebook('text-formats')) }, []) return } -export function NumberedList(): JSX.Element { +export const NumberedList: StoryFn = () => { useEffect(() => { router.actions.push(urls.notebook('numbered-list')) }, []) return } -export function BulletList(): JSX.Element { +export const BulletList: StoryFn = () => { useEffect(() => { router.actions.push(urls.notebook('bullet-list')) }, []) return } -export function RecordingsPlaylist(): JSX.Element { +export const RecordingsPlaylist: StoryFn = () => { useEffect(() => { router.actions.push(urls.notebook('recordings-playlist')) }, []) return } +RecordingsPlaylist.parameters = { + testOptions: { + waitForSelector: '.NotebookNode__content', // All stories with widget-style nodes needs this + }, +} -export function TextOnlyNotebook(): JSX.Element { +export const TextOnlyNotebook: StoryFn = () => { useEffect(() => { router.actions.push(urls.notebook('12345')) }, []) return } -export function EmptyNotebook(): JSX.Element { +export const EmptyNotebook: StoryFn = () => { useEffect(() => { router.actions.push(urls.notebook('empty')) }, []) return } -export function NotebookNotFound(): JSX.Element { +export const NotebookNotFound: StoryFn = () => { useEffect(() => { router.actions.push(urls.notebook('abcde')) }, []) diff --git a/frontend/src/scenes/notebooks/Notebook/notebookLogic.ts b/frontend/src/scenes/notebooks/Notebook/notebookLogic.ts index b1a7799d5d1..e87521c26ad 100644 --- a/frontend/src/scenes/notebooks/Notebook/notebookLogic.ts +++ b/frontend/src/scenes/notebooks/Notebook/notebookLogic.ts @@ -213,12 +213,18 @@ export const notebookLogic = kea([ } else if (props.shortId.startsWith('template-')) { response = values.notebookTemplates.find((template) => template.short_id === props.shortId) || null + if (!response) { + return null + } } else { - response = await api.notebooks.get(props.shortId) - } - - if (!response) { - throw new Error('Notebook not found') + try { + response = await api.notebooks.get(props.shortId) + } catch (e: any) { + if (e.status === 404) { + return null + } + throw e + } } const notebook = migrate(response) diff --git a/frontend/src/scenes/notebooks/NotebookSelectButton/notebookSelectButtonLogic.ts b/frontend/src/scenes/notebooks/NotebookSelectButton/notebookSelectButtonLogic.ts index 9cb172c18cc..ed446007c03 100644 --- a/frontend/src/scenes/notebooks/NotebookSelectButton/notebookSelectButtonLogic.ts +++ b/frontend/src/scenes/notebooks/NotebookSelectButton/notebookSelectButtonLogic.ts @@ -52,7 +52,7 @@ export const notebookSelectButtonLogic = kea([ [] as NotebookListItemType[], { loadAllNotebooks: async (_, breakpoint) => { - breakpoint(100) + await breakpoint(100) const response = await api.notebooks.list(undefined, undefined, values.searchQuery ?? undefined) // TODO for simplicity we'll assume the results will fit into one page return response.results @@ -63,7 +63,7 @@ export const notebookSelectButtonLogic = kea([ [] as NotebookListItemType[], { loadNotebooksContainingResource: async (_, breakpoint) => { - breakpoint(100) + await breakpoint(100) if (!props.resource) { return [] } diff --git a/frontend/src/scenes/project-homepage/ProjectHomepage.stories.tsx b/frontend/src/scenes/project-homepage/ProjectHomepage.stories.tsx index ee72de13b7a..fb2a439dbee 100644 --- a/frontend/src/scenes/project-homepage/ProjectHomepage.stories.tsx +++ b/frontend/src/scenes/project-homepage/ProjectHomepage.stories.tsx @@ -4,6 +4,7 @@ import { mswDecorator } from '~/mocks/browser' import { App } from 'scenes/App' import { router } from 'kea-router' import { urls } from 'scenes/urls' +import { EMPTY_PAGINATED_RESPONSE } from '~/mocks/handlers' const meta: Meta = { title: 'Scenes-App/Project Homepage', @@ -13,6 +14,8 @@ const meta: Meta = { '/api/projects/:team_id/dashboards/': require('../dashboard/__mocks__/dashboards.json'), '/api/projects/:team_id/dashboards/1/': require('../dashboard/__mocks__/dashboard1.json'), '/api/projects/:team_id/dashboards/1/collaborators/': [], + '/api/projects/:team_id/session_recordings/': EMPTY_PAGINATED_RESPONSE, + '/api/projects/:team_id/insights/my_last_viewed/': [], }, }), ], diff --git a/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.ts b/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.ts index 5ef3588fcf7..d9028d9034c 100644 --- a/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.ts +++ b/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.ts @@ -311,7 +311,7 @@ export const sessionRecordingDataLogic = kea([ if (!values.sessionPlayerMetaData) { return null } - breakpoint(100) + await breakpoint(100) await api.recordings.persist(props.sessionRecordingId) return { diff --git a/frontend/src/scenes/surveys/Surveys.stories.tsx b/frontend/src/scenes/surveys/Surveys.stories.tsx index f7217ccd16d..6caf5cba7f5 100644 --- a/frontend/src/scenes/surveys/Surveys.stories.tsx +++ b/frontend/src/scenes/surveys/Surveys.stories.tsx @@ -3,8 +3,15 @@ import { App } from 'scenes/App' import { urls } from 'scenes/urls' import { mswDecorator } from '~/mocks/browser' import { toPaginatedResponse } from '~/mocks/handlers' -import { PropertyFilterType, PropertyOperator, Survey, SurveyQuestionType, SurveyType } from '~/types' -import { Meta } from '@storybook/react' +import { + FeatureFlagBasicType, + PropertyFilterType, + PropertyOperator, + Survey, + SurveyQuestionType, + SurveyType, +} from '~/types' +import { Meta, StoryFn } from '@storybook/react' import { router } from 'kea-router' import { SurveyEditSection, surveyLogic } from './surveyLogic' @@ -103,26 +110,6 @@ const MOCK_SURVEY_WITH_RELEASE_CONS: Survey = { archived: false, } -// const MOCK_SURVEY_DISMISSED = { -// "clickhouse": "SELECT count() AS `survey dismissed` FROM events WHERE and(equals(events.team_id, 1), equals(events.event, %(hogql_val_0)s), equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, %(hogql_val_1)s), ''), 'null'), '^\"|\"$', ''), %(hogql_val_2)s)) LIMIT 100 SETTINGS readonly=2, max_execution_time=60", -// "columns": [ -// "survey dismissed" -// ], -// "hogql": "SELECT count() AS `survey dismissed` FROM events WHERE and(equals(event, 'survey dismissed'), equals(properties.$survey_id, '0188e637-3b72-0000-f407-07a338652af9')) LIMIT 100", -// "query": "select count() as 'survey dismissed' from events where event == 'survey dismissed' and properties.$survey_id == '0188e637-3b72-0000-f407-07a338652af9'", -// "results": [ -// [ -// 0 -// ] -// ], -// "types": [ -// [ -// "survey dismissed", -// "UInt64" -// ] -// ] -// } - const MOCK_SURVEY_SHOWN = { clickhouse: "SELECT count() AS `survey shown` FROM events WHERE and(equals(events.team_id, 1), equals(events.event, %(hogql_val_0)s), ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, %(hogql_val_1)s), ''), 'null'), '^\"|\"$', ''), %(hogql_val_2)s), 0)) LIMIT 100 SETTINGS readonly=2, max_execution_time=60", @@ -170,34 +157,42 @@ const meta: Meta = { '/api/projects/:team_id/surveys/0187c279-bcae-0000-34f5-4f121921f005/': MOCK_BASIC_SURVEY, '/api/projects/:team_id/surveys/0187c279-bcae-0000-34f5-4f121921f006/': MOCK_SURVEY_WITH_RELEASE_CONS, '/api/projects/:team_id/surveys/responses_count/': MOCK_RESPONSES_COUNT, + [`/api/projects/:team_id/feature_flags/${ + (MOCK_SURVEY_WITH_RELEASE_CONS.linked_flag as FeatureFlagBasicType).id + }`]: toPaginatedResponse([MOCK_SURVEY_WITH_RELEASE_CONS.linked_flag]), + [`/api/projects/:team_id/feature_flags/${ + (MOCK_SURVEY_WITH_RELEASE_CONS.targeting_flag as FeatureFlagBasicType).id + }`]: toPaginatedResponse([MOCK_SURVEY_WITH_RELEASE_CONS.targeting_flag]), }, post: { - '/api/projects/:team_id/query/': (req) => { - if ((req.body as any).kind == 'EventsQuery') { - return MOCK_SURVEY_RESULTS + '/api/projects/:team_id/query/': async (req, res, ctx) => { + const body = await req.json() + if (body.kind == 'EventsQuery') { + return res(ctx.json(MOCK_SURVEY_RESULTS)) + } else { + return res(ctx.json(MOCK_SURVEY_SHOWN)) } - return MOCK_SURVEY_SHOWN }, }, }), ], } export default meta -export function SurveysList(): JSX.Element { +export const SurveysList: StoryFn = () => { useEffect(() => { router.actions.push(urls.surveys()) }, []) return } -export function NewSurvey(): JSX.Element { +export const NewSurvey: StoryFn = () => { useEffect(() => { router.actions.push(urls.survey('new')) }, []) return } -export function NewSurveyCustomisationSection(): JSX.Element { +export const NewSurveyCustomisationSection: StoryFn = () => { useEffect(() => { router.actions.push(urls.survey('new')) surveyLogic({ id: 'new' }).mount() @@ -206,7 +201,7 @@ export function NewSurveyCustomisationSection(): JSX.Element { return } -export function NewSurveyPresentationSection(): JSX.Element { +export const NewSurveyPresentationSection: StoryFn = () => { useEffect(() => { router.actions.push(urls.survey('new')) surveyLogic({ id: 'new' }).mount() @@ -215,7 +210,7 @@ export function NewSurveyPresentationSection(): JSX.Element { return } -export function NewSurveyTargetingSection(): JSX.Element { +export const NewSurveyTargetingSection: StoryFn = () => { useEffect(() => { router.actions.push(urls.survey('new')) surveyLogic({ id: 'new' }).mount() @@ -233,7 +228,7 @@ export function NewSurveyTargetingSection(): JSX.Element { return } -export function NewSurveyAppearanceSection(): JSX.Element { +export const NewSurveyAppearanceSection: StoryFn = () => { useEffect(() => { router.actions.push(urls.survey('new')) surveyLogic({ id: 'new' }).mount() @@ -242,21 +237,26 @@ export function NewSurveyAppearanceSection(): JSX.Element { return } -export function SurveyView(): JSX.Element { +export const SurveyView: StoryFn = () => { useEffect(() => { router.actions.push(urls.survey(MOCK_SURVEY_WITH_RELEASE_CONS.id)) }, []) return } +SurveyView.parameters = { + testOptions: { + skip: true, // FIXME: Fix the mocked data so that survey results can actually load + }, +} -export function SurveyTemplates(): JSX.Element { +export const SurveyTemplates: StoryFn = () => { useEffect(() => { router.actions.push(urls.surveyTemplates()) }, []) return } -export function SurveyNotFound(): JSX.Element { +export const SurveyNotFound: StoryFn = () => { useEffect(() => { router.actions.push(urls.survey('1234566789')) }, []) diff --git a/frontend/src/scenes/surveys/surveyLogic.tsx b/frontend/src/scenes/surveys/surveyLogic.tsx index 4bcc438f669..72a43c98534 100644 --- a/frontend/src/scenes/surveys/surveyLogic.tsx +++ b/frontend/src/scenes/surveys/surveyLogic.tsx @@ -134,7 +134,10 @@ export const surveyLogic = kea([ actions.reportSurveyViewed(survey) return survey } catch (error: any) { - actions.setSurveyMissing() + if (error.status === 404) { + actions.setSurveyMissing() + return { ...NEW_SURVEY } + } throw error } } diff --git a/package.json b/package.json index b3edc80b5b2..ae692d85114 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "test:visual-regression:legacy:docker": "STORYBOOK_URL=http://host.docker.internal:6006 playwright test -u", "test:visual-regression:legacy:ci:update": "playwright test -u", "test:visual-regression:legacy:ci:verify": "playwright test", - "test:visual-regression:stories": "docker compose -f docker-compose.playwright.yml run --rm -it --build playwright pnpm test:visual-regression:stories:docker", + "test:visual-regression:stories": "rm -rf frontend/__snapshots__/__failures__/ && docker compose -f docker-compose.playwright.yml run --rm -it --build playwright pnpm test:visual-regression:stories:docker", "test:visual-regression:stories:docker": "NODE_OPTIONS=--max-old-space-size=6144 test-storybook -u --no-index-json --browsers chromium webkit --url http://host.docker.internal:6006", "test:visual-regression:stories:ci:update": "test-storybook -u --no-index-json --maxWorkers=2", "test:visual-regression:stories:ci:verify": "test-storybook --ci --no-index-json --maxWorkers=2", @@ -303,8 +303,7 @@ "!(posthog/hogql/grammar/*)*.{py,pyi}": [ "ruff format", "ruff check" - ], - "!(HogQL*)*.{c,cpp,h,hpp}": "clang-format -i" + ] }, "browserslist": { "development": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e98edd3e62d..390209826d3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -602,7 +602,7 @@ devDependencies: version: 7.0.1(monaco-editor@0.39.0)(webpack@5.88.2) msw: specifier: ^0.49.0 - version: 0.49.3(typescript@4.9.5) + version: 0.49.0(typescript@4.9.5) path-browserify: specifier: ^1.0.1 version: 1.0.1 @@ -3254,16 +3254,16 @@ packages: set-cookie-parser: 2.5.1 dev: true - /@mswjs/interceptors@0.17.6: - resolution: {integrity: sha512-201pBIWehTURb6q8Gheu4Zhvd3Ox1U4BJq5KiOQsYzkWyfiOG4pwcz5hPZIEryztgrf8/sdwABpvY757xMmfrQ==} + /@mswjs/interceptors@0.17.10: + resolution: {integrity: sha512-N8x7eSLGcmUFNWZRxT1vsHvypzIRgQYdG0rJey/rZCy6zT/30qDt8Joj7FxzGNLSwXbeZqJOMqDurp7ra4hgbw==} engines: {node: '>=14'} dependencies: '@open-draft/until': 1.0.3 '@types/debug': 4.1.7 '@xmldom/xmldom': 0.8.6 debug: 4.3.4(supports-color@8.1.1) - headers-polyfill: 3.1.2 - outvariant: 1.3.0 + headers-polyfill: 3.2.5 + outvariant: 1.4.0 strict-event-emitter: 0.2.8 web-encoding: 1.1.5 transitivePeerDependencies: @@ -5071,7 +5071,7 @@ packages: flat-cache: 3.0.4 micromatch: 4.0.5 react-docgen-typescript: 2.2.2(typescript@4.9.5) - tslib: 2.4.1 + tslib: 2.6.2 typescript: 4.9.5 webpack: 5.88.2(@swc/core@1.3.93)(esbuild@0.14.54)(webpack-cli@5.1.4) transitivePeerDependencies: @@ -5909,7 +5909,7 @@ packages: resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} dependencies: '@types/connect': 3.4.38 - '@types/node': 18.11.9 + '@types/node': 18.18.4 dev: true /@types/chart.js@2.9.37: @@ -5937,7 +5937,7 @@ packages: /@types/connect@3.4.38: resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} dependencies: - '@types/node': 18.11.9 + '@types/node': 18.18.4 dev: true /@types/cookie@0.4.1: @@ -6215,7 +6215,7 @@ packages: /@types/express-serve-static-core@4.17.41: resolution: {integrity: sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==} dependencies: - '@types/node': 18.11.9 + '@types/node': 18.18.4 '@types/qs': 6.9.10 '@types/range-parser': 1.2.7 '@types/send': 0.17.4 @@ -6545,7 +6545,7 @@ packages: resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} dependencies: '@types/mime': 1.3.5 - '@types/node': 18.11.9 + '@types/node': 18.18.4 dev: true /@types/serve-static@1.15.4: @@ -6561,7 +6561,7 @@ packages: dependencies: '@types/http-errors': 2.0.4 '@types/mime': 3.0.4 - '@types/node': 18.11.9 + '@types/node': 18.18.4 dev: true /@types/set-cookie-parser@2.4.2: @@ -11389,8 +11389,8 @@ packages: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} dev: true - /graphql@16.6.0: - resolution: {integrity: sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw==} + /graphql@16.8.1: + resolution: {integrity: sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} dev: true @@ -11486,8 +11486,8 @@ packages: hasBin: true dev: true - /headers-polyfill@3.1.2: - resolution: {integrity: sha512-tWCK4biJ6hcLqTviLXVR9DTRfYGQMXEIUj3gwJ2rZ5wO/at3XtkI4g8mCvFdUF9l1KMBNCfmNAdnahm1cgavQA==} + /headers-polyfill@3.2.5: + resolution: {integrity: sha512-tUCGvt191vNSQgttSyJoibR+VO+I6+iCHIUdhzEMJKE+EAL8BwCN7fUOZlY4ofOelNHsK+gEjxB/B+9N3EWtdA==} dev: true /helpertypes@0.0.19: @@ -12110,8 +12110,8 @@ packages: engines: {node: '>= 0.4'} dev: true - /is-node-process@1.0.1: - resolution: {integrity: sha512-5IcdXuf++TTNt3oGl9EBdkvndXA8gmc4bz/Y+mdEpWh3Mcn/+kOw6hI7LD5CocqJWMzeb0I0ClndRVNdEPuJXQ==} + /is-node-process@1.2.0: + resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} dev: true /is-number-object@1.0.7: @@ -14380,8 +14380,8 @@ packages: /ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - /msw@0.49.3(typescript@4.9.5): - resolution: {integrity: sha512-kRCbDNbNnRq5LC1H/NUceZlrPAvSrMH6Or0mirIuH69NY84xwDruPn/hkXTovIK1KwDwbk+ZdoSyJlpiekLxEA==} + /msw@0.49.0(typescript@4.9.5): + resolution: {integrity: sha512-xX5RMSMjN58j8G/V26Uaf5LP464VltuWyd66TQimLueVYfG47RKydGsd4JW165Jb/gjoaQxh5Tdvv31wdZAOlA==} engines: {node: '>=14'} hasBin: true requiresBuild: true @@ -14392,22 +14392,22 @@ packages: optional: true dependencies: '@mswjs/cookies': 0.2.2 - '@mswjs/interceptors': 0.17.6 + '@mswjs/interceptors': 0.17.10 '@open-draft/until': 1.0.3 '@types/cookie': 0.4.1 '@types/js-levenshtein': 1.1.1 chalk: 4.1.1 chokidar: 3.5.3 cookie: 0.4.2 - graphql: 16.6.0 - headers-polyfill: 3.1.2 + graphql: 16.8.1 + headers-polyfill: 3.2.5 inquirer: 8.2.5 - is-node-process: 1.0.1 + is-node-process: 1.2.0 js-levenshtein: 1.1.6 node-fetch: 2.6.7 - outvariant: 1.3.0 + outvariant: 1.4.0 path-to-regexp: 6.2.1 - strict-event-emitter: 0.4.6 + strict-event-emitter: 0.2.8 type-fest: 2.19.0 typescript: 4.9.5 yargs: 17.6.2 @@ -14801,8 +14801,8 @@ packages: resolution: {integrity: sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==} dev: true - /outvariant@1.3.0: - resolution: {integrity: sha512-yeWM9k6UPfG/nzxdaPlJkB2p08hCg4xP6Lx99F+vP8YF7xyZVfTmJjrrNalkmzudD4WFvNLVudQikqUmF8zhVQ==} + /outvariant@1.4.0: + resolution: {integrity: sha512-AlWY719RF02ujitly7Kk/0QlV+pXGFDHrHf9O2OKqyqgBieaPOIeuSkL8sRK6j2WK+/ZAURq2kZsY0d8JapUiw==} dev: true /p-limit@2.3.0: @@ -16694,7 +16694,7 @@ packages: react: 18.2.0 react-remove-scroll-bar: 2.3.4(@types/react@17.0.52)(react@18.2.0) react-style-singleton: 2.2.1(@types/react@17.0.52)(react@18.2.0) - tslib: 2.4.1 + tslib: 2.6.2 use-callback-ref: 1.3.0(@types/react@17.0.52)(react@18.2.0) use-sidecar: 1.1.2(@types/react@17.0.52)(react@18.2.0) dev: true @@ -17758,10 +17758,6 @@ packages: events: 3.3.0 dev: true - /strict-event-emitter@0.4.6: - resolution: {integrity: sha512-12KWeb+wixJohmnwNFerbyiBrAlq5qJLwIt38etRtKtmmHyDSoGlIqFE9wx+4IwG0aDjI7GV8tc8ZccjWZZtTg==} - dev: true - /string-argv@0.3.1: resolution: {integrity: sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==} engines: {node: '>=0.6.19'} diff --git a/test-runner-jest-environment.js b/test-runner-jest-environment.js new file mode 100644 index 00000000000..19351c30f90 --- /dev/null +++ b/test-runner-jest-environment.js @@ -0,0 +1,27 @@ +const { setupPage } = require('@storybook/test-runner') +const PlaywrightEnvironment = require('jest-playwright-preset/lib/PlaywrightEnvironment').default + +class CustomEnvironment extends PlaywrightEnvironment { + async setup() { + await super.setup() + await setupPage(this.global.page, this.global.context) + } + + async teardown() { + await super.teardown() + } + + async handleTestEvent(event) { + if (event.name === 'test_done' && event.test.errors.length > 0) { + // Take screenshots on test failures - these become Actions artifacts + const parentName = event.test.parent.parent.name.replace(/\W/g, '-').toLowerCase() + const specName = event.test.parent.name.replace(/\W/g, '-').toLowerCase() + await this.global.page.screenshot({ + path: `frontend/__snapshots__/__failures__/${parentName}--${specName}.png`, + }) + } + await super.handleTestEvent(event) + } +} + +module.exports = CustomEnvironment diff --git a/test-runner-jest.config.js b/test-runner-jest.config.js index e3bfc94297b..fd7b7a22e07 100644 --- a/test-runner-jest.config.js +++ b/test-runner-jest.config.js @@ -1,5 +1,8 @@ const { getJestConfig } = require('@storybook/test-runner') +/** + * @type {import('@jest/types').Config.InitialOptions} + */ module.exports = { // The default configuration comes from @storybook/test-runner ...getJestConfig(), @@ -7,7 +10,7 @@ module.exports = { * @see https://jestjs.io/docs/configuration */ forceExit: true, - // Remove obsolete snapshots in CI - // See https://github.com/americanexpress/jest-image-snapshot#removing-outdated-snapshots + // For jest-image-snapshot, see https://github.com/americanexpress/jest-image-snapshot#removing-outdated-snapshots reporters: ['default', 'jest-image-snapshot/src/outdated-snapshot-reporter.js'], + testEnvironment: './test-runner-jest-environment.js', }