Skip to content

Commit e302dcb

Browse files
Add mock tests for mlflow experiments and prompts pages (#7371)
* Add mock tests for mlflow experiments and prompts pages Signed-off-by: Nana Nosirova <10577112+nananosirova@users.noreply.github.com> * Update packages/cypress/cypress/tests/mocked/mlflow/mlflowExperiments.cy.ts Co-authored-by: Juntao Wang <37624318+DaoDaoNoCode@users.noreply.github.com> * Update packages/cypress/cypress/tests/mocked/mlflow/mlflowExperiments.cy.ts Co-authored-by: Juntao Wang <37624318+DaoDaoNoCode@users.noreply.github.com> --------- Signed-off-by: Nana Nosirova <10577112+nananosirova@users.noreply.github.com> Co-authored-by: Juntao Wang <37624318+DaoDaoNoCode@users.noreply.github.com>
1 parent d6302ef commit e302dcb

6 files changed

Lines changed: 242 additions & 40 deletions

File tree

packages/cypress/cypress/pages/appChrome.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@ class AppChrome {
4343
findNavItem(args: { name: string; rootSection?: string; subSection?: string }) {
4444
return this.findSideBar().findAppNavItem(args);
4545
}
46+
47+
findDarkThemeToggle() {
48+
return cy.findByTestId('dark-theme-toggle');
49+
}
50+
51+
findLightThemeToggle() {
52+
return cy.findByTestId('light-theme-toggle');
53+
}
4654
}
4755

4856
export const appChrome = new AppChrome();

packages/cypress/cypress/pages/mlflowExperiments.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -204,24 +204,12 @@ class MlflowExperiments {
204204
return cy.contains('Metrics', { timeout: 10000 });
205205
}
206206

207-
findDarkThemeToggle() {
208-
return cy.findByTestId('dark-theme-toggle');
209-
}
210-
211-
findLightThemeToggle() {
212-
return cy.findByTestId('light-theme-toggle');
213-
}
214-
215207
getMlflowDarkModeStorageValue(): Cypress.Chainable<string | null> {
216208
return cy.window().then((win) => {
217209
const value = win.localStorage.getItem(MLFLOW_DARK_MODE_KEY);
218210
return cy.wrap<string | null>(value);
219211
});
220212
}
221-
222-
getHtmlDarkModeClass(): Cypress.Chainable<boolean> {
223-
return cy.document().then((doc) => doc.documentElement.classList.contains('pf-v6-theme-dark'));
224-
}
225213
}
226214

227215
export const mlflowExperiments = new MlflowExperiments();

packages/cypress/cypress/pages/promptManagement.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,19 @@ class PromptManagement {
3636
}
3737

3838
findProjectSelector() {
39-
return cy.findByTestId('project-selector-dropdown');
39+
return cy.findByTestId('project-selector-toggle', { timeout: 30000 });
40+
}
41+
42+
findProjectInDropdown(name: string) {
43+
return cy.findByRole('menuitem', { name });
44+
}
45+
46+
shouldHaveWorkspace(workspace: string) {
47+
cy.url().should('include', `workspace=${workspace}`);
48+
}
49+
50+
findErrorEmptyState() {
51+
return cy.findByTestId('empty-state-title', { timeout: 10000 });
4052
}
4153

4254
findPromptsSearchInput() {
@@ -93,24 +105,12 @@ class PromptManagement {
93105
return cy.findByRole('radio', { name: 'Preview' });
94106
}
95107

96-
findDarkThemeToggle() {
97-
return cy.findByTestId('dark-theme-toggle');
98-
}
99-
100-
findLightThemeToggle() {
101-
return cy.findByTestId('light-theme-toggle');
102-
}
103-
104108
getMlflowDarkModeStorageValue(): Cypress.Chainable<string | null> {
105109
return cy.window().then((win) => {
106110
const value = win.localStorage.getItem(MLFLOW_DARK_MODE_KEY);
107111
return cy.wrap<string | null>(value);
108112
});
109113
}
110-
111-
getHtmlDarkModeClass(): Cypress.Chainable<boolean> {
112-
return cy.document().then((doc) => doc.documentElement.classList.contains('pf-v6-theme-dark'));
113-
}
114114
}
115115

