mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-08-02 10:00:17 +05:30
feat(renderer): add UpdateBanner for non-blocking update notifications
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
import { useUpdaterStore } from '@/lib/updater-store';
|
||||
import { ipc } from '@/lib/ipc';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export function UpdateBanner() {
|
||||
const { state, version, percent, install, check } = useUpdaterStore();
|
||||
|
||||
if (state === 'idle' || state === 'checking') return null;
|
||||
|
||||
if (state === 'error') {
|
||||
return (
|
||||
<div data-testid="update-banner" role="status" className="bg-amber-50 border-b border-amber-200 px-4 py-2 text-sm">
|
||||
Couldn't check for updates.{' '}
|
||||
<button onClick={async () => { try { await check(); } catch (e: any) { toast.error(e.message); } }} className="underline">
|
||||
Try again
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (state === 'downloading') {
|
||||
return (
|
||||
<div data-testid="update-banner" className="bg-blue-50 border-b border-blue-200 px-4 py-2 text-sm">
|
||||
Downloading update… {Math.round(percent)}%
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (state === 'ready') {
|
||||
return (
|
||||
<div data-testid="update-banner" className="bg-green-50 border-b border-green-200 px-4 py-2 text-sm flex items-center gap-3">
|
||||
<span>v{version} is ready.</span>
|
||||
<button onClick={() => ipc.app.openExternal(`https://github.com/amitwh/markdown-converter/releases/tag/v${version}`)} className="underline">
|
||||
View release notes
|
||||
</button>
|
||||
<button onClick={install} className="px-3 py-1 rounded bg-brand text-white">
|
||||
Restart to update
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { describe, expect, test, beforeEach, vi } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { useUpdaterStore } from '@/lib/updater-store';
|
||||
import { UpdateBanner } from '@/components/UpdateBanner';
|
||||
|
||||
vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
|
||||
vi.mock('@/lib/ipc', () => ({
|
||||
ipc: {
|
||||
app: { openExternal: vi.fn() },
|
||||
updater: { check: vi.fn(), install: vi.fn() },
|
||||
},
|
||||
}));
|
||||
|
||||
describe('UpdateBanner', () => {
|
||||
beforeEach(() => {
|
||||
useUpdaterStore.setState({ state: 'idle', version: null, percent: 0 });
|
||||
});
|
||||
|
||||
test('renders nothing when state is idle', () => {
|
||||
const { container } = render(<UpdateBanner />);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
test('shows version and restart button when state is ready', () => {
|
||||
useUpdaterStore.setState({ state: 'ready', version: '5.0.2' });
|
||||
render(<UpdateBanner />);
|
||||
expect(screen.getByText(/5\.0\.2/)).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /restart/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('shows download progress when state is downloading', () => {
|
||||
useUpdaterStore.setState({ state: 'downloading', percent: 42 });
|
||||
render(<UpdateBanner />);
|
||||
expect(screen.getByText(/42%/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('shows error copy when state is error', () => {
|
||||
useUpdaterStore.setState({ state: 'error' });
|
||||
render(<UpdateBanner />);
|
||||
expect(screen.getByText(/couldn.?t check/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('restart button calls ipc.updater.install', () => {
|
||||
useUpdaterStore.setState({ state: 'ready', version: '5.0.2' });
|
||||
render(<UpdateBanner />);
|
||||
fireEvent.click(screen.getByRole('button', { name: /restart/i }));
|
||||
expect(useUpdaterStore.getState().install).toBeDefined();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user