Skip to content

test: add tests for backgroundgadgets module - #59

Open
mrhapile wants to merge 1 commit into
inspektor-gadget:mainfrom
mrhapile:test/background-gadgets
Open

test: add tests for backgroundgadgets module#59
mrhapile wants to merge 1 commit into
inspektor-gadget:mainfrom
mrhapile:test/background-gadgets

Conversation

@mrhapile

@mrhapile mrhapile commented Mar 8, 2026

Copy link
Copy Markdown

Add tests for backgroundgadgets module

ref #20

This PR adds a test file for the backgroundgadgets module located in src/gadgets/backgroundgadgets.tsx.

The tests verify that the module exports the expected functionality and that its helper logic behaves correctly for normal and edge-case inputs.

How to use

Reviewers can validate this PR by running the test suite locally.

Steps:

  1. Checkout this branch.
  2. Install dependencies if necessary.
  3. Run the test suite.

Testing done

Commands executed:

npm install
npm test

Result:

Screenshot 2026-03-09 at 1 27 23 AM

All tests passed successfully and the new test file src/gadgets/backgroundgadgets.test.tsx executed without errors.

Signed-off-by: mrhapile <allinonegaming3456@gmail.com>
Copilot AI review requested due to automatic review settings March 8, 2026 19:59

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a Vitest/RTL test suite for the BackgroundRunning component in src/gadgets/backgroundgadgets.tsx (part of the gadgets UI), aiming to validate loading states, IG installation checks, localStorage persistence, and deletion flows.

Changes:

  • Introduces src/gadgets/backgroundgadgets.test.tsx with mocks for Headlamp CommonComponents/K8s/Utils and conn helpers.
  • Adds test cases for loading/empty/error states and for instance deletion behavior (headless vs non-headless).

💡 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.

Comment on lines +101 to +125
beforeEach(() => {
vi.clearAllMocks();
mockStorage = {};

const localStorageMock = {
getItem: vi.fn((key: string) => mockStorage[key] || null),
setItem: vi.fn((key: string, value: string) => { mockStorage[key] = value; }),
clear: vi.fn(() => { mockStorage = {}; }),
removeItem: vi.fn((key: string) => { delete mockStorage[key]; }),
};

Object.defineProperty(window, 'localStorage', {
value: localStorageMock,
writable: true
});

Object.defineProperty(global, 'localStorage', {
value: localStorageMock,
writable: true
});

mockUseListNode.mockReturnValue([[{}]]);
mockUseListPod.mockReturnValue([[{}]]);
(conn.isIGInstalled as any).mockReturnValue(true);
});

Copilot AI Mar 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In beforeEach, vi.clearAllMocks() only clears call history; it does not reset mocked implementations/return values. Since individual tests set conn.useGadgetConn.mockReturnValue(...), this can leak into later tests and create order-dependent behavior. Use vi.resetAllMocks() (or explicitly mockReset()/set a default return value for useGadgetConn in beforeEach) to keep tests isolated.

