UNPKG

12.7 kBJavaScriptView Raw
1import { mount } from '@vue/test-utils';
2import sprintf from './sprintf.vue';
3
4describe('sprintf component', () => {
5 let wrapper;
6 const objectPrototypeNames = Object.getOwnPropertyNames(Object.prototype).filter((name) =>
7 /^[a-z]/i.test(name)
8 );
9
10 const createComponent = (template = '', data = () => ({})) => {
11 wrapper = mount({
12 template: `<div class="wrapper">${template}</div>`,
13 components: {
14 sprintf,
15 },
16 data,
17 });
18 };
19
20 afterEach(() => {
21 wrapper.destroy();
22 });
23
24 describe('plain placeholders', () => {
25 it.each`
26 message
27 ${''}
28 ${'Foo'}
29 ${'%{author}'}
30 ${'Written by %{author}'}
31 ${'Written by %{author-name}'}
32 ${'Written by %{author1}'}
33 ${'Written by %{author_name}'}
34 `('should return message if slots have no data', ({ message }) => {
35 createComponent(`<sprintf message="${message}"/>`);
36
37 expect(wrapper.element.innerHTML).toBe(message);
38 });
39
40 it.each`
41 message | html
42 ${'%{author}'} | ${'<span>Author</span>'}
43 ${'Written by %{author}'} | ${'Written by <span>Author</span>'}
44 ${'Foo %{author} bar'} | ${'Foo <span>Author</span> bar'}
45 ${' %{author} '} | ${' <span>Author</span> '}
46 ${'%{author}%{author}'} | ${'<span>Author</span><span>Author</span>'}
47 ${'%{author} known as %{author-name}'} | ${'<span>Author</span> known as <span>John Doe</span>'}
48 ${'%{author1}'} | ${'<span>Author #1</span>'}
49 ${'%{author_name}'} | ${'<span>Author Name</span>'}
50 `('should replace placeholder with component', ({ message, html }) => {
51 createComponent(
52 `<sprintf message="${message}">
53 <template #author>
54 <span>Author</span>
55 </template>
56 <template #author-name>
57 <span>John Doe</span>
58 </template>
59 <template #author1>
60 <span>Author #1</span>
61 </template>
62 <template #author_name>
63 <span>Author Name</span>
64 </template>
65 </sprintf>`
66 );
67
68 expect(wrapper.element.innerHTML).toBe(html);
69 });
70
71 it('should be able to re-use a placeholder multiple times', () => {
72 createComponent(
73 `<sprintf message="%{author} is an excellent %{author}">
74 <template #author>
75 <span>Author</span>
76 </template>
77 </sprintf>`
78 );
79
80 expect(wrapper.element.innerHTML).toBe(
81 '<span>Author</span> is an excellent <span>Author</span>'
82 );
83 });
84
85 it('should be able to use templates as slots', () => {
86 createComponent(
87 `<sprintf message="Written by %{author}">
88 <template #author>Author</template>
89 </sprintf>`
90 );
91
92 expect(wrapper.element.innerHTML).toBe('Written by Author');
93 });
94
95 it('should work with a default slot', () => {
96 createComponent(
97 `<sprintf message="Written by %{default}">
98 <template>Author</template>
99 </sprintf>`
100 );
101
102 expect(wrapper.element.innerHTML).toBe('Written by Author');
103 });
104
105 describe('Object prototype names', () => {
106 it.each(objectPrototypeNames)(
107 'does not use Object.prototype.%s as slot if slot is not provided',
108 (prototypeName) => {
109 createComponent(`<sprintf message="%{${prototypeName}}"></sprintf>`);
110
111 expect(wrapper.element.innerHTML).toBe(`%{${prototypeName}}`);
112 }
113 );
114
115 it.each(objectPrototypeNames)('can use provided slot named "%s"', (prototypeName) => {
116 createComponent(
117 `<sprintf message="%{${prototypeName}}">
118 <template #${prototypeName}>${prototypeName} OK!</template>
119 </sprintf>`
120 );
121
122 expect(wrapper.element.innerHTML).toBe(`${prototypeName} OK!`);
123 });
124 });
125 });
126
127 describe('start/end placeholders', () => {
128 it('should work', () => {
129 createComponent(
130 `<sprintf message="Click %{linkStart}here%{linkEnd}, please">
131 <template #link="{ content }">
132 <a href="#">{{ content }}</a>
133 </template>
134 </sprintf>`
135 );
136
137 expect(wrapper.element.innerHTML).toBe('Click <a href="#">here</a>, please');
138 });
139
140 it('should work with a default slot', () => {
141 createComponent(
142 `<sprintf message="Foo %{defaultStart}default%{defaultEnd} baz">
143 <template #default="{ content }">{{ content }}</template>
144 </sprintf>`
145 );
146
147 expect(wrapper.element.innerHTML).toBe('Foo default baz');
148 });
149
150 it('does not render start/end content if slot does not consume it', () => {
151 createComponent(
152 `<sprintf message="Click %{linkStart}here%{linkEnd}, please">
153 <template #link>
154 <a href="#">foo</a>
155 </template>
156 </sprintf>`
157 );
158
159 expect(wrapper.element.innerHTML).toBe('Click <a href="#">foo</a>, please');
160 });
161
162 it('can interpolate multiple start/end placeholders', () => {
163 createComponent(
164 `<sprintf message="Foo %{barStart}bar%{barEnd} %{quxStart}qux%{quxEnd} baz">
165 <template #bar="{ content }">
166 <a>{{ content }}</a>
167 </template>
168 <template #qux="{ content }">
169 <b>{{ content }}</b>
170 </template>
171 </sprintf>`
172 );
173
174 expect(wrapper.element.innerHTML).toBe('Foo <a>bar</a> <b>qux</b> baz');
175 });
176
177 it('treats out-of-order start/end placeholders as plain slots', () => {
178 createComponent(
179 `<sprintf message="Foo %{barEnd}bar%{barStart} qux">
180 <template #bar="{ content }">
181 <a>{{ content }} fail if in output!</a>
182 </template>
183 <template #barStart>
184 <b>barStart</b>
185 </template>
186 <template #barEnd>
187 <i>barEnd</i>
188 </template>
189 </sprintf>`
190 );
191
192 expect(wrapper.element.innerHTML).toBe('Foo <i>barEnd</i>bar<b>barStart</b> qux');
193 });
194
195 it('should handle start/end placeholders at the beginning and end of the message', () => {
196 createComponent(
197 `<sprintf message="%{fooStart}bar%{fooEnd}">
198 <template #foo="{ content }"><b>{{ content }}</b></template>
199 </sprintf>`
200 );
201
202 expect(wrapper.element.innerHTML).toBe('<b>bar</b>');
203 });
204
205 it('treats a start placeholder without an end as a plain placeholder', () => {
206 createComponent(
207 `<sprintf message="foo %{barStart} baz">
208 <template #barStart>start</template>
209 </sprintf>`
210 );
211
212 expect(wrapper.element.innerHTML).toBe('foo start baz');
213 });
214
215 it('treats an end placeholder without a start as a plain placeholder', () => {
216 createComponent(
217 `<sprintf message="foo %{barEnd} baz">
218 <template #barEnd>end</template>
219 </sprintf>`
220 );
221
222 expect(wrapper.element.innerHTML).toBe('foo end baz');
223 });
224
225 it('should not interpolate more than one level deep, even if slots are provided', () => {
226 createComponent(
227 `<sprintf message="foo %{spanStart}foo %{baz} %{strongStart}strong%{strongEnd}%{spanEnd}">
228 <template #span="{ content }"><span>{{ content }}</span></template>
229 <template #baz>baz</template>
230 <template #strong="{ content }"><strong>{{ content }}</strong></template>
231 </sprintf>`
232 );
233
234 expect(wrapper.element.innerHTML).toBe(
235 'foo <span>foo %{baz} %{strongStart}strong%{strongEnd}</span>'
236 );
237 });
238
239 it('works with no content between start/end placeholders', () => {
240 createComponent(
241 `<sprintf message="foo %{barStart}%{barEnd} baz">
242 <template #bar="{ content }"><i>{{ content }}</i></template>
243 </sprintf>`
244 );
245
246 expect(wrapper.element.innerHTML).toBe('foo <i></i> baz');
247 });
248
249 it('returns the message if slot is not provided', () => {
250 createComponent(`<sprintf message="Click %{linkStart}here%{linkEnd}"></sprintf>`);
251
252 expect(wrapper.element.innerHTML).toBe('Click %{linkStart}here%{linkEnd}');
253 });
254
255 it('works with the example in the documentation', () => {
256 // From: https://gitlab.com/gitlab-org/gitlab/blob/v12.6.4-ee/doc/development/i18n/externalization.md#L300-303
257 createComponent(
258 `<sprintf message="Learn more about %{linkStart}zones%{linkEnd}">
259 <template #link="{ content }">
260 <a
261 href="https://cloud.google.com/compute/docs/regions-zones/regions-zones"
262 target="_blank"
263 rel="noopener noreferrer"
264 >{{ content }}</a>
265 </template>
266 </sprintf>`
267 );
268
269 expect(wrapper.element.innerHTML).toBe(
270 'Learn more about <a href="https://cloud.google.com/compute/docs/regions-zones/regions-zones" target="_blank" rel="noopener noreferrer">zones</a>'
271 );
272 });
273
274 it('resists XSS attacks', () => {
275 createComponent(
276 `<sprintf message="Click %{linkStart}<script>alert('hello')</script>%{linkEnd}, please">
277 <template #link="{ content }">
278 <a href="#">{{ content }}</a>
279 </template>
280 </sprintf>`
281 );
282
283 expect(wrapper.element.innerHTML).toBe(
284 'Click <a href="#">&lt;script&gt;alert(\'hello\')&lt;/script&gt;</a>, please'
285 );
286 });
287
288 describe('Object prototype names', () => {
289 it.each(objectPrototypeNames)(
290 'does not use Object.prototype.%s as slot if slot is not provided',
291 (prototypeName) => {
292 createComponent(
293 `<sprintf message="%{${prototypeName}Start} foo %{${prototypeName}End}"></sprintf>`
294 );
295
296 expect(wrapper.element.innerHTML).toBe(
297 `%{${prototypeName}Start} foo %{${prototypeName}End}`
298 );
299 }
300 );
301
302 it.each(objectPrototypeNames)('can use provided slot named "%s"', (prototypeName) => {
303 createComponent(
304 `<sprintf message="%{${prototypeName}Start}foo%{${prototypeName}End}">
305 <template #${prototypeName}="{ content }">{{ content }}</template>
306 </sprintf>`
307 );
308
309 expect(wrapper.element.innerHTML).toBe('foo');
310 });
311 });
312
313 describe('given custom placeholder start/end markers', () => {
314 it.each`
315 message | placeholders | expectedHtml
316 ${'%{aStart}foo%{aEnd}'} | ${undefined} | ${'<a>foo</a>'}
317 ${'%{aStart}foo%{aEnd}'} | ${{ a: ['aStart', 'aEnd'] }} | ${'<a>foo</a>'}
318 ${'%{start}foo%{end}'} | ${{ a: ['start', 'end'] }} | ${'<a>foo</a>'}
319 ${'%{bold}foo%{bold_end}'} | ${{ bold: ['bold', 'bold_end'] }} | ${'<b>foo</b>'}
320 ${'%{link_start}foo%{link_end}, %{open_bold}bar%{close}'} | ${{ a: ['link_start', 'link_end'], bold: ['open_bold', 'close'] }} | ${'<a>foo</a>, <b>bar</b>'}
321 ${'%{startLink}foo%{end}, %{startOtherLink}bar%{end}'} | ${{ a: ['startLink', 'end'], bold: ['startOtherLink', 'end'] }} | ${'<a>foo</a>, <b>bar</b>'}
322 ${'%{start}foo %{icon}%{end}'} | ${{ a: ['start', 'end'] }} | ${'<a>foo %{icon}</a>'}
323 ${'%{end}foo%{start}'} | ${{ a: ['start', 'end'] }} | ${'%{end}foo%{start}'}
324 ${'%{start}foo'} | ${{ a: ['start', 'end'] }} | ${'%{start}foo'}
325 ${'foo%{end}'} | ${{ a: ['start', 'end'] }} | ${'foo%{end}'}
326 `(
327 'renders $message as $expectedHtml given $placeholders',
328 ({ message, placeholders, expectedHtml }) => {
329 createComponent(
330 `<sprintf :message="message" :placeholders="placeholders">
331 <template #a="{ content }"><a>{{ content }}</a></template>
332 <template #bold="{ content }"><b>{{ content }}</b></template>
333 </sprintf>`,
334 () => ({
335 message,
336 placeholders,
337 })
338 );
339
340 expect(wrapper.element.innerHTML).toBe(expectedHtml);
341 }
342 );
343 });
344 });
345});