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
4 changes: 2 additions & 2 deletions .github/workflows/NativePipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ env:
# Pin the test project (mendix/Native-Mobile-Resources) to a SHA for reproducibility instead
# of tracking a moving `main`. Single source of truth — bump deliberately. The repo publishes
# no tags, so a commit SHA is the only stable ref. Override per run via the dispatch input.
NATIVE_MOBILE_RESOURCES_REF: ${{ github.event.inputs.test_project_ref || 'a915484ef2cfed403cfbdfa531638df3c46c9d00' }}
NATIVE_MOBILE_RESOURCES_REF: ${{ github.event.inputs.test_project_ref || '05255f5ad19de7b109f6a68088e4fb198447ed00' }}
permissions:
packages: write
jobs:
Expand Down Expand Up @@ -751,7 +751,7 @@ jobs:
# truncated — at 20 min several shards (accordion, background-image, bottom-sheet,
# color-picker, safe-area-view) were cancelled mid-flow. If a shard still hits 30 the cause
# is real app instability, not the cap.
timeout-minutes: 30
timeout-minutes: 35
strategy:
max-parallel: 4
matrix:
Expand Down
15 changes: 15 additions & 0 deletions maestro/helpers/helpers.sh
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,24 @@ stop_recording() {
REC_FILE=""
}

# Function to clean up stale XCTest processes (iOS only)
cleanup_xctest_processes() {
if [ "$PLATFORM" != "ios" ]; then
return 0
fi
echo "🧹 Cleaning up stale XCTest processes..."
# Kill any leftover XCTest runner processes that might be blocking driver initialization
pkill -9 -f "XCTRunner" 2>/dev/null || true
pkill -9 -f "xctest" 2>/dev/null || true
pkill -9 -f "maestro.*driver" 2>/dev/null || true
sleep 2
}