Copilot uses AI. Check for mistakes.
Comment on lines +198 to +200
let listSuccessCb: any;
const mockListGadgetInstances = vi.fn().mockImplementation((onSuccess) => {
listSuccessCb = onSuccess;

Copilot AI Mar 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

listSuccessCb is assigned but never used. If this file is type-checked with noUnusedLocals (common in TS configs), it can fail the build; even if it doesn't, it adds noise to the test. Remove it or add an assertion that uses it.

Suggested change
let listSuccessCb: any;
const mockListGadgetInstances = vi.fn().mockImplementation((onSuccess) => {
listSuccessCb = onSuccess;
const mockListGadgetInstances = vi.fn().mockImplementation((onSuccess) => {

Copilot uses AI. Check for mistakes.

test('deletes instance properly when NOT headless', async () => {
const mockListGadgetInstances = vi.fn().mockImplementation((onSuccess) => {
onSuccess([{ id: 'selected-id', name: 'my-gadget', isHeadless: false }]); // The mock parses this but overwrites its isHeadless!

Copilot AI Mar 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inline comment here is misleading: the value isn’t being overwritten by the test mock; BackgroundRunning maps instances from listGadgetInstances and forces isHeadless: true. Consider updating the comment to reflect the real reason you seed localStorage with a non-headless instance (to exercise the !instance.isHeadless branch).

Suggested change
onSuccess([{ id: 'selected-id', name: 'my-gadget', isHeadless: false }]); // The mock parses this but overwrites its isHeadless!
onSuccess([{ id: 'selected-id', name: 'my-gadget', isHeadless: false }]); // BackgroundRunning maps API instances to isHeadless: true; the actual non-headless case is exercised via the localStorage entry below.

Copilot uses AI. Check for mistakes.
Comment on lines +394 to +414
test('covers accessor fallbacks and pluralization', async () => {
const mockList = vi.fn().mockImplementation((onSuccess) => {
onSuccess([
{ id: 'inst1', isHeadless: true }, // missing name, getConfig, tags, nodes
{ id: 'inst2', name: '', gadgetConfig: { imageName: 'my-image' }, isHeadless: true }, // missing name, has imageName
{ id: 'inst3', name: '', gadgetConfig: { imageName: '' }, isHeadless: true }, // missing both, goes to Unnamed
{ id: 'selected-id', name: 'my-gadget', isHeadless: false, isEmbedded: true, kind: 'CustomKind', cluster: 'other-cluster' },
]);
});

(conn.useGadgetConn as any).mockReturnValue({
listGadgetInstances: mockList
});

render(<BackgroundRunning />);
await act(async () => { });

// Trigger selectedCount = 1, totalCount = 4 and re-render
const selectRowBtns = screen.getAllByTestId('select-row-btn');
fireEvent.click(selectRowBtns[0]);
});

Copilot AI Mar 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Several tests in this area don’t assert any observable behavior (no DOM assertions, no mock call assertions, no localStorage assertions). These will pass even if the component behavior breaks and primarily serve as coverage padding. Add expectations that validate the specific fallback/pluralization behavior being targeted (e.g., rendered name/link text, banner text like "row(s) selected", filtered rows by cluster).

Copilot uses AI. Check for mistakes.
listGadgetInstances: mockList
});
render(<BackgroundRunning />);
await act(async () => { });

Copilot AI Mar 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test currently has no assertions, so it can’t fail even if handling of instances || [] regresses. Add an assertion that the component remains rendered in a sane state (e.g., table present, headlamp_embeded_resources written with an array, or that listGadgetInstances was called and no console error was logged).

Suggested change
await act(async () => { });
await act(async () => { });
expect(mockList).toHaveBeenCalled();

Copilot uses AI. Check for mistakes.
Comment on lines +356 to +392
test('handles missing deleteGadgetInstance safely', async () => {
const mockList = vi.fn().mockImplementation((onSuccess) => {
onSuccess([{ id: 'selected-id', name: 'gadget', isHeadless: true }]);
});

(conn.useGadgetConn as any).mockReturnValue({
listGadgetInstances: mockList,
deleteGadgetInstance: vi.fn()
});

render(<BackgroundRunning />);

await act(async () => { });

fireEvent.click(screen.getByTestId('select-row-btn'));
fireEvent.click(screen.getByTestId('icon-mdi:delete'));
fireEvent.click(screen.getByTestId('confirm-btn'));
});

test('handles deletion when localStorage has no matching entry', async () => {
const mockList = vi.fn().mockImplementation((onSuccess) => {
onSuccess([{ id: 'selected-id', name: 'gadget', isHeadless: false }]);
});

(conn.useGadgetConn as any).mockReturnValue({
listGadgetInstances: mockList,
deleteGadgetInstance: vi.fn()
});

render(<BackgroundRunning />);

await act(async () => { });

fireEvent.click(screen.getByTestId('select-row-btn'));
fireEvent.click(screen.getByTestId('icon-mdi:delete'));
fireEvent.click(screen.getByTestId('confirm-btn'));
});

Copilot AI Mar 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two tests invoke delete flows but don’t assert outcomes (e.g., whether deleteGadgetInstance was/wasn’t called, whether localStorage changed, whether the confirm dialog closed). Also, the test name says "missing deleteGadgetInstance" but the mock includes it. Add concrete expectations and adjust the name/setup so the scenario matches what’s being validated.

Copilot uses AI. Check for mistakes.
Comment on lines +426 to +458
test('covers !tableInstance in handleDeleteInstances', async () => {
const mockList = vi.fn().mockImplementation((onSuccess) => {
onSuccess([{ id: 'inst1', name: 'my-gadget', isHeadless: true }]);
});
(conn.useGadgetConn as any).mockReturnValue({
listGadgetInstances: mockList
});

render(<BackgroundRunning />);
await act(async () => { });

// Trigger handleDeleteInstances while tableInstance is undefined
fireEvent.click(screen.getByTestId('force-confirm-btn'));
});

test('covers !instance in handleDeleteInstances', async () => {
const mockList = vi.fn().mockImplementation((onSuccess) => {
onSuccess([{ id: 'fake-trigger', name: 'my-gadget', isHeadless: true }]);
});
(conn.useGadgetConn as any).mockReturnValue({
listGadgetInstances: mockList
});

render(<BackgroundRunning />);
await act(async () => { });

// Select row to set tableInstance
fireEvent.click(screen.getByTestId('select-row-btn'));

// Trigger handleDeleteInstances where getSelectedRowModel returns 'missing-id'
fireEvent.click(screen.getByTestId('force-confirm-btn'));
});

Copilot AI Mar 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests trigger onConfirm via the mocked ConfirmDialog even when the dialog is not open (force-confirm-btn is always rendered). That execution path can’t happen in the real UI and may hide regressions in the actual confirm dialog wiring. Prefer opening the dialog via the delete icon and then confirming, or directly unit-test handleDeleteInstances via a more realistic component interaction.

Suggested change
test('covers !tableInstance in handleDeleteInstances', async () => {
const mockList = vi.fn().mockImplementation((onSuccess) => {
onSuccess([{ id: 'inst1', name: 'my-gadget', isHeadless: true }]);
});
(conn.useGadgetConn as any).mockReturnValue({
listGadgetInstances: mockList
});
render(<BackgroundRunning />);
await act(async () => { });
// Trigger handleDeleteInstances while tableInstance is undefined
fireEvent.click(screen.getByTestId('force-confirm-btn'));
});
test('covers !instance in handleDeleteInstances', async () => {
const mockList = vi.fn().mockImplementation((onSuccess) => {
onSuccess([{ id: 'fake-trigger', name: 'my-gadget', isHeadless: true }]);
});
(conn.useGadgetConn as any).mockReturnValue({
listGadgetInstances: mockList
});
render(<BackgroundRunning />);
await act(async () => { });
// Select row to set tableInstance
fireEvent.click(screen.getByTestId('select-row-btn'));
// Trigger handleDeleteInstances where getSelectedRowModel returns 'missing-id'
fireEvent.click(screen.getByTestId('force-confirm-btn'));
});

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants