Merge branch 'Pouzor:main' into main
This commit is contained in:
+276
@@ -0,0 +1,276 @@
|
|||||||
|
# Contributing to Homelable
|
||||||
|
|
||||||
|
Thanks for taking the time to contribute! This document covers everything you need to get started.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Ways to Contribute](#ways-to-contribute)
|
||||||
|
- [Reporting Bugs](#reporting-bugs)
|
||||||
|
- [Suggesting Features](#suggesting-features)
|
||||||
|
- [Development Setup](#development-setup)
|
||||||
|
- [Project Structure](#project-structure)
|
||||||
|
- [Coding Standards](#coding-standards)
|
||||||
|
- [Testing](#testing)
|
||||||
|
- [Submitting a Pull Request](#submitting-a-pull-request)
|
||||||
|
- [Commit Message Format](#commit-message-format)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ways to Contribute
|
||||||
|
|
||||||
|
- Report bugs or unexpected behavior
|
||||||
|
- Suggest new features or improvements
|
||||||
|
- Fix open issues (check the [issue tracker](https://github.com/Pouzor/homelable/issues))
|
||||||
|
- Improve documentation
|
||||||
|
- Add service signatures to `service_signatures.json`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reporting Bugs
|
||||||
|
|
||||||
|
Before opening an issue, search existing ones to avoid duplicates.
|
||||||
|
|
||||||
|
When filing a bug, include:
|
||||||
|
|
||||||
|
- **Homelable version** (visible in the sidebar bottom-left)
|
||||||
|
- **Deployment method** (Docker Compose, Proxmox LXC, source)
|
||||||
|
- **Steps to reproduce**
|
||||||
|
- **Expected vs actual behavior**
|
||||||
|
- **Relevant logs** (`docker compose logs backend` / `docker compose logs frontend`)
|
||||||
|
- **Browser console errors** if it's a UI issue
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Suggesting Features
|
||||||
|
|
||||||
|
Open an issue with the `enhancement` label. Describe:
|
||||||
|
|
||||||
|
- The problem you're trying to solve
|
||||||
|
- Your proposed solution
|
||||||
|
- Any alternatives you considered
|
||||||
|
|
||||||
|
For large changes, discuss first before writing code — it avoids wasted effort.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Development Setup
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- **Node.js 20+** and **npm**
|
||||||
|
- **Python 3.11–3.13** (3.14 not yet supported by all dependencies)
|
||||||
|
- **nmap** installed on your system (required for scanner)
|
||||||
|
- **Docker + Docker Compose** (optional, for full-stack testing)
|
||||||
|
|
||||||
|
### 1. Clone the repo
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/Pouzor/homelable.git
|
||||||
|
cd homelable
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Backend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
python3.13 -m venv .venv
|
||||||
|
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Copy and configure environment
|
||||||
|
cp .env.example .env # edit AUTH_PASSWORD_HASH, SECRET_KEY, etc.
|
||||||
|
|
||||||
|
# Start the backend (auto-reloads on change)
|
||||||
|
uvicorn app.main:app --reload --port 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
API docs available at `http://localhost:8000/docs`.
|
||||||
|
|
||||||
|
### 3. Frontend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm install
|
||||||
|
npm run dev # http://localhost:5173
|
||||||
|
```
|
||||||
|
|
||||||
|
Vite proxies `/api` to `localhost:8000` — the backend must be running.
|
||||||
|
|
||||||
|
### 4. Verify tooling
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/verify-tooling.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
homelable/
|
||||||
|
├── frontend/src/
|
||||||
|
│ ├── components/
|
||||||
|
│ │ ├── canvas/ # React Flow canvas, custom nodes & edges
|
||||||
|
│ │ ├── panels/ # Sidebar, detail panel, toolbar
|
||||||
|
│ │ ├── modals/ # Add/edit node, scan config, pending devices
|
||||||
|
│ │ └── ui/ # Shadcn/ui base components
|
||||||
|
│ ├── stores/ # Zustand state (canvas, auth, scan)
|
||||||
|
│ ├── hooks/ # Custom React hooks
|
||||||
|
│ ├── types/ # TypeScript interfaces & enums
|
||||||
|
│ ├── api/ # Axios client & typed endpoints
|
||||||
|
│ └── utils/ # Layout, export, color helpers
|
||||||
|
│
|
||||||
|
├── backend/app/
|
||||||
|
│ ├── api/routes/ # FastAPI route handlers
|
||||||
|
│ ├── services/ # Scanner, status checker, canvas service
|
||||||
|
│ ├── db/ # SQLAlchemy models, Alembic migrations
|
||||||
|
│ ├── schemas/ # Pydantic request/response schemas
|
||||||
|
│ └── core/ # Config, JWT, scheduler
|
||||||
|
│
|
||||||
|
├── docker/ # Nginx configs
|
||||||
|
├── scripts/ # LXC bootstrap, dev helpers
|
||||||
|
└── mcp/ # MCP server (AI integration)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Coding Standards
|
||||||
|
|
||||||
|
### General
|
||||||
|
|
||||||
|
- No untested code merged — every feature or fix must include tests
|
||||||
|
- Keep changes focused — one concern per PR
|
||||||
|
|
||||||
|
### Frontend (TypeScript + React)
|
||||||
|
|
||||||
|
- Strict TypeScript — no `any`, no type assertions unless truly necessary
|
||||||
|
- React Flow node domain fields go in `node.data`, never on the node root
|
||||||
|
- State management via Zustand stores — no prop drilling beyond 2 levels
|
||||||
|
- Styling via TailwindCSS utility classes — follow the existing [design system](#design-system)
|
||||||
|
- Run before committing:
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm run lint
|
||||||
|
npm run typecheck
|
||||||
|
npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backend (Python + FastAPI)
|
||||||
|
|
||||||
|
- Python 3.11+ syntax
|
||||||
|
- Pydantic v2 schemas for all request/response types
|
||||||
|
- SQLAlchemy async sessions — never block the event loop
|
||||||
|
- Scanner logic runs in a background thread — never in an async route directly
|
||||||
|
- All schema changes via Alembic migrations — never modify tables directly
|
||||||
|
- Run before committing:
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
source .venv/bin/activate
|
||||||
|
ruff check .
|
||||||
|
pytest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Design System
|
||||||
|
|
||||||
|
| Token | Value |
|
||||||
|
|---|---|
|
||||||
|
| Background | `#0d1117` |
|
||||||
|
| Surface | `#161b22` |
|
||||||
|
| Card | `#21262d` |
|
||||||
|
| Accent cyan | `#00d4ff` |
|
||||||
|
| Online | `#39d353` |
|
||||||
|
| Offline | `#f85149` |
|
||||||
|
| Pending | `#e3b341` |
|
||||||
|
| Font (UI) | Inter |
|
||||||
|
| Font (IPs/ports) | JetBrains Mono |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Tests run automatically via a pre-commit hook when frontend or backend files are staged.
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm test # run all tests
|
||||||
|
npm run test:coverage # with coverage report
|
||||||
|
```
|
||||||
|
|
||||||
|
Test files live in `__tests__/` next to their module, named `*.test.ts(x)`.
|
||||||
|
|
||||||
|
**What to test:** Zustand store actions, utility functions, non-trivial component logic.
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
source .venv/bin/activate
|
||||||
|
pytest # run all tests
|
||||||
|
pytest -v tests/test_nodes.py # single file
|
||||||
|
```
|
||||||
|
|
||||||
|
Test files live in `backend/tests/test_*.py`.
|
||||||
|
|
||||||
|
**What to test:** all API routes (happy path + error cases), auth flows, service logic.
|
||||||
|
|
||||||
|
Use the `client` and `headers` fixtures from `conftest.py` — they provide an in-memory SQLite database so tests are isolated and fast.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Submitting a Pull Request
|
||||||
|
|
||||||
|
1. **Fork** the repo and create a branch from `main`:
|
||||||
|
```bash
|
||||||
|
git checkout -b feat/my-feature
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Make your changes** — include tests.
|
||||||
|
|
||||||
|
3. **Run the full test suite** (frontend + backend) and make sure everything passes.
|
||||||
|
|
||||||
|
4. **Open a PR** against `main`:
|
||||||
|
- Use a clear title (see commit format below)
|
||||||
|
- Describe what changed and why
|
||||||
|
- Reference any related issues (`Closes #123`)
|
||||||
|
- Include screenshots for UI changes
|
||||||
|
|
||||||
|
5. Keep the PR focused — one feature or fix per PR. Large refactors should be discussed in an issue first.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Commit Message Format
|
||||||
|
|
||||||
|
Follow [Conventional Commits](https://www.conventionalcommits.org/):
|
||||||
|
|
||||||
|
```
|
||||||
|
<type>: <short description>
|
||||||
|
|
||||||
|
[optional body]
|
||||||
|
```
|
||||||
|
|
||||||
|
| Type | When to use |
|
||||||
|
|---|---|
|
||||||
|
| `feat` | New feature |
|
||||||
|
| `fix` | Bug fix |
|
||||||
|
| `docs` | Documentation only |
|
||||||
|
| `refactor` | Code change with no behavior change |
|
||||||
|
| `test` | Adding or fixing tests |
|
||||||
|
| `chore` | Build, deps, tooling |
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```
|
||||||
|
feat: add logout button to sidebar
|
||||||
|
fix: stop click propagation on pending device checkbox
|
||||||
|
docs: add CONTRIBUTING.md
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Questions?
|
||||||
|
|
||||||
|
Open a [GitHub Discussion](https://github.com/Pouzor/homelable/discussions) or drop a comment on a relevant issue.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Remy Jardinet
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.10.1",
|
"version": "1.10.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useState, useCallback, useEffect, useRef } from 'react'
|
import { useState, useCallback, useEffect, useRef } from 'react'
|
||||||
import { Plus, Save, ScanLine, ChevronLeft, ChevronRight, LayoutDashboard, Clock, EyeOff, Trash2, RefreshCw, Loader2, Square, Eye, Settings, StopCircle, X } from 'lucide-react'
|
import { Plus, Save, ScanLine, ChevronLeft, ChevronRight, LayoutDashboard, Clock, EyeOff, Trash2, RefreshCw, Loader2, Square, Eye, Settings, StopCircle, X, LogOut } from 'lucide-react'
|
||||||
import { Logo } from '@/components/ui/Logo'
|
import { Logo } from '@/components/ui/Logo'
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||||
import { useCanvasStore } from '@/stores/canvasStore'
|
import { useCanvasStore } from '@/stores/canvasStore'
|
||||||
|
import { useAuthStore } from '@/stores/authStore'
|
||||||
import { scanApi, settingsApi } from '@/api/client'
|
import { scanApi, settingsApi } from '@/api/client'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { useLatestRelease } from '@/hooks/useLatestRelease'
|
import { useLatestRelease } from '@/hooks/useLatestRelease'
|
||||||
@@ -43,6 +44,7 @@ interface SidebarProps {
|
|||||||
export function Sidebar({ onAddNode, onAddGroupRect, onScan, onSave, onNodeApproved, forceView, highlightPendingId }: SidebarProps) {
|
export function Sidebar({ onAddNode, onAddGroupRect, onScan, onSave, onNodeApproved, forceView, highlightPendingId }: SidebarProps) {
|
||||||
const [_collapsed, setCollapsed] = useState(false)
|
const [_collapsed, setCollapsed] = useState(false)
|
||||||
const [_activeView, setActiveView] = useState<SidebarView>('canvas')
|
const [_activeView, setActiveView] = useState<SidebarView>('canvas')
|
||||||
|
const logout = useAuthStore((s) => s.logout)
|
||||||
|
|
||||||
// When forceView is set, override local state without useEffect
|
// When forceView is set, override local state without useEffect
|
||||||
const collapsed = forceView ? false : _collapsed
|
const collapsed = forceView ? false : _collapsed
|
||||||
@@ -152,6 +154,14 @@ export function Sidebar({ onAddNode, onAddGroupRect, onScan, onSave, onNodeAppro
|
|||||||
onClick={() => setActiveView((v) => v === 'settings' ? 'canvas' : 'settings')}
|
onClick={() => setActiveView((v) => v === 'settings' ? 'canvas' : 'settings')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{!STANDALONE && (
|
||||||
|
<SidebarItem
|
||||||
|
icon={LogOut}
|
||||||
|
label="Logout"
|
||||||
|
collapsed={collapsed}
|
||||||
|
onClick={logout}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!collapsed && <VersionBadge />}
|
{!collapsed && <VersionBadge />}
|
||||||
@@ -385,8 +395,8 @@ function PendingDevicesPanel({ onNodeApproved, highlightId }: { onNodeApproved:
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={checkedIds.has(d.id)}
|
checked={checkedIds.has(d.id)}
|
||||||
onClick={(e) => toggleCheck(d.id, e)}
|
onClick={(e) => e.stopPropagation()}
|
||||||
onChange={() => {}}
|
onChange={(e) => { e.stopPropagation(); toggleCheck(d.id, e as unknown as React.MouseEvent) }}
|
||||||
className="w-3 h-3 accent-[#00d4ff] cursor-pointer shrink-0"
|
className="w-3 h-3 accent-[#00d4ff] cursor-pointer shrink-0"
|
||||||
/>
|
/>
|
||||||
<span className="text-foreground truncate font-medium">{title}</span>
|
<span className="text-foreground truncate font-medium">{title}</span>
|
||||||
@@ -627,7 +637,7 @@ function SettingsPanel() {
|
|||||||
min={10}
|
min={10}
|
||||||
max={3600}
|
max={3600}
|
||||||
value={interval}
|
value={interval}
|
||||||
onChange={(e) => setIntervalValue(Number(e.target.value))}
|
onChange={(e) => { const v = Number(e.target.value); if (!isNaN(v)) setIntervalValue(v) }}
|
||||||
className="w-24 px-2 py-1 rounded-md text-xs font-mono bg-[#0d1117] border border-border text-foreground focus:outline-none focus:border-[#00d4ff]"
|
className="w-24 px-2 py-1 rounded-md text-xs font-mono bg-[#0d1117] border border-border text-foreground focus:outline-none focus:border-[#00d4ff]"
|
||||||
/>
|
/>
|
||||||
<span className="text-xs text-muted-foreground">seconds</span>
|
<span className="text-xs text-muted-foreground">seconds</span>
|
||||||
@@ -664,7 +674,7 @@ function VersionBadge() {
|
|||||||
</a>
|
</a>
|
||||||
{hasUpdate && latest && (
|
{hasUpdate && latest && (
|
||||||
<a
|
<a
|
||||||
href={latest.url}
|
href={latest.url.startsWith('https://') ? latest.url : '#'}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-medium bg-[#e3b341]/15 text-[#e3b341] border border-[#e3b341]/30 hover:bg-[#e3b341]/25 transition-colors self-start"
|
className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-medium bg-[#e3b341]/15 text-[#e3b341] border border-[#e3b341]/30 hover:bg-[#e3b341]/25 transition-colors self-start"
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|||||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
||||||
import { Sidebar } from '../Sidebar'
|
import { Sidebar } from '../Sidebar'
|
||||||
import { useCanvasStore } from '@/stores/canvasStore'
|
import { useCanvasStore } from '@/stores/canvasStore'
|
||||||
|
import { useAuthStore } from '@/stores/authStore'
|
||||||
import type { Node } from '@xyflow/react'
|
import type { Node } from '@xyflow/react'
|
||||||
import type { NodeData } from '@/types'
|
import type { NodeData } from '@/types'
|
||||||
|
|
||||||
// ── Mocks ────────────────────────────────────────────────────────────────────
|
// ── Mocks ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
vi.mock('@/stores/canvasStore')
|
vi.mock('@/stores/canvasStore')
|
||||||
|
vi.mock('@/stores/authStore')
|
||||||
|
|
||||||
const mockBulkApprove = vi.fn()
|
const mockBulkApprove = vi.fn()
|
||||||
const mockBulkHide = vi.fn()
|
const mockBulkHide = vi.fn()
|
||||||
@@ -60,6 +62,7 @@ const makeNode = (id: string, status: NodeData['status'], type: NodeData['type']
|
|||||||
})
|
})
|
||||||
|
|
||||||
const mockToggleHideIp = vi.fn()
|
const mockToggleHideIp = vi.fn()
|
||||||
|
const mockLogout = vi.fn()
|
||||||
|
|
||||||
function mockStore(overrides: Partial<ReturnType<typeof useCanvasStore>> = {}) {
|
function mockStore(overrides: Partial<ReturnType<typeof useCanvasStore>> = {}) {
|
||||||
vi.mocked(useCanvasStore).mockReturnValue({
|
vi.mocked(useCanvasStore).mockReturnValue({
|
||||||
@@ -73,6 +76,12 @@ function mockStore(overrides: Partial<ReturnType<typeof useCanvasStore>> = {}) {
|
|||||||
} as ReturnType<typeof useCanvasStore>)
|
} as ReturnType<typeof useCanvasStore>)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mockAuth() {
|
||||||
|
vi.mocked(useAuthStore).mockImplementation((selector: (s: { logout: () => void }) => unknown) =>
|
||||||
|
selector({ logout: mockLogout }) as ReturnType<typeof useAuthStore>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
onAddNode: vi.fn(),
|
onAddNode: vi.fn(),
|
||||||
onAddGroupRect: vi.fn(),
|
onAddGroupRect: vi.fn(),
|
||||||
@@ -86,6 +95,7 @@ const defaultProps = {
|
|||||||
describe('Sidebar', () => {
|
describe('Sidebar', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockStore()
|
mockStore()
|
||||||
|
mockAuth()
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -267,6 +277,19 @@ describe('Sidebar', () => {
|
|||||||
fireEvent.click(screen.getByRole('button', { name: 'Settings' }))
|
fireEvent.click(screen.getByRole('button', { name: 'Settings' }))
|
||||||
expect(screen.queryByText('Status check interval (s)')).not.toBeInTheDocument()
|
expect(screen.queryByText('Status check interval (s)')).not.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ── Logout ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
it('shows Logout button in normal mode', () => {
|
||||||
|
render(<Sidebar {...defaultProps} />)
|
||||||
|
expect(screen.getByText('Logout')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls logout when Logout is clicked', () => {
|
||||||
|
render(<Sidebar {...defaultProps} />)
|
||||||
|
fireEvent.click(screen.getByText('Logout'))
|
||||||
|
expect(mockLogout).toHaveBeenCalledOnce()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// ── PendingDevicesPanel — bulk select ─────────────────────────────────────────
|
// ── PendingDevicesPanel — bulk select ─────────────────────────────────────────
|
||||||
@@ -298,6 +321,7 @@ const DEVICE_B = {
|
|||||||
describe('PendingDevicesPanel — bulk select', () => {
|
describe('PendingDevicesPanel — bulk select', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockStore()
|
mockStore()
|
||||||
|
mockAuth()
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
mockBulkApprove.mockResolvedValue({
|
mockBulkApprove.mockResolvedValue({
|
||||||
data: { approved: 2, node_ids: ['n1', 'n2'], device_ids: ['dev-a', 'dev-b'], skipped: 0 },
|
data: { approved: 2, node_ids: ['n1', 'n2'], device_ids: ['dev-a', 'dev-b'], skipped: 0 },
|
||||||
|
|||||||
Reference in New Issue
Block a user