Skip to content

Commit 3889d89

Browse files
committed
add diretive unit tests
1 parent 36da9f5 commit 3889d89

File tree

4 files changed

+297
-17
lines changed

4 files changed

+297
-17
lines changed

src/directive.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { Locale, TranslateOptions } from './core'
1010
import { NamedValue } from './message/runtime'
1111
import { I18nWarnCodes, getWarnMessage } from './warnings'
1212
import { isString, isPlainObject, isNumber, warn } from './utils'
13+
import { createI18nError, I18nErrorCodes } from './errors'
1314

1415
type VTDirectiveValue = {
1516
path: string
@@ -39,23 +40,22 @@ export function vTDirective(
3940
el: HTMLElement,
4041
{ instance, value, modifiers }: DirectiveBinding
4142
): void => {
43+
/* istanbul ignore if */
4244
if (!instance || !instance.$) {
43-
// TODO: should be raise Vue error
44-
throw new Error('TODO')
45+
throw createI18nError(I18nErrorCodes.UNEXPECTED_ERROR)
4546
}
4647

4748
const composer = getComposer(i18n, instance.$)
4849
if (!composer) {
49-
// TODO: should be error
50-
throw new Error('TODO')
50+
throw createI18nError(I18nErrorCodes.NOT_FOUND_COMPOSER)
5151
}
5252

5353
if (__DEV__ && modifiers.preserve) {
5454
warn(getWarnMessage(I18nWarnCodes.NOT_SUPPORTED_PRESERVE))
5555
}
5656

5757
const parsedValue = parseValue(value)
58-
el.innerText = composer.t(...makeParams(parsedValue))
58+
el.textContent = composer.t(...makeParams(parsedValue))
5959
}
6060

6161
return {
@@ -69,11 +69,11 @@ function parseValue(value: unknown): VTDirectiveValue {
6969
return { path: value }
7070
} else if (isPlainObject(value)) {
7171
if (!('path' in value)) {
72-
throw new Error('TODO')
72+
throw createI18nError(I18nErrorCodes.REQUIRED_VALUE, 'path')
7373
}
7474
return value as VTDirectiveValue
7575
} else {
76-
throw new Error('TODO')
76+
throw createI18nError(I18nErrorCodes.INVALID_VALUE)
7777
}
7878
}
7979

src/errors.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,29 @@ export const enum I18nErrorCodes {
1717
NOT_INSLALLED,
1818
UNEXPECTED_ERROR,
1919
NOT_AVAILABLE_IN_LEGACY_MODE,
20+
// diretive module errors
21+
REQUIRED_VALUE,
22+
INVALID_VALUE,
23+
NOT_FOUND_COMPOSER,
2024
// for enhancement
2125
__EXTEND_POINT__
2226
}
2327

24-
export function createI18nError(code: I18nErrorCodes): I18nError {
28+
export function createI18nError(code: I18nErrorCodes, ...args: unknown[]): I18nError {
2529
return createCompileError(
2630
code,
2731
null,
28-
__DEV__ ? { messages: errorMessages } : undefined
32+
__DEV__ ? { messages: errorMessages, args } : undefined
2933
)
3034
}
3135

3236
export const errorMessages: { [code: number]: string } = {
3337
[I18nErrorCodes.UNEXPECTED_RETURN_TYPE]: 'Unexpected return type in composer',
3438
[I18nErrorCodes.INVALID_ARGUMENT]: 'Invalid argument',
3539
[I18nErrorCodes.NOT_INSLALLED]: 'Need to install with use function',
36-
[I18nErrorCodes.UNEXPECTED_ERROR]: 'Unexpeced error in useI18n',
37-
[I18nErrorCodes.NOT_AVAILABLE_IN_LEGACY_MODE]: 'Not available in legacy mode'
40+
[I18nErrorCodes.UNEXPECTED_ERROR]: 'Unexpeced error',
41+
[I18nErrorCodes.NOT_AVAILABLE_IN_LEGACY_MODE]: 'Not available in legacy mode',
42+
[I18nErrorCodes.REQUIRED_VALUE]: `Required in value: {0}`,
43+
[I18nErrorCodes.INVALID_VALUE]: `Invalid value`,
44+
[I18nErrorCodes.NOT_FOUND_COMPOSER]: `Not found Composer`
3845
}

test/diretive.test.ts

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
/**
2+
* @jest-environment jsdom
3+
*/
4+
5+
/* eslint-disable @typescript-eslint/no-empty-function */
6+
7+
// utils
8+
jest.mock('../src/utils', () => ({
9+
...jest.requireActual('../src/utils'),
10+
warn: jest.fn()
11+
}))
12+
import { warn } from '../src/utils'
13+
14+
import { mount } from './helper'
15+
import { defineComponent, ref, h, withDirectives, resolveDirective } from 'vue'
16+
import { createI18n } from '../src/i18n'
17+
import { errorMessages, I18nErrorCodes } from '../src/errors'
18+
import { getWarnMessage, I18nWarnCodes } from '../src/warnings'
19+
import { format } from '../src/utils'
20+
21+
describe('basic', () => {
22+
test('literal', async () => {
23+
const i18n = createI18n({
24+
locale: 'en',
25+
messages: {
26+
en: {
27+
hello: 'hello!'
28+
}
29+
}
30+
})
31+
32+
const App = defineComponent({
33+
setup() {
34+
// <p v-t="'hello'"></p>
35+
const t = resolveDirective('t')
36+
return () => {
37+
return withDirectives(h('p'), [[t, 'hello']])
38+
}
39+
}
40+
})
41+
const wrapper = await mount(App, i18n)
42+
43+
expect(wrapper.html()).toEqual('<p>hello!</p>')
44+
})
45+
46+
test('binding', async () => {
47+
const i18n = createI18n({
48+
locale: 'en',
49+
messages: {
50+
en: {
51+
hello: 'hello!'
52+
}
53+
}
54+
})
55+
56+
const App = defineComponent({
57+
setup() {
58+
// <p v-t="msg"></p>
59+
const msg = ref('hello')
60+
const t = resolveDirective('t')
61+
return () => {
62+
return withDirectives(h('p'), [[t, msg.value]])
63+
}
64+
}
65+
})
66+
const wrapper = await mount(App, i18n)
67+
68+
expect(wrapper.html()).toEqual('<p>hello!</p>')
69+
})
70+
})
71+
72+
test('object literal', async () => {
73+
const i18n = createI18n({
74+
locale: 'en',
75+
messages: {
76+
en: {
77+
hello: 'hello, {name}!'
78+
},
79+
ja: {
80+
hello: 'こんにちは、{name}!'
81+
}
82+
}
83+
})
84+
85+
const App = defineComponent({
86+
setup() {
87+
// <p v-t="{ path: 'hello', locale: 'ja', args: { name } }"></p>
88+
const name = ref('kazupon')
89+
const t = resolveDirective('t')
90+
return () => {
91+
return withDirectives(h('p'), [[t, { path: 'hello', locale: 'ja', args: { name: name.value } }]])
92+
}
93+
}
94+
})
95+
const wrapper = await mount(App, i18n)
96+
97+
expect(wrapper.html()).toEqual('<p>こんにちは、kazupon!</p>')
98+
})
99+
100+
test('plural', async () => {
101+
const i18n = createI18n({
102+
locale: 'en',
103+
messages: {
104+
en: {
105+
banana: 'no bananas | {n} banana | {n} bananas'
106+
}
107+
}
108+
})
109+
110+
const App = defineComponent({
111+
setup() {
112+
// <p v-t="{ path: 'banana', choice: 2 }"></p>
113+
const t = resolveDirective('t')
114+
return () => {
115+
return withDirectives(h('p'), [[t, { path: 'banana', choice: 2 }]])
116+
}
117+
}
118+
})
119+
const wrapper = await mount(App, i18n)
120+
121+
expect(wrapper.html()).toEqual('<p>2 bananas</p>')
122+
})
123+
124+
test('preserve modifier', async () => {
125+
const mockWarn = warn as jest.MockedFunction<typeof warn>
126+
mockWarn.mockImplementation(() => {})
127+
128+
const i18n = createI18n({
129+
locale: 'en',
130+
messages: {
131+
en: {
132+
hello: 'hello!'
133+
}
134+
}
135+
})
136+
137+
const App = defineComponent({
138+
setup() {
139+
// <p v-t.preserve="'hello'"></p>
140+
const t = resolveDirective('t')
141+
return () => {
142+
return withDirectives(h('p'), [[t, 'hello', '', { preserve: true }]])
143+
}
144+
}
145+
})
146+
const wrapper = await mount(App, i18n)
147+
148+
expect(mockWarn).toHaveBeenCalledTimes(1)
149+
expect(mockWarn.mock.calls[0][0]).toEqual(getWarnMessage(I18nWarnCodes.NOT_SUPPORTED_PRESERVE))
150+
})
151+
152+
test('legacy mode', async () => {
153+
const i18n = createI18n({
154+
legacy: true,
155+
locale: 'en',
156+
messages: {
157+
en: {
158+
hello: 'hello!'
159+
}
160+
}
161+
})
162+
163+
const App = defineComponent({
164+
setup() {
165+
// <p v-t="'hello'"></p>
166+
const t = resolveDirective('t')
167+
return () => {
168+
return withDirectives(h('p'), [[t, 'hello']])
169+
}
170+
}
171+
})
172+
const wrapper = await mount(App, i18n)
173+
174+
expect(wrapper.html()).toEqual('<p>hello!</p>')
175+
})
176+
177+
describe('errors', () => {
178+
test(errorMessages[I18nErrorCodes.NOT_FOUND_COMPOSER], async () => {
179+
const i18n = createI18n({
180+
locale: 'en',
181+
messages: {
182+
en: {
183+
hello: 'hello!'
184+
}
185+
}
186+
})
187+
const spy = jest.spyOn(i18n, 'global', 'get')
188+
spy.mockImplementation(() => null)
189+
190+
const App = defineComponent({
191+
setup() {
192+
const t = resolveDirective('t')
193+
return () => {
194+
return withDirectives(h('p'), [[t, { locale: 'ja' }]])
195+
}
196+
}
197+
})
198+
199+
let error: Error | null = null
200+
try {
201+
await mount(App, i18n)
202+
} catch (e) {
203+
error = e
204+
}
205+
expect(error.message).toEqual(errorMessages[I18nErrorCodes.NOT_FOUND_COMPOSER])
206+
})
207+
208+
test(errorMessages[I18nErrorCodes.REQUIRED_VALUE], async () => {
209+
const i18n = createI18n({
210+
locale: 'en',
211+
messages: {
212+
en: {
213+
hello: 'hello!'
214+
}
215+
}
216+
})
217+
218+
const App = defineComponent({
219+
setup() {
220+
// <p v-t="{ locale: 'ja' }"></p>
221+
const t = resolveDirective('t')
222+
return () => {
223+
return withDirectives(h('p'), [[t, { locale: 'ja' }]])
224+
}
225+
}
226+
})
227+
228+
let error: Error | null = null
229+
try {
230+
await mount(App, i18n)
231+
} catch (e) {
232+
error = e
233+
}
234+
expect(error.message).toEqual(format(errorMessages[I18nErrorCodes.REQUIRED_VALUE], 'path'))
235+
})
236+
237+
test(errorMessages[I18nErrorCodes.INVALID_VALUE], async () => {
238+
const i18n = createI18n({
239+
locale: 'en',
240+
messages: {
241+
en: {
242+
hello: 'hello!'
243+
}
244+
}
245+
})
246+
247+
const App = defineComponent({
248+
setup() {
249+
// <p v-t="1"></p>
250+
const t = resolveDirective('t')
251+
return () => withDirectives(h('p'), [[t, 1]])
252+
}
253+
})
254+
255+
let error: Error | null = null
256+
try {
257+
await mount(App, i18n)
258+
} catch (e) {
259+
error = e
260+
}
261+
expect(error.message).toEqual(errorMessages[I18nErrorCodes.INVALID_VALUE])
262+
})
263+
})
264+
265+
/* eslint-enable @typescript-eslint/no-empty-function */

0 commit comments

Comments
 (0)