@@ -1598,4 +1598,180 @@ describe("DOM renderer", () => {
15981598 expect ( headingsAfter [ 0 ] ! . textContent ) . toBe ( "MyScreen" )
15991599 } )
16001600 } )
1601+
1602+ describe ( "control rendering for boolean and choice asks" , ( ) => {
1603+ it ( "boolean-backed ask renders as checkbox input" , ( ) => {
1604+ document . body . innerHTML = '<div id="root"></div>'
1605+
1606+ const Screen = screen ( "BoolCheckbox" , $ => {
1607+ const accepted = $ . state . boolean ( "acceptedTerms" )
1608+ const termsAsk = $ . ask ( "Accept terms" , accepted )
1609+ $ . surface ( "main" ) . contains ( termsAsk )
1610+ } )
1611+
1612+ const root = document . getElementById ( "root" ) !
1613+ renderDom ( Screen , { target : root } )
1614+
1615+ const checkbox = root . querySelector ( "input[type=checkbox]" )
1616+ expect ( checkbox ) . not . toBeNull ( )
1617+ expect ( checkbox ! . id ) . toBe ( "ask_accept_terms" )
1618+ expect ( ( checkbox as HTMLInputElement ) . checked ) . toBe ( false )
1619+ } )
1620+
1621+ it ( "checkbox change updates boolean state" , ( ) => {
1622+ document . body . innerHTML = '<div id="root"></div>'
1623+
1624+ const Screen = screen ( "BoolCheckboxUpdate" , $ => {
1625+ const accepted = $ . state . boolean ( "acceptedTerms" )
1626+ const termsAsk = $ . ask ( "Accept terms" , accepted )
1627+ $ . surface ( "main" ) . contains ( termsAsk )
1628+ } )
1629+
1630+ const root = document . getElementById ( "root" ) !
1631+ renderDom ( Screen , { target : root } )
1632+
1633+ const checkbox = root . querySelector ( "input[type=checkbox]" ) as HTMLInputElement
1634+ expect ( checkbox . checked ) . toBe ( false )
1635+
1636+ checkbox . checked = true
1637+ checkbox . dispatchEvent ( new Event ( "change" , { bubbles : true } ) )
1638+
1639+ const state = Screen . asks [ 0 ] ! . state as unknown as { value : boolean }
1640+ expect ( state . value ) . toBe ( true )
1641+
1642+ checkbox . checked = false
1643+ checkbox . dispatchEvent ( new Event ( "change" , { bubbles : true } ) )
1644+ expect ( state . value ) . toBe ( false )
1645+ } )
1646+
1647+ it ( "choice ask renders as select element with options" , ( ) => {
1648+ document . body . innerHTML = '<div id="root"></div>'
1649+
1650+ const Screen = screen ( "ChoiceSelect" , $ => {
1651+ const role = $ . state . choice ( "role" , {
1652+ initial : "member" ,
1653+ options : [ "admin" , "member" , "viewer" ] as const ,
1654+ } )
1655+ const roleAsk = $ . ask ( "Role" , role ) . asChoice ( )
1656+ $ . surface ( "main" ) . contains ( roleAsk )
1657+ } )
1658+
1659+ const root = document . getElementById ( "root" ) !
1660+ renderDom ( Screen , { target : root } )
1661+
1662+ const select = root . querySelector ( "select" )
1663+ expect ( select ) . not . toBeNull ( )
1664+ expect ( select ! . id ) . toBe ( "ask_role" )
1665+
1666+ const options = select ! . querySelectorAll ( "option" )
1667+ expect ( options ) . toHaveLength ( 3 )
1668+ expect ( options [ 0 ] ! . value ) . toBe ( "admin" )
1669+ expect ( options [ 1 ] ! . value ) . toBe ( "member" )
1670+ expect ( options [ 2 ] ! . value ) . toBe ( "viewer" )
1671+ expect ( select ! . value ) . toBe ( "member" )
1672+ } )
1673+
1674+ it ( "select change updates choice state" , ( ) => {
1675+ document . body . innerHTML = '<div id="root"></div>'
1676+
1677+ const Screen = screen ( "ChoiceSelectUpdate" , $ => {
1678+ const role = $ . state . choice ( "role" , {
1679+ initial : "member" ,
1680+ options : [ "admin" , "member" , "viewer" ] as const ,
1681+ } )
1682+ const roleAsk = $ . ask ( "Role" , role ) . asChoice ( )
1683+ $ . surface ( "main" ) . contains ( roleAsk )
1684+ } )
1685+
1686+ const root = document . getElementById ( "root" ) !
1687+ renderDom ( Screen , { target : root } )
1688+
1689+ const select = root . querySelector ( "select" ) as HTMLSelectElement
1690+ select . value = "admin"
1691+ select . dispatchEvent ( new Event ( "change" , { bubbles : true } ) )
1692+
1693+ const state = Screen . asks [ 0 ] ! . state as unknown as { value : string }
1694+ expect ( state . value ) . toBe ( "admin" )
1695+
1696+ select . value = "viewer"
1697+ select . dispatchEvent ( new Event ( "change" , { bubbles : true } ) )
1698+ expect ( state . value ) . toBe ( "viewer" )
1699+ } )
1700+
1701+ it ( "contact/secret/text inputs retain existing behavior" , ( ) => {
1702+ document . body . innerHTML = '<div id="root"></div>'
1703+
1704+ const Screen = screen ( "ExistingBehavior" , $ => {
1705+ const email = $ . state . text ( "email" )
1706+ const password = $ . state . text ( "password" )
1707+ const name = $ . state . text ( "name" )
1708+ const emailAsk = $ . ask ( "Email" , email ) . asContact ( "email" ) . required ( )
1709+ const passwordAsk = $ . ask ( "Password" , password ) . asSecret ( ) . required ( )
1710+ const nameAsk = $ . ask ( "Name" , name ) . required ( )
1711+ $ . surface ( "main" ) . contains ( emailAsk , passwordAsk , nameAsk )
1712+ } )
1713+
1714+ const root = document . getElementById ( "root" ) !
1715+ renderDom ( Screen , { target : root } )
1716+
1717+ const inputs = root . querySelectorAll ( "input" )
1718+ expect ( inputs ) . toHaveLength ( 3 )
1719+ expect ( inputs [ 0 ] ! . getAttribute ( "type" ) ) . toBe ( "email" )
1720+ expect ( inputs [ 1 ] ! . getAttribute ( "type" ) ) . toBe ( "password" )
1721+ expect ( inputs [ 2 ] ! . getAttribute ( "type" ) ) . toBe ( "text" )
1722+ } )
1723+
1724+ it ( "blocked reason has intent-blocked-reason class and role alert" , ( ) => {
1725+ document . body . innerHTML = '<div id="root"></div>'
1726+
1727+ const Screen = screen ( "BlockedReasonAccessible" , $ => {
1728+ const email = $ . state . text ( "email" )
1729+ const emailAsk = $ . ask ( "Email" , email ) . required ( )
1730+ const login = $ . act ( "Log in" ) . primary ( ) . when ( emailAsk . valid , "Enter your email." )
1731+ $ . surface ( "main" ) . contains ( emailAsk , login )
1732+ } )
1733+
1734+ const root = document . getElementById ( "root" ) !
1735+ renderDom ( Screen , { target : root } )
1736+
1737+ const reasonEl = document . querySelector ( ".intent-blocked-reason" )
1738+ expect ( reasonEl ) . not . toBeNull ( )
1739+ expect ( reasonEl ! . getAttribute ( "role" ) ) . toBe ( "alert" )
1740+ expect ( reasonEl ! . textContent ) . toBe ( "Enter your email." )
1741+ } )
1742+
1743+ it ( "button enables after filling text fields and checking checkbox" , ( ) => {
1744+ document . body . innerHTML = '<div id="root"></div>'
1745+
1746+ const Screen = screen ( "ButtonEnablesCheckbox" , $ => {
1747+ const name = $ . state . text ( "name" )
1748+ const accepted = $ . state . boolean ( "acceptedTerms" )
1749+ const nameAsk = $ . ask ( "Name" , name ) . required ( )
1750+ const termsAsk = $ . ask ( "Accept terms" , accepted )
1751+ . validate ( v => v === true ? true : "Accept the terms to continue" )
1752+ const submit = $ . act ( "Submit" ) . primary ( )
1753+ . when ( nameAsk . valid , "Enter your name" )
1754+ . when ( termsAsk . valid , "Accept the terms to continue" )
1755+ $ . surface ( "main" ) . contains ( nameAsk , termsAsk , submit )
1756+ } )
1757+
1758+ const root = document . getElementById ( "root" ) !
1759+ renderDom ( Screen , { target : root } )
1760+
1761+ const button = root . querySelector ( "button" ) as HTMLButtonElement
1762+ expect ( button . disabled ) . toBe ( true )
1763+
1764+ // Fill in text field — still blocked because checkbox not checked
1765+ const nameInput = root . querySelector ( "input[type=text]" ) as HTMLInputElement
1766+ nameInput . value = "Ada"
1767+ nameInput . dispatchEvent ( new Event ( "input" , { bubbles : true } ) )
1768+ expect ( button . disabled ) . toBe ( true )
1769+
1770+ // Check the checkbox — now enabled
1771+ const checkbox = root . querySelector ( "input[type=checkbox]" ) as HTMLInputElement
1772+ checkbox . checked = true
1773+ checkbox . dispatchEvent ( new Event ( "change" , { bubbles : true } ) )
1774+ expect ( button . disabled ) . toBe ( false )
1775+ } )
1776+ } )
16011777} )
0 commit comments