# Function to restart the iOS simulator
restart_simulator() {
echo "🔄 Restarting iOS Simulator..."
# Clean up XCTest processes first
cleanup_xctest_processes
# Shut down whatever is booted; the device is auto-selected in prepare_ios.sh,
# so we don't depend on a hardcoded device name here.
xcrun simctl shutdown all || true
Expand Down
Binary file modified maestro/images/expected/android/bg_image_dynamic_svg.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions maestro/run_maestro_jsactions_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,16 @@ run_jsactions_tests() {
fi
}

# Clean up any stale XCTest processes before starting (iOS only)
if [ "$PLATFORM" == "ios" ]; then
cleanup_xctest_processes
fi

# Fast-fail smoke check before running jsActions tests (same as widget tests)
if ! smoke_check; then
exit 1
fi

# Run jsActions tests
run_jsactions_tests
if [ $? -ne 0 ]; then
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ appId: "${APP_ID}"
text: "Bottom Sheet"
- tapOn:
text: "Expanding fullscreen"
# Because the image loading can take some time due to resources on emulator, we need to wait for the image to be visible
- extendedWaitUntil:
# Because the image loading and animation can take some time due to resources on emulator, we need to wait
- extendedWaitUntil:
visible: randText # Any random text that does not exist in the UI
optional: true # This should be true so that the test won't fail
timeout: 10000 # 10 seconds
timeout: 15000 # 15 seconds to allow for animation to complete
- takeScreenshot:
path: "maestro/images/actual/${PLATFORM}/bottom_sheet_expanding_fullscreen"
- assertScreenshot:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ appId: "${APP_ID}"
text: "Bottom Sheet"
- tapOn:
text: "Modal basic non native"
# Wait for the page to fully load before tapping Open
- extendedWaitUntil:
visible: "Open"
timeout: 10000
- tapOn:
text: "Open"

Expand Down
6 changes: 6 additions & 0 deletions packages/pluggableWidgets/intro-screen-native/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

## [Unreleased]

### Fixed

- We fixed an issue where reopening a page containing the intro screen would show the first slide instead of the slide referenced by the active slide attribute, and would report a slide change that the user did not make.
- We fixed an issue where the initial slide was scrolled to with an animation, which briefly left the slide content unavailable to screen readers.
- We fixed an issue where moving to a slide with the next, previous or pagination controls could jump back to the first slide and report an extra slide change.

## [4.4.1] - 2026-6-10

### Changed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ appId: "${APP_ID}"
timeout: 5000
- tapOn:
text: "NEXT"
- extendedWaitUntil:
visible: "Active slide: 3"
timeout: 5000
- tapOn:
text: "FINISH"
- extendedWaitUntil:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,41 +69,53 @@ const refreshActiveSlideAttribute = (slides: SlidesType[], activeSlide?: Editabl
export const SwipeableContainer = (props: SwipeableContainerProps): ReactElement => {
const [width, setWidth] = useState(0);
const [height, setHeight] = useState(0);
const [activeIndex, setActiveIndex] = useState(0);
// Start on the slide the attribute points at, so activeIndex never lags the slide that
// initialScrollIndex renders. Otherwise the first Next/Previous press moves relative to
// slide 0 instead of the visible slide.
const [activeIndex, setActiveIndex] = useState(() => refreshActiveSlideAttribute(props.slides, props.activeSlide));
const flashList = useRef<FlashListRef<any>>(null);
const isInitializing = useRef(true);
const hasAppliedInitialScroll = useRef(false);
const isUserDragging = useRef(false);
// Slide a programmatic scroll is currently heading for, or null when the list is
// wherever the user left it. Used to recognise momentum events that contradict the
// scroll we just asked for; see onMomentumScrollEnd.
const pendingScrollTarget = useRef<number | null>(null);

const rtlSafeIndex = useCallback(
(i: number): number => (isAndroidRTL ? props.slides.length - 1 - i : i),
[props.slides.length]
);

const goToSlide = useCallback(
(pageNum: number) => {
(pageNum: number, animated = true) => {
setActiveIndex(pageNum);
if (flashList && flashList.current) {
pendingScrollTarget.current = pageNum;
flashList.current.scrollToOffset({
offset: rtlSafeIndex(pageNum) * width
offset: rtlSafeIndex(pageNum) * width,
animated
});
}
},
[rtlSafeIndex, width]
);

useEffect(() => {
if (!width || props.activeSlide?.status !== ValueStatus.Available) {
return;
}
const slide = refreshActiveSlideAttribute(props.slides, props.activeSlide);
if (width && props.activeSlide?.status === ValueStatus.Available && slide !== activeIndex) {
// Once width is known, force the list to the attribute's slide even when activeIndex
// already matches: initialScrollIndex is applied before layout, so on remount the list
// can sit at offset 0 while activeIndex says otherwise.
if (!hasAppliedInitialScroll.current) {
hasAppliedInitialScroll.current = true;
// Jump without animation: activeIndex is applied immediately, and it drives which
// slide is exposed to accessibility. An animated scroll would leave the exposed
// slide off-screen until the animation lands.
goToSlide(slide, false);
} else if (slide !== activeIndex) {
goToSlide(slide);
if (isInitializing.current) {
if (isInitializing.current) {
// Use requestAnimationFrame twice to wait for the next frame after scroll.
requestAnimationFrame(() => {
requestAnimationFrame(() => {
isInitializing.current = false;
});
});
}
}
}
}, [props.activeSlide, activeIndex, width, props.slides, goToSlide]);

Expand Down Expand Up @@ -315,15 +327,46 @@ export const SwipeableContainer = (props: SwipeableContainerProps): ReactElement
);
};

const onScrollBeginDrag = useCallback(() => {
isUserDragging.current = true;
// The user takes over from here, so any programmatic scroll still in flight no
// longer describes where the list is heading. Clearing the target also keeps it
// from latching when a scroll never reports a momentum end of its own.
pendingScrollTarget.current = null;
}, []);

const onMomentumScrollEnd = useCallback(
(event: NativeSyntheticEvent<any>) => {
const wasUserDragging = isUserDragging.current;
isUserDragging.current = false;

if (!width) {
return;
}
const offset = event.nativeEvent.contentOffset.x;
const newIndex = rtlSafeIndex(Math.round(offset / width));

// While a programmatic scroll is in flight the list can report a momentum end
// for the position it is leaving rather than the one it is heading to. Trusting
// that offset rewrites activeIndex to a slide that is not on screen, and the
// drag flag cannot tell the two apart because a fling reports momentum without
// a fresh drag. Anything that disagrees with the requested slide is stale.
const pendingTarget = pendingScrollTarget.current;
if (pendingTarget !== null) {
if (newIndex === pendingTarget) {
pendingScrollTarget.current = null;
}
return;
}

if (newIndex === activeIndex) {
return;
}

if (isInitializing.current) {
// Only a user swipe reports a change here. Programmatic scrolls (initial positioning,
// Next/Previous/pagination) already fired onSlideChange, so re-firing would double
// count the change and, on remount, emit a spurious change back to slide 1.
if (!wasUserDragging) {
setActiveIndex(newIndex);
return;
}
Expand Down Expand Up @@ -366,6 +409,7 @@ export const SwipeableContainer = (props: SwipeableContainerProps): ReactElement
bounces={false}
style={styles.flatList}
renderItem={renderItem}
onScrollBeginDrag={onScrollBeginDrag}
onMomentumScrollEnd={onMomentumScrollEnd}
scrollEventThrottle={50}
extraData={[width, activeIndex]}
Expand Down
Loading
Loading