Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
ulixee
GitHub Repository: ulixee/secret-agent
Path: blob/main/client/lib/Tab.ts
1028 views
1
import inspectInstanceProperties from 'awaited-dom/base/inspectInstanceProperties';
2
import StateMachine from 'awaited-dom/base/StateMachine';
3
import { ISuperElement } from 'awaited-dom/base/interfaces/super';
4
import { IRequestInit } from 'awaited-dom/base/interfaces/official';
5
import SuperDocument from 'awaited-dom/impl/super-klasses/SuperDocument';
6
import Storage from 'awaited-dom/impl/official-klasses/Storage';
7
import CSSStyleDeclaration from 'awaited-dom/impl/official-klasses/CSSStyleDeclaration';
8
import Request from 'awaited-dom/impl/official-klasses/Request';
9
import { ILocationTrigger, LocationStatus } from '@secret-agent/interfaces/Location';
10
import IWaitForResourceOptions from '@secret-agent/interfaces/IWaitForResourceOptions';
11
import IWaitForElementOptions from '@secret-agent/interfaces/IWaitForElementOptions';
12
import Response from 'awaited-dom/impl/official-klasses/Response';
13
import IWaitForOptions from '@secret-agent/interfaces/IWaitForOptions';
14
import {
15
IElementIsolate,
16
IHTMLFrameElementIsolate,
17
IHTMLIFrameElementIsolate,
18
IHTMLObjectElementIsolate,
19
INodeIsolate,
20
} from 'awaited-dom/base/interfaces/isolate';
21
import IScreenshotOptions from '@secret-agent/interfaces/IScreenshotOptions';
22
import AwaitedPath from 'awaited-dom/base/AwaitedPath';
23
import { INodeVisibility } from '@secret-agent/interfaces/INodeVisibility';
24
import * as Util from 'util';
25
import CoreTab from './CoreTab';
26
import Resource, { createResource } from './Resource';
27
import IWaitForResourceFilter from '../interfaces/IWaitForResourceFilter';
28
import WebsocketResource from './WebsocketResource';
29
import AwaitedEventTarget from './AwaitedEventTarget';
30
import CookieStorage from './CookieStorage';
31
import Agent, { IState as IAgentState } from './Agent';
32
import FrameEnvironment from './FrameEnvironment';
33
import CoreFrameEnvironment from './CoreFrameEnvironment';
34
import IAwaitedOptions from '../interfaces/IAwaitedOptions';
35
import Dialog from './Dialog';
36
import FileChooser from './FileChooser';
37
38
const awaitedPathState = StateMachine<
39
any,
40
{ awaitedPath: AwaitedPath; awaitedOptions: IAwaitedOptions }
41
>();
42
const { getState, setState } = StateMachine<Tab, IState>();
43
const agentState = StateMachine<Agent, IAgentState>();
44
45
export interface IState {
46
agent: Agent;
47
coreTab: Promise<CoreTab>;
48
mainFrameEnvironment: FrameEnvironment;
49
frameEnvironments: FrameEnvironment[];
50
}
51
52
interface IEventType {
53
resource: Resource | WebsocketResource;
54
dialog: Dialog;
55
}
56
57
const propertyKeys: (keyof Tab)[] = [
58
'lastCommandId',
59
'tabId',
60
'url',
61
'cookieStorage',
62
'localStorage',
63
'sessionStorage',
64
'document',
65
'frameEnvironments',
66
'mainFrameEnvironment',
67
'Request',
68
];
69
70
export default class Tab extends AwaitedEventTarget<IEventType> {
71
constructor(agent: Agent, coreTab: Promise<CoreTab>) {
72
super(() => {
73
return { target: coreTab };
74
});
75
const mainFrameEnvironment = new FrameEnvironment(
76
agent,
77
this,
78
coreTab.then(x => x?.mainFrameEnvironment),
79
);
80
setState(this, {
81
agent,
82
coreTab,
83
mainFrameEnvironment,
84
frameEnvironments: [mainFrameEnvironment],
85
});
86
87
async function sendToTab(pluginId: string, ...args: any[]): Promise<any> {
88
return (await coreTab).commandQueue.run('Tab.runPluginCommand', pluginId, args);
89
}
90
91
for (const clientPlugin of agentState.getState(agent).clientPlugins) {
92
if (clientPlugin.onTab) clientPlugin.onTab(agent, this, sendToTab);
93
}
94
}
95
96
public get tabId(): Promise<number> {
97
return getCoreTab(this).then(x => x.tabId);
98
}
99
100
public get lastCommandId(): Promise<number> {
101
return getCoreTab(this).then(x => x.commandQueue.lastCommandId);
102
}
103
104
public get url(): Promise<string> {
105
return this.mainFrameEnvironment.url;
106
}
107
108
public get mainFrameEnvironment(): FrameEnvironment {
109
return getState(this).mainFrameEnvironment;
110
}
111
112
public get cookieStorage(): CookieStorage {
113
return this.mainFrameEnvironment.cookieStorage;
114
}
115
116
public get frameEnvironments(): Promise<FrameEnvironment[]> {
117
return getRefreshedFrameEnvironments(this);
118
}
119
120
public get document(): SuperDocument {
121
return this.mainFrameEnvironment.document;
122
}
123
124
public get localStorage(): Storage {
125
return this.mainFrameEnvironment.localStorage;
126
}
127
128
public get sessionStorage(): Storage {
129
return this.mainFrameEnvironment.sessionStorage;
130
}
131
132
public get Request(): typeof Request {
133
return this.mainFrameEnvironment.Request;
134
}
135
136
// METHODS
137
138
public async fetch(request: Request | string, init?: IRequestInit): Promise<Response> {
139
return await this.mainFrameEnvironment.fetch(request, init);
140
}
141
142
public async getFrameEnvironment(
143
element: IHTMLFrameElementIsolate | IHTMLIFrameElementIsolate | IHTMLObjectElementIsolate,
144
): Promise<FrameEnvironment | null> {
145
const { awaitedPath, awaitedOptions } = awaitedPathState.getState(element);
146
const elementCoreFrame = await awaitedOptions.coreFrame;
147
const frameMeta = await elementCoreFrame.getChildFrameEnvironment(awaitedPath.toJSON());
148
if (!frameMeta) return null;
149
150
const coreTab = await getCoreTab(this);
151
return await getOrCreateFrameEnvironment(this, coreTab.getCoreFrameForMeta(frameMeta));
152
}
153
154
public getComputedStyle(element: IElementIsolate, pseudoElement?: string): CSSStyleDeclaration {
155
return FrameEnvironment.getComputedStyle(element, pseudoElement);
156
}
157
158
public async goto(href: string, timeoutMs?: number): Promise<Resource> {
159
const coreTab = await getCoreTab(this);
160
const resource = await coreTab.goto(href, timeoutMs);
161
return createResource(Promise.resolve(coreTab), resource);
162
}
163
164
public async goBack(timeoutMs?: number): Promise<string> {
165
const coreTab = await getCoreTab(this);
166
return coreTab.goBack(timeoutMs);
167
}
168
169
public async goForward(timeoutMs?: number): Promise<string> {
170
const coreTab = await getCoreTab(this);
171
return coreTab.goForward(timeoutMs);
172
}
173
174
public async reload(timeoutMs?: number): Promise<Resource> {
175
const coreTab = await getCoreTab(this);
176
const resource = await coreTab.reload(timeoutMs);
177
return createResource(Promise.resolve(coreTab), resource);
178
}
179
180
public async getJsValue<T>(path: string): Promise<T> {
181
return await this.mainFrameEnvironment.getJsValue(path);
182
}
183
184
// @deprecated 2021-04-30: Replaced with getComputedVisibility
185
public async isElementVisible(element: IElementIsolate): Promise<boolean> {
186
return await this.getComputedVisibility(element as any).then(x => x.isVisible);
187
}
188
189
public async getComputedVisibility(node: INodeIsolate): Promise<INodeVisibility> {
190
return await FrameEnvironment.getComputedVisibility(node);
191
}
192
193
public async takeScreenshot(options?: IScreenshotOptions): Promise<Buffer> {
194
const coreTab = await getCoreTab(this);
195
return coreTab.takeScreenshot(options);
196
}
197
198
public async waitForFileChooser(options?: IWaitForOptions): Promise<FileChooser> {
199
const coreTab = await getCoreTab(this);
200
const prompt = await coreTab.waitForFileChooser(options);
201
const coreFrame = coreTab.frameEnvironmentsById.get(prompt.frameId);
202
return new FileChooser(Promise.resolve(coreFrame), prompt);
203
}
204
205
public async waitForPaintingStable(options?: IWaitForOptions): Promise<void> {
206
return await this.mainFrameEnvironment.waitForPaintingStable(options);
207
}
208
209
public async waitForLoad(status: LocationStatus, options?: IWaitForOptions): Promise<void> {
210
return await this.mainFrameEnvironment.waitForLoad(status, options);
211
}
212
213
public waitForResource(
214
filter: IWaitForResourceFilter,
215
options?: IWaitForResourceOptions,
216
): Promise<(Resource | WebsocketResource)[]> {
217
return Resource.waitFor(this, filter, options);
218
}
219
220
public async waitForElement(
221
element: ISuperElement,
222
options?: IWaitForElementOptions,
223
): Promise<void> {
224
return await this.mainFrameEnvironment.waitForElement(element, options);
225
}
226
227
public async waitForLocation(
228
trigger: ILocationTrigger,
229
options?: IWaitForOptions,
230
): Promise<Resource> {
231
return await this.mainFrameEnvironment.waitForLocation(trigger, options);
232
}
233
234
public async waitForMillis(millis: number): Promise<void> {
235
const coreTab = await getCoreTab(this);
236
await coreTab.waitForMillis(millis);
237
}
238
239
public focus(): Promise<void> {
240
const { agent, coreTab } = getState(this);
241
agentState.getState(agent).connection.activeTab = this;
242
return coreTab.then(x => x.focusTab());
243
}
244
245
public close(): Promise<void> {
246
const { agent, coreTab } = getState(this);
247
const { connection } = agentState.getState(agent);
248
connection.closeTab(this);
249
return coreTab.then(x => x.close());
250
}
251
252
public toJSON(): any {
253
// return empty so we can avoid infinite "stringifying" in jest
254
return {
255
type: this.constructor.name,
256
};
257
}
258
259
public [Util.inspect.custom](): any {
260
return inspectInstanceProperties(this, propertyKeys as any);
261
}
262
}
263
264
async function getOrCreateFrameEnvironment(
265
tab: Tab,
266
coreFrame: CoreFrameEnvironment,
267
): Promise<FrameEnvironment> {
268
const state = getState(tab);
269
const { frameEnvironments } = state;
270
271
for (const frameEnvironment of frameEnvironments) {
272
const frameId = await frameEnvironment.frameId;
273
if (frameId === coreFrame.frameId) return frameEnvironment;
274
}
275
const frameEnvironment = new FrameEnvironment(state.agent, tab, Promise.resolve(coreFrame));
276
frameEnvironments.push(frameEnvironment);
277
return frameEnvironment;
278
}
279
280
async function getRefreshedFrameEnvironments(tab: Tab): Promise<FrameEnvironment[]> {
281
const state = getState(tab);
282
const { frameEnvironments } = state;
283
const coreTab = await state.coreTab;
284
const coreFrames = await coreTab.getCoreFrameEnvironments();
285
286
const newFrameIds = coreFrames.map(x => x.frameId);
287
288
for (const frameEnvironment of frameEnvironments) {
289
const id = await frameEnvironment.frameId;
290
// remove frames that are gone
291
if (!newFrameIds.includes(id)) {
292
const idx = frameEnvironments.indexOf(frameEnvironment);
293
frameEnvironments.splice(idx, 1);
294
}
295
}
296
297
await Promise.all(coreFrames.map(x => getOrCreateFrameEnvironment(tab, x)));
298
299
return frameEnvironments;
300
}
301
302
export function getCoreTab(tab: Tab): Promise<CoreTab> {
303
return getState(tab).coreTab.then(x => {
304
if (x instanceof Error) throw x;
305
return x;
306
});
307
}
308
309
// CREATE
310
311
export function createTab(agent: Agent, coreTab: Promise<CoreTab>): Tab {
312
return new Tab(agent, coreTab);
313
}
314
315