mattermost-mobile-upstream/app/screens/report_a_problem/report_problem.tsx
yasser khan dfc94b3c3c
test(e2e): close manual release-test gaps (#10125)
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.
2026-09-11 09:48:02 +05:30

221 lines
8.1 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useEffect, useState} from 'react';
import {useIntl} from 'react-intl';
import {View, Text, ScrollView, Platform} from 'react-native';
import {SafeAreaView} from 'react-native-safe-area-context';
import {savePreference} from '@actions/remote/preference';
import Button from '@components/button';
import MenuDivider from '@components/menu_divider';
import SettingOption from '@components/settings/option';
import {Preferences, Screens} from '@constants';
import {DEFAULT_REPORT_A_PROBLEM_EMAIL} from '@constants/report_a_problem';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
import {navigateBack} from '@screens/navigation';
import {logDebug} from '@utils/log';
import {emailLogs, getDefaultReportAProblemLink, shareLogs} from '@utils/share_logs';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
import {tryOpenURL} from '@utils/url';
import AppLogs from './app_logs';
import CopyMetadata from './copy_metadata';
import {getCommonStyleSheet} from './styles';
import type {ReportAProblemMetadata} from '@typings/screens/report_a_problem';
type Props = {
reportAProblemMail?: string;
reportAProblemLink?: string;
siteName?: string;
allowDownloadLogs: boolean;
attachLogsEnabled: boolean;
currentUserId: string;
reportAProblemType?: string;
isFreeEdition: boolean;
metadata: ReportAProblemMetadata;
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => ({
...getCommonStyleSheet(theme),
container: {
flex: 1,
backgroundColor: theme.centerChannelBg,
paddingVertical: Platform.select({ios: 20, default: 0}),
gap: 20,
},
body: {
flex: 1,
},
content: {
gap: 16,
paddingHorizontal: 20,
},
detailsTitle: {
...typography('Heading', 200, 'SemiBold'),
color: theme.centerChannelColor,
},
detailsSection: {
gap: 8,
},
buttonContainer: {
paddingHorizontal: 20,
marginTop: 'auto',
borderTopWidth: 1,
borderColor: changeOpacity(theme.centerChannelColor, 0.08),
width: '100%',
paddingTop: 20,
marginBottom: Platform.select({ios: 20, default: 0}),
},
}));
const ReportProblem = ({
reportAProblemMail,
reportAProblemLink,
siteName,
allowDownloadLogs,
attachLogsEnabled,
currentUserId,
reportAProblemType,
isFreeEdition,
metadata,
}: Props) => {
const theme = useTheme();
const styles = getStyleSheet(theme);
const intl = useIntl();
const serverUrl = useServerUrl();
const [attachLogsToggle, setAttachLogsToggle] = useState(attachLogsEnabled);
useEffect(() => {
setAttachLogsToggle(attachLogsEnabled);
}, [attachLogsEnabled]);
const handleToggleAttachLogs = useCallback(async (value: boolean) => {
setAttachLogsToggle(value);
const {error} = await savePreference(serverUrl, [{
user_id: currentUserId,
category: Preferences.CATEGORIES.ADVANCED_SETTINGS,
name: Preferences.ATTACH_APP_LOGS,
value: String(value),
}]);
if (error) {
setAttachLogsToggle(!value);
}
}, [serverUrl, currentUserId]);
const handleReport = useCallback(async () => {
switch (reportAProblemType) {
case 'email':
await emailLogs(metadata, siteName, reportAProblemMail, !allowDownloadLogs);
return;
case 'link': {
let linkToUse = reportAProblemLink;
if (!linkToUse) {
logDebug('Report a problem link is not set');
linkToUse = getDefaultReportAProblemLink(!isFreeEdition);
}
tryOpenURL(linkToUse);
return;
}
case 'default': {
if (isFreeEdition) {
tryOpenURL(getDefaultReportAProblemLink(false));
} else {
await emailLogs(metadata, siteName, DEFAULT_REPORT_A_PROBLEM_EMAIL, !allowDownloadLogs);
}
return;
}
case 'hidden':
return;
}
// Old servers where reportAProblemType is not defined
if (!reportAProblemLink) {
await shareLogs(metadata, siteName, undefined, false);
return;
}
tryOpenURL(reportAProblemLink);
}, [reportAProblemType, reportAProblemLink, reportAProblemMail, metadata, siteName, allowDownloadLogs, isFreeEdition]);
useAndroidHardwareBackHandler(Screens.REPORT_PROBLEM, navigateBack);
const descriptionText = allowDownloadLogs ? intl.formatMessage({
id: 'screen.report_problem.details.description',
defaultMessage: 'When reporting a problem, share the metadata and app logs given below to help troubleshoot your problem faster',
}) : intl.formatMessage({
id: 'screen.report_problem.details.description_without_logs',
defaultMessage: 'When reporting a problem, share the metadata given below to help troubleshoot your problem faster',
});
return (
<SafeAreaView
style={styles.container}
testID='report_problem.screen'
>
<View style={styles.body}>
<ScrollView contentContainerStyle={styles.content}>
<View style={styles.detailsSection}>
<Text style={styles.detailsTitle}>
{intl.formatMessage({
id: 'screen.report_problem.details.title',
defaultMessage: 'Troubleshooting details',
})}
</Text>
<Text style={styles.bodyText}>
{descriptionText}
</Text>
</View>
<MenuDivider/>
<CopyMetadata
metadata={metadata}
componentId={Screens.REPORT_PROBLEM}
/>
{allowDownloadLogs && (
<>
<MenuDivider/>
<SettingOption
label={intl.formatMessage({
id: 'screen.report_problem.enable_log_attachments',
defaultMessage: 'Enable app log attachments',
})}
description={intl.formatMessage({
id: 'screen.report_problem.enable_log_attachments.description',
defaultMessage: 'Show an option to attach app logs in the file attachment menu',
})}
selected={attachLogsToggle}
type='toggle'
action={handleToggleAttachLogs}
testID='report_problem.enable_log_attachments'
/>
<MenuDivider/>
<AppLogs/>
</>
)}
</ScrollView>
</View>
{reportAProblemType !== 'hidden' && (
<View style={styles.buttonContainer}>
<Button
theme={theme}
text={intl.formatMessage({
id: 'screen.report_problem.button',
defaultMessage: 'Report a problem',
})}
onPress={handleReport}
iconName='open-in-new'
size='lg'
isIconOnTheRight={true}
testID='report_problem.report.button'
/>
</View>
)}
</SafeAreaView>
);
};
export default ReportProblem;