1 | import { mount } from '@vue/test-utils';
|
2 | import sprintf from './sprintf.vue';
|
3 |
|
4 | describe('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 |
|
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="#"><script>alert(\'hello\')</script></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 | });
|