@@ -369,4 +369,88 @@ public function getArrayRepresentationThrowsException(): void
369369
370370 $ subject ->getArrayRepresentation ();
371371 }
372+
373+ /**
374+ * @return array<non-empty-string, array{0: non-empty-string}>
375+ */
376+ public static function provideSelectorsWithEscapedQuotes (): array
377+ {
378+ return [
379+ 'escaped double quote in double-quoted attribute ' => ['a[href="test \\"value"] ' ],
380+ 'escaped single quote in single-quoted attribute ' => ['a[href= \'test \\\'value \'] ' ],
381+ 'multiple escaped double quotes in double-quoted attribute ' => ['a[title="say \\"hello \\" world"] ' ],
382+ 'multiple escaped single quotes in single-quoted attribute ' => ['a[title= \'say \\\'hello \\\' world \'] ' ],
383+ 'escaped quote at start of attribute value ' => ['a[data-test=" \\"start"] ' ],
384+ 'escaped quote at end of attribute value ' => ['a[data-test="end \\""] ' ],
385+ 'escaped backslash followed by quote ' => ['a[data-test="test \\\\"] ' ],
386+ 'escaped backslash before escaped quote ' => ['a[data-test="test \\\\\\"value"] ' ],
387+ 'triple backslash before quote ' => ['a[data-test="test \\\\\\""] ' ],
388+ ];
389+ }
390+
391+ /**
392+ * @test
393+ *
394+ * @param non-empty-string $selector
395+ *
396+ * @dataProvider provideSelectorsWithEscapedQuotes
397+ */
398+ public function parsesSelectorsWithEscapedQuotes (string $ selector ): void
399+ {
400+ $ result = Selector::parse (new ParserState ($ selector , Settings::create ()));
401+
402+ self ::assertInstanceOf (Selector::class, $ result );
403+ self ::assertSame ($ selector , $ result ->getSelector ());
404+ }
405+
406+ /**
407+ * @test
408+ *
409+ * @param non-empty-string $selector
410+ *
411+ * @dataProvider provideSelectorsWithEscapedQuotes
412+ */
413+ public function isValidForSelectorsWithEscapedQuotesReturnsTrue (string $ selector ): void
414+ {
415+ self ::assertTrue (Selector::isValid ($ selector ));
416+ }
417+
418+ /**
419+ * @test
420+ */
421+ public function parsingAttributeWithEscapedQuoteDoesNotPrematurelyCloseString (): void
422+ {
423+ $ selector = 'input[placeholder="Enter \\"quoted \\" text here"] ' ;
424+
425+ $ result = Selector::parse (new ParserState ($ selector , Settings::create ()));
426+
427+ self ::assertInstanceOf (Selector::class, $ result );
428+ self ::assertSame ($ selector , $ result ->getSelector ());
429+ }
430+
431+ /**
432+ * @test
433+ */
434+ public function parseDistinguishesEscapedFromUnescapedQuotes (): void
435+ {
436+ // One backslash = escaped quote (should not close string)
437+ $ selector = 'a[data-value="test \\"more"] ' ;
438+
439+ $ result = Selector::parse (new ParserState ($ selector , Settings::create ()));
440+
441+ self ::assertSame ($ selector , $ result ->getSelector ());
442+ }
443+
444+ /**
445+ * @test
446+ */
447+ public function parseHandlesEvenNumberOfBackslashesBeforeQuote (): void
448+ {
449+ // Two backslashes = escaped backslash + unescaped quote (should close string)
450+ $ selector = 'a[data-value="test \\\\"] ' ;
451+
452+ $ result = Selector::parse (new ParserState ($ selector , Settings::create ()));
453+
454+ self ::assertSame ($ selector , $ result ->getSelector ());
455+ }
372456}
0 commit comments