Skip to content

Commit

Permalink
feat: add test
Browse files Browse the repository at this point in the history
  • Loading branch information
duyet committed Nov 19, 2023
1 parent 85436d3 commit a1bd809
Show file tree
Hide file tree
Showing 24 changed files with 1,577 additions and 54 deletions.
35 changes: 23 additions & 12 deletions .github/workflows/build.yml → .github/workflows/base.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
name: Build
name: Reusable

on:
push:
branches:
- main
pull_request:
types:
- opened
- reopened
workflow_dispatch:
workflow_call:
inputs:
job-type:
required: true
type: string

env:
CLICKHOUSE_HOST: http://localhost:8123
Expand All @@ -17,11 +14,11 @@ env:

permissions:
contents: read
pages: write
id-token: write

jobs:
build:
job:
name: ${{ inputs.job-type }}
runs-on: ubuntu-latest
services:
clickhouse:
Expand Down Expand Up @@ -60,5 +57,19 @@ jobs:
- name: Install dependencies
run: yarn install

- name: Build Next.js
- name: Test e2e
if: ${{ inputs.job-type == 'test' }}
uses: cypress-io/github-action@v6
with:
command: yarn e2e:headless

- name: Test components
if: ${{ inputs.job-type == 'test' }}
uses: cypress-io/github-action@v6
with:
component: true
command: yarn component:headless

- name: Build App
if: ${{ inputs.job-type == 'build' }}
run: yarn build
26 changes: 26 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Build and Test

on:
push:
branches:
- main
pull_request:
types:
- opened
- reopened
workflow_dispatch:

permissions:
contents: read
id-token: write

jobs:
build:
uses: ./.github/workflows/base.yml
with:
job-type: build

test:
uses: ./.github/workflows/base.yml
with:
job-type: test
10 changes: 10 additions & 0 deletions app/[name]/loading.cy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from 'react'

import Loading from './loading'

describe('<Loading />', () => {
it('should render loading', () => {
cy.mount(<Loading />)
cy.get('div').contains('loading', { matchCase: false })
})
})
4 changes: 2 additions & 2 deletions app/[name]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { unstable_noStore as noStore } from 'next/cache'

import { fetchData } from '@/lib/clickhouse'
import { getQueryByName, queries } from '@/lib/clickhouse-queries'
import { getQueryConfigByName, queries } from '@/lib/clickhouse-queries'
import { cn } from '@/lib/utils'
import { DataTable } from '@/components/data-table/data-table'

