Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 53 additions & 84 deletions javascript/atoms/typescript/get-attribute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,12 @@
// specific language governing permissions and limitations
// under the License.

(function (): (element: Element, attribute: string) => string | null {
/**
* Common aliases for properties. This maps names that users use to the
* correct property name.
*/
(function (element: Element, attribute: string): string | null {
const PROPERTY_ALIASES: Record<string, string> = {
'class': 'className',
'readonly': 'readOnly',
};

/**
* Boolean properties extracted from the WHATWG spec:
* http://www.whatwg.org/specs/web-apps/current-work/
*
* These must all be lower-case.
*/
const BOOLEAN_PROPERTIES: string[] = [
'allowfullscreen',
'allowpaymentrequest',
Expand Down Expand Up @@ -129,90 +119,69 @@
return value !== null && (typeof value === 'object' || typeof value === 'function');
}

/**
* Get the value of the given property or attribute. If the "attribute" is for
* a boolean property, we return null in the case where the value is false. If
* the attribute name is "style" an attempt to convert that style into a string
* is done.
*
* @param element The element to use.
* @param attribute The name of the attribute to look up.
* @return The string value of the attribute or property, or null.
*/
return function get(element: Element, attribute: string): string | null {
const name = attribute.toLowerCase();

if (name === 'style') {
const style = (element as HTMLElement).style;
if (!style) {
return null;
}
return typeof style === 'string' ? style : style.cssText;
}
const name = attribute.toLowerCase();

if ((name === 'selected' || name === 'checked') && isSelectable(element)) {
return isSelected(element) ? 'true' : null;
if (name === 'style') {
const style = (element as HTMLElement).style;
if (!style) {
return null;
}
return typeof style === 'string' ? style : style.cssText;
}

const isLink = isElement(element, 'A');
const isImg = isElement(element, 'IMG');
if ((name === 'selected' || name === 'checked') && isSelectable(element)) {
return isSelected(element) ? 'true' : null;
}

if ((isImg && name === 'src') || (isLink && name === 'href')) {
const attrValue = getAttribute(element, name);
if (attrValue) {
return String(getProperty(element, name));
}
return attrValue;
const isLink = isElement(element, 'A');
const isImg = isElement(element, 'IMG');

if ((isImg && name === 'src') || (isLink && name === 'href')) {
const attrValue = getAttribute(element, name);
if (attrValue) {
return String(getProperty(element, name));
}
return attrValue;
}

if (name === 'spellcheck') {
const attrValue = getAttribute(element, name);
if (attrValue !== null) {
const lower = attrValue.toLowerCase();
if (lower === 'false') {
return 'false';
}
if (lower === 'true') {
return 'true';
}
if (name === 'spellcheck') {
const attrValue = getAttribute(element, name);
if (attrValue !== null) {
const lower = attrValue.toLowerCase();
if (lower === 'false') {
return 'false';
}
if (lower === 'true') {
return 'true';
}
return String(getProperty(element, name));
}
return String(getProperty(element, name));
}

const propName = PROPERTY_ALIASES[name] || attribute;
const propName = PROPERTY_ALIASES[name] || attribute;

if (BOOLEAN_PROPERTIES.indexOf(name) !== -1) {
const hasAttr = getAttribute(element, attribute) !== null;
const propValue = getProperty(element, propName);
return hasAttr || !!propValue ? 'true' : null;
}
if (BOOLEAN_PROPERTIES.indexOf(name) !== -1) {
const hasAttr = getAttribute(element, attribute) !== null;
const propValue = getProperty(element, propName);
return hasAttr || !!propValue ? 'true' : null;
}

// Special-case: for list items (<li>), the value property is numeric;
// callers expecting the attribute's literal string value should get the
// HTML attribute instead of the coerced numeric property.
if (name === 'value' && isElement(element, 'LI')) {
const attrValue = getAttribute(element, attribute);
return attrValue != null ? attrValue : null;
}
if (name === 'value' && isElement(element, 'LI')) {
const attrValue = getAttribute(element, attribute);
return attrValue != null ? attrValue : null;
}

// For regular attributes, try the property first since it may be updated
// dynamically (e.g., input.value set by JavaScript). Fall back to the
// HTML attribute only for cases where the property is null/undefined/object,
// such as event handlers in Firefox or expando properties.
let property: unknown;
try {
property = getProperty(element, propName);
} catch (_e) {
// Leaves property undefined; getAttribute below will be used as fallback.
}
let property: unknown;
try {
property = getProperty(element, propName);
} catch (_e) {
// getAttribute below will be used as fallback
}

// Fall back to getAttribute when property is null/undefined or an object.
// This handles event handlers in Firefox and other edge cases.
if (property == null || isObject(property)) {
const attrValue = getAttribute(element, attribute);
return attrValue != null ? attrValue : null;
}
if (property == null || isObject(property)) {
const attrValue = getAttribute(element, attribute);
return attrValue != null ? attrValue : null;
}

return property != null ? String(property) : null;
};
})()
return property != null ? String(property) : null;
})
6 changes: 3 additions & 3 deletions javascript/atoms/typescript/wrap-get-attribute-as-global.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,12 @@ window.bot.dom.typescript.getAttribute = `;

const input = fs.readFileSync(inputPath, 'utf8');

// The compiled TypeScript output begins with "(function () {". Prepend the
// The compiled TypeScript output begins with "(function (". Prepend the
// browser-global assignment so the atom is accessible as
// window.bot.dom.typescript.getAttribute in test pages.
if (!input.trimStart().startsWith('(function ()')) {
if (!input.trimStart().startsWith('(function (')) {
throw new Error(
`Unexpected compiled output format. Expected it to start with "(function ()", got: ${input.slice(0, 80)}`
`Unexpected compiled output format. Expected it to start with "(function (", got: ${input.slice(0, 80)}`
);
}

Expand Down
4 changes: 2 additions & 2 deletions javascript/selenium-webdriver/lib/atoms/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ js_run_binary(

js_run_binary(
name = "get_attribute",
srcs = ["//javascript/webdriver/atoms:get-attribute.js"],
srcs = ["//javascript/atoms/fragments:get-attribute-typescript.js"],
outs = ["get-attribute.js"],
args = [
"$(rootpath //javascript/webdriver/atoms:get-attribute.js)",
"$(rootpath //javascript/atoms/fragments:get-attribute-typescript.js)",
"$(rootpath :get-attribute.js)",
],
tool = ":make_atoms_module",
Expand Down
Loading