Events

For this kata, you will be creating a skill that emits events. Both from the front-end and the back-end. We’ll be creating a listener in your Skill as well as in your RootSkillView.

Kata Setup

Read setup:

Pre-requisites

  1. Make sure your Development Theatre is running.

Step 1: Create your skill

Create a new directory for your kata

cd ~/path/to/your/spruce/projects
mkdir katas

Create a new skill

cd katas
spruce create.skill events-kata

Name your skill

Note: Your skill name should be unique, so if you did this kata before, you may want to name it something different.

Open your skill in VS Code

Note: You can follow the instructions printed in the cli or use the command below.

cd events-kata && code .

Then, open the terminal in VS Code and run:

spruce setup.vscode

Hit Enter to accept all setup options.

Then complete the following:

  1. Open the Command Palette by using cmd+shift+p and search type: “Manage”
  2. Select “Tasks: Manage Automatic Tasks”
  3. Then select “Allow Automatic Tasks”
  4. Open the Command Palette again type “reload” and select “Reload Window”

The Test Runner should open and begin installing additional requirements.

When it’s done, you should see a message that says Ready and waiting...

Step 2: Create your first test

Create the test file

  1. Hit ctrl+space (if you have the shortcuts setup) and hit enter.
    • If you don’t have the shortcuts setup, you can type spruce create.test in your terminal and hit Enter.
  2. Select “Behavioral”
  3. For “What are you testing?”, type “Emitting events”
  4. For “Camel case name”, hit Enter (it should say “emittingEvents”)
  5. For “Which abstract test class do you want to extend?” select “AbstractSpruceFixtureTest”
  6. Close the terminal window and get back to the Test Runner.
    • There should be one failing test.
    • The test will explain that before you can do any tests, you need to run spruce set.remote
  7. Hit ctrl+space and type set.remote and hit Enter.
    • You will be prompted for more dependencies to install. Hit Enter to accept them all.
  8. For your remote, select “Local”
    • Allow the rest of the dependencies to install
    • If prompted for remote again, select “Local” again
  9. Close the terminal window and get back to the Test Runner.
    • The test should now be failing beacuse false does not equal true.
  10. Click on the failing test in the Test Runner and click “Open” to open the test file.

Prep the test file

  1. Clear out the contents of the first test
  2. Delete the second test
  3. Delete class EmittingEvents {} at the bottom of the test file

Your test should now be passing.

Events Kata

Rendering your RootSkillView

Test 1: Rendering your RootSkillView

In your first test, add the following:

@test()
protected async canCreateRootSkillView() {
    this.views.Controller('.root', {})
}

Note: It’s ok to have some type errors here, they’ll go away as you add more code.

Production 1: Creating your RootSkillView

In order for this test to pass, you need to create your first view, a RootSkillView.

  1. Hit ctrl+space and type create.view and hit Enter.
  2. Select “Skill View Controller”
    • Let the dependencies install
  3. When prompted for if you would like to create your root skill view controller, hit Enter to accept the default.
  4. Now update your failing test to reference the RootSkillView you just created.
@test()
protected async canCreateRootSkillView() {
    this.views.Controller('events-kata.root', {})
}

Note: The events-kata is the namespace of your skill and the root is the name of your view. The namespace will match whatever you named your skill, but you can check in your package.json to see what it is. Check under skill.namespace.

Previewing your work

Since this is a view kata, it will be much for fun if you can see the results of your work in the Development Theatre. Make sure the Development Theatre is open.

Registering your skill
  1. Hit ctrl+space and type register and hit Enter.

You will be asked for a name and a namespace, if this is your first time doing this, name it Events Kata and make sure the namespace is events-kata.

Watching for View changes
  1. Hit ctrl+space and type watch.watch and hit Enter.
  2. Once the watcher is running, change back to the Test Reporter.
Preview in the Development Theatre
  1. In the Development Theatre, hit Command + Shift + n
  2. In the “Jump to” Dialog, type events-kata.root and select the option in the dropdown.
  3. Hit “Go”

Note: For now, you’re going to see a blank screen. That is fine, just wait until you render your first card!

