diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/rich/InputRichBlocks.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/rich/InputRichBlocks.kt
index b5343c2062..7d0d275c53 100644
--- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/rich/InputRichBlocks.kt
+++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/rich/InputRichBlocks.kt
@@ -223,7 +223,7 @@ data class InputRichBlockBlockQuotation(
}
}
-/** A block quotation that is collapsed by default. */
+/** A block quotation that users can expand. */
@Serializable
data class InputRichBlockExpandableBlockQuotation(
@SerialName(textField)
diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/rich/RichBlocks.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/rich/RichBlocks.kt
index b6424a208e..f4f84de236 100644
--- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/rich/RichBlocks.kt
+++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/rich/RichBlocks.kt
@@ -305,7 +305,7 @@ data class RichBlockBlockQuotation(
}
/**
- * A block quotation that is collapsed by default.
+ * A block quotation that users can expand.
*
* @see RichBlockExpandableBlockQuotation
*/
@@ -326,9 +326,9 @@ data class RichBlockExpandableBlockQuotation(
companion object {
const val TYPE = "expandable_blockquote"
fun markdown(text: RichText, credit: RichText?): String =
- "
${text.markdown}${creditCiteMarkdown(credit)}
"
+ "${text.html}${creditCiteHtml(credit)}
"
fun html(text: RichText, credit: RichText?): String =
- "${text.html}${creditCiteHtml(credit)}
"
+ "${text.html}${creditCiteHtml(credit)}
"
}
}
diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/rich/RichMessageButton.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/rich/RichMessageButton.kt
index 4fcaa6dce7..a1e1ac9924 100644
--- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/rich/RichMessageButton.kt
+++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/rich/RichMessageButton.kt
@@ -32,6 +32,9 @@ sealed interface RichMessageButton {
val text: RichText
val style: RichMessageButtonStyle?
+ /** Creates rich HTML or Markdown button markup containing [text]. */
+ fun toRichMarkup(text: String): String
+
@Serializable
data class Url(
@SerialName(textField) override val text: RichText,
@@ -39,6 +42,10 @@ sealed interface RichMessageButton {
@SerialName(styleField) override val style: RichMessageButtonStyle? = null
) : RichMessageButton {
init { validateRichMessageButton(text, style) }
+ override fun toRichMarkup(text: String): String = richMarkup(text, style) {
+ attribute("type", "url")
+ attribute("url", url)
+ }
}
@Serializable
@@ -48,6 +55,10 @@ sealed interface RichMessageButton {
@SerialName(styleField) override val style: RichMessageButtonStyle? = null
) : RichMessageButton {
init { validateRichMessageButton(text, style, allowLinkStyle = true) }
+ override fun toRichMarkup(text: String): String = richMarkup(text, style) {
+ attribute("type", "callback_data")
+ attribute("data", callbackData)
+ }
}
@Serializable
@@ -57,6 +68,10 @@ sealed interface RichMessageButton {
@SerialName(styleField) override val style: RichMessageButtonStyle? = null
) : RichMessageButton {
init { validateRichMessageButton(text, style) }
+ override fun toRichMarkup(text: String): String = richMarkup(text, style) {
+ attribute("type", "web_app")
+ attribute("url", webApp.url)
+ }
}
@Serializable
@@ -69,6 +84,13 @@ sealed interface RichMessageButton {
validateRichMessageButton(text, style)
require(loginUrl.botUsername == null) { "Rich message login URL buttons do not support bot usernames" }
}
+ override fun toRichMarkup(text: String): String = richMarkup(text, style) {
+ attribute("type", "login_url")
+ attribute("url", loginUrl.url)
+ loginUrl.forwardText?.let { attribute("forward-text", it) }
+ loginUrl.botUsername?.let { attribute("bot-username", it) }
+ if (loginUrl.requestWriteAccess == true) add("request-write-access")
+ }
}
@Serializable
@@ -78,6 +100,10 @@ sealed interface RichMessageButton {
@SerialName(styleField) override val style: RichMessageButtonStyle? = null
) : RichMessageButton {
init { validateRichMessageButton(text, style) }
+ override fun toRichMarkup(text: String): String = richMarkup(text, style) {
+ attribute("type", "switch_inline_query")
+ attribute("query", switchInlineQuery)
+ }
}
@Serializable
@@ -87,6 +113,10 @@ sealed interface RichMessageButton {
@SerialName(styleField) override val style: RichMessageButtonStyle? = null
) : RichMessageButton {
init { validateRichMessageButton(text, style) }
+ override fun toRichMarkup(text: String): String = richMarkup(text, style) {
+ attribute("type", "switch_inline_query_current_chat")
+ attribute("query", switchInlineQueryCurrentChat)
+ }
}
@Serializable
@@ -96,6 +126,14 @@ sealed interface RichMessageButton {
@SerialName(styleField) override val style: RichMessageButtonStyle? = null
) : RichMessageButton {
init { validateRichMessageButton(text, style) }
+ override fun toRichMarkup(text: String): String = richMarkup(text, style) {
+ attribute("type", "switch_inline_query_chosen_chat")
+ switchInlineQueryChosenChat.query?.let { attribute("query", it) }
+ if (switchInlineQueryChosenChat.allowUsers) add("allow-user-chats")
+ if (switchInlineQueryChosenChat.allowBots) add("allow-bot-chats")
+ if (switchInlineQueryChosenChat.allowGroups) add("allow-group-chats")
+ if (switchInlineQueryChosenChat.allowChannels) add("allow-channel-chats")
+ }
}
@Serializable
@@ -105,6 +143,10 @@ sealed interface RichMessageButton {
@SerialName(styleField) override val style: RichMessageButtonStyle? = null
) : RichMessageButton {
init { validateRichMessageButton(text, style) }
+ override fun toRichMarkup(text: String): String = richMarkup(text, style) {
+ attribute("type", "copy_text")
+ attribute("text", copyText.text)
+ }
}
@Serializable
@@ -116,6 +158,10 @@ sealed interface RichMessageButton {
@SerialName(disabledField)
@kotlinx.serialization.EncodeDefault
val disabled: DisabledButton = DisabledButton
+
+ override fun toRichMarkup(text: String): String = richMarkup(text, style) {
+ attribute("type", "disabled")
+ }
}
}
@@ -124,7 +170,7 @@ private fun validateRichMessageButton(
style: RichMessageButtonStyle?,
allowLinkStyle: Boolean = false
) {
- require(text.isValidRichMessageButtonText()) {
+ require(text.isValidRichMessageButtonText) {
"Rich message button text can contain only plain text, custom emoji and date-time entities"
}
require(allowLinkStyle || style != RichMessageButtonStyle.Link) {
@@ -132,14 +178,6 @@ private fun validateRichMessageButton(
}
}
-private fun RichText.isValidRichMessageButtonText(): Boolean = when (this) {
- is RichTextPlain -> true
- is RichTextCustomEmoji -> true
- is RichTextDateTime -> text.isValidRichMessageButtonText()
- is RichTextGroup -> parts.all { it.isValidRichMessageButtonText() }
- else -> false
-}
-
/** Closed set of styles supported by [RichMessageButton]. */
@Serializable(RichMessageButtonStyle.Serializer::class)
sealed interface RichMessageButtonStyle {
@@ -234,57 +272,21 @@ sealed interface RichBlockButtonAlignment {
}
}
-internal fun RichMessageButton.toRichMarkup(text: String): String {
+private fun richMarkup(
+ text: String,
+ style: RichMessageButtonStyle?,
+ buildAttributes: MutableList.() -> Unit
+): String {
val attributes = mutableListOf()
- fun attribute(name: String, value: String) {
- attributes.add("$name=\"${value.escapeRichMarkupAttribute()}\"")
- }
- when (this) {
- is RichMessageButton.Url -> {
- attribute("type", "url")
- attribute("url", url)
- }
- is RichMessageButton.CallbackData -> {
- attribute("type", "callback_data")
- attribute("data", callbackData)
- }
- is RichMessageButton.WebApp -> {
- attribute("type", "web_app")
- attribute("url", webApp.url)
- }
- is RichMessageButton.LoginUrl -> {
- attribute("type", "login_url")
- attribute("url", loginUrl.url)
- loginUrl.forwardText?.let { attribute("forward-text", it) }
- loginUrl.botUsername?.let { attribute("bot-username", it) }
- if (loginUrl.requestWriteAccess == true) attributes.add("request-write-access")
- }
- is RichMessageButton.SwitchInlineQuery -> {
- attribute("type", "switch_inline_query")
- attribute("query", switchInlineQuery)
- }
- is RichMessageButton.SwitchInlineQueryCurrentChat -> {
- attribute("type", "switch_inline_query_current_chat")
- attribute("query", switchInlineQueryCurrentChat)
- }
- is RichMessageButton.SwitchInlineQueryChosenChat -> {
- attribute("type", "switch_inline_query_chosen_chat")
- switchInlineQueryChosenChat.query?.let { attribute("query", it) }
- if (switchInlineQueryChosenChat.allowUsers) attributes.add("allow-user-chats")
- if (switchInlineQueryChosenChat.allowBots) attributes.add("allow-bot-chats")
- if (switchInlineQueryChosenChat.allowGroups) attributes.add("allow-group-chats")
- if (switchInlineQueryChosenChat.allowChannels) attributes.add("allow-channel-chats")
- }
- is RichMessageButton.CopyText -> {
- attribute("type", "copy_text")
- attribute("text", copyText.text)
- }
- is RichMessageButton.Disabled -> attribute("type", "disabled")
- }
- style?.let { attribute("style", it.name) }
+ attributes.buildAttributes()
+ style?.let { attributes.attribute("style", it.name) }
return "$text"
}
+private fun MutableList.attribute(name: String, value: String) {
+ add("$name=\"${value.escapeRichMarkupAttribute()}\"")
+}
+
private fun String.escapeRichMarkupAttribute(): String = replace("&", "&")
.replace("\"", """)
.replace("<", "<")
diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/rich/RichText.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/rich/RichText.kt
index 62f920ec65..9a7c590609 100644
--- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/rich/RichText.kt
+++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/rich/RichText.kt
@@ -36,6 +36,9 @@ sealed interface RichText {
* [Rich HTML style](https://core.telegram.org/bots/api#rich-html-style) representation of this [RichText].
*/
val html: String
+
+ /** Whether this text is permitted as [RichMessageButton] text. */
+ val isValidRichMessageButtonText: Boolean
}
/**
@@ -48,6 +51,7 @@ data class RichTextPlain(
override val rawText: String = text
override val markdown: String = markdown(text)
override val html: String = html(text)
+ override val isValidRichMessageButtonText: Boolean = true
companion object {
fun markdown(text: String): String = text.escapeRichMarkdown()
@@ -65,6 +69,7 @@ data class RichTextGroup(
override val rawText: String = parts.joinToString(separator = "") { it.rawText }
override val markdown: String = markdown(parts)
override val html: String = html(parts)
+ override val isValidRichMessageButtonText: Boolean = parts.all { it.isValidRichMessageButtonText }
companion object {
fun markdown(parts: List): String = parts.joinToString(separator = "") { it.markdown }
@@ -81,6 +86,7 @@ sealed interface RichTextEntity : RichText {
override val markdown: String
override val html: String
+ override val isValidRichMessageButtonText: Boolean
}
object RichTextSerializer : KSerializer {
diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/rich/RichTextEntities.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/rich/RichTextEntities.kt
index 5681fc223e..572bdfb420 100644
--- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/rich/RichTextEntities.kt
+++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/rich/RichTextEntities.kt
@@ -46,6 +46,7 @@ data class RichTextButton(
override val rawText: String = button.text.rawText
override val markdown: String = markdown(button)
override val html: String = html(button)
+ override val isValidRichMessageButtonText: Boolean = false
companion object {
const val TYPE = "button"
@@ -71,6 +72,7 @@ data class RichTextBold(
override val rawText: String = text.rawText
override val markdown: String = markdown(text)
override val html: String = html(text)
+ override val isValidRichMessageButtonText: Boolean = false
companion object {
const val TYPE = "bold"
@@ -96,6 +98,7 @@ data class RichTextItalic(
override val rawText: String = text.rawText
override val markdown: String = markdown(text)
override val html: String = html(text)
+ override val isValidRichMessageButtonText: Boolean = false
companion object {
const val TYPE = "italic"
@@ -121,6 +124,7 @@ data class RichTextUnderline(
override val rawText: String = text.rawText
override val markdown: String = markdown(text)
override val html: String = html(text)
+ override val isValidRichMessageButtonText: Boolean = false
companion object {
const val TYPE = "underline"
@@ -146,6 +150,7 @@ data class RichTextStrikethrough(
override val rawText: String = text.rawText
override val markdown: String = markdown(text)
override val html: String = html(text)
+ override val isValidRichMessageButtonText: Boolean = false
companion object {
const val TYPE = "strikethrough"
@@ -171,6 +176,7 @@ data class RichTextSpoiler(
override val rawText: String = text.rawText
override val markdown: String = markdown(text)
override val html: String = html(text)
+ override val isValidRichMessageButtonText: Boolean = false
companion object {
const val TYPE = "spoiler"
@@ -196,6 +202,7 @@ data class RichTextSubscript(
override val rawText: String = text.rawText
override val markdown: String = markdown(text)
override val html: String = html(text)
+ override val isValidRichMessageButtonText: Boolean = false
companion object {
const val TYPE = "subscript"
@@ -221,6 +228,7 @@ data class RichTextSuperscript(
override val rawText: String = text.rawText
override val markdown: String = markdown(text)
override val html: String = html(text)
+ override val isValidRichMessageButtonText: Boolean = false
companion object {
const val TYPE = "superscript"
@@ -246,6 +254,7 @@ data class RichTextMarked(
override val rawText: String = text.rawText
override val markdown: String = markdown(text)
override val html: String = html(text)
+ override val isValidRichMessageButtonText: Boolean = false
companion object {
const val TYPE = "marked"
@@ -271,6 +280,7 @@ data class RichTextCode(
override val rawText: String = text.rawText
override val markdown: String = markdown(text)
override val html: String = html(text)
+ override val isValidRichMessageButtonText: Boolean = false
companion object {
const val TYPE = "code"
@@ -300,6 +310,7 @@ data class RichTextDateTime(
override val rawText: String = text.rawText
override val markdown: String = markdown(text, unixTime, dateTimeFormat)
override val html: String = html(text, unixTime, dateTimeFormat)
+ override val isValidRichMessageButtonText: Boolean = text.isValidRichMessageButtonText
companion object {
const val TYPE = "date_time"
@@ -329,6 +340,7 @@ data class RichTextTextMention(
override val rawText: String = text.rawText
override val markdown: String = markdown(text, user)
override val html: String = html(text, user)
+ override val isValidRichMessageButtonText: Boolean = false
companion object {
const val TYPE = "text_mention"
@@ -358,6 +370,7 @@ data class RichTextCustomEmoji(
override val rawText: String = alternativeText
override val markdown: String = markdown(customEmojiId, alternativeText)
override val html: String = html(customEmojiId, alternativeText)
+ override val isValidRichMessageButtonText: Boolean = true
companion object {
const val TYPE = "custom_emoji"
@@ -385,6 +398,7 @@ data class RichTextMathematicalExpression(
override val rawText: String = expression
override val markdown: String = markdown(expression)
override val html: String = html(expression)
+ override val isValidRichMessageButtonText: Boolean = false
companion object {
const val TYPE = "mathematical_expression"
@@ -412,6 +426,7 @@ data class RichTextUrl(
override val rawText: String = text.rawText
override val markdown: String = markdown(text, url)
override val html: String = html(text, url)
+ override val isValidRichMessageButtonText: Boolean = false
companion object {
const val TYPE = "url"
@@ -439,6 +454,7 @@ data class RichTextEmailAddress(
override val rawText: String = text.rawText
override val markdown: String = markdown(text, emailAddress)
override val html: String = html(text, emailAddress)
+ override val isValidRichMessageButtonText: Boolean = false
companion object {
const val TYPE = "email_address"
@@ -466,6 +482,7 @@ data class RichTextPhoneNumber(
override val rawText: String = text.rawText
override val markdown: String = markdown(text, phoneNumber)
override val html: String = html(text, phoneNumber)
+ override val isValidRichMessageButtonText: Boolean = false
companion object {
const val TYPE = "phone_number"
@@ -493,6 +510,7 @@ data class RichTextBankCardNumber(
override val rawText: String = text.rawText
override val markdown: String = markdown(text)
override val html: String = html(text)
+ override val isValidRichMessageButtonText: Boolean = false
companion object {
const val TYPE = "bank_card_number"
@@ -520,6 +538,7 @@ data class RichTextMention(
override val rawText: String = text.rawText
override val markdown: String = markdown(text)
override val html: String = html(text)
+ override val isValidRichMessageButtonText: Boolean = false
companion object {
const val TYPE = "mention"
@@ -547,6 +566,7 @@ data class RichTextHashtag(
override val rawText: String = text.rawText
override val markdown: String = markdown(text)
override val html: String = html(text)
+ override val isValidRichMessageButtonText: Boolean = false
companion object {
const val TYPE = "hashtag"
@@ -574,6 +594,7 @@ data class RichTextCashtag(
override val rawText: String = text.rawText
override val markdown: String = markdown(text)
override val html: String = html(text)
+ override val isValidRichMessageButtonText: Boolean = false
companion object {
const val TYPE = "cashtag"
@@ -601,6 +622,7 @@ data class RichTextBotCommand(
override val rawText: String = text.rawText
override val markdown: String = markdown(text)
override val html: String = html(text)
+ override val isValidRichMessageButtonText: Boolean = false
companion object {
const val TYPE = "bot_command"
@@ -626,6 +648,7 @@ data class RichTextAnchor(
override val rawText: String = ""
override val markdown: String = markdown(name)
override val html: String = html(name)
+ override val isValidRichMessageButtonText: Boolean = false
companion object {
const val TYPE = "anchor"
@@ -653,6 +676,7 @@ data class RichTextAnchorLink(
override val rawText: String = text.rawText
override val markdown: String = markdown(text, anchorName)
override val html: String = html(text, anchorName)
+ override val isValidRichMessageButtonText: Boolean = false
companion object {
const val TYPE = "anchor_link"
@@ -680,6 +704,7 @@ data class RichTextReference(
override val rawText: String = text.rawText
override val markdown: String = markdown(text, name)
override val html: String = html(text, name)
+ override val isValidRichMessageButtonText: Boolean = false
companion object {
const val TYPE = "reference"
@@ -707,6 +732,7 @@ data class RichTextReferenceLink(
override val rawText: String = text.rawText
override val markdown: String = markdown(text, referenceName)
override val html: String = html(text, referenceName)
+ override val isValidRichMessageButtonText: Boolean = false
companion object {
const val TYPE = "reference_link"
diff --git a/tgbotapi.core/src/commonTest/kotlin/dev/inmo/tgbotapi/types/RichMessageSerializationTest.kt b/tgbotapi.core/src/commonTest/kotlin/dev/inmo/tgbotapi/types/RichMessageSerializationTest.kt
index ac3df7373c..f96f8d46a0 100644
--- a/tgbotapi.core/src/commonTest/kotlin/dev/inmo/tgbotapi/types/RichMessageSerializationTest.kt
+++ b/tgbotapi.core/src/commonTest/kotlin/dev/inmo/tgbotapi/types/RichMessageSerializationTest.kt
@@ -114,6 +114,7 @@ class RichMessageSerializationTest {
)
validTexts.forEach { text ->
+ assertEquals(true, text.isValidRichMessageButtonText)
assertEquals(text, RichMessageButton.Disabled(text).text)
}
@@ -124,6 +125,7 @@ class RichMessageSerializationTest {
RichTextDateTime(RichTextBold(plain), TelegramDate(1L), "wDT")
)
invalidTexts.forEach { text ->
+ assertEquals(false, text.isValidRichMessageButtonText)
assertFailsWith { RichMessageButton.Disabled(text) }
}
diff --git a/tgbotapi.core/src/commonTest/kotlin/dev/inmo/tgbotapi/types/rich/RichBlockFormattingTest.kt b/tgbotapi.core/src/commonTest/kotlin/dev/inmo/tgbotapi/types/rich/RichBlockFormattingTest.kt
index e0b2b8b4cf..4d4adac7eb 100644
--- a/tgbotapi.core/src/commonTest/kotlin/dev/inmo/tgbotapi/types/rich/RichBlockFormattingTest.kt
+++ b/tgbotapi.core/src/commonTest/kotlin/dev/inmo/tgbotapi/types/rich/RichBlockFormattingTest.kt
@@ -1,7 +1,9 @@
package dev.inmo.tgbotapi.types.rich
+import dev.inmo.tgbotapi.types.CustomEmojiId
import kotlin.test.Test
import kotlin.test.assertEquals
+import kotlin.test.assertFalse
class RichBlockFormattingTest {
@Test
@@ -99,8 +101,18 @@ class RichBlockFormattingTest {
@Test
fun expandableBlockQuotationAndButtons() {
val quotation = RichBlockExpandableBlockQuotation(RichTextPlain("q"), RichTextPlain("credit"))
- assertEquals("qcredit
", quotation.markdown)
- assertEquals("qcredit
", quotation.html)
+ assertEquals("qcredit
", quotation.markdown)
+ assertEquals("qcredit
", quotation.html)
+ val customEmojiQuotation = RichBlockExpandableBlockQuotation(
+ RichTextCustomEmoji(CustomEmojiId("emoji"), "emoji"),
+ RichTextCustomEmoji(CustomEmojiId("credit"), "credit")
+ )
+ assertEquals(
+ "emoji" +
+ "credit
",
+ customEmojiQuotation.markdown
+ )
+ assertFalse(customEmojiQuotation.markdown.contains("!["))
val buttons = RichBlockButtons(listOf(RichMessageButton.Disabled(RichTextPlain("Disabled"))), RichBlockButtonAlignment.Right)
assertEquals("Disabled", buttons.html)
}
diff --git a/tgbotapi.core/src/commonTest/kotlin/dev/inmo/tgbotapi/types/rich/RichTextFormattingTest.kt b/tgbotapi.core/src/commonTest/kotlin/dev/inmo/tgbotapi/types/rich/RichTextFormattingTest.kt
index e48217ca03..05d085036f 100644
--- a/tgbotapi.core/src/commonTest/kotlin/dev/inmo/tgbotapi/types/rich/RichTextFormattingTest.kt
+++ b/tgbotapi.core/src/commonTest/kotlin/dev/inmo/tgbotapi/types/rich/RichTextFormattingTest.kt
@@ -245,7 +245,9 @@ class RichTextFormattingTest {
SwitchInlineQueryChosenChatParameters(rawAttribute, allowUsers = true)
) to "type=\"switch_inline_query_chosen_chat\" query=\"$escapedAttribute\" allow-user-chats",
RichMessageButton.CopyText(text, CopyTextButtonData(rawAttribute)) to
- "type=\"copy_text\" text=\"$escapedAttribute\""
+ "type=\"copy_text\" text=\"$escapedAttribute\"",
+ RichMessageButton.Disabled(text) to
+ "type=\"disabled\""
)
buttonsWithExpectedAttributes.forEach { (button, expectedAttributes) ->