Skip to content

Commit bfbc053

Browse files
committed
Enhance multi-container testing
Signed-off-by: Adameska <[email protected]>
1 parent b919362 commit bfbc053

File tree

2 files changed

+134
-11
lines changed

2 files changed

+134
-11
lines changed

packages/webview/src/component/pods/PodLogs.spec.ts

Lines changed: 89 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,69 @@ describe('PodLogs', () => {
196196
expect(writtenLog).toContain('\u001b[33m{\u001b[0m'); // yellow brace
197197
expect(writtenLog).toContain('\u001b[32m42\u001b[0m'); // green number
198198
});
199+
200+
test('should handle malformed JSON gracefully', () => {
201+
const pod = createPod(['containerName']);
202+
const mockedTerminal = createMockTerminal();
203+
setupTerminalMock(mockedTerminal);
204+
205+
render(PodLogs, { object: pod });
206+
207+
streamPodLogsMock.sendData({
208+
podName: 'podName',
209+
namespace: 'namespace',
210+
containerName: 'containerName',
211+
data: '{"level":"info"', // incomplete JSON
212+
});
213+
214+
const writtenLog = vi.mocked(mockedTerminal.write).mock.calls[0][0] as string;
215+
// Should still output the log without crashing - may have ANSI codes but text preserved
216+
expect(writtenLog).toContain('level');
217+
expect(writtenLog).toContain('info');
218+
});
219+
220+
test.each([
221+
{ level: 'error:', color: '\u001b[31;1m', desc: 'error (bright red)' },
222+
{ level: 'warn:', color: '\u001b[33m', desc: 'warn (yellow)' },
223+
{ level: 'debug:', color: '\u001b[32m', desc: 'debug (green)' },
224+
])('should colorize $desc log level', ({ level, color }) => {
225+
const pod = createPod(['containerName']);
226+
const mockedTerminal = createMockTerminal();
227+
setupTerminalMock(mockedTerminal);
228+
229+
render(PodLogs, { object: pod });
230+
231+
streamPodLogsMock.sendData({
232+
podName: 'podName',
233+
namespace: 'namespace',
234+
containerName: 'containerName',
235+
data: `${level} Some message`,
236+
});
237+
238+
const writtenLog = vi.mocked(mockedTerminal.write).mock.calls[0][0] as string;
239+
expect(writtenLog).toContain(color);
240+
expect(writtenLog).toContain('\u001b[0m');
241+
});
242+
243+
test('should pad shorter container names to align with longest', () => {
244+
const pod = createPod(['a', 'longername']);
245+
const mockedTerminal = createMockTerminal();
246+
setupTerminalMock(mockedTerminal);
247+
248+
render(PodLogs, { object: pod });
249+
250+
streamPodLogsMock.sendData({
251+
podName: 'podName',
252+
namespace: 'namespace',
253+
containerName: 'a',
254+
data: 'short name log',
255+
});
256+
257+
const writtenLog = vi.mocked(mockedTerminal.write).mock.calls[0][0] as string;
258+
// 'a' should be padded with spaces to match 'longername' length (10 chars)
259+
// Format: <padding><colored-name>|<log>
260+
expect(writtenLog).toContain('\u001b[36ma\u001b[0m|short name log');
261+
});
199262
});
200263

