diff --git a/src/__fixtures__/fixtures.ts b/src/__fixtures__/fixtures.ts index d60832de72..cfa261b622 100644 --- a/src/__fixtures__/fixtures.ts +++ b/src/__fixtures__/fixtures.ts @@ -13,6 +13,14 @@ export const vegetables = [ '', ].join(''); +export const meats = [ + '', +].join(''); + export const divcontainers = [ '
', '
First
', diff --git a/src/api/attributes.spec.ts b/src/api/attributes.spec.ts index 79fe03fc57..fd72074a1e 100644 --- a/src/api/attributes.spec.ts +++ b/src/api/attributes.spec.ts @@ -8,6 +8,7 @@ import { chocolates, inputs, mixedText, + meats, } from '../__fixtures__/fixtures.js'; function withClass(attr: string) { @@ -42,6 +43,34 @@ describe('$(...)', () => { expect(attr).toBe('autofocus'); }); + it('(valid key) should get uppercase attr with lowercase name in HTML mode', () => { + const $casetest = load(meats); + const $meats = $casetest('.beef'); + expect($meats.attr('COOKED')).toBe('mediumrare'); + expect($meats.attr('cooked')).toBe('mediumrare'); + }); + + it('(valid key) should get lowercase attr with uppercase name in HTML mode', () => { + const $casetest = load(meats); + const $meats = $casetest('.beef'); + expect($meats.attr('CLASS')).toBe('beef'); + expect($meats.attr('class')).toBe('beef'); + }); + + it('(valid key) should get uppercase attr with uppercase name only in XML mode', () => { + const $casetest = load(meats, { xml: true }); + const $meats = $casetest('[class="beef"]'); + expect($meats.attr('COOKED')).toBe('mediumrare'); + expect($meats.attr('cooked')).toBeUndefined(); + }); + + it('(valid key) should get lowercase attr with lowercase name only in XML mode', () => { + const $casetest = load(meats, { xml: true }); + const $meats = $casetest('[class="beef"]'); + expect($meats.attr('CLASS')).toBeUndefined(); + expect($meats.attr('class')).toBe('beef'); + }); + it('(key, value) : should set one attr', () => { const $pear = $('.pear').attr('id', 'pear'); expect($('#pear')).toHaveLength(1); @@ -65,6 +94,20 @@ describe('$(...)', () => { expect($src[0]).toBeUndefined(); }); + it('(key, value) should save uppercase attr name as lowercase in HTML mode', () => { + const $casetest = load(meats); + const $meats = $casetest('.beef').attr('USDA', 'choice'); + expect($meats.attr('USDA')).toBe('choice'); + expect($meats.attr('usda')).toBe('choice'); + }); + + it('(key, value) should save uppercase attr name as uppercase in XML mode', () => { + const $casetest = load(meats, { xml: true }); + const $meats = $casetest('[class="beef"]').attr('USDA', 'choice'); + expect($meats.attr('USDA')).toBe('choice'); + expect($meats.attr('usda')).toBeUndefined(); + }); + it('(map) : object map should set multiple attributes', () => { $('.apple').attr({ id: 'apple', @@ -168,19 +211,24 @@ describe('$(...)', () => { }); it('(chaining) setting value and calling attr returns result', () => { + const pearAttr = $('.pear').attr('fizz', 'buzz').attr('fizz'); + expect(pearAttr).toBe('buzz'); + }); + + it('(chaining) overwriting value and calling attr returns result', () => { const pearAttr = $('.pear').attr('foo', 'bar').attr('foo'); expect(pearAttr).toBe('bar'); }); it('(chaining) setting attr to null returns a $', () => { - const $pear = $('.pear').attr('foo', null); + const $pear = $('.pear').attr('bar', null); expect($pear).toBeInstanceOf($); }); it('(chaining) setting attr to undefined returns a $', () => { - const $pear = $('.pear').attr('foo', undefined); + const $pear = $('.pear').attr('bar', undefined); expect($('.pear')).toHaveLength(1); - expect($('.pear').attr('foo')).toBeUndefined(); + expect($('.pear').attr('bar')).toBeUndefined(); expect($pear).toBeInstanceOf($); }); diff --git a/src/api/attributes.ts b/src/api/attributes.ts index 356bc551ff..6a306d9e48 100644 --- a/src/api/attributes.ts +++ b/src/api/attributes.ts @@ -56,13 +56,18 @@ function getAttr( return elem.attribs; } - if (hasOwn.call(elem.attribs, name)) { + // Coerce attribute names to lowercase to match load() and setAttr() behavior (HTML only) + const nameToUse = xmlMode ? name : name.toLowerCase(); + + if (hasOwn.call(elem.attribs, nameToUse)) { // Get the (decoded) attribute - return !xmlMode && rboolean.test(name) ? name : elem.attribs[name]; + return !xmlMode && rboolean.test(nameToUse) + ? nameToUse + : elem.attribs[nameToUse]; } // Mimic the DOM and return text content as value for `option's` - if (elem.name === 'option' && name === 'value') { + if (elem.name === 'option' && nameToUse === 'value') { return text(elem.children); } @@ -70,7 +75,7 @@ function getAttr( if ( elem.name === 'input' && (elem.attribs['type'] === 'radio' || elem.attribs['type'] === 'checkbox') && - name === 'value' + nameToUse === 'value' ) { return 'on'; } @@ -86,12 +91,21 @@ function getAttr( * @param el - The element to set the attribute on. * @param name - The attribute's name. * @param value - The attribute's value. + * @param xmlMode - True if running in XML mode. */ -function setAttr(el: Element, name: string, value: string | null) { +function setAttr( + el: Element, + name: string, + value: string | null, + xmlMode?: boolean +) { + // Coerce attr names to lowercase to match load() behavior (HTML only) + const nameToUse = xmlMode ? name : name.toLowerCase(); + if (value === null) { - removeAttribute(el, name); + removeAttribute(el, nameToUse); } else { - el.attribs[name] = `${value}`; + el.attribs[nameToUse] = `${value}`; } } @@ -197,7 +211,14 @@ export function attr( } } return domEach(this, (el, i) => { - if (isTag(el)) setAttr(el, name, value.call(el, i, el.attribs[name])); + if (isTag(el)) { + setAttr( + el, + name, + value.call(el, i, el.attribs[name]), + this.options.xmlMode + ); + } }); } return domEach(this, (el) => { @@ -206,10 +227,10 @@ export function attr( if (typeof name === 'object') { for (const objName of Object.keys(name)) { const objValue = name[objName]; - setAttr(el, objName, objValue); + setAttr(el, objName, objValue, this.options.xmlMode); } } else { - setAttr(el, name as string, value as string); + setAttr(el, name as string, value as string, this.options.xmlMode); } }); }