1 | "use strict";
|
2 |
|
3 | var Grammar = require("../lib/grammar");
|
4 | var assert = require("assert");
|
5 | var Testutils = require("eyeglass-dev-testutils");
|
6 | var testutils = new Testutils({
|
7 | engines: {
|
8 | sass: require("node-sass"),
|
9 | eyeglass: require("eyeglass")
|
10 | }
|
11 | });
|
12 |
|
13 | var defaultKnownTypes = ["button", "close-button", "dialog", "container", "window"];
|
14 |
|
15 | var nestedContextStack = [
|
16 | [
|
17 | new Map([
|
18 | ["description", ["small"]],
|
19 | ["type", "window"]
|
20 | ]),
|
21 | new Map([
|
22 | ["description", ["large"]],
|
23 | ["type", "window"]
|
24 | ])
|
25 | ],
|
26 | [
|
27 | new Map([
|
28 | ["type", "dialog"]
|
29 | ])
|
30 | ]
|
31 | ];
|
32 |
|
33 | var defaultAliases = new Map();
|
34 | defaultAliases.set("alias1", "button");
|
35 | defaultAliases.set("alias2", ["small", "button"]);
|
36 |
|
37 | var defaultGrammarEngines = [
|
38 | function splitOnDots(Grammar) {
|
39 | if (this.description) {
|
40 | this.description = this.description.join(Grammar.WORD_DELIM).replace(/\.+/g, Grammar.WORD_DELIM).split(Grammar.WORD_DELIM);
|
41 | }
|
42 | },
|
43 |
|
44 | function btnsAreButtons(Grammars, allowedTypes) {
|
45 | if (!this.type && this.description && allowedTypes.indexOf("button") !== -1) {
|
46 | this.description = this.description.filter(function(word) {
|
47 | if (word === "btn") {
|
48 | this.type = "button";
|
49 | return false;
|
50 | }
|
51 | return true;
|
52 | }.bind(this));
|
53 | }
|
54 | }
|
55 | ];
|
56 |
|
57 | var ERRORS = {
|
58 | noType: /A type could not be found in the description .*\. Please specify one of the registered types: .*/,
|
59 | ambiguous: /The description .* is incomplete and cannot be understood\. Ambiguous word .* found but no type was found\./,
|
60 | unused: /The description .* could not be understood\. The following words were found without being bound to a type: .*/
|
61 | };
|
62 |
|
63 | var testData = [
|
64 | {
|
65 | name: "single type",
|
66 | data: {
|
67 | description: "button"
|
68 | },
|
69 | expectedGrammar: {
|
70 | description: null,
|
71 | type: "button"
|
72 | }
|
73 | },
|
74 | {
|
75 | name: "description only (Array)",
|
76 | data: {
|
77 | description: ["a", "large", "primary", "button"]
|
78 | },
|
79 | expectedGrammar: {
|
80 | description: ["large", "primary"],
|
81 | type: "button"
|
82 | }
|
83 | },
|
84 | {
|
85 | name: "description only (String)",
|
86 | data: {
|
87 | description: "a large primary button"
|
88 | },
|
89 | expectedGrammar: {
|
90 | description: ["large", "primary"],
|
91 | type: "button"
|
92 | }
|
93 | },
|
94 | {
|
95 | name: "description only (String)",
|
96 | data: {
|
97 | description: "a large primary button"
|
98 | },
|
99 | expectedGrammar: {
|
100 | description: ["large", "primary"],
|
101 | type: "button"
|
102 | }
|
103 | },
|
104 | {
|
105 | name: "description + type",
|
106 | data: {
|
107 | description: "something else",
|
108 | type: "my-type"
|
109 | },
|
110 | expectedGrammar: {
|
111 | description: ["something", "else"],
|
112 | type: "my-type"
|
113 | }
|
114 | },
|
115 | {
|
116 | name: "empty description",
|
117 | data: {
|
118 | description: [],
|
119 | type: "my-type"
|
120 | },
|
121 | expectedGrammar: {
|
122 | description: null,
|
123 | type: "my-type"
|
124 | }
|
125 | },
|
126 | {
|
127 | name: "description only with a context",
|
128 | data: {
|
129 | description: "a large primary button in a dialog"
|
130 | },
|
131 | expectedGrammar: {
|
132 | description: ["large", "primary", "in-dialog"],
|
133 | type: "button"
|
134 | }
|
135 | },
|
136 | {
|
137 | name: "description with a context (in)",
|
138 | data: {
|
139 | description: "a large primary button in a dialog"
|
140 | },
|
141 | expectedGrammar: {
|
142 | description: ["large", "primary", "in-dialog"],
|
143 | type: "button"
|
144 | }
|
145 | },
|
146 | {
|
147 | name: "description with a context (within)",
|
148 | data: {
|
149 | description: "a large primary button within a dialog"
|
150 | },
|
151 | expectedGrammar: {
|
152 | description: ["large", "primary", "within-dialog"],
|
153 | type: "button"
|
154 | }
|
155 | },
|
156 | {
|
157 | name: "description with multiple context",
|
158 | data: {
|
159 | description: "a large primary button in a container within a dialog"
|
160 | },
|
161 | expectedGrammar: {
|
162 | description: ["large", "primary", "in-container", "within-dialog"],
|
163 | type: "button"
|
164 | }
|
165 | },
|
166 | {
|
167 | name: "description with direct descendant",
|
168 | data: {
|
169 | description: "dialog > a large primary button"
|
170 | },
|
171 | expectedGrammar: {
|
172 | description: ["large", "primary", "in-dialog"],
|
173 | type: "button"
|
174 | }
|
175 | },
|
176 | {
|
177 | name: "description with direct descendant",
|
178 | data: {
|
179 | description: "dialog > a large primary button"
|
180 | },
|
181 | expectedGrammar: {
|
182 | description: ["large", "primary", "in-dialog"],
|
183 | type: "button"
|
184 | }
|
185 | },
|
186 | {
|
187 | name: "simple `on` in description",
|
188 | data: {
|
189 | description: "leaf button on a dialog"
|
190 | },
|
191 | expectedGrammar: {
|
192 | description: ["leaf", "on-dialog"],
|
193 | type: "button"
|
194 | }
|
195 | },
|
196 | {
|
197 | name: "multiple contexts (in with)",
|
198 | data: {
|
199 | description: "close-button with a shadow in a modeless dialog with a header"
|
200 | },
|
201 | expectedGrammar: {
|
202 | description: ["with-shadow", "in-dialog-modeless", "in-dialog", "in-dialog-with-header"],
|
203 | type: "close-button"
|
204 | }
|
205 | },
|
206 | {
|
207 | name: "multiple contexts (in in)",
|
208 | data: {
|
209 | description: "close-button in a dialog in a window"
|
210 | },
|
211 | expectedGrammar: {
|
212 | description: ["in-dialog", "in-dialog-in-window"],
|
213 | type: "close-button"
|
214 | }
|
215 | },
|
216 | {
|
217 | name: "multiple contexts (in with within with)",
|
218 | data: {
|
219 | description: "button in a dialog with a shadow within a window with a box"
|
220 | },
|
221 | expectedGrammar: {
|
222 | description: ["in-dialog", "in-dialog-with-shadow", "within-window", "within-window-with-box"],
|
223 | type: "button"
|
224 | }
|
225 | },
|
226 | {
|
227 | name: "multiple contexts (in with and in)",
|
228 | data: {
|
229 | description: "button in a dialog with a shadow and a box in a window"
|
230 | },
|
231 | expectedGrammar: {
|
232 | description: ["in-dialog", "in-dialog-with-shadow", "in-dialog-with-box", "in-dialog-in-window"],
|
233 | type: "button"
|
234 | }
|
235 | },
|
236 | {
|
237 | name: "remove duplicate words",
|
238 | data: {
|
239 | description: "small small small primary button"
|
240 | },
|
241 | expectedGrammar: {
|
242 | description: ["small", "primary"],
|
243 | type: "button"
|
244 | }
|
245 | },
|
246 | {
|
247 | name: "invalid description without a recognized type",
|
248 | data: {
|
249 | description: "something else"
|
250 | },
|
251 | expectedError: ERRORS.noType
|
252 | },
|
253 | {
|
254 | name: "invalid description with ambiguous words",
|
255 | data: {
|
256 | description: "something else in a window"
|
257 | },
|
258 | expectedError: ERRORS.ambiguous
|
259 | },
|
260 | {
|
261 | name: "invalid description with unused words",
|
262 | data: {
|
263 | description: "button in a car in a window"
|
264 | },
|
265 | expectedError: ERRORS.unused
|
266 | },
|
267 | {
|
268 | name: "simple alias (as description)",
|
269 | data: {
|
270 | description: "alias1"
|
271 | },
|
272 | expectedGrammar: {
|
273 | description: null,
|
274 | type: "button"
|
275 | }
|
276 | },
|
277 | {
|
278 | name: "alias without aliases",
|
279 | data: {
|
280 | description: "alias1",
|
281 | aliases: []
|
282 | },
|
283 | expectedError: ERRORS.noType
|
284 | },
|
285 | {
|
286 | name: "simple alias (as type)",
|
287 | data: {
|
288 | type: "alias1"
|
289 | },
|
290 | expectedGrammar: {
|
291 | description: null,
|
292 | type: "button"
|
293 | }
|
294 | },
|
295 | {
|
296 | name: "alias with modifier",
|
297 | data: {
|
298 | description: "small alias1"
|
299 | },
|
300 | expectedGrammar: {
|
301 | description: ["small"],
|
302 | type: "button"
|
303 | }
|
304 | },
|
305 | {
|
306 | name: "complex alias with modifier",
|
307 | data: {
|
308 | description: "super alias2"
|
309 | },
|
310 | expectedGrammar: {
|
311 | description: ["super", "small"],
|
312 | type: "button"
|
313 | }
|
314 | },
|
315 | {
|
316 | name: "custom grammar engine",
|
317 | data: {
|
318 | description: "this will test.a.custom.engine button"
|
319 | },
|
320 | expectedGrammar: {
|
321 | description: ["will", "test", "custom", "engine"],
|
322 | type: "button"
|
323 | }
|
324 | },
|
325 | {
|
326 | name: "custom grammar engine 2",
|
327 | data: {
|
328 | description: "custom btn"
|
329 | },
|
330 | expectedGrammar: {
|
331 | description: ["custom"],
|
332 | type: "button"
|
333 | }
|
334 | },
|
335 | {
|
336 | name: "no custom grammar engine",
|
337 | data: {
|
338 | description: "custom btn",
|
339 | grammarEngines: []
|
340 | },
|
341 | expectedError: ERRORS.noType
|
342 | },
|
343 | {
|
344 | name: "without knownTypes arg",
|
345 | data: {
|
346 | description: "a button",
|
347 | knownTypes: null
|
348 | },
|
349 | expectedError: ERRORS.noType
|
350 | },
|
351 | {
|
352 | name: "nested context",
|
353 | data: {
|
354 | type: "button",
|
355 | contextStack: nestedContextStack
|
356 | },
|
357 | expectedGrammar: {
|
358 | description: ["within-window-small", "within-window", "within-window-large", "within-dialog"],
|
359 | type: "button"
|
360 | }
|
361 | },
|
362 | {
|
363 | name: "nested context",
|
364 | data: {
|
365 | type: "button",
|
366 | description: ["small"],
|
367 | contextStack: nestedContextStack
|
368 | },
|
369 | expectedGrammar: {
|
370 | description: ["small", "within-window-small", "within-window", "within-window-large", "within-dialog"],
|
371 | type: "button"
|
372 | }
|
373 | }
|
374 | ];
|
375 |
|
376 |
|
377 | describe("grammar", function() {
|
378 | function testGrammar(test) {
|
379 | return new Grammar(
|
380 | test.data.description,
|
381 | test.data.type,
|
382 | test.data.knownTypes === undefined ? defaultKnownTypes : test.data.knownTypes,
|
383 | test.data.aliases === undefined ? defaultAliases : test.data.aliases,
|
384 | test.data.contextStack || [],
|
385 | test.data.grammarEngines === undefined ? defaultGrammarEngines : test.data.grammarEngines
|
386 | );
|
387 | }
|
388 |
|
389 | testData.forEach(function(test) {
|
390 | var testGrammarFn = testGrammar.bind(testGrammar, test);
|
391 |
|
392 | it(test.name, function() {
|
393 | if (test.expectedError) {
|
394 | assert.throws(testGrammarFn, test.expectedError, "should throw an error");
|
395 | }
|
396 | else {
|
397 | assert.deepEqual(testGrammarFn(), test.expectedGrammar, "the grammar should match");
|
398 | }
|
399 | });
|
400 | });
|
401 | });
|
402 |
|
403 | describe("adding custom grammar engine", function() {
|
404 | var data = "@import 'restyle'; @include restyle-define(test); /* #{inspect(-restyle--grammar(simple test))} */";
|
405 | var expectedCSS = "/* (description: (custom,), type: test) */";
|
406 |
|
407 | function customGrammarEngine() {
|
408 | this.description = ["custom"];
|
409 | }
|
410 |
|
411 | it("should allow a custom engine via options", function(done) {
|
412 | var options = {
|
413 | data: data,
|
414 | restyle: {
|
415 | grammarEngines: [customGrammarEngine]
|
416 | }
|
417 | };
|
418 | testutils.assertCompiles(options, expectedCSS, done);
|
419 | });
|
420 |
|
421 | it("should allow a custom engine via #addGrammarEngine", function(done) {
|
422 |
|
423 | var Eyeglass = testutils.engines.eyeglass;
|
424 | var eyeglass = new Eyeglass({
|
425 | data: data
|
426 | });
|
427 |
|
428 | var restyle = eyeglass.modules.find("restyle");
|
429 |
|
430 | restyle.addGrammarEngine(customGrammarEngine);
|
431 |
|
432 | testutils.assertCompiles(eyeglass.options, expectedCSS, done);
|
433 | });
|
434 | });
|