Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Public interface tests #598

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions kotlin-css/src/commonMain/kotlin/kotlinx/css/CssBuilder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@ interface CssBuilder : StyledElement, RuleContainer {
}
}

/**
* Add custom property to CSS [declarations]. If the variable name is in the camelCase, it turns it to snake-case
*/
fun setCustomProperty(name: String, value: CssValue) {
put("--$name", value.value)
}
Expand All @@ -144,8 +147,8 @@ interface CssBuilder : StyledElement, RuleContainer {
fun max(v1: LinearDimension, v2: LinearDimension): LinearDimension =
LinearDimension("max($v1, $v2)")

fun clamp(min: LinearDimension, max: LinearDimension, preferred: LinearDimension): LinearDimension =
LinearDimension("clamp($min, $max, $preferred)")
fun clamp(min: LinearDimension, preferred: LinearDimension, max: LinearDimension): LinearDimension =
LinearDimension("clamp($min, $preferred, $max)")

// Operator overrides
operator fun RuleSet.unaryPlus()
Expand Down
82 changes: 44 additions & 38 deletions kotlin-styled-next/src/jsMain/kotlin/styled/StyledCss.kt
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,13 @@ internal open class StyledCss(
}
}

private fun buildRules(outerSelector: String = ""): List<String> {
return rules.filter { (selector) -> !withAmpersand(selector) && !withMedia(selector) }
.flatMap { (selector, _, css) ->
val delimiter = if (isPseudoClass(selector)) "" else " "
css.getCssRules(selector.split(",").joinToString { "$outerSelector$delimiter${it.trim()}" })
}
}

private fun isPseudoClass(selector: Selector) = selector.trim().startsWith(":")
private fun withAmpersand(selector: Selector) = selector.contains("&")
private fun withMedia(selector: Selector) = selector.trim().startsWith("@media")
private fun withContainer(selector: Selector) = selector.trim().startsWith("@container")
private fun withAmpersand(selector: Selector) = selector.contains("&")
private fun withFontFace(selector: Selector) = selector.trim().startsWith("@font-face")
private fun withSupports(selector: Selector) = selector.trim().startsWith("@supports")
private fun withCustomHandle(selector: Selector) =
withMedia(selector) || withContainer(selector) || withSupports(selector) || withAmpersand(selector) || withFontFace(selector)

