diff --git a/.gitattributes b/.gitattributes index a7e9da5a2..8e5a868c4 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,4 @@ gradlew linguist-generated=true gradlew.bat linguist-generated=true config/bin/* filter=lfs diff=lfs merge=lfs -text +**/snapshots/**/*.png filter=lfs diff=lfs merge=lfs -text \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e2e646369..69a08c13e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,6 +22,11 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + lfs: 'true' + + - name: Check LFS files + uses: actionsdesk/lfs-warning@v3.2 - name: Export JDK version shell: bash @@ -54,6 +59,7 @@ jobs: name: reports path: | **/build/reports/** + **/src/jvmTest/snapshots/**/*_compare.png publish-snapshot: name: 'Publish snapshot (main only)' diff --git a/config/git/hooks/pre-receive.sh b/config/git/hooks/pre-receive.sh new file mode 100755 index 000000000..5c07ce423 --- /dev/null +++ b/config/git/hooks/pre-receive.sh @@ -0,0 +1,8 @@ +# compares files that match .gitattributes filter to those actually tracked by git-lfs +diff <(git ls-files ':(attr:filter=lfs)' | sort) <(git lfs ls-files -n | sort) >/dev/null + +ret=$? +if [[ $ret -ne 0 ]]; then + echo >&2 "This remote has detected files committed without using Git LFS. Run 'brew install git-lfs && git lfs install' to install it and re-commit your files."; + exit 1; +fi \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a6df22d3a..7910288af 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -23,6 +23,7 @@ jdk = "21" jvmTarget = "17" jewel = "0.26.2" jna = "5.15.0" +junit = "4.13.1" kaml = "0.62.2" kotlin = "2.0.21" kotlinx-serialization = "1.7.3" @@ -38,6 +39,7 @@ nullawayGradle = "2.1.0" okhttp = "5.0.0-alpha.12" okio = "3.9.1" retrofit = "2.11.0" +roborazzi = "1.30.1" slack-lint = "0.8.2" sortDependencies = "0.12" spotless = "7.0.0.BETA4" @@ -69,6 +71,7 @@ moshiGradlePlugin = { id = "dev.zacsweers.moshix", version.ref = "moshix" } moshix = { id = "dev.zacsweers.moshix", version.ref = "moshix" } pluginUploader = { id = "dev.bmac.intellij.plugin-uploader", version = "1.3.5" } retry = { id = "org.gradle.test-retry", version.ref = "gradle-retry" } +roborazzi = { id = "io.github.takahirom.roborazzi", version.ref = "roborazzi" } spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } sortDependencies = { id = "com.squareup.sort-dependencies", version.ref = "sortDependencies" } versionsPlugin = { id = "com.github.ben-manes.versions", version.ref = "versionsPlugin" } @@ -122,6 +125,7 @@ kotlin-gradlePlugins-bom = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin kotlin-poet = { module = "com.squareup:kotlinpoet", version.ref = "kotlinPoet" } kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinx-serialization" } +kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } kotlinShell = "eu.jrie.jetbrains:kotlin-shell-core:0.2.1" ktfmt = { module = "com.facebook:ktfmt", version.ref = "ktfmt" } jewel-bridge = { module = "org.jetbrains.jewel:jewel-ide-laf-bridge-242", version.ref = "jewel" } @@ -143,11 +147,16 @@ okio-fakefilesystem = { module = "com.squareup.okio:okio-fakefilesystem", versio oshi = "com.github.oshi:oshi-core:6.6.5" retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } retrofit-converters-wire = { module = "com.squareup.retrofit2:converter-wire", version.ref = "retrofit" } +roborazzi = { module = "io.github.takahirom.roborazzi:roborazzi-compose-desktop", version.ref = "roborazzi" } rxjava = "io.reactivex.rxjava3:rxjava:3.1.9" sarif4k = "io.github.detekt.sarif4k:sarif4k:0.6.0" slackLints-checks = { module = "com.slack.lint:slack-lint-checks", version.ref = "slack-lint" } slackLints-annotations = { module = "com.slack.lint:slack-lint-annotations", version.ref = "slack-lint" } slf4jNop = "org.slf4j:slf4j-nop:2.0.16" +testing-roborazzi = { module = "io.github.takahirom.roborazzi:roborazzi", version.ref = "roborazzi" } +testing-roborazzi-rules = { module = "io.github.takahirom.roborazzi:roborazzi-junit-rule", version.ref = "roborazzi" } +testing-roborazzi-compose = { module = "io.github.takahirom.roborazzi:roborazzi-compose", version.ref = "roborazzi" } +testing-roborazzi-core = { module = "io.github.takahirom.roborazzi:roborazzi-core", version.ref = "roborazzi" } tikxml-htmlEscape = { module = "com.tickaroo.tikxml:converter-htmlescape", version = "0.8.15" } truth = "com.google.truth:truth:1.4.4" xmlutil-core = { module = "io.github.pdvrieze.xmlutil:core", version.ref = "xmlutil" } diff --git a/platforms/intellij/compose/gradle.properties b/platforms/intellij/compose/gradle.properties index 74b2de374..8ce59d7fd 100644 --- a/platforms/intellij/compose/gradle.properties +++ b/platforms/intellij/compose/gradle.properties @@ -2,3 +2,5 @@ POM_ARTIFACT_ID=skate-compose POM_NAME=Skate (Compose) POM_DESCRIPTION=Skate (Compose) INTELLIJ_PLUGIN=true +# Set the default path for Roborazzi record images +roborazzi.record.filePathStrategy=relativePathFromRoborazziContextOutputDirectory \ No newline at end of file diff --git a/platforms/intellij/compose/playground/build.gradle.kts b/platforms/intellij/compose/playground/build.gradle.kts index 456b212de..49bf53e6f 100644 --- a/platforms/intellij/compose/playground/build.gradle.kts +++ b/platforms/intellij/compose/playground/build.gradle.kts @@ -20,6 +20,15 @@ plugins { alias(libs.plugins.compose) alias(libs.plugins.kotlin.plugin.compose) alias(libs.plugins.lint) + alias(libs.plugins.roborazzi) +} + +roborazzi { + outputDir.set( + layout.projectDirectory.dir( + "src/jvmTest/kotlin/foundry/intellij/compose/playground/snapshots/images" + ) + ) } kotlin { @@ -47,6 +56,19 @@ kotlin { implementation(projects.platforms.intellij.compose) } } + jvmTest { + dependencies { + api(libs.testing.roborazzi.rules) + + implementation(libs.kotlin.test) + implementation(libs.roborazzi) + implementation(libs.testing.roborazzi) + implementation(libs.testing.roborazzi.core) + implementation(libs.testing.roborazzi.compose) + implementation(compose.desktop.common) + implementation(compose.desktop.uiTestJUnit4) + } + } } } diff --git a/platforms/intellij/compose/playground/src/jvmMain/kotlin/foundry/intellij/compose/playground/MarkdownPlayground.kt b/platforms/intellij/compose/playground/src/jvmMain/kotlin/foundry/intellij/compose/playground/MarkdownPlayground.kt index 5d1af3f01..7f5140900 100644 --- a/platforms/intellij/compose/playground/src/jvmMain/kotlin/foundry/intellij/compose/playground/MarkdownPlayground.kt +++ b/platforms/intellij/compose/playground/src/jvmMain/kotlin/foundry/intellij/compose/playground/MarkdownPlayground.kt @@ -18,11 +18,13 @@ package foundry.intellij.compose.playground import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.dp import androidx.compose.ui.window.singleWindowApplication import foundry.intellij.compose.markdown.ui.MarkdownContent @@ -31,11 +33,19 @@ import org.jetbrains.jewel.intui.standalone.theme.IntUiTheme import org.jetbrains.jewel.ui.component.DefaultButton import org.jetbrains.jewel.ui.component.Text -fun main() = singleWindowApplication { +fun main() { + singleWindowApplication { MarkdownPlayground() } +} + +@Composable +fun MarkdownPlayground(modifier: Modifier = Modifier) { var isDark by remember { mutableStateOf(false) } IntUiTheme(isDark) { - Column(Modifier.background(JewelTheme.globalColors.panelBackground)) { - DefaultButton(modifier = Modifier.padding(16.dp), onClick = { isDark = !isDark }) { + Column(modifier.background(JewelTheme.globalColors.panelBackground)) { + DefaultButton( + modifier = Modifier.padding(16.dp).testTag(TestTags.DARK_MODE_TOGGLE), + onClick = { isDark = !isDark }, + ) { Text("Toggle dark mode") } MarkdownContent { MARKDOWN } @@ -43,6 +53,10 @@ fun main() = singleWindowApplication { } } +object TestTags { + const val DARK_MODE_TOGGLE = "dark-mode-toggle" +} + private const val MARKDOWN = """ # Markdown Playground diff --git a/platforms/intellij/compose/playground/src/jvmTest/kotlin/foundry/intellij/compose/playground/MarkdownPlaygroundTest.kt b/platforms/intellij/compose/playground/src/jvmTest/kotlin/foundry/intellij/compose/playground/MarkdownPlaygroundTest.kt new file mode 100644 index 000000000..fb40d1f4a --- /dev/null +++ b/platforms/intellij/compose/playground/src/jvmTest/kotlin/foundry/intellij/compose/playground/MarkdownPlaygroundTest.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2024 Slack Technologies, LLC + * + * 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 + * + * https://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 foundry.intellij.compose.playground + +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onRoot +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.runDesktopComposeUiTest +import com.github.takahirom.roborazzi.DEFAULT_ROBORAZZI_OUTPUT_DIR_PATH +import com.github.takahirom.roborazzi.ExperimentalRoborazziApi +import com.github.takahirom.roborazzi.RoborazziOptions +import io.github.takahirom.roborazzi.captureRoboImage +import kotlin.test.Test + +class MarkdownPlaygroundTest { + @OptIn(ExperimentalTestApi::class, ExperimentalRoborazziApi::class) + @Test + fun snapshot() = runDesktopComposeUiTest { + setContent { MarkdownPlayground() } + + val roborazziOptions = + RoborazziOptions( + recordOptions = RoborazziOptions.RecordOptions(resizeScale = 0.5), + compareOptions = + RoborazziOptions.CompareOptions(outputDirectoryPath = DEFAULT_ROBORAZZI_OUTPUT_DIR_PATH), + ) + + onRoot().captureRoboImage(roborazziOptions = roborazziOptions) + + onNodeWithTag(TestTags.DARK_MODE_TOGGLE).performClick() + + onRoot().captureRoboImage(roborazziOptions = roborazziOptions) + } +} diff --git a/platforms/intellij/compose/playground/src/jvmTest/kotlin/foundry/intellij/compose/playground/snapshots/images/foundry.intellij.compose.playground.MarkdownPlaygroundTest.test.png b/platforms/intellij/compose/playground/src/jvmTest/kotlin/foundry/intellij/compose/playground/snapshots/images/foundry.intellij.compose.playground.MarkdownPlaygroundTest.test.png new file mode 100644 index 000000000..e7d6f4fa7 --- /dev/null +++ b/platforms/intellij/compose/playground/src/jvmTest/kotlin/foundry/intellij/compose/playground/snapshots/images/foundry.intellij.compose.playground.MarkdownPlaygroundTest.test.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a28db7245a0b40fd02b3c70d62345767f9472bfeb2a5ead8fb7d1a0f072c78ad +size 22262 diff --git a/platforms/intellij/compose/playground/src/jvmTest/kotlin/foundry/intellij/compose/playground/snapshots/images/foundry.intellij.compose.playground.MarkdownPlaygroundTest.test_2.png b/platforms/intellij/compose/playground/src/jvmTest/kotlin/foundry/intellij/compose/playground/snapshots/images/foundry.intellij.compose.playground.MarkdownPlaygroundTest.test_2.png new file mode 100644 index 000000000..9d5b94ab0 --- /dev/null +++ b/platforms/intellij/compose/playground/src/jvmTest/kotlin/foundry/intellij/compose/playground/snapshots/images/foundry.intellij.compose.playground.MarkdownPlaygroundTest.test_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:985e8d862317262f57c36085e16a00360b63195d01c71949c7db72d15d4f4517 +size 21779