compose-designer/testSrc/com/android/tools/idea/compose/preview/ComposePreviewRepresentationTest.kt (1,086 lines of code) (raw):

/* * Copyright (C) 2020 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.idea.compose.preview import com.android.testutils.delayUntilCondition import com.android.testutils.waitForCondition import com.android.tools.adtui.swing.findDescendant import com.android.tools.analytics.AnalyticsSettings import com.android.tools.idea.common.error.DesignerCommonIssuePanel import com.android.tools.idea.common.error.SharedIssuePanelProvider import com.android.tools.idea.common.surface.getDesignSurface import com.android.tools.idea.compose.ComposeProjectRule import com.android.tools.idea.compose.PsiComposePreviewElementInstance import com.android.tools.idea.compose.preview.actions.ReRunUiCheckModeAction import com.android.tools.idea.compose.preview.actions.UiCheckReopenTabAction import com.android.tools.idea.compose.preview.animation.ComposeAnimationSubscriber import com.android.tools.idea.compose.preview.resize.ResizePanel import com.android.tools.idea.compose.preview.util.previewElement import com.android.tools.idea.concurrency.AndroidDispatchers.uiThread import com.android.tools.idea.concurrency.AndroidDispatchers.workerThread import com.android.tools.idea.concurrency.asCollection import com.android.tools.idea.concurrency.awaitStatus import com.android.tools.idea.concurrency.coroutineScope import com.android.tools.idea.editors.build.RenderingBuildStatus import com.android.tools.idea.editors.fast.FastPreviewManager import com.android.tools.idea.flags.StudioFlags import com.android.tools.idea.preview.PreviewInvalidationManager import com.android.tools.idea.preview.actions.getPreviewManager import com.android.tools.idea.preview.analytics.PreviewRefreshTracker import com.android.tools.idea.preview.analytics.PreviewRefreshTrackerForTest import com.android.tools.idea.preview.fast.FastPreviewSurface import com.android.tools.idea.preview.flow.PreviewFlowManager import com.android.tools.idea.preview.groups.PreviewGroupManager import com.android.tools.idea.preview.modes.DEFAULT_LAYOUT_OPTION import com.android.tools.idea.preview.modes.PreviewMode import com.android.tools.idea.preview.modes.PreviewModeManager import com.android.tools.idea.preview.modes.UI_CHECK_LAYOUT_OPTION import com.android.tools.idea.preview.modes.UiCheckInstance import com.android.tools.idea.preview.mvvm.PREVIEW_VIEW_MODEL_STATUS import com.android.tools.idea.preview.mvvm.PreviewViewModelStatus import com.android.tools.idea.preview.uicheck.UiCheckModeFilter import com.android.tools.idea.projectsystem.ProjectSystemBuildManager import com.android.tools.idea.projectsystem.ProjectSystemService import com.android.tools.idea.projectsystem.TestProjectSystem import com.android.tools.idea.rendering.BuildTargetReference import com.android.tools.idea.rendering.tokens.FakeBuildSystemFilePreviewServices import com.android.tools.idea.run.configuration.execution.findElementByText import com.android.tools.idea.testing.addFileToProjectAndInvalidate import com.android.tools.idea.testing.flags.overrideForTest import com.android.tools.idea.uibuilder.editor.multirepresentation.PreferredVisibility import com.android.tools.idea.uibuilder.editor.multirepresentation.TextEditorWithMultiRepresentationPreview import com.android.tools.idea.uibuilder.editor.multirepresentation.sourcecode.SourceCodeEditorProvider import com.android.tools.idea.uibuilder.options.NlOptionsConfigurable import com.android.tools.idea.uibuilder.surface.NlDesignSurface import com.android.tools.idea.uibuilder.surface.NlSurfaceBuilder import com.android.tools.idea.util.TestToolWindowManager import com.google.common.base.Preconditions.checkState import com.google.common.truth.Truth.assertThat import com.google.wireless.android.sdk.stats.PreviewRefreshEvent import com.intellij.analysis.problemsView.toolWindow.ProblemsView import com.intellij.analysis.problemsView.toolWindow.ProblemsViewToolWindowUtils import com.intellij.ide.impl.HeadlessDataManager import com.intellij.openapi.actionSystem.DataProvider import com.intellij.openapi.actionSystem.impl.ActionToolbarImpl import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.EDT import com.intellij.openapi.application.ModalityState import com.intellij.openapi.application.asContextElement import com.intellij.openapi.application.runWriteActionAndWait import com.intellij.openapi.command.WriteCommandAction.runWriteCommandAction import com.intellij.openapi.diagnostic.LogLevel import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.fileEditor.FileEditorManagerKeys import com.intellij.openapi.fileEditor.FileEditorProvider import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx import com.intellij.openapi.fileEditor.impl.FileEditorManagerImpl import com.intellij.openapi.project.DumbService import com.intellij.openapi.util.Disposer import com.intellij.openapi.wm.RegisterToolWindowTask import com.intellij.openapi.wm.ToolWindowManager import com.intellij.psi.PsiFile import com.intellij.testFramework.TestActionEvent import com.intellij.testFramework.assertInstanceOf import com.intellij.testFramework.fixtures.CodeInsightTestFixture import com.intellij.testFramework.replaceService import com.intellij.testFramework.runInEdtAndWait import java.nio.file.Path import java.util.UUID import java.util.concurrent.CountDownLatch import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest import kotlinx.coroutines.withContext import org.jetbrains.android.uipreview.AndroidEditorSettings import org.jetbrains.android.uipreview.ModuleClassLoaderOverlays import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNotEquals import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule import org.junit.Test class ComposePreviewRepresentationTest { private val logger = Logger.getInstance(ComposePreviewRepresentationTest::class.java) @get:Rule val projectRule = ComposeProjectRule() private val project get() = projectRule.project private val fixture get() = projectRule.fixture private var composePreviewEssentialsModeEnabled: Boolean = false set(value) { if ( field == value && AndroidEditorSettings.getInstance().globalState.isPreviewEssentialsModeEnabled == value ) return runWriteActionAndWait { AndroidEditorSettings.getInstance().globalState.isPreviewEssentialsModeEnabled = value ApplicationManager.getApplication() .messageBus .syncPublisher(NlOptionsConfigurable.Listener.TOPIC) .onOptionsChanged() } field = value } @Before fun setup() { logger.setLevel(LogLevel.ALL) Logger.getInstance(ComposePreviewRepresentation::class.java).setLevel(LogLevel.ALL) Logger.getInstance(FastPreviewManager::class.java).setLevel(LogLevel.ALL) Logger.getInstance(RenderingBuildStatus::class.java).setLevel(LogLevel.ALL) logger.info("setup") val testProjectSystem = TestProjectSystem(project).apply { usesCompose = true } runInEdtAndWait { testProjectSystem.useInTests() } logger.info("setup complete") project.replaceService( ToolWindowManager::class.java, TestToolWindowManager(project), fixture.testRootDisposable, ) ToolWindowManager.getInstance(project) .registerToolWindow(RegisterToolWindowTask(ProblemsView.ID)) } @After fun tearDown() { StudioFlags.COMPOSE_UI_CHECK_FOR_WEAR.clearOverride() StudioFlags.COMPOSE_PREVIEW_RESIZING.clearOverride() composePreviewEssentialsModeEnabled = false } @Test fun testPreviewInitialization() = runComposePreviewRepresentationTest { val preview = createPreviewAndCompile() mainSurface.models.forEach { assertTrue(preview.navigationHandler.defaultNavigationMap.contains(it)) } // Animation should be disabled in Default and Focus modes mainSurface.sceneManagers.forEach { assertTrue(it.sceneRenderConfiguration.disableAnimation) } assertThat(preview.composePreviewFlowManager.availableGroupsFlow.value.map { it.displayName }) .containsExactly("groupA") val status = preview.status() val debugStatus = preview.debugStatusForTesting() assertFalse(debugStatus.toString(), status.isOutOfDate) // Ensure the only warning message is the missing Android SDK message assertTrue( debugStatus.renderResult .flatMap { it.logger.messages } .none { !it.html.contains("No Android SDK found.") } ) } @Test fun testPreviewRefreshMetricsAreTracked() = runComposePreviewRepresentationTest { try { AnalyticsSettings.optedIn = true var refreshTrackerFailed = false var successEventCount = 0 val refreshTracker = PreviewRefreshTrackerForTest { if ( it.result != PreviewRefreshEvent.RefreshResult.SUCCESS || it.previewRendersList.isEmpty() ) { return@PreviewRefreshTrackerForTest } try { assertTrue(it.hasInQueueTimeMillis()) assertTrue(it.hasRefreshTimeMillis()) assertTrue(it.hasType()) assertTrue(it.hasResult()) assertTrue(it.hasPreviewsCount()) assertTrue(it.hasPreviewsToRefresh()) assertTrue(it.previewRendersList.isNotEmpty()) assertTrue( it.previewRendersList.all { render -> render.hasResult() render.hasRenderTimeMillis() render.hasRenderQuality() render.hasInflate() } ) successEventCount++ } catch (t: Throwable) { refreshTrackerFailed = true } } PreviewRefreshTracker.setInstanceForTest(mainSurface, refreshTracker) createPreviewAndCompile() waitForCondition(5.seconds) { successEventCount > 0 } assertFalse(refreshTrackerFailed) } finally { PreviewRefreshTracker.cleanAfterTesting(mainSurface) AnalyticsSettings.optedIn = false } } @Test fun testUiCheckMode() = runComposePreviewRepresentationTest { val originalScale = 0.6 mainSurface.zoomController.setScale(originalScale) val preview = createPreviewAndCompile() assertInstanceOf<UiCheckModeFilter.Disabled<PsiComposePreviewElementInstance>>( preview.uiCheckFilterFlow.value ) val previewElements = mainSurface.models.mapNotNull { it.dataProvider?.previewElement() } val uiCheckElement = previewElements.single { it.methodFqn == "TestKt.Preview1" } val problemsView = ProblemsView.getToolWindow(project)!! val contentManager = runBlocking(uiThread) { problemsView.contentManager } withContext(uiThread) { ProblemsViewToolWindowUtils.addTab(project, SharedIssuePanelProvider(project)) assertEquals(1, contentManager.contents.size) } // Start UI Check mode setModeAndWaitForRefresh( PreviewMode.UiCheck(UiCheckInstance(uiCheckElement, isWearPreview = false)) ) // Animation should be enabled in not Default and not Focus modes mainSurface.sceneManagers.forEach { assertFalse(it.sceneRenderConfiguration.disableAnimation) } assertInstanceOf<UiCheckModeFilter.Enabled<PsiComposePreviewElementInstance>>( preview.uiCheckFilterFlow.value ) delayUntilCondition(250) { UI_CHECK_LAYOUT_OPTION == mainSurface.layoutManagerSwitcher?.currentLayoutOption?.value } assertThat(preview.composePreviewFlowManager.availableGroupsFlow.value.map { it.displayName }) .containsExactly("Screen sizes", "Font scales", "Light/Dark", "Colorblind filters") .inOrder() preview.renderedPreviewElementsInstancesFlowForTest().awaitStatus( "Failed set uiCheckMode", 25.seconds, ) { it.asCollection().size > 2 } fun PsiComposePreviewElementInstance.print(): String { val configurationDeviceSpecText = configuration.deviceSpec .takeIf { str -> str.isNotBlank() && str != "Devices.DEFAULT" } ?.let { "$it\n" } ?: "" return "${methodFqn}\n$configurationDeviceSpecText${displaySettings}\n" } assertEquals( """ TestKt.Preview1 spec:width=411dp,height=891dp PreviewDisplaySettings(name=Medium Phone - Preview1, baseName=Preview1, parameterName=Medium Phone, group=Screen sizes, showDecoration=true, showBackground=false, backgroundColor=null, displayPositioning=NORMAL, organizationGroup=TestKt.Preview1Screen sizes, organizationName=Screen sizes - Preview1) TestKt.Preview1 spec:width=673dp,height=841dp PreviewDisplaySettings(name=Unfolded Foldable - Preview1, baseName=Preview1, parameterName=Unfolded Foldable, group=Screen sizes, showDecoration=true, showBackground=false, backgroundColor=null, displayPositioning=NORMAL, organizationGroup=TestKt.Preview1Screen sizes, organizationName=Screen sizes - Preview1) TestKt.Preview1 spec:width=1280dp,height=800dp,dpi=240 PreviewDisplaySettings(name=Medium Tablet - Preview1, baseName=Preview1, parameterName=Medium Tablet, group=Screen sizes, showDecoration=true, showBackground=false, backgroundColor=null, displayPositioning=NORMAL, organizationGroup=TestKt.Preview1Screen sizes, organizationName=Screen sizes - Preview1) TestKt.Preview1 spec:width=1920dp,height=1080dp,dpi=160 PreviewDisplaySettings(name=Desktop - Preview1, baseName=Preview1, parameterName=Desktop, group=Screen sizes, showDecoration=true, showBackground=false, backgroundColor=null, displayPositioning=NORMAL, organizationGroup=TestKt.Preview1Screen sizes, organizationName=Screen sizes - Preview1) TestKt.Preview1 spec:parent=_device_class_phone,orientation=landscape PreviewDisplaySettings(name=Medium Phone-Landscape - Preview1, baseName=Preview1, parameterName=Medium Phone-Landscape, group=Screen sizes, showDecoration=true, showBackground=false, backgroundColor=null, displayPositioning=NORMAL, organizationGroup=TestKt.Preview1Screen sizes, organizationName=Screen sizes - Preview1) TestKt.Preview1 PreviewDisplaySettings(name=85% - Preview1, baseName=Preview1, parameterName=85%, group=Font scales, showDecoration=false, showBackground=false, backgroundColor=null, displayPositioning=NORMAL, organizationGroup=TestKt.Preview1Font scales, organizationName=Font scales - Preview1) TestKt.Preview1 PreviewDisplaySettings(name=100% - Preview1, baseName=Preview1, parameterName=100%, group=Font scales, showDecoration=false, showBackground=false, backgroundColor=null, displayPositioning=NORMAL, organizationGroup=TestKt.Preview1Font scales, organizationName=Font scales - Preview1) TestKt.Preview1 PreviewDisplaySettings(name=115% - Preview1, baseName=Preview1, parameterName=115%, group=Font scales, showDecoration=false, showBackground=false, backgroundColor=null, displayPositioning=NORMAL, organizationGroup=TestKt.Preview1Font scales, organizationName=Font scales - Preview1) TestKt.Preview1 PreviewDisplaySettings(name=130% - Preview1, baseName=Preview1, parameterName=130%, group=Font scales, showDecoration=false, showBackground=false, backgroundColor=null, displayPositioning=NORMAL, organizationGroup=TestKt.Preview1Font scales, organizationName=Font scales - Preview1) TestKt.Preview1 PreviewDisplaySettings(name=180% - Preview1, baseName=Preview1, parameterName=180%, group=Font scales, showDecoration=false, showBackground=false, backgroundColor=null, displayPositioning=NORMAL, organizationGroup=TestKt.Preview1Font scales, organizationName=Font scales - Preview1) TestKt.Preview1 PreviewDisplaySettings(name=200% - Preview1, baseName=Preview1, parameterName=200%, group=Font scales, showDecoration=false, showBackground=false, backgroundColor=null, displayPositioning=NORMAL, organizationGroup=TestKt.Preview1Font scales, organizationName=Font scales - Preview1) TestKt.Preview1 PreviewDisplaySettings(name=Light - Preview1, baseName=Preview1, parameterName=Light, group=Light/Dark, showDecoration=false, showBackground=false, backgroundColor=null, displayPositioning=NORMAL, organizationGroup=TestKt.Preview1Light/Dark, organizationName=Light/Dark - Preview1) TestKt.Preview1 PreviewDisplaySettings(name=Dark - Preview1, baseName=Preview1, parameterName=Dark, group=Light/Dark, showDecoration=false, showBackground=false, backgroundColor=null, displayPositioning=NORMAL, organizationGroup=TestKt.Preview1Light/Dark, organizationName=Light/Dark - Preview1) TestKt.Preview1 PreviewDisplaySettings(name=Original - Preview1, baseName=Preview1, parameterName=Original, group=Colorblind filters, showDecoration=false, showBackground=false, backgroundColor=null, displayPositioning=NORMAL, organizationGroup=TestKt.Preview1Colorblind filters, organizationName=Colorblind filters - Preview1) TestKt.Preview1 PreviewDisplaySettings(name=Protanopes - Preview1, baseName=Preview1, parameterName=Protanopes, group=Colorblind filters, showDecoration=false, showBackground=false, backgroundColor=null, displayPositioning=NORMAL, organizationGroup=TestKt.Preview1Colorblind filters, organizationName=Colorblind filters - Preview1) TestKt.Preview1 PreviewDisplaySettings(name=Protanomaly - Preview1, baseName=Preview1, parameterName=Protanomaly, group=Colorblind filters, showDecoration=false, showBackground=false, backgroundColor=null, displayPositioning=NORMAL, organizationGroup=TestKt.Preview1Colorblind filters, organizationName=Colorblind filters - Preview1) TestKt.Preview1 PreviewDisplaySettings(name=Deuteranopes - Preview1, baseName=Preview1, parameterName=Deuteranopes, group=Colorblind filters, showDecoration=false, showBackground=false, backgroundColor=null, displayPositioning=NORMAL, organizationGroup=TestKt.Preview1Colorblind filters, organizationName=Colorblind filters - Preview1) TestKt.Preview1 PreviewDisplaySettings(name=Deuteranomaly - Preview1, baseName=Preview1, parameterName=Deuteranomaly, group=Colorblind filters, showDecoration=false, showBackground=false, backgroundColor=null, displayPositioning=NORMAL, organizationGroup=TestKt.Preview1Colorblind filters, organizationName=Colorblind filters - Preview1) TestKt.Preview1 PreviewDisplaySettings(name=Tritanopes - Preview1, baseName=Preview1, parameterName=Tritanopes, group=Colorblind filters, showDecoration=false, showBackground=false, backgroundColor=null, displayPositioning=NORMAL, organizationGroup=TestKt.Preview1Colorblind filters, organizationName=Colorblind filters - Preview1) TestKt.Preview1 PreviewDisplaySettings(name=Tritanomaly - Preview1, baseName=Preview1, parameterName=Tritanomaly, group=Colorblind filters, showDecoration=false, showBackground=false, backgroundColor=null, displayPositioning=NORMAL, organizationGroup=TestKt.Preview1Colorblind filters, organizationName=Colorblind filters - Preview1) """ .trimIndent(), preview.renderedPreviewElementsInstancesFlowForTest().value.asCollection().joinToString( "\n" ) { it.print() }, ) // Change the scale of the surface val scaleUpdate = originalScale + 0.5 mainSurface.zoomController.setScale(scaleUpdate) // Check that the UI Check tab has been created assertEquals(2, contentManager.contents.size) assertEquals(uiCheckElement.instanceId, contentManager.selectedContent?.tabName) // Stop UI Check mode setModeAndWaitForRefresh(PreviewMode.Default()) assertInstanceOf<UiCheckModeFilter.Disabled<PsiComposePreviewElementInstance>>( preview.uiCheckFilterFlow.value ) delayUntilCondition(250) { DEFAULT_LAYOUT_OPTION == mainSurface.layoutManagerSwitcher?.currentLayoutOption?.value } // Check that the surface zoom stays unchanged when exiting UI check mode. assertEquals(scaleUpdate, mainSurface.zoomController.scale, 0.001) preview.renderedPreviewElementsInstancesFlowForTest().awaitStatus( "Failed stop uiCheckMode", 25.seconds, ) { it.asCollection().size == 2 } assertEquals( """ TestKt.Preview1 PreviewDisplaySettings(name=Preview1, baseName=Preview1, parameterName=null, group=null, showDecoration=false, showBackground=false, backgroundColor=null, displayPositioning=NORMAL, organizationGroup=TestKt.Preview1, organizationName=Preview1) TestKt.Preview2 PreviewDisplaySettings(name=preview2 - Preview2, baseName=Preview2, parameterName=preview2, group=groupA, showDecoration=false, showBackground=true, backgroundColor=null, displayPositioning=NORMAL, organizationGroup=TestKt.Preview2, organizationName=Preview2) """ .trimIndent(), preview.renderedPreviewElementsInstancesFlowForTest().value.asCollection().joinToString( "\n" ) { it.print() }, ) // Check that the UI Check tab is still present assertEquals(2, contentManager.contents.size) assertEquals(uiCheckElement.instanceId, contentManager.selectedContent?.tabName) // Restart UI Check mode on the same preview setModeAndWaitForRefresh( PreviewMode.UiCheck(UiCheckInstance(uiCheckElement, isWearPreview = false)) ) { UI_CHECK_LAYOUT_OPTION == mainSurface.layoutManagerSwitcher?.currentLayoutOption?.value } // Check that the UI Check tab is being reused assertEquals(2, contentManager.contents.size) assertEquals(uiCheckElement.instanceId, contentManager.selectedContent?.tabName) problemsView.show() val reopenTabAction = UiCheckReopenTabAction(preview) // Check that UiCheckReopenTabAction is disabled when the UI Check tab is visible and selected run { val actionEvent = withContext(uiThread) { TestActionEvent.createTestEvent() } reopenTabAction.update(actionEvent) assertFalse(actionEvent.presentation.isEnabled) } // Check that UiCheckReopenTabAction is enabled when the UI Check tab is not selected contentManager.setSelectedContent(contentManager.getContent(0)!!) assertNotEquals(uiCheckElement.instanceId, contentManager.selectedContent?.tabName) run { val actionEvent = withContext(uiThread) { TestActionEvent.createTestEvent() } reopenTabAction.update(actionEvent) assertTrue(actionEvent.presentation.isEnabled) } // Check that performing UiCheckReopenTabAction selects the UI Check tab withContext(uiThread) { val actionEvent = TestActionEvent.createTestEvent() reopenTabAction.actionPerformed(actionEvent) assertEquals(uiCheckElement.instanceId, contentManager.selectedContent?.tabName) } // Check that UiCheckReopenTabAction is enabled when the UI Check tab has been closed withContext(uiThread) { ProblemsViewToolWindowUtils.removeTab(project, uiCheckElement.instanceId) } assertEquals(1, contentManager.contents.size) run { val actionEvent = withContext(uiThread) { TestActionEvent.createTestEvent() } reopenTabAction.update(actionEvent) assertTrue(actionEvent.presentation.isEnabled) } // Check that performing UiCheckReopenTabAction recreates the UI Check tab withContext(uiThread) { val actionEvent = TestActionEvent.createTestEvent() reopenTabAction.actionPerformed(actionEvent) } // We set the modality state here because we're removing and recreating the tab using the // APIs from ProblemsViewToolWindowUtils, which use invokeLater when creating the components. // By setting the modality state to the problems view component, we'll make sure the runnable // below will execute only after the component is ready. withContext( Dispatchers.EDT + ModalityState.stateForComponent(problemsView.component).asContextElement() ) { assertEquals(2, contentManager.contents.size) assertEquals(uiCheckElement.instanceId, contentManager.selectedContent?.tabName) } setModeAndWaitForRefresh(PreviewMode.Default()) { DEFAULT_LAYOUT_OPTION == mainSurface.layoutManagerSwitcher?.currentLayoutOption?.value } } @Test fun testPreviewManagersShouldBeRegisteredInDataProvider() = runComposePreviewRepresentationTest { createPreviewAndCompile() assertTrue(getData(PreviewModeManager.KEY.name) is PreviewModeManager) assertTrue(getData(PreviewGroupManager.KEY.name) is PreviewGroupManager) assertTrue(getData(PreviewFlowManager.KEY.name) is PreviewFlowManager<*>) assertTrue(getData(PREVIEW_VIEW_MODEL_STATUS.name) is PreviewViewModelStatus) assertTrue(getData(FastPreviewSurface.KEY.name) is FastPreviewSurface) assertTrue(getData(PreviewInvalidationManager.KEY.name) is PreviewInvalidationManager) } @Test fun testExitingAnimationModeClearsComposeAnimationSubscriber() = runComposePreviewRepresentationTest { val composePreviewRepresentation = createPreviewAndCompile() assertThat(ComposeAnimationSubscriber.getHandlerForTests()).isNull() val previewElements = mainSurface.models.mapNotNull { it.dataProvider?.previewElement() } val animationElement = previewElements[1] composePreviewRepresentation.setMode(PreviewMode.AnimationInspection(animationElement)) delayUntilCondition(200) { ComposeAnimationSubscriber.getHandlerForTests() != null } assertThat(ComposeAnimationSubscriber.getHandlerForTests()).isNotNull() composePreviewRepresentation.setMode(PreviewMode.Default()) delayUntilCondition(200) { ComposeAnimationSubscriber.getHandlerForTests() == null } assertThat(ComposeAnimationSubscriber.getHandlerForTests()).isNull() } @Test fun testActivationDoesNotCleanOverlayClassLoader() = runBlocking(workerThread) { val composeTest = runWriteActionAndWait { fixture.addFileToProjectAndInvalidate( "Test.kt", // language=kotlin """ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.runtime.Composable @Composable @Preview fun Preview1() { } """ .trimIndent(), ) } val mainSurface: NlDesignSurface = NlSurfaceBuilder.builder(fixture.project, fixture.testRootDisposable, false).build() val composeView = TestComposePreviewView(mainSurface) val previewRepresentation = ComposePreviewRepresentation(composeTest, PreferredVisibility.SPLIT) { _, _, _, _, _, _ -> composeView } Disposer.register(fixture.testRootDisposable, previewRepresentation) Disposer.register(fixture.testRootDisposable, mainSurface) // Compile the project so that 'buildSucceeded' is called during build listener setup ProjectSystemService.getInstance(project).projectSystem.getBuildManager().compileProject() val job = launch { while (!previewRepresentation.hasBuildListenerSetupFinished()) { delay(500) } } val overlayClassLoader = ModuleClassLoaderOverlays.getInstance(BuildTargetReference.gradleOnly(fixture.module)) assertTrue(overlayClassLoader.state.paths.isEmpty()) overlayClassLoader.pushOverlayPath(Path.of("/tmp/test")) assertFalse(overlayClassLoader.state.paths.isEmpty()) assertFalse(previewRepresentation.hasBuildListenerSetupFinished()) previewRepresentation.onActivate() job.join() assertTrue(previewRepresentation.hasBuildListenerSetupFinished()) assertFalse(overlayClassLoader.state.paths.isEmpty()) } @Test fun testRerunUiCheckAction() { // Use the real FileEditorManager project.putUserData(FileEditorManagerKeys.ALLOW_IN_LIGHT_PROJECT, true) project.replaceService( FileEditorManager::class.java, FileEditorManagerImpl(project, project.coroutineScope), projectRule.fixture.testRootDisposable, ) HeadlessDataManager.fallbackToProductionDataManager(projectRule.fixture.testRootDisposable) val testPsiFile = runWriteActionAndWait { // Do not use addFileToProjectAndInvalidate(..) here. It generates/caches a document with null // virtual file, which results in the inconsistency with the document for the PSI virtual file // after updating PSI. See b/381432038 for further information. fixture.addFileToProject( "Test.kt", // language=kotlin """ import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.compose.runtime.Composable @Composable @Preview @Preview(name = "preview2", apiLevel = 12, group = "groupA", showBackground = true) fun Preview() { } """ .trimIndent(), ) } testPsiFile.putUserData(FileEditorProvider.KEY, SourceCodeEditorProvider()) val editor = runBlocking(uiThread) { val editor = withContext(uiThread) { val editors = FileEditorManager.getInstance(project).openFile(testPsiFile.virtualFile, true, true) (editors[0] as TextEditorWithMultiRepresentationPreview<*>) } delayUntilCondition(250) { editor.getPreviewManager<ComposePreviewManager>() != null } editor } val mainSurface = runBlocking(uiThread) { editor.getDesignSurface() as NlDesignSurface } runComposePreviewRepresentationTest(testPsiFile, mainSurface) { val preview = editor.getPreviewManager<ComposePreviewManager>() as ComposePreviewRepresentation createPreviewAndCompile(preview) // Start UI Check mode val previewElements = mainSurface.models.mapNotNull { it.dataProvider?.previewElement() } val uiCheckElement = previewElements[1] run { waitForAllRefreshesToFinish(30.seconds) preview.setMode(PreviewMode.UiCheck(UiCheckInstance(uiCheckElement, isWearPreview = false))) delayUntilCondition(250) { preview.uiCheckFilterFlow.value is UiCheckModeFilter.Enabled } } val contentManager = withContext(uiThread) { ToolWindowManager.getInstance(project).getToolWindow(ProblemsView.ID)!!.contentManager } delayUntilCondition(250) { contentManager.selectedContent?.tabName == uiCheckElement.instanceId } val tab = contentManager.selectedContent!! val dataContext = withContext(uiThread) { ((tab.component as DesignerCommonIssuePanel).toolbar as ActionToolbarImpl) .toolbarDataContext } // Check that the rerun action is disabled val rerunAction = ReRunUiCheckModeAction() run { val actionEvent = TestActionEvent.createTestEvent(dataContext) rerunAction.update(actionEvent) assertTrue(actionEvent.presentation.isVisible) assertFalse(actionEvent.presentation.isEnabled) } // Stop UI Check mode run { waitForAllRefreshesToFinish(30.seconds) preview.setMode(PreviewMode.Default()) delayUntilCondition(250) { preview.uiCheckFilterFlow.value is UiCheckModeFilter.Disabled } } // Check that the rerun action is enabled run { val actionEvent = TestActionEvent.createTestEvent(dataContext) rerunAction.update(actionEvent) assertTrue(actionEvent.presentation.isEnabledAndVisible) } // Rerun UI check with the problems panel action withContext(uiThread) { rerunAction.actionPerformed(TestActionEvent.createTestEvent(dataContext)) } delayUntilCondition(250) { (preview.uiCheckFilterFlow.value as? UiCheckModeFilter.Enabled)?.basePreviewInstance == uiCheckElement } // Check that the rerun action is disabled run { val actionEvent = TestActionEvent.createTestEvent(dataContext) rerunAction.update(actionEvent) assertTrue(actionEvent.presentation.isVisible) assertFalse(actionEvent.presentation.isEnabled) } // Stop UI Check mode run { waitForAllRefreshesToFinish(30.seconds) preview.setMode(PreviewMode.Default()) delayUntilCondition(250) { preview.uiCheckFilterFlow.value is UiCheckModeFilter.Disabled } } // Check that the rerun action is enabled run { val actionEvent = TestActionEvent.createTestEvent(dataContext) rerunAction.update(actionEvent) assertTrue(actionEvent.presentation.isEnabledAndVisible) } // Delete the preview annotation that is linked with the UI check runWriteCommandAction(project) { testPsiFile .findElementByText( "@Preview(name = \"preview2\", apiLevel = 12, group = \"groupA\", showBackground = true)" ) .delete() } // Check that the rerun action is hidden run { val actionEvent = TestActionEvent.createTestEvent(dataContext) rerunAction.update(actionEvent) assertFalse(actionEvent.presentation.isVisible) } waitForAllRefreshesToFinish(30.seconds) withContext(uiThread) { FileEditorManagerEx.getInstanceEx(project).closeAllFiles() } } } // Test for b/381975273 @Test fun hideAndShowPreview() { // Use the real FileEditorManager project.putUserData(FileEditorManagerKeys.ALLOW_IN_LIGHT_PROJECT, true) project.replaceService( FileEditorManager::class.java, FileEditorManagerImpl(project, project.coroutineScope), projectRule.fixture.testRootDisposable, ) HeadlessDataManager.fallbackToProductionDataManager(projectRule.fixture.testRootDisposable) val testPsiFile = runWriteActionAndWait { // Do not use addFileToProjectAndInvalidate(..) here. It generates/caches a document with null // virtual file, which results in the inconsistency with the document for the PSI virtual file // after updating PSI. See b/381432038 for further information. fixture.addFileToProject( "Test.kt", // language=kotlin """ import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.compose.runtime.Composable @Composable @Preview @Preview(name = "preview2", apiLevel = 12, group = "groupA", showBackground = true) fun Preview() { } """ .trimIndent(), ) } testPsiFile.putUserData(FileEditorProvider.KEY, SourceCodeEditorProvider()) val editor = runBlocking(uiThread) { val editor = withContext(uiThread) { val editors = FileEditorManager.getInstance(project).openFile(testPsiFile.virtualFile, true, true) (editors[0] as TextEditorWithMultiRepresentationPreview<*>) } delayUntilCondition(250) { editor.getPreviewManager<ComposePreviewManager>() != null } editor } val mainSurface = runBlocking(uiThread) { editor.getDesignSurface() as NlDesignSurface } runComposePreviewRepresentationTest(testPsiFile, mainSurface) { delayUntilCondition(2000) { editor.preview.previewIsActive } editor.preview.component.isVisible = false delayUntilCondition(2000) { !editor.preview.previewIsActive } withContext(uiThread) { FileEditorManagerEx.getInstanceEx(project).closeAllFiles() } } } @Test fun testInteractivePreviewManagerFpsLimitIsInitializedWhenEssentialsModeIsDisabled() = runComposePreviewRepresentationTest { val preview = createPreviewAndCompile() assertEquals(30, preview.interactiveManager.fpsLimit) } @Test fun testInteractivePreviewManagerFpsLimitIsInitializedWhenEssentialsModeIsEnabled() = runComposePreviewRepresentationTest { composePreviewEssentialsModeEnabled = true // Only one preview/model is shown in focus mode val preview = createPreviewAndCompile(expectedModelCount = 1) assertEquals(10, preview.interactiveManager.fpsLimit) } @Test fun testResizePanelIsCreatedInFocusMode_flagTrue() = runComposePreviewRepresentationTest { StudioFlags.COMPOSE_PREVIEW_RESIZING.overrideForTest( true, projectRule.fixture.testRootDisposable, ) createPreviewAndCompile() val previewElements = mainSurface.models.mapNotNull { it.dataProvider?.previewElement() } val focusElement = previewElements[0] setModeAndWaitForRefresh(PreviewMode.Focus(focusElement)) { composeView.focusMode != null } assertThat(composeView.focusMode!!.component.components[1] as? ResizePanel).isNotNull() setModeAndWaitForRefresh(PreviewMode.Default()) { composeView.focusMode == null } } @Test fun testResizePanelIsNotCreatedInFocusMode_flagFalse() = runComposePreviewRepresentationTest { StudioFlags.COMPOSE_PREVIEW_RESIZING.overrideForTest( false, projectRule.fixture.testRootDisposable, ) createPreviewAndCompile() val previewElements = mainSurface.models.mapNotNull { it.dataProvider?.previewElement() } val focusElement = previewElements[0] setModeAndWaitForRefresh(PreviewMode.Focus(focusElement)) { composeView.focusMode != null } assertThat(composeView.focusMode!!.component.components.size).isEqualTo(1) setModeAndWaitForRefresh(PreviewMode.Default()) { composeView.focusMode == null } } @Test fun testResizePanelIsWorkingForFileWithSinglePreview() { val testFile = runWriteActionAndWait { fixture.addFileToProjectAndInvalidate( "SinglePreview.kt", // language=kotlin """ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.runtime.Composable @Composable @Preview fun SinglePreview() { } """ .trimIndent(), ) } return runComposePreviewRepresentationTest(testFile) { StudioFlags.COMPOSE_PREVIEW_RESIZING.overrideForTest( true, projectRule.fixture.testRootDisposable, ) createPreviewAndCompile(expectedModelCount = 1) val previewElements = mainSurface.models.mapNotNull { it.dataProvider?.previewElement() } assertThat(mainSurface.models.size).isEqualTo(1) val singleElement = previewElements.single() // Don't wait for refresh as it's not going to happen when we switch form grid with one // element to Focus mode setModeAndWaitForRefresh(PreviewMode.Focus(singleElement), waitForRefresh = false) { composeView.focusMode != null } val resizePanel = composeView.focusMode!!.component.findDescendant<ResizePanel>()!! assertThat(resizePanel.getCurrentPreviewElementForTest()).isEqualTo(singleElement) setModeAndWaitForRefresh(PreviewMode.Default()) { composeView.focusMode == null } // Don't wait for refresh as it's not going to happen when we switch form grid with one // element to Focus mode setModeAndWaitForRefresh(PreviewMode.Focus(singleElement), waitForRefresh = false) { composeView.focusMode != null } val resizePanel2 = composeView.focusMode!!.component.findDescendant<ResizePanel>()!! assertThat(resizePanel2.getCurrentPreviewElementForTest()).isEqualTo(singleElement) } } @Test fun testResizePanelIsCreatedInFocusMode_flagFalse() = runComposePreviewRepresentationTest { StudioFlags.COMPOSE_PREVIEW_RESIZING.overrideForTest( false, projectRule.fixture.testRootDisposable, ) createPreviewAndCompile() val previewElements = mainSurface.models.mapNotNull { it.dataProvider?.previewElement() } val focusElement = previewElements[0] setModeAndWaitForRefresh(PreviewMode.Focus(focusElement)) { composeView.focusMode != null } assertThat(composeView.focusMode!!.component.components.size).isEqualTo(1) } @Test fun testInteractivePreviewManagerFpsLimitIsUpdatedWhenEssentialsModeChanges() = runComposePreviewRepresentationTest { val preview = createPreviewAndCompile() assertEquals(30, preview.interactiveManager.fpsLimit) composePreviewEssentialsModeEnabled = true delayUntilCondition(delayPerIterationMs = 500) { preview.interactiveManager.fpsLimit == 10 } composePreviewEssentialsModeEnabled = false delayUntilCondition(delayPerIterationMs = 500) { preview.interactiveManager.fpsLimit == 30 } } @Test fun testWearUiCheckMode() { StudioFlags.COMPOSE_UI_CHECK_FOR_WEAR.overrideForTest( true, projectRule.fixture.testRootDisposable, ) val testPsiFile = runWriteActionAndWait { fixture.addFileToProjectAndInvalidate( "Test.kt", // language=kotlin """ import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.compose.runtime.Composable @Composable @Preview(device = "id:wearos_small_round") fun Preview() { } """ .trimIndent(), ) } runComposePreviewRepresentationTest(testPsiFile) { // The file above contains only 1 preview/model val preview = createPreviewAndCompile(expectedModelCount = 1) assertInstanceOf<UiCheckModeFilter.Disabled<PsiComposePreviewElementInstance>>( preview.uiCheckFilterFlow.value ) val uiCheckElement = mainSurface.models.mapNotNull { it.dataProvider?.previewElement() }[0] val problemsView = ProblemsView.getToolWindow(project)!! val contentManager = runBlocking(uiThread) { problemsView.contentManager } withContext(uiThread) { ProblemsViewToolWindowUtils.addTab(project, SharedIssuePanelProvider(project)) assertEquals(1, contentManager.contents.size) } // Start UI Check mode setModeAndWaitForRefresh( PreviewMode.UiCheck(UiCheckInstance(uiCheckElement, isWearPreview = true)) ) assertInstanceOf<UiCheckModeFilter.Enabled<PsiComposePreviewElementInstance>>( preview.uiCheckFilterFlow.value ) delayUntilCondition(250) { UI_CHECK_LAYOUT_OPTION == mainSurface.layoutManagerSwitcher?.currentLayoutOption?.value } assertThat(preview.composePreviewFlowManager.availableGroupsFlow.value.map { it.displayName }) .containsExactly("Wear OS Devices", "Font scales", "Colorblind filters") preview.renderedPreviewElementsInstancesFlowForTest().awaitStatus( "Failed set uiCheckMode", 25.seconds, ) { it.asCollection().size == 15 } assertEquals( """ TestKt.Preview id:wearos_large_round PreviewDisplaySettings(name=Wear OS Large Round - Preview, baseName=Preview, parameterName=Wear OS Large Round, group=Wear OS Devices, showDecoration=true, showBackground=false, backgroundColor=null, displayPositioning=NORMAL, organizationGroup=TestKt.PreviewWear OS Devices, organizationName=Wear OS Devices - Preview) TestKt.Preview id:wearos_small_round PreviewDisplaySettings(name=Wear OS Small Round - Preview, baseName=Preview, parameterName=Wear OS Small Round, group=Wear OS Devices, showDecoration=true, showBackground=false, backgroundColor=null, displayPositioning=NORMAL, organizationGroup=TestKt.PreviewWear OS Devices, organizationName=Wear OS Devices - Preview) TestKt.Preview id:wearos_small_round PreviewDisplaySettings(name=Small - Preview, baseName=Preview, parameterName=Small, group=Font scales, showDecoration=true, showBackground=false, backgroundColor=null, displayPositioning=NORMAL, organizationGroup=TestKt.PreviewFont scales, organizationName=Font scales - Preview) TestKt.Preview id:wearos_small_round PreviewDisplaySettings(name=Normal - Preview, baseName=Preview, parameterName=Normal, group=Font scales, showDecoration=true, showBackground=false, backgroundColor=null, displayPositioning=NORMAL, organizationGroup=TestKt.PreviewFont scales, organizationName=Font scales - Preview) TestKt.Preview id:wearos_small_round PreviewDisplaySettings(name=Medium - Preview, baseName=Preview, parameterName=Medium, group=Font scales, showDecoration=true, showBackground=false, backgroundColor=null, displayPositioning=NORMAL, organizationGroup=TestKt.PreviewFont scales, organizationName=Font scales - Preview) TestKt.Preview id:wearos_small_round PreviewDisplaySettings(name=Large - Preview, baseName=Preview, parameterName=Large, group=Font scales, showDecoration=true, showBackground=false, backgroundColor=null, displayPositioning=NORMAL, organizationGroup=TestKt.PreviewFont scales, organizationName=Font scales - Preview) TestKt.Preview id:wearos_small_round PreviewDisplaySettings(name=Larger - Preview, baseName=Preview, parameterName=Larger, group=Font scales, showDecoration=true, showBackground=false, backgroundColor=null, displayPositioning=NORMAL, organizationGroup=TestKt.PreviewFont scales, organizationName=Font scales - Preview) TestKt.Preview id:wearos_small_round PreviewDisplaySettings(name=Largest - Preview, baseName=Preview, parameterName=Largest, group=Font scales, showDecoration=true, showBackground=false, backgroundColor=null, displayPositioning=NORMAL, organizationGroup=TestKt.PreviewFont scales, organizationName=Font scales - Preview) TestKt.Preview id:wearos_small_round PreviewDisplaySettings(name=Original - Preview, baseName=Preview, parameterName=Original, group=Colorblind filters, showDecoration=true, showBackground=false, backgroundColor=null, displayPositioning=NORMAL, organizationGroup=TestKt.PreviewColorblind filters, organizationName=Colorblind filters - Preview) TestKt.Preview id:wearos_small_round PreviewDisplaySettings(name=Protanopes - Preview, baseName=Preview, parameterName=Protanopes, group=Colorblind filters, showDecoration=true, showBackground=false, backgroundColor=null, displayPositioning=NORMAL, organizationGroup=TestKt.PreviewColorblind filters, organizationName=Colorblind filters - Preview) TestKt.Preview id:wearos_small_round PreviewDisplaySettings(name=Protanomaly - Preview, baseName=Preview, parameterName=Protanomaly, group=Colorblind filters, showDecoration=true, showBackground=false, backgroundColor=null, displayPositioning=NORMAL, organizationGroup=TestKt.PreviewColorblind filters, organizationName=Colorblind filters - Preview) TestKt.Preview id:wearos_small_round PreviewDisplaySettings(name=Deuteranopes - Preview, baseName=Preview, parameterName=Deuteranopes, group=Colorblind filters, showDecoration=true, showBackground=false, backgroundColor=null, displayPositioning=NORMAL, organizationGroup=TestKt.PreviewColorblind filters, organizationName=Colorblind filters - Preview) TestKt.Preview id:wearos_small_round PreviewDisplaySettings(name=Deuteranomaly - Preview, baseName=Preview, parameterName=Deuteranomaly, group=Colorblind filters, showDecoration=true, showBackground=false, backgroundColor=null, displayPositioning=NORMAL, organizationGroup=TestKt.PreviewColorblind filters, organizationName=Colorblind filters - Preview) TestKt.Preview id:wearos_small_round PreviewDisplaySettings(name=Tritanopes - Preview, baseName=Preview, parameterName=Tritanopes, group=Colorblind filters, showDecoration=true, showBackground=false, backgroundColor=null, displayPositioning=NORMAL, organizationGroup=TestKt.PreviewColorblind filters, organizationName=Colorblind filters - Preview) TestKt.Preview id:wearos_small_round PreviewDisplaySettings(name=Tritanomaly - Preview, baseName=Preview, parameterName=Tritanomaly, group=Colorblind filters, showDecoration=true, showBackground=false, backgroundColor=null, displayPositioning=NORMAL, organizationGroup=TestKt.PreviewColorblind filters, organizationName=Colorblind filters - Preview) """ .trimIndent(), preview.renderedPreviewElementsInstancesFlowForTest().value.asCollection().joinToString( "\n" ) { val configurationDeviceSpecText = "${it.configuration.deviceSpec}\n".takeIf { str -> str.isNotBlank() } ?: "" "${it.methodFqn}\n$configurationDeviceSpecText${it.displaySettings}\n" }, ) // Check that the UI Check tab has been created assertEquals(2, contentManager.contents.size) assertEquals(uiCheckElement.instanceId, contentManager.selectedContent?.tabName) // Stop UI Check mode setModeAndWaitForRefresh(PreviewMode.Default()) assertInstanceOf<UiCheckModeFilter.Disabled<PsiComposePreviewElementInstance>>( preview.uiCheckFilterFlow.value ) } } // Regression test for b/353458840 @Test fun multiPreviewsAreOrderedByNameWhenNotInUICheckMode() { val testPsiFile = fixture.addFileToProjectAndInvalidate( "Test.kt", // language=kotlin """ import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.compose.runtime.Composable @Preview(name = "1", group = "2") @Preview(name = "2", group = "2") @Preview(name = "3", group = "3") @Preview(name = "4", group = "3") @Preview(name = "5", group = "1") @Preview(name = "6", group = "1") annotation class MyMultiPreview @Composable @Preview fun Preview() { } @Composable @MyMultiPreview fun MultiPreview() { } """ .trimIndent(), ) runComposePreviewRepresentationTest(testPsiFile) { val preview = createPreviewAndCompile() assertEquals( """ TestKt.Preview PreviewDisplaySettings(name=Preview, baseName=Preview, parameterName=null, group=null, showDecoration=false, showBackground=false, backgroundColor=null, displayPositioning=NORMAL, organizationGroup=TestKt.Preview, organizationName=Preview) TestKt.MultiPreview PreviewDisplaySettings(name=1 - MultiPreview, baseName=MultiPreview, parameterName=1, group=2, showDecoration=false, showBackground=false, backgroundColor=null, displayPositioning=NORMAL, organizationGroup=TestKt.MultiPreview, organizationName=MultiPreview) TestKt.MultiPreview PreviewDisplaySettings(name=2 - MultiPreview, baseName=MultiPreview, parameterName=2, group=2, showDecoration=false, showBackground=false, backgroundColor=null, displayPositioning=NORMAL, organizationGroup=TestKt.MultiPreview, organizationName=MultiPreview) TestKt.MultiPreview PreviewDisplaySettings(name=3 - MultiPreview, baseName=MultiPreview, parameterName=3, group=3, showDecoration=false, showBackground=false, backgroundColor=null, displayPositioning=NORMAL, organizationGroup=TestKt.MultiPreview, organizationName=MultiPreview) TestKt.MultiPreview PreviewDisplaySettings(name=4 - MultiPreview, baseName=MultiPreview, parameterName=4, group=3, showDecoration=false, showBackground=false, backgroundColor=null, displayPositioning=NORMAL, organizationGroup=TestKt.MultiPreview, organizationName=MultiPreview) TestKt.MultiPreview PreviewDisplaySettings(name=5 - MultiPreview, baseName=MultiPreview, parameterName=5, group=1, showDecoration=false, showBackground=false, backgroundColor=null, displayPositioning=NORMAL, organizationGroup=TestKt.MultiPreview, organizationName=MultiPreview) TestKt.MultiPreview PreviewDisplaySettings(name=6 - MultiPreview, baseName=MultiPreview, parameterName=6, group=1, showDecoration=false, showBackground=false, backgroundColor=null, displayPositioning=NORMAL, organizationGroup=TestKt.MultiPreview, organizationName=MultiPreview) """ .trimIndent(), preview.renderedPreviewElementsInstancesFlowForTest().value.asCollection().joinToString( "\n" ) { "${it.methodFqn}\n${it.displaySettings}\n" }, ) } } @Test fun previewPagination() { StudioFlags.PREVIEW_PAGINATION.overrideForTest(true, projectRule.fixture.testRootDisposable) val testPsiFile = fixture.addFileToProjectAndInvalidate( "Test.kt", // language=kotlin """ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.runtime.Composable @Composable @Preview(name = "1") @Preview(name = "2") @Preview(name = "3") @Preview(name = "4") @Preview(name = "5") fun MyFun() { } """ .trimIndent(), ) runComposePreviewRepresentationTest(testPsiFile) { val preview = createPreviewAndCompile() delayUntilCondition(delayPerIterationMs = 100) { """ 1 - MyFun 2 - MyFun 3 - MyFun 4 - MyFun 5 - MyFun """ .trimIndent() == preview.getRenderedPreviewNames() } preview.composePreviewFlowManager.previewFlowPaginator.pageSize = 2 delayUntilCondition(delayPerIterationMs = 100) { """ 1 - MyFun 2 - MyFun """ .trimIndent() == preview.getRenderedPreviewNames() } preview.composePreviewFlowManager.previewFlowPaginator.selectedPage = 1 delayUntilCondition(delayPerIterationMs = 100) { """ 3 - MyFun 4 - MyFun """ .trimIndent() == preview.getRenderedPreviewNames() } preview.composePreviewFlowManager.previewFlowPaginator.selectedPage = 2 delayUntilCondition(delayPerIterationMs = 100) { """ 5 - MyFun """ .trimIndent() == preview.getRenderedPreviewNames() } // Increasing page size from 2 to 3 will make the page 2 (0-indexed) disappear, so the // selectedPage should automatically change to 1. preview.composePreviewFlowManager.previewFlowPaginator.pageSize = 3 delayUntilCondition(delayPerIterationMs = 100) { """ 4 - MyFun 5 - MyFun """ .trimIndent() == preview.getRenderedPreviewNames() } assertEquals(1, preview.composePreviewFlowManager.previewFlowPaginator.selectedPage) } } private fun runComposePreviewRepresentationTest( previewPsiFile: PsiFile = createPreviewPsiFile(), mainSurface: NlDesignSurface = NlSurfaceBuilder.builder(fixture.project, fixture.testRootDisposable, false).build(), block: suspend ComposePreviewRepresentationTestContext.() -> Unit, ) = runTest { val context = ComposePreviewRepresentationTestContext( scope = backgroundScope, previewPsiFile, mainSurface, fixture, logger, projectRule.buildSystemServices, ) try { context.block() } finally { context.cleanup() } } private fun createPreviewPsiFile(): PsiFile { return runWriteActionAndWait { fixture.addFileToProjectAndInvalidate( "Test.kt", // language=kotlin """ import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.compose.runtime.Composable @Composable @Preview fun Preview1() { } @Composable @Preview(name = "preview2", apiLevel = 12, group = "groupA", showBackground = true) fun Preview2() { } """ .trimIndent(), ) } } /** * Wrapper class to perform operations and expose properties that are common to most tests in this * test class. */ private class ComposePreviewRepresentationTestContext( val scope: CoroutineScope, private val previewPsiFile: PsiFile, val mainSurface: NlDesignSurface, private val fixture: CodeInsightTestFixture, private val logger: Logger, private val buildSystemServices: FakeBuildSystemFilePreviewServices, ) { private lateinit var preview: ComposePreviewRepresentation lateinit var composeView: TestComposePreviewView private lateinit var dataProvider: DataProvider private lateinit var newModelAddedLatch: CountDownLatch init { scope.launch { mainSurface.modelChanged.collect { models -> val id = UUID.randomUUID().toString().substring(0, 5) logger.info("modelChanged ($id)") repeat(models.size) { newModelAddedLatch.countDown() } } } } suspend fun createPreviewAndCompile( previewOverride: ComposePreviewRepresentation? = null, expectedModelCount: Int = 2, ): ComposePreviewRepresentation { newModelAddedLatch = CountDownLatch(expectedModelCount) composeView = TestComposePreviewView(mainSurface) preview = previewOverride ?: ComposePreviewRepresentation(previewPsiFile, PreferredVisibility.SPLIT) { _, _, _, provider, _, _ -> dataProvider = provider composeView } Disposer.register(fixture.testRootDisposable, preview) withContext(workerThread) { logger.info("compile") buildSystemServices.simulateArtifactBuild(ProjectSystemBuildManager.BuildStatus.SUCCESS) logger.info("activate") preview.onActivate() newModelAddedLatch.await() delayWhileRefreshingOrDumb(preview) } return preview } suspend fun setModeAndWaitForRefresh( previewMode: PreviewMode, waitForRefresh: Boolean = true, // In addition to refresh, we can wait for another condition before returning. additionalCondition: () -> Boolean = { true }, ) { waitForAllRefreshesToFinish(30.seconds) var refresh = !waitForRefresh if (waitForRefresh) { composeView.refreshCompletedListeners.add { refresh = true } } preview.setMode(previewMode) delayUntilCondition(250) { refresh && additionalCondition() } } private suspend fun delayWhileRefreshingOrDumb(preview: ComposePreviewRepresentation) { delayUntilCondition(250) { !(preview.status().isRefreshing || DumbService.getInstance(fixture.project).isDumb) } } fun getData(dataId: String): Any? { checkState( ::dataProvider.isInitialized, "createPreviewAndCompile() must be called before getData() to make sure the DataProvider " + "is initialized.", ) return dataProvider.getData(dataId) } fun cleanup() { if (::preview.isInitialized) { preview.onDeactivate() } } } private fun ComposePreviewRepresentation.getRenderedPreviewNames() = renderedPreviewElementsInstancesFlowForTest().value.asCollection().joinToString("\n") { it.displaySettings.name } }