Rendering a Card in your RootSkillView

Test 1: Rendering a Card in your RootSkillView
@test()
protected canCreateRootSkillView() {
    this.views.Controller('events-kata.root', {})
}

// Step 1. Create a new test and use the `vcAssert` utility to assert the SkillView renders a card
@test()
protected rendersACard() {
    const vc = this.views.Controller('events-kata.root', {})
    vcAssert.assertSkillViewRendersCard(vc)
}
Production 1: Rendering a Card in your RootSkillView
// Step 2. Declare the cardVc property (declare property after constructing the card using 'Command + .')
private cardVc: CardViewController

public constructor(options: ViewControllerOptions) {
   super(options)

   // Step 1. Construct a CardViewController
   this.cardVc = this.Controller('card', {
       header: {
           title: 'A title!',
       },
   })
}

public render(): SkillView {
   return {
       layouts: [
           {
               // Step 3. Render the card
               cards: [this.cardVc.render()],
           },
       ],
   }
}
Test 2: Refactor your test
// Step 3. Declare the 'vc' property that will be used in all tests. 
// Use "!" to suppress the error about it not being initialized in the constructor
private vc!: RootSkillViewController

// Step 1. Declare beforeEach()
protected async beforeEach() {
    await super.beforeEach()
    // Step 2. Move the vc declaration here
    this.vc = this.views.Controller('events-kata.root', {})
}

// Step 4. delete the 'canCreateRootSkillView' test 

@test()
protected rendersACard() {
    // Step 5. User 'this.vc' instead of constructing a new vc
    vcAssert.assertSkillViewRendersCard(this.vc)
}
Production 2: Refactor your production code
public constructor(options: ViewControllerOptions) {
    super(options)

    // Step 1. Select the construction or your skill view and hit 'Ctrl + Shift + r'
    // and select 'Extract to method in class...'. Name it `CardVc`
    this.cardVc = this.CardVc()
}

private CardVc(): CardViewController {
    return this.Controller('card', {
        header: {
            title: 'A title!',
        },
    })
}

Note: Now is a good time to view your progress! In your Development Theatre, hit Command + r to refresh the page. You should see a card with a title of “A title!”

Rendering a button

Test 1: Asserting your card renders a button
@test()
protected cardRendersButton() {
    buttonAssert.cardRendersButton(this.vc.getCardVc(), 'my-button')
}

Note: You will get an error that ‘getCardVc()’ does not exist on your View Controller. This is a-ok because we’re about to make a Test Double!

Test 2: Creating your Test Double
@fake.login()
@suite()
export default class EmittingEventsTest extends AbstractSpruceFixtureTest {
    // Step 4. Change the type on the 'vc' property to be your Spy
    private vc!: SpyRootSkillView

    protected async beforeEach() {
        // Step 2. Override the Class for your View Controller to be your Spy
        this.views.setController('events-kata.root', SpyRootSkillView)

        // Step 3. Typecast the vc to be your Spy
        this.vc = this.views.Controller(
            'events-kata.root',
            {}
        ) as SpyRootSkillView
    }

    @test()
    protected rendersACard() {
        vcAssert.assertSkillViewRendersCard(this.vc)
    }

    @test()
    protected cardRendersButton() {
        buttonAssert.cardRendersButton(this.vc.getCardVc(), 'my-button')
    }
}

// Step 1. Create your Test Double (a Spy) that extends your RootSkillViewController
class SpyRootSkillView extends RootSkillViewController {
    public getCardVc() {
        return this.cardVc
    }
}

Note: You will get an error that 'this.cardVc` in your spy is not accessible because it is private, lets fix that next!

Note: Also, your test is not passing, that’s fine too. That is next.

Production 1: Making your 'cardVc' property protected
// Step 1. Change the cardVc property to be protected in your Root.svc
protected cardVc: CardViewController
Production 2: Render a Button in your Card

