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
- 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.
- Name:
Events Kata
- Description:
A kata to practice creating events!
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:
- Open the Command Palette by using
cmd+shift+p
and search type: “Manage” - Select “Tasks: Manage Automatic Tasks”
- Then select “Allow Automatic Tasks”
- 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
- 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 hitEnter
.
- If you don’t have the shortcuts setup, you can type
- Select “Behavioral”
- For “What are you testing?”, type “Emitting events”
- For “Camel case name”, hit Enter (it should say “emittingEvents”)
- For “Which abstract test class do you want to extend?” select “AbstractSpruceFixtureTest”
- 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
- Hit
ctrl+space
and typeset.remote
and hitEnter
.- You will be prompted for more dependencies to install. Hit
Enter
to accept them all.
- You will be prompted for more dependencies to install. Hit
- For your remote, select “Local”
- Allow the rest of the dependencies to install
- If prompted for remote again, select “Local” again
- Close the terminal window and get back to the Test Runner.
- The test should now be failing beacuse
false
does not equaltrue
.
- The test should now be failing beacuse
- Click on the failing test in the Test Runner and click “Open” to open the test file.
Prep the test file
- Clear out the contents of the first test
- Delete the second test
- 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
.
- Hit
ctrl+space
and typecreate.view
and hitEnter
. - Select “Skill View Controller”
- Let the dependencies install
- When prompted for if you would like to create your root skill view controller, hit
Enter
to accept the default. - 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 thenamespace
of your skill and theroot
is the name of your view. Thenamespace
will match whatever you named your skill, but you can check in yourpackage.json
to see what it is. Check underskill.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
- Hit
ctrl+space
and typeregister
and hitEnter
.
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
- Hit
ctrl+space
and typewatch.watch
and hitEnter
. - Once the watcher is running, change back to the Test Reporter.
Preview in the Development Theatre
- In the
Development Theatre
, hitCommand + Shift + n
- In the “Jump to” Dialog, type
events-kata.root
and select the option in the dropdown. - 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
, hitCommand + 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
- Hit
ctrl+space
and typecreate.event
- For Readable Name, type
My first event
(or whatever you want, really) - For Kebab Case Name, just hit
enter
- For Camel Case Name, just hit
enter
- For Version, select the latest version (if prompted )
Production 4: Defining the Event
We need to define 4 things in our event:
- The Event’s Emit Target
- The Event’s Emit Payload
- The Event’s Response Payload
- The Event’s Permissions