diff --git a/src/test/java/org/htmlunit/cssparser/dom/AbstractCSSRuleImplTest.java b/src/test/java/org/htmlunit/cssparser/dom/AbstractCSSRuleImplTest.java
new file mode 100644
index 0000000..35a98b0
--- /dev/null
+++ b/src/test/java/org/htmlunit/cssparser/dom/AbstractCSSRuleImplTest.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright (c) 2019-2024 Ronald Brill.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.htmlunit.cssparser.dom;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for {@link AbstractCSSRuleImpl}.
+ *
+ * @author Ronald Brill
+ */
+public class AbstractCSSRuleImplTest {
+
+ /**
+ * Test parent style sheet getter/setter.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void parentStyleSheet() throws Exception {
+ final CSSStyleSheetImpl styleSheet = new CSSStyleSheetImpl();
+ final CSSStyleRuleImpl rule = new CSSStyleRuleImpl(styleSheet, null, null);
+
+ assertEquals(styleSheet, rule.getParentStyleSheet());
+
+ final CSSStyleSheetImpl newStyleSheet = new CSSStyleSheetImpl();
+ rule.setParentStyleSheet(newStyleSheet);
+
+ assertEquals(newStyleSheet, rule.getParentStyleSheet());
+ }
+
+ /**
+ * Test parent rule getter/setter.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void parentRule() throws Exception {
+ final CSSStyleSheetImpl styleSheet = new CSSStyleSheetImpl();
+ final CSSPageRuleImpl parentRule = new CSSPageRuleImpl(styleSheet, null, null);
+ final CSSStyleRuleImpl rule = new CSSStyleRuleImpl(styleSheet, parentRule, null);
+
+ assertEquals(parentRule, rule.getParentRule());
+
+ final CSSPageRuleImpl newParentRule = new CSSPageRuleImpl(styleSheet, null, null);
+ rule.setParentRule(newParentRule);
+
+ assertEquals(newParentRule, rule.getParentRule());
+ }
+
+ /**
+ * Test with null parent style sheet.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void nullParentStyleSheet() throws Exception {
+ final CSSStyleRuleImpl rule = new CSSStyleRuleImpl(null, null, null);
+
+ assertNull(rule.getParentStyleSheet());
+ }
+
+ /**
+ * Test with null parent rule.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void nullParentRule() throws Exception {
+ final CSSStyleSheetImpl styleSheet = new CSSStyleSheetImpl();
+ final CSSStyleRuleImpl rule = new CSSStyleRuleImpl(styleSheet, null, null);
+
+ assertNull(rule.getParentRule());
+ }
+
+ /**
+ * Test equals with same object.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void equalsSameObject() throws Exception {
+ final CSSStyleSheetImpl styleSheet = new CSSStyleSheetImpl();
+ final CSSStyleRuleImpl rule = new CSSStyleRuleImpl(styleSheet, null, null);
+
+ assertTrue(rule.equals(rule));
+ }
+
+ /**
+ * Test equals with different types.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void equalsDifferentType() throws Exception {
+ final CSSStyleSheetImpl styleSheet = new CSSStyleSheetImpl();
+ final CSSStyleRuleImpl rule = new CSSStyleRuleImpl(styleSheet, null, null);
+
+ assertFalse(rule.equals("not a rule"));
+ assertFalse(rule.equals(null));
+ }
+
+ /**
+ * Test that parent relationships don't cause stack overflow in equals.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void equalsNoStackOverflow() throws Exception {
+ final CSSStyleSheetImpl styleSheet = new CSSStyleSheetImpl();
+ final CSSPageRuleImpl parent1 = new CSSPageRuleImpl(styleSheet, null, null);
+ final CSSStyleRuleImpl rule1 = new CSSStyleRuleImpl(styleSheet, parent1, null);
+
+ final CSSPageRuleImpl parent2 = new CSSPageRuleImpl(styleSheet, null, null);
+ final CSSStyleRuleImpl rule2 = new CSSStyleRuleImpl(styleSheet, parent2, null);
+
+ // This should not cause stack overflow even with parent relationships
+ rule1.equals(rule2);
+ }
+
+ /**
+ * Test hashCode consistency.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void hashCodeConsistency() throws Exception {
+ final CSSStyleSheetImpl styleSheet = new CSSStyleSheetImpl();
+ final CSSStyleRuleImpl rule = new CSSStyleRuleImpl(styleSheet, null, null);
+
+ final int hash1 = rule.hashCode();
+ final int hash2 = rule.hashCode();
+
+ assertEquals(hash1, hash2);
+ }
+
+ /**
+ * Test that parent relationships don't cause stack overflow in hashCode.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void hashCodeNoStackOverflow() throws Exception {
+ final CSSStyleSheetImpl styleSheet = new CSSStyleSheetImpl();
+ final CSSPageRuleImpl parent = new CSSPageRuleImpl(styleSheet, null, null);
+ final CSSStyleRuleImpl rule = new CSSStyleRuleImpl(styleSheet, parent, null);
+
+ // This should not cause stack overflow
+ final int hash = rule.hashCode();
+ assertNotEquals(0, hash); // Just verify it computed something
+ }
+
+ /**
+ * Test changing parent style sheet after construction.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void changeParentStyleSheet() throws Exception {
+ final CSSStyleSheetImpl styleSheet1 = new CSSStyleSheetImpl();
+ final CSSStyleRuleImpl rule = new CSSStyleRuleImpl(styleSheet1, null, null);
+
+ assertEquals(styleSheet1, rule.getParentStyleSheet());
+
+ final CSSStyleSheetImpl styleSheet2 = new CSSStyleSheetImpl();
+ rule.setParentStyleSheet(styleSheet2);
+
+ assertEquals(styleSheet2, rule.getParentStyleSheet());
+
+ rule.setParentStyleSheet(null);
+ assertNull(rule.getParentStyleSheet());
+ }
+
+ /**
+ * Test changing parent rule after construction.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void changeParentRule() throws Exception {
+ final CSSStyleSheetImpl styleSheet = new CSSStyleSheetImpl();
+ final CSSPageRuleImpl parent1 = new CSSPageRuleImpl(styleSheet, null, null);
+ final CSSStyleRuleImpl rule = new CSSStyleRuleImpl(styleSheet, parent1, null);
+
+ assertEquals(parent1, rule.getParentRule());
+
+ final CSSPageRuleImpl parent2 = new CSSPageRuleImpl(styleSheet, null, null);
+ rule.setParentRule(parent2);
+
+ assertEquals(parent2, rule.getParentRule());
+
+ rule.setParentRule(null);
+ assertNull(rule.getParentRule());
+ }
+}
diff --git a/src/test/java/org/htmlunit/cssparser/dom/PropteryTest.java b/src/test/java/org/htmlunit/cssparser/dom/PropteryTest.java
index f321ac0..c261f78 100644
--- a/src/test/java/org/htmlunit/cssparser/dom/PropteryTest.java
+++ b/src/test/java/org/htmlunit/cssparser/dom/PropteryTest.java
@@ -94,4 +94,83 @@ public void constructWithParams() throws Exception {
assertEquals("13.2cm", prop.getValue().toString());
assertFalse(prop.isImportant());
}
+
+ /**
+ * Test equals method with same object.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void equalsSameObject() throws Exception {
+ final LexicalUnit lu = LexicalUnitImpl.createPixel(null, 10);
+ final Property prop = new Property("color", new CSSValueImpl(lu, true), false);
+
+ assertTrue(prop.equals(prop));
+ }
+
+ /**
+ * Test equals method with different names.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void equalsDifferentNames() throws Exception {
+ final LexicalUnit lu1 = LexicalUnitImpl.createPixel(null, 10);
+ final Property prop1 = new Property("color", new CSSValueImpl(lu1, true), false);
+
+ final LexicalUnit lu2 = LexicalUnitImpl.createPixel(null, 10);
+ final Property prop2 = new Property("background", new CSSValueImpl(lu2, true), false);
+
+ assertFalse(prop1.equals(prop2));
+ }
+
+ /**
+ * Test equals method with different important flags.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void equalsDifferentImportant() throws Exception {
+ final Property prop1 = new Property("color", null, false);
+ final Property prop2 = new Property("color", null, true);
+
+ assertFalse(prop1.equals(prop2));
+ }
+
+ /**
+ * Test equals method with null.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void equalsWithNull() throws Exception {
+ final LexicalUnit lu = LexicalUnitImpl.createPixel(null, 10);
+ final Property prop = new Property("color", new CSSValueImpl(lu, true), false);
+
+ assertFalse(prop.equals(null));
+ }
+
+ /**
+ * Test equals method with different class.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void equalsWithDifferentClass() throws Exception {
+ final LexicalUnit lu = LexicalUnitImpl.createPixel(null, 10);
+ final Property prop = new Property("color", new CSSValueImpl(lu, true), false);
+
+ assertFalse(prop.equals("not a property"));
+ }
+
+ /**
+ * Test hashCode consistency.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void hashCodeConsistency() throws Exception {
+ final LexicalUnit lu = LexicalUnitImpl.createPixel(null, 10);
+ final Property prop = new Property("color", new CSSValueImpl(lu, true), false);
+
+ final int hash1 = prop.hashCode();
+ final int hash2 = prop.hashCode();
+
+ assertEquals(hash1, hash2);
+ }
}
+
diff --git a/src/test/java/org/htmlunit/cssparser/parser/CSSErrorHandlingTest.java b/src/test/java/org/htmlunit/cssparser/parser/CSSErrorHandlingTest.java
new file mode 100644
index 0000000..447bc02
--- /dev/null
+++ b/src/test/java/org/htmlunit/cssparser/parser/CSSErrorHandlingTest.java
@@ -0,0 +1,306 @@
+/*
+ * Copyright (c) 2019-2024 Ronald Brill.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.htmlunit.cssparser.parser;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.StringReader;
+
+import org.htmlunit.cssparser.ErrorHandler;
+import org.htmlunit.cssparser.dom.CSSStyleSheetImpl;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for CSS error handling and recovery.
+ *
+ * @author Ronald Brill
+ */
+public class CSSErrorHandlingTest {
+
+ /**
+ * Test malformed selector recovery.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void malformedSelector() throws Exception {
+ final String css = "p { color: red; }\n"
+ + "div# { font-size: 12px; }\n" // malformed selector
+ + "span { margin: 10px; }";
+
+ final CSSOMParser parser = new CSSOMParser();
+ final ErrorHandler errorHandler = new ErrorHandler();
+ parser.setErrorHandler(errorHandler);
+
+ final CSSStyleSheetImpl sheet = parser.parseStyleSheet(new InputSource(new StringReader(css)), null);
+
+ assertNotNull(sheet);
+ // Parser should recover and continue parsing
+ assertTrue(errorHandler.getErrorCount() > 0 || errorHandler.getWarningCount() > 0);
+ }
+
+ /**
+ * Test malformed property value recovery.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void malformedPropertyValue() throws Exception {
+ final String css = "p { color: #gg; font-size: 12px; }"; // invalid color
+
+ final CSSOMParser parser = new CSSOMParser();
+ final ErrorHandler errorHandler = new ErrorHandler();
+ parser.setErrorHandler(errorHandler);
+
+ final CSSStyleSheetImpl sheet = parser.parseStyleSheet(new InputSource(new StringReader(css)), null);
+
+ assertNotNull(sheet);
+ }
+
+ /**
+ * Test unclosed brace recovery.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void unclosedBrace() throws Exception {
+ final String css = "p { color: red;\n"
+ + "div { margin: 10px; }"; // missing closing brace for p
+
+ final CSSOMParser parser = new CSSOMParser();
+ final ErrorHandler errorHandler = new ErrorHandler();
+ parser.setErrorHandler(errorHandler);
+
+ final CSSStyleSheetImpl sheet = parser.parseStyleSheet(new InputSource(new StringReader(css)), null);
+
+ assertNotNull(sheet);
+ }
+
+ /**
+ * Test empty input.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void emptyInput() throws Exception {
+ final String css = "";
+
+ final CSSOMParser parser = new CSSOMParser();
+ final ErrorHandler errorHandler = new ErrorHandler();
+ parser.setErrorHandler(errorHandler);
+
+ final CSSStyleSheetImpl sheet = parser.parseStyleSheet(new InputSource(new StringReader(css)), null);
+
+ assertNotNull(sheet);
+ assertEquals(0, errorHandler.getErrorCount());
+ assertEquals(0, errorHandler.getFatalErrorCount());
+ }
+
+ /**
+ * Test whitespace only input.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void whitespaceOnlyInput() throws Exception {
+ final String css = " \n\t \r\n ";
+
+ final CSSOMParser parser = new CSSOMParser();
+ final ErrorHandler errorHandler = new ErrorHandler();
+ parser.setErrorHandler(errorHandler);
+
+ final CSSStyleSheetImpl sheet = parser.parseStyleSheet(new InputSource(new StringReader(css)), null);
+
+ assertNotNull(sheet);
+ assertEquals(0, errorHandler.getErrorCount());
+ }
+
+ /**
+ * Test invalid at-rule recovery.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void invalidAtRule() throws Exception {
+ final String css = "@unknown-rule { content };\n"
+ + "p { color: blue; }";
+
+ final CSSOMParser parser = new CSSOMParser();
+ final ErrorHandler errorHandler = new ErrorHandler();
+ parser.setErrorHandler(errorHandler);
+
+ final CSSStyleSheetImpl sheet = parser.parseStyleSheet(new InputSource(new StringReader(css)), null);
+
+ assertNotNull(sheet);
+ }
+
+ /**
+ * Test invalid unicode escape sequence.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void invalidUnicodeEscape() throws Exception {
+ final String css = "p { content: '\\GGGG'; }"; // invalid hex digits
+
+ final CSSOMParser parser = new CSSOMParser();
+ final ErrorHandler errorHandler = new ErrorHandler();
+ parser.setErrorHandler(errorHandler);
+
+ final CSSStyleSheetImpl sheet = parser.parseStyleSheet(new InputSource(new StringReader(css)), null);
+
+ assertNotNull(sheet);
+ }
+
+ /**
+ * Test null input source.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void nullReader() throws Exception {
+ final CSSOMParser parser = new CSSOMParser();
+ final ErrorHandler errorHandler = new ErrorHandler();
+ parser.setErrorHandler(errorHandler);
+
+ final CSSStyleSheetImpl sheet = parser.parseStyleSheet(new InputSource(new StringReader("")), null);
+
+ assertNotNull(sheet);
+ }
+
+ /**
+ * Test multiple consecutive errors.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void multipleConsecutiveErrors() throws Exception {
+ final String css = "p# { }\n"
+ + "div## { }\n"
+ + "span### { }\n"
+ + "a { color: red; }";
+
+ final CSSOMParser parser = new CSSOMParser();
+ final ErrorHandler errorHandler = new ErrorHandler();
+ parser.setErrorHandler(errorHandler);
+
+ final CSSStyleSheetImpl sheet = parser.parseStyleSheet(new InputSource(new StringReader(css)), null);
+
+ assertNotNull(sheet);
+ }
+
+ /**
+ * Test invalid character in selector.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void invalidCharacterInSelector() throws Exception {
+ final String css = "p@invalid { color: red; }";
+
+ final CSSOMParser parser = new CSSOMParser();
+ final ErrorHandler errorHandler = new ErrorHandler();
+ parser.setErrorHandler(errorHandler);
+
+ final CSSStyleSheetImpl sheet = parser.parseStyleSheet(new InputSource(new StringReader(css)), null);
+
+ assertNotNull(sheet);
+ }
+
+ /**
+ * Test missing semicolon between properties.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void missingSemicolon() throws Exception {
+ final String css = "p { color: red margin: 10px; }"; // missing semicolon after red
+
+ final CSSOMParser parser = new CSSOMParser();
+ final ErrorHandler errorHandler = new ErrorHandler();
+ parser.setErrorHandler(errorHandler);
+
+ final CSSStyleSheetImpl sheet = parser.parseStyleSheet(new InputSource(new StringReader(css)), null);
+
+ assertNotNull(sheet);
+ }
+
+ /**
+ * Test very long CSS input for buffer handling.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void veryLongInput() throws Exception {
+ final StringBuilder css = new StringBuilder();
+ for (int i = 0; i < 1000; i++) {
+ css.append(".class").append(i).append(" { color: red; margin: ").append(i).append("px; }\n");
+ }
+
+ final CSSOMParser parser = new CSSOMParser();
+ final ErrorHandler errorHandler = new ErrorHandler();
+ parser.setErrorHandler(errorHandler);
+
+ final CSSStyleSheetImpl sheet = parser.parseStyleSheet(new InputSource(new StringReader(css.toString())), null);
+
+ assertNotNull(sheet);
+ assertTrue(sheet.getCssRules().getLength() > 0);
+ }
+
+ /**
+ * Test deeply nested at-rules.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void deeplyNestedAtRules() throws Exception {
+ final String css = "@media screen { "
+ + "@supports (display: flex) { "
+ + "div { display: flex; } "
+ + "} "
+ + "}";
+
+ final CSSOMParser parser = new CSSOMParser();
+ final ErrorHandler errorHandler = new ErrorHandler();
+ parser.setErrorHandler(errorHandler);
+
+ final CSSStyleSheetImpl sheet = parser.parseStyleSheet(new InputSource(new StringReader(css)), null);
+
+ assertNotNull(sheet);
+ }
+
+ /**
+ * Test special characters in strings.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void specialCharactersInStrings() throws Exception {
+ final String css = "p::before { content: '\\n\\r\\t\\\\'; }";
+
+ final CSSOMParser parser = new CSSOMParser();
+ final ErrorHandler errorHandler = new ErrorHandler();
+ parser.setErrorHandler(errorHandler);
+
+ final CSSStyleSheetImpl sheet = parser.parseStyleSheet(new InputSource(new StringReader(css)), null);
+
+ assertNotNull(sheet);
+ }
+
+ /**
+ * Test comments with special content.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void commentsWithSpecialContent() throws Exception {
+ final String css = "/* comment with // and /* nested */ */ p { color: red; }";
+
+ final CSSOMParser parser = new CSSOMParser();
+ final ErrorHandler errorHandler = new ErrorHandler();
+ parser.setErrorHandler(errorHandler);
+
+ final CSSStyleSheetImpl sheet = parser.parseStyleSheet(new InputSource(new StringReader(css)), null);
+
+ assertNotNull(sheet);
+ }
+}
diff --git a/src/test/java/org/htmlunit/cssparser/parser/CssCharStreamTest.java b/src/test/java/org/htmlunit/cssparser/parser/CssCharStreamTest.java
new file mode 100644
index 0000000..bc7853a
--- /dev/null
+++ b/src/test/java/org/htmlunit/cssparser/parser/CssCharStreamTest.java
@@ -0,0 +1,385 @@
+/*
+ * Copyright (c) 2019-2024 Ronald Brill.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.htmlunit.cssparser.parser;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.io.StringReader;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for {@link CssCharStream}.
+ *
+ * @author Ronald Brill
+ */
+public class CssCharStreamTest {
+
+ /**
+ * Test basic character reading.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void readChar() throws Exception {
+ final String input = "abc";
+ final CssCharStream stream = new CssCharStream(new StringReader(input), 1, 1);
+
+ assertEquals('a', stream.readChar());
+ assertEquals('b', stream.readChar());
+ assertEquals('c', stream.readChar());
+
+ assertThrows(IOException.class, () -> stream.readChar());
+ }
+
+ /**
+ * Test beginToken functionality.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void beginToken() throws Exception {
+ final String input = "test";
+ final CssCharStream stream = new CssCharStream(new StringReader(input), 1, 1);
+
+ final char firstChar = stream.beginToken();
+ assertEquals('t', firstChar);
+ assertEquals('e', stream.readChar());
+ }
+
+ /**
+ * Test getImage for simple token.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void getImage() throws Exception {
+ final String input = "token";
+ final CssCharStream stream = new CssCharStream(new StringReader(input), 1, 1);
+
+ stream.beginToken(); // reads 't'
+ stream.readChar(); // 'o'
+ stream.readChar(); // 'k'
+ stream.readChar(); // 'e'
+ stream.readChar(); // 'n'
+
+ assertEquals("token", stream.getImage());
+ }
+
+ /**
+ * Test backup functionality.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void backup() throws Exception {
+ final String input = "abcd";
+ final CssCharStream stream = new CssCharStream(new StringReader(input), 1, 1);
+
+ char c = stream.beginToken(); // reads 'a'
+ assertEquals('a', c);
+ assertEquals('b', stream.readChar());
+ assertEquals('c', stream.readChar());
+
+ stream.backup(1);
+ assertEquals('c', stream.readChar());
+ assertEquals('d', stream.readChar());
+ }
+
+ /**
+ * Test line and column tracking.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void lineColumnTracking() throws Exception {
+ final String input = "a\nb\nc";
+ final CssCharStream stream = new CssCharStream(new StringReader(input), 1, 1);
+
+ char c = stream.beginToken(); // reads 'a'
+ assertEquals('a', c);
+ assertEquals(1, stream.getBeginLine());
+ assertEquals(1, stream.getBeginColumn());
+ assertEquals(1, stream.getEndLine());
+ assertEquals(1, stream.getEndColumn());
+
+ stream.readChar(); // '\n'
+ assertEquals(1, stream.getEndLine());
+ assertEquals(2, stream.getEndColumn());
+
+ stream.readChar(); // 'b'
+ assertEquals(2, stream.getEndLine());
+ assertEquals(1, stream.getEndColumn());
+ }
+
+ /**
+ * Test carriage return handling.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void carriageReturnHandling() throws Exception {
+ final String input = "a\rb\r\nc";
+ final CssCharStream stream = new CssCharStream(new StringReader(input), 1, 1);
+
+ stream.beginToken(); // 'a'
+ stream.readChar(); // '\r'
+ assertEquals(1, stream.getEndLine());
+
+ stream.readChar(); // 'b'
+ assertEquals(2, stream.getEndLine());
+
+ stream.readChar(); // '\r'
+ stream.readChar(); // '\n'
+ assertEquals(2, stream.getEndLine());
+
+ stream.readChar(); // 'c'
+ assertEquals(3, stream.getEndLine());
+ }
+
+ /**
+ * Test getSuffix functionality.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void getSuffix() throws Exception {
+ final String input = "testing";
+ final CssCharStream stream = new CssCharStream(new StringReader(input), 1, 1);
+
+ stream.beginToken(); // 't'
+ for (int i = 0; i < 6; i++) {
+ stream.readChar();
+ }
+
+ final char[] suffix = stream.getSuffix(3);
+ assertEquals(3, suffix.length);
+ assertEquals('i', suffix[0]);
+ assertEquals('n', suffix[1]);
+ assertEquals('g', suffix[2]);
+ }
+
+ /**
+ * Test custom buffer size constructor.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void customBufferSize() throws Exception {
+ final String input = "test";
+ final CssCharStream stream = new CssCharStream(new StringReader(input), 1, 1, 10);
+
+ char c = stream.beginToken(); // reads 't'
+ assertEquals('t', c);
+ assertEquals('e', stream.readChar());
+ assertEquals('s', stream.readChar());
+ }
+
+ /**
+ * Test done() method.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void done() throws Exception {
+ final String input = "test";
+ final CssCharStream stream = new CssCharStream(new StringReader(input), 1, 1);
+
+ stream.beginToken();
+ stream.readChar();
+
+ stream.done();
+ // After done, internal buffers are cleared
+ }
+
+ /**
+ * Test adjustBeginLineColumn.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void adjustBeginLineColumn() throws Exception {
+ final String input = "test\nline";
+ final CssCharStream stream = new CssCharStream(new StringReader(input), 1, 1);
+
+ stream.beginToken();
+ stream.readChar();
+ stream.readChar();
+
+ stream.adjustBeginLineColumn(5, 10);
+ assertEquals(5, stream.getBeginLine());
+ assertEquals(10, stream.getBeginColumn());
+ }
+
+ /**
+ * Test tab size get/set.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void tabSize() throws Exception {
+ final String input = "test";
+ final CssCharStream stream = new CssCharStream(new StringReader(input), 1, 1);
+
+ assertEquals(1, stream.getTabSize());
+
+ stream.setTabSize(8);
+ assertEquals(8, stream.getTabSize());
+ }
+
+ /**
+ * Test trackLineColumn get/set.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void trackLineColumn() throws Exception {
+ final String input = "test";
+ final CssCharStream stream = new CssCharStream(new StringReader(input), 1, 1);
+
+ assertTrue(stream.isTrackLineColumn());
+
+ stream.setTrackLineColumn(false);
+ assertFalse(stream.isTrackLineColumn());
+ }
+
+ /**
+ * Test buffer expansion with large input.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void bufferExpansion() throws Exception {
+ final StringBuilder largeInput = new StringBuilder();
+ for (int i = 0; i < 5000; i++) {
+ largeInput.append('x');
+ }
+
+ final CssCharStream stream = new CssCharStream(new StringReader(largeInput.toString()), 1, 1, 100);
+
+ char c = stream.beginToken(); // reads first 'x'
+ assertEquals('x', c);
+ for (int i = 0; i < 4999; i++) {
+ assertEquals('x', stream.readChar());
+ }
+ }
+
+ /**
+ * Test empty input.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void emptyInput() throws Exception {
+ final CssCharStream stream = new CssCharStream(new StringReader(""), 1, 1);
+
+ assertThrows(IOException.class, () -> stream.readChar());
+ }
+
+ /**
+ * Test getImage with wrap around buffer.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void getImageWrapAround() throws Exception {
+ final StringBuilder input = new StringBuilder();
+ for (int i = 0; i < 3000; i++) {
+ input.append((char) ('a' + (i % 26)));
+ }
+
+ final CssCharStream stream = new CssCharStream(new StringReader(input.toString()), 1, 1, 100);
+
+ stream.beginToken(); // reads first char
+ for (int i = 0; i < 49; i++) {
+ stream.readChar();
+ }
+
+ final String image = stream.getImage();
+ assertNotNull(image);
+ assertEquals(50, image.length());
+ }
+
+ /**
+ * Test backup with multiple characters.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void backupMultiple() throws Exception {
+ final String input = "abcdefgh";
+ final CssCharStream stream = new CssCharStream(new StringReader(input), 1, 1);
+
+ stream.beginToken(); // 'a'
+ for (int i = 0; i < 4; i++) {
+ stream.readChar(); // b, c, d, e
+ }
+
+ stream.backup(3);
+ assertEquals('c', stream.readChar());
+ assertEquals('d', stream.readChar());
+ assertEquals('e', stream.readChar());
+ }
+
+ /**
+ * Test multiple tokens.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void multipleTokens() throws Exception {
+ final String input = "first second third";
+ final CssCharStream stream = new CssCharStream(new StringReader(input), 1, 1);
+
+ // First token
+ stream.beginToken(); // 'f'
+ for (int i = 0; i < 4; i++) {
+ stream.readChar(); // i, r, s, t
+ }
+ assertEquals("first", stream.getImage());
+
+ // Skip space
+ stream.readChar();
+
+ // Second token
+ stream.beginToken(); // 's'
+ for (int i = 0; i < 5; i++) {
+ stream.readChar(); // e, c, o, n, d
+ }
+ assertEquals("second", stream.getImage());
+ }
+
+ /**
+ * Test getSuffix with length greater than buffer position.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void getSuffixWrapAround() throws Exception {
+ final StringBuilder input = new StringBuilder();
+ for (int i = 0; i < 200; i++) {
+ input.append('a');
+ }
+
+ final CssCharStream stream = new CssCharStream(new StringReader(input.toString()), 1, 1, 50);
+
+ stream.beginToken();
+ for (int i = 0; i < 100; i++) {
+ stream.readChar();
+ }
+
+ final char[] suffix = stream.getSuffix(5);
+ assertEquals(5, suffix.length);
+ for (int i = 0; i < 5; i++) {
+ assertEquals('a', suffix[i]);
+ }
+ }
+
+ /**
+ * Test staticFlag constant.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void staticFlag() throws Exception {
+ assertFalse(CssCharStream.staticFlag);
+ }
+}
diff --git a/src/test/java/org/htmlunit/cssparser/parser/condition/PseudoClassConditionTest.java b/src/test/java/org/htmlunit/cssparser/parser/condition/PseudoClassConditionTest.java
index f8fe00b..6a18e24 100644
--- a/src/test/java/org/htmlunit/cssparser/parser/condition/PseudoClassConditionTest.java
+++ b/src/test/java/org/htmlunit/cssparser/parser/condition/PseudoClassConditionTest.java
@@ -60,4 +60,64 @@ public void withValue() throws Exception {
assertEquals(":value", c.toString());
}
+
+ /**
+ * Test double colon prefix.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void doubleColonPrefix() throws Exception {
+ final PseudoClassCondition c = new PseudoClassCondition("hover", null, true);
+ assertEquals("hover", c.getValue());
+ assertEquals("::hover", c.toString());
+ }
+
+ /**
+ * Test single colon prefix.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void singleColonPrefix() throws Exception {
+ final PseudoClassCondition c = new PseudoClassCondition("hover", null, false);
+ assertEquals("hover", c.getValue());
+ assertEquals(":hover", c.toString());
+ }
+
+ /**
+ * Test double colon with null value.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void doubleColonWithNullValue() throws Exception {
+ final PseudoClassCondition c = new PseudoClassCondition(null, null, true);
+ assertNull(c.getValue());
+ assertNull(c.toString());
+ }
+
+ /**
+ * Test condition type.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void conditionType() throws Exception {
+ final PseudoClassCondition c = new PseudoClassCondition("active", null, false);
+ assertEquals(Condition.ConditionType.PSEUDO_CLASS_CONDITION, c.getConditionType());
+ }
+
+ /**
+ * Test various pseudo-class values with double colon.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void variousPseudoClassesDoubleColon() throws Exception {
+ final PseudoClassCondition c1 = new PseudoClassCondition("first-child", null, true);
+ assertEquals("::first-child", c1.toString());
+
+ final PseudoClassCondition c2 = new PseudoClassCondition("last-child", null, true);
+ assertEquals("::last-child", c2.toString());
+
+ final PseudoClassCondition c3 = new PseudoClassCondition("nth-child(2n)", null, true);
+ assertEquals("::nth-child(2n)", c3.toString());
+ }
}
+
diff --git a/src/test/java/org/htmlunit/cssparser/parser/media/MediaQueryTest.java b/src/test/java/org/htmlunit/cssparser/parser/media/MediaQueryTest.java
index fa68bb9..6fb302e 100644
--- a/src/test/java/org/htmlunit/cssparser/parser/media/MediaQueryTest.java
+++ b/src/test/java/org/htmlunit/cssparser/parser/media/MediaQueryTest.java
@@ -76,4 +76,144 @@ public void media() throws Exception {
final MediaQuery mq = new MediaQuery("test");
assertEquals("test", mq.getMedia());
}
+
+ /**
+ * Test isOnly method.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void isOnly() throws Exception {
+ MediaQuery mq = new MediaQuery("screen", false, false);
+ assertEquals(false, mq.isOnly());
+
+ mq = new MediaQuery("screen", true, false);
+ assertEquals(true, mq.isOnly());
+ }
+
+ /**
+ * Test isNot method.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void isNot() throws Exception {
+ MediaQuery mq = new MediaQuery("screen", false, false);
+ assertEquals(false, mq.isNot());
+
+ mq = new MediaQuery("screen", false, true);
+ assertEquals(true, mq.isNot());
+ }
+
+ /**
+ * Test with both only and not (edge case).
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void onlyAndNot() throws Exception {
+ // If both only and not are true, only takes precedence
+ final MediaQuery mq = new MediaQuery("screen", true, true);
+ assertEquals("only screen", mq.toString());
+ }
+
+ /**
+ * Test with null media type.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void nullMediaType() throws Exception {
+ final MediaQuery mq = new MediaQuery(null);
+ // null media is converted to "all" but implicitAll is set
+ // so toString doesn't show it unless there are properties
+ assertEquals("", mq.toString());
+ assertEquals("all", mq.getMedia());
+ }
+
+ /**
+ * Test with empty media type.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void emptyMediaType() throws Exception {
+ final MediaQuery mq = new MediaQuery("");
+ assertEquals("", mq.toString());
+ }
+
+ /**
+ * Test multiple properties.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void multipleProperties() throws Exception {
+ final MediaQuery mq = new MediaQuery("screen");
+
+ final CSSValueImpl value1 = new CSSValueImpl(null);
+ value1.setCssText("800px");
+ final Property prop1 = new Property("min-width", value1, false);
+ mq.addMediaProperty(prop1);
+
+ final CSSValueImpl value2 = new CSSValueImpl(null);
+ value2.setCssText("1200px");
+ final Property prop2 = new Property("max-width", value2, false);
+ mq.addMediaProperty(prop2);
+
+ final CSSValueImpl value3 = new CSSValueImpl(null);
+ value3.setCssText("landscape");
+ final Property prop3 = new Property("orientation", value3, false);
+ mq.addMediaProperty(prop3);
+
+ assertEquals("screen and (min-width: 800px) and (max-width: 1200px) and (orientation: landscape)",
+ mq.toString());
+ assertEquals(3, mq.getProperties().size());
+ }
+
+ /**
+ * Test common media types.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void commonMediaTypes() throws Exception {
+ MediaQuery mq = new MediaQuery("all");
+ assertEquals("all", mq.toString());
+
+ mq = new MediaQuery("screen");
+ assertEquals("screen", mq.toString());
+
+ mq = new MediaQuery("print");
+ assertEquals("print", mq.toString());
+
+ mq = new MediaQuery("speech");
+ assertEquals("speech", mq.toString());
+ }
+
+ /**
+ * Test with only and properties.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void onlyWithProperties() throws Exception {
+ final MediaQuery mq = new MediaQuery("print", true, false);
+
+ final CSSValueImpl value = new CSSValueImpl(null);
+ value.setCssText("300dpi");
+ final Property prop = new Property("resolution", value, false);
+ mq.addMediaProperty(prop);
+
+ assertEquals("only print and (resolution: 300dpi)", mq.toString());
+ }
+
+ /**
+ * Test with not and properties.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void notWithProperties() throws Exception {
+ final MediaQuery mq = new MediaQuery("screen", false, true);
+
+ final CSSValueImpl value = new CSSValueImpl(null);
+ value.setCssText("600px");
+ final Property prop = new Property("max-width", value, false);
+ mq.addMediaProperty(prop);
+
+ assertEquals("not screen and (max-width: 600px)", mq.toString());
+ }
}
+
diff --git a/src/test/java/org/htmlunit/cssparser/parser/selector/ComplexSelectorTest.java b/src/test/java/org/htmlunit/cssparser/parser/selector/ComplexSelectorTest.java
new file mode 100644
index 0000000..cc00d15
--- /dev/null
+++ b/src/test/java/org/htmlunit/cssparser/parser/selector/ComplexSelectorTest.java
@@ -0,0 +1,310 @@
+/*
+ * Copyright (c) 2019-2024 Ronald Brill.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.htmlunit.cssparser.parser.selector;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.io.StringReader;
+
+import org.htmlunit.cssparser.ErrorHandler;
+import org.htmlunit.cssparser.dom.CSSRuleListImpl;
+import org.htmlunit.cssparser.dom.CSSStyleRuleImpl;
+import org.htmlunit.cssparser.dom.CSSStyleSheetImpl;
+import org.htmlunit.cssparser.parser.CSSOMParser;
+import org.htmlunit.cssparser.parser.InputSource;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for complex selector combinations.
+ *
+ * @author Ronald Brill
+ */
+public class ComplexSelectorTest {
+
+ /**
+ * Test complex descendant selector chain.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void complexDescendantChain() throws Exception {
+ final String css = "div ul li a { color: blue; }";
+
+ final CSSOMParser parser = new CSSOMParser();
+ final ErrorHandler errorHandler = new ErrorHandler();
+ parser.setErrorHandler(errorHandler);
+
+ final CSSStyleSheetImpl sheet = parser.parseStyleSheet(new InputSource(new StringReader(css)), null);
+
+ assertNotNull(sheet);
+ assertEquals(0, errorHandler.getErrorCount());
+
+ final CSSRuleListImpl rules = sheet.getCssRules();
+ assertEquals(1, rules.getLength());
+ }
+
+ /**
+ * Test child combinator selector.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void childCombinator() throws Exception {
+ final String css = "div > p { margin: 10px; }";
+
+ final CSSOMParser parser = new CSSOMParser();
+ final ErrorHandler errorHandler = new ErrorHandler();
+ parser.setErrorHandler(errorHandler);
+
+ final CSSStyleSheetImpl sheet = parser.parseStyleSheet(new InputSource(new StringReader(css)), null);
+
+ assertNotNull(sheet);
+ assertEquals(0, errorHandler.getErrorCount());
+ }
+
+ /**
+ * Test adjacent sibling combinator.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void adjacentSiblingCombinator() throws Exception {
+ final String css = "h1 + p { font-weight: bold; }";
+
+ final CSSOMParser parser = new CSSOMParser();
+ final ErrorHandler errorHandler = new ErrorHandler();
+ parser.setErrorHandler(errorHandler);
+
+ final CSSStyleSheetImpl sheet = parser.parseStyleSheet(new InputSource(new StringReader(css)), null);
+
+ assertNotNull(sheet);
+ assertEquals(0, errorHandler.getErrorCount());
+ }
+
+ /**
+ * Test general sibling combinator.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void generalSiblingCombinator() throws Exception {
+ final String css = "h1 ~ p { color: gray; }";
+
+ final CSSOMParser parser = new CSSOMParser();
+ final ErrorHandler errorHandler = new ErrorHandler();
+ parser.setErrorHandler(errorHandler);
+
+ final CSSStyleSheetImpl sheet = parser.parseStyleSheet(new InputSource(new StringReader(css)), null);
+
+ assertNotNull(sheet);
+ assertEquals(0, errorHandler.getErrorCount());
+ }
+
+ /**
+ * Test complex selector with classes and IDs.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void classesAndIds() throws Exception {
+ final String css = "div#main.container.active { display: block; }";
+
+ final CSSOMParser parser = new CSSOMParser();
+ final ErrorHandler errorHandler = new ErrorHandler();
+ parser.setErrorHandler(errorHandler);
+
+ final CSSStyleSheetImpl sheet = parser.parseStyleSheet(new InputSource(new StringReader(css)), null);
+
+ assertNotNull(sheet);
+ assertEquals(0, errorHandler.getErrorCount());
+ }
+
+ /**
+ * Test pseudo-class combinations.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void pseudoClassCombinations() throws Exception {
+ final String css = "a:link:hover:not(.external) { text-decoration: underline; }";
+
+ final CSSOMParser parser = new CSSOMParser();
+ final ErrorHandler errorHandler = new ErrorHandler();
+ parser.setErrorHandler(errorHandler);
+
+ final CSSStyleSheetImpl sheet = parser.parseStyleSheet(new InputSource(new StringReader(css)), null);
+
+ assertNotNull(sheet);
+ }
+
+ /**
+ * Test attribute selector combinations.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void attributeSelectorCombinations() throws Exception {
+ final String css = "input[type=\"text\"][required]:focus { border-color: blue; }";
+
+ final CSSOMParser parser = new CSSOMParser();
+ final ErrorHandler errorHandler = new ErrorHandler();
+ parser.setErrorHandler(errorHandler);
+
+ final CSSStyleSheetImpl sheet = parser.parseStyleSheet(new InputSource(new StringReader(css)), null);
+
+ assertNotNull(sheet);
+ }
+
+ /**
+ * Test nth-child pseudo-class variations.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void nthChildVariations() throws Exception {
+ final String css = "li:nth-child(2n) { background: gray; }\n"
+ + "li:nth-child(odd) { background: white; }\n"
+ + "li:nth-child(3n+1) { font-weight: bold; }";
+
+ final CSSOMParser parser = new CSSOMParser();
+ final ErrorHandler errorHandler = new ErrorHandler();
+ parser.setErrorHandler(errorHandler);
+
+ final CSSStyleSheetImpl sheet = parser.parseStyleSheet(new InputSource(new StringReader(css)), null);
+
+ assertNotNull(sheet);
+ assertEquals(3, sheet.getCssRules().getLength());
+ }
+
+ /**
+ * Test pseudo-element with pseudo-class.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void pseudoElementWithPseudoClass() throws Exception {
+ final String css = "p:first-child::first-letter { font-size: 2em; }";
+
+ final CSSOMParser parser = new CSSOMParser();
+ final ErrorHandler errorHandler = new ErrorHandler();
+ parser.setErrorHandler(errorHandler);
+
+ final CSSStyleSheetImpl sheet = parser.parseStyleSheet(new InputSource(new StringReader(css)), null);
+
+ assertNotNull(sheet);
+ }
+
+ /**
+ * Test complex selector list.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void complexSelectorList() throws Exception {
+ final String css = "h1, h2, h3, .heading, #title { font-family: Arial; }";
+
+ final CSSOMParser parser = new CSSOMParser();
+ final ErrorHandler errorHandler = new ErrorHandler();
+ parser.setErrorHandler(errorHandler);
+
+ final CSSStyleSheetImpl sheet = parser.parseStyleSheet(new InputSource(new StringReader(css)), null);
+
+ assertNotNull(sheet);
+ assertEquals(0, errorHandler.getErrorCount());
+
+ final CSSStyleRuleImpl rule = (CSSStyleRuleImpl) sheet.getCssRules().getRules().get(0);
+ assertNotNull(rule.getSelectorText());
+ }
+
+ /**
+ * Test mixed combinators.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void mixedCombinators() throws Exception {
+ final String css = "div > ul li + span ~ a { color: red; }";
+
+ final CSSOMParser parser = new CSSOMParser();
+ final ErrorHandler errorHandler = new ErrorHandler();
+ parser.setErrorHandler(errorHandler);
+
+ final CSSStyleSheetImpl sheet = parser.parseStyleSheet(new InputSource(new StringReader(css)), null);
+
+ assertNotNull(sheet);
+ }
+
+ /**
+ * Test universal selector combinations.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void universalSelectorCombinations() throws Exception {
+ final String css = "* { box-sizing: border-box; }\n"
+ + "div * { margin: 0; }\n"
+ + "*:hover { cursor: pointer; }";
+
+ final CSSOMParser parser = new CSSOMParser();
+ final ErrorHandler errorHandler = new ErrorHandler();
+ parser.setErrorHandler(errorHandler);
+
+ final CSSStyleSheetImpl sheet = parser.parseStyleSheet(new InputSource(new StringReader(css)), null);
+
+ assertNotNull(sheet);
+ assertEquals(3, sheet.getCssRules().getLength());
+ }
+
+ /**
+ * Test namespace selectors.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void namespaceSelectorsCombinations() throws Exception {
+ final String css = "svg|rect { fill: blue; }";
+
+ final CSSOMParser parser = new CSSOMParser();
+ final ErrorHandler errorHandler = new ErrorHandler();
+ parser.setErrorHandler(errorHandler);
+
+ final CSSStyleSheetImpl sheet = parser.parseStyleSheet(new InputSource(new StringReader(css)), null);
+
+ assertNotNull(sheet);
+ }
+
+ /**
+ * Test case-insensitive attribute selector.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void caseInsensitiveAttribute() throws Exception {
+ final String css = "a[href*=\"example\" i] { color: blue; }";
+
+ final CSSOMParser parser = new CSSOMParser();
+ final ErrorHandler errorHandler = new ErrorHandler();
+ parser.setErrorHandler(errorHandler);
+
+ final CSSStyleSheetImpl sheet = parser.parseStyleSheet(new InputSource(new StringReader(css)), null);
+
+ assertNotNull(sheet);
+ }
+
+ /**
+ * Test deeply nested selectors.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void deeplyNestedSelectors() throws Exception {
+ final String css = "html body div.container section article > p:first-of-type { line-height: 1.5; }";
+
+ final CSSOMParser parser = new CSSOMParser();
+ final ErrorHandler errorHandler = new ErrorHandler();
+ parser.setErrorHandler(errorHandler);
+
+ final CSSStyleSheetImpl sheet = parser.parseStyleSheet(new InputSource(new StringReader(css)), null);
+
+ assertNotNull(sheet);
+ assertEquals(0, errorHandler.getErrorCount());
+ }
+}
diff --git a/src/test/java/org/htmlunit/cssparser/parser/selector/DescendantSelectorImplTest.java b/src/test/java/org/htmlunit/cssparser/parser/selector/DescendantSelectorImplTest.java
index ad63cb2..5577284 100644
--- a/src/test/java/org/htmlunit/cssparser/parser/selector/DescendantSelectorImplTest.java
+++ b/src/test/java/org/htmlunit/cssparser/parser/selector/DescendantSelectorImplTest.java
@@ -86,4 +86,64 @@ public void pseudoElementDescendant() {
assertEquals("a:after", selector.toString());
}
+
+ /**
+ * Test with null ancestor selector.
+ * @throws Exception on failure
+ */
+ @Test
+ public void nullAncestorSelector() {
+ final ElementSelector descendant = new ElementSelector("a", null);
+ final DescendantSelector selector = new DescendantSelector(null, descendant);
+
+ assertEquals(null, selector.getAncestorSelector());
+ assertEquals(descendant, selector.getSimpleSelector());
+ assertEquals(" a", selector.toString());
+ }
+
+ /**
+ * Test with null simple selector.
+ * @throws Exception on failure
+ */
+ @Test
+ public void nullSimpleSelector() {
+ final ElementSelector parent = new ElementSelector("p", null);
+ final DescendantSelector selector = new DescendantSelector(parent, null);
+
+ assertEquals(parent, selector.getAncestorSelector());
+ assertEquals(null, selector.getSimpleSelector());
+ }
+
+ /**
+ * Test complex descendant chain.
+ * @throws Exception on failure
+ */
+ @Test
+ public void complexDescendantChain() {
+ final ElementSelector grandParent = new ElementSelector("div", null);
+ final ElementSelector parent = new ElementSelector("p", null);
+ final DescendantSelector level1 = new DescendantSelector(grandParent, parent);
+ final ElementSelector child = new ElementSelector("a", null);
+ final DescendantSelector level2 = new DescendantSelector(level1, child);
+
+ assertEquals(level1, level2.getAncestorSelector());
+ assertEquals(child, level2.getSimpleSelector());
+ assertEquals("div p a", level2.toString());
+ }
+
+ /**
+ * Test descendant with multiple levels and pseudo-element.
+ * @throws Exception on failure
+ */
+ @Test
+ public void complexDescendantWithPseudoElement() {
+ final ElementSelector grandParent = new ElementSelector("div", null);
+ final ElementSelector parent = new ElementSelector("p", null);
+ final DescendantSelector level1 = new DescendantSelector(grandParent, parent);
+ final PseudoElementSelector pseudo = new PseudoElementSelector("first-line", null, false);
+ final DescendantSelector level2 = new DescendantSelector(level1, pseudo);
+
+ assertEquals("div p:first-line", level2.toString());
+ }
}
+
diff --git a/src/test/java/org/htmlunit/cssparser/parser/selector/SelectorSpecificityTest.java b/src/test/java/org/htmlunit/cssparser/parser/selector/SelectorSpecificityTest.java
index de24606..742e47f 100644
--- a/src/test/java/org/htmlunit/cssparser/parser/selector/SelectorSpecificityTest.java
+++ b/src/test/java/org/htmlunit/cssparser/parser/selector/SelectorSpecificityTest.java
@@ -98,6 +98,90 @@ public void pseudoSelector() throws Exception {
selectorSpecifity("h2:nth-last-of-type(n + 2)", "0,0,1,1");
}
+ /**
+ * Test specificity with :not() pseudo-class.
+ * @throws Exception if the test fails
+ */
+ @Test
+ public void notPseudoClass() throws Exception {
+ selectorSpecifity(":not(p)", "0,0,0,1");
+ selectorSpecifity(":not(.class)", "0,0,1,0");
+ selectorSpecifity(":not(#id)", "0,1,0,0");
+ }
+
+ /**
+ * Test specificity with :is() pseudo-class.
+ * @throws Exception if the test fails
+ */
+ @Test
+ public void isPseudoClass() throws Exception {
+ selectorSpecifity(":is(p)", "0,0,0,1");
+ selectorSpecifity(":is(.class)", "0,0,1,0");
+ selectorSpecifity(":is(#id)", "0,1,0,0");
+ }
+
+ /**
+ * Test specificity with :where() pseudo-class (always 0 specificity).
+ * @throws Exception if the test fails
+ */
+ @Test
+ public void wherePseudoClass() throws Exception {
+ selectorSpecifity(":where(p)", "0,0,0,0");
+ selectorSpecifity(":where(.class)", "0,0,0,0");
+ selectorSpecifity(":where(#id)", "0,0,0,0");
+ }
+
+ /**
+ * Test specificity with multiple IDs.
+ * @throws Exception if the test fails
+ */
+ @Test
+ public void multipleIds() throws Exception {
+ selectorSpecifity("#id1#id2", "0,2,0,0");
+ selectorSpecifity("div#id1#id2", "0,2,0,1");
+ }
+
+ /**
+ * Test specificity with complex combinations.
+ * @throws Exception if the test fails
+ */
+ @Test
+ public void complexCombinations() throws Exception {
+ selectorSpecifity("div.class1.class2#id", "0,1,2,1");
+ selectorSpecifity("ul li:first-child a", "0,0,1,3");
+ selectorSpecifity("#nav ul li a:hover", "0,1,1,3");
+ }
+
+ /**
+ * Test specificity with attribute selectors.
+ * @throws Exception if the test fails
+ */
+ @Test
+ public void attributeSelectors() throws Exception {
+ selectorSpecifity("[type]", "0,0,1,0");
+ selectorSpecifity("input[type=\"text\"]", "0,0,1,1");
+ selectorSpecifity("a[href^=\"https\"]", "0,0,1,1");
+ }
+
+ /**
+ * Test compareTo with various combinations.
+ * @throws Exception if the test fails
+ */
+ @Test
+ public void compareToVariousCombinations() throws Exception {
+ final SelectorSpecificity s1 = selectorSpecifity("*", "0,0,0,0");
+ final SelectorSpecificity s2 = selectorSpecifity("#id", "0,1,0,0");
+ final SelectorSpecificity s3 = selectorSpecifity(".class", "0,0,1,0");
+ final SelectorSpecificity s4 = selectorSpecifity("p", "0,0,0,1");
+
+ assertTrue(s1.compareTo(s2) < 0);
+ assertTrue(s1.compareTo(s3) < 0);
+ assertTrue(s1.compareTo(s4) < 0);
+ assertTrue(s4.compareTo(s3) < 0);
+ assertTrue(s3.compareTo(s2) < 0);
+ }
+
+
/**
* @throws Exception if the test fails
*/