import { describe, it, expect, beforeEach } from 'vitest';
import { load, type CheerioAPI } from '../index.js';
import { Cheerio } from '../cheerio.js';
import { type AnyNode, type Element, type Text, isText } from 'domhandler';
import {
  cheerio,
  food,
  fruits,
  eleven,
  drinks,
  text,
  forms,
  mixedText,
  vegetables,
} from '../__fixtures__/fixtures.js';

function getText(el: Cheerio<Element>) {
  if (el.length === 0) return undefined;
  const [firstChild] = el[0].childNodes;
  return isText(firstChild) ? firstChild.data : undefined;
}

describe('$(...)', () => {
  let $: CheerioAPI;

  beforeEach(() => {
    $ = load(fruits);
  });

  describe('.load', () => {
    it('should throw a TypeError if given invalid input', () => {
      expect(() => {
        // @ts-expect-error Testing invalid input
        load();
      }).toThrow('cheerio.load() expects a string');
    });
  });

  describe('.find', () => {
    it('() : should find nothing', () => {
      expect($('ul').find()).toHaveLength(0);
    });

    it('(single) : should find one descendant', () => {
      expect($('#fruits').find('.apple')[0].attribs).toHaveProperty(
        'class',
        'apple',
      );
    });

    // #1679 - text tags not filtered
    it('(single) : should filter out text nodes', () => {
      const $root = $(`<html>\n${fruits.replace(/></g, '>\n<')}\n</html>`);
      expect($root.find('.apple')[0].attribs).toHaveProperty('class', 'apple');
    });

    it('(many) : should find all matching descendant', () => {
      expect($('#fruits').find('li')).toHaveLength(3);
    });

    it('(many) : should merge all selected elems with matching descendants', () => {
      expect($('#fruits, #food', food).find('.apple')).toHaveLength(1);
    });

    it('(invalid single) : should return empty if cant find', () => {
      expect($('ul').find('blah')).toHaveLength(0);
    });

    it('(invalid single) : should query descendants only', () => {
      expect($('#fruits').find('ul')).toHaveLength(0);
    });

    it('should return empty if search already empty result', () => {
      expect($('#not-fruits').find('li')).toHaveLength(0);
    });

    it('should lowercase selectors', () => {
      expect($('#fruits').find('LI')).toHaveLength(3);
    });

    it('should query immediate descendant only', () => {
      const q = load('<foo><bar><bar></bar><bar></bar></bar></foo>');
      expect(q('foo').find('> bar')).toHaveLength(1);
    });

    it('should find siblings', () => {
      const q = load('<p class=a><p class=b></p>');
      expect(q('.a').find('+.b')).toHaveLength(1);
      expect(q('.a').find('~.b')).toHaveLength(1);
      expect(q('.a').find('+.a')).toHaveLength(0);
      expect(q('.a').find('~.a')).toHaveLength(0);
    });

    it('should query case-sensitively when in xml mode', () => {
      const q = load('<caseSenSitive allTheWay>', { xml: true });
      expect(q('caseSenSitive')).toHaveLength(1);
      expect(q('[allTheWay]')).toHaveLength(1);
      expect(q('casesensitive')).toHaveLength(0);
      expect(q('[alltheway]')).toHaveLength(0);
    });

    it('should throw an Error if given an invalid selector', () => {
      expect(() => {
        $('#fruits').find(':bah');
      }).toThrow('Unknown pseudo-class :bah');
    });

    it('should respect the `lowerCaseTags` option (#3495)', () => {
      const q = load(
        `<parentTag class="myClass">
          <firstTag> <child> blah </child> </firstTag>
          <secondTag> <child> blah </child> </secondTag>
        </parentTag> `,
        {
          xml: {
            xmlMode: true,
            decodeEntities: false,
            lowerCaseTags: true,
            lowerCaseAttributeNames: false,
            recognizeSelfClosing: true,
          },
        },
      );
      expect(q('.myClass').find('firstTag > child')).toHaveLength(1);
    });

    describe('(cheerio object) :', () => {
      it('returns only those nodes contained within the current selection', () => {
        const q = load(food);
        const $selection = q('#fruits').find(q('li'));

        expect($selection).toHaveLength(3);
        expect($selection[0]).toBe(q('.apple')[0]);
        expect($selection[1]).toBe(q('.orange')[0]);
        expect($selection[2]).toBe(q('.pear')[0]);
      });
      it('returns only those nodes contained within any element in the current selection', () => {
        const q = load(food);
        const $selection = q('.apple, #vegetables').find(q('li'));

        expect($selection).toHaveLength(2);
        expect($selection[0]).toBe(q('.carrot')[0]);
        expect($selection[1]).toBe(q('.sweetcorn')[0]);
      });
    });

    describe('(node) :', () => {
      it('returns node when contained within the current selection', () => {
        const q = load(food);
        const $selection = q('#fruits').find(q('.apple')[0]);

        expect($selection).toHaveLength(1);
        expect($selection[0]).toBe(q('.apple')[0]);
      });
      it('returns node when contained within any element the current selection', () => {
        const q = load(food);
        const $selection = q('#fruits, #vegetables').find(q('.carrot')[0]);

        expect($selection).toHaveLength(1);
        expect($selection[0]).toBe(q('.carrot')[0]);
      });
      it('does not return node that is not contained within the current selection', () => {
        const q = load(food);
        const $selection = q('#fruits').find(q('.carrot')[0]);

        expect($selection).toHaveLength(0);
      });
    });
  });

  describe('.children', () => {
    it('() : should get all children', () => {
      expect($('ul').children()).toHaveLength(3);
    });

    it('() : should skip text nodes', () => {
      expect($(mixedText).children()).toHaveLength(0);
    });

    it('() : should return children of all matched elements', () => {
      expect($('ul ul', food).children()).toHaveLength(5);
    });

    it('(selector) : should return children matching selector', () => {
      const { attribs } = $('ul').children('.orange')[0];
      expect(attribs).toHaveProperty('class', 'orange');
    });

    it('(invalid selector) : should return empty', () => {
      expect($('ul').children('.lulz')).toHaveLength(0);
    });

    it('should only match immediate children, not ancestors', () => {
      expect($(food).children('li')).toHaveLength(0);
    });
  });

  describe('.contents', () => {
    beforeEach(() => {
      $ = load(text);
    });

    it('() : should get all contents', () => {
      expect($('p').contents()).toHaveLength(5);
    });

    it('() : should skip text nodes', () => {
      expect($(mixedText).contents()).toHaveLength(2);
    });

    it('() : should include text nodes', () => {
      expect($('p').contents().first()[0].type).toBe('text');
    });

    it('() : should include comment nodes', () => {
      expect($('p').contents().last()[0].type).toBe('comment');
    });
  });

  describe('.next', () => {
    it('() : should return next element', () => {
      const { attribs } = $('.orange').next()[0];
      expect(attribs).toHaveProperty('class', 'pear');
    });

    it('() : should skip text nodes', () => {
      expect($(mixedText).next()[0]).toHaveProperty('name', 'b');
    });

    it('(no next) : should return empty for last child', () => {
      expect($('.pear').next()).toHaveLength(0);
    });

    it('(next on empty object) : should return empty', () => {
      expect($('.banana').next()).toHaveLength(0);
    });

    it('() : should operate over all elements in the selection', () => {
      expect($('.apple, .orange', food).next()).toHaveLength(2);
    });

    it('() : should return elements in order', () => {
      const result = load(eleven)('.red').next();
      expect(result).toHaveLength(2);
      expect(result.eq(0).text()).toBe('Six');
      expect(result.eq(1).text()).toBe('Ten');
    });

    it('should reject elements that violate the filter', () => {
      expect($('.apple').next('.non-existent')).toHaveLength(0);
    });

    it('should accept elements that satisify the filter', () => {
      expect($('.apple').next('.orange')).toHaveLength(1);
    });

    describe('(selector) :', () => {
      it('should reject elements that violate the filter', () => {
        expect($('.apple').next('.non-existent')).toHaveLength(0);
      });

      it('should accept elements that satisify the filter', () => {
        expect($('.apple').next('.orange')).toHaveLength(1);
      });
    });
  });

  describe('.nextAll', () => {
    it('() : should return all following siblings', () => {
      const elems = $('.apple').nextAll();
      expect(elems).toHaveLength(2);
      expect(elems[0].attribs).toHaveProperty('class', 'orange');
      expect(elems[1].attribs).toHaveProperty('class', 'pear');
    });

    it('(no next) : should return empty for last child', () => {
      expect($('.pear').nextAll()).toHaveLength(0);
    });

    it('(nextAll on empty object) : should return empty', () => {
      expect($('.banana').nextAll()).toHaveLength(0);
    });

    it('() : should operate over all elements in the selection', () => {
      expect($('.apple, .carrot', food).nextAll()).toHaveLength(3);
    });

    it('() : should not contain duplicate elements', () => {
      const elems = $('.apple, .orange', food);
      expect(elems.nextAll()).toHaveLength(2);
    });

    it('() : should not contain text elements', () => {
      const elems = $('.apple', fruits.replace(/></g, '>\n<'));
      expect(elems.nextAll()).toHaveLength(2);
    });

    describe('(selector) :', () => {
      it('should filter according to the provided selector', () => {
        expect($('.apple').nextAll('.pear')).toHaveLength(1);
      });

      it("should not consider siblings' contents when filtering", () => {
        expect($('#fruits', food).nextAll('li')).toHaveLength(0);
      });
    });
  });

  describe('.nextUntil', () => {
    it('() : should return all following siblings if no selector specified', () => {
      const elems = $('.apple', food).nextUntil();
      expect(elems).toHaveLength(2);
      expect(elems[0].attribs).toHaveProperty('class', 'orange');
      expect(elems[1].attribs).toHaveProperty('class', 'pear');
    });

    it('() : should filter out non-element nodes', () => {
      const elems = $('<div><div></div><!-- comment -->text<div></div></div>');
      const div = elems.children().eq(0);
      expect(div.nextUntil()).toHaveLength(1);
    });

    it('() : should operate over all elements in the selection', () => {
      const elems = $('.apple, .carrot', food);
      expect(elems.nextUntil()).toHaveLength(3);
    });

    it('() : should not contain duplicate elements', () => {
      const elems = $('.apple, .orange', food);
      expect(elems.nextUntil()).toHaveLength(2);
    });

    it('(selector) : should return all following siblings until selector', () => {
      const elems = $('.apple', food).nextUntil('.pear');
      expect(elems).toHaveLength(1);
      expect(elems[0].attribs).toHaveProperty('class', 'orange');
    });

    it('(selector) : should support selector matching multiple elements', () => {
      const elems = $('#disabled', forms).nextUntil('option, #unnamed');
      expect(elems).toHaveLength(2);
      expect(elems[0].attribs).toHaveProperty('id', 'submit');
      expect(elems[1].attribs).toHaveProperty('id', 'select');
    });

    it('(selector not sibling) : should return all following siblings', () => {
      const elems = $('.apple').nextUntil('#vegetables');
      expect(elems).toHaveLength(2);
    });

    it('(selector, filterString) : should return all following siblings until selector, filtered by filter', () => {
      const elems = $('.beer', drinks).nextUntil('.water', '.milk');
      expect(elems).toHaveLength(1);
      expect(elems[0].attribs).toHaveProperty('class', 'milk');
    });

    it('(null, filterString) : should return all following siblings until selector, filtered by filter', () => {
      const elems = $('<ul><li></li><li><p></p></li></ul>');
      const empty = elems.find('li').eq(0).nextUntil(null, 'p');
      expect(empty).toHaveLength(0);
    });

    it('() : should return an empty object for last child', () => {
      expect($('.pear').nextUntil()).toHaveLength(0);
    });

    it('() : should return an empty object when called on an empty object', () => {
      expect($('.banana').nextUntil()).toHaveLength(0);
    });

    it('(node) : should return all following siblings until the node', () => {
      const $fruits = $('#fruits').children();
      const elems = $fruits.eq(0).nextUntil($fruits[2]);
      expect(elems).toHaveLength(1);
    });

    it('(cheerio object) : should return all following siblings until any member of the cheerio object', () => {
      const $drinks = $(drinks).children();
      const $until = $([$drinks[4], $drinks[3]]);
      const elems = $drinks.eq(0).nextUntil($until);
      expect(elems).toHaveLength(2);
    });
  });

  describe('.prev', () => {
    it('() : should return previous element', () => {
      const { attribs } = $('.orange').prev()[0];
      expect(attribs).toHaveProperty('class', 'apple');
    });

    it('() : should skip text nodes', () => {
      expect($($(mixedText)[2]).prev()[0]).toHaveProperty('name', 'a');
    });

    it('(no prev) : should return empty for first child', () => {
      expect($('.apple').prev()).toHaveLength(0);
    });

    it('(prev on empty object) : should return empty', () => {
      expect($('.banana').prev()).toHaveLength(0);
    });

    it('() : should operate over all elements in the selection', () => {
      expect($('.orange, .pear', food).prev()).toHaveLength(2);
    });

    it('() : should maintain elements order', () => {
      const sel = load(eleven)('.sel');
      expect(sel).toHaveLength(3);
      expect(sel.eq(0).text()).toBe('Three');
      expect(sel.eq(1).text()).toBe('Nine');
      expect(sel.eq(2).text()).toBe('Eleven');

      // Swap last elements
      const el = sel[2];
      sel[2] = sel[1];
      sel[1] = el;

      const result = sel.prev();
      expect(result).toHaveLength(3);
      expect(result.eq(0).text()).toBe('Two');
      expect(result.eq(1).text()).toBe('Ten');
      expect(result.eq(2).text()).toBe('Eight');
    });

    describe('(selector) :', () => {
      it('should reject elements that violate the filter', () => {
        expect($('.orange').prev('.non-existent')).toHaveLength(0);
      });

      it('should accept elements that satisify the filter', () => {
        expect($('.orange').prev('.apple')).toHaveLength(1);
      });

      it('(selector) : should reject elements that violate the filter', () => {
        expect($('.orange').prev('.non-existent')).toHaveLength(0);
      });

      it('(selector) : should accept elements that satisify the filter', () => {
        expect($('.orange').prev('.apple')).toHaveLength(1);
      });
    });
  });

  describe('.prevAll', () => {
    it('() : should return all preceding siblings', () => {
      const elems = $('.pear').prevAll();
      expect(elems).toHaveLength(2);
      expect(elems[0].attribs).toHaveProperty('class', 'orange');
      expect(elems[1].attribs).toHaveProperty('class', 'apple');
    });

    it('() : should not contain text elements', () => {
      const elems = $('.pear', fruits.replace(/></g, '>\n<'));
      expect(elems.prevAll()).toHaveLength(2);
    });

    it('(no prev) : should return empty for first child', () => {
      expect($('.apple').prevAll()).toHaveLength(0);
    });

    it('(prevAll on empty object) : should return empty', () => {
      expect($('.banana').prevAll()).toHaveLength(0);
    });

    it('() : should operate over all elements in the selection', () => {
      expect($('.orange, .sweetcorn', food).prevAll()).toHaveLength(2);
    });

    it('() : should not contain duplicate elements', () => {
      const elems = $('.orange, .pear', food);
      expect(elems.prevAll()).toHaveLength(2);
    });

    describe('(selector) :', () => {
      it('should filter returned elements', () => {
        const elems = $('.pear').prevAll('.apple');
        expect(elems).toHaveLength(1);
      });

      it("should not consider siblings's descendents", () => {
        const elems = $('#vegetables', food).prevAll('li');
        expect(elems).toHaveLength(0);
      });
    });
  });

  describe('.prevUntil', () => {
    it('() : should return all preceding siblings if no selector specified', () => {
      const elems = $('.pear').prevUntil();
      expect(elems).toHaveLength(2);
      expect(elems[0].attribs).toHaveProperty('class', 'orange');
      expect(elems[1].attribs).toHaveProperty('class', 'apple');
    });

    it('() : should filter out non-element nodes', () => {
      const elems = $(
        '<div class="1"><div class="2"></div><!-- comment -->text<div class="3"></div></div>',
      );
      const div = elems.children().last();
      expect(div.prevUntil()).toHaveLength(1);
    });

    it('() : should operate over all elements in the selection', () => {
      const elems = $('.pear, .sweetcorn', food);
      expect(elems.prevUntil()).toHaveLength(3);
    });

    it('() : should not contain duplicate elements', () => {
      const elems = $('.orange, .pear', food);
      expect(elems.prevUntil()).toHaveLength(2);
    });

    it('(selector) : should return all preceding siblings until selector', () => {
      const elems = $('.pear').prevUntil('.apple');
      expect(elems).toHaveLength(1);
      expect(elems[0].attribs).toHaveProperty('class', 'orange');
    });

    it('(selector) : should support selector matching multiple elements', () => {
      const elems = $('#unnamed', forms).prevUntil('option, #disabled');
      expect(elems).toHaveLength(2);
      expect(elems[0].attribs).toHaveProperty('id', 'select');
      expect(elems[1].attribs).toHaveProperty('id', 'submit');
    });

    it('(selector not sibling) : should return all preceding siblings', () => {
      const elems = $('.sweetcorn', food).prevUntil('#fruits');
      expect(elems).toHaveLength(1);
      expect(elems[0].attribs).toHaveProperty('class', 'carrot');
    });

    it('(selector, filterString) : should return all preceding siblings until selector, filtered by filter', () => {
      const elems = $('.cider', drinks).prevUntil('.juice', '.water');
      expect(elems).toHaveLength(1);
      expect(elems[0].attribs).toHaveProperty('class', 'water');
    });

    it('(selector, filterString) : should return all preceding siblings until selector', () => {
      const elems = $('<ul><li><p></p></li><li></li></ul>');
      const empty = elems.find('li').eq(1).prevUntil(null, 'p');
      expect(empty).toHaveLength(0);
    });

    it('() : should return an empty object for first child', () => {
      expect($('.apple').prevUntil()).toHaveLength(0);
    });

    it('() : should return an empty object when called on an empty object', () => {
      expect($('.banana').prevUntil()).toHaveLength(0);
    });

    it('(node) : should return all previous siblings until the node', () => {
      const $fruits = $('#fruits').children();
      const elems = $fruits.eq(2).prevUntil($fruits[0]);
      expect(elems).toHaveLength(1);
    });

    it('(cheerio object) : should return all previous siblings until any member of the cheerio object', () => {
      const $drinks = $(drinks).children();
      const $until = $([$drinks[0], $drinks[1]]);
      const elems = $drinks.eq(4).prevUntil($until);
      expect(elems).toHaveLength(2);
    });
  });

  describe('.siblings', () => {
    it('() : should get all the siblings', () => {
      expect($('.orange').siblings()).toHaveLength(2);
      expect($('#fruits').siblings()).toHaveLength(0);
      expect($('.apple, .carrot', food).siblings()).toHaveLength(3);
    });

    it('(selector) : should get all siblings that match the selector', () => {
      expect($('.orange').siblings('.apple')).toHaveLength(1);
      expect($('.orange').siblings('.peach')).toHaveLength(0);
    });

    it('(selector) : should throw an Error if given an invalid selector', () => {
      expect(() => {
        $('.orange').siblings(':bah');
      }).toThrow('Unknown pseudo-class :bah');
    });

    it('(selector) : does not consider the contents of siblings when filtering (GH-374)', () => {
      expect($('#fruits', food).siblings('li')).toHaveLength(0);
    });

    it('() : when two elements are siblings to each other they have to be included', () => {
      const result = load(eleven)('.sel').siblings();
      expect(result).toHaveLength(7);
      expect(result.eq(0).text()).toBe('One');
      expect(result.eq(1).text()).toBe('Two');
      expect(result.eq(2).text()).toBe('Four');
      expect(result.eq(3).text()).toBe('Eight');
      expect(result.eq(4).text()).toBe('Nine');
      expect(result.eq(5).text()).toBe('Ten');
      expect(result.eq(6).text()).toBe('Eleven');
    });

    it('(selector) : when two elements are siblings to each other they have to be included', () => {
      const result = load(eleven)('.sel').siblings('.red');
      expect(result).toHaveLength(2);
      expect(result.eq(0).text()).toBe('Four');
      expect(result.eq(1).text()).toBe('Nine');
    });

    it('(cheerio) : test filtering with cheerio object', () => {
      const doc = load(eleven);
      const result = doc('.sel').siblings(doc(':not([class])'));
      expect(result).toHaveLength(4);
      expect(result.eq(0).text()).toBe('One');
      expect(result.eq(1).text()).toBe('Two');
      expect(result.eq(2).text()).toBe('Eight');
      expect(result.eq(3).text()).toBe('Ten');
    });
  });

  describe('.parents', () => {
    beforeEach(() => {
      $ = load(food);
    });

    it('() : should get all of the parents in logical order', () => {
      const orange = $('.orange').parents();
      expect(orange).toHaveLength(4);
      expect(orange[0].attribs).toHaveProperty('id', 'fruits');
      expect(orange[1].attribs).toHaveProperty('id', 'food');
      expect(orange[2].tagName).toBe('body');
      expect(orange[3].tagName).toBe('html');
      const fruits = $('#fruits').parents();
      expect(fruits).toHaveLength(3);
      expect(fruits[0].attribs).toHaveProperty('id', 'food');
      expect(fruits[1].tagName).toBe('body');
      expect(fruits[2].tagName).toBe('html');
    });

    it('(selector) : should get all of the parents that match the selector in logical order', () => {
      const fruits = $('.orange').parents('#fruits');
      expect(fruits).toHaveLength(1);
      expect(fruits[0].attribs).toHaveProperty('id', 'fruits');
      const uls = $('.orange').parents('ul');
      expect(uls).toHaveLength(2);
      expect(uls[0].attribs).toHaveProperty('id', 'fruits');
      expect(uls[1].attribs).toHaveProperty('id', 'food');
    });

    it('() : should not break if the selector does not have any results', () => {
      const result = $('.saladbar').parents();
      expect(result).toHaveLength(0);
    });

    it('() : should return an empty set for top-level elements', () => {
      const result = $('html').parents();
      expect(result).toHaveLength(0);
    });

    it('() : should return the parents of every element in the *reveresed* collection, omitting duplicates', () => {
      const $parents = $('li').parents();

      expect($parents).toHaveLength(5);
      expect($parents[0]).toBe($('#vegetables')[0]);
      expect($parents[1]).toBe($('#fruits')[0]);
      expect($parents[2]).toBe($('#food')[0]);
      expect($parents[3]).toBe($('body')[0]);
      expect($parents[4]).toBe($('html')[0]);
    });
  });

  describe('.parentsUntil', () => {
    beforeEach(() => {
      $ = load(food);
    });

    it('() : should get all of the parents in logical order', () => {
      const result = $('.orange').parentsUntil();
      expect(result).toHaveLength(4);
      expect(result[0].attribs).toHaveProperty('id', 'fruits');
      expect(result[1].attribs).toHaveProperty('id', 'food');
      expect(result[2].tagName).toBe('body');
      expect(result[3].tagName).toBe('html');
    });

    it('() : should get all of the parents in reversed order, omitting duplicates', () => {
      const result = $('.apple, .sweetcorn').parentsUntil();
      expect(result).toHaveLength(5);
      expect(result[0]).toBe($('#vegetables')[0]);
      expect(result[1]).toBe($('#fruits')[0]);
      expect(result[2]).toBe($('#food')[0]);
      expect(result[3]).toBe($('body')[0]);
      expect(result[4]).toBe($('html')[0]);
    });

    it('(selector) : should get all of the parents until selector', () => {
      const food = $('.orange').parentsUntil('#food');
      expect(food).toHaveLength(1);
      expect(food[0].attribs).toHaveProperty('id', 'fruits');
      const fruits = $('.orange').parentsUntil('#fruits');
      expect(fruits).toHaveLength(0);
    });

    it('(selector) : Less simple parentsUntil check with selector', () => {
      const result = $('#fruits').parentsUntil('html, body');
      expect(result.eq(0).attr('id')).toBe('food');
    });

    it('(selector not parent) : should return all parents', () => {
      const result = $('.orange').parentsUntil('.apple');
      expect(result).toHaveLength(4);
      expect(result[0].attribs).toHaveProperty('id', 'fruits');
      expect(result[1].attribs).toHaveProperty('id', 'food');
      expect(result[2].tagName).toBe('body');
      expect(result[3].tagName).toBe('html');
    });

    it('(selector, filter) : should get all of the parents that match the filter', () => {
      const result = $('.apple, .sweetcorn').parentsUntil(
        '.saladbar',
        '#vegetables',
      );
      expect(result).toHaveLength(1);
      expect(result[0].attribs).toHaveProperty('id', 'vegetables');
    });

    it('(selector, filter) : Multiple-filtered parentsUntil check', () => {
      const result = $('.orange').parentsUntil('html', 'ul,body');
      expect(result).toHaveLength(3);
      expect(result.eq(0).attr('id')).toBe('fruits');
      expect(result.eq(1).attr('id')).toBe('food');
      expect(result.eq(2).prop('tagName')).toBe('BODY');
    });

    it('() : should return empty object when called on an empty object', () => {
      const result = $('.saladbar').parentsUntil();
      expect(result).toHaveLength(0);
    });

    it('() : should return an empty set for top-level elements', () => {
      const result = $('html').parentsUntil();
      expect(result).toHaveLength(0);
    });

    it('(cheerio object) : should return all parents until any member of the cheerio object', () => {
      const $fruits = $('#fruits');
      const $until = $('#food');
      const result = $fruits.children().eq(1).parentsUntil($until);
      expect(result).toHaveLength(1);
      expect(result[0].attribs).toHaveProperty('id', 'fruits');
    });

    it('(cheerio object) : should return all parents until body element', () => {
      const body = $('body')[0];
      const result = $('.carrot').parentsUntil(body);
      expect(result).toHaveLength(2);
      expect(result.eq(0).is('ul#vegetables')).toBe(true);
    });
  });

  describe('.parent', () => {
    it('() : should return the parent of each matched element', () => {
      let result = $('.orange').parent();
      expect(result).toHaveLength(1);
      expect(result[0].attribs).toHaveProperty('id', 'fruits');
      result = $('li', food).parent();
      expect(result).toHaveLength(2);
      expect(result[0].attribs).toHaveProperty('id', 'fruits');
      expect(result[1].attribs).toHaveProperty('id', 'vegetables');
    });

    it('(undefined) : should not throw an exception', () => {
      expect(() => {
        $('li').parent(undefined);
      }).not.toThrow();
    });

    it('() : should return an empty object for top-level elements', () => {
      const result = $('html').parent();
      expect(result).toHaveLength(0);
    });

    it('() : should not contain duplicate elements', () => {
      const result = $('li').parent();
      expect(result).toHaveLength(1);
    });

    it('(selector) : should filter the matched parent elements by the selector', () => {
      const parents = $('.orange').parent();
      expect(parents).toHaveLength(1);
      expect(parents[0].attribs).toHaveProperty('id', 'fruits');
      const fruits = $('li', food).parent('#fruits');
      expect(fruits).toHaveLength(1);
      expect(fruits[0].attribs).toHaveProperty('id', 'fruits');
    });
  });

  describe('.closest', () => {
    it('() : should return an empty array', () => {
      const result = $('.orange').closest();
      expect(result).toHaveLength(0);
      expect(result).toBeInstanceOf(Cheerio);
    });

    it('(selector) : should find the closest element that matches the selector, searching through its ancestors and itself', () => {
      expect($('.orange').closest('.apple')).toHaveLength(0);
      expect(
        ($('.orange', food).closest('#food')[0] as Element).attribs,
      ).toHaveProperty('id', 'food');
      expect(
        ($('.orange', food).closest('ul')[0] as Element).attribs,
      ).toHaveProperty('id', 'fruits');
      expect(
        ($('.orange', food).closest('li')[0] as Element).attribs,
      ).toHaveProperty('class', 'orange');
    });

    it('(selector) : should find the closest element of each item, removing duplicates', () => {
      const result = $('li', food).closest('ul');
      expect(result).toHaveLength(2);
    });

    it('() : should not break if the selector does not have any results', () => {
      const result = $('.saladbar', food).closest('ul');
      expect(result).toHaveLength(0);
    });

    it('(selector) : should find closest element for text nodes', () => {
      const textNode = $('.apple', food).contents().first();
      const result = textNode.closest('#food') as Cheerio<Element>;
      expect(result[0].attribs).toHaveProperty('id', 'food');
    });
  });

  describe('.each', () => {
    it('( (i, elem) -> ) : should loop selected returning fn with (i, elem)', () => {
      const items: Element[] = [];
      const classes = ['apple', 'orange', 'pear'];
      $('li').each(function (idx, elem) {
        items[idx] = elem;
        expect(this.attribs).toHaveProperty('class', classes[idx]);
      });
      expect(items[0].attribs).toHaveProperty('class', 'apple');
      expect(items[1].attribs).toHaveProperty('class', 'orange');
      expect(items[2].attribs).toHaveProperty('class', 'pear');
    });

    it('( (i, elem) -> ) : should break iteration when the iterator function returns false', () => {
      let iterationCount = 0;
      $('li').each((idx) => {
        iterationCount++;
        return idx < 1;
      });

      expect(iterationCount).toBe(2);
    });
  });

  if (typeof Symbol !== 'undefined') {
    describe('[Symbol.iterator]', () => {
      it('should yield each element', () => {
        // The equivalent of: for (const element of $('li')) ...
        const $li = $('li');
        const iterator = $li[Symbol.iterator]() as Iterator<Element, Element>;
        expect(iterator.next().value.attribs).toHaveProperty('class', 'apple');
        expect(iterator.next().value.attribs).toHaveProperty('class', 'orange');
        expect(iterator.next().value.attribs).toHaveProperty('class', 'pear');
        expect(iterator.next().done).toBe(true);
      });
    });
  }

  describe('.map', () => {
    it('(fn) : should be invoked with the correct arguments and context', () => {
      const $fruits = $('li');
      const args: [number, AnyNode][] = [];
      const thisVals: AnyNode[] = [];

      $fruits.map(function (...myArgs) {
        args.push(myArgs);
        thisVals.push(this);
        return undefined;
      });

      expect(args).toStrictEqual([
        [0, $fruits[0]],
        [1, $fruits[1]],
        [2, $fruits[2]],
      ]);
      expect(thisVals).toStrictEqual([$fruits[0], $fruits[1], $fruits[2]]);
    });

    it('(fn) : should return an Cheerio object wrapping the returned items', () => {
      const $fruits = $('li');
      const $mapped = $fruits.map((i) => $fruits[2 - i]);

      expect($mapped).toHaveLength(3);
      expect($mapped[0]).toBe($fruits[2]);
      expect($mapped[1]).toBe($fruits[1]);
      expect($mapped[2]).toBe($fruits[0]);
    });

    it('(fn) : should ignore `null` and `undefined` returned by iterator', () => {
      const $fruits = $('li');
      const retVals = [null, undefined, $fruits[1]];

      const $mapped = $fruits.map((i) => retVals[i]);

      expect($mapped).toHaveLength(1);
      expect($mapped[0]).toBe($fruits[1]);
    });

    it('(fn) : should preform a shallow merge on arrays returned by iterator', () => {
      const $fruits = $('li');

      const $mapped = $fruits.map(() => [1, [3, 4]]);

      expect($mapped.get()).toStrictEqual([1, [3, 4], 1, [3, 4], 1, [3, 4]]);
    });

    it('(fn) : should tolerate `null` and `undefined` when flattening arrays returned by iterator', () => {
      const $fruits = $('li');

      const $mapped = $fruits.map(() => [null, undefined]);

      expect($mapped.get()).toStrictEqual([
        null,
        undefined,
        null,
        undefined,
        null,
        undefined,
      ]);
    });
  });

  describe('.filter', () => {
    it('(selector) : should reduce the set of matched elements to those that match the selector', () => {
      const pear = $('li').filter('.pear').text();
      expect(pear).toBe('Pear');
    });

    it('(selector) : should not consider nested elements', () => {
      const lis = $('#fruits').filter('li');
      expect(lis).toHaveLength(0);
    });

    it('(selection) : should reduce the set of matched elements to those that are contained in the provided selection', () => {
      const $fruits = $('li');
      const $pear = $fruits.filter('.pear, .apple');
      expect($fruits.filter($pear)).toHaveLength(2);
    });

    it('(element) : should reduce the set of matched elements to those that specified directly', () => {
      const $fruits = $('li');
      const pear = $fruits.filter('.pear')[0];
      expect($fruits.filter(pear)).toHaveLength(1);
    });

    it("(fn) : should reduce the set of matched elements to those that pass the function's test", () => {
      const orange = $('li')
        .filter(function (i, el) {
          expect(this).toBe(el);
          expect(el.tagName).toBe('li');
          expect(typeof i).toBe('number');
          return $(this).attr('class') === 'orange';
        })
        .text();

      expect(orange).toBe('Orange');
    });

    it('should also iterate over text nodes (#1867)', () => {
      const text = $('<a>a</a>b<c></c>').filter((_, el): el is Text =>
        isText(el),
      );

      expect(text[0].data).toBe('b');
    });
  });

  describe('.not', () => {
    it('(selector) : should reduce the set of matched elements to those that do not match the selector', () => {
      const $fruits = $('li');

      const $notPear = $fruits.not('.pear');

      expect($notPear).toHaveLength(2);
      expect($notPear[0]).toBe($fruits[0]);
      expect($notPear[1]).toBe($fruits[1]);
    });

    it('(selector) : should not consider nested elements', () => {
      const lis = $('#fruits').not('li');
      expect(lis).toHaveLength(1);
    });

    it('(selection) : should reduce the set of matched elements to those that are mot contained in the provided selection', () => {
      const $fruits = $('li');
      const $orange = $('.orange');

      const $notOrange = $fruits.not($orange);

      expect($notOrange).toHaveLength(2);
      expect($notOrange[0]).toBe($fruits[0]);
      expect($notOrange[1]).toBe($fruits[2]);
    });

    it('(element) : should reduce the set of matched elements to those that specified directly', () => {
      const $fruits = $('li');
      const apple = $('.apple')[0];

      const $notApple = $fruits.not(apple);

      expect($notApple).toHaveLength(2);
      expect($notApple[0]).toBe($fruits[1]);
      expect($notApple[1]).toBe($fruits[2]);
    });

    it("(fn) : should reduce the set of matched elements to those that do not pass the function's test", () => {
      const $fruits = $('li');

      const $notOrange = $fruits.not(function (i, el) {
        expect(this).toBe(el);
        expect(el).toHaveProperty('name', 'li');
        expect(typeof i).toBe('number');
        return $(this).attr('class') === 'orange';
      });

      expect($notOrange).toHaveLength(2);
      expect($notOrange[0]).toBe($fruits[0]);
      expect($notOrange[1]).toBe($fruits[2]);
    });
  });

  describe('.has', () => {
    beforeEach(() => {
      $ = load(food);
    });

    it('(selector) : should reduce the set of matched elements to those with descendants that match the selector', () => {
      const $fruits = $('#fruits,#vegetables').has('.pear');
      expect($fruits).toHaveLength(1);
      expect($fruits[0]).toBe($('#fruits')[0]);
    });

    it('(selector) : should only consider nested elements', () => {
      const $empty = $('#fruits').has('#fruits');
      expect($empty).toHaveLength(0);
    });

    it('(element) : should reduce the set of matched elements to those that are ancestors of the provided element', () => {
      const $fruits = $('#fruits,#vegetables').has($('.pear')[0]);
      expect($fruits).toHaveLength(1);
      expect($fruits[0]).toBe($('#fruits')[0]);
    });

    it('(element) : should only consider nested elements', () => {
      const $fruits = $('#fruits');
      const fruitsEl = $fruits[0];
      const $empty = $fruits.has(fruitsEl);

      expect($empty).toHaveLength(0);
    });
  });

  describe('.first', () => {
    it('() : should return the first item', () => {
      const $src = $(
        '<span>foo</span><span>bar</span><span>baz</span>',
      ) as Cheerio<Element>;
      const $elem = $src.first();
      expect($elem.length).toBe(1);
      expect($elem[0].childNodes[0]).toHaveProperty('data', 'foo');
    });

    it('() : should return an empty object for an empty object', () => {
      const $src = $();
      const $first = $src.first();
      expect($first.length).toBe(0);
      expect($first[0]).toBeUndefined();
    });
  });

  describe('.last', () => {
    it('() : should return the last element', () => {
      const $src = $(
        '<span>foo</span><span>bar</span><span>baz</span>',
      ) as Cheerio<Element>;
      const $elem = $src.last();
      expect($elem.length).toBe(1);
      expect($elem[0].childNodes[0]).toHaveProperty('data', 'baz');
    });

    it('() : should return an empty object for an empty object', () => {
      const $src = $();
      const $last = $src.last();
      expect($last.length).toBe(0);
      expect($last[0]).toBeUndefined();
    });
  });

  describe('.first & .last', () => {
    it('() : should return equivalent collections if only one element', () => {
      const $src = $('<span>bar</span>') as Cheerio<Element>;
      const $first = $src.first();
      const $last = $src.last();
      expect($first.length).toBe(1);
      expect($first[0].childNodes[0]).toHaveProperty('data', 'bar');
      expect($last.length).toBe(1);
      expect($last[0].childNodes[0]).toHaveProperty('data', 'bar');
      expect($first[0]).toBe($last[0]);
    });
  });

  describe('.eq', () => {
    it('(i) : should return the element at the specified index', () => {
      expect(getText($('li').eq(0))).toBe('Apple');
      expect(getText($('li').eq(1))).toBe('Orange');
      expect(getText($('li').eq(2))).toBe('Pear');
      expect(getText($('li').eq(3))).toBeUndefined();
      expect(getText($('li').eq(-1))).toBe('Pear');
    });
  });

  describe('.get', () => {
    it('(i) : should return the element at the specified index', () => {
      const children = $('#fruits').children();
      expect(children.get(0)).toBe(children[0]);
      expect(children.get(1)).toBe(children[1]);
      expect(children.get(2)).toBe(children[2]);
    });

    it('(-1) : should return the element indexed from the end of the collection', () => {
      const children = $('#fruits').children();
      expect(children.get(-1)).toBe(children[2]);
      expect(children.get(-2)).toBe(children[1]);
      expect(children.get(-3)).toBe(children[0]);
    });

    it('() : should return an array containing all of the collection', () => {
      const children = $('#fruits').children();
      const all = children.get();
      expect(Array.isArray(all)).toBe(true);
      expect(all).toStrictEqual([children[0], children[1], children[2]]);
    });
  });

  describe('.index', () => {
    describe('() :', () => {
      it('returns the index of a child amongst its siblings', () => {
        expect($('.orange').index()).toBe(1);
      });
      it('returns -1 when the selection has no parent', () => {
        expect($('<div/>').index()).toBe(-1);
      });
    });

    describe('(selector) :', () => {
      it('returns the index of the first element in the set matched by `selector`', () => {
        expect($('.apple').index('#fruits, li')).toBe(1);
      });
      it('returns -1 when the item is not present in the set matched by `selector`', () => {
        expect($('.apple').index('#fuits')).toBe(-1);
      });
      it('returns -1 when the first element in the set has no parent', () => {
        expect($('<div/>').index('*')).toBe(-1);
      });
    });

    describe('(node) :', () => {
      it('returns the index of the given node within the current selection', () => {
        const $lis = $('li');
        expect($lis.index($lis.get(1))).toBe(1);
      });
      it('returns the index of the given node within the current selection when the current selection has no parent', () => {
        const $apple = $('.apple').remove();

        expect($apple.index($apple.get(0))).toBe(0);
      });
      it('returns -1 when the given node is not present in the current selection', () => {
        expect($('li').index($('#fruits').get(0))).toBe(-1);
      });
      it('returns -1 when the current selection is empty', () => {
        expect($('.not-fruit').index($('#fruits').get(0))).toBe(-1);
      });
    });

    describe('(selection) :', () => {
      it('returns the index of the first node in the provided selection within the current selection', () => {
        const $lis = $('li');
        expect($lis.index($('.orange, .pear'))).toBe(1);
      });
      it('returns -1 when the given node is not present in the current selection', () => {
        expect($('li').index($('#fruits'))).toBe(-1);
      });
      it('returns -1 when the current selection is empty', () => {
        expect($('.not-fruit').index($('#fruits'))).toBe(-1);
      });
    });
  });

  describe('.slice', () => {
    it('(start) : should return all elements after the given index', () => {
      const sliced = $('li').slice(1);
      expect(sliced).toHaveLength(2);
      expect(getText(sliced.eq(0))).toBe('Orange');
      expect(getText(sliced.eq(1))).toBe('Pear');
    });

    it('(start, end) : should return all elements matching the given range', () => {
      const sliced = $('li').slice(1, 2);
      expect(sliced).toHaveLength(1);
      expect(getText(sliced.eq(0))).toBe('Orange');
    });

    it('(-start) : should return element matching the offset from the end', () => {
      const sliced = $('li').slice(-1);
      expect(sliced).toHaveLength(1);
      expect(getText(sliced.eq(0))).toBe('Pear');
    });
  });

  describe('.end() :', () => {
    let $fruits: Cheerio<Element>;

    beforeEach(() => {
      $fruits = $('#fruits').children();
    });

    it('returns an empty object at the end of the chain', () => {
      expect($fruits.end().end().end()).toBeTruthy();
      expect($fruits.end().end().end()).toHaveLength(0);
    });
    it('find', () => {
      expect($fruits.find('.apple').end()).toBe($fruits);
    });
    it('filter', () => {
      expect($fruits.filter('.apple').end()).toBe($fruits);
    });
    it('map', () => {
      expect(
        $fruits
          .map(function () {
            return this;
          })
          .end(),
      ).toBe($fruits);
    });
    it('contents', () => {
      expect($fruits.contents().end()).toBe($fruits);
    });
    it('eq', () => {
      expect($fruits.eq(1).end()).toBe($fruits);
    });
    it('first', () => {
      expect($fruits.first().end()).toBe($fruits);
    });
    it('last', () => {
      expect($fruits.last().end()).toBe($fruits);
    });
    it('slice', () => {
      expect($fruits.slice(1).end()).toBe($fruits);
    });
    it('children', () => {
      expect($fruits.children().end()).toBe($fruits);
    });
    it('parent', () => {
      expect($fruits.parent().end()).toBe($fruits);
    });
    it('parents', () => {
      expect($fruits.parents().end()).toBe($fruits);
    });
    it('closest', () => {
      expect($fruits.closest('ul').end()).toBe($fruits);
    });
    it('siblings', () => {
      expect($fruits.siblings().end()).toBe($fruits);
    });
    it('next', () => {
      expect($fruits.next().end()).toBe($fruits);
    });
    it('nextAll', () => {
      expect($fruits.nextAll().end()).toBe($fruits);
    });
    it('prev', () => {
      expect($fruits.prev().end()).toBe($fruits);
    });
    it('prevAll', () => {
      expect($fruits.prevAll().end()).toBe($fruits);
    });
    it('clone', () => {
      expect($fruits.clone().end()).toBe($fruits);
    });
  });

  describe('.add()', () => {
    let $fruits: Cheerio<AnyNode>;
    let $apple: Cheerio<Element>;
    let $orange: Cheerio<Element>;
    let $pear: Cheerio<Element>;

    beforeEach(() => {
      $ = load(food);
      $fruits = $('#fruits');
      $apple = $('.apple');
      $orange = $('.orange');
      $pear = $('.pear');
    });

    describe('(selector) matched element :', () => {
      it('occurs before current selection', () => {
        const $selection = $orange.add('.apple');

        expect($selection).toHaveLength(2);
        expect($selection[0]).toBe($apple[0]);
        expect($selection[1]).toBe($orange[0]);
      });
      it('is identical to the current selection', () => {
        const $selection = $orange.add('.orange');

        expect($selection).toHaveLength(1);
        expect($selection[0]).toBe($orange[0]);
      });
      it('occurs after current selection', () => {
        const $selection = $orange.add('.pear');

        expect($selection).toHaveLength(2);
        expect($selection[0]).toBe($orange[0]);
        expect($selection[1]).toBe($pear[0]);
      });
      it('contains the current selection', () => {
        const $selection = $orange.add('#fruits');

        expect($selection).toHaveLength(2);
        expect($selection[0]).toBe($fruits[0]);
        expect($selection[1]).toBe($orange[0]);
      });
      it('is a child of the current selection', () => {
        const $selection = $fruits.add('.orange');

        expect($selection).toHaveLength(2);
        expect($selection[0]).toBe($fruits[0]);
        expect($selection[1]).toBe($orange[0]);
      });
      it('is root object preserved', () => {
        const $selection = $('<div></div>').add('#fruits');

        expect($selection).toHaveLength(2);
        expect($selection.eq(0).is('div')).toBe(true);
        expect($selection.eq(1).is($fruits.eq(0))).toBe(true);
      });
    });
    describe('(selector) matched elements :', () => {
      it('occur before the current selection', () => {
        const $selection = $pear.add('.apple, .orange');

        expect($selection).toHaveLength(3);
        expect($selection[0]).toBe($apple[0]);
        expect($selection[1]).toBe($orange[0]);
        expect($selection[2]).toBe($pear[0]);
      });
      it('include the current selection', () => {
        const $selection = $pear.add('#fruits li');

        expect($selection).toHaveLength(3);
        expect($selection[0]).toBe($apple[0]);
        expect($selection[1]).toBe($orange[0]);
        expect($selection[2]).toBe($pear[0]);
      });
      it('occur after the current selection', () => {
        const $selection = $apple.add('.orange, .pear');

        expect($selection).toHaveLength(3);
        expect($selection[0]).toBe($apple[0]);
        expect($selection[1]).toBe($orange[0]);
        expect($selection[2]).toBe($pear[0]);
      });
      it('occur within the current selection', () => {
        const $selection = $fruits.add('#fruits li');

        expect($selection).toHaveLength(4);
        expect($selection[0]).toBe($fruits[0]);
        expect($selection[1]).toBe($apple[0]);
        expect($selection[2]).toBe($orange[0]);
        expect($selection[3]).toBe($pear[0]);
      });
    });
    describe('(selector, context) :', () => {
      it(', context)', () => {
        const $selection = $fruits.add('li', '#vegetables');
        expect($selection).toHaveLength(3);
        expect($selection[0]).toBe($fruits[0]);
        expect($selection[1]).toBe($('.carrot')[0]);
        expect($selection[2]).toBe($('.sweetcorn')[0]);
      });
    });

    describe('(element) honors document order when element occurs :', () => {
      it('before the current selection', () => {
        const $selection = $orange.add($apple[0]);

        expect($selection).toHaveLength(2);
        expect($selection[0]).toBe($apple[0]);
        expect($selection[1]).toBe($orange[0]);
      });
      it('after the current selection', () => {
        const $selection = $orange.add($pear[0]);

        expect($selection).toHaveLength(2);
        expect($selection[0]).toBe($orange[0]);
        expect($selection[1]).toBe($pear[0]);
      });
      it('within the current selection', () => {
        const $selection = $fruits.add($orange[0]);

        expect($selection).toHaveLength(2);
        expect($selection[0]).toBe($fruits[0]);
        expect($selection[1]).toBe($orange[0]);
      });
      it('as an ancestor of the current selection', () => {
        const $selection = $orange.add($fruits[0]);

        expect($selection).toHaveLength(2);
        expect($selection[0]).toBe($fruits[0]);
        expect($selection[1]).toBe($orange[0]);
      });
      it('does not insert an element already contained within the current selection', () => {
        const $selection = $apple.add($apple[0]);

        expect($selection).toHaveLength(1);
        expect($selection[0]).toBe($apple[0]);
      });
    });
    describe('([elements]) : elements', () => {
      it('occur before the current selection', () => {
        const $selection = $pear.add($('.apple, .orange').get());

        expect($selection).toHaveLength(3);
        expect($selection[0]).toBe($apple[0]);
        expect($selection[1]).toBe($orange[0]);
        expect($selection[2]).toBe($pear[0]);
      });
      it('include the current selection', () => {
        const $selection = $pear.add($('#fruits li').get());

        expect($selection).toHaveLength(3);
        expect($selection[0]).toBe($apple[0]);
        expect($selection[1]).toBe($orange[0]);
        expect($selection[2]).toBe($pear[0]);
      });
      it('occur after the current selection', () => {
        const $selection = $apple.add($('.orange, .pear').get());

        expect($selection).toHaveLength(3);
        expect($selection[0]).toBe($apple[0]);
        expect($selection[1]).toBe($orange[0]);
        expect($selection[2]).toBe($pear[0]);
      });
      it('occur within the current selection', () => {
        const $selection = $fruits.add($('#fruits li').get());

        expect($selection).toHaveLength(4);
        expect($selection[0]).toBe($fruits[0]);
        expect($selection[1]).toBe($apple[0]);
        expect($selection[2]).toBe($orange[0]);
        expect($selection[3]).toBe($pear[0]);
      });
    });

    /**
     * Element order is undefined in this case, so it should not be asserted
     * here.
     *
     * If the collection consists of elements from different documents or ones
     * not in any document, the sort order is undefined.
     *
     * @see {@link https://api.jquery.com/add/}
     */
    it('(html) : correctly parses and adds the new elements', () => {
      const $selection = $apple.add('<li class="banana">banana</li>');

      expect($selection).toHaveLength(2);
      expect($selection.is('.apple')).toBe(true);
      expect($selection.is('.banana')).toBe(true);
    });

    describe('(selection) element in selection :', () => {
      it('occurs before current selection', () => {
        const $selection = $orange.add($('.apple'));

        expect($selection).toHaveLength(2);
        expect($selection[0]).toBe($apple[0]);
        expect($selection[1]).toBe($orange[0]);
      });
      it('is identical to the current selection', () => {
        const $selection = $orange.add($('.orange'));

        expect($selection).toHaveLength(1);
        expect($selection[0]).toBe($orange[0]);
      });
      it('occurs after current selection', () => {
        const $selection = $orange.add($('.pear'));

        expect($selection).toHaveLength(2);
        expect($selection[0]).toBe($orange[0]);
        expect($selection[1]).toBe($pear[0]);
      });
      it('contains the current selection', () => {
        const $selection = $orange.add($('#fruits'));

        expect($selection).toHaveLength(2);
        expect($selection[0]).toBe($fruits[0]);
        expect($selection[1]).toBe($orange[0]);
      });
      it('is a child of the current selection', () => {
        const $selection = $fruits.add($('.orange'));

        expect($selection).toHaveLength(2);
        expect($selection[0]).toBe($fruits[0]);
        expect($selection[1]).toBe($orange[0]);
      });
    });
    describe('(selection) elements in the selection :', () => {
      it('occur before the current selection', () => {
        const $selection = $pear.add($('.apple, .orange'));

        expect($selection).toHaveLength(3);
        expect($selection[0]).toBe($apple[0]);
        expect($selection[1]).toBe($orange[0]);
        expect($selection[2]).toBe($pear[0]);
      });
      it('include the current selection', () => {
        const $selection = $pear.add($('#fruits li'));

        expect($selection).toHaveLength(3);
        expect($selection[0]).toBe($apple[0]);
        expect($selection[1]).toBe($orange[0]);
        expect($selection[2]).toBe($pear[0]);
      });
      it('occur after the current selection', () => {
        const $selection = $apple.add($('.orange, .pear'));

        expect($selection).toHaveLength(3);
        expect($selection[0]).toBe($apple[0]);
        expect($selection[1]).toBe($orange[0]);
        expect($selection[2]).toBe($pear[0]);
      });
      it('occur within the current selection', () => {
        const $selection = $fruits.add($('#fruits li'));

        expect($selection).toHaveLength(4);
        expect($selection[0]).toBe($fruits[0]);
        expect($selection[1]).toBe($apple[0]);
        expect($selection[2]).toBe($orange[0]);
        expect($selection[3]).toBe($pear[0]);
      });
    });

    describe('(selection) :', () => {
      it('modifying nested selections should not impact the parent [#834]', () => {
        const apple_pear = $apple.add($pear);

        // Applies red to apple and pear
        apple_pear.addClass('red');

        expect($apple.hasClass('red')).toBe(true); // This is true
        expect($pear.hasClass('red')).toBe(true); // This is true

        // Applies green to pear... AND should not affect apple
        $pear.addClass('green');
        expect($pear.hasClass('green')).toBe(true); // Currently this is true
        expect($apple.hasClass('green')).toBe(false); // And this should be false!
      });
    });
  });

  describe('.addBack', () => {
    describe('() :', () => {
      it('includes siblings and self', () => {
        const $selection = $('.orange').siblings().addBack();

        expect($selection).toHaveLength(3);
        expect($selection[0]).toBe($('.apple')[0]);
        expect($selection[1]).toBe($('.orange')[0]);
        expect($selection[2]).toBe($('.pear')[0]);
      });
      it('includes children and self', () => {
        const $selection = $('#fruits').children().addBack();

        expect($selection).toHaveLength(4);
        expect($selection[0]).toBe($('#fruits')[0]);
        expect($selection[1]).toBe($('.apple')[0]);
        expect($selection[2]).toBe($('.orange')[0]);
        expect($selection[3]).toBe($('.pear')[0]);
      });
      it('includes parent and self', () => {
        const $selection = $('.apple').parent().addBack();

        expect($selection).toHaveLength(2);
        expect($selection[0]).toBe($('#fruits')[0]);
        expect($selection[1]).toBe($('.apple')[0]);
      });
      it('includes parents and self', () => {
        const q = load(food);
        const $selection = q('.apple').parents().addBack();

        expect($selection).toHaveLength(5);
        expect($selection[0]).toBe(q('html')[0]);
        expect($selection[1]).toBe(q('body')[0]);
        expect($selection[2]).toBe(q('#food')[0]);
        expect($selection[3]).toBe(q('#fruits')[0]);
        expect($selection[4]).toBe(q('.apple')[0]);
      });
    });
    it('(filter) : filters the previous selection', () => {
      const $selection = $('li').eq(1).addBack('.apple');

      expect($selection).toHaveLength(2);
      expect($selection[0]).toBe($('.apple')[0]);
      expect($selection[1]).toBe($('.orange')[0]);
    });
    it('() : fails gracefully when no args are passed', () => {
      const $div = cheerio('<div>');
      expect($div.addBack()).toBe($div);
    });
  });

  describe('.is', () => {
    it('() : should return false', () => {
      expect($('li.apple').is()).toBe(false);
    });

    it('(true selector) : should return true', () => {
      expect(cheerio('#vegetables', vegetables).is('ul')).toBe(true);
    });

    it('(false selector) : should return false', () => {
      expect(cheerio('#vegetables', vegetables).is('div')).toBe(false);
    });

    it('(true selection) : should return true', () => {
      const $vegetables = cheerio('li', vegetables);
      expect($vegetables.is($vegetables.eq(1))).toBe(true);
    });

    it('(false selection) : should return false', () => {
      const $vegetableList = cheerio(vegetables);
      const $vegetables = $vegetableList.find('li');
      expect($vegetables.is($vegetableList)).toBe(false);
    });

    it('(true element) : should return true', () => {
      const $vegetables = cheerio('li', vegetables);
      expect($vegetables.is($vegetables[0])).toBe(true);
    });

    it('(false element) : should return false', () => {
      const $vegetableList = cheerio(vegetables);
      const $vegetables = $vegetableList.find('li');
      expect($vegetables.is($vegetableList[0])).toBe(false);
    });

    it('(true predicate) : should return true', () => {
      const result = $('li').is(function () {
        return this.tagName === 'li' && $(this).hasClass('pear');
      });
      expect(result).toBe(true);
    });

    it('(false predicate) : should return false', () => {
      const result = $('li')
        .last()
        .is(function () {
          return this.tagName === 'ul';
        });
      expect(result).toBe(false);
    });
  });
});
