Refactor example app and add location tracking functionality with tests

This commit is contained in:
2026-02-03 23:06:09 +05:30
parent fb56521d84
commit d1e383baf5
9 changed files with 149 additions and 150 deletions

View File

@@ -15,20 +15,24 @@ Basic usage:
```ts
import React from 'react';
import { MapView, Marker } from '@lynkedup/map-sdk';
import { MapView, Marker, useLocationTracking, Polyline } from '@lynkedup/map-sdk';
export default function App() {
const { isTracking, path, startTracking, stopTracking, clear } = useLocationTracking();
return (
<MapView
style={{ flex: 1 }}
initialRegion={{ latitude: 37.78825, longitude: -122.4324, latitudeDelta: 0.0922, longitudeDelta: 0.0421 }}
>
<MapView style={{ flex: 1 }} initialRegion={{ latitude: 37.78825, longitude: -122.4324, latitudeDelta: 0.0922, longitudeDelta: 0.0421 }}>
<Marker coordinate={{ latitude: 37.78825, longitude: -122.4324 }} />
{path.length > 1 && <Polyline coordinates={path} strokeWidth={4} strokeColor="#007AFF" />}
</MapView>
);
}
```
Tracking notes:
- `useLocationTracking()` uses the platform geolocation API (`navigator.geolocation.watchPosition`). Request permissions on iOS/Android as usual before starting tracking.
- It returns `{ isTracking, path, current, startTracking, stopTracking, clear }` so you can display the polyline and follow the user.
## Camera Controls 🔧
This package exposes a small imperative `MapHandle` API via `ref` on `MapView`. Use it to animate the camera or fit bounds.

View File

@@ -0,0 +1,13 @@
module.exports = {
transform: {
'^.+\\.(ts|tsx)$': 'ts-jest',
'^.+\\.(js|jsx)$': 'babel-jest'
},
transformIgnorePatterns: ["node_modules/(?!(react-native|@react-native|@react-native/polyfills|@react-native-maps)/)"],
testEnvironment: 'jsdom',
moduleNameMapper: {
'^react-native$': 'react-native'
},
setupFilesAfterEnv: ['@testing-library/jest-native/extend-expect'],
testMatch: ['**/__tests__/**/*.test.(ts|tsx|js)']
};

View File

@@ -9,7 +9,8 @@
],
"scripts": {
"build": "tsc -p tsconfig.json",
"lint": "eslint . --ext .ts,.tsx"
"lint": "eslint . --ext .ts,.tsx",
"test": "jest --runInBand"
},
"peerDependencies": {
"react": ">=17",
@@ -21,7 +22,18 @@
"devDependencies": {
"typescript": "^5.0.0",
"eslint": "^8.0.0",
"@types/react": "^18.0.0",
"@types/react-native": "^0.70.0"
"@types/react": "^19.0.0",
"@types/react-native": "^0.70.0",
"jest": "^29.0.0",
"@types/jest": "^29.0.0",
"babel-jest": "^29.0.0",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/react-native": "^12.9.0",
"@testing-library/jest-native": "^5.4.3",
"react-test-renderer": "^19.0.0",
"react": "^19.2.4",
"ts-jest": "^29.0.0",
"babel-jest": "^29.0.0",
"jest-environment-jsdom": "^29.0.0"
}
}

View File

@@ -0,0 +1,48 @@
require('react-test-renderer');
const { renderHook, act } = require('@testing-library/react-hooks');
const { useLocationTracking } = require('../useLocationTracking');
describe('useLocationTracking', () => {
let origGeo;
beforeEach(() => {
origGeo = global.navigator && global.navigator.geolocation;
global.navigator = global.navigator || {};
global.navigator.geolocation = {
watchPosition: jest.fn((success) => {
// Immediately call with a sample position
success({ coords: { latitude: 10, longitude: 20 } });
// Return a fake id
return 42;
}),
clearWatch: jest.fn()
};
});
afterEach(() => {
if (origGeo) global.navigator.geolocation = origGeo;
else delete global.navigator.geolocation;
jest.resetAllMocks();
});
it('starts and stops tracking and collects path', () => {
const { result } = renderHook(() => useLocationTracking());
expect(result.current.isTracking).toBe(false);
act(() => result.current.startTracking());
expect(result.current.isTracking).toBe(true);
expect(result.current.path.length).toBeGreaterThanOrEqual(1);
act(() => result.current.stopTracking());
expect(result.current.isTracking).toBe(false);
expect(global.navigator.geolocation.clearWatch).toHaveBeenCalled();
});
it('clears path', () => {
const { result } = renderHook(() => useLocationTracking());
act(() => result.current.startTracking());
expect(result.current.path.length).toBeGreaterThanOrEqual(1);
act(() => result.current.clear());
expect(result.current.path.length).toBe(0);
});
});

View File

@@ -0,0 +1,63 @@
import { useRef, useState, useCallback } from 'react';
import type { LatLng } from 'react-native-maps';
export type TrackingOptions = {
distanceFilter?: number; // meters
interval?: number; // ms
fastestInterval?: number; // ms
accuracy?: 'high' | 'balanced' | 'low';
};
export function useLocationTracking(opts: TrackingOptions = {}) {
const [isTracking, setIsTracking] = useState(false);
const [path, setPath] = useState<LatLng[]>([]);
const [current, setCurrent] = useState<LatLng | null>(null);
const watchIdRef = useRef<number | null>(null);
const onPosition = useCallback((pos: any) => {
const lat = pos.coords?.latitude;
const lng = pos.coords?.longitude;
if (typeof lat === 'number' && typeof lng === 'number') {
const point = { latitude: lat, longitude: lng } as LatLng;
setPath((p) => [...p, point]);
setCurrent(point);
}
}, []);
const startTracking = useCallback(() => {
if (isTracking) return;
if (!global.navigator?.geolocation?.watchPosition) {
console.warn('[useLocationTracking] Geolocation.watchPosition not available.');
return;
}
const id = global.navigator.geolocation.watchPosition(
onPosition,
(err: any) => console.warn('geolocation error', err),
{
enableHighAccuracy: opts.accuracy === 'high',
distanceFilter: opts.distanceFilter ?? 0,
interval: opts.interval
}
);
watchIdRef.current = id as unknown as number;
setIsTracking(true);
}, [isTracking, onPosition, opts.accuracy, opts.distanceFilter, opts.interval]);
const stopTracking = useCallback(() => {
const id = watchIdRef.current;
if (id != null && global.navigator?.geolocation?.clearWatch) {
global.navigator.geolocation.clearWatch(id);
}
watchIdRef.current = null;
setIsTracking(false);
}, []);
const clear = useCallback(() => {
setPath([]);
setCurrent(null);
}, []);
return { isTracking, path, current, startTracking, stopTracking, clear } as const;
}

View File

@@ -2,6 +2,7 @@ export { default as MapView } from './MapView';
export type { MapHandle } from './MapView';
export { Marker, Polyline, PROVIDER_GOOGLE } from 'react-native-maps';
export { CameraPresets } from './cameraPresets';
export { useLocationTracking } from './hooks/useLocationTracking';
// High-level components
export { default as PropertyMap } from './components/PropertyMap';