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 | 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 |
|
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="#"><script>alert(\'hello\')</script></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 | });
|