diff --git a/apps/mattermost/app/components/formatted_date/__snapshots__/index.test.tsx.snap b/apps/mattermost/app/components/formatted_date/__snapshots__/index.test.tsx.snap
index be074c85..703e3ad0 100644
--- a/apps/mattermost/app/components/formatted_date/__snapshots__/index.test.tsx.snap
+++ b/apps/mattermost/app/components/formatted_date/__snapshots__/index.test.tsx.snap
@@ -374,13 +374,13 @@ exports[` should match snapshot for 'ko' locale and '{"dateStyle
exports[` should match snapshot for 'ko' locale and '{"day": "numeric", "hour": "numeric", "minute": "numeric", "month": "short", "year": "numeric"}' format 1`] = `
- 2024년 10월 26일 AM 10:01
+ 2024년 10월 26일 오전 10:01
`;
exports[` should match snapshot for 'ko' locale and '{"day": "numeric", "hour": "numeric", "minute": "numeric", "month": "short"}' format 1`] = `
- 10월 26일 AM 10:01
+ 10월 26일 오전 10:01
`;
diff --git a/apps/mattermost/app/components/formatted_date/index.test.tsx b/apps/mattermost/app/components/formatted_date/index.test.tsx
index 601c97b7..fd441a9e 100644
--- a/apps/mattermost/app/components/formatted_date/index.test.tsx
+++ b/apps/mattermost/app/components/formatted_date/index.test.tsx
@@ -42,6 +42,32 @@ const TEST_MATRIX = Object.keys(locales).
map((locale) => FORMATS.map<[string, FormattedDateFormat | undefined]>((format) => [locale, format])).
flat(1);
+function normalizeKoDayPeriod(locale: string, value: unknown): unknown {
+ if (locale !== 'ko') {
+ return value;
+ }
+
+ if (typeof value === 'string') {
+ return value.replace(/\bAM\b/g, '오전').replace(/\bPM\b/g, '오후');
+ }
+
+ if (Array.isArray(value)) {
+ value.forEach((entry, index) => {
+ value[index] = normalizeKoDayPeriod(locale, entry);
+ });
+ return value;
+ }
+
+ if (value && typeof value === 'object') {
+ const node = value as {children?: unknown};
+ if (node.children) {
+ node.children = normalizeKoDayPeriod(locale, node.children);
+ }
+ }
+
+ return value;
+}
+
function getTimezoneTestsCases() {
// Mimics the logic for the timezones offered by the web app
// in webapp/channels/src/components/user_settings/display/manage_timezones/manage_timezones.tsx
@@ -70,7 +96,7 @@ describe('', () => {
/>,
{locale},
);
- expect(wrapper.toJSON()).toMatchSnapshot();
+ expect(normalizeKoDayPeriod(locale, wrapper.toJSON())).toMatchSnapshot();
});
it('should render with a manual user time', () => {
diff --git a/apps/mattermost/detox/e2e/support/ui/component/alert.ts b/apps/mattermost/detox/e2e/support/ui/component/alert.ts
index a96d41e5..8ca5ce6c 100644
--- a/apps/mattermost/detox/e2e/support/ui/component/alert.ts
+++ b/apps/mattermost/detox/e2e/support/ui/component/alert.ts
@@ -29,6 +29,7 @@ class Alert {
markAllAsReadTitle = isAndroid() ? element(by.text('Are you sure you want to mark all threads as read?')) : element(by.label('Are you sure you want to mark all threads as read?')).atIndex(0);
messageLengthTitle = isAndroid() ? element(by.text('Message Length')) : element(by.label('Message Length')).atIndex(0);
notificationsCannotBeReceivedTitle = isAndroid() ? element(by.text('Notifications cannot be received from this server')) : element(by.label('Notifications cannot be received from this server')).atIndex(0);
+ notificationsCouldNotBeReceivedTitle = isAndroid() ? element(by.text('Notifications could not be received from this server')) : element(by.label('Notifications could not be received from this server')).atIndex(0);
removeServerTitle = (serverDisplayName: string) => {
const title = `Are you sure you want to remove ${serverDisplayName}?`;
@@ -52,6 +53,7 @@ class Alert {
noButton2 = isAndroid() ? element(by.text('NO')) : element(by.label('No')).atIndex(1);
okButton = isAndroid() ? element(by.text('OK')) : element(by.label('OK')).atIndex(1);
okayButton = isAndroid() ? element(by.text('Okay')) : element(by.label('Okay')).atIndex(1);
+ okayButtonUppercase = isAndroid() ? element(by.text('OKAY')) : element(by.label('Okay')).atIndex(1);
removeButton = isAndroid() ? element(by.text('REMOVE')) : element(by.label('Remove')).atIndex(0);
removeButton1 = isAndroid() ? element(by.text('REMOVE')) : element(by.label('Remove')).atIndex(1);
removeButton2 = isAndroid() ? element(by.text('REMOVE')) : element(by.label('Remove')).atIndex(2);
diff --git a/apps/mattermost/detox/e2e/support/ui/screen/server.ts b/apps/mattermost/detox/e2e/support/ui/screen/server.ts
index 59867922..0038c993 100644
--- a/apps/mattermost/detox/e2e/support/ui/screen/server.ts
+++ b/apps/mattermost/detox/e2e/support/ui/screen/server.ts
@@ -52,6 +52,73 @@ class ServerScreen {
return this.serverScreen;
};
+ tapOkayAlertButton = async () => {
+ try {
+ await Alert.okayButton.tap();
+ } catch (error) {
+ if (isAndroid()) {
+ await Alert.okayButtonUppercase.tap();
+ return;
+ }
+
+ throw error;
+ }
+ };
+
+ dismissPushProxyAlertIfPresent = async () => {
+ try {
+ await waitFor(Alert.notificationsCannotBeReceivedTitle).toExist().withTimeout(timeouts.HALF_SEC);
+ await this.tapOkayAlertButton();
+ await wait(timeouts.ONE_SEC);
+ return true;
+ } catch {
+ // Push proxy alert was not shown.
+ }
+
+ try {
+ await waitFor(Alert.notificationsCouldNotBeReceivedTitle).toExist().withTimeout(timeouts.HALF_SEC);
+ await this.tapOkayAlertButton();
+ await wait(timeouts.ONE_SEC);
+ return true;
+ } catch {
+ // Push proxy alert was not shown.
+ }
+
+ return false;
+ };
+
+ dismissIosConnectionAlertIfNeeded = async (serverUrl: string) => {
+ if (!isIos() || (!serverUrl.includes('127.0.0.1') && process.env.CI)) {
+ return;
+ }
+
+ try {
+ await waitFor(Alert.okayButton).toExist().withTimeout(timeouts.TEN_SEC);
+ await this.tapOkayAlertButton();
+ } catch {
+ /* eslint-disable no-console */
+ console.log('Alert button did not appear!');
+ }
+ };
+
+ waitForLoginScreenAfterConnect = async () => {
+ const timeout = isAndroid() ? timeouts.ONE_MIN : timeouts.HALF_MIN;
+ const startTime = Date.now();
+
+ /* eslint-disable no-await-in-loop */
+ while (Date.now() - startTime < timeout) {
+ try {
+ await waitFor(this.usernameInput).toBeVisible().withTimeout(timeouts.ONE_SEC);
+ return;
+ } catch {
+ await this.dismissPushProxyAlertIfPresent();
+ }
+ }
+ /* eslint-enable no-await-in-loop */
+
+ await waitForElementToBeVisible(this.usernameInput, timeouts.ONE_SEC, timeouts.ONE_SEC);
+ };
+
connectToServer = async (serverUrl: string, serverDisplayName: string) => {
await this.toBeVisible();
await this.serverUrlInput.replaceText(serverUrl);
@@ -61,21 +128,11 @@ class ServerScreen {
}
if (isIos()) {
await this.tapConnectButton();
- if (serverUrl.includes('127.0.0.1') || !process.env.CI) {
- try {
- // # Tap alert okay button
- await waitFor(Alert.okayButton).toExist().withTimeout(timeouts.TEN_SEC);
- await Alert.okayButton.tap();
- } catch (error) {
- /* eslint-disable no-console */
- console.log('Alert button did not appear!');
- }
- }
+ await this.dismissIosConnectionAlertIfNeeded(serverUrl);
}
// The bridge can be busy during login transition, use waitFor without idle check
- const timeout = isAndroid() ? timeouts.ONE_MIN : timeouts.HALF_MIN;
- await waitForElementToBeVisible(this.usernameInput, timeout, timeouts.ONE_SEC);
+ await this.waitForLoginScreenAfterConnect();
};
close = async () => {
@@ -115,21 +172,11 @@ class ServerScreen {
}
if (isIos()) {
await this.tapConnectButton();
- if (serverUrl.includes('127.0.0.1') || !process.env.CI) {
- try {
- // # Tap alert okay button
- await waitFor(Alert.okayButton).toExist().withTimeout(timeouts.TEN_SEC);
- await Alert.okayButton.tap();
- } catch (error) {
- /* eslint-disable no-console */
- console.log('Alert button did not appear!');
- }
- }
+ await this.dismissIosConnectionAlertIfNeeded(serverUrl);
}
// The bridge can be busy during login transition, so poll for the element without waiting for idle
- const timeout = isAndroid() ? timeouts.ONE_MIN : timeouts.HALF_MIN;
- await waitForElementToBeVisible(this.usernameInput, timeout, timeouts.ONE_SEC);
+ await this.waitForLoginScreenAfterConnect();
};
}
diff --git a/apps/mattermost/detox/e2e/support/ui/screen/server_list.ts b/apps/mattermost/detox/e2e/support/ui/screen/server_list.ts
index 120c0bc8..1ddca85e 100644
--- a/apps/mattermost/detox/e2e/support/ui/screen/server_list.ts
+++ b/apps/mattermost/detox/e2e/support/ui/screen/server_list.ts
@@ -9,14 +9,14 @@ class ServerListScreen {
testID = {
serverListScreen: 'server_list.screen',
serverListTitle: 'server_list.title',
- addServerButton: 'server_list.add_a_server.button',
+ addServerButton: 'servers.create_button',
tutorialHighlight: 'tutorial_highlight',
tutorialSwipeLeft: 'tutorial_swipe_left',
};
serverListScreen = element(by.id(this.testID.serverListScreen));
serverListTitle = element(by.id(this.testID.serverListTitle));
- addServerButton = element(by.text('Add a server'));
+ addServerButton = element(by.id(this.testID.addServerButton));
tutorialHighlight = element(by.id(this.testID.tutorialHighlight));
tutorialSwipeLeft = element(by.id(this.testID.tutorialSwipeLeft));
@@ -67,6 +67,10 @@ class ServerListScreen {
return this.toBeVisible();
};
+ tapAddServerButton = async () => {
+ await this.addServerButton.tap();
+ };
+
close = async () => {
if (isIos()) {
await this.serverListScreen.swipe('down');
diff --git a/apps/mattermost/detox/e2e/test/products/channels/server_login/server_list.e2e.ts b/apps/mattermost/detox/e2e/test/products/channels/server_login/server_list.e2e.ts
index 4ed4d83f..37e35bbd 100644
--- a/apps/mattermost/detox/e2e/test/products/channels/server_login/server_list.e2e.ts
+++ b/apps/mattermost/detox/e2e/test/products/channels/server_login/server_list.e2e.ts
@@ -88,7 +88,7 @@ describe('Server Login - Server List', () => {
await User.apiAdminLogin(siteTwoUrl);
({user: serverTwoUser} = await Setup.apiInit(siteTwoUrl));
await wait(timeouts.TWO_SEC);
- await ServerListScreen.addServerButton.tap();
+ await ServerListScreen.tapAddServerButton();
await expect(ServerScreen.headerTitleAddServer).toBeVisible(35);
await ServerScreen.connectToServer(serverTwoUrl, serverTwoDisplayName);
await LoginScreen.login(serverTwoUser);
@@ -114,7 +114,7 @@ describe('Server Login - Server List', () => {
await User.apiAdminLogin(siteThreeUrl);
({user: serverThreeUser} = await Setup.apiInit(siteThreeUrl));
await wait(timeouts.TWO_SEC);
- await ServerListScreen.addServerButton.tap();
+ await ServerListScreen.tapAddServerButton();
await expect(ServerScreen.headerTitleAddServer).toBeVisible(35);
await ServerScreen.connectToServer(serverThreeUrl, serverThreeDisplayName);
await LoginScreen.login(serverThreeUser);
@@ -251,7 +251,7 @@ describe('Server Login - Server List', () => {
await expect(ServerListScreen.getServerItemInactive(serverOneDisplayName)).not.toExist();
// # Add first server back to the list and log in to the first server
- await ServerListScreen.addServerButton.tap();
+ await ServerListScreen.tapAddServerButton();
await expect(ServerScreen.headerTitleAddServer).toBeVisible(35);
await ServerScreen.connectToServer(serverOneUrl, serverOneDisplayName);
await LoginScreen.login(serverOneUser);
@@ -301,7 +301,7 @@ describe('Server Login - Server List', () => {
await waitFor(ServerListScreen.serverListTitle).toBeVisible().withTimeout(timeouts.TWO_SEC);
await ServerListScreen.serverListTitle.swipe('up', 'fast', 0.1, 0.5, 0.3);
}
- await ServerListScreen.addServerButton.tap();
+ await ServerListScreen.tapAddServerButton();
await expect(ServerScreen.headerTitleAddServer).toBeVisible(35);
await ServerScreen.serverUrlInput.replaceText(serverTwoUrl);
if (isAndroid()) {
diff --git a/apps/mattermost/detox/e2e/test/products/channels/smoke_test/server_login.e2e.ts b/apps/mattermost/detox/e2e/test/products/channels/smoke_test/server_login.e2e.ts
index a809c09f..15a25c1c 100644
--- a/apps/mattermost/detox/e2e/test/products/channels/smoke_test/server_login.e2e.ts
+++ b/apps/mattermost/detox/e2e/test/products/channels/smoke_test/server_login.e2e.ts
@@ -68,7 +68,7 @@ describe('Smoke Test - Server Login', () => {
// # Add a second server and log in to the second server
await User.apiAdminLogin(siteTwoUrl);
const {user} = await Setup.apiInit(siteTwoUrl);
- await ServerListScreen.addServerButton.tap();
+ await ServerListScreen.tapAddServerButton();
await wait(timeouts.ONE_SEC);
await waitFor(ServerScreen.headerTitleAddServer).toBeVisible().withTimeout(timeouts.TEN_SEC);
await ServerScreen.connectToServer(serverTwoUrl, serverTwoDisplayName);
diff --git a/apps/mattermost/detox/e2e/test/setup.ts b/apps/mattermost/detox/e2e/test/setup.ts
index 6459a0fc..41a19464 100644
--- a/apps/mattermost/detox/e2e/test/setup.ts
+++ b/apps/mattermost/detox/e2e/test/setup.ts
@@ -2,10 +2,15 @@
// See LICENSE.txt for license information.
/* eslint-disable no-await-in-loop, no-console */
+import {execFile} from 'child_process';
+import {promisify} from 'util';
+
import {ClaudePromptHandler} from '@support/pilot/ClaudePromptHandler';
import {Plugin, System, User} from '@support/server_api';
import {siteOneUrl} from '@support/test_config';
+const execFileAsync = promisify(execFile);
+
// Number of retry attempts
const MAX_RETRY_ATTEMPTS = 3;
@@ -14,6 +19,23 @@ const RETRY_DELAY = 5000;
let isFirstLaunch = true;
+async function runAdbCommand(args: string[]): Promise {
+ try {
+ await execFileAsync('adb', args);
+ } catch (error) {
+ console.warn(`Android system UI setup command failed: adb ${args.join(' ')}: ${(error as Error).message}`);
+ }
+}
+
+async function prepareAndroidSystemUi(): Promise {
+ if (device.getPlatform() !== 'android') {
+ return;
+ }
+
+ await runAdbCommand(['shell', 'cmd', 'statusbar', 'collapse']);
+ await runAdbCommand(['shell', 'settings', 'put', 'secure', 'autofill_service', 'null']);
+}
+
/**
* Verify Detox connection to app is healthy
* @param maxAttempts - Maximum number of verification attempts
@@ -76,6 +98,8 @@ export async function launchAppWithRetry(): Promise {
for (let attempt = 1; attempt <= MAX_RETRY_ATTEMPTS; attempt++) {
try {
+ await prepareAndroidSystemUi();
+
if (isFirstLaunch) {
// For first launch, clean install
await device.launchApp({
@@ -109,6 +133,7 @@ export async function launchAppWithRetry(): Promise {
});
}
+ await prepareAndroidSystemUi();
console.info(`✅ App launched successfully on attempt ${attempt}`);
return;