diff --git a/src/builtin/tags/assign.ts b/src/builtin/tags/assign.ts index 2eaa2f5777..520b76893f 100644 --- a/src/builtin/tags/assign.ts +++ b/src/builtin/tags/assign.ts @@ -14,10 +14,9 @@ export default { this.key = match[1] this.value = match[2] }, - render: function (scope: Scope) { + render: async function (scope: Scope) { const ctx = new AssignScope() - ctx[this.key] = this.liquid.evalValue(this.value, scope) + ctx[this.key] = await this.liquid.evalValue(this.value, scope) scope.push(ctx) - return Promise.resolve('') } } as ITagImplOptions diff --git a/src/builtin/tags/case.ts b/src/builtin/tags/case.ts index 6310527d39..198a7a13a6 100644 --- a/src/builtin/tags/case.ts +++ b/src/builtin/tags/case.ts @@ -30,11 +30,11 @@ export default { stream.start() }, - render: function (scope: Scope) { + render: async function (scope: Scope) { for (let i = 0; i < this.cases.length; i++) { const branch = this.cases[i] - const val = evalExp(branch.val, scope) - const cond = evalExp(this.cond, scope) + const val = await evalExp(branch.val, scope) + const cond = await evalExp(this.cond, scope) if (val === cond) { return this.liquid.renderer.renderTemplates(branch.templates, scope) } diff --git a/src/builtin/tags/cycle.ts b/src/builtin/tags/cycle.ts index 56df884405..5dd7ba0c86 100644 --- a/src/builtin/tags/cycle.ts +++ b/src/builtin/tags/cycle.ts @@ -24,8 +24,8 @@ export default { assert(this.candidates.length, `empty candidates: ${tagToken.raw}`) }, - render: function (scope: Scope) { - const group = evalValue(this.group, scope) + render: async function (scope: Scope) { + const group = await evalValue(this.group, scope) const fingerprint = `cycle:${group}:` + this.candidates.join(',') const groups = scope.groups let idx = groups[fingerprint] diff --git a/src/builtin/tags/for.ts b/src/builtin/tags/for.ts index 60f5af1e91..d53fedfeac 100644 --- a/src/builtin/tags/for.ts +++ b/src/builtin/tags/for.ts @@ -42,7 +42,7 @@ export default { stream.start() }, render: async function (scope: Scope, hash: Hash) { - let collection = evalExp(this.collection, scope) + let collection = await evalExp(this.collection, scope) if (!isArray(collection)) { if (isString(collection) && collection.length > 0) { diff --git a/src/builtin/tags/if.ts b/src/builtin/tags/if.ts index 49effaf17f..1f4a4beeca 100644 --- a/src/builtin/tags/if.ts +++ b/src/builtin/tags/if.ts @@ -33,9 +33,9 @@ export default { stream.start() }, - render: function (scope: Scope) { + render: async function (scope: Scope) { for (const branch of this.branches) { - const cond = evalExp(branch.cond, scope) + const cond = await evalExp(branch.cond, scope) if (isTruthy(cond)) { return this.liquid.renderer.renderTemplates(branch.templates, scope) } diff --git a/src/builtin/tags/include.ts b/src/builtin/tags/include.ts index fcac6f7aff..7eb4d7a951 100644 --- a/src/builtin/tags/include.ts +++ b/src/builtin/tags/include.ts @@ -34,7 +34,7 @@ export default { const template = this.value.slice(1, -1) filepath = await this.liquid.parseAndRender(template, scope.getAll(), scope.opts) } else { - filepath = evalValue(this.value, scope) + filepath = await evalValue(this.value, scope) } } else { filepath = this.staticValue @@ -47,7 +47,7 @@ export default { scope.blocks = {} scope.blockMode = BlockMode.OUTPUT if (this.with) { - hash[filepath] = evalValue(this.with, scope) + hash[filepath] = await evalValue(this.with, scope) } const templates = await this.liquid.getTemplate(filepath, scope.opts) scope.push(hash) diff --git a/src/builtin/tags/layout.ts b/src/builtin/tags/layout.ts index 4b650cfbdd..592dec4485 100644 --- a/src/builtin/tags/layout.ts +++ b/src/builtin/tags/layout.ts @@ -26,7 +26,7 @@ export default { }, render: async function (scope: Scope, hash: Hash) { const layout = scope.opts.dynamicPartials - ? evalValue(this.layout, scope) + ? await evalValue(this.layout, scope) : this.staticLayout assert(layout, `cannot apply layout with empty filename`) diff --git a/src/builtin/tags/tablerow.ts b/src/builtin/tags/tablerow.ts index 2562b83748..4cf16dc9dc 100644 --- a/src/builtin/tags/tablerow.ts +++ b/src/builtin/tags/tablerow.ts @@ -36,7 +36,7 @@ export default { }, render: async function (scope: Scope, hash: Hash) { - let collection = evalExp(this.collection, scope) || [] + let collection = await evalExp(this.collection, scope) || [] const offset = hash.offset || 0 const limit = (hash.limit === undefined) ? collection.length : hash.limit diff --git a/src/builtin/tags/unless.ts b/src/builtin/tags/unless.ts index c5c45491df..a2a6687c28 100644 --- a/src/builtin/tags/unless.ts +++ b/src/builtin/tags/unless.ts @@ -25,8 +25,8 @@ export default { stream.start() }, - render: function (scope: Scope) { - const cond = evalExp(this.cond, scope) + render: async function (scope: Scope) { + const cond = await evalExp(this.cond, scope) return isFalsy(cond) ? this.liquid.renderer.renderTemplates(this.templates, scope) : this.liquid.renderer.renderTemplates(this.elseTemplates, scope) diff --git a/src/drop/drop.ts b/src/drop/drop.ts index 82c86d1bfa..7015e39a1d 100644 --- a/src/drop/drop.ts +++ b/src/drop/drop.ts @@ -3,7 +3,7 @@ export abstract class Drop { return undefined } - liquidMethodMissing (key: string): string | undefined { + liquidMethodMissing (key: string): Promise | string | undefined { return undefined } } diff --git a/src/render/syntax.ts b/src/render/syntax.ts index b4041b457f..3f9d0f36be 100644 --- a/src/render/syntax.ts +++ b/src/render/syntax.ts @@ -48,7 +48,7 @@ const binaryOperators: {[key: string]: (lhs: any, rhs: any) => boolean} = { 'or': (l: any, r: any) => isTruthy(l) || isTruthy(r) } -export function parseExp (exp: string, scope: Scope): any { +export async function parseExp (exp: string, scope: Scope): Promise { assert(scope, 'unable to parseExp: scope undefined') const operatorREs = lexical.operators let match @@ -56,28 +56,28 @@ export function parseExp (exp: string, scope: Scope): any { const operatorRE = operatorREs[i] const expRE = new RegExp(`^(${lexical.quoteBalanced.source})(${operatorRE.source})(${lexical.quoteBalanced.source})$`) if ((match = exp.match(expRE))) { - const l = parseExp(match[1], scope) + const l = await parseExp(match[1], scope) const op = binaryOperators[match[2].trim()] - const r = parseExp(match[3], scope) + const r = await parseExp(match[3], scope) return op(l, r) } } if ((match = exp.match(lexical.rangeLine))) { - const low = evalValue(match[1], scope) - const high = evalValue(match[2], scope) - return range(low, high + 1) + const low = await evalValue(match[1], scope) + const high = await evalValue(match[2], scope) + return range(+low, +high + 1) } return parseValue(exp, scope) } -export function evalExp (str: string, scope: Scope): any { - const value = parseExp(str, scope) +export async function evalExp (str: string, scope: Scope): Promise { + const value = await parseExp(str, scope) return value instanceof Drop ? value.valueOf() : value } -function parseValue (str: string | undefined, scope: Scope): any { +async function parseValue (str: string | undefined, scope: Scope): Promise { if (!str) return null str = str.trim() @@ -91,8 +91,8 @@ function parseValue (str: string | undefined, scope: Scope): any { return scope.get(str) } -export function evalValue (str: string | undefined, scope: Scope): any { - const value = parseValue(str, scope) +export async function evalValue (str: string | undefined, scope: Scope) { + const value = await parseValue(str, scope) return value instanceof Drop ? value.valueOf() : value } diff --git a/src/scope/context.ts b/src/scope/context.ts new file mode 100644 index 0000000000..7789e0ad42 --- /dev/null +++ b/src/scope/context.ts @@ -0,0 +1,10 @@ +import { Drop } from '../drop/drop' + +type PlainObject = { + [key: string]: any + liquid_method_missing?: (key: string) => any // eslint-disable-line + to_liquid?: () => any // eslint-disable-line + toLiquid?: () => any // eslint-disable-line +} + +export type Context = PlainObject | Drop \ No newline at end of file diff --git a/src/scope/scope.ts b/src/scope/scope.ts index 91a37e1eda..1c00f31426 100644 --- a/src/scope/scope.ts +++ b/src/scope/scope.ts @@ -4,13 +4,7 @@ import { __assign } from 'tslib' import assert from '../util/assert' import { NormalizedFullOptions, applyDefault } from '../liquid-options' import BlockMode from './block-mode' - -export type Context = { - [key: string]: any - liquid_method_missing?: (key: string) => any // eslint-disable-line - to_liquid?: () => any // eslint-disable-line - toLiquid?: () => any // eslint-disable-line -} +import { Context } from './context' export default class Scope { opts: NormalizedFullOptions @@ -25,19 +19,19 @@ export default class Scope { getAll () { return this.contexts.reduce((ctx, val) => __assign(ctx, val), {}) } - get (path: string): any { - const paths = this.propertyAccessSeq(path) - const scope = this.findContextFor(paths[0]) || _.last(this.contexts) - return paths.reduce((value, key) => { - const val = this.readProperty(value, key) - if (_.isNil(val) && this.opts.strictVariables) { - throw new TypeError(`undefined variable: ${key}`) + async get (path: string) { + const paths = await this.propertyAccessSeq(path) + let ctx = this.findContextFor(paths[0]) || _.last(this.contexts) + for (let path of paths) { + ctx = this.readProperty(ctx, path) + if (_.isNil(ctx) && this.opts.strictVariables) { + throw new TypeError(`undefined variable: ${path}`) } - return val - }, scope) + } + return ctx } - set (path: string, v: any): void { - const paths = this.propertyAccessSeq(path) + async set (path: string, v: any) { + const paths = await this.propertyAccessSeq(path) let scope = this.findContextFor(paths[0]) || _.last(this.contexts) paths.some((key, i) => { if (!_.isObject(scope)) { @@ -99,7 +93,7 @@ export default class Scope { * accessSeq("foo['b]r']") // ['foo', 'b]r'] * accessSeq("foo[bar.coo]") // ['foo', 'bar'], for bar.coo == 'bar' */ - propertyAccessSeq (str: string) { + async propertyAccessSeq (str: string) { str = String(str) const seq: string[] = [] let name = '' @@ -122,7 +116,7 @@ export default class Scope { assert(j !== -1, `unbalanced []: ${str}`) name = str.slice(i + 1, j) if (!/^[+-]?\d+$/.test(name)) { // foo[bar] vs. foo[1] - name = String(this.get(name)) + name = String(await this.get(name)) } push() i = j + 1 @@ -152,9 +146,9 @@ export default class Scope { } function readSize (obj: Context) { - if (!_.isNil(obj.size)) return obj.size + if (!_.isNil(obj['size'])) return obj['size'] if (_.isArray(obj) || _.isString(obj)) return obj.length - return obj.size + return obj['size'] } function matchRightBracket (str: string, begin: number) { diff --git a/src/template/filter/filter.ts b/src/template/filter/filter.ts index a06c4f6587..e931415c81 100644 --- a/src/template/filter/filter.ts +++ b/src/template/filter/filter.ts @@ -19,9 +19,13 @@ export class Filter { this.impl = impl || (x => x) this.args = args } - render (value: any, scope: Scope): any { - const args = this.args.map(arg => isArray(arg) ? [arg[0], evalValue(arg[1], scope)] : evalValue(arg, scope)) - return this.impl.apply(null, [value, ...args]) + async render (value: any, scope: Scope) { + const argv: any[] = [] + for(let arg of this.args) { + if (isArray(arg)) argv.push([arg[0], await evalValue(arg[1], scope)]) + else argv.push(await evalValue(arg, scope)) + } + return this.impl.apply(null, [value, ...argv]) } static register (name: string, filter: FilterImpl) { Filter.impls[name] = filter diff --git a/src/template/tag/hash.ts b/src/template/tag/hash.ts index 66a587e940..a95d29aae4 100644 --- a/src/template/tag/hash.ts +++ b/src/template/tag/hash.ts @@ -10,13 +10,15 @@ import Scope from '../../scope/scope' */ export default class Hash { [key: string]: any - constructor (markup: string, scope: Scope) { + static async create (markup: string, scope: Scope) { + const instance = new Hash() let match hashCapture.lastIndex = 0 while ((match = hashCapture.exec(markup))) { const k = match[1] const v = match[2] - this[k] = evalValue(v, scope) + instance[k] = await evalValue(v, scope) } + return instance } } diff --git a/src/template/tag/tag.ts b/src/template/tag/tag.ts index f5f8908d28..e6e79870f0 100644 --- a/src/template/tag/tag.ts +++ b/src/template/tag/tag.ts @@ -28,7 +28,7 @@ export default class Tag extends Template implements ITemplate { } } async render (scope: Scope) { - const hash = new Hash(this.token.args, scope) + const hash = await Hash.create(this.token.args, scope) const impl = this.impl if (typeof impl.render !== 'function') { return '' diff --git a/src/template/value.ts b/src/template/value.ts index a590e4631b..23418136d7 100644 --- a/src/template/value.ts +++ b/src/template/value.ts @@ -47,10 +47,12 @@ export default class Value { } this.filters.push(new Filter(name, args, this.strictFilters)) } - value (scope: Scope) { - return this.filters.reduce( - (prev, filter) => filter.render(prev, scope), - evalExp(this.initial, scope)) + async value (scope: Scope) { + let val = await evalExp(this.initial, scope) + for (let filter of this.filters) { + val = await filter.render(val, scope) + } + return val } static tokenize (str: string): Array<'|' | ',' | ':' | string> { const tokens = [] diff --git a/test/e2e/eval-value.ts b/test/e2e/eval-value.ts index cba0f539c9..4727cd0cbb 100644 --- a/test/e2e/eval-value.ts +++ b/test/e2e/eval-value.ts @@ -5,7 +5,7 @@ describe('.evalValue()', function () { var engine: Liquid beforeEach(() => { engine = new Liquid() }) - it('should throw when scope undefined', function () { - expect(() => engine.evalValue('{{"foo"}}', null as any)).to.throw(/scope undefined/) + it('should throw when scope undefined', async function () { + return expect(engine.evalValue('{{"foo"}}', null as any)).to.be.rejectedWith(/scope undefined/) }) }) diff --git a/test/integration/builtin/filters/array.ts b/test/integration/builtin/filters/array.ts index a062e073ef..5d2c2ba5a0 100644 --- a/test/integration/builtin/filters/array.ts +++ b/test/integration/builtin/filters/array.ts @@ -33,9 +33,9 @@ describe('filters/array', function () { ' | split: ", " %}{{ my_array | size }}', '4') }) - it('should also be used with dot notation - string', + it('should be respected with .size notation', () => test('{% assign my_string = "Ground control to Major Tom." %}{{ my_string.size }}', '28')) - it('should also be used with dot notation - array', + it('should be respected with .size notation', () => test('{% assign my_array = "apples, oranges, peaches, plums" | split: ", " %}{{ my_array.size }}', '4')) }) describe('slice', function () { diff --git a/test/integration/builtin/tags/for.ts b/test/integration/builtin/tags/for.ts index a5b3921cd3..2861915d9b 100644 --- a/test/integration/builtin/tags/for.ts +++ b/test/integration/builtin/tags/for.ts @@ -1,7 +1,7 @@ import Liquid from '../../../../src/liquid' import { expect, use } from 'chai' import * as chaiAsPromised from 'chai-as-promised' -import { Context } from '../../../../src/scope/scope' +import { Context } from '../../../../src/scope/context' use(chaiAsPromised) diff --git a/test/integration/drop/drop.ts b/test/integration/drop/drop.ts index 8510bf8e83..3ac1674b81 100644 --- a/test/integration/drop/drop.ts +++ b/test/integration/drop/drop.ts @@ -8,7 +8,7 @@ describe('drop/drop', function () { class CustomDrop extends Liquid.Types.Drop { name: string = 'NAME' getName () { - return 'GETNAME' + return 'GET NAME' } } class CustomDropWithMethodMissing extends CustomDrop { @@ -16,9 +16,18 @@ describe('drop/drop', function () { return key.toUpperCase() } } + class PromiseDrop extends Liquid.Types.Drop { + name = Promise.resolve('NAME') + async getName () { + return 'GET NAME' + } + async liquidMethodMissing (key: string) { + return key.toUpperCase() + } + } it('should call corresponding method', async function () { const html = await liquid.parseAndRender(`{{obj.getName}}`, { obj: new CustomDrop() }) - expect(html).to.equal('GETNAME') + expect(html).to.equal('GET NAME') }) it('should read corresponding property', async function () { const html = await liquid.parseAndRender(`{{obj.name}}`, { obj: new CustomDrop() }) @@ -32,4 +41,16 @@ describe('drop/drop', function () { const html = await liquid.parseAndRender(`{{obj.foo}}`, { obj: new CustomDropWithMethodMissing() }) expect(html).to.equal('FOO') }) + it('should call corresponding promise method', async function () { + const html = await liquid.parseAndRender(`{{obj.getName}}`, { obj: new PromiseDrop() }) + expect(html).to.equal('GET NAME') + }) + it('should read corresponding promise property', async function () { + const html = await liquid.parseAndRender(`{{obj.name}}`, { obj: new PromiseDrop() }) + expect(html).to.equal('NAME') + }) + it('should support promise returned by liquidMethodMissing', async function () { + const html = await liquid.parseAndRender(`{{obj.foo}}`, { obj: new PromiseDrop() }) + expect(html).to.equal('FOO') + }) }) diff --git a/test/unit/render/syntax.ts b/test/unit/render/syntax.ts index 5621c3e0c8..486bf1869f 100644 --- a/test/unit/render/syntax.ts +++ b/test/unit/render/syntax.ts @@ -18,35 +18,35 @@ describe('render/syntax', function () { }) describe('.evalValue()', function () { - it('should eval boolean literal', function () { - expect(evalValue('true', scope)).to.equal(true) - expect(evalValue('TrUE', scope)).to.equal(undefined) - expect(evalValue('false', scope)).to.equal(false) + it('should eval boolean literal', async function () { + expect(await evalValue('true', scope)).to.equal(true) + expect(await evalValue('TrUE', scope)).to.equal(undefined) + expect(await evalValue('false', scope)).to.equal(false) }) - it('should eval number literal', function () { - expect(evalValue('2.3', scope)).to.equal(2.3) - expect(evalValue('.32', scope)).to.equal(0.32) - expect(evalValue('-23.', scope)).to.equal(-23) - expect(evalValue('23', scope)).to.equal(23) + it('should eval number literal', async function () { + expect(await evalValue('2.3', scope)).to.equal(2.3) + expect(await evalValue('.32', scope)).to.equal(0.32) + expect(await evalValue('-23.', scope)).to.equal(-23) + expect(await evalValue('23', scope)).to.equal(23) }) - it('should eval string literal', function () { - expect(evalValue('"ab\'c"', scope)).to.equal("ab'c") - expect(evalValue("'ab\"c'", scope)).to.equal('ab"c') + it('should eval string literal', async function () { + expect(await evalValue('"ab\'c"', scope)).to.equal("ab'c") + expect(await evalValue("'ab\"c'", scope)).to.equal('ab"c') }) - it('should eval nil literal', function () { - expect(evalValue('nil', scope)).to.be.null + it('should eval nil literal', async function () { + expect(await evalValue('nil', scope)).to.be.null }) - it('should eval null literal', function () { - expect(evalValue('null', scope)).to.be.null + it('should eval null literal', async function () { + expect(await evalValue('null', scope)).to.be.null }) - it('should eval scope variables', function () { - expect(evalValue('one', scope)).to.equal(1) - expect(evalValue('has_value?', scope)).to.equal(true) - expect(evalValue('x', scope)).to.equal('XXX') + it('should eval scope variables', async function () { + expect(await evalValue('one', scope)).to.equal(1) + expect(await evalValue('has_value?', scope)).to.equal(true) + expect(await evalValue('x', scope)).to.equal('XXX') }) }) - describe('.isTruthy()', function () { + describe('.isTruthy()', async function () { // Spec: https://shopify.github.io/liquid/basics/truthy-and-falsy/ expect(isTruthy(true)).to.be.true expect(isTruthy(false)).to.be.false @@ -61,44 +61,42 @@ describe('render/syntax', function () { }) describe('.evalExp()', function () { - it('should throw when scope undefined', function () { - expect(function () { - (evalExp as any)('') - }).to.throw(/scope undefined/) + it('should throw when scope undefined', async function () { + return expect((evalExp as any)('')).to.be.rejectedWith(/scope undefined/) }) - it('should eval simple expression', function () { - expect(evalExp('1<2', scope)).to.equal(true) - expect(evalExp('2<=2', scope)).to.equal(true) - expect(evalExp('one<=two', scope)).to.equal(true) - expect(evalExp('x contains "x"', scope)).to.equal(false) - expect(evalExp('x contains "X"', scope)).to.equal(true) - expect(evalExp('1 contains "x"', scope)).to.equal(false) - expect(evalExp('y contains "x"', scope)).to.equal(false) - expect(evalExp('z contains "x"', scope)).to.equal(false) - expect(evalExp('(1..5) contains 3', scope)).to.equal(true) - expect(evalExp('(1..5) contains 6', scope)).to.equal(false) - expect(evalExp('"<=" == "<="', scope)).to.equal(true) + it('should eval simple expression', async function () { + expect(await evalExp('1<2', scope)).to.equal(true) + expect(await evalExp('2<=2', scope)).to.equal(true) + expect(await evalExp('one<=two', scope)).to.equal(true) + expect(await evalExp('x contains "x"', scope)).to.equal(false) + expect(await evalExp('x contains "X"', scope)).to.equal(true) + expect(await evalExp('1 contains "x"', scope)).to.equal(false) + expect(await evalExp('y contains "x"', scope)).to.equal(false) + expect(await evalExp('z contains "x"', scope)).to.equal(false) + expect(await evalExp('(1..5) contains 3', scope)).to.equal(true) + expect(await evalExp('(1..5) contains 6', scope)).to.equal(false) + expect(await evalExp('"<=" == "<="', scope)).to.equal(true) }) describe('complex expression', function () { - it('should support value or value', function () { - expect(evalExp('false or true', scope)).to.equal(true) + it('should support value or value', async function () { + expect(await evalExp('false or true', scope)).to.equal(true) }) - it('should support < and contains', function () { - expect(evalExp('1<2 and x contains "x"', scope)).to.equal(false) + it('should support < and contains', async function () { + expect(await evalExp('1<2 and x contains "x"', scope)).to.equal(false) }) - it('should support < or contains', function () { - expect(evalExp('1<2 or x contains "x"', scope)).to.equal(true) + it('should support < or contains', async function () { + expect(await evalExp('1<2 or x contains "x"', scope)).to.equal(true) }) - it('should support value and !=', function () { - expect(evalExp('empty and empty != ""', scope)).to.equal(false) + it('should support value and !=', async function () { + expect(await evalExp('empty and empty != ""', scope)).to.equal(false) }) }) - it('should eval range expression', function () { - expect(evalExp('(2..4)', scope)).to.deep.equal([2, 3, 4]) - expect(evalExp('(two..4)', scope)).to.deep.equal([2, 3, 4]) + it('should eval range expression', async function () { + expect(await evalExp('(2..4)', scope)).to.deep.equal([2, 3, 4]) + expect(await evalExp('(two..4)', scope)).to.deep.equal([2, 3, 4]) }) }) }) diff --git a/test/unit/scope/scope.ts b/test/unit/scope/scope.ts index dccd4781d1..70cc2ee2d9 100644 --- a/test/unit/scope/scope.ts +++ b/test/unit/scope/scope.ts @@ -1,5 +1,6 @@ import * as chai from 'chai' -import Scope, { Context } from '../../../src/scope/scope' +import Scope from '../../../src/scope/scope' +import { Context } from '../../../src/scope/context' const expect = chai.expect @@ -20,168 +21,156 @@ describe('scope', function () { }) describe('#propertyAccessSeq()', function () { - it('should handle dot syntax', function () { - expect(scope.propertyAccessSeq('foo.bar')) + it('should handle dot syntax', async function () { + expect(await scope.propertyAccessSeq('foo.bar')) .to.deep.equal(['foo', 'bar']) }) - it('should handle [] syntax', function () { - expect(scope.propertyAccessSeq('foo["bar"]')) + it('should handle [] syntax', async function () { + expect(await scope.propertyAccessSeq('foo["bar"]')) .to.deep.equal(['foo', 'bar']) }) - it('should handle [] syntax', function () { - expect(scope.propertyAccessSeq('foo[foo]')) + it('should handle [] syntax', async function () { + expect(await scope.propertyAccessSeq('foo[foo]')) .to.deep.equal(['foo', 'zoo']) }) - it('should handle nested access 1', function () { - expect(scope.propertyAccessSeq('foo[bar.zoo]')) + it('should handle nested access 1', async function () { + expect(await scope.propertyAccessSeq('foo[bar.zoo]')) .to.deep.equal(['foo', 'coo']) }) - it('should handle nested access 2', function () { - expect(scope.propertyAccessSeq('foo[bar["zoo"]]')) + it('should handle nested access 2', async function () { + expect(await scope.propertyAccessSeq('foo[bar["zoo"]]')) .to.deep.equal(['foo', 'coo']) }) - it('should handle nested access 3', function () { - expect(scope.propertyAccessSeq('bar["foo"].zoo')) + it('should handle nested access 3', async function () { + expect(await scope.propertyAccessSeq('bar["foo"].zoo')) .to.deep.equal(['bar', 'foo', 'zoo']) }) - it('should handle nested access 4', function () { - expect(scope.propertyAccessSeq('foo[0].bar')) + it('should handle nested access 4', async function () { + expect(await scope.propertyAccessSeq('foo[0].bar')) .to.deep.equal(['foo', '0', 'bar']) }) - it('should handle nested access 5', function () { - expect(scope.propertyAccessSeq('foo[one].bar')) + it('should handle nested access 5', async function () { + expect(await scope.propertyAccessSeq('foo[one].bar')) .to.deep.equal(['foo', '1', 'bar']) }) - it('should handle nested access 6', function () { - expect(scope.propertyAccessSeq('foo[two].bar')) + it('should handle nested access 6', async function () { + expect(await scope.propertyAccessSeq('foo[two].bar')) .to.deep.equal(['foo', 'undefined', 'bar']) }) }) describe('#get()', function () { - it('should get direct property', function () { - expect(scope.get('foo')).equal('zoo') + it('should get direct property', async function () { + expect(await await scope.get('foo')).equal('zoo') }) - it('undefined property should yield undefined', function () { - function fn () { - scope.get('notdefined') - } - expect(fn).to.not.throw() - expect(scope.get('notdefined')).to.equal(undefined) - expect(scope.get(false as any)).to.equal(undefined) + it('undefined property should yield undefined', async function () { + expect(scope.get('notdefined')).to.be.rejected + expect(await scope.get('notdefined')).to.equal(undefined) + expect(await scope.get(false as any)).to.equal(undefined) }) - it('should throw for invalid path', function () { - function fn () { - scope.get('') - } - expect(fn).to.throw('invalid path:""') + it('should throw for invalid path', async function () { + expect(scope.get('')).to.be.rejectedWith('invalid path:""') }) - it('should throw when [] unbalanced', function () { - expect(function () { - scope.get('foo[bar') - }).to.throw(/unbalanced \[\]/) + it('should throw when [] unbalanced', async function () { + expect(scope.get('foo[bar')).to.be.rejectedWith(/unbalanced \[\]/) }) - it('should throw when "" unbalanced', function () { - expect(function () { - scope.get('foo["bar]') - }).to.throw(/unbalanced "/) + it('should throw when "" unbalanced', async function () { + expect(scope.get('foo["bar]')).to.be.rejectedWith(/unbalanced "/) }) - it("should throw when '' unbalanced", function () { - expect(function () { - scope.get("foo['bar]") - }).to.throw(/unbalanced '/) + it("should throw when '' unbalanced", async function () { + expect(scope.get("foo['bar]")).to.be.rejectedWith(/unbalanced '/) }) - it('should respect to to_liquid', function () { + it('should respect to to_liquid', async function () { const scope = new Scope({ foo: { to_liquid: () => ({ bar: 'BAR' }), bar: 'bar' } }) - expect(scope.get('foo.bar')).to.equal('BAR') + expect(await scope.get('foo.bar')).to.equal('BAR') }) - it('should respect to toLiquid', function () { + it('should respect to toLiquid', async function () { const scope = new Scope({ foo: { toLiquid: () => ({ bar: 'BAR' }), bar: 'bar' } }) - expect(scope.get('foo.bar')).to.equal('BAR') + expect(await scope.get('foo.bar')).to.equal('BAR') }) - it('should access child property via dot syntax', function () { - expect(scope.get('bar.zoo')).to.equal('coo') - expect(scope.get('bar.arr')).to.deep.equal(['a', 'b']) + it('should access child property via dot syntax', async function () { + expect(await scope.get('bar.zoo')).to.equal('coo') + expect(await scope.get('bar.arr')).to.deep.equal(['a', 'b']) }) - it('should access child property via [] syntax', function () { - expect(scope.get('bar["zoo"]')).to.equal('coo') + it('should access child property via [] syntax', async function () { + expect(await scope.get('bar["zoo"]')).to.equal('coo') }) - it('should access child property via [] syntax', function () { - expect(scope.get('bar.arr[0]')).to.equal('a') + it('should access child property via [] syntax', async function () { + expect(await scope.get('bar.arr[0]')).to.equal('a') }) - it('should access child property via [] syntax', function () { - expect(scope.get('bar[foo]')).to.equal('coo') + it('should access child property via [] syntax', async function () { + expect(await scope.get('bar[foo]')).to.equal('coo') }) - it('should return undefined when not exist', function () { - expect(scope.get('foo.foo.foo')).to.be.undefined + it('should return undefined when not exist', async function () { + expect(await scope.get('foo.foo.foo')).to.be.undefined }) - it('should return string length as size', function () { - expect(scope.get('foo.size')).to.equal(3) + it('should return string length as size', async function () { + expect(await scope.get('foo.size')).to.equal(3) }) - it('should return array length as size', function () { - expect(scope.get('bar.arr.size')).to.equal(2) + it('should return array length as size', async function () { + expect(await scope.get('bar.arr.size')).to.equal(2) }) - it('should return size property if exists', function () { - expect(scope.get('zoo.size')).to.equal(4) + it('should return size property if exists', async function () { + expect(await scope.get('zoo.size')).to.equal(4) }) - it('should return undefined if do not have size and length', function () { - expect(scope.get('one.size')).to.equal(undefined) + it('should return undefined if do not have size and length', async function () { + expect(await scope.get('one.size')).to.equal(undefined) }) }) describe('#set', function () { - it('should set nested value', function () { - scope.set('posts', { + it('should set nested value', async function () { + await scope.set('posts', { 'first': { 'name': 'A Nice Day' } }) - scope.set('category', { + await scope.set('category', { 'diary': ['first'] }) - expect(scope.get('posts[category.diary[0]].name'), 'A Nice Day') + expect(await scope.get('posts[category.diary[0]].name'), 'A Nice Day') }) - it('should create parent if needed', function () { - scope.set('a.b.c.d', 'COO') - expect(scope.get('a.b.c.d')).to.equal('COO') + it('should create parent if needed', async function () { + await scope.set('a.b.c.d', 'COO') + expect(await scope.get('a.b.c.d')).to.equal('COO') }) - it('should keep other properties of parent', function () { + it('should keep other properties of parent', async function () { scope.push({ obj: { foo: 'FOO' } }) - scope.set('obj.bar', 'BAR') - expect(scope.get('obj.foo')).to.equal('FOO') + await scope.set('obj.bar', 'BAR') + expect(await scope.get('obj.foo')).to.equal('FOO') }) - it('should abort if property cannot be set', function () { + it('should abort if property cannot be set', async function () { scope.push({ obj: { foo: 'FOO' } }) - scope.set('obj.foo.bar', 'BAR') - expect(scope.get('obj.foo')).to.equal('FOO') + await scope.set('obj.foo.bar', 'BAR') + expect(await scope.get('obj.foo')).to.equal('FOO') }) - it("should set parents' corresponding value", function () { + it("should set parents' corresponding value", async function () { scope.push({}) - scope.set('foo', 'bar') + await scope.set('foo', 'bar') scope.pop() - expect(scope.get('foo')).to.equal('bar') + expect(await scope.get('foo')).to.equal('bar') }) }) - describe('strictVariables', function () { + describe('strictVariables', async function () { let scope: Scope beforeEach(function () { scope = new Scope(ctx, { @@ -189,66 +178,57 @@ describe('scope', function () { } as any) }) it('should throw when variable not defined', function () { - function fn () { - scope.get('notdefined') - } - expect(fn).to.throw(/undefined variable: notdefined/) + return expect(scope.get('notdefined')).to.be.rejectedWith(/undefined variable: notdefined/) }) - it('should throw when deep variable not exist', function () { - scope.set('foo', 'FOO') - function fn () { - scope.get('foo.bar.not.defined') - } - expect(fn).to.throw(/undefined variable: bar/) + it('should throw when deep variable not exist', async function () { + await scope.set('foo', 'FOO') + return expect(scope.get('foo.bar.not.defined')).to.be.rejectedWith(/undefined variable: bar/) }) - it('should throw when itself not defined', function () { - scope.set('foo', 'bar') - function fn () { - scope.get('foo.BAR') - } - expect(fn).to.throw(/undefined variable: BAR/) + it('should throw when itself not defined', async function () { + await scope.set('foo', 'bar') + return expect(scope.get('foo.BAR')).to.be.rejectedWith(/undefined variable: BAR/) }) - it('should find variable in parent scope', function () { - scope.set('foo', 'foo') + it('should find variable in parent scope', async function () { + await scope.set('foo', 'foo') scope.push({ 'bar': 'bar' }) - expect(scope.get('foo')).to.equal('foo') + expect(await scope.get('foo')).to.equal('foo') }) }) describe('.getAll()', function () { - it('should get all properties when arguments empty', function () { - expect(scope.getAll()).deep.equal(ctx) + it('should get all properties when arguments empty', async function () { + expect(await scope.getAll()).deep.equal(ctx) }) }) describe('.push()', function () { - it('should push scope', function () { - scope.set('bar', 'bar') + it('should push scope', async function () { + await scope.set('bar', 'bar') scope.push({ foo: 'foo' }) - expect(scope.get('foo')).to.equal('foo') - expect(scope.get('bar')).to.equal('bar') + expect(await scope.get('foo')).to.equal('foo') + expect(await scope.get('bar')).to.equal('bar') }) - it('should hide deep properties by push', function () { - scope.set('bar', { bar: 'bar' }) + it('should hide deep properties by push', async function () { + await scope.set('bar', { bar: 'bar' }) scope.push({ bar: { foo: 'foo' } }) - expect(scope.get('bar.foo')).to.equal('foo') - expect(scope.get('bar.bar')).to.equal(undefined) + expect(await scope.get('bar.foo')).to.equal('foo') + expect(await scope.get('bar.bar')).to.equal(undefined) }) }) describe('.pop()', function () { - it('should pop scope', function () { + it('should pop scope', async function () { scope.push({ foo: 'foo' }) scope.pop() - expect(scope.get('foo')).to.equal('zoo') + expect(await scope.get('foo')).to.equal('zoo') }) }) - it('should pop specified scope', function () { + it('should pop specified scope', async function () { const scope1 = { foo: 'foo' } @@ -257,11 +237,11 @@ describe('scope', function () { } scope.push(scope1) scope.push(scope2) - expect(scope.get('foo')).to.equal('foo') - expect(scope.get('bar')).to.equal('bar') + expect(await scope.get('foo')).to.equal('foo') + expect(await scope.get('bar')).to.equal('bar') scope.pop(scope1) - expect(scope.get('foo')).to.equal('zoo') - expect(scope.get('bar')).to.equal('bar') + expect(await scope.get('foo')).to.equal('zoo') + expect(await scope.get('bar')).to.equal('bar') }) it('should throw when specified scope not found', function () { const scope1 = { diff --git a/test/unit/template/filter/filter.ts b/test/unit/template/filter/filter.ts index 86dab239a1..7293313bc1 100644 --- a/test/unit/template/filter/filter.ts +++ b/test/unit/template/filter/filter.ts @@ -13,34 +13,34 @@ describe('filter', function () { Filter.clear() scope = new Scope() }) - it('should create default filter if not registered', function () { + it('should create default filter if not registered', async function () { const result = new Filter('foo', [], false) expect(result.name).to.equal('foo') }) - it('should render input if filter not registered', function () { - expect(new Filter('undefined', [], false).render('foo', scope)).to.equal('foo') + it('should render input if filter not registered', async function () { + expect(await new Filter('undefined', [], false).render('foo', scope)).to.equal('foo') }) - it('should call filter impl with corrct arguments', function () { + it('should call filter impl with corrct arguments', async function () { const spy = sinon.spy() Filter.register('foo', spy) - new Filter('foo', ['33'], false).render('foo', scope) + await new Filter('foo', ['33'], false).render('foo', scope) expect(spy).to.have.been.calledWith('foo', 33) }) - it('should render a simple filter', function () { + it('should render a simple filter', async function () { Filter.register('upcase', x => x.toUpperCase()) - expect(new Filter('upcase', [], false).render('foo', scope)).to.equal('FOO') + expect(await new Filter('upcase', [], false).render('foo', scope)).to.equal('FOO') }) - it('should render filters with argument', function () { + it('should render filters with argument', async function () { Filter.register('add', (a, b) => a + b) - expect(new Filter('add', ['2'], false).render(3, scope)).to.equal(5) + expect(await new Filter('add', ['2'], false).render(3, scope)).to.equal(5) }) - it('should render filters with multiple arguments', function () { + it('should render filters with multiple arguments', async function () { Filter.register('add', (a, b, c) => a + b + c) - expect(new Filter('add', ['2', '"c"'], false).render(3, scope)).to.equal('5c') + expect(await new Filter('add', ['2', '"c"'], false).render(3, scope)).to.equal('5c') }) it('should not throw when filter name illegal', function () { diff --git a/test/unit/template/value.ts b/test/unit/template/value.ts index 2753dff797..7608325793 100644 --- a/test/unit/template/value.ts +++ b/test/unit/template/value.ts @@ -99,7 +99,7 @@ describe('Value', function () { }) describe('#value()', function () { - it('should call chained filters correctly', function () { + it('should call chained filters correctly', async function () { const date = sinon.stub().returns('y') const time = sinon.spy() Filter.register('date', date) @@ -108,7 +108,7 @@ describe('Value', function () { const scope = new Scope({ foo: { bar: 'bar' } }) - tpl.value(scope) + await tpl.value(scope) expect(date).to.have.been.calledWith('bar', 'b') expect(time).to.have.been.calledWith('y', 2) })