Skip to content

Commit 52d92cd

Browse files
Merge pull request #797 from iNecas/ols-tool-ui
OU-1264: mcpToolUI basic implementation for OpenShift Lightpseed + obs-mcp + Perses
2 parents 11c9227 + 07184ff commit 52d92cd

28 files changed

Lines changed: 845 additions & 17 deletions

AGENTS.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,54 @@ npm run cypress:open
283283

284284
For detailed testing instructions, see `web/cypress/CYPRESS_TESTING_GUIDE.md`
285285

286+
### Cypress Component Testing
287+
288+
#### Overview
289+
290+
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.
291+
292+
- **Test location**: `web/cypress/component/`
293+
- **Support file**: `web/cypress/support/component.ts`
294+
- **Config**: `component` section in `web/cypress.config.ts`
295+
296+
#### When to Create Component Tests
297+
298+
- Testing a component's rendering logic (conditional display, empty states)
299+
- Verifying props are handled correctly
300+
- Validating user interactions within a single component
301+
- When E2E tests would be overkill for the behavior under test
302+
303+
#### Quick Test Commands
304+
305+
```bash
306+
cd web
307+
308+
# Interactive mode
309+
npm run cypress:open:component
310+
311+
# Headless mode - all component tests
312+
npm run cypress:run:component
313+
314+
# Run a single component test file
315+
npx cypress run --component --spec cypress/component/labels.cy.tsx
316+
```
317+
318+
#### Writing a Component Test
319+
320+
Component test files use the `.cy.tsx` extension and go in `web/cypress/component/`:
321+
322+
```typescript
323+
import React from 'react';
324+
import { MyComponent } from '../../src/components/MyComponent';
325+
326+
describe('MyComponent', () => {
327+
it('renders correctly', () => {
328+
cy.mount(<MyComponent prop="value" />);
329+
cy.contains('expected text').should('be.visible');
330+
});
331+
});
332+
```
333+
286334
### Release Pipeline:
287335

288336
- **Konflux**: Handles CI/CD and release automation

