Skip to content
Open
8 changes: 8 additions & 0 deletions src/__fixtures__/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ export const vegetables = [
'</ul>',
].join('');

export const meats = [
'<ul id="meats">',
'<li class="beef" COOKED="mediumrare">Beef</li>',
'<li class="chicken">Chicken</li>',
'<li class="pork">Pork</li>',
'</ul>',
].join('');

export const divcontainers = [
'<div class="container">',
'<div class="inner">First</div>',
Expand Down
54 changes: 51 additions & 3 deletions src/api/attributes.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
chocolates,
inputs,
mixedText,
meats,
} from '../__fixtures__/fixtures.js';

function withClass(attr: string) {
Expand Down Expand Up @@ -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);
Expand All @@ -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',
Expand Down Expand Up @@ -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($);
});

Expand Down
41 changes: 31 additions & 10 deletions src/api/attributes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,21 +56,26 @@ 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);
}

// Mimic DOM with default value for radios/checkboxes
if (
elem.name === 'input' &&
(elem.attribs['type'] === 'radio' || elem.attribs['type'] === 'checkbox') &&
name === 'value'
nameToUse === 'value'
) {
return 'on';
}
Expand All @@ -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}`;
}
}

Expand Down Expand Up @@ -197,7 +211,14 @@ export function attr<T extends AnyNode>(
}
}
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) => {
Expand All @@ -206,10 +227,10 @@ export function attr<T extends AnyNode>(
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);
}
});
}
Expand Down