0
0
mirror of https://github.com/PostHog/posthog.git synced 2024-11-21 21:49:51 +01:00

test: Regular checkup of visual regression tests (#18469)

* test: Regular checkup of visual regression tests

* Fix billing gauge animation

Animations done in JS can't be stopped automatically by the Storybook test runner. CSS animations can, easily, and they are anyway the cleaner and more performant way of achieving the same here.

* Rename misleading feature flag `recentInsights` to `relatedInsights`

* Mock homepage endpoints to avoid error toasts

* Wait for the recordings list in the notebook node story

* Fix `featureFlagLogic`

* Wait for `.NotebookNode__content`

* Try to optimize

* Screenshot failures and upload as artifacts

* Fix remaining failures

* Increase timeouts

* Fix rendering of Survey stories

* Remove `clang-format`

* Update UI snapshots for `chromium` (2)

* Update UI snapshots for `chromium` (2)

* Update UI snapshots for `chromium` (2)

* Fix alignment of series name in insights details

* Try to fix experiment story flakiness

* Include toasts in loaders

* Fix superfluous toast

* Fix un-awaited breakpoints

* Update UI snapshots for `chromium` (1)

* Update UI snapshots for `chromium` (1)

* Update UI snapshots for `chromium` (1)

* Make login snapshots slightly stabler

* Update UI snapshots for `chromium` (2)

* Update UI snapshots for `chromium` (2)

* Skip incorrect Surveys story

* Update UI snapshots for `chromium` (2)

* Revert msw upgrade

---------

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Michael Matloka 2023-11-13 15:32:10 +01:00 committed by GitHub
parent 8ee0ac490e
commit fded6fdf62
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 227 additions and 152 deletions

View File

@ -36,3 +36,5 @@
!unit.json
!plugin-transpiler/src
!plugin-transpiler/*.*
!test-runner-jest.config.js
!test-runner-jest-environment.js

View File

@ -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

1
.gitignore vendored
View File

@ -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

View File

@ -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<void> {
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}`

View File

@ -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/

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 107 KiB

View File

@ -137,7 +137,7 @@ export const activationLogic = kea<activationLogicType>([
0,
{
loadCustomEvents: async (_, breakpoint) => {
breakpoint(200)
await breakpoint(200)
const url = api.eventDefinitions.determineListEndpoint({
event_type: EventDefinitionType.EventCustom,
})

View File

@ -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;
}

View File

@ -14,6 +14,11 @@ const meta: Meta<typeof Map> = {
center: coordinates,
className: 'h-60',
},
parameters: {
testOptions: {
skip: true,
},
},
}
type Story = StoryObj<typeof Map>

View File

@ -109,7 +109,7 @@ export function TaxonomicPropertyFilter({
const { ref: wrapperRef, size } = useResizeBreakpoints({
0: 'tiny',
400: 'small',
300: 'small',
550: 'medium',
})

View File

@ -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<typeof sceneDashboardChoiceModalLogic.build>
beforeEach(async () => {

View File

@ -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 <Login />
}
export const CloudEU = (): JSX.Element => {
export const CloudEU: StoryFn = () => {
useStorybookMocks({
get: {
'/_preflight': {
@ -56,7 +58,8 @@ export const CloudEU = (): JSX.Element => {
})
return <Login />
}
export const CloudWithGoogleLoginEnforcement = (): JSX.Element => {
export const CloudWithGoogleLoginEnforcement: StoryFn = () => {
useStorybookMocks({
get: {
'/_preflight': {
@ -78,7 +81,13 @@ export const CloudWithGoogleLoginEnforcement = (): JSX.Element => {
}, [])
return <Login />
}
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 <Login />
}
export const SelfHostedWithSAML = (): JSX.Element => {
export const SelfHostedWithSAML: StoryFn = () => {
useStorybookMocks({
get: {
'/_preflight': {
@ -105,8 +114,13 @@ export const SelfHostedWithSAML = (): JSX.Element => {
})
return <Login />
}
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 <Login />
}
export const SecondFactor = (): JSX.Element => {
export const SecondFactor: StoryFn = () => {
useEffect(() => {
// Change the URL
router.actions.push(urls.login2FA())

View File

@ -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);
}
}

View File

@ -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
<div className={`BillingGaugeItem absolute top-0 left-0 bottom-0 h-2 ${className}`} style={{ width: width }}>
<div
className={`BillingGaugeItem absolute top-0 left-0 bottom-0 h-2 ${className}`}
style={{ '--billing-gauge-item-width': width } as React.CSSProperties}
>
<div className="absolute right-0 w-px h-full bg-bg-light" />
<Tooltip title={value.toLocaleString()} placement={'right'}>
<div
@ -41,22 +44,16 @@ export type BillingGaugeProps = {
}
export function BillingGauge({ items }: BillingGaugeProps): JSX.Element {
const [expanded, setExpanded] = useState(false)
const maxScale = useMemo(() => {
return Math.max(100, ...items.map((item) => item.value)) * 1.3
}, [items])
useEffect(() => {
// On mount, animate the gauge to full width
setExpanded(true)
}, [])
return (
<div className="relative h-2 bg-border-light my-16">
{items.map((item, i) => (
<BillingGaugeItem
key={i}
width={expanded ? `${(item.value / maxScale) * 100}%` : '0%'}
width={`${(item.value / maxScale) * 100}%`}
className={`bg-${item.color}`}
tooltip={<b>{item.text}</b>}
top={item.top}

View File

@ -231,7 +231,7 @@ export const billingLogic = kea<billingLogicType>([
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,

View File

@ -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 <App />
}
export function CompleteFunnelExperiment(): JSX.Element {
export const CompleteFunnelExperiment: StoryFn = () => {
useAvailableFeatures([AvailableFeature.EXPERIMENTATION])
useEffect(() => {
router.actions.push(urls.experiment(MOCK_FUNNEL_EXPERIMENT.id))
}, [])
return <App />
}
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 <App />
}
RunningTrendExperiment.parameters = {
testOptions: {
waitForSelector: '.card-secondary',
},
}
export function ExperimentsListPayGate(): JSX.Element {
export const ExperimentsListPayGate: StoryFn = () => {
useEffect(() => {
router.actions.push(urls.experiments())
}, [])
return <App />
}
export function ViewExperimentPayGate(): JSX.Element {
export const ViewExperimentPayGate: StoryFn = () => {
useEffect(() => {
router.actions.push(urls.experiment(MOCK_FUNNEL_EXPERIMENT.id))
}, [])
return <App />
}
export function ExperimentNotFound(): JSX.Element {
export const ExperimentNotFound: StoryFn = () => {
useAvailableFeatures([AvailableFeature.EXPERIMENTATION])
useEffect(() => {
router.actions.push(urls.experiment('1200000'))

View File

@ -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 (
<CompactList
title="Insights that use this feature flag"
loading={recentInsightsLoading}
loading={relatedInsightsLoading}
emptyMessage={{
title: 'You have no insights that use this feature flag',
description: "Explore this feature flag's insights by creating one below.",
@ -21,7 +21,7 @@ export function RecentFeatureFlagInsights(): JSX.Element {
breakdown: `$feature/${featureFlag.key}`,
}),
}}
items={recentInsights.slice(0, 5)}
items={relatedInsights.slice(0, 5)}
renderRow={(insight: InsightModel, index) => <InsightRow key={index} insight={insight} />}
/>
)

View File

@ -524,10 +524,10 @@ export const featureFlagLogic = kea<featureFlagLogicType>([
}
},
},
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<featureFlagLogicType>([
}
},
loadFeatureFlagSuccess: async () => {
actions.loadRecentInsights()
actions.loadRelatedInsights()
actions.loadAllInsightsForFlag()
},
loadInsightAtIndex: async ({ index, filters }) => {
@ -1010,7 +1010,7 @@ export const featureFlagLogic = kea<featureFlagLogicType>([
if (foundFlag) {
const formatPayloads = variantKeyToIndexFeatureFlagPayloads(foundFlag)
actions.setFeatureFlag(formatPayloads)
actions.loadRecentInsights()
actions.loadRelatedInsights()
actions.loadAllInsightsForFlag()
} else if (props.id !== 'new') {
actions.loadFeatureFlag()

View File

@ -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 <App />
}
export function Headings(): JSX.Element {
export const Headings: StoryFn = () => {
useEffect(() => {
router.actions.push(urls.notebook('headings'))
}, [])
return <App />
}
export function TextFormats(): JSX.Element {
export const TextFormats: StoryFn = () => {
useEffect(() => {
router.actions.push(urls.notebook('text-formats'))
}, [])
return <App />
}
export function NumberedList(): JSX.Element {
export const NumberedList: StoryFn = () => {
useEffect(() => {
router.actions.push(urls.notebook('numbered-list'))
}, [])
return <App />
}
export function BulletList(): JSX.Element {
export const BulletList: StoryFn = () => {
useEffect(() => {
router.actions.push(urls.notebook('bullet-list'))
}, [])
return <App />
}
export function RecordingsPlaylist(): JSX.Element {
export const RecordingsPlaylist: StoryFn = () => {
useEffect(() => {
router.actions.push(urls.notebook('recordings-playlist'))
}, [])
return <App />
}
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 <App />
}
export function EmptyNotebook(): JSX.Element {
export const EmptyNotebook: StoryFn = () => {
useEffect(() => {
router.actions.push(urls.notebook('empty'))
}, [])
return <App />
}
export function NotebookNotFound(): JSX.Element {
export const NotebookNotFound: StoryFn = () => {
useEffect(() => {
router.actions.push(urls.notebook('abcde'))
}, [])

View File

@ -213,12 +213,18 @@ export const notebookLogic = kea<notebookLogicType>([
} 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)

View File

@ -52,7 +52,7 @@ export const notebookSelectButtonLogic = kea<notebookSelectButtonLogicType>([
[] 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<notebookSelectButtonLogicType>([
[] as NotebookListItemType[],
{
loadNotebooksContainingResource: async (_, breakpoint) => {
breakpoint(100)
await breakpoint(100)
if (!props.resource) {
return []
}

View File

@ -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/': [],
},
}),
],

View File

@ -311,7 +311,7 @@ export const sessionRecordingDataLogic = kea<sessionRecordingDataLogicType>([
if (!values.sessionPlayerMetaData) {
return null
}
breakpoint(100)
await breakpoint(100)
await api.recordings.persist(props.sessionRecordingId)
return {

View File

@ -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 <App />
}
export function NewSurvey(): JSX.Element {
export const NewSurvey: StoryFn = () => {
useEffect(() => {
router.actions.push(urls.survey('new'))
}, [])
return <App />
}
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 <App />
}
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 <App />
}
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 <App />
}
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 <App />
}
export function SurveyView(): JSX.Element {
export const SurveyView: StoryFn = () => {
useEffect(() => {
router.actions.push(urls.survey(MOCK_SURVEY_WITH_RELEASE_CONS.id))
}, [])
return <App />
}
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 <App />
}
export function SurveyNotFound(): JSX.Element {
export const SurveyNotFound: StoryFn = () => {
useEffect(() => {
router.actions.push(urls.survey('1234566789'))
}, [])

View File

@ -134,7 +134,10 @@ export const surveyLogic = kea<surveyLogicType>([
actions.reportSurveyViewed(survey)
return survey
} catch (error: any) {
actions.setSurveyMissing()
if (error.status === 404) {
actions.setSurveyMissing()
return { ...NEW_SURVEY }
}
throw error
}
}

View File

@ -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": [

View File

@ -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'}

View File

@ -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

View File

@ -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',
}