mirror of
https://github.com/mattermost/mattermost-mobile.git
synced 2026-11-06 02:40:15 +00:00
Closes manual release-test gaps for mobile (Detox + Maestro), automating new test cases and hardening flaky/CI-only failures uncovered along the way. ### New coverage automated - **MM-T416** offline/online post-retry (Android via airplane mode; iOS ultimately marked unsupported — see below) - **MM-T1194 / MM-T1193** custom ToS accept/decline survives relaunch - **MM-T3251** status change via account UI (cross-user visibility) - **MM-T510** channel-wide/keyword mention delivery — later expanded to a full 5-channel mention matrix with exact server-side counts - **MM-T3261 (_1/_2)** Report a Problem external hand-off (browser link mode, email mode) - **MM-T2940** iOS managed-config (MDM) pre-fill/autoconnect - **MM-T3271/3272** opening the app from a push notification (synthetic notification payloads, background/closed states) - Un-skipped several tests that turned out to already pass (stale skips) ### Root-caused failing tests - **Android SQLite corruption**: traced to native Kotlin DB connections not opening in WAL mode while JSI did (post #10109) — journal-mode mismatch corrupted the store on push-triggered writes. Fixed properly with a **WAL patch** (#10117) rather than the original destructive "wipe app database" workaround, which was reverted. - **Fabric crash** (`addViewAt`/reparent) on `SlideUpPanelItem` taps — root-caused to view flattening racing sheet mounts; fixed with `collapsable={false}` (went through a revert/re-revert once CI reproduced the crash without it). - **Search race**: stale in-flight search results could overwrite a cleared search box — fixed with request-id tagging. - **iOS offline detection**: multiple iterations (AAAA sampling → Cloudflare-prefix check → final decision) landed on refusing iOS offline simulation outright, since all E2E servers are Cloudflare-fronted/anycast and the address space can't be enumerated/blocked. - **MM-T3261 config patch** intermittently silently ignored by the server — root-caused to `ReportAProblemType` being environment-managed on PR Spinwicks (config PATCH returns 200 but is silently dropped); now detected and reported as a skip with proven cause, not a failure. - **MM-T416 failed-post timing**: multiple rounds of tightening the wait bound to match the client's actual retry/backoff/timeout ceiling (ended at ~150s, derived from OkHttp/retry-interceptor math, not guessed). - **MM-T5603 upload wait**: raised to match the client's real 180s upload timeout. - **Classification banner suite**: fixed to verify config state directly instead of trusting a PATCH reply that the cloud sometimes drops. ### CI/reporting infra fixes - Dedicated Maestro steps (MM-T3261, MM-T67856_4) weren't feeding into the merged JUnit report and couldn't gate pass/fail — fixed report merging/naming so they now count. - Android Maestro logcat capture added (previously iOS-only diagnostics). - Jest retry logic no longer discards passing results from a shard killed by timeout. - Sharding raised 24→28. - Removed dead Detox Pilot/Playbooks scaffolding (unused, uncovered by CI). ### Cleanup - Comment/docs pass stripping narrative session-archaeology from long explanatory comments, keeping only load-bearing facts and CI references.
167 lines
5.6 KiB
TypeScript
167 lines
5.6 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import {type ImageSource, type ImageStyle} from 'expo-image';
|
|
import React from 'react';
|
|
import {type StyleProp, Text, type TextStyle, TouchableHighlight, View, type ViewStyle} from 'react-native';
|
|
|
|
import CompassIcon, {type CompassIconName} from '@components/compass_icon';
|
|
import ExpoImage from '@components/expo_image';
|
|
import {useTheme} from '@context/theme';
|
|
import {usePreventDoubleTap} from '@hooks/utils';
|
|
import {urlSafeBase64Encode} from '@utils/security';
|
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
|
import {typography} from '@utils/typography';
|
|
import {isValidUrl} from '@utils/url';
|
|
|
|
type SlideUpPanelProps = {
|
|
destructive?: boolean;
|
|
leftIcon?: CompassIconName | ImageSource;
|
|
leftImageStyles?: StyleProp<ImageStyle>;
|
|
leftIconStyles?: StyleProp<TextStyle>;
|
|
rightIcon?: CompassIconName | ImageSource;
|
|
rightImageStyles?: StyleProp<ImageStyle>;
|
|
rightIconStyles?: StyleProp<TextStyle>;
|
|
onPress: () => void;
|
|
textStyles?: TextStyle;
|
|
testID?: string;
|
|
text: string;
|
|
}
|
|
|
|
export const ITEM_HEIGHT = 48;
|
|
|
|
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|
return {
|
|
container: {
|
|
height: ITEM_HEIGHT,
|
|
marginHorizontal: -20,
|
|
paddingHorizontal: 20,
|
|
},
|
|
destructive: {
|
|
color: theme.dndIndicator,
|
|
},
|
|
row: {
|
|
width: '100%',
|
|
flexDirection: 'row',
|
|
},
|
|
iconContainer: {
|
|
height: ITEM_HEIGHT,
|
|
justifyContent: 'center',
|
|
marginRight: 10,
|
|
},
|
|
noIconContainer: {
|
|
height: ITEM_HEIGHT,
|
|
width: 18,
|
|
},
|
|
icon: {
|
|
color: changeOpacity(theme.centerChannelColor, 0.56),
|
|
},
|
|
textContainer: {
|
|
justifyContent: 'center',
|
|
flex: 1,
|
|
height: ITEM_HEIGHT,
|
|
marginRight: 5,
|
|
},
|
|
text: {
|
|
color: theme.centerChannelColor,
|
|
...typography('Body', 200, 'Regular'),
|
|
},
|
|
};
|
|
});
|
|
|
|
const SlideUpPanelItem = ({
|
|
destructive = false,
|
|
leftIcon,
|
|
leftImageStyles,
|
|
leftIconStyles,
|
|
rightIcon,
|
|
rightImageStyles,
|
|
rightIconStyles,
|
|
onPress,
|
|
testID,
|
|
text,
|
|
textStyles,
|
|
}: SlideUpPanelProps) => {
|
|
const theme = useTheme();
|
|
const style = getStyleSheet(theme);
|
|
|
|
const {image: leftImage, iconStyle: leftIconStyle} = useImageAndStyle(leftIcon, leftImageStyles, leftIconStyles, destructive);
|
|
const {image: rightImage, iconStyle: rightIconStyle} = useImageAndStyle(rightIcon, rightImageStyles, rightIconStyles, destructive);
|
|
|
|
const handleOnPress = usePreventDoubleTap(onPress);
|
|
|
|
// The row View gets collapsable={false} so it stays a real native view. It only has
|
|
// layout props, so Fabric flattens it away until TouchableHighlight applies its
|
|
// pressed opacity, at which point Fabric materialises the row and moves the text into
|
|
// it, then flattens it again on release. On Android that reparenting of a
|
|
// ReactTextView mid-press races the sheet's own mounting and throws
|
|
// "addViewAt: cannot insert view: View already has a parent", which red-boxes the app
|
|
// (CI 34185558418: browse-channels dropdown, plus menu, failed-post retry, set-status;
|
|
// reproduced locally on the first tap). A view that is always a host view gets a plain
|
|
// opacity update instead of a reparent. No visual change.
|
|
return (
|
|
<TouchableHighlight
|
|
onPress={handleOnPress}
|
|
style={style.container}
|
|
testID={testID}
|
|
underlayColor={changeOpacity(theme.buttonBg, 0.08)}
|
|
>
|
|
<View
|
|
style={style.row}
|
|
collapsable={false}
|
|
>
|
|
{Boolean(leftImage) &&
|
|
<View style={leftIconStyle}>{leftImage}</View>
|
|
}
|
|
<View style={style.textContainer}>
|
|
<Text style={[style.text, destructive && style.destructive, textStyles]}>{text}</Text>
|
|
</View>
|
|
{Boolean(rightImage) &&
|
|
<View style={rightIconStyle}>{rightImage}</View>
|
|
}
|
|
</View>
|
|
</TouchableHighlight>
|
|
);
|
|
};
|
|
|
|
const useImageAndStyle = (icon: CompassIconName | ImageSource | undefined, imageStyles: StyleProp<ImageStyle>, iconStyles: StyleProp<TextStyle>, destructive: boolean) => {
|
|
const theme = useTheme();
|
|
const style = getStyleSheet(theme);
|
|
|
|
let image;
|
|
let iconStyle: Array<StyleProp<ViewStyle>> = [style.iconContainer];
|
|
if (icon) {
|
|
if (typeof icon === 'object') {
|
|
if (icon.uri && isValidUrl(icon.uri)) {
|
|
const imageStyle: StyleProp<ImageStyle> = [imageStyles];
|
|
imageStyle.push({width: 24, height: 24});
|
|
image = (
|
|
<ExpoImage
|
|
id={`slide-up-panel-item-${urlSafeBase64Encode(icon.uri)}`}
|
|
source={icon}
|
|
style={imageStyle}
|
|
/>
|
|
);
|
|
} else {
|
|
iconStyle = [style.noIconContainer];
|
|
}
|
|
} else {
|
|
const compassIconStyle = [style.icon, iconStyles];
|
|
if (destructive) {
|
|
compassIconStyle.push(style.destructive);
|
|
}
|
|
image = (
|
|
<CompassIcon
|
|
name={icon}
|
|
size={24}
|
|
style={compassIconStyle}
|
|
/>
|
|
);
|
|
}
|
|
}
|
|
|
|
return {image, iconStyle};
|
|
};
|
|
|
|
export default SlideUpPanelItem;
|