From dd7191a96b5a8b7007f7bf3f4a493f71bd56b538 Mon Sep 17 00:00:00 2001 From: Artur Pata Date: Wed, 14 Aug 2024 15:52:44 +0300 Subject: [PATCH] Test query period picking behaviour --- CHANGELOG.md | 2 + assets/jest.config.json | 14 +- assets/js/dashboard/comparison-input.js | 8 +- assets/js/dashboard/datepicker.js | 11 +- assets/js/dashboard/extra/funnel.js | 6 +- assets/js/dashboard/query-dates.test.tsx | 183 ++++++++++++++++++ assets/js/dashboard/router.js | 6 +- .../js/dashboard/stats/graph/graph-tooltip.js | 4 +- .../dashboard/stats/graph/interval-picker.js | 6 +- assets/js/dashboard/stats/graph/top-stats.js | 8 +- assets/js/dashboard/stats/locations/map.tsx | 16 +- assets/package-lock.json | 170 ++++++++++++++++ assets/package.json | 2 + assets/test-utils/app-context-providers.tsx | 75 +++++++ assets/test-utils/reset-state.ts | 13 ++ assets/test-utils/set-fixed-timezone.ts | 13 ++ assets/tsconfig.json | 1 + 17 files changed, 503 insertions(+), 35 deletions(-) create mode 100644 assets/js/dashboard/query-dates.test.tsx create mode 100644 assets/test-utils/app-context-providers.tsx create mode 100644 assets/test-utils/reset-state.ts create mode 100644 assets/test-utils/set-fixed-timezone.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 83cbe40a4f5e5..0511859cdc9c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ All notable changes to this project will be documented in this file. - Add search and pagination functionality into Google Keywords > Details modal - ClickHouse system.query_log table log_comment column now contains information about source of queries. Useful for debugging - New /debug/clickhouse route for super admins which shows information on clickhouse queries executed by user +- Typescript support for `/assets` +- Testing framework for `/assets` ### Removed - Deprecate `ECTO_IPV6` and `ECTO_CH_IPV6` env vars in CE plausible/analytics#4245 diff --git a/assets/jest.config.json b/assets/jest.config.json index 06e5453a9f4e8..e7a84c44332f6 100644 --- a/assets/jest.config.json +++ b/assets/jest.config.json @@ -2,6 +2,16 @@ "clearMocks": true, "coverageDirectory": "coverage", "coverageProvider": "v8", - "preset": "ts-jest", - "testEnvironment": "jsdom" + "testEnvironment": "jsdom", + "globals": { + "BUILD_EXTRA": true + }, + "setupFiles": ["/test-utils/set-fixed-timezone.ts"], + "setupFilesAfterEnv": ["/test-utils/reset-state.ts"], + "transform": { + "^.+.[tj]sx?$": ["ts-jest", {}] + }, + "moduleNameMapper": { + "d3": "/node_modules/d3/dist/d3.min.js" + } } diff --git a/assets/js/dashboard/comparison-input.js b/assets/js/dashboard/comparison-input.js index 3981e78a6bb8d..30c125c856987 100644 --- a/assets/js/dashboard/comparison-input.js +++ b/assets/js/dashboard/comparison-input.js @@ -1,14 +1,14 @@ import React, { Fragment, useState, useRef, useEffect } from 'react' -import { useAppNavigate } from './navigation/use-app-navigate.js' +import { useAppNavigate } from './navigation/use-app-navigate' import { navigateToQuery } from './query' import { Menu, Transition } from '@headlessui/react' import { ChevronDownIcon } from '@heroicons/react/20/solid' import classNames from 'classnames' import * as storage from './util/storage' import Flatpickr from 'react-flatpickr' -import { parseNaiveDate, formatISO, formatDateRange } from './util/date.js' -import { useQueryContext } from './query-context.js' -import { useSiteContext } from './site-context.js' +import { parseNaiveDate, formatISO, formatDateRange } from './util/date' +import { useQueryContext } from './query-context' +import { useSiteContext } from './site-context' const COMPARISON_MODES = { 'off': 'Disable comparison', diff --git a/assets/js/dashboard/datepicker.js b/assets/js/dashboard/datepicker.js index 7d7bf05408b94..abfd9960ae6ab 100644 --- a/assets/js/dashboard/datepicker.js +++ b/assets/js/dashboard/datepicker.js @@ -26,11 +26,11 @@ import { isSameDate } from "./util/date"; import { navigateToQuery, QueryLink, QueryButton } from "./query"; -import { shouldIgnoreKeypress } from "./keybinding.js"; -import { COMPARISON_DISABLED_PERIODS, toggleComparisons, isComparisonEnabled } from "../dashboard/comparison-input.js"; +import { shouldIgnoreKeypress } from "./keybinding"; +import { COMPARISON_DISABLED_PERIODS, toggleComparisons, isComparisonEnabled } from "../dashboard/comparison-input"; import classNames from "classnames"; -import { useQueryContext } from "./query-context.js"; -import { useSiteContext } from "./site-context.js"; +import { useQueryContext } from "./query-context"; +import { useSiteContext } from "./site-context"; function KeyBindHint({children}) { return ( @@ -178,7 +178,7 @@ function DatePicker() { const handleKeydown = useCallback((e) => { if (shouldIgnoreKeypress(e)) return true - + const newSearch = { period: null, from: null, @@ -326,6 +326,7 @@ function DatePicker() { if (mode === "menu") { return (
diff --git a/assets/js/dashboard/extra/funnel.js b/assets/js/dashboard/extra/funnel.js index c3dd7204073d7..477b110d4d415 100644 --- a/assets/js/dashboard/extra/funnel.js +++ b/assets/js/dashboard/extra/funnel.js @@ -1,7 +1,7 @@ import React, { useEffect, useState, useRef } from 'react'; import FlipMove from 'react-flip-move'; import Chart from 'chart.js/auto'; -import FunnelTooltip from './funnel-tooltip.js'; +import FunnelTooltip from './funnel-tooltip'; import ChartDataLabels from 'chartjs-plugin-datalabels'; import numberFormatter from '../util/number-formatter'; import Bar from '../stats/bar'; @@ -10,8 +10,8 @@ import RocketIcon from '../stats/modals/rocket-icon'; import * as api from '../api'; import LazyLoader from '../components/lazy-loader'; -import { useQueryContext } from '../query-context.js'; -import { useSiteContext } from '../site-context.js'; +import { useQueryContext } from '../query-context'; +import { useSiteContext } from '../site-context'; export default function Funnel({ funnelName, tabs }) { diff --git a/assets/js/dashboard/query-dates.test.tsx b/assets/js/dashboard/query-dates.test.tsx new file mode 100644 index 0000000000000..531c78e62e721 --- /dev/null +++ b/assets/js/dashboard/query-dates.test.tsx @@ -0,0 +1,183 @@ +/** @format */ + +import React from 'react' +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import '@testing-library/jest-dom' +import DatePicker from './datepicker' +import { TestContextProviders } from '../../test-utils/app-context-providers' +import { stringifySearch } from './util/url' +import { useNavigate } from 'react-router-dom' +import { getRouterBasepath } from './router' + +const domain = 'picking-query-dates.test' +const periodStorageKey = `period__${domain}` + +test('if no period is stored, loads with default value of "Last 30 days", all expected options are present', async () => { + expect(localStorage.getItem(periodStorageKey)).toBe(null) + render(, { + wrapper: (props) => ( + + ) + }) + + await userEvent.click(screen.getByText('Last 30 days')) + + expect(screen.getByTestId('datemenu')).toBeVisible() + expect(screen.getAllByRole('link').map((el) => el.textContent)).toEqual( + [ + ['Today', 'D'], + ['Yesterday', 'E'], + ['Realtime', 'R'], + ['Last 7 Days', 'W'], + ['Last 30 Days', 'T'], + ['Month to Date', 'M'], + ['Last Month', ''], + ['Year to Date', 'Y'], + ['Last 12 months', 'L'], + ['All time', 'A'] + ].map((a) => a.join('')) + ) + expect(screen.getByText('Custom Range').textContent).toEqual( + ['Custom Range', 'C'].join('') + ) + expect(screen.getByText('Compare').textContent).toEqual( + ['Compare', 'X'].join('') + ) +}) + +test('user can select a new period and its value is stored', async () => { + render(, { + wrapper: (props) => ( + + ) + }) + + await userEvent.click(screen.getByText('Last 30 days')) + expect(screen.getByTestId('datemenu')).toBeVisible() + await userEvent.click(screen.getByText('All time')) + expect(screen.queryByTestId('datemenu')).toBeNull() + expect(localStorage.getItem(periodStorageKey)).toBe('all') +}) + +test('stored period "all" is respected, and Compare option is not present for it in menu', async () => { + localStorage.setItem(periodStorageKey, 'all') + + render(, { + wrapper: (props) => ( + + ) + }) + + await userEvent.click(screen.getByText('All time')) + expect(screen.getByTestId('datemenu')).toBeVisible() + expect(screen.queryByText('Compare')).toBeNull() +}) + +test.each([ + [{ period: 'all' }, 'All time'], + [{ period: 'month' }, 'Month to Date'], + [{ period: 'year' }, 'Year to Date'] +])( + 'the query period from search %p is respected and stored', + async (searchRecord, buttonText) => { + const startUrl = `${getRouterBasepath({ domain, shared: false })}${stringifySearch(searchRecord)}` + + render(, { + wrapper: (props) => ( + + ) + }) + + expect(screen.getByText(buttonText)).toBeVisible() + expect(localStorage.getItem(periodStorageKey)).toBe(searchRecord.period) + } +) + +test.each([ + [ + { period: 'custom', from: '2024-08-10', to: '2024-08-20' }, + '10 Aug - 20 Aug' + ], + [{ period: 'realtime' }, 'Realtime'] +])( + 'the query period from search %p is respected but not stored', + async (searchRecord, buttonText) => { + const startUrl = `${getRouterBasepath({ domain, shared: false })}${stringifySearch(searchRecord)}` + + render(, { + wrapper: (props) => ( + + ) + }) + expect(screen.getByText(buttonText)).toBeVisible() + expect(localStorage.getItem(periodStorageKey)).toBe(null) + } +) + +test.each([ + ['all', '7d', 'Last 7 days'], + ['30d', 'month', 'Month to Date'] +])( + 'if the stored period is %p but query period is %p, query is respected and the stored period is overwritten', + async (storedPeriod, queryPeriod, buttonText) => { + localStorage.setItem(periodStorageKey, storedPeriod) + const startUrl = `${getRouterBasepath({ domain, shared: false })}${stringifySearch({ period: queryPeriod })}` + + render(, { + wrapper: (props) => ( + + ) + }) + + await userEvent.click(screen.getByText(buttonText)) + expect(screen.getByTestId('datemenu')).toBeVisible() + expect(localStorage.getItem(periodStorageKey)).toBe(queryPeriod) + } +) + +test('going back resets the stored query period to previous value', async () => { + const BrowserBackButton = () => { + const navigate = useNavigate() + return ( + + ) + } + render( + <> + + + , + { + wrapper: (props) => ( + + ) + } + ) + + await userEvent.click(screen.getByText('Last 30 days')) + await userEvent.click(screen.getByText('Year to Date')) + expect(localStorage.getItem(periodStorageKey)).toBe('year') + + await userEvent.click(screen.getByText('Year to Date')) + await userEvent.click(screen.getByText('Month to Date')) + expect(localStorage.getItem(periodStorageKey)).toBe('month') + + await userEvent.click(screen.getByTestId('browser-back')) + expect(screen.getByText('Year to Date')).toBeVisible() + expect(localStorage.getItem(periodStorageKey)).toBe('year') +}) diff --git a/assets/js/dashboard/router.js b/assets/js/dashboard/router.js index 8a5c2d85d0036..147df4bf01bed 100644 --- a/assets/js/dashboard/router.js +++ b/assets/js/dashboard/router.js @@ -156,11 +156,15 @@ export const filterRoute = { element: } -export function createAppRouter(site) { +export function getRouterBasepath(site) { const basepath = site.shared ? `/share/${encodeURIComponent(site.domain)}` : `/${encodeURIComponent(site.domain)}` + return basepath +} +export function createAppRouter(site) { + const basepath = getRouterBasepath(site) const router = createBrowserRouter( [ { diff --git a/assets/js/dashboard/stats/graph/graph-tooltip.js b/assets/js/dashboard/stats/graph/graph-tooltip.js index f17ce0ae1a6b8..1763d7bb7283c 100644 --- a/assets/js/dashboard/stats/graph/graph-tooltip.js +++ b/assets/js/dashboard/stats/graph/graph-tooltip.js @@ -1,5 +1,5 @@ -import { METRIC_FORMATTER, METRIC_LABELS } from './graph-util.js' -import dateFormatter from './date-formatter.js' +import { METRIC_FORMATTER, METRIC_LABELS } from './graph-util' +import dateFormatter from './date-formatter' const renderBucketLabel = function(query, graphData, label, comparison = false) { let isPeriodFull = graphData.full_intervals?.[label] diff --git a/assets/js/dashboard/stats/graph/interval-picker.js b/assets/js/dashboard/stats/graph/interval-picker.js index 34a3856b97e66..26cbb5c1cce25 100644 --- a/assets/js/dashboard/stats/graph/interval-picker.js +++ b/assets/js/dashboard/stats/graph/interval-picker.js @@ -3,9 +3,9 @@ import { ChevronDownIcon } from '@heroicons/react/20/solid'; import React, { Fragment, useCallback, useEffect } from 'react'; import classNames from 'classnames'; import * as storage from '../../util/storage'; -import { isKeyPressed } from '../../keybinding.js'; -import { useQueryContext } from '../../query-context.js'; -import { useSiteContext } from '../../site-context.js'; +import { isKeyPressed } from '../../keybinding'; +import { useQueryContext } from '../../query-context'; +import { useSiteContext } from '../../site-context'; const INTERVAL_LABELS = { 'minute': 'Minutes', diff --git a/assets/js/dashboard/stats/graph/top-stats.js b/assets/js/dashboard/stats/graph/top-stats.js index e4566cd9639d8..53faaa8c72212 100644 --- a/assets/js/dashboard/stats/graph/top-stats.js +++ b/assets/js/dashboard/stats/graph/top-stats.js @@ -4,10 +4,10 @@ import { SecondsSinceLastLoad } from '../../util/seconds-since-last-load'; import classNames from "classnames"; import numberFormatter, { durationFormatter } from '../../util/number-formatter'; import * as storage from '../../util/storage'; -import { formatDateRange } from '../../util/date.js'; -import { getGraphableMetrics } from "./graph-util.js"; -import { useQueryContext } from "../../query-context.js"; -import { useSiteContext } from "../../site-context.js"; +import { formatDateRange } from '../../util/date'; +import { getGraphableMetrics } from "./graph-util"; +import { useQueryContext } from "../../query-context"; +import { useSiteContext } from "../../site-context"; function Maybe({ condition, children }) { if (condition) { diff --git a/assets/js/dashboard/stats/locations/map.tsx b/assets/js/dashboard/stats/locations/map.tsx index defc96ab21296..9cd017b8f0656 100644 --- a/assets/js/dashboard/stats/locations/map.tsx +++ b/assets/js/dashboard/stats/locations/map.tsx @@ -2,28 +2,20 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import * as d3 from 'd3' import classNames from 'classnames' -// @ts-expect-error untyped import * as api from '../../api' -// @ts-expect-error untyped import { navigateToQuery } from '../../query' -// @ts-expect-error untyped import { replaceFilterByPrefix, cleanLabels } from '../../util/filters' import { useAppNavigate } from '../../navigation/use-app-navigate' -// @ts-expect-error untyped import numberFormatter from '../../util/number-formatter' import * as topojson from 'topojson-client' import { useQuery } from '@tanstack/react-query' import { useSiteContext } from '../../site-context' -// @ts-expect-error untyped import { useQueryContext } from '../../query-context' import worldJson from 'visionscarto-world-atlas/world/110m.json' import { UIMode, useTheme } from '../../theme-context' import { apiPath } from '../../util/url' -// @ts-expect-error untyped import MoreLink from '../more-link' -// @ts-expect-error untyped import { countriesRoute } from '../../router' -// @ts-expect-error untyped import { MIN_HEIGHT } from '../reports/list' import { MapTooltip } from './map-tooltip' import { GeolocationNotice } from './geolocation-notice' @@ -49,7 +41,8 @@ const WorldMap = ({ const navigate = useAppNavigate() const { mode } = useTheme() const site = useSiteContext() - const { query } = useQueryContext() + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { query } = useQueryContext() as { query: any } const svgRef = useRef(null) const [tooltip, setTooltip] = useState<{ x: number @@ -193,9 +186,10 @@ const WorldMap = ({ list={data?.results ?? []} linkProps={{ path: countriesRoute.path, - // @ts-expect-error MoreLink not typed yet - search: (search) => search + search: (search: Record) => search }} + className={undefined} + onClick={undefined} /> {site.isDbip && }
diff --git a/assets/package-lock.json b/assets/package-lock.json index 1f5deddedc99a..0e327b4705df6 100644 --- a/assets/package-lock.json +++ b/assets/package-lock.json @@ -43,7 +43,9 @@ }, "devDependencies": { "@testing-library/dom": "^10.4.0", + "@testing-library/jest-dom": "^6.4.8", "@testing-library/react": "^16.0.0", + "@testing-library/user-event": "^14.5.2", "@types/classnames": "^2.3.1", "@types/d3": "^7.4.3", "@types/jest": "^29.5.12", @@ -69,6 +71,12 @@ "typescript": "^5.5.4" } }, + "node_modules/@adobe/css-tools": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.0.tgz", + "integrity": "sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==", + "dev": true + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "license": "MIT", @@ -1833,6 +1841,100 @@ "node": ">=8" } }, + "node_modules/@testing-library/jest-dom": { + "version": "6.4.8", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.4.8.tgz", + "integrity": "sha512-JD0G+Zc38f5MBHA4NgxQMR5XtO5Jx9g86jqturNTt2WUfRmLDIY7iKkWHDCCTiDuFMre6nxAD5wHw9W5kI4rGw==", + "dev": true, + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "@babel/runtime": "^7.9.2", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "lodash": "^4.17.21", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true + }, + "node_modules/@testing-library/jest-dom/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@testing-library/react": { "version": "16.0.0", "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.0.0.tgz", @@ -1860,6 +1962,19 @@ } } }, + "node_modules/@testing-library/user-event": { + "version": "14.5.2", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.2.tgz", + "integrity": "sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==", + "dev": true, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -3610,6 +3725,12 @@ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" } }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true + }, "node_modules/cssesc": { "version": "3.0.0", "license": "MIT", @@ -5825,6 +5946,15 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/inflight": { "version": "1.0.6", "license": "ISC", @@ -8307,6 +8437,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, "node_modules/lodash.castarray": { "version": "4.4.0", "license": "MIT" @@ -8487,6 +8623,15 @@ "node": ">=6" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/mini-svg-data-uri": { "version": "1.4.4", "license": "MIT", @@ -9411,6 +9556,19 @@ "node": ">=8.10.0" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", @@ -10029,6 +10187,18 @@ "node": ">=6" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", diff --git a/assets/package.json b/assets/package.json index c933cbb986b78..558f793d5312f 100644 --- a/assets/package.json +++ b/assets/package.json @@ -46,7 +46,9 @@ }, "devDependencies": { "@testing-library/dom": "^10.4.0", + "@testing-library/jest-dom": "^6.4.8", "@testing-library/react": "^16.0.0", + "@testing-library/user-event": "^14.5.2", "@types/classnames": "^2.3.1", "@types/d3": "^7.4.3", "@types/jest": "^29.5.12", diff --git a/assets/test-utils/app-context-providers.tsx b/assets/test-utils/app-context-providers.tsx new file mode 100644 index 0000000000000..a0d38d2bb620e --- /dev/null +++ b/assets/test-utils/app-context-providers.tsx @@ -0,0 +1,75 @@ +/** @format */ + +import React, { ReactNode } from 'react' +import SiteContextProvider, { + PlausibleSite +} from '../js/dashboard/site-context' +import UserContextProvider, { Role } from '../js/dashboard/user-context' +import { MemoryRouter, MemoryRouterProps } from 'react-router-dom' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import QueryContextProvider from '../js/dashboard/query-context' +import { getRouterBasepath } from '../js/dashboard/router' + +type TestContextProvidersProps = { + children: ReactNode + routerProps?: Pick + siteOptions?: Partial +} + +export const TestContextProviders = ({ + children, + routerProps, + siteOptions +}: TestContextProvidersProps) => { + const defaultSite: PlausibleSite = { + domain: 'plausible.io/unit', + offset: '0', + hasGoals: false, + hasProps: false, + funnelsAvailable: false, + propsAvailable: false, + conversionsOptedOut: false, + funnelsOptedOut: false, + propsOptedOut: false, + revenueGoals: [], + funnels: [], + statsBegin: '', + nativeStatsBegin: '', + embedded: '', + background: '', + isDbip: false, + flags: {}, + validIntervalsByPeriod: {}, + shared: false + } + + const site = { ...defaultSite, ...siteOptions } + + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + refetchOnWindowFocus: false + } + } + }) + + const defaultInitialEntries = [getRouterBasepath(site)] + + return ( + // not interactive component, default value is suitable + + + + + {children} + + + + + // + ) +} diff --git a/assets/test-utils/reset-state.ts b/assets/test-utils/reset-state.ts new file mode 100644 index 0000000000000..24124fa3ccc4d --- /dev/null +++ b/assets/test-utils/reset-state.ts @@ -0,0 +1,13 @@ +/** @format */ + +/** + * @returns clears the state that the app stores, + * to avoid individual tests impacting each other + */ +function clearStoredAppState() { + localStorage.clear() +} + +beforeEach(() => { + clearStoredAppState() +}) diff --git a/assets/test-utils/set-fixed-timezone.ts b/assets/test-utils/set-fixed-timezone.ts new file mode 100644 index 0000000000000..42da38697fe16 --- /dev/null +++ b/assets/test-utils/set-fixed-timezone.ts @@ -0,0 +1,13 @@ +/** + * @format + */ + +/** + * @returns sets a fixed timezone for the test process, + * otherwise test runs on different servers and machines may be inconsistent + */ +function setFixedTimezone() { + process.env.TZ = 'UTC' +} + +setFixedTimezone() diff --git a/assets/tsconfig.json b/assets/tsconfig.json index ce902c9f26af5..f8f962af9f635 100644 --- a/assets/tsconfig.json +++ b/assets/tsconfig.json @@ -3,6 +3,7 @@ "jsx": "react", "target": "es2017", "module": "commonjs", + "allowJs": true, "resolveJsonModule": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true,