Test infra: shared accessibility IDs + PageObjects + AAA_SeedTests

Ports iOS HoneyDueUITests AccessibilityIdentifiers + PageObjects pattern
to Android Compose UI Test. Kotlin AccessibilityIds object mirrors Swift
verbatim so scripts/verify_test_tag_parity.sh can gate on divergence.

AAA_SeedTests bracketed first alphanumerically; SuiteZZ cleanup to follow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey T
2026-04-18 14:19:37 -05:00
parent 42c21bfca1
commit 2d80ade6bc
12 changed files with 1076 additions and 0 deletions

View File

@@ -0,0 +1,177 @@
package com.tt.honeyDue
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.tt.honeyDue.data.DataManager
import com.tt.honeyDue.data.PersistenceManager
import com.tt.honeyDue.fixtures.TestResidence
import com.tt.honeyDue.fixtures.TestTask
import com.tt.honeyDue.fixtures.TestUser
import com.tt.honeyDue.models.LoginRequest
import com.tt.honeyDue.models.RegisterRequest
import com.tt.honeyDue.network.APILayer
import com.tt.honeyDue.network.ApiResult
import com.tt.honeyDue.storage.TaskCacheManager
import com.tt.honeyDue.storage.TaskCacheStorage
import com.tt.honeyDue.storage.ThemeStorageManager
import com.tt.honeyDue.storage.TokenManager
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
/**
* Phase 1 — Seed tests run sequentially before the parallel suites.
*
* Ports `iosApp/HoneyDueUITests/AAA_SeedTests.swift`. The AAA prefix keeps
* these tests alphabetically first under JUnit's default sorter so seed
* state (a verified test user, a residence, a task) exists before
* `Suite*` tests run in parallel. `SuiteZZ_CleanupTests` (future) removes
* the leftover data at the end of a run.
*
* These hit the real dev backend configured in `ApiConfig.CURRENT_ENV`.
* If the backend is unreachable the tests fail fast — no silent skip.
*/
@RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
class AAA_SeedTests {
private val testUser: TestUser = TestUser.seededTestUser()
private val adminUser: TestUser = TestUser.seededAdminUser()
@Before
fun setUp() {
val context = ApplicationProvider.getApplicationContext<Context>()
if (!DataManager.isInitializedValue()) {
// Mirror MainActivity.onCreate minus UI deps so APILayer has
// everything it needs to persist the auth token.
DataManager.initialize(
tokenMgr = TokenManager.getInstance(context),
themeMgr = ThemeStorageManager.getInstance(context),
persistenceMgr = PersistenceManager.getInstance(context),
)
}
// Task cache is consulted during prefetchAllData — initialize to
// avoid NPEs inside the APILayer success path.
TaskCacheStorage.initialize(TaskCacheManager.getInstance(context))
}
@Test
fun a01_seedTestUserCreated() = runBlocking {
// Try logging in first; account may already exist on the dev backend.
val loginResult = APILayer.login(
LoginRequest(username = testUser.username, password = testUser.password),
)
if (loginResult is ApiResult.Success) {
assertNotNull("Auth token must be populated after login", loginResult.data.token)
return@runBlocking
}
val registerResult = APILayer.register(
RegisterRequest(
username = testUser.username,
email = testUser.email,
password = testUser.password,
firstName = testUser.firstName,
lastName = testUser.lastName,
),
)
assertTrue(
"Expected to create seed testuser; got $registerResult",
registerResult is ApiResult.Success,
)
}
@Test
fun a02_seedAdminUserExists() = runBlocking {
val loginResult = APILayer.login(
LoginRequest(username = adminUser.username, password = adminUser.password),
)
if (loginResult is ApiResult.Success) {
assertNotNull("Auth token populated for admin login", loginResult.data.token)
return@runBlocking
}
val registerResult = APILayer.register(
RegisterRequest(
username = adminUser.username,
email = adminUser.email,
password = adminUser.password,
firstName = adminUser.firstName,
lastName = adminUser.lastName,
),
)
assertTrue(
"Expected to create seed admin; got $registerResult",
registerResult is ApiResult.Success,
)
}
@Test
fun a03_seedResidenceCreated() = runBlocking {
// Ensure we have a session for the test user.
val loginResult = APILayer.login(
LoginRequest(username = testUser.username, password = testUser.password),
)
assertTrue(
"Must be logged in as testuser before creating residence",
loginResult is ApiResult.Success,
)
val residenceResult = APILayer.createResidence(
TestResidence.house().toCreateRequest(),
)
assertTrue(
"Expected to create seed residence; got $residenceResult",
residenceResult is ApiResult.Success,
)
}
@Test
fun a04_seedTaskCreatedOnResidence() = runBlocking {
val loginResult = APILayer.login(
LoginRequest(username = testUser.username, password = testUser.password),
)
assertTrue(
"Must be logged in as testuser before creating task",
loginResult is ApiResult.Success,
)
// Use the first residence that comes back from `prefetchAllData`, which
// APILayer.login already kicked off. Fall back to creating one.
val residences = DataManager.residences.value
val residenceId = residences.firstOrNull()?.id
?: run {
val create = APILayer.createResidence(TestResidence.house().toCreateRequest())
(create as? ApiResult.Success)?.data?.id
?: error("Cannot create residence for task seed: $create")
}
val taskResult = APILayer.createTask(
TestTask.basic(residenceId = residenceId).toCreateRequest(),
)
assertTrue(
"Expected to create seed task; got $taskResult",
taskResult is ApiResult.Success,
)
}
// ---- Helpers ----
private fun DataManager.isInitializedValue(): Boolean {
// DataManager exposes `isInitialized` as a StateFlow<Boolean>.
return try {
val field = DataManager::class.java.getDeclaredField("_isInitialized")
field.isAccessible = true
@Suppress("UNCHECKED_CAST")
val flow = field.get(DataManager) as kotlinx.coroutines.flow.MutableStateFlow<Boolean>
flow.value
} catch (e: Throwable) {
false
}
}
}

