Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 168 additions & 0 deletions src/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 12 additions & 1 deletion src/css-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading