Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
ulixee
GitHub Repository: ulixee/secret-agent
Path: blob/main/core/lib/JsPath.ts
1029 views
1
import { IJsPath } from 'awaited-dom/base/AwaitedPath';
2
import IExecJsPathResult from '@secret-agent/interfaces/IExecJsPathResult';
3
import IWindowOffset from '@secret-agent/interfaces/IWindowOffset';
4
import TypeSerializer from '@secret-agent/commons/TypeSerializer';
5
import { IBoundLog } from '@secret-agent/interfaces/ILog';
6
import Log from '@secret-agent/commons/Logger';
7
import { INodeVisibility } from '@secret-agent/interfaces/INodeVisibility';
8
import INodePointer from 'awaited-dom/base/INodePointer';
9
import IJsPathResult from '@secret-agent/interfaces/IJsPathResult';
10
import IPoint from '@secret-agent/interfaces/IPoint';
11
import FrameEnvironment from './FrameEnvironment';
12
import InjectedScripts from './InjectedScripts';
13
import { Serializable } from '../interfaces/ISerializable';
14
import InjectedScriptError from './InjectedScriptError';
15
16
const { log } = Log(module);
17
18
export class JsPath {
19
public hasNewExecJsPathHistory = false;
20
public readonly execHistory: IJsPathHistory[] = [];
21
22
private readonly frameEnvironment: FrameEnvironment;
23
private readonly logger: IBoundLog;
24
private readonly recordJsPaths: boolean = false;
25
26
private nodeIdToHistoryLocation = new Map<
27
number,
28
{ sourceIndex: number; isFromIterable: boolean }
29
>();
30
31
constructor(frameEnvironment: FrameEnvironment, recordJsPaths: boolean) {
32
this.frameEnvironment = frameEnvironment;
33
this.recordJsPaths = recordJsPaths;
34
this.logger = log.createChild(module, {
35
sessionId: frameEnvironment.session.id,
36
frameId: frameEnvironment.id,
37
});
38
}
39
40
public exec<T>(jsPath: IJsPath, containerOffset: IPoint): Promise<IExecJsPathResult<T>> {
41
return this.runJsPath<T>(`exec`, jsPath, containerOffset);
42
}
43
44
public waitForElement(
45
jsPath: IJsPath,
46
containerOffset: IPoint,
47
waitForVisible: boolean,
48
timeoutMillis: number,
49
): Promise<IExecJsPathResult<INodeVisibility>> {
50
return this.runJsPath<INodeVisibility>(
51
`waitForElement`,
52
jsPath,
53
containerOffset,
54
waitForVisible,
55
timeoutMillis,
56
);
57
}
58
59
public simulateOptionClick(jsPath: IJsPath): Promise<IExecJsPathResult<boolean>> {
60
return this.runJsPath(`simulateOptionClick`, jsPath);
61
}
62
63
public getWindowOffset(): Promise<IWindowOffset> {
64
return this.frameEnvironment.runIsolatedFn(`${InjectedScripts.JsPath}.getWindowOffset`);
65
}
66
67
public waitForScrollOffset(
68
scrollX: number,
69
scrollY: number,
70
timeoutMillis = 2e3,
71
): Promise<boolean> {
72
return this.frameEnvironment.runIsolatedFn(
73
`${InjectedScripts.JsPath}.waitForScrollOffset`,
74
[scrollX, scrollY],
75
timeoutMillis,
76
);
77
}
78
79
public async runJsPaths(
80
jsPaths: IJsPathHistory[],
81
containerOffset: IPoint,
82
): Promise<IJsPathResult[]> {
83
if (!jsPaths?.length) return [];
84
85
const results = await this.frameEnvironment.runIsolatedFn<IJsPathResult[]>(
86
`${InjectedScripts.JsPath}.execJsPaths`,
87
jsPaths as any,
88
containerOffset,
89
);
90
91
for (const { result, jsPath } of results) {
92
if (result?.isValueSerialized === true) {
93
result.isValueSerialized = undefined;
94
result.value = TypeSerializer.revive(result.value, 'BROWSER');
95
}
96
this.recordExecResult(jsPath, result, false);
97
}
98
return results;
99
}
100
101
private async runJsPath<T>(
102
fnName: string,
103
jsPath: IJsPath,
104
...args: Serializable[]
105
): Promise<IExecJsPathResult<T>> {
106
const result = await this.frameEnvironment.runIsolatedFn<IExecJsPathResult<T>>(
107
`${InjectedScripts.JsPath}.${fnName}`,
108
jsPath,
109
...args,
110
);
111
if (result.pathError) {
112
throw new InjectedScriptError(result.pathError.error, result.pathError.pathState);
113
} else if (result?.isValueSerialized === true) {
114
result.isValueSerialized = undefined;
115
result.value = TypeSerializer.revive(result.value, 'BROWSER');
116
}
117
118
if (this.recordJsPaths && fnName === 'exec') {
119
this.recordExecResult(jsPath, result);
120
}
121
return result;
122
}
123
124
private recordExecResult(
125
jsPath: IJsPath,
126
result: IExecJsPathResult<any>,
127
isLiveQuery = true,
128
): void {
129
let sourceIndex: number;
130
if (isLiveQuery) this.hasNewExecJsPathHistory = true;
131
// if jspath starts with an id, this is a nested query
132
if (typeof jsPath[0] === 'number') {
133
const id = jsPath[0];
134
const queryIndex = this.nodeIdToHistoryLocation.get(id);
135
const operator = queryIndex.isFromIterable ? '*.' : '.';
136
const plan = <IJsPathHistory>{
137
jsPath: [operator, ...jsPath.slice(1)],
138
sourceIndex: queryIndex.sourceIndex,
139
};
140
const stringified = JSON.stringify(plan.jsPath);
141
const match = this.execHistory.find(
142
x => JSON.stringify(x.jsPath) === stringified && x.sourceIndex === queryIndex.sourceIndex,
143
);
144
if (match) {
145
sourceIndex = this.execHistory.indexOf(match);
146
} else {
147
this.execHistory.push(plan);
148
sourceIndex = this.execHistory.length - 1;
149
}
150
} else {
151
this.execHistory.push({
152
jsPath,
153
});
154
sourceIndex = this.execHistory.length - 1;
155
}
156
157
if (result.nodePointer) {
158
const { id, iterableItems, iterableIsState } = result.nodePointer;
159
160
const queryIndex = this.nodeIdToHistoryLocation.get(id);
161
if (!queryIndex) {
162
this.nodeIdToHistoryLocation.set(id, {
163
isFromIterable: false,
164
sourceIndex,
165
});
166
}
167
if (iterableIsState) {
168
for (const nodePointer of iterableItems as INodePointer[]) {
169
this.nodeIdToHistoryLocation.set(nodePointer.id, {
170
isFromIterable: true,
171
sourceIndex,
172
});
173
}
174
}
175
}
176
}
177
}
178
179
export interface IJsPathHistory {
180
jsPath: IJsPath;
181
sourceIndex?: number;
182
}
183
184