CONTRIBUTING.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,23 @@ cd web/cypress
411411
npm run cypress:run --spec "cypress/e2e/**/regression/**"
412412
```
413413

414+
### Component Tests (Cypress)
415+
416+
For testing individual React components in isolation (no cluster required):
417+
418+
- Test files: `web/cypress/component/` (`.cy.tsx` extension)
419+
- Support file: `web/cypress/support/component.ts`
420+
421+
```bash
422+
cd web
423+
424+
# Interactive mode
425+
npm run cypress:open:component
426+
427+
# Headless mode
428+
npm run cypress:run:component
429+
```
430+
414431
---
415432

416433
## Internationalization (i18n)

config/perses-dashboards.patch.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,5 +119,18 @@
119119
"component": { "$codeRef": "DashboardPage" }
120120
}
121121
}
122+
},
123+
{
124+
"op": "add",
125+
"path": "/extensions/1",
126+
"value": {
127+
"type": "ols.tool-ui",
128+
"properties": {
129+
"id": "mcp-obs/show-timeseries",
130+
"component": {
131+
"$codeRef": "ols-tool-ui.ShowTimeseries"
132+
}
133+
}
134+
}
122135
}
123136
]

web/cypress.config.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as fs from 'fs-extra';
33
import * as console from 'console';
44
import * as path from 'path';
55
import registerCypressGrep from '@cypress/grep/src/plugin';
6+
import { DefinePlugin, NormalModuleReplacementPlugin } from 'webpack';
67

78
const getLoginCredentials = (index: number): { username: string; password: string } => {
89
const users = (process.env.CYPRESS_LOGIN_USERS || '').split(',').filter(Boolean);
@@ -175,4 +176,61 @@ export default defineConfig({
175176
experimentalMemoryManagement: true,
176177
experimentalStudio: true,
177178
},
179+
component: {
180+
devServer: {
181+
framework: 'react',
182+
bundler: 'webpack',
183+
webpackConfig: {
184+
resolve: {
185+
extensions: ['.ts', '.tsx', '.js', '.jsx'],
186+
alias: {
187+
'@perses-dev/plugin-system': path.resolve(__dirname, 'cypress/component/mocks/perses-plugin-system.tsx'),
188+
'@perses-dev/dashboards': path.resolve(__dirname, 'cypress/component/mocks/perses-dashboards.tsx'),
189+
'@perses-dev/prometheus-plugin': path.resolve(__dirname, 'cypress/component/mocks/perses-prometheus-plugin.ts'),
190+
},
191+
},
192+
module: {
193+
rules: [
194+
{
195+
test: /\.(jsx?|tsx?)$/,
196+
exclude: /node_modules/,
197+
use: { loader: 'swc-loader' },
198+
},
199+
{
200+
test: /\.scss$/,
201+
exclude: /node_modules\/(?!(@patternfly|@openshift-console\/plugin-shared)\/).*/,
202+
use: ['style-loader', 'css-loader', 'sass-loader'],
203+
},
204+
{
205+
test: /\.css$/,
206+
use: ['style-loader', 'css-loader'],
207+
},
208+
{
209+
test: /\.(png|jpg|jpeg|gif|svg|woff2?|ttf|eot|otf)(\?.*$|$)/,
210+
type: 'asset/resource',
211+
},
212+
{
213+
test: /\.m?js/,
214+
resolve: { fullySpecified: false },
215+
},
216+
],
217+
},
218+
plugins: [
219+
new DefinePlugin({
220+
'process.env.I18N_NAMESPACE': JSON.stringify('plugin__monitoring-plugin'),
221+
}),
222+
new NormalModuleReplacementPlugin(
223+
/helpers\/OlsToolUIPersesWrapper/,
224+
path.resolve(__dirname, 'cypress/component/mocks/OlsToolUIPersesWrapper.tsx'),
225+
),
226+
new NormalModuleReplacementPlugin(
227+
/helpers\/AddToDashboardButton/,
228+
path.resolve(__dirname, 'cypress/component/mocks/AddToDashboardButton.tsx'),
229+
),
230+
],
231+
},
232+
},
233+
specPattern: './cypress/component/**/*.cy.{js,jsx,ts,tsx}',
234+
supportFile: './cypress/support/component.ts',
235+
},
178236
});

web/cypress/CYPRESS_TESTING_GUIDE.md

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ The Monitoring Plugin uses a 3-layer architecture for test organization:
6767

6868
```
6969
cypress/
70+
├── component/ # Component tests (isolated, no cluster needed)
7071
├── e2e/
7172
│ ├── monitoring/ # Core monitoring tests (Administrator)
7273
│ │ ├── 00.bvt_admin.cy.ts
@@ -81,7 +82,9 @@ cypress/
8182
│ │ ├── 02.reg_metrics.cy.ts
8283
│ │ └── 03.reg_legacy_dashboards.cy.ts
8384
│ ├── perses/ # COO/Perses scenarios
84-
│ └── commands/ # Custom Cypress commands
85+
│ ├── commands/ # Custom Cypress commands
86+
│ ├── component.ts # Component test support (mount command)
87+
│ └── component-index.html # HTML template for component mounting
8588
└── views/ # Page object models (reusable actions)
8689
```
8790

@@ -92,7 +95,65 @@ cypress/
9295

9396
---
9497

95-
## Creating Tests
98+
## Component Testing
99+
100+
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.
101+
102+
### When to Use Component Tests vs E2E Tests
103+
104+
| Use Component Tests When | Use E2E Tests When |
105+
|---|---|
106+
| Testing rendering and visual output | Testing full user workflows |
107+
| Verifying props and conditional display | Testing navigation between pages |
108+
| Validating empty/error states | Testing API integration |
109+
| Fast feedback during development | Testing cross-component interactions |
110+
111+
### Writing Component Tests
112+
113+
Component test files use the `.cy.tsx` extension and live in `cypress/component/`:
114+
115+
```typescript
116+
import React from 'react';
117+
import { Labels } from '../../src/components/labels';
118+
119+
describe('Labels', () => {
120+
it('renders "No labels" when labels is empty', () => {
121+
cy.mount(<Labels labels={{}} />);
122+
cy.contains('No labels').should('be.visible');
123+
});
124+
125+
it('renders a single label', () => {
126+
cy.mount(<Labels labels={{ app: 'monitoring' }} />);
127+
cy.contains('app').should('be.visible');
128+
cy.contains('monitoring').should('be.visible');
129+
});
130+
});
131+
```
132+
133+
### Running Component Tests
134+
135+
```bash
136+
cd web
137+
138+
# Interactive mode (GUI) - best for development
139+
npm run cypress:open:component
140+
141+
# Headless mode - best for CI
142+
npm run cypress:run:component
143+
144+
# Run a single component test file
145+
npx cypress run --component --spec cypress/component/labels.cy.tsx
146+
```
147+
148+
### Key Differences from E2E Tests
149+
150+
- **No cluster required**: Components are mounted directly in the browser
151+
- **Custom mount command**: Use `cy.mount(<Component />)` instead of `cy.visit()`
152+
- **Support file**: Uses `cypress/support/component.ts` (not `cypress/support/index.ts`)
153+
154+
---
155+
156+
## Creating E2E Tests
96157

97158
### Workflow
98159

@@ -133,11 +194,12 @@ export const runAlertTests = (perspective: string) => {
133194

134195
| Scenario | Action |
135196
|----------|--------|
136-
| New UI feature | Create new test scenario in support/ |
197+
| New UI feature | Create new E2E test scenario in support/ |
137198
| Bug fix | Add test case to existing support file |
138199
| Component update | Update existing test scenarios |
139-
| New Perses feature | Create new test scenario in support/ |
140-
| ACM integration | Add test in e2e/coo/ |
200+
| New Perses feature | Create new E2E test scenario in support/ |
201+
| ACM integration | Add E2E test in e2e/coo/ |
202+
| Isolated component logic | Add component test in component/ |
141203

142204
### Best Practices
143205

web/cypress/README.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -352,20 +352,34 @@ export CYPRESS_SESSION=true
352352

353353
---
354354

355+
## Component Testing
356+
357+
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
358+
to write and run the tests.
359+
360+
### Configuration
361+
362+
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`.
363+
364+
---
365+
355366
## Test Organization
356367