View File

@@ -0,0 +1,115 @@
package com.tt.honeyDue
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import com.tt.honeyDue.testing.AccessibilityIds
import com.tt.honeyDue.ui.screens.MainTabScreen
import com.tt.honeyDue.ui.screens.Screens
/**
* Reusable helpers that mirror `iosApp/HoneyDueUITests/UITestHelpers.swift`.
*
* Each helper drives off [com.tt.honeyDue.testing.AccessibilityIds] so the
* same semantic contract holds across iOS and Android. When the production
* app changes authentication or nav flow, update these helpers rather than
* every individual test.
*/
object UITestHelpers {
/** Default credentials for the seeded "testuser" account (matches iOS). */
const val DEFAULT_TEST_USERNAME = "testuser"
const val DEFAULT_TEST_PASSWORD = "TestPass123!"
private fun tagNode(rule: ComposeTestRule, testTag: String): SemanticsNodeInteraction =
rule.onNodeWithTag(testTag, useUnmergedTree = true)
/** Non-throwing existence check for a test-tag semantics node. */
private fun exists(rule: ComposeTestRule, testTag: String): Boolean = try {
tagNode(rule, testTag).assertExists()
true
} catch (e: AssertionError) {
false
}
/** Waits up to [timeoutMs] for a semantics node with [testTag] to exist. */
private fun waitForTag(
rule: ComposeTestRule,
testTag: String,
timeoutMs: Long = 10_000L,
): Boolean = try {
rule.waitUntil(timeoutMs) { exists(rule, testTag) }
true
} catch (e: Throwable) {
false
}
private fun isOnLoginScreen(rule: ComposeTestRule): Boolean =
exists(rule, AccessibilityIds.Authentication.usernameField)
private fun isLoggedIn(rule: ComposeTestRule): Boolean =
exists(rule, AccessibilityIds.Navigation.residencesTab)
/**
* Logs out if currently signed in. Noop if already on the login screen.
*/
fun logout(rule: ComposeTestRule) {
if (waitForTag(rule, AccessibilityIds.Authentication.usernameField, timeoutMs = 2_000L)) return
if (!isLoggedIn(rule)) return
val tabs = MainTabScreen(rule)
tabs.goToSettings()
// Some builds back the logout behind an outer profile tab instead;
// either path converges on the `Profile.LogoutButton` test tag.
if (waitForTag(rule, AccessibilityIds.Profile.logoutButton, timeoutMs = 5_000L)) {
tabs.tapLogout()
}
// Wait until we transition back to login (15s budget matches iOS).
waitForTag(rule, AccessibilityIds.Authentication.usernameField, timeoutMs = 15_000L)
}
/**
* Logs in using the provided credentials. Waits for the main tabs to
* appear before returning. Throws if the tab bar never shows up.
*/
fun loginAsTestUser(
rule: ComposeTestRule,
username: String = DEFAULT_TEST_USERNAME,
password: String = DEFAULT_TEST_PASSWORD,
): MainTabScreen {
ensureOnLoginScreen(rule)
val tabs = Screens.login(rule).login(username, password)
waitForTag(rule, AccessibilityIds.Navigation.residencesTab, timeoutMs = 15_000L)
return tabs
}
/**
* Best-effort navigation to the login screen from whatever state the app
* is in. If we are already logged in, logs the user out first.
*/
fun ensureOnLoginScreen(rule: ComposeTestRule) {
if (isOnLoginScreen(rule)) return
if (isLoggedIn(rule)) {
logout(rule)
if (isOnLoginScreen(rule)) return
}
// Onboarding flow: tap the "login" affordance if present.
if (exists(rule, AccessibilityIds.Onboarding.loginButton)) {
tagNode(rule, AccessibilityIds.Onboarding.loginButton).performClick()
}
waitForTag(rule, AccessibilityIds.Authentication.usernameField, timeoutMs = 20_000L)
}
/**
* Resets the UI to a known-good starting state. Called from test
* teardown so residual state from one test doesn't poison the next.
*/
fun tearDown(rule: ComposeTestRule) {
try {
logout(rule)
} catch (t: Throwable) {
// Swallow — teardown must never fail a test.
}
}
}

