diff --git a/AGENTS.md b/AGENTS.md index 1d09a7e48..16917c5bc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -283,6 +283,54 @@ npm run cypress:open For detailed testing instructions, see `web/cypress/CYPRESS_TESTING_GUIDE.md` +### Cypress Component Testing + +#### Overview + +Cypress component tests mount individual React components in isolation, without requiring a running OpenShift cluster. They are useful for testing component rendering, user interactions, and visual behavior with fast feedback. + +- **Test location**: `web/cypress/component/` +- **Support file**: `web/cypress/support/component.ts` +- **Config**: `component` section in `web/cypress.config.ts` + +#### When to Create Component Tests + +- Testing a component's rendering logic (conditional display, empty states) +- Verifying props are handled correctly +- Validating user interactions within a single component +- When E2E tests would be overkill for the behavior under test + +#### Quick Test Commands + +```bash +cd web + +# Interactive mode +npm run cypress:open:component + +# Headless mode - all component tests +npm run cypress:run:component + +# Run a single component test file +npx cypress run --component --spec cypress/component/labels.cy.tsx +``` + +#### Writing a Component Test + +Component test files use the `.cy.tsx` extension and go in `web/cypress/component/`: + +```typescript +import React from 'react'; +import { MyComponent } from '../../src/components/MyComponent'; + +describe('MyComponent', () => { + it('renders correctly', () => { + cy.mount(); + cy.contains('expected text').should('be.visible'); + }); +}); +``` + ### Release Pipeline: - **Konflux**: Handles CI/CD and release automation diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2e9d26d38..f74d2cf23 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -411,6 +411,23 @@ cd web/cypress npm run cypress:run --spec "cypress/e2e/**/regression/**" ``` +### Component Tests (Cypress) + +For testing individual React components in isolation (no cluster required): + +- Test files: `web/cypress/component/` (`.cy.tsx` extension) +- Support file: `web/cypress/support/component.ts` + +```bash +cd web + +# Interactive mode +npm run cypress:open:component + +# Headless mode +npm run cypress:run:component +``` + --- ## Internationalization (i18n) diff --git a/config/perses-dashboards.patch.json b/config/perses-dashboards.patch.json index 2b90e7ac9..79d650595 100644 --- a/config/perses-dashboards.patch.json +++ b/config/perses-dashboards.patch.json @@ -119,5 +119,18 @@ "component": { "$codeRef": "DashboardPage" } } } + }, + { + "op": "add", + "path": "/extensions/1", + "value": { + "type": "ols.tool-ui", + "properties": { + "id": "mcp-obs/show-timeseries", + "component": { + "$codeRef": "ols-tool-ui.ShowTimeseries" + } + } + } } ] diff --git a/web/cypress.config.ts b/web/cypress.config.ts index 6c6a516b5..b2a94f02c 100644 --- a/web/cypress.config.ts +++ b/web/cypress.config.ts @@ -3,6 +3,7 @@ import * as fs from 'fs-extra'; import * as console from 'console'; import * as path from 'path'; import registerCypressGrep from '@cypress/grep/src/plugin'; +import { DefinePlugin, NormalModuleReplacementPlugin } from 'webpack'; export default defineConfig({ screenshotsFolder: './cypress/screenshots', @@ -159,4 +160,61 @@ export default defineConfig({ experimentalMemoryManagement: true, experimentalStudio: true, }, + component: { + devServer: { + framework: 'react', + bundler: 'webpack', + webpackConfig: { + resolve: { + extensions: ['.ts', '.tsx', '.js', '.jsx'], + alias: { + '@perses-dev/plugin-system': path.resolve(__dirname, 'cypress/component/mocks/perses-plugin-system.tsx'), + '@perses-dev/dashboards': path.resolve(__dirname, 'cypress/component/mocks/perses-dashboards.tsx'), + '@perses-dev/prometheus-plugin': path.resolve(__dirname, 'cypress/component/mocks/perses-prometheus-plugin.ts'), + }, + }, + module: { + rules: [ + { + test: /\.(jsx?|tsx?)$/, + exclude: /node_modules/, + use: { loader: 'swc-loader' }, + }, + { + test: /\.scss$/, + exclude: /node_modules\/(?!(@patternfly|@openshift-console\/plugin-shared)\/).*/, + use: ['style-loader', 'css-loader', 'sass-loader'], + }, + { + test: /\.css$/, + use: ['style-loader', 'css-loader'], + }, + { + test: /\.(png|jpg|jpeg|gif|svg|woff2?|ttf|eot|otf)(\?.*$|$)/, + type: 'asset/resource', + }, + { + test: /\.m?js/, + resolve: { fullySpecified: false }, + }, + ], + }, + plugins: [ + new DefinePlugin({ + 'process.env.I18N_NAMESPACE': JSON.stringify('plugin__monitoring-plugin'), + }), + new NormalModuleReplacementPlugin( + /helpers\/OlsToolUIPersesWrapper/, + path.resolve(__dirname, 'cypress/component/mocks/OlsToolUIPersesWrapper.tsx'), + ), + new NormalModuleReplacementPlugin( + /helpers\/AddToDashboardButton/, + path.resolve(__dirname, 'cypress/component/mocks/AddToDashboardButton.tsx'), + ), + ], + }, + }, + specPattern: './cypress/component/**/*.cy.{js,jsx,ts,tsx}', + supportFile: './cypress/support/component.ts', + }, }); \ No newline at end of file diff --git a/web/cypress/CYPRESS_TESTING_GUIDE.md b/web/cypress/CYPRESS_TESTING_GUIDE.md index 697caf6f5..071d03f80 100644 --- a/web/cypress/CYPRESS_TESTING_GUIDE.md +++ b/web/cypress/CYPRESS_TESTING_GUIDE.md @@ -67,6 +67,7 @@ The Monitoring Plugin uses a 3-layer architecture for test organization: ``` cypress/ +├── component/ # Component tests (isolated, no cluster needed) ├── e2e/ │ ├── monitoring/ # Core monitoring tests (Administrator) │ │ ├── 00.bvt_admin.cy.ts @@ -81,7 +82,9 @@ cypress/ │ │ ├── 02.reg_metrics.cy.ts │ │ └── 03.reg_legacy_dashboards.cy.ts │ ├── perses/ # COO/Perses scenarios -│ └── commands/ # Custom Cypress commands +│ ├── commands/ # Custom Cypress commands +│ ├── component.ts # Component test support (mount command) +│ └── component-index.html # HTML template for component mounting └── views/ # Page object models (reusable actions) ``` @@ -92,7 +95,65 @@ cypress/ --- -## Creating Tests +## Component Testing + +Component tests mount individual React components in isolation using Cypress, without requiring a running OpenShift cluster. They provide fast feedback for rendering logic, props handling, and interactions. + +### When to Use Component Tests vs E2E Tests + +| Use Component Tests When | Use E2E Tests When | +|---|---| +| Testing rendering and visual output | Testing full user workflows | +| Verifying props and conditional display | Testing navigation between pages | +| Validating empty/error states | Testing API integration | +| Fast feedback during development | Testing cross-component interactions | + +### Writing Component Tests + +Component test files use the `.cy.tsx` extension and live in `cypress/component/`: + +```typescript +import React from 'react'; +import { Labels } from '../../src/components/labels'; + +describe('Labels', () => { + it('renders "No labels" when labels is empty', () => { + cy.mount(); + cy.contains('No labels').should('be.visible'); + }); + + it('renders a single label', () => { + cy.mount(); + cy.contains('app').should('be.visible'); + cy.contains('monitoring').should('be.visible'); + }); +}); +``` + +### Running Component Tests + +```bash +cd web + +# Interactive mode (GUI) - best for development +npm run cypress:open:component + +# Headless mode - best for CI +npm run cypress:run:component + +# Run a single component test file +npx cypress run --component --spec cypress/component/labels.cy.tsx +``` + +### Key Differences from E2E Tests + +- **No cluster required**: Components are mounted directly in the browser +- **Custom mount command**: Use `cy.mount()` instead of `cy.visit()` +- **Support file**: Uses `cypress/support/component.ts` (not `cypress/support/index.ts`) + +--- + +## Creating E2E Tests ### Workflow @@ -133,11 +194,12 @@ export const runAlertTests = (perspective: string) => { | Scenario | Action | |----------|--------| -| New UI feature | Create new test scenario in support/ | +| New UI feature | Create new E2E test scenario in support/ | | Bug fix | Add test case to existing support file | | Component update | Update existing test scenarios | -| New Perses feature | Create new test scenario in support/ | -| ACM integration | Add test in e2e/coo/ | +| New Perses feature | Create new E2E test scenario in support/ | +| ACM integration | Add E2E test in e2e/coo/ | +| Isolated component logic | Add component test in component/ | ### Best Practices diff --git a/web/cypress/README.md b/web/cypress/README.md index 9e0c0fc87..2c0e5e4a6 100644 --- a/web/cypress/README.md +++ b/web/cypress/README.md @@ -407,20 +407,34 @@ export CYPRESS_SESSION=true --- +## Component Testing + +Cypress component tests mount individual React components in isolation, without a running cluster. They provide fast feedback for testing rendering logic, props handling, and user interactions. See **[CYPRESS_TESTING_GUIDE.md](CYPRESS_TESTING_GUIDE.md)** for more guidance on how +to write and run the tests. + +### Configuration + +Component testing is configured in the `component` section of `web/cypress.config.ts`. It uses a standalone webpack config with `swc-loader` and a custom `mount` command (compatible with React 17) defined in `cypress/support/component.ts`. + +--- + ## Test Organization ### Directory Structure ``` cypress/ -├── e2e/ # Test files by perspective +├── component/ # Component test files (.cy.tsx) +├── e2e/ # E2E test files by perspective │ ├── monitoring/ # Core monitoring (Administrator) │ ├── coo/ # COO-specific tests │ └── virtualization/ # Virtualization integration ├── support/ # Reusable test scenarios │ ├── monitoring/ # Test scenario modules │ ├── perses/ # Perses scenarios -│ └── commands/ # Custom Cypress commands +│ ├── commands/ # Custom Cypress commands +│ ├── component.ts # Component test support +│ └── component-index.html # Component test HTML template ├── views/ # Page object models ├── fixtures/ # Test data and mocks └── E2E_TEST_SCENARIOS.md # Complete test catalog diff --git a/web/cypress/component/labels.cy.tsx b/web/cypress/component/labels.cy.tsx new file mode 100644 index 000000000..fbe7009c1 --- /dev/null +++ b/web/cypress/component/labels.cy.tsx @@ -0,0 +1,44 @@ +import { Labels } from '../../src/components/labels'; + +describe('Labels', () => { + it('renders "No labels" when labels is empty', () => { + cy.mount(); + cy.contains('No labels').should('be.visible'); + }); + + it('renders "No labels" when labels is undefined', () => { + cy.mount(); + cy.contains('No labels').should('be.visible'); + }); + + it('renders a single label', () => { + cy.mount(); + cy.contains('app').should('be.visible'); + cy.contains('monitoring').should('be.visible'); + }); + + it('renders multiple labels', () => { + const labels = { + app: 'monitoring', + env: 'production', + team: 'platform', + }; + cy.mount(); + + cy.contains('app').should('be.visible'); + cy.contains('monitoring').should('be.visible'); + cy.contains('env').should('be.visible'); + cy.contains('production').should('be.visible'); + cy.contains('team').should('be.visible'); + cy.contains('platform').should('be.visible'); + }); + + it('renders label with key=value format', () => { + cy.mount(); + cy.get('.pf-v6-c-label').within(() => { + cy.contains('severity'); + cy.contains('='); + cy.contains('critical'); + }); + }); +}); diff --git a/web/cypress/component/mocks/AddToDashboardButton.tsx b/web/cypress/component/mocks/AddToDashboardButton.tsx new file mode 100644 index 000000000..6f0a17276 --- /dev/null +++ b/web/cypress/component/mocks/AddToDashboardButton.tsx @@ -0,0 +1,8 @@ +export const AddToDashboardButton = ({ query, name, description }) => ( +