feat: promise support for drops, working on #65

This commit is contained in:
Jun Yang
2019-03-10 18:05:52 +08:00
parent ad0930f152
commit 4a8088d4e4
25 changed files with 260 additions and 250 deletions
+2 -2
View File
@@ -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/)
})
})
+2 -2
View File
@@ -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 <string>.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 <array>.size notation',
() => test('{% assign my_array = "apples, oranges, peaches, plums" | split: ", " %}{{ my_array.size }}', '4'))
})
describe('slice', function () {
+1 -1
View File
@@ -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)
+23 -2
View File
@@ -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')
})
})
+46 -48
View File
@@ -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])
})
})
})
+101 -121
View File
@@ -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 [<String>] syntax', function () {
expect(scope.propertyAccessSeq('foo["bar"]'))
it('should handle [<String>] syntax', async function () {
expect(await scope.propertyAccessSeq('foo["bar"]'))
.to.deep.equal(['foo', 'bar'])
})
it('should handle [<Identifier>] syntax', function () {
expect(scope.propertyAccessSeq('foo[foo]'))
it('should handle [<Identifier>] 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 [<String>] syntax', function () {
expect(scope.get('bar["zoo"]')).to.equal('coo')
it('should access child property via [<String>] syntax', async function () {
expect(await scope.get('bar["zoo"]')).to.equal('coo')
})
it('should access child property via [<Number>] syntax', function () {
expect(scope.get('bar.arr[0]')).to.equal('a')
it('should access child property via [<Number>] syntax', async function () {
expect(await scope.get('bar.arr[0]')).to.equal('a')
})
it('should access child property via [<Identifier>] syntax', function () {
expect(scope.get('bar[foo]')).to.equal('coo')
it('should access child property via [<Identifier>] 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 = {
+11 -11
View File
@@ -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 () {
+2 -2
View File
@@ -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)
})