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 = [
+ '
',
'
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);
}
});
}