View File

@@ -0,0 +1,60 @@
package com.tt.honeyDue.fixtures
import com.tt.honeyDue.models.ResidenceCreateRequest
import kotlin.random.Random
/**
* Test residence fixture mirroring `TestFixtures.TestResidence` in Swift.
* Produces the exact payload shape the Go API expects so seed tests can
* call `APILayer.createResidence(fixture.toCreateRequest())`.
*/
data class TestResidence(
val name: String,
val streetAddress: String,
val city: String,
val stateProvince: String,
val postalCode: String,
val country: String = "USA",
val bedrooms: Int? = null,
val bathrooms: Double? = null,
val isPrimary: Boolean = false,
) {
fun toCreateRequest(): ResidenceCreateRequest = ResidenceCreateRequest(
name = name,
streetAddress = streetAddress,
city = city,
stateProvince = stateProvince,
postalCode = postalCode,
country = country,
bedrooms = bedrooms,
bathrooms = bathrooms,
isPrimary = isPrimary,
)
companion object {
fun house(suffix: String = randomSuffix()): TestResidence = TestResidence(
name = "Test House $suffix",
streetAddress = "123 Test St",
city = "Testville",
stateProvince = "CA",
postalCode = "94000",
bedrooms = 3,
bathrooms = 2.0,
isPrimary = true,
)
fun apartment(suffix: String = randomSuffix()): TestResidence = TestResidence(
name = "Test Apt $suffix",
streetAddress = "456 Mock Ave",
city = "Testville",
stateProvince = "CA",
postalCode = "94001",
bedrooms = 1,
bathrooms = 1.0,
isPrimary = false,
)
private fun randomSuffix(): String =
Random.nextInt(1000, 9999).toString()
}
}

View File

