Skip to content

Commit

Permalink
Add testing framework (#4440)
Browse files Browse the repository at this point in the history
* Add testing framework

* Test query period picking behaviour
  • Loading branch information
apata authored Aug 15, 2024
1 parent 4d4c9a8 commit 8af3f73
Show file tree
Hide file tree
Showing 24 changed files with 8,335 additions and 2,311 deletions.
1 change: 1 addition & 0 deletions .github/workflows/node.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@ jobs:
- run: npm install --prefix ./tracker
- run: npm run lint --prefix ./assets
- run: npm run check-format --prefix ./assets
- run: npm run test --prefix ./assets
- run: npm run deploy --prefix ./tracker
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ npm-debug.log
/assets/node_modules/
/tracker/node_modules/

# test coverage directory
/assets/coverage

# Since we are building assets from assets/,
# we ignore priv/static. You may want to comment
# this depending on your deployment strategy.
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions assets/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
"root": true,
"env": {
"browser": true,
"es6": true
"es6": true,
"jest/globals": true
},
"plugins": ["import"],
"plugins": ["import", "jest"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:jest/recommended",
"plugin:jsx-a11y/recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
Expand Down
20 changes: 20 additions & 0 deletions assets/jest.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"clearMocks": true,
"coverageDirectory": "coverage",
"coverageProvider": "v8",
"testEnvironment": "jsdom",
"globals": {
"BUILD_EXTRA": true
},
"setupFiles": ["<rootDir>/test-utils/set-fixed-timezone.ts"],
"setupFilesAfterEnv": [
"<rootDir>/test-utils/extend-expect.ts",
"<rootDir>/test-utils/reset-state.ts"
],
"transform": {
"^.+.[tj]sx?$": ["ts-jest", {}]
},
"moduleNameMapper": {
"d3": "<rootDir>/node_modules/d3/dist/d3.min.js"
}
}
8 changes: 4 additions & 4 deletions assets/js/dashboard/comparison-input.js
Original file line number Diff line number Diff line change
@@ -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',
Expand Down
11 changes: 6 additions & 5 deletions assets/js/dashboard/datepicker.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -178,7 +178,7 @@ function DatePicker() {

const handleKeydown = useCallback((e) => {
if (shouldIgnoreKeypress(e)) return true

const newSearch = {
period: null,
from: null,
Expand Down Expand Up @@ -326,6 +326,7 @@ function DatePicker() {
if (mode === "menu") {
return (
<div
data-testid="datemenu"
id="datemenu"
className="absolute w-full left-0 right-0 md:w-56 md:absolute md:top-auto md:left-auto md:right-0 mt-2 origin-top-right z-10"
>
Expand Down
6 changes: 3 additions & 3 deletions assets/js/dashboard/extra/funnel.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 }) {
Expand Down
182 changes: 182 additions & 0 deletions assets/js/dashboard/query-dates.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
/** @format */

import React from 'react'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
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(<DatePicker />, {
wrapper: (props) => (
<TestContextProviders siteOptions={{ domain }} {...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(<DatePicker />, {
wrapper: (props) => (
<TestContextProviders siteOptions={{ domain }} {...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(<DatePicker />, {
wrapper: (props) => (
<TestContextProviders siteOptions={{ domain }} {...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(<DatePicker />, {
wrapper: (props) => (
<TestContextProviders
siteOptions={{ domain }}
routerProps={{ initialEntries: [startUrl] }}
{...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(<DatePicker />, {
wrapper: (props) => (
<TestContextProviders
siteOptions={{ domain }}
routerProps={{ initialEntries: [startUrl] }}
{...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(<DatePicker />, {
wrapper: (props) => (
<TestContextProviders
siteOptions={{ domain, shared: false }}
routerProps={{
initialEntries: [startUrl]
}}
{...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 (
<button data-testid="browser-back" onClick={() => navigate(-1)}></button>
)
}
render(
<>
<DatePicker />
<BrowserBackButton />
</>,
{
wrapper: (props) => (
<TestContextProviders siteOptions={{ domain }} {...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')
})
6 changes: 5 additions & 1 deletion assets/js/dashboard/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,11 +156,15 @@ export const filterRoute = {
element: <FilterModal />
}

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(
[
{
Expand Down
4 changes: 2 additions & 2 deletions assets/js/dashboard/stats/graph/graph-tooltip.js
Original file line number Diff line number Diff line change
@@ -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]
Expand Down
6 changes: 3 additions & 3 deletions assets/js/dashboard/stats/graph/interval-picker.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
8 changes: 4 additions & 4 deletions assets/js/dashboard/stats/graph/top-stats.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading

0 comments on commit 8af3f73

Please sign in to comment.