Skip to content

Commit a3f33b3

Browse files
test(secrets): add integration test for scope dropdown labels
Verify that MultiSelect trigger displays plain text labels like "AI Gateway" instead of raw enum numbers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent dcd1de0 commit a3f33b3

File tree

1 file changed

+154
-0
lines changed

1 file changed

+154
-0
lines changed
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/**
2+
* Copyright 2025 Redpanda Data, Inc.
3+
*
4+
* Use of this software is governed by the Business Source License
5+
* included in the file https://github.com/redpanda-data/redpanda/blob/dev/licenses/bsl.md
6+
*
7+
* As of the Change Date specified in that file, in accordance with the Business Source License,
8+
* use of this software will be governed by the Apache License, Version 2.0
9+
*/
10+
11+
import { render, screen } from '@testing-library/react';
12+
import userEvent from '@testing-library/user-event';
13+
import {
14+
MultiSelect,
15+
MultiSelectContent,
16+
MultiSelectEmpty,
17+
MultiSelectItem,
18+
MultiSelectList,
19+
MultiSelectTrigger,
20+
MultiSelectValue,
21+
} from 'components/redpanda-ui/components/multi-select';
22+
import { useState } from 'react';
23+
import { beforeAll, describe, expect, it } from 'vitest';
24+
25+
import { SCOPE_OPTIONS } from './secret-form-shared';
26+
27+
// cmdk calls scrollIntoView which is not available in jsdom
28+
beforeAll(() => {
29+
Element.prototype.scrollIntoView = () => {};
30+
});
31+
32+
/**
33+
* Helper component that mirrors the scope MultiSelect usage in the
34+
* secret create / edit pages, but without any form or routing wiring.
35+
*/
36+
function ScopeMultiSelect({ defaultValue = [] }: { defaultValue?: string[] }) {
37+
const [value, setValue] = useState<string[]>(defaultValue);
38+
39+
return (
40+
<MultiSelect onValueChange={setValue} value={value}>
41+
<MultiSelectTrigger data-testid="scope-trigger">
42+
<MultiSelectValue placeholder="Select scopes" />
43+
</MultiSelectTrigger>
44+
<MultiSelectContent>
45+
<MultiSelectList>
46+
{SCOPE_OPTIONS.map((option) => (
47+
<MultiSelectItem key={option.value} label={option.label} value={option.value}>
48+
<span className="flex items-center gap-2">
49+
<option.icon className="size-4" />
50+
{option.label}
51+
</span>
52+
</MultiSelectItem>
53+
))}
54+
</MultiSelectList>
55+
<MultiSelectEmpty>No items found</MultiSelectEmpty>
56+
</MultiSelectContent>
57+
</MultiSelect>
58+
);
59+
}
60+
61+
describe('SCOPE_OPTIONS labels', () => {
62+
it('all labels are plain strings, not JSX or raw enum numbers', () => {
63+
for (const option of SCOPE_OPTIONS) {
64+
expect(typeof option.label).toBe('string');
65+
// The label should not be a stringified number (raw enum value)
66+
expect(Number.isNaN(Number(option.label))).toBe(true);
67+
}
68+
});
69+
70+
it('each option has a string label and a renderable icon component', () => {
71+
const expectedLabels = ['AI Gateway', 'MCP Server', 'AI Agent', 'Redpanda Connect', 'Redpanda Cluster'];
72+
73+
expect(SCOPE_OPTIONS.map((o) => o.label)).toEqual(expectedLabels);
74+
75+
for (const option of SCOPE_OPTIONS) {
76+
// Icons can be functions (lucide) or objects (forwardRef components)
77+
expect(['function', 'object']).toContain(typeof option.icon);
78+
}
79+
});
80+
81+
it('each option value is a numeric string matching a Scope enum value', () => {
82+
for (const option of SCOPE_OPTIONS) {
83+
expect(Number.isNaN(Number(option.value))).toBe(false);
84+
expect(Number(option.value)).toBeGreaterThan(0);
85+
}
86+
});
87+
});
88+
89+
describe('Scope MultiSelect trigger', () => {
90+
it('shows placeholder when no scopes are selected', () => {
91+
render(<ScopeMultiSelect />);
92+
93+
expect(screen.getByText('Select scopes')).toBeInTheDocument();
94+
});
95+
96+
it('displays plain text label after selecting a scope from the dropdown', async () => {
97+
const user = userEvent.setup();
98+
99+
render(<ScopeMultiSelect />);
100+
101+
const trigger = screen.getByTestId('scope-trigger');
102+
103+
// Open the dropdown by clicking the trigger
104+
await user.click(trigger);
105+
106+
// Click the first option ("AI Gateway")
107+
const firstOption = SCOPE_OPTIONS[0];
108+
const optionEl = await screen.findByText(firstOption.label);
109+
await user.click(optionEl);
110+
111+
// The trigger should now show the human-readable label text
112+
expect(trigger).toHaveTextContent(firstOption.label);
113+
114+
// It should NOT show the raw enum value as standalone text
115+
const triggerText = trigger.textContent ?? '';
116+
const rawNumberPattern = new RegExp(`(^|\\s)${firstOption.value}(\\s|$)`);
117+
if (!firstOption.label.includes(firstOption.value)) {
118+
expect(triggerText).not.toMatch(rawNumberPattern);
119+
}
120+
});
121+
122+
it('displays plain text labels for multiple selected scopes', async () => {
123+
const user = userEvent.setup();
124+
125+
render(<ScopeMultiSelect />);
126+
127+
const trigger = screen.getByTestId('scope-trigger');
128+
129+
// Open dropdown and select two options
130+
await user.click(trigger);
131+
132+
const first = SCOPE_OPTIONS[0];
133+
const second = SCOPE_OPTIONS[1];
134+
135+
await user.click(await screen.findByText(first.label));
136+
await user.click(await screen.findByText(second.label));
137+
138+
// Close the dropdown
139+
await user.keyboard('{Escape}');
140+
141+
// Both labels should appear in the trigger
142+
expect(trigger).toHaveTextContent(first.label);
143+
expect(trigger).toHaveTextContent(second.label);
144+
145+
// Raw numeric enum values should not appear as standalone text
146+
const triggerText = trigger.textContent ?? '';
147+
for (const option of [first, second]) {
148+
const rawNumberPattern = new RegExp(`(^|\\s)${option.value}(\\s|$)`);
149+
if (!option.label.includes(option.value)) {
150+
expect(triggerText).not.toMatch(rawNumberPattern);
151+
}
152+
}
153+
});
154+
});

0 commit comments

Comments
 (0)