@@ -0,0 +1,51 @@
package com.tt.honeyDue.fixtures
import com.tt.honeyDue.models.TaskCreateRequest
import kotlin.random.Random
/**
* Test task fixture mirroring `TestFixtures.TestTask` in Swift.
*
* The Go API requires a residenceId and assigns category/priority IDs from
* DataManager lookups — callers pass the ID of a seeded test residence plus
* optional lookup IDs after a prefetch.
*/
data class TestTask(
val title: String,
val description: String,
val residenceId: Int,
val priorityId: Int? = null,
val categoryId: Int? = null,
val estimatedCost: Double? = null,
) {
fun toCreateRequest(): TaskCreateRequest = TaskCreateRequest(
residenceId = residenceId,
title = title,
description = description,
categoryId = categoryId,
priorityId = priorityId,
estimatedCost = estimatedCost,
)
companion object {
fun basic(residenceId: Int, suffix: String = randomSuffix()): TestTask = TestTask(
title = "Test Task $suffix",
description = "A test task",
residenceId = residenceId,
)
fun urgent(
residenceId: Int,
priorityId: Int? = null,
suffix: String = randomSuffix(),
): TestTask = TestTask(
title = "Urgent Task $suffix",
description = "An urgent task",
residenceId = residenceId,
priorityId = priorityId,
)
private fun randomSuffix(): String =
Random.nextInt(1000, 9999).toString()
}
}

View File

@@ -0,0 +1,48 @@
package com.tt.honeyDue.fixtures
import kotlin.random.Random
/**
* Test user fixture mirroring `TestFixtures.TestUser` in Swift.
*
* `seededTestUser()` yields the known-good backend account that
* `AAA_SeedTests` ensures exists before the parallel suites run.
*/
data class TestUser(
val username: String,
val email: String,
val password: String,
val firstName: String = "Test",
val lastName: String = "User",
) {
companion object {
/** Pre-existing user seeded against the dev backend. */
fun seededTestUser(): TestUser = TestUser(
username = "testuser",
email = "testuser@honeydue.com",
password = "TestPass123!",
)
/** Admin account used by admin-gated flows. */
fun seededAdminUser(): TestUser = TestUser(
username = "admin",
email = "admin@honeydue.com",
password = "Test1234",
)
/**
* Unique, ephemeral user used for registration flows that cannot
* re-use an existing account. Cleaned up by `SuiteZZ_CleanupTests`.
*/
fun ephemeralUser(suffix: String = randomSuffix()): TestUser = TestUser(
username = "uitest_$suffix",
email = "uitest_$suffix@test.com",
password = "TestPassword123!",
firstName = "Test",
lastName = "User",
)
private fun randomSuffix(): String =
Random.nextInt(100_000, 999_999).toString()
}
}

View File

@@ -0,0 +1,85 @@
package com.tt.honeyDue.ui
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
/**
* Base class for Android Compose UI test page objects.
*
* Mirrors `iosApp/HoneyDueUITests/PageObjects/BaseScreen.swift`: provides
* condition-based waits so screen objects can interact with Compose nodes
* without reaching for ad-hoc `Thread.sleep` calls.
*/
abstract class BaseScreen(protected val rule: ComposeTestRule) {
/** Returns a node interaction for the given test tag (unmerged tree for testability). */
protected fun tag(testTag: String): SemanticsNodeInteraction =
rule.onNodeWithTag(testTag, useUnmergedTree = true)
/** Returns a node interaction for the given display text. */
protected fun text(value: String): SemanticsNodeInteraction =
rule.onNodeWithText(value, useUnmergedTree = true)
/** Waits until a node with [testTag] exists in the semantics tree. */
protected fun waitFor(testTag: String, timeoutMs: Long = DEFAULT_TIMEOUT_MS) {
rule.waitUntil(timeoutMs) {
try {
tag(testTag).assertExists()
true
} catch (e: AssertionError) {
false
}
}
}
/** Waits until a node with the given visible [value] text exists. */
protected fun waitForText(value: String, timeoutMs: Long = DEFAULT_TIMEOUT_MS) {
rule.waitUntil(timeoutMs) {
try {
text(value).assertExists()
true
} catch (e: AssertionError) {
false
}
}
}
/** Waits until a node with [testTag] is actually displayed (not just present). */
protected fun waitForDisplayed(testTag: String, timeoutMs: Long = DEFAULT_TIMEOUT_MS) {
rule.waitUntil(timeoutMs) {
try {
tag(testTag).assertIsDisplayed()
true
} catch (e: AssertionError) {
false
}
}
}
/** Non-throwing existence check. */
protected fun exists(testTag: String): Boolean = try {
tag(testTag).assertExists()
true
} catch (e: AssertionError) {
false
}
/** Non-throwing text existence check. */
protected fun textExists(value: String): Boolean = try {
text(value).assertExists()
true
} catch (e: AssertionError) {
false
}
/** Subclasses report whether their screen is currently visible. */
abstract fun isDisplayed(): Boolean
companion object {
const val DEFAULT_TIMEOUT_MS: Long = 10_000L
const val SHORT_TIMEOUT_MS: Long = 5_000L
}
}

