Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export default {
'^figures$': '<rootDir>/tests/__mocks__/figures.ts',
'^is-unicode-supported$': '<rootDir>/tests/__mocks__/is-unicode-supported.ts',
'^conf$': '<rootDir>/tests/__mocks__/conf.ts',
'^signal-exit$': '<rootDir>/tests/__mocks__/signal-exit.ts',
},

// Transform configuration
Expand Down
5 changes: 3 additions & 2 deletions tests/__mocks__/signal-exit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ const noop = () => () => {};

export default noop;
export const onExit = noop;


export const load = () => {};
export const unload = () => {};
export const signals: string[] = [];
26 changes: 13 additions & 13 deletions tests/__tests__/commands/devbox/create-mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,12 @@ describe("createDevbox --mcp flag", () => {

const { createDevbox } = await import("@/commands/devbox/create.js");
await createDevbox({
mcp: ["github-readonly,my_secret"],
mcp: ["GH_TOKEN=github-readonly,my_secret"],
});

expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
mcp: [{ mcp_config: "github-readonly", secret: "my_secret" }],
mcp: { GH_TOKEN: { mcp_config: "github-readonly", secret: "my_secret" } },
}),
);
expect(console.log).toHaveBeenCalledWith("dbx_mcp_test");
Expand All @@ -52,15 +52,15 @@ describe("createDevbox --mcp flag", () => {

const { createDevbox } = await import("@/commands/devbox/create.js");
await createDevbox({
mcp: ["github-readonly,secret1", "jira-config,secret2"],
mcp: ["GH_TOKEN=github-readonly,secret1", "JIRA_TOKEN=jira-config,secret2"],
});

expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
mcp: [
{ mcp_config: "github-readonly", secret: "secret1" },
{ mcp_config: "jira-config", secret: "secret2" },
],
mcp: {
GH_TOKEN: { mcp_config: "github-readonly", secret: "secret1" },
JIRA_TOKEN: { mcp_config: "jira-config", secret: "secret2" },
},
}),
);
});
Expand All @@ -76,10 +76,10 @@ describe("createDevbox --mcp flag", () => {
expect(createArg.mcp).toBeUndefined();
});

it("should report error for invalid MCP spec format (missing comma)", async () => {
it("should report error for invalid MCP spec format (missing equals)", async () => {
const { createDevbox } = await import("@/commands/devbox/create.js");
await createDevbox({
mcp: ["invalid-no-comma"],
mcp: ["invalid-no-equals"],
});

expect(mockOutputError).toHaveBeenCalledWith(
Expand All @@ -96,15 +96,15 @@ describe("createDevbox --mcp flag", () => {
const { createDevbox } = await import("@/commands/devbox/create.js");
await createDevbox({
name: "my-devbox",
mcp: ["github-readonly,my_secret"],
mcp: ["GH_TOKEN=github-readonly,my_secret"],
blueprint: "my-blueprint",
});

const createArg = mockCreate.mock.calls[0][0] as Record<string, unknown>;
expect(createArg.name).toBe("my-devbox");
expect(createArg.blueprint_name).toBe("my-blueprint");
expect(createArg.mcp).toEqual([
{ mcp_config: "github-readonly", secret: "my_secret" },
]);
expect(createArg.mcp).toEqual({
GH_TOKEN: { mcp_config: "github-readonly", secret: "my_secret" },
});
});
});
77 changes: 40 additions & 37 deletions tests/__tests__/components/UpdateNotification.test.tsx
Original file line number Diff line number Diff line change
@@ -1,77 +1,84 @@
/**
* Tests for UpdateNotification component
* Tests for UpdateNotification component.
*
* The component uses the useUpdateCheck hook which calls global.fetch
* internally. We mock fetch to control update-check responses.
*
* Note: the setup-components.ts mocks for useUpdateCheck and
* UpdateNotification don't apply here due to module path resolution
* differences (.ts vs .js mapping), so the real component and hook run.
*/
import React from 'react';
import { jest } from '@jest/globals';
import { render } from 'ink-testing-library';
import { UpdateNotification } from '../../../src/components/UpdateNotification.js';

// Mock fetch
global.fetch = jest.fn() as jest.Mock;
// Helper: wait for async state updates to propagate
function waitForUpdates(ms = 150): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}

describe('UpdateNotification', () => {
const originalFetch = global.fetch;

beforeEach(() => {
jest.clearAllMocks();
global.fetch = jest.fn() as jest.Mock;
});

afterEach(() => {
global.fetch = originalFetch;
});

it('renders without crashing', () => {
(global.fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
json: async () => ({ version: '0.1.0' }),
});

const { lastFrame } = render(<UpdateNotification />);
expect(lastFrame()).toBeDefined();
});

it('shows nothing while checking', () => {
(global.fetch as jest.Mock).mockImplementation(() =>
new Promise(() => {}) // Never resolves
(global.fetch as jest.Mock).mockImplementation(
() => new Promise(() => {}), // Never resolves
);

const { lastFrame } = render(<UpdateNotification />);
// Should be empty while checking
expect(lastFrame()).toBe('');
});

it('shows nothing when on latest version', async () => {
(global.fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
json: async () => ({ version: '0.1.0' }), // Same as current
json: async () => ({ version: '0.0.1' }), // Older than current
});

const { lastFrame } = render(<UpdateNotification />);

// Wait for effect to run
await new Promise((resolve) => setTimeout(resolve, 50));

await waitForUpdates();
expect(lastFrame()).toBe('');
});

it('shows nothing on fetch error', async () => {
(global.fetch as jest.Mock).mockRejectedValueOnce(new Error('Network error'));

(global.fetch as jest.Mock).mockRejectedValueOnce(
new Error('Network error'),
);

const { lastFrame } = render(<UpdateNotification />);

// Wait for effect to run
await new Promise((resolve) => setTimeout(resolve, 50));

await waitForUpdates();
expect(lastFrame()).toBe('');
});

it('shows update notification when newer version available', async () => {
(global.fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
json: async () => ({ version: '99.99.99' }), // Much higher version
json: async () => ({ version: '99.99.99' }),
});

const { lastFrame } = render(<UpdateNotification />);

// Wait for effect to run
await new Promise((resolve) => setTimeout(resolve, 100));

await waitForUpdates();

const frame = lastFrame() || '';
// Should show update notification
expect(frame).toContain('Update available');
expect(frame).toContain('99.99.99');
});
Expand All @@ -81,24 +88,20 @@ describe('UpdateNotification', () => {
ok: true,
json: async () => ({ version: '99.99.99' }),
});

const { lastFrame } = render(<UpdateNotification />);

await new Promise((resolve) => setTimeout(resolve, 100));

await waitForUpdates();
expect(lastFrame()).toContain('npm i -g @runloop/rl-cli@latest');
});

it('handles non-ok response', async () => {
it('shows nothing on non-ok response', async () => {
(global.fetch as jest.Mock).mockResolvedValueOnce({
ok: false,
status: 404,
});

const { lastFrame } = render(<UpdateNotification />);

await new Promise((resolve) => setTimeout(resolve, 50));

await waitForUpdates();
expect(lastFrame()).toBe('');
});
});
Expand Down
Loading