357368
### Directory Structure
358369

359370
```
360371
cypress/
361-
├── e2e/ # Test files by perspective
372+
├── component/ # Component test files (.cy.tsx)
373+
├── e2e/ # E2E test files by perspective
362374
│ ├── monitoring/ # Core monitoring (Administrator)
363375
│ ├── coo/ # COO-specific tests
364376
│ └── virtualization/ # Virtualization integration
365377
├── support/ # Reusable test scenarios
366378
│ ├── monitoring/ # Test scenario modules
367379
│ ├── perses/ # Perses scenarios
368-
│ └── commands/ # Custom Cypress commands
380+
│ ├── commands/ # Custom Cypress commands
381+
│ ├── component.ts # Component test support
382+
│ └── component-index.html # Component test HTML template
369383
├── views/ # Page object models
370384
├── fixtures/ # Test data and mocks
371385
└── E2E_TEST_SCENARIOS.md # Complete test catalog
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { Labels } from '../../src/components/labels';
2+
3+
describe('Labels', () => {
4+
it('renders "No labels" when labels is empty', () => {
5+
cy.mount(<Labels labels={{}} />);
6+
cy.contains('No labels').should('be.visible');
7+
});
8+
9+
it('renders "No labels" when labels is undefined', () => {
10+
cy.mount(<Labels labels={undefined} />);
11+
cy.contains('No labels').should('be.visible');
12+
});
13+
14+
it('renders a single label', () => {
15+
cy.mount(<Labels labels={{ app: 'monitoring' }} />);
16+
cy.contains('app').should('be.visible');
17+
cy.contains('monitoring').should('be.visible');
18+
});
19+
20+
it('renders multiple labels', () => {
21+
const labels = {
22+
app: 'monitoring',
23+
env: 'production',
24+
team: 'platform',
25+
};
26+
cy.mount(<Labels labels={labels} />);
27+
28+
cy.contains('app').should('be.visible');
29+
cy.contains('monitoring').should('be.visible');
30+
cy.contains('env').should('be.visible');
31+
cy.contains('production').should('be.visible');
32+
cy.contains('team').should('be.visible');
33+
cy.contains('platform').should('be.visible');
34+
});
35+
36+
it('renders label with key=value format', () => {
37+
cy.mount(<Labels labels={{ severity: 'critical' }} />);
38+
cy.get('.pf-v6-c-label').within(() => {
39+
cy.contains('severity');
40+
cy.contains('=');
41+
cy.contains('critical');
42+
});
43+
});
44+
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export const AddToDashboardButton = ({ query, name, description }) => (
2+
<button
3+
data-testid="add-to-dashboard"
4+
data-name={name}
5+
data-query={query}
6+
data-description={description}
7+
/>
8+
);
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export const OlsToolUIPersesWrapper = ({
2+
children,
3+
initialTimeRange,
4+
initialTimeZone = 'local',
5+
}) => (
6+
<div
7+
data-testid="perses-wrapper"
8+
data-time-range={JSON.stringify(initialTimeRange)}
9+
data-timezone={initialTimeZone}
10+
>
11+
{children}
12+
</div>
13+
);
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export const Panel = ({ definition, panelOptions }) => (
2+
<div data-testid="mock-panel">
3+
<div data-testid="panel-definition" data-definition={JSON.stringify(definition)} />
4+
{panelOptions?.extra && <div data-testid="panel-extra">{panelOptions.extra()}</div>}
5+
</div>
6+
);
7+
8+
export const VariableProvider = ({ children }) => <>{children}</>;

0 commit comments

Comments
 (0)