Skip to content

Add registry provider to UI components #1705

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
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
17 changes: 17 additions & 0 deletions packages/ui-components/src/__fixtures__/RegistryManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { vi } from 'vitest';
import type { RegistryManager } from '$lib/providers/registry/RegistryManager';

const mockDefaultRegistry = 'https://example.com/default-registry.json';
let mockCurrentRegistry: string | null = mockDefaultRegistry;

export const initialRegistry: Partial<RegistryManager> = {
getCurrentRegistry: vi.fn(() => mockCurrentRegistry ?? mockDefaultRegistry),
setRegistry: vi.fn((newRegistry: string) => {
mockCurrentRegistry = newRegistry;
}),
resetToDefault: vi.fn(() => {
mockCurrentRegistry = mockDefaultRegistry;
}),
updateUrlWithRegistry: vi.fn(),
isCustomRegistry: vi.fn(() => mockCurrentRegistry !== mockDefaultRegistry)
};
104 changes: 50 additions & 54 deletions packages/ui-components/src/__tests__/InputRegistryUrl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,27 @@ import { render, screen, fireEvent } from '@testing-library/svelte';
import { vi } from 'vitest';
import InputRegistryUrl from '../lib/components/input/InputRegistryUrl.svelte';
import userEvent from '@testing-library/user-event';
import { loadRegistryUrl } from '../lib/services/loadRegistryUrl';
import { initialRegistry } from '../__fixtures__/RegistryManager';
import { useRegistry } from '$lib/providers/registry/useRegistry';
import type { RegistryManager } from '$lib/providers/registry/RegistryManager';

