blob: 85fb6b6a10a402139c2ea27cd9edddb50beded02 [file] [log] [blame]
/*
* Copyright 2021 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 androidx.compose.ui.window.window
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.material.Button
import androidx.compose.material.Slider
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.Recomposer
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.LeakDetector
import androidx.compose.ui.Modifier
import androidx.compose.ui.awt.ComposeWindow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.launchApplication
import androidx.compose.ui.window.rememberWindowState
import androidx.compose.ui.window.runApplicationTest
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.swing.Swing
import org.junit.Assume.assumeFalse
import org.junit.Test
import java.awt.Dimension
import java.awt.GraphicsEnvironment
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
@OptIn(ExperimentalComposeUiApi::class)
class WindowTest {
@Test
fun `open and close custom window`() = runApplicationTest {
var window: ComposeWindow? = null
launchApplication {
var isOpen by remember { mutableStateOf(true) }
fun createWindow() = ComposeWindow().apply {
size = Dimension(300, 200)
addWindowListener(object : WindowAdapter() {
override fun windowClosing(e: WindowEvent) {
isOpen = false
}
})
}
if (isOpen) {
Window(
create = ::createWindow,
dispose = ComposeWindow::dispose
) {
window = this.window
Box(Modifier.size(32.dp).background(Color.Red))
}
}
}
awaitIdle()
assertThat(window?.isShowing).isTrue()
window?.dispatchEvent(WindowEvent(window, WindowEvent.WINDOW_CLOSING))
}
@Test
fun `update custom window`() = runApplicationTest {
var window: ComposeWindow? = null
var isOpen by mutableStateOf(true)
var title by mutableStateOf("Title1")
launchApplication {
fun createWindow() = ComposeWindow().apply {
size = Dimension(300, 200)
addWindowListener(object : WindowAdapter() {
override fun windowClosing(e: WindowEvent) {
isOpen = false
}
})
}
if (isOpen) {
Window(
create = ::createWindow,
dispose = ComposeWindow::dispose,
update = { it.title = title }
) {
window = this.window
Box(Modifier.size(32.dp).background(Color.Red))
}
}
}
awaitIdle()
assertThat(window?.isShowing).isTrue()
assertThat(window?.title).isEqualTo(title)
title = "Title2"
awaitIdle()
assertThat(window?.title).isEqualTo(title)
isOpen = false
}
@Test
fun `open and close window`() = runApplicationTest {
var window: ComposeWindow? = null
launchApplication {
Window(onCloseRequest = ::exitApplication) {
window = this.window
Box(Modifier.size(32.dp).background(Color.Red))
}
}
awaitIdle()
assertThat(window?.isShowing).isTrue()
window?.dispatchEvent(WindowEvent(window, WindowEvent.WINDOW_CLOSING))
}
@Test
fun `disable closing window`() = runApplicationTest {
var isOpen by mutableStateOf(true)
var isCloseCalled by mutableStateOf(false)
var window: ComposeWindow? = null
launchApplication {
if (isOpen) {
Window(
onCloseRequest = {
isCloseCalled = true
}
) {
window = this.window
Box(Modifier.size(32.dp).background(Color.Red))
}
}
}
awaitIdle()
window?.dispatchEvent(WindowEvent(window, WindowEvent.WINDOW_CLOSING))
awaitIdle()
assertThat(isCloseCalled).isTrue()
assertThat(window?.isShowing).isTrue()
isOpen = false
awaitIdle()
assertThat(window?.isShowing).isFalse()
}
@Test
fun `show splash screen`() = runApplicationTest {
var window1: ComposeWindow? = null
var window2: ComposeWindow? = null
var isOpen by mutableStateOf(true)
var isLoading by mutableStateOf(true)
launchApplication {
if (isOpen) {
if (isLoading) {
Window(onCloseRequest = {}) {
window1 = this.window
Box(Modifier.size(32.dp).background(Color.Red))
}
} else {
Window(onCloseRequest = {}) {
window2 = this.window
Box(Modifier.size(32.dp).background(Color.Blue))
}
}
}
}
awaitIdle()
assertThat(window1?.isShowing).isTrue()
assertThat(window2).isNull()
isLoading = false
awaitIdle()
assertThat(window1?.isShowing).isFalse()
assertThat(window2?.isShowing).isTrue()
isOpen = false
awaitIdle()
assertThat(window1?.isShowing).isFalse()
assertThat(window2?.isShowing).isFalse()
}
@Test
fun `open two windows`() = runApplicationTest {
var window1: ComposeWindow? = null
var window2: ComposeWindow? = null
var isOpen by mutableStateOf(true)
launchApplication {
if (isOpen) {
Window(onCloseRequest = {}) {
window1 = this.window
Box(Modifier.size(32.dp).background(Color.Red))
}
Window(onCloseRequest = {}) {
window2 = this.window
Box(Modifier.size(32.dp).background(Color.Blue))
}
}
}
awaitIdle()
assertThat(window1?.isShowing).isTrue()
assertThat(window2?.isShowing).isTrue()
isOpen = false
awaitIdle()
assertThat(window1?.isShowing).isFalse()
assertThat(window2?.isShowing).isFalse()
}
@Test
fun `open nested window`() = runApplicationTest {
var window1: ComposeWindow? = null
var window2: ComposeWindow? = null
var isOpen by mutableStateOf(true)
var isNestedOpen by mutableStateOf(true)
launchApplication {
if (isOpen) {
Window(
onCloseRequest = {},
state = rememberWindowState(
size = DpSize(600.dp, 600.dp),
)
) {
window1 = this.window
Box(Modifier.size(32.dp).background(Color.Red))
if (isNestedOpen) {
Window(
onCloseRequest = {},
state = rememberWindowState(
size = DpSize(300.dp, 300.dp),
)
) {
window2 = this.window
Box(Modifier.size(32.dp).background(Color.Blue))
}
}
}
}
}
awaitIdle()
assertThat(window1?.isShowing).isTrue()
assertThat(window2?.isShowing).isTrue()
isNestedOpen = false
awaitIdle()
assertThat(window1?.isShowing).isTrue()
assertThat(window2?.isShowing).isFalse()
isNestedOpen = true
awaitIdle()
assertThat(window1?.isShowing).isTrue()
assertThat(window2?.isShowing).isTrue()
isOpen = false
awaitIdle()
assertThat(window1?.isShowing).isFalse()
assertThat(window2?.isShowing).isFalse()
}
@Test
fun `pass composition local to windows`() = runApplicationTest {
var actualValue1: Int? = null
var actualValue2: Int? = null
var actualValue3: Int? = null
var isOpen by mutableStateOf(true)
val local1TestValue = compositionLocalOf { 0 }
val local2TestValue = compositionLocalOf { 0 }
var locals by mutableStateOf(arrayOf(local1TestValue provides 1))
launchApplication {
if (isOpen) {
CompositionLocalProvider(*locals) {
Window(
onCloseRequest = {},
state = rememberWindowState(
size = DpSize(600.dp, 600.dp),
)
) {
actualValue1 = local1TestValue.current
actualValue2 = local2TestValue.current
Box(Modifier.size(32.dp).background(Color.Red))
Window(
onCloseRequest = {},
state = rememberWindowState(
size = DpSize(300.dp, 300.dp),
)
) {
actualValue3 = local1TestValue.current
Box(Modifier.size(32.dp).background(Color.Blue))
}
}
}
}
}
awaitIdle()
assertThat(actualValue1).isEqualTo(1)
assertThat(actualValue2).isEqualTo(0)
assertThat(actualValue3).isEqualTo(1)
locals = arrayOf(local1TestValue provides 42)
awaitIdle()
assertThat(actualValue1).isEqualTo(42)
assertThat(actualValue2).isEqualTo(0)
assertThat(actualValue3).isEqualTo(42)
locals = arrayOf(local1TestValue provides 43)
awaitIdle()
assertThat(actualValue1).isEqualTo(43)
assertThat(actualValue2).isEqualTo(0)
assertThat(actualValue3).isEqualTo(43)
locals = arrayOf(local1TestValue provides 43, local2TestValue provides 12)
awaitIdle()
assertThat(actualValue1).isEqualTo(43)
assertThat(actualValue2).isEqualTo(12)
assertThat(actualValue3).isEqualTo(43)
locals = emptyArray()
awaitIdle()
assertThat(actualValue1).isEqualTo(0)
assertThat(actualValue2).isEqualTo(0)
assertThat(actualValue3).isEqualTo(0)
isOpen = false
}
@Test
fun `DisposableEffect call order`() = runApplicationTest {
var initCount = 0
var disposeCount = 0
var isOpen by mutableStateOf(true)
launchApplication {
if (isOpen) {
Window(onCloseRequest = {}) {
DisposableEffect(Unit) {
initCount++
onDispose {
disposeCount++
}
}
}
}
}
awaitIdle()
assertThat(initCount).isEqualTo(1)
assertThat(disposeCount).isEqualTo(0)
isOpen = false
awaitIdle()
assertThat(initCount).isEqualTo(1)
assertThat(disposeCount).isEqualTo(1)
}
@Test(timeout = 30000)
fun `window dispose should not cause a memory leak`() {
assumeFalse(GraphicsEnvironment.getLocalGraphicsEnvironment().isHeadlessInstance)
val leakDetector = LeakDetector()
val oldRecomposers = Recomposer.runningRecomposers.value
runBlocking(Dispatchers.Swing) {
repeat(10) {
val window = ComposeWindow()
window.size = Dimension(200, 200)
window.isVisible = true
window.setContent {
Button({}) {}
Slider(0f, {})
}
window.dispose()
leakDetector.observeObject(window)
}
while (Recomposer.runningRecomposers.value != oldRecomposers) {
delay(100)
}
assertThat(leakDetector.noLeak()).isTrue()
}
}
}