Compose all the things
Mike North
Wicked Good Ember 2015
@MichaelLNorth
modernwebui.org
Modern Web UI
advertising.yahoo.com
Yahoo Ads & Data
Hi
ember-resize
ember-orientation
ember-cpm
ember-cli-materialize
…and more
@MichaelLNorth
Composability
(Mike working w/ a composable system)
//TODO
• The state of ember at yahoo
• What’s composability, and why do we care?
• 4 areas where you can compose today
• Style
• CPMs
• Components
• Tests
@MichaelLNorth
Yahoo Ads & Data
• 14 Ember Apps
• 68 Ember-focused developers
• A “flagship” app that ’s huge (70K lines JS)
• An internal collection of add ons
Ember @ Yahoo
@MichaelLNorth
@MichaelLNorth
Composability
• Recombinant self-
contained pieces
• Built around
established contracts
and conventions
• Promotes reuse
What do I mean?
@MichaelLNorth
Why composability?
• Leverage existing code repeatedly
• Build apps in a more expressive way (DSLs)
• Opportunities for unforeseen uses!
@MichaelLNorth
Great places to start
Style
Tests
Computed Properties
Components
@MichaelLNorth
Great places to startStyle
Declarative CSS
A
<div id=“myThing”>
...
</div>
#myThing {
float: left;
color: white;
}
B
<div class=“pull-left white-text">
...
</div>
.pull-left {
float: left;
}
.white-text {
color: white;
}
@MichaelLNorth
Great places to startStyle
• Atomic CSS classes
• Expressive HTML
• Promotes consistency
Declarative CSS
<div class=“pull-left white-text">
...
</div>
.pull-left {
float: left;
}
.white-text {
color: white;
}
@MichaelLNorth
Great places to startStyle
• You may have classes and/or attributes for
• Testing
• Style
• Behavior
Keep attributes & classes organized
<input class="first-name large-input”
data-autoid="first-name" />
.large-input {
font-size: 32px;
}
style
fillIn(‘input[data-autoid="first-name"]',
'Mike');
testing
@MichaelLNorth
Great places to start
Style
Tests
Computed Properties
Components
@MichaelLNorth
Great places to startComputed Properties
How does a computed property work?
GET
Has cached
value?
Recalculate
No
Yes
X
Allows
caching?
Cache
X
XYes
No
ReturnX
@MichaelLNorth
Great places to startComputed Properties
How does a computed property work?
DEPENDENT CHANGED
Cache
X
I’ve
changed!
ViewProperty
obj.get(‘val’)
@MichaelLNorth
Great places to startComputed Properties
CPs can be thought of as filters
CP
r
g
b
#ff1a99
get() set()
(sometimes)
CP
r
b
g
#ff1a99
@MichaelLNorth
Great places to startComputed Properties
• Ember.computed.*
Macros make this even easier
function product(prop, coeff) {
return Ember.computed(prop, {
get() {
return this.get(prop) * coeff;
}
});
}
@MichaelLNorth
Great places to startComputed Properties
totalAmount: sum(
'subtotal',
'tipAmount',
'taxAmount',
product('discount', -1)
),
Composable CPs can be mixed and
matched
ember-cpm
@MichaelLNorth
Great places to startComputed Properties
Example
@MichaelLNorth
Great places to start
Style
Tests
Computed Properties
Components
@MichaelLNorth
Great places to startComponents
• Not source of truth for state ✔
• Promotes Reuse ✔
• Recombinant ?
Components are pretty close…
ember-cli-materialize
@MichaelLNorth
Great places to startComponents
Looking for this
<div class="card blue-grey darken-1">
<div class="card-content white-text">
<div class="card-title">
Wicked Good Ember
</div>
This will go in the body of the card
</div>
<div class="card-action">
<a>
<span {{action “accept”}}>Accept</span>
</a>
<a>
<span {{action “cancel”}}>Cancel</span>
</a>
</div>
</div>
@MichaelLNorth
Great places to startComponents
One option - lowest common element
<div class="card blue-grey darken-1">
<div class="card-content white-text">
<div class="card-title">
Wicked Good Ember
</div>
This will go in the body of the card
</div>
<div class="card-action">
<a>
<span {{action “accept”}}>Accept</span>
</a>
<a>
<span {{action “cancel”}}>Cancel</span>
</a>
</div>
</div>
{{#wge-card}}
<div class="card-content white-text">
<div class="card-title">
Wicked Good Ember
</div>
This will go in the body of the card
</div>
<div class="card-action">
{{#wge-card-action}}
<span {{action "accept"}}>
Accept
</span>
{{/wge-card-action}}
{{#wge-card-action}}
<span {{action "cancel"}}>
Cancel
</span>
{{/wge-card-action}}
</div>
{{/wge-card}}
@MichaelLNorth
Great places to startComponents
One option - lowest common element
<div class="card blue-grey darken-1">
<div class="card-content white-text">
<div class="card-title">
Wicked Good Ember
</div>
This will go in the body of the card
</div>
<div class="card-action">
<a>
<span {{action “accept”}}>Accept</span>
</a>
<a>
<span {{action “cancel”}}>Cancel</span>
</a>
</div>
</div>
{{#wge-card}}
<div class="card-content white-text">
<div class="card-title">
Wicked Good Ember
</div>
This will go in the body of the card
</div>
<div class="card-action">
{{#wge-card-action}}
<span {{action "accept"}}>
Accept
</span>
{{/wge-card-action}}
{{#wge-card-action}}
<span {{action "cancel"}}>
Cancel
</span>
{{/wge-card-action}}
</div>
{{/wge-card}}
Not Useful
@MichaelLNorth
Great places to startComponents
Another option - parent does everything
<div class="card blue-grey darken-1">
<div class="card-content white-text">
<div class="card-title">
Wicked Good Ember
</div>
This will go in the body of the card
</div>
<div class="card-action">
<a>
<span {{action “accept”}}>Accept</span>
</a>
<a>
<span {{action “cancel”}}>Cancel</span>
</a>
</div>
</div>
{{#wge-card
title="Wicked Good Ember"
cardActions=myCardActions}}
This will go in the body
of the card
{{/wge-card}}
@MichaelLNorth
Great places to startComponents
Another option - parent does everything
<div class="card blue-grey darken-1">
<div class="card-content white-text">
<div class="card-title">
Wicked Good Ember
</div>
This will go in the body of the card
</div>
<div class="card-action">
<a>
<span {{action “accept”}}>Accept</span>
</a>
<a>
<span {{action “cancel”}}>Cancel</span>
</a>
</div>
</div>
{{#wge-card
title="Wicked Good Ember"
cardActions=myCardActions}}
This will go in the body
of the card
{{/wge-card}}
Not Composable
@MichaelLNorth
Great places to startComponents
The expressive option
{{#wge-card title="Wicked Good Ember"}}
This will go in the body of the card
{{#wge-card-action}}
<span {{action "accept"}}>Accept</span>
{{/wge-card-action}}
{{#wge-card-action}}
<span {{action "cancel"}}>Cancel</span>
{{/wge-card-action}}
{{/wge-card}}
@MichaelLNorth
Great places to start
{{#wge-card
title="Wicked Good Ember"}}
This will go in the
body of the card
{{#wge-card-action}}
<span {{action “accept"}}>
Accept
</span>
{{/wge-card-action}}
{{#wge-card-action}}
<span {{action “cancel”}}>
Cancel
</span>
{{/wge-card-action}}
{{/wge-card}}
Components
Content projection - ruh roh
<div class="card blue-grey darken-1">
<div class="card-content white-text">
<div class="card-title">
Wicked Good Ember
</div>
This will go in the body of the card
</div>
<div class="card-action">
<a>
<span {{action “accept”}}>Accept</span>
</a>
<a>
<span {{action “cancel”}}>Cancel</span>
</a>
</div>
</div>
4 distinct pieces of content
@MichaelLNorth
Great places to start
{{#wge-card
title="Wicked Good Ember"}}
This will go in the
body of the card
{{#wge-card-action}}
<span {{action “accept"}}>
Accept
</span>
{{/wge-card-action}}
{{#wge-card-action}}
<span {{action “cancel”}}>
Cancel
</span>
{{/wge-card-action}}
{{/wge-card}}
Components
Content projection - ruh roh
<div class="card blue-grey darken-1">
<div class="card-content white-text">
<div class="card-title">
Wicked Good Ember
</div>
This will go in the body of the card
</div>
<div class="card-action">
<a>
<span {{action “accept”}}>Accept</span>
</a>
<a>
<span {{action “cancel”}}>Cancel</span>
</a>
</div>
</div>
4 distinct pieces of content
Sub-components
project into parent
{{yield}}
Simple property binding
@MichaelLNorth
Great places to startComponents
Content projection approach
{{#wge-card
title="Wicked Good Ember"}}
This will go in the
body of the card
{{#wge-card-action}}
<span {{action “accept"}}>
Accept
</span>
{{/wge-card-action}}
{{#wge-card-action}}
<span {{action “cancel”}}>
Cancel
</span>
{{/wge-card-action}}
{{/wge-card}}
• Child components won’t
render directly
• Parent will handle
rendering of children
• Register/unregister to
parent
@MichaelLNorth
Great places to start
Child
export default Ember.Component.extend({
didInsertElement() {
this.nearestWithProperty('_wgeCard')
.registerWgeAction(this);
},
willDestroyElement() {
this.nearestWithProperty('_wgeCard')
.unregisterWgeAction(this);
},
render() {} // Don't render
});
const { computed: {alias, empty} } = Ember;
export default Ember.Component.extend({
classNames: ['card'],
_wgeCard: true,
_cardActions: [],
// Needed to ensure context of
// child component templates
// is the controller
parentController: alias('targetObject'),
registerWgeAction(component) {
this.get('_cardActions')
.addObject(component);
},
unregisterWgeAction(component) {
this.get('_cardActions')
.removeObject(component);
}
});
Parent
Components
@MichaelLNorth
Great places to start
import Ember from 'ember';
const { computed: {alias, empty} } = Ember;
export default Ember.Component.extend({
classNames: ['card'],
_wgeCard: true,
_cardActions: [],
// Needed to ensure context of
// child component templates
// is the controller
parentController: alias('targetObject'),
registerWgeAction(component) {
this.get('_cardActions')
.addObject(component);
},
unregisterWgeAction(component) {
this.get('_cardActions')
.removeObject(component);
}
});
Parent
<div class="card-content white-text">
<div class="card-title">{{title}}</div>
{{yield}}
</div>
{{#if _cardActions.length}}
<div class="card-action">
{{#each _cardActions as |cardAction|}}
{{view Ember.View
tagName='a'
template=cardAction.template
controller=parentController}}
{{/each}}
</div>
{{/if}}
Components
Parent.hbs
@MichaelLNorth
Great places to startComponents
<div class="card-content white-text">
<div class="card-title">{{title}}</div>
{{yield}}
</div>
{{#if _cardActions.length}}
<div class="card-action">
{{#each _cardActions as |cardAction|}}
{{view Ember.View
tagName='a'
template=cardAction.template
controller=parentController}}
{{/each}}
</div>
{{/if}}
“Captured”
template
Needed for
actions &
bindings
@MichaelLNorth
Great places to start
Style
Tests
Computed Properties
Components
@MichaelLNorth
Great places to startTests
A lot of tests are verbose and ugly
• Sensitivity to order and/or timing
• Brittle selectors to interact with the DOM
• 1 change —> break N tests
@MichaelLNorth
Great places to startTests
test(“authorized user should end up at account's search list page”, function(assert) {
server.get(`${apiHost.url}/me`, json(200, me));
server.get(`${apiHost.url}/campaigns/:id`, json(200, campaign1));
server.get(`${apiHost.url}/seats/2`, json(200, seat2));
server.get(`${apiHost.url}/seats/1`, json(200, seat1));
server.get(`${apiHost.url}/account/:id`, json(200, account1));
server.get(`${apiHost.url}/breadcrumbs`, json(200, breadcrumbs.campaign));
visit('/app/account/1/campaigns');
andThen(function() {
assert.equal(currentPath(), ‘app.account.campaign', 'Current url is search list page for campaig
assert.equal(Ember.$('.top-navbar .brand-logo .active-title').text().trim(), campaign1.campaign
assert.equal(Ember.$('.resource-tiles-container .card').length, campaigns.campaigns.length, 'On
assert.deepEqual(Ember.$('.resource-tiles-container .resource-tile:first-child .card .card-cont
['Created', 'Updated'], 'Columns are correct');
assert.equal(Ember.$('.new-campaign-button').length, 1, 'New Campaign button is on the screen')
});
});
A lot of tests are verbose and ugly
@MichaelLNorth
Great places to startTests
A wild PageObject appears
• Prime pretender
• Access to controls
• Specific asserts
fillInclick
currentURL
andThenvisit
triggerEvent
$().val()$
$().click()$().trigger()
setFirstName
clickResetButton setAge
openSettings
PageObject API
Ember Testing API
DOMSee:
@MichaelLNorth
Great places to startTests
Writing PageObjects
• Return this
• To assert or not to assert?
• Build PageObjects for components
Example
The recombinant
part!
@MichaelLNorth
Great places to startSome final thoughts
Even more composability on the way!
• Add-ons
• Ember.Service
• Engines (TBD)
• Components ( {{yield}}, block params,
etc…)
@MichaelLNorth
Conclusion
Style
Tests
Computed Properties
Components
truenorth/wge-examples

Compose all the things (Wicked Good Ember 2015)

  • 1.
    Compose all thethings Mike North Wicked Good Ember 2015
  • 2.
    @MichaelLNorth modernwebui.org Modern Web UI advertising.yahoo.com YahooAds & Data Hi ember-resize ember-orientation ember-cpm ember-cli-materialize …and more
  • 3.
  • 4.
    //TODO • The stateof ember at yahoo • What’s composability, and why do we care? • 4 areas where you can compose today • Style • CPMs • Components • Tests
  • 5.
    @MichaelLNorth Yahoo Ads &Data • 14 Ember Apps • 68 Ember-focused developers • A “flagship” app that ’s huge (70K lines JS) • An internal collection of add ons Ember @ Yahoo
  • 6.
  • 7.
    @MichaelLNorth Composability • Recombinant self- containedpieces • Built around established contracts and conventions • Promotes reuse What do I mean?
  • 8.
    @MichaelLNorth Why composability? • Leverageexisting code repeatedly • Build apps in a more expressive way (DSLs) • Opportunities for unforeseen uses!
  • 9.
    @MichaelLNorth Great places tostart Style Tests Computed Properties Components
  • 10.
    @MichaelLNorth Great places tostartStyle Declarative CSS A <div id=“myThing”> ... </div> #myThing { float: left; color: white; } B <div class=“pull-left white-text"> ... </div> .pull-left { float: left; } .white-text { color: white; }
  • 11.
    @MichaelLNorth Great places tostartStyle • Atomic CSS classes • Expressive HTML • Promotes consistency Declarative CSS <div class=“pull-left white-text"> ... </div> .pull-left { float: left; } .white-text { color: white; }
  • 12.
    @MichaelLNorth Great places tostartStyle • You may have classes and/or attributes for • Testing • Style • Behavior Keep attributes & classes organized <input class="first-name large-input” data-autoid="first-name" /> .large-input { font-size: 32px; } style fillIn(‘input[data-autoid="first-name"]', 'Mike'); testing
  • 13.
    @MichaelLNorth Great places tostart Style Tests Computed Properties Components
  • 14.
    @MichaelLNorth Great places tostartComputed Properties How does a computed property work? GET Has cached value? Recalculate No Yes X Allows caching? Cache X XYes No ReturnX
  • 15.
    @MichaelLNorth Great places tostartComputed Properties How does a computed property work? DEPENDENT CHANGED Cache X I’ve changed! ViewProperty obj.get(‘val’)
  • 16.
    @MichaelLNorth Great places tostartComputed Properties CPs can be thought of as filters CP r g b #ff1a99 get() set() (sometimes) CP r b g #ff1a99
  • 17.
    @MichaelLNorth Great places tostartComputed Properties • Ember.computed.* Macros make this even easier function product(prop, coeff) { return Ember.computed(prop, { get() { return this.get(prop) * coeff; } }); }
  • 18.
    @MichaelLNorth Great places tostartComputed Properties totalAmount: sum( 'subtotal', 'tipAmount', 'taxAmount', product('discount', -1) ), Composable CPs can be mixed and matched ember-cpm
  • 19.
    @MichaelLNorth Great places tostartComputed Properties Example
  • 20.
    @MichaelLNorth Great places tostart Style Tests Computed Properties Components
  • 21.
    @MichaelLNorth Great places tostartComponents • Not source of truth for state ✔ • Promotes Reuse ✔ • Recombinant ? Components are pretty close… ember-cli-materialize
  • 22.
    @MichaelLNorth Great places tostartComponents Looking for this <div class="card blue-grey darken-1"> <div class="card-content white-text"> <div class="card-title"> Wicked Good Ember </div> This will go in the body of the card </div> <div class="card-action"> <a> <span {{action “accept”}}>Accept</span> </a> <a> <span {{action “cancel”}}>Cancel</span> </a> </div> </div>
  • 23.
    @MichaelLNorth Great places tostartComponents One option - lowest common element <div class="card blue-grey darken-1"> <div class="card-content white-text"> <div class="card-title"> Wicked Good Ember </div> This will go in the body of the card </div> <div class="card-action"> <a> <span {{action “accept”}}>Accept</span> </a> <a> <span {{action “cancel”}}>Cancel</span> </a> </div> </div> {{#wge-card}} <div class="card-content white-text"> <div class="card-title"> Wicked Good Ember </div> This will go in the body of the card </div> <div class="card-action"> {{#wge-card-action}} <span {{action "accept"}}> Accept </span> {{/wge-card-action}} {{#wge-card-action}} <span {{action "cancel"}}> Cancel </span> {{/wge-card-action}} </div> {{/wge-card}}
  • 24.
    @MichaelLNorth Great places tostartComponents One option - lowest common element <div class="card blue-grey darken-1"> <div class="card-content white-text"> <div class="card-title"> Wicked Good Ember </div> This will go in the body of the card </div> <div class="card-action"> <a> <span {{action “accept”}}>Accept</span> </a> <a> <span {{action “cancel”}}>Cancel</span> </a> </div> </div> {{#wge-card}} <div class="card-content white-text"> <div class="card-title"> Wicked Good Ember </div> This will go in the body of the card </div> <div class="card-action"> {{#wge-card-action}} <span {{action "accept"}}> Accept </span> {{/wge-card-action}} {{#wge-card-action}} <span {{action "cancel"}}> Cancel </span> {{/wge-card-action}} </div> {{/wge-card}} Not Useful
  • 25.
    @MichaelLNorth Great places tostartComponents Another option - parent does everything <div class="card blue-grey darken-1"> <div class="card-content white-text"> <div class="card-title"> Wicked Good Ember </div> This will go in the body of the card </div> <div class="card-action"> <a> <span {{action “accept”}}>Accept</span> </a> <a> <span {{action “cancel”}}>Cancel</span> </a> </div> </div> {{#wge-card title="Wicked Good Ember" cardActions=myCardActions}} This will go in the body of the card {{/wge-card}}
  • 26.
    @MichaelLNorth Great places tostartComponents Another option - parent does everything <div class="card blue-grey darken-1"> <div class="card-content white-text"> <div class="card-title"> Wicked Good Ember </div> This will go in the body of the card </div> <div class="card-action"> <a> <span {{action “accept”}}>Accept</span> </a> <a> <span {{action “cancel”}}>Cancel</span> </a> </div> </div> {{#wge-card title="Wicked Good Ember" cardActions=myCardActions}} This will go in the body of the card {{/wge-card}} Not Composable
  • 27.
    @MichaelLNorth Great places tostartComponents The expressive option {{#wge-card title="Wicked Good Ember"}} This will go in the body of the card {{#wge-card-action}} <span {{action "accept"}}>Accept</span> {{/wge-card-action}} {{#wge-card-action}} <span {{action "cancel"}}>Cancel</span> {{/wge-card-action}} {{/wge-card}}
  • 28.
    @MichaelLNorth Great places tostart {{#wge-card title="Wicked Good Ember"}} This will go in the body of the card {{#wge-card-action}} <span {{action “accept"}}> Accept </span> {{/wge-card-action}} {{#wge-card-action}} <span {{action “cancel”}}> Cancel </span> {{/wge-card-action}} {{/wge-card}} Components Content projection - ruh roh <div class="card blue-grey darken-1"> <div class="card-content white-text"> <div class="card-title"> Wicked Good Ember </div> This will go in the body of the card </div> <div class="card-action"> <a> <span {{action “accept”}}>Accept</span> </a> <a> <span {{action “cancel”}}>Cancel</span> </a> </div> </div> 4 distinct pieces of content
  • 29.
    @MichaelLNorth Great places tostart {{#wge-card title="Wicked Good Ember"}} This will go in the body of the card {{#wge-card-action}} <span {{action “accept"}}> Accept </span> {{/wge-card-action}} {{#wge-card-action}} <span {{action “cancel”}}> Cancel </span> {{/wge-card-action}} {{/wge-card}} Components Content projection - ruh roh <div class="card blue-grey darken-1"> <div class="card-content white-text"> <div class="card-title"> Wicked Good Ember </div> This will go in the body of the card </div> <div class="card-action"> <a> <span {{action “accept”}}>Accept</span> </a> <a> <span {{action “cancel”}}>Cancel</span> </a> </div> </div> 4 distinct pieces of content Sub-components project into parent {{yield}} Simple property binding
  • 30.
    @MichaelLNorth Great places tostartComponents Content projection approach {{#wge-card title="Wicked Good Ember"}} This will go in the body of the card {{#wge-card-action}} <span {{action “accept"}}> Accept </span> {{/wge-card-action}} {{#wge-card-action}} <span {{action “cancel”}}> Cancel </span> {{/wge-card-action}} {{/wge-card}} • Child components won’t render directly • Parent will handle rendering of children • Register/unregister to parent
  • 31.
    @MichaelLNorth Great places tostart Child export default Ember.Component.extend({ didInsertElement() { this.nearestWithProperty('_wgeCard') .registerWgeAction(this); }, willDestroyElement() { this.nearestWithProperty('_wgeCard') .unregisterWgeAction(this); }, render() {} // Don't render }); const { computed: {alias, empty} } = Ember; export default Ember.Component.extend({ classNames: ['card'], _wgeCard: true, _cardActions: [], // Needed to ensure context of // child component templates // is the controller parentController: alias('targetObject'), registerWgeAction(component) { this.get('_cardActions') .addObject(component); }, unregisterWgeAction(component) { this.get('_cardActions') .removeObject(component); } }); Parent Components
  • 32.
    @MichaelLNorth Great places tostart import Ember from 'ember'; const { computed: {alias, empty} } = Ember; export default Ember.Component.extend({ classNames: ['card'], _wgeCard: true, _cardActions: [], // Needed to ensure context of // child component templates // is the controller parentController: alias('targetObject'), registerWgeAction(component) { this.get('_cardActions') .addObject(component); }, unregisterWgeAction(component) { this.get('_cardActions') .removeObject(component); } }); Parent <div class="card-content white-text"> <div class="card-title">{{title}}</div> {{yield}} </div> {{#if _cardActions.length}} <div class="card-action"> {{#each _cardActions as |cardAction|}} {{view Ember.View tagName='a' template=cardAction.template controller=parentController}} {{/each}} </div> {{/if}} Components Parent.hbs
  • 33.
    @MichaelLNorth Great places tostartComponents <div class="card-content white-text"> <div class="card-title">{{title}}</div> {{yield}} </div> {{#if _cardActions.length}} <div class="card-action"> {{#each _cardActions as |cardAction|}} {{view Ember.View tagName='a' template=cardAction.template controller=parentController}} {{/each}} </div> {{/if}} “Captured” template Needed for actions & bindings
  • 34.
    @MichaelLNorth Great places tostart Style Tests Computed Properties Components
  • 35.
    @MichaelLNorth Great places tostartTests A lot of tests are verbose and ugly • Sensitivity to order and/or timing • Brittle selectors to interact with the DOM • 1 change —> break N tests
  • 36.
    @MichaelLNorth Great places tostartTests test(“authorized user should end up at account's search list page”, function(assert) { server.get(`${apiHost.url}/me`, json(200, me)); server.get(`${apiHost.url}/campaigns/:id`, json(200, campaign1)); server.get(`${apiHost.url}/seats/2`, json(200, seat2)); server.get(`${apiHost.url}/seats/1`, json(200, seat1)); server.get(`${apiHost.url}/account/:id`, json(200, account1)); server.get(`${apiHost.url}/breadcrumbs`, json(200, breadcrumbs.campaign)); visit('/app/account/1/campaigns'); andThen(function() { assert.equal(currentPath(), ‘app.account.campaign', 'Current url is search list page for campaig assert.equal(Ember.$('.top-navbar .brand-logo .active-title').text().trim(), campaign1.campaign assert.equal(Ember.$('.resource-tiles-container .card').length, campaigns.campaigns.length, 'On assert.deepEqual(Ember.$('.resource-tiles-container .resource-tile:first-child .card .card-cont ['Created', 'Updated'], 'Columns are correct'); assert.equal(Ember.$('.new-campaign-button').length, 1, 'New Campaign button is on the screen') }); }); A lot of tests are verbose and ugly
  • 37.
    @MichaelLNorth Great places tostartTests A wild PageObject appears • Prime pretender • Access to controls • Specific asserts fillInclick currentURL andThenvisit triggerEvent $().val()$ $().click()$().trigger() setFirstName clickResetButton setAge openSettings PageObject API Ember Testing API DOMSee:
  • 38.
    @MichaelLNorth Great places tostartTests Writing PageObjects • Return this • To assert or not to assert? • Build PageObjects for components Example The recombinant part!
  • 39.
    @MichaelLNorth Great places tostartSome final thoughts Even more composability on the way! • Add-ons • Ember.Service • Engines (TBD) • Components ( {{yield}}, block params, etc…)
  • 40.