fix(ui): сохранить имя строки при прокрутке таблицы

This commit is contained in:
Denozordec committed 2026-10-04 04:38:23 +07:00
1 parent 5fbfc752fb
commit e0f83bdf42
15 files changed
+59 -12

No files matched your search

@@ -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 }) => (
<DataGridSortHeader column={column} title="Сертификат" className="ml-1" />
),
@@ -97,7 +99,7 @@ function CertificatesDataGrid({ certificates, serverMap, isLoading, emptyTitle =
</Button>
<div className="min-w-0">
<div className="flex items-center gap-2 min-w-0">
<span className="shrink-0">{cfg.icon}</span>
<span className="hidden sm:inline shrink-0">{cfg.icon}</span>
<span className="font-medium text-sm truncate" title={cert.name}>
{cert.name}
</span>
@@ -196,6 +198,7 @@ function CertificatesDataGrid({ certificates, serverMap, isLoading, emptyTitle =
},
{
id: "status",
size: 120,
accessorKey: "status",
header: ({ column }) => <DataGridSortHeader column={column} title="Статус" />,
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 =
<DataGridShell
table={table}
recordCount={certificates.length}
tableLayout={{ ...DEFAULT_DATA_GRID_LAYOUT, columnsPinnable: true }}
pagination={certificates.length > 50 || pageSize !== 50}
isLoading={isLoading}
loadingMode="skeleton"
+13 -3
View File
@@ -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<T> {
accessorKey?: keyof T & string
cell?: (row: T) => ReactNode
enableSorting?: boolean
size?: number
headerClassName?: string
cellClassName?: string
}
@@ -32,6 +34,7 @@ interface CompactDataGridProps<T extends { id: string }> {
columns: CompactDataGridColumn<T>[]
isLoading?: boolean
horizontalScroll?: boolean
pinnedColumnId?: string
emptyTitle?: string
emptyDescription?: string
onRowClick?: (row: T) => void
@@ -43,6 +46,7 @@ function CompactDataGrid<T extends { id: string }>({
columns,
isLoading,
horizontalScroll = false,
pinnedColumnId,
emptyTitle = "Нет записей",
emptyDescription,
onRowClick,
@@ -52,6 +56,7 @@ function CompactDataGrid<T extends { id: string }>({
() =>
columns.map((col, index) => ({
id: col.id,
size: col.size,
accessorKey: col.accessorKey ?? col.id,
header: ({ column }) => (
<DataGridSortHeader column={column} title={col.header} className={index === 0 ? "ml-1" : undefined} />
@@ -86,7 +91,7 @@ function CompactDataGrid<T extends { id: string }>({
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<T extends { id: string }>({
<DataGridShell
table={table}
recordCount={data.length}
tableLayout={pinnedColumnId ? { ...DEFAULT_DATA_GRID_LAYOUT, columnsPinnable: true } : undefined}
pagination={data.length > 50 || table.state.pagination.pageSize !== 50}
isLoading={isLoading}
loadingMode="skeleton"
@@ -123,7 +129,11 @@ function CompactDataGrid<T extends { id: string }>({
>
{horizontalScroll ? (
<DataGridContainer border={false} className="w-full rounded-none border-0">
<DataGridScrollArea orientation="horizontal">
<DataGridScrollArea
orientation="horizontal"
className={pinnedColumnId ? "[&_[data-slot=scroll-area-viewport]]:[scroll-padding-inline-start:var(--pinned-column-width)]" : undefined}
style={pinnedColumnId ? { "--pinned-column-width": `${table.getColumn(pinnedColumnId)?.getSize() ?? 0}px` } as CSSProperties : undefined}
>
<DataGridTable />
</DataGridScrollArea>
</DataGridContainer>
@@ -37,12 +37,14 @@ export function StatisticsPivotGrid({
const columns: CompactDataGridColumn<PivotGridRow>[] = [
{
id: "label",
size: 140,
header: "Измерение",
accessorKey: "label",
cell: (row) => <span className="truncate font-medium">{row.label}</span>,
},
...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({
<CompactDataGrid
data={rows}
horizontalScroll
pinnedColumnId="label"
columns={columns}
isLoading={isLoading}
emptyTitle="Нет данных сводной"
+2
View File
@@ -68,3 +68,5 @@ Statistics pivot в backend/src/services/statistics-aggregate.ts огранич
12.3.15: live расписание на1000 rows дополнительно проверено при503 на page20, freshness/cache/recovery1/no POST;50 rows/739 section nodes/1778 page nodes. Demo ProbesScheduleDataGrid не использует API и начинается с3 INIT_RULES; исключать его из live migration, но не считать произвольный большой demo-list проверенным.
12.3.16:certificates1000/329034 JSON bytes —1000→50 rows/29666→2145 DOM nodes,1098ms final dev ready. Whole-catalog search/URL/reload/sort/page20/SAN/refresh/1000→1/320px/axe проверены. Осталось23 manualPagination:true файла; закрепление идентификатора строки при горизонтальной прокрутке — следующий UX блок.
12.3.17: закреплённое имя и значения полностью видимы на320px после корректировки размеров; pivot action при focus не перекрывается.29 browser/axe/typecheck/build прошли. Certificate final50 rows/2145 nodes; pinning ограничивает положение колонок, не число загружаемых DTO.
+1 -1
View File
@@ -4,7 +4,7 @@
## Актуальная точка продолжения — 2026-10-04
Этап 11Б завершён в границах локальной приёмки (11Б.7); внешний выпуск не проверен. 12.1 принят локально: доступность оболочки/клавиатура/health в описанных сценариях. 12.2.1 принят локально: strict BGP catalog/partial/nullable counters. 12.2.2 принят локально: strict traffic DTO и агрегация без выдуманных точек. 12.2.3 принят локально: observations для server traffic от backend до dashboard. 12.2.4 принят локально: strict servers/probes и unknown server status. 12.2.5 принят локально: реальные ping samples/unknown/ось RTT/Recharts. 12.2.6 принят локально: strict GRE/WireGuard/VXLAN и device failures. 12.2.7 принят локально: strict Internet path snapshot/fallback. 12.2.8 принят локально: подтверждение полного backend сбора. 12.2.9 принят локально: метрики участков и наблюдаемый WAN без эвристической подмены. 12.3.1 принят локально:1000 серверов/штатная пагинация. 12.3.2 принят локально: пагинация CompactDataGrid/1000 строк статистики. 12.3.3 принят локально: порционный DOM очереди проблем. 12.3.4 принят локально: большой BGP каталог/страница при обновлении. Production build и123 профильных browser приняты локально на3978976. 12.2.10 принят локально: обязательные RouterOS поля. 12.3.5 принят локально: страница CompactDataGrid сохраняется при опросе,89 browser и повторный production build прошли. 12.3.6 сохранён: baseline1000 speed probes (ограничение DOM пока не выполнено). 12.3.7 принят локально:50 строк speed-проб/сохранение страницы/глобальная сортировка/1000→1; каталог1001 устройства остаётся в DOM. 12.3.8 принят локально:50 строк ресурсов, URL/поиск/сохранение страницы/1000→1; исправлена pagination оболочки с children, production build и8 больших browser прошли. 12.3.9 принят локально:порционное раскрытие1000 предупреждений ресурсов/клавиатура/axe; исходная ошибка ESLint speedDraft подтверждена. 12.3.10 принят локально:недоступные интерфейсы очищаются в обработчиках,54 browser/полный ESLint/production build прошли. 12.3.11 принят локально:ServerTileRail50 устройств/URL/поиск/клавиатура/две площадки/mobile Sheet/axe обе темы;47 browser и production build прошли. 12.3.12 принят локально:BGP ручная страница при выбранной URL сессии/refresh/reload,16 browser/build прошли. 12.4.1 записан:main1091 сценарий/1088 passed/3 устаревших fixtures; оба исправленных файла6 passed. 12.4.2 принят локально:context205/24 файла и unit28 прошли. 12.3.13 принят локально:wide pivot51×16/ScrollArea/axe/24+2 browser/builds. 12.3.14 принят локально:history200/50 DOM/54+1 browser/build. 12.3.15 принят локально:live расписание1000/page20 error cache/15 browser. 12.3.16 принят локально:certificates1000/50 DOM/refresh деталей/19+3 browser/build. Следующий блок12.3.17:имя строки при horizontal scroll, затем users/backups/network grids и отдельная достоверность history;12.4/12.5 ещё не завершены. См. docs/ux-ui-stage-12.md. Внешние ограничения 11Б сохраняются.
Этап 11Б завершён в границах локальной приёмки (11Б.7); внешний выпуск не проверен. 12.1 принят локально: доступность оболочки/клавиатура/health в описанных сценариях. 12.2.1 принят локально: strict BGP catalog/partial/nullable counters. 12.2.2 принят локально: strict traffic DTO и агрегация без выдуманных точек. 12.2.3 принят локально: observations для server traffic от backend до dashboard. 12.2.4 принят локально: strict servers/probes и unknown server status. 12.2.5 принят локально: реальные ping samples/unknown/ось RTT/Recharts. 12.2.6 принят локально: strict GRE/WireGuard/VXLAN и device failures. 12.2.7 принят локально: strict Internet path snapshot/fallback. 12.2.8 принят локально: подтверждение полного backend сбора. 12.2.9 принят локально: метрики участков и наблюдаемый WAN без эвристической подмены. 12.3.1 принят локально:1000 серверов/штатная пагинация. 12.3.2 принят локально: пагинация CompactDataGrid/1000 строк статистики. 12.3.3 принят локально: порционный DOM очереди проблем. 12.3.4 принят локально: большой BGP каталог/страница при обновлении. Production build и123 профильных browser приняты локально на3978976. 12.2.10 принят локально: обязательные RouterOS поля. 12.3.5 принят локально: страница CompactDataGrid сохраняется при опросе,89 browser и повторный production build прошли. 12.3.6 сохранён: baseline1000 speed probes (ограничение DOM пока не выполнено). 12.3.7 принят локально:50 строк speed-проб/сохранение страницы/глобальная сортировка/1000→1; каталог1001 устройства остаётся в DOM. 12.3.8 принят локально:50 строк ресурсов, URL/поиск/сохранение страницы/1000→1; исправлена pagination оболочки с children, production build и8 больших browser прошли. 12.3.9 принят локально:порционное раскрытие1000 предупреждений ресурсов/клавиатура/axe; исходная ошибка ESLint speedDraft подтверждена. 12.3.10 принят локально:недоступные интерфейсы очищаются в обработчиках,54 browser/полный ESLint/production build прошли. 12.3.11 принят локально:ServerTileRail50 устройств/URL/поиск/клавиатура/две площадки/mobile Sheet/axe обе темы;47 browser и production build прошли. 12.3.12 принят локально:BGP ручная страница при выбранной URL сессии/refresh/reload,16 browser/build прошли. 12.4.1 записан:main1091 сценарий/1088 passed/3 устаревших fixtures; оба исправленных файла6 passed. 12.4.2 принят локально:context205/24 файла и unit28 прошли. 12.3.13 принят локально:wide pivot51×16/ScrollArea/axe/24+2 browser/builds. 12.3.14 принят локально:history200/50 DOM/54+1 browser/build. 12.3.15 принят локально:live расписание1000/page20 error cache/15 browser. 12.3.16 принят локально:certificates1000/50 DOM/refresh деталей/19+3 browser/build. 12.3.17 принят локально:pinning идентификатора/видимость значений/29 browser/build. Следующий блок12.3.18:users1000, затем backups/network grids и отдельная достоверность history;12.4/12.5 ещё не завершены. См. docs/ux-ui-stage-12.md. Внешние ограничения 11Б сохраняются.
Реальные pg_dump/pg_restore, PG rollback и остановка UDP worker требуют внешней приёмки. Датированные продолжения ниже — история; точный следующий шаг также указан в начале [журнала](ux-ui-progress.md).
+8
View File
@@ -36,6 +36,14 @@
## 12.3.17 — идентификатор строки при горизонтальной прокрутке (2026-10-04)
Имя сертификата и измерение pivot закреплены штатным TanStack columnPinning/start и ReUI columnsPinnable. CompactDataGrid получает optional pinnedColumnId и размеры колонок; default consumers сохраняют прежний режим. Мобильные размеры проверены по реальным bounding boxes: имя160/status120 для сертификатов, измерение140/метрики и итог110 для pivot. Ранние180/150 ширины перекрывали часть значения на320px; финальные размеры устраняют это. Дублирующий status icon в имени сертификата скрыт только на телефоне, полное имя остаётся в title/details.
Scoped scroll-padding-inline-start на viewport обеспечивает видимость кнопки ячейки при keyboard focus возле закреплённого измерения. Проверены полностью видимые статус/итог/действие вне pinned области, границы имени, Enter→двойной срез,320/1440/axe/sort/search/refresh/expanded/1000→1.29 browser в7 файлах прошли после финальной правки. Typecheck/ESLint и production frontend/contracts34 страницы прошли. Снимки сертификатов/pivot/history обновлены и просмотрены. Повторяющаяся UNKNOWN open historical PNG в certificate-layout устранена использованием штатного info.outputPath: текущие layout артефакты отдельно от сохранённых исторических снимков, assertions не ослаблялись.
Следующий блок12.3.18:users1000 через strict API, пагинация/весь поиск/refresh деталей/ошибка и восстановление/правильный пользователь в действии/320px. Затем backups/network grids; достоверность history из12.3.14 и финальная матрица12.4 остаются. Полный main/context после delta ещё не повторён;12.5 внешняя приёмка отдельно. Локальный коммит без push.
## 12.3.16 — каталог1000 сертификатов и refresh деталей (2026-10-04)
Fixture strict per-device certificatesStateSchema,1000 сертификатов одного устройства/329034 JSON bytes. Baseline1000 rows/29666 DOM nodes/2689ms dev ready; после client pagination50 и горизонтального ReUI ScrollArea:50 rows/2145 nodes/1098ms. autoResetPageIndex=false/clamp сохраняют страницу и корректно сокращают1000→1. Воспроизведено закрытие SAN при refresh: autoResetExpanded=false сохраняет детали по стабильному составному ID устройства/RouterOS.
+8
View File
@@ -273,3 +273,11 @@ Fixture strict per-device certificatesStateSchema,1000 сертификатов
Проверены page20/Enter/SAN, refresh страницы и раскрытия, поиск записи1000 по полному каталогу с URL/server/reload, глобальная числовая сортировка срока, ScrollArea/reflow320, tagged axe grid, сокращение и отсутствие POST. После последней production правки19 из20 browser прошли; narrow layout прерван разовой UNKNOWN open PNG ошибкой Windows. Повтор всех3 layout прошёл без изменения UI. Typecheck/ESLint и production frontend/contracts build34 страницы прошли. Снимок просмотрен; из-за горизонтальной прокрутки имя строки уходит из видимой области — следующий UX блок сохраняет идентификацию строк.
Следующий блок12.3.17: штатное закрепление колонки имени сертификата и измерения pivot при horizontal scroll, проверить доступ к остальным ячейкам на320px/ПК. Затем users/backups/network grids. Осталось23 manualPagination:true файла, включая demo-only и bounded источники; это не23 доказанных дефекта. Достоверность speed history, итоговая матрица12.4 и внешняя приёмка12.5 остаются. Коммит локальный, без push.
## 12.3.17 — идентификатор строки при горизонтальной прокрутке (2026-10-04)
Имя сертификата и измерение pivot закреплены штатным TanStack columnPinning/start и ReUI columnsPinnable. CompactDataGrid получает optional pinnedColumnId и размеры колонок; default consumers сохраняют прежний режим. Мобильные размеры проверены по реальным bounding boxes: имя160/status120 для сертификатов, измерение140/метрики и итог110 для pivot. Ранние180/150 ширины перекрывали часть значения на320px; финальные размеры устраняют это. Дублирующий status icon в имени сертификата скрыт только на телефоне, полное имя остаётся в title/details.
Scoped scroll-padding-inline-start на viewport обеспечивает видимость кнопки ячейки при keyboard focus возле закреплённого измерения. Проверены полностью видимые статус/итог/действие вне pinned области, границы имени, Enter→двойной срез,320/1440/axe/sort/search/refresh/expanded/1000→1.29 browser в7 файлах прошли после финальной правки. Typecheck/ESLint и production frontend/contracts34 страницы прошли. Снимки сертификатов/pivot/history обновлены и просмотрены. Повторяющаяся UNKNOWN open historical PNG в certificate-layout устранена использованием штатного info.outputPath: текущие layout артефакты отдельно от сохранённых исторических снимков, assertions не ослаблялись.
Следующий блок12.3.18:users1000 через strict API, пагинация/весь поиск/refresh деталей/ошибка и восстановление/правильный пользователь в действии/320px. Затем backups/network grids; достоверность history из12.3.14 и финальная матрица12.4 остаются. Полный main/context после delta ещё не повторён;12.5 внешняя приёмка отдельно. Локальный коммит без push.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 50 KiB

+4 -4
View File
@@ -1,7 +1,7 @@
import { test, expect } from "@playwright/test"
import { certificateRoutes, openIssue } from "./fixtures/certificate-issue"
import { certificate, certificateState } from "./fixtures/certificates"
for (const [name, width] of [["desktop", 1440], ["mobile", 390], ["narrow", 320]] as const) test("Сертификаты: клавиатура и адаптивность " + name, async ({ page }) => {
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<HTMLElement>("*")) 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()
})
+6 -1
View File
@@ -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)
+1
View File
@@ -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)
+6 -1
View File
@@ -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)