UNPKG

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