test: 💍 add some tests for the cli

This commit is contained in:
Christian Kaisermann 2019-11-28 22:57:05 -03:00
parent 60ba73682f
commit 15ed03185a
5 changed files with 155 additions and 109 deletions

View File

@ -1,53 +1,35 @@
import {
Node,
ObjectExpression,
Property,
ImportDeclaration,
ImportSpecifier,
CallExpression,
Identifier,
Literal,
} from 'estree'
import delve from 'dlv'
import { walk } from 'estree-walker'
import { Ast } from 'svelte/types/compiler/interfaces'
import { parse } from 'svelte/compiler'
import { deepSet } from './includes/deepSet'
import { getObjFromExpression } from './includes/getObjFromExpression'
import { Message } from './types'
const LIB_NAME = 'svelte-i18n'
const DEFINE_MESSAGES_METHOD_NAME = 'defineMessages'
const FORMAT_METHOD_NAMES = new Set(['format', '_'])
interface Message {
node: Node
meta: {
id?: string
default?: string
[key: string]: any
}
}
const isNumberString = (n: string) => !Number.isNaN(parseInt(n))
function deepSet(obj: any, path: string, value: any) {
const parts = path.replace(/\[(\w+)\]/gi, '.$1').split('.')
return parts.reduce((ref, part, i) => {
if (part in ref) return (ref = ref[part])
if (i < parts.length - 1) {
if (isNumberString(parts[i + 1])) {
return (ref = ref[part] = [])
}
return (ref = ref[part] = {})
}
return (ref[part] = value)
}, obj)
}
const FORMAT_METHOD_NAMES = new Set(['format', '_', 't'])
const IGNORED_UTILITIES = new Set(['number', 'date', 'time'])
function isFormatCall(node: Node, imports: Set<string>) {
if (node.type !== 'CallExpression') return false
let identifier: Identifier
if (node.callee.type === 'MemberExpression') {
if (
node.callee.type === 'MemberExpression' &&
node.callee.property.type === 'Identifier' &&
!IGNORED_UTILITIES.has(node.callee.property.name)
) {
identifier = node.callee.object as Identifier
} else if (node.callee.type === 'Identifier') {
identifier = node.callee
@ -92,22 +74,6 @@ function getFormatSpecifiers(decl: ImportDeclaration) {
) as ImportSpecifier[]
}
function getObjFromExpression(exprNode: Node | ObjectExpression) {
if (exprNode.type !== 'ObjectExpression') return null
return exprNode.properties.reduce<Message>(
(acc, prop: Property) => {
// we only want primitives
if (prop.value.type !== 'Literal') return acc
if (prop.value.value !== Object(prop.value.value)) {
const key = (prop.key as Identifier).name as string
acc.meta[key] = prop.value.value
}
return acc
},
{ node: exprNode, meta: {} }
)
}
export function collectFormatCalls(ast: Ast) {
const importDecls = getLibImportDeclarations(ast)
@ -122,14 +88,14 @@ export function collectFormatCalls(ast: Ast) {
if (imports.size === 0) return []
const calls: CallExpression[] = []
function formatCallsWalker(node: Node) {
function enter(node: Node) {
if (isFormatCall(node, imports)) {
calls.push(node as CallExpression)
this.skip()
}
}
walk(ast.instance as any, { enter: formatCallsWalker })
walk(ast.html as any, { enter: formatCallsWalker })
walk(ast.instance as any, { enter })
walk(ast.html as any, { enter })
return calls
}
@ -157,7 +123,9 @@ export function collectMessageDefinitions(ast: Ast) {
})
return definitions.flatMap(definitionDict =>
definitionDict.properties.map(propNode => propNode.value)
definitionDict.properties.map(
propNode => propNode.value as ObjectExpression
)
)
}
@ -173,19 +141,18 @@ export function collectMessages(markup: string): Message[] {
return getObjFromExpression(pathNode)
}
if (pathNode.type !== 'Literal' || typeof pathNode.value !== 'string') {
return null
}
const node = pathNode as Literal
const id = node.value as string
if (options && options.type === 'ObjectExpression') {
const messageObj = getObjFromExpression(options)
messageObj.meta.id = pathNode.value
messageObj.meta.id = id
return messageObj
}
return {
node: pathNode,
meta: { id: pathNode.value },
node,
meta: { id },
}
}),
].filter(Boolean)

View File