Expand All @@ -18,7 +18,7 @@ export default async function Page({ params: { name } }: PageProps) {
noStore()

// Get the query config
const config = getQueryByName(name)
const config = getQueryConfigByName(name)
if (!config) {
return <div>404</div>
}
Expand Down
Binary file added components/data-table/.data-table.cy.tsx.swp
Binary file not shown.
142 changes: 142 additions & 0 deletions components/data-table/data-table.cy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import React from 'react'

import { QueryConfig } from '@/lib/clickhouse-queries'

import { DataTable } from './data-table'

describe('<DataTable />', () => {
// Define mock config
const config: QueryConfig = {
name: 'settings',
sql: '/* No need */',
columns: ['col1', 'col2'],
}

it('renders', () => {
// Define some mock data
const data = [
{ col1: 'val1', col2: 'val1' },
{ col1: 'val2', col2: 'val2' },
{ col1: 'val3', col2: 'val3' },
]

cy.mount(<DataTable title="Test Table" config={config} data={data} />)

cy.get('h1').contains('Test Table')
cy.get('table').should('have.length', 1)
cy.get('thead').should('have.length', 1)
cy.get('thead').contains('col1')
cy.get('thead').contains('col2')
cy.get('tbody').contains('val1')
cy.get('tbody').contains('val2')
cy.get('div').contains(`${data.length} row`, { matchCase: false })
})

it('render paging', () => {
let data = []

for (let i = 0; i < 100; i++) {
data.push({ col1: `val${i}`, col2: `val${i}` })
}

cy.mount(
<DataTable
title="Test Table"
config={config}
data={data}
defaultPageSize={50}
/>
)

// "Go to previous page" button should be disabled
cy.get('button')
.contains('button', 'Go to previous page')
.should('be.disabled')
// "Go to next page" button should be enabled
cy.get('button').contains('button', 'Go to next page').should('be.enabled')

// Contains 100 rows
cy.get('div').contains(`${data.length} row`, { matchCase: false })

// "Rows per page" should be 50
cy.get('div')
.contains('Rows per page')
.parent()
.get('button')
.contains('50')

// Page 1 of 2
cy.get('div').contains('Page 1 of 2')
})

it('render paging, click on next page', () => {
let data = []

for (let i = 0; i < 100; i++) {
data.push({ col1: `val${i}`, col2: `val${i}` })
}

cy.mount(
<DataTable
title="Test Table"
config={config}
data={data}
defaultPageSize={50}
/>
)

// Page 1 of 2
cy.get('div').contains('Page 1 of 2')

// "Go to next page" button should be enabled
cy.get('button')
.contains('button', 'Go to next page')
.should('be.enabled')
.click()

// Page 2 of 2
cy.get('div').contains('Page 2 of 2')
})

it('should adjust column visibility', () => {
// Define some mock data
const data = [
{ col1: 'val1', col2: 'val1' },
{ col1: 'val2', col2: 'val2' },
{ col1: 'val3', col2: 'val3' },
]

cy.mount(<DataTable title="Test Table" config={config} data={data} />)

// Before click: table should contains 2 columns
cy.get('thead tr th').should('have.length', 2)

// Click on "Column Options" button, should showing 2 checkboxes: col1 and col2
// Click on col1 to toggle hide it
cy.get('button[aria-label="Column Options"]').click()
cy.get('[role="checkbox"]').should('have.length', 2)
cy.get('[role="checkbox"]').contains('col1').click()

// After click: table should contains only col2
cy.get('thead tr th').should('have.length', 1)

// Click on "Column Options" button, should showing 2 checkboxes: col1 and col2
// Click on col2 to toggle hide it
cy.get('button[aria-label="Column Options"]').click()
cy.get('[role="checkbox"]').should('have.length', 2)
cy.get('[role="checkbox"]').contains('col2').click()

// After click: table should contains no column
cy.get('thead tr th').should('have.length', 0)

// Click on "Column Options" button, should showing 2 checkboxes: col1 and col2
// Click on col2 to toggle show it again
cy.get('button[aria-label="Column Options"]').click()
cy.get('[role="checkbox"]').should('have.length', 2)
cy.get('[role="checkbox"]').contains('col1').click()
cy.get('button[aria-label="Column Options"]').click()
cy.get('[role="checkbox"]').contains('col2').click()

cy.get('thead tr th').should('have.length', 2)
})
})
12 changes: 10 additions & 2 deletions components/data-table/data-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,14 @@ interface DataTableProps<TData> {
title: string
config: QueryConfig
data: TData[]
defaultPageSize?: number
}

export function DataTable<TData, TValue>({
title = '',
config,
data,
defaultPageSize = 50,
}: DataTableProps<TData>) {
const columns = getColumns(config) as ColumnDef<TData, TValue>[]
const [sorting, setSorting] = useState<SortingState>([])
Expand All @@ -63,7 +65,7 @@ export function DataTable<TData, TValue>({
},
initialState: {
pagination: {
pageSize: 50,
pageSize: defaultPageSize,
},
},
})
Expand All @@ -77,7 +79,11 @@ export function DataTable<TData, TValue>({
<div className="flex items-center gap-3">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="ml-auto">
<Button
variant="outline"
className="ml-auto"
aria-label="Column Options"
>
<ColumnsIcon className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
Expand All @@ -93,6 +99,8 @@ export function DataTable<TData, TValue>({
onCheckedChange={(value) =>
column.toggleVisibility(!!value)
}
role="checkbox"
aria-label={column.id}
>
{column.id}
</DropdownMenuCheckboxItem>
Expand Down
50 changes: 43 additions & 7 deletions components/reload-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@ import { useInterval } from 'usehooks-ts'

import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { useAppContext } from '@/app/context'

interface ReloadButtonProps {
Expand All @@ -27,12 +41,34 @@ export function ReloadButton({ className }: ReloadButtonProps) {
useInterval(refreshRouter, reloadInterval)

return (
<Button
variant="outline"
className={cn('ml-auto', className)}
onClick={onClickReload}
>
<ReloadIcon className={cn('h-4 w-4', isLoading ? 'animate-spin' : '')} />
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">
<ReloadIcon
className={cn('h-4 w-4', isLoading ? 'animate-spin' : '')}
/>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuItem onClick={onClickReload}>
Reload Now
<DropdownMenuShortcut>⌘R</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuGroup>
<DropdownMenuSub>
<DropdownMenuSubTrigger>Interval</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuItem>5s</DropdownMenuItem>
<DropdownMenuItem>30s</DropdownMenuItem>
<DropdownMenuItem>10m</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem>Stop Auto Reload</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
18 changes: 18 additions & 0 deletions cypress.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { defineConfig } from 'cypress'

export default defineConfig({
defaultCommandTimeout: 15000,
fixturesFolder: false,
e2e: {
baseUrl: 'http://localhost:3000',
setupNodeEvents() {
// implement node event listeners here
},
},
component: {
devServer: {
framework: 'next',
bundler: 'webpack',
},
},
})
13 changes: 13 additions & 0 deletions cypress.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { mount } from 'cypress/react'

// Augment the Cypress namespace to include type definitions for
// your custom command.
// Alternatively, can be defined in cypress/support/component.d.ts
// with a <reference path="./component" /> at the top of your spec.
declare global {
namespace Cypress {
interface Chainable {
mount: typeof mount
}
}
}
Loading

0 comments on commit a1bd809

Please sign in to comment.