import { subscribe as instrumentationSubscribe, unsubscribe as instrumentationUnsubscribe, } from '@ember/instrumentation'; import { getCurrentRunLoop } from '@ember/runloop'; import { set, computed } from 'ember-metal'; import { EMBER_IMPROVED_INSTRUMENTATION } from '@ember/canary-features'; import { RenderingTest, moduleFor } from '../../utils/test-case'; import { strip } from '../../utils/abstract-test-case'; import { Component, INVOKE } from '../../utils/helpers'; if (EMBER_IMPROVED_INSTRUMENTATION) { moduleFor( 'Helpers test: closure {{action}} improved instrumentation', class extends RenderingTest { subscribe(eventName, options) { this.subscriber = instrumentationSubscribe(eventName, options); } teardown() { if (this.subscriber) { instrumentationUnsubscribe(this.subscriber); } super.teardown(); } ['@test interaction event subscriber should be passed parameters']() { let actionParam = 'So krispy'; let beforeParameters = []; let afterParameters = []; let InnerComponent = Component.extend({ actions: { fireAction() { this.attrs.submit(actionParam); }, }, }); let OuterComponent = Component.extend({ outerSubmit() {}, }); this.registerComponent('inner-component', { ComponentClass: InnerComponent, template: '', }); this.registerComponent('outer-component', { ComponentClass: OuterComponent, template: '{{inner-component submit=(action outerSubmit)}}', }); this.subscribe('interaction.ember-action', { before(name, timestamp, payload) { beforeParameters.push(payload.args); }, after(name, timestamp, payload) { afterParameters.push(payload.args); }, }); this.render(`{{outer-component}}`); this.runTask(() => { this.$('#instrument-button').trigger('click'); }); this.assert.deepEqual( beforeParameters, [[], [actionParam]], 'instrumentation subscriber before function was passed closure action parameters' ); this.assert.deepEqual( afterParameters, [[actionParam], []], 'instrumentation subscriber after function was passed closure action parameters' ); } ['@test interaction event subscriber should be passed target']() { let beforeParameters = []; let afterParameters = []; let InnerComponent = Component.extend({ myProperty: 'inner-thing', actions: { fireAction() { this.attrs.submit(); }, }, }); let OuterComponent = Component.extend({ myProperty: 'outer-thing', outerSubmit() {}, }); this.registerComponent('inner-component', { ComponentClass: InnerComponent, template: '', }); this.registerComponent('outer-component', { ComponentClass: OuterComponent, template: '{{inner-component submit=(action outerSubmit)}}', }); this.subscribe('interaction.ember-action', { before(name, timestamp, payload) { beforeParameters.push(payload.target.get('myProperty')); }, after(name, timestamp, payload) { afterParameters.push(payload.target.get('myProperty')); }, }); this.render(`{{outer-component}}`); this.runTask(() => { this.$('#instrument-button').trigger('click'); }); this.assert.deepEqual( beforeParameters, ['inner-thing', 'outer-thing'], 'instrumentation subscriber before function was passed target' ); this.assert.deepEqual( afterParameters, ['outer-thing', 'inner-thing'], 'instrumentation subscriber after function was passed target' ); } ['@test instrumented action should return value']() { let returnedValue = 'Chris P is so krispy'; let actualReturnedValue; let InnerComponent = Component.extend({ actions: { fireAction() { actualReturnedValue = this.attrs.submit(); }, }, }); let OuterComponent = Component.extend({ outerSubmit() { return returnedValue; }, }); this.registerComponent('inner-component', { ComponentClass: InnerComponent, template: '', }); this.registerComponent('outer-component', { ComponentClass: OuterComponent, template: '{{inner-component submit=(action outerSubmit)}}', }); this.subscribe('interaction.ember-action', { before() {}, after() {}, }); this.render(`{{outer-component}}`); this.runTask(() => { this.$('#instrument-button').trigger('click'); }); this.assert.equal(actualReturnedValue, returnedValue, 'action can return to caller'); } } ); } moduleFor( 'Helpers test: closure {{action}}', class extends RenderingTest { ['@test action should be called']() { let outerActionCalled = false; let component; let InnerComponent = Component.extend({ init() { this._super(...arguments); component = this; }, fireAction() { this.attrs.submit(); }, }); let OuterComponent = Component.extend({ outerSubmit() { outerActionCalled = true; }, }); this.registerComponent('inner-component', { ComponentClass: InnerComponent, template: 'inner', }); this.registerComponent('outer-component', { ComponentClass: OuterComponent, template: '{{inner-component submit=(action outerSubmit)}}', }); this.render('{{outer-component}}'); this.runTask(() => { component.fireAction(); }); this.assert.ok(outerActionCalled, 'the action was called'); } ['@test an error is triggered when bound action function is undefined']() { this.registerComponent('inner-component', { template: 'inner', }); this.registerComponent('outer-component', { template: '{{inner-component submit=(action somethingThatIsUndefined)}}', }); expectAssertion(() => { this.render('{{outer-component}}'); }, /Action passed is null or undefined in \(action[^)]*\) from .*\./); } ['@test an error is triggered when bound action being passed in is a non-function']() { this.registerComponent('inner-component', { template: 'inner', }); this.registerComponent('outer-component', { ComponentClass: Component.extend({ nonFunctionThing: {}, }), template: '{{inner-component submit=(action nonFunctionThing)}}', }); expectAssertion(() => { this.render('{{outer-component}}'); }, /An action could not be made for `.*` in .*\. Please confirm that you are using either a quoted action name \(i\.e\. `\(action '.*'\)`\) or a function available in .*\./); } ['@test [#12718] a nice error is shown when a bound action function is undefined and it is passed as attrs.foo']() { this.registerComponent('inner-component', { template: '', }); this.registerComponent('outer-component', { template: '{{inner-component}}', }); expectAssertion(() => { this.render('{{outer-component}}'); }, /Action passed is null or undefined in \(action[^)]*\) from .*\./); } ['@test action value is returned']() { let expectedValue = 'terrible tom'; let returnedValue; let innerComponent; let InnerComponent = Component.extend({ init() { this._super(...arguments); innerComponent = this; }, fireAction() { returnedValue = this.attrs.submit(); }, }); let OuterComponent = Component.extend({ outerSubmit() { return expectedValue; }, }); this.registerComponent('inner-component', { ComponentClass: InnerComponent, template: 'inner', }); this.registerComponent('outer-component', { ComponentClass: OuterComponent, template: '{{inner-component submit=(action outerSubmit)}}', }); this.render('{{outer-component}}'); this.runTask(() => { innerComponent.fireAction(); }); this.assert.equal(returnedValue, expectedValue, 'action can return to caller'); } ['@test action should be called on the correct scope']() { let innerComponent; let outerComponent; let actualComponent; let InnerComponent = Component.extend({ init() { this._super(...arguments); innerComponent = this; }, fireAction() { this.attrs.submit(); }, }); let OuterComponent = Component.extend({ init() { this._super(...arguments); outerComponent = this; }, isOuterComponent: true, outerSubmit() { actualComponent = this; }, }); this.registerComponent('inner-component', { ComponentClass: InnerComponent, template: 'inner', }); this.registerComponent('outer-component', { ComponentClass: OuterComponent, template: '{{inner-component submit=(action outerSubmit)}}', }); this.render('{{outer-component}}'); this.runTask(() => { innerComponent.fireAction(); }); this.assert.equal(actualComponent, outerComponent, 'action has the correct context'); this.assert.ok(actualComponent.isOuterComponent, 'action has the correct context'); } ['@test arguments to action are passed, curry']() { let first = 'mitch'; let second = 'martin'; let third = 'matt'; let fourth = 'wacky wycats'; let innerComponent; let actualArgs; let InnerComponent = Component.extend({ init() { this._super(...arguments); innerComponent = this; }, fireAction() { this.attrs.submit(fourth); }, }); let OuterComponent = Component.extend({ third, outerSubmit() { // eslint-disable-line no-unused-vars actualArgs = [...arguments]; }, }); this.registerComponent('inner-component', { ComponentClass: InnerComponent, template: 'inner', }); this.registerComponent('outer-component', { ComponentClass: OuterComponent, template: `{{inner-component submit=(action (action outerSubmit "${first}") "${second}" third)}}`, }); this.render('{{outer-component}}'); this.runTask(() => { innerComponent.fireAction(); }); this.assert.deepEqual( actualArgs, [first, second, third, fourth], 'action has the correct args' ); } ['@test `this` can be passed as an argument']() { let value = {}; let component; let innerComponent; let InnerComponent = Component.extend({ init() { this._super(...arguments); innerComponent = this; }, fireAction() { this.attrs.submit(); }, }); let OuterComponent = Component.extend({ init() { this._super(...arguments); component = this; }, actions: { outerAction(incomingValue) { value = incomingValue; }, }, }); this.registerComponent('inner-component', { ComponentClass: InnerComponent, template: 'inner', }); this.registerComponent('outer-component', { ComponentClass: OuterComponent, template: '{{inner-component submit=(action "outerAction" this)}}', }); this.render('{{outer-component}}'); this.runTask(() => { innerComponent.fireAction(); }); this.assert.strictEqual(value, component, 'the component is passed at `this`'); } ['@test arguments to action are bound']() { let value = 'lazy leah'; let innerComponent; let outerComponent; let actualArg; let InnerComponent = Component.extend({ init() { this._super(...arguments); innerComponent = this; }, fireAction() { this.attrs.submit(); }, }); let OuterComponent = Component.extend({ init() { this._super(...arguments); outerComponent = this; }, value: '', outerSubmit(incomingValue) { actualArg = incomingValue; }, }); this.registerComponent('inner-component', { ComponentClass: InnerComponent, template: 'inner', }); this.registerComponent('outer-component', { ComponentClass: OuterComponent, template: `{{inner-component submit=(action outerSubmit value)}}`, }); this.render('{{outer-component}}'); this.runTask(() => { innerComponent.fireAction(); }); this.assert.strictEqual(actualArg, '', 'action has the correct first arg'); this.runTask(() => { outerComponent.set('value', value); }); this.runTask(() => { innerComponent.fireAction(); }); this.assert.strictEqual(actualArg, value, 'action has the correct first arg'); } ['@test array arguments are passed correctly to action']() { let first = 'foo'; let second = [3, 5]; let third = [4, 9]; let actualFirst; let actualSecond; let actualThird; let innerComponent; let outerComponent; let InnerComponent = Component.extend({ init() { this._super(...arguments); innerComponent = this; }, fireAction() { this.attrs.submit(second, third); }, }); let OuterComponent = Component.extend({ init() { this._super(...arguments); outerComponent = this; }, outerSubmit(incomingFirst, incomingSecond, incomingThird) { actualFirst = incomingFirst; actualSecond = incomingSecond; actualThird = incomingThird; }, }); this.registerComponent('inner-component', { ComponentClass: InnerComponent, template: 'inner', }); this.registerComponent('outer-component', { ComponentClass: OuterComponent, template: `{{inner-component submit=(action outerSubmit first)}}`, }); this.render('{{outer-component}}'); this.runTask(() => { outerComponent.set('first', first); outerComponent.set('second', second); }); this.runTask(() => { innerComponent.fireAction(); }); this.assert.equal(actualFirst, first, 'action has the correct first arg'); this.assert.equal(actualSecond, second, 'action has the correct second arg'); this.assert.equal(actualThird, third, 'action has the correct third arg'); } ['@test mut values can be wrapped in actions, are settable']() { let newValue = 'trollin trek'; let innerComponent; let outerComponent; let InnerComponent = Component.extend({ init() { this._super(...arguments); innerComponent = this; }, fireAction() { this.attrs.submit(newValue); }, }); let OuterComponent = Component.extend({ init() { this._super(...arguments); outerComponent = this; }, outerMut: 'patient peter', }); this.registerComponent('inner-component', { ComponentClass: InnerComponent, template: 'inner', }); this.registerComponent('outer-component', { ComponentClass: OuterComponent, template: `{{inner-component submit=(action (mut outerMut))}}`, }); this.render('{{outer-component}}'); this.runTask(() => { innerComponent.fireAction(); }); this.assert.equal(outerComponent.get('outerMut'), newValue, 'mut value is set'); } ['@test mut values can be wrapped in actions, are settable with a curry']() { let newValue = 'trollin trek'; let innerComponent; let outerComponent; let InnerComponent = Component.extend({ init() { this._super(...arguments); innerComponent = this; }, fireAction() { this.attrs.submit(); }, }); let OuterComponent = Component.extend({ init() { this._super(...arguments); outerComponent = this; }, outerMut: 'patient peter', }); this.registerComponent('inner-component', { ComponentClass: InnerComponent, template: 'inner', }); this.registerComponent('outer-component', { ComponentClass: OuterComponent, template: `{{inner-component submit=(action (mut outerMut) '${newValue}')}}`, }); this.render('{{outer-component}}'); this.runTask(() => { innerComponent.fireAction(); }); this.assert.equal(outerComponent.get('outerMut'), newValue, 'mut value is set'); } ['@test action can create closures over actions']() { let first = 'raging robert'; let second = 'mild machty'; let returnValue = 'butch brian'; let actualFirst; let actualSecond; let actualReturnedValue; let innerComponent; let InnerComponent = Component.extend({ init() { this._super(...arguments); innerComponent = this; }, fireAction() { actualReturnedValue = this.attrs.submit(second); }, }); let OuterComponent = Component.extend({ actions: { outerAction(incomingFirst, incomingSecond) { actualFirst = incomingFirst; actualSecond = incomingSecond; return returnValue; }, }, }); this.registerComponent('inner-component', { ComponentClass: InnerComponent, template: 'inner', }); this.registerComponent('outer-component', { ComponentClass: OuterComponent, template: `{{inner-component submit=(action 'outerAction' '${first}')}}`, }); this.render('{{outer-component}}'); this.runTask(() => { innerComponent.fireAction(); }); this.assert.equal(actualReturnedValue, returnValue, 'return value is present'); this.assert.equal(actualFirst, first, 'first argument is correct'); this.assert.equal(actualSecond, second, 'second argument is correct'); } ['@test provides a helpful error if an action is not present']() { let InnerComponent = Component.extend({}); let OuterComponent = Component.extend({ actions: { something() { // this is present to ensure `actions` hash is present // a different error is triggered if `actions` is missing // completely }, }, }); this.registerComponent('inner-component', { ComponentClass: InnerComponent, template: 'inner', }); this.registerComponent('outer-component', { ComponentClass: OuterComponent, template: `{{inner-component submit=(action 'doesNotExist')}}`, }); expectAssertion(() => { this.render('{{outer-component}}'); }, /An action named 'doesNotExist' was not found in /); } ['@test provides a helpful error if actions hash is not present']() { let InnerComponent = Component.extend({}); let OuterComponent = Component.extend({}); this.registerComponent('inner-component', { ComponentClass: InnerComponent, template: 'inner', }); this.registerComponent('outer-component', { ComponentClass: OuterComponent, template: `{{inner-component submit=(action 'doesNotExist')}}`, }); expectAssertion(() => { this.render('{{outer-component}}'); }, /An action named 'doesNotExist' was not found in /); } ['@test action can create closures over actions with target']() { let innerComponent; let actionCalled = false; let InnerComponent = Component.extend({ init() { this._super(...arguments); innerComponent = this; }, fireAction() { this.attrs.submit(); }, }); let OuterComponent = Component.extend({ otherComponent: computed(function() { return { actions: { outerAction() { actionCalled = true; }, }, }; }), }); this.registerComponent('inner-component', { ComponentClass: InnerComponent, template: 'inner', }); this.registerComponent('outer-component', { ComponentClass: OuterComponent, template: `{{inner-component submit=(action 'outerAction' target=otherComponent)}}`, }); this.render('{{outer-component}}'); this.runTask(() => { innerComponent.fireAction(); }); this.assert.ok(actionCalled, 'action called on otherComponent'); } ['@test value can be used with action over actions']() { let newValue = 'yelping yehuda'; let innerComponent; let actualValue; let InnerComponent = Component.extend({ init() { this._super(...arguments); innerComponent = this; }, fireAction() { this.attrs.submit({ readProp: newValue, }); }, }); let OuterComponent = Component.extend({ outerContent: { readProp: newValue, }, actions: { outerAction(incomingValue) { actualValue = incomingValue; }, }, }); this.registerComponent('inner-component', { ComponentClass: InnerComponent, template: 'inner', }); this.registerComponent('outer-component', { ComponentClass: OuterComponent, template: `{{inner-component submit=(action 'outerAction' value="readProp")}}`, }); this.render('{{outer-component}}'); this.runTask(() => { innerComponent.fireAction(); }); this.assert.equal(actualValue, newValue, 'value is read'); } ['@test action will read the value of a first property']() { let newValue = 'irate igor'; let innerComponent; let actualValue; let InnerComponent = Component.extend({ init() { this._super(...arguments); innerComponent = this; }, fireAction() { this.attrs.submit({ readProp: newValue, }); }, }); let OuterComponent = Component.extend({ outerAction(incomingValue) { actualValue = incomingValue; }, }); this.registerComponent('inner-component', { ComponentClass: InnerComponent, template: 'inner', }); this.registerComponent('outer-component', { ComponentClass: OuterComponent, template: `{{inner-component submit=(action outerAction value="readProp")}}`, }); this.render('{{outer-component}}'); this.runTask(() => { innerComponent.fireAction(); }); this.assert.equal(actualValue, newValue, 'property is read'); } ['@test action will read the value of a curried first argument property']() { let newValue = 'kissing kris'; let innerComponent; let actualValue; let InnerComponent = Component.extend({ init() { this._super(...arguments); innerComponent = this; }, fireAction() { this.attrs.submit(); }, }); let OuterComponent = Component.extend({ objectArgument: { readProp: newValue, }, outerAction(incomingValue) { actualValue = incomingValue; }, }); this.registerComponent('inner-component', { ComponentClass: InnerComponent, template: 'inner', }); this.registerComponent('outer-component', { ComponentClass: OuterComponent, template: `{{inner-component submit=(action outerAction objectArgument value="readProp")}}`, }); this.render('{{outer-component}}'); this.runTask(() => { innerComponent.fireAction(); }); this.assert.equal(actualValue, newValue, 'property is read'); } ['@test action closure does not get auto-mut wrapped'](assert) { let first = 'raging robert'; let second = 'mild machty'; let returnValue = 'butch brian'; let innerComponent; let actualFirst; let actualSecond; let actualReturnedValue; let InnerComponent = Component.extend({ init() { this._super(...arguments); innerComponent = this; }, fireAction() { this.get('submit')(second); this.get('attrs-submit')(second); let attrsSubmitReturnValue = this.attrs['attrs-submit'](second); let submitReturnValue = this.attrs.submit(second); assert.equal( attrsSubmitReturnValue, submitReturnValue, 'both attrs.foo and foo should behave the same' ); return submitReturnValue; }, }); let MiddleComponent = Component.extend({}); let OuterComponent = Component.extend({ actions: { outerAction(incomingFirst, incomingSecond) { actualFirst = incomingFirst; actualSecond = incomingSecond; return returnValue; }, }, }); this.registerComponent('inner-component', { ComponentClass: InnerComponent, template: 'inner', }); this.registerComponent('middle-component', { ComponentClass: MiddleComponent, template: `{{inner-component attrs-submit=attrs.submit submit=submit}}`, }); this.registerComponent('outer-component', { ComponentClass: OuterComponent, template: `{{middle-component submit=(action 'outerAction' '${first}')}}`, }); this.render('{{outer-component}}'); this.runTask(() => { actualReturnedValue = innerComponent.fireAction(); }); this.assert.equal(actualFirst, first, 'first argument is correct'); this.assert.equal(actualSecond, second, 'second argument is correct'); this.assert.equal(actualReturnedValue, returnValue, 'return value is present'); } ['@test action should be called within a run loop']() { let innerComponent; let capturedRunLoop; let InnerComponent = Component.extend({ init() { this._super(...arguments); innerComponent = this; }, fireAction() { this.attrs.submit(); }, }); let OuterComponent = Component.extend({ actions: { submit() { capturedRunLoop = getCurrentRunLoop(); }, }, }); this.registerComponent('inner-component', { ComponentClass: InnerComponent, template: 'inner', }); this.registerComponent('outer-component', { ComponentClass: OuterComponent, template: `{{inner-component submit=(action 'submit')}}`, }); this.render('{{outer-component}}'); this.runTask(() => { innerComponent.fireAction(); }); this.assert.ok(capturedRunLoop, 'action is called within a run loop'); } ['@test objects that define INVOKE can be casted to actions']() { let innerComponent; let actionArgs; let invokableArgs; let InnerComponent = Component.extend({ init() { this._super(...arguments); innerComponent = this; }, fireAction() { actionArgs = this.attrs.submit(4, 5, 6); }, }); let OuterComponent = Component.extend({ foo: 123, submitTask: computed(function() { return { [INVOKE]: (...args) => { invokableArgs = args; return this.foo; }, }; }), }); this.registerComponent('inner-component', { ComponentClass: InnerComponent, template: 'inner', }); this.registerComponent('outer-component', { ComponentClass: OuterComponent, template: `{{inner-component submit=(action submitTask 1 2 3)}}`, }); this.render('{{outer-component}}'); this.runTask(() => { innerComponent.fireAction(); }); this.assert.equal(actionArgs, 123); this.assert.deepEqual(invokableArgs, [1, 2, 3, 4, 5, 6]); } ['@test closure action with `(mut undefinedThing)` works properly [GH#13959]']() { 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 closure actions does not cause component hooks to fire unnecessarily [GH#14305] [GH#14654]']( assert ) { let clicked = 0; let didReceiveAttrsFired = 0; let ClickMeComponent = Component.extend({ tagName: 'button', click() { this.get('onClick').call(undefined, ++clicked); }, didReceiveAttrs() { didReceiveAttrsFired++; }, }); this.registerComponent('click-me', { ComponentClass: ClickMeComponent, }); let outer; let OuterComponent = Component.extend({ clicked: 0, actions: { 'on-click': function() { this.incrementProperty('clicked'); }, }, init() { this._super(); outer = this; this.set('onClick', () => this.incrementProperty('clicked')); }, }); this.registerComponent('outer-component', { ComponentClass: OuterComponent, template: strip`
clicked: {{clicked}}; foo: {{foo}}
{{click-me id="string-action" onClick=(action "on-click")}} {{click-me id="function-action" onClick=(action onClick)}} {{click-me id="mut-action" onClick=(action (mut clicked))}} `, }); this.render('{{outer-component foo=foo}}', { foo: 1 }); this.assertText('clicked: 0; foo: 1'); assert.equal(didReceiveAttrsFired, 3); this.runTask(() => this.rerender()); this.assertText('clicked: 0; foo: 1'); assert.equal(didReceiveAttrsFired, 3); this.runTask(() => set(this.context, 'foo', 2)); this.assertText('clicked: 0; foo: 2'); assert.equal(didReceiveAttrsFired, 3); this.runTask(() => this.$('#string-action').click()); this.assertText('clicked: 1; foo: 2'); assert.equal(didReceiveAttrsFired, 3); this.runTask(() => this.$('#function-action').click()); this.assertText('clicked: 2; foo: 2'); assert.equal(didReceiveAttrsFired, 3); this.runTask(() => set(outer, 'onClick', function() { outer.incrementProperty('clicked'); }) ); this.assertText('clicked: 2; foo: 2'); assert.equal(didReceiveAttrsFired, 3); this.runTask(() => this.$('#function-action').click()); this.assertText('clicked: 3; foo: 2'); assert.equal(didReceiveAttrsFired, 3); this.runTask(() => this.$('#mut-action').click()); this.assertText('clicked: 4; foo: 2'); assert.equal(didReceiveAttrsFired, 3); } } );