201264
describe('terminal initialization', () => {
@@ -212,24 +275,46 @@ describe('PodLogs', () => {
212275
});
213276

214277
describe('multi-container prefix colors', () => {
215-
test('should apply colored prefixes for each container', () => {
278+
test('should apply colored prefixes for each container', async () => {
216279
const pod = createPod(['container1', 'container2', 'container3']);
217280
const mockedTerminal = createMockTerminal();
218281
setupTerminalMock(mockedTerminal);
219282

220283
render(PodLogs, { object: pod });
221284

285+
// Wait for all 3 container subscriptions to be set up (async onMount)
286+
await streamPodLogsMock.waitForSubscriptions(3);
287+
222288
// Send log from first container
223289
streamPodLogsMock.sendData({
224290
podName: 'podName',
225291
namespace: 'namespace',
226292
containerName: 'container1',
227-
data: 'log message',
293+
data: 'log from container1',
294+
});
295+
296+
// Send log from second container
297+
streamPodLogsMock.sendData({
298+
podName: 'podName',
299+
namespace: 'namespace',
300+
containerName: 'container2',
301+
data: 'log from container2',
302+
});
303+
304+
// Send log from third container
305+
streamPodLogsMock.sendData({
306+
podName: 'podName',
307+
namespace: 'namespace',
308+
containerName: 'container3',
309+
data: 'log from container3',
228310
});
229311

230312
const calls = vi.mocked(mockedTerminal.write).mock.calls;
231-
// First container should have cyan prefix
232-
expect(calls[0][0]).toContain('\u001b[36mcontainer1\u001b[0m|');
313+
// Each container should have a colored prefix with pipe separator
314+
// Colors cycle: cyan, yellow, green
315+
expect(calls[0][0]).toContain('\u001b[36mcontainer1\u001b[0m|log from container1');
316+
expect(calls[1][0]).toContain('\u001b[33mcontainer2\u001b[0m|log from container2');
317+
expect(calls[2][0]).toContain('\u001b[32mcontainer3\u001b[0m|log from container3');
233318
});
234319
});
235320
});

packages/webview/src/stream/util/fake-stream-object.svelte.ts

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,63 @@
1616
* SPDX-License-Identifier: Apache-2.0
1717
***********************************************************************/
1818

19-
import type { StreamObject } from './stream-object';
2019
import { type IDisposable } from '@kubernetes-dashboard/channels';
20+
import { SvelteMap } from 'svelte/reactivity';
21+
import type { StreamObject } from './stream-object';
2122

2223
/**
23-
* Fake StreamObject for tests
24+
* Fake StreamObject for tests.
25+
* Supports multiple subscriptions keyed by podName/namespace/containerName.
2426
*/
2527
export class FakeStreamObject<T> implements StreamObject<T> {
26-
#callback: (data: T) => void;
28+
#callbacks = new SvelteMap<string, (data: T) => void>();
29+
2730
async subscribe(
2831
podName: string,
2932
namespace: string,
3033
containerName: string,
3134
callback: (data: T) => void,
3235
): Promise<IDisposable> {
33-
this.#callback = callback;
34-
return { dispose: () => {} } as IDisposable;
36+
const key = `${podName}/${namespace}/${containerName}`;
37+
this.#callbacks.set(key, callback);
38+
return {
39+
dispose: () => {
40+
this.#callbacks.delete(key);
41+
},
42+
} as IDisposable;
43+
}
44+
45+
/**
46+
* Send data to the callback registered for the matching podName/namespace/containerName.
47+
* The data object must have these properties to route correctly.
48+
*/
49+
sendData(data: T & { podName?: string; namespace?: string; containerName?: string }): void {
50+
const key = `${data.podName}/${data.namespace}/${data.containerName}`;
51+
const callback = this.#callbacks.get(key);
52+
if (callback) {
53+
callback(data);
54+
}
55+
}
56+
57+
/**
58+
* Returns the number of active subscriptions.
59+
* Useful for tests to verify subscriptions are set up.
60+
*/
61+
get subscriptionCount(): number {
62+
return this.#callbacks.size;
3563
}
3664

37-
sendData(data: T): void {
38-
this.#callback(data);
65+
/**
66+
* Wait for a specific number of subscriptions to be registered.
67+
* Useful for tests that need to wait for async onMount to complete.
68+
*/
69+
async waitForSubscriptions(count: number, timeoutMs: number = 1000): Promise<void> {
70+
const start = Date.now();
71+
while (this.#callbacks.size < count) {
72+
if (Date.now() - start > timeoutMs) {
73+
throw new Error(`Timeout waiting for ${count} subscriptions. Current: ${this.#callbacks.size}`);
74+
}
75+
await new Promise(resolve => setTimeout(resolve, 10));
76+
}
3977
}
4078
}

0 commit comments

Comments
 (0)