-
Notifications
You must be signed in to change notification settings - Fork 297
Add mock tests for mlflow experiments and prompts pages #7371
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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'); | ||
| }); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+63
to
+73
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Dark-mode test leaks localStorage across specs/tests. The MLflow dark-mode flag is written to Make the pre-conditions explicit so the test is idempotent and actually proves the toggle behavior. 🔧 Proposed fix describe('Dark mode toggle', () => {
it('should sync localStorage on toggle', () => {
+ cy.clearLocalStorage();
promptManagement.visit(PROJECT_A);
+ // Baseline: light theme
+ promptManagement.getMlflowDarkModeStorageValue().should('not.equal', 'true');
+
appChrome.findDarkThemeToggle().click();
promptManagement.getMlflowDarkModeStorageValue().should('equal', 'true');
appChrome.findLightThemeToggle().click();
promptManagement.getMlflowDarkModeStorageValue().should('equal', 'false');
});
});As per coding guidelines: "Tests must be idempotent: runnable in any order without shared state." 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same idempotency gap as in
promptManagement.cy.ts, plus a misleading title.localStorage. Either drop "class" from the title or also assert the<html>class (e.g.cy.get('html').should('have.class', 'pf-v6-theme-dark')).localStorageis not cleared between tests, so the firstclick()may be toggling from an already-dark baseline and the'true'assertion passes trivially. Addcy.clearLocalStorage()(or assert the pre-toggle baseline) to make the test prove a real round-trip.🔧 Proposed fix
describe('Dark mode toggle', () => { - it('should sync dark mode class and localStorage on toggle', () => { + it('should sync localStorage on toggle', () => { + cy.clearLocalStorage(); mlflowExperiments.visit(PROJECT_A); + mlflowExperiments.getMlflowDarkModeStorageValue().should('not.equal', 'true'); appChrome.findDarkThemeToggle().click(); mlflowExperiments.getMlflowDarkModeStorageValue().should('equal', 'true'); appChrome.findLightThemeToggle().click(); mlflowExperiments.getMlflowDarkModeStorageValue().should('equal', 'false'); }); });As per coding guidelines: "Tests must be idempotent: runnable in any order without shared state."
🤖 Prompt for AI Agents