Refactor example app and add location tracking functionality with tests
This commit is contained in:
@@ -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.
|
||||
|
||||
13
packages/map-sdk/jest.config.js
Normal file
13
packages/map-sdk/jest.config.js
Normal 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)']
|
||||
};
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
63
packages/map-sdk/src/hooks/useLocationTracking.ts
Normal file
63
packages/map-sdk/src/hooks/useLocationTracking.ts
Normal 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;
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user