View File

@@ -0,0 +1,64 @@
package com.tt.honeyDue.ui.screens
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextInput
import com.tt.honeyDue.testing.AccessibilityIds
import com.tt.honeyDue.ui.BaseScreen
/**
* Page object for the login screen.
* Mirrors `iosApp/HoneyDueUITests/PageObjects/LoginScreen.swift`.
*/
class LoginScreen(rule: ComposeTestRule) : BaseScreen(rule) {
fun enterUsername(value: String): LoginScreen {
waitFor(AccessibilityIds.Authentication.usernameField)
tag(AccessibilityIds.Authentication.usernameField).performTextInput(value)
return this
}
fun enterPassword(value: String): LoginScreen {
waitFor(AccessibilityIds.Authentication.passwordField)
tag(AccessibilityIds.Authentication.passwordField).performTextInput(value)
return this
}
fun tapLogin(): MainTabScreen {
waitFor(AccessibilityIds.Authentication.loginButton)
tag(AccessibilityIds.Authentication.loginButton).performClick()
return MainTabScreen(rule)
}
fun tapSignUp(): RegisterScreen {
waitFor(AccessibilityIds.Authentication.signUpButton)
tag(AccessibilityIds.Authentication.signUpButton).performClick()
return RegisterScreen(rule)
}
fun tapForgotPassword(): LoginScreen {
waitFor(AccessibilityIds.Authentication.forgotPasswordButton)
tag(AccessibilityIds.Authentication.forgotPasswordButton).performClick()
return this
}
fun togglePasswordVisibility(): LoginScreen {
waitFor(AccessibilityIds.Authentication.passwordVisibilityToggle)
tag(AccessibilityIds.Authentication.passwordVisibilityToggle).performClick()
return this
}
/** Convenience: enter credentials and submit. */
fun login(username: String, password: String): MainTabScreen {
enterUsername(username)
enterPassword(password)
return tapLogin()
}
/** Waits for the main tab bar to appear post-login. */
fun waitForMainTabs(timeoutMs: Long = DEFAULT_TIMEOUT_MS) {
waitFor(AccessibilityIds.Navigation.residencesTab, timeoutMs)
}
override fun isDisplayed(): Boolean = exists(AccessibilityIds.Authentication.usernameField)
}

View File