@ -0,0 +1,17 @@
const isNumberString = (n: string) => !Number.isNaN(parseInt(n))
export function deepSet(obj: any, path: string, value: any) {
const parts = path.replace(/\[(\w+)\]/gi, '.$1').split('.')
return parts.reduce((ref, part, i) => {
if (part in ref) return (ref = ref[part])
if (i < parts.length - 1) {
if (isNumberString(parts[i + 1])) {
return (ref = ref[part] = [])
}
return (ref = ref[part] = {})
}
return (ref[part] = value)
}, obj)
}

View File

@ -0,0 +1,18 @@
import { ObjectExpression, Property, Identifier } from 'estree'
import { Message } from '../types'
export function getObjFromExpression(exprNode: ObjectExpression) {
return exprNode.properties.reduce<Message>(
(acc, prop: Property) => {
if (prop.value.type !== 'Literal') return acc
// we only want primitives
if (prop.value.value !== Object(prop.value.value)) {
const key = (prop.key as Identifier).name as string
acc.meta[key] = prop.value.value
}
return acc
},
{ node: exprNode, meta: {} }
)
}

10
src/cli/types/index.ts Normal file
View File

@ -0,0 +1,10 @@
import { Node } from 'estree'
export interface Message {
node: Node
meta: {
id?: string
default?: string
[key: string]: any
}
}

View File

