Skip to content

Commit

Permalink
Merge pull request #58 from /issues/51-element-clear
Browse files Browse the repository at this point in the history
feat: element clear endpoint
  • Loading branch information
poftadeh authored Oct 28, 2019
2 parents 3135ef5 + 881d173 commit b3b3f2c
Show file tree
Hide file tree
Showing 15 changed files with 495 additions and 113 deletions.
34 changes: 34 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,40 @@
"${workspaceFolder}/node_modules/**/*.js",
"<node_internals>/**/*.js"
]
},
{
"name": "Debug Jest Tests in Linux",
"type": "node",
"request": "launch",
"runtimeArgs": [
"--inspect-brk",
"${workspaceRoot}/node_modules/.bin/jest",
"--runInBand"
],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"port": 9229,
"skipFiles": [
"${workspaceFolder}/node_modules/**/*.js",
"<node_internals>/**/*.js"
]
},
{
"name": "Debug Jest Tests in Windows",
"type": "node",
"request": "launch",
"runtimeArgs": [
"--inspect-brk",
"${workspaceRoot}/node_modules/jest/bin/jest.js",
"--runInBand"
],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"port": 9229,
"skipFiles": [
"${workspaceFolder}/node_modules/**/*.js",
"<node_internals>/**/*.js"
]
}
]
}
13 changes: 7 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,16 @@
]
},
"scripts": {
"test": "tsc && jest --watch",
"test-mocha": "mocha --exit",
"test": "jest",
"test:compile": "tsc && jest",
"test:mocha": "mocha --exit",
"lint": "eslint",
"start": "node ./build/index.js",
"compile": "tsc",
"build-linux": "tsc && pkg . --target latest-linux-x64",
"build-win": "tsc && pkg . --target latest-win-x64",
"build-macos": "tsc && pkg . --target latest-mac-x64",
"build-all": "tsc && pkg . --target latest-linux-x64,latest-win-x64,latest-mac-x64",
"build:linux": "tsc && pkg . --target latest-linux-x64",
"build:win": "tsc && pkg . --target latest-win-x64",
"build:macos": "tsc && pkg . --target latest-mac-x64",
"build:all": "tsc && pkg . --target latest-linux-x64,latest-win-x64,latest-mac-x64",
"generate-docs": "./node_modules/.bin/typedoc --out docs ./src/ --options ./typedoc.json &>/dev/null",
"watch": "nodemon --watch src/ --exec 'npm run compile-ts && npm start' -e ts"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export class ElementNotInteractable extends BadRequest {
constructor() {
super();
this.name = 'ElementNotInteractableError';
this.message = `${this.command} could not be completed because the element is not pointer- or keyboard interactable.`;
this.message = `${this.command} could not be completed because the element is not pointer or keyboard interactable.`;
this.JSONCodeError = 'element not interactable';
}
}
11 changes: 11 additions & 0 deletions src/Error/InvalidElementState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { BadRequest } from './BadRequest';

export class InvalidElementState extends BadRequest {
constructor() {
super();
this.message =
'A command could not be completed because the element is in an invalid state';
this.name = 'InvalidElementStateError';
this.JSONCodeError = 'invalid element state';
}
}
4 changes: 3 additions & 1 deletion src/Error/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@ import { MethodNotAllowed } from './MethodNotAllowedError';
import { NoSuchElement } from './NoSuchElement';
import { NotFoundError } from './NotFoundError';
import { InvalidArgument } from './InvalidArgument';
import { InvalidElementState } from './InvalidElementState';
import { SessionNotCreated } from './SessionNotCreated';
import { InternalServerError } from './InternalServerError';
import { ElementNotInteractable } from './ElementNotInteractableError';
import { ElementNotInteractable } from './ElementNotInteractable';
import { NoSuchWindow } from './NoSuchWindow';

export {
MethodNotAllowed,
NoSuchElement,
NotFoundError,
InvalidArgument,
InvalidElementState,
SessionNotCreated,
InternalServerError,
ElementNotInteractable,
Expand Down
7 changes: 6 additions & 1 deletion src/Session/Session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,10 +169,15 @@ class Session {
);
break;
case COMMANDS.ELEMENT_CLICK:
if (!this.browser.dom) throw new NoSuchWindow();
if (!this.browser.dom.window) throw new NoSuchWindow();
this.browser.getKnownElement(urlVariables.elementId).click();
response = { value: null };
break;
case COMMANDS.ELEMENT_CLEAR:
if (!this.browser.dom.window) throw new NoSuchWindow();
this.browser.getKnownElement(urlVariables.elementId).clear();
response = { value: null };
break;
default:
break;
}
Expand Down
74 changes: 68 additions & 6 deletions src/WebElement/WebElement.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import * as uuidv1 from 'uuid/v1';
import isFocusableAreaElement from 'jsdom/lib/jsdom/living/helpers/focusing';
import jsdomUtils from 'jsdom/lib/jsdom/living/generated/utils';
import { isFocusableAreaElement } from 'jsdom/lib/jsdom/living/helpers/focusing';
import { implSymbol } from 'jsdom/lib/jsdom/living/generated/utils';
import { ELEMENT, ElementBooleanAttributeValues } from '../constants/constants';
import { InvalidArgument } from '../Error/errors';
import { InvalidArgument, InvalidElementState } from '../Error/errors';
import {
isInputElement,
isMutableFormControlElement,
isMutableElement,
} from '../utils/utils';

// TODO: find a more efficient way to import this
import { JSDOM } from 'jsdom';
Expand All @@ -16,11 +21,12 @@ class WebElement {
this.element = element;
this[ELEMENT] = uuidv1();
}

/**
* Wrapper for the jsdom isFocusableAreaElement method
*/
isInteractable(): boolean {
return isFocusableAreaElement(this.element[jsdomUtils.implSymbol]);
return isFocusableAreaElement(this.element[implSymbol]);
}

/**
Expand Down Expand Up @@ -67,10 +73,10 @@ class WebElement {
findAncestor(tagName: string): HTMLElement | null {
let { parentElement: nextParent } = this.element;

const isMatchingOrIsFalsy = (): boolean =>
const isMatchingOrFalsy = (): boolean =>
!nextParent || nextParent.tagName.toLowerCase() === tagName.toLowerCase();

while (!isMatchingOrIsFalsy()) {
while (!isMatchingOrFalsy()) {
const { parentElement } = nextParent;
nextParent = parentElement;
}
Expand Down Expand Up @@ -174,6 +180,62 @@ class WebElement {
);
});
}

/**
* Clears a mutable element (https://www.w3.org/TR/webdriver/#dfn-mutable-element)
* @returns {undefined}
*/
private clearContentEditableElement(element: HTMLElement): void {
if (element.innerHTML === '') return;
element.focus();
element.innerHTML = '';
element.blur();
}

/**
* Clears a resettable element (https://www.w3.org/TR/webdriver/#dfn-clear-a-resettable-element)
* @returns {undefined}
*/
private clearResettableElement(
element: HTMLInputElement | HTMLTextAreaElement,
): void {
let isEmpty: boolean;

if (
isInputElement(element) &&
Object.prototype.hasOwnProperty.call(element, 'files')
) {
isEmpty = element.files.length === 0;
} else {
isEmpty = element.value === '';
}

if (isEmpty) return;

element.focus();
element.value = '';
element.blur();
}

/**
* clicks the WebElement's HTML element.
* @returns {undefined}
*/
async clear(): Promise<void> {
const { element } = this;

if (isMutableFormControlElement(element)) {
this.clearResettableElement(
isInputElement(element)
? (element as HTMLInputElement)
: (element as HTMLTextAreaElement),
);
} else if (isMutableElement(element)) {
this.clearContentEditableElement(element);
} else {
throw new InvalidElementState();
}
}
}

