diff --git a/package-lock.json b/package-lock.json index b7b44e132..f7172544f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,6 +47,7 @@ "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.1", "eslint-plugin-svelte": "^2.36.0", + "fast-check": "^3.23.2", "globals": "^15.0.0", "jsdom": "^24.0.0", "lodash": "^4.17.21", @@ -10292,6 +10293,29 @@ "integrity": "sha512-nifxXJ95JNLFR2NgRV4/MxVP45G9909wJTEKz5fg/TZS20JJZA6hfgRVh/bC9bwl2zBtBNcYPjiBE4njQHVBwQ==", "license": "MIT" }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -17101,6 +17125,23 @@ "node": ">=6" } }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, "node_modules/qrcode": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.3.tgz", diff --git a/package.json b/package.json index b7f7b38a2..d08eb2274 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.1", "eslint-plugin-svelte": "^2.36.0", + "fast-check": "^3.23.2", "globals": "^15.0.0", "jsdom": "^24.0.0", "lodash": "^4.17.21", diff --git a/packages/ui-components/src/__tests__/handleShareChoices.test.ts b/packages/ui-components/src/__tests__/handleShareChoices.test.ts index 98b4343ef..e48ced893 100644 --- a/packages/ui-components/src/__tests__/handleShareChoices.test.ts +++ b/packages/ui-components/src/__tests__/handleShareChoices.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { handleShareChoices } from '../lib/services/handleShareChoices'; import type { DotrainOrderGui } from '@rainlanguage/orderbook/js_api'; +import fc from 'fast-check'; describe('handleShareChoices', () => { beforeEach(() => { @@ -50,3 +51,42 @@ describe('handleShareChoices', () => { expect(navigator.clipboard.writeText).toHaveBeenCalledWith('http://example.com/?state='); }); }); + +describe('property-based tests', () => { + beforeEach(() => { + Object.assign(navigator, { + clipboard: { + writeText: vi.fn() + } + }); + + vi.mock('$app/stores', () => ({ + page: { + subscribe: vi.fn((fn) => { + fn({ url: new URL('http://example.com') }); + return () => {}; + }) + } + })); + }); + + it('should always create valid URLs with any state string', async () => { + await fc.assert( + fc.asyncProperty(fc.string(), async (state) => { + const mockGui = { + serializeState: vi.fn().mockReturnValue(state) + }; + + await handleShareChoices(mockGui as unknown as DotrainOrderGui); + + const clipboardText = (navigator.clipboard.writeText as any).mock.calls[0][0]; + const url = new URL(clipboardText); + + // Property: URL should always be valid and contain the state parameter + expect(url.searchParams.has('state')).toBe(true); + // Compare with the encoded state value + expect(url.searchParams.get('state')).toBe(encodeURIComponent(state)); + }) + ); + }); +});