private CardVc(): CardViewController {
    return this.Controller('card', {
        header: {
            title: 'A title!',
        },
        // Step 1. Add a footer with a single button with the id of 'my-button'
        footer: {
            buttons: [
                {
                    id: 'my-button',
                    // Step 2 (optional): Play around with different properties of the button
                    label: 'My button',
                    type: 'primary',
                },
            ],
        },
    })
}

Note: Everything should be passing now! Refresh the front end! Also, play around with different properties on the button and refresh to see their effect!

Clicking the Button emits an Event

Test 1: Handling the click of the Button

// Step 1. Declare the new test
@test()
protected async clickingButtonEmitsEvent() {
    // Step 4. Declare variable to track whether the event was hit
    let wasHit = false

    // Step 2. Use the eventFaker utility from '@sprucelabs/spruce-test-fixtures' to listen to the event. Give it a random name to start because we have no defined our new event yet
    await eventFaker.on('event-kata', () => {
        // Step 3. Track that the event listener was hit
        wasHit = true
    })

    // Step 5. Use the interactor utility to click the button
    await interactor.clickButton(this.vc.getCardVc(), 'my-button')
}

Note: Your test will be failing because your button does not have an onClick handler yet. We’ll do that next.

Production 1: Adding the onClick handler to the button
private CardVc(): CardViewController {
    return this.Controller('card', {
        header: {
            title: 'A card title',
        },
        footer: {
            buttons: [
                {
                    id: 'my-button',
                    label: 'My button',
                    // Step 1. Add the onClick handler to the button, it will do nothing
                    onClick: () => {},
                },
            ],
        },
    })
}

Note: Now your test is passing! But to be fair, it’s only checking if there was an onClick handler. Let’s actually check if the event is emitted and the listener hit.

Test 2: Checking if the Event is emitted
@test()
protected async clickingButtonEmitsEvent() {
    let wasHit = false
    await eventFaker.on('event-kata', () => {
        wasHit = true
    })

    await interactor.clickButton(this.vc.getCardVc(), 'my-button')

    // Step 1. Assert that wasHit is true!
    assert.isTrue(wasHit, 'Event was not emitted')
}
Production 2: Emitting the event
private CardVc(): CardViewController {
    return this.Controller('card', {
        header: {
            title: 'A card title',
        },
        footer: {
            buttons: [
                {
                    id: 'my-button',
                    label: 'My button',
                    // Step 1. Change the callback to be a method defined in the class
                    onClick: this.handleClickMyButton.bind(this),
                },
            ],
        },
    })
}

// Step 2. Create the method that will be called when the button is clicked
private async handleClickMyButton() {
    // Step 3. Connect to the API
    const client = await this.connectToApi()
    // Step 4. Emit the event
    await client.emitAndFlattenResponses('my-event-kata')
}

Note: Now your test will be failing (and types too) because of the event name not existing. Lets fix that next.

Production 3: Create the Event
  1. Hit ctrl+space and type create.event
  2. For Readable Name, type My first event (or whatever you want, really)
  3. For Kebab Case Name, just hit enter
  4. For Camel Case Name, just hit enter
  5. For Version, select the latest version (if prompted)
Production 4: Defining the Event

We need to define 4 things in our event:

  1. The Event’s Emit Target
  2. The Event’s Emit Payload
  3. The Event’s Response Payload
  4. The Event’s Permissions

You can put whatever you would like in here, but you should visit the Events documentation to learn more.

For now, here is what we’re petting in each:

Emit Payload: src/events/my-first-event/v2025_04_24/emitPayload.builder.ts:

import { buildSchema } from '@sprucelabs/schema'

const myFirstEventEmitPayloadBuilder = buildSchema({
    id: 'myFirstEventEmitPayload',
    fields: {
        randomValue: { // Step 1: We're defining a field called randomValue
            type: 'text', // Step 2: The type is text, but you can use any type you want
            isRequired: true, // Step 3: This field is required for now
        },
    },
})

export default myFirstEventEmitPayloadBuilder

Emit Target: src/events/my-first-event/v2025_04_24/emitTarget.builder.ts:

import { buildSchema } from '@sprucelabs/schema'

