From d6d7584677a2eba109b39a5e04ef9efffb41c94f Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Tue, 21 Apr 2026 01:47:25 +0700 Subject: [PATCH] feat(web): wire TickerStrip + status bar role into DashboardLayout (TEC-3047) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../(dashboard)/__tests__/layout.spec.tsx | 110 ++++++++++++++++++ apps/web/app/[locale]/(dashboard)/layout.tsx | 17 +++ .../design-system/dashboard-layout.tsx | 6 +- 3 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 apps/web/app/[locale]/(dashboard)/__tests__/layout.spec.tsx diff --git a/apps/web/app/[locale]/(dashboard)/__tests__/layout.spec.tsx b/apps/web/app/[locale]/(dashboard)/__tests__/layout.spec.tsx new file mode 100644 index 0000000..9847d81 --- /dev/null +++ b/apps/web/app/[locale]/(dashboard)/__tests__/layout.spec.tsx @@ -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( + Header} + statusBar={Online} + > +
Content
+
, + ); + expect(screen.getByRole('banner')).toBeInTheDocument(); + }); + + it('renders status bar với role=status', () => { + render( + Header} + statusBar={Đã kết nối} + > +
Content
+
, + ); + 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( + Ticker} + header={
Header
} + > +
Content
+
, + ); + expect(screen.getByTestId('ticker')).toBeInTheDocument(); + }); + + it('không render ticker khi không truyền prop', () => { + render( + Header}> +
Content
+
, + ); + // Không có vùng ticker + expect(screen.queryByTestId('ticker')).not.toBeInTheDocument(); + }); + + it('renders sidebar khi được truyền prop', () => { + render( + Sidebar} + header={
Header
} + > +
Nội dung chính
+
, + ); + expect(screen.getByRole('navigation', { name: 'sidebar-nav' })).toBeInTheDocument(); + }); + + it('renders children trong main content', () => { + render( + Header}> +

Nội dung con

+
, + ); + expect(screen.getByText('Nội dung con')).toBeInTheDocument(); + }); + + it('sidebar collapsed có width 56px', () => { + const { container } = render( + Nav} + sidebarCollapsed + header={
Header
} + > +
Content
+
, + ); + // 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( + Nav} + sidebarCollapsed={false} + sidebarWidth={240} + header={
Header
} + > +
Content
+
, + ); + const aside = container.querySelector('aside'); + expect(aside).toHaveStyle({ width: '240px' }); + }); +}); diff --git a/apps/web/app/[locale]/(dashboard)/layout.tsx b/apps/web/app/[locale]/(dashboard)/layout.tsx index 987c126..ebe6be9 100644 --- a/apps/web/app/[locale]/(dashboard)/layout.tsx +++ b/apps/web/app/[locale]/(dashboard)/layout.tsx @@ -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 = ; + // ── Status bar ─────────────────────────────────────────────────────────── const statusBar = ( <> @@ -315,6 +331,7 @@ export default function AppDashboardLayout({ children }: { children: React.React diff --git a/apps/web/components/design-system/dashboard-layout.tsx b/apps/web/components/design-system/dashboard-layout.tsx index 1722aa5..be998ee 100644 --- a/apps/web/components/design-system/dashboard-layout.tsx +++ b/apps/web/components/design-system/dashboard-layout.tsx @@ -65,7 +65,11 @@ export function DashboardLayout({ {statusBar ? ( -
+
{statusBar}
) : null}