From 299570c4bb843ebeb76c51fc43ea104fe15157d9 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 24 Jun 2026 19:07:49 +0000 Subject: [PATCH 1/2] fix: return undefined instead of null for value on nodes without a value concept Node types like SelectorList, TypeSelector, Selector, Block, etc. have no value property in their type definitions. The value getter was falling through to the arena lookup and returning null (length === 0) for these nodes when it should return undefined, since null signals "has a value field but it is absent" while undefined signals "value is not a property of this node type at all". Added a guard in the value getter so only DECLARATION, FUNCTION, ATTRIBUTE_SELECTOR, SUPPORTS_QUERY, and PRELUDE_SELECTORLIST reach the arena value_start/value_length lookup. All other types now correctly return undefined. Added 20 regression tests covering the reported node types and representatives from every category (structural, selector, value, at-rule). Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01KETKttcckhFQtg8JzjxvuU --- src/api.test.ts | 162 ++++++++++++++++++++++++++++++++++++++++++++++++ src/css-node.ts | 13 +++- 2 files changed, 174 insertions(+), 1 deletion(-) diff --git a/src/api.test.ts b/src/api.test.ts index 0f6dc38..43a001d 100644 --- a/src/api.test.ts +++ b/src/api.test.ts @@ -1045,6 +1045,168 @@ 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". + + test('StyleSheet.value is undefined', () => { + const root = parse('div { color: red; }') + expect(root.value).toBeUndefined() + }) + + test('Rule.value is undefined', () => { + const root = parse('div { color: red; }') + const rule = root.first_child! + expect(rule.value).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(selectorList.value).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(selector.value).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(typeSelector.value).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(classSelector.value).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(idSelector.value).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(pseudo.value).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(pseudo.value).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(combinator.value).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(universal.value).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(nth.value).toBeUndefined() + }) + + test('Block.value is undefined', () => { + const root = parse('div { color: red; }') + const block = (root.first_child! as Rule).block! + expect(block.value).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(valueNode.value).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(identifier.value).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(hash.value).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(str.value).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(atrule.value).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(mediaQuery.value).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(raw.value).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 From 6c72af8745cb6cfe1e0fbe78246b84b95849d779 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 24 Jun 2026 19:18:59 +0000 Subject: [PATCH 2/2] fix: cast through CSSNode class in value-undefined regression tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TypeScript correctly rejects .value access on types that omit the property (StyleSheet, Rule, SelectorList, etc.) — that's the whole point. Tests that verify the runtime getter returns undefined must cast through the base CSSNode class, which has the generic value getter. Use a local get_value() helper to avoid repeating the cast on every assertion. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01KETKttcckhFQtg8JzjxvuU --- src/api.test.ts | 46 ++++++++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/src/api.test.ts b/src/api.test.ts index 43a001d..b2ae8be 100644 --- a/src/api.test.ts +++ b/src/api.test.ts @@ -1048,16 +1048,22 @@ 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(root.value).toBeUndefined() + expect(get_value(root)).toBeUndefined() }) test('Rule.value is undefined', () => { const root = parse('div { color: red; }') const rule = root.first_child! - expect(rule.value).toBeUndefined() + expect(get_value(rule)).toBeUndefined() }) test('SelectorList.value is undefined', () => { @@ -1065,35 +1071,35 @@ describe('value property - undefined for nodes without a value concept', () => { const rule = root.first_child! const selectorList = rule.first_child! expect(selectorList.type_name).toBe('SelectorList') - expect(selectorList.value).toBeUndefined() + 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(selector.value).toBeUndefined() + 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(typeSelector.value).toBeUndefined() + 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(classSelector.value).toBeUndefined() + 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(idSelector.value).toBeUndefined() + expect(get_value(idSelector)).toBeUndefined() }) test('PseudoClassSelector.value is undefined', () => { @@ -1101,7 +1107,7 @@ describe('value property - undefined for nodes without a value concept', () => { const selector = root.first_child!.first_child!.first_child! const pseudo = selector.first_child!.next_sibling! expect(pseudo.type_name).toBe('PseudoClassSelector') - expect(pseudo.value).toBeUndefined() + expect(get_value(pseudo)).toBeUndefined() }) test('PseudoElementSelector.value is undefined', () => { @@ -1109,7 +1115,7 @@ describe('value property - undefined for nodes without a value concept', () => { const selector = root.first_child!.first_child!.first_child! const pseudo = selector.first_child!.next_sibling! expect(pseudo.type_name).toBe('PseudoElementSelector') - expect(pseudo.value).toBeUndefined() + expect(get_value(pseudo)).toBeUndefined() }) test('Combinator.value is undefined', () => { @@ -1117,14 +1123,14 @@ describe('value property - undefined for nodes without a value concept', () => { const selector = root.first_child!.first_child!.first_child! const combinator = selector.first_child!.next_sibling! expect(combinator.type_name).toBe('Combinator') - expect(combinator.value).toBeUndefined() + 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(universal.value).toBeUndefined() + expect(get_value(universal)).toBeUndefined() }) test('NthSelector.value is undefined', () => { @@ -1133,13 +1139,13 @@ describe('value property - undefined for nodes without a value concept', () => { const pseudo = selector.first_child!.next_sibling! const nth = pseudo.first_child! expect(nth.type_name).toBe('Nth') - expect(nth.value).toBeUndefined() + 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(block.value).toBeUndefined() + expect(get_value(block)).toBeUndefined() }) test('Value node.value is undefined', () => { @@ -1148,7 +1154,7 @@ describe('value property - undefined for nodes without a value concept', () => { const decl = block.first_child! as Declaration const valueNode = decl.value! expect(valueNode.type_name).toBe('Value') - expect(valueNode.value).toBeUndefined() + expect(get_value(valueNode)).toBeUndefined() }) test('Identifier.value is undefined', () => { @@ -1157,7 +1163,7 @@ describe('value property - undefined for nodes without a value concept', () => { const decl = block.first_child! as Declaration const identifier = decl.value!.first_child! expect(identifier.type_name).toBe('Identifier') - expect(identifier.value).toBeUndefined() + expect(get_value(identifier)).toBeUndefined() }) test('Hash.value is undefined', () => { @@ -1166,7 +1172,7 @@ describe('value property - undefined for nodes without a value concept', () => { const decl = block.first_child! as Declaration const hash = decl.value!.first_child! expect(hash.type_name).toBe('Hash') - expect(hash.value).toBeUndefined() + expect(get_value(hash)).toBeUndefined() }) test('String.value is undefined', () => { @@ -1175,7 +1181,7 @@ describe('value property - undefined for nodes without a value concept', () => { const decl = block.first_child! as Declaration const str = decl.value!.first_child! expect(str.type_name).toBe('String') - expect(str.value).toBeUndefined() + expect(get_value(str)).toBeUndefined() }) test('Atrule.value is undefined', () => { @@ -1184,7 +1190,7 @@ describe('value property - undefined for nodes without a value concept', () => { }) const atrule = root.first_child! expect(atrule.type_name).toBe('Atrule') - expect(atrule.value).toBeUndefined() + expect(get_value(atrule)).toBeUndefined() }) test('MediaQuery.value is undefined', () => { @@ -1192,7 +1198,7 @@ describe('value property - undefined for nodes without a value concept', () => { const prelude = root.first_child!.first_child! const mediaQuery = prelude.first_child! expect(mediaQuery.type_name).toBe('MediaQuery') - expect(mediaQuery.value).toBeUndefined() + expect(get_value(mediaQuery)).toBeUndefined() }) test('Raw.value is undefined', () => { @@ -1203,7 +1209,7 @@ describe('value property - undefined for nodes without a value concept', () => { const rule = root.first_child! const raw = rule.first_child! expect(raw.type_name).toBe('Raw') - expect(raw.value).toBeUndefined() + expect(get_value(raw)).toBeUndefined() }) })