export { WebElement };
1 change: 1 addition & 0 deletions src/constants/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,5 @@ export const COMMANDS = {
DELETE_COOKIE: 'DELETE COOKIE',
DELETE_ALL_COOKIES: 'DELETE ALL COOKIES',
ELEMENT_CLICK: 'ELEMENT_CLICK',
ELEMENT_CLEAR: 'ELEMENT_CLEAR',
};
8 changes: 7 additions & 1 deletion src/routes/elements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ element.post(
);

// clear element
element.post('/clear', (req, res, next) => {});
element.post(
'/clear',
sessionEndpointExceptionHandler(
defaultSessionEndpointLogic,
COMMANDS.ELEMENT_CLEAR,
),
);

export default element;
46 changes: 46 additions & 0 deletions src/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,3 +207,49 @@ export const endpoint = {
}
},
};

export const isInputElement = (
element: HTMLElement,
): element is HTMLInputElement => {
return element.tagName.toLowerCase() === 'input';
};

export const isTextAreaElement = (
element: HTMLElement,
): element is HTMLTextAreaElement => {
return element.tagName.toLowerCase() === 'textarea';
};

export const isEditableFormControlElement = (
element: HTMLInputElement | HTMLTextAreaElement,
): boolean => {
return !element.hidden && !element.readOnly && !element.disabled;
};

export const isMutableFormControlElement = (element: HTMLElement): boolean => {
let isMutable: boolean;

if (isTextAreaElement(element)) {
isMutable = isEditableFormControlElement(element);
} else if (isInputElement(element)) {
const mutableInputPattern = new RegExp(
'^(text|search|url|tel|email|password|date|month|week|time|datetime-local|number|range|color|file)$',
);
isMutable =
isEditableFormControlElement(element) &&
mutableInputPattern.test(element.type);
} else {
isMutable = false;
}

return isMutable;
};

export const isMutableElement = (element: HTMLElement): boolean => {
const {
contentEditable,
ownerDocument: { designMode },
} = element;

return contentEditable === 'true' || designMode === 'on';
};
Loading

0 comments on commit b3b3f2c

Please sign in to comment.