@@ -0,0 +1,63 @@
package com.tt.honeyDue.ui.screens
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.performClick
import com.tt.honeyDue.testing.AccessibilityIds
import com.tt.honeyDue.ui.BaseScreen
/**
* Page object for the main tab scaffold visible after login.
* Mirrors `iosApp/HoneyDueUITests/PageObjects/MainTabScreen.swift`.
*/
class MainTabScreen(rule: ComposeTestRule) : BaseScreen(rule) {
fun tapResidencesTab(): MainTabScreen {
waitFor(AccessibilityIds.Navigation.residencesTab)
tag(AccessibilityIds.Navigation.residencesTab).performClick()
return this
}
fun tapTasksTab(): MainTabScreen {
waitFor(AccessibilityIds.Navigation.tasksTab)
tag(AccessibilityIds.Navigation.tasksTab).performClick()
return this
}
fun tapContractorsTab(): MainTabScreen {
waitFor(AccessibilityIds.Navigation.contractorsTab)
tag(AccessibilityIds.Navigation.contractorsTab).performClick()
return this
}
fun tapDocumentsTab(): MainTabScreen {
waitFor(AccessibilityIds.Navigation.documentsTab)
tag(AccessibilityIds.Navigation.documentsTab).performClick()
return this
}
fun tapProfileTab(): MainTabScreen {
waitFor(AccessibilityIds.Navigation.profileTab)
tag(AccessibilityIds.Navigation.profileTab).performClick()
return this
}
/** Opens the settings/profile sheet via the settings affordance. */
fun goToSettings(): MainTabScreen {
tapResidencesTab()
waitFor(AccessibilityIds.Navigation.settingsButton)
tag(AccessibilityIds.Navigation.settingsButton).performClick()
return this
}
/**
* Taps logout from the profile sheet. Caller is responsible for waiting
* on the logout confirmation dialog if the app shows one.
*/
fun tapLogout(): MainTabScreen {
waitFor(AccessibilityIds.Profile.logoutButton)
tag(AccessibilityIds.Profile.logoutButton).performClick()
return this
}
override fun isDisplayed(): Boolean = exists(AccessibilityIds.Navigation.residencesTab)
}

View File

@@ -0,0 +1,63 @@
package com.tt.honeyDue.ui.screens
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextInput
import com.tt.honeyDue.testing.AccessibilityIds
import com.tt.honeyDue.ui.BaseScreen
/**
* Page object for the registration screen.
* Mirrors `iosApp/HoneyDueUITests/PageObjects/RegisterScreen.swift`.
*/
class RegisterScreen(rule: ComposeTestRule) : BaseScreen(rule) {
fun enterUsername(value: String): RegisterScreen {
waitFor(AccessibilityIds.Authentication.registerUsernameField)
tag(AccessibilityIds.Authentication.registerUsernameField).performTextInput(value)
return this
}
fun enterEmail(value: String): RegisterScreen {
waitFor(AccessibilityIds.Authentication.registerEmailField)
tag(AccessibilityIds.Authentication.registerEmailField).performTextInput(value)
return this
}
fun enterPassword(value: String): RegisterScreen {
waitFor(AccessibilityIds.Authentication.registerPasswordField)
tag(AccessibilityIds.Authentication.registerPasswordField).performTextInput(value)
return this
}
fun enterConfirmPassword(value: String): RegisterScreen {
waitFor(AccessibilityIds.Authentication.registerConfirmPasswordField)
tag(AccessibilityIds.Authentication.registerConfirmPasswordField).performTextInput(value)
return this
}
fun tapRegister(): MainTabScreen {
waitFor(AccessibilityIds.Authentication.registerButton)
tag(AccessibilityIds.Authentication.registerButton).performClick()
return MainTabScreen(rule)
}
fun tapCancel(): LoginScreen {
waitFor(AccessibilityIds.Authentication.registerCancelButton)
tag(AccessibilityIds.Authentication.registerCancelButton).performClick()
return LoginScreen(rule)
}
/** Convenience: fill out the form and submit. */
fun register(username: String, email: String, password: String): MainTabScreen {
enterUsername(username)
enterEmail(email)
enterPassword(password)
enterConfirmPassword(password)
return tapRegister()
}
override fun isDisplayed(): Boolean =
exists(AccessibilityIds.Authentication.registerUsernameField) ||
exists(AccessibilityIds.Authentication.registerEmailField)
}

View File

@@ -0,0 +1,13 @@
package com.tt.honeyDue.ui.screens
import androidx.compose.ui.test.junit4.ComposeTestRule
/**
* Factory helpers for page objects — keeps test bodies concise.
* Mirrors `iosApp/HoneyDueUITests/PageObjects/Screens.swift`.
*/
object Screens {
fun login(rule: ComposeTestRule): LoginScreen = LoginScreen(rule)
fun register(rule: ComposeTestRule): RegisterScreen = RegisterScreen(rule)
fun mainTabs(rule: ComposeTestRule): MainTabScreen = MainTabScreen(rule)
}