Repository: streamlit/streamlit
Pull request: Markdown support for radio button labels
## Describe your changes
Allows markdown in the options of `st.radio` - the same ones as permitted in Radio's label, with the exception of links because it is a clickable element.

## GitHub Issue Link (if applicable)
Closes #6085

## Testing Plan

- FE/JS unit tests: Re-wrote both `Radio.test.tsx` files to to RTL
- E2E/Snapshot tests: Added snapshot test w/ markdown in radio options

**Before:**
![BeforeRadioMarkdownScreenshot](https://github.com/streamlit/streamlit/assets/63436329/b6192dc3-1050-437a-abb8-fc8070426c3c)

**After:**
![After - no links](https://github.com/streamlit/streamlit/assets/63436329/edb25652-485b-4546-a1d7-e772751550d3)

---

**Contribution License Agreement**

By submitting this pull request you agree that all contributions to this project are made under the Apache 2.0 license.


Problem statement:
Markdown support for radio buttons
### Problem

Colored text and other markdown elements work in the label of `st.radio` but not in the texts of the radio elements. This is a bit weird since we do support it in the texts of checkboxes (where the text next to the checkbox is the label). 


### Solution

Allow markdown in the options of `st.radio`. 


---

Community voting on feature requests enables the Streamlit team to understand which features are most important to our users.

**If you'd like the Streamlit team to prioritize this feature request, please use the 👍 (thumbs up emoji) reaction in response to the initial post.**

Hints from issue discussion:

Commit message: Add updated snaps

Patch to review:
diff --git a/e2e/scripts/st_radio.py b/e2e/scripts/st_radio.py
index 2394c567ff76..0eb963c656aa 100644
--- a/e2e/scripts/st_radio.py
+++ b/e2e/scripts/st_radio.py
@@ -19,6 +19,16 @@
 from tests.streamlit import pyspark_mocks
 
 options = ("female", "male")
+markdown_options = (
+    "**bold text**",
+    "*italics text*",
+    "~strikethrough text~",
+    "shortcode: :blush:",
+    # link should not work in radio options
+    "[link text](www.example.com)",
+    "`code text`",
+    ":red[red] :blue[blue] :green[green] :violet[violet] :orange[orange]",
+)
 i1 = st.radio("radio 1", options, 1)
 st.write("value 1:", i1)
 
@@ -43,14 +53,16 @@
 i8 = st.radio("radio 8", options, label_visibility="collapsed")
 st.write("value 8:", i8)
 
+i9 = st.radio("radio 9", markdown_options)
+st.write("value 9:", i9)
 
 if runtime.exists():
 
     def on_change():
         st.session_state.radio_changed = True
 
-    st.radio("radio 9", options, 1, key="radio9", on_change=on_change)
-    st.write("value 9:", st.session_state.radio9)
+    st.radio("radio 10", options, 1, key="radio10", on_change=on_change)
+    st.write("value 10:", st.session_state.radio10)
     st.write("radio changed:", "radio_changed" in st.session_state)
 
 st.radio("PySpark radio", pyspark_mocks.DataFrame())  # type: ignore
diff --git a/e2e/specs/st_radio.spec.js b/e2e/specs/st_radio.spec.js
index 87373c438a00..672232444a22 100644
--- a/e2e/specs/st_radio.spec.js
+++ b/e2e/specs/st_radio.spec.js
@@ -22,7 +22,7 @@ describe("st.radio", () => {
   });
 
   it("shows widget correctly", () => {
-    cy.get(".stRadio").should("have.length", 10);
+    cy.get(".stRadio").should("have.length", 11);
 
     cy.get(".stRadio").each((el, idx) => {
       return cy.wrap(el).matchThemedSnapshots("radio" + idx);
@@ -95,7 +95,8 @@ describe("st.radio", () => {
         "value 6: female" +
         "value 7: female" +
         "value 8: female" +
-        "value 9: male" +
+        "value 9: bold text" +
+        "value 10: male" +
         "radio changed: False"
     );
   });
@@ -139,13 +140,14 @@ describe("st.radio", () => {
         "value 6: male" +
         "value 7: male" +
         "value 8: male" +
-        "value 9: male" +
+        "value 9: red blue green violet orange" +
+        "value 10: male" +
         "radio changed: False"
     );
   });
 
   it("calls callback if one is registered", () => {
-    cy.getIndexed(".stRadio", 8).then(el => {
+    cy.getIndexed(".stRadio", 9).then(el => {
       return cy
         .wrap(el)
         .find("input")
@@ -163,7 +165,8 @@ describe("st.radio", () => {
         "value 6: female" +
         "value 7: female" +
         "value 8: female" +
-        "value 9: female" +
+        "value 9: bold text" +
+        "value 10: female" +
         "radio changed: True"
     );
   });
diff --git a/frontend/cypress/snapshots/linux/2x/st_radio.spec.js/radio10-dark.snap.png b/frontend/cypress/snapshots/linux/2x/st_radio.spec.js/radio10-dark.snap.png
new file mode 100644
index 000000000000..1bde91ed3278
Binary files /dev/null and b/frontend/cypress/snapshots/linux/2x/st_radio.spec.js/radio10-dark.snap.png differ
diff --git a/frontend/cypress/snapshots/linux/2x/st_radio.spec.js/radio10.snap.png b/frontend/cypress/snapshots/linux/2x/st_radio.spec.js/radio10.snap.png
new file mode 100644
index 000000000000..d5e0bb5f92a0
Binary files /dev/null and b/frontend/cypress/snapshots/linux/2x/st_radio.spec.js/radio10.snap.png differ
diff --git a/frontend/cypress/snapshots/linux/2x/st_radio.spec.js/radio8-dark.snap.png b/frontend/cypress/snapshots/linux/2x/st_radio.spec.js/radio8-dark.snap.png
index 6ab9115d4181..2388ad3f3c56 100644
Binary files a/frontend/cypress/snapshots/linux/2x/st_radio.spec.js/radio8-dark.snap.png and b/frontend/cypress/snapshots/linux/2x/st_radio.spec.js/radio8-dark.snap.png differ
diff --git a/frontend/cypress/snapshots/linux/2x/st_radio.spec.js/radio8.snap.png b/frontend/cypress/snapshots/linux/2x/st_radio.spec.js/radio8.snap.png
index 91de8ceb40d0..a82cf77ab72e 100644
Binary files a/frontend/cypress/snapshots/linux/2x/st_radio.spec.js/radio8.snap.png and b/frontend/cypress/snapshots/linux/2x/st_radio.spec.js/radio8.snap.png differ
diff --git a/frontend/cypress/snapshots/linux/2x/st_radio.spec.js/radio9-dark.snap.png b/frontend/cypress/snapshots/linux/2x/st_radio.spec.js/radio9-dark.snap.png
index aa675f07ea3d..d44eae20a0e1 100644
Binary files a/frontend/cypress/snapshots/linux/2x/st_radio.spec.js/radio9-dark.snap.png and b/frontend/cypress/snapshots/linux/2x/st_radio.spec.js/radio9-dark.snap.png differ
diff --git a/frontend/cypress/snapshots/linux/2x/st_radio.spec.js/radio9.snap.png b/frontend/cypress/snapshots/linux/2x/st_radio.spec.js/radio9.snap.png
index c55d3ce0451e..42d5de1d07ba 100644
Binary files a/frontend/cypress/snapshots/linux/2x/st_radio.spec.js/radio9.snap.png and b/frontend/cypress/snapshots/linux/2x/st_radio.spec.js/radio9.snap.png differ
diff --git a/frontend/lib/src/components/shared/Radio/Radio.test.tsx b/frontend/lib/src/components/shared/Radio/Radio.test.tsx
index a69cc7f74eb8..f2bce73125e1 100644
--- a/frontend/lib/src/components/shared/Radio/Radio.test.tsx
+++ b/frontend/lib/src/components/shared/Radio/Radio.test.tsx
@@ -15,9 +15,10 @@
  */
 
 import React from "react"
-import { mount } from "@streamlit/lib/src/test_util"
+import { render } from "@streamlit/lib/src/test_util"
+import { screen, fireEvent } from "@testing-library/react"
+import "@testing-library/jest-dom"
 
-import { Radio as UIRadio, RadioGroup, ALIGN } from "baseui/radio"
 import { LabelVisibilityOptions } from "@streamlit/lib/src/util/utils"
 import { mockTheme } from "@streamlit/lib/src/mocks/mockTheme"
 import Radio, { Props } from "./Radio"
@@ -37,112 +38,110 @@ const getProps = (props: Partial<Props> = {}): Props => ({
 describe("Radio widget", () => {
   it("renders without crashing", () => {
     const props = getProps()
-    const wrapper = mount(<Radio {...props} />)
+    render(<Radio {...props} />)
+    const radioGroup = screen.getByRole("radiogroup")
+    const radioOptions = screen.getAllByRole("radio")
 
-    expect(wrapper.find(RadioGroup).length).toBe(1)
-    expect(wrapper.find(UIRadio).length).toBe(3)
+    expect(radioGroup).toBeInTheDocument()
+    expect(radioOptions).toHaveLength(3)
   })
 
   it("renders without crashing if no label is provided", () => {
     const props = getProps({ label: undefined })
-    const wrapper = mount(<Radio {...props} />)
-    expect(wrapper.find(RadioGroup).length).toBe(1)
-    expect(wrapper.find(UIRadio).length).toBe(3)
+    render(<Radio {...props} />)
+    const widgetLabel = screen.queryByText("Label")
+    const radioOptions = screen.getByRole("radiogroup")
+
+    expect(widgetLabel).toBeNull()
+    expect(radioOptions).toBeInTheDocument()
   })
 
   it("pass labelVisibility prop to StyledWidgetLabel correctly when hidden", () => {
     const props = getProps({
       labelVisibility: LabelVisibilityOptions.Hidden,
     })
-    const wrapper = mount(<Radio {...props} />)
-    expect(wrapper.find("StyledWidgetLabel").prop("labelVisibility")).toEqual(
-      LabelVisibilityOptions.Hidden
-    )
+    render(<Radio {...props} />)
+
+    const widgetLabel = screen.getByText("Label")
+    expect(widgetLabel).toHaveStyle("visibility: hidden")
+    expect(widgetLabel).not.toBeVisible()
   })
 
   it("pass labelVisibility prop to StyledWidgetLabel correctly when collapsed", () => {
     const props = getProps({
       labelVisibility: LabelVisibilityOptions.Collapsed,
     })
-    const wrapper = mount(<Radio {...props} />)
-    expect(wrapper.find("StyledWidgetLabel").prop("labelVisibility")).toEqual(
-      LabelVisibilityOptions.Collapsed
-    )
+    render(<Radio {...props} />)
+    const widgetLabel = screen.getByText("Label")
+    expect(widgetLabel).not.toBeVisible()
   })
 
   it("has correct className and style", () => {
     const props = getProps()
-    const wrapper = mount(<Radio {...props} />)
-    const wrappedDiv = wrapper.find("div").first()
-
-    const { className, style } = wrappedDiv.props()
-    // @ts-expect-error
-    const splittedClassName = className.split(" ")
+    render(<Radio {...props} />)
+    const radioElement = screen.getByTestId("stRadio")
 
-    expect(splittedClassName).toContain("row-widget")
-    expect(splittedClassName).toContain("stRadio")
-
-    // @ts-expect-error
-    expect(style.width).toBe(getProps().width)
+    expect(radioElement).toHaveClass("row-widget")
+    expect(radioElement).toHaveClass("stRadio")
+    expect(radioElement).toHaveStyle(`width: ${props.width}px`)
   })
 
   it("renders a label", () => {
     const props = getProps()
-    const wrapper = mount(<Radio {...props} />)
-    expect(wrapper.find("StyledWidgetLabel").text()).toBe(props.label)
+    render(<Radio {...props} />)
+    const widgetLabel = screen.queryByText(`${props.label}`)
+
+    expect(widgetLabel).toBeInTheDocument()
   })
 
   it("has a default value", () => {
     const props = getProps()
-    const wrapper = mount(<Radio {...props} />)
-    expect(wrapper.find(RadioGroup).prop("value")).toBe(props.value.toString())
+    render(<Radio {...props} />)
+    const radioOptions = screen.getAllByRole("radio")
+    expect(radioOptions).toHaveLength(3)
+
+    const checked = radioOptions[props.value]
+    expect(checked).toBeChecked()
   })
 
   it("can be disabled", () => {
-    const props = getProps()
-    const wrapper = mount(<Radio {...props} />)
-    expect(wrapper.find(RadioGroup).prop("disabled")).toBe(props.disabled)
-  })
+    const props = getProps({ disabled: true })
+    render(<Radio {...props} />)
+    const radioOptions = screen.getAllByRole("radio")
 
-  it("can be horizontally aligned", () => {
-    const props = getProps({ horizontal: true })
-    const wrapper = mount(<Radio {...props} />)
-    expect(wrapper.find(RadioGroup).prop("align")).toBe(ALIGN.horizontal)
+    radioOptions.forEach(option => {
+      expect(option).toBeDisabled()
+    })
   })
 
   it("has the correct options", () => {
     const props = getProps()
-    const wrapper = mount(<Radio {...props} />)
-    const options = wrapper.find(UIRadio)
+    render(<Radio {...props} />)
 
-    options.forEach((option, index) => {
-      expect(option.prop("value")).toBe(index.toString())
-      expect(option.prop("children")).toBe(props.options[index])
+    props.options.forEach(option => {
+      expect(screen.getByText(option)).toBeInTheDocument()
     })
   })
 
   it("shows a message when there are no options to be shown", () => {
     const props = getProps({ options: [] })
-    const wrapper = mount(<Radio {...props} />)
+    render(<Radio {...props} />)
+    const radioOptions = screen.getAllByRole("radio")
+    const noOptionLabel = screen.getByText("No options to select.")
 
-    expect(wrapper.find(UIRadio).length).toBe(1)
-    expect(wrapper.find(UIRadio).prop("children")).toBe(
-      "No options to select."
-    )
+    expect(radioOptions).toHaveLength(1)
+    expect(noOptionLabel).toBeInTheDocument()
   })
 
   it("handles value changes", () => {
     const props = getProps()
-    const wrapper = mount(<Radio {...props} />)
+    render(<Radio {...props} />)
+    const radioOptions = screen.getAllByRole("radio")
+
+    const secondOption = radioOptions[1]
 
-    // @ts-expect-error
-    wrapper.find(RadioGroup).prop("onChange")({
-      target: {
-        value: "1",
-      },
-    } as React.ChangeEvent<HTMLInputElement>)
-    wrapper.update()
+    fireEvent.click(secondOption)
 
-    expect(wrapper.find(RadioGroup).prop("value")).toBe("1")
+    expect(secondOption).toBeChecked()
   })
 })
diff --git a/frontend/lib/src/components/shared/Radio/Radio.tsx b/frontend/lib/src/components/shared/Radio/Radio.tsx
index 7f162dbd6068..b86fc8cbe708 100644
--- a/frontend/lib/src/components/shared/Radio/Radio.tsx
+++ b/frontend/lib/src/components/shared/Radio/Radio.tsx
@@ -25,6 +25,7 @@ import TooltipIcon from "@streamlit/lib/src/components/shared/TooltipIcon"
 import { LabelVisibilityOptions } from "@streamlit/lib/src/util/utils"
 import { Placement } from "@streamlit/lib/src/components/shared/Tooltip"
 import { EmotionTheme } from "@streamlit/lib/src/theme"
+import StreamlitMarkdown from "@streamlit/lib/src/components/shared/StreamlitMarkdown/StreamlitMarkdown"
 
 export interface Props {
   disabled: boolean
@@ -85,7 +86,7 @@ class Radio extends React.PureComponent<Props, State> {
     }
 
     return (
-      <div className="row-widget stRadio" style={style}>
+      <div className="row-widget stRadio" data-testid="stRadio" style={style}>
         <WidgetLabel
           label={label}
           disabled={disabled}
@@ -103,6 +104,7 @@ class Radio extends React.PureComponent<Props, State> {
           disabled={disabled}
           align={horizontal ? ALIGN.horizontal : ALIGN.vertical}
           aria-label={label}
+          data-testid="stRadioGroup"
         >
           {options.map((option: string, index: number) => (
             <UIRadio
@@ -158,7 +160,12 @@ class Radio extends React.PureComponent<Props, State> {
                 },
               }}
             >
-              {option}
+              <StreamlitMarkdown
+                source={option}
+                allowHTML={false}
+                isLabel
+                isButton
+              />
             </UIRadio>
           ))}
         </RadioGroup>
diff --git a/frontend/lib/src/components/shared/StreamlitMarkdown/StreamlitMarkdown.test.tsx b/frontend/lib/src/components/shared/StreamlitMarkdown/StreamlitMarkdown.test.tsx
index 425673421830..c68b0b340dc7 100644
--- a/frontend/lib/src/components/shared/StreamlitMarkdown/StreamlitMarkdown.test.tsx
+++ b/frontend/lib/src/components/shared/StreamlitMarkdown/StreamlitMarkdown.test.tsx
@@ -248,14 +248,13 @@ describe("StreamlitMarkdown", () => {
 
   it("doesn't render links when isButton is true", () => {
     // Valid markdown further restricted with buttons to eliminate links
-    const source = "Link: [text](www.example.com)"
-    const wrapper = render(
+    const source = "[Link text](www.example.com)"
+    render(
       <StreamlitMarkdown source={source} allowHTML={false} isLabel isButton />
     )
-    const container = wrapper.getByTestId("stMarkdownContainer")
-    const invalidTag = container.querySelector("a")
-    expect(invalidTag).toBeNull()
-    expect(container).toHaveTextContent("Link: ")
+    const tag = screen.getByText("Link text")
+    const isLinkTag = tag instanceof HTMLAnchorElement ? true : false
+    expect(isLinkTag).toBe(false)
   })
 
   it("renders smaller text sizing when isToast is true", () => {
@@ -266,6 +265,21 @@ describe("StreamlitMarkdown", () => {
     expect(textTag).toHaveStyle("font-size: 14px")
   })
 
+  it("renders regular text sizing when largerLabel is true", () => {
+    const source = "Here is some checkbox label text"
+    render(
+      <StreamlitMarkdown
+        source={source}
+        allowHTML={false}
+        isLabel
+        largerLabel
+      />
+    )
+
+    const textTag = screen.getByText("Here is some checkbox label text")
+    expect(textTag).toHaveStyle("font-size: inherit")
+  })
+
   it("colours text properly", () => {
     const colorMapping = new Map([
       ["red", colors.red80],
diff --git a/frontend/lib/src/components/shared/StreamlitMarkdown/StreamlitMarkdown.tsx b/frontend/lib/src/components/shared/StreamlitMarkdown/StreamlitMarkdown.tsx
index 16ed58a91f25..44e23bee7c07 100644
--- a/frontend/lib/src/components/shared/StreamlitMarkdown/StreamlitMarkdown.tsx
+++ b/frontend/lib/src/components/shared/StreamlitMarkdown/StreamlitMarkdown.tsx
@@ -87,14 +87,14 @@ export interface Props {
   isLabel?: boolean
 
   /**
-   * Does not allow links
+   * Checkbox labels have larger font sizing
    */
-  isButton?: boolean
+  largerLabel?: boolean
 
   /**
-   * Checkbox has larger label font sizing
+   * Does not allow links & has larger font sizing
    */
-  isCheckbox?: boolean
+  isButton?: boolean
 
   /**
    * Toast has smaller font sizing
@@ -233,7 +233,7 @@ export interface RenderedMarkdownProps {
   isLabel?: boolean
 
   /**
-   * Does not allow links
+   * Does not allow links & has larger font sizing
    */
   isButton?: boolean
 }
@@ -387,8 +387,8 @@ class StreamlitMarkdown extends PureComponent<Props> {
       style,
       isCaption,
       isLabel,
+      largerLabel,
       isButton,
-      isCheckbox,
       isToast,
     } = this.props
     const isInSidebar = this.context
@@ -398,8 +398,8 @@ class StreamlitMarkdown extends PureComponent<Props> {
         isCaption={Boolean(isCaption)}
         isInSidebar={isInSidebar}
         isLabel={isLabel}
+        largerLabel={largerLabel}
         isButton={isButton}
-        isCheckbox={isCheckbox}
         isToast={isToast}
         style={style}
         data-testid={isCaption ? "stCaptionContainer" : "stMarkdownContainer"}
diff --git a/frontend/lib/src/components/shared/StreamlitMarkdown/styled-components.ts b/frontend/lib/src/components/shared/StreamlitMarkdown/styled-components.ts
index 6b3e50d65ccb..ee75d979e77e 100644
--- a/frontend/lib/src/components/shared/StreamlitMarkdown/styled-components.ts
+++ b/frontend/lib/src/components/shared/StreamlitMarkdown/styled-components.ts
@@ -21,8 +21,8 @@ export interface StyledStreamlitMarkdownProps {
   isCaption: boolean
   isInSidebar: boolean
   isLabel?: boolean
+  largerLabel?: boolean
   isButton?: boolean
-  isCheckbox?: boolean
   isToast?: boolean
 }
 
@@ -45,13 +45,13 @@ export const StyledStreamlitMarkdown =
       isCaption,
       isInSidebar,
       isLabel,
+      largerLabel,
       isButton,
-      isCheckbox,
       isToast,
     }) => {
-      // Widget Labels have smaller font size with exception of Buttons/Checkboxes
+      // Widget Labels have smaller font size with exception of Button/Checkbox/Radio Button labels
       // Toasts also have smaller font size
-      const labelFontSize = (isLabel && !isCheckbox && !isButton) || isToast
+      const labelFontSize = (isLabel && !largerLabel && !isButton) || isToast
       return {
         fontFamily: theme.genericFonts.bodyFont,
         marginBottom: isLabel ? "" : `-${theme.spacing.lg}`,
diff --git a/frontend/lib/src/components/widgets/Checkbox/Checkbox.tsx b/frontend/lib/src/components/widgets/Checkbox/Checkbox.tsx
index fec67ff6d5bf..637e79734758 100644
--- a/frontend/lib/src/components/widgets/Checkbox/Checkbox.tsx
+++ b/frontend/lib/src/components/widgets/Checkbox/Checkbox.tsx
@@ -211,7 +211,7 @@ class Checkbox extends React.PureComponent<Props, State> {
               source={element.label}
               allowHTML={false}
               isLabel
-              isCheckbox
+              largerLabel
             />
             {element.help && (
               <StyledWidgetLabelHelpInline color={color}>
diff --git a/frontend/lib/src/components/widgets/Radio/Radio.test.tsx b/frontend/lib/src/components/widgets/Radio/Radio.test.tsx
index 6e624ecce2c7..6e772a745e84 100644
--- a/frontend/lib/src/components/widgets/Radio/Radio.test.tsx
+++ b/frontend/lib/src/components/widgets/Radio/Radio.test.tsx
@@ -15,14 +15,18 @@
  */
 
 import React from "react"
-import { mount } from "@streamlit/lib/src/test_util"
-import { WidgetStateManager } from "@streamlit/lib/src/WidgetStateManager"
+import { render } from "@streamlit/lib/src/test_util"
+import { screen, fireEvent } from "@testing-library/react"
+import "@testing-library/jest-dom"
 
-import { Radio as UIRadio, RadioGroup } from "baseui/radio"
+import { WidgetStateManager } from "@streamlit/lib/src/WidgetStateManager"
 import { Radio as RadioProto } from "@streamlit/lib/src/proto"
 import Radio, { Props } from "./Radio"
 
-const getProps = (elementProps: Partial<RadioProto> = {}): Props => ({
+const getProps = (
+  elementProps: Partial<RadioProto> = {},
+  otherProps: Partial<Props> = {}
+): Props => ({
   element: RadioProto.create({
     id: "1",
     label: "Label",
@@ -36,21 +40,24 @@ const getProps = (elementProps: Partial<RadioProto> = {}): Props => ({
     sendRerunBackMsg: jest.fn(),
     formsDataChanged: jest.fn(),
   }),
+  ...otherProps,
 })
 
 describe("Radio widget", () => {
   it("renders without crashing", () => {
     const props = getProps()
-    const wrapper = mount(<Radio {...props} />)
+    render(<Radio {...props} />)
+    const radioGroup = screen.getByRole("radiogroup")
+    const radioOptions = screen.getAllByRole("radio")
 
-    expect(wrapper.find(RadioGroup).length).toBe(1)
-    expect(wrapper.find(UIRadio).length).toBe(3)
+    expect(radioGroup).toBeInTheDocument()
+    expect(radioOptions).toHaveLength(3)
   })
 
   it("sets widget value on mount", () => {
     const props = getProps()
     jest.spyOn(props.widgetMgr, "setIntValue")
-    mount(<Radio {...props} />)
+    render(<Radio {...props} />)
 
     expect(props.widgetMgr.setIntValue).toHaveBeenCalledWith(
       props.element,
@@ -61,78 +68,77 @@ describe("Radio widget", () => {
 
   it("has correct className and style", () => {
     const props = getProps()
-    const wrapper = mount(<Radio {...props} />)
-    const wrappedDiv = wrapper.find("div").first()
-
-    const { className, style } = wrappedDiv.props()
-    // @ts-expect-error
-    const splittedClassName = className.split(" ")
-
-    expect(splittedClassName).toContain("row-widget")
-    expect(splittedClassName).toContain("stRadio")
+    render(<Radio {...props} />)
+    const radioElement = screen.getByTestId("stRadio")
 
-    // @ts-expect-error
-    expect(style.width).toBe(getProps().width)
+    expect(radioElement).toHaveClass("row-widget")
+    expect(radioElement).toHaveClass("stRadio")
+    expect(radioElement).toHaveStyle(`width: ${props.width}px`)
   })
 
   it("renders a label", () => {
     const props = getProps()
-    const wrapper = mount(<Radio {...props} />)
-    expect(wrapper.find("StyledWidgetLabel").text()).toBe(props.element.label)
+    render(<Radio {...props} />)
+    const widgetLabel = screen.queryByText(`${props.element.label}`)
+
+    expect(widgetLabel).toBeInTheDocument()
   })
 
   it("has a default value", () => {
     const props = getProps()
-    const wrapper = mount(<Radio {...props} />)
-    expect(wrapper.find(RadioGroup).prop("value")).toBe(
-      props.element.default.toString()
-    )
+    render(<Radio {...props} />)
+    const radioOptions = screen.getAllByRole("radio")
+    expect(radioOptions).toHaveLength(3)
+
+    const checked = radioOptions[props.element.default]
+    expect(checked).toBeChecked()
   })
 
   it("can be disabled", () => {
-    const props = getProps()
-    const wrapper = mount(<Radio {...props} />)
-    expect(wrapper.find(RadioGroup).prop("disabled")).toBe(props.disabled)
+    const props = getProps({}, { disabled: true })
+    render(<Radio {...props} />)
+    const radioOptions = screen.getAllByRole("radio")
+
+    radioOptions.forEach(option => {
+      expect(option).toBeDisabled()
+    })
   })
 
   it("has the correct options", () => {
     const props = getProps()
-    const wrapper = mount(<Radio {...props} />)
-    const options = wrapper.find(UIRadio)
+    render(<Radio {...props} />)
 
-    options.forEach((option, index) => {
-      expect(option.prop("value")).toBe(index.toString())
-      expect(option.prop("children")).toBe(props.element.options[index])
+    props.element.options.forEach(option => {
+      expect(screen.getByText(option)).toBeInTheDocument()
     })
   })
 
   it("shows a message when there are no options to be shown", () => {
     const props = getProps({ options: [] })
-    const wrapper = mount(<Radio {...props} />)
+    render(<Radio {...props} />)
 
-    expect(wrapper.find(UIRadio).length).toBe(1)
-    expect(wrapper.find(UIRadio).prop("children")).toBe(
-      "No options to select."
-    )
+    const radioOptions = screen.getAllByRole("radio")
+    const noOptionLabel = screen.getByText("No options to select.")
+
+    expect(radioOptions).toHaveLength(1)
+    expect(noOptionLabel).toBeInTheDocument()
   })
 
   it("sets the widget value when an option is selected", () => {
     const props = getProps()
     jest.spyOn(props.widgetMgr, "setIntValue")
-    const wrapper = mount(<Radio {...props} />)
+    render(<Radio {...props} />)
+    const radioOptions = screen.getAllByRole("radio")
+    const secondOption = radioOptions[1]
 
-    // @ts-expect-error
-    wrapper.find(RadioGroup).prop("onChange")({
-      target: { value: "1" },
-    } as React.ChangeEvent<HTMLInputElement>)
-    wrapper.update()
+    fireEvent.click(secondOption)
 
-    expect(wrapper.find(RadioGroup).prop("value")).toBe("1")
     expect(props.widgetMgr.setIntValue).toHaveBeenLastCalledWith(
       props.element,
       1,
       { fromUi: true }
     )
+    expect(secondOption).toBeChecked()
   })
 
   it("resets its value when form is cleared", () => {
@@ -141,17 +147,15 @@ describe("Radio widget", () => {
     props.widgetMgr.setFormClearOnSubmit("form", true)
 
     jest.spyOn(props.widgetMgr, "setIntValue")
+    render(<Radio {...props} />)
 
-    const wrapper = mount(<Radio {...props} />)
+    const radioOptions = screen.getAllByRole("radio")
+    const secondOption = radioOptions[1]
 
     // Change the widget value
-    // @ts-expect-error
-    wrapper.find(RadioGroup).prop("onChange")({
-      target: { value: "1" },
-    } as React.ChangeEvent<HTMLInputElement>)
-    wrapper.update()
+    fireEvent.click(secondOption)
+    expect(secondOption).toBeChecked()
 
-    expect(wrapper.find(RadioGroup).prop("value")).toBe("1")
     expect(props.widgetMgr.setIntValue).toHaveBeenLastCalledWith(
       props.element,
       1,
@@ -160,12 +164,11 @@ describe("Radio widget", () => {
 
     // "Submit" the form
     props.widgetMgr.submitForm("form")
-    wrapper.update()
 
     // Our widget should be reset, and the widgetMgr should be updated
-    expect(wrapper.find(RadioGroup).prop("value")).toBe(
-      props.element.default.toString()
-    )
+    const defaultValue = radioOptions[props.element.default]
+    expect(defaultValue).toBeChecked()
+
     expect(props.widgetMgr.setIntValue).toHaveBeenLastCalledWith(
       props.element,
       props.element.default,


Code review comments:
diff_hunk: @@ -248,14 +248,13 @@ describe("StreamlitMarkdown", () => {
 
   it("doesn't render links when isButton is true", () => {
     // Valid markdown further restricted with buttons to eliminate links
-    const source = "Link: [text](www.example.com)"
-    const wrapper = render(
+    const source = "[Link text](www.example.com)"
+    render(
       <StreamlitMarkdown source={source} allowHTML={false} isLabel isButton />
     )
-    const container = wrapper.getByTestId("stMarkdownContainer")
-    const invalidTag = container.querySelector("a")
-    expect(invalidTag).toBeNull()
-    expect(container).toHaveTextContent("Link: ")
+    const tag = screen.getByText("Link text")
+    const isLinkTag = tag instanceof HTMLAnchorElement ? true : false
+    expect(isLinkTag).toBe(false)
path: frontend/lib/src/components/shared/StreamlitMarkdown/StreamlitMarkdown.test.tsx
text: @user1:
nit: I think this can just be `expect(tag instanceof HTMLAnchorElement).toBe(false)`, or alternatively

```typescript
    const isLinkTag = tag instanceof HTMLAnchorElement
    expect(isLinkTag).toBe(false)
```

if you want to keep the intermediate variable name for clarity