diff --git a/src/api.test.ts b/src/api.test.ts index 0f6dc38..b2ae8be 100644 --- a/src/api.test.ts +++ b/src/api.test.ts @@ -1045,6 +1045,174 @@ describe('CSSNode', () => { }) }) +describe('value property - undefined for nodes without a value concept', () => { + // Regression: these node types were incorrectly returning null instead of undefined. + // null means "has a value, but it is absent"; undefined means "value is not a property of this node type". + // + // These types intentionally omit `value` from their TypeScript interfaces, so we + // access the runtime getter via the base CSSNode class to test the JS behavior. + function get_value(node: unknown) { + return (node as CSSNode).value + } + + test('StyleSheet.value is undefined', () => { + const root = parse('div { color: red; }') + expect(get_value(root)).toBeUndefined() + }) + + test('Rule.value is undefined', () => { + const root = parse('div { color: red; }') + const rule = root.first_child! + expect(get_value(rule)).toBeUndefined() + }) + + test('SelectorList.value is undefined', () => { + const root = parse('div { color: red; }') + const rule = root.first_child! + const selectorList = rule.first_child! + expect(selectorList.type_name).toBe('SelectorList') + expect(get_value(selectorList)).toBeUndefined() + }) + + test('Selector.value is undefined', () => { + const root = parse('div { color: red; }') + const selector = root.first_child!.first_child!.first_child! + expect(selector.type_name).toBe('Selector') + expect(get_value(selector)).toBeUndefined() + }) + + test('TypeSelector.value is undefined', () => { + const root = parse('div { color: red; }') + const typeSelector = root.first_child!.first_child!.first_child!.first_child! + expect(typeSelector.type_name).toBe('TypeSelector') + expect(get_value(typeSelector)).toBeUndefined() + }) + + test('ClassSelector.value is undefined', () => { + const root = parse('.foo { color: red; }') + const classSelector = root.first_child!.first_child!.first_child!.first_child! + expect(classSelector.type_name).toBe('ClassSelector') + expect(get_value(classSelector)).toBeUndefined() + }) + + test('IdSelector.value is undefined', () => { + const root = parse('#bar { color: red; }') + const idSelector = root.first_child!.first_child!.first_child!.first_child! + expect(idSelector.type_name).toBe('IdSelector') + expect(get_value(idSelector)).toBeUndefined() + }) + + test('PseudoClassSelector.value is undefined', () => { + const root = parse('a:hover { color: red; }') + const selector = root.first_child!.first_child!.first_child! + const pseudo = selector.first_child!.next_sibling! + expect(pseudo.type_name).toBe('PseudoClassSelector') + expect(get_value(pseudo)).toBeUndefined() + }) + + test('PseudoElementSelector.value is undefined', () => { + const root = parse('p::before { content: ""; }') + const selector = root.first_child!.first_child!.first_child! + const pseudo = selector.first_child!.next_sibling! + expect(pseudo.type_name).toBe('PseudoElementSelector') + expect(get_value(pseudo)).toBeUndefined() + }) + + test('Combinator.value is undefined', () => { + const root = parse('div > span { color: red; }') + const selector = root.first_child!.first_child!.first_child! + const combinator = selector.first_child!.next_sibling! + expect(combinator.type_name).toBe('Combinator') + expect(get_value(combinator)).toBeUndefined() + }) + + test('UniversalSelector.value is undefined', () => { + const root = parse('* { color: red; }') + const universal = root.first_child!.first_child!.first_child!.first_child! + expect(universal.type_name).toBe('UniversalSelector') + expect(get_value(universal)).toBeUndefined() + }) + + test('NthSelector.value is undefined', () => { + const root = parse('li:nth-child(2n+1) { color: red; }') + const selector = root.first_child!.first_child!.first_child! + const pseudo = selector.first_child!.next_sibling! + const nth = pseudo.first_child! + expect(nth.type_name).toBe('Nth') + expect(get_value(nth)).toBeUndefined() + }) + + test('Block.value is undefined', () => { + const root = parse('div { color: red; }') + const block = (root.first_child! as Rule).block! + expect(get_value(block)).toBeUndefined() + }) + + test('Value node.value is undefined', () => { + const root = parse('div { color: red; }') + const block = (root.first_child! as Rule).block! + const decl = block.first_child! as Declaration + const valueNode = decl.value! + expect(valueNode.type_name).toBe('Value') + expect(get_value(valueNode)).toBeUndefined() + }) + + test('Identifier.value is undefined', () => { + const root = parse('div { color: red; }') + const block = (root.first_child! as Rule).block! + const decl = block.first_child! as Declaration + const identifier = decl.value!.first_child! + expect(identifier.type_name).toBe('Identifier') + expect(get_value(identifier)).toBeUndefined() + }) + + test('Hash.value is undefined', () => { + const root = parse('div { color: #fff; }') + const block = (root.first_child! as Rule).block! + const decl = block.first_child! as Declaration + const hash = decl.value!.first_child! + expect(hash.type_name).toBe('Hash') + expect(get_value(hash)).toBeUndefined() + }) + + test('String.value is undefined', () => { + const root = parse('p::before { content: "hello"; }') + const block = (root.first_child! as Rule).block! + const decl = block.first_child! as Declaration + const str = decl.value!.first_child! + expect(str.type_name).toBe('String') + expect(get_value(str)).toBeUndefined() + }) + + test('Atrule.value is undefined', () => { + const root = parse('@media screen { div { color: red; } }', { + parse_atrule_preludes: false, + }) + const atrule = root.first_child! + expect(atrule.type_name).toBe('Atrule') + expect(get_value(atrule)).toBeUndefined() + }) + + test('MediaQuery.value is undefined', () => { + const root = parse('@media screen and (min-width: 600px) { div { color: red; } }') + const prelude = root.first_child!.first_child! + const mediaQuery = prelude.first_child! + expect(mediaQuery.type_name).toBe('MediaQuery') + expect(get_value(mediaQuery)).toBeUndefined() + }) + + test('Raw.value is undefined', () => { + const root = parse('div { color: red; }', { + parse_selectors: false, + parse_values: false, + }) + const rule = root.first_child! + const raw = rule.first_child! + expect(raw.type_name).toBe('Raw') + expect(get_value(raw)).toBeUndefined() + }) +}) + describe('NODE_TYPES namespace', () => { test('should work as alternative to individual imports', async () => { // Import namespace object diff --git a/src/css-node.ts b/src/css-node.ts index aef7001..376b019 100644 --- a/src/css-node.ts +++ b/src/css-node.ts @@ -415,7 +415,18 @@ export class CSSNode { return this.get_content() } - // For other nodes, return as string + // Only these types store a value in the arena value_start/value_length field. + // Every other node type has no value concept and must return undefined. + if ( + type !== DECLARATION && + type !== FUNCTION && + type !== ATTRIBUTE_SELECTOR && + type !== SUPPORTS_QUERY && + type !== PRELUDE_SELECTORLIST + ) { + return undefined + } + let start = this.arena.get_value_start(this.index) let length = this.arena.get_value_length(this.index) if (length === 0) return null