From bf7b48e4f921a0f4f90d2af960c6a16274cebe83 Mon Sep 17 00:00:00 2001 From: Jamie Harding Date: Mon, 28 Apr 2025 14:41:45 +0200 Subject: [PATCH 01/24] Add registryProvider to ui-components --- .../src/__tests__/InputRegistryUrl.test.ts | 110 +++++++------ .../ui-components/src/lib/__mocks__/stores.ts | 24 +++ .../deployment/DeploymentSteps.svelte | 5 +- .../components/input/InputRegistryUrl.svelte | 50 ++++-- packages/ui-components/src/lib/index.ts | 5 + .../lib/providers/registry/RegistryManager.ts | 148 ++++++++++++++++++ .../registry/RegistryProvider.svelte | 10 ++ .../src/lib/providers/registry/context.ts | 68 ++++++++ .../src/lib/providers/registry/useRegistry.ts | 38 +++++ .../ui-components/src/lib/services/index.ts | 1 + .../src/lib/services/loadRegistryUrl.test.ts | 76 +++++++++ .../src/lib/services/loadRegistryUrl.ts | 24 +++ .../ui-components/src/lib/types/registry.ts | 4 + packages/ui-components/test-setup.ts | 6 +- 14 files changed, 496 insertions(+), 73 deletions(-) create mode 100644 packages/ui-components/src/lib/providers/registry/RegistryManager.ts create mode 100644 packages/ui-components/src/lib/providers/registry/RegistryProvider.svelte create mode 100644 packages/ui-components/src/lib/providers/registry/context.ts create mode 100644 packages/ui-components/src/lib/providers/registry/useRegistry.ts create mode 100644 packages/ui-components/src/lib/services/loadRegistryUrl.test.ts create mode 100644 packages/ui-components/src/lib/services/loadRegistryUrl.ts create mode 100644 packages/ui-components/src/lib/types/registry.ts diff --git a/packages/ui-components/src/__tests__/InputRegistryUrl.test.ts b/packages/ui-components/src/__tests__/InputRegistryUrl.test.ts index 63b7741f7..87e6cf14e 100644 --- a/packages/ui-components/src/__tests__/InputRegistryUrl.test.ts +++ b/packages/ui-components/src/__tests__/InputRegistryUrl.test.ts @@ -2,41 +2,29 @@ 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'; -describe('InputRegistryUrl', () => { - const mockPushState = vi.fn(); - const mockReload = vi.fn(); - const mockLocalStorageSetItem = vi.fn(); - const mockLocalStorageGetItem = vi.fn(); +const { mockRegistryStore, initialRegistry } = await vi.hoisted( + () => import('../lib/__mocks__/stores') +); +// Mock the loadRegistryUrl function +vi.mock('$lib/services/loadRegistryUrl', () => ({ + loadRegistryUrl: vi.fn() +})); - beforeEach(() => { - vi.stubGlobal('localStorage', { - setItem: mockLocalStorageSetItem, - getItem: mockLocalStorageGetItem - }); - - Object.defineProperty(window, 'location', { - value: { - pathname: '/test-path', - reload: mockReload - }, - writable: true - }); +// Mock the useRegistry hook +vi.mock('../../providers/registry/useRegistry', () => ({ + useRegistry: mockRegistryStore +})); - window.history.pushState = mockPushState; - - mockPushState.mockClear(); - mockReload.mockClear(); - mockLocalStorageSetItem.mockClear(); - mockLocalStorageGetItem.mockClear(); - }); - - afterEach(() => { - vi.unstubAllGlobals(); +describe('InputRegistryUrl', () => { + beforeEach(() => { + vi.clearAllMocks(); + // Make loadRegistryUrl return a resolved promise by default + vi.mocked(loadRegistryUrl).mockResolvedValue(undefined); }); it('should render input and button', () => { - mockLocalStorageGetItem.mockReturnValue(''); render(InputRegistryUrl); const input = screen.getByPlaceholderText('Enter URL to raw strategy registry file'); @@ -46,54 +34,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); - - 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(input).toHaveValue(initialUrl); }); - it('should handle empty URL', async () => { - mockLocalStorageGetItem.mockReturnValue(''); + 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'); + const button = screen.getByText('Load Registry URL'); await fireEvent.click(button); - expect(mockPushState).toHaveBeenCalledWith({}, '', '/test-path?registry='); - expect(mockReload).toHaveBeenCalled(); - expect(mockLocalStorageSetItem).toHaveBeenCalledWith('registry', ''); + expect(await screen.findByTestId('registry-error')).toHaveTextContent('Test error'); }); - it('should load initial value from localStorage', () => { - const initialUrl = 'https://example.com/registry.json'; - mockLocalStorageGetItem.mockReturnValue(initialUrl); + it('should show loading state when request is in progress', async () => { + vi.useFakeTimers(); + + vi.mocked(loadRegistryUrl).mockImplementation(() => { + return new Promise((resolve) => { + setTimeout(() => resolve(), 1000); + }); + }); + + const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime }); render(InputRegistryUrl); - const input = screen.getByPlaceholderText('Enter URL to raw strategy registry file'); - expect(input).toHaveValue(initialUrl); - expect(mockLocalStorageGetItem).toHaveBeenCalledWith('registry'); + const button = screen.getByText('Load Registry URL'); + await user.click(button); + + expect(screen.getByText('Loading registry...')).toBeInTheDocument(); + expect(button).toBeDisabled(); + + await vi.runAllTimersAsync(); + + expect(screen.getByText('Load Registry URL')).toBeInTheDocument(); + expect(button).not.toBeDisabled(); + + vi.useRealTimers(); }); }); diff --git a/packages/ui-components/src/lib/__mocks__/stores.ts b/packages/ui-components/src/lib/__mocks__/stores.ts index 9ad1b6d95..da47a5fd4 100644 --- a/packages/ui-components/src/lib/__mocks__/stores.ts +++ b/packages/ui-components/src/lib/__mocks__/stores.ts @@ -4,6 +4,23 @@ import settingsFixture from '../__fixtures__/settings-12-11-24.json'; import { type Config } from '@wagmi/core'; import { mockWeb3Config } from './mockWeb3Config'; +import type { RegistryManager } from '../providers/registry/RegistryManager'; +import { vi } from 'vitest'; + +const mockDefaultRegistry = 'https://example.com/default-registry.json'; +let mockCurrentRegistry: string | null = mockDefaultRegistry; // Start with default + +export const initialRegistry: Partial = { + 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) +}; const initialPageState = { data: { @@ -38,6 +55,7 @@ const mockChainIdWritable = writable(0); const mockConnectedWritable = writable(true); const mockWagmiConfigWritable = writable(mockWeb3Config); const mockShowMyItemsOnlyWritable = writable(false); +const mockRegistryWritable = writable(initialRegistry as RegistryManager); export const mockSettingsStore = { subscribe: mockSettingsWritable.subscribe, @@ -144,3 +162,9 @@ export const mockPageStore = { }, reset: () => mockPageWritable.set(initialPageState) }; + +export const mockRegistryStore = { + subscribe: mockRegistryWritable.subscribe, + set: mockRegistryWritable.set, + mockSetSubscribeValue: (value: RegistryManager): void => mockRegistryWritable.set(value) +}; diff --git a/packages/ui-components/src/lib/components/deployment/DeploymentSteps.svelte b/packages/ui-components/src/lib/components/deployment/DeploymentSteps.svelte index 4b7059106..441c5f7f0 100644 --- a/packages/ui-components/src/lib/components/deployment/DeploymentSteps.svelte +++ b/packages/ui-components/src/lib/components/deployment/DeploymentSteps.svelte @@ -27,6 +27,7 @@ import { type DeploymentArgs } from '$lib/types/transaction'; import { fade } from 'svelte/transition'; import ShareChoicesButton from './ShareChoicesButton.svelte'; + import { useRegistry } from '$lib/providers/registry/useRegistry'; interface Deployment { key: string; @@ -43,7 +44,6 @@ export let wagmiConnected: Writable; export let appKitModal: Writable; export let settings: Writable; - export let registryUrl: string; let allDepositFields: GuiDepositCfg[] = []; let allTokenOutputs: OrderIOCfg[] = []; @@ -58,6 +58,7 @@ const { account } = useAccount(); const gui = useGui(); + const registry = useRegistry(); let deploymentStepsError = DeploymentStepsError.error; @@ -151,7 +152,7 @@ } async function _handleShareChoices() { - await handleShareChoices(gui, registryUrl); + await handleShareChoices(gui, $registry.getCurrentRegistry()); } async function onSelectTokenSelect() { diff --git a/packages/ui-components/src/lib/components/input/InputRegistryUrl.svelte b/packages/ui-components/src/lib/components/input/InputRegistryUrl.svelte index d0a857bfa..e86f5ba47 100644 --- a/packages/ui-components/src/lib/components/input/InputRegistryUrl.svelte +++ b/packages/ui-components/src/lib/components/input/InputRegistryUrl.svelte @@ -1,21 +1,43 @@ -
- - +
+
+ + +
+
+ {#if error} +

{error}

+ {/if} +
diff --git a/packages/ui-components/src/lib/index.ts b/packages/ui-components/src/lib/index.ts index 6510a900a..5158bfdf6 100644 --- a/packages/ui-components/src/lib/index.ts +++ b/packages/ui-components/src/lib/index.ts @@ -125,10 +125,15 @@ export { default as logoDark } from './assets/logo-dark.svg'; // Providers export { default as GuiProvider } from './providers/GuiProvider.svelte'; export { default as WalletProvider } from './providers/wallet/WalletProvider.svelte'; +export { default as RegistryProvider } from './providers/registry/RegistryProvider.svelte'; // Hooks export { useGui } from './hooks/useGui'; export { useAccount } from './providers/wallet/useAccount'; +export { useRegistry } from './providers/registry/useRegistry'; + +// Classes +export { RegistryManager } from './providers/registry/RegistryManager'; // Mocks export { mockPageStore } from './__mocks__/stores'; diff --git a/packages/ui-components/src/lib/providers/registry/RegistryManager.ts b/packages/ui-components/src/lib/providers/registry/RegistryManager.ts new file mode 100644 index 000000000..6f327cc82 --- /dev/null +++ b/packages/ui-components/src/lib/providers/registry/RegistryManager.ts @@ -0,0 +1,148 @@ +/** + * Manages registry URL settings, persisting values in localStorage and URL parameters + */ +export class RegistryManager { + /** The default registry URL to fall back to */ + private defaultRegistry: string; + + /** The currently selected registry URL */ + private currentRegistry: string | null; + + /** Key used for localStorage and URL parameters */ + private static STORAGE_KEY = 'registry'; + + /** + * Create a new RegistryManager + * @param defaultRegistry The default registry URL to use, defaults to REGISTRY_URL constant + */ + constructor(defaultRegistry: string) { + this.defaultRegistry = defaultRegistry; + this.currentRegistry = this.loadRegistryFromStorageOrUrl(); + } + + /** + * Initialize registry from URL param or local storage + * @returns The registry URL to use + */ + private loadRegistryFromStorageOrUrl(): string { + const urlParam = this.getRegistryParamFromUrl(); + if (urlParam) { + this.setRegistryToLocalStorage(urlParam); + return urlParam; + } + return this.getRegistryFromLocalStorage() ?? this.defaultRegistry; + } + + /** + * Get the registry from the URL param + * @returns The registry value from URL or null if not present + * @throws Error if URL parsing fails + */ + private getRegistryParamFromUrl(): string | null { + try { + return new URL(window.location.href).searchParams.get(RegistryManager.STORAGE_KEY); + } catch (error) { + throw new Error( + 'Failed to get registry parameter: ' + + (error instanceof Error ? error.message : String(error)) + ); + } + } + + /** + * Save the registry to local storage + * @param registry The registry URL to save + * @throws Error if localStorage is not available + */ + private setRegistryToLocalStorage(registry: string): void { + try { + localStorage.setItem(RegistryManager.STORAGE_KEY, registry); + } catch (error) { + throw new Error( + 'Failed to save to localStorage: ' + + (error instanceof Error ? error.message : String(error)) + ); + } + } + + /** + * Retrieve the registry from local storage + * @returns The stored registry URL or null if not found + * @throws Error if localStorage is not available + */ + private getRegistryFromLocalStorage(): string | null { + try { + console.log('getting from local storage'); + return localStorage.getItem(RegistryManager.STORAGE_KEY); + } catch (error) { + throw new Error( + 'Failed to access localStorage: ' + (error instanceof Error ? error.message : String(error)) + ); + } + } + + /** + * Get the currently active registry + * @returns The current registry URL, falling back to default if not set + */ + public getCurrentRegistry(): string { + return this.currentRegistry ?? this.defaultRegistry; + } + + /** + * Set the registry and update both localStorage and URL + * @param registry The new registry URL to set + */ + public setRegistry(registry: string): void { + this.currentRegistry = registry; + this.setRegistryToLocalStorage(registry); + this.updateUrlWithRegistry(); + } + + /** + * Reset to the default registry, clearing both localStorage and URL param + * @throws Error if localStorage is not available + */ + public resetToDefault(): void { + this.currentRegistry = this.defaultRegistry; + try { + localStorage.removeItem(RegistryManager.STORAGE_KEY); + } catch (error) { + throw new Error( + 'Failed to clear registry from localStorage: ' + + (error instanceof Error ? error.message : String(error)) + ); + } + this.updateUrlWithRegistry(null); + } + + /** + * Update the URL param to reflect the current or specified registry + * @param value The registry value to set in URL, defaults to current registry + * @throws Error if URL manipulation fails + */ + public updateUrlWithRegistry(value: string | null = this.currentRegistry): void { + try { + const url = new URL(window.location.href); + if (value) { + url.searchParams.set(RegistryManager.STORAGE_KEY, value); + } else { + url.searchParams.delete(RegistryManager.STORAGE_KEY); + } + window.history.pushState({}, '', url.toString()); + } catch (error) { + throw new Error( + 'Failed to update URL parameter: ' + + (error instanceof Error ? error.message : String(error)) + ); + } + } + + /** + * Check if the current registry is custom (different from the default) + * @returns True if using a non-default registry + */ + public isCustomRegistry(): boolean { + return !!this.currentRegistry && this.currentRegistry !== this.defaultRegistry; + } +} diff --git a/packages/ui-components/src/lib/providers/registry/RegistryProvider.svelte b/packages/ui-components/src/lib/providers/registry/RegistryProvider.svelte new file mode 100644 index 000000000..4bfab0bb6 --- /dev/null +++ b/packages/ui-components/src/lib/providers/registry/RegistryProvider.svelte @@ -0,0 +1,10 @@ + + + diff --git a/packages/ui-components/src/lib/providers/registry/context.ts b/packages/ui-components/src/lib/providers/registry/context.ts new file mode 100644 index 000000000..4ef17e795 --- /dev/null +++ b/packages/ui-components/src/lib/providers/registry/context.ts @@ -0,0 +1,68 @@ +import { getContext, setContext } from 'svelte'; +import type { Readable } from 'svelte/store'; +import type { RegistryManager } from './RegistryManager'; + +export const REGISTRY_KEY = 'registry_key'; + +/** + * Type for the registry store + */ +export type Registry = Readable; + +/** + * Retrieves the registry manager store directly from Svelte's context + */ +export const getRegistryContext = (): Registry => { + const registry = getContext(REGISTRY_KEY); + if (!registry) { + throw new Error( + 'No registry manager was found in Svelte context. Did you forget to wrap your component with RegistryProvider?' + ); + } + return registry; +}; + +/** + * Sets the registry manager store in Svelte's context + */ +export const setRegistryContext = (registry: Registry) => { + setContext(REGISTRY_KEY, registry); +}; + +if (import.meta.vitest) { + const { describe, it, expect, vi, beforeEach } = import.meta.vitest; + + vi.mock('svelte', async (importOriginal) => ({ + ...((await importOriginal()) as object), + getContext: vi.fn() + })); + + describe('getRegistryContext', () => { + const mockGetContext = vi.mocked(getContext); + + beforeEach(() => { + mockGetContext.mockReset(); + }); + + it('should return the registry from context when it exists', () => { + const mockRegistry = {} as Registry; + + mockGetContext.mockImplementation((key) => { + if (key === REGISTRY_KEY) return mockRegistry; + return undefined; + }); + + const result = getRegistryContext(); + expect(mockGetContext).toHaveBeenCalledWith(REGISTRY_KEY); + expect(result).toEqual(mockRegistry); + }); + + it('should throw an error when registry is not in context', () => { + mockGetContext.mockReturnValue(undefined); + + expect(() => getRegistryContext()).toThrow( + 'No registry manager was found in Svelte context. Did you forget to wrap your component with RegistryProvider?' + ); + }); + }); +} diff --git a/packages/ui-components/src/lib/providers/registry/useRegistry.ts b/packages/ui-components/src/lib/providers/registry/useRegistry.ts new file mode 100644 index 000000000..826fac2f7 --- /dev/null +++ b/packages/ui-components/src/lib/providers/registry/useRegistry.ts @@ -0,0 +1,38 @@ +import { getRegistryContext } from './context'; +import type { RegistryStore } from '$lib/types/registry'; + +/** + * Hook to access registry manager information from context + * Must be used within a component that is a child of RegistryProvider + * @returns An object containing the registry manager store + */ +export function useRegistry() { + const registry = getRegistryContext(); + return registry; +} + +if (import.meta.vitest) { + const { describe, it, expect, vi, beforeEach } = import.meta.vitest; + + vi.mock('./context', () => ({ + getRegistryContext: vi.fn() + })); + + describe('useRegistry', () => { + const mockGetRegistryContext = vi.mocked(getRegistryContext); + + beforeEach(() => { + mockGetRegistryContext.mockReset(); + }); + + it('should return registry', () => { + const mockRegistry = {} as RegistryStore; + mockGetRegistryContext.mockReturnValue(mockRegistry); + + const result = useRegistry(); + + expect(mockGetRegistryContext).toHaveBeenCalled(); + expect(result).toEqual(mockRegistry); + }); + }); +} diff --git a/packages/ui-components/src/lib/services/index.ts b/packages/ui-components/src/lib/services/index.ts index 15a6bc0f2..b802e969c 100644 --- a/packages/ui-components/src/lib/services/index.ts +++ b/packages/ui-components/src/lib/services/index.ts @@ -1,2 +1,3 @@ export { fetchParseRegistry, fetchRegistryDotrains, validateStrategies } from './registry'; +export { loadRegistryUrl } from './loadRegistryUrl'; export type { RegistryDotrain, RegistryFile } from './registry'; diff --git a/packages/ui-components/src/lib/services/loadRegistryUrl.test.ts b/packages/ui-components/src/lib/services/loadRegistryUrl.test.ts new file mode 100644 index 000000000..841d7b3e8 --- /dev/null +++ b/packages/ui-components/src/lib/services/loadRegistryUrl.test.ts @@ -0,0 +1,76 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import type { Mock } from 'vitest'; +import { loadRegistryUrl } from './loadRegistryUrl'; +import { fetchRegistryDotrains } from './registry'; +import { RegistryManager } from '../providers/registry/RegistryManager'; +import { initialRegistry } from '../__mocks__/stores'; + +// Mock dependencies +vi.mock('./registry', () => ({ + fetchRegistryDotrains: vi.fn(), + validateStrategies: vi.fn() +})); + +describe('loadRegistryUrl', () => { + beforeEach(() => { + vi.resetAllMocks(); + // Reset window.location + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (global.window as any).location = undefined; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (global.window as any).location = { reload: vi.fn() }; + }); + + it('should throw an error if no URL is provided', async () => { + const mockRegistryManager = initialRegistry as RegistryManager; + await expect(loadRegistryUrl('', mockRegistryManager)).rejects.toThrow('No URL provided'); + }); + + it('should throw an error if no registry manager is provided', async () => { + await expect( + loadRegistryUrl('https://example.com/registry', null as unknown as RegistryManager) + ).rejects.toThrow('Registry manager is required'); + }); + + it('should successfully load registry URL and reload the page', async () => { + const testUrl = 'https://example.com/registry'; + const mockRegistryManager = initialRegistry as RegistryManager; + + (fetchRegistryDotrains as Mock).mockResolvedValueOnce(undefined); + await loadRegistryUrl(testUrl, mockRegistryManager); + expect(fetchRegistryDotrains).toHaveBeenCalledWith(testUrl); + expect(mockRegistryManager.setRegistry).toHaveBeenCalledWith(testUrl); + expect(window.location.reload).toHaveBeenCalled(); + }); + + it('should throw an error if fetching registry dotrains fails', async () => { + const testUrl = 'https://example.com/registry'; + const errorMessage = 'Fetch failed'; + const mockRegistryManager = { + setRegistry: vi.fn() + } as unknown as RegistryManager; + + (fetchRegistryDotrains as Mock).mockRejectedValueOnce(new Error(errorMessage)); + + await expect(loadRegistryUrl(testUrl, mockRegistryManager)).rejects.toThrow(errorMessage); + + expect(mockRegistryManager.setRegistry).not.toHaveBeenCalled(); + expect(window.location.reload).not.toHaveBeenCalled(); + }); + + it('should handle non-Error exception during registry fetch', async () => { + const testUrl = 'https://example.com/registry'; + const mockRegistryManager = { + setRegistry: vi.fn() + } as unknown as RegistryManager; + + (fetchRegistryDotrains as Mock).mockRejectedValueOnce('String error'); + + await expect(loadRegistryUrl(testUrl, mockRegistryManager)).rejects.toThrow( + 'Failed to update registry URL' + ); + + expect(mockRegistryManager.setRegistry).not.toHaveBeenCalled(); + expect(window.location.reload).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/ui-components/src/lib/services/loadRegistryUrl.ts b/packages/ui-components/src/lib/services/loadRegistryUrl.ts new file mode 100644 index 000000000..5f370f7a0 --- /dev/null +++ b/packages/ui-components/src/lib/services/loadRegistryUrl.ts @@ -0,0 +1,24 @@ +import { RegistryManager } from '../providers/registry/RegistryManager'; +import { fetchRegistryDotrains } from './registry'; + +export async function loadRegistryUrl( + url: string, + registryManager: RegistryManager +): Promise { + if (!url) { + throw new Error('No URL provided'); + } + + if (!registryManager) { + throw new Error('Registry manager is required'); + } + + try { + await fetchRegistryDotrains(url); + registryManager.setRegistry(url); + window.location.reload(); + } catch (e) { + const errorMessage = e instanceof Error ? e.message : 'Failed to update registry URL'; + throw new Error(errorMessage); + } +} diff --git a/packages/ui-components/src/lib/types/registry.ts b/packages/ui-components/src/lib/types/registry.ts new file mode 100644 index 000000000..f98cd0c6f --- /dev/null +++ b/packages/ui-components/src/lib/types/registry.ts @@ -0,0 +1,4 @@ +import type { Readable } from 'svelte/store'; +import type { RegistryManager } from '../providers/registry/RegistryManager'; + +export type RegistryStore = Readable; diff --git a/packages/ui-components/test-setup.ts b/packages/ui-components/test-setup.ts index e12c9cf0c..4e41efb1f 100644 --- a/packages/ui-components/test-setup.ts +++ b/packages/ui-components/test-setup.ts @@ -1,10 +1,14 @@ import '@testing-library/jest-dom/vitest'; import { vi } from 'vitest'; - +import { mockRegistryStore } from './src/lib/__mocks__/stores'; vi.mock('codemirror-rainlang', () => ({ RainlangLR: vi.fn() })); +vi.mock('$lib/providers/registry/useRegistry', () => ({ + useRegistry: vi.fn().mockReturnValue(mockRegistryStore) +})); + vi.mock('$app/stores', async () => { const { readable, writable } = await import('svelte/store'); /** From 48de062a40f0c5b5240c633dfaceddf071a422fb Mon Sep 17 00:00:00 2001 From: Jamie Harding Date: Mon, 28 Apr 2025 14:43:21 +0200 Subject: [PATCH 02/24] webapp changes --- .../webapp/src/routes/deploy/+layout.svelte | 62 +++++++++++-------- packages/webapp/src/routes/deploy/+layout.ts | 53 +++++++++------- .../webapp/src/routes/deploy/+page.svelte | 41 ++++++------ 3 files changed, 85 insertions(+), 71 deletions(-) diff --git a/packages/webapp/src/routes/deploy/+layout.svelte b/packages/webapp/src/routes/deploy/+layout.svelte index ed10ed46e..8193899b7 100644 --- a/packages/webapp/src/routes/deploy/+layout.svelte +++ b/packages/webapp/src/routes/deploy/+layout.svelte @@ -1,35 +1,47 @@ - - + + {#if $registryManagerStore} +
- {#if isDeployPage} - (advancedMode = !advancedMode)}> - Advanced mode - - {/if} -
-
- - {#if customRegistry} - - {/if} - -
-
- {#if advancedMode && isDeployPage} -
- +
+ {#if $registryManagerStore.isCustomRegistry()} + + {:else if $page.url.pathname === '/deploy'} +
+ {/if} + {#if $page.url.pathname === '/deploy'} + (advancedMode = !advancedMode)}> + Advanced mode + + {/if} +
+
+ {#if advancedMode && $page.url.pathname === '/deploy'} +
+ +
+ {/if} +
+ {/if} -
- + diff --git a/packages/webapp/src/routes/deploy/+layout.ts b/packages/webapp/src/routes/deploy/+layout.ts index 1db507563..efde4264f 100644 --- a/packages/webapp/src/routes/deploy/+layout.ts +++ b/packages/webapp/src/routes/deploy/+layout.ts @@ -1,23 +1,28 @@ import { REGISTRY_URL } from '$lib/constants'; -import { - validateStrategies, - fetchRegistryDotrains, - type RegistryDotrain -} from '@rainlanguage/ui-components/services'; import type { LayoutLoad } from './$types'; -import type { ValidStrategyDetail, InvalidStrategyDetail } from '@rainlanguage/ui-components'; import type { Mock } from 'vitest'; +import type { InvalidStrategyDetail, ValidStrategyDetail } from '@rainlanguage/ui-components'; +import { fetchRegistryDotrains, validateStrategies } from '@rainlanguage/ui-components/services'; +import type { RegistryDotrain } from '@rainlanguage/ui-components/services'; + +type LoadResult = { + registryFromUrl: string; + registryDotrains: RegistryDotrain[]; + validStrategies: ValidStrategyDetail[]; + invalidStrategies: InvalidStrategyDetail[]; + error: string | null; +}; export const load: LayoutLoad = async ({ url }) => { - const registry = url.searchParams.get('registry') || REGISTRY_URL; + const registryFromUrl = url.searchParams.get('registry') || REGISTRY_URL; try { - const registryDotrains = await fetchRegistryDotrains(registry); + const registryDotrains = await fetchRegistryDotrains(registryFromUrl); const { validStrategies, invalidStrategies } = await validateStrategies(registryDotrains); return { - registry, + registryFromUrl, registryDotrains, validStrategies, invalidStrategies, @@ -25,7 +30,7 @@ export const load: LayoutLoad = async ({ url }) => { }; } catch (error: unknown) { return { - registry, + registryFromUrl, registryDotrains: [], validStrategies: [], invalidStrategies: [], @@ -42,19 +47,19 @@ if (import.meta.vitest) { invalidStrategies: ['invalidStrategy'] as unknown as InvalidStrategyDetail[] }; - type LoadResult = { - registry: string; - registryDotrains: RegistryDotrain[]; - validStrategies: ValidStrategyDetail[]; - invalidStrategies: InvalidStrategyDetail[]; - error: string | null; - }; - vi.mock('@rainlanguage/ui-components/services', () => ({ validateStrategies: vi.fn(), fetchRegistryDotrains: vi.fn() })); + vi.mock('$lib/services/RegistryManager', () => ({ + default: { + isCustomRegistry: vi.fn(), + setToStorage: vi.fn(), + clearFromStorage: vi.fn() + } + })); + describe('Layout load function', () => { beforeEach(() => { vi.resetAllMocks(); @@ -81,7 +86,7 @@ if (import.meta.vitest) { expect(validateStrategies).toHaveBeenCalledWith(mockDotrains); expect(result).toEqual({ - registry: REGISTRY_URL, + registryFromUrl: REGISTRY_URL, registryDotrains: mockDotrains, validStrategies: mockValidated.validStrategies, invalidStrategies: mockValidated.invalidStrategies, @@ -97,7 +102,7 @@ if (import.meta.vitest) { const result = await load(createUrlMock(customRegistry)); expect(result).toEqual({ - registry: customRegistry, + registryFromUrl: customRegistry, registryDotrains: mockDotrains, validStrategies: mockValidated.validStrategies, invalidStrategies: mockValidated.invalidStrategies, @@ -113,7 +118,7 @@ if (import.meta.vitest) { expect(validateStrategies).not.toHaveBeenCalled(); expect(result).toEqual({ - registry: REGISTRY_URL, + registryFromUrl: REGISTRY_URL, registryDotrains: [], validStrategies: [], invalidStrategies: [], @@ -132,7 +137,7 @@ if (import.meta.vitest) { const result = await load(createUrlMock(null)); expect(result).toEqual({ - registry: REGISTRY_URL, + registryFromUrl: REGISTRY_URL, registryDotrains: [], validStrategies: [], invalidStrategies: [], @@ -145,7 +150,7 @@ if (import.meta.vitest) { const result = await load(createUrlMock(null)); expect(result).toEqual({ - registry: REGISTRY_URL, + registryFromUrl: REGISTRY_URL, registryDotrains: [], validStrategies: [], invalidStrategies: [], @@ -169,7 +174,7 @@ if (import.meta.vitest) { expect(validateStrategies).toHaveBeenCalledWith(emptyDotrains); expect(result).toEqual({ - registry: REGISTRY_URL, + registryFromUrl: REGISTRY_URL, registryDotrains: emptyDotrains, validStrategies: emptyValidated.validStrategies, invalidStrategies: emptyValidated.invalidStrategies, diff --git a/packages/webapp/src/routes/deploy/+page.svelte b/packages/webapp/src/routes/deploy/+page.svelte index 167055f85..7287adfd4 100644 --- a/packages/webapp/src/routes/deploy/+page.svelte +++ b/packages/webapp/src/routes/deploy/+page.svelte @@ -1,36 +1,33 @@
Strategies
+ +
+

+ Raindex empowers you to take full control of your trading strategies. All the strategies here + are non-custodial, perpetual, and automated strategies built with our open-source, DeFi-native + language Rainlang +

+
{#if error}
- Error loading registry:{error} + Failed to load strategies:{error}
+ {:else if validStrategies.length === 0 && invalidStrategies.length === 0} +
No strategies found
{:else} -
-

- Raindex empowers you to take full control of your trading strategies. All the strategies - here are non-custodial, perpetual, and automated strategies built with our open-source, - DeFi-native language Rainlang -

-
- {#if validStrategies.length === 0 && invalidStrategies.length === 0} -
No strategies found
- {:else} - {#if validStrategies.length > 0} - - {/if} - {#if invalidStrategies.length > 0} - - {/if} + {#if validStrategies.length > 0} + + {/if} + {#if invalidStrategies.length > 0} + {/if} {/if}
From 5e653b0030f6102a6168744a71562a746ec99e1c Mon Sep 17 00:00:00 2001 From: Jamie Harding Date: Mon, 28 Apr 2025 14:48:03 +0200 Subject: [PATCH 03/24] update tests --- .../ui-components/src/__tests__/InputRegistryUrl.test.ts | 6 +++--- .../src/lib/components/input/InputRegistryUrl.svelte | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/ui-components/src/__tests__/InputRegistryUrl.test.ts b/packages/ui-components/src/__tests__/InputRegistryUrl.test.ts index 87e6cf14e..c549cff54 100644 --- a/packages/ui-components/src/__tests__/InputRegistryUrl.test.ts +++ b/packages/ui-components/src/__tests__/InputRegistryUrl.test.ts @@ -62,7 +62,7 @@ describe('InputRegistryUrl', () => { render(InputRegistryUrl); - const button = screen.getByText('Load Registry URL'); + const button = screen.getByText('Load registry URL'); await fireEvent.click(button); expect(await screen.findByTestId('registry-error')).toHaveTextContent('Test error'); @@ -81,7 +81,7 @@ describe('InputRegistryUrl', () => { render(InputRegistryUrl); - const button = screen.getByText('Load Registry URL'); + const button = screen.getByText('Load registry URL'); await user.click(button); expect(screen.getByText('Loading registry...')).toBeInTheDocument(); @@ -89,7 +89,7 @@ describe('InputRegistryUrl', () => { await vi.runAllTimersAsync(); - expect(screen.getByText('Load Registry URL')).toBeInTheDocument(); + expect(screen.getByText('Load registry URL')).toBeInTheDocument(); expect(button).not.toBeDisabled(); vi.useRealTimers(); diff --git a/packages/ui-components/src/lib/components/input/InputRegistryUrl.svelte b/packages/ui-components/src/lib/components/input/InputRegistryUrl.svelte index e86f5ba47..fb0d3c975 100644 --- a/packages/ui-components/src/lib/components/input/InputRegistryUrl.svelte +++ b/packages/ui-components/src/lib/components/input/InputRegistryUrl.svelte @@ -32,7 +32,7 @@ bind:value={newRegistryUrl} />
From 4b16b3bf501fe405301c3e92a23fd8d2ff6225fd Mon Sep 17 00:00:00 2001 From: Jamie Harding Date: Mon, 28 Apr 2025 14:59:38 +0200 Subject: [PATCH 04/24] Modals causing mocking error --- .../src/__tests__/DepositOrWithdrawModal.test.ts | 14 +++++++++----- .../webapp/src/__tests__/OrderRemoveModal.test.ts | 10 +++++++--- .../src/__tests__/handleGuiInitialization.test.ts | 10 ++++++++++ packages/webapp/test-setup.ts | 11 +---------- 4 files changed, 27 insertions(+), 18 deletions(-) diff --git a/packages/webapp/src/__tests__/DepositOrWithdrawModal.test.ts b/packages/webapp/src/__tests__/DepositOrWithdrawModal.test.ts index 375e4e9af..1a2202c70 100644 --- a/packages/webapp/src/__tests__/DepositOrWithdrawModal.test.ts +++ b/packages/webapp/src/__tests__/DepositOrWithdrawModal.test.ts @@ -16,11 +16,15 @@ const { mockAppKitModalStore, mockConnectedStore, mockWagmiConfigStore } = await () => import('../lib/__mocks__/stores') ); -vi.mock('@rainlanguage/orderbook', () => ({ - getVaultDepositCalldata: vi.fn().mockResolvedValue({ to: '0x123', data: '0x456' }), - getVaultApprovalCalldata: vi.fn().mockResolvedValue({ to: '0x789', data: '0xabc' }), - getVaultWithdrawCalldata: vi.fn().mockResolvedValue({ to: '0xdef', data: '0xghi' }) -})); +vi.mock('@rainlanguage/orderbook', async (importOriginal) => { + const original = (await importOriginal()) as object; + return { + ...original, + getVaultDepositCalldata: vi.fn().mockResolvedValue({ to: '0x123', data: '0x456' }), + getVaultApprovalCalldata: vi.fn().mockResolvedValue({ to: '0x789', data: '0xabc' }), + getVaultWithdrawCalldata: vi.fn().mockResolvedValue({ to: '0xdef', data: '0xghi' }) + }; +}); vi.mock('../lib/stores/wagmi', () => ({ appKitModal: mockAppKitModalStore, diff --git a/packages/webapp/src/__tests__/OrderRemoveModal.test.ts b/packages/webapp/src/__tests__/OrderRemoveModal.test.ts index 76c4954cb..2397ba199 100644 --- a/packages/webapp/src/__tests__/OrderRemoveModal.test.ts +++ b/packages/webapp/src/__tests__/OrderRemoveModal.test.ts @@ -4,9 +4,13 @@ import OrderRemoveModal from '$lib/components/OrderRemoveModal.svelte'; import { transactionStore } from '@rainlanguage/ui-components'; import type { OrderRemoveModalProps } from '@rainlanguage/ui-components'; -vi.mock('@rainlanguage/orderbook', () => ({ - getRemoveOrderCalldata: vi.fn().mockResolvedValue('0x123') -})); +vi.mock('@rainlanguage/orderbook', async (importOriginal) => { + const original = (await importOriginal()) as object; + return { + ...original, + getRemoveOrderCalldata: vi.fn().mockResolvedValue('0x123') + }; +}); vi.useFakeTimers(); diff --git a/packages/webapp/src/__tests__/handleGuiInitialization.test.ts b/packages/webapp/src/__tests__/handleGuiInitialization.test.ts index ef12e0b85..ad43d840b 100644 --- a/packages/webapp/src/__tests__/handleGuiInitialization.test.ts +++ b/packages/webapp/src/__tests__/handleGuiInitialization.test.ts @@ -3,6 +3,16 @@ import { handleGuiInitialization } from '../lib/services/handleGuiInitialization import { pushGuiStateToUrlHistory } from '../lib/services/handleUpdateGuiState'; import { DotrainOrderGui, type WasmEncodedResult } from '@rainlanguage/orderbook'; +vi.mock('@rainlanguage/orderbook', () => { + const DotrainOrderGui = vi.fn(); + DotrainOrderGui.prototype.deserializeState = vi.fn(); + DotrainOrderGui.prototype.chooseDeployment = vi.fn(); + return { + DotrainOrderGui + }; +}); + + describe('handleGuiInitialization', () => { let guiInstance: DotrainOrderGui; const mockDotrain = 'mockDotrain'; diff --git a/packages/webapp/test-setup.ts b/packages/webapp/test-setup.ts index 3be3e4bfc..7d086373b 100644 --- a/packages/webapp/test-setup.ts +++ b/packages/webapp/test-setup.ts @@ -7,13 +7,4 @@ vi.mock('@reown/appkit', () => ({ // Mock for codemirror-rainlang vi.mock('codemirror-rainlang', () => ({ RainlangLR: vi.fn() -})); - -vi.mock('@rainlanguage/orderbook', () => { - const DotrainOrderGui = vi.fn(); - DotrainOrderGui.prototype.deserializeState = vi.fn(); - DotrainOrderGui.prototype.chooseDeployment = vi.fn(); - return { - DotrainOrderGui - }; -}); +})); \ No newline at end of file From bfcafbde5443b645bb3fa28c1e6717fd50ab0f84 Mon Sep 17 00:00:00 2001 From: Jamie Harding Date: Mon, 28 Apr 2025 15:27:49 +0200 Subject: [PATCH 05/24] remove inline mock of orderbook --- .../src/lib/services/registry.test.ts | 249 +++++++++++++++++ .../src/lib/services/registry.ts | 254 +----------------- 2 files changed, 250 insertions(+), 253 deletions(-) create mode 100644 packages/ui-components/src/lib/services/registry.test.ts diff --git a/packages/ui-components/src/lib/services/registry.test.ts b/packages/ui-components/src/lib/services/registry.test.ts new file mode 100644 index 000000000..20e06ba28 --- /dev/null +++ b/packages/ui-components/src/lib/services/registry.test.ts @@ -0,0 +1,249 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { fetchParseRegistry, fetchRegistryDotrains, validateStrategies } from './registry'; +import { DotrainOrderGui } from '@rainlanguage/orderbook'; +import type { Mock } from 'vitest'; + +// Mock the DotrainOrderGui dependency +vi.mock('@rainlanguage/orderbook', () => ({ + DotrainOrderGui: { + getStrategyDetails: vi.fn() + } +})); + +describe('fetchParseRegistry', () => { + it('should parse registry file content correctly', async () => { + const mockResponse = `file1.js https://example.com/file1.js +file2.js https://example.com/file2.js`; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + text: () => Promise.resolve(mockResponse) + }); + + const result = await fetchParseRegistry('https://example.com/registry'); + expect(result).toEqual([ + { name: 'file1.js', url: 'https://example.com/file1.js' }, + { name: 'file2.js', url: 'https://example.com/file2.js' } + ]); + }); + + it('should handle failed fetch response', async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: false + }); + + await expect(fetchParseRegistry('https://example.com/registry')).rejects.toThrow( + 'Failed to fetch registry' + ); + }); + + it('should handle network errors', async () => { + global.fetch = vi.fn().mockRejectedValue(new Error('Network error')); + + await expect(fetchParseRegistry('https://example.com/registry')).rejects.toThrow( + 'Network error' + ); + }); +}); + +describe('fetchRegistryDotrains', () => { + it('should fetch and parse dotrains correctly', async () => { + const mockRegistry = `file1.rain https://example.com/file1.rain +file2.rain https://example.com/file2.rain`; + + const mockDotrain1 = 'content of file1'; + const mockDotrain2 = 'content of file2'; + + global.fetch = vi + .fn() + .mockResolvedValueOnce({ + ok: true, + text: () => Promise.resolve(mockRegistry) + }) + .mockResolvedValueOnce({ + ok: true, + text: () => Promise.resolve(mockDotrain1) + }) + .mockResolvedValueOnce({ + ok: true, + text: () => Promise.resolve(mockDotrain2) + }); + + const result = await fetchRegistryDotrains('https://example.com/registry'); + expect(result).toEqual([ + { name: 'file1.rain', dotrain: mockDotrain1 }, + { name: 'file2.rain', dotrain: mockDotrain2 } + ]); + }); + + it('should handle failed dotrain fetch', async () => { + const mockRegistry = `file1.rain https://example.com/file1.rain`; + + global.fetch = vi + .fn() + .mockResolvedValueOnce({ + ok: true, + text: () => Promise.resolve(mockRegistry) + }) + .mockResolvedValueOnce({ + ok: false + }); + + await expect(fetchRegistryDotrains('https://example.com/registry')).rejects.toThrow( + 'Failed to fetch dotrain for file1.rain' + ); + }); + + it('should handle network errors during dotrain fetch', async () => { + const mockRegistry = `file1.rain https://example.com/file1.rain`; + + global.fetch = vi + .fn() + .mockResolvedValueOnce({ + ok: true, + text: () => Promise.resolve(mockRegistry) + }) + .mockRejectedValueOnce(new Error('Network error')); + + await expect(fetchRegistryDotrains('https://example.com/registry')).rejects.toThrow( + 'Error fetching dotrain for file1.rain: Network error' + ); + }); +}); + +describe('validateStrategies', () => { + beforeEach(() => { + vi.resetAllMocks(); + }); + + it('should validate strategies and categorize them properly', async () => { + // Input data + const registryDotrains = [ + { name: 'valid.rain', dotrain: 'valid dotrain content' }, + { name: 'invalid.rain', dotrain: 'invalid dotrain content' }, + { name: 'another-valid.rain', dotrain: 'another valid content' } + ]; + + // Set up mock responses for the DotrainOrderGui + (DotrainOrderGui.getStrategyDetails as Mock) + .mockResolvedValueOnce({ + value: { name: 'Valid Strategy', description: 'A valid strategy' }, + error: null + }) + .mockResolvedValueOnce({ + error: { msg: 'Invalid syntax' }, + value: null + }) + .mockResolvedValueOnce({ + value: { name: 'Another Valid', description: 'Another valid strategy' }, + error: null + }); + + // Call the function with our test data + const result = await validateStrategies(registryDotrains); + + // Verify DotrainOrderGui was called correctly + expect(DotrainOrderGui.getStrategyDetails).toHaveBeenCalledTimes(3); + expect(DotrainOrderGui.getStrategyDetails).toHaveBeenCalledWith('valid dotrain content'); + expect(DotrainOrderGui.getStrategyDetails).toHaveBeenCalledWith('invalid dotrain content'); + expect(DotrainOrderGui.getStrategyDetails).toHaveBeenCalledWith('another valid content'); + + // Verify the valid strategies are processed correctly + expect(result.validStrategies).toHaveLength(2); + expect(result.validStrategies[0].name).toBe('valid.rain'); + expect(result.validStrategies[0].dotrain).toBe('valid dotrain content'); + expect(result.validStrategies[0].details).toEqual({ + name: 'Valid Strategy', + description: 'A valid strategy' + }); + + // Verify the invalid strategies are processed correctly + expect(result.invalidStrategies).toHaveLength(1); + expect(result.invalidStrategies[0].name).toBe('invalid.rain'); + expect(result.invalidStrategies[0].error).toBe('Invalid syntax'); + }); + + it('should handle exceptions thrown during strategy validation', async () => { + // Input data + const registryDotrains = [{ name: 'error.rain', dotrain: 'will throw error' }]; + + // Mock the DotrainOrderGui to throw an exception + (DotrainOrderGui.getStrategyDetails as Mock).mockRejectedValueOnce( + new Error('Unexpected parsing error') + ); + + // Call the function + const result = await validateStrategies(registryDotrains); + + // Verify results + expect(result.validStrategies).toHaveLength(0); + expect(result.invalidStrategies).toHaveLength(1); + expect(result.invalidStrategies[0].name).toBe('error.rain'); + expect(result.invalidStrategies[0].error).toBe('Unexpected parsing error'); + }); + + it('should handle non-Error objects being thrown', async () => { + // Input data + const registryDotrains = [{ name: 'string-error.rain', dotrain: 'will throw string' }]; + + // Mock the DotrainOrderGui to throw a string instead of an Error + (DotrainOrderGui.getStrategyDetails as Mock).mockRejectedValueOnce('String error message'); + + // Call the function + const result = await validateStrategies(registryDotrains); + + // Verify results + expect(result.validStrategies).toHaveLength(0); + expect(result.invalidStrategies).toHaveLength(1); + expect(result.invalidStrategies[0].name).toBe('string-error.rain'); + expect(result.invalidStrategies[0].error).toBe('String error message'); + }); + + it('should process an empty array of strategies', async () => { + const result = await validateStrategies([]); + + expect(result.validStrategies).toEqual([]); + expect(result.invalidStrategies).toEqual([]); + expect(DotrainOrderGui.getStrategyDetails).not.toHaveBeenCalled(); + }); + + it('should handle mixed validation results correctly', async () => { + // Create a mix of scenarios + const registryDotrains = [ + { name: 'valid1.rain', dotrain: 'valid content 1' }, + { name: 'error.rain', dotrain: 'will throw error' }, + { name: 'valid2.rain', dotrain: 'valid content 2' }, + { name: 'invalid.rain', dotrain: 'invalid content' } + ]; + + // Set up mock responses + (DotrainOrderGui.getStrategyDetails as Mock) + .mockResolvedValueOnce({ + value: { strategyName: 'Strategy 1', description: 'Description 1' }, + error: null + }) + .mockRejectedValueOnce(new Error('Processing error')) + .mockResolvedValueOnce({ + value: { strategyName: 'Strategy 2', description: 'Description 2' }, + error: null + }) + .mockResolvedValueOnce({ + error: { msg: 'Validation failed' }, + value: null + }); + + // Call the function + const result = await validateStrategies(registryDotrains); + + // Verify results + expect(result.validStrategies).toHaveLength(2); + expect(result.validStrategies[0].name).toBe('valid1.rain'); + expect(result.validStrategies[1].name).toBe('valid2.rain'); + + expect(result.invalidStrategies).toHaveLength(2); + expect(result.invalidStrategies[0].name).toBe('error.rain'); + expect(result.invalidStrategies[0].error).toBe('Processing error'); + expect(result.invalidStrategies[1].name).toBe('invalid.rain'); + expect(result.invalidStrategies[1].error).toBe('Validation failed'); + }); +}); \ No newline at end of file diff --git a/packages/ui-components/src/lib/services/registry.ts b/packages/ui-components/src/lib/services/registry.ts index f3de6ab91..9c40fa5a4 100644 --- a/packages/ui-components/src/lib/services/registry.ts +++ b/packages/ui-components/src/lib/services/registry.ts @@ -116,256 +116,4 @@ export async function validateStrategies( .map((result) => result.data as InvalidStrategyDetail); return { validStrategies, invalidStrategies }; -} - -if (import.meta.vitest) { - const { describe, it, expect, vi } = import.meta.vitest; - - describe('getFileRegistry', () => { - it('should parse registry file content correctly', async () => { - const mockResponse = `file1.js https://example.com/file1.js -file2.js https://example.com/file2.js`; - - global.fetch = vi.fn().mockResolvedValue({ - ok: true, - text: () => Promise.resolve(mockResponse) - }); - - const result = await fetchParseRegistry('https://example.com/registry'); - expect(result).toEqual([ - { name: 'file1.js', url: 'https://example.com/file1.js' }, - { name: 'file2.js', url: 'https://example.com/file2.js' } - ]); - }); - - it('should handle failed fetch response', async () => { - global.fetch = vi.fn().mockResolvedValue({ - ok: false - }); - - await expect(fetchParseRegistry('https://example.com/registry')).rejects.toThrow( - 'Failed to fetch registry' - ); - }); - - it('should handle network errors', async () => { - global.fetch = vi.fn().mockRejectedValue(new Error('Network error')); - - await expect(fetchParseRegistry('https://example.com/registry')).rejects.toThrow( - 'Network error' - ); - }); - }); - - describe('fetchRegistryDotrains', () => { - it('should fetch and parse dotrains correctly', async () => { - const mockRegistry = `file1.rain https://example.com/file1.rain -file2.rain https://example.com/file2.rain`; - - const mockDotrain1 = 'content of file1'; - const mockDotrain2 = 'content of file2'; - - global.fetch = vi - .fn() - .mockResolvedValueOnce({ - ok: true, - text: () => Promise.resolve(mockRegistry) - }) - .mockResolvedValueOnce({ - ok: true, - text: () => Promise.resolve(mockDotrain1) - }) - .mockResolvedValueOnce({ - ok: true, - text: () => Promise.resolve(mockDotrain2) - }); - - const result = await fetchRegistryDotrains('https://example.com/registry'); - expect(result).toEqual([ - { name: 'file1.rain', dotrain: mockDotrain1 }, - { name: 'file2.rain', dotrain: mockDotrain2 } - ]); - }); - - it('should handle failed dotrain fetch', async () => { - const mockRegistry = `file1.rain https://example.com/file1.rain`; - - global.fetch = vi - .fn() - .mockResolvedValueOnce({ - ok: true, - text: () => Promise.resolve(mockRegistry) - }) - .mockResolvedValueOnce({ - ok: false - }); - - await expect(fetchRegistryDotrains('https://example.com/registry')).rejects.toThrow( - 'Failed to fetch dotrain for file1.rain' - ); - }); - - it('should handle network errors during dotrain fetch', async () => { - const mockRegistry = `file1.rain https://example.com/file1.rain`; - - global.fetch = vi - .fn() - .mockResolvedValueOnce({ - ok: true, - text: () => Promise.resolve(mockRegistry) - }) - .mockRejectedValueOnce(new Error('Network error')); - - await expect(fetchRegistryDotrains('https://example.com/registry')).rejects.toThrow( - 'Error fetching dotrain for file1.rain: Network error' - ); - }); - }); - - describe('validateStrategies', async () => { - // Mock the DotrainOrderGui dependency - vi.mock('@rainlanguage/orderbook', () => ({ - DotrainOrderGui: { - getStrategyDetails: vi.fn() - } - })); - - // Import DotrainOrderGui after mocking - const { DotrainOrderGui } = await import('@rainlanguage/orderbook'); - - beforeEach(() => { - vi.resetAllMocks(); - }); - - it('should validate strategies and categorize them properly', async () => { - // Input data - const registryDotrains = [ - { name: 'valid.rain', dotrain: 'valid dotrain content' }, - { name: 'invalid.rain', dotrain: 'invalid dotrain content' }, - { name: 'another-valid.rain', dotrain: 'another valid content' } - ]; - - // Set up mock responses for the DotrainOrderGui - (DotrainOrderGui.getStrategyDetails as Mock) - .mockResolvedValueOnce({ - value: { name: 'Valid Strategy', description: 'A valid strategy' }, - error: null - }) - .mockResolvedValueOnce({ - error: { msg: 'Invalid syntax' }, - value: null - }) - .mockResolvedValueOnce({ - value: { name: 'Another Valid', description: 'Another valid strategy' }, - error: null - }); - - // Call the function with our test data - const result = await validateStrategies(registryDotrains); - - // Verify DotrainOrderGui was called correctly - expect(DotrainOrderGui.getStrategyDetails).toHaveBeenCalledTimes(3); - expect(DotrainOrderGui.getStrategyDetails).toHaveBeenCalledWith('valid dotrain content'); - expect(DotrainOrderGui.getStrategyDetails).toHaveBeenCalledWith('invalid dotrain content'); - expect(DotrainOrderGui.getStrategyDetails).toHaveBeenCalledWith('another valid content'); - - // Verify the valid strategies are processed correctly - expect(result.validStrategies).toHaveLength(2); - expect(result.validStrategies[0].name).toBe('valid.rain'); - expect(result.validStrategies[0].dotrain).toBe('valid dotrain content'); - expect(result.validStrategies[0].details).toEqual({ - name: 'Valid Strategy', - description: 'A valid strategy' - }); - - // Verify the invalid strategies are processed correctly - expect(result.invalidStrategies).toHaveLength(1); - expect(result.invalidStrategies[0].name).toBe('invalid.rain'); - expect(result.invalidStrategies[0].error).toBe('Invalid syntax'); - }); - - it('should handle exceptions thrown during strategy validation', async () => { - // Input data - const registryDotrains = [{ name: 'error.rain', dotrain: 'will throw error' }]; - - // Mock the DotrainOrderGui to throw an exception - (DotrainOrderGui.getStrategyDetails as Mock).mockRejectedValueOnce( - new Error('Unexpected parsing error') - ); - - // Call the function - const result = await validateStrategies(registryDotrains); - - // Verify results - expect(result.validStrategies).toHaveLength(0); - expect(result.invalidStrategies).toHaveLength(1); - expect(result.invalidStrategies[0].name).toBe('error.rain'); - expect(result.invalidStrategies[0].error).toBe('Unexpected parsing error'); - }); - - it('should handle non-Error objects being thrown', async () => { - // Input data - const registryDotrains = [{ name: 'string-error.rain', dotrain: 'will throw string' }]; - - // Mock the DotrainOrderGui to throw a string instead of an Error - (DotrainOrderGui.getStrategyDetails as Mock).mockRejectedValueOnce('String error message'); - - // Call the function - const result = await validateStrategies(registryDotrains); - - // Verify results - expect(result.validStrategies).toHaveLength(0); - expect(result.invalidStrategies).toHaveLength(1); - expect(result.invalidStrategies[0].name).toBe('string-error.rain'); - expect(result.invalidStrategies[0].error).toBe('String error message'); - }); - - it('should process an empty array of strategies', async () => { - const result = await validateStrategies([]); - - expect(result.validStrategies).toEqual([]); - expect(result.invalidStrategies).toEqual([]); - expect(DotrainOrderGui.getStrategyDetails).not.toHaveBeenCalled(); - }); - - it('should handle mixed validation results correctly', async () => { - // Create a mix of scenarios - const registryDotrains = [ - { name: 'valid1.rain', dotrain: 'valid content 1' }, - { name: 'error.rain', dotrain: 'will throw error' }, - { name: 'valid2.rain', dotrain: 'valid content 2' }, - { name: 'invalid.rain', dotrain: 'invalid content' } - ]; - - // Set up mock responses - (DotrainOrderGui.getStrategyDetails as Mock) - .mockResolvedValueOnce({ - value: { strategyName: 'Strategy 1', description: 'Description 1' }, - error: null - }) - .mockRejectedValueOnce(new Error('Processing error')) - .mockResolvedValueOnce({ - value: { strategyName: 'Strategy 2', description: 'Description 2' }, - error: null - }) - .mockResolvedValueOnce({ - error: { msg: 'Validation failed' }, - value: null - }); - - // Call the function - const result = await validateStrategies(registryDotrains); - - // Verify results - expect(result.validStrategies).toHaveLength(2); - expect(result.validStrategies[0].name).toBe('valid1.rain'); - expect(result.validStrategies[1].name).toBe('valid2.rain'); - - expect(result.invalidStrategies).toHaveLength(2); - expect(result.invalidStrategies[0].name).toBe('error.rain'); - expect(result.invalidStrategies[0].error).toBe('Processing error'); - expect(result.invalidStrategies[1].name).toBe('invalid.rain'); - expect(result.invalidStrategies[1].error).toBe('Validation failed'); - }); - }); -} +} \ No newline at end of file From af075ccc3fe09c23c39d2138e7f0df4c763f78d6 Mon Sep 17 00:00:00 2001 From: Jamie Harding Date: Mon, 28 Apr 2025 15:40:08 +0200 Subject: [PATCH 06/24] Move tests back to main --- packages/webapp/src/lib/__mocks__/stores.ts | 8 +- .../[deploymentKey]/+page.svelte | 4 - .../webapp/src/routes/deploy/page.test.ts | 188 ++++++++++++++++++ 3 files changed, 195 insertions(+), 5 deletions(-) create mode 100644 packages/webapp/src/routes/deploy/page.test.ts diff --git a/packages/webapp/src/lib/__mocks__/stores.ts b/packages/webapp/src/lib/__mocks__/stores.ts index e4e359401..f43b3cee7 100644 --- a/packages/webapp/src/lib/__mocks__/stores.ts +++ b/packages/webapp/src/lib/__mocks__/stores.ts @@ -60,5 +60,11 @@ export const mockAppKitModalStore = { export const mockPageStore = { subscribe: mockPageWritable.subscribe, set: mockPageWritable.set, - mockSetSubscribeValue: (value: typeof initialPageState): void => mockPageWritable.set(value) + mockSetSubscribeValue: (newValue: Partial): void => { + mockPageWritable.update((currentValue) => ({ + ...currentValue, + ...newValue + })); + }, + reset: () => mockPageWritable.set(initialPageState) }; diff --git a/packages/webapp/src/routes/deploy/[strategyName]/[deploymentKey]/+page.svelte b/packages/webapp/src/routes/deploy/[strategyName]/[deploymentKey]/+page.svelte index 50ed067c6..06cabaf40 100644 --- a/packages/webapp/src/routes/deploy/[strategyName]/[deploymentKey]/+page.svelte +++ b/packages/webapp/src/routes/deploy/[strategyName]/[deploymentKey]/+page.svelte @@ -7,7 +7,6 @@ import { DotrainOrderGui } from '@rainlanguage/orderbook'; import { onMount } from 'svelte'; import { handleGuiInitialization } from '$lib/services/handleGuiInitialization'; - import { REGISTRY_URL } from '$lib/constants'; const { settings } = $page.data.stores; const { dotrain, deployment, strategyDetail } = $page.data; @@ -16,8 +15,6 @@ let gui: DotrainOrderGui | null = null; let getGuiError: string | null = null; - $: registryUrl = $page.url.searchParams?.get('registry') || REGISTRY_URL; - if (!dotrain || !deployment) { setTimeout(() => { goto('/deploy'); @@ -60,7 +57,6 @@ {appKitModal} {onDeploy} {settings} - {registryUrl} /> {:else if getGuiError} diff --git a/packages/webapp/src/routes/deploy/page.test.ts b/packages/webapp/src/routes/deploy/page.test.ts new file mode 100644 index 000000000..bbad0b9c5 --- /dev/null +++ b/packages/webapp/src/routes/deploy/page.test.ts @@ -0,0 +1,188 @@ +import { describe, beforeEach, it, expect, vi, type Mock } from 'vitest'; +import { render, screen, waitFor } from '@testing-library/svelte'; +import Page from './+page.svelte'; +import { readable } from 'svelte/store'; +import { + useRegistry, + type ValidStrategyDetail, + type InvalidStrategyDetail +} from '@rainlanguage/ui-components'; + +const { mockPageStore } = await vi.hoisted(() => import('$lib/__mocks__/stores')); + +vi.mock('$app/stores', async (importOriginal) => { + return { + ...((await importOriginal()) as object), + page: mockPageStore + }; +}); + +const mockValidStrategy1: ValidStrategyDetail = { + details: { + name: 'Strategy One', + description: 'This is the first valid strategy.', + short_description: 'Valid 1' + }, + name: 'strategy1.dotrain', + dotrain: ';;' +}; + +const mockRegistry = vi.fn(); +const mockIsCustomRegistry = vi.fn(); + +const mockValidStrategy2: ValidStrategyDetail = { + details: { + name: 'Strategy Two', + description: 'Another valid strategy.', + short_description: 'Valid 2' + }, + name: 'strategy2.dotrain', + dotrain: ';;' +}; + +const mockInvalidStrategy1: InvalidStrategyDetail = { + name: 'invalidStrategy.dotrain', + error: 'Syntax error on line 1' +}; + +vi.mock('@rainlanguage/ui-components', async (importOriginal) => { + return { + ...((await importOriginal()) as object), + useRegistry: vi.fn() + }; +}); + +const mockGetCurrentRegistry = vi.fn().mockReturnValue(readable({})); + +describe('Page Component', () => { + const mockValidated = { + validStrategies: [mockValidStrategy1, mockValidStrategy2], + invalidStrategies: [mockInvalidStrategy1] + }; + + beforeEach(() => { + vi.resetAllMocks(); + (useRegistry as Mock).mockReturnValue( + readable({ + getCurrentRegistry: mockGetCurrentRegistry, + isCustomRegistry: mockIsCustomRegistry, + subscribe: vi.fn() + }) + ); + mockIsCustomRegistry.mockReturnValue(true); + mockPageStore.reset(); + }); + + it('should display error message when fetching strategies fails', async () => { + mockPageStore.mockSetSubscribeValue({ + data: { + error: 'Failed to fetch registry dotrains' + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any + }); + + render(Page, { + context: new Map([['$$_registry', mockRegistry]]) + }); + + await waitFor(() => { + const errorMessage = screen.getByTestId('error-message'); + expect(errorMessage).toBeInTheDocument(); + expect(errorMessage).toHaveTextContent('Failed to fetch registry dotrains'); + }); + }); + + it('should display error message when validating strategies fails', async () => { + mockPageStore.mockSetSubscribeValue({ + data: { + error: 'Failed to validate strategies' + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as unknown as any + }); + + render(Page, { + context: new Map([['$$_registry', mockRegistry]]) + }); + + await waitFor(() => { + const errorMessage = screen.getByTestId('error-message'); + expect(errorMessage).toBeInTheDocument(); + expect(errorMessage).toHaveTextContent('Failed to validate strategies'); + }); + }); + + it('should display no strategies found when no strategies are available', async () => { + mockPageStore.mockSetSubscribeValue({ + // @ts-ignore - Type mismatch is expected in test + data: { + error: null, + validStrategies: [], + invalidStrategies: [] + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as unknown as any + }); + + render(Page, { + context: new Map([['$$_registry', mockRegistry]]) + }); + + await waitFor(() => { + expect(screen.getByText('No strategies found')).toBeInTheDocument(); + }); + }); + + it('should display valid strategies when they are available', async () => { + mockPageStore.mockSetSubscribeValue({ + data: { + validStrategies: mockValidated.validStrategies, + invalidStrategies: [] + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any + }); + + render(Page, { + context: new Map([['$$_registry', mockRegistry]]) + }); + + await waitFor(() => { + expect(screen.getByTestId('valid-strategies')).toBeInTheDocument(); + }); + }); + + it('should display invalid strategies when they are available', async () => { + mockPageStore.mockSetSubscribeValue({ + data: { + validStrategies: [], + invalidStrategies: mockValidated.invalidStrategies + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any + }); + + render(Page, { + context: new Map([['$$_registry', mockRegistry]]) + }); + + await waitFor(() => { + expect(screen.getByTestId('invalid-strategies')).toBeInTheDocument(); + }); + }); + + it('should display valid and invalid strategies when both are available', async () => { + mockPageStore.mockSetSubscribeValue({ + data: { + validStrategies: mockValidated.validStrategies, + invalidStrategies: mockValidated.invalidStrategies + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any + }); + + render(Page, { + context: new Map([['$$_registry', mockRegistry]]) + }); + + await waitFor(() => { + expect(screen.getByTestId('valid-strategies')).toBeInTheDocument(); + expect(screen.getByTestId('invalid-strategies')).toBeInTheDocument(); + }); + }); +}); From 6ba629ca9672bef4fb17cd05c80220456878e012 Mon Sep 17 00:00:00 2001 From: Jamie Harding Date: Mon, 28 Apr 2025 15:54:49 +0200 Subject: [PATCH 07/24] format and lint --- .../src/lib/services/registry.test.ts | 458 +++++++++--------- .../src/lib/services/registry.ts | 3 +- .../__tests__/handleGuiInitialization.test.ts | 1 - .../webapp/src/routes/deploy/page.test.ts | 1 - 4 files changed, 230 insertions(+), 233 deletions(-) diff --git a/packages/ui-components/src/lib/services/registry.test.ts b/packages/ui-components/src/lib/services/registry.test.ts index 20e06ba28..da858bf02 100644 --- a/packages/ui-components/src/lib/services/registry.test.ts +++ b/packages/ui-components/src/lib/services/registry.test.ts @@ -5,245 +5,245 @@ import type { Mock } from 'vitest'; // Mock the DotrainOrderGui dependency vi.mock('@rainlanguage/orderbook', () => ({ - DotrainOrderGui: { - getStrategyDetails: vi.fn() - } + DotrainOrderGui: { + getStrategyDetails: vi.fn() + } })); describe('fetchParseRegistry', () => { - it('should parse registry file content correctly', async () => { - const mockResponse = `file1.js https://example.com/file1.js + it('should parse registry file content correctly', async () => { + const mockResponse = `file1.js https://example.com/file1.js file2.js https://example.com/file2.js`; - global.fetch = vi.fn().mockResolvedValue({ - ok: true, - text: () => Promise.resolve(mockResponse) - }); - - const result = await fetchParseRegistry('https://example.com/registry'); - expect(result).toEqual([ - { name: 'file1.js', url: 'https://example.com/file1.js' }, - { name: 'file2.js', url: 'https://example.com/file2.js' } - ]); - }); - - it('should handle failed fetch response', async () => { - global.fetch = vi.fn().mockResolvedValue({ - ok: false - }); - - await expect(fetchParseRegistry('https://example.com/registry')).rejects.toThrow( - 'Failed to fetch registry' - ); - }); - - it('should handle network errors', async () => { - global.fetch = vi.fn().mockRejectedValue(new Error('Network error')); - - await expect(fetchParseRegistry('https://example.com/registry')).rejects.toThrow( - 'Network error' - ); - }); + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + text: () => Promise.resolve(mockResponse) + }); + + const result = await fetchParseRegistry('https://example.com/registry'); + expect(result).toEqual([ + { name: 'file1.js', url: 'https://example.com/file1.js' }, + { name: 'file2.js', url: 'https://example.com/file2.js' } + ]); + }); + + it('should handle failed fetch response', async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: false + }); + + await expect(fetchParseRegistry('https://example.com/registry')).rejects.toThrow( + 'Failed to fetch registry' + ); + }); + + it('should handle network errors', async () => { + global.fetch = vi.fn().mockRejectedValue(new Error('Network error')); + + await expect(fetchParseRegistry('https://example.com/registry')).rejects.toThrow( + 'Network error' + ); + }); }); describe('fetchRegistryDotrains', () => { - it('should fetch and parse dotrains correctly', async () => { - const mockRegistry = `file1.rain https://example.com/file1.rain + it('should fetch and parse dotrains correctly', async () => { + const mockRegistry = `file1.rain https://example.com/file1.rain file2.rain https://example.com/file2.rain`; - const mockDotrain1 = 'content of file1'; - const mockDotrain2 = 'content of file2'; - - global.fetch = vi - .fn() - .mockResolvedValueOnce({ - ok: true, - text: () => Promise.resolve(mockRegistry) - }) - .mockResolvedValueOnce({ - ok: true, - text: () => Promise.resolve(mockDotrain1) - }) - .mockResolvedValueOnce({ - ok: true, - text: () => Promise.resolve(mockDotrain2) - }); - - const result = await fetchRegistryDotrains('https://example.com/registry'); - expect(result).toEqual([ - { name: 'file1.rain', dotrain: mockDotrain1 }, - { name: 'file2.rain', dotrain: mockDotrain2 } - ]); - }); - - it('should handle failed dotrain fetch', async () => { - const mockRegistry = `file1.rain https://example.com/file1.rain`; - - global.fetch = vi - .fn() - .mockResolvedValueOnce({ - ok: true, - text: () => Promise.resolve(mockRegistry) - }) - .mockResolvedValueOnce({ - ok: false - }); - - await expect(fetchRegistryDotrains('https://example.com/registry')).rejects.toThrow( - 'Failed to fetch dotrain for file1.rain' - ); - }); - - it('should handle network errors during dotrain fetch', async () => { - const mockRegistry = `file1.rain https://example.com/file1.rain`; - - global.fetch = vi - .fn() - .mockResolvedValueOnce({ - ok: true, - text: () => Promise.resolve(mockRegistry) - }) - .mockRejectedValueOnce(new Error('Network error')); - - await expect(fetchRegistryDotrains('https://example.com/registry')).rejects.toThrow( - 'Error fetching dotrain for file1.rain: Network error' - ); - }); + const mockDotrain1 = 'content of file1'; + const mockDotrain2 = 'content of file2'; + + global.fetch = vi + .fn() + .mockResolvedValueOnce({ + ok: true, + text: () => Promise.resolve(mockRegistry) + }) + .mockResolvedValueOnce({ + ok: true, + text: () => Promise.resolve(mockDotrain1) + }) + .mockResolvedValueOnce({ + ok: true, + text: () => Promise.resolve(mockDotrain2) + }); + + const result = await fetchRegistryDotrains('https://example.com/registry'); + expect(result).toEqual([ + { name: 'file1.rain', dotrain: mockDotrain1 }, + { name: 'file2.rain', dotrain: mockDotrain2 } + ]); + }); + + it('should handle failed dotrain fetch', async () => { + const mockRegistry = `file1.rain https://example.com/file1.rain`; + + global.fetch = vi + .fn() + .mockResolvedValueOnce({ + ok: true, + text: () => Promise.resolve(mockRegistry) + }) + .mockResolvedValueOnce({ + ok: false + }); + + await expect(fetchRegistryDotrains('https://example.com/registry')).rejects.toThrow( + 'Failed to fetch dotrain for file1.rain' + ); + }); + + it('should handle network errors during dotrain fetch', async () => { + const mockRegistry = `file1.rain https://example.com/file1.rain`; + + global.fetch = vi + .fn() + .mockResolvedValueOnce({ + ok: true, + text: () => Promise.resolve(mockRegistry) + }) + .mockRejectedValueOnce(new Error('Network error')); + + await expect(fetchRegistryDotrains('https://example.com/registry')).rejects.toThrow( + 'Error fetching dotrain for file1.rain: Network error' + ); + }); }); describe('validateStrategies', () => { - beforeEach(() => { - vi.resetAllMocks(); - }); - - it('should validate strategies and categorize them properly', async () => { - // Input data - const registryDotrains = [ - { name: 'valid.rain', dotrain: 'valid dotrain content' }, - { name: 'invalid.rain', dotrain: 'invalid dotrain content' }, - { name: 'another-valid.rain', dotrain: 'another valid content' } - ]; - - // Set up mock responses for the DotrainOrderGui - (DotrainOrderGui.getStrategyDetails as Mock) - .mockResolvedValueOnce({ - value: { name: 'Valid Strategy', description: 'A valid strategy' }, - error: null - }) - .mockResolvedValueOnce({ - error: { msg: 'Invalid syntax' }, - value: null - }) - .mockResolvedValueOnce({ - value: { name: 'Another Valid', description: 'Another valid strategy' }, - error: null - }); - - // Call the function with our test data - const result = await validateStrategies(registryDotrains); - - // Verify DotrainOrderGui was called correctly - expect(DotrainOrderGui.getStrategyDetails).toHaveBeenCalledTimes(3); - expect(DotrainOrderGui.getStrategyDetails).toHaveBeenCalledWith('valid dotrain content'); - expect(DotrainOrderGui.getStrategyDetails).toHaveBeenCalledWith('invalid dotrain content'); - expect(DotrainOrderGui.getStrategyDetails).toHaveBeenCalledWith('another valid content'); - - // Verify the valid strategies are processed correctly - expect(result.validStrategies).toHaveLength(2); - expect(result.validStrategies[0].name).toBe('valid.rain'); - expect(result.validStrategies[0].dotrain).toBe('valid dotrain content'); - expect(result.validStrategies[0].details).toEqual({ - name: 'Valid Strategy', - description: 'A valid strategy' - }); - - // Verify the invalid strategies are processed correctly - expect(result.invalidStrategies).toHaveLength(1); - expect(result.invalidStrategies[0].name).toBe('invalid.rain'); - expect(result.invalidStrategies[0].error).toBe('Invalid syntax'); - }); - - it('should handle exceptions thrown during strategy validation', async () => { - // Input data - const registryDotrains = [{ name: 'error.rain', dotrain: 'will throw error' }]; - - // Mock the DotrainOrderGui to throw an exception - (DotrainOrderGui.getStrategyDetails as Mock).mockRejectedValueOnce( - new Error('Unexpected parsing error') - ); - - // Call the function - const result = await validateStrategies(registryDotrains); - - // Verify results - expect(result.validStrategies).toHaveLength(0); - expect(result.invalidStrategies).toHaveLength(1); - expect(result.invalidStrategies[0].name).toBe('error.rain'); - expect(result.invalidStrategies[0].error).toBe('Unexpected parsing error'); - }); - - it('should handle non-Error objects being thrown', async () => { - // Input data - const registryDotrains = [{ name: 'string-error.rain', dotrain: 'will throw string' }]; - - // Mock the DotrainOrderGui to throw a string instead of an Error - (DotrainOrderGui.getStrategyDetails as Mock).mockRejectedValueOnce('String error message'); - - // Call the function - const result = await validateStrategies(registryDotrains); - - // Verify results - expect(result.validStrategies).toHaveLength(0); - expect(result.invalidStrategies).toHaveLength(1); - expect(result.invalidStrategies[0].name).toBe('string-error.rain'); - expect(result.invalidStrategies[0].error).toBe('String error message'); - }); - - it('should process an empty array of strategies', async () => { - const result = await validateStrategies([]); - - expect(result.validStrategies).toEqual([]); - expect(result.invalidStrategies).toEqual([]); - expect(DotrainOrderGui.getStrategyDetails).not.toHaveBeenCalled(); - }); - - it('should handle mixed validation results correctly', async () => { - // Create a mix of scenarios - const registryDotrains = [ - { name: 'valid1.rain', dotrain: 'valid content 1' }, - { name: 'error.rain', dotrain: 'will throw error' }, - { name: 'valid2.rain', dotrain: 'valid content 2' }, - { name: 'invalid.rain', dotrain: 'invalid content' } - ]; - - // Set up mock responses - (DotrainOrderGui.getStrategyDetails as Mock) - .mockResolvedValueOnce({ - value: { strategyName: 'Strategy 1', description: 'Description 1' }, - error: null - }) - .mockRejectedValueOnce(new Error('Processing error')) - .mockResolvedValueOnce({ - value: { strategyName: 'Strategy 2', description: 'Description 2' }, - error: null - }) - .mockResolvedValueOnce({ - error: { msg: 'Validation failed' }, - value: null - }); - - // Call the function - const result = await validateStrategies(registryDotrains); - - // Verify results - expect(result.validStrategies).toHaveLength(2); - expect(result.validStrategies[0].name).toBe('valid1.rain'); - expect(result.validStrategies[1].name).toBe('valid2.rain'); - - expect(result.invalidStrategies).toHaveLength(2); - expect(result.invalidStrategies[0].name).toBe('error.rain'); - expect(result.invalidStrategies[0].error).toBe('Processing error'); - expect(result.invalidStrategies[1].name).toBe('invalid.rain'); - expect(result.invalidStrategies[1].error).toBe('Validation failed'); - }); -}); \ No newline at end of file + beforeEach(() => { + vi.resetAllMocks(); + }); + + it('should validate strategies and categorize them properly', async () => { + // Input data + const registryDotrains = [ + { name: 'valid.rain', dotrain: 'valid dotrain content' }, + { name: 'invalid.rain', dotrain: 'invalid dotrain content' }, + { name: 'another-valid.rain', dotrain: 'another valid content' } + ]; + + // Set up mock responses for the DotrainOrderGui + (DotrainOrderGui.getStrategyDetails as Mock) + .mockResolvedValueOnce({ + value: { name: 'Valid Strategy', description: 'A valid strategy' }, + error: null + }) + .mockResolvedValueOnce({ + error: { msg: 'Invalid syntax' }, + value: null + }) + .mockResolvedValueOnce({ + value: { name: 'Another Valid', description: 'Another valid strategy' }, + error: null + }); + + // Call the function with our test data + const result = await validateStrategies(registryDotrains); + + // Verify DotrainOrderGui was called correctly + expect(DotrainOrderGui.getStrategyDetails).toHaveBeenCalledTimes(3); + expect(DotrainOrderGui.getStrategyDetails).toHaveBeenCalledWith('valid dotrain content'); + expect(DotrainOrderGui.getStrategyDetails).toHaveBeenCalledWith('invalid dotrain content'); + expect(DotrainOrderGui.getStrategyDetails).toHaveBeenCalledWith('another valid content'); + + // Verify the valid strategies are processed correctly + expect(result.validStrategies).toHaveLength(2); + expect(result.validStrategies[0].name).toBe('valid.rain'); + expect(result.validStrategies[0].dotrain).toBe('valid dotrain content'); + expect(result.validStrategies[0].details).toEqual({ + name: 'Valid Strategy', + description: 'A valid strategy' + }); + + // Verify the invalid strategies are processed correctly + expect(result.invalidStrategies).toHaveLength(1); + expect(result.invalidStrategies[0].name).toBe('invalid.rain'); + expect(result.invalidStrategies[0].error).toBe('Invalid syntax'); + }); + + it('should handle exceptions thrown during strategy validation', async () => { + // Input data + const registryDotrains = [{ name: 'error.rain', dotrain: 'will throw error' }]; + + // Mock the DotrainOrderGui to throw an exception + (DotrainOrderGui.getStrategyDetails as Mock).mockRejectedValueOnce( + new Error('Unexpected parsing error') + ); + + // Call the function + const result = await validateStrategies(registryDotrains); + + // Verify results + expect(result.validStrategies).toHaveLength(0); + expect(result.invalidStrategies).toHaveLength(1); + expect(result.invalidStrategies[0].name).toBe('error.rain'); + expect(result.invalidStrategies[0].error).toBe('Unexpected parsing error'); + }); + + it('should handle non-Error objects being thrown', async () => { + // Input data + const registryDotrains = [{ name: 'string-error.rain', dotrain: 'will throw string' }]; + + // Mock the DotrainOrderGui to throw a string instead of an Error + (DotrainOrderGui.getStrategyDetails as Mock).mockRejectedValueOnce('String error message'); + + // Call the function + const result = await validateStrategies(registryDotrains); + + // Verify results + expect(result.validStrategies).toHaveLength(0); + expect(result.invalidStrategies).toHaveLength(1); + expect(result.invalidStrategies[0].name).toBe('string-error.rain'); + expect(result.invalidStrategies[0].error).toBe('String error message'); + }); + + it('should process an empty array of strategies', async () => { + const result = await validateStrategies([]); + + expect(result.validStrategies).toEqual([]); + expect(result.invalidStrategies).toEqual([]); + expect(DotrainOrderGui.getStrategyDetails).not.toHaveBeenCalled(); + }); + + it('should handle mixed validation results correctly', async () => { + // Create a mix of scenarios + const registryDotrains = [ + { name: 'valid1.rain', dotrain: 'valid content 1' }, + { name: 'error.rain', dotrain: 'will throw error' }, + { name: 'valid2.rain', dotrain: 'valid content 2' }, + { name: 'invalid.rain', dotrain: 'invalid content' } + ]; + + // Set up mock responses + (DotrainOrderGui.getStrategyDetails as Mock) + .mockResolvedValueOnce({ + value: { strategyName: 'Strategy 1', description: 'Description 1' }, + error: null + }) + .mockRejectedValueOnce(new Error('Processing error')) + .mockResolvedValueOnce({ + value: { strategyName: 'Strategy 2', description: 'Description 2' }, + error: null + }) + .mockResolvedValueOnce({ + error: { msg: 'Validation failed' }, + value: null + }); + + // Call the function + const result = await validateStrategies(registryDotrains); + + // Verify results + expect(result.validStrategies).toHaveLength(2); + expect(result.validStrategies[0].name).toBe('valid1.rain'); + expect(result.validStrategies[1].name).toBe('valid2.rain'); + + expect(result.invalidStrategies).toHaveLength(2); + expect(result.invalidStrategies[0].name).toBe('error.rain'); + expect(result.invalidStrategies[0].error).toBe('Processing error'); + expect(result.invalidStrategies[1].name).toBe('invalid.rain'); + expect(result.invalidStrategies[1].error).toBe('Validation failed'); + }); +}); diff --git a/packages/ui-components/src/lib/services/registry.ts b/packages/ui-components/src/lib/services/registry.ts index 9c40fa5a4..a1a540c01 100644 --- a/packages/ui-components/src/lib/services/registry.ts +++ b/packages/ui-components/src/lib/services/registry.ts @@ -1,6 +1,5 @@ import type { InvalidStrategyDetail, ValidStrategyDetail } from '$lib/types/strategy'; import { DotrainOrderGui } from '@rainlanguage/orderbook'; -import type { Mock } from 'vitest'; export type RegistryFile = { name: string; @@ -116,4 +115,4 @@ export async function validateStrategies( .map((result) => result.data as InvalidStrategyDetail); return { validStrategies, invalidStrategies }; -} \ No newline at end of file +} diff --git a/packages/webapp/src/__tests__/handleGuiInitialization.test.ts b/packages/webapp/src/__tests__/handleGuiInitialization.test.ts index ad43d840b..951f17304 100644 --- a/packages/webapp/src/__tests__/handleGuiInitialization.test.ts +++ b/packages/webapp/src/__tests__/handleGuiInitialization.test.ts @@ -12,7 +12,6 @@ vi.mock('@rainlanguage/orderbook', () => { }; }); - describe('handleGuiInitialization', () => { let guiInstance: DotrainOrderGui; const mockDotrain = 'mockDotrain'; diff --git a/packages/webapp/src/routes/deploy/page.test.ts b/packages/webapp/src/routes/deploy/page.test.ts index bbad0b9c5..ffcb15530 100644 --- a/packages/webapp/src/routes/deploy/page.test.ts +++ b/packages/webapp/src/routes/deploy/page.test.ts @@ -113,7 +113,6 @@ describe('Page Component', () => { it('should display no strategies found when no strategies are available', async () => { mockPageStore.mockSetSubscribeValue({ - // @ts-ignore - Type mismatch is expected in test data: { error: null, validStrategies: [], From 636e102015ed625327daa945108192aa661b5627 Mon Sep 17 00:00:00 2001 From: Jamie Harding Date: Mon, 28 Apr 2025 17:11:45 +0200 Subject: [PATCH 08/24] remove log --- .../ui-components/src/lib/providers/registry/RegistryManager.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/ui-components/src/lib/providers/registry/RegistryManager.ts b/packages/ui-components/src/lib/providers/registry/RegistryManager.ts index 6f327cc82..ffbd7527d 100644 --- a/packages/ui-components/src/lib/providers/registry/RegistryManager.ts +++ b/packages/ui-components/src/lib/providers/registry/RegistryManager.ts @@ -72,7 +72,6 @@ export class RegistryManager { */ private getRegistryFromLocalStorage(): string | null { try { - console.log('getting from local storage'); return localStorage.getItem(RegistryManager.STORAGE_KEY); } catch (error) { throw new Error( From 652fa9d965dbb461fdf13a0e5bdf4abf4408cb77 Mon Sep 17 00:00:00 2001 From: Jamie Harding Date: Mon, 28 Apr 2025 17:28:15 +0200 Subject: [PATCH 09/24] revert modal --- .../src/__tests__/DepositOrWithdrawModal.test.ts | 14 +++++--------- .../webapp/src/__tests__/OrderRemoveModal.test.ts | 10 +++------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/packages/webapp/src/__tests__/DepositOrWithdrawModal.test.ts b/packages/webapp/src/__tests__/DepositOrWithdrawModal.test.ts index 1a2202c70..375e4e9af 100644 --- a/packages/webapp/src/__tests__/DepositOrWithdrawModal.test.ts +++ b/packages/webapp/src/__tests__/DepositOrWithdrawModal.test.ts @@ -16,15 +16,11 @@ const { mockAppKitModalStore, mockConnectedStore, mockWagmiConfigStore } = await () => import('../lib/__mocks__/stores') ); -vi.mock('@rainlanguage/orderbook', async (importOriginal) => { - const original = (await importOriginal()) as object; - return { - ...original, - getVaultDepositCalldata: vi.fn().mockResolvedValue({ to: '0x123', data: '0x456' }), - getVaultApprovalCalldata: vi.fn().mockResolvedValue({ to: '0x789', data: '0xabc' }), - getVaultWithdrawCalldata: vi.fn().mockResolvedValue({ to: '0xdef', data: '0xghi' }) - }; -}); +vi.mock('@rainlanguage/orderbook', () => ({ + getVaultDepositCalldata: vi.fn().mockResolvedValue({ to: '0x123', data: '0x456' }), + getVaultApprovalCalldata: vi.fn().mockResolvedValue({ to: '0x789', data: '0xabc' }), + getVaultWithdrawCalldata: vi.fn().mockResolvedValue({ to: '0xdef', data: '0xghi' }) +})); vi.mock('../lib/stores/wagmi', () => ({ appKitModal: mockAppKitModalStore, diff --git a/packages/webapp/src/__tests__/OrderRemoveModal.test.ts b/packages/webapp/src/__tests__/OrderRemoveModal.test.ts index 2397ba199..76c4954cb 100644 --- a/packages/webapp/src/__tests__/OrderRemoveModal.test.ts +++ b/packages/webapp/src/__tests__/OrderRemoveModal.test.ts @@ -4,13 +4,9 @@ import OrderRemoveModal from '$lib/components/OrderRemoveModal.svelte'; import { transactionStore } from '@rainlanguage/ui-components'; import type { OrderRemoveModalProps } from '@rainlanguage/ui-components'; -vi.mock('@rainlanguage/orderbook', async (importOriginal) => { - const original = (await importOriginal()) as object; - return { - ...original, - getRemoveOrderCalldata: vi.fn().mockResolvedValue('0x123') - }; -}); +vi.mock('@rainlanguage/orderbook', () => ({ + getRemoveOrderCalldata: vi.fn().mockResolvedValue('0x123') +})); vi.useFakeTimers(); From bc525e29e3471426f4061782dd40f85a983f3bb4 Mon Sep 17 00:00:00 2001 From: Jamie Harding Date: Mon, 28 Apr 2025 17:43:44 +0200 Subject: [PATCH 10/24] ai comments --- .../ui-components/src/__tests__/InputRegistryUrl.test.ts | 2 +- packages/webapp/src/routes/deploy/+layout.svelte | 9 +++++---- packages/webapp/src/routes/deploy/+layout.ts | 4 ++++ 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/ui-components/src/__tests__/InputRegistryUrl.test.ts b/packages/ui-components/src/__tests__/InputRegistryUrl.test.ts index c549cff54..56822bc16 100644 --- a/packages/ui-components/src/__tests__/InputRegistryUrl.test.ts +++ b/packages/ui-components/src/__tests__/InputRegistryUrl.test.ts @@ -13,7 +13,7 @@ vi.mock('$lib/services/loadRegistryUrl', () => ({ })); // Mock the useRegistry hook -vi.mock('../../providers/registry/useRegistry', () => ({ +vi.mock('$lib/providers/registry/useRegistry', () => ({ useRegistry: mockRegistryStore })); diff --git a/packages/webapp/src/routes/deploy/+layout.svelte b/packages/webapp/src/routes/deploy/+layout.svelte index 8193899b7..1a4b0e695 100644 --- a/packages/webapp/src/routes/deploy/+layout.svelte +++ b/packages/webapp/src/routes/deploy/+layout.svelte @@ -15,7 +15,8 @@ const registryManager = new RegistryManager(REGISTRY_URL); const registryManagerStore = writable(registryManager); - $: advancedMode = registryManager?.isCustomRegistry?.() ?? false; + $: advancedMode = registryManager.isCustomRegistry(); + $: isDeployPage = $page.url.pathname === '/deploy'; @@ -25,17 +26,17 @@
{#if $registryManagerStore.isCustomRegistry()} - {:else if $page.url.pathname === '/deploy'} + {:else if isDeployPage}
{/if} - {#if $page.url.pathname === '/deploy'} + {#if isDeployPage} (advancedMode = !advancedMode)}> Advanced mode {/if}
- {#if advancedMode && $page.url.pathname === '/deploy'} + {#if advancedMode && isDeployPage}
diff --git a/packages/webapp/src/routes/deploy/+layout.ts b/packages/webapp/src/routes/deploy/+layout.ts index efde4264f..4a9ef056b 100644 --- a/packages/webapp/src/routes/deploy/+layout.ts +++ b/packages/webapp/src/routes/deploy/+layout.ts @@ -5,6 +5,10 @@ import type { InvalidStrategyDetail, ValidStrategyDetail } from '@rainlanguage/u import { fetchRegistryDotrains, validateStrategies } from '@rainlanguage/ui-components/services'; import type { RegistryDotrain } from '@rainlanguage/ui-components/services'; +/** ++ * Type defining the structure of the load function's return value, ++ * including registry information and validation results. ++ */ type LoadResult = { registryFromUrl: string; registryDotrains: RegistryDotrain[]; From 7d5a61033b0ab6bb9a4fe6b29964a33b6339f170 Mon Sep 17 00:00:00 2001 From: Jamie Harding Date: Tue, 29 Apr 2025 11:40:16 +0200 Subject: [PATCH 11/24] move tests --- .../src/__tests__/InputRegistryUrl.test.ts | 11 +++++------ .../services => __tests__}/loadRegistryUrl.test.ts | 10 +++++----- .../src/{lib/services => __tests__}/registry.test.ts | 2 +- packages/ui-components/test-setup.ts | 6 +----- 4 files changed, 12 insertions(+), 17 deletions(-) rename packages/ui-components/src/{lib/services => __tests__}/loadRegistryUrl.test.ts (89%) rename packages/ui-components/src/{lib/services => __tests__}/registry.test.ts (99%) diff --git a/packages/ui-components/src/__tests__/InputRegistryUrl.test.ts b/packages/ui-components/src/__tests__/InputRegistryUrl.test.ts index 56822bc16..52293982b 100644 --- a/packages/ui-components/src/__tests__/InputRegistryUrl.test.ts +++ b/packages/ui-components/src/__tests__/InputRegistryUrl.test.ts @@ -7,20 +7,19 @@ import { loadRegistryUrl } from '$lib/services/loadRegistryUrl'; const { mockRegistryStore, initialRegistry } = await vi.hoisted( () => import('../lib/__mocks__/stores') ); -// Mock the loadRegistryUrl function -vi.mock('$lib/services/loadRegistryUrl', () => ({ + +vi.mock('../lib/services/loadRegistryUrl', () => ({ loadRegistryUrl: vi.fn() })); -// Mock the useRegistry hook -vi.mock('$lib/providers/registry/useRegistry', () => ({ - useRegistry: mockRegistryStore + +vi.mock('../lib/providers/registry/useRegistry', () => ({ + useRegistry: vi.fn().mockReturnValue(mockRegistryStore) })); describe('InputRegistryUrl', () => { beforeEach(() => { vi.clearAllMocks(); - // Make loadRegistryUrl return a resolved promise by default vi.mocked(loadRegistryUrl).mockResolvedValue(undefined); }); diff --git a/packages/ui-components/src/lib/services/loadRegistryUrl.test.ts b/packages/ui-components/src/__tests__/loadRegistryUrl.test.ts similarity index 89% rename from packages/ui-components/src/lib/services/loadRegistryUrl.test.ts rename to packages/ui-components/src/__tests__/loadRegistryUrl.test.ts index 841d7b3e8..675dfa344 100644 --- a/packages/ui-components/src/lib/services/loadRegistryUrl.test.ts +++ b/packages/ui-components/src/__tests__/loadRegistryUrl.test.ts @@ -1,12 +1,12 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import type { Mock } from 'vitest'; -import { loadRegistryUrl } from './loadRegistryUrl'; -import { fetchRegistryDotrains } from './registry'; -import { RegistryManager } from '../providers/registry/RegistryManager'; -import { initialRegistry } from '../__mocks__/stores'; +import { loadRegistryUrl } from '../lib/services/loadRegistryUrl'; +import { fetchRegistryDotrains } from '../lib/services/registry'; +import { RegistryManager } from '../lib/providers/registry/RegistryManager'; +import { initialRegistry } from '../lib/__mocks__/stores'; // Mock dependencies -vi.mock('./registry', () => ({ +vi.mock('../lib/services/registry', () => ({ fetchRegistryDotrains: vi.fn(), validateStrategies: vi.fn() })); diff --git a/packages/ui-components/src/lib/services/registry.test.ts b/packages/ui-components/src/__tests__/registry.test.ts similarity index 99% rename from packages/ui-components/src/lib/services/registry.test.ts rename to packages/ui-components/src/__tests__/registry.test.ts index da858bf02..41637e7ef 100644 --- a/packages/ui-components/src/lib/services/registry.test.ts +++ b/packages/ui-components/src/__tests__/registry.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { fetchParseRegistry, fetchRegistryDotrains, validateStrategies } from './registry'; +import { fetchParseRegistry, fetchRegistryDotrains, validateStrategies } from '../lib/services/registry'; import { DotrainOrderGui } from '@rainlanguage/orderbook'; import type { Mock } from 'vitest'; diff --git a/packages/ui-components/test-setup.ts b/packages/ui-components/test-setup.ts index 4e41efb1f..e12c9cf0c 100644 --- a/packages/ui-components/test-setup.ts +++ b/packages/ui-components/test-setup.ts @@ -1,14 +1,10 @@ import '@testing-library/jest-dom/vitest'; import { vi } from 'vitest'; -import { mockRegistryStore } from './src/lib/__mocks__/stores'; + vi.mock('codemirror-rainlang', () => ({ RainlangLR: vi.fn() })); -vi.mock('$lib/providers/registry/useRegistry', () => ({ - useRegistry: vi.fn().mockReturnValue(mockRegistryStore) -})); - vi.mock('$app/stores', async () => { const { readable, writable } = await import('svelte/store'); /** From 4b1c7fac7435682bdbf7c792a6a2725c5a2b2e00 Mon Sep 17 00:00:00 2001 From: Jamie Harding Date: Tue, 29 Apr 2025 11:56:16 +0200 Subject: [PATCH 12/24] formatted --- .../ui-components/src/__tests__/InputRegistryUrl.test.ts | 1 - packages/ui-components/src/__tests__/registry.test.ts | 6 +++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/ui-components/src/__tests__/InputRegistryUrl.test.ts b/packages/ui-components/src/__tests__/InputRegistryUrl.test.ts index 52293982b..e3f61dd16 100644 --- a/packages/ui-components/src/__tests__/InputRegistryUrl.test.ts +++ b/packages/ui-components/src/__tests__/InputRegistryUrl.test.ts @@ -12,7 +12,6 @@ vi.mock('../lib/services/loadRegistryUrl', () => ({ loadRegistryUrl: vi.fn() })); - vi.mock('../lib/providers/registry/useRegistry', () => ({ useRegistry: vi.fn().mockReturnValue(mockRegistryStore) })); diff --git a/packages/ui-components/src/__tests__/registry.test.ts b/packages/ui-components/src/__tests__/registry.test.ts index 41637e7ef..a49303b72 100644 --- a/packages/ui-components/src/__tests__/registry.test.ts +++ b/packages/ui-components/src/__tests__/registry.test.ts @@ -1,5 +1,9 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { fetchParseRegistry, fetchRegistryDotrains, validateStrategies } from '../lib/services/registry'; +import { + fetchParseRegistry, + fetchRegistryDotrains, + validateStrategies +} from '../lib/services/registry'; import { DotrainOrderGui } from '@rainlanguage/orderbook'; import type { Mock } from 'vitest'; From ca6d5da298b2ccbb766bd72ac97bd3b1a675156e Mon Sep 17 00:00:00 2001 From: Jamie Harding Date: Tue, 29 Apr 2025 12:02:46 +0200 Subject: [PATCH 13/24] remove unused --- packages/webapp/src/routes/deploy/+layout.ts | 146 ------------------- 1 file changed, 146 deletions(-) diff --git a/packages/webapp/src/routes/deploy/+layout.ts b/packages/webapp/src/routes/deploy/+layout.ts index 4a9ef056b..6831d6b58 100644 --- a/packages/webapp/src/routes/deploy/+layout.ts +++ b/packages/webapp/src/routes/deploy/+layout.ts @@ -1,6 +1,5 @@ import { REGISTRY_URL } from '$lib/constants'; import type { LayoutLoad } from './$types'; -import type { Mock } from 'vitest'; import type { InvalidStrategyDetail, ValidStrategyDetail } from '@rainlanguage/ui-components'; import { fetchRegistryDotrains, validateStrategies } from '@rainlanguage/ui-components/services'; import type { RegistryDotrain } from '@rainlanguage/ui-components/services'; @@ -42,148 +41,3 @@ export const load: LayoutLoad = async ({ url }) => { }; } }; - -if (import.meta.vitest) { - const { describe, it, expect } = import.meta.vitest; - const mockDotrains = ['dotrain1', 'dotrain2'] as unknown as RegistryDotrain[]; - const mockValidated = { - validStrategies: ['strategy1', 'strategy2'] as unknown as ValidStrategyDetail[], - invalidStrategies: ['invalidStrategy'] as unknown as InvalidStrategyDetail[] - }; - - vi.mock('@rainlanguage/ui-components/services', () => ({ - validateStrategies: vi.fn(), - fetchRegistryDotrains: vi.fn() - })); - - vi.mock('$lib/services/RegistryManager', () => ({ - default: { - isCustomRegistry: vi.fn(), - setToStorage: vi.fn(), - clearFromStorage: vi.fn() - } - })); - - describe('Layout load function', () => { - beforeEach(() => { - vi.resetAllMocks(); - }); - - const createUrlMock = (registryParam: string | null) => - ({ - url: { - searchParams: { - get: vi.fn().mockReturnValue(registryParam) - } - } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - }) as any; - - it('should load strategies from default registry URL when no registry param is provided', async () => { - (validateStrategies as Mock).mockResolvedValue(mockValidated); - (fetchRegistryDotrains as Mock).mockResolvedValue(mockDotrains); - - const result = await load(createUrlMock(null)); - - expect(fetchRegistryDotrains).toHaveBeenCalledWith(REGISTRY_URL); - - expect(validateStrategies).toHaveBeenCalledWith(mockDotrains); - - expect(result).toEqual({ - registryFromUrl: REGISTRY_URL, - registryDotrains: mockDotrains, - validStrategies: mockValidated.validStrategies, - invalidStrategies: mockValidated.invalidStrategies, - error: null - }); - }); - - it('should load strategies from custom registry URL when registry param is provided', async () => { - const customRegistry = 'https://custom.registry.url'; - (fetchRegistryDotrains as Mock).mockResolvedValue(mockDotrains); - (validateStrategies as Mock).mockResolvedValue(mockValidated); - - const result = await load(createUrlMock(customRegistry)); - - expect(result).toEqual({ - registryFromUrl: customRegistry, - registryDotrains: mockDotrains, - validStrategies: mockValidated.validStrategies, - invalidStrategies: mockValidated.invalidStrategies, - error: null - }); - }); - - it('should handle errors when fetchRegistryDotrains fails', async () => { - const errorMessage = 'Failed to fetch registry dotrains'; - (fetchRegistryDotrains as Mock).mockRejectedValue(new Error(errorMessage)); - const result = await load(createUrlMock(null)); - - expect(validateStrategies).not.toHaveBeenCalled(); - - expect(result).toEqual({ - registryFromUrl: REGISTRY_URL, - registryDotrains: [], - validStrategies: [], - invalidStrategies: [], - error: errorMessage - }); - - const typedResult = result as LoadResult; - if (typedResult.error) { - expect(typedResult.error).toBe(errorMessage); - } - }); - - it('should handle errors when validateStrategies fails', async () => { - const errorMessage = 'Failed to validate strategies'; - (validateStrategies as Mock).mockRejectedValue(new Error(errorMessage)); - const result = await load(createUrlMock(null)); - - expect(result).toEqual({ - registryFromUrl: REGISTRY_URL, - registryDotrains: [], - validStrategies: [], - invalidStrategies: [], - error: errorMessage - }); - }); - - it('should handle non-Error exceptions with an "Unknown error" message', async () => { - (fetchRegistryDotrains as Mock).mockRejectedValue('Not an error object'); - const result = await load(createUrlMock(null)); - - expect(result).toEqual({ - registryFromUrl: REGISTRY_URL, - registryDotrains: [], - validStrategies: [], - invalidStrategies: [], - error: 'Unknown error occurred' - }); - }); - - it('should handle when fetchRegistryDotrains and validateStrategies return empty arrays', async () => { - const emptyDotrains: RegistryDotrain[] = []; - const emptyValidated = { - validStrategies: [] as ValidStrategyDetail[], - invalidStrategies: [] as InvalidStrategyDetail[] - }; - - (fetchRegistryDotrains as Mock).mockResolvedValue(emptyDotrains); - (validateStrategies as Mock).mockResolvedValue(emptyValidated); - - const result = await load(createUrlMock(null)); - - expect(fetchRegistryDotrains).toHaveBeenCalledWith(REGISTRY_URL); - expect(validateStrategies).toHaveBeenCalledWith(emptyDotrains); - - expect(result).toEqual({ - registryFromUrl: REGISTRY_URL, - registryDotrains: emptyDotrains, - validStrategies: emptyValidated.validStrategies, - invalidStrategies: emptyValidated.invalidStrategies, - error: null - }); - }); - }); -} From c1f65ffd0775d1b68f260721024ee2ed125b8ccc Mon Sep 17 00:00:00 2001 From: Jamie Harding Date: Tue, 29 Apr 2025 12:06:44 +0200 Subject: [PATCH 14/24] add return type --- packages/webapp/src/routes/deploy/+layout.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webapp/src/routes/deploy/+layout.ts b/packages/webapp/src/routes/deploy/+layout.ts index 6831d6b58..73fe515d1 100644 --- a/packages/webapp/src/routes/deploy/+layout.ts +++ b/packages/webapp/src/routes/deploy/+layout.ts @@ -16,7 +16,7 @@ type LoadResult = { error: string | null; }; -export const load: LayoutLoad = async ({ url }) => { +export const load: LayoutLoad = async ({ url }) => { const registryFromUrl = url.searchParams.get('registry') || REGISTRY_URL; try { From 42dac7d89b086728b036f1f7a62b53471ca50758 Mon Sep 17 00:00:00 2001 From: Jamie Harding Date: Tue, 29 Apr 2025 12:30:20 +0200 Subject: [PATCH 15/24] fix test --- .../src/__tests__/InputRegistryUrl.test.ts | 22 +++++++++++++++---- .../src/__tests__/loadRegistryUrl.test.ts | 2 +- .../ui-components/src/lib/__mocks__/stores.ts | 18 +-------------- 3 files changed, 20 insertions(+), 22 deletions(-) diff --git a/packages/ui-components/src/__tests__/InputRegistryUrl.test.ts b/packages/ui-components/src/__tests__/InputRegistryUrl.test.ts index e3f61dd16..6819b0cab 100644 --- a/packages/ui-components/src/__tests__/InputRegistryUrl.test.ts +++ b/packages/ui-components/src/__tests__/InputRegistryUrl.test.ts @@ -3,10 +3,23 @@ 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'; - -const { mockRegistryStore, initialRegistry } = await vi.hoisted( - () => import('../lib/__mocks__/stores') -); +import type { RegistryManager } from '$lib/providers/registry/RegistryManager'; + +const { mockRegistryStore } = await vi.hoisted(() => import('../lib/__mocks__/stores')); +const mockDefaultRegistry = 'https://example.com/default-registry.json'; +let mockCurrentRegistry: string | null = mockDefaultRegistry; // Start with default + +export const initialRegistry: Partial = { + 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) +}; vi.mock('../lib/services/loadRegistryUrl', () => ({ loadRegistryUrl: vi.fn() @@ -18,6 +31,7 @@ vi.mock('../lib/providers/registry/useRegistry', () => ({ describe('InputRegistryUrl', () => { beforeEach(() => { + mockRegistryStore.mockSetSubscribeValue(initialRegistry as RegistryManager); vi.clearAllMocks(); vi.mocked(loadRegistryUrl).mockResolvedValue(undefined); }); diff --git a/packages/ui-components/src/__tests__/loadRegistryUrl.test.ts b/packages/ui-components/src/__tests__/loadRegistryUrl.test.ts index 675dfa344..3a4c3154a 100644 --- a/packages/ui-components/src/__tests__/loadRegistryUrl.test.ts +++ b/packages/ui-components/src/__tests__/loadRegistryUrl.test.ts @@ -3,7 +3,7 @@ import type { Mock } from 'vitest'; import { loadRegistryUrl } from '../lib/services/loadRegistryUrl'; import { fetchRegistryDotrains } from '../lib/services/registry'; import { RegistryManager } from '../lib/providers/registry/RegistryManager'; -import { initialRegistry } from '../lib/__mocks__/stores'; +import { initialRegistry } from './InputRegistryUrl.test'; // Mock dependencies vi.mock('../lib/services/registry', () => ({ diff --git a/packages/ui-components/src/lib/__mocks__/stores.ts b/packages/ui-components/src/lib/__mocks__/stores.ts index da47a5fd4..569b14d62 100644 --- a/packages/ui-components/src/lib/__mocks__/stores.ts +++ b/packages/ui-components/src/lib/__mocks__/stores.ts @@ -5,22 +5,6 @@ import settingsFixture from '../__fixtures__/settings-12-11-24.json'; import { type Config } from '@wagmi/core'; import { mockWeb3Config } from './mockWeb3Config'; import type { RegistryManager } from '../providers/registry/RegistryManager'; -import { vi } from 'vitest'; - -const mockDefaultRegistry = 'https://example.com/default-registry.json'; -let mockCurrentRegistry: string | null = mockDefaultRegistry; // Start with default - -export const initialRegistry: Partial = { - 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) -}; const initialPageState = { data: { @@ -55,7 +39,7 @@ const mockChainIdWritable = writable(0); const mockConnectedWritable = writable(true); const mockWagmiConfigWritable = writable(mockWeb3Config); const mockShowMyItemsOnlyWritable = writable(false); -const mockRegistryWritable = writable(initialRegistry as RegistryManager); +const mockRegistryWritable = writable(); export const mockSettingsStore = { subscribe: mockSettingsWritable.subscribe, From 80858cbb5b9ccf34a7d73b3793d8f0f6b70e5377 Mon Sep 17 00:00:00 2001 From: Jamie Harding Date: Tue, 29 Apr 2025 12:32:42 +0200 Subject: [PATCH 16/24] add warning --- .../lib/components/CustomRegistryWarning.svelte | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/webapp/src/lib/components/CustomRegistryWarning.svelte b/packages/webapp/src/lib/components/CustomRegistryWarning.svelte index d8ebea033..6e1191745 100644 --- a/packages/webapp/src/lib/components/CustomRegistryWarning.svelte +++ b/packages/webapp/src/lib/components/CustomRegistryWarning.svelte @@ -1,5 +1,11 @@ + +
You are using a custom strategies registry. - Use default. { + $registry.resetToDefault(); + }} + href="/deploy" + data-sveltekit-reload + class="font-semibold underline hover:text-blue-800 dark:hover:text-blue-900" > + Use default. +
From b66a092c32d7fe743ef258a9702af89b5f04ef5e Mon Sep 17 00:00:00 2001 From: Jamie Harding Date: Tue, 29 Apr 2025 12:34:02 +0200 Subject: [PATCH 17/24] reload on deploy click --- packages/webapp/src/lib/components/Sidebar.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/webapp/src/lib/components/Sidebar.svelte b/packages/webapp/src/lib/components/Sidebar.svelte index d1167a715..fad3a3ba3 100644 --- a/packages/webapp/src/lib/components/Sidebar.svelte +++ b/packages/webapp/src/lib/components/Sidebar.svelte @@ -65,7 +65,7 @@ {#if !sideBarHidden} (sideBarHidden = true)} /> {/if} @@ -82,7 +82,7 @@ > - + From cda4102066f3b0652ea78397de0b0f55f44f3c39 Mon Sep 17 00:00:00 2001 From: Jamie Harding Date: Tue, 29 Apr 2025 12:36:29 +0200 Subject: [PATCH 18/24] format --- packages/webapp/src/lib/components/Sidebar.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webapp/src/lib/components/Sidebar.svelte b/packages/webapp/src/lib/components/Sidebar.svelte index fad3a3ba3..580e8009e 100644 --- a/packages/webapp/src/lib/components/Sidebar.svelte +++ b/packages/webapp/src/lib/components/Sidebar.svelte @@ -65,7 +65,7 @@ {#if !sideBarHidden} (sideBarHidden = true)} /> {/if} From 112cc5c02c0be476ee0e7aa00e1718cb82b94208 Mon Sep 17 00:00:00 2001 From: Jamie Harding Date: Tue, 29 Apr 2025 12:59:39 +0200 Subject: [PATCH 19/24] ai comments --- .../src/__tests__/RegistryManager.test.ts | 178 ++++++++++++++++++ .../src/__tests__/loadRegistryUrl.test.ts | 11 +- 2 files changed, 184 insertions(+), 5 deletions(-) create mode 100644 packages/ui-components/src/__tests__/RegistryManager.test.ts diff --git a/packages/ui-components/src/__tests__/RegistryManager.test.ts b/packages/ui-components/src/__tests__/RegistryManager.test.ts new file mode 100644 index 000000000..8a4673220 --- /dev/null +++ b/packages/ui-components/src/__tests__/RegistryManager.test.ts @@ -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 = {}; + 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) => { + 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(); + }); +}); diff --git a/packages/ui-components/src/__tests__/loadRegistryUrl.test.ts b/packages/ui-components/src/__tests__/loadRegistryUrl.test.ts index 3a4c3154a..145be029f 100644 --- a/packages/ui-components/src/__tests__/loadRegistryUrl.test.ts +++ b/packages/ui-components/src/__tests__/loadRegistryUrl.test.ts @@ -14,11 +14,12 @@ vi.mock('../lib/services/registry', () => ({ describe('loadRegistryUrl', () => { beforeEach(() => { vi.resetAllMocks(); - // Reset window.location - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (global.window as any).location = undefined; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (global.window as any).location = { reload: vi.fn() }; + const originalLocation = window.location; + const mockLocation = { ...originalLocation, reload: vi.fn() }; + Object.defineProperty(window, 'location', { + writable: true, + value: mockLocation + }); }); it('should throw an error if no URL is provided', async () => { From 40665c8d2b8c3b0dc52eba199578cd67fc9c016f Mon Sep 17 00:00:00 2001 From: Jamie Harding Date: Tue, 29 Apr 2025 13:03:57 +0200 Subject: [PATCH 20/24] moced and tested --- .../src/__fixtures__/RegistryManager.ts | 17 +++++++++++++++++ .../src/__tests__/InputRegistryUrl.test.ts | 15 +-------------- .../src/__tests__/loadRegistryUrl.test.ts | 2 +- 3 files changed, 19 insertions(+), 15 deletions(-) create mode 100644 packages/ui-components/src/__fixtures__/RegistryManager.ts diff --git a/packages/ui-components/src/__fixtures__/RegistryManager.ts b/packages/ui-components/src/__fixtures__/RegistryManager.ts new file mode 100644 index 000000000..a7234308e --- /dev/null +++ b/packages/ui-components/src/__fixtures__/RegistryManager.ts @@ -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; // Start with default + +export const initialRegistry: Partial = { + 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) +}; diff --git a/packages/ui-components/src/__tests__/InputRegistryUrl.test.ts b/packages/ui-components/src/__tests__/InputRegistryUrl.test.ts index 6819b0cab..2b42c716d 100644 --- a/packages/ui-components/src/__tests__/InputRegistryUrl.test.ts +++ b/packages/ui-components/src/__tests__/InputRegistryUrl.test.ts @@ -4,22 +4,9 @@ import InputRegistryUrl from '../lib/components/input/InputRegistryUrl.svelte'; import userEvent from '@testing-library/user-event'; import { loadRegistryUrl } from '$lib/services/loadRegistryUrl'; import type { RegistryManager } from '$lib/providers/registry/RegistryManager'; +import { initialRegistry } from '../__fixtures__/RegistryManager'; const { mockRegistryStore } = await vi.hoisted(() => import('../lib/__mocks__/stores')); -const mockDefaultRegistry = 'https://example.com/default-registry.json'; -let mockCurrentRegistry: string | null = mockDefaultRegistry; // Start with default - -export const initialRegistry: Partial = { - 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) -}; vi.mock('../lib/services/loadRegistryUrl', () => ({ loadRegistryUrl: vi.fn() diff --git a/packages/ui-components/src/__tests__/loadRegistryUrl.test.ts b/packages/ui-components/src/__tests__/loadRegistryUrl.test.ts index 145be029f..cd157e3ad 100644 --- a/packages/ui-components/src/__tests__/loadRegistryUrl.test.ts +++ b/packages/ui-components/src/__tests__/loadRegistryUrl.test.ts @@ -3,7 +3,7 @@ import type { Mock } from 'vitest'; import { loadRegistryUrl } from '../lib/services/loadRegistryUrl'; import { fetchRegistryDotrains } from '../lib/services/registry'; import { RegistryManager } from '../lib/providers/registry/RegistryManager'; -import { initialRegistry } from './InputRegistryUrl.test'; +import { initialRegistry } from '../__fixtures__/RegistryManager'; // Mock dependencies vi.mock('../lib/services/registry', () => ({ From dc2ef5a708cf8ae8a43905d7dcb313c4c93ed7d9 Mon Sep 17 00:00:00 2001 From: Jamie Harding Date: Tue, 29 Apr 2025 13:04:06 +0200 Subject: [PATCH 21/24] rm comment --- packages/ui-components/src/__fixtures__/RegistryManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui-components/src/__fixtures__/RegistryManager.ts b/packages/ui-components/src/__fixtures__/RegistryManager.ts index a7234308e..c9e220f26 100644 --- a/packages/ui-components/src/__fixtures__/RegistryManager.ts +++ b/packages/ui-components/src/__fixtures__/RegistryManager.ts @@ -2,7 +2,7 @@ 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; // Start with default +let mockCurrentRegistry: string | null = mockDefaultRegistry; export const initialRegistry: Partial = { getCurrentRegistry: vi.fn(() => mockCurrentRegistry ?? mockDefaultRegistry), From e733de5be4f05d739b137944b1d995b58b79c100 Mon Sep 17 00:00:00 2001 From: Jamie Harding Date: Mon, 5 May 2025 15:57:19 +0200 Subject: [PATCH 22/24] Remove store, use RegistryManager directly --- .../src/__tests__/InputRegistryUrl.test.ts | 11 +++++------ .../ui-components/src/lib/__mocks__/stores.ts | 8 -------- .../deployment/DeploymentSteps.svelte | 2 +- .../components/input/InputRegistryUrl.svelte | 6 +++--- .../registry/RegistryProvider.svelte | 6 +++--- .../src/lib/providers/registry/context.ts | 19 ++++++------------- .../src/lib/providers/registry/useRegistry.ts | 6 +++--- .../ui-components/src/lib/types/registry.ts | 4 ---- .../components/CustomRegistryWarning.svelte | 2 +- .../webapp/src/routes/deploy/+layout.svelte | 7 +++---- 10 files changed, 25 insertions(+), 46 deletions(-) delete mode 100644 packages/ui-components/src/lib/types/registry.ts diff --git a/packages/ui-components/src/__tests__/InputRegistryUrl.test.ts b/packages/ui-components/src/__tests__/InputRegistryUrl.test.ts index 2b42c716d..7074f3a56 100644 --- a/packages/ui-components/src/__tests__/InputRegistryUrl.test.ts +++ b/packages/ui-components/src/__tests__/InputRegistryUrl.test.ts @@ -2,25 +2,24 @@ 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 type { RegistryManager } from '$lib/providers/registry/RegistryManager'; +import { loadRegistryUrl } from '../lib/services/loadRegistryUrl'; import { initialRegistry } from '../__fixtures__/RegistryManager'; - -const { mockRegistryStore } = await vi.hoisted(() => import('../lib/__mocks__/stores')); +import { useRegistry } from '$lib/providers/registry/useRegistry'; +import type { RegistryManager } from '$lib/providers/registry/RegistryManager'; vi.mock('../lib/services/loadRegistryUrl', () => ({ loadRegistryUrl: vi.fn() })); vi.mock('../lib/providers/registry/useRegistry', () => ({ - useRegistry: vi.fn().mockReturnValue(mockRegistryStore) + useRegistry: vi.fn() })); describe('InputRegistryUrl', () => { beforeEach(() => { - mockRegistryStore.mockSetSubscribeValue(initialRegistry as RegistryManager); vi.clearAllMocks(); vi.mocked(loadRegistryUrl).mockResolvedValue(undefined); + vi.mocked(useRegistry).mockReturnValue(initialRegistry as RegistryManager); }); it('should render input and button', () => { diff --git a/packages/ui-components/src/lib/__mocks__/stores.ts b/packages/ui-components/src/lib/__mocks__/stores.ts index 569b14d62..9ad1b6d95 100644 --- a/packages/ui-components/src/lib/__mocks__/stores.ts +++ b/packages/ui-components/src/lib/__mocks__/stores.ts @@ -4,7 +4,6 @@ import settingsFixture from '../__fixtures__/settings-12-11-24.json'; import { type Config } from '@wagmi/core'; import { mockWeb3Config } from './mockWeb3Config'; -import type { RegistryManager } from '../providers/registry/RegistryManager'; const initialPageState = { data: { @@ -39,7 +38,6 @@ const mockChainIdWritable = writable(0); const mockConnectedWritable = writable(true); const mockWagmiConfigWritable = writable(mockWeb3Config); const mockShowMyItemsOnlyWritable = writable(false); -const mockRegistryWritable = writable(); export const mockSettingsStore = { subscribe: mockSettingsWritable.subscribe, @@ -146,9 +144,3 @@ export const mockPageStore = { }, reset: () => mockPageWritable.set(initialPageState) }; - -export const mockRegistryStore = { - subscribe: mockRegistryWritable.subscribe, - set: mockRegistryWritable.set, - mockSetSubscribeValue: (value: RegistryManager): void => mockRegistryWritable.set(value) -}; diff --git a/packages/ui-components/src/lib/components/deployment/DeploymentSteps.svelte b/packages/ui-components/src/lib/components/deployment/DeploymentSteps.svelte index 441c5f7f0..fc6cc0d20 100644 --- a/packages/ui-components/src/lib/components/deployment/DeploymentSteps.svelte +++ b/packages/ui-components/src/lib/components/deployment/DeploymentSteps.svelte @@ -152,7 +152,7 @@ } async function _handleShareChoices() { - await handleShareChoices(gui, $registry.getCurrentRegistry()); + await handleShareChoices(gui, registry.getCurrentRegistry()); } async function onSelectTokenSelect() { diff --git a/packages/ui-components/src/lib/components/input/InputRegistryUrl.svelte b/packages/ui-components/src/lib/components/input/InputRegistryUrl.svelte index fb0d3c975..6917261e7 100644 --- a/packages/ui-components/src/lib/components/input/InputRegistryUrl.svelte +++ b/packages/ui-components/src/lib/components/input/InputRegistryUrl.svelte @@ -4,7 +4,7 @@ import { loadRegistryUrl } from '$lib/services/loadRegistryUrl'; const registry = useRegistry(); - let newRegistryUrl = $registry.getCurrentRegistry(); + let newRegistryUrl = registry.getCurrentRegistry(); let error: string | null = null; let loading: boolean = false; @@ -12,10 +12,10 @@ loading = true; error = null; try { - if (!$registry) { + if (!registry) { throw new Error('Registry manager not yet available.'); } - await loadRegistryUrl(newRegistryUrl, $registry); + await loadRegistryUrl(newRegistryUrl, registry); } catch (err) { error = err instanceof Error ? err.message : 'Unknown error'; } diff --git a/packages/ui-components/src/lib/providers/registry/RegistryProvider.svelte b/packages/ui-components/src/lib/providers/registry/RegistryProvider.svelte index 4bfab0bb6..21731228f 100644 --- a/packages/ui-components/src/lib/providers/registry/RegistryProvider.svelte +++ b/packages/ui-components/src/lib/providers/registry/RegistryProvider.svelte @@ -1,10 +1,10 @@ diff --git a/packages/ui-components/src/lib/providers/registry/context.ts b/packages/ui-components/src/lib/providers/registry/context.ts index 4ef17e795..053cc4d57 100644 --- a/packages/ui-components/src/lib/providers/registry/context.ts +++ b/packages/ui-components/src/lib/providers/registry/context.ts @@ -1,19 +1,12 @@ import { getContext, setContext } from 'svelte'; -import type { Readable } from 'svelte/store'; import type { RegistryManager } from './RegistryManager'; export const REGISTRY_KEY = 'registry_key'; - -/** - * Type for the registry store - */ -export type Registry = Readable; - /** - * Retrieves the registry manager store directly from Svelte's context + * Retrieves the registry manager directly from Svelte's context */ -export const getRegistryContext = (): Registry => { - const registry = getContext(REGISTRY_KEY); +export const getRegistryContext = (): RegistryManager => { + const registry = getContext(REGISTRY_KEY); if (!registry) { throw new Error( 'No registry manager was found in Svelte context. Did you forget to wrap your component with RegistryProvider?' @@ -23,9 +16,9 @@ export const getRegistryContext = (): Registry => { }; /** - * Sets the registry manager store in Svelte's context + * Sets the registry manager in Svelte's context */ -export const setRegistryContext = (registry: Registry) => { +export const setRegistryContext = (registry: RegistryManager) => { setContext(REGISTRY_KEY, registry); }; @@ -45,7 +38,7 @@ if (import.meta.vitest) { }); it('should return the registry from context when it exists', () => { - const mockRegistry = {} as Registry; + const mockRegistry = {} as RegistryManager; mockGetContext.mockImplementation((key) => { if (key === REGISTRY_KEY) return mockRegistry; diff --git a/packages/ui-components/src/lib/providers/registry/useRegistry.ts b/packages/ui-components/src/lib/providers/registry/useRegistry.ts index 826fac2f7..1e3988480 100644 --- a/packages/ui-components/src/lib/providers/registry/useRegistry.ts +++ b/packages/ui-components/src/lib/providers/registry/useRegistry.ts @@ -1,10 +1,10 @@ import { getRegistryContext } from './context'; -import type { RegistryStore } from '$lib/types/registry'; +import type { RegistryManager } from './RegistryManager'; /** * Hook to access registry manager information from context * Must be used within a component that is a child of RegistryProvider - * @returns An object containing the registry manager store + * @returns An object containing the registry manager */ export function useRegistry() { const registry = getRegistryContext(); @@ -26,7 +26,7 @@ if (import.meta.vitest) { }); it('should return registry', () => { - const mockRegistry = {} as RegistryStore; + const mockRegistry = {} as RegistryManager; mockGetRegistryContext.mockReturnValue(mockRegistry); const result = useRegistry(); diff --git a/packages/ui-components/src/lib/types/registry.ts b/packages/ui-components/src/lib/types/registry.ts deleted file mode 100644 index f98cd0c6f..000000000 --- a/packages/ui-components/src/lib/types/registry.ts +++ /dev/null @@ -1,4 +0,0 @@ -import type { Readable } from 'svelte/store'; -import type { RegistryManager } from '../providers/registry/RegistryManager'; - -export type RegistryStore = Readable; diff --git a/packages/webapp/src/lib/components/CustomRegistryWarning.svelte b/packages/webapp/src/lib/components/CustomRegistryWarning.svelte index 6e1191745..d43f735fe 100644 --- a/packages/webapp/src/lib/components/CustomRegistryWarning.svelte +++ b/packages/webapp/src/lib/components/CustomRegistryWarning.svelte @@ -23,7 +23,7 @@ You are using a custom strategies registry. { - $registry.resetToDefault(); + registry.resetToDefault(); }} href="/deploy" data-sveltekit-reload diff --git a/packages/webapp/src/routes/deploy/+layout.svelte b/packages/webapp/src/routes/deploy/+layout.svelte index 1a4b0e695..0a614d6a8 100644 --- a/packages/webapp/src/routes/deploy/+layout.svelte +++ b/packages/webapp/src/routes/deploy/+layout.svelte @@ -14,17 +14,16 @@ let advancedMode = false; const registryManager = new RegistryManager(REGISTRY_URL); - const registryManagerStore = writable(registryManager); $: advancedMode = registryManager.isCustomRegistry(); $: isDeployPage = $page.url.pathname === '/deploy'; - - {#if $registryManagerStore} + + {#if registryManager}
- {#if $registryManagerStore.isCustomRegistry()} + {#if registryManager.isCustomRegistry()} {:else if isDeployPage}
From fdc4c4c3278242068a0edb5da5507b45a348c84b Mon Sep 17 00:00:00 2001 From: Jamie Harding Date: Mon, 5 May 2025 15:57:38 +0200 Subject: [PATCH 23/24] remove unused --- packages/webapp/src/routes/deploy/+layout.svelte | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/webapp/src/routes/deploy/+layout.svelte b/packages/webapp/src/routes/deploy/+layout.svelte index 0a614d6a8..bf04a00df 100644 --- a/packages/webapp/src/routes/deploy/+layout.svelte +++ b/packages/webapp/src/routes/deploy/+layout.svelte @@ -8,7 +8,6 @@ } from '@rainlanguage/ui-components'; import { Toggle } from 'flowbite-svelte'; import { page } from '$app/stores'; - import { writable } from 'svelte/store'; import { REGISTRY_URL } from '$lib/constants'; import { slide } from 'svelte/transition'; let advancedMode = false; From b852aa2ad940e984dbfc72f8939bdc50e8dc436e Mon Sep 17 00:00:00 2001 From: Jamie Harding Date: Wed, 7 May 2025 19:53:10 +0200 Subject: [PATCH 24/24] address AI comments --- .../webapp/src/routes/deploy/+layout.svelte | 46 +++++++++---------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/packages/webapp/src/routes/deploy/+layout.svelte b/packages/webapp/src/routes/deploy/+layout.svelte index bf04a00df..6354444b6 100644 --- a/packages/webapp/src/routes/deploy/+layout.svelte +++ b/packages/webapp/src/routes/deploy/+layout.svelte @@ -18,29 +18,27 @@ - {#if registryManager} - -
-
- {#if registryManager.isCustomRegistry()} - - {:else if isDeployPage} -
- {/if} - {#if isDeployPage} - (advancedMode = !advancedMode)}> - Advanced mode - - {/if} -
-
- {#if advancedMode && isDeployPage} -
- -
- {/if} -
+ +
+
+ {#if advancedMode} + + {:else if isDeployPage} +
+ {/if} + {#if isDeployPage} + (advancedMode = !advancedMode)}> + Advanced mode + + {/if}
- - {/if} +
+ {#if advancedMode && isDeployPage} +
+ +
+ {/if} +
+
+