Skip to content

Commit 533fc6a

Browse files
feat(custom command): test-ucc-ui as custom ui test command (#1834)
**Issue number:** https://splunk.atlassian.net/browse/ADDON-82195 ### PR Type **What kind of change does this PR introduce?** * [x] Feature * [ ] Bug Fix * [ ] Refactoring (no functional or API changes) * [ ] Documentation Update * [ ] Maintenance (dependency updates, CI, etc.) ## Summary Custom Ui command to run basic ui tests with test coverage. ### Changes The command enables to test inputs and configuration page based on their globalConfig.json. It enables users to render each page and run tests accordingly. One issue is that mocking for Splunk libraries does not work, so ie. features like hideForPlatform is not testable with this approach as mocking for search-job is not possible. ### User experience Additional command available. ## Checklist If an item doesn't apply to your changes, leave it unchecked. ### Review * [x] self-review - I have performed a self-review of this change according to the [development guidelines](https://splunk.github.io/addonfactory-ucc-generator/contributing/#development-guidelines) * [x] Changes are documented. The documentation is understandable, examples work [(more info)](https://splunk.github.io/addonfactory-ucc-generator/contributing/#documentation-guidelines) * [x] PR title and description follows the [contributing principles](https://splunk.github.io/addonfactory-ucc-generator/contributing/#pull-requests) * [ ] meeting - I have scheduled a meeting or recorded a demo to explain these changes (if there is a video, put a link below and in the ticket) ### Tests See [the testing doc](https://splunk.github.io/addonfactory-ucc-generator/contributing/#build-and-test). * [ ] Unit - tests have been added/modified to cover the changes * [ ] Smoke - tests have been added/modified to cover the changes * [ ] UI - tests have been added/modified to cover the changes * [ ] coverage - I have checked the code coverage of my changes [(see more)](https://splunk.github.io/addonfactory-ucc-generator/contributing/#checking-the-code-coverage) **Demo/meeting:** *Reviewers are encouraged to request meetings or demos if any part of the change is unclear*
1 parent a44a581 commit 533fc6a

File tree

9 files changed

+426
-11
lines changed

9 files changed

+426
-11
lines changed
Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
# Testing UI Components in UCC Framework
2+
3+
This guide shows you how to test custom UI components in your Splunk Technology Add-on (TA) using the Unified Configuration Console (UCC) framework.
4+
5+
---
6+
7+
## What You'll Need
8+
9+
Before starting, make sure you have:
10+
11+
- **Node.js** version 22 or higher
12+
- **npm** version 10 or higher
13+
14+
---
15+
16+
## Getting Started
17+
18+
### 1. Set Up Your UI Project
19+
20+
First, create your UI sub-project by following the [UI Sub-Project Setup Guide](./custom_project_init.md).
21+
22+
### 2. Create Your Test Files
23+
24+
Create test files anywhere in your `src` directory. Use any of these file extensions:
25+
26+
- `.test.js`, `.test.ts`, `.test.jsx`, `.test.tsx`
27+
- `.spec.js`, `.spec.ts`, `.spec.jsx`, `.spec.tsx`
28+
29+
The testing framework will automatically find files matching the pattern `src/**/*.{js,jsx,ts,tsx}`.
30+
31+
### 3. Add the Test Command
32+
33+
Update your `package.json` file to include the test command:
34+
35+
```json
36+
{
37+
"scripts": {
38+
"ucc-test": "test-ucc-ui"
39+
}
40+
}
41+
```
42+
43+
#### Complete package.json Example
44+
45+
If you followed the UI Sub-Project Setup Guide, your `package.json` should look like this:
46+
47+
<details>
48+
<summary>View complete package.json</summary>
49+
50+
```json
51+
{
52+
"name": "ui",
53+
"private": true,
54+
"version": "0.0.0",
55+
"type": "module",
56+
"scripts": {
57+
"ucc-gen": "ucc-gen-ui ta_name=Splunk_TA_Name init_file_dir=src/ucc-ui.ts",
58+
"ucc-test": "test-ucc-ui"
59+
},
60+
"dependencies": {
61+
"@splunk/add-on-ucc-framework": "^5.65.0",
62+
"@splunk/react-ui": "^4.42.0",
63+
"@splunk/splunk-utils": "^3.1.0",
64+
"@splunk/themes": "^0.23.0",
65+
"react": "16.14.0",
66+
"react-dom": "16.14.0"
67+
},
68+
"devDependencies": {
69+
"@eslint/js": "^9.20.0",
70+
"@types/node": "^22.13.1",
71+
"@types/react": "16.14.62",
72+
"@types/react-dom": "16.9.25",
73+
"typescript": "^5.8.2"
74+
},
75+
"overrides": {
76+
"react": "16.14.0",
77+
"react-dom": "16.14.0",
78+
"@types/react": "16.14.62",
79+
"@types/react-dom": "16.9.25"
80+
},
81+
"engines": {
82+
"node": ">=22",
83+
"npm": ">=10"
84+
}
85+
}
86+
```
87+
88+
</details>
89+
90+
---
91+
92+
## Testing Your UI Components
93+
94+
The UCC framework provides two main functions to help you test your pages:
95+
96+
### Testing Configuration Pages
97+
98+
Use `renderConfigurationPage()` to test your configuration pages (like account settings).
99+
100+
**Parameters:**
101+
102+
- `globalConfig` (required): Your globalConfig.json file content
103+
- `customComponents` (optional): Object containing your custom UI components
104+
105+
**Example:**
106+
107+
```typescript
108+
import { screen, waitForElementToBeRemoved } from "@testing-library/react";
109+
import { it, expect } from "vitest";
110+
import userEvent from "@testing-library/user-event";
111+
112+
import { getGlobalConfig } from "./utils";
113+
import AdvancedInputsTabClass from "../ucc-ui-extensions/AdvancedInputsTab";
114+
import DateInputClass from "../ucc-ui-extensions/DateInput";
115+
116+
it("Should open account addition form", async () => {
117+
mockResponse();
118+
119+
renderConfigurationPage(getGlobalConfig(), {
120+
DateInput: {
121+
component: DateInputClass,
122+
type: "control",
123+
},
124+
AdvancedInputsTab: {
125+
component: AdvancedInputsTabClass,
126+
type: "tab",
127+
},
128+
});
129+
130+
// Wait for page to load
131+
await waitForElementToBeRemoved(() => screen.getByText("Waiting"));
132+
133+
// Check if page elements are present
134+
expect(screen.getByText("Configuration")).toBeInTheDocument();
135+
expect(await screen.findByText("Mocked Account name")).toBeInTheDocument();
136+
137+
// Test clicking the Add button
138+
const addButton = screen.getByRole("button", { name: "Add" });
139+
expect(addButton).toBeInTheDocument();
140+
141+
await userEvent.click(addButton);
142+
expect(await screen.findByText("Add Accounts")).toBeInTheDocument();
143+
});
144+
```
145+
146+
### Testing Input Pages
147+
148+
Use `renderInputsPage()` to test your input pages (like data input configurations).
149+
150+
**Parameters:** Same as `renderConfigurationPage()`
151+
152+
- `globalConfig` (required): Your globalConfig.json file content
153+
- `customComponents` (optional): Object containing your custom UI components
154+
155+
**Example:**
156+
157+
```typescript
158+
import { screen, waitForElementToBeRemoved } from "@testing-library/react";
159+
import { it, expect } from "vitest";
160+
import userEvent from "@testing-library/user-event";
161+
162+
import { getGlobalConfig } from "./utils";
163+
import AdvancedInputsTabClass from "../ucc-ui-extensions/AdvancedInputsTab";
164+
import DateInputClass from "../ucc-ui-extensions/DateInput";
165+
166+
it("Should open inputs addition form", async () => {
167+
mockResponse();
168+
169+
renderInputsPage(getGlobalConfig(), {
170+
DateInput: {
171+
component: DateInputClass,
172+
type: "control",
173+
},
174+
AdvancedInputsTab: {
175+
component: AdvancedInputsTabClass,
176+
type: "tab",
177+
},
178+
});
179+
180+
// Wait for page to load
181+
await waitForElementToBeRemoved(() => screen.getByText("Waiting"));
182+
183+
// Check if page elements are present
184+
expect(screen.getByText("Inputs")).toBeInTheDocument();
185+
expect(await screen.findByText("Mocked Input name")).toBeInTheDocument();
186+
187+
// Test clicking the Create button
188+
const createButton = screen.getByRole("button", { name: "Create New Input" });
189+
expect(createButton).toBeInTheDocument();
190+
191+
await userEvent.click(createButton);
192+
expect(await screen.findByText("Add Example service name")).toBeInTheDocument();
193+
});
194+
```
195+
196+
---
197+
198+
## Testing Best Practices
199+
200+
### Mock API Responses
201+
202+
We recommend using [MSW (Mock Service Worker)](https://mswjs.io/) to mock API calls in your tests.
203+
204+
**1. Set up the server (server.ts):**
205+
206+
```typescript
207+
import { setupServer } from "msw/node";
208+
import { afterAll, afterEach } from "vitest";
209+
210+
export const server = setupServer();
211+
212+
server.listen({
213+
onUnhandledRequest: "warn",
214+
});
215+
216+
afterEach(() => server.resetHandlers());
217+
afterAll(() => server.close());
218+
219+
process.once("SIGINT", () => server.close());
220+
process.once("SIGTERM", () => server.close());
221+
```
222+
223+
**2. Mock responses in your tests:**
224+
225+
```typescript
226+
function mockResponse() {
227+
server.use(
228+
http.get(`/servicesNS/nobody/-/:endpointUrl/:serviceName`, () => {
229+
return HttpResponse.json(mockServerResponseWithContent);
230+
}),
231+
http.get(`/servicesNS/nobody/-/:endpointUrl`, () => {
232+
return HttpResponse.json(mockServerResponseWithContent);
233+
})
234+
);
235+
}
236+
```
237+
238+
**3. Use standard response format:**
239+
240+
```typescript
241+
export const mockServerResponseWithContent = {
242+
links: {
243+
create: `/servicesNS/nobody/Splunk_TA_Example/account/_new`,
244+
},
245+
updated: "2023-08-21T11:54:12+00:00",
246+
entry: [
247+
{
248+
id: 1,
249+
name: "Mocked Input name",
250+
content: {
251+
disabled: true,
252+
fields1: "value1",
253+
fields2: "value2",
254+
},
255+
},
256+
],
257+
messages: [],
258+
};
259+
```
260+
261+
---
262+
263+
## Running Your Tests
264+
265+
Once everything is set up, run your tests with:
266+
267+
```bash
268+
npm run ucc-test
269+
```
270+
271+
This will execute all test files in your `src` directory and show you the results.

mkdocs.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,8 @@ nav:
107107
- Custom tab: "custom_ui_extensions/standard/custom_tab.md"
108108
- Context:
109109
- Overview: "custom_ui_extensions/context/overview.md"
110-
- UI project init: "custom_ui_extensions/context/custom_project_init.md"
110+
- Initialize UI project : "custom_ui_extensions/context/custom_project_init.md"
111+
- Test UI project : "custom_ui_extensions/context/custom_test_ui_command.md"
111112
- Custom cell : "custom_ui_extensions/context/custom_cell_context.md"
112113
- Custom Tab : "custom_ui_extensions/context/custom_tab_context.md"
113114
- Custom Control : "custom_ui_extensions/context/custom_control_context.md"

ui/cli/ucc-gen-test-ui.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/usr/bin/env node
2+
import { execSync } from 'child_process';
3+
import { fileURLToPath } from 'url';
4+
import { dirname } from 'path';
5+
6+
const fileName = fileURLToPath(import.meta.url);
7+
const dirName = dirname(fileName);
8+
const viteTestUccConfigPath = `${dirName}/vite.config_ucc-test-gen-ui.ts`;
9+
10+
execSync(`vitest run --coverage --config ${viteTestUccConfigPath}`, {
11+
stdio: 'inherit',
12+
});

ui/cli/ucc-vite-test-setup.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/* eslint-disable import/no-extraneous-dependencies */
2+
import '@testing-library/jest-dom';
3+
4+
import { beforeEach, MockInstance, vi } from 'vitest';
5+
6+
// eslint-disable-next-line import/no-mutable-exports
7+
export let consoleError: MockInstance<{
8+
(...data: unknown[]): void;
9+
(message?: unknown, ...optionalParams: unknown[]): void;
10+
}>;
11+
// eslint-disable-next-line no-console
12+
const originalConsoleError = console.error;
13+
beforeEach(() => {
14+
consoleError = vi.spyOn(console, 'error');
15+
consoleError.mockImplementation((...args: Parameters<typeof console.error>) => {
16+
originalConsoleError(...args);
17+
throw new Error(
18+
`Console error was called. Call vi.spyOn(console, 'error').mockImplementation(() => {}) if this is expected.`
19+
);
20+
});
21+
});
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { defineConfig } from 'vite';
2+
import type { ViteUserConfig as VitestUserConfigInterface } from 'vitest/config';
3+
import react from '@vitejs/plugin-react';
4+
import checker from 'vite-plugin-checker';
5+
import { fileURLToPath } from 'url';
6+
import { dirname } from 'path';
7+
8+
const fileName = fileURLToPath(import.meta.url);
9+
const dirName = dirname(fileName);
10+
const ucctestSetup = `${dirName}/ucc-vite-test-setup.ts`;
11+
12+
const vitestTestConfig: VitestUserConfigInterface = {
13+
test: {
14+
watch: false,
15+
globals: true,
16+
environment: 'jsdom',
17+
setupFiles: ucctestSetup,
18+
server: {
19+
deps: {
20+
inline: ['jspdf'],
21+
},
22+
},
23+
coverage: {
24+
all: true,
25+
provider: 'istanbul',
26+
reporter: ['text'],
27+
include: ['src/**/*.{js,jsx,ts,tsx}'],
28+
},
29+
exclude: [
30+
'**/node_modules/**',
31+
'**/dist/**',
32+
'**/**.stories.**',
33+
'**/*.types.ts',
34+
'**/tests/**',
35+
'**/test/**',
36+
'**/mock*',
37+
'**/__mocks__/**',
38+
'**/*.d.ts',
39+
'**/*.types.ts',
40+
],
41+
},
42+
};
43+
44+
export default defineConfig({
45+
logLevel: 'info',
46+
plugins: [
47+
react(),
48+
checker({
49+
typescript: true,
50+
}),
51+
],
52+
resolve: {
53+
extensions: ['.tsx', '.ts', '.js'],
54+
},
55+
base: '',
56+
test: vitestTestConfig.test,
57+
});

0 commit comments

Comments
 (0)