11import { describe , test , expect } from 'vitest'
22import { parse } from './parse'
33import { parse_atrule_prelude } from './parse-atrule-prelude'
4+ import { CSSNode , type PlainCSSNode } from './css-node'
45import type {
56 Atrule ,
67 AtrulePrelude ,
@@ -12,12 +13,13 @@ import type {
1213 MediaFeature ,
1314 FeatureRange ,
1415 Function ,
15- CSSNode ,
1616 LayerName ,
1717 SupportsQuery ,
1818 SupportsDeclaration ,
1919 Url ,
2020 PreludeSelectorList ,
21+ Ratio ,
22+ Number as NumberNode ,
2123} from './node-types'
2224import {
2325 AT_RULE ,
@@ -41,6 +43,7 @@ import {
4143 NUMBER ,
4244 SELECTOR_LIST ,
4345 VALUE ,
46+ RATIO ,
4447} from './arena'
4548
4649describe ( 'At-Rule Prelude Nodes' , ( ) => {
@@ -453,6 +456,7 @@ describe('At-Rule Prelude Nodes', () => {
453456 // Feature should have content
454457 const feature = query . first_child as MediaFeature | null
455458 expect ( feature ?. property ) . toBe ( 'min-width' )
459+ expect ( feature ?. value ?. type_name ) . toBe ( 'Dimension' )
456460 } )
457461
458462 test ( 'should parse media feature (hover)' , ( ) => {
@@ -636,6 +640,83 @@ describe('At-Rule Prelude Nodes', () => {
636640 expect ( feature ?. value ?. text ) . toBe ( 'env(safe-area-inset-top)' )
637641 } )
638642
643+ test ( 'should parse ratio value (aspect-ratio: 16/9)' , ( ) => {
644+ const css = '@media (aspect-ratio: 16/9) { }'
645+ const ast = parse ( css )
646+ const atRule = ast . first_child ! as Atrule
647+ const queryChildren =
648+ ( ( atRule . prelude as AtrulePrelude | null ) ?. children [ 0 ] as MediaQuery | undefined )
649+ ?. children || [ ]
650+ const feature = queryChildren . find ( ( c ) => c . type === MEDIA_FEATURE ) as
651+ | MediaFeature
652+ | undefined
653+
654+ expect ( feature ?. property ) . toBe ( 'aspect-ratio' )
655+ expect ( feature ?. value ?. type ) . toBe ( RATIO )
656+ expect ( feature ?. value ?. text ) . toBe ( '16/9' )
657+
658+ const ratio = feature ?. value as Ratio | undefined
659+ expect ( ratio ?. left . type ) . toBe ( NUMBER )
660+ expect ( ratio ?. left . text ) . toBe ( '16' )
661+ expect ( ratio ?. left . value ) . toBe ( 16 )
662+ expect ( ratio ?. right . type ) . toBe ( NUMBER )
663+ expect ( ratio ?. right . text ) . toBe ( '9' )
664+ expect ( ratio ?. right . value ) . toBe ( 9 )
665+ } )
666+
667+ test ( 'clone() serializes Ratio.left/right as plain objects, not live CSSNode instances' , ( ) => {
668+ const css = '@media (aspect-ratio: 16/9) { }'
669+ const ast = parse ( css )
670+ const atRule = ast . first_child ! as Atrule
671+ const queryChildren =
672+ ( ( atRule . prelude as AtrulePrelude | null ) ?. children [ 0 ] as MediaQuery | undefined )
673+ ?. children || [ ]
674+ const feature = queryChildren . find ( ( c ) => c . type === MEDIA_FEATURE ) as
675+ | MediaFeature
676+ | undefined
677+
678+ const clone = feature ! . clone ( )
679+ const ratio = clone . value as PlainCSSNode
680+
681+ expect ( ratio . type_name ) . toBe ( 'Ratio' )
682+ expect ( ratio . left ) . not . toBeInstanceOf ( CSSNode )
683+ expect ( ( ratio . left as PlainCSSNode ) . value ) . toBe ( 16 )
684+ expect ( ( ratio . right as PlainCSSNode ) . value ) . toBe ( 9 )
685+ expect ( JSON . stringify ( clone ) ) . not . toContain ( '"arena"' )
686+ } )
687+
688+ test ( 'should parse ratio value with whitespace around the slash' , ( ) => {
689+ const css = '@media (aspect-ratio: 16 / 9) { }'
690+ const ast = parse ( css )
691+ const atRule = ast . first_child ! as Atrule
692+ const queryChildren =
693+ ( ( atRule . prelude as AtrulePrelude | null ) ?. children [ 0 ] as MediaQuery | undefined )
694+ ?. children || [ ]
695+ const feature = queryChildren . find ( ( c ) => c . type === MEDIA_FEATURE ) as
696+ | MediaFeature
697+ | undefined
698+
699+ const ratio = feature ?. value as Ratio | undefined
700+ expect ( ratio ?. type ) . toBe ( RATIO )
701+ expect ( ratio ?. left . text ) . toBe ( '16' )
702+ expect ( ratio ?. right . text ) . toBe ( '9' )
703+ } )
704+
705+ test ( 'should parse bare number value (aspect-ratio: 1), not a Ratio' , ( ) => {
706+ const css = '@media (aspect-ratio: 1) { }'
707+ const ast = parse ( css )
708+ const atRule = ast . first_child ! as Atrule
709+ const queryChildren =
710+ ( ( atRule . prelude as AtrulePrelude | null ) ?. children [ 0 ] as MediaQuery | undefined )
711+ ?. children || [ ]
712+ const feature = queryChildren . find ( ( c ) => c . type === MEDIA_FEATURE ) as
713+ | MediaFeature
714+ | undefined
715+
716+ expect ( feature ?. value ?. type ) . toBe ( NUMBER )
717+ expect ( ( feature ?. value as NumberNode | undefined ) ?. value ) . toBe ( 1 )
718+ } )
719+
639720 test ( 'should have null value for boolean features' , ( ) => {
640721 const css = '@media (hover) { }'
641722 const ast = parse ( css )
@@ -648,6 +729,7 @@ describe('At-Rule Prelude Nodes', () => {
648729 | undefined
649730
650731 expect ( feature ?. value ) . toBeNull ( )
732+ expect ( feature ?. first_child ) . toBeNull ( )
651733 } )
652734
653735 test ( 'should parse vendor-prefixed media feature (-ms-high-contrast: active)' , ( ) => {
0 commit comments