Skip to content

Commit

Permalink
feat(client): focus and xpath dom extenders
Browse files Browse the repository at this point in the history
  • Loading branch information
blakebyrnes committed Feb 3, 2022
1 parent fd4b7ff commit 0888c8b
Show file tree
Hide file tree
Showing 9 changed files with 169 additions and 5 deletions.
26 changes: 22 additions & 4 deletions client/lib/CoreFrameEnvironment.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IInteractionGroups } from '@ulixee/hero-interfaces/IInteractions';
import { IInteractionGroups, isMousePositionXY } from '@ulixee/hero-interfaces/IInteractions';
import ISessionMeta from '@ulixee/hero-interfaces/ISessionMeta';
import { ILoadStatus, ILocationTrigger } from '@ulixee/hero-interfaces/Location';
import AwaitedPath, { IJsPath } from 'awaited-dom/base/AwaitedPath';
Expand All @@ -8,16 +8,23 @@ import IExecJsPathResult from '@ulixee/hero-interfaces/IExecJsPathResult';
import { IRequestInit } from 'awaited-dom/base/interfaces/official';
import INodePointer from 'awaited-dom/base/INodePointer';
import ISetCookieOptions from '@ulixee/hero-interfaces/ISetCookieOptions';
import { getComputedVisibilityFnName } from '@ulixee/hero-interfaces/jsPathFnNames';
import {
getComputedVisibilityFnName,
isFocusedFnName,
} from '@ulixee/hero-interfaces/jsPathFnNames';
import IWaitForOptions from '@ulixee/hero-interfaces/IWaitForOptions';
import IFrameMeta from '@ulixee/hero-interfaces/IFrameMeta';
import CoreCommandQueue from './CoreCommandQueue';
import IResourceMeta from '@ulixee/hero-interfaces/IResourceMeta';
import { INodeVisibility } from '@ulixee/hero-interfaces/INodeVisibility';
import { createInstanceWithNodePointer, delegate as AwaitedHandler } from './SetupAwaitedHandler';
import {
convertJsPathArgs,
createInstanceWithNodePointer,
delegate as AwaitedHandler,
} from './SetupAwaitedHandler';
import StateMachine from 'awaited-dom/base/StateMachine';
import IAwaitedOptions from '../interfaces/IAwaitedOptions';
import { INodeIsolate } from 'awaited-dom/base/interfaces/isolate';
import { IElementIsolate, INodeIsolate } from 'awaited-dom/base/interfaces/isolate';
import CoreTab from './CoreTab';
import { ISuperElement } from 'awaited-dom/base/interfaces/super';
import TimeoutError from '@ulixee/commons/interfaces/TimeoutError';
Expand Down Expand Up @@ -108,13 +115,24 @@ export default class CoreFrameEnvironment {
}

public async interact(interactionGroups: IInteractionGroups): Promise<void> {
for (const interactionGroup of interactionGroups) {
for (const interactionStep of interactionGroup) {
if (interactionStep.mousePosition && !isMousePositionXY(interactionStep.mousePosition)) {
convertJsPathArgs(interactionStep.mousePosition);
}
}
}
await this.commandQueue.run('FrameEnvironment.interact', ...interactionGroups);
}

public async getComputedVisibility(node: INodeIsolate): Promise<INodeVisibility> {
return await AwaitedHandler.runMethod(awaitedPathState, node, getComputedVisibilityFnName, []);
}

public async isFocused(element: IElementIsolate): Promise<boolean> {
return await AwaitedHandler.runMethod(awaitedPathState, element, isFocusedFnName, []);
}

public async getNodePointer(node: INodeIsolate): Promise<INodePointer> {
return await AwaitedHandler.createNodePointer(awaitedPathState, node);
}
Expand Down
31 changes: 30 additions & 1 deletion client/lib/DomExtender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ import { ITypeInteraction } from '../interfaces/IInteractions';
import CoreFrameEnvironment from './CoreFrameEnvironment';
import IAwaitedOptions from '../interfaces/IAwaitedOptions';
import Interactor from './Interactor';
import XPathResult from 'awaited-dom/impl/official-klasses/XPathResult';
import { createSuperNode } from 'awaited-dom/impl/create';
import { getAwaitedPathAsMethodArg } from './SetupAwaitedHandler';

