From 7042f8f6436621a0cfa2f58afd7f6602d6332a3c Mon Sep 17 00:00:00 2001 From: Grace Nakiyemba Date: Tue, 27 Oct 2026 14:13:46 +0300 Subject: [PATCH 1/8] fix(formplayer): match gps renderer by format --- .../src/renderers/GPSQuestionRenderer.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/formulus-formplayer/src/renderers/GPSQuestionRenderer.tsx b/formulus-formplayer/src/renderers/GPSQuestionRenderer.tsx index 79093350b..1893b6ba7 100644 --- a/formulus-formplayer/src/renderers/GPSQuestionRenderer.tsx +++ b/formulus-formplayer/src/renderers/GPSQuestionRenderer.tsx @@ -1,5 +1,5 @@ import React, { useState, useMemo, useRef, useCallback } from 'react'; -import { rankWith, ControlProps, formatIs } from '@jsonforms/core'; +import { rankWith, ControlProps, schemaMatches } from '@jsonforms/core'; import { withJsonFormsControlProps } from '@jsonforms/react'; import { Typography, @@ -341,10 +341,11 @@ const GPSQuestionRenderer: React.FC = props => { ); }; -// Tester function to determine when this renderer should be used +// Match on format alone so both documented object schemas and legacy +// string schemas use the GPS renderer. export const gpsQuestionTester = rankWith( - 10, // Priority - higher than default string renderer - formatIs('gps'), + 10, + schemaMatches(schema => schema.format === 'gps'), ); export default withJsonFormsControlProps(GPSQuestionRenderer); From b46e4e135fafdf92c11f802f0625b119fd8e8852 Mon Sep 17 00:00:00 2001 From: Grace Nakiyemba Date: Tue, 27 Oct 2026 14:13:46 +0300 Subject: [PATCH 2/8] test(formplayer): cover gps object schema selection --- .../renderers/GPSQuestionRenderer.test.tsx | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 formulus-formplayer/src/renderers/GPSQuestionRenderer.test.tsx diff --git a/formulus-formplayer/src/renderers/GPSQuestionRenderer.test.tsx b/formulus-formplayer/src/renderers/GPSQuestionRenderer.test.tsx new file mode 100644 index 000000000..f6e66298d --- /dev/null +++ b/formulus-formplayer/src/renderers/GPSQuestionRenderer.test.tsx @@ -0,0 +1,31 @@ +// @vitest-environment jsdom +import { describe, expect, it } from 'vitest'; +import { gpsQuestionTester } from './GPSQuestionRenderer'; + +describe('gpsQuestionTester', () => { + it('matches an object schema with gps format', () => { + const rootSchema = { + type: 'object', + properties: { + location: { + type: 'object', + format: 'gps', + title: 'Current Location', + properties: { + latitude: { type: 'number' }, + longitude: { type: 'number' }, + }, + }, + }, + }; + + const uischema = { + type: 'Control', + scope: '#/properties/location', + }; + + expect( + gpsQuestionTester(uischema as any, rootSchema as any, {} as any), + ).toBe(10); + }); +}); From e30e5108c846c0a655194d7c7de5a7872e25e36d Mon Sep 17 00:00:00 2001 From: Grace Nakiyemba Date: Tue, 27 Oct 2026 14:17:19 +0300 Subject: [PATCH 3/8] test(formplayer): cover gps object observation values --- .../renderers/GPSQuestionRenderer.test.tsx | 66 ++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/formulus-formplayer/src/renderers/GPSQuestionRenderer.test.tsx b/formulus-formplayer/src/renderers/GPSQuestionRenderer.test.tsx index f6e66298d..693c79342 100644 --- a/formulus-formplayer/src/renderers/GPSQuestionRenderer.test.tsx +++ b/formulus-formplayer/src/renderers/GPSQuestionRenderer.test.tsx @@ -1,6 +1,51 @@ // @vitest-environment jsdom +import React from 'react'; import { describe, expect, it } from 'vitest'; -import { gpsQuestionTester } from './GPSQuestionRenderer'; +import { render, screen } from '@testing-library/react'; +import { JsonForms } from '@jsonforms/react'; +import Ajv from 'ajv'; +import GPSQuestionRenderer, { gpsQuestionTester } from './GPSQuestionRenderer'; + +const ajv = new Ajv({ strict: false }); +ajv.addFormat('gps', () => true); + +const objectGpsSchema = { + type: 'object', + properties: { + location: { + type: 'object', + format: 'gps', + title: 'Current Location', + properties: { + latitude: { type: 'number' }, + longitude: { type: 'number' }, + accuracy: { type: 'number' }, + timestamp: { type: 'string' }, + }, + }, + }, +}; + +const gpsUischema = { + type: 'Control', + scope: '#/properties/location', +}; + +const renderers = [ + { tester: gpsQuestionTester, renderer: GPSQuestionRenderer }, +]; + +function ObjectGpsForm({ value }: { value: Record }) { + return ( + + ); +} describe('gpsQuestionTester', () => { it('matches an object schema with gps format', () => { @@ -29,3 +74,22 @@ describe('gpsQuestionTester', () => { ).toBe(10); }); }); + +describe('GPS object observation values', () => { + it('displays an existing object GPS value', () => { + render( + , + ); + + expect(screen.getByText('Location Captured')).toBeInTheDocument(); + expect(screen.getByText('0.347596° N')).toBeInTheDocument(); + expect(screen.getByText('32.582520° E')).toBeInTheDocument(); + }); +}); From cf9b5f6e48bc4098b469848bc3f43636f28dc972 Mon Sep 17 00:00:00 2001 From: Grace Nakiyemba Date: Tue, 27 Oct 2026 14:17:19 +0300 Subject: [PATCH 4/8] refactor(formplayer): parse object and legacy gps values --- .../src/renderers/GPSQuestionRenderer.tsx | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/formulus-formplayer/src/renderers/GPSQuestionRenderer.tsx b/formulus-formplayer/src/renderers/GPSQuestionRenderer.tsx index 1893b6ba7..c820163d6 100644 --- a/formulus-formplayer/src/renderers/GPSQuestionRenderer.tsx +++ b/formulus-formplayer/src/renderers/GPSQuestionRenderer.tsx @@ -44,21 +44,31 @@ interface LocationDisplayData { } function parseLocationDisplayData(data: unknown): LocationDisplayData | null { - if (!data || typeof data !== 'string') { + if (!data) { return null; } - try { - const parsed = JSON.parse(data); - if ( - parsed && - typeof parsed.latitude === 'number' && - typeof parsed.longitude === 'number' - ) { - return parsed as LocationDisplayData; - } - } catch (e) { - console.warn('Failed to parse existing location data:', e); + + const parsed = + typeof data === 'string' + ? (() => { + try { + return JSON.parse(data); + } catch (e) { + console.warn('Failed to parse existing location data:', e); + return null; + } + })() + : data; + + if ( + parsed && + typeof parsed === 'object' && + typeof (parsed as LocationDisplayData).latitude === 'number' && + typeof (parsed as LocationDisplayData).longitude === 'number' + ) { + return parsed as LocationDisplayData; } + return null; } From 0cfeb7d3be89bf032f19a6bb9eb142b13d0babe2 Mon Sep 17 00:00:00 2001 From: Grace Nakiyemba Date: Tue, 27 Oct 2026 14:19:47 +0300 Subject: [PATCH 5/8] test(formplayer): cover gps capture storage by schema --- .../renderers/GPSQuestionRenderer.test.tsx | 55 ++++++++++++++++++- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/formulus-formplayer/src/renderers/GPSQuestionRenderer.test.tsx b/formulus-formplayer/src/renderers/GPSQuestionRenderer.test.tsx index 693c79342..9feac6761 100644 --- a/formulus-formplayer/src/renderers/GPSQuestionRenderer.test.tsx +++ b/formulus-formplayer/src/renderers/GPSQuestionRenderer.test.tsx @@ -1,10 +1,11 @@ // @vitest-environment jsdom import React from 'react'; -import { describe, expect, it } from 'vitest'; -import { render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { JsonForms } from '@jsonforms/react'; import Ajv from 'ajv'; import GPSQuestionRenderer, { gpsQuestionTester } from './GPSQuestionRenderer'; +import FormulusClient from '../services/FormulusInterface'; const ajv = new Ajv({ strict: false }); ajv.addFormat('gps', () => true); @@ -75,6 +76,56 @@ describe('gpsQuestionTester', () => { }); }); +describe('GPS capture storage', () => { + it('stores captured GPS data as an object for an object schema', async () => { + vi.spyOn(FormulusClient.getInstance(), 'requestLocation').mockResolvedValue( + { + status: 'success', + data: { + type: 'location', + latitude: 0.347596, + longitude: 32.58252, + accuracy: 5, + altitude: null, + altitudeAccuracy: null, + timestamp: '2026-10-27T10:00:00.000Z', + }, + }, + ); + + const onChange = vi.fn(); + + render( + , + ); + + fireEvent.click( + screen.getByRole('button', { name: 'Capture GPS location' }), + ); + + await waitFor(() => { + expect(onChange).toHaveBeenCalled(); + const latest = onChange.mock.calls.at(-1)?.[0]?.data; + + expect(latest?.location).toEqual({ + latitude: 0.347596, + longitude: 32.58252, + accuracy: 5, + altitude: undefined, + altitudeAccuracy: undefined, + timestamp: '2026-10-27T10:00:00.000Z', + }); + }); + }); +}); + describe('GPS object observation values', () => { it('displays an existing object GPS value', () => { render( From 35233506e922046e8956d91b4477da2c71a1c4bd Mon Sep 17 00:00:00 2001 From: Grace Nakiyemba Date: Tue, 27 Oct 2026 14:19:47 +0300 Subject: [PATCH 6/8] fix(formplayer): store gps values by schema type --- formulus-formplayer/src/renderers/GPSQuestionRenderer.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/formulus-formplayer/src/renderers/GPSQuestionRenderer.tsx b/formulus-formplayer/src/renderers/GPSQuestionRenderer.tsx index c820163d6..8e2bb06dc 100644 --- a/formulus-formplayer/src/renderers/GPSQuestionRenderer.tsx +++ b/formulus-formplayer/src/renderers/GPSQuestionRenderer.tsx @@ -110,7 +110,10 @@ const GPSQuestionRenderer: React.FC = props => { altitudeAccuracy: result.data.altitudeAccuracy ?? undefined, timestamp: result.data.timestamp, }; - handleChange(path, JSON.stringify(stored)); + handleChange( + path, + schema.type === 'object' ? stored : JSON.stringify(stored), + ); } else { const msg = result.message || @@ -141,7 +144,7 @@ const GPSQuestionRenderer: React.FC = props => { } finally { setIsCapturing(false); } - }, [enabled, fieldId, handleChange, path]); + }, [enabled, fieldId, handleChange, path, schema.type]); const handleDeleteLocation = () => { setError(null); From 103335ae7319aae0217da0b0a7a53cf920a1031a Mon Sep 17 00:00:00 2001 From: Grace Nakiyemba Date: Tue, 27 Oct 2026 14:21:22 +0300 Subject: [PATCH 7/8] test(formplayer): isolate gps renderer tests --- .../src/renderers/GPSQuestionRenderer.test.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/formulus-formplayer/src/renderers/GPSQuestionRenderer.test.tsx b/formulus-formplayer/src/renderers/GPSQuestionRenderer.test.tsx index 9feac6761..5c79a24f1 100644 --- a/formulus-formplayer/src/renderers/GPSQuestionRenderer.test.tsx +++ b/formulus-formplayer/src/renderers/GPSQuestionRenderer.test.tsx @@ -1,7 +1,13 @@ // @vitest-environment jsdom import React from 'react'; -import { describe, expect, it, vi } from 'vitest'; -import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { + cleanup, + fireEvent, + render, + screen, + waitFor, +} from '@testing-library/react'; import { JsonForms } from '@jsonforms/react'; import Ajv from 'ajv'; import GPSQuestionRenderer, { gpsQuestionTester } from './GPSQuestionRenderer'; @@ -36,6 +42,11 @@ const renderers = [ { tester: gpsQuestionTester, renderer: GPSQuestionRenderer }, ]; +afterEach(() => { + cleanup(); + vi.restoreAllMocks(); +}); + function ObjectGpsForm({ value }: { value: Record }) { return ( Date: Wed, 28 Oct 2026 15:43:18 +0300 Subject: [PATCH 8/8] test(formplayer): cover gps legacy and sparse schemas --- .../renderers/GPSQuestionRenderer.test.tsx | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/formulus-formplayer/src/renderers/GPSQuestionRenderer.test.tsx b/formulus-formplayer/src/renderers/GPSQuestionRenderer.test.tsx index 5c79a24f1..4672a37d5 100644 --- a/formulus-formplayer/src/renderers/GPSQuestionRenderer.test.tsx +++ b/formulus-formplayer/src/renderers/GPSQuestionRenderer.test.tsx @@ -85,6 +85,28 @@ describe('gpsQuestionTester', () => { gpsQuestionTester(uischema as any, rootSchema as any, {} as any), ).toBe(10); }); + + it('matches an object GPS schema without properties', () => { + const rootSchema = { + type: 'object', + properties: { + location: { + type: 'object', + format: 'gps', + title: 'Current Location', + }, + }, + }; + + const uischema = { + type: 'Control', + scope: '#/properties/location', + }; + + expect( + gpsQuestionTester(uischema as any, rootSchema as any, {} as any), + ).toBe(10); + }); }); describe('GPS capture storage', () => { @@ -154,4 +176,38 @@ describe('GPS object observation values', () => { expect(screen.getByText('0.347596° N')).toBeInTheDocument(); expect(screen.getByText('32.582520° E')).toBeInTheDocument(); }); + + it('displays an existing legacy string GPS value', () => { + const legacyStringGpsSchema = { + type: 'object', + properties: { + location: { + type: 'string', + format: 'gps', + title: 'Current Location', + }, + }, + }; + + render( + , + ); + + expect(screen.getByText('Location Captured')).toBeInTheDocument(); + expect(screen.getByText('0.347596° N')).toBeInTheDocument(); + expect(screen.getByText('32.582520° E')).toBeInTheDocument(); + }); });