skip to content

In Cypress component tests, how do you pass slot or child content to `cy.mount()`?

level: middleimportance: should knowfreq 46%

answer

  1. Content the component wraps, not a value
  2. Each adapter has its own channel
  3. React puts it in the element itself
  4. Vue keys it by slot name
  5. Projected content is DOM you can query

basics

~20 s

Through a different channel per adapter. In React the children are part of the JSX you hand to cy.mount(). In Vue you fill Vue Test Utils' slots option, keyed by slot name. In Angular you mount a template string with the content inline.

solid answer

~40 s

Props are values; slot or child content is markup the component wraps, and each adapter takes it somewhere different. With `cypress/react` you write it inside the element itself — `cy.mount(<Toast tone="success"><button data-cy="undo">Undo</button></Toast>)` — because the second argument to mount carries options such as `log` and `strict`, not content. With `cypress/vue` you use Vue Test Utils' `slots` option, keyed by slot name: `cy.mount(Toast, { props: { tone: 'success' }, slots: { default: 'Row saved', action: '<button>Undo</button>' } })`, where `default` is the reserved key for the unnamed slot. With `cypress/angular` you mount a template string and put the projected content inside it, alongside `componentProperties`. Whichever adapter, the projected content is ordinary DOM once rendered, so you assert on it with `cy.get()` and `cy.contains()`.

code

javascript · 17 lines
javascript
import { Toast } from './Toast'

it('renders the action slot and reports the undo click', () => {
  const onUndo = cy.spy().as('onUndo')

  cy.mount(
    <Toast tone="success" title="Row saved">
      <button data-cy="undo" onClick={onUndo}>
        Undo
      </button>
    </Toast>
  )

  cy.contains('[data-cy=toast]', 'Row saved').should('be.visible')
  cy.get('[data-cy=toast] [data-cy=undo]').click()
  cy.get('@onUndo').should('have.been.calledOnce')
})

go deeper

for a junior

Know that content a component wraps is passed differently from a prop, and that in React it is simply written between the tags of the element you mount.

for a middle

Be ready to name the channel per adapter — JSX children, the slots option, an Angular template string — and to say what happens to a slot key that matches nothing.

for a senior

Show that you test the empty case too: a library component's fallback rendering only appears when nothing is projected, and no props-only test will ever reach it.

for a principal

Decide how far a component suite goes in reproducing real composition. Every projected fragment a test invents is a second, unversioned consumer of the component's contract.

Props are values a component reads; **projected content** is markup the component wraps. A design-system `Toast` takes a `tone` prop but renders whatever message and action buttons the caller nests inside it. A `DataGrid` takes `rows` but lets the caller supply the empty-state block. React calls that content *children*, Vue calls it *slots*, Angular calls it *content projection* — and `cy.mount()` reaches it through a different channel in each adapter. Getting the channel wrong is a **silent** failure: the component still mounts, the slot is simply empty, and a test that only checks "it rendered" stays green. ## React: children are part of the element There is no separate option. Write the children between the tags, exactly as application code would: ```jsx cy.mount( <Toast tone="success" title="Row saved"> <button data-cy="undo">Undo</button> </Toast> ) ``` The React adapter's second argument holds mount options — `log`, `strict`, `ReactDom` — not content. If you find yourself looking for a `children` key in there, you are writing Vue in React's syntax. ## Vue: the `slots` option `cypress/vue` forwards Vue Test Utils' mounting options, so slot content goes in `slots`, keyed by slot name: ```js cy.mount(Toast, { props: { tone: 'success' }, slots: { default: 'Row saved', action: '<button data-cy="undo">Undo</button>', }, }) ``` Two things to know about that shape: - `default` is the reserved key for the unnamed `<slot />`; every other key must match a `name` attribute in the template. A key that matches nothing is **dropped silently**, which is why the assertion belongs on the rendered text and never on the option you passed. - A slot given as a plain markup string carries no bound handlers. When the projected content itself has to call a spy, supply a render function for that slot, or assert on the event the component emits instead. The JSX form of the same thing passes an object of slot functions as the element's child, which is worth knowing when a spec file already uses JSX for everything else. ## Angular: mount a template string The Angular adapter will mount a template string rather than a component class, and that string is where projected content lives — you write the host markup with the content nested inside it, and pass the inputs and output spies in `componentProperties` alongside. It is the most verbose of the three, and also the most literal: what you write is what gets compiled. ## Choosing what to project | You want to check | What to pass | | --- | --- | | the component renders the caller's message | default slot or a child text node | | named regions land in the right place | one entry per slot name | | an action inside the projected block works | a child element carrying a spy, or the component's own emitted event | | the fallback rendering when nothing is projected | nothing at all — mount with no children | That last row earns its place. A library `Toast` usually has a default rendering for an empty action region, and mounting *without* content is the only way to exercise it. ## Asserting on projected content Once rendered, projected content is ordinary DOM, so the assertions are the usual ones: 1. `cy.contains('[data-cy=toast]', 'Row saved').should('be.visible')` proves the text arrived. 2. `cy.get('[data-cy=toast] [data-cy=undo]').should('exist')` proves *placement* — the descendant selector is what shows the content landed inside the region you meant rather than somewhere else in the tree. 3. `cy.get('[data-cy=undo]').click()` followed by `cy.get('@onUndo').should('have.been.calledOnce')` closes the loop when the projected element carries a spy. Combining both halves — content in, spy out — is what makes a slot test worth writing: it shows the component displays what it was given *and* wires the interaction back to the caller. ## Mistakes worth naming - Passing slot content as a prop because that is the shape the other framework uses. - Trusting a `slots` key that never matched a slot; only an assertion on rendered content catches it. - Expecting an inline handler inside a markup-string slot to fire. - Asserting only that the component mounted, so a slot that quietly vanished still passes. - Projecting a whole page fragment when a single element would have proved the same wiring.

  • How do you fill a named slot rather than the default one?
    Key the object by the slot's name: `cy.mount(Toast, { slots: { default: 'Row saved', action: '<button data-cy="undo">Undo</button>' } })` fills `<slot />` and `<slot name="action" />` respectively. `default` is reserved for the unnamed slot, and any other key must match a `name` attribute in the template — an unmatched key renders nothing and reports nothing.
  • Can projected content carry a Cypress spy?
    In React, yes and directly: the child is ordinary JSX, so `onClick={cy.spy().as('onUndo')}` works. In Vue a slot given as a plain markup string has no handler bound to it, so pass a render function for that slot when the content must call a spy, or assert on the event the component emits instead. Either way the check is `cy.get('@onUndo').should('have.been.calledOnce')`.

saying these in an interview costs you the question

  • Treating slot content as just another prop in every framework
  • Expecting a markup-string slot to carry event handlers
  • Using a slot key that matches no slot and never checking
  • Asserting on the mount options rather than the rendered DOM