@ -10,13 +10,27 @@ import {
} from '../../src/cli/extract'
describe('collecting format calls', () => {
it('should return nothing if there are no imports from the library', () => {
const ast = parse(`<script>const $_ = () => 0; $_();</script>`)
test('returns nothing if there are no imports', () => {
const ast = parse(`<script>
import Foo from 'foo';
const $_ = () => 0; $_();
</script>`)
const calls = collectFormatCalls(ast)
expect(calls).toHaveLength(0)
})
it('should collect all format calls in the instance script', () => {
test('returns nothing if there are no format imports', () => {
const ast = parse(
`<script>
import { init } from 'svelte-i18n';
init({})
</script>`
)
const calls = collectFormatCalls(ast)
expect(calls).toHaveLength(0)
})
test('collects all format calls in the instance script', () => {
const ast = parse(`<script>
import { format, _ } from 'svelte-i18n'
$format('foo')
@ -30,24 +44,12 @@ describe('collecting format calls', () => {
expect(calls[1]).toMatchObject({ type: 'CallExpression' })
})
it('should collect all format calls with renamed imports', () => {
test('collects all format calls with renamed imports', () => {
const ast = parse(`<script>
import { format as _x, _ as intl } from 'svelte-i18n'
import { format as _x, _ as intl, t as f } from 'svelte-i18n'
$_x('foo')
$intl({ id: 'bar' })
</script>`)
const calls = collectFormatCalls(ast)
expect(calls).toHaveLength(2)
expect(calls[0]).toMatchObject({ type: 'CallExpression' })
expect(calls[1]).toMatchObject({ type: 'CallExpression' })
})
it('should collect all format utility calls', () => {
const ast = parse(`<script>
import { _ } from 'svelte-i18n'
$_.title('foo')
$_.capitalize({ id: 'bar' })
$_.number(10000)
$f({ id: 'bar' })
</script>`)
const calls = collectFormatCalls(ast)
expect(calls).toHaveLength(3)
@ -55,20 +57,47 @@ describe('collecting format calls', () => {
expect(calls[1]).toMatchObject({ type: 'CallExpression' })
expect(calls[2]).toMatchObject({ type: 'CallExpression' })
})
test('collects all string format utility calls', () => {
const ast = parse(`<script>
import { _ } from 'svelte-i18n'
$_.title('foo')
$_.capitalize({ id: 'bar' })
$_.lower({ id: 'bar' })
$_.upper({ id: 'bar' })
</script>`)
const calls = collectFormatCalls(ast)
expect(calls).toHaveLength(4)
expect(calls[0]).toMatchObject({ type: 'CallExpression' })
expect(calls[1]).toMatchObject({ type: 'CallExpression' })
expect(calls[2]).toMatchObject({ type: 'CallExpression' })
expect(calls[3]).toMatchObject({ type: 'CallExpression' })
})
test('ignores date, time and number calls', () => {
const ast = parse(`<script>
import { _ } from 'svelte-i18n'
$_.number(1000)
$_.date(new Date())
$_.time(new Date())
</script>`)
const calls = collectFormatCalls(ast)
expect(calls).toHaveLength(0)
})
})
describe('collecting message definitions', () => {
it('should return nothing if there are no imports from the library', () => {
test('returns nothing if there are no imports from the library', () => {
const ast = parse(
`<script>
import foo from 'foo';
import { dictionary } from 'svelte-i18n';
</script>`,
</script>`
)
expect(collectMessageDefinitions(ast)).toHaveLength(0)
})
it('should get all message definition objects', () => {
test('gets all message definition objects', () => {
const ast = parse(`<script>
import { defineMessages } from 'svelte-i18n';
defineMessages({ foo: { id: 'foo' }, bar: { id: 'bar' } })
@ -84,10 +113,11 @@ describe('collecting message definitions', () => {
})
describe('collecting messages', () => {
it('should collect all messages in both instance and html ASTs', () => {
test('collects all messages in both instance and html ASTs', () => {
const markup = `
<script>
import { _, defineMessages } from 'svelte-i18n';
console.log($_({ id: 'foo' }))
console.log($_.title({ id: 'page.title' }))
@ -96,10 +126,13 @@ describe('collecting messages', () => {
disabled: { id: 'disabled', default: 'Disabled' }
})
</script>
<div>{$_('msg_1')}</div>
<div>{$_({id: 'msg_2'})}</div>
<div>{$_('msg_3', { default: 'Message'})}</div>`
const messages = collectMessages(markup)
expect(messages).toHaveLength(7)
expect(messages).toEqual(
expect.arrayContaining([
@ -114,15 +147,16 @@ describe('collecting messages', () => {
expect.objectContaining({
meta: { id: 'enabled', default: 'Enabled' },
}),
]),
])
)
})
})
describe('messages extraction', () => {
it('should return a object built based on all found message paths', () => {
const markup = `
<script>import { _ } from 'svelte-i18n';</script>
test('returns a object built based on all found message paths', () => {
const markup = `<script>
import { _ } from 'svelte-i18n';
</script>
<h1>{$_.title('title')}</h1>
<h2>{$_({ id: 'subtitle'})}</h2>
@ -131,16 +165,16 @@ describe('messages extraction', () => {
expect(dict).toMatchObject({ title: '', subtitle: '' })
})
it('creates deep nested properties', () => {
test('creates deep nested properties', () => {
const markup = `
<script>import { _ } from 'svelte-i18n';</script>
<h1>{$_.title('home.page.title')}</h1>
<h2>{$_({ id: 'home.page.subtitle'})}</h2>
<ul>
<li>{$_('list[0]')}</li>
<li>{$_('list[1]')}</li>
<li>{$_('list[2]')}</li>
<li>{$_('list.0')}</li>
<li>{$_('list.1')}</li>
<li>{$_('list.2')}</li>
</ul>
`
const dict = extractMessages(markup)
@ -150,38 +184,38 @@ describe('messages extraction', () => {
})
})
it('creates a shallow dictionary', () => {
test('creates a shallow dictionary', () => {
const markup = `
<script>import { _ } from 'svelte-i18n';</script>
<h1>{$_.title('home.page.title')}</h1>
<h2>{$_({ id: 'home.page.subtitle'})}</h2>
<ul>
<li>{$_('list[0]')}</li>
<li>{$_('list[1]')}</li>
<li>{$_('list[2]')}</li>
<li>{$_('list.0')}</li>
<li>{$_('list.1')}</li>
<li>{$_('list.2')}</li>
</ul>
`
const dict = extractMessages(markup, { shallow: true })
expect(dict).toMatchObject({
'home.page.title': '',
'home.page.subtitle': '',
'list[0]': '',
'list[1]': '',
'list[2]': '',
'list.0': '',
'list.1': '',
'list.2': '',
})
})
it('allow to pass a initial dictionary and only append non-existing props', () => {
test('allow to pass a initial dictionary and only append non-existing props', () => {
const markup = `
<script>import { _ } from 'svelte-i18n';</script>
<h1>{$_.title('home.page.title')}</h1>
<h2>{$_({ id: 'home.page.subtitle'})}</h2>
<ul>
<li>{$_('list[0]')}</li>
<li>{$_('list[1]')}</li>
<li>{$_('list[2]')}</li>
<li>{$_('list.0')}</li>
<li>{$_('list.1')}</li>
<li>{$_('list.2')}</li>
</ul>
`
const dict = extractMessages(markup, {
@ -205,7 +239,7 @@ describe('messages extraction', () => {
})
})
it('allow to pass a initial dictionary and only append shallow non-existing props', () => {
test('allow to pass a initial dictionary and only append shallow non-existing props', () => {
const markup = `
<script>import { _ } from 'svelte-i18n';</script>