import { RenderingTest, moduleFor } from '../../utils/test-case'; import { strip } from '../../utils/abstract-test-case'; import { Component } from '../../utils/helpers'; import { set } from '@ember/-internals/metal'; import { subscribe as instrumentationSubscribe, reset as instrumentationReset, } from '@ember/instrumentation'; import { EMBER_IMPROVED_INSTRUMENTATION } from '@ember/canary-features'; import { Object as EmberObject, A as emberA } from '@ember/-internals/runtime'; import { ActionManager } from '@ember/-internals/views'; function getActionAttributes(element) { let attributes = element.attributes; let actionAttrs = []; for (let i = 0; i < attributes.length; i++) { let attr = attributes.item(i); if (attr.name.indexOf('data-ember-action-') === 0) { actionAttrs.push(attr.name); } } return actionAttrs; } function getActionIds(element) { return getActionAttributes(element).map(attribute => attribute.slice('data-ember-action-'.length) ); } const isIE11 = !window.ActiveXObject && 'ActiveXObject' in window; if (EMBER_IMPROVED_INSTRUMENTATION) { moduleFor( 'Helpers test: element action instrumentation', class extends RenderingTest { teardown() { super.teardown(); instrumentationReset(); } ['@test action should fire interaction event with proper params']() { let subscriberCallCount = 0; let subscriberPayload = null; let ExampleComponent = Component.extend({ actions: { foo() {}, }, }); this.registerComponent('example-component', { ComponentClass: ExampleComponent, template: '', }); instrumentationSubscribe('interaction.ember-action', { before() { subscriberCallCount++; }, after(name, time, payload) { subscriberPayload = payload; }, }); this.render('{{example-component}}'); this.assert.equal(subscriberCallCount, 0, 'subscriber has not been called'); this.runTask(() => this.rerender()); this.assert.equal(subscriberCallCount, 0, 'subscriber has not been called'); this.runTask(() => { this.$('button').click(); }); this.assert.equal(subscriberCallCount, 1, 'subscriber has been called 1 time'); this.assert.equal(subscriberPayload.name, 'foo', 'subscriber called with correct name'); this.assert.equal(subscriberPayload.args[0], 'bar', 'subscriber called with correct args'); } } ); } moduleFor( 'Helpers test: element action', class extends RenderingTest { ['@test it can call an action on its enclosing component']() { let fooCallCount = 0; let ExampleComponent = Component.extend({ actions: { foo() { fooCallCount++; }, }, }); this.registerComponent('example-component', { ComponentClass: ExampleComponent, template: '', }); this.render('{{example-component}}'); this.assert.equal(fooCallCount, 0, 'foo has not been called'); this.runTask(() => this.rerender()); this.assert.equal(fooCallCount, 0, 'foo has not been called'); this.runTask(() => { this.$('button').click(); }); this.assert.equal(fooCallCount, 1, 'foo has been called 1 time'); this.runTask(() => { this.$('button').click(); }); this.assert.equal(fooCallCount, 2, 'foo has been called 2 times'); } ['@test it can call an action with parameters']() { let fooArgs = []; let component; let ExampleComponent = Component.extend({ member: 'a', init() { this._super(...arguments); component = this; }, actions: { foo(thing) { fooArgs.push(thing); }, }, }); this.registerComponent('example-component', { ComponentClass: ExampleComponent, template: '', }); this.render('{{example-component}}'); this.assert.deepEqual(fooArgs, [], 'foo has not been called'); this.runTask(() => this.rerender()); this.assert.deepEqual(fooArgs, [], 'foo has not been called'); this.runTask(() => { this.$('button').click(); }); this.assert.deepEqual(fooArgs, ['a'], 'foo has not been called'); this.runTask(() => { component.set('member', 'b'); }); this.runTask(() => { this.$('button').click(); }); this.assert.deepEqual(fooArgs, ['a', 'b'], 'foo has been called with an updated value'); } ['@test it should output a marker attribute with a guid']() { this.render(''); let button = this.$('button'); let attributes = getActionAttributes(button[0]); this.assert.ok( button.attr('data-ember-action').match(''), 'An empty data-ember-action attribute was added' ); this.assert.ok( attributes[0].match(/data-ember-action-\d+/), 'A data-ember-action-xyz attribute with a guid was added' ); } ['@test it should allow alternative events to be handled']() { let showCalled = false; let ExampleComponent = Component.extend({ actions: { show() { showCalled = true; }, }, }); this.registerComponent('example-component', { ComponentClass: ExampleComponent, template: '
', }); this.render('{{example-component}}'); this.runTask(() => { this.$('#show').trigger('mouseup'); }); this.assert.ok(showCalled, 'show action was called on mouseUp'); } ['@test inside a yield, the target points at the original target']() { let targetWatted = false; let innerWatted = false; let TargetComponent = Component.extend({ actions: { wat() { targetWatted = true; }, }, }); let InnerComponent = Component.extend({ actions: { wat() { innerWatted = true; }, }, }); this.registerComponent('inner-component', { ComponentClass: InnerComponent, template: '{{yield}}', }); this.registerComponent('target-component', { ComponentClass: TargetComponent, template: strip` {{#inner-component}} {{/inner-component}} `, }); this.render('{{target-component}}'); this.runTask(() => { this.$('button').click(); }); this.assert.ok(targetWatted, 'the correct target was watted'); this.assert.notOk(innerWatted, 'the inner target was not watted'); } ['@test it should allow a target to be specified']() { let targetWatted = false; let TargetComponent = Component.extend({ actions: { wat() { targetWatted = true; }, }, }); let OtherComponent = Component.extend({}); this.registerComponent('target-component', { ComponentClass: TargetComponent, template: '{{yield this}}', }); this.registerComponent('other-component', { ComponentClass: OtherComponent, template: 'Wat?', }); this.render(strip` {{#target-component as |parent|}} {{other-component anotherTarget=parent}} {{/target-component}} `); this.runTask(() => { this.$('a').click(); }); this.assert.equal(targetWatted, true, 'the specified target was watted'); } ['@test it should lazily evaluate the target']() { let firstEdit = 0; let secondEdit = 0; let component; let first = { edit() { firstEdit++; }, }; let second = { edit() { secondEdit++; }, }; let ExampleComponent = Component.extend({ init() { this._super(...arguments); component = this; }, theTarget: first, }); this.registerComponent('example-component', { ComponentClass: ExampleComponent, template: 'Edit', }); this.render('{{example-component}}'); this.runTask(() => { this.$('a').click(); }); this.assert.equal(firstEdit, 1); this.runTask(() => { set(component, 'theTarget', second); }); this.runTask(() => { this.$('a').click(); }); this.assert.equal(firstEdit, 1); this.assert.equal(secondEdit, 1); } ['@test it should register an event handler']() { let editHandlerWasCalled = false; let shortcutHandlerWasCalled = false; let ExampleComponent = Component.extend({ actions: { edit() { editHandlerWasCalled = true; }, shortcut() { shortcutHandlerWasCalled = true; }, }, }); this.registerComponent('example-component', { ComponentClass: ExampleComponent, template: 'click me
click me too
', }); this.render('{{example-component}}'); this.runTask(() => { this.$('a[data-ember-action]').trigger('click', { altKey: true }); }); this.assert.equal(editHandlerWasCalled, true, 'the event handler was called'); this.runTask(() => { this.$('div[data-ember-action]').trigger('click', { ctrlKey: true }); }); this.assert.equal( shortcutHandlerWasCalled, true, 'the "any" shortcut\'s event handler was called' ); } ['@test it handles whitelisted bound modifier keys']() { let editHandlerWasCalled = false; let shortcutHandlerWasCalled = false; let ExampleComponent = Component.extend({ altKey: 'alt', anyKey: 'any', actions: { edit() { editHandlerWasCalled = true; }, shortcut() { shortcutHandlerWasCalled = true; }, }, }); this.registerComponent('example-component', { ComponentClass: ExampleComponent, template: 'click me
click me too
', }); this.render('{{example-component}}'); this.runTask(() => { this.$('a[data-ember-action]').trigger('click', { altKey: true }); }); this.assert.equal(editHandlerWasCalled, true, 'the event handler was called'); this.runTask(() => { this.$('div[data-ember-action]').trigger('click', { ctrlKey: true }); }); this.assert.equal( shortcutHandlerWasCalled, true, 'the "any" shortcut\'s event handler was called' ); } ['@test it handles whitelisted bound modifier keys with current value']() { let editHandlerWasCalled = false; let component; let ExampleComponent = Component.extend({ init() { this._super(...arguments); component = this; }, acceptedKeys: 'alt', actions: { edit() { editHandlerWasCalled = true; }, }, }); this.registerComponent('example-component', { ComponentClass: ExampleComponent, template: 'click me', }); this.render('{{example-component}}'); this.runTask(() => { this.$('a[data-ember-action]').trigger('click', { altKey: true }); }); this.assert.equal(editHandlerWasCalled, true, 'the event handler was called'); editHandlerWasCalled = false; this.runTask(() => { component.set('acceptedKeys', ''); }); this.runTask(() => { this.$('div[data-ember-action]').click(); }); this.assert.equal(editHandlerWasCalled, false, 'the event handler was not called'); } ['@test should be able to use action more than once for the same event within a view']() { let editHandlerWasCalled = false; let deleteHandlerWasCalled = false; let originalHandlerWasCalled = false; let component; let ExampleComponent = Component.extend({ init() { this._super(...arguments); component = this; }, actions: { edit() { editHandlerWasCalled = true; }, delete() { deleteHandlerWasCalled = true; }, }, click() { originalHandlerWasCalled = true; }, }); this.registerComponent('example-component', { ComponentClass: ExampleComponent, template: 'editdelete', }); this.render('{{example-component}}'); this.runTask(() => { this.$('#edit').click(); }); this.assert.equal(editHandlerWasCalled, true, 'the edit action was called'); this.assert.equal(deleteHandlerWasCalled, false, 'the delete action was not called'); this.assert.equal( originalHandlerWasCalled, true, 'the click handler was called (due to bubbling)' ); editHandlerWasCalled = deleteHandlerWasCalled = originalHandlerWasCalled = false; this.runTask(() => { this.$('#delete').click(); }); this.assert.equal(editHandlerWasCalled, false, 'the edit action was not called'); this.assert.equal(deleteHandlerWasCalled, true, 'the delete action was called'); this.assert.equal( originalHandlerWasCalled, true, 'the click handler was called (due to bubbling)' ); editHandlerWasCalled = deleteHandlerWasCalled = originalHandlerWasCalled = false; this.runTask(() => { this.wrap(component.element).click(); }); this.assert.equal(editHandlerWasCalled, false, 'the edit action was not called'); this.assert.equal(deleteHandlerWasCalled, false, 'the delete action was not called'); this.assert.equal(originalHandlerWasCalled, true, 'the click handler was called'); } ['@test the event should not bubble if `bubbles=false` is passed']() { let editHandlerWasCalled = false; let deleteHandlerWasCalled = false; let originalHandlerWasCalled = false; let component; let ExampleComponent = Component.extend({ init() { this._super(...arguments); component = this; }, actions: { edit() { editHandlerWasCalled = true; }, delete() { deleteHandlerWasCalled = true; }, }, click() { originalHandlerWasCalled = true; }, }); this.registerComponent('example-component', { ComponentClass: ExampleComponent, template: 'editdelete', }); this.render('{{example-component}}'); this.runTask(() => { this.$('#edit').click(); }); this.assert.equal(editHandlerWasCalled, true, 'the edit action was called'); this.assert.equal(deleteHandlerWasCalled, false, 'the delete action was not called'); this.assert.equal(originalHandlerWasCalled, false, 'the click handler was not called'); editHandlerWasCalled = deleteHandlerWasCalled = originalHandlerWasCalled = false; this.runTask(() => { this.$('#delete').click(); }); this.assert.equal(editHandlerWasCalled, false, 'the edit action was not called'); this.assert.equal(deleteHandlerWasCalled, true, 'the delete action was called'); this.assert.equal(originalHandlerWasCalled, false, 'the click handler was not called'); editHandlerWasCalled = deleteHandlerWasCalled = originalHandlerWasCalled = false; this.runTask(() => { this.wrap(component.element).click(); }); this.assert.equal(editHandlerWasCalled, false, 'the edit action was not called'); this.assert.equal(deleteHandlerWasCalled, false, 'the delete action was not called'); this.assert.equal(originalHandlerWasCalled, true, 'the click handler was called'); } ['@test the event should not bubble if `bubbles=false` is passed bound']() { let editHandlerWasCalled = false; let deleteHandlerWasCalled = false; let originalHandlerWasCalled = false; let component; let ExampleComponent = Component.extend({ init() { this._super(...arguments); component = this; }, isFalse: false, actions: { edit() { editHandlerWasCalled = true; }, delete() { deleteHandlerWasCalled = true; }, }, click() { originalHandlerWasCalled = true; }, }); this.registerComponent('example-component', { ComponentClass: ExampleComponent, template: 'editdelete', }); this.render('{{example-component}}'); this.runTask(() => { this.$('#edit').click(); }); this.assert.equal(editHandlerWasCalled, true, 'the edit action was called'); this.assert.equal(deleteHandlerWasCalled, false, 'the delete action was not called'); this.assert.equal(originalHandlerWasCalled, false, 'the click handler was not called'); editHandlerWasCalled = deleteHandlerWasCalled = originalHandlerWasCalled = false; this.runTask(() => { this.$('#delete').click(); }); this.assert.equal(editHandlerWasCalled, false, 'the edit action was not called'); this.assert.equal(deleteHandlerWasCalled, true, 'the delete action was called'); this.assert.equal(originalHandlerWasCalled, false, 'the click handler was not called'); editHandlerWasCalled = deleteHandlerWasCalled = originalHandlerWasCalled = false; this.runTask(() => { this.wrap(component.element).click(); }); this.assert.equal(editHandlerWasCalled, false, 'the edit action was not called'); this.assert.equal(deleteHandlerWasCalled, false, 'the delete action was not called'); this.assert.equal(originalHandlerWasCalled, true, 'the click handler was called'); } ['@test the bubbling depends on the bound parameter']() { let editHandlerWasCalled = false; let originalHandlerWasCalled = false; let component; let ExampleComponent = Component.extend({ init() { this._super(...arguments); component = this; }, shouldBubble: false, actions: { edit() { editHandlerWasCalled = true; }, }, click() { originalHandlerWasCalled = true; }, }); this.registerComponent('example-component', { ComponentClass: ExampleComponent, template: 'edit', }); this.render('{{example-component}}'); this.runTask(() => { this.$('#edit').click(); }); this.assert.equal(editHandlerWasCalled, true, 'the edit action was called'); this.assert.equal(originalHandlerWasCalled, false, 'the click handler was not called'); editHandlerWasCalled = originalHandlerWasCalled = false; this.runTask(() => { component.set('shouldBubble', true); }); this.runTask(() => { this.$('#edit').click(); }); this.assert.equal(editHandlerWasCalled, true, 'the edit action was called'); this.assert.equal(originalHandlerWasCalled, true, 'the click handler was called'); } ['@test it should work properly in an #each block']() { let editHandlerWasCalled = false; let ExampleComponent = Component.extend({ items: emberA([1, 2, 3, 4]), actions: { edit() { editHandlerWasCalled = true; }, }, }); this.registerComponent('example-component', { ComponentClass: ExampleComponent, template: '{{#each items as |item|}}click me{{/each}}', }); this.render('{{example-component}}'); this.runTask(() => { this.$('a').click(); }); this.assert.equal(editHandlerWasCalled, true, 'the event handler was called'); } ['@test it should work properly in a {{#with foo as |bar|}} block']() { let editHandlerWasCalled = false; let ExampleComponent = Component.extend({ something: { ohai: 'there' }, actions: { edit() { editHandlerWasCalled = true; }, }, }); this.registerComponent('example-component', { ComponentClass: ExampleComponent, template: '{{#with something as |somethingElse|}}click me{{/with}}', }); this.render('{{example-component}}'); this.runTask(() => { this.$('a').click(); }); this.assert.equal(editHandlerWasCalled, true, 'the event handler was called'); } ['@test it should unregister event handlers when an element action is removed'](assert) { let ExampleComponent = Component.extend({ actions: { edit() {}, }, }); this.registerComponent('example-component', { ComponentClass: ExampleComponent, template: '{{#if isActive}}click me{{/if}}', }); this.render('{{example-component isActive=isActive}}', { isActive: true, }); assert.equal(this.$('a[data-ember-action]').length, 1, 'The element is rendered'); let actionId; actionId = getActionIds(this.$('a[data-ember-action]')[0])[0]; assert.ok(ActionManager.registeredActions[actionId], 'An action is registered'); this.runTask(() => this.rerender()); assert.equal(this.$('a[data-ember-action]').length, 1, 'The element is still present'); assert.ok(ActionManager.registeredActions[actionId], 'The action is still registered'); this.runTask(() => set(this.context, 'isActive', false)); assert.strictEqual(this.$('a[data-ember-action]').length, 0, 'The element is removed'); assert.ok(!ActionManager.registeredActions[actionId], 'The action is unregistered'); this.runTask(() => set(this.context, 'isActive', true)); assert.equal(this.$('a[data-ember-action]').length, 1, 'The element is rendered'); actionId = getActionIds(this.$('a[data-ember-action]')[0])[0]; assert.ok(ActionManager.registeredActions[actionId], 'A new action is registered'); } ['@test it should capture events from child elements and allow them to trigger the action']() { let editHandlerWasCalled = false; let ExampleComponent = Component.extend({ actions: { edit() { editHandlerWasCalled = true; }, }, }); this.registerComponent('example-component', { ComponentClass: ExampleComponent, template: '
', }); this.render('{{example-component}}'); this.runTask(() => { this.$('button').click(); }); this.assert.ok( editHandlerWasCalled, 'event on a child target triggered the action of its parent' ); } ['@test it should allow bubbling of events from action helper to original parent event']() { let editHandlerWasCalled = false; let originalHandlerWasCalled = false; let ExampleComponent = Component.extend({ actions: { edit() { editHandlerWasCalled = true; }, }, click() { originalHandlerWasCalled = true; }, }); this.registerComponent('example-component', { ComponentClass: ExampleComponent, template: 'click me', }); this.render('{{example-component}}'); this.runTask(() => { this.$('a').click(); }); this.assert.ok( editHandlerWasCalled && originalHandlerWasCalled, 'both event handlers were called' ); } ['@test it should not bubble an event from action helper to original parent event if `bubbles=false` is passed']() { let editHandlerWasCalled = false; let originalHandlerWasCalled = false; let ExampleComponent = Component.extend({ actions: { edit() { editHandlerWasCalled = true; }, }, click() { originalHandlerWasCalled = true; }, }); this.registerComponent('example-component', { ComponentClass: ExampleComponent, template: 'click me', }); this.render('{{example-component}}'); this.runTask(() => { this.$('a').click(); }); this.assert.ok(editHandlerWasCalled, 'the child event handler was called'); this.assert.notOk(originalHandlerWasCalled, 'the parent handler was not called'); } ['@test it should allow "send" as the action name (#594)']() { let sendHandlerWasCalled = false; let ExampleComponent = Component.extend({ actions: { send() { sendHandlerWasCalled = true; }, }, }); this.registerComponent('example-component', { ComponentClass: ExampleComponent, template: 'click me', }); this.render('{{example-component}}'); this.runTask(() => { this.$('a').click(); }); this.assert.ok(sendHandlerWasCalled, 'the event handler was called'); } ['@test it should send the view, event, and current context to the action']() { let passedTarget; let passedContext; let targetThis; let TargetComponent = Component.extend({ init() { this._super(...arguments); targetThis = this; }, actions: { edit(context) { passedTarget = this === targetThis; passedContext = context; }, }, }); let aContext; let ExampleComponent = Component.extend({ init() { this._super(...arguments); aContext = this; }, }); this.registerComponent('target-component', { ComponentClass: TargetComponent, template: '{{yield this}}', }); this.registerComponent('example-component', { ComponentClass: ExampleComponent, template: strip` {{#target-component as |aTarget|}} click me {{/target-component}} `, }); this.render('{{example-component}}'); this.runTask(() => { this.$('#edit').click(); }); this.assert.ok(passedTarget, 'the action is called with the target as this'); this.assert.strictEqual(passedContext, aContext, 'the parameter is passed along'); } ['@test it should only trigger actions for the event they were registered on']() { let editHandlerWasCalled = false; let ExampleComponent = Component.extend({ actions: { edit() { editHandlerWasCalled = true; }, }, }); this.registerComponent('example-component', { ComponentClass: ExampleComponent, template: 'click me', }); this.render('{{example-component}}'); this.runTask(() => { this.$('a').click(); }); this.assert.ok(editHandlerWasCalled, 'the event handler was called on click'); editHandlerWasCalled = false; this.runTask(() => { this.$('a').trigger('mouseover'); }); this.assert.notOk(editHandlerWasCalled, 'the event handler was not called on mouseover'); } ['@test it should allow multiple contexts to be specified']() { let passedContexts; let models = [EmberObject.create(), EmberObject.create()]; let ExampleComponent = Component.extend({ modelA: models[0], modelB: models[1], actions: { edit(...args) { passedContexts = args; }, }, }); this.registerComponent('example-component', { ComponentClass: ExampleComponent, template: '', }); this.render('{{example-component}}'); this.runTask(() => { this.$('button').click(); }); this.assert.deepEqual( passedContexts, models, 'the action was called with the passed contexts' ); } ['@test it should allow multiple contexts to be specified mixed with string args']() { let passedContexts; let model = EmberObject.create(); let ExampleComponent = Component.extend({ model: model, actions: { edit(...args) { passedContexts = args; }, }, }); this.registerComponent('example-component', { ComponentClass: ExampleComponent, template: '', }); this.render('{{example-component}}'); this.runTask(() => { this.$('button').click(); }); this.assert.deepEqual( passedContexts, ['herp', model], 'the action was called with the passed contexts' ); } ['@test it should not trigger action with special clicks']() { let showCalled = false; let component; let ExampleComponent = Component.extend({ init() { this._super(...arguments); component = this; }, actions: { show() { showCalled = true; }, }, }); this.registerComponent('example-component', { ComponentClass: ExampleComponent, template: '', }); this.render('{{example-component}}'); let assert = this.assert; let checkClick = (prop, value, expected) => { showCalled = false; let event = this.wrap(component.element) .findAll('button') .trigger('click', { [prop]: value })[0]; if (expected) { assert.ok(showCalled, `should call action with ${prop}:${value}`); // IE11 does not allow simulated events to have a valid `defaultPrevented` if (!isIE11) { assert.ok(event.defaultPrevented, 'should prevent default'); } } else { assert.notOk(showCalled, `should not call action with ${prop}:${value}`); assert.notOk(event.defaultPrevented, 'should not prevent default'); } }; checkClick('ctrlKey', true, false); checkClick('altKey', true, false); checkClick('metaKey', true, false); checkClick('shiftKey', true, false); checkClick('button', 0, true); checkClick('button', 1, false); checkClick('button', 2, false); checkClick('button', 3, false); checkClick('button', 4, false); } ['@test it can trigger actions for keyboard events']() { let showCalled = false; let ExampleComponent = Component.extend({ actions: { show() { showCalled = true; }, }, }); this.registerComponent('example-component', { ComponentClass: ExampleComponent, template: '', }); this.render('{{example-component}}'); this.runTask(() => { this.$('input').trigger('keyup', { char: 'a', which: 65 }); }); this.assert.ok(showCalled, 'the action was called with keyup'); } ['@test a quoteless parameter should allow dynamic lookup of the actionName']() { let lastAction; let actionOrder = []; let component; let ExampleComponent = Component.extend({ init() { this._super(...arguments); component = this; }, hookMeUp: 'rock', actions: { rock() { lastAction = 'rock'; actionOrder.push('rock'); }, paper() { lastAction = 'paper'; actionOrder.push('paper'); }, scissors() { lastAction = 'scissors'; actionOrder.push('scissors'); }, }, }); this.registerComponent('example-component', { ComponentClass: ExampleComponent, template: 'Whistle tips go woop woooop', }); this.render('{{example-component}}'); let test = this; let testBoundAction = propertyValue => { test.runTask(() => { component.set('hookMeUp', propertyValue); }); test.runTask(() => { this.wrap(component.element) .findAll('#bound-param') .click(); }); test.assert.ok(lastAction, propertyValue, `lastAction set to ${propertyValue}`); }; testBoundAction('rock'); testBoundAction('paper'); testBoundAction('scissors'); this.assert.deepEqual( actionOrder, ['rock', 'paper', 'scissors'], 'action name was looked up properly' ); } ['@test a quoteless string parameter should resolve actionName, including path']() { let lastAction; let actionOrder = []; let component; let ExampleComponent = Component.extend({ init() { this._super(...arguments); component = this; }, allactions: emberA([ { title: 'Rock', name: 'rock' }, { title: 'Paper', name: 'paper' }, { title: 'Scissors', name: 'scissors' }, ]), actions: { rock() { lastAction = 'rock'; actionOrder.push('rock'); }, paper() { lastAction = 'paper'; actionOrder.push('paper'); }, scissors() { lastAction = 'scissors'; actionOrder.push('scissors'); }, }, }); this.registerComponent('example-component', { ComponentClass: ExampleComponent, template: '{{#each allactions as |allaction|}}{{allaction.title}}{{/each}}', }); this.render('{{example-component}}'); let test = this; let testBoundAction = propertyValue => { test.runTask(() => { this.wrap(component.element) .findAll(`#${propertyValue}`) .click(); }); test.assert.ok(lastAction, propertyValue, `lastAction set to ${propertyValue}`); }; testBoundAction('rock'); testBoundAction('paper'); testBoundAction('scissors'); this.assert.deepEqual( actionOrder, ['rock', 'paper', 'scissors'], 'action name was looked up properly' ); } ['@test a quoteless function parameter should be called, including arguments']() { let submitCalled = false; let incomingArg; let arg = 'rough ray'; let ExampleComponent = Component.extend({ submit(actualArg) { incomingArg = actualArg; submitCalled = true; }, }); this.registerComponent('example-component', { ComponentClass: ExampleComponent, template: `Hi`, }); this.render('{{example-component}}'); this.runTask(() => { this.$('a').click(); }); this.assert.ok(submitCalled, 'submit function called'); this.assert.equal(incomingArg, arg, 'argument passed'); } ['@test a quoteless parameter that does not resolve to a value asserts']() { let ExampleComponent = Component.extend({ actions: { ohNoeNotValid() {}, }, }); this.registerComponent('example-component', { ComponentClass: ExampleComponent, template: 'Hi', }); expectAssertion(() => { this.render('{{example-component}}'); }, 'You specified a quoteless path, `ohNoeNotValid`, to the {{action}} helper ' + 'which did not resolve to an action name (a string). ' + 'Perhaps you meant to use a quoted actionName? (e.g. {{action "ohNoeNotValid"}}).'); } ['@test allows multiple actions on a single element']() { let clickActionWasCalled = false; let doubleClickActionWasCalled = false; let ExampleComponent = Component.extend({ actions: { clicked() { clickActionWasCalled = true; }, doubleClicked() { doubleClickActionWasCalled = true; }, }, }); this.registerComponent('example-component', { ComponentClass: ExampleComponent, template: strip` click me`, }); this.render('{{example-component}}'); this.runTask(() => { this.$('a').trigger('click'); }); this.assert.ok(clickActionWasCalled, 'the clicked action was called'); this.runTask(() => { this.$('a').trigger('dblclick'); }); this.assert.ok(doubleClickActionWasCalled, 'the doubleClicked action was called'); } ['@test it should respect preventDefault option if provided']() { let ExampleComponent = Component.extend({ actions: { show() {}, }, }); this.registerComponent('example-component', { ComponentClass: ExampleComponent, template: 'Hi', }); this.render('{{example-component}}'); let event; this.runTask(() => { event = this.$('a').click()[0]; }); this.assert.equal(event.defaultPrevented, false, 'should not preventDefault'); } ['@test it should respect preventDefault option if provided bound']() { let component; let ExampleComponent = Component.extend({ shouldPreventDefault: false, init() { this._super(...arguments); component = this; }, actions: { show() {}, }, }); this.registerComponent('example-component', { ComponentClass: ExampleComponent, template: 'Hi', }); this.render('{{example-component}}'); let event; this.runTask(() => { event = this.$('a').trigger(event)[0]; }); this.assert.equal(event.defaultPrevented, false, 'should not preventDefault'); this.runTask(() => { component.set('shouldPreventDefault', true); event = this.$('a').trigger('click')[0]; }); // IE11 does not allow simulated events to have a valid `defaultPrevented` if (!isIE11) { this.assert.equal(event.defaultPrevented, true, 'should preventDefault'); } } ['@test it should target the proper component when `action` is in yielded block [GH #12409]']() { let outerActionCalled = false; let innerClickCalled = false; let OuterComponent = Component.extend({ actions: { hey() { outerActionCalled = true; }, }, }); let MiddleComponent = Component.extend({}); let InnerComponent = Component.extend({ click() { innerClickCalled = true; expectDeprecation(() => { this.sendAction(); }, /You called (.*).sendAction\((.*)\) but Component#sendAction is deprecated. Please use closure actions instead./); }, }); this.registerComponent('outer-component', { ComponentClass: OuterComponent, template: strip` {{#middle-component}} {{inner-component action="hey"}} {{/middle-component}} `, }); this.registerComponent('middle-component', { ComponentClass: MiddleComponent, template: '{{yield}}', }); this.registerComponent('inner-component', { ComponentClass: InnerComponent, template: strip` {{yield}} `, }); this.render('{{outer-component}}'); this.runTask(() => { this.$('button').click(); }); this.assert.ok(outerActionCalled, 'the action fired on the proper target'); this.assert.ok(innerClickCalled, 'the click was triggered'); } ['@test element action with (mut undefinedThing) works properly']() { let component; let ExampleComponent = Component.extend({ label: undefined, init() { this._super(...arguments); component = this; }, }); this.registerComponent('example-component', { ComponentClass: ExampleComponent, template: '', }); this.render('{{example-component}}'); this.assertText('Click me'); this.assertStableRerender(); this.runTask(() => { this.$('button').click(); }); this.assertText('Clicked!'); this.runTask(() => { component.set('label', 'Dun clicked'); }); this.assertText('Dun clicked'); this.runTask(() => { this.$('button').click(); }); this.assertText('Clicked!'); this.runTask(() => { component.set('label', undefined); }); this.assertText('Click me'); } ['@test it supports non-registered actions [GH#14888]']() { this.render( ` {{#if show}} {{/if}} `, { show: true } ); this.assert.equal( this.$('button') .text() .trim(), 'Show (true)' ); // We need to focus in to simulate an actual click. this.runTask(() => { document.getElementById('ddButton').focus(); document.getElementById('ddButton').click(); }); } ["@test action handler that shifts element attributes doesn't trigger multiple invocations"]() { let actionCount = 0; let ExampleComponent = Component.extend({ selected: false, actions: { toggleSelected() { actionCount++; this.toggleProperty('selected'); }, }, }); this.registerComponent('example-component', { ComponentClass: ExampleComponent, template: '', }); this.render('{{example-component}}'); this.runTask(() => { this.$('button').click(); }); this.assert.equal(actionCount, 1, 'Click action only fired once.'); this.assert.ok( this.$('button').hasClass('selected'), "Element with action handler has properly updated it's conditional class" ); this.runTask(() => { this.$('button').click(); }); this.assert.equal(actionCount, 2, 'Second click action only fired once.'); this.assert.ok( !this.$('button').hasClass('selected'), "Element with action handler has properly updated it's conditional class" ); } } );