const myFirstEventEmitTargetBuilder = buildSchema({
    id: 'myFirstEventEmitTarget',
    fields: {}, // Step 1: Clear out all fields, we don't need any for this kata
})

export default myFirstEventEmitTargetBuilder

Event Options: src/events/my-first-event/v2025_04_24/event.options.ts:

import { EventSignature } from '@sprucelabs/mercury-types'
import '#spruce/permissions/permissions.types'
import '@sprucelabs/mercury-core-events'

type Options = Omit<
    EventSignature,
    | 'responsePayloadSchema'
    | 'emitPayloadSchema'
    | 'listenPermissionContract'
    | 'emitPermissionContract'
>

const eventOptions: Options = {
    isGlobal: true, // Step 1: Set to true because we are not scoping to a location or organization
}

export default eventOptions

Response Payload: src/events/my-first-event/v2025_04_24/responsePayload.builder.ts:

import { buildSchema } from '@sprucelabs/schema'

const myFirstEventResponsePayloadBuilder = buildSchema({
    id: 'myFirstEventResponsePayload',
    fields: {
        wasSuccesful: { // Step 1: Define a field called wasSuccesful
            type: 'boolean', // Step 2: The type is boolean, but you can use any type you want
            isRequired: true, // Step 3: This field is required for now
        },
    },
})

export default myFirstEventResponsePayloadBuilder
Production 5: Sync events

To have the event contract built from the builders you just created, you need to run the following command:

spruce sync.events
Production 6: Fix the type errors

Inside your Root.svc.ts, you will need to fix the type error on client.emitAndFlattenResponses my updating it to match the fully qualified event name you just created.

private async handleClickMyButton() {
    const client = await this.connectToApi()
    await client.emitAndFlattenResponses(
        'events-kata.my-first-event::v2025_04_24' // Step 1: Use the event name you just created
    )
}

Note: The easiest way to get event names right is to delete the quotes and type a single quote, then start typing the namespace of your skill and choose the event from the autocomplete.

Test 3: Fix the type errors

Now we have to do the same thing for our test!

@test()
protected async clickingButtonEmitsEvent() {
    let wasHit = false
    await eventFaker.on('events-kata.my-first-event::v2025_04_24', () => { // Step 1: Use the fully qualified event name
        wasHit = true
    })

    await interactor.clickButton(this.vc.getCardVc(), 'my-button')
    assert.isTrue(wasHit, 'Event was not emitted')
}
Production 7: Fix the failing test

You should now be getting an error that reads like this:

Error: The emit payload you passed to "events-kata.my-first-event::v2025_04_24" is invalid:

'myFirstEventEmitTargetAndPayload' has 1 error!

1. 'payload' is required.

So, lets go drop something in for now:

private async handleClickMyButton() {
    const client = await this.connectToApi()
    await client.emitAndFlattenResponses(
        'events-kata.my-first-event::v2025_04_24',
        {
            payload: { // Step 1: Add the payload in the object passed as the second argument
                randomValue: 'aoeuaoue', // Step 2: Put in gibberish for now, we'll test it later
            },
        }
    )
}
Test 4: Fix the test

You should now be getting an error that reads like this:

Error: The response payload to "events-kata.my-first-event::v2025_04_24" is invalid:

'myFirstEventResponsePayload' has 1 error!

1. 'wasSuccesful' is required.

Note: Pay careful attention to the error message, it is telling you that the response payload is missing a field called wasSuccesful. Sometimes its easy to miss the fact the error was in the response payload, not the emit payload.

Now lets fix the test:

@test()
protected async clickingButtonEmitsEvent() {
    let wasHit = false
    await eventFaker.on('events-kata.my-first-event::v2025_04_24', (response) => {
        wasHit = true

        return { // Step 1: Add the wasSuccesful field to the response
            wasSuccesful: true, 
        }
    })

    await interactor.clickButton(this.vc.getCardVc(), 'my-button')
    assert.isTrue(wasHit, 'Event was not emitted')
}

Test the front end

If you aren’t running spruce watch.views, run it again and then visit:

http://localhost:8080/#views/events-kata.root

