Skip to content

Commit

Permalink
feat: add multi element support
Browse files Browse the repository at this point in the history
  • Loading branch information
fczbkk committed Aug 15, 2021
1 parent df4ec59 commit 3f42092
Show file tree
Hide file tree
Showing 8 changed files with 141 additions and 42 deletions.
2 changes: 1 addition & 1 deletion src/identifiable-parent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function getIdentifiableParent (
if (selector !== '') {
if (testSelector(currentElement, selector, root)) {
return {
foundElement: currentElement,
foundElements: [currentElement],
selector
}
}
Expand Down
30 changes: 22 additions & 8 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import {getFallbackSelector} from './selector-fallback'
import {sanitizeOptions} from './utilities-options'
import {getClosestIdentifiableParent} from './utilities-selectors'
import {
getClosestIdentifiableParent,
sanitizeSelectorNeedle
} from './utilities-selectors';
import {CssSelector} from './types'
import {testSelector} from './utilities-dom';
import {SELECTOR_SEPARATOR} from './constants';

/**
* Generates unique CSS selector for an element.
*/
export function getCssSelector (
element: Element,
needle: unknown,
custom_options = {}
): CssSelector {
const options = sanitizeOptions(element, custom_options)
const elements = sanitizeSelectorNeedle(needle)
const options = sanitizeOptions(elements[0], custom_options)
let partialSelector = ''
let currentRoot = options.root

Expand All @@ -19,7 +25,7 @@ export function getCssSelector (
*/
function updateIdentifiableParent () {
return getClosestIdentifiableParent(
element,
elements,
currentRoot,
partialSelector,
options
Expand All @@ -29,18 +35,26 @@ export function getCssSelector (
let closestIdentifiableParent = updateIdentifiableParent()
while (closestIdentifiableParent) {
const {
foundElement,
foundElements,
selector
} = closestIdentifiableParent
if (foundElement === element) {
if (testSelector(elements, selector)) {
return selector
}
currentRoot = foundElement
currentRoot = foundElements[0]
partialSelector = selector
closestIdentifiableParent = updateIdentifiableParent()
}

return getFallbackSelector(element)
// if failed to find single selector matching all elements, try to find
// selector for each standalone element and join them together
if (elements.length > 1) {
return elements
.map((element) => getCssSelector(element, options))
.join(SELECTOR_SEPARATOR)
}

return getFallbackSelector(elements)
}

export default getCssSelector
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,6 @@ export type CssSelectorGeneratorOptions = {

export type IdentifiableParent =
null
| { foundElement: Element, selector: CssSelector }
| { foundElements: Element[], selector: CssSelector }

export type SelectorNeedle = Element | Array<Element>
35 changes: 25 additions & 10 deletions src/utilities-dom.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import isElement from 'iselement'
import {CssSelector} from './types'
import {CssSelector, SelectorNeedle} from './types';
import {getIntersection} from './utilities-data';
import {sanitizeSelectorNeedle} from './utilities-selectors';

/**
* Check whether element is matched uniquely by selector.
*/
export function testSelector (
element: Element,
needle: SelectorNeedle,
selector: CssSelector,
root: ParentNode = document
): boolean {
const result = root.querySelectorAll(selector)
return (result.length === 1 && result[0] === element)
const elements = sanitizeSelectorNeedle(needle)
const result = [...root.querySelectorAll(selector)]
return (
result.length === elements.length
&& elements.every((element) => result.includes(element))
)
}

export function testMultiSelector (
Expand All @@ -22,13 +28,10 @@ export function testMultiSelector (
return result.includes(element)
}

/**
* Find all parent elements of the element.
*/
export function getParents (
export function getElementParents (
element: Element,
root = getRootNode(element)
): Array<Element> {
root: ParentNode
): Element[] {
const result = []
let parent = element
while (isElement(parent) && parent !== root) {
Expand All @@ -38,6 +41,18 @@ export function getParents (
return result
}

/**
* Find all parent elements of the element.
*/
export function getParents (
needle: SelectorNeedle,
root?: ParentNode
): Array<Element> {
const elements = sanitizeSelectorNeedle(needle)
root = root ?? getRootNode(elements[0])
return getIntersection(elements.map((element) => getElementParents(element, root)))
}

/**
* Returns root node for given element. This needs to be used because of document-less environments, e.g. jsdom.
*/
Expand Down
65 changes: 45 additions & 20 deletions src/utilities-selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import {getTagSelector} from './selector-tag'
import {
convertMatchListToRegExp,
flattenArray,
getCombinations
} from './utilities-data'
getCombinations, getIntersection
} from './utilities-data';
import {getParents, testSelector} from './utilities-dom'
import {
CssSelector,
Expand All @@ -23,6 +23,7 @@ import {
CssSelectorType,
IdentifiableParent, SelectorNeedle
} from './types';
import isElement from 'iselement';

export const ESCAPED_COLON = ':'
.charCodeAt(0)
Expand Down Expand Up @@ -64,10 +65,17 @@ export const SELECTOR_TYPE_GETTERS = {
* Returns list of selectors of given type for the element.
*/
export function getSelectorsByType (
element: Element,
needle: SelectorNeedle,
selector_type: CssSelectorType
): Array<CssSelector> {
return (SELECTOR_TYPE_GETTERS[selector_type] || ((): Array<CssSelector> => []))(element)
const elements = sanitizeSelectorNeedle(needle)
return getIntersection(elements.map((element) => {
const getter = (
SELECTOR_TYPE_GETTERS[selector_type]
?? ((): Array<CssSelector> => [])
)
return getter(element)
}))
}

/**
Expand Down Expand Up @@ -108,11 +116,12 @@ export function orderSelectors (
* Returns list of unique selectors applicable to given element.
*/
export function getAllSelectors (
element: Element,
needle: SelectorNeedle,
root: ParentNode,
options: CssSelectorGeneratorOptions
): Array<CssSelector> {
const selectors_list = getSelectorsList(element, options)
const elements = sanitizeSelectorNeedle(needle)
const selectors_list = getSelectorsList(elements, options)
const type_combinations = getTypeCombinations(selectors_list, options)
const all_selectors = flattenArray(type_combinations) as Array<CssSelector>
return [...new Set(all_selectors)]
Expand All @@ -122,9 +131,10 @@ export function getAllSelectors (
* Creates object containing all selector types and their potential values.
*/
export function getSelectorsList (
element: Element,
needle: SelectorNeedle,
options: CssSelectorGeneratorOptions
): CssSelectorData {
const elements = sanitizeSelectorNeedle(needle)
const {
blacklist,
whitelist,
Expand All @@ -135,7 +145,7 @@ export function getSelectorsList (
const whitelist_re = convertMatchListToRegExp(whitelist)

const reducer = (data: CssSelectorData, selector_type: CssSelectorType) => {
const selectors_by_type = getSelectorsByType(element, selector_type)
const selectors_by_type = getSelectorsByType(elements, selector_type)
const filtered_selectors =
filterSelectors(selectors_by_type, blacklist_re, whitelist_re)
const found_selectors = orderSelectors(filtered_selectors, whitelist_re)
Expand Down Expand Up @@ -269,16 +279,18 @@ export function constructSelector (
* Generator of CSS selector candidates for given element, from simplest child selectors to more complex descendant selectors.
*/
export function getElementSelectorCandidates (
element: Element,
needle: SelectorNeedle,
root: ParentNode,
options: CssSelectorGeneratorOptions
): Array<CssSelector> {
const elements = sanitizeSelectorNeedle(needle)
const result = []
const selectorCandidates = getAllSelectors(element, root, options)
const selectorCandidates = getAllSelectors(elements, root, options)
for (const selectorCandidate of selectorCandidates) {
result.push(CHILD_OPERATOR + selectorCandidate)
}
if (root === element.parentNode) {

if (elements.every((element) => element.parentNode === root)) {
for (const selectorCandidate of selectorCandidates) {
result.push(DESCENDANT_OPERATOR + selectorCandidate)
}
Expand All @@ -290,16 +302,17 @@ export function getElementSelectorCandidates (
* Tries to find an unique CSS selector for element within given parent.
*/
export function getSelectorWithinRoot (
element: Element,
needle: SelectorNeedle,
root: ParentNode,
rootSelector: CssSelector = '',
options: CssSelectorGeneratorOptions
): (null | CssSelector) {
const elements = sanitizeSelectorNeedle(needle)
const selectorCandidates =
getElementSelectorCandidates(element, options.root, options)
getElementSelectorCandidates(elements, options.root, options)
for (const candidateSelector of selectorCandidates) {
const attemptSelector = (rootSelector + candidateSelector).trim()
if (testSelector(element, attemptSelector, options.root)) {
if (testSelector(elements, attemptSelector, options.root)) {
return attemptSelector
}
}
Expand All @@ -310,24 +323,36 @@ export function getSelectorWithinRoot (
* Climbs through parents of the element and tries to find the one that is identifiable by unique CSS selector.
*/
export function getClosestIdentifiableParent (
element: Element,
needle: SelectorNeedle,
root: ParentNode,
rootSelector: CssSelector = '',
options: CssSelectorGeneratorOptions
): IdentifiableParent {
for (const currentElement of getParents(element, root)) {
const elements = sanitizeSelectorNeedle(needle)

if (elements.length === 0) {
return null
}

const candidatesList = [
(elements.length > 1) ? elements : [],
...getParents(elements, root).map((element) => [element])
]

for (const currentElements of candidatesList) {
const result =
getSelectorWithinRoot(currentElement, root, rootSelector, options)
getSelectorWithinRoot(currentElements, root, rootSelector, options)
if (result) {
return {
foundElement: currentElement,
foundElements: currentElements,
selector: result
}
}
}

return null
}

export function sanitizeSelectorNeedle (needle: SelectorNeedle) {
return Array.isArray(needle) ? needle : [needle]
export function sanitizeSelectorNeedle (needle: unknown): Element[] {
return [...new Set((Array.isArray(needle) ? needle : [needle]).filter(isElement))]
}
4 changes: 2 additions & 2 deletions test/identifiable-parent.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ describe('Identifiable Parent', () => {
)
const parentElement = root.querySelector('[data-parent]')
assert.deepEqual(result, {
foundElement: parentElement,
foundElements: [parentElement],
selector: '#mockParent'
})
})
Expand All @@ -60,7 +60,7 @@ describe('Identifiable Parent', () => {
)
const parentElement = root.querySelector('[data-parent]')
assert.deepEqual(result, {
foundElement: parentElement,
foundElements: [parentElement],
selector: '.mockParent'
})
})
Expand Down
28 changes: 28 additions & 0 deletions test/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,32 @@ describe('CssSelectorGenerator', function () {

})

describe('multiple elements', () => {

it('should get single selector matching multiple elements', () => {
root.innerHTML = `
<div class="aaa bbb"></div>
<span class="bbb ccc"></span>
`
const elements = [...root.querySelectorAll('.bbb')]
const result = getCssSelector(elements)
assert.equal(result, '.bbb')
})

it('should get combined selector matching multiple elements', () => {
root.innerHTML = '<a></a><span></span>'
const elements = [root.children[0], root.children[1]]
const result = getCssSelector(elements)
assert.equal(result, 'a, span')
})

it('should get fallback selectors for multiple elements', () => {
root.innerHTML = '<div></div><div></div><div></div>'
const elements = [root.children[0], root.children[1]]
const result = getCssSelector(elements)
assert.ok(testSelector(elements, result))
})

})

})
17 changes: 17 additions & 0 deletions test/utilities-get-parents.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,21 @@ describe('utilities - getParents', function () {
assert.lengthOf(result, 0)
})

it('should find common parents for multiple elements', () => {
root.innerHTML = `
<div>
<ul>
<li></li>
</ul>
<ul>
<li></li>
</ul>
</div>
`
const elements = [...root.querySelectorAll('li')]
const result = getParents(elements, root)
assert.lengthOf(result, 1)
assert.equal(result[0].tagName, 'DIV')
})

})

0 comments on commit 3f42092

Please sign in to comment.