Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/workflows/native-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,24 @@ jobs:
CODE_SIGN_IDENTITY="" \
| xcbeautify --renderer github-actions

- name: Run marker drag UI test
if: matrix.provider == 'apple'
run: |
ruby example/scripts/configure-marker-drag-ui-tests.rb example/ios/NitroMapsExample.xcodeproj
simulator_id="$(xcrun simctl list devices available -j | ruby -rjson -e 'devices = JSON.parse(STDIN.read).fetch("devices").values.flatten; device = devices.find { |entry| entry["isAvailable"] && entry["name"].start_with?("iPhone") }; abort "No available iPhone simulator was found." unless device; puts device.fetch("udid")')"
set -o pipefail
NSUnbufferedIO=YES xcodebuild test \
-workspace example/ios/NitroMapsExample.xcworkspace \
-scheme NitroMapsExample \
-configuration Release \
-destination "platform=iOS Simulator,id=${simulator_id}" \
-derivedDataPath "$RUNNER_TEMP/MarkerDragUITests" \
-resultBundlePath "$RUNNER_TEMP/MarkerDragUITests.xcresult" \
-parallel-testing-enabled NO \
-only-testing:NitroMapsExampleUITests/MarkerDragUITests/testDraggingPinAndImageMarkersOnlyFiresDragEnd \
CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY="" \
| xcbeautify --renderer github-actions

android:
name: Android
# Pinned: the Android SDK and NDK contents differ between runner images.
Expand Down
10 changes: 10 additions & 0 deletions .github/workflows/native.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ on:
- 'package/tsconfig.plugin.json'
- 'example/app.config.js'
- 'example/app.json'
- 'example/App.tsx'
- 'example/assets/markerred.png'
- 'example/examples/**'
- 'example/ios-ui-tests/**'
- 'example/scripts/**'
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- 'example/package.json'
- 'bun.lock'
pull_request:
Expand All @@ -54,6 +59,11 @@ on:
- 'package/tsconfig.plugin.json'
- 'example/app.config.js'
- 'example/app.json'
- 'example/App.tsx'
- 'example/assets/markerred.png'
- 'example/examples/**'
- 'example/ios-ui-tests/**'
- 'example/scripts/**'
- 'example/package.json'
- 'bun.lock'

Expand Down
55 changes: 52 additions & 3 deletions example/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import {
createCustomMarkerImagesScenario,
createScenarioOverlayProps,
CUSTOM_MARKER_IMAGES_SCENARIO_ID,
MARKER_DRAG_REGRESSION_SCENARIO_ID,
nextApplePoiDetailPresentation,
} from './examples';