private var memoizedHashCode: Int? = null
override fun hashCode(): Int {
Expand All @@ -79,45 +75,55 @@ internal open class StyledCss(
*/
fun getCssRules(outerSelector: String?, indent: String = ""): List<String> {
val result = mutableListOf<String>()
val (ampersandRules, rules) = rules.partition { withAmpersand(it.selector) }
if (rules.isNotEmpty() || declarations.isNotEmpty()) {
if (outerSelector != null) {
result.add(
buildString {
append("$indent$outerSelector {\n")
append(declarations)
append("$indent}\n")
result.addAll(buildRules(outerSelector))
}
)
} else {
result.addAll(buildRules())
}
}

rules.filter { withMedia(it.selector) }.forEach { (selector, _, css) ->
val (rules, handleRules) = rules.partition { !withCustomHandle(it.selector) }
if (declarations.isNotEmpty() && outerSelector != null) {
result.add(
buildString {
append("$indent$selector {\n")
css.getCssRules(outerSelector, " $indent").forEach { appendLine(it); }
append("$indent$outerSelector {\n")
append(declarations)
append("$indent}\n")
}
)
}
result.addAll(buildRules(rules, outerSelector ?: ""))

ampersandRules.forEach {
result.add(
buildString {
val selector = resolveRelativeSelector(it.selector, outerSelector!!)
result.addAll(it.css.getCssRules(selector, indent))
}
)
handleRules.forEach { (selector, _, css) ->
val resolvedSelector = resolveRelativeSelector(selector, outerSelector)
if (withMedia(resolvedSelector)) {
result.add(
buildString {
append("$indent$selector {\n")
css.getCssRules(outerSelector, " $indent").forEach { appendLine(it); }
append("$indent}\n")
}
)
} else if (withContainer(resolvedSelector) || withSupports(resolvedSelector)) {
result.add(
buildString {
append("$indent$selector {\n")
css.getCssRules(" $indent").forEach { appendLine(it); }
append("$indent}\n")
}
)
} else {
result.addAll(css.getCssRules(resolvedSelector))
}
}
return result
}

private fun resolveRelativeSelector(selector: Selector, selfClassName: ClassName) = selfClassName.split(",")
.joinToString { selector.replace("&", it.trim()) }
private fun resolveRelativeSelector(selector: Selector, selfClassName: ClassName?): Selector {
if (selfClassName == null) return selector
return selfClassName.split(",").joinToString { selector.replace("&", it.trim()) }
}
}

private fun isPseudoClass(selector: Selector) = selector.trim().startsWith(":")
private fun buildRules(rules: List<StyledRule>, outerSelector: String): List<String> {
return rules.flatMap { (selector, _, css) ->
val delimiter = if (isPseudoClass(selector)) "" else " "
css.getCssRules(selector.split(",").joinToString { "$outerSelector$delimiter${it.trim()}" })
}
}

private fun Rule.toStyledRule(parent: RuleContainer, block: RuleSet = this.block): StyledRule {
Expand Down
18 changes: 13 additions & 5 deletions kotlin-styled-next/src/jsTest/kotlin/TestUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@ import kotlinx.dom.clear
import org.w3c.dom.Element
import org.w3c.dom.HTMLElement
import org.w3c.dom.HTMLStyleElement
import org.w3c.dom.css.CSSRuleList
import org.w3c.dom.css.CSSStyleDeclaration
import org.w3c.dom.css.CSSStyleSheet
import org.w3c.dom.css.*
import react.ComponentType
import react.RProps
import react.createElement
Expand Down Expand Up @@ -49,8 +47,18 @@ class TestScope : CoroutineScope by testScope {
return getStylesheet().cssRules
}

fun Element.getStyle(): CSSStyleDeclaration {
return window.getComputedStyle(this)
fun Element.getStyle(pseudoElt: String? = null): CSSStyleDeclaration {
return window.getComputedStyle(this, pseudoElt)
}

fun Element.color(pseudoElt: String? = null): String {
return getStyle(pseudoElt).color
}

fun CSSRuleList.forEach(block: (rule: CSSRule?) -> Unit) {
for (i in 0 until this.length) {
block(this[i])
}
}

fun assertChildrenCount(n: Int) {
Expand Down
88 changes: 88 additions & 0 deletions kotlin-styled-next/src/jsTest/kotlin/test/AtRulesTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package test

import kotlinx.css.*
import react.RProps
import react.dom.div
import react.fc
import runTest
import styled.css
import styled.styledDiv
import styled.styledSpan
import kotlin.test.Test
import kotlin.test.assertEquals

/**
* Check every public interface function in [CssBuilder]
*/
class AtRulesTest : TestBase() {
@Test
fun supports() = runTest {
val query = "(color: $firstColor)"
val styledComponent = fc<RProps> {
styledDiv {
css {
supports(query) {
"div" {
color = firstColor
}
}
}
div {}
}
}
val element = clearAndInject(styledComponent)
assertEquals(firstColor.toString(), element.childAt(0).color())
assertCssInjected("@supports $query", listOf("color" to firstColor.toString()))
}

@Test
fun fontFace() = runTest {
val styledComponent = fc<RProps> {
styledDiv {
css {
fontFace {
fontFamily = "Roboto"
}
}
}
}
clearAndInject(styledComponent)
assertCssInjected("@font-face", listOf("font-family" to "Roboto"))
}

@Test
fun retina() = runTest {
val styledComponent = fc<RProps> {
styledDiv {
css {
fontSize = 15.px
retina {
fontSize = 18.px
}
}
}
}
clearAndInject(styledComponent)
assertCssInjected("@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi)", listOf("font-size" to "18px"))
}

@Test
fun media() = runTest {
val query = "only screen and (max-width: 600px)"
val styledComponent = fc<RProps> {
styledSpan {
css {
media(query) {
textTransform = TextTransform.capitalize
}
}
}
}
clearAndInject(styledComponent)
assertCssInjected(
"@media $query", listOf(
"text-transform" to "capitalize",
)
)
}
}
122 changes: 100 additions & 22 deletions kotlin-styled-next/src/jsTest/kotlin/test/ElementTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,10 @@ class ElementTest : TestBase() {
}
}
}
clearAndInject(first)
inject(second)
val firstElement = clearAndInject(first)
val secondElement = inject(second)
val rules = getStylesheet().cssRules
assertEquals(firstElement.className, secondElement.className)
assertEquals(1, rules.length)
}

Expand Down Expand Up @@ -224,26 +225,6 @@ class ElementTest : TestBase() {
assertEquals(firstColor.toString(), element.childAt(1).getStyle().color)
}

@Test
fun mediaRuleTest() = runTest {
val query = "only screen and (max-width: 600px)"
val styledComponent = fc<RProps> {
styledSpan {
css {
media(query) {
textTransform = TextTransform.capitalize
}
}
}
}
clearAndInject(styledComponent)
assertCssInjected(
"@media $query", listOf(
"text-transform" to "capitalize",
)
)
}

@Test
fun animationTest() = runTest {
val styledComponent = fc<RProps> {
Expand Down Expand Up @@ -282,4 +263,101 @@ class ElementTest : TestBase() {
)
)
}


@Test
fun setCustomProperty() = runTest {
val styledComponent = fc<RProps> {
styledDiv {
css {
setCustomProperty("first-color", firstColor)
}
styledSpan {
css {
color = Color("first-color".toCustomProperty())
}
}
}
}
val element = clearAndInject(styledComponent)
assertEquals(firstColor.toString(), element.getStyle().getPropertyValue("--first-color").trim())
assertEquals(firstColor.toString(), element.childAt(0).color())
}

@Test
fun min() = runTest {
val styledComponent = fc<RProps> {
styledDiv {
css {
paddingLeft = min(1.px, 2.px)
}
}
}
val element = clearAndInject(styledComponent)
assertEquals("1px", element.getStyle().paddingLeft)
}

@Test
fun max() = runTest {
val styledComponent = fc<RProps> {
styledDiv {
css {
paddingLeft = max(1.px, 2.px)
}
}
}
val element = clearAndInject(styledComponent)
assertEquals("2px", element.getStyle().paddingLeft)
}

@Test
fun clamp() = runTest {
val styledComponent = fc<RProps> {
styledDiv {
css {
paddingTop = clamp(2.px, 3.px, 4.px) // value inside bounds
paddingLeft = clamp(2.px, 1.px, 4.px) // value lesser than lower bound
paddingRight = clamp(2.px, 5.px, 4.px) // value greater than higher bound
}
}
}
val element = clearAndInject(styledComponent)
assertEquals("3px", element.getStyle().paddingTop)
assertEquals("2px", element.getStyle().paddingLeft)
assertEquals("4px", element.getStyle().paddingRight)
}

@Test
fun root() = runTest {
val css = CssBuilder().apply {
root {
color = firstColor
}
}
injectGlobal(css)
val styledComponent = fc<RProps> {
div {}
}
val element = clearAndInject(styledComponent)
assertEquals(firstColor.toString(), element.color())
}

@Test
fun compareTo() = runTest {
val styledComponent = fc<RProps> {
styledDiv {
css {
color = firstColor
this > "div" {
color = secondColor
}
}
div {}
span { div {} }
}
}
val element = clearAndInject(styledComponent)
assertEquals(firstColor.toString(), element.color())
assertEquals(secondColor.toString(), element.childAt(0).color())
}
}
Loading