Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add testing framework #4440

Merged
merged 2 commits into from
Aug 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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