test(mattermost): Detox 로그인 플로우를 안정화한다

This commit is contained in:
toki 2026-05-30 12:07:29 +09:00
parent 5c46726bea
commit 5d1c31fb7e
6 changed files with 109 additions and 31 deletions

View file

@ -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);

View file

@ -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();
};
}

View file

@ -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');

View file

@ -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()) {

View file

@ -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);

View file

@ -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<void> {
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<void> {
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<void> {
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<void> {
});
}
await prepareAndroidSystemUi();
console.info(`✅ App launched successfully on attempt ${attempt}`);
return;