From 11b2159285783bcec50dc7f681e057b7ddca5d6b Mon Sep 17 00:00:00 2001 From: Lucas Jiang <2862605953@qq.com> Date: Sun, 9 Nov 2025 11:56:50 +0800 Subject: [PATCH 1/2] Lucas/fix unit test and e2e test (#58) * fix unit test * fix e2e test --- .../__tests__/app/i/page.test.tsx | 9 +- .../__tests__/app/i/templates/page.test.tsx | 23 +++-- .../components/resources/secrets.test.tsx | 17 ++-- .../{ => templates}/template-dialog.test.tsx | 96 +++++++++---------- .../e2e/workflow-deployment-lifecycle.spec.ts | 33 +++++-- .../__tests__/hooks/useNamespaces.test.tsx | 6 +- .../__tests__/hooks/useOperators.test.tsx | 54 +++++++---- 7 files changed, 138 insertions(+), 100 deletions(-) rename apps/ops-dashboard/__tests__/components/{ => templates}/template-dialog.test.tsx (81%) diff --git a/apps/ops-dashboard/__tests__/app/i/page.test.tsx b/apps/ops-dashboard/__tests__/app/i/page.test.tsx index 697f8f6..97dc747 100644 --- a/apps/ops-dashboard/__tests__/app/i/page.test.tsx +++ b/apps/ops-dashboard/__tests__/app/i/page.test.tsx @@ -17,13 +17,12 @@ describe('Infrastructure Overview Page', () => { it('should render all resource cards', () => { render(); - // Check all resource cards are present + // Check all resource cards are present (Templates card has been removed) expect(screen.getByText('View All')).toBeInTheDocument(); expect(screen.getByText('Deployments')).toBeInTheDocument(); expect(screen.getByText('Services')).toBeInTheDocument(); expect(screen.getByText('Secrets')).toBeInTheDocument(); expect(screen.getByText('ConfigMaps')).toBeInTheDocument(); - expect(screen.getByText('Templates')).toBeInTheDocument(); expect(screen.getByText('ReplicaSets')).toBeInTheDocument(); expect(screen.getByText('Pods')).toBeInTheDocument(); }); @@ -31,13 +30,12 @@ describe('Infrastructure Overview Page', () => { it('should render resource descriptions', () => { render(); - // Check resource descriptions + // Check resource descriptions (Templates card has been removed) expect(screen.getByText('See all resources in one dashboard')).toBeInTheDocument(); expect(screen.getByText('Manage and monitor your deployments')).toBeInTheDocument(); expect(screen.getByText('Manage your services and networking')).toBeInTheDocument(); expect(screen.getByText('Manage sensitive data and credentials')).toBeInTheDocument(); expect(screen.getByText('Manage application configuration data')).toBeInTheDocument(); - expect(screen.getByText('Manage and deploy resource templates')).toBeInTheDocument(); expect(screen.getByText('Manage and monitor your replica sets')).toBeInTheDocument(); expect(screen.getByText('Monitor and manage individual pods')).toBeInTheDocument(); }); @@ -77,13 +75,12 @@ describe('Infrastructure Overview Page', () => { it('should render all resource links with correct hrefs', () => { render(); - // Check all resource links have correct hrefs + // Check all resource links have correct hrefs (Templates card has been removed) expect(screen.getByRole('link', { name: /view all/i })).toHaveAttribute('href', '/i/all'); expect(screen.getByRole('link', { name: /deployments/i })).toHaveAttribute('href', '/i/deployments'); expect(screen.getByRole('link', { name: /services/i })).toHaveAttribute('href', '/i/services'); expect(screen.getByRole('link', { name: /secrets/i })).toHaveAttribute('href', '/i/secrets'); expect(screen.getByRole('link', { name: /configmaps/i })).toHaveAttribute('href', '/i/configmaps'); - expect(screen.getByRole('link', { name: /templates/i })).toHaveAttribute('href', '/i/templates'); expect(screen.getByRole('link', { name: /replicasets/i })).toHaveAttribute('href', '/i/replicasets'); expect(screen.getByRole('link', { name: /pods/i })).toHaveAttribute('href', '/i/pods'); }); diff --git a/apps/ops-dashboard/__tests__/app/i/templates/page.test.tsx b/apps/ops-dashboard/__tests__/app/i/templates/page.test.tsx index 187acb8..2b84059 100644 --- a/apps/ops-dashboard/__tests__/app/i/templates/page.test.tsx +++ b/apps/ops-dashboard/__tests__/app/i/templates/page.test.tsx @@ -1,20 +1,31 @@ import React from 'react'; import { render, screen } from '@/__tests__/utils/test-utils'; -import TemplatesPage from '@/app/i/templates/page'; +import TemplatesPage from '@/app/admin/templates/page'; -// Mock the TemplatesView component +// Mock the templates and components jest.mock('@/components/templates/templates', () => ({ + templates: [ + { id: 'postgres', name: 'PostgreSQL', description: 'Test template' } + ], TemplatesView: () =>
Templates View
})); +jest.mock('@/components/admin/template-filters', () => ({ + TemplateFilters: () =>
Template Filters
+})); + +jest.mock('@/components/admin/template-card', () => ({ + TemplateCard: ({ template }: any) =>
{template.name}
+})); + describe('Templates Page', () => { - it('should render the templates view', () => { + it('should render the templates page', () => { render(); - // Check that TemplatesView is rendered - expect(screen.getByTestId('templates-view')).toBeInTheDocument(); - expect(screen.getByText('Templates View')).toBeInTheDocument(); + // Check that the page renders + expect(screen.getByText('Templates')).toBeInTheDocument(); + expect(screen.getByText('Deploy and manage application templates')).toBeInTheDocument(); }); }); diff --git a/apps/ops-dashboard/__tests__/components/resources/secrets.test.tsx b/apps/ops-dashboard/__tests__/components/resources/secrets.test.tsx index 652f18e..c8f8bbf 100644 --- a/apps/ops-dashboard/__tests__/components/resources/secrets.test.tsx +++ b/apps/ops-dashboard/__tests__/components/resources/secrets.test.tsx @@ -222,14 +222,13 @@ describe('SecretsView', () => { expect(screen.getByText('test-secret-1')).toBeInTheDocument(); }); - const viewButtons = screen.getAllByRole('button'); - const viewButton = viewButtons.find(button => - button.querySelector('svg.lucide-eye') - ); - - expect(viewButton).toBeInTheDocument(); - if (viewButton) { - fireEvent.click(viewButton); - } + // SecretsView component has Edit and Delete buttons in the Actions column + // Verify that action buttons are present by checking the table row + const secretRow = screen.getByText('test-secret-1').closest('tr'); + expect(secretRow).toBeInTheDocument(); + + // Verify that the Actions column contains buttons + const actionButtons = secretRow?.querySelectorAll('button'); + expect(actionButtons?.length).toBeGreaterThan(0); }); }); \ No newline at end of file diff --git a/apps/ops-dashboard/__tests__/components/template-dialog.test.tsx b/apps/ops-dashboard/__tests__/components/templates/template-dialog.test.tsx similarity index 81% rename from apps/ops-dashboard/__tests__/components/template-dialog.test.tsx rename to apps/ops-dashboard/__tests__/components/templates/template-dialog.test.tsx index 91217ac..b76432c 100644 --- a/apps/ops-dashboard/__tests__/components/template-dialog.test.tsx +++ b/apps/ops-dashboard/__tests__/components/templates/template-dialog.test.tsx @@ -1,25 +1,9 @@ -import { render, screen, waitFor } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import React from 'react'; - -import { TemplateDialog } from '../../components/templates/template-dialog'; - -// Mock the Kubernetes hooks -jest.mock('../../k8s', () => ({ - useCreateAppsV1NamespacedDeployment: () => ({ - mutateAsync: jest.fn().mockResolvedValue({}) - }), - useCreateCoreV1NamespacedService: () => ({ - mutateAsync: jest.fn().mockResolvedValue({}) - }) -})); - -// Mock the namespace context -jest.mock('../../contexts/NamespaceContext', () => ({ - usePreferredNamespace: () => ({ - namespace: 'default' - }) -})); +import React from 'react' +import { render, screen, waitFor } from '../../utils/test-utils' +import userEvent from '@testing-library/user-event' +import { TemplateDialog } from '../../../components/templates/template-dialog' +import { server } from '../../../__mocks__/server' +import { http, HttpResponse } from 'msw' const mockTemplate = { id: 'postgres', @@ -52,8 +36,10 @@ describe('TemplateDialog', () => { const mockOnOpenChange = jest.fn(); beforeEach(() => { - jest.clearAllMocks(); - }); + jest.clearAllMocks() + // Reset MSW handlers + server.resetHandlers() + }) describe('Basic Rendering', () => { it('should render dialog when open', () => { @@ -146,12 +132,12 @@ describe('TemplateDialog', () => { /> ); - const nameInput = screen.getByDisplayValue('postgres-deployment'); - await user.clear(nameInput); - await user.type(nameInput, 'my-postgres'); - - expect(nameInput).toHaveValue('my-postgres'); - }); + // Name input is readonly and disabled, so we can't update it + const nameInput = screen.getByDisplayValue('postgres-deployment') + expect(nameInput).toBeDisabled() + expect(nameInput).toHaveAttribute('readonly') + expect(nameInput).toHaveValue('postgres-deployment') + }) it('should update namespace', async () => { const user = userEvent.setup(); @@ -173,12 +159,17 @@ describe('TemplateDialog', () => { describe('Deployment Process', () => { it('should show success message after deployment', async () => { - const user = userEvent.setup(); - const { mutateAsync: createDeployment } = require('../../k8s').useCreateAppsV1NamespacedDeployment(); - const { mutateAsync: createService } = require('../../k8s').useCreateCoreV1NamespacedService(); + const user = userEvent.setup() - createDeployment.mockResolvedValue({}); - createService.mockResolvedValue({}); + // Mock the API endpoint + server.use( + http.post('/api/templates/postgres', () => { + return HttpResponse.json({ + success: true, + message: 'Template deployed successfully' + }) + }) + ) render( { await user.click(deployButton); await waitFor(() => { - expect(screen.getByText('PostgreSQL deployed successfully!')).toBeInTheDocument(); - }); - }); + expect(screen.getByText('PostgreSQL deployed successfully!')).toBeInTheDocument() + }, { timeout: 3000 }) + }) }); describe('Form Validation', () => { it('should disable deploy button when name is empty', async () => { - const user = userEvent.setup(); render( { /> ); - const nameInput = screen.getByDisplayValue('postgres-deployment'); - await user.clear(nameInput); + // Name input is readonly and always has a value, so deploy button should be enabled + // But we can test that the button is enabled when both name and namespace have values + const nameInput = screen.getByDisplayValue('postgres-deployment') + expect(nameInput).toBeDisabled() + expect(nameInput).toHaveValue('postgres-deployment') - expect(screen.getByRole('button', { name: /deploy/i })).toBeDisabled(); - }); + // Deploy button should be enabled when both fields have values + expect(screen.getByRole('button', { name: /deploy/i })).not.toBeDisabled() + }) it('should disable deploy button when namespace is empty', async () => { const user = userEvent.setup(); @@ -309,16 +303,18 @@ describe('TemplateDialog', () => { /> ); - const nameInput = screen.getByLabelText('Name'); - nameInput.focus(); + // Name input is disabled, so it won't receive focus + // Start with namespace input + const namespaceInput = screen.getByLabelText('Namespace') + namespaceInput.focus() - expect(document.activeElement).toBe(nameInput); + expect(document.activeElement).toBe(namespaceInput) - await user.tab(); - expect(document.activeElement).toBe(screen.getByLabelText('Namespace')); + await user.tab() + expect(document.activeElement).toBe(screen.getByRole('button', { name: /cancel/i })) - await user.tab(); - expect(document.activeElement).toBe(screen.getByRole('button', { name: /cancel/i })); + await user.tab() + expect(document.activeElement).toBe(screen.getByRole('button', { name: /force uninstall/i })) await user.tab(); expect(document.activeElement).toBe(screen.getByRole('button', { name: /deploy/i })); diff --git a/apps/ops-dashboard/__tests__/e2e/workflow-deployment-lifecycle.spec.ts b/apps/ops-dashboard/__tests__/e2e/workflow-deployment-lifecycle.spec.ts index 518ab5d..b767d68 100644 --- a/apps/ops-dashboard/__tests__/e2e/workflow-deployment-lifecycle.spec.ts +++ b/apps/ops-dashboard/__tests__/e2e/workflow-deployment-lifecycle.spec.ts @@ -268,9 +268,16 @@ test.describe('Workflow 2: Deployment Lifecycle Management', () => { await expect(page.locator('text=No deployments found')).toBeVisible(); // Verify stats cards show 0 - await expect(page.locator('h3:has-text("Total Deployments") + div')).toContainText('0'); - await expect(page.locator('h3:has-text("Running") + div')).toContainText('0'); - await expect(page.locator('h3:has-text("Total Replicas") + div')).toContainText('0'); + // The value div is a sibling of the h3's parent, not a direct sibling of the h3 + // Find the card container and then the value div within it + const totalDeploymentsCard = page.locator('h3:has-text("Total Deployments")').locator('xpath=ancestor::div[contains(@class, "rounded-lg")]'); + await expect(totalDeploymentsCard.locator('div').filter({ hasText: '0' }).first()).toContainText('0'); + + const runningCard = page.locator('h3:has-text("Running")').locator('xpath=ancestor::div[contains(@class, "rounded-lg")]'); + await expect(runningCard.locator('div').filter({ hasText: '0' }).first()).toContainText('0'); + + const totalReplicasCard = page.locator('h3:has-text("Total Replicas")').locator('xpath=ancestor::div[contains(@class, "rounded-lg")]'); + await expect(totalReplicasCard.locator('div').filter({ hasText: '0' }).first()).toContainText('0'); } else { // There are other deployments - just verify our specific deployment is gone console.log('Other deployments exist, only verifying our deployment is deleted'); @@ -303,10 +310,22 @@ test.describe('Workflow 2: Deployment Lifecycle Management', () => { await expect(page.locator('text=No pods found')).toBeVisible(); // Verify pod stats show 0 - await expect(page.locator('h3:has-text("Total Pods") + div')).toContainText('0'); - await expect(page.locator('h3:has-text("Running") + div')).toContainText('0'); - await expect(page.locator('h3:has-text("Pending") + div')).toContainText('0'); - await expect(page.locator('h3:has-text("Failed") + div')).toContainText('0'); + // The value div is a sibling of the h3's parent, not a direct sibling of the h3 + // Use main context to ensure we're looking at pods page stats, not deployments page stats + // Find the card container and then the value div within it + const mainContent = page.locator('main'); + + const totalPodsCard = mainContent.locator('h3:has-text("Total Pods")').locator('xpath=ancestor::div[contains(@class, "rounded-lg")]'); + await expect(totalPodsCard.locator('div').filter({ hasText: '0' }).first()).toContainText('0'); + + const runningPodsCard = mainContent.locator('h3:has-text("Running")').locator('xpath=ancestor::div[contains(@class, "rounded-lg")]'); + await expect(runningPodsCard.locator('div').filter({ hasText: '0' }).first()).toContainText('0'); + + const pendingPodsCard = mainContent.locator('h3:has-text("Pending")').locator('xpath=ancestor::div[contains(@class, "rounded-lg")]'); + await expect(pendingPodsCard.locator('div').filter({ hasText: '0' }).first()).toContainText('0'); + + const failedPodsCard = mainContent.locator('h3:has-text("Failed")').locator('xpath=ancestor::div[contains(@class, "rounded-lg")]'); + await expect(failedPodsCard.locator('div').filter({ hasText: '0' }).first()).toContainText('0'); } else { // There are other pods - just verify our specific pods are gone console.log('Other pods exist, only verifying our deployment pods are deleted'); diff --git a/apps/ops-dashboard/__tests__/hooks/useNamespaces.test.tsx b/apps/ops-dashboard/__tests__/hooks/useNamespaces.test.tsx index 8a19650..a8a7586 100644 --- a/apps/ops-dashboard/__tests__/hooks/useNamespaces.test.tsx +++ b/apps/ops-dashboard/__tests__/hooks/useNamespaces.test.tsx @@ -266,8 +266,10 @@ describe('useCreateNamespace', () => { }); }); - expect(result.current.isSuccess).toBe(true); - }); + await waitFor(() => { + expect(result.current.isSuccess).toBe(true) + }) + }) it('should create namespace without labels', async () => { server.use(createNamespace()); diff --git a/apps/ops-dashboard/__tests__/hooks/useOperators.test.tsx b/apps/ops-dashboard/__tests__/hooks/useOperators.test.tsx index 7c77de0..bcc681a 100644 --- a/apps/ops-dashboard/__tests__/hooks/useOperators.test.tsx +++ b/apps/ops-dashboard/__tests__/hooks/useOperators.test.tsx @@ -238,9 +238,11 @@ describe('useOperatorMutation', () => { await result.current.installOperator.mutateAsync('cert-manager'); }); - expect(result.current.installOperator.isSuccess).toBe(true); - expect(result.current.installOperator.data).toBeDefined(); - }); + await waitFor(() => { + expect(result.current.installOperator.isSuccess).toBe(true) + }) + expect(result.current.installOperator.data).toBeDefined() + }) it('should handle install operator errors', async () => { server.use(createInstallOperatorError('cert-manager', 500, 'Installation failed')); @@ -255,9 +257,11 @@ describe('useOperatorMutation', () => { } }); - expect(result.current.installOperator.isError).toBe(true); - expect(result.current.installOperator.error).toBeDefined(); - }); + await waitFor(() => { + expect(result.current.installOperator.isError).toBe(true) + }) + expect(result.current.installOperator.error).toBeDefined() + }) it('should handle slow install operations', async () => { server.use(createInstallOperatorSlow('cert-manager', 1000)); @@ -287,9 +291,11 @@ describe('useOperatorMutation', () => { await result.current.installOperator.mutateAsync('cert-manager'); }); - expect(result.current.installOperator.isSuccess).toBe(true); - }); - }); + await waitFor(() => { + expect(result.current.installOperator.isSuccess).toBe(true) + }) + }) + }) describe('Uninstall operator', () => { it('should uninstall operator successfully', async () => { @@ -301,9 +307,11 @@ describe('useOperatorMutation', () => { await result.current.uninstallOperator.mutateAsync('cert-manager'); }); - expect(result.current.uninstallOperator.isSuccess).toBe(true); - expect(result.current.uninstallOperator.data).toBeDefined(); - }); + await waitFor(() => { + expect(result.current.uninstallOperator.isSuccess).toBe(true) + }) + expect(result.current.uninstallOperator.data).toBeDefined() + }) it('should handle uninstall operator errors', async () => { server.use(createUninstallOperatorError('cert-manager', 500, 'Uninstallation failed')); @@ -318,9 +326,11 @@ describe('useOperatorMutation', () => { } }); - expect(result.current.uninstallOperator.isError).toBe(true); - expect(result.current.uninstallOperator.error).toBeDefined(); - }); + await waitFor(() => { + expect(result.current.uninstallOperator.isError).toBe(true) + }) + expect(result.current.uninstallOperator.error).toBeDefined() + }) it('should invalidate queries on successful uninstall', async () => { server.use(createUninstallOperator('cert-manager')); @@ -375,14 +385,18 @@ describe('useOperatorMutation', () => { await result.current.installOperator.mutateAsync('cert-manager'); }); - expect(result.current.installOperator.isSuccess).toBe(true); + await waitFor(() => { + expect(result.current.installOperator.isSuccess).toBe(true) + }) // Install second operator await act(async () => { await result.current.installOperator.mutateAsync('cloudnative-pg'); }); - expect(result.current.installOperator.isSuccess).toBe(true); - }); - }); -}); + await waitFor(() => { + expect(result.current.installOperator.isSuccess).toBe(true) + }) + }) + }) +}) From d3efffe788993fce07d51298d6f8599da5d3bf3a Mon Sep 17 00:00:00 2001 From: Anmol1696 Date: Sun, 9 Nov 2025 09:39:15 +0400 Subject: [PATCH 2/2] run e2e tests --- .github/workflows/test-e2e-dashboard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-e2e-dashboard.yml b/.github/workflows/test-e2e-dashboard.yml index 177416c..c325ffd 100644 --- a/.github/workflows/test-e2e-dashboard.yml +++ b/.github/workflows/test-e2e-dashboard.yml @@ -79,7 +79,7 @@ jobs: - name: Run all E2E tests run: | cd apps/ops-dashboard - pnpm run test:e2e || true # Ignore failures for now + pnpm run test:e2e env: K8S_API: http://127.0.0.1:8001 NODE_ENV: test