From 2787b86d053f7a9694c46323db5d53a7e37f4bb2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 4 Aug 2025 08:20:01 +0000 Subject: [PATCH 1/9] Initial plan From 48ab22067712a3a7494cd24a560dc13454b91b43 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 4 Aug 2025 08:35:59 +0000 Subject: [PATCH 2/9] fix: conditionally show threads tab based on API response data Co-authored-by: Raubzeug <42094364+Raubzeug@users.noreply.github.com> --- src/containers/Node/Node.tsx | 6 +- src/containers/Node/__tests__/Node.test.ts | 84 ++++++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 src/containers/Node/__tests__/Node.test.ts diff --git a/src/containers/Node/Node.tsx b/src/containers/Node/Node.tsx index 861d4e566c..482fb1e960 100644 --- a/src/containers/Node/Node.tsx +++ b/src/containers/Node/Node.tsx @@ -75,12 +75,16 @@ export function Node() { if (isDiskPagesAvailable) { actulaNodeTabs = actulaNodeTabs.filter((el) => el.id !== 'structure'); } + // Filter out threads tab if there's no thread data in the API response + if (!node?.Threads || node.Threads.length === 0) { + actulaNodeTabs = actulaNodeTabs.filter((el) => el.id !== 'threads'); + } const actualActiveTab = actulaNodeTabs.find(({id}) => id === activeTabId) ?? actulaNodeTabs[0]; return {activeTab: actualActiveTab, nodeTabs: actulaNodeTabs}; - }, [isStorageNode, isDiskPagesAvailable, activeTabId]); + }, [isStorageNode, isDiskPagesAvailable, activeTabId, node?.Threads]); const tenantName = node?.Tenants?.[0] || tenantNameFromQuery?.toString(); diff --git a/src/containers/Node/__tests__/Node.test.ts b/src/containers/Node/__tests__/Node.test.ts new file mode 100644 index 0000000000..e221b154e4 --- /dev/null +++ b/src/containers/Node/__tests__/Node.test.ts @@ -0,0 +1,84 @@ +import {NODE_TABS} from '../NodePages'; + +describe('Node tab filtering logic', () => { + it('should filter out threads tab when no thread data is provided', () => { + // Simulate the filtering logic that happens in the Node component + const isStorageNode = false; + const isDiskPagesAvailable = false; + const node: {Threads?: any[]} = { + // No Threads property + }; + + let actualNodeTabs = isStorageNode + ? NODE_TABS + : NODE_TABS.filter((el) => el.id !== 'storage'); + + if (isDiskPagesAvailable) { + actualNodeTabs = actualNodeTabs.filter((el) => el.id !== 'structure'); + } + + // Filter out threads tab if there's no thread data in the API response + if (!node.Threads || node.Threads.length === 0) { + actualNodeTabs = actualNodeTabs.filter((el) => el.id !== 'threads'); + } + + // Should not include threads tab + expect(actualNodeTabs.some((tab) => tab.id === 'threads')).toBe(false); + // Should include other tabs + expect(actualNodeTabs.some((tab) => tab.id === 'tablets')).toBe(true); + }); + + it('should include threads tab when thread data is provided', () => { + // Simulate the filtering logic that happens in the Node component + const isStorageNode = false; + const isDiskPagesAvailable = false; + const node: {Threads?: any[]} = { + Threads: [{Name: 'TestPool', Threads: 4}], + }; + + let actualNodeTabs = isStorageNode + ? NODE_TABS + : NODE_TABS.filter((el) => el.id !== 'storage'); + + if (isDiskPagesAvailable) { + actualNodeTabs = actualNodeTabs.filter((el) => el.id !== 'structure'); + } + + // Filter out threads tab if there's no thread data in the API response + if (!node.Threads || node.Threads.length === 0) { + actualNodeTabs = actualNodeTabs.filter((el) => el.id !== 'threads'); + } + + // Should include threads tab + expect(actualNodeTabs.some((tab) => tab.id === 'threads')).toBe(true); + // Should include other tabs + expect(actualNodeTabs.some((tab) => tab.id === 'tablets')).toBe(true); + }); + + it('should filter out threads tab when thread data is empty array', () => { + // Simulate the filtering logic that happens in the Node component + const isStorageNode = false; + const isDiskPagesAvailable = false; + const node: {Threads?: any[]} = { + Threads: [], // Empty array + }; + + let actualNodeTabs = isStorageNode + ? NODE_TABS + : NODE_TABS.filter((el) => el.id !== 'storage'); + + if (isDiskPagesAvailable) { + actualNodeTabs = actualNodeTabs.filter((el) => el.id !== 'structure'); + } + + // Filter out threads tab if there's no thread data in the API response + if (!node.Threads || node.Threads.length === 0) { + actualNodeTabs = actualNodeTabs.filter((el) => el.id !== 'threads'); + } + + // Should not include threads tab + expect(actualNodeTabs.some((tab) => tab.id === 'threads')).toBe(false); + // Should include other tabs + expect(actualNodeTabs.some((tab) => tab.id === 'tablets')).toBe(true); + }); +}); From f07b7c37496170e264f035e52ea42e8ef771e6c9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 4 Aug 2025 08:58:38 +0000 Subject: [PATCH 3/9] fix: move tests to e2e suite and fix node Threads data flow Co-authored-by: Raubzeug <42094364+Raubzeug@users.noreply.github.com> --- src/containers/Node/__tests__/Node.test.ts | 84 ------------ src/store/reducers/node/types.ts | 5 +- src/store/reducers/node/utils.ts | 8 +- tests/suites/nodes/NodePage.ts | 42 ++++++ tests/suites/nodes/nodes.test.ts | 151 +++++++++++++++++++++ 5 files changed, 204 insertions(+), 86 deletions(-) delete mode 100644 src/containers/Node/__tests__/Node.test.ts create mode 100644 tests/suites/nodes/NodePage.ts diff --git a/src/containers/Node/__tests__/Node.test.ts b/src/containers/Node/__tests__/Node.test.ts deleted file mode 100644 index e221b154e4..0000000000 --- a/src/containers/Node/__tests__/Node.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -import {NODE_TABS} from '../NodePages'; - -describe('Node tab filtering logic', () => { - it('should filter out threads tab when no thread data is provided', () => { - // Simulate the filtering logic that happens in the Node component - const isStorageNode = false; - const isDiskPagesAvailable = false; - const node: {Threads?: any[]} = { - // No Threads property - }; - - let actualNodeTabs = isStorageNode - ? NODE_TABS - : NODE_TABS.filter((el) => el.id !== 'storage'); - - if (isDiskPagesAvailable) { - actualNodeTabs = actualNodeTabs.filter((el) => el.id !== 'structure'); - } - - // Filter out threads tab if there's no thread data in the API response - if (!node.Threads || node.Threads.length === 0) { - actualNodeTabs = actualNodeTabs.filter((el) => el.id !== 'threads'); - } - - // Should not include threads tab - expect(actualNodeTabs.some((tab) => tab.id === 'threads')).toBe(false); - // Should include other tabs - expect(actualNodeTabs.some((tab) => tab.id === 'tablets')).toBe(true); - }); - - it('should include threads tab when thread data is provided', () => { - // Simulate the filtering logic that happens in the Node component - const isStorageNode = false; - const isDiskPagesAvailable = false; - const node: {Threads?: any[]} = { - Threads: [{Name: 'TestPool', Threads: 4}], - }; - - let actualNodeTabs = isStorageNode - ? NODE_TABS - : NODE_TABS.filter((el) => el.id !== 'storage'); - - if (isDiskPagesAvailable) { - actualNodeTabs = actualNodeTabs.filter((el) => el.id !== 'structure'); - } - - // Filter out threads tab if there's no thread data in the API response - if (!node.Threads || node.Threads.length === 0) { - actualNodeTabs = actualNodeTabs.filter((el) => el.id !== 'threads'); - } - - // Should include threads tab - expect(actualNodeTabs.some((tab) => tab.id === 'threads')).toBe(true); - // Should include other tabs - expect(actualNodeTabs.some((tab) => tab.id === 'tablets')).toBe(true); - }); - - it('should filter out threads tab when thread data is empty array', () => { - // Simulate the filtering logic that happens in the Node component - const isStorageNode = false; - const isDiskPagesAvailable = false; - const node: {Threads?: any[]} = { - Threads: [], // Empty array - }; - - let actualNodeTabs = isStorageNode - ? NODE_TABS - : NODE_TABS.filter((el) => el.id !== 'storage'); - - if (isDiskPagesAvailable) { - actualNodeTabs = actualNodeTabs.filter((el) => el.id !== 'structure'); - } - - // Filter out threads tab if there's no thread data in the API response - if (!node.Threads || node.Threads.length === 0) { - actualNodeTabs = actualNodeTabs.filter((el) => el.id !== 'threads'); - } - - // Should not include threads tab - expect(actualNodeTabs.some((tab) => tab.id === 'threads')).toBe(false); - // Should include other tabs - expect(actualNodeTabs.some((tab) => tab.id === 'tablets')).toBe(true); - }); -}); diff --git a/src/store/reducers/node/types.ts b/src/store/reducers/node/types.ts index ef7df529e3..2b727c0a9d 100644 --- a/src/store/reducers/node/types.ts +++ b/src/store/reducers/node/types.ts @@ -1,3 +1,4 @@ +import type {TThreadPoolInfo} from '../../../types/api/threads'; import type {PreparedPDisk, PreparedVDisk} from '../../../utils/disks/types'; import type {PreparedNodeSystemState} from '../../../utils/nodes'; @@ -18,4 +19,6 @@ export interface PreparedStructurePDisk extends PreparedPDisk { export type PreparedNodeStructure = Record; -export interface PreparedNode extends Partial {} +export interface PreparedNode extends Partial { + Threads?: TThreadPoolInfo[]; +} diff --git a/src/store/reducers/node/utils.ts b/src/store/reducers/node/utils.ts index 6be190f825..9fd19faf49 100644 --- a/src/store/reducers/node/utils.ts +++ b/src/store/reducers/node/utils.ts @@ -9,6 +9,12 @@ export const prepareNodeData = (data: TEvSystemStateResponse): PreparedNode => { } const nodeData = data.SystemStateInfo[0]; + + const preparedSystemState = prepareNodeSystemState(nodeData); - return prepareNodeSystemState(nodeData); + // Include Threads from the top-level response for the tab filtering logic + return { + ...preparedSystemState, + Threads: data.Threads, + }; }; diff --git a/tests/suites/nodes/NodePage.ts b/tests/suites/nodes/NodePage.ts new file mode 100644 index 0000000000..6e418faf6a --- /dev/null +++ b/tests/suites/nodes/NodePage.ts @@ -0,0 +1,42 @@ +import type {Locator, Page} from '@playwright/test'; + +import {PageModel} from '../../models/PageModel'; + +export class NodePage extends PageModel { + readonly tabs: Locator; + readonly threadsTab: Locator; + readonly tabletsTab: Locator; + readonly storageTab: Locator; + + constructor(page: Page, nodeId: string) { + super(page, `node/${nodeId}`); + + this.tabs = this.selector.locator('.node__tab-list'); + this.threadsTab = this.tabs.locator('[value="threads"]'); + this.tabletsTab = this.tabs.locator('[value="tablets"]'); + this.storageTab = this.tabs.locator('[value="storage"]'); + } + + async waitForNodePageLoad() { + // Wait for the page to load and tabs to be visible + await this.tabs.waitFor({state: 'visible'}); + } + + async isThreadsTabVisible() { + try { + await this.threadsTab.waitFor({state: 'visible', timeout: 1000}); + return true; + } catch { + return false; + } + } + + async clickThreadsTab() { + await this.threadsTab.click(); + } + + async getAllTabNames() { + const tabs = await this.tabs.locator('.g-tabs__item').allTextContents(); + return tabs; + } +} \ No newline at end of file diff --git a/tests/suites/nodes/nodes.test.ts b/tests/suites/nodes/nodes.test.ts index ba80e4f128..5a1bba1abb 100644 --- a/tests/suites/nodes/nodes.test.ts +++ b/tests/suites/nodes/nodes.test.ts @@ -2,6 +2,7 @@ import {expect, test} from '@playwright/test'; import {backend} from '../../utils/constants'; import {NodesPage} from '../nodes/NodesPage'; +import {NodePage} from '../nodes/NodePage'; import {ClusterNodesTable} from '../paginatedTable/paginatedTable'; test.describe('Test Nodes page', async () => { @@ -193,3 +194,153 @@ test.describe('Test Nodes Paginated Table', async () => { expect(hostValues.length).toBeGreaterThan(0); }); }); + +test.describe('Test Node Page Threads Tab', async () => { + test('Threads tab is hidden when node has no thread data', async ({page}) => { + // Mock the node API to return no thread data + await page.route(`${backend}/viewer/json/sysinfo?*`, async (route) => { + await route.fulfill({ + json: { + SystemStateInfo: [{ + Host: 'localhost', + NodeId: 1, + SystemState: 1, // Green + Version: 'test-version', + }], + // No Threads property + } + }); + }); + + const nodesPage = new NodesPage(page); + await nodesPage.goto(); + + // Get first node ID to navigate to + const paginatedTable = new ClusterNodesTable(page); + await paginatedTable.waitForTableToLoad(); + await paginatedTable.waitForTableData(); + + // Click on first node to navigate to node page + const firstRowLink = page.locator('.ydb-paginated-table__table tbody tr:first-child a').first(); + await firstRowLink.click(); + + // Wait for navigation to complete + await page.waitForURL(/\/node\/\d+/); + + const nodeId = await page.url().match(/\/node\/(\d+)/)?.[1]; + expect(nodeId).toBeDefined(); + + const nodePage = new NodePage(page, nodeId!); + await nodePage.waitForNodePageLoad(); + + // Verify threads tab is not visible + const isThreadsTabVisible = await nodePage.isThreadsTabVisible(); + expect(isThreadsTabVisible).toBe(false); + + // Verify other tabs are still visible + const tabNames = await nodePage.getAllTabNames(); + expect(tabNames).toContain('Tablets'); + expect(tabNames).not.toContain('Threads'); + }); + + test('Threads tab is visible when node has thread data', async ({page}) => { + // Mock the node API to return thread data + await page.route(`${backend}/viewer/json/sysinfo?*`, async (route) => { + await route.fulfill({ + json: { + SystemStateInfo: [{ + Host: 'localhost', + NodeId: 1, + SystemState: 1, // Green + Version: 'test-version', + }], + Threads: [{ + Name: 'TestPool', + Threads: 4 + }] + } + }); + }); + + const nodesPage = new NodesPage(page); + await nodesPage.goto(); + + // Get first node ID to navigate to + const paginatedTable = new ClusterNodesTable(page); + await paginatedTable.waitForTableToLoad(); + await paginatedTable.waitForTableData(); + + // Click on first node to navigate to node page + const firstRowLink = page.locator('.ydb-paginated-table__table tbody tr:first-child a').first(); + await firstRowLink.click(); + + // Wait for navigation to complete + await page.waitForURL(/\/node\/\d+/); + + const nodeId = await page.url().match(/\/node\/(\d+)/)?.[1]; + expect(nodeId).toBeDefined(); + + const nodePage = new NodePage(page, nodeId!); + await nodePage.waitForNodePageLoad(); + + // Verify threads tab is visible + const isThreadsTabVisible = await nodePage.isThreadsTabVisible(); + expect(isThreadsTabVisible).toBe(true); + + // Verify can click on threads tab + await nodePage.clickThreadsTab(); + await page.waitForURL(/\/node\/\d+\/threads/); + + // Verify other tabs are also visible + const tabNames = await nodePage.getAllTabNames(); + expect(tabNames).toContain('Tablets'); + expect(tabNames).toContain('Threads'); + }); + + test('Threads tab is hidden when node has empty thread array', async ({page}) => { + // Mock the node API to return empty thread data + await page.route(`${backend}/viewer/json/sysinfo?*`, async (route) => { + await route.fulfill({ + json: { + SystemStateInfo: [{ + Host: 'localhost', + NodeId: 1, + SystemState: 1, // Green + Version: 'test-version', + }], + Threads: [] // Empty array + } + }); + }); + + const nodesPage = new NodesPage(page); + await nodesPage.goto(); + + // Get first node ID to navigate to + const paginatedTable = new ClusterNodesTable(page); + await paginatedTable.waitForTableToLoad(); + await paginatedTable.waitForTableData(); + + // Click on first node to navigate to node page + const firstRowLink = page.locator('.ydb-paginated-table__table tbody tr:first-child a').first(); + await firstRowLink.click(); + + // Wait for navigation to complete + await page.waitForURL(/\/node\/\d+/); + + const nodeId = await page.url().match(/\/node\/(\d+)/)?.[1]; + expect(nodeId).toBeDefined(); + + const nodePage = new NodePage(page, nodeId!); + await nodePage.waitForNodePageLoad(); + + // Verify threads tab is not visible + const isThreadsTabVisible = await nodePage.isThreadsTabVisible(); + expect(isThreadsTabVisible).toBe(false); + + // Verify other tabs are still visible + const tabNames = await nodePage.getAllTabNames(); + expect(tabNames).toContain('Tablets'); + expect(tabNames).not.toContain('Threads'); + }); +}); From 48eecc935d7c87ababd71625653f0d4fe10668ed Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 4 Aug 2025 09:27:06 +0000 Subject: [PATCH 4/9] fix: correct linter errors and formatting issues Co-authored-by: Raubzeug <42094364+Raubzeug@users.noreply.github.com> --- src/store/reducers/node/utils.ts | 2 +- tests/suites/nodes/NodePage.ts | 2 +- tests/suites/nodes/nodes.test.ts | 118 +++++++++++++++++-------------- 3 files changed, 68 insertions(+), 54 deletions(-) diff --git a/src/store/reducers/node/utils.ts b/src/store/reducers/node/utils.ts index 9fd19faf49..be9ee56318 100644 --- a/src/store/reducers/node/utils.ts +++ b/src/store/reducers/node/utils.ts @@ -9,7 +9,7 @@ export const prepareNodeData = (data: TEvSystemStateResponse): PreparedNode => { } const nodeData = data.SystemStateInfo[0]; - + const preparedSystemState = prepareNodeSystemState(nodeData); // Include Threads from the top-level response for the tab filtering logic diff --git a/tests/suites/nodes/NodePage.ts b/tests/suites/nodes/NodePage.ts index 6e418faf6a..a1abd34596 100644 --- a/tests/suites/nodes/NodePage.ts +++ b/tests/suites/nodes/NodePage.ts @@ -39,4 +39,4 @@ export class NodePage extends PageModel { const tabs = await this.tabs.locator('.g-tabs__item').allTextContents(); return tabs; } -} \ No newline at end of file +} diff --git a/tests/suites/nodes/nodes.test.ts b/tests/suites/nodes/nodes.test.ts index 5a1bba1abb..2851cc81fb 100644 --- a/tests/suites/nodes/nodes.test.ts +++ b/tests/suites/nodes/nodes.test.ts @@ -1,8 +1,8 @@ import {expect, test} from '@playwright/test'; import {backend} from '../../utils/constants'; -import {NodesPage} from '../nodes/NodesPage'; import {NodePage} from '../nodes/NodePage'; +import {NodesPage} from '../nodes/NodesPage'; import {ClusterNodesTable} from '../paginatedTable/paginatedTable'; test.describe('Test Nodes page', async () => { @@ -201,42 +201,46 @@ test.describe('Test Node Page Threads Tab', async () => { await page.route(`${backend}/viewer/json/sysinfo?*`, async (route) => { await route.fulfill({ json: { - SystemStateInfo: [{ - Host: 'localhost', - NodeId: 1, - SystemState: 1, // Green - Version: 'test-version', - }], + SystemStateInfo: [ + { + Host: 'localhost', + NodeId: 1, + SystemState: 1, // Green + Version: 'test-version', + }, + ], // No Threads property - } + }, }); }); const nodesPage = new NodesPage(page); await nodesPage.goto(); - + // Get first node ID to navigate to const paginatedTable = new ClusterNodesTable(page); await paginatedTable.waitForTableToLoad(); await paginatedTable.waitForTableData(); - + // Click on first node to navigate to node page - const firstRowLink = page.locator('.ydb-paginated-table__table tbody tr:first-child a').first(); + const firstRowLink = page + .locator('.ydb-paginated-table__table tbody tr:first-child a') + .first(); await firstRowLink.click(); - + // Wait for navigation to complete await page.waitForURL(/\/node\/\d+/); - + const nodeId = await page.url().match(/\/node\/(\d+)/)?.[1]; expect(nodeId).toBeDefined(); - + const nodePage = new NodePage(page, nodeId!); await nodePage.waitForNodePageLoad(); - + // Verify threads tab is not visible const isThreadsTabVisible = await nodePage.isThreadsTabVisible(); expect(isThreadsTabVisible).toBe(false); - + // Verify other tabs are still visible const tabNames = await nodePage.getAllTabNames(); expect(tabNames).toContain('Tablets'); @@ -248,49 +252,55 @@ test.describe('Test Node Page Threads Tab', async () => { await page.route(`${backend}/viewer/json/sysinfo?*`, async (route) => { await route.fulfill({ json: { - SystemStateInfo: [{ - Host: 'localhost', - NodeId: 1, - SystemState: 1, // Green - Version: 'test-version', - }], - Threads: [{ - Name: 'TestPool', - Threads: 4 - }] - } + SystemStateInfo: [ + { + Host: 'localhost', + NodeId: 1, + SystemState: 1, // Green + Version: 'test-version', + }, + ], + Threads: [ + { + Name: 'TestPool', + Threads: 4, + }, + ], + }, }); }); const nodesPage = new NodesPage(page); await nodesPage.goto(); - + // Get first node ID to navigate to const paginatedTable = new ClusterNodesTable(page); await paginatedTable.waitForTableToLoad(); await paginatedTable.waitForTableData(); - + // Click on first node to navigate to node page - const firstRowLink = page.locator('.ydb-paginated-table__table tbody tr:first-child a').first(); + const firstRowLink = page + .locator('.ydb-paginated-table__table tbody tr:first-child a') + .first(); await firstRowLink.click(); - + // Wait for navigation to complete await page.waitForURL(/\/node\/\d+/); - + const nodeId = await page.url().match(/\/node\/(\d+)/)?.[1]; expect(nodeId).toBeDefined(); - + const nodePage = new NodePage(page, nodeId!); await nodePage.waitForNodePageLoad(); - + // Verify threads tab is visible const isThreadsTabVisible = await nodePage.isThreadsTabVisible(); expect(isThreadsTabVisible).toBe(true); - + // Verify can click on threads tab await nodePage.clickThreadsTab(); await page.waitForURL(/\/node\/\d+\/threads/); - + // Verify other tabs are also visible const tabNames = await nodePage.getAllTabNames(); expect(tabNames).toContain('Tablets'); @@ -302,42 +312,46 @@ test.describe('Test Node Page Threads Tab', async () => { await page.route(`${backend}/viewer/json/sysinfo?*`, async (route) => { await route.fulfill({ json: { - SystemStateInfo: [{ - Host: 'localhost', - NodeId: 1, - SystemState: 1, // Green - Version: 'test-version', - }], - Threads: [] // Empty array - } + SystemStateInfo: [ + { + Host: 'localhost', + NodeId: 1, + SystemState: 1, // Green + Version: 'test-version', + }, + ], + Threads: [], // Empty array + }, }); }); const nodesPage = new NodesPage(page); await nodesPage.goto(); - + // Get first node ID to navigate to const paginatedTable = new ClusterNodesTable(page); await paginatedTable.waitForTableToLoad(); await paginatedTable.waitForTableData(); - + // Click on first node to navigate to node page - const firstRowLink = page.locator('.ydb-paginated-table__table tbody tr:first-child a').first(); + const firstRowLink = page + .locator('.ydb-paginated-table__table tbody tr:first-child a') + .first(); await firstRowLink.click(); - + // Wait for navigation to complete await page.waitForURL(/\/node\/\d+/); - + const nodeId = await page.url().match(/\/node\/(\d+)/)?.[1]; expect(nodeId).toBeDefined(); - + const nodePage = new NodePage(page, nodeId!); await nodePage.waitForNodePageLoad(); - + // Verify threads tab is not visible const isThreadsTabVisible = await nodePage.isThreadsTabVisible(); expect(isThreadsTabVisible).toBe(false); - + // Verify other tabs are still visible const tabNames = await nodePage.getAllTabNames(); expect(tabNames).toContain('Tablets'); From 349f6ff88e60371afc37ebf02321a8d98f5fee7a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 4 Aug 2025 14:41:12 +0000 Subject: [PATCH 5/9] fix: correct typo in variable name and simplify e2e test navigation Co-authored-by: Raubzeug <42094364+Raubzeug@users.noreply.github.com> --- src/containers/Node/Node.tsx | 10 ++--- tests/suites/nodes/nodes.test.ts | 72 ++++---------------------------- 2 files changed, 14 insertions(+), 68 deletions(-) diff --git a/src/containers/Node/Node.tsx b/src/containers/Node/Node.tsx index 482fb1e960..1b2411a4c4 100644 --- a/src/containers/Node/Node.tsx +++ b/src/containers/Node/Node.tsx @@ -69,21 +69,21 @@ export function Node() { const isStorageNode = node?.Roles?.find((el) => el === STORAGE_ROLE); const {activeTab, nodeTabs} = React.useMemo(() => { - let actulaNodeTabs = isStorageNode + let actualNodeTabs = isStorageNode ? NODE_TABS : NODE_TABS.filter((el) => el.id !== 'storage'); if (isDiskPagesAvailable) { - actulaNodeTabs = actulaNodeTabs.filter((el) => el.id !== 'structure'); + actualNodeTabs = actualNodeTabs.filter((el) => el.id !== 'structure'); } // Filter out threads tab if there's no thread data in the API response if (!node?.Threads || node.Threads.length === 0) { - actulaNodeTabs = actulaNodeTabs.filter((el) => el.id !== 'threads'); + actualNodeTabs = actualNodeTabs.filter((el) => el.id !== 'threads'); } const actualActiveTab = - actulaNodeTabs.find(({id}) => id === activeTabId) ?? actulaNodeTabs[0]; + actualNodeTabs.find(({id}) => id === activeTabId) ?? actualNodeTabs[0]; - return {activeTab: actualActiveTab, nodeTabs: actulaNodeTabs}; + return {activeTab: actualActiveTab, nodeTabs: actualNodeTabs}; }, [isStorageNode, isDiskPagesAvailable, activeTabId, node?.Threads]); const tenantName = node?.Tenants?.[0] || tenantNameFromQuery?.toString(); diff --git a/tests/suites/nodes/nodes.test.ts b/tests/suites/nodes/nodes.test.ts index 2851cc81fb..802baf757c 100644 --- a/tests/suites/nodes/nodes.test.ts +++ b/tests/suites/nodes/nodes.test.ts @@ -214,27 +214,9 @@ test.describe('Test Node Page Threads Tab', async () => { }); }); - const nodesPage = new NodesPage(page); - await nodesPage.goto(); - - // Get first node ID to navigate to - const paginatedTable = new ClusterNodesTable(page); - await paginatedTable.waitForTableToLoad(); - await paginatedTable.waitForTableData(); - - // Click on first node to navigate to node page - const firstRowLink = page - .locator('.ydb-paginated-table__table tbody tr:first-child a') - .first(); - await firstRowLink.click(); - - // Wait for navigation to complete - await page.waitForURL(/\/node\/\d+/); - - const nodeId = await page.url().match(/\/node\/(\d+)/)?.[1]; - expect(nodeId).toBeDefined(); - - const nodePage = new NodePage(page, nodeId!); + // Navigate directly to node page + const nodePage = new NodePage(page, '1'); + await nodePage.goto(); await nodePage.waitForNodePageLoad(); // Verify threads tab is not visible @@ -270,27 +252,9 @@ test.describe('Test Node Page Threads Tab', async () => { }); }); - const nodesPage = new NodesPage(page); - await nodesPage.goto(); - - // Get first node ID to navigate to - const paginatedTable = new ClusterNodesTable(page); - await paginatedTable.waitForTableToLoad(); - await paginatedTable.waitForTableData(); - - // Click on first node to navigate to node page - const firstRowLink = page - .locator('.ydb-paginated-table__table tbody tr:first-child a') - .first(); - await firstRowLink.click(); - - // Wait for navigation to complete - await page.waitForURL(/\/node\/\d+/); - - const nodeId = await page.url().match(/\/node\/(\d+)/)?.[1]; - expect(nodeId).toBeDefined(); - - const nodePage = new NodePage(page, nodeId!); + // Navigate directly to node page + const nodePage = new NodePage(page, '1'); + await nodePage.goto(); await nodePage.waitForNodePageLoad(); // Verify threads tab is visible @@ -325,27 +289,9 @@ test.describe('Test Node Page Threads Tab', async () => { }); }); - const nodesPage = new NodesPage(page); - await nodesPage.goto(); - - // Get first node ID to navigate to - const paginatedTable = new ClusterNodesTable(page); - await paginatedTable.waitForTableToLoad(); - await paginatedTable.waitForTableData(); - - // Click on first node to navigate to node page - const firstRowLink = page - .locator('.ydb-paginated-table__table tbody tr:first-child a') - .first(); - await firstRowLink.click(); - - // Wait for navigation to complete - await page.waitForURL(/\/node\/\d+/); - - const nodeId = await page.url().match(/\/node\/(\d+)/)?.[1]; - expect(nodeId).toBeDefined(); - - const nodePage = new NodePage(page, nodeId!); + // Navigate directly to node page + const nodePage = new NodePage(page, '1'); + await nodePage.goto(); await nodePage.waitForNodePageLoad(); // Verify threads tab is not visible From 844fd7aa92fadbeb3abb9a66deac6f06c6ea3fc3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 4 Aug 2025 14:49:03 +0000 Subject: [PATCH 6/9] Changes before error encountered Co-authored-by: Raubzeug <42094364+Raubzeug@users.noreply.github.com> --- tests/suites/nodes/NodePage.ts | 20 ++++++++++++++++---- tests/suites/nodes/nodes.test.ts | 27 +++++++++++++++++++++------ 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/tests/suites/nodes/NodePage.ts b/tests/suites/nodes/NodePage.ts index a1abd34596..2cc7983fdd 100644 --- a/tests/suites/nodes/NodePage.ts +++ b/tests/suites/nodes/NodePage.ts @@ -19,24 +19,36 @@ export class NodePage extends PageModel { async waitForNodePageLoad() { // Wait for the page to load and tabs to be visible - await this.tabs.waitFor({state: 'visible'}); + try { + await this.tabs.waitFor({state: 'visible', timeout: 10000}); + console.log('Node page tabs loaded successfully'); + } catch (error) { + console.error('Failed to load node page tabs:', error); + throw error; + } } async isThreadsTabVisible() { + const threadsTab = this.tabs.locator('.g-tab:has-text("Threads")'); try { - await this.threadsTab.waitFor({state: 'visible', timeout: 1000}); + await threadsTab.waitFor({state: 'visible', timeout: 2000}); + console.log('Threads tab is visible'); return true; } catch { + console.log('Threads tab is not visible'); return false; } } async clickThreadsTab() { - await this.threadsTab.click(); + const threadsTab = this.tabs.locator('.g-tab:has-text("Threads")'); + console.log('Clicking threads tab'); + await threadsTab.click(); } async getAllTabNames() { - const tabs = await this.tabs.locator('.g-tabs__item').allTextContents(); + const tabs = await this.tabs.locator('.g-tab').allTextContents(); + console.log('Available tabs:', tabs); return tabs; } } diff --git a/tests/suites/nodes/nodes.test.ts b/tests/suites/nodes/nodes.test.ts index 802baf757c..daacda74ae 100644 --- a/tests/suites/nodes/nodes.test.ts +++ b/tests/suites/nodes/nodes.test.ts @@ -199,8 +199,13 @@ test.describe('Test Node Page Threads Tab', async () => { test('Threads tab is hidden when node has no thread data', async ({page}) => { // Mock the node API to return no thread data await page.route(`${backend}/viewer/json/sysinfo?*`, async (route) => { + const url = route.request().url(); + console.log('Intercepted sysinfo request:', url); + await route.fulfill({ - json: { + status: 200, + contentType: 'application/json', + body: JSON.stringify({ SystemStateInfo: [ { Host: 'localhost', @@ -210,7 +215,7 @@ test.describe('Test Node Page Threads Tab', async () => { }, ], // No Threads property - }, + }), }); }); @@ -232,8 +237,13 @@ test.describe('Test Node Page Threads Tab', async () => { test('Threads tab is visible when node has thread data', async ({page}) => { // Mock the node API to return thread data await page.route(`${backend}/viewer/json/sysinfo?*`, async (route) => { + const url = route.request().url(); + console.log('Intercepted sysinfo request:', url); + await route.fulfill({ - json: { + status: 200, + contentType: 'application/json', + body: JSON.stringify({ SystemStateInfo: [ { Host: 'localhost', @@ -248,7 +258,7 @@ test.describe('Test Node Page Threads Tab', async () => { Threads: 4, }, ], - }, + }), }); }); @@ -274,8 +284,13 @@ test.describe('Test Node Page Threads Tab', async () => { test('Threads tab is hidden when node has empty thread array', async ({page}) => { // Mock the node API to return empty thread data await page.route(`${backend}/viewer/json/sysinfo?*`, async (route) => { + const url = route.request().url(); + console.log('Intercepted sysinfo request:', url); + await route.fulfill({ - json: { + status: 200, + contentType: 'application/json', + body: JSON.stringify({ SystemStateInfo: [ { Host: 'localhost', @@ -285,7 +300,7 @@ test.describe('Test Node Page Threads Tab', async () => { }, ], Threads: [], // Empty array - }, + }), }); }); From 2123638ff6b59290818cc6bf9ea106951949ff8d Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Mon, 4 Aug 2025 20:12:36 +0300 Subject: [PATCH 7/9] fixes --- src/containers/Node/Node.tsx | 6 ++++-- src/store/reducers/node/types.ts | 5 +---- src/store/reducers/node/utils.ts | 8 +------- tests/suites/nodes/NodePage.ts | 11 ++++------- tests/suites/nodes/nodes.test.ts | 23 +++++++---------------- 5 files changed, 17 insertions(+), 36 deletions(-) diff --git a/src/containers/Node/Node.tsx b/src/containers/Node/Node.tsx index 1b2411a4c4..3a33fa0cc9 100644 --- a/src/containers/Node/Node.tsx +++ b/src/containers/Node/Node.tsx @@ -68,6 +68,8 @@ export function Node() { const isStorageNode = node?.Roles?.find((el) => el === STORAGE_ROLE); + const threadsQuantity = node?.Threads?.length; + const {activeTab, nodeTabs} = React.useMemo(() => { let actualNodeTabs = isStorageNode ? NODE_TABS @@ -76,7 +78,7 @@ export function Node() { actualNodeTabs = actualNodeTabs.filter((el) => el.id !== 'structure'); } // Filter out threads tab if there's no thread data in the API response - if (!node?.Threads || node.Threads.length === 0) { + if (!threadsQuantity) { actualNodeTabs = actualNodeTabs.filter((el) => el.id !== 'threads'); } @@ -84,7 +86,7 @@ export function Node() { actualNodeTabs.find(({id}) => id === activeTabId) ?? actualNodeTabs[0]; return {activeTab: actualActiveTab, nodeTabs: actualNodeTabs}; - }, [isStorageNode, isDiskPagesAvailable, activeTabId, node?.Threads]); + }, [isStorageNode, isDiskPagesAvailable, activeTabId, threadsQuantity]); const tenantName = node?.Tenants?.[0] || tenantNameFromQuery?.toString(); diff --git a/src/store/reducers/node/types.ts b/src/store/reducers/node/types.ts index 2b727c0a9d..ef7df529e3 100644 --- a/src/store/reducers/node/types.ts +++ b/src/store/reducers/node/types.ts @@ -1,4 +1,3 @@ -import type {TThreadPoolInfo} from '../../../types/api/threads'; import type {PreparedPDisk, PreparedVDisk} from '../../../utils/disks/types'; import type {PreparedNodeSystemState} from '../../../utils/nodes'; @@ -19,6 +18,4 @@ export interface PreparedStructurePDisk extends PreparedPDisk { export type PreparedNodeStructure = Record; -export interface PreparedNode extends Partial { - Threads?: TThreadPoolInfo[]; -} +export interface PreparedNode extends Partial {} diff --git a/src/store/reducers/node/utils.ts b/src/store/reducers/node/utils.ts index be9ee56318..6be190f825 100644 --- a/src/store/reducers/node/utils.ts +++ b/src/store/reducers/node/utils.ts @@ -10,11 +10,5 @@ export const prepareNodeData = (data: TEvSystemStateResponse): PreparedNode => { const nodeData = data.SystemStateInfo[0]; - const preparedSystemState = prepareNodeSystemState(nodeData); - - // Include Threads from the top-level response for the tab filtering logic - return { - ...preparedSystemState, - Threads: data.Threads, - }; + return prepareNodeSystemState(nodeData); }; diff --git a/tests/suites/nodes/NodePage.ts b/tests/suites/nodes/NodePage.ts index 2cc7983fdd..48c5772fd6 100644 --- a/tests/suites/nodes/NodePage.ts +++ b/tests/suites/nodes/NodePage.ts @@ -1,6 +1,7 @@ import type {Locator, Page} from '@playwright/test'; import {PageModel} from '../../models/PageModel'; +import {VISIBILITY_TIMEOUT} from '../tenant/TenantPage'; export class NodePage extends PageModel { readonly tabs: Locator; @@ -20,8 +21,7 @@ export class NodePage extends PageModel { async waitForNodePageLoad() { // Wait for the page to load and tabs to be visible try { - await this.tabs.waitFor({state: 'visible', timeout: 10000}); - console.log('Node page tabs loaded successfully'); + await this.tabs.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT}); } catch (error) { console.error('Failed to load node page tabs:', error); throw error; @@ -31,24 +31,21 @@ export class NodePage extends PageModel { async isThreadsTabVisible() { const threadsTab = this.tabs.locator('.g-tab:has-text("Threads")'); try { - await threadsTab.waitFor({state: 'visible', timeout: 2000}); - console.log('Threads tab is visible'); + await threadsTab.waitFor({state: 'visible'}); return true; } catch { - console.log('Threads tab is not visible'); + console.error('Threads tab is not visible'); return false; } } async clickThreadsTab() { const threadsTab = this.tabs.locator('.g-tab:has-text("Threads")'); - console.log('Clicking threads tab'); await threadsTab.click(); } async getAllTabNames() { const tabs = await this.tabs.locator('.g-tab').allTextContents(); - console.log('Available tabs:', tabs); return tabs; } } diff --git a/tests/suites/nodes/nodes.test.ts b/tests/suites/nodes/nodes.test.ts index daacda74ae..496655bbb9 100644 --- a/tests/suites/nodes/nodes.test.ts +++ b/tests/suites/nodes/nodes.test.ts @@ -196,12 +196,9 @@ test.describe('Test Nodes Paginated Table', async () => { }); test.describe('Test Node Page Threads Tab', async () => { - test('Threads tab is hidden when node has no thread data', async ({page}) => { + test.only('Threads tab is hidden when node has no thread data', async ({page}) => { // Mock the node API to return no thread data - await page.route(`${backend}/viewer/json/sysinfo?*`, async (route) => { - const url = route.request().url(); - console.log('Intercepted sysinfo request:', url); - + await page.route(`**/viewer/json/sysinfo?*`, async (route) => { await route.fulfill({ status: 200, contentType: 'application/json', @@ -210,7 +207,7 @@ test.describe('Test Node Page Threads Tab', async () => { { Host: 'localhost', NodeId: 1, - SystemState: 1, // Green + SystemState: 'Green', Version: 'test-version', }, ], @@ -236,10 +233,7 @@ test.describe('Test Node Page Threads Tab', async () => { test('Threads tab is visible when node has thread data', async ({page}) => { // Mock the node API to return thread data - await page.route(`${backend}/viewer/json/sysinfo?*`, async (route) => { - const url = route.request().url(); - console.log('Intercepted sysinfo request:', url); - + await page.route(`**/viewer/json/sysinfo?*`, async (route) => { await route.fulfill({ status: 200, contentType: 'application/json', @@ -248,7 +242,7 @@ test.describe('Test Node Page Threads Tab', async () => { { Host: 'localhost', NodeId: 1, - SystemState: 1, // Green + SystemState: 'Green', Version: 'test-version', }, ], @@ -283,10 +277,7 @@ test.describe('Test Node Page Threads Tab', async () => { test('Threads tab is hidden when node has empty thread array', async ({page}) => { // Mock the node API to return empty thread data - await page.route(`${backend}/viewer/json/sysinfo?*`, async (route) => { - const url = route.request().url(); - console.log('Intercepted sysinfo request:', url); - + await page.route(`**/viewer/json/sysinfo?*`, async (route) => { await route.fulfill({ status: 200, contentType: 'application/json', @@ -295,7 +286,7 @@ test.describe('Test Node Page Threads Tab', async () => { { Host: 'localhost', NodeId: 1, - SystemState: 1, // Green + SystemState: 'Green', Version: 'test-version', }, ], From adc8545ec874585a3e2d2db1cdfdee8a6f5af253 Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Mon, 4 Aug 2025 20:33:24 +0300 Subject: [PATCH 8/9] fix: tests --- tests/suites/nodes/nodes.test.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/tests/suites/nodes/nodes.test.ts b/tests/suites/nodes/nodes.test.ts index 496655bbb9..da94b1f2e3 100644 --- a/tests/suites/nodes/nodes.test.ts +++ b/tests/suites/nodes/nodes.test.ts @@ -196,7 +196,7 @@ test.describe('Test Nodes Paginated Table', async () => { }); test.describe('Test Node Page Threads Tab', async () => { - test.only('Threads tab is hidden when node has no thread data', async ({page}) => { + test('Threads tab is hidden when node has no thread data', async ({page}) => { // Mock the node API to return no thread data await page.route(`**/viewer/json/sysinfo?*`, async (route) => { await route.fulfill({ @@ -221,10 +221,6 @@ test.describe('Test Node Page Threads Tab', async () => { await nodePage.goto(); await nodePage.waitForNodePageLoad(); - // Verify threads tab is not visible - const isThreadsTabVisible = await nodePage.isThreadsTabVisible(); - expect(isThreadsTabVisible).toBe(false); - // Verify other tabs are still visible const tabNames = await nodePage.getAllTabNames(); expect(tabNames).toContain('Tablets'); @@ -300,10 +296,6 @@ test.describe('Test Node Page Threads Tab', async () => { await nodePage.goto(); await nodePage.waitForNodePageLoad(); - // Verify threads tab is not visible - const isThreadsTabVisible = await nodePage.isThreadsTabVisible(); - expect(isThreadsTabVisible).toBe(false); - // Verify other tabs are still visible const tabNames = await nodePage.getAllTabNames(); expect(tabNames).toContain('Tablets'); From 4357d84228f2b54c6c3faf58be0bd34618bc2fea Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Tue, 5 Aug 2025 08:18:01 +0300 Subject: [PATCH 9/9] fix: tests --- tests/suites/nodes/NodePage.ts | 2 +- tests/suites/nodes/nodes.test.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/suites/nodes/NodePage.ts b/tests/suites/nodes/NodePage.ts index 48c5772fd6..5aa10bc239 100644 --- a/tests/suites/nodes/NodePage.ts +++ b/tests/suites/nodes/NodePage.ts @@ -31,7 +31,7 @@ export class NodePage extends PageModel { async isThreadsTabVisible() { const threadsTab = this.tabs.locator('.g-tab:has-text("Threads")'); try { - await threadsTab.waitFor({state: 'visible'}); + await threadsTab.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT}); return true; } catch { console.error('Threads tab is not visible'); diff --git a/tests/suites/nodes/nodes.test.ts b/tests/suites/nodes/nodes.test.ts index da94b1f2e3..e2b0a65413 100644 --- a/tests/suites/nodes/nodes.test.ts +++ b/tests/suites/nodes/nodes.test.ts @@ -240,12 +240,12 @@ test.describe('Test Node Page Threads Tab', async () => { NodeId: 1, SystemState: 'Green', Version: 'test-version', - }, - ], - Threads: [ - { - Name: 'TestPool', - Threads: 4, + Threads: [ + { + Name: 'TestPool', + Threads: 4, + }, + ], }, ], }),