Expand Down Expand Up @@ -272,6 +273,8 @@ const ScenarioChip = memo(function ScenarioChip({
>
<ScalePressable
onPress={onPress}
accessibilityRole="button"
accessibilityLabel={label}
style={[styles.scenarioChip, active && styles.scenarioChipActive]}
>
<Text
Expand Down Expand Up @@ -667,8 +670,8 @@ const MapScene = memo(function MapScene({
onRegionChangeComplete,
};

if (provider === 'apple') {
return (
const mapView =
provider === 'apple' ? (
<MapView
ref={ref}
{...commonMapProps}
Expand All @@ -678,24 +681,35 @@ const MapScene = memo(function MapScene({
scenario.advanced?.applePoiDetailPresentation
}
/>
) : (
<MapView ref={ref} {...commonMapProps} provider="google" />
);

if (scenario.id === MARKER_DRAG_REGRESSION_SCENARIO_ID) {
return (
<View style={styles.map} testID="marker-drag-regression-map">
{mapView}
</View>
);
}

return <MapView ref={ref} {...commonMapProps} provider="google" />;
return mapView;
});

type StatusHeaderProps = {
status: string;
scenarioIndex: number;
mapReady: boolean;
showsScale: boolean;
verificationStatus?: string;
};

const StatusHeader = memo(function StatusHeader({
status,
scenarioIndex,
mapReady,
showsScale,
verificationStatus,
}: StatusHeaderProps) {
const badgeScale = useSharedValue(1);

Expand Down Expand Up @@ -743,6 +757,14 @@ const StatusHeader = memo(function StatusHeader({
</Text>
</Animated.View>
</View>
{verificationStatus != null ? (
<Text
testID="marker-drag-verification-status"
style={styles.verificationStatus}
>
{verificationStatus}
</Text>
) : null}
</View>
</Animated.View>
);
Expand All @@ -757,6 +779,10 @@ export default function App() {
const [animationOptionIndex, setAnimationOptionIndex] = useState(0);
const [status, setStatus] = useState('Waiting for map...');
const [mapReady, setMapReady] = useState(false);
const [markerDragEndCounts, setMarkerDragEndCounts] = useState<
Record<string, number>
>({});
const [mapLongPressCount, setMapLongPressCount] = useState(0);
const [dockExpanded, setDockExpanded] = useState(false);
const [customMarkerRotation, setCustomMarkerRotation] = useState(45);
const [customMarkerFlat, setCustomMarkerFlat] = useState(true);
Expand Down Expand Up @@ -788,6 +814,7 @@ export default function App() {
const animationOption = ANIMATION_OPTIONS[animationOptionIndex];
const showsScale = scenario.advanced?.showsScale === true;
const showsCompass = scenario.advanced?.showsCompass === true;
const markerDragVerificationStatus = `Drag ends: pin=${markerDragEndCounts.pin ?? 0}, image=${markerDragEndCounts.image ?? 0}; long=${mapLongPressCount}; ready=${mapReady}`;
const mapPadding = useMemo(
() =>
mergeMapPadding(scenario.advanced?.mapPadding, showsScale, showsCompass),
Expand Down Expand Up @@ -869,6 +896,8 @@ export default function App() {
}
setScenarioIndex(index);
setMapReady(false);
setMarkerDragEndCounts({});
setMapLongPressCount(0);
setStatus(MAP_SCENARIOS[index].name);
},
[scenarioIndex],
Expand Down Expand Up @@ -900,6 +929,10 @@ export default function App() {

const handleMarkerDragEnd = useCallback(
(id: string, coordinate: Coordinate) => {
setMarkerDragEndCounts((counts) => ({
...counts,
[id]: (counts[id] ?? 0) + 1,
}));
setStatus(
`${id} → ${coordinate.latitude.toFixed(4)}, ${coordinate.longitude.toFixed(4)}`,
);
Expand Down Expand Up @@ -963,6 +996,7 @@ export default function App() {
}, []);

const handleMapLongPress = useCallback((coordinate: Coordinate) => {
setMapLongPressCount((count) => count + 1);
setStatus(
`Long · ${coordinate.latitude.toFixed(4)}, ${coordinate.longitude.toFixed(4)}`,
);
Expand Down Expand Up @@ -1010,6 +1044,11 @@ export default function App() {
scenarioIndex={scenarioIndex}
mapReady={mapReady}
showsScale={showsScale}
verificationStatus={
scenario.id === MARKER_DRAG_REGRESSION_SCENARIO_ID
? markerDragVerificationStatus
: undefined
}
/>

<ScenarioDock
Expand Down Expand Up @@ -1132,6 +1171,16 @@ const styles = StyleSheet.create({
color: palette.text,
fontVariant: ['tabular-nums'],
},
verificationStatus: {
alignSelf: 'flex-start',
borderRadius: 8,
paddingHorizontal: 8,
paddingVertical: 5,
backgroundColor: palette.surface,
color: palette.text,
fontSize: 11,
fontVariant: ['tabular-nums'],
},
dock: {
position: 'absolute',
left: 16,
Expand Down
6 changes: 6 additions & 0 deletions example/examples/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ import { deliveryZoneScenario } from './deliveryZone';
import { geojsonScenario } from './geojson';
import { landmarksScenario } from './landmarks';
import { mountEffectCameraScenario } from './mountEffectCamera';
import {
MARKER_DRAG_REGRESSION_SCENARIO_ID,
markerDragRegressionScenario,
} from './markerDragRegression';
import { createScenarioOverlayProps } from './overlaySource';
import { riverRouteScenario } from './riverRoute';
import type { MapScenario } from './types';
Expand All @@ -28,6 +32,7 @@ export {
createCustomMarkerImagesScenario,
createScenarioOverlayProps,
CUSTOM_MARKER_IMAGES_SCENARIO_ID,
MARKER_DRAG_REGRESSION_SCENARIO_ID,
nextApplePoiDetailPresentation,
};

Expand All @@ -41,4 +46,5 @@ export const MAP_SCENARIOS: MapScenario[] = [
advancedFeaturesScenario,
applePoiDetailsScenario,
mountEffectCameraScenario,
markerDragRegressionScenario,
];
34 changes: 34 additions & 0 deletions example/examples/markerDragRegression.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import markerRed from '../assets/markerred.png';
import type { MapScenario } from './types';

export const MARKER_DRAG_REGRESSION_SCENARIO_ID = 'marker-drag-regression';

export const markerDragRegressionScenario: MapScenario = {
id: MARKER_DRAG_REGRESSION_SCENARIO_ID,
name: 'Marker drag regression',
description: 'Simulator test fixture for pin and image marker dragging',
region: {
latitude: 52.2297,
longitude: 21.0122,
latitudeDelta: 0.08,
longitudeDelta: 0.08,
},
markers: [
{
id: 'pin',
coordinate: { latitude: 52.2201, longitude: 20.9962 },
title: 'Draggable pin',
draggable: true,
enteringAnimation: false,
},
{
id: 'image',
coordinate: { latitude: 52.2201, longitude: 21.0282 },
title: 'Draggable image',
image: markerRed,
anchor: { x: 0.5, y: 1 },
draggable: true,
enteringAnimation: false,
},
],
};
68 changes: 68 additions & 0 deletions example/ios-ui-tests/MarkerDragUITests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import XCTest

final class MarkerDragUITests: XCTestCase {
private var app: XCUIApplication!

override func setUpWithError() throws {
continueAfterFailure = false
app = XCUIApplication()
app.launch()
}

func testDraggingPinAndImageMarkersOnlyFiresDragEnd() throws {
let scenarioButton = app.buttons["Marker drag regression"]
XCTAssertTrue(scenarioButton.waitForExistence(timeout: 30))

let scenarioStrip = app.scrollViews.firstMatch
for _ in 0..<12 {
if scenarioButton.isHittable {
break
}
scenarioStrip.swipeLeft()
}
XCTAssertTrue(scenarioButton.isHittable)
scenarioButton.tap()

let map = app.descendants(matching: .any)
.matching(identifier: "marker-drag-regression-map")
.firstMatch
XCTAssertTrue(map.waitForExistence(timeout: 30))

let result = app.staticTexts["marker-drag-verification-status"]
XCTAssertTrue(result.waitForExistence(timeout: 30))
waitForStatus(result, containing: ["pin=0", "image=0", "long=0", "ready=true"])

let pin = app.descendants(matching: .any)
.matching(identifier: "react-native-better-maps.marker.pin")
.firstMatch
XCTAssertTrue(pin.waitForExistence(timeout: 30), "The pin annotation must be exposed at its rendered position.")
XCTAssertTrue(pin.isHittable, "The rendered pin annotation must be hittable.")
XCTAssertGreaterThan(pin.frame.width, 0)
XCTAssertGreaterThan(pin.frame.height, 0)

let pinStart = pin.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5))
let pinEnd = map.coordinate(withNormalizedOffset: CGVector(dx: 0.40, dy: 0.48))
pinStart.press(forDuration: 1.0, thenDragTo: pinEnd)
waitForStatus(result, containing: ["pin=1", "image=0", "long=0"])

let image = app.descendants(matching: .any)
.matching(identifier: "react-native-better-maps.marker.image")
.firstMatch
XCTAssertTrue(image.waitForExistence(timeout: 30), "The image annotation must be exposed at its rendered position.")
XCTAssertTrue(image.isHittable, "The rendered image annotation must be hittable.")
XCTAssertGreaterThan(image.frame.width, 0)
XCTAssertGreaterThan(image.frame.height, 0)

let imageStart = image.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5))
let imageEnd = map.coordinate(withNormalizedOffset: CGVector(dx: 0.60, dy: 0.48))
imageStart.press(forDuration: 1.0, thenDragTo: imageEnd)
waitForStatus(result, containing: ["pin=1", "image=1", "long=0"])
}

private func waitForStatus(_ element: XCUIElement, containing fragments: [String]) {
let format = fragments.map { "label CONTAINS %@" }.joined(separator: " AND ")
let predicate = NSPredicate(format: format, argumentArray: fragments)
let expectation = XCTNSPredicateExpectation(predicate: predicate, object: element)
XCTAssertEqual(XCTWaiter.wait(for: [expectation], timeout: 20), .completed, element.label)
}
}
13 changes: 13 additions & 0 deletions example/ios-ui-tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# iOS marker-drag UI regression

This UI test launches the Expo example on an iOS simulator and performs long-press drags on both a MapKit pin annotation and a bundled-image annotation. It verifies that each native drag-end callback is delivered and that the map-level long-press callback count stays at zero.

Run it on macOS with Xcode and CocoaPods installed:

```bash
bash example/scripts/run-marker-drag-ui-tests.sh
```

The script runs Expo prebuild, installs pods, adds a generated UI-test target to the ignored Xcode project, and executes the test. The target is generated instead of committed because `example/ios/` is intentionally ignored and recreated by Expo prebuild.

The same UI test runs on the Apple-provider leg of the native GitHub Actions workflow.
Loading