const awaitedPathState = StateMachine<
any,
Expand All @@ -41,6 +44,7 @@ interface IBaseExtendNode {
$isVisible: Promise<boolean>;
$exists: Promise<boolean>;
$isClickable: Promise<boolean>;
$hasFocus: Promise<boolean>;
$clearValue(): Promise<void>;
$click(verification?: IElementInteractVerification): Promise<void>;
$collect(name: string): Promise<void>;
Expand All @@ -49,6 +53,7 @@ interface IBaseExtendNode {
$waitForClickable(options?: { timeoutMs?: number }): Promise<ISuperElement>;
$waitForHidden(options?: { timeoutMs?: number }): Promise<ISuperElement>;
$waitForVisible(options?: { timeoutMs?: number }): Promise<ISuperElement>;
$xpathSelector(selector: string): ISuperNode;
}

interface IBaseExtendNodeList {
Expand Down Expand Up @@ -76,7 +81,10 @@ declare module 'awaited-dom/base/interfaces/official' {
interface IHTMLCollection extends IBaseExtendNodeList {}
}

const NodeExtensionFns: Omit<IBaseExtendNode, '$isClickable' | '$isVisible' | '$exists'> = {
const NodeExtensionFns: Omit<
IBaseExtendNode,
'$isClickable' | '$isVisible' | '$exists' | '$hasFocus'
> = {
async $click(verification: IElementInteractVerification = 'elementAtPath'): Promise<void> {
const coreFrame = await getCoreFrame(this);
await Interactor.run(coreFrame, [{ click: { element: this, verification } }]);
Expand Down Expand Up @@ -136,6 +144,23 @@ const NodeExtensionFns: Omit<IBaseExtendNode, '$isClickable' | '$isVisible' | '$
{ keyPress: KeyboardKey.Backspace },
]);
},
$xpathSelector(selector: string, orderedNodeResults = false): ISuperNode {
const { awaitedOptions, awaitedPath } = awaitedPathState.getState(this);
const newPath = new AwaitedPath(
null,
'document',
[
'evaluate',
getAwaitedPathAsMethodArg(awaitedPath),
null,
orderedNodeResults
? XPathResult.FIRST_ORDERED_NODE_TYPE
: XPathResult.ANY_UNORDERED_NODE_TYPE,
],
'singleNodeValue',
);
return createSuperNode(newPath, awaitedOptions);
},
};

const NodeExtensionGetters = {
Expand All @@ -154,6 +179,10 @@ const NodeExtensionGetters = {
const visibility = await coreFrame.getComputedVisibility(this);
return visibility.isVisible;
},
async $hasFocus(): Promise<boolean> {
const coreFrame = await getCoreFrame(this);
return coreFrame.isFocused(this);
},
};

const NodeListExtensionFns: IBaseExtendNodeList = {
Expand Down
29 changes: 29 additions & 0 deletions client/lib/FrameEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ISuperElement, ISuperNode, ISuperNodeList } from 'awaited-dom/base/inte
import AwaitedPath from 'awaited-dom/base/AwaitedPath';
import { IRequestInit } from 'awaited-dom/base/interfaces/official';
import SuperDocument from 'awaited-dom/impl/super-klasses/SuperDocument';
import XPathResult from 'awaited-dom/impl/official-klasses/XPathResult';
import Storage from 'awaited-dom/impl/official-klasses/Storage';
import CSSStyleDeclaration from 'awaited-dom/impl/official-klasses/CSSStyleDeclaration';
import {
Expand Down Expand Up @@ -219,6 +220,34 @@ export default class FrameEnvironment {
return createSuperNodeList(awaitedPath, awaitedOptions);
}

public xpathSelector(xpath: string, orderedNodeResults = false): ISuperNode {
return this.document.evaluate(
xpath,
this.document,
null,
orderedNodeResults
? XPathResult.FIRST_ORDERED_NODE_TYPE
: XPathResult.ANY_UNORDERED_NODE_TYPE,
).singleNodeValue;
}

public async xpathSelectorAll(xpath: string, orderedNodeResults = false): Promise<ISuperNode[]> {
const results = await this.document.evaluate(
xpath,
this.document,
null,
orderedNodeResults
? XPathResult.ORDERED_NODE_ITERATOR_TYPE
: XPathResult.UNORDERED_NODE_ITERATOR_TYPE,
);
const nodes: ISuperNode[] = [];
let node: ISuperNode;
while ((node = await results.iterateNext())) {
nodes.push(node);
}
return nodes;
}

public async waitForPaintingStable(options?: IWaitForOptions): Promise<void> {
const coreFrame = await this.#coreFramePromise;
await coreFrame.waitForLoad(LocationStatus.PaintingStable, options);
Expand Down
8 changes: 8 additions & 0 deletions client/lib/Hero.ts
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,14 @@ export default class Hero extends AwaitedEventTarget<{
return this.activeTab.querySelectorAll(selector);
}

public xpathSelector(xpath: string, orderedNodeResults = false): ISuperNode {
return this.activeTab.xpathSelector(xpath, orderedNodeResults);
}

public xpathSelectorAll(xpath: string, orderedNodeResults = false): Promise<ISuperNode[]> {
return this.activeTab.xpathSelectorAll(xpath, orderedNodeResults);
}

public takeScreenshot(options?: IScreenshotOptions): Promise<Buffer> {
return this.activeTab.takeScreenshot(options);
}
Expand Down
8 changes: 8 additions & 0 deletions client/lib/Tab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,14 @@ export default class Tab extends AwaitedEventTarget<IEventType> {
return this.mainFrameEnvironment.querySelectorAll(selector);
}

public xpathSelector(xpath: string, orderedNodeResults = false): ISuperNode {
return this.mainFrameEnvironment.xpathSelector(xpath, orderedNodeResults);
}

public xpathSelectorAll(xpath: string, orderedNodeResults = false): Promise<ISuperNode[]> {
return this.mainFrameEnvironment.xpathSelectorAll(xpath, orderedNodeResults);
}

public async takeScreenshot(options?: IScreenshotOptions): Promise<Buffer> {
const coreTab = await this.#coreTabPromise;
return coreTab.takeScreenshot(options);
Expand Down
4 changes: 4 additions & 0 deletions core/injected-scripts/jsPath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,10 @@ class ObjectAtPath {
return this;
}

public isFocused(): boolean {
return this.closestElement === document.activeElement;
}

public toReturnError(error: Error): IExecJsPathResult {
const pathError = <IJsPathError>{
error: String(error),
Expand Down
64 changes: 64 additions & 0 deletions fullstack/test/domExtenders.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Helpers } from '@ulixee/hero-testing';
import { ITestKoaServer } from '@ulixee/hero-testing/helpers';
import Hero from '../index';

let koaServer: ITestKoaServer;
beforeAll(async () => {
koaServer = await Helpers.runKoaServer();
});
afterAll(Helpers.afterAll);
afterEach(Helpers.afterEach);

describe('basic DomExtender tests', () => {
it('can run xpathSelector', async () => {
koaServer.get('/domextender-xpath', ctx => {
ctx.body = `
<body>
<h2>here</h2>
</body>
`;
});
const hero = await openBrowser(`/domextender-xpath`);

await expect(hero.xpathSelector('//h2').textContent).resolves.toBe('here');
});

it('can run xpathSelectorAll', async () => {
koaServer.get('/domextender-xpath-all', ctx => {
ctx.body = `
<body>
<h2>here 1</h2>
<h2>here 2</h2>
<h2>here 3</h2>
</body>
`;
});
const hero = await openBrowser(`/domextender-xpath-all`);

const xpathAll = await hero.xpathSelectorAll('//h2');

let counter = 0;
for (const entry of xpathAll) {
counter += 1;
await expect(entry.textContent).resolves.toBe(`here ${counter}`);
}
});

it('can find the focused field', async () => {
koaServer.get('/domextender-focused', ctx => {
ctx.body = `<body><input id="field" type="text"/></body>`;
});
const hero = await openBrowser(`/domextender-focused`);
await expect(hero.querySelector('#field').$hasFocus).resolves.toBe(false);
await expect(hero.querySelector('#field').$click()).resolves.toBe(undefined);
await expect(hero.querySelector('#field').$hasFocus).resolves.toBe(true);
});
});

async function openBrowser(path: string) {
const hero = new Hero();
Helpers.needsClosing.push(hero);
await hero.goto(`${koaServer.baseUrl}${path}`);
await hero.waitForPaintingStable();
return hero;
}
2 changes: 2 additions & 0 deletions interfaces/jsPathFnNames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ const getNodePointerFnName = '__getNodePointer__';
const getClientRectFnName = '__getClientRect__';
const getComputedVisibilityFnName = '__getComputedVisibility__';
const getComputedStyleFnName = '__getComputedStyle__';
const isFocusedFnName = '__isFocused__';
const getNodeIdFnName = '__getNodeId__';

export {
getNodePointerFnName,
getClientRectFnName,
getNodeIdFnName,
getComputedStyleFnName,
isFocusedFnName,
getComputedVisibilityFnName,
};
2 changes: 2 additions & 0 deletions puppet/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import PuppetChrome from '@ulixee/hero-puppet-chrome';
import { IBoundLog } from '@ulixee/commons/interfaces/ILog';
import Log from '@ulixee/commons/lib/Logger';
import { CanceledPromiseError } from '@ulixee/commons/interfaces/IPendingWaitEvent';
import IPuppetLauncher from '@ulixee/hero-interfaces/IPuppetLauncher';
import IPuppetBrowser from '@ulixee/hero-interfaces/IPuppetBrowser';
import IBrowserEngine from '@ulixee/hero-interfaces/IBrowserEngine';
Expand Down Expand Up @@ -103,6 +104,7 @@ export default class Puppet extends TypedEventEmitter<{ close: void }> {
proxy?: IProxyConnectionOptions,
isIncognito = true,
): Promise<IPuppetContext> {
if (!this.isReady) throw new CanceledPromiseError('This Puppet has been shut down');
await this.isReady.promise;
if (!this.browser) {
throw new Error('This Puppet instance has not had start() called on it');
Expand Down

0 comments on commit 0888c8b

Please sign in to comment.