diff --git a/packages/cypress/cypress/pages/appChrome.ts b/packages/cypress/cypress/pages/appChrome.ts index 3282c1794c..62f7cbcd85 100644 --- a/packages/cypress/cypress/pages/appChrome.ts +++ b/packages/cypress/cypress/pages/appChrome.ts @@ -43,6 +43,14 @@ class AppChrome { findNavItem(args: { name: string; rootSection?: string; subSection?: string }) { return this.findSideBar().findAppNavItem(args); } + + findDarkThemeToggle() { + return cy.findByTestId('dark-theme-toggle'); + } + + findLightThemeToggle() { + return cy.findByTestId('light-theme-toggle'); + } } export const appChrome = new AppChrome(); diff --git a/packages/cypress/cypress/pages/mlflowExperiments.ts b/packages/cypress/cypress/pages/mlflowExperiments.ts index f82bff3b76..d2c239f914 100644 --- a/packages/cypress/cypress/pages/mlflowExperiments.ts +++ b/packages/cypress/cypress/pages/mlflowExperiments.ts @@ -204,24 +204,12 @@ class MlflowExperiments { return cy.contains('Metrics', { timeout: 10000 }); } - findDarkThemeToggle() { - return cy.findByTestId('dark-theme-toggle'); - } - - findLightThemeToggle() { - return cy.findByTestId('light-theme-toggle'); - } - getMlflowDarkModeStorageValue(): Cypress.Chainable { return cy.window().then((win) => { const value = win.localStorage.getItem(MLFLOW_DARK_MODE_KEY); return cy.wrap(value); }); } - - getHtmlDarkModeClass(): Cypress.Chainable { - return cy.document().then((doc) => doc.documentElement.classList.contains('pf-v6-theme-dark')); - } } export const mlflowExperiments = new MlflowExperiments(); diff --git a/packages/cypress/cypress/pages/promptManagement.ts b/packages/cypress/cypress/pages/promptManagement.ts index bff43b7cc6..eb80c66b5f 100644 --- a/packages/cypress/cypress/pages/promptManagement.ts +++ b/packages/cypress/cypress/pages/promptManagement.ts @@ -36,7 +36,19 @@ class PromptManagement { } findProjectSelector() { - return cy.findByTestId('project-selector-dropdown'); + return cy.findByTestId('project-selector-toggle', { timeout: 30000 }); + } + + findProjectInDropdown(name: string) { + return cy.findByRole('menuitem', { name }); + } + + shouldHaveWorkspace(workspace: string) { + cy.url().should('include', `workspace=${workspace}`); + } + + findErrorEmptyState() { + return cy.findByTestId('empty-state-title', { timeout: 10000 }); } findPromptsSearchInput() { @@ -93,24 +105,12 @@ class PromptManagement { return cy.findByRole('radio', { name: 'Preview' }); } - findDarkThemeToggle() { - return cy.findByTestId('dark-theme-toggle'); - } - - findLightThemeToggle() { - return cy.findByTestId('light-theme-toggle'); - } - getMlflowDarkModeStorageValue(): Cypress.Chainable { return cy.window().then((win) => { const value = win.localStorage.getItem(MLFLOW_DARK_MODE_KEY); return cy.wrap(value); }); } - - getHtmlDarkModeClass(): Cypress.Chainable { - return cy.document().then((doc) => doc.documentElement.classList.contains('pf-v6-theme-dark')); - } } export const promptManagement = new PromptManagement(); diff --git a/packages/cypress/cypress/tests/e2e/promptManagement/testPromptManagement.cy.ts b/packages/cypress/cypress/tests/e2e/promptManagement/testPromptManagement.cy.ts index 099e0a1b33..6749ff2674 100644 --- a/packages/cypress/cypress/tests/e2e/promptManagement/testPromptManagement.cy.ts +++ b/packages/cypress/cypress/tests/e2e/promptManagement/testPromptManagement.cy.ts @@ -64,7 +64,7 @@ describe('Verify Prompt Management page', () => { it( 'Create a prompt and verify it appears in the prompts table', { - tags: ['@Sanity', '@SanitySet1', '@PromptManagement', '@MLflow'], + tags: ['@Sanity', '@SanitySet1', '@PromptManagement', '@MLflow', '@NonConcurrent'], }, () => { const prompt = testData.prompts[0]; @@ -145,20 +145,6 @@ describe('Verify Prompt Management page', () => { cy.step('Verify the prompt persists after navigation'); promptManagement.findPromptInTable(prompt.name).should('be.visible'); - - cy.step('Toggle dark mode on'); - promptManagement.findDarkThemeToggle().click(); - - cy.step('Verify dark theme is applied'); - promptManagement.getHtmlDarkModeClass().should('equal', true); - promptManagement.getMlflowDarkModeStorageValue().should('equal', 'true'); - - cy.step('Toggle light mode back on'); - promptManagement.findLightThemeToggle().click(); - - cy.step('Verify light theme is restored'); - promptManagement.getHtmlDarkModeClass().should('equal', false); - promptManagement.getMlflowDarkModeStorageValue().should('equal', 'false'); }, ); }); diff --git a/packages/cypress/cypress/tests/mocked/mlflow/mlflowExperiments.cy.ts b/packages/cypress/cypress/tests/mocked/mlflow/mlflowExperiments.cy.ts new file mode 100644 index 0000000000..3c0b7735e3 --- /dev/null +++ b/packages/cypress/cypress/tests/mocked/mlflow/mlflowExperiments.cy.ts @@ -0,0 +1,111 @@ +import { + mockDashboardConfig, + mockK8sResourceList, + mockProjectK8sResource, +} from '@odh-dashboard/internal/__mocks__'; +import { mockDscStatus } from '@odh-dashboard/internal/__mocks__/mockDscStatus'; +import { DataScienceStackComponent } from '@odh-dashboard/internal/concepts/areas/types'; +import { ProjectModel } from '../../../utils/models'; +import { asProductAdminUser } from '../../../utils/mockUsers'; +import { interceptMlflowStatus } from '../../../utils/mlflowUtils'; +import { mlflowExperiments } from '../../../pages/mlflowExperiments'; +import { appChrome } from '../../../pages/appChrome'; + +const PROJECT_A = 'test-project-a'; +const PROJECT_B = 'test-project-b'; + +const initIntercepts = ({ mlflowConfigured = true }: { mlflowConfigured?: boolean } = {}) => { + asProductAdminUser(); + cy.interceptOdh('GET /api/config', mockDashboardConfig({})); + interceptMlflowStatus(mlflowConfigured); + + const projectA = mockProjectK8sResource({ k8sName: PROJECT_A, displayName: PROJECT_A }); + const projectB = mockProjectK8sResource({ k8sName: PROJECT_B, displayName: PROJECT_B }); + cy.interceptK8sList(ProjectModel, mockK8sResourceList([projectA, projectB])); + cy.interceptK8s(ProjectModel, projectA); +}; + +describe('MLflow Experiments page wrapper', () => { + beforeEach(() => { + initIntercepts(); + }); + + describe('Page chrome and navigation', () => { + it('should display page title and Launch MLflow button', () => { + mlflowExperiments.visit(PROJECT_A); + mlflowExperiments.findPageTitle().should('be.visible'); + mlflowExperiments + .findLaunchMlflowButton() + .should('be.visible') + .should('have.attr', 'href', '/mlflow') + .should('have.attr', 'target', '_blank'); + }); + + it('should navigate via sidebar and show active nav item', () => { + cy.visitWithLogin('/'); + appChrome.findMainContent().should('be.visible'); + + mlflowExperiments.findNavSection().click(); + mlflowExperiments.navigate(); + + mlflowExperiments.shouldHaveExperimentsUrl(); + mlflowExperiments.findNavItem().should('have.attr', 'aria-current', 'page'); + }); + }); + + describe('Project selector', () => { + it('should switch workspace when selecting a different project', () => { + mlflowExperiments.visit(PROJECT_A); + mlflowExperiments.findProjectSelector().should('contain', PROJECT_A); + + mlflowExperiments.findProjectSelector().click(); + mlflowExperiments.findProjectInDropdown(PROJECT_B).click(); + mlflowExperiments.shouldHaveWorkspace(PROJECT_B); + + mlflowExperiments.findProjectSelector().click(); + mlflowExperiments.findProjectInDropdown(PROJECT_A).click(); + mlflowExperiments.shouldHaveWorkspace(PROJECT_A); + }); + }); + + describe('Dark mode toggle', () => { + it('should sync localStorage on toggle', () => { + mlflowExperiments.visit(PROJECT_A); + + appChrome.findDarkThemeToggle().click(); + mlflowExperiments.getMlflowDarkModeStorageValue().should('equal', 'true'); + + appChrome.findLightThemeToggle().click(); + mlflowExperiments.getMlflowDarkModeStorageValue().should('equal', 'false'); + }); + }); + + describe('Error states', () => { + it('should show error state for invalid workspace', () => { + const invalidWorkspace = 'nonexistent-project'; + mlflowExperiments.visit(invalidWorkspace); + mlflowExperiments + .findErrorEmptyState() + .should('be.visible') + .should('contain', invalidWorkspace); + }); + + it('should show unavailable state when MLflow is not configured', () => { + initIntercepts({ mlflowConfigured: false }); + mlflowExperiments.visit(PROJECT_A); + mlflowExperiments.findMlflowUnavailableState().should('be.visible'); + }); + + it('should hide nav item when MLflow operator is removed', () => { + const dscStatus = mockDscStatus({}); + dscStatus.components = { + ...dscStatus.components, + [DataScienceStackComponent.MLFLOW]: { managementState: 'Removed' }, + }; + cy.interceptOdh('GET /api/dsc/status', dscStatus); + + cy.visitWithLogin('/'); + mlflowExperiments.findNavItem().should('not.exist'); + }); + }); +}); diff --git a/packages/cypress/cypress/tests/mocked/mlflow/promptManagement.cy.ts b/packages/cypress/cypress/tests/mocked/mlflow/promptManagement.cy.ts new file mode 100644 index 0000000000..cf1890175d --- /dev/null +++ b/packages/cypress/cypress/tests/mocked/mlflow/promptManagement.cy.ts @@ -0,0 +1,109 @@ +import { + mockDashboardConfig, + mockK8sResourceList, + mockProjectK8sResource, +} from '@odh-dashboard/internal/__mocks__'; +import { mockDscStatus } from '@odh-dashboard/internal/__mocks__/mockDscStatus'; +import { DataScienceStackComponent } from '@odh-dashboard/internal/concepts/areas/types'; +import { ProjectModel } from '../../../utils/models'; +import { asProductAdminUser } from '../../../utils/mockUsers'; +import { interceptMlflowStatus } from '../../../utils/mlflowUtils'; +import { promptManagement } from '../../../pages/promptManagement'; +import { appChrome } from '../../../pages/appChrome'; + +const PROJECT_A = 'test-project-a'; +const PROJECT_B = 'test-project-b'; + +const initIntercepts = ({ + mlflowConfigured = true, + genAiStudio = true, +}: { mlflowConfigured?: boolean; genAiStudio?: boolean } = {}) => { + asProductAdminUser(); + cy.interceptOdh('GET /api/config', mockDashboardConfig({ genAiStudio })); + interceptMlflowStatus(mlflowConfigured); + + const projectA = mockProjectK8sResource({ k8sName: PROJECT_A, displayName: PROJECT_A }); + const projectB = mockProjectK8sResource({ k8sName: PROJECT_B, displayName: PROJECT_B }); + cy.interceptK8sList(ProjectModel, mockK8sResourceList([projectA, projectB])); + cy.interceptK8s(ProjectModel, projectA); +}; + +describe('Prompt Management page wrapper', () => { + beforeEach(() => { + initIntercepts(); + }); + + describe('Page chrome', () => { + it('should display page title and Launch MLflow button', () => { + promptManagement.visit(PROJECT_A); + promptManagement.findPageTitle().should('be.visible'); + promptManagement + .findLaunchMlflowButton() + .should('be.visible') + .should('have.attr', 'href', '/mlflow') + .should('have.attr', 'target', '_blank'); + }); + }); + + describe('Project selector', () => { + it('should switch workspace when selecting a different project', () => { + promptManagement.visit(PROJECT_A); + promptManagement.findProjectSelector().should('contain', PROJECT_A); + + promptManagement.findProjectSelector().click(); + promptManagement.findProjectInDropdown(PROJECT_B).click(); + promptManagement.shouldHaveWorkspace(PROJECT_B); + + promptManagement.findProjectSelector().click(); + promptManagement.findProjectInDropdown(PROJECT_A).click(); + promptManagement.shouldHaveWorkspace(PROJECT_A); + }); + }); + + describe('Dark mode toggle', () => { + it('should sync localStorage on toggle', () => { + promptManagement.visit(PROJECT_A); + + appChrome.findDarkThemeToggle().click(); + promptManagement.getMlflowDarkModeStorageValue().should('equal', 'true'); + + appChrome.findLightThemeToggle().click(); + promptManagement.getMlflowDarkModeStorageValue().should('equal', 'false'); + }); + }); + + describe('Error states', () => { + it('should show error state for invalid workspace', () => { + const invalidWorkspace = 'nonexistent-project'; + promptManagement.visit(invalidWorkspace); + promptManagement + .findErrorEmptyState() + .should('be.visible') + .should('contain', invalidWorkspace); + }); + + it('should show unavailable state when MLflow is not configured', () => { + initIntercepts({ mlflowConfigured: false }); + promptManagement.visit(PROJECT_A); + promptManagement.findMlflowUnavailableState().should('be.visible'); + }); + + it('should hide nav item when genAiStudio feature flag is disabled', () => { + initIntercepts({ genAiStudio: false }); + cy.visitWithLogin('/'); + promptManagement.findNavItem().should('not.exist'); + }); + + it('should hide nav item when MLflow operator is removed', () => { + const dscStatus = mockDscStatus({}); + dscStatus.components = { + ...dscStatus.components, + [DataScienceStackComponent.MLFLOW]: { managementState: 'Removed' }, + }; + cy.interceptOdh('GET /api/dsc/status', dscStatus); + + cy.visitWithLogin('/'); + promptManagement.findNavItem().should('not.exist'); + }); + }); +});