describe('InputRegistryUrl', () => {
const mockPushState = vi.fn();
const mockReload = vi.fn();
const mockLocalStorageSetItem = vi.fn();
const mockLocalStorageGetItem = vi.fn();

beforeEach(() => {
vi.stubGlobal('localStorage', {
setItem: mockLocalStorageSetItem,
getItem: mockLocalStorageGetItem
});
vi.mock('../lib/services/loadRegistryUrl', () => ({
loadRegistryUrl: vi.fn()
}));

Object.defineProperty(window, 'location', {
value: {
pathname: '/test-path',
reload: mockReload
},
writable: true
});

window.history.pushState = mockPushState;

mockPushState.mockClear();
mockReload.mockClear();
mockLocalStorageSetItem.mockClear();
mockLocalStorageGetItem.mockClear();
});
vi.mock('../lib/providers/registry/useRegistry', () => ({
useRegistry: vi.fn()
}));

afterEach(() => {
vi.unstubAllGlobals();
describe('InputRegistryUrl', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(loadRegistryUrl).mockResolvedValue(undefined);
vi.mocked(useRegistry).mockReturnValue(initialRegistry as RegistryManager);
});

it('should render input and button', () => {
mockLocalStorageGetItem.mockReturnValue('');
render(InputRegistryUrl);

const input = screen.getByPlaceholderText('Enter URL to raw strategy registry file');
Expand All @@ -46,54 +32,64 @@ describe('InputRegistryUrl', () => {
expect(button).toBeInTheDocument();
});

it('should bind input value to newRegistryUrl', async () => {
mockLocalStorageGetItem.mockReturnValue('');
it('should call loadRegistryUrl when button is clicked', async () => {
render(InputRegistryUrl);

const input = screen.getByPlaceholderText('Enter URL to raw strategy registry file');
const testUrl = 'https://example.com/registry.json';

await userEvent.clear(input);
await userEvent.type(input, testUrl);

expect(input).toHaveValue(testUrl);
const button = screen.getByText('Load registry URL');
await fireEvent.click(button);

expect(loadRegistryUrl).toHaveBeenCalledWith(testUrl, initialRegistry);
});

it('should handle registry URL loading when button is clicked', async () => {
mockLocalStorageGetItem.mockReturnValue('');
it('should load initial value from registry manager', () => {
const initialUrl = 'https://example.com/registry.json';
initialRegistry.getCurrentRegistry = vi.fn().mockReturnValue(initialUrl);
render(InputRegistryUrl);

const input = screen.getByPlaceholderText('Enter URL to raw strategy registry file');
const testUrl = 'https://example.com/registry.json';
await userEvent.type(input, testUrl);
expect(input).toHaveValue(initialUrl);
});

it('should display error message when loadRegistryUrl fails', async () => {
vi.mocked(loadRegistryUrl).mockRejectedValueOnce(new Error('Test error'));

render(InputRegistryUrl);

const button = screen.getByText('Load registry URL');
await fireEvent.click(button);

expect(mockPushState).toHaveBeenCalledWith({}, '', '/test-path?registry=' + testUrl);
expect(mockReload).toHaveBeenCalled();
expect(mockLocalStorageSetItem).toHaveBeenCalledWith('registry', testUrl);
expect(await screen.findByTestId('registry-error')).toHaveTextContent('Test error');
});

it('should handle empty URL', async () => {
mockLocalStorageGetItem.mockReturnValue('');
it('should show loading state when request is in progress', async () => {
vi.useFakeTimers();

vi.mocked(loadRegistryUrl).mockImplementation(() => {
return new Promise<void>((resolve) => {
setTimeout(() => resolve(), 1000);
});
});

const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });

render(InputRegistryUrl);

const button = screen.getByText('Load registry URL');
await fireEvent.click(button);
await user.click(button);

expect(mockPushState).toHaveBeenCalledWith({}, '', '/test-path?registry=');
expect(mockReload).toHaveBeenCalled();
expect(mockLocalStorageSetItem).toHaveBeenCalledWith('registry', '');
});
expect(screen.getByText('Loading registry...')).toBeInTheDocument();
expect(button).toBeDisabled();

it('should load initial value from localStorage', () => {
const initialUrl = 'https://example.com/registry.json';
mockLocalStorageGetItem.mockReturnValue(initialUrl);
await vi.runAllTimersAsync();

render(InputRegistryUrl);
expect(screen.getByText('Load registry URL')).toBeInTheDocument();
expect(button).not.toBeDisabled();

const input = screen.getByPlaceholderText('Enter URL to raw strategy registry file');
expect(input).toHaveValue(initialUrl);
expect(mockLocalStorageGetItem).toHaveBeenCalledWith('registry');
vi.useRealTimers();
});
});
178 changes: 178 additions & 0 deletions packages/ui-components/src/__tests__/RegistryManager.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { RegistryManager } from '../lib/providers/registry/RegistryManager';

const DEFAULT_REGISTRY_URL = 'https://default.registry.url/registry.json';
const CUSTOM_REGISTRY_URL = 'https://custom.registry.url/registry.json';
const STORAGE_KEY = 'registry';

const mockLocalStorage = (() => {
let store: Record<string, string> = {};
return {
getItem: vi.fn((key: string) => store[key] || null),
setItem: vi.fn((key: string, value: string) => {
store[key] = value;
}),
removeItem: vi.fn((key: string) => {
delete store[key];
}),
clear: () => {
store = {};
},
getStore: () => store
};
})();

const mockLocation = (searchParams: URLSearchParams) => ({
href: `http://localhost/?${searchParams.toString()}`,
searchParams
});

const mockHistory = {
pushState: vi.fn()
};

vi.stubGlobal('localStorage', mockLocalStorage);
vi.stubGlobal('history', mockHistory);

const setMockLocation = (params: Record<string, string>) => {
const searchParams = new URLSearchParams(params);
Object.defineProperty(window, 'location', {
value: mockLocation(searchParams),
writable: true
});
};

describe('RegistryManager', () => {
beforeEach(() => {
mockLocalStorage.clear();
vi.clearAllMocks();
setMockLocation({});
});

it('should initialize with default registry if no URL param or localStorage', () => {
const manager = new RegistryManager(DEFAULT_REGISTRY_URL);
expect(manager.getCurrentRegistry()).toBe(DEFAULT_REGISTRY_URL);
expect(mockLocalStorage.getItem).toHaveBeenCalledWith(STORAGE_KEY);
expect(mockLocalStorage.setItem).not.toHaveBeenCalled();
});

it('should initialize with URL parameter if present', () => {
setMockLocation({ [STORAGE_KEY]: CUSTOM_REGISTRY_URL });
const manager = new RegistryManager(DEFAULT_REGISTRY_URL);
expect(manager.getCurrentRegistry()).toBe(CUSTOM_REGISTRY_URL);
expect(mockLocalStorage.setItem).toHaveBeenCalledWith(STORAGE_KEY, CUSTOM_REGISTRY_URL);
});

it('should initialize with localStorage value if present (and no URL param)', () => {
mockLocalStorage.setItem(STORAGE_KEY, CUSTOM_REGISTRY_URL);
const manager = new RegistryManager(DEFAULT_REGISTRY_URL);
expect(manager.getCurrentRegistry()).toBe(CUSTOM_REGISTRY_URL);
expect(mockLocalStorage.getItem).toHaveBeenCalledWith(STORAGE_KEY);
expect(mockLocalStorage.setItem).toHaveBeenCalledTimes(1);
});

it('should prioritize URL parameter over localStorage on initialization', () => {
const urlRegistry = 'https://from.url/registry.json';
setMockLocation({ [STORAGE_KEY]: urlRegistry });
mockLocalStorage.setItem(STORAGE_KEY, CUSTOM_REGISTRY_URL);

const manager = new RegistryManager(DEFAULT_REGISTRY_URL);
expect(manager.getCurrentRegistry()).toBe(urlRegistry);
expect(mockLocalStorage.setItem).toHaveBeenCalledWith(STORAGE_KEY, urlRegistry);
});

it('getCurrentRegistry() should return the current registry', () => {
const manager = new RegistryManager(DEFAULT_REGISTRY_URL);
expect(manager.getCurrentRegistry()).toBe(DEFAULT_REGISTRY_URL);

setMockLocation({ [STORAGE_KEY]: CUSTOM_REGISTRY_URL });
const manager2 = new RegistryManager(DEFAULT_REGISTRY_URL);
expect(manager2.getCurrentRegistry()).toBe(CUSTOM_REGISTRY_URL);
});

it('setRegistry() should update current registry, localStorage, and URL', () => {
const manager = new RegistryManager(DEFAULT_REGISTRY_URL);
manager.setRegistry(CUSTOM_REGISTRY_URL);

expect(manager.getCurrentRegistry()).toBe(CUSTOM_REGISTRY_URL);
expect(mockLocalStorage.setItem).toHaveBeenCalledWith(STORAGE_KEY, CUSTOM_REGISTRY_URL);
expect(mockHistory.pushState).toHaveBeenCalledTimes(1);
const expectedUrl = new URL(window.location.href);
expectedUrl.searchParams.set(STORAGE_KEY, CUSTOM_REGISTRY_URL);
expect(mockHistory.pushState).toHaveBeenCalledWith({}, '', expectedUrl.toString());
});

it('resetToDefault() should reset registry, clear localStorage, and update URL', () => {
mockLocalStorage.setItem(STORAGE_KEY, CUSTOM_REGISTRY_URL);
setMockLocation({ [STORAGE_KEY]: CUSTOM_REGISTRY_URL });
const manager = new RegistryManager(DEFAULT_REGISTRY_URL);
expect(manager.getCurrentRegistry()).toBe(CUSTOM_REGISTRY_URL);

manager.resetToDefault();

expect(manager.getCurrentRegistry()).toBe(DEFAULT_REGISTRY_URL);
expect(mockLocalStorage.removeItem).toHaveBeenCalledWith(STORAGE_KEY);
expect(mockHistory.pushState).toHaveBeenCalledTimes(1);
const expectedUrl = new URL(window.location.href);
expectedUrl.searchParams.delete(STORAGE_KEY);
expect(mockHistory.pushState).toHaveBeenCalledWith({}, '', expectedUrl.toString());
});

it('updateUrlWithRegistry() should update URL search parameter', () => {
const manager = new RegistryManager(DEFAULT_REGISTRY_URL);
manager.updateUrlWithRegistry(CUSTOM_REGISTRY_URL);

expect(mockHistory.pushState).toHaveBeenCalledTimes(1);
const expectedUrl = new URL(window.location.href);
expectedUrl.searchParams.set(STORAGE_KEY, CUSTOM_REGISTRY_URL);
expect(mockHistory.pushState).toHaveBeenCalledWith({}, '', expectedUrl.toString());
});

it('updateUrlWithRegistry() should remove URL search parameter when value is null', () => {
setMockLocation({ [STORAGE_KEY]: CUSTOM_REGISTRY_URL });
const manager = new RegistryManager(DEFAULT_REGISTRY_URL);
manager.updateUrlWithRegistry(null);

expect(mockHistory.pushState).toHaveBeenCalledTimes(1);
const expectedUrl = new URL(window.location.href);
expectedUrl.searchParams.delete(STORAGE_KEY);
expect(mockHistory.pushState).toHaveBeenCalledWith({}, '', expectedUrl.toString());
});

it('isCustomRegistry() should return false when using default registry', () => {
const manager = new RegistryManager(DEFAULT_REGISTRY_URL);
expect(manager.isCustomRegistry()).toBe(false);
});

it('isCustomRegistry() should return true when using a custom registry', () => {
mockLocalStorage.setItem(STORAGE_KEY, CUSTOM_REGISTRY_URL);
const manager = new RegistryManager(DEFAULT_REGISTRY_URL);
expect(manager.isCustomRegistry()).toBe(true);
});

it('isCustomRegistry() should return false after resetting to default', () => {
mockLocalStorage.setItem(STORAGE_KEY, CUSTOM_REGISTRY_URL);
const manager = new RegistryManager(DEFAULT_REGISTRY_URL);
expect(manager.isCustomRegistry()).toBe(true);
manager.resetToDefault();
expect(manager.isCustomRegistry()).toBe(false);
});

it('should handle localStorage errors gracefully (at least not crash)', () => {
vi.spyOn(mockLocalStorage, 'getItem').mockImplementation(() => {
throw new Error('localStorage unavailable');
});
vi.spyOn(mockLocalStorage, 'setItem').mockImplementation(() => {
throw new Error('localStorage unavailable');
});
vi.spyOn(mockLocalStorage, 'removeItem').mockImplementation(() => {
throw new Error('localStorage unavailable');
});

expect(() => new RegistryManager(DEFAULT_REGISTRY_URL)).toThrow(
/Failed to access localStorage|Failed to save to localStorage/
);

vi.restoreAllMocks();
});
});
Loading