Skip to content

Commit 201d7bd

Browse files
Move the stateColor to a composable
1 parent 36431d6 commit 201d7bd

3 files changed

Lines changed: 418 additions & 96 deletions

File tree

Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
import { useStateColor } from '@shell/composables/useStateColor';
2+
import type { StateSummaryEntry } from '@shell/composables/useStateColor';
3+
4+
const mockGetters: Record<string, any> = {};
5+
const mockDispatch = jest.fn();
6+
7+
jest.mock('vuex', () => ({
8+
useStore: () => ({
9+
getters: new Proxy(mockGetters, {
10+
get(target, prop: string) {
11+
return target[prop];
12+
},
13+
}),
14+
dispatch: mockDispatch,
15+
}),
16+
}));
17+
18+
jest.mock('@shell/plugins/steve/steve-pagination-utils', () => ({
19+
__esModule: true,
20+
default: { createParamsForPagination: jest.fn(() => 'page=1&pagesize=1') },
21+
}));
22+
23+
jest.mock('@shell/plugins/steve/projectAndNamespaceFiltering.utils', () => ({
24+
__esModule: true,
25+
default: { createParam: jest.fn(() => '') },
26+
}));
27+
28+
describe('composable: useStateColor', () => {
29+
beforeEach(() => {
30+
jest.clearAllMocks();
31+
Object.keys(mockGetters).forEach((key) => delete mockGetters[key]);
32+
});
33+
34+
describe('toStateColor', () => {
35+
it.each([
36+
['running', 'success'],
37+
['active', 'success'],
38+
['completed', 'success'],
39+
['error', 'error'],
40+
['failed', 'error'],
41+
['stopped', 'error'],
42+
['warning', 'warning'],
43+
['initializing', 'warning'],
44+
['pending', 'info'],
45+
['waiting', 'info'],
46+
['creating', 'info'],
47+
])('should return %s color for known state "%s"', (state, expectedColor) => {
48+
const { toStateColor } = useStateColor();
49+
50+
expect(toStateColor(state)).toStrictEqual(expectedColor);
51+
});
52+
53+
it('should return "disabled" for state with "darker" color', () => {
54+
const { toStateColor } = useStateColor();
55+
56+
expect(toStateColor('off')).toStrictEqual('disabled');
57+
});
58+
59+
it('should return "info" for unknown states', () => {
60+
const { toStateColor } = useStateColor();
61+
62+
expect(toStateColor('unknown-state')).toStrictEqual('info');
63+
});
64+
65+
it('should be case-insensitive', () => {
66+
const { toStateColor } = useStateColor();
67+
68+
expect(toStateColor('Running')).toStrictEqual(toStateColor('running'));
69+
expect(toStateColor('ERROR')).toStrictEqual(toStateColor('error'));
70+
});
71+
72+
it('should handle empty string', () => {
73+
const { toStateColor } = useStateColor();
74+
75+
expect(toStateColor('')).toStrictEqual('info');
76+
});
77+
78+
it('should cache results across calls', () => {
79+
const { toStateColor } = useStateColor();
80+
81+
const first = toStateColor('running');
82+
const second = toStateColor('running');
83+
84+
expect(first).toStrictEqual(second);
85+
expect(first).toStrictEqual('success');
86+
});
87+
});
88+
89+
describe('resolveStateColors', () => {
90+
const schema = { links: { collection: '/k8s/clusters/local/v1/pods' } };
91+
92+
beforeEach(() => {
93+
mockGetters['cluster/schemaFor'] = () => schema;
94+
});
95+
96+
it('should not make requests when all states are already known', async() => {
97+
const { resolveStateColors } = useStateColor();
98+
99+
const entries: StateSummaryEntry[] = [{
100+
type: 'pod',
101+
summary: [{ property: 'metadata.state.name', counts: { running: 5 } }],
102+
}];
103+
104+
await resolveStateColors(entries);
105+
106+
expect(mockDispatch).not.toHaveBeenCalled();
107+
});
108+
109+
it('should fetch a resource to resolve unknown state colors', async() => {
110+
const { resolveStateColors } = useStateColor();
111+
112+
mockDispatch.mockResolvedValueOnce({
113+
data: [{
114+
metadata: {
115+
state: {
116+
name: 'customState', error: true, transitioning: false
117+
}
118+
}
119+
}],
120+
});
121+
122+
const entries: StateSummaryEntry[] = [{
123+
type: 'pod',
124+
summary: [{ property: 'metadata.state.name', counts: { customState: 3 } }],
125+
}];
126+
127+
await resolveStateColors(entries);
128+
129+
expect(mockDispatch).toHaveBeenCalledWith('cluster/request', { url: `${ schema.links.collection }?page=1&pagesize=1` });
130+
});
131+
132+
it('should resolve error states from resource metadata', async() => {
133+
const { toStateColor, resolveStateColors } = useStateColor();
134+
135+
mockDispatch.mockResolvedValueOnce({
136+
data: [{
137+
metadata: {
138+
state: {
139+
name: 'init:Error', error: true, transitioning: false
140+
}
141+
}
142+
}],
143+
});
144+
145+
const entries: StateSummaryEntry[] = [{
146+
type: 'pod',
147+
summary: [{ property: 'metadata.state.name', counts: { 'init:Error': 1 } }],
148+
}];
149+
150+
await resolveStateColors(entries);
151+
152+
expect(toStateColor('init:Error')).toStrictEqual('error');
153+
});
154+
155+
it('should resolve transitioning states as info', async() => {
156+
const { toStateColor, resolveStateColors } = useStateColor();
157+
158+
mockDispatch.mockResolvedValueOnce({
159+
data: [{
160+
metadata: {
161+
state: {
162+
name: 'init:0/1', error: false, transitioning: true
163+
}
164+
}
165+
}],
166+
});
167+
168+
const entries: StateSummaryEntry[] = [{
169+
type: 'pod',
170+
summary: [{ property: 'metadata.state.name', counts: { 'init:0/1': 2 } }],
171+
}];
172+
173+
await resolveStateColors(entries);
174+
175+
expect(toStateColor('init:0/1')).toStrictEqual('info');
176+
});
177+
178+
it('should skip entries with null summary', async() => {
179+
const { resolveStateColors } = useStateColor();
180+
181+
const entries: StateSummaryEntry[] = [{
182+
type: 'pod',
183+
summary: null,
184+
}];
185+
186+
await resolveStateColors(entries);
187+
188+
expect(mockDispatch).not.toHaveBeenCalled();
189+
});
190+
191+
it('should skip summary properties that are not metadata.state.name', async() => {
192+
const { resolveStateColors } = useStateColor();
193+
194+
const entries: StateSummaryEntry[] = [{
195+
type: 'pod',
196+
summary: [{ property: 'metadata.namespace', counts: { default: 5 } }],
197+
}];
198+
199+
await resolveStateColors(entries);
200+
201+
expect(mockDispatch).not.toHaveBeenCalled();
202+
});
203+
204+
it('should handle API errors gracefully and fall back to default color', async() => {
205+
const { toStateColor, resolveStateColors } = useStateColor();
206+
207+
mockDispatch.mockRejectedValueOnce(new Error('network error'));
208+
209+
const entries: StateSummaryEntry[] = [{
210+
type: 'pod',
211+
summary: [{ property: 'metadata.state.name', counts: { networkFailState: 1 } }],
212+
}];
213+
214+
await resolveStateColors(entries);
215+
216+
expect(toStateColor('networkFailState')).toStrictEqual('info');
217+
});
218+
219+
it('should handle response with no resource data', async() => {
220+
const { toStateColor, resolveStateColors } = useStateColor();
221+
222+
mockDispatch.mockResolvedValueOnce({ data: [] });
223+
224+
const entries: StateSummaryEntry[] = [{
225+
type: 'pod',
226+
summary: [{ property: 'metadata.state.name', counts: { emptyResult: 1 } }],
227+
}];
228+
229+
await resolveStateColors(entries);
230+
231+
expect(toStateColor('emptyResult')).toStrictEqual('info');
232+
});
233+
234+
it('should handle resource without metadata.state', async() => {
235+
const { toStateColor, resolveStateColors } = useStateColor();
236+
237+
mockDispatch.mockResolvedValueOnce({ data: [{ metadata: {} }] });
238+
239+
const entries: StateSummaryEntry[] = [{
240+
type: 'pod',
241+
summary: [{ property: 'metadata.state.name', counts: { noStateField: 1 } }],
242+
}];
243+
244+
await resolveStateColors(entries);
245+
246+
expect(toStateColor('noStateField')).toStrictEqual('info');
247+
});
248+
249+
it('should use schema.links.collection for the request URL', async() => {
250+
const { resolveStateColors } = useStateColor();
251+
const customSchema = { links: { collection: '/k8s/clusters/c-m-abc123/v1/apps.deployments' } };
252+
253+
mockGetters['cluster/schemaFor'] = () => customSchema;
254+
mockDispatch.mockResolvedValueOnce({ data: [] });
255+
256+
const entries: StateSummaryEntry[] = [{
257+
type: 'apps.deployments',
258+
summary: [{ property: 'metadata.state.name', counts: { urlTestState: 1 } }],
259+
}];
260+
261+
await resolveStateColors(entries);
262+
263+
expect(mockDispatch).toHaveBeenCalledWith('cluster/request', { url: `${ customSchema.links.collection }?page=1&pagesize=1` });
264+
});
265+
266+
it('should resolve multiple unknown states across entries', async() => {
267+
const { toStateColor, resolveStateColors } = useStateColor();
268+
269+
mockDispatch
270+
.mockResolvedValueOnce({
271+
data: [{
272+
metadata: {
273+
state: {
274+
name: 'stateA', error: true, transitioning: false
275+
}
276+
}
277+
}],
278+
})
279+
.mockResolvedValueOnce({
280+
data: [{
281+
metadata: {
282+
state: {
283+
name: 'stateB', error: false, transitioning: false
284+
}
285+
}
286+
}],
287+
});
288+
289+
const entries: StateSummaryEntry[] = [
290+
{
291+
type: 'pod',
292+
summary: [{ property: 'metadata.state.name', counts: { stateA: 1 } }],
293+
},
294+
{
295+
type: 'apps.deployments',
296+
summary: [{ property: 'metadata.state.name', counts: { stateB: 2 } }],
297+
},
298+
];
299+
300+
await resolveStateColors(entries);
301+
302+
expect(toStateColor('stateA')).toStrictEqual('error');
303+
expect(toStateColor('stateB')).toStrictEqual('warning');
304+
});
305+
});
306+
});

0 commit comments

Comments
 (0)