chore: migrate test cases from Chai to Jest

This commit is contained in:
Harttle
2023-03-20 00:41:06 +08:00
committed by Jun Yang
parent dccb90c591
commit c6cde9cd10
97 changed files with 8163 additions and 26440 deletions
+124
View File
@@ -0,0 +1,124 @@
import * as fs from './fs-impl-browser'
import * as sinon from 'sinon'
import { JSDOM } from 'jsdom'
describe('fs/browser', function () {
if (+(process.version.match(/^v(\d+)/) as RegExpMatchArray)[1] < 8) {
console.info('jsdom not supported, skipping template-browser...')
return
}
beforeEach(function () {
const dom = new JSDOM(``, {
url: 'https://example.com/foo/bar/',
contentType: 'text/html',
includeNodeLocations: true
});
(global as any).document = dom.window.document
})
afterEach(function () {
delete (global as any).document
})
describe('#resolve()', function () {
it('should support relative root', function () {
expect(fs.resolve('./views/', 'foo', '')).toBe('https://example.com/foo/bar/views/foo')
})
it('should treat root as directory', function () {
expect(fs.resolve('./views', 'foo', '')).toBe('https://example.com/foo/bar/views/foo')
})
it('should support absolute root', function () {
expect(fs.resolve('/views', 'foo', '')).toBe('https://example.com/views/foo')
})
it('should support empty root', function () {
expect(fs.resolve('', 'page.html', '')).toBe('https://example.com/foo/bar/page.html')
})
it('should support full url as root', function () {
expect(fs.resolve('https://example.com/views/', 'page.html', '')).toBe('https://example.com/views/page.html')
})
it('should add extname when absent', function () {
expect(fs.resolve('https://example.com/views/', 'page', '.html')).toBe('https://example.com/views/page.html')
})
it('should add extname for urls have searchParams', function () {
expect(fs.resolve('https://example.com/views/', 'page?foo=bar', '.html')).toBe('https://example.com/views/page.html?foo=bar')
})
it('should not add extname when full url is given', function () {
expect(fs.resolve('https://example.com/views/', 'https://google.com/page.php', '.html')).toBe('https://google.com/page.php')
})
it('should not add extname when already have one', function () {
expect(fs.resolve('https://example.com/views/', 'page.php', '.html')).toBe('https://example.com/views/page.php')
})
})
describe('#dirname()', () => {
it('should return dirname of file', async function () {
const val = fs.dirname('https://example.com/views/foo/bar')
expect(val).toBe('https://example.com/views/foo/')
})
})
describe('#exists()', () => {
it('should always return true', async function () {
const val = await fs.exists('/foo/bar')
expect(val).toBe(true)
})
})
describe('#existsSync()', () => {
it('should always return true', function () {
expect(fs.existsSync('/foo/bar')).toBe(true)
})
})
describe('#readFile()', () => {
let server: sinon.SinonFakeServer
beforeEach(() => {
server = sinon.fakeServer.create()
server.autoRespond = true
server.respondWith('GET', 'https://example.com/views/hello.html',
[200, { 'Content-Type': 'text/plain' }, 'hello {{name}}']);
(global as any).XMLHttpRequest = sinon.useFakeXMLHttpRequest()
})
afterEach(() => {
server.restore()
delete (global as any).XMLHttpRequest
})
it('should get corresponding text', async function () {
const html = await fs.readFile('https://example.com/views/hello.html')
return expect(html).toBe('hello {{name}}')
})
it('should throw 404', () => {
return expect(fs.readFile('https://example.com/not/exist.html'))
.rejects.toHaveProperty('message', 'Not Found')
})
it('should throw error', function () {
const result = expect(fs.readFile('https://example.com/views/hello.html'))
.rejects.toHaveProperty('message', 'An error occurred whilst receiving the response.')
server.requests[0].error()
return result
})
})
describe('#readFileSync()', () => {
let server: sinon.SinonFakeServer
beforeEach(() => {
server = sinon.fakeServer.create()
server.autoRespond = true
server.respondWith(
'GET', 'https://example.com/views/hello.html',
[200, { 'Content-Type': 'text/plain' }, 'hello {{name}}']
);
(global as any).XMLHttpRequest = sinon.useFakeXMLHttpRequest()
})
afterEach(() => {
server.restore()
delete (global as any).XMLHttpRequest
})
it('should get corresponding text', function () {
const html = fs.readFileSync('https://example.com/views/hello.html')
return expect(html).toBe('hello {{name}}')
})
it('should throw 404', () => {
return expect(() => fs.readFileSync('https://example.com/not/exist.html'))
.toThrow('Not Found')
})
})
})
@@ -0,0 +1,7 @@
import { StreamedEmitter } from './streamed-emitter-browser'
describe('build/streamed-emitter-browser', () => {
it('should throw when try to constructing', () => {
expect(() => new StreamedEmitter()).toThrow(/streaming not supported/)
})
})
@@ -1,4 +1,4 @@
import { Emitter } from './emitter'
import { Emitter } from '../emitters'
export class StreamedEmitter implements Emitter {
public buffer = '';
+67
View File
@@ -0,0 +1,67 @@
import { LRU } from './lru'
describe('LRU', () => {
it('should perform read()/write()', () => {
const lru = new LRU(2)
expect(lru.limit).toEqual(2)
lru.write('foo', 'FOO')
lru.write('bar', 'BAR')
expect(lru.read('foo')).toEqual('FOO')
expect(lru.read('bar')).toEqual('BAR')
})
it('should perform clear()', () => {
const lru = new LRU(2)
lru.write('foo', 'FOO')
lru.write('bar', 'BAR')
expect(lru.size).toEqual(2)
lru.clear()
expect(lru.size).toEqual(0)
expect(lru.read('foo')).toBe(undefined)
})
it('should remove lrc item when full(limit=-1)', () => {
const lru = new LRU(-1)
lru.write('foo', 'FOO')
lru.write('bar', 'BAR')
expect(lru.size).toEqual(0)
expect(lru.read('foo')).toBe(undefined)
expect(lru.read('bar')).toBe(undefined)
})
it('should remove lrc item when full(limit=0)', () => {
const lru = new LRU(0)
lru.write('foo', 'FOO')
lru.write('bar', 'BAR')
expect(lru.size).toEqual(0)
expect(lru.read('foo')).toBe(undefined)
expect(lru.read('bar')).toBe(undefined)
})
it('should remove lrc item when full(limit=1)', () => {
const lru = new LRU(1)
lru.write('foo', 'FOO')
lru.write('bar', 'BAR')
expect(lru.size).toEqual(1)
expect(lru.read('foo')).toBe(undefined)
expect(lru.read('bar')).toEqual('BAR')
})
it('should remove lrc item when full(limit=2)', () => {
const lru = new LRU(2)
expect(lru.size).toEqual(0)
lru.write('foo', 'FOO')
expect(lru.size).toEqual(1)
lru.write('bar', 'BAR')
expect(lru.size).toEqual(2)
lru.write('coo', 'COO')
expect(lru.size).toEqual(2)
expect(lru.read('foo')).toBe(undefined)
expect(lru.read('bar')).toEqual('BAR')
expect(lru.read('coo')).toEqual('COO')
})
it('should overwrite item the with same key', () => {
const lru = new LRU(2)
lru.write('foo', 'FOO')
expect(lru.size).toEqual(1)
lru.write('foo', 'BAR')
expect(lru.size).toEqual(1)
expect(lru.read('foo')).toEqual('BAR')
})
})
+219
View File
@@ -0,0 +1,219 @@
import { Context } from './context'
import { Scope } from './scope'
describe('Context', function () {
let ctx: any, scope: Scope
beforeEach(function () {
scope = {
foo: 'zoo',
one: 1,
zoo: { size: 4 },
map: new Map([['foo', 'FOO']]),
obj: {
first: 'f',
last: 'l'
},
func: () => 'FUNC',
objFunc: () => ({ prop: 'PROP' }),
bar: {
zoo: 'coo',
'Mr.Smith': 'John',
arr: ['a', 'b']
},
arr: ['a', 'b', 'c', 'd']
}
ctx = new Context(scope)
})
describe('#get()', function () {
it('should get direct property', async function () {
expect(ctx.get(['foo'])).toEqual('zoo')
})
it('should read nested property', async function () {
expect(ctx.get(['obj', 'first'])).toEqual('f')
expect(ctx.get(['obj', 'last'])).toEqual('l')
expect(ctx.get(['obj', 'size'])).toEqual(2)
})
it('undefined property should yield undefined', async function () {
expect(ctx.get(['notdefined'])).toEqual(undefined)
expect(ctx.get([false as any])).toEqual(undefined)
})
it('should respect to toLiquid', async function () {
const scope = new Context({ foo: {
toLiquid: () => ({ bar: 'BAR' }),
bar: 'bar'
} })
// eslint-disable-next-line deprecation/deprecation
expect(scope.get(['foo', 'bar'])).toEqual('BAR')
})
it('should return undefined when not exist', async function () {
expect(ctx.get(['foo', 'foo', 'foo'])).toBeUndefined()
})
it('should return string length as size', async function () {
expect(ctx.get(['foo', 'size'])).toEqual(3)
})
it('should return array length as size', async function () {
expect(ctx.get(['bar', 'arr', 'size'])).toEqual(2)
})
it('should return map size as size', async function () {
expect(ctx.get(['map', 'size'])).toEqual(1)
})
it('should return undefined if not have a size', async function () {
expect(ctx.get(['one', 'size'])).toBeUndefined()
expect(ctx.get(['non-exist', 'size'])).toBeUndefined()
})
it('should read .first of array', async function () {
expect(ctx.get(['bar', 'arr', 'first'])).toEqual('a')
})
it('should read .last of array', async function () {
expect(ctx.get(['bar', 'arr', 'last'])).toEqual('b')
})
it('should read element of array', async function () {
expect(ctx.get(['arr', 1])).toEqual('b')
})
it('should read element of array from end', async function () {
expect(ctx.get(['arr', -2])).toEqual('c')
})
it('should call function', async function () {
expect(ctx.get(['func'])).toEqual('FUNC')
})
it('should call function before read nested property', async function () {
expect(ctx.get(['objFunc', 'prop'])).toEqual('PROP')
})
})
describe('#getFromScope()', function () {
it('should support string', () => {
expect(ctx.getFromScope({ obj: { foo: 'FOO' } }, 'obj.foo')).toEqual('FOO')
})
})
describe('strictVariables', function () {
let ctx: Context
beforeEach(function () {
ctx = new Context(ctx, {
strictVariables: true
} as any)
})
it('should throw when variable not defined', function () {
return expect(() => ctx.getSync(['notdefined'])).toThrow(/undefined variable: notdefined/)
})
it('should throw when deep variable not exist', async function () {
ctx.push({ foo: 'FOO' })
return expect(() => ctx.getSync(['foo', 'bar', 'not', 'defined'])).toThrow(/undefined variable: foo.bar/)
})
it('should throw when itself not defined', async function () {
ctx.push({ foo: 'FOO' })
return expect(() => ctx.getSync(['foo', 'BAR'])).toThrow(/undefined variable: foo.BAR/)
})
it('should find variable in parent scope', async function () {
ctx.push({ 'foo': 'foo' })
ctx.push({
'bar': 'bar'
})
expect(ctx.getSync(['foo'])).toEqual('foo')
})
})
describe('ownPropertyOnly', function () {
let ctx: Context
beforeEach(function () {
ctx = new Context(ctx, {
ownPropertyOnly: true
} as any)
})
it('should return undefined for prototype object property', function () {
ctx.push({ foo: Object.create({ bar: 'BAR' }) })
return expect(ctx.getSync(['foo', 'bar'])).toEqual(undefined)
})
it('should use prototype when ownPropertyOnly=false', function () {
ctx = new Context({ foo: Object.create({ bar: 'BAR' }) }, { ownPropertyOnly: false } as any)
return expect(ctx.getSync(['foo', 'bar'])).toEqual('BAR')
})
it('renderOptions.ownPropertyOnly should override options.ownPropertyOnly', function () {
ctx = new Context({ foo: Object.create({ bar: 'BAR' }) }, { ownPropertyOnly: false } as any, { ownPropertyOnly: true })
return expect(ctx.getSync(['foo', 'bar'])).toEqual(undefined)
})
it('should return undefined for Array.prototype.reduce', function () {
ctx.push({ foo: [] })
return expect(ctx.getSync(['foo', 'reduce'])).toEqual(undefined)
})
it('should return undefined for function prototype property', function () {
function Foo () {}
Foo.prototype.bar = 'BAR'
ctx.push({ foo: new (Foo as any)() })
return expect(ctx.getSync(['foo', 'bar'])).toEqual(undefined)
})
it('should allow function constructor properties', function () {
function Foo (this: any) { this.bar = 'BAR' }
ctx.push({ foo: new (Foo as any)() })
return expect(ctx.getSync(['foo', 'bar'])).toEqual('BAR')
})
it('should return undefined for class method', function () {
class Foo { bar () {} }
ctx.push({ foo: new Foo() })
return expect(ctx.getSync(['foo', 'bar'])).toEqual(undefined)
})
it('should allow class property', function () {
class Foo { bar = 'BAR' }
ctx.push({ foo: new Foo() })
return expect(ctx.getSync(['foo', 'bar'])).toEqual('BAR')
})
it('should allow Array.prototype.length', function () {
ctx.push({ foo: [1, 2] })
return expect(ctx.getSync(['foo', 'length'])).toEqual(2)
})
it('should allow size to access Array.prototype.length', function () {
ctx.push({ foo: [1, 2] })
return expect(ctx.getSync(['foo', 'size'])).toEqual(2)
})
it('should allow size to access Set.prototype.size', function () {
ctx.push({ foo: new Set([1, 2]) })
return expect(ctx.getSync(['foo', 'size'])).toEqual(2)
})
it('should allow size to access Object key count', function () {
ctx.push({ foo: { bar: 'BAR', coo: 'COO' } })
return expect(ctx.getSync(['foo', 'size'])).toEqual(2)
})
it('should throw when property is hidden and strictVariables is true', function () {
ctx = new Context(ctx, {
ownPropertyOnly: true,
strictVariables: true
} as any)
ctx.push({ foo: Object.create({ bar: 'BAR' }) })
return expect(() => ctx.getSync(['foo', 'bar'])).toThrow(/undefined variable: foo.bar/)
})
})
describe('.getAll()', function () {
it('should get all properties when arguments empty', async function () {
expect(ctx.getAll()).toEqual(scope)
})
})
describe('.push()', function () {
it('should push scope', async function () {
ctx.push({ 'bar': 'bar' })
ctx.push({
foo: 'foo'
})
expect(ctx.getSync(['foo'])).toEqual('foo')
expect(ctx.getSync(['bar'])).toEqual('bar')
})
it('should hide deep properties by push', async function () {
ctx.push({ bar: { bar: 'bar' } })
ctx.push({ bar: { foo: 'foo' } })
expect(ctx.getSync(['bar', 'foo'])).toEqual('foo')
expect(ctx.getSync(['bar', 'bar'])).toEqual(undefined)
})
})
describe('.pop()', function () {
it('should pop scope', async function () {
ctx.push({
foo: 'foo'
})
ctx.pop()
expect(ctx.getSync(['foo'])).toEqual('zoo')
})
})
})
+4 -1
View File
@@ -58,9 +58,12 @@ export class Context {
.reduce((ctx, val) => __assign(ctx, val), {})
}
/**
* @deprecated use `_get()` instead
* @deprecated use `_get()` or `getSync()` instead
*/
public get (paths: PropertyKey[]): unknown {
return this.getSync(paths)
}
public getSync (paths: PropertyKey[]): unknown {
return toValueSync(this._get(paths))
}
public * _get (paths: PropertyKey[]): IterableIterator<unknown> {
+53
View File
@@ -0,0 +1,53 @@
import * as fs from './fs-impl'
import * as path from 'path'
describe('fs-impl', function () {
describe('.resolve()', function () {
it('should resolve based on root', async function () {
const filepath = fs.resolve('/foo', 'bar.html', '.liquid')
const expected = path.resolve('/foo/bar.html')
return expect(filepath).toBe(expected)
})
it('should add extension if it has no extension', async function () {
const filepath = fs.resolve('/foo', 'bar', '.liquid')
const expected = path.resolve('/foo/bar.liquid')
return expect(filepath).toBe(expected)
})
})
describe('.existsSync', () => {
it('should resolve as false if not exists', () => {
expect(fs.existsSync('/foo/bar')).toBeFalsy()
})
it('should resolve as true if exists', () => {
expect(fs.existsSync(__filename)).toBeTruthy()
})
})
describe('.exists', () => {
it('should resolve as false if not exists', async () => {
const result = await fs.exists('/foo/bar')
expect(result).toBeFalsy()
})
it('should resolve as true if exists', async () => {
const result = await fs.exists(__filename)
expect(result).toBeTruthy()
})
})
describe('.readFileSync', function () {
it('should throw when not exist', function () {
return expect(() => fs.readFileSync('/foo/bar')).toThrow('ENOENT')
})
it('should read content if exists', function () {
const content = fs.readFileSync(__filename)
expect(content).toContain('should read content if exists')
})
})
describe('.readFile', function () {
it('should throw when not exist', function () {
return expect(fs.readFile('/foo/bar')).rejects.toHaveProperty('message', expect.stringMatching('ENOENT'))
})
it('should read content if exists', async function () {
const content = await fs.readFile(__filename)
expect(content).toContain('should read content if exists')
})
})
})
+31
View File
@@ -0,0 +1,31 @@
import * as fs from './fs-impl'
import { Loader } from './loader'
describe('fs/loader', function () {
describe('.candidates()', function () {
it('should resolve relatively', async function () {
const loader = new Loader({ relativeReference: true, fs, extname: '' } as any)
const candidates = [...loader.candidates('./foo/bar', ['/root', '/root/foo'], '/root/current', true)]
expect(candidates).toContain('/root/foo/bar')
})
it('should not include out of root candidates', async function () {
const loader = new Loader({ relativeReference: true, fs, extname: '' } as any)
const candidates = [...loader.candidates('../foo/bar', ['/root'], '/root/current', true)]
expect(candidates).toHaveLength(0)
})
it('should treat root as a terminated path', async function () {
const loader = new Loader({ relativeReference: true, fs, extname: '' } as any)
const candidates = [...loader.candidates('../root-dir/bar', ['/root'], '/root/current', true)]
expect(candidates).toHaveLength(0)
})
it('should default `.contains()` to () => true', async function () {
const customFs = {
...fs,
contains: undefined
}
const loader = new Loader({ relativeReference: true, fs: customFs, extname: '' } as any)
const candidates = [...loader.candidates('../foo/bar', ['/root'], '/root/current', true)]
expect(candidates).toContain('/foo/bar')
})
})
})
+10
View File
@@ -0,0 +1,10 @@
import { normalize } from './liquid-options'
describe('liquid-options', () => {
describe('.normalize()', () => {
it('should set cache to undefined if specified to falsy', () => {
const options = normalize({ cache: false })
expect(options.cache).toBeUndefined()
})
})
})
+3 -3
View File
@@ -1,7 +1,7 @@
import { assert, isArray, isString, isFunction } from './util'
import { LRU, LiquidCache } from './cache'
import { FS, LookupType } from './fs'
import * as fs from './fs/node'
import * as fs from './fs/fs-impl'
import { defaultOperators, Operators } from './render'
import { json } from './filters/misc'
import { escape } from './filters/html'
@@ -180,8 +180,8 @@ export function normalize (options: LiquidOptions): NormalizedFullOptions {
options.cache = cache
}
options = { ...defaultOptions, ...(options.jekyllInclude ? { dynamicPartials: false } : {}), ...options }
if (!options.fs!.dirname && options.relativeReference) {
console.warn('[LiquidJS] `fs.dirname` is required for relativeReference, set relativeReference to `false` to suppress this warning, or provide implementation for `fs.dirname`')
if ((!options.fs!.dirname || !options.fs!.sep) && options.relativeReference) {
console.warn('[LiquidJS] `fs.dirname` and `fs.sep` are required for relativeReference, set relativeReference to `false` to suppress this warning')
options.relativeReference = false
}
options.root = normalizeDirectoryList(options.root)
+28
View File
@@ -0,0 +1,28 @@
import { matchOperator } from './match-operator'
import { defaultOperators } from '..'
import { createTrie } from '../util/operator-trie'
describe('parser/matchOperator()', function () {
const trie = createTrie(defaultOperators)
it('should match contains', () => {
expect(matchOperator('contains', 0, trie)).toBe(8)
})
it('should match comparision', () => {
expect(matchOperator('>', 0, trie)).toBe(1)
expect(matchOperator('>=', 0, trie)).toBe(2)
expect(matchOperator('<', 0, trie)).toBe(1)
expect(matchOperator('<=', 0, trie)).toBe(2)
})
it('should match binary logic', () => {
expect(matchOperator('and', 0, trie)).toBe(3)
expect(matchOperator('or', 0, trie)).toBe(2)
})
it('should not match if word not terminate', () => {
expect(matchOperator('true1', 0, trie)).toBe(-1)
expect(matchOperator('containsa', 0, trie)).toBe(-1)
})
it('should match if word boundary found', () => {
expect(matchOperator('>=1', 0, trie)).toBe(2)
expect(matchOperator('contains b', 0, trie)).toBe(8)
})
})
+12
View File
@@ -0,0 +1,12 @@
import { ParseStream } from './parse-stream'
import { Token } from '../tokens'
describe('parseStream', () => {
it('should trigger "token" event', () => {
const token = { kind: 4 } as Token
const ps = new ParseStream([token], (token) => ({ token } as any))
let got
ps.on('token', token => { got = token }).start()
expect(got).toEqual(token)
})
})
+34
View File
@@ -0,0 +1,34 @@
import { parseStringLiteral } from './parse-string-literal'
describe('parseStringLiteral()', function () {
it('should parse octal escape', () => {
expect(parseStringLiteral(String.raw`"\1010"`)).toBe('A0')
expect(parseStringLiteral(String.raw`"\12"`)).toBe('\n')
expect(parseStringLiteral(String.raw`"\01"`)).toBe('\u0001')
expect(parseStringLiteral(String.raw`"\0"`)).toBe('\0')
})
it('should skip invalid octal escape', () => {
expect(parseStringLiteral(String.raw`"\9"`)).toBe('9')
})
it('should parse \\n, \\t, \\r', () => {
expect(parseStringLiteral(String.raw`"fo\no"`)).toBe('fo\no')
expect(parseStringLiteral(String.raw`'fo\to'`)).toBe('fo\to')
expect(parseStringLiteral(String.raw`'fo\ro'`)).toBe('fo\ro')
})
it('should parse unicode(hex) escape', () => {
expect(parseStringLiteral('"\\u003C"')).toBe('<')
expect(parseStringLiteral('"\\u003cZ"')).toBe('<Z')
expect(parseStringLiteral('"\\u41"')).toBe('A')
})
it('should skip invalid unicode(hex) escape', () => {
expect(parseStringLiteral('"\\u41Z"')).toBe('AZ')
expect(parseStringLiteral('"\\uZ"')).toBe('\0Z')
})
it('should parse quote escape', () => {
expect(parseStringLiteral(String.raw`"fo\'o"`)).toBe("fo'o")
expect(parseStringLiteral(String.raw`'fo\"o'`)).toBe('fo"o')
})
it('should parse slash escape', () => {
expect(parseStringLiteral(String.raw`'fo\\o'`)).toBe('fo\\o')
})
})
+546
View File
@@ -0,0 +1,546 @@
import { LiquidTagToken, HTMLToken, QuotedToken, OutputToken, TagToken, OperatorToken, RangeToken, PropertyAccessToken, NumberToken, IdentifierToken } from '../tokens'
import { Tokenizer } from './tokenizer'
describe('Tokenizer', function () {
it('should read quoted', () => {
expect(new Tokenizer('"foo" ff').readQuoted()!.getText()).toBe('"foo"')
expect(new Tokenizer(' "foo"ff').readQuoted()!.getText()).toBe('"foo"')
})
it('should read value', () => {
expect(new Tokenizer('a[ b][ "c d" ]').readValueOrThrow().getText()).toBe('a[ b][ "c d" ]')
expect(new Tokenizer('a.b[c[d.e]]').readValueOrThrow().getText()).toBe('a.b[c[d.e]]')
})
it('should read identifier', () => {
expect(new Tokenizer('foo bar').readIdentifier()).toHaveProperty('content', 'foo')
// eslint-disable-next-line deprecation/deprecation
expect(new Tokenizer('foo bar').readWord()).toHaveProperty('content', 'foo')
})
it('should read number value', () => {
const token: NumberToken = new Tokenizer('2.33.2').readValueOrThrow() as any
expect(token).toBeInstanceOf(NumberToken)
expect(token.whole.getText()).toBe('2')
expect(token.decimal!.getText()).toBe('33')
expect(token.getText()).toBe('2.33')
})
it('should read quoted value', () => {
const value = new Tokenizer('"foo"a').readValue()
expect(value).toBeInstanceOf(QuotedToken)
expect(value!.getText()).toBe('"foo"')
})
it('should read property access value', () => {
expect(new Tokenizer('a[b]["c d"]').readValueOrThrow().getText()).toBe('a[b]["c d"]')
})
it('should read quoted property access value', () => {
const value = new Tokenizer('["a prop"]').readValue()
expect(value).toBeInstanceOf(PropertyAccessToken)
expect((value as PropertyAccessToken).variable.getText()).toBe('"a prop"')
})
it('should throw for broken quoted property access', () => {
const tokenizer = new Tokenizer('[5]')
expect(() => tokenizer.readValueOrThrow()).toThrow()
})
it('should throw for incomplete quoted property access', () => {
const tokenizer = new Tokenizer('["a prop"')
expect(() => tokenizer.readValueOrThrow()).toThrow()
})
it('should read hash', () => {
const hash1 = new Tokenizer('foo: 3').readHash()
expect(hash1!.name.content).toBe('foo')
expect(hash1!.value!.getText()).toBe('3')
const hash2 = new Tokenizer(', foo: a[ "bar"]').readHash()
expect(hash2!.name.content).toBe('foo')
expect(hash2!.value!.getText()).toBe('a[ "bar"]')
})
it('should read multiple hashs', () => {
const hashes = new Tokenizer(', limit: 3 reverse offset:off').readHashes()
expect(hashes).toHaveLength(3)
const [limit, reverse, offset] = hashes
expect(limit.name.content).toBe('limit')
expect(limit.value!.getText()).toBe('3')
expect(reverse.name.content).toBe('reverse')
expect(reverse.value).toBeUndefined()
expect(offset.name.content).toBe('offset')
expect(offset.value!.getText()).toBe('off')
})
it('should read hash value with property access', () => {
const hashes = new Tokenizer('cols: 2, rows: data["rows"]').readHashes()
expect(hashes).toHaveLength(2)
const [cols, rols] = hashes
expect(cols.name.content).toBe('cols')
expect(cols.value!.getText()).toBe('2')
expect(rols.name.content).toBe('rows')
expect(rols.value!.getText()).toBe('data["rows"]')
})
describe('#readTopLevelTokens()', () => {
it('should read HTML token', function () {
const html = '<html><body><p>Lorem Ipsum</p></body></html>'
const tokenizer = new Tokenizer(html)
const tokens = tokenizer.readTopLevelTokens()
expect(tokens.length).toBe(1)
expect(tokens[0]).toBeInstanceOf(HTMLToken)
expect((tokens[0] as HTMLToken).getContent()).toBe(html)
})
it('should read tag token', function () {
const html = '<p>{% for p in a[1]%}</p>'
const tokenizer = new Tokenizer(html)
const tokens = tokenizer.readTopLevelTokens()
expect(tokens.length).toBe(3)
const tag = tokens[1] as TagToken
expect(tag).toBeInstanceOf(TagToken)
expect(tag.name).toBe('for')
expect(tag.args).toBe('p in a[1]')
})
it('should allow unclosed tag inside {% raw %}', function () {
const html = '{%raw%} {%if%} {%else {%endraw%}'
const tokenizer = new Tokenizer(html)
const tokens = tokenizer.readTopLevelTokens()
expect(tokens.length).toBe(3)
expect(tokens[0]).toHaveProperty('name', 'raw')
expect((tokens[1] as any).getContent()).toBe(' {%if%} {%else ')
})
it('should allow unclosed endraw tag inside {% raw %}', function () {
const html = '{%raw%} {%endraw {%raw%} {%endraw%}'
const tokenizer = new Tokenizer(html)
const tokens = tokenizer.readTopLevelTokens()
expect(tokens.length).toBe(3)
expect(tokens[0]).toHaveProperty('name', 'raw')
expect((tokens[1] as any).getContent()).toBe(' {%endraw {%raw%} ')
})
it('should throw when {% raw %} not closed', function () {
const html = '{%raw%} {%endraw {%raw%}'
const tokenizer = new Tokenizer(html)
expect(() => tokenizer.readTopLevelTokens()).toThrow('raw "{%raw%} {%end..." not closed, line:1, col:8')
})
it('should read output token', function () {
const html = '<p>{{foo | date: "%Y-%m-%d"}}</p>'
const tokenizer = new Tokenizer(html)
const tokens = tokenizer.readTopLevelTokens()
expect(tokens.length).toBe(3)
const output = tokens[1] as OutputToken
expect(output).toBeInstanceOf(OutputToken)
expect(output.content).toBe('foo | date: "%Y-%m-%d"')
})
it('should handle consecutive value and tags', function () {
const html = '{{foo}}{{bar}}{%foo%}{%bar%}'
const tokenizer = new Tokenizer(html)
const tokens = tokenizer.readTopLevelTokens()
expect(tokens.length).toBe(4)
const o1 = tokens[0] as OutputToken
const o2 = tokens[1] as OutputToken
const t1 = tokens[2] as TagToken
const t2 = tokens[3] as TagToken
expect(o1).toBeInstanceOf(OutputToken)
expect(o2).toBeInstanceOf(OutputToken)
expect(t1).toBeInstanceOf(TagToken)
expect(t2).toBeInstanceOf(TagToken)
expect(o1.content).toBe('foo')
expect(o2.content).toBe('bar')
expect(t1.name).toBe('foo')
expect(t1.args).toBe('')
expect(t2.name).toBe('bar')
expect(t2.args).toBe('')
})
it('should keep white spaces and newlines', function () {
const html = '{%foo%}\n{%bar %} \n {%alice%}'
const tokenizer = new Tokenizer(html)
const tokens = tokenizer.readTopLevelTokens()
expect(tokens.length).toBe(5)
expect(tokens[1]).toBeInstanceOf(HTMLToken)
expect(tokens[1].getText()).toBe('\n')
expect(tokens[3]).toBeInstanceOf(HTMLToken)
expect(tokens[3].getText()).toBe(' \n ')
})
it('should handle multiple lines tag', function () {
const html = '{%foo\na:a\nb:1.23\n%}'
const tokenizer = new Tokenizer(html)
const tokens = tokenizer.readTopLevelTokens()
expect(tokens.length).toBe(1)
expect(tokens[0]).toBeInstanceOf(TagToken)
expect((tokens[0] as TagToken).args).toBe('a:a\nb:1.23')
expect(tokens[0].getText()).toBe('{%foo\na:a\nb:1.23\n%}')
})
it('should handle multiple lines value', function () {
const html = '{{foo\n|date:\n"%Y-%m-%d"\n}}'
const tokenizer = new Tokenizer(html)
const tokens = tokenizer.readTopLevelTokens()
expect(tokens.length).toBe(1)
expect(tokens[0]).toBeInstanceOf(OutputToken)
expect(tokens[0].getText()).toBe('{{foo\n|date:\n"%Y-%m-%d"\n}}')
})
it('should handle complex object property access', function () {
const html = '{{ obj["my:property with anything"] }}'
const tokenizer = new Tokenizer(html)
const tokens = tokenizer.readTopLevelTokens()
expect(tokens.length).toBe(1)
const output = tokens[0] as OutputToken
expect(output).toBeInstanceOf(OutputToken)
expect(output.content).toBe('obj["my:property with anything"]')
})
it('should throw if tag not closed', function () {
const html = '{% assign foo = bar {{foo}}'
const tokenizer = new Tokenizer(html)
expect(() => tokenizer.readTopLevelTokens()).toThrow(/tag "{% assign foo..." not closed/)
})
it('should throw if output not closed', function () {
const tokenizer = new Tokenizer('{{name}')
expect(() => tokenizer.readTopLevelTokens()).toThrow(/output "{{name}" not closed/)
})
})
describe('#readTagToken()', () => {
it('should skip quoted delimiters', function () {
const html = '{% assign a = "%} {% }} {{" %}'
const tokenizer = new Tokenizer(html)
const token = tokenizer.readTagToken()
expect(token).toBeInstanceOf(TagToken)
expect(token.name).toBe('assign')
expect(token.args).toBe('a = "%} {% }} {{"')
})
})
describe('#readOutputToken()', () => {
it('should skip quoted delimiters', function () {
const html = '{{ "%} {%" | append: "}} {{" }}'
const tokenizer = new Tokenizer(html)
const token = tokenizer.readOutputToken()
expect(token).toBeInstanceOf(OutputToken)
expect(token.content).toBe('"%} {%" | append: "}} {{"')
})
})
describe('#readRange()', () => {
it('should read `(1..3)`', () => {
const range = new Tokenizer('(1..3)').readRange()
expect(range).toBeInstanceOf(RangeToken)
expect(range!.getText()).toEqual('(1..3)')
const { lhs, rhs } = range!
expect(lhs).toBeInstanceOf(NumberToken)
expect(lhs.getText()).toBe('1')
expect(rhs).toBeInstanceOf(NumberToken)
expect(rhs.getText()).toBe('3')
})
it('should throw for `(..3)`', () => {
expect(() => new Tokenizer('(..3)').readRange()).toThrow('unexpected token "..3)", value expected')
})
it('should read `(a.b..c["..d"])`', () => {
const range = new Tokenizer('(a.b..c["..d"])').readRange()
expect(range).toBeInstanceOf(RangeToken)
expect(range!.getText()).toEqual('(a.b..c["..d"])')
})
})
describe('#readFilter()', () => {
it('should read a simple filter', function () {
const tokenizer = new Tokenizer('| plus')
const token = tokenizer.readFilter()
expect(token).toHaveProperty('name', 'plus')
expect(token).toHaveProperty('args', [])
})
it('should read a filter with argument', function () {
const tokenizer = new Tokenizer(' | plus: 1')
const token = tokenizer.readFilter()
expect(token).toHaveProperty('name', 'plus')
expect(token!.args).toHaveLength(1)
const one: NumberToken = token!.args[0] as any
expect(one).toBeInstanceOf(NumberToken)
expect(one.getText()).toBe('1')
})
it('should read a filter with colon but no argument', function () {
const tokenizer = new Tokenizer('| plus:')
const token = tokenizer.readFilter()
expect(token).toHaveProperty('name', 'plus')
expect(token).toHaveProperty('args', [])
})
it('should read null if name not found', function () {
const tokenizer = new Tokenizer('|')
const token = tokenizer.readFilter()
expect(token).toBeNull()
})
it('should read a filter with k/v argument', function () {
const tokenizer = new Tokenizer(' | plus: a:1')
const token = tokenizer.readFilter()
expect(token).toHaveProperty('name', 'plus')
expect(token!.args).toHaveLength(1)
const [k, v]: [string, NumberToken] = token!.args[0] as any
expect(k).toBe('a')
expect(v).toBeInstanceOf(NumberToken)
expect(v.getText()).toBe('1')
})
it('should read a filter with "arr[0]" argument', function () {
const tokenizer = new Tokenizer('| plus: arr[0]')
const token = tokenizer.readFilter()
expect(token).toHaveProperty('name', 'plus')
expect(token!.args).toHaveLength(1)
const pa: PropertyAccessToken = token!.args[0] as any
expect(token!.args[0]).toBeInstanceOf(PropertyAccessToken)
expect((pa.variable as any).content).toBe('arr')
expect(pa.props).toHaveLength(1)
expect(pa.props[0]).toBeInstanceOf(NumberToken)
expect(pa.props[0].getText()).toBe('0')
})
it('should read a filter with obj.foo argument', function () {
const tokenizer = new Tokenizer('| plus: obj.foo')
const token = tokenizer.readFilter()
expect(token).toHaveProperty('name', 'plus')
expect(token!.args).toHaveLength(1)
const pa: PropertyAccessToken = token!.args[0] as any
expect(token!.args[0]).toBeInstanceOf(PropertyAccessToken)
expect((pa.variable as any).content).toBe('obj')
expect(pa.props).toHaveLength(1)
expect(pa.props[0]).toBeInstanceOf(IdentifierToken)
expect(pa.props[0].getText()).toBe('foo')
})
it('should read a filter with obj["foo"] argument', function () {
const tokenizer = new Tokenizer('| plus: obj["good luck"]')
const token = tokenizer.readFilter()
expect(token).toHaveProperty('name', 'plus')
expect(token!.args).toHaveLength(1)
const pa: PropertyAccessToken = token!.args[0] as any
expect(token!.args[0]).toBeInstanceOf(PropertyAccessToken)
expect(pa.getText()).toBe('obj["good luck"]')
expect((pa.variable as any).content).toBe('obj')
expect(pa.props[0].getText()).toBe('"good luck"')
})
})
describe('#readFilters()', () => {
it('should read simple filters', function () {
const tokenizer = new Tokenizer('| plus: 3 | capitalize')
const tokens = tokenizer.readFilters()
expect(tokens).toHaveLength(2)
expect(tokens[0]).toHaveProperty('name', 'plus')
expect(tokens[0].args).toHaveLength(1)
expect(tokens[0].args[0]).toBeInstanceOf(NumberToken)
expect((tokens[0].args[0] as any).getText()).toBe('3')
expect(tokens[1]).toHaveProperty('name', 'capitalize')
expect(tokens[1].args).toHaveLength(0)
})
it('should read filters', function () {
const tokenizer = new Tokenizer('| plus: a:3 | capitalize | append: foo[a.b["c d"]]')
const tokens = tokenizer.readFilters()
expect(tokens).toHaveLength(3)
expect(tokens[0]).toHaveProperty('name', 'plus')
expect(tokens[0].args).toHaveLength(1)
const [k, v]: [string, NumberToken] = tokens[0].args[0] as any
expect(k).toBe('a')
expect(v).toBeInstanceOf(NumberToken)
expect(v.getText()).toBe('3')
expect(tokens[1]).toHaveProperty('name', 'capitalize')
expect(tokens[1].args).toHaveLength(0)
expect(tokens[2]).toHaveProperty('name', 'append')
expect(tokens[2].args).toHaveLength(1)
expect(tokens[2].args[0]).toBeInstanceOf(PropertyAccessToken)
expect((tokens[2].args[0] as any).getText()).toBe('foo[a.b["c d"]]')
expect((tokens[2].args[0] as any).props[0].getText()).toBe('a.b["c d"]')
})
})
describe('#readExpression()', () => {
it('should read expression `a `', () => {
const exp = [...new Tokenizer('a ').readExpressionTokens()]
expect(exp).toHaveLength(1)
expect(exp[0]).toBeInstanceOf(PropertyAccessToken)
expect(exp[0].getText()).toEqual('a')
})
it('should read expression `a[][b]`', () => {
const exp = [...new Tokenizer('a[][b]').readExpressionTokens()]
expect(exp).toHaveLength(1)
const pa = exp[0] as PropertyAccessToken
expect(pa).toBeInstanceOf(PropertyAccessToken)
expect((pa.variable as any).content).toEqual('a')
expect(pa.props).toHaveLength(2)
const [p1, p2] = pa.props
expect(p1).toBeInstanceOf(IdentifierToken)
expect(p1.getText()).toBe('')
expect(p2).toBeInstanceOf(PropertyAccessToken)
expect(p2.getText()).toBe('b')
})
it('should read expression `a.`', () => {
const exp = [...new Tokenizer('a.').readExpressionTokens()]
expect(exp).toHaveLength(1)
const pa = exp[0] as PropertyAccessToken
expect(pa).toBeInstanceOf(PropertyAccessToken)
expect((pa.variable as any).content).toEqual('a')
expect(pa.props).toHaveLength(0)
})
it('should read expression `a ==`', () => {
const exp = [...new Tokenizer('a ==').readExpressionTokens()]
expect(exp).toHaveLength(2)
expect(exp[0]).toBeInstanceOf(PropertyAccessToken)
expect(exp[0].getText()).toEqual('a')
expect(exp[1]).toBeInstanceOf(OperatorToken)
expect(exp[1].getText()).toEqual('==')
})
it('should read expression `a==b`', () => {
const exp = new Tokenizer('a==b').readExpressionTokens()
const [a, equals, b] = exp
expect(a).toBeInstanceOf(PropertyAccessToken)
expect(a.getText()).toEqual('a')
expect(equals).toBeInstanceOf(OperatorToken)
expect(equals.getText()).toBe('==')
expect(b).toBeInstanceOf(PropertyAccessToken)
expect(b.getText()).toEqual('b')
})
it('should read expression `^`', () => {
const exp = new Tokenizer('^').readExpressionTokens()
expect([...exp]).toEqual([])
})
it('should read expression `a == b`', () => {
const exp = new Tokenizer('a == b').readExpressionTokens()
const [a, equals, b] = exp
expect(a).toBeInstanceOf(PropertyAccessToken)
expect(a.getText()).toEqual('a')
expect(equals).toBeInstanceOf(OperatorToken)
expect(equals.getText()).toBe('==')
expect(b).toBeInstanceOf(PropertyAccessToken)
expect(b.getText()).toEqual('b')
})
it('should read expression `(1..3) contains 3`', () => {
const exp = new Tokenizer('(1..3) contains 3').readExpressionTokens()
const [range, contains, rhs] = exp
expect(range).toBeInstanceOf(RangeToken)
expect(range.getText()).toEqual('(1..3)')
expect(contains).toBeInstanceOf(OperatorToken)
expect(contains.getText()).toBe('contains')
expect(rhs).toBeInstanceOf(NumberToken)
expect(rhs.getText()).toEqual('3')
})
it('should read expression `a[b] == c`', () => {
const exp = new Tokenizer('a[b] == c').readExpressionTokens()
const [lhs, contains, rhs] = exp
expect(lhs).toBeInstanceOf(PropertyAccessToken)
expect(lhs.getText()).toEqual('a[b]')
expect(contains).toBeInstanceOf(OperatorToken)
expect(contains.getText()).toBe('==')
expect(rhs).toBeInstanceOf(PropertyAccessToken)
expect(rhs.getText()).toEqual('c')
})
it('should read expression `c[a["b"]] >= c`', () => {
const exp = new Tokenizer('c[a["b"]] >= c').readExpressionTokens()
const [lhs, op, rhs] = exp
expect(lhs).toBeInstanceOf(PropertyAccessToken)
expect(lhs.getText()).toEqual('c[a["b"]]')
expect(op).toBeInstanceOf(OperatorToken)
expect(op.getText()).toBe('>=')
expect(rhs).toBeInstanceOf(PropertyAccessToken)
expect(rhs.getText()).toEqual('c')
})
it('should read expression `"][" == var`', () => {
const exp = new Tokenizer('"][" == var').readExpressionTokens()
const [lhs, equals, rhs] = exp
expect(lhs).toBeInstanceOf(QuotedToken)
expect(lhs.getText()).toEqual('"]["')
expect(equals).toBeInstanceOf(OperatorToken)
expect(equals.getText()).toBe('==')
expect(rhs).toBeInstanceOf(PropertyAccessToken)
expect(rhs.getText()).toEqual('var')
})
it('should read expression `"\\\'" == "\\""`', () => {
const exp = new Tokenizer('"\\\'" == "\\""').readExpressionTokens()
const [lhs, equals, rhs] = exp
expect(lhs).toBeInstanceOf(QuotedToken)
expect(lhs.getText()).toEqual('"\\\'"')
expect(equals).toBeInstanceOf(OperatorToken)
expect(equals.getText()).toBe('==')
expect(rhs).toBeInstanceOf(QuotedToken)
expect(rhs.getText()).toEqual('"\\""')
})
})
describe('#readLiquidTagTokens', () => {
it('should read newline terminated tokens', () => {
const tokenizer = new Tokenizer('echo \'hello\'')
const tokens = tokenizer.readLiquidTagTokens()
expect(tokens.length).toBe(1)
const tag = tokens[0]
expect(tag).toBeInstanceOf(LiquidTagToken)
expect(tag.name).toBe('echo')
expect(tag.args).toBe('\'hello\'')
})
it('should gracefully handle empty lines', () => {
const tokenizer = new Tokenizer(`
echo 'hello'
decrement foo
`)
const tokens = tokenizer.readLiquidTagTokens()
expect(tokens.length).toBe(2)
})
it('should throw if line does not start with an identifier', () => {
const tokenizer = new Tokenizer('!')
expect(() => tokenizer.readLiquidTagTokens()).toThrow(/illegal liquid tag syntax/)
})
})
describe('#read inline comment tags', () => {
it('should allow hash characters in tag names', () => {
const tokenizer = new Tokenizer('{% # some comment %}')
const tokens = tokenizer.readTopLevelTokens()
expect(tokens.length).toBe(1)
const tag = tokens[0] as TagToken
expect(tag).toBeInstanceOf(TagToken)
expect(tag.name).toBe('#')
expect(tag.args).toBe('some comment')
})
it('should handle leading whitespace', () => {
const tokenizer = new Tokenizer('{%\n # some comment %}')
const tokens = tokenizer.readTopLevelTokens()
expect(tokens.length).toBe(1)
const tag = tokens[0] as TagToken
expect(tag).toBeInstanceOf(TagToken)
expect(tag.name).toBe('#')
expect(tag.args).toBe('some comment')
})
it('should handle no trailing whitespace', () => {
const tokenizer = new Tokenizer('{%\n #some comment %}')
const tokens = tokenizer.readTopLevelTokens()
expect(tokens.length).toBe(1)
const tag = tokens[0] as TagToken
expect(tag).toBeInstanceOf(TagToken)
expect(tag.name).toBe('#')
expect(tag.args).toBe('some comment')
})
})
})
+1 -2
View File
@@ -199,10 +199,9 @@ export class Tokenizer {
}
/**
* @deprecated
* @deprecated use #readIdentifier instead
*/
readWord () {
console.warn('Tokenizer#readWord() will be removed, use #readIdentifier instead')
return this.readIdentifier()
}
+94
View File
@@ -0,0 +1,94 @@
import { isTruthy, isFalsy } from './boolean'
import { Context } from '../context'
describe('boolean Shopify', function () {
describe('.isTruthy()', function () {
const ctx = {
opts: {
jsTruthy: false
}
} as unknown as Context
//
// Spec: https://shopify.github.io/liquid/basics/truthy-and-falsy/
it('true is truthy', function () {
expect(isTruthy(true, ctx)).toBeTruthy()
})
it('false is falsy', function () {
expect(isTruthy(false, ctx)).toBeFalsy()
})
it('null is falsy', function () {
expect(isTruthy(null, ctx)).toBeFalsy()
})
it('"foo" is truthy', function () {
expect(isTruthy('foo', ctx)).toBeTruthy()
})
it('"" is truthy', function () {
expect(isTruthy('', ctx)).toBeTruthy()
})
it('0 is truthy', function () {
expect(isTruthy(0, ctx)).toBeTruthy()
})
it('1 is truthy', function () {
expect(isTruthy(1, ctx)).toBeTruthy()
})
it('1.1 is truthy', function () {
expect(isTruthy(1.1, ctx)).toBeTruthy()
})
it('[1] is truthy', function () {
expect(isTruthy([1], ctx)).toBeTruthy()
})
it('[] is truthy', function () {
expect(isTruthy([], ctx)).toBeTruthy()
})
})
})
describe('boolean jsTruthy', function () {
const ctx = {
opts: {
jsTruthy: true
}
} as unknown as Context
describe('.isFalsy()', function () {
it('null is always falsy', function () {
expect(isFalsy(null, ctx)).toBeTruthy()
})
})
describe('.isTruthy()', function () {
it('true is truthy', function () {
expect(isTruthy(true, ctx)).toBeTruthy()
})
it('false is falsy', function () {
expect(isTruthy(false, ctx)).toBeFalsy()
})
it('null is always falsy', function () {
expect(isTruthy(null, ctx)).toBeFalsy()
})
it('null is always falsy', function () {
expect(isTruthy(null, ctx)).toBeFalsy()
})
it('"foo" is truthy', function () {
expect(isTruthy('foo', ctx)).toBeTruthy()
})
it('"" is falsy', function () {
expect(isTruthy('', ctx)).toBeFalsy()
})
it('0 is falsy', function () {
expect(isTruthy(0, ctx)).toBeFalsy()
})
it('1 is truthy', function () {
expect(isTruthy(1, ctx)).toBeTruthy()
})
it('1.1 is truthy', function () {
expect(isTruthy(1.1, ctx)).toBeTruthy()
})
it('[1] is truthy', function () {
expect(isTruthy([1], ctx)).toBeTruthy()
})
it('[] is truthy', function () {
expect(isTruthy([], ctx)).toBeTruthy()
})
})
})
+190
View File
@@ -0,0 +1,190 @@
import { Tokenizer } from '../parser'
import { Drop } from '../drop'
import { Context } from '../context'
import { toPromise, toValueSync } from '../util'
describe('Expression', function () {
const ctx = new Context({})
const create = (str: string) => new Tokenizer(str).readExpression()
it('should throw when context not defined', done => {
toPromise(create('foo').evaluate(undefined!, false))
.then(() => done(new Error('should not resolved')))
.catch(err => {
expect(err.message).toMatch(/context not defined/)
done()
})
})
describe('single value', function () {
it('should eval literal', async function () {
expect(await toPromise(create('2.4').evaluate(ctx, false))).toBe(2.4)
expect(await toPromise(create('"foo"').evaluate(ctx, false))).toBe('foo')
expect(await toPromise(create('false').evaluate(ctx, false))).toBe(false)
})
it('should eval range expression', async function () {
const ctx = new Context({ two: 2 })
expect(await toPromise(create('(2..4)').evaluate(ctx, false))).toEqual([2, 3, 4])
expect(await toPromise(create('(two..4)').evaluate(ctx, false))).toEqual([2, 3, 4])
})
it('should eval literal', async function () {
expect(await toPromise(create('2.4').evaluate(ctx, false))).toBe(2.4)
expect(await toPromise(create('"foo"').evaluate(ctx, false))).toBe('foo')
expect(await toPromise(create('false').evaluate(ctx, false))).toBe(false)
})
it('should eval property access', async function () {
const ctx = new Context({
foo: { bar: 'BAR' },
coo: 'bar',
doo: { foo: 'bar', bar: { foo: 'bar' } }
})
expect(await toPromise(create('foo.bar').evaluate(ctx, false))).toBe('BAR')
expect(await toPromise(create('foo["bar"]').evaluate(ctx, false))).toBe('BAR')
expect(await toPromise(create('foo[coo]').evaluate(ctx, false))).toBe('BAR')
expect(await toPromise(create('foo[doo.foo]').evaluate(ctx, false))).toBe('BAR')
expect(await toPromise(create('foo[doo["foo"]]').evaluate(ctx, false))).toBe('BAR')
expect(await toPromise(create('doo[coo].foo').evaluate(ctx, false))).toBe('bar')
})
})
describe('simple expression', function () {
it('should return false for "1==2"', async () => {
expect(await toPromise(create('1==2').evaluate(ctx, false))).toBe(false)
})
it('should apply deep equal for arrays', async () => {
const ctx = new Context({
arr1: [1, 2],
arr2: [1, 2],
arr3: [1, 2, 3]
})
expect(await toPromise(create('arr1==arr2').evaluate(ctx, false))).toBe(true)
expect(await toPromise(create('arr1==arr3').evaluate(ctx, false))).toBe(false)
})
it('should return true for "1<2"', async () => {
expect(await toPromise(create('1<2').evaluate(ctx, false))).toBe(true)
})
it('should return true for "1 < 2"', async () => {
expect(await toPromise(create('1 < 2').evaluate(ctx, false))).toBe(true)
})
it('should return true for "1 < 2"', async () => {
expect(await toPromise(create('1 < 2').evaluate(ctx, false))).toBe(true)
})
it('should return true for "2 <= 2"', async () => {
expect(await toPromise(create('2 <= 2').evaluate(ctx, false))).toBe(true)
})
it('should return true for "one <= two"', async () => {
const ctx = new Context({ one: 1, two: 2 })
expect(await toPromise(create('one <= two').evaluate(ctx, false))).toBe(true)
})
it('should return false for "x contains "x""', async () => {
const ctx = new Context({ x: 'XXX' })
expect(await toPromise(create('x contains "x"').evaluate(ctx, false))).toBe(false)
})
it('should return true for "x contains "X""', async () => {
const ctx = new Context({ x: 'XXX' })
expect(await toPromise(create('x contains "X"').evaluate(ctx, false))).toBe(true)
})
it('should return false for "1 contains "x""', async () => {
const ctx = new Context({ x: 'XXX' })
expect(await toPromise(create('1 contains "x"').evaluate(ctx, false))).toBe(false)
})
it('should return false for "y contains "x""', async () => {
const ctx = new Context({ x: 'XXX' })
expect(await toPromise(create('y contains "x"').evaluate(ctx, false))).toBe(false)
})
it('should return false for "z contains "x""', async () => {
const ctx = new Context({ x: 'XXX' })
expect(await toPromise(create('z contains "x"').evaluate(ctx, false))).toBe(false)
})
it('should return true for "(1..5) contains 3"', async () => {
const ctx = new Context({ x: 'XXX' })
expect(await toPromise(create('(1..5) contains 3').evaluate(ctx, false))).toBe(true)
})
it('should return false for "(1..5) contains 6"', async () => {
const ctx = new Context({ x: 'XXX' })
expect(await toPromise(create('(1..5) contains 6').evaluate(ctx, false))).toBe(false)
})
it('should return true for ""<=" == "<=""', async () => {
expect(await toPromise(create('"<=" == "<="').evaluate(ctx, false))).toBe(true)
})
})
it('should allow space in quoted value', async function () {
const ctx = new Context({ space: ' ' })
expect(await toPromise(create('" " == space').evaluate(ctx, false))).toBe(true)
})
describe('escape', () => {
it('should escape quote', async function () {
const ctx = new Context({ quote: '"' })
expect(await toPromise(create('"\\"" == quote').evaluate(ctx, false))).toBe(true)
})
it('should escape square bracket', async function () {
const ctx = new Context({ obj: { ']': 'bracket' } })
expect(await toPromise(create('obj["]"] == "bracket"').evaluate(ctx, false))).toBe(true)
})
})
describe('complex expression', function () {
it('should support value or value', async function () {
expect(await toPromise(create('false or true').evaluate(ctx, false))).toBe(true)
})
it('should support < and contains', async function () {
expect(await toPromise(create('1 < 2 and x contains "x"').evaluate(ctx, false))).toBe(false)
})
it('should support < or contains', async function () {
expect(await toPromise(create('1 < 2 or x contains "x"').evaluate(ctx, false))).toBe(true)
})
it('should support Drops for "x contains "x""', async () => {
class TemplateDrop extends Drop {
valueOf () { return 'X' }
}
const ctx = new Context({ x: 'XXX', X: new TemplateDrop() })
expect(await toPromise(create('x contains X').evaluate(ctx, false))).toBe(true)
})
it('should support value and !=', async function () {
const ctx = new Context({ empty: '' })
expect(await toPromise(create('empty and empty != ""').evaluate(ctx, false))).toBe(false)
})
it('should recognize quoted value', async function () {
expect(await toPromise(create('">"').evaluate(ctx, false))).toBe('>')
})
it('should evaluate from right to left', async function () {
expect(await toPromise(create('true or false and false').evaluate(ctx, false))).toBe(true)
expect(await toPromise(create('true and false and false or true').evaluate(ctx, false))).toBe(false)
})
it('should recognize property access', async function () {
const ctx = new Context({ obj: { foo: true } })
expect(await toPromise(create('obj["foo"] and true').evaluate(ctx, false))).toBe(true)
})
it('should allow nested property access', async function () {
const ctx = new Context({ obj: { foo: 'FOO' }, keys: { "what's this": 'foo' } })
expect(await toPromise(create('obj[keys["what\'s this"]]').evaluate(ctx, false))).toBe('FOO')
})
it('should support not', async function () {
expect(await toPromise(create('not 1 < 2').evaluate(ctx))).toBe(false)
})
it('not should have higher precedence than and/or', async function () {
expect(await toPromise(create('not 1 < 2 or not 1 > 2').evaluate(ctx))).toBe(true)
expect(await toPromise(create('not 1 < 2 and not 1 > 2').evaluate(ctx))).toBe(false)
})
})
describe('sync', function () {
it('should eval literal', function () {
expect(toValueSync(create('2.4').evaluate(ctx, false))).toBe(2.4)
})
it('should return false for "1==2"', () => {
expect(toValueSync(create('1==2').evaluate(ctx, false))).toBe(false)
})
it('should escape quote', function () {
const ctx = new Context({ quote: '"' })
expect(toValueSync(create('"\\"" == quote').evaluate(ctx, false))).toBe(true)
})
it('should allow nested property access', function () {
const ctx = new Context({ obj: { foo: 'FOO' }, keys: { "what's this": 'foo' } })
expect(toValueSync(create('obj[keys["what\'s this"]]').evaluate(ctx, false))).toBe('FOO')
})
})
})
+65
View File
@@ -0,0 +1,65 @@
import { Context } from '../context'
import { HTMLToken, TagToken } from '../tokens'
import { Render } from './render'
import { Tag, HTML } from '../template'
import { SimpleEmitter } from '../emitters'
import { toPromise } from '../util'
describe('render', function () {
let render: Render
beforeEach(function () {
render = new Render()
})
describe('.renderTemplates()', function () {
it('should render html', async function () {
const scope = new Context()
const token = { getContent: () => '<p>' } as HTMLToken
const html = await toPromise(render.renderTemplates([new HTML(token)], scope, new SimpleEmitter()))
return expect(html).toBe('<p>')
})
})
describe('.renderTemplatesToNodeStream()', function () {
it('should render to html stream', function (done) {
const scope = new Context()
const tpls = [
new HTML({ getContent: () => '<p>' } as HTMLToken),
new HTML({ getContent: () => '</p>' } as HTMLToken)
]
const stream = render.renderTemplatesToNodeStream(tpls, scope)
let result = ''
stream.on('data', (data) => {
result += data
})
stream.on('end', () => {
expect(result).toBe('<p></p>')
done()
})
})
it('should render to html stream asyncly', function (done) {
const scope = new Context()
class CustomTag extends Tag {
render () {
return new Promise(
resolve => setTimeout(() => resolve('async tag'), 10)
)
}
}
const tpls = [
new HTML({ getContent: () => '<p>' } as HTMLToken),
new CustomTag({ content: 'foo', args: '', name: 'foo' } as TagToken, [], {} as any),
new HTML({ getContent: () => '</p>' } as HTMLToken)
]
const stream = render.renderTemplatesToNodeStream(tpls, scope)
let result = ''
stream.on('data', (data) => {
result += data
})
stream.on('end', () => {
expect(result).toBe('<p>async tag</p>')
done()
})
})
})
})
+55
View File
@@ -0,0 +1,55 @@
import { Context } from '../context'
import { toPromise } from '../util'
import { IdentifierToken, NumberToken, QuotedToken } from '../tokens'
import { Filter } from './filter'
describe('filter', function () {
const ctx = new Context({ thirty: 30 })
const liquid = { testVersion: '1.0' } as any
it('should not change input if filter not registered', async function () {
const filter = new Filter('foo', undefined as any, [], liquid)
expect(await toPromise(filter.render('value', ctx))).toBe('value')
})
it('should call filter impl with correct arguments', async function () {
const spy = jest.fn()
const thirty = new NumberToken(new IdentifierToken('30', 0, 2), undefined)
const filter = new Filter('foo', spy, [thirty], liquid)
await toPromise(filter.render('foo', ctx))
expect(spy).toHaveBeenCalledWith('foo', 30)
})
it('should call filter impl with correct this', async function () {
const spy = jest.fn(function * (valStr, diff): Generator<string> {
const val = yield this.context._get([valStr])
return `${this.liquid.testVersion}: ${val + diff}`
})
const ten = new NumberToken(new IdentifierToken('10', 0, 2), undefined)
const filter = new Filter('add', spy, [ten], liquid)
const val = await toPromise(filter.render('thirty', ctx))
expect(val).toEqual('1.0: 40')
})
it('should render a simple filter', async function () {
expect(await toPromise(new Filter('upcase', (x: string) => x.toUpperCase(), [], liquid).render('foo', ctx))).toBe('FOO')
})
it('should render filters with argument', async function () {
const two = new NumberToken(new IdentifierToken('2', 0, 1), undefined)
expect(await toPromise(new Filter('add', (a: number, b: number) => a + b, [two], liquid).render(3, ctx))).toBe(5)
})
it('should render filters with multiple arguments', async function () {
const two = new NumberToken(new IdentifierToken('2', 0, 1), undefined)
const c = new QuotedToken('"c"', 0, 3)
expect(await toPromise(new Filter('add', (a: number, b: number, c: number) => a + b + c, [two, c], liquid).render(3, ctx))).toBe('5c')
})
it('should pass Objects/Drops as it is', async function () {
class Foo {}
expect(await toPromise(new Filter('name', (a: any) => a.constructor.name, [], liquid).render(new Foo(), ctx))).toBe('Foo')
})
it('should support key value pairs', async function () {
const two = new NumberToken(new IdentifierToken('2', 0, 1), undefined)
expect(await toPromise(new Filter('add', (a: number, b: number[]) => b[0] + ':' + (a + b[1]), [['num', two]], liquid).render(3, ctx))).toBe('num:5')
})
})
+42
View File
@@ -0,0 +1,42 @@
import { toPromise } from '../util'
import { Hash } from './hash'
import { Context } from '../context'
describe('Hash', function () {
it('should parse "reverse"', async function () {
const hash = await toPromise(new Hash('reverse').render(new Context({ foo: 3 })))
expect(hash).toHaveProperty('reverse')
expect(hash.reverse).toBeTruthy()
})
it('should parse "num:foo"', async function () {
const hash = await toPromise(new Hash('num:foo').render(new Context({ foo: 3 })))
expect(hash.num).toBe(3)
})
it('should parse "num:3"', async function () {
const hash = await toPromise(new Hash('num:3').render(new Context()))
expect(hash.num).toBe(3)
})
it('should parse "num: arr[0]"', async function () {
const hash = await toPromise(new Hash('num:3').render(new Context({ arr: [3] })))
expect(hash.num).toBe(3)
})
it('should parse "num: 2.3"', async function () {
const hash = await toPromise(new Hash('num:2.3').render(new Context()))
expect(hash.num).toBe(2.3)
})
it('should parse "num:bar.coo"', async function () {
const pending = new Hash('num:bar.coo').render(new Context({ bar: { coo: 3 } }))
const hash = await toPromise(pending)
expect(hash.num).toBe(3)
})
it('should parse "num1:2.3 reverse,num2:bar.coo\n num3: arr[0]"', async function () {
const ctx = new Context({ bar: { coo: 3 }, arr: [4] })
const hash = await toPromise(new Hash('num1:2.3 reverse,num2:bar.coo\n num3: arr[0]').render(ctx))
expect(hash).toEqual({
num1: 2.3,
reverse: true,
num2: 3,
num3: 4
})
})
})
+86
View File
@@ -0,0 +1,86 @@
import { toPromise } from '../util'
import { Context } from '../context'
import { Output } from '../template'
import { OutputToken } from '../tokens'
import { defaultOptions } from '../liquid-options'
describe('Output', function () {
const emitter: any = { write: (html: string) => (emitter.html += html), html: '' }
const liquid = { options: {} } as any
beforeEach(() => { emitter.html = '' })
it('should stringify objects', async function () {
const scope = new Context({
foo: { obj: { arr: ['a', 2] } }
})
const output = new Output({ content: 'foo' } as OutputToken, liquid)
await toPromise(output.render(scope, emitter))
return expect(emitter.html).toBe('[object Object]')
})
it('should skip function property', async function () {
const scope = new Context({ obj: { foo: 'foo', bar: (x: any) => x } })
const output = new Output({ content: 'obj' } as OutputToken, liquid)
await toPromise(output.render(scope, emitter))
return expect(emitter.html).toBe('[object Object]')
})
it('should respect to .toString()', async () => {
const scope = new Context({ obj: { toString: () => 'FOO' } })
const output = new Output({ content: 'obj' } as OutputToken, liquid)
await toPromise(output.render(scope, emitter))
return expect(emitter.html).toBe('FOO')
})
it('should respect to .toString()', async () => {
const scope = new Context({ obj: { toString: () => 'FOO' } })
const output = new Output({ content: 'obj' } as OutputToken, liquid)
await toPromise(output.render(scope, emitter))
return expect(emitter.html).toBe('FOO')
})
describe('when keepOutputType is enabled', () => {
const emitter: any = {
write: (html: any) => {
if (emitter.keepOutputType && typeof html !== 'string') {
emitter.html = html
} else {
emitter.html += html as string
}
},
html: '',
keepOutputType: true
}
beforeEach(() => { emitter.html = '' })
it('should respect output variable number type', async () => {
const scope = new Context({
foo: 42
}, { ...defaultOptions, keepOutputType: true })
const output = new Output({ content: 'foo' } as OutputToken, liquid)
await toPromise(output.render(scope, emitter))
return expect(emitter.html).toBe(42)
})
it('should respect output variable boolean type', async () => {
const scope = new Context({
foo: true
}, { ...defaultOptions, keepOutputType: true })
const output = new Output({ content: 'foo' } as OutputToken, liquid)
await toPromise(output.render(scope, emitter))
return expect(emitter.html).toBe(true)
})
it('should respect output variable object type', async () => {
const scope = new Context({
foo: 'test'
}, { ...defaultOptions, keepOutputType: true })
const output = new Output({ content: 'foo' } as OutputToken, liquid)
await toPromise(output.render(scope, emitter))
return expect(emitter.html).toBe('test')
})
it('should respect output variable string type', async () => {
const scope = new Context({
foo: { a: { b: 42 } }
}, { ...defaultOptions, keepOutputType: true })
const output = new Output({ content: 'foo' } as OutputToken, liquid)
await toPromise(output.render(scope, emitter))
return expect(emitter.html).toEqual({ a: { b: 42 } })
})
})
})
+37
View File
@@ -0,0 +1,37 @@
import { Liquid } from '../liquid'
import { QuotedToken } from '../tokens'
import { toPromise } from '../util'
import { Context } from '../context'
import { Value } from '../template'
describe('Value', function () {
const liquid = new Liquid()
describe('#constructor()', function () {
it('should parse filters in value content', function () {
const f = new Value('o | foo: a: "a"', liquid)
expect(f.filters[0].name).toBe('foo')
expect(f.filters[0].args).toHaveLength(1)
const [k, v] = f.filters[0].args[0] as any
expect(k).toBe('a')
expect(v).toBeInstanceOf(QuotedToken)
expect((v as QuotedToken).getText()).toBe('"a"')
})
})
describe('#value()', function () {
it('should call chained filters correctly', async function () {
const date = jest.fn(() => 'y')
const time = jest.fn()
liquid.registerFilter('date', date)
liquid.registerFilter('time', time)
const tpl = new Value('foo.bar | date: "b" | time:2', liquid)
const scope = new Context({
foo: { bar: 'bar' }
})
await toPromise(tpl.value(scope, false))
expect(date).toHaveBeenCalledWith('bar', 'b')
expect(time).toHaveBeenCalledWith('y', 2)
})
})
})
+14
View File
@@ -0,0 +1,14 @@
import { QuotedToken, PropertyAccessToken, IdentifierToken } from '.'
describe('PropertyAccessToken', function () {
describe('#propertyName', function () {
it('should return correct value for IdentifierToken', function () {
const token = new PropertyAccessToken(new IdentifierToken('foo', 0, 3), [], 3)
expect(token.propertyName).toBe('foo')
})
it('should return correct value for QuotedToken', function () {
const token = new PropertyAccessToken(new QuotedToken('"foo bar"', 0, 9), [], 9)
expect(token.propertyName).toBe('foo bar')
})
})
})
+16
View File
@@ -0,0 +1,16 @@
import { assert } from './assert'
describe('assert', function () {
it('should not throw if predicate is truthy', function () {
const fn = () => assert('foo', () => 'bar')
expect(fn).not.toThrow()
})
it('should not throw if predicate is truthy', function () {
const fn = () => assert('', () => 'bar')
expect(fn).toThrow(/bar/)
})
it('should populate default message', function () {
const fn = () => assert(false)
expect(fn).toThrow(/expect false to be true/)
})
})
+112
View File
@@ -0,0 +1,112 @@
import { toPromise, toValueSync } from './async'
describe('utils/async', () => {
describe('#toPromise()', function () {
it('should return a promise', async () => {
function * foo () {
return 'foo'
}
const result = await toPromise(foo())
expect(result).toBe('foo')
})
it('should support iterable with single return statement', async () => {
function * foo () {
return 'foo'
}
const result = await toPromise(foo())
expect(result).toBe('foo')
})
it('should support promise', async () => {
function foo () {
return Promise.resolve('foo')
}
const result = await toPromise(foo())
expect(result).toBe('foo')
})
it('should resolve dependency', async () => {
function * foo (): Generator<Generator<string>> {
return yield bar()
}
function * bar (): Generator<string> {
return 'bar'
}
const result = await toPromise(foo())
expect(result).toBe('bar')
})
it('should support promise dependency', async () => {
function * foo (): Generator<Promise<string>> {
return yield Promise.resolve('foo')
}
const result = await toPromise(foo())
expect(result).toBe('foo')
})
it('should reject Promise if dependency throws syncly', done => {
function * foo (): Generator<Generator<never>> {
return yield bar()
}
function * bar (): Generator<never> {
throw new Error('bar')
}
toPromise(foo()).catch(err => {
expect(err.message).toBe('bar')
done()
return 0 as any
})
})
it('should resume promise after catch', async () => {
function * foo () {
let ret = ''
try {
yield bar()
} catch (e) {
ret += 'bar'
}
ret += 'foo'
return ret
}
function * bar (): Generator<never> {
throw new Error('bar')
}
const ret = await toPromise(foo())
expect(ret).toBe('barfoo')
})
})
describe('#toValueSync()', function () {
it('should throw Error if dependency throws syncly', () => {
function * foo (): Generator<Generator<never>> {
return yield bar()
}
function * bar (): Generator<never> {
throw new Error('bar')
}
expect(() => toValueSync(foo())).toThrow('bar')
})
it('should resume yield after catch', () => {
function * foo (): Generator<unknown, never, never> {
try {
yield bar()
} catch (e) {}
return yield 'foo'
}
function * bar (): Generator<never> {
throw new Error('bar')
}
expect(toValueSync(foo())).toBe('foo')
})
it('should resume return after catch', () => {
function * foo (): Generator<Generator<never>, string> {
try {
yield bar()
} catch (e) {}
return 'foo'
}
function * bar (): Generator<never> {
throw new Error('bar')
}
expect(toValueSync(foo())).toBe('foo')
})
it('should return non iterator value as it is', () => {
expect(toValueSync('foo')).toBe('foo')
})
})
})
+217
View File
@@ -0,0 +1,217 @@
import { strftime as t } from './strftime'
import { DateWithTimezone } from '../../test/stub/date-with-timezone'
describe('util/strftime', function () {
const now = new Date('2016-01-04 13:15:23')
const then = new Date('2016-03-06 03:05:03')
describe('Date (Year, Month, Day)', () => {
it('should format %C as century', function () {
expect(t(now, '%C')).toBe('20')
})
it('should format %B as month name', function () {
expect(t(now, '%B')).toBe('January')
})
it('should format %e as space padded date', function () {
expect(t(now, '%e')).toBe(' 4')
})
it('should format %y as 2-digit year', function () {
expect(t(now, '%y')).toBe('16')
})
describe('%j', function () {
it('should format %j as day of year', function () {
expect(t(then, '%j')).toBe('066')
})
it('should take count of leap years', function () {
const date = new Date('2001 03 01')
expect(t(date, '%j')).toBe('060')
})
it('should take count of leap years', function () {
const date = new Date('2000 03 01')
expect(t(date, '%j')).toBe('061')
})
})
it('should format %q as date suffix', function () {
const st = new Date('2016-03-01 03:05:03')
const nd = new Date('2016-03-02 03:05:03')
const rd = new Date('2016-03-03 03:05:03')
expect(t(st, '%q')).toBe('st')
expect(t(nd, '%q')).toBe('nd')
expect(t(rd, '%q')).toBe('rd')
expect(t(now, '%q')).toBe('th')
})
})
describe('Time (Hour, Minute, Second, Subsecond)', function () {
it('should format %I as 0 padded hour12', function () {
expect(t(now, '%I')).toBe('01')
})
it('should format %I as 12 for 00:00', function () {
const date = new Date('2016-01-01 00:00:00')
expect(t(date, '%I')).toBe('12')
})
it('should format %k as space padded hour', function () {
expect(t(then, '%k')).toBe(' 3')
})
it('should format %l as space padded hour12', function () {
expect(t(now, '%l')).toBe(' 1')
})
it('should format %l as 12 for 00:00', function () {
const date = new Date('2016-01-01 00:00:00')
expect(t(date, '%l')).toBe('12')
})
it('should format %L as 0 padded millisecond', function () {
expect(t(then, '%L')).toBe('000')
})
it('should format %N as fractional seconds digits', function () {
const time = new Date('2019-12-15 01:21:00.129')
expect(t(time, '%N')).toBe('129000000')
expect(t(time, '%2N')).toBe('12')
expect(t(time, '%10N')).toBe('1290000000')
expect(t(time, '%0N')).toBe('129000000')
})
it('should format %p as upper cased am/pm', function () {
expect(t(now, '%p')).toBe('PM')
expect(t(then, '%p')).toBe('AM')
})
it('should format %P as lower cased am/pm', function () {
expect(t(now, '%P')).toBe('pm')
expect(t(now, '%^8P')).toBe(' PM')
expect(t(then, '%P')).toBe('am')
})
})
describe('Weekday', function () {
it('should format %A as Monday', function () {
expect(t(now, '%A')).toBe('Monday')
expect(t(now, '%^A')).toBe('MONDAY')
expect(t(now, '%#A')).toBe('MONDAY')
})
it('should format %a as Mon', function () {
expect(t(now, '%a')).toBe('Mon')
expect(t(now, '%^a')).toBe('MON')
})
it('should format %u as day of week(1-7)', function () {
expect(t(now, '%u')).toBe('1')
expect(t(then, '%u')).toBe('7')
})
it('should format %w as day of week(0-7)', function () {
expect(t(now, '%w')).toBe('1')
expect(t(then, '%w')).toBe('0')
})
})
describe('Seconds since the Unix Epoch', () => {
it('should format %s as UNIX seconds', function () {
expect(t(now, '%s')).toMatch(/\d+/)
})
})
describe('Week number', () => {
it('should format %U as week of year, starts with 0', function () {
expect(t(now, '%U')).toBe('01')
})
it('should format %W as week of year, starts with 1', function () {
expect(t(now, '%W')).toBe('01')
})
})
describe('Time zone', () => {
it('should format %z as time zone', function () {
// suppose we're in +8:00
const now = new DateWithTimezone('2016-01-04 13:15:23', -480)
expect(t(now, '%z')).toBe('+0800')
})
it('should format %z as negative time zone', function () {
// suppose we're in -8:00
const date = new DateWithTimezone('2016-01-04T13:15:23.000Z', 480)
expect(t(date, '%z')).toBe('-0800')
})
})
describe('combination', () => {
it('should format %x as local date string', function () {
expect(t(now, '%x')).toBe(now.toLocaleDateString())
})
it('should format %X as local time string', function () {
expect(t(now, '%X')).toBe(now.toLocaleTimeString())
})
it('should format detailed datetime', function () {
expect(t(now, '%Y-%m-%d %H:%M:%S')).toBe('2016-01-04 13:15:23')
})
it('should format %c as local string', function () {
expect(t(now, '%c')).toBe(now.toLocaleString())
})
})
describe('literal strings', () => {
it('should escape %% as %', function () {
expect(t(now, '%%')).toBe('%')
})
it('should escape %n as \\n', function () {
expect(t(now, '%n')).toBe('\n')
})
it('should escape %t as \\t', function () {
expect(t(now, '%t')).toBe('\t')
})
it('should retain un-recognized formaters', function () {
expect(t(now, '%o')).toBe('%o')
})
})
describe('width field', () => {
it('should support width field', () => {
expect(t(now, '%8Y')).toBe('00002016')
})
it('should ignore invalid width', () => {
expect(t(then, '%1Y')).toBe('2016')
expect(t(then, '%1H')).toBe('3')
})
it('should have higher priority than H', () => {
expect(t(then, '%0H')).toBe('03')
})
})
describe('modifier field', () => {
it('should ignore E modifier', () => {
expect(t(now, '%EY')).toBe('2016')
})
it('should ignore O modifier', () => {
expect(t(now, '%OY')).toBe('2016')
})
it('should support modifier with width field', () => {
expect(t(now, '%8EY')).toBe('00002016')
})
})
describe('flags field', () => {
it('should support - flag', () => {
expect(t(now, '%-m')).toBe('1')
})
it('should support _ flag', () => {
expect(t(now, '%_m')).toBe(' 1')
})
it('should support 0 flag', () => {
expect(t(now, '%0m')).toBe('01')
})
it('should support ^ flag', () => {
expect(t(now, '%^B')).toBe('JANUARY')
})
it('should respect to specific conversion', () => {
expect(t(now, '%^P')).toBe('PM')
expect(t(now, '%P')).toBe('pm')
})
it('should support # flag', () => {
expect(t(now, '%#B')).toBe('JANUARY')
expect(t(now, '%#P')).toBe('PM')
})
it('should support : flag', () => {
// suppose we're in +8:00
const date = new DateWithTimezone('2016-01-04T13:15:23.000Z', -480)
expect(t(date, '%:z')).toBe('+08:00')
expect(t(date, '%z')).toBe('+0800')
})
it('should support multiple flags', () => {
expect(t(now, '%^08P')).toBe('000000PM')
})
})
})
+37
View File
@@ -0,0 +1,37 @@
import { TimezoneDate } from './timezone-date'
describe('TimezoneDate', () => {
it('should respect timezone set to 00:00', () => {
const date = new TimezoneDate('2021-10-06T14:26:00.000+08:00', 0)
expect(date.getTimezoneOffset()).toBe(0)
expect(date.getHours()).toBe(6)
expect(date.getMinutes()).toBe(26)
})
it('should respect timezone set to -06:00', () => {
const date = new TimezoneDate('2021-10-06T14:26:00.000+08:00', -360)
expect(date.getTimezoneOffset()).toBe(-360)
expect(date.getMinutes()).toBe(26)
})
it('should support Date as argument', () => {
const date = new TimezoneDate(new Date('2021-10-06T14:26:00.000+08:00'), 0)
expect(date.getHours()).toBe(6)
})
it('should support .getMilliseconds()', () => {
const date = new TimezoneDate('2021-10-06T14:26:00.001+00:00', 0)
expect(date.getMilliseconds()).toBe(1)
})
it('should support .getDay()', () => {
const date = new TimezoneDate('2021-12-07T00:00:00.001+08:00', -480)
expect(date.getDay()).toBe(2)
})
it('should support .toLocaleTimeString()', () => {
const date = new TimezoneDate('2021-10-06T00:00:00.001+00:00', -480)
expect(date.toLocaleTimeString('en-US')).toBe('8:00:00 AM')
expect(() => date.toLocaleDateString()).not.toThrow()
})
it('should support .toLocaleDateString()', () => {
const date = new TimezoneDate('2021-10-06T22:00:00.001+00:00', -480)
expect(date.toLocaleDateString('en-US')).toBe('10/7/2021')
expect(() => date.toLocaleDateString()).not.toThrow()
})
})
+122
View File
@@ -0,0 +1,122 @@
import * as _ from './underscore'
describe('util/underscore', function () {
describe('.isString()', function () {
it('should return true for literal string', function () {
expect(_.isString('foo')).toBeTruthy()
})
it('should return true String instance', function () {
expect(_.isString(String('foo'))).toBeTruthy()
})
it('should return false for 123 ', function () {
expect(_.isString(123)).toBeFalsy()
})
})
describe('.isNumber()', function () {
it('should return false for "foo"', function () {
expect(_.isNumber('foo')).toBeFalsy()
})
it('should return true for 0', function () {
expect(_.isNumber(0)).toBeTruthy()
})
})
describe('.stringify()', function () {
it('should return "" for null', function () {
expect(_.stringify(null)).toBe('')
})
it('should return "" for undefined', function () {
expect(_.stringify(undefined)).toBe('')
})
it('should return regex string for RegExp', function () {
const reg = /foo/g
expect(_.stringify(reg)).toBe('/foo/g')
})
it('should return locale string for date', function () {
const date = new Date('2018-10-01T14:51:00.000Z')
// Mon Oct 01 2018 22:51:00 GMT+0800 (CST)
expect(_.stringify(date)).toBe(date.toString())
})
})
describe('.forOwn()', function () {
it('should iterate all properties', function () {
const spy = jest.fn()
const obj = {
foo: 'bar'
}
_.forOwn(obj, spy)
expect(spy).toHaveBeenCalledWith('bar', 'foo', obj)
})
it('should default to empty object', function () {
const spy = jest.fn()
_.forOwn(undefined, spy)
expect(spy).not.toHaveBeenCalled()
})
it('should not iterate over properties on prototype', function () {
const spy = jest.fn()
const obj = Object.create({
bar: 'foo'
})
obj.foo = 'bar'
_.forOwn(obj, spy)
expect(spy).toHaveBeenCalledTimes(1)
expect(spy).toHaveBeenCalledWith('bar', 'foo', obj)
})
it('should break when returned false', function () {
const spy = jest.fn(() => false)
_.forOwn({
'foo': 'foo',
'bar': 'foo'
}, spy)
expect(spy).toHaveBeenCalledTimes(1)
})
})
describe('.range()', function () {
it('should return a range of integers', function () {
expect(_.range(3, 5)).toEqual([3, 4])
})
})
describe('.isObject()', function () {
it('should return true for function', function () {
expect(_.isObject((x: any) => x)).toBeTruthy()
})
it('should return true for plain object', function () {
expect(_.isObject({})).toBeTruthy()
})
it('should return false for null', function () {
expect(_.isObject(null)).toBeFalsy()
})
it('should return false for number', function () {
expect(_.isObject(2)).toBeFalsy()
})
})
describe('.padEnd()', function () {
it('should default ch to " "', () => {
expect(_.padEnd('foo', 5)).toBe('foo ')
})
})
describe('.changeCase()', function () {
it('should to upper case if there is one lowercase', () => {
expect(_.changeCase('fooA')).toBe('FOOA')
})
it('should to lower case if all upper case', () => {
expect(_.changeCase('FOOA')).toBe('fooa')
})
})
describe('.caseInsensitiveCompare()', function () {
it('should "foo" > "bar"', () => {
expect(_.caseInsensitiveCompare('foo', 'bar')).toBe(1)
})
it('should "foo" < null', () => {
expect(_.caseInsensitiveCompare('foo', null)).toBe(-1)
})
it('should null > "foo"', () => {
expect(_.caseInsensitiveCompare(null, 'foo')).toBe(1)
})
it('should -1 < 0', () => {
expect(_.caseInsensitiveCompare(-1, 0)).toBe(-1)
})
it('should 1 > 0', () => {
expect(_.caseInsensitiveCompare(1, 0)).toBe(1)
})
})
})