diff --git a/components/data-grids/certificates-data-grid.tsx b/components/data-grids/certificates-data-grid.tsx index 71247363..0bbe664b 100644 --- a/components/data-grids/certificates-data-grid.tsx +++ b/components/data-grids/certificates-data-grid.tsx @@ -18,6 +18,7 @@ import { DATA_GRID_CELL_PAD, DATA_GRID_CELL_PAD_FIRST, DATA_GRID_CELL_PAD_LAST, + DEFAULT_DATA_GRID_LAYOUT, } from "@/components/data-grids/shared/data-grid-layout" import { DataGridSortHeader } from "@/components/data-grids/shared/data-grid-sort-header" import { @@ -79,6 +80,7 @@ function CertificatesDataGrid({ certificates, serverMap, isLoading, emptyTitle = { id: "name", accessorKey: "name", + size: 160, header: ({ column }) => ( ), @@ -97,7 +99,7 @@ function CertificatesDataGrid({ certificates, serverMap, isLoading, emptyTitle =
- {cfg.icon} + {cfg.icon} {cert.name} @@ -196,6 +198,7 @@ function CertificatesDataGrid({ certificates, serverMap, isLoading, emptyTitle = }, { id: "status", + size: 120, accessorKey: "status", header: ({ column }) => , cell: ({ row }) => { @@ -226,7 +229,7 @@ function CertificatesDataGrid({ certificates, serverMap, isLoading, emptyTitle = manualPagination: false, autoResetPageIndex: false, autoResetExpanded: false, - initialState: { pagination: { pageIndex: 0, pageSize: 50 } }, + initialState: { pagination: { pageIndex: 0, pageSize: 50 }, columnPinning: { start: ["name"], end: [] } }, data: certificates, columns, getRowId: (row) => row.id, @@ -254,6 +257,7 @@ function CertificatesDataGrid({ certificates, serverMap, isLoading, emptyTitle = 50 || pageSize !== 50} isLoading={isLoading} loadingMode="skeleton" diff --git a/components/data-grids/compact-data-grid.tsx b/components/data-grids/compact-data-grid.tsx index 27241f0d..e3ebc7cd 100644 --- a/components/data-grids/compact-data-grid.tsx +++ b/components/data-grids/compact-data-grid.tsx @@ -2,7 +2,7 @@ import { DataGridContainer, DataGridScrollArea, DataGridTable, dataGridFeatures, type DataGridFeatures } from "@/components/reui/data-grid" -import { useMemo, useEffect, type ReactNode } from "react" +import { useMemo, useEffect, type ReactNode, type CSSProperties } from "react" import { type ColumnDef, useTable, @@ -12,6 +12,7 @@ import { DATA_GRID_CELL_PAD, DATA_GRID_CELL_PAD_FIRST, DATA_GRID_CELL_PAD_LAST, + DEFAULT_DATA_GRID_LAYOUT, } from "@/components/data-grids/shared/data-grid-layout" import { DataGridSortHeader } from "@/components/data-grids/shared/data-grid-sort-header" import { EmptyState } from "@/components/empty-state" @@ -23,6 +24,7 @@ export interface CompactDataGridColumn { accessorKey?: keyof T & string cell?: (row: T) => ReactNode enableSorting?: boolean + size?: number headerClassName?: string cellClassName?: string } @@ -32,6 +34,7 @@ interface CompactDataGridProps { columns: CompactDataGridColumn[] isLoading?: boolean horizontalScroll?: boolean + pinnedColumnId?: string emptyTitle?: string emptyDescription?: string onRowClick?: (row: T) => void @@ -43,6 +46,7 @@ function CompactDataGrid({ columns, isLoading, horizontalScroll = false, + pinnedColumnId, emptyTitle = "Нет записей", emptyDescription, onRowClick, @@ -52,6 +56,7 @@ function CompactDataGrid({ () => columns.map((col, index) => ({ id: col.id, + size: col.size, accessorKey: col.accessorKey ?? col.id, header: ({ column }) => ( @@ -86,7 +91,7 @@ function CompactDataGrid({ features: dataGridFeatures, manualPagination: false, autoResetPageIndex: false, - initialState: { pagination: { pageIndex: 0, pageSize: 50 } }, + initialState: { pagination: { pageIndex: 0, pageSize: 50 }, columnPinning: { start: pinnedColumnId ? [pinnedColumnId] : [], end: [] } }, data, columns: columnDefs, getRowId: (row) => row.id, @@ -103,6 +108,7 @@ function CompactDataGrid({ 50 || table.state.pagination.pageSize !== 50} isLoading={isLoading} loadingMode="skeleton" @@ -123,7 +129,11 @@ function CompactDataGrid({ > {horizontalScroll ? ( - + diff --git a/components/statistics/statistics-pivot-grid.tsx b/components/statistics/statistics-pivot-grid.tsx index 3049ed6d..5b4c079b 100644 --- a/components/statistics/statistics-pivot-grid.tsx +++ b/components/statistics/statistics-pivot-grid.tsx @@ -37,12 +37,14 @@ export function StatisticsPivotGrid({ const columns: CompactDataGridColumn[] = [ { id: "label", + size: 140, header: "Измерение", accessorKey: "label", cell: (row) => {row.label}, }, ...data.columns.map((col) => ({ id: `c:${col.id}`, + size: 110, header: col.label, accessorKey: `c:${col.id}` as const, cell: (row: PivotGridRow) => { @@ -71,6 +73,7 @@ export function StatisticsPivotGrid({ })), { id: "total", + size: 110, header: "Итого", accessorKey: "total", cell: (row) => ( @@ -85,6 +88,7 @@ export function StatisticsPivotGrid({ { +for (const [name, width] of [["desktop", 1440], ["mobile", 390], ["narrow", 320]] as const) test("Сертификаты: клавиатура и адаптивность " + name, async ({ page }, info) => { await page.setViewportSize({ width, height: 900 }); await certificateRoutes(page) const long = { ...certificate, name: "certificate-with-a-very-long-name-for-narrow-screens", commonName: "very-long-subdomain-for-responsive-layout.example.test", sans: ["very-long-alternative-domain-for-responsive-layout.example.test"] } await page.route("**/api/certificates/state", r => r.fulfill({ json: certificateState([long]) })) @@ -10,15 +10,15 @@ for (const [name, width] of [["desktop", 1440], ["mobile", 390], ["narrow", 320] expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth + 1)).toBe(true) const detail = page.getByRole("button", { name: "Детали сертификата " + long.name, exact: true }); await detail.focus(); await page.keyboard.press("Enter") await expect(detail).toHaveAttribute("aria-expanded", "true"); await expect(page.getByText("Доверие", { exact: true })).toBeVisible() - await page.screenshot({ path: "docs/ux-ui/screens/certificates-details-" + name + ".png", fullPage: true }) + await page.screenshot({ path: info.outputPath("certificates-details-" + name + ".png"), fullPage: true }) await page.evaluate(() => { for (const element of document.querySelectorAll("*")) if (element.scrollTop > 0) element.scrollTop = 0 }) - await page.screenshot({ path: "docs/ux-ui/screens/certificates-overview-" + name + ".png", fullPage: true }) + await page.screenshot({ path: info.outputPath("certificates-overview-" + name + ".png"), fullPage: true }) await page.keyboard.press("Enter"); await expect(detail).toHaveAttribute("aria-expanded", "false") const search = page.getByRole("textbox", { name: "Поиск по имени, CN, эмитенту…" }); await search.fill("нет-совпадений"); await expect(page.getByText("Нет совпадений", { exact: true })).toBeVisible(); await search.fill("") const dialog = await openIssue(page) await expect.poll(async () => { const box = await dialog.boundingBox(); return !!box && box.x >= 0 && box.x + box.width <= width + 1 && (width >= 640 || box.width >= width - 2) }).toBe(true) expect(await dialog.evaluate(element => element.scrollWidth <= element.clientWidth + 1)).toBe(true) - await page.screenshot({ path: "docs/ux-ui/screens/certificates-issue-" + name + ".png", fullPage: true }) + await page.screenshot({ path: info.outputPath("certificates-issue-" + name + ".png"), fullPage: true }) await page.keyboard.press("Escape"); await expect(dialog).toHaveCount(0) await expect(page.getByRole("button", { name: "Выпустить сертификат", exact: true })).toBeFocused() }) diff --git a/tests/e2e/large-certificates.spec.ts b/tests/e2e/large-certificates.spec.ts index bc1d4152..1c983aa9 100644 --- a/tests/e2e/large-certificates.spec.ts +++ b/tests/e2e/large-certificates.spec.ts @@ -44,12 +44,17 @@ test("Certificates1000: DOM, страницы, детали, поиск, refresh await expect(scroll).toHaveCount(1) expect(await scroll.evaluate(el => el.scrollWidth > el.clientWidth)).toBe(true) await scroll.evaluate(el => { el.scrollLeft = el.scrollWidth }) + await expect.poll(() => scroll.evaluate(el => el.scrollLeft)).toBeGreaterThan(0) + const pinned = rows.first().getByRole("cell").first() + expect(await pinned.evaluate(el => { const cell = el.getBoundingClientRect(), viewport = el.closest('[data-slot="scroll-area-viewport"]')!.getBoundingClientRect(); return cell.left >= viewport.left - 1 && cell.right <= viewport.right + 1 })).toBe(true) + const statusValue = rows.first().getByText("Действителен", { exact: true }) + expect(await statusValue.evaluate(el => { const value = el.getBoundingClientRect(), viewport = el.closest('[data-slot="scroll-area-viewport"]')!.getBoundingClientRect(), pinned = el.closest("tr")!.querySelector("td")!.getBoundingClientRect(); return value.left >= pinned.right - 1 && value.right <= viewport.right + 1 })).toBe(true) expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true) await page.addScriptTag({ content: axe.source }) const audit = await page.evaluate(async () => (window as unknown as { axe: typeof axe }).axe.run(document.querySelector('[data-slot="data-grid"]')!, { runOnly: { type: "tag", values: ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"] } })) expect(audit.violations).toEqual([]) await page.addStyleTag({ content: "nextjs-portal { display: none !important; }" }); mkdirSync("docs/ux-ui/screens", { recursive: true }) - await page.getByRole("table").scrollIntoViewIfNeeded(); await page.screenshot({ path: "docs/ux-ui/screens/large-certificates-320.png", animations: "disabled" }) + await page.getByRole("table").getByRole("button", { name: "Сертификат", exact: true }).scrollIntoViewIfNeeded(); await page.screenshot({ path: "docs/ux-ui/screens/large-certificates-320.png", animations: "disabled" }) await page.getByRole("button", { name: "Следующая страница", exact: true }).click() certificates = [certificates[999]]; const previous = calls; await page.getByRole("button", { name: "Обновить", exact: true }).click() await expect.poll(() => calls).toBeGreaterThan(previous); await expect(rows).toHaveCount(1) diff --git a/tests/e2e/large-speed-history.spec.ts b/tests/e2e/large-speed-history.spec.ts index 98fe2981..f53c6c14 100644 --- a/tests/e2e/large-speed-history.spec.ts +++ b/tests/e2e/large-speed-history.spec.ts @@ -42,6 +42,7 @@ test("Speed history200: DOM, сортировка, refresh, уменьшение const audit = await page.evaluate(async () => (window as unknown as { axe: typeof axe }).axe.run(document.querySelector('[data-slot="data-grid"]')!, { runOnly: { type: "tag", values: ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"] } })) expect(audit.violations).toEqual([]) await page.addStyleTag({ content: "nextjs-portal { display: none !important; }" }); mkdirSync("docs/ux-ui/screens", { recursive: true }) + await page.getByText("История тестов", { exact: true }).scrollIntoViewIfNeeded() await page.screenshot({ path: "docs/ux-ui/screens/large-speed-history-320.png", animations: "disabled" }) runs = [runs[199]]; const previous = calls; await page.getByRole("button", { name: "Обновить", exact: true }).click() await expect.poll(() => calls).toBeGreaterThan(previous) diff --git a/tests/e2e/large-statistics-pivot.spec.ts b/tests/e2e/large-statistics-pivot.spec.ts index c3ef29c8..00baa667 100644 --- a/tests/e2e/large-statistics-pivot.spec.ts +++ b/tests/e2e/large-statistics-pivot.spec.ts @@ -36,6 +36,9 @@ for (const width of [1440, 320]) test("Pivot51×16: границы, сортир expect(await scroll.evaluate(el => el.scrollWidth > el.clientWidth)).toBe(true) await scroll.evaluate(el => { el.scrollLeft = el.scrollWidth }) await expect.poll(() => scroll.evaluate(el => el.scrollLeft)).toBeGreaterThan(0) + const pinned = rendered.first().getByRole("cell").first() + expect(await pinned.evaluate(el => { const cell = el.getBoundingClientRect(), viewport = el.closest('[data-slot="scroll-area-viewport"]')!.getBoundingClientRect(); return cell.left >= viewport.left - 1 && cell.right <= viewport.right + 1 })).toBe(true) + expect(await rendered.first().getByRole("cell").last().evaluate(el => { const total = el.getBoundingClientRect(), viewport = el.closest('[data-slot="scroll-area-viewport"]')!.getBoundingClientRect(), pinned = el.closest("tr")!.querySelector("td")!.getBoundingClientRect(); return total.left >= pinned.right - 1 && total.right <= viewport.right + 1 })).toBe(true) expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true) await expect(rendered.first().getByRole("button")).toHaveCount(0) await expect(rendered.nth(1).getByRole("cell").nth(16).getByRole("button")).toHaveCount(0) @@ -46,7 +49,9 @@ for (const width of [1440, 320]) test("Pivot51×16: границы, сортир mkdirSync("docs/ux-ui/screens", { recursive: true }) await page.screenshot({ path: "docs/ux-ui/screens/wide-statistics-pivot-" + width + ".png", animations: "disabled" }) const lastCell = rendered.nth(1).getByRole("cell").nth(15).getByRole("button") - await lastCell.focus(); await page.keyboard.press("Enter") + await lastCell.focus() + expect(await lastCell.evaluate(el => { const value = el.getBoundingClientRect(), viewport = el.closest('[data-slot="scroll-area-viewport"]')!.getBoundingClientRect(), pinned = el.closest("tr")!.querySelector("td")!.getBoundingClientRect(); return value.left >= pinned.right - 1 && value.right <= viewport.right + 1 })).toBe(true) + await page.keyboard.press("Enter") await expect(page).toHaveURL(/view=explore/) const selected = new URL(page.url()); expect(selected.searchParams.get("userId")).toBe("50"); expect(selected.searchParams.get("service")).toBe("service-15") expect(reads.some(url => url.pathname === "/api/statistics" && url.searchParams.get("userId") === "50" && url.searchParams.get("service") === "service-15")).toBe(true)