And click the button. Nothing happens! Now, check the error console! You should see something like this:

Unhandled Promise Rejection: Error: Oh no! No skill is listening to events-kata.my-first-event::v2025_04_24! I've let the proper humans know!

We don’t want unhandled promise rejections! So lets fix that on the front-end first!

Rendering an alert when emitting throws

Test 1: Rendering an alert when emitting throws
@test()
protected async rendersAnAlertWhenEventThrows() { // Step 1. Declare the test
    await eventFaker.makeEventThrow( // Step 2. Use the eventFaker utility to make the event throw
        'events-kata.my-first-event::v2025_04_24'
    )

    await vcAssert.assertRendersAlert(this.vc, () =>  // Step 3. Assert that an alert is rendered when clicking the button
        interactor.clickButton(this.vc.getCardVc(), 'my-button')
    )
}
Production 1: Rendering an alert when emitting throws
private async handleClickMyButton() {
    try { // Step 1. Wrap the emit in a try/catch block
        const client = await this.connectToApi()
        await client.emitAndFlattenResponses(
            'events-kata.my-first-event::v2025_04_24',
            {
                payload: {
                    randomValue: 'aoeuaoue',
                },
            }
        )
    } catch (err: any) { // Step 2. Catch the error and type as any or unknown
        this.log.error( // Step 3. Log the error (always helpful when degugging)
            `Failed to emit the event!`,
            err.stack ?? err.message
        )
        await this.alert({ // Step 4. Render an alert!
            message: `Oh no! The event failed to emit!`,
        })
    }
}

Test the front end again

Refresh http://localhost:8080/#views/events-kata.root and click the button. You should see an alert! That is much better!

Finishing up the front-end tests

Test 1: Checking the emit payload
@test()
protected async clickingButtonEmitsEvent() {
    let wasHit = false
    let passedPayload: // Step 1. Declare a variable to track the payload passed to the event listener
        | SpruceSchemas.EventsKata.v2025_04_24.MyFirstEventEmitTargetAndPayload['payload']
        | undefined
    await eventFaker.on(
        'events-kata.my-first-event::v2025_04_24',
        ({ payload }) => {
            wasHit = true
            passedPayload = payload // Step 2. Track the payload passed to the event listener
            return {
                wasSuccesful: true,
            }
        }
    )

    await interactor.clickButton(this.vc.getCardVc(), 'my-button')
    assert.isTrue(wasHit, 'Event was not emitted')
    assert.isEqualDeep(passedPayload, { // Step 3. Assert that the payload is what we expect
        randomValue: 'I love katas!',
    })
}
Production 1: Update the emit payload
await client.emitAndFlattenResponses(
    'events-kata.my-first-event::v2025_04_24',
    {
        payload: {
            randomValue: 'I love katas!', // Step 1. Update to match the test
        },
    }
)
Test 2a: Extract out cardVcThere is a lot of duplication and uneeded code in the test now, let's refactor it!
export default class EmittingEventsTest extends AbstractSpruceFixtureTest {
    ...

    @test()
    protected cardRendersButton() {
        // Step 1: Extract out this.vc.getCardVc() to a method called this.cardVc
        // and change it to a getter (see step 2)
        buttonAssert.cardRendersButton(this.cardVc, 'my-button')
    }

    @test()
    protected async clickingButtonEmitsEvent() {
        let wasHit = false
        let passedPayload:
            | SpruceSchemas.EventsKata.v2025_04_24.MyFirstEventEmitTargetAndPayload['payload']
            | undefined
        await eventFaker.on(
            'events-kata.my-first-event::v2025_04_24',
            ({ payload }) => {
                wasHit = true
                passedPayload = payload
                return {
                    wasSuccesful: true,
                }
            }
        )

        // Step 3: Use this.cardVc instead of this.vc.getCardVc()
        await interactor.clickButton(this.cardVc, 'my-button')
        assert.isTrue(wasHit, 'Event was not emitted')
        assert.isEqualDeep(passedPayload, {
            randomValue: 'I love katas!',
        })
    }

