feat(web): wire TickerStrip + status bar role into DashboardLayout (TEC-3047)
- Import TickerStrip vào dashboard layout, truyền vào DashboardLayout.ticker - Thêm placeholder top-8 quận với TODO comment chờ /analytics/districts API - Thêm role="status" aria-live="polite" vào status bar div trong DashboardLayout - 8 Vitest unit tests cho DashboardLayout: role=banner, role=status, ticker, sidebar collapse/expand width, main content (tất cả pass) Note: listings.spec.tsx failure là pre-existing trên HEAD, không liên quan TEC-3047. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
110
apps/web/app/[locale]/(dashboard)/__tests__/layout.spec.tsx
Normal file
110
apps/web/app/[locale]/(dashboard)/__tests__/layout.spec.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
/* eslint-disable import-x/order */
|
||||
/**
|
||||
* Kiểm thử layout dashboard: header role=banner, status bar role=status,
|
||||
* ticker hiển thị, sidebar và main content.
|
||||
*/
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import * as React from 'react';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { DashboardLayout } from '@/components/design-system/dashboard-layout';
|
||||
|
||||
describe('DashboardLayout', () => {
|
||||
it('renders header với role=banner', () => {
|
||||
render(
|
||||
<DashboardLayout
|
||||
header={<header role="banner">Header</header>}
|
||||
statusBar={<span>Online</span>}
|
||||
>
|
||||
<div>Content</div>
|
||||
</DashboardLayout>,
|
||||
);
|
||||
expect(screen.getByRole('banner')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders status bar với role=status', () => {
|
||||
render(
|
||||
<DashboardLayout
|
||||
header={<header role="banner">Header</header>}
|
||||
statusBar={<span>Đã kết nối</span>}
|
||||
>
|
||||
<div>Content</div>
|
||||
</DashboardLayout>,
|
||||
);
|
||||
expect(screen.getByRole('status')).toBeInTheDocument();
|
||||
expect(screen.getByRole('status')).toHaveTextContent('Đã kết nối');
|
||||
});
|
||||
|
||||
it('renders ticker strip khi được truyền prop ticker', () => {
|
||||
render(
|
||||
<DashboardLayout
|
||||
ticker={<div data-testid="ticker">Ticker</div>}
|
||||
header={<header role="banner">Header</header>}
|
||||
>
|
||||
<div>Content</div>
|
||||
</DashboardLayout>,
|
||||
);
|
||||
expect(screen.getByTestId('ticker')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('không render ticker khi không truyền prop', () => {
|
||||
render(
|
||||
<DashboardLayout header={<header role="banner">Header</header>}>
|
||||
<div>Content</div>
|
||||
</DashboardLayout>,
|
||||
);
|
||||
// Không có vùng ticker
|
||||
expect(screen.queryByTestId('ticker')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders sidebar khi được truyền prop', () => {
|
||||
render(
|
||||
<DashboardLayout
|
||||
sidebar={<nav aria-label="sidebar-nav">Sidebar</nav>}
|
||||
header={<header role="banner">Header</header>}
|
||||
>
|
||||
<div>Nội dung chính</div>
|
||||
</DashboardLayout>,
|
||||
);
|
||||
expect(screen.getByRole('navigation', { name: 'sidebar-nav' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders children trong main content', () => {
|
||||
render(
|
||||
<DashboardLayout header={<header role="banner">Header</header>}>
|
||||
<p>Nội dung con</p>
|
||||
</DashboardLayout>,
|
||||
);
|
||||
expect(screen.getByText('Nội dung con')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('sidebar collapsed có width 56px', () => {
|
||||
const { container } = render(
|
||||
<DashboardLayout
|
||||
sidebar={<nav>Nav</nav>}
|
||||
sidebarCollapsed
|
||||
header={<header role="banner">Header</header>}
|
||||
>
|
||||
<div>Content</div>
|
||||
</DashboardLayout>,
|
||||
);
|
||||
// aside phải có inline style width: 56px
|
||||
const aside = container.querySelector('aside');
|
||||
expect(aside).toHaveStyle({ width: '56px' });
|
||||
});
|
||||
|
||||
it('sidebar expanded sử dụng sidebarWidth prop', () => {
|
||||
const { container } = render(
|
||||
<DashboardLayout
|
||||
sidebar={<nav>Nav</nav>}
|
||||
sidebarCollapsed={false}
|
||||
sidebarWidth={240}
|
||||
header={<header role="banner">Header</header>}
|
||||
>
|
||||
<div>Content</div>
|
||||
</DashboardLayout>,
|
||||
);
|
||||
const aside = container.querySelector('aside');
|
||||
expect(aside).toHaveStyle({ width: '240px' });
|
||||
});
|
||||
});
|
||||
@@ -27,6 +27,8 @@ import { useTranslations } from 'next-intl';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { DashboardLayout } from '@/components/design-system/dashboard-layout';
|
||||
import { CompactHeader } from '@/components/design-system/compact-header';
|
||||
import { TickerStrip } from '@/components/design-system/ticker-strip';
|
||||
import type { TickerItem } from '@/components/design-system/ticker-strip';
|
||||
import { NotificationBell } from '@/components/notifications/notification-bell';
|
||||
import { useTheme } from '@/components/providers/theme-provider';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -296,6 +298,20 @@ export default function AppDashboardLayout({ children }: { children: React.React
|
||||
/>
|
||||
);
|
||||
|
||||
// ── Ticker strip (top 8 quận, placeholder → TODO: /analytics/districts) ───
|
||||
// TODO: thay thế bằng dữ liệu thực từ /analytics/districts khi API sẵn sàng (TEC-3047)
|
||||
const tickerItems: TickerItem[] = [
|
||||
{ id: 'q1', label: 'Quận 1', changePercent: 2.4, direction: 'up' },
|
||||
{ id: 'q2', label: 'Quận 2', changePercent: -0.8, direction: 'down' },
|
||||
{ id: 'q3', label: 'Quận 3', changePercent: 1.1, direction: 'up' },
|
||||
{ id: 'q7', label: 'Quận 7', changePercent: 3.2, direction: 'up' },
|
||||
{ id: 'binhthanh', label: 'Bình Thạnh', changePercent: 0.0, direction: 'neutral' },
|
||||
{ id: 'thuduc', label: 'Thủ Đức', changePercent: 1.7, direction: 'up' },
|
||||
{ id: 'tanbinh', label: 'Tân Bình', changePercent: -1.3, direction: 'down' },
|
||||
{ id: 'phuninh', label: 'Phú Nhuận', changePercent: 0.5, direction: 'up' },
|
||||
];
|
||||
const ticker = <TickerStrip items={tickerItems} />;
|
||||
|
||||
// ── Status bar ───────────────────────────────────────────────────────────
|
||||
const statusBar = (
|
||||
<>
|
||||
@@ -315,6 +331,7 @@ export default function AppDashboardLayout({ children }: { children: React.React
|
||||
<DashboardLayout
|
||||
sidebar={sidebar}
|
||||
header={header}
|
||||
ticker={ticker}
|
||||
statusBar={statusBar}
|
||||
sidebarCollapsed
|
||||
>
|
||||
|
||||
@@ -65,7 +65,11 @@ export function DashboardLayout({
|
||||
</div>
|
||||
</div>
|
||||
{statusBar ? (
|
||||
<div className="flex h-6 items-center gap-4 border-t border-border bg-background-elevated px-4 text-[11px] text-foreground-muted">
|
||||
<div
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
className="flex h-6 items-center gap-4 border-t border-border bg-background-elevated px-4 text-[11px] text-foreground-muted"
|
||||
>
|
||||
{statusBar}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
Reference in New Issue
Block a user