Skip to content

Commit

Permalink
Merge pull request #885 from capricorn86/task/883-update-from-v992-to…
Browse files Browse the repository at this point in the history
…-v9102-breaks-input-value-attribute-with-react-testing-library-getbytext

#883@patch: Fixes issue with attribute query selectors not using apos…
  • Loading branch information
capricorn86 committed May 2, 2023
2 parents 12a5e92 + f036561 commit f8618cd
Show file tree
Hide file tree
Showing 5 changed files with 65 additions and 22 deletions.
6 changes: 6 additions & 0 deletions packages/happy-dom/src/query-selector/QuerySelector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,12 @@ export default class QuerySelector {
* @returns Result.
*/
public static match(element: IElement, selector: string): ISelectorMatch | null {
if (selector === '*') {
return {
priorityWeight: 1
};
}

for (const items of SelectorParser.getSelectorGroups(selector)) {
const result = this.matchSelector(element, element, items.reverse());

Expand Down
8 changes: 2 additions & 6 deletions packages/happy-dom/src/query-selector/SelectorItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import ISelectorPseudo from './ISelectorPseudo';
* Selector item.
*/
export default class SelectorItem {
public all: string | null;
public tagName: string | null;
public id: string | null;
public classNames: string[] | null;
Expand All @@ -25,23 +24,20 @@ export default class SelectorItem {
*
* @param [options] Options.
* @param [options.combinator] Combinator.
* @param [options.all] All.
* @param [options.tagName] Tag name.
* @param [options.id] ID.
* @param [options.classNames] Class names.
* @param [options.attributes] Attributes.
* @param [options.pseudos] Pseudos.
*/
constructor(options?: {
all?: string;
tagName?: string;
id?: string;
classNames?: string[];
attributes?: ISelectorAttribute[];
pseudos?: ISelectorPseudo[];
combinator?: SelectorCombinatorEnum;
}) {
this.all = options?.all || null;
this.tagName = options?.tagName || null;
this.id = options?.id || null;
this.classNames = options?.classNames || null;
Expand All @@ -61,7 +57,7 @@ export default class SelectorItem {

// Tag name match
if (this.tagName) {
if (this.tagName !== element.tagName) {
if (this.tagName !== '*' && this.tagName !== element.tagName) {
return null;
}
priorityWeight += 1;
Expand Down Expand Up @@ -354,7 +350,7 @@ export default class SelectorItem {
* @returns Selector string.
*/
private getSelectorString(): string {
return `${this.all || ''}${this.tagName || ''}${this.id ? `#${this.id}` : ''}${
return `${this.tagName || ''}${this.id ? `#${this.id}` : ''}${
this.classNames ? `.${this.classNames.join('.')}` : ''
}${
this.attributes
Expand Down
46 changes: 30 additions & 16 deletions packages/happy-dom/src/query-selector/SelectorParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,19 @@ import DOMException from '../exception/DOMException';
* Group 3: ID (e.g. "#id")
* Group 4: Class (e.g. ".class")
* Group 5: Attribute name when no value (e.g. "attr1")
* Group 6: Attribute name when there is a value (e.g. "attr1")
* Group 7: Attribute operator (e.g. "~")
* Group 8: Attribute value (e.g. "value1")
* Group 9: Pseudo name when arguments (e.g. "nth-child")
* Group 10: Arguments of pseudo (e.g. "2n + 1")
* Group 11: Pseudo name when no arguments (e.g. "empty")
* Group 12: Combinator.
* Group 6: Attribute name when there is a value using apostrophe (e.g. "attr1")
* Group 7: Attribute operator when using apostrophe (e.g. "~")
* Group 8: Attribute value when using apostrophe (e.g. "value1")
* Group 9: Attribute name when threre is a value not using apostrophe (e.g. "attr1")
* Group 10: Attribute operator when not using apostrophe (e.g. "~")
* Group 11: Attribute value when notusing apostrophe (e.g. "value1")
* Group 12: Pseudo name when arguments (e.g. "nth-child")
* Group 13: Arguments of pseudo (e.g. "2n + 1")
* Group 14: Pseudo name when no arguments (e.g. "empty")
* Group 15: Combinator.
*/
const SELECTOR_REGEXP =
/(\*)|([a-zA-Z0-9-]+)|#((?:[a-zA-Z0-9-_]|\\.)+)|\.((?:[a-zA-Z0-9-_]|\\.)+)|\[([a-zA-Z0-9-_]+)\]|\[([a-zA-Z0-9-_]+)([~|^$*]{0,1}) *= *["']{0,1}([^"']*)["']{0,1}\]|:([a-zA-Z-]+) *\(([^)]+)\)|:([a-zA-Z-]+)|([ ,+>]*)/g;
/(\*)|([a-zA-Z0-9-]+)|#((?:[a-zA-Z0-9-_]|\\.)+)|\.((?:[a-zA-Z0-9-_]|\\.)+)|\[([a-zA-Z0-9-_]+)\]|\[([a-zA-Z0-9-_]+)([~|^$*]{0,1}) *= *["']{1}([^"']*)["']{1}\]|\[([a-zA-Z0-9-_]+)([~|^$*]{0,1}) *= *([^\]]*)\]|:([a-zA-Z-]+) *\(([^)]+)\)|:([a-zA-Z-]+)|([ ,+>]*)/g;

/**
* Escaped Character RegExp.
Expand Down Expand Up @@ -56,6 +59,10 @@ export default class SelectorParser {
* @returns Selector groups.
*/
public static getSelectorGroups(selector: string): Array<Array<SelectorItem>> {
if (selector === '*') {
return [[new SelectorItem({ tagName: '*' })]];
}

const simpleMatch = selector.match(SIMPLE_SELECTOR_REGEXP);

if (simpleMatch) {
Expand All @@ -82,7 +89,7 @@ export default class SelectorParser {
isValid = true;

if (match[1]) {
currentSelectorItem.all = '*';
currentSelectorItem.tagName = '*';
} else if (match[2]) {
currentSelectorItem.tagName = match[2].toUpperCase();
} else if (match[3]) {
Expand All @@ -104,20 +111,27 @@ export default class SelectorParser {
operator: match[7] || null,
value: match[8]
});
} else if (match[9] && match[10]) {
} else if (match[9] && match[11] !== undefined) {
currentSelectorItem.attributes = currentSelectorItem.attributes || [];
currentSelectorItem.attributes.push({
name: match[9].toLowerCase(),
operator: match[10] || null,
value: match[11]
});
} else if (match[12] && match[13]) {
currentSelectorItem.pseudos = currentSelectorItem.pseudos || [];
currentSelectorItem.pseudos.push({
name: match[9].toLowerCase(),
arguments: match[10]
name: match[12].toLowerCase(),
arguments: match[13]
});
} else if (match[11]) {
} else if (match[14]) {
currentSelectorItem.pseudos = currentSelectorItem.pseudos || [];
currentSelectorItem.pseudos.push({
name: match[11].toLowerCase(),
name: match[14].toLowerCase(),
arguments: null
});
} else if (match[12]) {
switch (match[12].trim()) {
} else if (match[15]) {
switch (match[15].trim()) {
case ',':
currentSelectorItem = new SelectorItem({
combinator: SelectorCombinatorEnum.descendant
Expand Down
14 changes: 14 additions & 0 deletions packages/happy-dom/test/query-selector/QuerySelector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,20 @@ describe('QuerySelector', () => {
expect(elements[2] === container.children[0].children[1].children[2]).toBe(true);
});

it('Returns all elements with tag name and matching attributes using Testing Library query "[type=submit], input[type=button], input[type=reset]".', () => {
const container = document.createElement('div');

container.innerHTML = `<input type="submit"></input><input type="reset"></input>`;

const elements = container.querySelectorAll(
'input[type=submit], input[type=button], input[type=reset]'
);

expect(elements.length).toBe(2);
expect(elements[0] === container.children[0]).toBe(true);
expect(elements[1] === container.children[1]).toBe(true);
});

it('Returns all elements with tag name and multiple matching attributes using "span[attr1="value1"][attr2="word1 word2"]".', () => {
const container = document.createElement('div');
container.innerHTML = QuerySelectorHTML;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,17 @@ describe('TestingLibrary', () => {

expect(changeHandler).toHaveBeenCalledTimes(1);
});

it('Finds elements using "screen.getByText()".', async () => {
const user = userEvent.setup();
const clickHandler = jest.fn();

render(<input type="submit" value="Submit Button" onClick={clickHandler} />);

const element = screen.getByText('Submit Button');

await user.click(element);

expect(clickHandler).toHaveBeenCalledTimes(1);
});
});

0 comments on commit f8618cd

Please sign in to comment.