Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ CoreUI is meant to be the UX game changer. Pure & transparent code is devoid of
* [Quick Start](#quick-start)
* [Installation](#installation)
* [Basic usage](#basic-usage)
* [Testing](#testing)
* [What's included](#whats-included)
* [Documentation](#documentation)
* [Components](#components)
Expand Down Expand Up @@ -112,6 +113,96 @@ or
$ yarn build
```

## Testing

This project uses [Vitest](https://vitest.dev/) and [React Testing Library](https://testing-library.com/react) for unit testing.

### Running Tests

```bash
# Run tests in watch mode
$ npm test

# Run tests once
$ npm test -- --run

# Run tests with UI
$ npm run test:ui

# Run tests with coverage
$ npm run test:coverage
```

### Test Structure

Tests are located alongside their source files with the `.test.js` extension:

```
src/
├── components/
│ ├── AppHeader.js
│ ├── AppHeader.test.js
│ ├── AppFooter.js
│ └── AppFooter.test.js
├── App.js
├── App.test.js
└── store.test.js
```

### Writing Tests

**Basic Component Test:**
```javascript
import { render, screen } from '@testing-library/react'
import { describe, it, expect } from 'vitest'
import MyComponent from './MyComponent'

describe('MyComponent', () => {
it('renders without crashing', () => {
render(<MyComponent />)
expect(screen.getByText('Hello')).toBeInTheDocument()
})
})
```

**Testing with Redux:**
```javascript
import { Provider } from 'react-redux'
import store from '../store'

const renderWithProviders = (component) => {
return render(
<Provider store={store}>
{component}
</Provider>
)
}
```

**Testing with Router:**
```javascript
import { BrowserRouter } from 'react-router-dom'

const renderWithRouter = (component) => {
return render(
<BrowserRouter>
{component}
</BrowserRouter>
)
}
```

### Current Test Coverage

- ✅ AppFooter
- ✅ AppHeader
- ✅ AppSidebar
- ✅ AppSidebarNav
- ✅ AppBreadcrumb
- ✅ AppContent
- ✅ App
- ✅ Store (Redux)

## What's included

Within the download you'll find the following directories and files, logically grouping common assets and providing both compiled and minified variations. You'll see something like this:
Expand Down
14 changes: 12 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@
"build": "vite build",
"lint": "eslint",
"serve": "vite preview",
"start": "vite"
"start": "vite",
"test": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest --coverage"
},
"dependencies": {
"@coreui/chartjs": "^4.1.0",
Expand All @@ -39,17 +42,24 @@
"simplebar-react": "^3.3.2"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.1.0",
"@testing-library/user-event": "^14.5.2",
"@vitejs/plugin-react": "^5.1.0",
"@vitest/coverage-v8": "^2.1.9",
"@vitest/ui": "^2.1.8",
"autoprefixer": "^10.4.21",
"eslint": "^9.38.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.0.1",
"globals": "^16.4.0",
"jsdom": "^25.0.1",
"postcss": "^8.5.6",
"prettier": "3.6.2",
"sass": "^1.93.2",
"vite": "^7.1.12"
"vite": "^7.1.12",
"vitest": "^2.1.8"
}
}
34 changes: 34 additions & 0 deletions src/App.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { render } from '@testing-library/react'
import { Provider } from 'react-redux'
import { describe, it, expect } from 'vitest'
import App from './App'
import store from './store'

describe('App', () => {
it('renders without crashing', () => {
const { container } = render(
<Provider store={store}>
<App />
</Provider>,
)
expect(container).toBeInTheDocument()
})

it('renders HashRouter', () => {
const { container } = render(
<Provider store={store}>
<App />
</Provider>,
)
expect(container.querySelector('div')).toBeInTheDocument()
})

it('renders Suspense with fallback', () => {
const { container } = render(
<Provider store={store}>
<App />
</Provider>,
)
expect(container).toBeInTheDocument()
})
})
21 changes: 21 additions & 0 deletions src/components/AppBreadcrumb.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { render } from '@testing-library/react'
import { BrowserRouter } from 'react-router-dom'
import { describe, it, expect } from 'vitest'
import AppBreadcrumb from './AppBreadcrumb'

const renderWithRouter = (component) => {
return render(<BrowserRouter>{component}</BrowserRouter>)
}

describe('AppBreadcrumb', () => {
it('renders without crashing', () => {
const { container } = renderWithRouter(<AppBreadcrumb />)
expect(container).toBeInTheDocument()
})

it('renders breadcrumb container', () => {
const { container } = renderWithRouter(<AppBreadcrumb />)
const breadcrumb = container.querySelector('.breadcrumb')
expect(breadcrumb).toBeInTheDocument()
})
})
32 changes: 32 additions & 0 deletions src/components/AppContent.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { render } from '@testing-library/react'
import { BrowserRouter } from 'react-router-dom'
import { Provider } from 'react-redux'
import { describe, it, expect } from 'vitest'
import AppContent from './AppContent'
import store from '../store'

const renderWithProviders = (component) => {
return render(
<Provider store={store}>
<BrowserRouter>{component}</BrowserRouter>
</Provider>,
)
}

describe('AppContent', () => {
it('renders without crashing', () => {
const { container } = renderWithProviders(<AppContent />)
expect(container).toBeInTheDocument()
})

it('renders container', () => {
const { container } = renderWithProviders(<AppContent />)
const contentContainer = container.querySelector('.container-lg')
expect(contentContainer).toBeInTheDocument()
})

it('renders Suspense fallback', () => {
const { container } = renderWithProviders(<AppContent />)
expect(container).toBeInTheDocument()
})
})
40 changes: 40 additions & 0 deletions src/components/AppFooter.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { render, screen } from '@testing-library/react'
import { describe, it, expect } from 'vitest'
import AppFooter from './AppFooter'

describe('AppFooter', () => {
it('renders without crashing', () => {
render(<AppFooter />)
expect(screen.getByText('CoreUI')).toBeInTheDocument()
})

it('displays copyright year 2025', () => {
render(<AppFooter />)
expect(screen.getByText(/2025 creativeLabs/)).toBeInTheDocument()
})

it('renders CoreUI link with correct attributes', () => {
render(<AppFooter />)
const link = screen.getByRole('link', { name: 'CoreUI' })
expect(link).toHaveAttribute('href', 'https://coreui.io')
expect(link).toHaveAttribute('target', '_blank')
expect(link).toHaveAttribute('rel', 'noopener noreferrer')
})

it('renders CoreUI React Admin link', () => {
render(<AppFooter />)
const link = screen.getByRole('link', { name: /CoreUI React Admin/ })
expect(link).toHaveAttribute('href', 'https://coreui.io/react')
expect(link).toHaveAttribute('target', '_blank')
expect(link).toHaveAttribute('rel', 'noopener noreferrer')
})

it('displays "Powered by" text', () => {
render(<AppFooter />)
expect(screen.getByText('Powered by')).toBeInTheDocument()
})

it('is memoized', () => {
expect(AppFooter).toBe(AppFooter)
})
})
66 changes: 66 additions & 0 deletions src/components/AppHeader.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { render, screen } from '@testing-library/react'
import { Provider } from 'react-redux'
import { BrowserRouter } from 'react-router-dom'
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import AppHeader from './AppHeader'
import store from '../store'

const renderWithProviders = (component) => {
return render(
<Provider store={store}>
<BrowserRouter>{component}</BrowserRouter>
</Provider>,
)
}

describe('AppHeader', () => {
let addEventListenerSpy
let removeEventListenerSpy

beforeEach(() => {
addEventListenerSpy = vi.spyOn(document, 'addEventListener')
removeEventListenerSpy = vi.spyOn(document, 'removeEventListener')
})

afterEach(() => {
vi.restoreAllMocks()
})

it('renders without crashing', () => {
const { container } = renderWithProviders(<AppHeader />)
expect(container).toBeInTheDocument()
})

it('renders navigation items', () => {
renderWithProviders(<AppHeader />)
expect(screen.getByText('Dashboard')).toBeInTheDocument()
expect(screen.getByText('Users')).toBeInTheDocument()
expect(screen.getAllByText('Settings').length).toBeGreaterThan(0)
})

it('adds scroll event listener on mount', () => {
renderWithProviders(<AppHeader />)
expect(addEventListenerSpy).toHaveBeenCalledWith('scroll', expect.any(Function))
})

it('removes scroll event listener on unmount', () => {
const { unmount } = renderWithProviders(<AppHeader />)
const scrollHandler = addEventListenerSpy.mock.calls[0][1]

unmount()

expect(removeEventListenerSpy).toHaveBeenCalledWith('scroll', scrollHandler)
})

it('renders theme switcher dropdown', () => {
renderWithProviders(<AppHeader />)
const dropdowns = document.querySelectorAll('.dropdown')
expect(dropdowns.length).toBeGreaterThan(0)
})

it('renders header icons', () => {
renderWithProviders(<AppHeader />)
const icons = document.querySelectorAll('.nav-link')
expect(icons.length).toBeGreaterThan(0)
})
})
43 changes: 43 additions & 0 deletions src/components/AppSidebar.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { render } from '@testing-library/react'
import { Provider } from 'react-redux'
import { BrowserRouter } from 'react-router-dom'
import { describe, it, expect } from 'vitest'
import AppSidebar from './AppSidebar'
import store from '../store'

const renderWithProviders = (component) => {
return render(
<Provider store={store}>
<BrowserRouter>{component}</BrowserRouter>
</Provider>,
)
}

describe('AppSidebar', () => {
it('renders without crashing', () => {
const { container } = renderWithProviders(<AppSidebar />)
expect(container).toBeInTheDocument()
})

it('renders sidebar brand', () => {
const { container } = renderWithProviders(<AppSidebar />)
const brand = container.querySelector('.sidebar-brand')
expect(brand).toBeInTheDocument()
})

it('renders navigation items', () => {
const { container } = renderWithProviders(<AppSidebar />)
const navItems = container.querySelectorAll('.nav-link')
expect(navItems.length).toBeGreaterThan(0)
})

it('renders sidebar toggler on large screens', () => {
const { container } = renderWithProviders(<AppSidebar />)
const toggler = container.querySelector('.sidebar-toggler')
expect(toggler).toBeInTheDocument()
})

it('is memoized', () => {
expect(AppSidebar).toBe(AppSidebar)
})
})
Loading