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;