From 42935036f20ceda2e7eef88aae8bc9ce6141f298 Mon Sep 17 00:00:00 2001
From: simbiozizv
Date: Wed, 30 Sep 2026 18:03:30 +0300
Subject: [PATCH 1/8] fix(TutorialsHistory): align layout and controls with
design
---
.../HistoryFilter/HistoryFilter.scss | 2 +-
.../HistoryFilter/HistoryFilter.tsx | 4 +-
.../SearchWithButtons/SearchWithButtons.scss | 2 +-
src/modules/HistoryHeader/HistoryHeader.tsx | 5 +-
src/modules/HistoryHeader/i18n/dicts.ts | 4 ++
src/modules/HistoryHeader/i18n/en.json | 4 ++
src/modules/HistoryHeader/i18n/index.ts | 4 ++
src/modules/HistoryHeader/i18n/ru.json | 4 ++
.../internal/FullSearchToggleButton.tsx | 14 +++--
src/modules/HistoryLayout/HistoryLayout.scss | 23 ++++++++
src/modules/HistoryLayout/HistoryLayout.tsx | 28 ++++++++--
src/modules/TutorialRow/TutorialRow.scss | 8 +++
src/modules/TutorialRow/TutorialRow.tsx | 7 ++-
.../TutorialSearchRow/TutorialSearchRow.tsx | 2 +-
.../TutorialsHistory/TutorialsHistory.scss | 11 +++-
.../TutorialsHistory.stories.tsx | 54 +++++++++++++++++++
.../TutorialsHistory/TutorialsHistory.tsx | 2 +
src/types/tutorial.ts | 7 ++-
tests/components/TutorialRows.test.tsx | 19 +++++++
19 files changed, 186 insertions(+), 18 deletions(-)
create mode 100644 src/modules/HistoryHeader/i18n/dicts.ts
create mode 100644 src/modules/HistoryHeader/i18n/en.json
create mode 100644 src/modules/HistoryHeader/i18n/index.ts
create mode 100644 src/modules/HistoryHeader/i18n/ru.json
diff --git a/src/components/HistoryFilter/HistoryFilter.scss b/src/components/HistoryFilter/HistoryFilter.scss
index d80c1bf..531f24b 100644
--- a/src/components/HistoryFilter/HistoryFilter.scss
+++ b/src/components/HistoryFilter/HistoryFilter.scss
@@ -2,4 +2,4 @@
&__container {
padding: 20px;
}
-}
\ No newline at end of file
+}
diff --git a/src/components/HistoryFilter/HistoryFilter.tsx b/src/components/HistoryFilter/HistoryFilter.tsx
index 5537934..2a1b15b 100644
--- a/src/components/HistoryFilter/HistoryFilter.tsx
+++ b/src/components/HistoryFilter/HistoryFilter.tsx
@@ -28,7 +28,9 @@ export const HistoryFilter: FC = ({
diff --git a/src/components/SearchWithButtons/SearchWithButtons.scss b/src/components/SearchWithButtons/SearchWithButtons.scss
index e2ec207..44afae7 100644
--- a/src/components/SearchWithButtons/SearchWithButtons.scss
+++ b/src/components/SearchWithButtons/SearchWithButtons.scss
@@ -5,6 +5,6 @@
}
&__inner-buttons {
- margin-right: 4px;
+ margin-right: 2px;
}
}
diff --git a/src/modules/HistoryHeader/HistoryHeader.tsx b/src/modules/HistoryHeader/HistoryHeader.tsx
index 07fd5e0..005ebdc 100644
--- a/src/modules/HistoryHeader/HistoryHeader.tsx
+++ b/src/modules/HistoryHeader/HistoryHeader.tsx
@@ -2,7 +2,8 @@ import React, {FC, useEffect, useState} from 'react';
import {FullSearchToggleButton} from './internal/FullSearchToggleButton';
import {HistoryFilter} from '../../components/HistoryFilter';
import {SearchWithButtons} from '../../components/SearchWithButtons';
-import {QueryListFilterConfig} from '../../types/queryList';
+import type {QueryListFilterConfig} from '../../types/queryList';
+import i18n from './i18n';
type Props = {
search?: string;
@@ -46,6 +47,8 @@ export const HistoryHeader: FC = ({
return (
= ({active, onClick}) => {
return (
-
}
+ showFiltersHint
+ />,
+ );
+ expect(container.textContent).toBe('Custom');
+ render();
+ expect(container.textContent).toContain('Nothing found');
+ render();
+ expect(container.textContent).toContain('No files');
+ });
+
+ it('preserves the default QueriesList appearance and supports panel empty overrides', () => {
+ render(
+ ,
+ );
+ expect(container.querySelector('.qp-queries-list_panel')).toBeNull();
+ expect(container.querySelector('.qp-history-layout__controls')).toBeNull();
+ expect(container.querySelector('input')?.getAttribute('placeholder')).toBeNull();
+ expect(container.textContent).toContain('Nothing found');
+ render(
+ ,
+ );
+ expect(container.textContent).toContain('No data');
+ render(
+ ,
+ );
+ expect(container.querySelector('.qp-empty-content')).toBeNull();
+ render(
+ ,
+ );
+ expect(container.textContent).toContain('Nothing found');
+ });
+
+ it('keeps the tutorials alias equivalent to panel and retains search mode interactions', () => {
+ const onUpdate = vi.fn();
+ for (const variant of ['panel', 'tutorials'] as const) {
+ render(
+ }
+ >
+ Body
+ ,
+ );
+ expect(container.querySelector('.qp-history-layout__controls')).not.toBeNull();
+ expect(container.querySelector('input')?.placeholder).toBe('Search');
+ const toggle = container.querySelector('button[aria-label="Search in query text"]');
+ expect(toggle?.classList.contains('g-button_view_flat-secondary')).toBe(true);
+ act(() => toggle?.dispatchEvent(new MouseEvent('click', {bubbles: true})));
+ expect(onUpdate).toHaveBeenLastCalledWith({value: 'select', fullSearch: true});
+ render(null);
+ }
+ });
+
+ it('allows placeholder text overrides including an explicitly hidden description', () => {
+ render();
+ expect(container.textContent).toBe('Custom title');
+ render();
+ expect(container.textContent).toContain('Try to change filters');
+ });
+});
From 9fdb36f81775e08e737315c27a365a884a5c9196 Mon Sep 17 00:00:00 2001
From: simbiozizv
Date: Thu, 1 Oct 2026 23:30:17 +0300
Subject: [PATCH 5/8] docs: require evidence for code review conclusions
---
.agents/skills/code-review-checklist/SKILL.md | 3 +++
.agents/skills/github-pr-review/SKILL.md | 3 +++
2 files changed, 6 insertions(+)
diff --git a/.agents/skills/code-review-checklist/SKILL.md b/.agents/skills/code-review-checklist/SKILL.md
index a58a208..4bdfe71 100644
--- a/.agents/skills/code-review-checklist/SKILL.md
+++ b/.agents/skills/code-review-checklist/SKILL.md
@@ -80,6 +80,8 @@ Do not report style-only nits. Order findings from highest to lowest severity.
## Response Format
+Attach a rationale to every conclusion, including findings, existing-comment statuses, and a conclusion that no issues were found. Cite concrete evidence (file and line references, relevant contracts, or check results) and explain how it supports the conclusion. Distinguish verified facts from assumptions; for a no-findings conclusion, state the reviewed scope and verification limits rather than implying proof of correctness.
+
Lead with confirmed findings. For each finding include:
- **Location:** `path/to/file:line`
@@ -87,6 +89,7 @@ Lead with confirmed findings. For each finding include:
- **Finding:** concise description of the defect
- **Condition:** input or execution path that triggers it
- **Impact:** observable consequence
+- **Rationale:** concrete evidence and the causal link between the triggering condition and the impact
- **Recommendation:** specific fix or mitigation
For existing PR comments, also include **Status** using the vocabulary required by `github-pr-review`.
diff --git a/.agents/skills/github-pr-review/SKILL.md b/.agents/skills/github-pr-review/SKILL.md
index 40cf047..cc3d3ae 100644
--- a/.agents/skills/github-pr-review/SKILL.md
+++ b/.agents/skills/github-pr-review/SKILL.md
@@ -19,6 +19,8 @@ If the PR already contains comments from other reviewers:
1. **Relevance:** Determine whether each comment still applies. Mark it if the issue has already been fixed in newer commits.
2. **Validity:** Check each comment against the current checklist. Point out any comment that conflicts with the project's standards or the checklist.
+Attach a rationale to every conclusion using the checklist's evidence requirements. For each comment status, cite the current code, relevant change, or project rule that justifies marking it Confirmed, Fixed, or Outdated; the status alone is not an explanation.
+
## Response Format
Use the following format for every finding, whether new or an existing PR comment:
@@ -27,4 +29,5 @@ Use the following format for every finding, whether new or an existing PR commen
- **Status:** (New / Confirmed / Fixed / Outdated)
- **Severity:** (High / Medium / Low)
- **Issue:** A concise description of the problem based on the checklist.
+- **Rationale:** Concrete evidence with file and line references, relevant contracts, or check results, explaining why the issue and its status follow from that evidence.
- **Recommendation:** A concrete example of the corrected code.
From 0be0e1b04a5cfbf3ae248b7b7b09585ef6e19089 Mon Sep 17 00:00:00 2001
From: simbiozizv
Date: Thu, 1 Oct 2026 23:45:46 +0300
Subject: [PATCH 6/8] fix(RowsList): disable hover highlighting for group
headers
---
src/modules/RowsList/RowsList.tsx | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/src/modules/RowsList/RowsList.tsx b/src/modules/RowsList/RowsList.tsx
index 84b3b60..d02273b 100644
--- a/src/modules/RowsList/RowsList.tsx
+++ b/src/modules/RowsList/RowsList.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, {useMemo} from 'react';
import cn from 'bem-cn-lite';
import type {
QueryListComparisonConfig,
@@ -56,6 +56,10 @@ export const RowsList = ({
className,
onItemClick,
}: RowsListProps) => {
+ const listItems = useMemo(
+ () => items.map((item) => ('header' in item ? {...item, disabled: true} : item)),
+ [items],
+ );
const getItemHeight = (item: QueryListItem) =>
rowVariant === 'search' && !('header' in item) ? SEARCH_ROW_HEIGHT : item.height;
const selectedItemIndex =
@@ -76,7 +80,7 @@ export const RowsList = ({
return (
>
className={block(null, className)}
- items={items}
+ items={listItems}
itemHeight={getItemHeight}
renderItem={(item, isActive, index) =>
renderRow(
From db8bbd1883829d382cfa2cef84d6ff87d7f7d4c7 Mon Sep 17 00:00:00 2001
From: simbiozizv
Date: Fri, 2 Oct 2026 12:43:51 +0300
Subject: [PATCH 7/8] fix(QueriesNavigation): align rows and spacing with
design
---
.../NavigationItemRow/NavigationItemRow.tsx | 11 ++++++++---
src/modules/QueriesNavigation/QueriesNavigation.scss | 8 ++++++++
src/modules/QueriesNavigation/QueriesNavigation.tsx | 2 +-
3 files changed, 17 insertions(+), 4 deletions(-)
diff --git a/src/components/NavigationItemRow/NavigationItemRow.tsx b/src/components/NavigationItemRow/NavigationItemRow.tsx
index d20895f..6e54929 100644
--- a/src/components/NavigationItemRow/NavigationItemRow.tsx
+++ b/src/components/NavigationItemRow/NavigationItemRow.tsx
@@ -14,9 +14,14 @@ export type NavigationItemRowProps = {
export const NavigationItemRow: FC = ({item}) => {
return (
- {item.icon ?? (
-
- )}
+
+ {item.icon ?? (
+
+ )}
+
+
Date: Fri, 2 Oct 2026 12:56:51 +0300
Subject: [PATCH 8/8] fix(QueriesNavigation): add View search and constrain
tabs width
---
src/helpers/filterNavigationRows.ts | 29 ++++
.../NavigationDetail/NavigationDetail.scss | 1 +
.../NavigationDetail/NavigationDetail.tsx | 13 +-
.../helpers/filterPreviewRows.ts | 30 +---
src/modules/NavigationView/NavigationView.tsx | 54 ++++--
src/modules/NavigationView/i18n/en.json | 3 +-
src/modules/NavigationView/i18n/ru.json | 3 +-
.../internal/NavigationViewSectionItem.tsx | 10 +-
.../story/NavigationView.stories.tsx | 16 ++
.../helpers/createTableDetailConfig.tsx | 11 +-
tests/components/NavigationView.test.tsx | 154 ++++++++++++++++++
11 files changed, 268 insertions(+), 56 deletions(-)
create mode 100644 src/helpers/filterNavigationRows.ts
create mode 100644 tests/components/NavigationView.test.tsx
diff --git a/src/helpers/filterNavigationRows.ts b/src/helpers/filterNavigationRows.ts
new file mode 100644
index 0000000..4a89433
--- /dev/null
+++ b/src/helpers/filterNavigationRows.ts
@@ -0,0 +1,29 @@
+import type {NavigationPreviewRow} from '../types/navigation';
+
+const stringifyCell = (value: unknown): string => {
+ if (value === undefined || value === null) {
+ return '';
+ }
+ if (typeof value === 'string') {
+ return value;
+ }
+ if (typeof value === 'number' || typeof value === 'boolean') {
+ return String(value);
+ }
+ return '';
+};
+
+export function filterNavigationRows(
+ rows: TRow[],
+ columns: string[],
+ search?: string,
+): TRow[] {
+ const query = search?.trim().toLowerCase();
+ if (!query) {
+ return rows;
+ }
+
+ return rows.filter((row) =>
+ columns.some((column) => stringifyCell(row[column]).toLowerCase().includes(query)),
+ );
+}
diff --git a/src/modules/NavigationDetail/NavigationDetail.scss b/src/modules/NavigationDetail/NavigationDetail.scss
index 23f3f79..2cecc9f 100644
--- a/src/modules/NavigationDetail/NavigationDetail.scss
+++ b/src/modules/NavigationDetail/NavigationDetail.scss
@@ -15,6 +15,7 @@
}
&__tabs {
+ min-width: 0;
overflow-x: auto;
}
diff --git a/src/modules/NavigationDetail/NavigationDetail.tsx b/src/modules/NavigationDetail/NavigationDetail.tsx
index fa16df4..7e0b7cd 100644
--- a/src/modules/NavigationDetail/NavigationDetail.tsx
+++ b/src/modules/NavigationDetail/NavigationDetail.tsx
@@ -107,12 +107,13 @@ export const NavigationDetail: React.FC = ({
/>
{hasTabs ? (
<>
-
+
+
+
{config.hasSearch && (
{
- if (value === undefined || value === null) {
- return '';
- }
- if (typeof value === 'string') {
- return value;
- }
- if (typeof value === 'number' || typeof value === 'boolean') {
- return String(value);
- }
- return '';
-};
-
-export function filterPreviewRows(
- rows: TRow[],
- columns: string[],
- search?: string,
-): TRow[] {
- const query = search?.trim().toLowerCase();
- if (!query) {
- return rows;
- }
-
- return rows.filter((row) =>
- columns.some((column) => stringifyCell(row[column]).toLowerCase().includes(query)),
- );
-}
+export {filterNavigationRows as filterPreviewRows} from '../../../helpers/filterNavigationRows';
diff --git a/src/modules/NavigationView/NavigationView.tsx b/src/modules/NavigationView/NavigationView.tsx
index 8f5b8f0..333f477 100644
--- a/src/modules/NavigationView/NavigationView.tsx
+++ b/src/modules/NavigationView/NavigationView.tsx
@@ -1,11 +1,13 @@
-import React from 'react';
+import React, {useState} from 'react';
import {Flex, Text} from '@gravity-ui/uikit';
import cn from 'bem-cn-lite';
import type {Column} from '../../components/DataTable';
import {EmptyContent} from '../../components/EmptyContent';
import {SkeletonRows} from '../../components/SkeletonRows';
+import {SearchWithButtons} from '../../components/SearchWithButtons';
import type {NavigationViewConfig, NavigationViewRow} from '../../types/navigation';
import {NavigationViewSectionItem} from './internal/NavigationViewSectionItem';
+import i18n from './i18n';
import './NavigationView.scss';
const block = cn('qp-navigation-view');
@@ -18,16 +20,31 @@ export type NavigationViewViewConfig = {
data: NavigationViewConfig;
view?: NavigationViewViewConfig;
+ search?: string;
+ onSearchUpdate?: (value: string) => void;
+ searchPlaceholder?: string;
className?: string;
};
export function NavigationView({
data,
view,
+ search: searchProp,
+ onSearchUpdate,
+ searchPlaceholder,
className,
}: NavigationViewProps) {
const {sections, loading, loaded, errorContent} = data;
const {tableColumns, extraColumns} = view ?? {};
+ const [searchState, setSearchState] = useState('');
+ const search = searchProp ?? searchState;
+
+ const handleSearchUpdate = (value: string) => {
+ if (searchProp === undefined) {
+ setSearchState(value);
+ }
+ onSearchUpdate?.(value);
+ };
if (errorContent) {
return (
@@ -37,26 +54,31 @@ export function NavigationView
- );
- }
-
- if (sections.length === 0) {
- return ;
+ content = ;
+ } else if (sections.length === 0) {
+ content = ;
+ } else {
+ content = sections.map((section) => (
+
+ key={section.id}
+ section={section}
+ search={search}
+ tableColumns={tableColumns}
+ extraColumns={extraColumns}
+ />
+ ));
}
return (
- {sections.map((section) => (
-
- key={section.id}
- section={section}
- tableColumns={tableColumns}
- extraColumns={extraColumns}
- />
- ))}
+
+ {content}
);
}
diff --git a/src/modules/NavigationView/i18n/en.json b/src/modules/NavigationView/i18n/en.json
index bb41253..b2ea5cf 100644
--- a/src/modules/NavigationView/i18n/en.json
+++ b/src/modules/NavigationView/i18n/en.json
@@ -1,3 +1,4 @@
{
- "value_empty": "—"
+ "value_empty": "—",
+ "field_search-placeholder": "Search"
}
diff --git a/src/modules/NavigationView/i18n/ru.json b/src/modules/NavigationView/i18n/ru.json
index bb41253..324ef76 100644
--- a/src/modules/NavigationView/i18n/ru.json
+++ b/src/modules/NavigationView/i18n/ru.json
@@ -1,3 +1,4 @@
{
- "value_empty": "—"
+ "value_empty": "—",
+ "field_search-placeholder": "Поиск"
}
diff --git a/src/modules/NavigationView/internal/NavigationViewSectionItem.tsx b/src/modules/NavigationView/internal/NavigationViewSectionItem.tsx
index 7dbb244..fa9777e 100644
--- a/src/modules/NavigationView/internal/NavigationViewSectionItem.tsx
+++ b/src/modules/NavigationView/internal/NavigationViewSectionItem.tsx
@@ -4,6 +4,7 @@ import cn from 'bem-cn-lite';
import type {Column} from '../../../components/DataTable';
import {DataTable} from '../../../components/DataTable';
import {NavigationActionButtons} from '../../../components/NavigationActionButtons';
+import {filterNavigationRows} from '../../../helpers/filterNavigationRows';
import type {NavigationViewRow, NavigationViewSection} from '../../../types/navigation';
import {buildViewColumns} from '../helpers/buildViewColumns';
import i18n from '../i18n';
@@ -12,12 +13,14 @@ const block = cn('qp-navigation-view');
export type NavigationViewSectionItemProps = {
section: NavigationViewSection;
+ search?: string;
tableColumns?: Array>;
extraColumns?: Array>;
};
export function NavigationViewSectionItem({
section,
+ search,
tableColumns,
extraColumns,
}: NavigationViewSectionItemProps) {
@@ -27,6 +30,10 @@ export function NavigationViewSectionItem(section.columns, i18n), ...(extraColumns ?? [])];
}, [tableColumns, extraColumns, section.columns]);
+ const rows = useMemo(
+ () => filterNavigationRows(section.rows, section.columns, search),
+ [section.rows, section.columns, search],
+ );
return (
columns={resolvedColumns}
- data={section.rows}
+ data={rows}
+ emptyVariant={search?.trim() ? 'nothing-found' : 'no-data'}
loading={section.loading}
loaded={section.loaded}
settings={{displayIndices: false}}
diff --git a/src/modules/NavigationView/story/NavigationView.stories.tsx b/src/modules/NavigationView/story/NavigationView.stories.tsx
index dd38c68..7394bf4 100644
--- a/src/modules/NavigationView/story/NavigationView.stories.tsx
+++ b/src/modules/NavigationView/story/NavigationView.stories.tsx
@@ -35,6 +35,22 @@ export const Default: Story = {
},
};
+const SearchStory = () => {
+ const [search, setSearch] = useState('field_2');
+
+ return (
+
+ );
+};
+
+export const Search: Story = {
+ render: () => ,
+};
+
export const Loading: Story = {
args: {
data: {sections: [], loading: true},
diff --git a/src/modules/QueriesNavigation/helpers/createTableDetailConfig.tsx b/src/modules/QueriesNavigation/helpers/createTableDetailConfig.tsx
index 7722318..8572705 100644
--- a/src/modules/QueriesNavigation/helpers/createTableDetailConfig.tsx
+++ b/src/modules/QueriesNavigation/helpers/createTableDetailConfig.tsx
@@ -112,9 +112,16 @@ export const createTableDetailConfig = <
{
id: 'view',
title: i18n('tab_view'),
- renderContent: () => {
+ renderContent: ({search, onSearchUpdate, searchPlaceholder}) => {
const view = resolveView?.(item);
- return data={view ?? {sections: []}} />;
+ return (
+
+ data={view ?? {sections: []}}
+ search={search}
+ onSearchUpdate={onSearchUpdate}
+ searchPlaceholder={searchPlaceholder}
+ />
+ );
},
},
],
diff --git a/tests/components/NavigationView.test.tsx b/tests/components/NavigationView.test.tsx
new file mode 100644
index 0000000..ac36eb6
--- /dev/null
+++ b/tests/components/NavigationView.test.tsx
@@ -0,0 +1,154 @@
+// @vitest-environment jsdom
+
+import React, {act} from 'react';
+import {ThemeProvider} from '@gravity-ui/uikit';
+import type {Root} from 'react-dom/client';
+import {createRoot} from 'react-dom/client';
+import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
+import {NavigationView} from '../../src/modules/NavigationView';
+import {createTableDetailConfig} from '../../src/modules/QueriesNavigation/helpers/createTableDetailConfig';
+import type {NavigationViewConfig} from '../../src/types/navigation';
+
+function makeData(): NavigationViewConfig {
+ return {
+ loaded: true,
+ sections: [
+ {
+ id: 'primary',
+ title: 'Primary view',
+ columns: ['name', 'count'],
+ rows: [
+ {name: 'Alpha record', count: 42},
+ {name: 'Beta record', count: 7, hidden: 'alpha'},
+ ],
+ loaded: true,
+ defaultExpanded: true,
+ },
+ {
+ id: 'secondary',
+ title: 'Secondary view',
+ columns: ['description'],
+ rows: [{description: 'Alpha description'}, {description: 'Gamma description'}],
+ loaded: true,
+ defaultExpanded: true,
+ },
+ ],
+ };
+}
+
+describe('NavigationView search', () => {
+ let container: HTMLDivElement;
+ let root: Root;
+
+ beforeEach(() => {
+ globalThis.IS_REACT_ACT_ENVIRONMENT = true;
+ container = document.createElement('div');
+ document.body.append(container);
+ root = createRoot(container);
+ });
+
+ afterEach(() => {
+ act(() => root.unmount());
+ container.remove();
+ });
+
+ function renderContent(content: React.ReactNode) {
+ act(() => root.render({content}));
+ }
+
+ function updateSearch(value: string) {
+ const input = container.querySelector('input');
+ expect(input).not.toBeNull();
+ act(() => {
+ Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set?.call(
+ input,
+ value,
+ );
+ input?.dispatchEvent(new Event('input', {bubbles: true}));
+ });
+ }
+
+ it('filters each section by its columns and restores all rows when cleared', () => {
+ renderContent();
+
+ updateSearch(' ALPHA ');
+ expect(container.textContent).toContain('Alpha record');
+ expect(container.textContent).toContain('Alpha description');
+ expect(container.textContent).not.toContain('Beta record');
+ expect(container.textContent).not.toContain('Gamma description');
+
+ updateSearch('42');
+ expect(container.textContent).toContain('Alpha record');
+ expect(container.textContent).not.toContain('Alpha description');
+
+ updateSearch('');
+ expect(container.textContent).toContain('Beta record');
+ expect(container.textContent).toContain('Gamma description');
+ });
+
+ it('keeps section headings and shows nothing-found for unmatched rows', () => {
+ renderContent();
+
+ updateSearch('missing');
+
+ expect(container.textContent).toContain('Primary view');
+ expect(container.textContent).toContain('Secondary view');
+ expect(container.textContent).not.toContain('Alpha record');
+ expect(container.textContent?.match(/Nothing found/g)).toHaveLength(2);
+ });
+
+ it('forwards controlled search, its placeholder and updates through the View tab factory', () => {
+ const data = makeData();
+ const config = createTableDetailConfig({resolveView: () => data})({
+ path: '/table',
+ title: 'table',
+ });
+ const viewTab = config.tabs.find((tab) => tab.id === 'view');
+ const onSearchUpdate = vi.fn();
+ const render = (search: string) => {
+ renderContent(
+ viewTab?.renderContent?.({
+ search,
+ onSearchUpdate,
+ searchPlaceholder: 'Find view rows',
+ }),
+ );
+ };
+
+ render('Beta');
+ expect(container.querySelector('input')?.placeholder).toBe('Find view rows');
+ expect(container.querySelector('input')?.value).toBe('Beta');
+ expect(container.textContent).toContain('Beta record');
+ expect(container.textContent).not.toContain('Alpha record');
+
+ updateSearch('Alpha');
+ expect(onSearchUpdate).toHaveBeenLastCalledWith('Alpha');
+ expect(container.textContent).toContain('Beta record');
+
+ render('Alpha');
+ expect(container.textContent).toContain('Alpha record');
+ expect(container.textContent).not.toContain('Beta record');
+ });
+
+ it('preserves expansion and passes the original section to actions while filtering', () => {
+ const data = makeData();
+ const section = data.sections[0];
+ const onClick = vi.fn();
+ section.actions = [{id: 'open', title: 'Open view', content: 'Open', onClick}];
+ renderContent();
+
+ const toggle = Array.from(container.querySelectorAll('button')).find((button) =>
+ button.textContent?.includes('Secondary view'),
+ );
+ expect(toggle?.getAttribute('aria-expanded')).toBe('true');
+ act(() => toggle?.click());
+ expect(toggle?.getAttribute('aria-expanded')).toBe('false');
+
+ updateSearch('Alpha');
+ expect(toggle?.getAttribute('aria-expanded')).toBe('false');
+ act(() => container.querySelector('[aria-label="Open view"]')?.click());
+ expect(onClick).toHaveBeenCalledWith(section);
+ expect(onClick.mock.calls[0][0]).toBe(section);
+ expect(section.rows).toHaveLength(2);
+ });
+});