    @test()
    protected async rendersAnAlertWhenEventThrows() {
        await eventFaker.makeEventThrow(
            'events-kata.my-first-event::v2025_04_24'
        )

        await vcAssert.assertRendersAlert(this.vc, () =>
            // Step 4: Use this.cardVc instead of this.vc.getCardVc()
            interactor.clickButton(this.cardVc, 'my-button')
        )
    }

    // Step 2: Move method to the bottom and add `get` to the method name 
    // to make it a getter
    private get cardVc() {
        return this.vc.getCardVc()
    }
}
Step 2b: Extract out click interaction
export default class EmittingEventsTest extends AbstractSpruceFixtureTest {
    private vc!: SpyRootSkillView

    protected async beforeEach(): Promise<void> {
        await super.beforeEach()

        this.views.setController('events-kata.root', SpyRootSkillView)
        this.vc = this.views.Controller(
            'events-kata.root',
            {}
        ) as SpyRootSkillView
    }

    @test()
    protected rendersACard() {
        vcAssert.assertSkillViewRendersCard(this.vc)
    }

    @test()
    protected cardRendersButton() {
        buttonAssert.cardRendersButton(this.cardVc, 'my-button')
    }

    @test()
    protected async clickingButtonEmitsEvent() {
        let wasHit = false
        let passedPayload:
            | SpruceSchemas.EventsKata.v2025_04_24.MyFirstEventEmitTargetAndPayload['payload']
            | undefined
        await eventFaker.on(
            'events-kata.my-first-event::v2025_04_24',
            ({ payload }) => {
                wasHit = true
                passedPayload = payload
                return {
                    wasSuccesful: true,
                }
            }
        )
        // Step 1: Extract the interaction to a method called clickMyButton
        await this.clickMyButton()
        assert.isTrue(wasHit, 'Event was not emitted')
        assert.isEqualDeep(passedPayload, {
            randomValue: 'I love katas!',
        })
    }

    @test()
    protected async rendersAnAlertWhenEventThrows() {
        await eventFaker.makeEventThrow(
            'events-kata.my-first-event::v2025_04_24'
        )

        // Step 3: Use the clickMyButton method instead of interactor.clickButton
        await vcAssert.assertRendersAlert(this.vc, () => this.clickMyButton())
    }

    private get cardVc() {
        return this.vc.getCardVc()
    }

    // Step 2: Move the new method to the bottom of the class
    private async clickMyButton() {
        await interactor.clickButton(this.cardVc, 'my-button')
    }
}
Step 3c: Remove uneeded code

We no longer need the wasHit variable because we’re testing the passed payload.

@test()
protected async clickingButtonEmitsEvent() {
    let passedPayload:
        | SpruceSchemas.EventsKata.v2025_04_24.MyFirstEventEmitTargetAndPayload['payload']
        | undefined

    // Step 1: Remove the wasHit variable
    await eventFaker.on(
        'events-kata.my-first-event::v2025_04_24',
        ({ payload }) => {
            // Step 2: Remove the wasHit variable assignment
            passedPayload = payload
            return {
                wasSuccesful: true,
            }
        }
    )

    await this.clickMyButton()

    // Step 3: Remove the assert.isTrue(wasHit, 'Event was not emitted') line
    assert.isEqualDeep(passedPayload, {
        randomValue: 'I love katas!',
    })
}

Testing the back-end (creating a listener)

Test 1a: Create a new test

Hit ctrl+space and type create.test, then fill out the form with the following:

Test 1b: Update the test
@fake.login()
@suite()
export default class MyFirstEventListenerTest extends AbstractSpruceFixtureTest {
    // Step 1: Remove existing tests and class declaration at the bottom
    @test()
    protected async skillIsListening() { // Step 2: Declare the new test
        await this.bootSkill() // Step 3: Boot the skill so it can listen to events
        await this.fakedClient.emitAndFlattenResponses( // Step 4: Emit the event
            'events-kata.my-first-event::v2025_04_24'
        )
    }
}
Production 1: Create the listener
  1. Hit ctrl+space and type create.listener
  2. For “Select Namespace”, select EventsKata
  3. For “Select an Event”, select events-kata.my-first-event::v2025_04_24

