Skip to content

Commit

Permalink
feat(core): magic selectors
Browse files Browse the repository at this point in the history
  • Loading branch information
blakebyrnes committed Dec 20, 2021
1 parent 88661a2 commit f79170b
Showing 16 changed files with 383 additions and 47 deletions.
17 changes: 16 additions & 1 deletion client/lib/CoreFrameEnvironment.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
import { IInteractionGroups } from '@ulixee/hero-interfaces/IInteractions';
import ISessionMeta from '@ulixee/hero-interfaces/ISessionMeta';
import { ILoadStatus, ILocationTrigger } from '@ulixee/hero-interfaces/Location';
import { IJsPath } from 'awaited-dom/base/AwaitedPath';
import AwaitedPath, { IJsPath } from 'awaited-dom/base/AwaitedPath';
import { ICookie } from '@ulixee/hero-interfaces/ICookie';
import IWaitForElementOptions from '@ulixee/hero-interfaces/IWaitForElementOptions';
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 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 { 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';

const awaitedPathState = StateMachine<
any,
{ awaitedPath: AwaitedPath; awaitedOptions: IAwaitedOptions; nodePointer?: INodePointer }
>();

export default class CoreFrameEnvironment {
public tabId: number;
@@ -91,6 +102,10 @@ export default class CoreFrameEnvironment {
await this.commandQueue.run('FrameEnvironment.interact', ...interactionGroups);
}

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

public async getCookies(): Promise<ICookie[]> {
return await this.commandQueue.run('FrameEnvironment.getCookies');
}
16 changes: 12 additions & 4 deletions client/lib/DomExtender.ts
Original file line number Diff line number Diff line change
@@ -6,6 +6,8 @@ import SuperNode from 'awaited-dom/impl/super-klasses/SuperNode';
import SuperHTMLElement from 'awaited-dom/impl/super-klasses/SuperHTMLElement';
import { ITypeInteraction } from '../interfaces/IInteractions';
import Interactor from './Interactor';
import { INodeVisibility } from '@ulixee/hero-interfaces/INodeVisibility';
import CoreFrameEnvironment from './CoreFrameEnvironment';

const { getState } = StateMachine<ISuperElement, ISuperElementProperties>();

@@ -14,6 +16,7 @@ interface IBaseExtend {
click: () => Promise<void>;
type: (...typeInteractions: ITypeInteraction[]) => Promise<void>;
waitForVisible: () => Promise<void>;
getComputedVisibility: () => Promise<INodeVisibility>;
};
}

@@ -28,12 +31,12 @@ for (const Super of [SuperElement, SuperNode, SuperHTMLElement]) {
get: function $() {
const click = async (): Promise<void> => {
const { awaitedOptions } = getState(this);
const coreFrame = await awaitedOptions?.coreFrame;
const coreFrame: CoreFrameEnvironment = await awaitedOptions?.coreFrame;
await Interactor.run(coreFrame, [{ click: this }]);
};
const type = async (...typeInteractions: ITypeInteraction[]): Promise<void> => {
const { awaitedOptions } = getState(this);
const coreFrame = await awaitedOptions?.coreFrame;
const coreFrame: CoreFrameEnvironment = await awaitedOptions?.coreFrame;
await click();
await Interactor.run(
coreFrame,
@@ -42,11 +45,16 @@ for (const Super of [SuperElement, SuperNode, SuperHTMLElement]) {
};
const waitForVisible = async (): Promise<void> => {
const { awaitedPath, awaitedOptions } = getState(this);
const coreFrame = await awaitedOptions?.coreFrame;
const coreFrame: CoreFrameEnvironment = await awaitedOptions?.coreFrame;
await coreFrame.waitForElement(awaitedPath.toJSON(), { waitForVisible: true });
};
const getComputedVisibility = async (): Promise<void> => {
const { awaitedOptions } = getState(this);
const coreFrame: CoreFrameEnvironment = await awaitedOptions?.coreFrame;
await coreFrame.getComputedVisibility(this);
};

return { click, type, waitForVisible };
return { click, type, waitForVisible, getComputedVisibility };
},
});
}
60 changes: 34 additions & 26 deletions client/lib/FrameEnvironment.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import inspectInstanceProperties from 'awaited-dom/base/inspectInstanceProperties';
import * as Util from 'util';
import StateMachine from 'awaited-dom/base/StateMachine';
import { ISuperElement } from 'awaited-dom/base/interfaces/super';
import { ISuperElement, ISuperNode, ISuperNodeList } from 'awaited-dom/base/interfaces/super';
import AwaitedPath from 'awaited-dom/base/AwaitedPath';
import { IRequestInit } from 'awaited-dom/base/interfaces/official';
import SuperDocument from 'awaited-dom/impl/super-klasses/SuperDocument';
@@ -12,6 +12,8 @@ import {
createResponse,
createStorage,
createSuperDocument,
createSuperNode,
createSuperNodeList,
} from 'awaited-dom/impl/create';
import Request from 'awaited-dom/impl/official-klasses/Request';
import { ILoadStatus, ILocationTrigger, LocationStatus } from '@ulixee/hero-interfaces/Location';
@@ -26,21 +28,21 @@ import {
INodeIsolate,
} from 'awaited-dom/base/interfaces/isolate';
import { INodeVisibility } from '@ulixee/hero-interfaces/INodeVisibility';
import { getComputedVisibilityFnName } from '@ulixee/hero-interfaces/jsPathFnNames';
import { INodePointer } from '@ulixee/hero-interfaces/AwaitedDom';
import IAwaitedOptions from '../interfaces/IAwaitedOptions';
import RequestGenerator, { getRequestIdOrUrl } from './Request';
import CookieStorage, { createCookieStorage } from './CookieStorage';
import Hero, { IState as IHeroState } from './Hero';
import {
createHeroCommandAwaitedPath,
createInstanceWithNodePointer,
delegate as AwaitedHandler,
getAwaitedPathAsMethodArg,
} from './SetupAwaitedHandler';
import CoreFrameEnvironment from './CoreFrameEnvironment';
import Tab, { IState as ITabState } from './Tab';
import { IMousePosition } from '../interfaces/IInteractions';
import Resource, { createResource } from './Resource';
import IMagicSelectorOptions from '@ulixee/hero-interfaces/IMagicSelectorOptions';

const { getState, setState } = StateMachine<FrameEnvironment, IState>();
const { getState: getTabState } = StateMachine<Tab, ITabState>();
@@ -187,11 +189,22 @@ export default class FrameEnvironment {
}

public getComputedStyle(element: IElementIsolate, pseudoElement?: string): CSSStyleDeclaration {
return FrameEnvironment.getComputedStyle(element, pseudoElement);
const { awaitedPath: elementAwaitedPath, awaitedOptions } = awaitedPathState.getState(element);
const awaitedPath = new AwaitedPath(null, 'window', [
'getComputedStyle',
getAwaitedPathAsMethodArg(elementAwaitedPath),
pseudoElement,
]);
return createCSSStyleDeclaration<IAwaitedOptions>(
awaitedPath,
awaitedOptions,
) as CSSStyleDeclaration;
}

public async getComputedVisibility(node: INodeIsolate): Promise<INodeVisibility> {
return await FrameEnvironment.getComputedVisibility(node);
if (!node) return { isVisible: false, nodeExists: false };
const coreFrame = await getCoreFrameEnvironment(this);
return await coreFrame.getComputedVisibility(node);
}

// @deprecated 2021-04-30: Replaced with getComputedVisibility
@@ -204,6 +217,22 @@ export default class FrameEnvironment {
return coreFrame.getJsValue<T>(path);
}

public magicSelector(selectorOrOptions?: string | IMagicSelectorOptions): ISuperNode {
const awaitedPath = createHeroCommandAwaitedPath('FrameEnvironment.magicSelector', [
selectorOrOptions,
]);
const awaitedOptions: IAwaitedOptions = { coreFrame: getState(this).coreFrame };
return createSuperNode(awaitedPath, awaitedOptions);
}

public magicSelectorAll(selectorOrOptions?: string | IMagicSelectorOptions): ISuperNodeList {
const awaitedPath = createHeroCommandAwaitedPath('FrameEnvironment.magicSelectorAll', [
selectorOrOptions,
]);
const awaitedOptions: IAwaitedOptions = { coreFrame: getState(this).coreFrame };
return createSuperNodeList(awaitedPath, awaitedOptions);
}

public async waitForPaintingStable(options?: IWaitForOptions): Promise<void> {
const coreFrame = await getCoreFrameEnvironment(this);
await coreFrame.waitForLoad(LocationStatus.PaintingStable, options);
@@ -252,27 +281,6 @@ export default class FrameEnvironment {
public [Util.inspect.custom](): any {
return inspectInstanceProperties(this, propertyKeys as any);
}

public static getComputedStyle(
element: IElementIsolate,
pseudoElement?: string,
): CSSStyleDeclaration {
const { awaitedPath: elementAwaitedPath, awaitedOptions } = awaitedPathState.getState(element);
const awaitedPath = new AwaitedPath(null, 'window', [
'getComputedStyle',
getAwaitedPathAsMethodArg(elementAwaitedPath),
pseudoElement,
]);
return createCSSStyleDeclaration<IAwaitedOptions>(
awaitedPath,
awaitedOptions,
) as CSSStyleDeclaration;
}

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

export function getFrameState(object: any): IState {
11 changes: 10 additions & 1 deletion client/lib/Hero.ts
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@ import IDomStorage from '@ulixee/hero-interfaces/IDomStorage';
import IUserProfile from '@ulixee/hero-interfaces/IUserProfile';
import { IRequestInit } from 'awaited-dom/base/interfaces/official';
import Response from 'awaited-dom/impl/official-klasses/Response';
import { ISuperElement } from 'awaited-dom/base/interfaces/super';
import { ISuperElement, ISuperNode, ISuperNodeList } from 'awaited-dom/base/interfaces/super';
import IWaitForResourceOptions from '@ulixee/hero-interfaces/IWaitForResourceOptions';
import IWaitForElementOptions from '@ulixee/hero-interfaces/IWaitForElementOptions';
import { ILocationTrigger } from '@ulixee/hero-interfaces/Location';
@@ -59,6 +59,7 @@ import CoreFrameEnvironment from './CoreFrameEnvironment';
import ConnectionManager from './ConnectionManager';
import './DomExtender';
import IPageStateDefinitions from '../interfaces/IPageStateDefinitions';
import IMagicSelectorOptions from '@ulixee/hero-interfaces/IMagicSelectorOptions';

export const DefaultOptions = {
defaultBlockedResourceTypes: [BlockedResourceType.None],
@@ -380,6 +381,14 @@ export default class Hero extends AwaitedEventTarget<{
return await this.getComputedVisibility(element as any).then(x => x.isVisible);
}

public magicSelector(selectorOrOptions?: string | IMagicSelectorOptions): ISuperNode {
return this.activeTab.magicSelector(selectorOrOptions);
}

public magicSelectorAll(selectorOrOptions?: string | IMagicSelectorOptions): ISuperNodeList {
return this.activeTab.magicSelectorAll(selectorOrOptions);
}

public takeScreenshot(options?: IScreenshotOptions): Promise<Buffer> {
return this.activeTab.takeScreenshot(options);
}
4 changes: 4 additions & 0 deletions client/lib/SetupAwaitedHandler.ts
Original file line number Diff line number Diff line change
@@ -103,6 +103,10 @@ export async function getAwaitedState<TClass>(
return { awaitedPath, coreFrame: awaitedCoreFrame, awaitedOptions };
}

export function createHeroCommandAwaitedPath(command: string, args: any[]): AwaitedPath {
return new AwaitedPath(null, 'Hero', [command, args]);
}

export function getAwaitedPathAsMethodArg(awaitedPath: AwaitedPath): string {
return `$$jsPath=${JSON.stringify(awaitedPath.toJSON())}`;
}
15 changes: 12 additions & 3 deletions client/lib/Tab.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import inspectInstanceProperties from 'awaited-dom/base/inspectInstanceProperties';
import StateMachine from 'awaited-dom/base/StateMachine';
import { ISuperElement } from 'awaited-dom/base/interfaces/super';
import { ISuperElement, ISuperNode, ISuperNodeList } from 'awaited-dom/base/interfaces/super';
import { IRequestInit } from 'awaited-dom/base/interfaces/official';
import SuperDocument from 'awaited-dom/impl/super-klasses/SuperDocument';
import Storage from 'awaited-dom/impl/official-klasses/Storage';
@@ -36,6 +36,7 @@ import Dialog from './Dialog';
import FileChooser from './FileChooser';
import PageState from './PageState';
import IPageStateDefinitions from '../interfaces/IPageStateDefinitions';
import IMagicSelectorOptions from '@ulixee/hero-interfaces/IMagicSelectorOptions';

const awaitedPathState = StateMachine<
any,
@@ -169,7 +170,7 @@ export default class Tab extends AwaitedEventTarget<IEventType> {
}

public getComputedStyle(element: IElementIsolate, pseudoElement?: string): CSSStyleDeclaration {
return FrameEnvironment.getComputedStyle(element, pseudoElement);
return this.mainFrameEnvironment.getComputedStyle(element, pseudoElement);
}

public async goto(href: string, timeoutMs?: number): Promise<Resource> {
@@ -204,7 +205,15 @@ export default class Tab extends AwaitedEventTarget<IEventType> {
}

public async getComputedVisibility(node: INodeIsolate): Promise<INodeVisibility> {
return await FrameEnvironment.getComputedVisibility(node);
return await this.mainFrameEnvironment.getComputedVisibility(node);
}

public magicSelector(selectorOrOptions?: string | IMagicSelectorOptions): ISuperNode {
return this.mainFrameEnvironment.magicSelector(selectorOrOptions);
}

public magicSelectorAll(selectorOrOptions?: string | IMagicSelectorOptions): ISuperNodeList {
return this.mainFrameEnvironment.magicSelectorAll(selectorOrOptions);
}

public async takeScreenshot(options?: IScreenshotOptions): Promise<Buffer> {
60 changes: 60 additions & 0 deletions core/injected-scripts/jsPath.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import IExecJsPathResult from '@ulixee/hero-interfaces/IExecJsPathResult';
import type INodePointer from 'awaited-dom/base/INodePointer';
import IElementRect from '@ulixee/hero-interfaces/IElementRect';
import IMagicSelectorOptions from '@ulixee/hero-interfaces/IMagicSelectorOptions';
import IPoint from '@ulixee/hero-interfaces/IPoint';
import { IJsPathError } from '@ulixee/hero-interfaces/IJsPathError';
import { INodeVisibility } from '@ulixee/hero-interfaces/INodeVisibility';
@@ -225,6 +226,7 @@ class ObjectAtPath {

private _obstructedByElement: Element;
private lookupStep: IPathStep;
private lookupStepMagicSelectorMatches: number[];
private lookupStepIndex = 0;
private nodePointer: INodePointer;

@@ -408,6 +410,61 @@ class ObjectAtPath {
return this;
}

public magicSelector(options: IMagicSelectorOptions): Node {
const { querySelectors, minMatchingSelectors } = options;
const results: number[] = [];
this.lookupStepMagicSelectorMatches = results;
const candidates = new Map<Node, number>();
for (const selector of querySelectors) {
try {
const result = document.querySelectorAll(selector);
results.push(result.length);
if (result.length === 1) {
const node = result[0];
const count = (candidates.get(node) ?? 0) + 1;
candidates.set(node, count);
if (count >= minMatchingSelectors) {
return node;
}
}
} catch (err) {
results.push(-1);
}
}
}

public magicSelectorAll(options: IMagicSelectorOptions): NodeList {
const { querySelectors, minMatchingSelectors } = options;
const results: number[] = [];
this.lookupStepMagicSelectorMatches = results;
const countByStringifiedNodeIds = new Map<string, number>();
for (const selector of querySelectors) {
try {
const result = document.querySelectorAll(selector);
results.push(result.length);
const nodeIds: number[] = [];
for (const node of result) {
const nodeId = NodeTracker.watchNode(node);
nodeIds.push(nodeId);
}

const stringifiedIds = nodeIds.toString();
const count = (countByStringifiedNodeIds.get(stringifiedIds) ?? 0) + 1;
countByStringifiedNodeIds.set(stringifiedIds, count);
if (count >= minMatchingSelectors) {
return result;
}
} catch (err) {
results.push(-1);
}
}
// create an empty node list if we didn't match anything
const emptyResult = document.querySelectorAll('Hero.Empty');
if (emptyResult.length !== 0)
throw new Error('MagicSelectorAllFailed. Could not create falsified empty resultset');
return emptyResult;
}

public toReturnError(error: Error): IExecJsPathResult {
const pathError = <IJsPathError>{
error: String(error),
@@ -416,6 +473,9 @@ class ObjectAtPath {
index: this.lookupStepIndex,
},
};
if (this.lookupStepMagicSelectorMatches) {
pathError.pathState.magicSelectorMatches = this.lookupStepMagicSelectorMatches;
}
return {
value: null,
pathError,
Loading

0 comments on commit f79170b

Please sign in to comment.