Skip to content

Commit 02c2412

Browse files
committed
add tests for the github issues
1 parent c2039f0 commit 02c2412

1 file changed

Lines changed: 251 additions & 2 deletions

File tree

test/parse.test.ts

Lines changed: 251 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
import type CssParseError from '../src/CssParseError';
2-
import { parse } from '../src/index';
3-
import type { CssMediaAST, CssRuleAST } from '../src/type';
2+
import { parse, stringify } from '../src/index';
3+
import {
4+
type CssDeclarationAST,
5+
type CssMediaAST,
6+
type CssPageAST,
7+
type CssPageMarginBoxAST,
8+
type CssRuleAST,
9+
CssTypes,
10+
} from '../src/type';
411

512
describe('parse(str)', () => {
613
it('should save the filename and source', () => {
@@ -111,4 +118,246 @@ describe('parse(str)', () => {
111118
decl = rule.declarations[0];
112119
expect(decl.parent).toBe(rule);
113120
});
121+
122+
// GitHub Issue #210: @page with @left-middle crashes parser
123+
// https://github.com/adobe/css-tools/issues/210
124+
describe('issue #210: @page with margin box at-rules', () => {
125+
it('should parse @page with @left-middle without crashing', () => {
126+
const css = '@page { margin: 2cm; @left-middle { content: "Hello"; } }';
127+
const ast = parse(css);
128+
const page = ast.stylesheet.rules[0] as CssPageAST;
129+
130+
expect(page.type).toBe(CssTypes.page);
131+
expect(page.declarations.length).toBe(2);
132+
133+
const marginDecl = page.declarations[0] as CssDeclarationAST;
134+
expect(marginDecl.type).toBe(CssTypes.declaration);
135+
expect(marginDecl.property).toBe('margin');
136+
expect(marginDecl.value).toBe('2cm');
137+
138+
const marginBox = page.declarations[1] as CssPageMarginBoxAST;
139+
expect(marginBox.type).toBe(CssTypes.pageMarginBox);
140+
expect(marginBox.name).toBe('left-middle');
141+
expect(marginBox.declarations.length).toBe(1);
142+
expect((marginBox.declarations[0] as CssDeclarationAST).property).toBe(
143+
'content',
144+
);
145+
});
146+
147+
it('should parse all 16 page margin box at-rules', () => {
148+
const marginBoxNames = [
149+
'top-left-corner',
150+
'top-left',
151+
'top-center',
152+
'top-right',
153+
'top-right-corner',
154+
'bottom-left-corner',
155+
'bottom-left',
156+
'bottom-center',
157+
'bottom-right',
158+
'bottom-right-corner',
159+
'left-top',
160+
'left-middle',
161+
'left-bottom',
162+
'right-top',
163+
'right-middle',
164+
'right-bottom',
165+
];
166+
167+
for (const name of marginBoxNames) {
168+
const css = `@page { @${name} { content: "x"; } }`;
169+
const ast = parse(css);
170+
const page = ast.stylesheet.rules[0] as CssPageAST;
171+
const box = page.declarations[0] as CssPageMarginBoxAST;
172+
expect(box.type).toBe(CssTypes.pageMarginBox);
173+
expect(box.name).toBe(name);
174+
}
175+
});
176+
177+
it('should roundtrip @page with margin boxes', () => {
178+
const css =
179+
'@page :first {\n margin: 2cm;\n @top-center {\n content: "Title";\n }\n @bottom-center {\n content: counter(page);\n }\n}';
180+
expect(stringify(parse(css))).toBe(css);
181+
});
182+
});
183+
184+
// GitHub Issue #122: CSS nesting support
185+
// https://github.com/adobe/css-tools/issues/122
186+
describe('issue #122: CSS nesting', () => {
187+
it('should parse nested rules', () => {
188+
const css = '.parent { color: red; .child { color: blue; } }';
189+
const ast = parse(css);
190+
const rule = ast.stylesheet.rules[0] as CssRuleAST;
191+
192+
expect(rule.selectors).toEqual(['.parent']);
193+
expect(rule.declarations.length).toBe(2);
194+
195+
const decl = rule.declarations[0] as CssDeclarationAST;
196+
expect(decl.property).toBe('color');
197+
expect(decl.value).toBe('red');
198+
199+
const nested = rule.declarations[1] as CssRuleAST;
200+
expect(nested.type).toBe(CssTypes.rule);
201+
expect(nested.selectors).toEqual(['.child']);
202+
expect((nested.declarations[0] as CssDeclarationAST).value).toBe('blue');
203+
});
204+
205+
it('should parse deeply nested rules', () => {
206+
const css = '.a { .b { .c { color: red; } } }';
207+
const ast = parse(css);
208+
const a = ast.stylesheet.rules[0] as CssRuleAST;
209+
const b = a.declarations[0] as CssRuleAST;
210+
const c = b.declarations[0] as CssRuleAST;
211+
212+
expect(a.selectors).toEqual(['.a']);
213+
expect(b.selectors).toEqual(['.b']);
214+
expect(c.selectors).toEqual(['.c']);
215+
expect((c.declarations[0] as CssDeclarationAST).value).toBe('red');
216+
});
217+
218+
it('should parse & selector nesting', () => {
219+
const css = 'a { &:hover { color: red; } &::before { content: "x"; } }';
220+
const ast = parse(css);
221+
const rule = ast.stylesheet.rules[0] as CssRuleAST;
222+
223+
expect(rule.declarations.length).toBe(2);
224+
expect((rule.declarations[0] as CssRuleAST).selectors).toEqual([
225+
'&:hover',
226+
]);
227+
expect((rule.declarations[1] as CssRuleAST).selectors).toEqual([
228+
'&::before',
229+
]);
230+
});
231+
232+
it('should parse nested @media inside a rule', () => {
233+
const css =
234+
'.card { padding: 1rem; @media (min-width: 768px) { padding: 2rem; } }';
235+
const ast = parse(css);
236+
const rule = ast.stylesheet.rules[0] as CssRuleAST;
237+
238+
expect(rule.declarations.length).toBe(2);
239+
const media = rule.declarations[1] as CssMediaAST;
240+
expect(media.type).toBe(CssTypes.media);
241+
expect(media.media).toBe('(min-width: 768px)');
242+
});
243+
244+
it('should roundtrip nested CSS', () => {
245+
const css =
246+
'.parent {\n color: red;\n .child {\n color: blue;\n }\n}';
247+
expect(stringify(parse(css))).toBe(css);
248+
});
249+
250+
it('should handle declarations after nested rules', () => {
251+
const css = '.a { .b { color: red; } margin: 0; }';
252+
const ast = parse(css);
253+
const rule = ast.stylesheet.rules[0] as CssRuleAST;
254+
expect(rule.declarations.length).toBe(2);
255+
expect((rule.declarations[0] as CssRuleAST).selectors).toEqual(['.b']);
256+
expect((rule.declarations[1] as CssDeclarationAST).property).toBe(
257+
'margin',
258+
);
259+
});
260+
});
261+
262+
// GitHub Issue #175: Comment with { in selector causes parse failure
263+
// https://github.com/adobe/css-tools/issues/175
264+
describe('issue #175: comments with braces in selectors', () => {
265+
it('should parse selector with commented-out parts containing braces', () => {
266+
const css = 'head, /* footer, */body/*, nav */ { foo: bar; }';
267+
const ast = parse(css);
268+
const rule = ast.stylesheet.rules[0] as CssRuleAST;
269+
270+
expect(rule.selectors).toEqual(['head', 'body']);
271+
expect(rule.declarations.length).toBe(1);
272+
expect((rule.declarations[0] as CssDeclarationAST).property).toBe('foo');
273+
expect((rule.declarations[0] as CssDeclarationAST).value).toBe('bar');
274+
});
275+
276+
it('should parse selector with comment before opening brace', () => {
277+
const css = '.a /* comment */ { color: red; }';
278+
const ast = parse(css);
279+
const rule = ast.stylesheet.rules[0] as CssRuleAST;
280+
281+
expect(rule.selectors).toEqual(['.a']);
282+
expect((rule.declarations[0] as CssDeclarationAST).value).toBe('red');
283+
});
284+
285+
it('should roundtrip selector with comments stripped', () => {
286+
const css = 'head, /* footer, */body { color: red; }';
287+
const output = stringify(parse(css));
288+
expect(output).toContain('head,');
289+
expect(output).toContain('body');
290+
expect(output).toContain('color: red');
291+
expect(output).not.toContain('footer');
292+
});
293+
});
294+
295+
// GitHub Issue #188: Stylesheets with errors / silent mode recovery
296+
// https://github.com/adobe/css-tools/issues/188
297+
describe('issue #188: error recovery in silent mode', () => {
298+
it('should recover valid declarations after invalid ones', () => {
299+
const css = '* { aa; display: block; }';
300+
const ast = parse(css, { silent: true });
301+
const rule = ast.stylesheet.rules[0] as CssRuleAST;
302+
303+
expect(rule.selectors).toEqual(['*']);
304+
expect(rule.declarations.length).toBe(1);
305+
expect((rule.declarations[0] as CssDeclarationAST).property).toBe(
306+
'display',
307+
);
308+
expect((rule.declarations[0] as CssDeclarationAST).value).toBe('block');
309+
});
310+
311+
it('should continue parsing rules after error recovery', () => {
312+
const css = '.broken { badprop; } .ok { color: red; }';
313+
const ast = parse(css, { silent: true });
314+
const rules = ast.stylesheet.rules;
315+
316+
expect(rules.length).toBe(2);
317+
const okRule = rules[1] as CssRuleAST;
318+
expect(okRule.selectors).toEqual(['.ok']);
319+
expect((okRule.declarations[0] as CssDeclarationAST).value).toBe('red');
320+
});
321+
322+
it('should recover from extra closing braces', () => {
323+
const css = '.a {} } .b { color: blue; }';
324+
const ast = parse(css, { silent: true });
325+
const rules = ast.stylesheet.rules;
326+
327+
expect(rules.length).toBe(2);
328+
expect((rules[0] as CssRuleAST).selectors).toEqual(['.a']);
329+
expect((rules[1] as CssRuleAST).selectors).toEqual(['.b']);
330+
expect(
331+
((rules[1] as CssRuleAST).declarations[0] as CssDeclarationAST).value,
332+
).toBe('blue');
333+
});
334+
335+
it('should recover from multiple errors in one rule', () => {
336+
const css = '.x { bad1; bad2; color: green; font-size: 1rem; }';
337+
const ast = parse(css, { silent: true });
338+
const rule = ast.stylesheet.rules[0] as CssRuleAST;
339+
340+
expect(rule.selectors).toEqual(['.x']);
341+
const decls = rule.declarations.filter(
342+
(d) => d.type === CssTypes.declaration,
343+
) as CssDeclarationAST[];
344+
expect(decls.length).toBe(2);
345+
expect(decls[0].property).toBe('color');
346+
expect(decls[1].property).toBe('font-size');
347+
});
348+
349+
it('should record parsing errors', () => {
350+
const css = '* { aa; display: block; }';
351+
const ast = parse(css, { silent: true });
352+
353+
expect(ast.stylesheet.parsingErrors).toBeDefined();
354+
expect(ast.stylesheet.parsingErrors?.length).toBeGreaterThan(0);
355+
});
356+
357+
it('should not recover when silent is false', () => {
358+
expect(() => {
359+
parse('* { aa; display: block; }');
360+
}).toThrow();
361+
});
362+
});
114363
});

0 commit comments

Comments
 (0)