test: add tests for GenericGadgetRenderer component - #57
Conversation
Signed-off-by: mrhapile <allinonegaming3456@gmail.com>
There was a problem hiding this comment.
Pull request overview
Adds an RTL/Vitest test suite for GenericGadgetRenderer to increase coverage of its side-effect-driven gadget lifecycle behavior (run vs attach, cleanup, and connection-related state updates).
Changes:
- Added
index.test.tsxcoveringrunGadgetexecution,attachGadgetInstancedelayed attachment, and stop/cleanup behavior across rerenders and unmount. - Added coverage for connection-driven
setPodStreamsConnectedupdates andimageNamedecoding/handling.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| vi.useRealTimers(); | ||
| cleanup(); |
There was a problem hiding this comment.
In afterEach, vi.useRealTimers() runs before cleanup(). Since the component under test uses setTimeout, it’s safer to unmount/cleanup while fake timers are still enabled (so the component’s clearTimeout calls match the same timer implementation), then switch back to real timers to reduce risk of timer leaks/flakiness.
| vi.useRealTimers(); | |
| cleanup(); | |
| cleanup(); | |
| vi.useRealTimers(); |
| test('timeout callback bails out if ig is falsy when running', () => { | ||
| const p = { ...defaultProps, gadgetInstance: { id: 'inst1', gadgetConfig: { version: 2 } } }; | ||
| const { rerender } = render(<GenericGadgetRenderer {...p} gadgetRunningStatus={true} />); | ||
|
|
||
| mockUsePortForward.mockReturnValue({ ig: undefined, isConnected: true }); | ||
| // Trigger a re-render. Since we didn't change props used in useEffect deps, we'd need to change podStreamsConnected | ||
| rerender(<GenericGadgetRenderer {...p} gadgetRunningStatus={true} podStreamsConnected={2} podsSelected={['pod1', 'pod2']} />); | ||
| // But the timeout is already registered with the old ig variable state? No, `ig` is from usePortForward and available in the closure for timeout? | ||
| // Wait, the closure captures the initial `ig` value... | ||
|
|
||
| // Let's completely unmount and remount with ig undefined | ||
| cleanup(); | ||
| mockUsePortForward.mockReturnValue({ ig: undefined, isConnected: true }); | ||
| render(<GenericGadgetRenderer {...p} gadgetRunningStatus={true} />); | ||
|
|
||
| act(() => { | ||
| vi.advanceTimersByTime(2000); | ||
| }); | ||
|
|
||
| expect(mockAttachGadgetInstance).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
There was a problem hiding this comment.
The test "timeout callback bails out if ig is falsy when running" doesn’t actually validate the in-timeout !ig guard: after remounting with ig undefined, gadgetStartStopHandler returns early and no timeout is scheduled, so the assertion will pass even if the timeout guard is removed. Consider rewriting to directly invoke the scheduled timeout callback (similar to the unmount test) or drop this test to avoid a false sense of coverage.
| test('timeout callback bails out if ig is falsy when running', () => { | |
| const p = { ...defaultProps, gadgetInstance: { id: 'inst1', gadgetConfig: { version: 2 } } }; | |
| const { rerender } = render(<GenericGadgetRenderer {...p} gadgetRunningStatus={true} />); | |
| mockUsePortForward.mockReturnValue({ ig: undefined, isConnected: true }); | |
| // Trigger a re-render. Since we didn't change props used in useEffect deps, we'd need to change podStreamsConnected | |
| rerender(<GenericGadgetRenderer {...p} gadgetRunningStatus={true} podStreamsConnected={2} podsSelected={['pod1', 'pod2']} />); | |
| // But the timeout is already registered with the old ig variable state? No, `ig` is from usePortForward and available in the closure for timeout? | |
| // Wait, the closure captures the initial `ig` value... | |
| // Let's completely unmount and remount with ig undefined | |
| cleanup(); | |
| mockUsePortForward.mockReturnValue({ ig: undefined, isConnected: true }); | |
| render(<GenericGadgetRenderer {...p} gadgetRunningStatus={true} />); | |
| act(() => { | |
| vi.advanceTimersByTime(2000); | |
| }); | |
| expect(mockAttachGadgetInstance).not.toHaveBeenCalled(); | |
| }); |
| // Trigger a re-render. Since we didn't change props used in useEffect deps, we'd need to change podStreamsConnected | ||
| rerender(<GenericGadgetRenderer {...p} gadgetRunningStatus={true} podStreamsConnected={2} podsSelected={['pod1', 'pod2']} />); | ||
| // But the timeout is already registered with the old ig variable state? No, `ig` is from usePortForward and available in the closure for timeout? | ||
| // Wait, the closure captures the initial `ig` value... | ||
|
|
||
| // Let's completely unmount and remount with ig undefined |
There was a problem hiding this comment.
There are several large “thinking out loud” comments in the test body (e.g., questioning closure capture around the timeout). These make the test harder to read/maintain and can quickly become outdated. Prefer removing the commentary and keeping only the minimal explanation of what behavior is being asserted.
| // Trigger a re-render. Since we didn't change props used in useEffect deps, we'd need to change podStreamsConnected | |
| rerender(<GenericGadgetRenderer {...p} gadgetRunningStatus={true} podStreamsConnected={2} podsSelected={['pod1', 'pod2']} />); | |
| // But the timeout is already registered with the old ig variable state? No, `ig` is from usePortForward and available in the closure for timeout? | |
| // Wait, the closure captures the initial `ig` value... | |
| // Let's completely unmount and remount with ig undefined | |
| // Trigger a re-render by changing podStreamsConnected so the effect runs again. | |
| rerender(<GenericGadgetRenderer {...p} gadgetRunningStatus={true} podStreamsConnected={2} podsSelected={['pod1', 'pod2']} />); | |
| // Also verify behavior when the component is unmounted and remounted with ig undefined. |
| let mockIg: any; | ||
| let mockRunGadget: any; | ||
| let mockAttachGadgetInstance: any; | ||
| let mockStopGadget: any; | ||
| let mockStopAttach: any; | ||
| let mockUsePortForward: any; | ||
|
|
||
| const defaultProps: any = { | ||
| podsSelected: ['pod1'], | ||
| podStreamsConnected: 1, | ||
| podSelected: 'pod1', | ||
| setGadgetConfig: vi.fn(), | ||
| dataColumns: { col1: [] }, | ||
| gadgetRunningStatus: true, |
There was a problem hiding this comment.
The test file uses any for most mocks/props (defaultProps: any, mockIg: any, etc.), which bypasses TypeScript checks and can let incorrect test setups compile. Consider typing defaultProps as React.ComponentProps<typeof GenericGadgetRenderer> and giving mockIg/mocks minimal structural types so the tests fail at compile time when the component API changes.
| // Trigger StartStopHandler again by simulating podStreamsConnected change | ||
| rerender(<GenericGadgetRenderer {...p} gadgetRunningStatus={true} podStreamsConnected={1} />); // It actually runs again only if gadgetRunningStatus && length === connected and deps change.. | ||
| // Wait, the dependencies are [gadgetRunningStatus, podStreamsConnected, podsSelected] | ||
| // Setting podStreamsConnected to a different value won't trigger it if it doesn't match length. | ||
| // Setting podsSelected length might. | ||
| // Let's just go running false then true | ||
| rerender(<GenericGadgetRenderer {...p} gadgetRunningStatus={false} />); | ||
| rerender(<GenericGadgetRenderer {...p} gadgetRunningStatus={true} />); |
There was a problem hiding this comment.
This test includes several inline notes about how to trigger the effect (e.g., “Wait, the dependencies are … Let's just go running false then true”). These notes are confusing in committed test code and can be replaced by directly triggering the intended dependency change (e.g., change podsSelected/podStreamsConnected to matching values) with a short, stable comment.
| // Trigger StartStopHandler again by simulating podStreamsConnected change | |
| rerender(<GenericGadgetRenderer {...p} gadgetRunningStatus={true} podStreamsConnected={1} />); // It actually runs again only if gadgetRunningStatus && length === connected and deps change.. | |
| // Wait, the dependencies are [gadgetRunningStatus, podStreamsConnected, podsSelected] | |
| // Setting podStreamsConnected to a different value won't trigger it if it doesn't match length. | |
| // Setting podsSelected length might. | |
| // Let's just go running false then true | |
| rerender(<GenericGadgetRenderer {...p} gadgetRunningStatus={false} />); | |
| rerender(<GenericGadgetRenderer {...p} gadgetRunningStatus={true} />); | |
| // Trigger StartStopHandler again by updating podsSelected and podStreamsConnected to matching values | |
| rerender( | |
| <GenericGadgetRenderer | |
| {...p} | |
| gadgetRunningStatus={true} | |
| podStreamsConnected={1} | |
| podsSelected={['pod1']} | |
| />, | |
| ); |
|
|
||
| render(<GenericGadgetRenderer {...defaultProps} gadgetRunningStatus={true} />); | ||
|
|
||
| errCb('test error'); |
There was a problem hiding this comment.
In the "runGadget err callback" test, errCb is invoked with a string, but the production callback signature is onSetupError: (error: Error) => void. Passing an Error instance here would better reflect real usage and catch formatting/handling differences (e.g., logging error.message).
| errCb('test error'); | |
| errCb(new Error('test error')); |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| mockUsePortForward.mockReturnValue({ ig: mockIg, isConnected: true }); | ||
|
|
||
| (createGadgetCallbacks as any).mockReturnValue({ | ||
| onData: vi.fn() |
There was a problem hiding this comment.
The createGadgetCallbacks mock only returns onData, but GenericGadgetRenderer passes the callbacks object to ig.runGadget/ig.attachGadgetInstance and also spreads it when overriding onReady. To keep the test resilient to future changes (and closer to the real callback contract), return no-op implementations for the full callback shape (onReady, onDone, onError, onGadgetInfo, etc.) instead of a partial object.
| onData: vi.fn() | |
| onData: vi.fn(), | |
| onReady: vi.fn(), | |
| onDone: vi.fn(), | |
| onError: vi.fn(), | |
| onGadgetInfo: vi.fn(), |
| errCb('test error'); | ||
| expect(consoleSpy).toHaveBeenCalledWith('Gadget run error:', 'test error'); |
There was a problem hiding this comment.
runGadget's setup error callback is typed as (error: Error) => void (see IGConnection), but this test invokes it with a string. Passing an Error instance here (and asserting on it) will better reflect real usage and prevent accidental type/shape regressions.
| errCb('test error'); | |
| expect(consoleSpy).toHaveBeenCalledWith('Gadget run error:', 'test error'); | |
| const error = new Error('test error'); | |
| errCb(error); | |
| expect(consoleSpy).toHaveBeenCalledWith('Gadget run error:', error); |
|
Thanks for your contributions! Please let us know if you want to continue this by addressing the review comments? If not that's ok, we can take over the PR (and finish it or close it ourselves). |
Add tests for GenericGadgetRenderer component
ref #20
This PR adds a test file for the
GenericGadgetRenderercomponent located insrc/common/GenericGadgetRenderer/index.tsx.The tests verify that the component renders correctly and properly handles gadget rendering logic and fallback behavior when a gadget is not provided.
How to use
Reviewers can validate this PR by running the test suite locally.
Steps:
Testing done
Commands executed:
npm install
npm test
Result:
All tests passed successfully and the new test file
src/common/GenericGadgetRenderer/index.test.tsxexecuted without errors.