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

Merged
merged 30 commits into from
May 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
bf7b48e
Add registryProvider to ui-components
hardingjam Apr 28, 2025
48de062
webapp changes
hardingjam Apr 28, 2025
5e653b0
update tests
hardingjam Apr 28, 2025
4b16b3b
Modals causing mocking error
hardingjam Apr 28, 2025
bfcafbd
remove inline mock of orderbook
hardingjam Apr 28, 2025
af075cc
Move tests back to main
hardingjam Apr 28, 2025
6ba629c
format and lint
hardingjam Apr 28, 2025
636e102
remove log
hardingjam Apr 28, 2025
652fa9d
revert modal
hardingjam Apr 28, 2025
4efba7a
Merge branch 'main' into Add-RegistryProvider-to-ui-components
hardingjam Apr 28, 2025
bc525e2
ai comments
hardingjam Apr 28, 2025
b7425fa
Merge branch 'main' into Add-RegistryProvider-to-ui-components
hardingjam Apr 29, 2025
7d5a610
move tests
hardingjam Apr 29, 2025
4b1c7fa
formatted
hardingjam Apr 29, 2025
ca6d5da
remove unused
hardingjam Apr 29, 2025
c1f65ff
add return type
hardingjam Apr 29, 2025
42dac7d
fix test
hardingjam Apr 29, 2025
80858cb
add warning
hardingjam Apr 29, 2025
b66a092
reload on deploy click
hardingjam Apr 29, 2025
cda4102
format
hardingjam Apr 29, 2025
112cc5c
ai comments
hardingjam Apr 29, 2025
40665c8
moced and tested
hardingjam Apr 29, 2025
dc2ef5a
rm comment
hardingjam Apr 29, 2025
e733de5
Remove store, use RegistryManager directly
hardingjam May 5, 2025
fdc4c4c
remove unused
hardingjam May 5, 2025
f5027c9
Merge branch 'main' into Add-RegistryProvider-to-ui-components
hardingjam May 5, 2025
b852aa2
address AI comments
hardingjam May 7, 2025
96018a4
remove consts
hardingjam May 9, 2025
a9aa4f4
add ai checks
hardingjam May 9, 2025
4b82043
Merge branch 'main' into Add-RegistryProvider-to-ui-components
hardyjosh May 9, 2025
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