import { get, set } from '@ember/-internals/metal'; import { moduleFor, RenderingTest } from '../../utils/test-case'; import { Object as EmberObject, ObjectProxy } from '@ember/-internals/runtime'; import { strip } from '../../utils/abstract-test-case'; import { HAS_NATIVE_SYMBOL } from '@ember/-internals/utils'; import { applyMixins } from '../../utils/abstract-test-case'; import { TogglingSyntaxConditionalsTest, TruthyGenerator, FalsyGenerator, } from '../../utils/shared-conditional-tests'; function EmptyFunction() {} function NonEmptyFunction() {} NonEmptyFunction.foo = 'bar'; class EmptyConstructor {} class NonEmptyConstructor {} NonEmptyConstructor.foo = 'bar'; class TogglingEachInTest extends TogglingSyntaxConditionalsTest { templateFor({ cond, truthy, falsy }) { return `{{#each-in ${cond} as |key|}}${truthy}{{else}}${falsy}{{/each-in}}`; } } class BasicEachInTest extends TogglingEachInTest {} class BasicSyntaxTest extends BasicEachInTest { get truthyValue() { return { 'Not Empty': 1 }; } get falsyValue() { return {}; } } class EachInProxyTest extends TogglingEachInTest {} applyMixins( BasicEachInTest, new TruthyGenerator([ { foo: 1 }, EmberObject.create({ 'Not Empty': 1 }), [1], NonEmptyFunction, NonEmptyConstructor, ]), new FalsyGenerator([ null, undefined, false, '', 0, [], EmptyFunction, EmptyConstructor, {}, Object.create(null), Object.create({}), Object.create({ 'Not Empty': 1 }), EmberObject.create(), ]) ); applyMixins( EachInProxyTest, new TruthyGenerator([ObjectProxy.create({ content: { 'Not empty': 1 } })]), new FalsyGenerator([ ObjectProxy.create(), ObjectProxy.create({ content: null }), ObjectProxy.create({ content: {} }), ObjectProxy.create({ content: Object.create(null) }), ObjectProxy.create({ content: Object.create({}) }), ObjectProxy.create({ content: Object.create({ 'Not Empty': 1 }) }), ObjectProxy.create({ content: EmberObject.create() }), ]) ); // Truthy/Falsy tests moduleFor( 'Syntax test: {{#each-in}} with `ObjectProxy`', class extends EachInProxyTest { get truthyValue() { return ObjectProxy.create({ content: { 'Not Empty': 1 } }); } get falsyValue() { return ObjectProxy.create({ content: null }); } } ); moduleFor('Syntax test: {{#each-in}}', BasicSyntaxTest); // Rendering tests class AbstractEachInTest extends RenderingTest { createHash(/* hash */) { throw new Error('Not implemented: `createHash`'); } makeHash(obj) { let { hash, delegate } = this.createHash(obj); this.hash = hash; this.delegate = delegate; return hash; } replaceHash(hash) { this.runTask(() => set(this.context, 'hash', this.createHash(hash).hash)); } clear() { return this.runTask(() => set(this.context, 'hash', this.createHash({}).hash)); } setProp(key, value) { return this.runTask(() => this.delegate.setProp(this.context, key, value)); } updateNestedValue(key, innerKey, value) { return this.runTask(() => this.delegate.updateNestedValue(this.context, key, innerKey, value)); } render(template, context = {}) { if (this.hash !== undefined) { context.hash = this.hash; } if (this.type !== undefined) { context.type = this.type; } context.secretKey = 'asd'; return super.render(template, context); } } class EachInTest extends AbstractEachInTest { [`@test it repeats the given block for each item in the hash`]() { this.makeHash({ Smartphones: 8203, 'JavaScript Frameworks': Infinity }); this.render( `` ); this.assertText('Smartphones: 8203JavaScript Frameworks: Infinity'); this.assertStableRerender(); if (this.allowsSetProp) { // Not al backing data structures allow kvo tracking. Maps and Iterables don't this.setProp('Tweets', 100); this.assertText('Smartphones: 8203JavaScript Frameworks: InfinityTweets: 100'); } this.clear(); this.assertText('Empty!'); } [`@test it can render sub-paths of each item`](assert) { this.makeHash({ Smartphones: { reports: { unitsSold: 8203 } }, 'JavaScript Frameworks': { reports: { unitsSold: Infinity } }, }); this.render( `` ); this.assertText('Smartphones: 8203JavaScript Frameworks: Infinity'); this.assertStableRerender(); if (this.allowsSetProp) { this.setProp('Tweets', { reports: { unitsSold: 100 } }); this.assertText('Smartphones: 8203JavaScript Frameworks: InfinityTweets: 100'); } this.runTask(() => this.updateNestedValue('Smartphones', 'reports.unitsSold', 8204)); assert.ok(this.textValue().indexOf('Smartphones: 8204') > -1); this.clear(); this.assertText('Empty!'); } [`@test it can render duplicate items`]() { this.makeHash({ Smartphones: 8203, Tablets: 8203, 'JavaScript Frameworks': Infinity, Bugs: Infinity, }); this.render( `` ); this.assertText('Smartphones: 8203Tablets: 8203JavaScript Frameworks: InfinityBugs: Infinity'); this.assertStableRerender(); if (this.allowsSetProp) { this.setProp('Smartphones', 100); this.setProp('Tweets', 443115); this.assertText( 'Smartphones: 100Tablets: 8203JavaScript Frameworks: InfinityBugs: InfinityTweets: 443115' ); } this.clear(); this.assertText(''); } [`@test it repeats the given block when the hash is dynamic`]() { let { hash: categories } = this.createHash({ Smartphones: 8203, 'JavaScript Frameworks': Infinity, }); let { hash: otherCategories } = this.createHash({ Emberinios: 533462, Tweets: 7323, }); let context = { hashes: { categories, otherCategories, type: 'categories', }, }; this.render( ``, context ); this.assertText('Smartphones: 8203JavaScript Frameworks: Infinity'); this.assertStableRerender(); this.runTask(() => set(context, 'hashes.type', 'otherCategories')); this.assertText('Emberinios: 533462Tweets: 7323'); this.runTask(() => set(context, 'hashes.type', 'categories')); this.assertText('Smartphones: 8203JavaScript Frameworks: Infinity'); this.runTask(() => set(context, 'hashes.type', 'nonExistent')); this.clear(); this.assertText('Empty!'); this.runTask(() => set(context, 'hashes.type', 'categories')); this.assertText('Smartphones: 8203JavaScript Frameworks: Infinity'); } ['@test keying off of `undefined` does not render']() { this.makeHash({}); this.render(`{{#each-in hash as |key value|}}{{key}}: {{value.baz}}{{else}}Empty!{{/each-in}}`); this.assertText('Empty!'); this.assertStableRerender(); this.replaceHash({ bar: { baz: 'Here!' } }); this.assertText('bar: Here!'); this.clear(); this.assertText('Empty!'); } [`@test it can render items with a key of empty string`]() { this.makeHash({ '': 'empty-string', a: 'a' }); this.render( `` ); this.assertText(': empty-stringa: a'); this.assertStableRerender(); this.clear(); this.assertText('Empty!'); } } moduleFor( 'Syntax test: {{#each-in}} with POJOs', class extends EachInTest { constructor() { super(...arguments); this.allowsSetProp = true; } createHash(pojo) { return { hash: pojo, delegate: { setProp(context, key, value) { set(context.hash, key, value); }, updateNestedValue(context, key, innerKey, value) { let target = context.hash[key]; set(target, innerKey, value); }, }, }; } [`@test it only iterates over an object's own properties`]() { let protoCategories = { Smartphones: 8203, 'JavaScript Frameworks': Infinity, }; let categories = Object.create(protoCategories); categories['Televisions'] = 183; categories['Alarm Clocks'] = 999; this.render( ``, { categories } ); this.assertText('Televisions: 183Alarm Clocks: 999'); this.assertStableRerender(); this.runTask(() => { set(protoCategories, 'Robots', 666); set(categories, 'Tweets', 443115); }); this.assertText('Televisions: 183Alarm Clocks: 999Tweets: 443115'); categories = Object.create(protoCategories); categories['Televisions'] = 183; categories['Alarm Clocks'] = 999; } [`@test it does not observe direct property mutations (not going through set) on the object`]() { this.render( strip` `, { categories: { Smartphones: 8203, 'JavaScript Frameworks': Infinity, }, } ); this.assertHTML(strip` `); this.assertStableRerender(); this.runTask(() => { let categories = get(this.context, 'categories'); delete categories.Smartphones; }); this.assertInvariants(); this.runTask(() => { let categories = get(this.context, 'categories'); categories['Emberinios'] = 123456; }); this.assertInvariants(); this.runTask(() => { set(this.context, 'categories', { Emberinios: 123456, }); }); this.assertHTML(strip` `); this.runTask(() => { set(this.context, 'categories', { Smartphones: 8203, 'JavaScript Frameworks': Infinity, }); }); this.assertHTML(strip` `); } ['@test it skips holes in sparse arrays']() { let arr = []; arr[5] = 'foo'; arr[6] = 'bar'; this.render( strip` {{#each-in arr as |key value|}} [{{key}}:{{value}}] {{/each-in}}`, { arr } ); this.assertText('[5:foo][6:bar]'); this.assertStableRerender(); } ['@test it iterate over array with `in` instead of walking over elements']() { let arr = [1, 2, 3]; arr.foo = 'bar'; this.render( strip` {{#each-in arr as |key value|}} [{{key}}:{{value}}] {{/each-in}}`, { arr } ); this.assertText('[0:1][1:2][2:3][foo:bar]'); this.runTask(() => this.rerender()); this.assertText('[0:1][1:2][2:3][foo:bar]'); this.runTask(() => { set(arr, 'zomg', 'lol'); }); this.assertText('[0:1][1:2][2:3][foo:bar][zomg:lol]'); arr = [1, 2, 3]; arr.foo = 'bar'; this.runTask(() => set(this.context, 'arr', arr)); this.assertText('[0:1][1:2][2:3][foo:bar]'); } } ); moduleFor( 'Syntax test: {{#each-in}} with EmberObjects', class extends EachInTest { constructor() { super(...arguments); this.allowsSetProp = true; } createHash(pojo) { let hash = EmberObject.create(pojo); return { hash, delegate: { setProp(context, key, value) { set(context, `hash.${key}`, value); }, updateNestedValue(context, key, innerKey, value) { let target = get(context.hash, key); set(target, innerKey, value); }, }, }; } } ); moduleFor( 'Syntax test: {{#each-in}} with object proxies', class extends EachInTest { constructor() { super(...arguments); this.allowsSetProp = true; } createHash(pojo) { let hash = ObjectProxy.create({ content: pojo }); return { hash, delegate: { setProp(context, key, value) { set(context, `hash.${key}`, value); }, updateNestedValue(context, key, innerKey, value) { let target = get(context.hash, key); set(target, innerKey, value); }, }, }; } ['@test it iterates over the content, not the proxy']() { let content = { Smartphones: 8203, 'JavaScript Frameworks': Infinity, }; let proxy = ObjectProxy.create({ content, foo: 'bar', }); this.render( strip` `, { categories: proxy } ); this.assertHTML(strip` `); this.assertStableRerender(); this.runTask(() => { set(proxy, 'content.Smartphones', 100); set(proxy, 'content.Tweets', 443115); }); this.assertHTML(strip` `); this.runTask(() => { set(proxy, 'content', { Smartphones: 100, Tablets: 20, }); }); this.assertHTML(strip` `); this.runTask(() => set( this.context, 'categories', ObjectProxy.create({ content: { Smartphones: 8203, 'JavaScript Frameworks': Infinity, }, }) ) ); this.assertHTML(strip` `); } } ); moduleFor( 'Syntax test: {{#each-in}} with ES6 Maps', class extends EachInTest { createHash(pojo) { let map = new Map(); Object.keys(pojo).forEach(key => { map.set(key, pojo[key]); }); return { hash: map, delegate: { updateNestedValue(context, key, innerKey, value) { let target = context.hash.get(key); set(target, innerKey, value); }, }, }; } [`@test it supports having objects as keys on ES6 Maps`]() { let map = new Map(); map.set({ name: 'one' }, 'foo'); map.set({ name: 'two' }, 'bar'); this.render( strip` `, { map } ); this.assertHTML(strip` `); this.assertStableRerender(); this.runTask(() => { let map = new Map(); map.set({ name: 'three' }, 'qux'); set(this.context, 'map', map); }); this.assertHTML(strip` `); } } ); if (HAS_NATIVE_SYMBOL) { moduleFor( 'Syntax test: {{#each-in}} with custom iterables', class extends EachInTest { createHash(pojo) { let ary = Object.keys(pojo).reduce((accum, key) => { return accum.concat([[key, pojo[key]]]); }, []); let iterable = { [Symbol.iterator]: () => makeIterator(ary), }; return { hash: iterable, delegate: { updateNestedValue(context, key, innerKey, value) { let ary = Array.from(context.hash); let target = ary.find(([k]) => k === key)[1]; set(target, innerKey, value); }, }, }; } } ); } // Utils function makeIterator(ary) { var index = 0; return { next() { return index < ary.length ? { value: ary[index++], done: false } : { done: true }; }, }; }