From f75f7964c5971bc555785106bb8064e3db5b6b18 Mon Sep 17 00:00:00 2001 From: highonhopium Date: Fri, 14 Feb 2025 13:46:29 +0000 Subject: [PATCH 1/3] share button fix --- .../src/lib/components/deployment/DeploymentSteps.svelte | 8 ++++++-- packages/webapp/src/lib/services/handleUpdateGuiState.ts | 3 ++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/ui-components/src/lib/components/deployment/DeploymentSteps.svelte b/packages/ui-components/src/lib/components/deployment/DeploymentSteps.svelte index 6cdaf3f66..984aed812 100644 --- a/packages/ui-components/src/lib/components/deployment/DeploymentSteps.svelte +++ b/packages/ui-components/src/lib/components/deployment/DeploymentSteps.svelte @@ -190,8 +190,12 @@ } async function handleShareChoices() { - // copy the current url to the clipboard - navigator.clipboard.writeText($page.url.toString()); + // get the current url + const url = $page.url; + // get the current state + const state = gui?.serializeState(); + url.searchParams.set('state', state || ''); + navigator.clipboard.writeText(url.toString()); } onMount(async () => { diff --git a/packages/webapp/src/lib/services/handleUpdateGuiState.ts b/packages/webapp/src/lib/services/handleUpdateGuiState.ts index 2ddd071a9..369d0aeed 100644 --- a/packages/webapp/src/lib/services/handleUpdateGuiState.ts +++ b/packages/webapp/src/lib/services/handleUpdateGuiState.ts @@ -1,3 +1,4 @@ +import { pushState } from '$app/navigation'; import type { DotrainOrderGui } from '@rainlanguage/orderbook/js_api'; import { debounce } from 'lodash'; @@ -8,6 +9,6 @@ export function handleUpdateGuiState(gui: DotrainOrderGui) { const pushGuiStateToUrlHistory = debounce((gui: DotrainOrderGui) => { const serializedState = gui.serializeState(); if (serializedState) { - window.history.pushState({}, '', `?state=${serializedState}`); + pushState(`?state=${serializedState}`, { serializedState }); } }, 1000); From bf3c5cd48099ecca7b183879c89c03554491132e Mon Sep 17 00:00:00 2001 From: highonhopium Date: Fri, 14 Feb 2025 13:56:39 +0000 Subject: [PATCH 2/3] tests --- .../deployment/DeploymentSteps.svelte | 14 ++-- .../src/lib/services/handleShareChoices.ts | 66 ++++++++++++++++++ .../src/lib/services/handleUpdateGuiState.ts | 68 +++++++++++++++++++ 3 files changed, 139 insertions(+), 9 deletions(-) create mode 100644 packages/ui-components/src/lib/services/handleShareChoices.ts diff --git a/packages/ui-components/src/lib/components/deployment/DeploymentSteps.svelte b/packages/ui-components/src/lib/components/deployment/DeploymentSteps.svelte index 984aed812..261cdbcfc 100644 --- a/packages/ui-components/src/lib/components/deployment/DeploymentSteps.svelte +++ b/packages/ui-components/src/lib/components/deployment/DeploymentSteps.svelte @@ -24,7 +24,7 @@ import { page } from '$app/stores'; import { onMount } from 'svelte'; import ShareChoicesButton from './ShareChoicesButton.svelte'; - + import { handleShareChoices } from '$lib/services/handleShareChoices'; enum DeploymentStepErrors { NO_GUI = 'Error loading GUI', NO_STRATEGY = 'No valid strategy exists at this URL', @@ -189,13 +189,9 @@ } } - async function handleShareChoices() { - // get the current url - const url = $page.url; - // get the current state - const state = gui?.serializeState(); - url.searchParams.set('state', state || ''); - navigator.clipboard.writeText(url.toString()); + async function _handleShareChoices() { + if (!gui) return; + await handleShareChoices(gui); } onMount(async () => { @@ -287,7 +283,7 @@ {:else} {/if} - +
{#if error} diff --git a/packages/ui-components/src/lib/services/handleShareChoices.ts b/packages/ui-components/src/lib/services/handleShareChoices.ts new file mode 100644 index 000000000..3cd8315b6 --- /dev/null +++ b/packages/ui-components/src/lib/services/handleShareChoices.ts @@ -0,0 +1,66 @@ +import type { DotrainOrderGui } from '@rainlanguage/orderbook/js_api'; +import { page } from '$app/stores'; +import { get } from 'svelte/store'; + +export async function handleShareChoices(gui: DotrainOrderGui) { + // get the current url + const url = get(page).url; + // get the current state + const state = gui?.serializeState(); + url.searchParams.set('state', state || ''); + navigator.clipboard.writeText(url.toString()); +} + +// tests +if (import.meta.vitest) { + const { describe, it, expect, vi } = import.meta.vitest; + + describe('handleShareChoices', () => { + beforeEach(() => { + // Mock clipboard API + Object.assign(navigator, { + clipboard: { + writeText: vi.fn() + } + }); + + // Mock Svelte's page store + vi.mock('$app/stores', () => ({ + page: { + subscribe: vi.fn((fn) => { + fn({ url: new URL('http://example.com') }); + return () => {}; + }) + } + })); + }); + + it('should share the choices with state', async () => { + const mockGui = { + serializeState: vi.fn().mockReturnValue('mockState123') + }; + + await handleShareChoices(mockGui as unknown as DotrainOrderGui); + + expect(navigator.clipboard.writeText).toHaveBeenCalledWith( + 'http://example.com/?state=mockState123' + ); + }); + + it('should handle null state', async () => { + const mockGui = { + serializeState: vi.fn().mockReturnValue(null) + }; + + await handleShareChoices(mockGui as unknown as DotrainOrderGui); + + expect(navigator.clipboard.writeText).toHaveBeenCalledWith('http://example.com/?state='); + }); + + it('should handle undefined gui', async () => { + await handleShareChoices(undefined as unknown as DotrainOrderGui); + + expect(navigator.clipboard.writeText).toHaveBeenCalledWith('http://example.com/?state='); + }); + }); +} diff --git a/packages/webapp/src/lib/services/handleUpdateGuiState.ts b/packages/webapp/src/lib/services/handleUpdateGuiState.ts index 369d0aeed..ef558faf5 100644 --- a/packages/webapp/src/lib/services/handleUpdateGuiState.ts +++ b/packages/webapp/src/lib/services/handleUpdateGuiState.ts @@ -12,3 +12,71 @@ const pushGuiStateToUrlHistory = debounce((gui: DotrainOrderGui) => { pushState(`?state=${serializedState}`, { serializedState }); } }, 1000); + +if (import.meta.vitest) { + const { describe, it, expect, vi } = import.meta.vitest; + + // Mock pushState + vi.mock('$app/navigation', () => ({ + pushState: vi.fn() + })); + + describe('handleUpdateGuiState', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it('should push state to URL history when serializedState exists', async () => { + const mockSerializedState = 'mockSerializedState123'; + const mockGui = { + serializeState: vi.fn().mockReturnValue(mockSerializedState) + } as unknown as DotrainOrderGui; + + handleUpdateGuiState(mockGui); + + // Fast-forward timers to trigger debounced function + await vi.advanceTimersByTimeAsync(1000); + + expect(pushState).toHaveBeenCalledWith(`?state=${mockSerializedState}`, { + serializedState: mockSerializedState + }); + }); + + it('should not push state when serializedState is falsy', async () => { + const mockGui = { + serializeState: vi.fn().mockReturnValue(null) + } as unknown as DotrainOrderGui; + + handleUpdateGuiState(mockGui); + + await vi.advanceTimersByTimeAsync(1000); + + expect(pushState).not.toHaveBeenCalled(); + }); + + it('should debounce multiple calls', async () => { + const mockSerializedState = 'mockSerializedState123'; + const mockGui = { + serializeState: vi.fn().mockReturnValue(mockSerializedState) + } as unknown as DotrainOrderGui; + + // Call multiple times in quick succession + handleUpdateGuiState(mockGui); + handleUpdateGuiState(mockGui); + handleUpdateGuiState(mockGui); + + await vi.advanceTimersByTimeAsync(1000); + + // Should only be called once due to debouncing + expect(pushState).toHaveBeenCalledTimes(1); + expect(pushState).toHaveBeenCalledWith(`?state=${mockSerializedState}`, { + serializedState: mockSerializedState + }); + }); + }); +} From f84439f937f9ad280817041fd5386241fad8da8a Mon Sep 17 00:00:00 2001 From: highonhopium Date: Fri, 14 Feb 2025 16:34:06 +0000 Subject: [PATCH 3/3] moving tests --- .../src/__tests__/handleShareChoices.test.ts | 52 ++++++++++++++++++ .../src/lib/services/handleShareChoices.ts | 54 ------------------- 2 files changed, 52 insertions(+), 54 deletions(-) create mode 100644 packages/ui-components/src/__tests__/handleShareChoices.test.ts diff --git a/packages/ui-components/src/__tests__/handleShareChoices.test.ts b/packages/ui-components/src/__tests__/handleShareChoices.test.ts new file mode 100644 index 000000000..98b4343ef --- /dev/null +++ b/packages/ui-components/src/__tests__/handleShareChoices.test.ts @@ -0,0 +1,52 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { handleShareChoices } from '../lib/services/handleShareChoices'; +import type { DotrainOrderGui } from '@rainlanguage/orderbook/js_api'; + +describe('handleShareChoices', () => { + beforeEach(() => { + // Mock clipboard API + Object.assign(navigator, { + clipboard: { + writeText: vi.fn() + } + }); + + // Mock Svelte's page store + vi.mock('$app/stores', () => ({ + page: { + subscribe: vi.fn((fn) => { + fn({ url: new URL('http://example.com') }); + return () => {}; + }) + } + })); + }); + + it('should share the choices with state', async () => { + const mockGui = { + serializeState: vi.fn().mockReturnValue('mockState123') + }; + + await handleShareChoices(mockGui as unknown as DotrainOrderGui); + + expect(navigator.clipboard.writeText).toHaveBeenCalledWith( + 'http://example.com/?state=mockState123' + ); + }); + + it('should handle null state', async () => { + const mockGui = { + serializeState: vi.fn().mockReturnValue(null) + }; + + await handleShareChoices(mockGui as unknown as DotrainOrderGui); + + expect(navigator.clipboard.writeText).toHaveBeenCalledWith('http://example.com/?state='); + }); + + it('should handle undefined gui', async () => { + await handleShareChoices(undefined as unknown as DotrainOrderGui); + + expect(navigator.clipboard.writeText).toHaveBeenCalledWith('http://example.com/?state='); + }); +}); diff --git a/packages/ui-components/src/lib/services/handleShareChoices.ts b/packages/ui-components/src/lib/services/handleShareChoices.ts index 3cd8315b6..de44105ea 100644 --- a/packages/ui-components/src/lib/services/handleShareChoices.ts +++ b/packages/ui-components/src/lib/services/handleShareChoices.ts @@ -10,57 +10,3 @@ export async function handleShareChoices(gui: DotrainOrderGui) { url.searchParams.set('state', state || ''); navigator.clipboard.writeText(url.toString()); } - -// tests -if (import.meta.vitest) { - const { describe, it, expect, vi } = import.meta.vitest; - - describe('handleShareChoices', () => { - beforeEach(() => { - // Mock clipboard API - Object.assign(navigator, { - clipboard: { - writeText: vi.fn() - } - }); - - // Mock Svelte's page store - vi.mock('$app/stores', () => ({ - page: { - subscribe: vi.fn((fn) => { - fn({ url: new URL('http://example.com') }); - return () => {}; - }) - } - })); - }); - - it('should share the choices with state', async () => { - const mockGui = { - serializeState: vi.fn().mockReturnValue('mockState123') - }; - - await handleShareChoices(mockGui as unknown as DotrainOrderGui); - - expect(navigator.clipboard.writeText).toHaveBeenCalledWith( - 'http://example.com/?state=mockState123' - ); - }); - - it('should handle null state', async () => { - const mockGui = { - serializeState: vi.fn().mockReturnValue(null) - }; - - await handleShareChoices(mockGui as unknown as DotrainOrderGui); - - expect(navigator.clipboard.writeText).toHaveBeenCalledWith('http://example.com/?state='); - }); - - it('should handle undefined gui', async () => { - await handleShareChoices(undefined as unknown as DotrainOrderGui); - - expect(navigator.clipboard.writeText).toHaveBeenCalledWith('http://example.com/?state='); - }); - }); -}