Show BanGUI app version in sidebar, fix version tooltips
- Inject __APP_VERSION__ at build time via vite.config.ts define (reads
frontend/package.json#version); declare the global in vite-env.d.ts.
- Render 'BanGUI v{__APP_VERSION__}' in the sidebar footer (MainLayout)
when expanded; hidden when collapsed.
- Rename fail2ban version tooltip to 'fail2ban daemon version' in
ServerStatusBar so it is visually distinct from the app version.
- Sync frontend/package.json version (0.9.0 → 0.9.3) to match
Docker/VERSION; update release.sh to keep them in sync on every bump.
- Add vitest define stub for __APP_VERSION__ so tests compile cleanly.
- Add ServerStatusBar and MainLayout test suites (10 new test cases).
This commit is contained in:
@@ -62,6 +62,12 @@ fi
|
|||||||
echo "${NEW_TAG}" > "${VERSION_FILE}"
|
echo "${NEW_TAG}" > "${VERSION_FILE}"
|
||||||
echo "Version file updated → ${VERSION_FILE}"
|
echo "Version file updated → ${VERSION_FILE}"
|
||||||
|
|
||||||
|
# Keep frontend/package.json in sync so __APP_VERSION__ matches Docker/VERSION.
|
||||||
|
FRONT_VERSION="${NEW_TAG#v}"
|
||||||
|
FRONT_PKG="${SCRIPT_DIR}/../frontend/package.json"
|
||||||
|
sed -i "s/\"version\": \"[^\"]*\"/\"version\": \"${FRONT_VERSION}\"/" "${FRONT_PKG}"
|
||||||
|
echo "frontend/package.json version updated → ${FRONT_VERSION}"
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Push
|
# Push
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -4,82 +4,52 @@ This document breaks the entire BanGUI project into development stages, ordered
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Task: Ensure Required fail2ban Jail Config Files Exist ✅ DONE
|
## Open Issues
|
||||||
|
|
||||||
**Implemented:** `backend/app/utils/jail_config.py` — `ensure_jail_configs(jail_d_path)` creates missing `manual-Jail.conf`, `manual-Jail.local`, `blocklist-import.conf`, and `blocklist-import.local` files with correct default content. Called from the lifespan hook in `main.py` using `Path(settings.fail2ban_config_dir) / "jail.d"`. Tests in `backend/tests/test_utils/test_jail_config.py` (6 cases: all missing, all present, only locals missing, directory creation, idempotency, correct content).
|
### ~~1. Dashboard — Version Tag Mismatch~~ ✅ Done
|
||||||
|
|
||||||
The backend must guarantee that two specific jail configuration files are present inside the fail2ban jail directory before the application starts (or on first use). If either file is missing it must be created with the correct default content. The files live inside the directory that fail2ban uses for per-jail drop-in configs (e.g. `jail.d/`). The path to that directory should be read from the application settings (config key `fail2ban_jail_d_path` or equivalent); do not hard-code it.
|
**Implemented:**
|
||||||
|
- `frontend/vite.config.ts`: reads `package.json#version` at build time and injects it as the global `__APP_VERSION__` via Vite `define`.
|
||||||
|
- `frontend/src/vite-env.d.ts`: adds `declare const __APP_VERSION__: string` so TypeScript knows about the global.
|
||||||
|
- `frontend/src/layouts/MainLayout.tsx`: renders `BanGUI v{__APP_VERSION__}` in the sidebar footer when expanded (hidden when collapsed).
|
||||||
|
- `frontend/src/components/ServerStatusBar.tsx`: tooltip changed from `"fail2ban version"` to `"fail2ban daemon version"`.
|
||||||
|
- `Docker/release.sh`: after bumping `VERSION`, also updates `frontend/package.json#version` via `sed` to keep them in sync.
|
||||||
|
- `frontend/package.json`: version bumped from `0.9.0` to `0.9.3` to match `Docker/VERSION`.
|
||||||
|
- Tests added: `src/components/__tests__/ServerStatusBar.test.tsx`, `src/layouts/__tests__/MainLayout.test.tsx`.
|
||||||
|
|
||||||
### Files to create if missing
|
**Problem:** The `ServerStatusBar` component on the Dashboard displays `v{status.version}`, which is the **fail2ban daemon version** (e.g. `v1.1.0`). The BanGUI application version lives in `Docker/VERSION` (e.g. `v0.9.3`) and is unrelated to the fail2ban version. Users see a version number they don't recognise and assume it reflects the BanGUI release.
|
||||||
|
|
||||||
**`manual-Jail.conf`**
|
**Goal:** Make the distinction clear and expose the BanGUI application version.
|
||||||
The file must contain a `[manual-Jail]` section with `enabled = false` and all other jail parameters (filter, logpath, backend, maxretry, findtime, bantime, ignoreip) set to the same defaults already documented in `Docker/fail2ban-dev-config/fail2ban/jail.d/manual-Jail.conf`. Only `enabled` must be forced to `false` in this template — it is the `.local` override (see below) that activates the jail.
|
|
||||||
|
|
||||||
**`blocklist-import.conf`**
|
**Suggested approach:**
|
||||||
The file must contain a `[blocklist-import]` section with `enabled = false` and all other jail parameters (filter, logpath, backend, maxretry, findtime, bantime, ignoreip) set to the same defaults already documented in `Docker/fail2ban-dev-config/fail2ban/jail.d/blocklist-import.conf`. Same rule: `enabled = false` here; the `.local` file enables it.
|
1. Inject the BanGUI app version at build time — add a `define` entry in `frontend/vite.config.ts` that reads the `version` field from `frontend/package.json` (e.g. `__APP_VERSION__`). Keep `frontend/package.json` and `Docker/VERSION` in sync (update the release script `Docker/release.sh` or `Makefile` to write `package.json#version` from `VERSION`).
|
||||||
|
2. Show the BanGUI version in the sidebar footer inside `MainLayout.tsx` (collapsed view: show only when expanded, or via tooltip). This is the natural place for an "about" version tag.
|
||||||
|
3. Update the fail2ban version tooltip in `ServerStatusBar.tsx` from the generic `"fail2ban version"` to something like `"fail2ban daemon version"` so the two are no longer visually indistinguishable.
|
||||||
|
|
||||||
### Local override files
|
**Files:** `frontend/vite.config.ts`, `frontend/package.json`, `Docker/VERSION`, `Docker/release.sh`, `frontend/src/layouts/MainLayout.tsx`, `frontend/src/components/ServerStatusBar.tsx`.
|
||||||
|
|
||||||
For each `.conf` file above there must also be a corresponding `.local` file checked — and created if missing. The `.local` files must contain **only** the section header and the single `enabled = true` line. Nothing else. fail2ban merges `.local` on top of `.conf` at startup, so all other settings come from the `.conf`.
|
|
||||||
|
|
||||||
```
|
|
||||||
[manual-Jail]
|
|
||||||
enabled = true
|
|
||||||
```
|
|
||||||
|
|
||||||
```
|
|
||||||
[blocklist-import]
|
|
||||||
enabled = true
|
|
||||||
```
|
|
||||||
|
|
||||||
### Implementation notes
|
|
||||||
|
|
||||||
- Perform the check in a backend startup routine (e.g. in a `lifespan` hook or a dedicated `ensure_jail_configs()` function called from `main.py`).
|
|
||||||
- Only create a file if it does **not** already exist. Never overwrite an existing file.
|
|
||||||
- Log an `INFO` message for each file that is created and a `DEBUG` message when a file already exists.
|
|
||||||
- Add unit tests that exercise: (a) all four files missing → all four created with correct content, (b) all four files present → nothing is overwritten, (c) only the `.local` files missing → only they are created.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Task: World Map — Country Tooltip on Hover
|
### 2. Dashboard — Improve "Failures" Tooltip
|
||||||
|
|
||||||
Currently the world map (`WorldMap.tsx`) shows a ban-count label painted directly onto each country's SVG path and reacts to click events. Add a floating tooltip that appears when the user hovers over a country.
|
**Problem:** The `ServerStatusBar` shows a "Failures: 42" counter with the tooltip `"Currently failing IPs"`. In fail2ban terminology *failures* are individual **failed authentication attempts** tracked in the fail2ban DB, not the number of unique IPs that failed. The current wording is ambiguous and misleading — users may think it means broken connections or error states.
|
||||||
|
|
||||||
### Required behaviour
|
**Goal:** Replace the tooltip with accurate, self-explanatory wording.
|
||||||
|
|
||||||
- When the mouse enters a country geography, display a small floating tooltip near the cursor that shows:
|
**Suggested fix:** Change the `Tooltip` content for the Failures stat in `ServerStatusBar.tsx` from `"Currently failing IPs"` to something like `"Total failed authentication attempts currently tracked by fail2ban across all active jails"`. Additionally, consider renaming the label from `"Failures:"` to `"Failed Attempts:"` to match the tooltip language.
|
||||||
- The country's full name (already available via `country_names` from `useMapData()`).
|
|
||||||
- The ban count for that country (from the `countries` map; show `0` if the country has no entry).
|
|
||||||
- The tooltip must follow the mouse while inside the country (or at minimum appear near the cursor when it first enters).
|
|
||||||
- When the mouse leaves the country the tooltip must disappear.
|
|
||||||
- Countries with zero bans must also show the tooltip (name + "0 bans").
|
|
||||||
|
|
||||||
### Implementation notes
|
**Files:** `frontend/src/components/ServerStatusBar.tsx`.
|
||||||
|
|
||||||
- Store tooltip state (visible, content, x, y) in a `useState` hook local to `WorldMap.tsx`.
|
|
||||||
- Use `onMouseEnter`, `onMouseMove`, and `onMouseLeave` props on the `<Geography>` element (react-simple-maps already forwards these as standard SVG mouse events).
|
|
||||||
- Render the tooltip as an absolutely-positioned `<div>` overlaid on the map container. Apply a `pointer-events: none` style so it does not interfere with hover detection on the map itself.
|
|
||||||
- Reuse the existing Fluent UI design tokens (background, border, shadow, typography) so the tooltip matches the rest of the UI. Do not introduce a new third-party tooltip library.
|
|
||||||
- Add a Vitest / React Testing Library test that mounts `WorldMap` with mock data, fires a `mouseenter` event on a geography, and asserts the tooltip text is visible.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Task: Main Menu — Tooltips on Navigation Items
|
### 3. Config → Server Tab — Move "Service Health" to Top
|
||||||
|
|
||||||
The main navigation sidebar (or top bar, whichever is used) currently has no tooltips. Add a tooltip to each navigation item so that users who are unfamiliar with icon-only or collapsed menus can see the destination name without navigating.
|
**Problem:** In the Config page → Server tab, the `Service Health` panel (`ServerHealthSection`) is rendered at the bottom of the tab, after all settings sections (log level, log target, DB purge settings, map thresholds, reload/restart buttons). This means users must scroll past all editable fields to check service connectivity status, even though the health status is the most critical piece of context — it indicates whether the server is reachable at all.
|
||||||
|
|
||||||
### Required behaviour
|
**Goal:** Move the `<ServerHealthSection />` block to the **top** of the `ServerTab` render output, before any settings fields.
|
||||||
|
|
||||||
- Each navigation item must show a tooltip containing the item's label (e.g. "Dashboard", "Map", "Blocklist", "Settings") when the user hovers over it.
|
**Suggested fix:** In `frontend/src/components/config/ServerTab.tsx`, move the `{/* Service Health & Log Viewer section */}` block (currently at the end of the JSX return around line 415) to be the first section rendered inside the tab container.
|
||||||
- Tooltips should appear after a short delay (≈ 300 ms) to avoid flickering during fast cursor movement past the menu.
|
|
||||||
- The tooltip must be dismissed when the cursor leaves the item.
|
|
||||||
- If the menu is already showing a full text label next to the icon, the tooltip is still added (it reinforces accessibility); but consider hiding it when the sidebar is expanded and the label is already visible, to avoid redundancy.
|
|
||||||
|
|
||||||
### Implementation notes
|
**Files:** `frontend/src/components/config/ServerTab.tsx`.
|
||||||
|
|
||||||
- Use the Fluent UI `<Tooltip>` component (from `@fluentui/react-components`) which is already a project dependency. Wrap each navigation `<NavLink>` (or equivalent element) with `<Tooltip content="…" relationship="label">`.
|
|
||||||
- Keep the tooltip content string co-located with the route definition so that if a label changes in one place it changes everywhere.
|
|
||||||
- Do not introduce any new npm dependencies.
|
|
||||||
- Add a Vitest / React Testing Library test that renders the navigation component, triggers a hover on each item, and asserts the correct tooltip text is present in the DOM.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "bangui-frontend",
|
"name": "bangui-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.9.0",
|
"version": "0.9.3",
|
||||||
"description": "BanGUI frontend — fail2ban web management interface",
|
"description": "BanGUI frontend — fail2ban web management interface",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ export function ServerStatusBar(): React.JSX.Element {
|
|||||||
{/* Version */}
|
{/* Version */}
|
||||||
{/* ---------------------------------------------------------------- */}
|
{/* ---------------------------------------------------------------- */}
|
||||||
{status?.version != null && (
|
{status?.version != null && (
|
||||||
<Tooltip content="fail2ban version" relationship="description">
|
<Tooltip content="fail2ban daemon version" relationship="description">
|
||||||
<Text size={200} className={styles.statValue}>
|
<Text size={200} className={styles.statValue}>
|
||||||
v{status.version}
|
v{status.version}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
151
frontend/src/components/__tests__/ServerStatusBar.test.tsx
Normal file
151
frontend/src/components/__tests__/ServerStatusBar.test.tsx
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
/**
|
||||||
|
* Tests for the ServerStatusBar component.
|
||||||
|
*
|
||||||
|
* Covers loading state, online / offline rendering, and correct tooltip
|
||||||
|
* wording that distinguishes the fail2ban daemon version from the BanGUI
|
||||||
|
* application version.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import { FluentProvider, webLightTheme } from "@fluentui/react-components";
|
||||||
|
import { ServerStatusBar } from "../ServerStatusBar";
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Mock useServerStatus so tests never touch the network.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
vi.mock("../../hooks/useServerStatus");
|
||||||
|
|
||||||
|
import { useServerStatus } from "../../hooks/useServerStatus";
|
||||||
|
|
||||||
|
const mockedUseServerStatus = vi.mocked(useServerStatus);
|
||||||
|
|
||||||
|
function renderBar(): void {
|
||||||
|
render(
|
||||||
|
<FluentProvider theme={webLightTheme}>
|
||||||
|
<ServerStatusBar />
|
||||||
|
</FluentProvider>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe("ServerStatusBar", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows a spinner while the initial load is in progress", () => {
|
||||||
|
mockedUseServerStatus.mockReturnValue({
|
||||||
|
status: null,
|
||||||
|
loading: true,
|
||||||
|
error: null,
|
||||||
|
refresh: vi.fn(),
|
||||||
|
});
|
||||||
|
renderBar();
|
||||||
|
// The status-area spinner is labelled "Checking\u2026".
|
||||||
|
expect(screen.getByText("Checking\u2026")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders an Online badge when the server is reachable", () => {
|
||||||
|
mockedUseServerStatus.mockReturnValue({
|
||||||
|
status: {
|
||||||
|
online: true,
|
||||||
|
version: "1.1.0",
|
||||||
|
active_jails: 3,
|
||||||
|
total_bans: 10,
|
||||||
|
total_failures: 5,
|
||||||
|
},
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
refresh: vi.fn(),
|
||||||
|
});
|
||||||
|
renderBar();
|
||||||
|
expect(screen.getByText("Online")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders an Offline badge when the server is unreachable", () => {
|
||||||
|
mockedUseServerStatus.mockReturnValue({
|
||||||
|
status: {
|
||||||
|
online: false,
|
||||||
|
version: null,
|
||||||
|
active_jails: 0,
|
||||||
|
total_bans: 0,
|
||||||
|
total_failures: 0,
|
||||||
|
},
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
refresh: vi.fn(),
|
||||||
|
});
|
||||||
|
renderBar();
|
||||||
|
expect(screen.getByText("Offline")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("displays the daemon version string when available", () => {
|
||||||
|
mockedUseServerStatus.mockReturnValue({
|
||||||
|
status: {
|
||||||
|
online: true,
|
||||||
|
version: "1.2.3",
|
||||||
|
active_jails: 1,
|
||||||
|
total_bans: 0,
|
||||||
|
total_failures: 0,
|
||||||
|
},
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
refresh: vi.fn(),
|
||||||
|
});
|
||||||
|
renderBar();
|
||||||
|
expect(screen.getByText("v1.2.3")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render the version element when version is null", () => {
|
||||||
|
mockedUseServerStatus.mockReturnValue({
|
||||||
|
status: {
|
||||||
|
online: false,
|
||||||
|
version: null,
|
||||||
|
active_jails: 0,
|
||||||
|
total_bans: 0,
|
||||||
|
total_failures: 0,
|
||||||
|
},
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
refresh: vi.fn(),
|
||||||
|
});
|
||||||
|
renderBar();
|
||||||
|
// No version string should appear in the document.
|
||||||
|
expect(screen.queryByText(/^v\d/)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows jail / ban / failure counts when the server is online", () => {
|
||||||
|
mockedUseServerStatus.mockReturnValue({
|
||||||
|
status: {
|
||||||
|
online: true,
|
||||||
|
version: "1.0.0",
|
||||||
|
active_jails: 4,
|
||||||
|
total_bans: 21,
|
||||||
|
total_failures: 99,
|
||||||
|
},
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
refresh: vi.fn(),
|
||||||
|
});
|
||||||
|
renderBar();
|
||||||
|
expect(screen.getByText("4")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("21")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("99")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders an error message when the status fetch fails", () => {
|
||||||
|
mockedUseServerStatus.mockReturnValue({
|
||||||
|
status: null,
|
||||||
|
loading: false,
|
||||||
|
error: "Network error",
|
||||||
|
refresh: vi.fn(),
|
||||||
|
});
|
||||||
|
renderBar();
|
||||||
|
expect(screen.getByText("Network error")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -145,6 +145,16 @@ const useStyles = makeStyles({
|
|||||||
padding: tokens.spacingVerticalS,
|
padding: tokens.spacingVerticalS,
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
},
|
},
|
||||||
|
versionText: {
|
||||||
|
display: "block",
|
||||||
|
color: tokens.colorNeutralForeground4,
|
||||||
|
fontSize: "11px",
|
||||||
|
paddingLeft: tokens.spacingHorizontalS,
|
||||||
|
paddingRight: tokens.spacingHorizontalS,
|
||||||
|
paddingBottom: tokens.spacingVerticalXS,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
overflow: "hidden",
|
||||||
|
},
|
||||||
|
|
||||||
// Main content
|
// Main content
|
||||||
main: {
|
main: {
|
||||||
@@ -301,6 +311,11 @@ export function MainLayout(): React.JSX.Element {
|
|||||||
|
|
||||||
{/* Footer — Logout */}
|
{/* Footer — Logout */}
|
||||||
<div className={styles.sidebarFooter}>
|
<div className={styles.sidebarFooter}>
|
||||||
|
{!collapsed && (
|
||||||
|
<Text className={styles.versionText}>
|
||||||
|
BanGUI v{__APP_VERSION__}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={collapsed ? "Sign out" : ""}
|
content={collapsed ? "Sign out" : ""}
|
||||||
relationship="label"
|
relationship="label"
|
||||||
|
|||||||
78
frontend/src/layouts/__tests__/MainLayout.test.tsx
Normal file
78
frontend/src/layouts/__tests__/MainLayout.test.tsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
/**
|
||||||
|
* Tests for the MainLayout component.
|
||||||
|
*
|
||||||
|
* Covers:
|
||||||
|
* - BanGUI application version displayed in the footer when the sidebar is expanded.
|
||||||
|
* - Version text hidden when the sidebar is collapsed.
|
||||||
|
* - Navigation items rendered correctly.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import { MemoryRouter } from "react-router-dom";
|
||||||
|
import { FluentProvider, webLightTheme } from "@fluentui/react-components";
|
||||||
|
import { MainLayout } from "../../layouts/MainLayout";
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Mocks
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
vi.mock("../../providers/AuthProvider", () => ({
|
||||||
|
useAuth: () => ({ logout: vi.fn() }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../../hooks/useServerStatus", () => ({
|
||||||
|
useServerStatus: () => ({
|
||||||
|
status: { online: true, version: "1.0.0", active_jails: 1, total_bans: 0, total_failures: 0 },
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
refresh: vi.fn(),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../../hooks/useBlocklist", () => ({
|
||||||
|
useBlocklistStatus: () => ({ hasErrors: false }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function renderLayout(): void {
|
||||||
|
render(
|
||||||
|
<FluentProvider theme={webLightTheme}>
|
||||||
|
<MemoryRouter initialEntries={["/"]}>
|
||||||
|
<MainLayout />
|
||||||
|
</MemoryRouter>
|
||||||
|
</FluentProvider>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe("MainLayout", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders the navigation sidebar", () => {
|
||||||
|
renderLayout();
|
||||||
|
expect(screen.getByRole("navigation", { name: "Main navigation" })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows the BanGUI version in the sidebar footer when expanded", () => {
|
||||||
|
renderLayout();
|
||||||
|
// __APP_VERSION__ is stubbed to "0.0.0-test" via vitest.config.ts define.
|
||||||
|
expect(screen.getByText("BanGUI v0.0.0-test")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hides the BanGUI version text when the sidebar is collapsed", async () => {
|
||||||
|
renderLayout();
|
||||||
|
const toggleButton = screen.getByRole("button", { name: /collapse sidebar/i });
|
||||||
|
await userEvent.click(toggleButton);
|
||||||
|
expect(screen.queryByText("BanGUI v0.0.0-test")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
3
frontend/src/vite-env.d.ts
vendored
3
frontend/src/vite-env.d.ts
vendored
@@ -7,3 +7,6 @@ interface ImportMetaEnv {
|
|||||||
interface ImportMeta {
|
interface ImportMeta {
|
||||||
readonly env: ImportMetaEnv;
|
readonly env: ImportMetaEnv;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** BanGUI application version — injected at build time via Vite define. */
|
||||||
|
declare const __APP_VERSION__: string;
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
import { resolve } from "path";
|
import { resolve } from "path";
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
|
||||||
|
const pkg = JSON.parse(
|
||||||
|
readFileSync(resolve(__dirname, "package.json"), "utf-8"),
|
||||||
|
) as { version: string };
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
|
define: {
|
||||||
|
/** BanGUI application version injected at build time from package.json. */
|
||||||
|
__APP_VERSION__: JSON.stringify(pkg.version),
|
||||||
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
"@": resolve(__dirname, "src"),
|
"@": resolve(__dirname, "src"),
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import { resolve } from "path";
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
|
define: {
|
||||||
|
/** Stub app version for tests — mirrors the vite.config.ts define. */
|
||||||
|
__APP_VERSION__: JSON.stringify("0.0.0-test"),
|
||||||
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
"@": resolve(__dirname, "src"),
|
"@": resolve(__dirname, "src"),
|
||||||
|
|||||||
Reference in New Issue
Block a user