116116
export const promptManagement = new PromptManagement();

packages/cypress/cypress/tests/e2e/promptManagement/testPromptManagement.cy.ts

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ describe('Verify Prompt Management page', () => {
6464
it(
6565
'Create a prompt and verify it appears in the prompts table',
6666
{
67-
tags: ['@Sanity', '@SanitySet1', '@PromptManagement', '@MLflow'],
67+
tags: ['@Sanity', '@SanitySet1', '@PromptManagement', '@MLflow', '@NonConcurrent'],
6868
},
6969
() => {
7070
const prompt = testData.prompts[0];
@@ -145,20 +145,6 @@ describe('Verify Prompt Management page', () => {
145145

146146
cy.step('Verify the prompt persists after navigation');
147147
promptManagement.findPromptInTable(prompt.name).should('be.visible');
148-
149-
cy.step('Toggle dark mode on');
150-
promptManagement.findDarkThemeToggle().click();
151-
152-
cy.step('Verify dark theme is applied');
153-
promptManagement.getHtmlDarkModeClass().should('equal', true);
154-
promptManagement.getMlflowDarkModeStorageValue().should('equal', 'true');
155-
156-
cy.step('Toggle light mode back on');
157-
promptManagement.findLightThemeToggle().click();
158-
159-
cy.step('Verify light theme is restored');
160-
promptManagement.getHtmlDarkModeClass().should('equal', false);
161-
promptManagement.getMlflowDarkModeStorageValue().should('equal', 'false');
162148
},
163149
);
164150
});
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import {
2+
mockDashboardConfig,
3+
mockK8sResourceList,
4+
mockProjectK8sResource,
5+
} from '@odh-dashboard/internal/__mocks__';
6+
import { mockDscStatus } from '@odh-dashboard/internal/__mocks__/mockDscStatus';
7+
import { DataScienceStackComponent } from '@odh-dashboard/internal/concepts/areas/types';
8+
import { ProjectModel } from '../../../utils/models';
9+
import { asProductAdminUser } from '../../../utils/mockUsers';
10+
import { interceptMlflowStatus } from '../../../utils/mlflowUtils';
11+
import { mlflowExperiments } from '../../../pages/mlflowExperiments';
12+
import { appChrome } from '../../../pages/appChrome';
13+
14+
const PROJECT_A = 'test-project-a';
15+
const PROJECT_B = 'test-project-b';
16+
17+
const initIntercepts = ({ mlflowConfigured = true }: { mlflowConfigured?: boolean } = {}) => {
18+
asProductAdminUser();
19+
cy.interceptOdh('GET /api/config', mockDashboardConfig({}));
20+
interceptMlflowStatus(mlflowConfigured);
21+
22+
const projectA = mockProjectK8sResource({ k8sName: PROJECT_A, displayName: PROJECT_A });
23+
const projectB = mockProjectK8sResource({ k8sName: PROJECT_B, displayName: PROJECT_B });
24+
cy.interceptK8sList(ProjectModel, mockK8sResourceList([projectA, projectB]));
25+
cy.interceptK8s(ProjectModel, projectA);
26+
};
27+
28+
describe('MLflow Experiments page wrapper', () => {
29+
beforeEach(() => {
30+
initIntercepts();
31+
});
32+
33+
describe('Page chrome and navigation', () => {
34+
it('should display page title and Launch MLflow button', () => {
35+
mlflowExperiments.visit(PROJECT_A);
36+
mlflowExperiments.findPageTitle().should('be.visible');
37+
mlflowExperiments
38+
.findLaunchMlflowButton()
39+
.should('be.visible')
40+
.should('have.attr', 'href', '/mlflow')
41+
.should('have.attr', 'target', '_blank');
42+
});
43+
44+
it('should navigate via sidebar and show active nav item', () => {
45+
cy.visitWithLogin('/');
46+
appChrome.findMainContent().should('be.visible');
47+
48+
mlflowExperiments.findNavSection().click();
49+
mlflowExperiments.navigate();
50+
51+
mlflowExperiments.shouldHaveExperimentsUrl();
52+
mlflowExperiments.findNavItem().should('have.attr', 'aria-current', 'page');
53+
});
54+
});
55+
56+
describe('Project selector', () => {
57+
it('should switch workspace when selecting a different project', () => {
58+
mlflowExperiments.visit(PROJECT_A);
59+
mlflowExperiments.findProjectSelector().should('contain', PROJECT_A);
60+
61+
mlflowExperiments.findProjectSelector().click();
62+
mlflowExperiments.findProjectInDropdown(PROJECT_B).click();
63+
mlflowExperiments.shouldHaveWorkspace(PROJECT_B);
64+
65+
mlflowExperiments.findProjectSelector().click();
66+
mlflowExperiments.findProjectInDropdown(PROJECT_A).click();
67+
mlflowExperiments.shouldHaveWorkspace(PROJECT_A);
68+
});
69+
});
70+
71+
describe('Dark mode toggle', () => {
72+
it('should sync localStorage on toggle', () => {
73+
mlflowExperiments.visit(PROJECT_A);
74+
75+
appChrome.findDarkThemeToggle().click();
76+
mlflowExperiments.getMlflowDarkModeStorageValue().should('equal', 'true');
77+
78+
appChrome.findLightThemeToggle().click();
79+
mlflowExperiments.getMlflowDarkModeStorageValue().should('equal', 'false');
80+
});
81+
});
82+
83+
describe('Error states', () => {
84+
it('should show error state for invalid workspace', () => {
85+
const invalidWorkspace = 'nonexistent-project';
86+
mlflowExperiments.visit(invalidWorkspace);
87+
mlflowExperiments
88+
.findErrorEmptyState()
89+
.should('be.visible')
90+
.should('contain', invalidWorkspace);
91+
});
92+
93+
it('should show unavailable state when MLflow is not configured', () => {
94+
initIntercepts({ mlflowConfigured: false });
95+
mlflowExperiments.visit(PROJECT_A);
96+
mlflowExperiments.findMlflowUnavailableState().should('be.visible');
97+
});
98+
99+
it('should hide nav item when MLflow operator is removed', () => {
100+
const dscStatus = mockDscStatus({});
101+
dscStatus.components = {
102+
...dscStatus.components,
103+
[DataScienceStackComponent.MLFLOW]: { managementState: 'Removed' },
104+
};
105+
cy.interceptOdh('GET /api/dsc/status', dscStatus);
106+
107+
cy.visitWithLogin('/');
108+
mlflowExperiments.findNavItem().should('not.exist');
109+
});
110+
});
111+
});
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import {
2+
mockDashboardConfig,
3+
mockK8sResourceList,
4+
mockProjectK8sResource,
5+
} from '@odh-dashboard/internal/__mocks__';
6+
import { mockDscStatus } from '@odh-dashboard/internal/__mocks__/mockDscStatus';
7+
import { DataScienceStackComponent } from '@odh-dashboard/internal/concepts/areas/types';
8+
import { ProjectModel } from '../../../utils/models';
9+
import { asProductAdminUser } from '../../../utils/mockUsers';
10+
import { interceptMlflowStatus } from '../../../utils/mlflowUtils';
11+
import { promptManagement } from '../../../pages/promptManagement';
12+
import { appChrome } from '../../../pages/appChrome';
13+
14+
const PROJECT_A = 'test-project-a';
15+
const PROJECT_B = 'test-project-b';
16+
17+
const initIntercepts = ({
18+
mlflowConfigured = true,
19+
genAiStudio = true,
20+
}: { mlflowConfigured?: boolean; genAiStudio?: boolean } = {}) => {
21+
asProductAdminUser();
22+
cy.interceptOdh('GET /api/config', mockDashboardConfig({ genAiStudio }));
23+
interceptMlflowStatus(mlflowConfigured);
24+
25+
const projectA = mockProjectK8sResource({ k8sName: PROJECT_A, displayName: PROJECT_A });
26+
const projectB = mockProjectK8sResource({ k8sName: PROJECT_B, displayName: PROJECT_B });
27+
cy.interceptK8sList(ProjectModel, mockK8sResourceList([projectA, projectB]));
28+
cy.interceptK8s(ProjectModel, projectA);
29+
};
30+
31+
describe('Prompt Management page wrapper', () => {
32+
beforeEach(() => {
33+
initIntercepts();
34+
});
35+
36+
describe('Page chrome', () => {
37+
it('should display page title and Launch MLflow button', () => {
38+
promptManagement.visit(PROJECT_A);
39+
promptManagement.findPageTitle().should('be.visible');
40+
promptManagement
41+
.findLaunchMlflowButton()
42+
.should('be.visible')
43+
.should('have.attr', 'href', '/mlflow')
44+
.should('have.attr', 'target', '_blank');
45+
});
46+
});
47+
48+
describe('Project selector', () => {
49+
it('should switch workspace when selecting a different project', () => {
50+
promptManagement.visit(PROJECT_A);
51+
promptManagement.findProjectSelector().should('contain', PROJECT_A);
52+
53+
promptManagement.findProjectSelector().click();
54+
promptManagement.findProjectInDropdown(PROJECT_B).click();
55+
promptManagement.shouldHaveWorkspace(PROJECT_B);
56+
57+
promptManagement.findProjectSelector().click();
58+
promptManagement.findProjectInDropdown(PROJECT_A).click();
59+
promptManagement.shouldHaveWorkspace(PROJECT_A);
60+
});
61+
});
62+
63+
describe('Dark mode toggle', () => {
64+
it('should sync localStorage on toggle', () => {
65+
promptManagement.visit(PROJECT_A);
66+
67+
appChrome.findDarkThemeToggle().click();
68+
promptManagement.getMlflowDarkModeStorageValue().should('equal', 'true');
69+
70+
appChrome.findLightThemeToggle().click();
71+
promptManagement.getMlflowDarkModeStorageValue().should('equal', 'false');
72+
});
73+
});
74+
75+
describe('Error states', () => {
76+
it('should show error state for invalid workspace', () => {
77+
const invalidWorkspace = 'nonexistent-project';
78+
promptManagement.visit(invalidWorkspace);
79+
promptManagement
80+
.findErrorEmptyState()
81+
.should('be.visible')
82+
.should('contain', invalidWorkspace);
83+
});
84+
85+
it('should show unavailable state when MLflow is not configured', () => {
86+
initIntercepts({ mlflowConfigured: false });
87+
promptManagement.visit(PROJECT_A);
88+
promptManagement.findMlflowUnavailableState().should('be.visible');
89+
});
90+
91+
it('should hide nav item when genAiStudio feature flag is disabled', () => {
92+
initIntercepts({ genAiStudio: false });
93+
cy.visitWithLogin('/');
94+
promptManagement.findNavItem().should('not.exist');
95+
});
96+
97+
it('should hide nav item when MLflow operator is removed', () => {
98+
const dscStatus = mockDscStatus({});
99+
dscStatus.components = {
100+
...dscStatus.components,
101+
[DataScienceStackComponent.MLFLOW]: { managementState: 'Removed' },
102+
};
103+
cy.interceptOdh('GET /api/dsc/status', dscStatus);
104+
105+
cy.visitWithLogin('/');
106+
promptManagement.findNavItem().should('not.exist');
107+
});
108+
});
109+
});

0 commit comments

Comments
 (0)