@@ -407,6 +407,161 @@ describe('Locator', () => {
407407 expect ( nodes ) . to . have . length ( 9 )
408408 } )
409409
410+ it ( 'withClass: single class (word-exact)' , ( ) => {
411+ const l = Locator . build ( 'a' ) . withClass ( 'ps-menu-button' )
412+ const nodes = xpath . select ( l . toXPath ( ) , doc )
413+ expect ( nodes ) . to . have . length ( 10 , l . toXPath ( ) )
414+ } )
415+
416+ it ( 'withClass: variadic ANDs class conditions' , ( ) => {
417+ const l = Locator . build ( 'a' ) . withClass ( 'ps-menu-button' , 'active' )
418+ const nodes = xpath . select ( l . toXPath ( ) , doc )
419+ expect ( nodes ) . to . have . length ( 1 , l . toXPath ( ) )
420+ } )
421+
422+ it ( 'withClass: word-exact (does not match partial class)' , ( ) => {
423+ const l = Locator . build ( 'div' ) . withClass ( 'form-' )
424+ const nodes = xpath . select ( l . toXPath ( ) , doc )
425+ expect ( nodes ) . to . have . length ( 0 , l . toXPath ( ) )
426+ } )
427+
428+ it ( 'withoutClass: excludes elements carrying the class' , ( ) => {
429+ const l = Locator . build ( 'a' ) . withClass ( 'ps-menu-button' ) . withoutClass ( 'active' )
430+ const nodes = xpath . select ( l . toXPath ( ) , doc )
431+ expect ( nodes ) . to . have . length ( 9 , l . toXPath ( ) )
432+ } )
433+
434+ it ( 'withoutText: excludes elements containing text' , ( ) => {
435+ const l = Locator . build ( 'span' ) . withoutText ( 'Hey' )
436+ const nodes = xpath . select ( l . toXPath ( ) , doc )
437+ const matchesHey = nodes . find ( n => n . firstChild && n . firstChild . data === 'Hey boy' )
438+ expect ( matchesHey ) . to . be . undefined
439+ } )
440+
441+ it ( 'withoutAttr: excludes matching attribute value' , ( ) => {
442+ const l = Locator . build ( 'input' ) . withoutAttr ( { type : 'hidden' } )
443+ const nodes = xpath . select ( l . toXPath ( ) , doc )
444+ nodes . forEach ( n => expect ( n . getAttribute ( 'type' ) ) . to . not . equal ( 'hidden' ) )
445+ } )
446+
447+ it ( 'withoutDescendant: excludes elements with a descendant match' , ( ) => {
448+ const l = Locator . build ( 'a' ) . withClass ( 'ps-menu-button' ) . withoutDescendant ( '.ps-submenu-expand-icon' )
449+ const nodes = xpath . select ( l . toXPath ( ) , doc )
450+ expect ( nodes ) . to . have . length ( 1 , l . toXPath ( ) )
451+ } )
452+
453+ it ( 'withoutChild: excludes elements with a direct child match' , ( ) => {
454+ const l = Locator . build ( 'p' ) . withoutChild ( 'span' )
455+ const nodes = xpath . select ( l . toXPath ( ) , doc )
456+ expect ( nodes ) . to . have . length ( 0 , l . toXPath ( ) )
457+ } )
458+
459+ it ( 'and: appends raw xpath predicate' , ( ) => {
460+ const l = Locator . build ( 'input' ) . and ( '@type="checkbox"' )
461+ const nodes = xpath . select ( l . toXPath ( ) , doc )
462+ expect ( nodes ) . to . have . length ( 1 , l . toXPath ( ) )
463+ } )
464+
465+ it ( 'andNot: wraps raw xpath predicate in not()' , ( ) => {
466+ const l = Locator . build ( 'a' ) . withClass ( 'ps-menu-button' ) . andNot ( './/span[contains(@class, "ps-submenu-expand-icon")]' )
467+ const nodes = xpath . select ( l . toXPath ( ) , doc )
468+ expect ( nodes ) . to . have . length ( 1 , l . toXPath ( ) )
469+ } )
470+
471+ describe ( 'combined filters' , ( ) => {
472+ it ( 'withClass + withoutClass: active vs inactive menu buttons' , ( ) => {
473+ const l = Locator . build ( 'a' ) . withClass ( 'ps-menu-button' ) . withoutClass ( 'active' )
474+ const nodes = xpath . select ( l . toXPath ( ) , doc )
475+ expect ( nodes ) . to . have . length ( 9 , l . toXPath ( ) )
476+ } )
477+
478+ it ( 'withClass + withAttr + withDescendant: dashboard menu with expand icon' , ( ) => {
479+ const l = Locator . build ( 'a' )
480+ . withClass ( 'ps-menu-button' )
481+ . withAttr ( { title : 'Dashboard' } )
482+ . withDescendant ( '.ps-submenu-expand-icon' )
483+ const nodes = xpath . select ( l . toXPath ( ) , doc )
484+ expect ( nodes ) . to . have . length ( 1 , l . toXPath ( ) )
485+ } )
486+
487+ it ( 'withClass + withoutDescendant: single active menu without expand icon (user red-btn pattern)' , ( ) => {
488+ const l = Locator . build ( 'a' ) . withClass ( 'ps-menu-button' , 'active' ) . withoutDescendant ( '.ps-submenu-expand-icon' )
489+ const nodes = xpath . select ( l . toXPath ( ) , doc )
490+ expect ( nodes ) . to . have . length ( 1 , l . toXPath ( ) )
491+ } )
492+
493+ it ( 'withText + withoutText: td with Edit but not Also Edit' , ( ) => {
494+ const l = Locator . build ( 'td' ) . withText ( 'Edit' ) . withoutText ( 'Also' )
495+ const nodes = xpath . select ( l . toXPath ( ) , doc )
496+ expect ( nodes ) . to . have . length ( 1 , l . toXPath ( ) )
497+ expect ( nodes [ 0 ] . firstChild . data ) . to . eql ( 'Edit' )
498+ } )
499+
500+ it ( 'withClass + withDescendant(locate(...).withTextEquals(...)): Authoring menu item' , ( ) => {
501+ const l = Locator . build ( 'a' )
502+ . withClass ( 'ps-menu-button' )
503+ . withDescendant ( Locator . build ( 'span' ) . withTextEquals ( 'Authoring' ) )
504+ const nodes = xpath . select ( l . toXPath ( ) , doc )
505+ expect ( nodes ) . to . have . length ( 1 , l . toXPath ( ) )
506+ } )
507+
508+ it ( 'withClass + withDescendant(nested withClass) + withoutDescendant' , ( ) => {
509+ // active home menu, reached via its icon
510+ const l = Locator . build ( 'a' )
511+ . withClass ( 'ps-menu-button' , 'active' )
512+ . withDescendant ( Locator . build ( 'i' ) . withClass ( 'icon' , 'home' ) )
513+ . withoutDescendant ( '.ps-submenu-expand-icon' )
514+ const nodes = xpath . select ( l . toXPath ( ) , doc )
515+ expect ( nodes ) . to . have . length ( 1 , l . toXPath ( ) )
516+ } )
517+
518+ it ( 'or: union of two distinct filtered locators' , ( ) => {
519+ const active = Locator . build ( 'a' ) . withClass ( 'ps-menu-button' , 'active' )
520+ const dashboard = Locator . build ( 'a' ) . withAttr ( { title : 'Dashboard' } )
521+ const l = active . or ( dashboard )
522+ const nodes = xpath . select ( l . toXPath ( ) , doc )
523+ expect ( nodes ) . to . have . length ( 2 , l . toXPath ( ) )
524+ } )
525+
526+ it ( 'and: raw predicate combined with DSL filters' , ( ) => {
527+ const l = Locator . build ( 'a' ) . withClass ( 'ps-menu-button' ) . and ( '@title="Dashboard"' )
528+ const nodes = xpath . select ( l . toXPath ( ) , doc )
529+ expect ( nodes ) . to . have . length ( 1 , l . toXPath ( ) )
530+ } )
531+
532+ it ( 'andNot + withClass: class present but no matching descendant' , ( ) => {
533+ const l = Locator . build ( 'li' ) . withClass ( 'ps-submenu-root' ) . andNot ( './/span[text()="Authoring"]' )
534+ const nodes = xpath . select ( l . toXPath ( ) , doc )
535+ // 9 submenu-root items total, 1 contains "Authoring" → 8 remain
536+ expect ( nodes ) . to . have . length ( 8 , l . toXPath ( ) )
537+ } )
538+
539+ it ( 'deep chain: find + withClass + first + find + withText' , ( ) => {
540+ const l = Locator . build ( '#fieldset-buttons' ) . find ( 'tr' ) . first ( ) . find ( 'td' ) . withText ( 'Edit' ) . withoutText ( 'Also' )
541+ const nodes = xpath . select ( l . toXPath ( ) , doc )
542+ expect ( nodes ) . to . have . length ( 1 , l . toXPath ( ) )
543+ expect ( nodes [ 0 ] . firstChild . data ) . to . eql ( 'Edit' )
544+ } )
545+
546+ it ( 'withClass + withoutChild: submenu-root li with no child named `i`' , ( ) => {
547+ const l = Locator . build ( 'li' ) . withClass ( 'ps-submenu-root' ) . withoutChild ( 'i' )
548+ const nodes = xpath . select ( l . toXPath ( ) , doc )
549+ // every submenu li has no direct `i` child (i is wrapped in a span) — all 9 match
550+ expect ( nodes ) . to . have . length ( 9 , l . toXPath ( ) )
551+ } )
552+
553+ it ( 'user button example: multi-class + text + not-descendant (applied to menu fixture)' , ( ) => {
554+ // mirrors:
555+ // locate('button').withClass('red-btn', 'btn-lg').withText('Save').withoutDescendant('svg')
556+ const l = Locator . build ( 'a' )
557+ . withClass ( 'ps-menu-button' , 'active' )
558+ . withText ( 'aaa' )
559+ . withoutDescendant ( '.ps-submenu-expand-icon' )
560+ const nodes = xpath . select ( l . toXPath ( ) , doc )
561+ expect ( nodes ) . to . have . length ( 1 , l . toXPath ( ) )
562+ } )
563+ } )
564+
410565 it ( 'should build locator to match element containing a text' , ( ) => {
411566 const l = Locator . build ( 'span' ) . withText ( 'Hey' )
412567 const nodes = xpath . select ( l . toXPath ( ) , doc )
0 commit comments