Note: Your test will now fail because we’re not passing a payload. We’ll fix that next.

Test 2: Emit the propert payload
@test()
protected async skillIsListening() {
    await this.bootSkill()
    await this.fakedClient.emitAndFlattenResponses(
        'events-kata.my-first-event::v2025_04_24',
        {
            payload: { // Step 1: Add the payload to the emit
                randomValue: generateId(), // Step 2: Put in random id for now (will need to import)
            },
        }
    )
}

Note: Your test will now fail because the listener is throwing an error. Let’s fix that now!

Production 2: Update the listener

Jump into your new listener at: src/listeners/events-kata/my-first-event.v2025_04_24.listener.ts

// Step 1: Remove the current contents of the listener and all unused imports
export default async (
    _event: SpruceEvent<SkillEventContract, EmitPayload> // Step 2: Underscore event to disable unused variable warning
): SpruceEventResponse<ResponsePayload> => {
    return { // Step 3: Return an object with the wasSuccesful field
        wasSuccesful: false, // Step 4: Set wasSuccesful to false for now
    }
}
Test 3: Test the listener returns true
@test()
protected async listenerReturnsWasSuccesful() { // Step 1: Declare a new test
    await this.bootSkill() // Step 2: Boot the skill
    const [{ wasSuccesful }] =
        await this.fakedClient.emitAndFlattenResponses( // Step 3: Emit the event and flatten the responses
            'events-kata.my-first-event::v2025_04_24',
            {
                payload: {
                    randomValue: generateId(),
                },
            }
        )

    assert.isTrue(wasSuccesful, 'Expected wasSuccesful to be true') // Step 4: Assert that wasSuccesful is true
}
Production 3: Update the listener to return true (and log for fun)

We’ll also do some logging here so we can see the payload that was passed in when we test the front-end.

export default async (
    event: SpruceEvent<SkillEventContract, EmitPayload>
): SpruceEventResponse<ResponsePayload> => {
    const { payload } = event // Step 1: Destructure the payload from the event
    console.log('Received payload:', payload) // Step 2: Log the payload to the console
    return {
        wasSuccesful: true, // Step 3: Set wasSuccesful to true
    }
}
Test 4: Refactor the test
export default class MyFirstEventListenerTest extends AbstractSpruceFixtureTest {

    // Step 1: Declare beforeEach to boot the skill
    // Step 2: Remove bootSkill from your other tests
    protected async beforeEach(): Promise<void> {
        await super.beforeEach()
        await this.bootSkill()
    }

    @test()
    protected async skillIsListening() {
        // Step 3: Extract the emit to a method called emit
        await this.emit()
    }

    @test()
    protected async listenerReturnsWasSuccesful() {
        // Step 6: Swap out the emitAndFlattenResponses call for the emit method
        const wasSuccesful = await this.emit()
        assert.isTrue(wasSuccesful, 'Expected wasSuccesful to be true')
    }

    // Step 4: Move the emit method to the bottom of the class
    // Step 5: Destructure the response to get wasSuccesful and return it directly
    private async emit() {
        const [{ wasSuccesful }] =
            await this.fakedClient.emitAndFlattenResponses(
                'events-kata.my-first-event::v2025_04_24',
                {
                    payload: {
                        randomValue: generateId(),
                    },
                }
            )

        return wasSuccesful
    }
}

Test the front end again

Whenever you change the back-end, you’ll need to start the watcher again (since watch.views only watches the front-end). Also, the view watcher is not the best for viewing logs, So kill the watcher and go to the debug pane (cmd+d) and switch the dropdown to boot and click the play button.

Then, Load up the front-end at http://localhost:8080/#views/events-kata.root and click the button!

The alert that no skill is listening should be gone, and you should see the console log in the debug pane with the payload that was passed in!

Something Missing?

Request Documentation Enhancement
It looks like you are using Internet Explorer. While the basic content is available, this is no longer a supported browser by the manufacturer, and no attention is being given to having IE work well here.