New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

auto-chrome

Package Overview
Dependencies
Maintainers
1
Versions
83
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

auto-chrome - npm Package Compare versions

Comparing version 0.1.0 to 0.2.0

lib/Event.js

9

index.js

@@ -6,7 +6,7 @@ const childProcess = require('child_process')

const helper = require('./lib/helper')
const { signale, promise } = helper
const { signale, zPromise } = helper
async function index(options) {
let { executablePath, args = [], ignoreHTTPSErrors, emulate } = options
let { args = [], executablePath, ignoreHTTPSErrors, emulate } = options

@@ -45,3 +45,3 @@ args.push("--remote-debugging-port=9222")

let linePromise = promise(30000)
let linePromise = new zPromise({ time: 30000 })

@@ -55,2 +55,3 @@ rl.on('line', function (data) {

// 获取webSocket连接地址
let webSocketDebuggerUrl = await linePromise.catch(function (error) {

@@ -62,3 +63,3 @@ throw error

let awaitOpen = promise()
let awaitOpen = new zPromise()

@@ -65,0 +66,0 @@ ws.on('open', awaitOpen.resolve);

@@ -1,15 +0,9 @@

const EventEmitter = require('events');
const debug = require('debug');
const Page = require('./Page');
const { signale, promise } = require('./helper');
const Event = require('./Event');
const { signale, zPromise } = require('./helper');
const debugSend = debug('chrome:send');
const debugMessage = debug('chrome:message');
const debugMessageNoId = debug('chrome:message:no id');
class Chrome extends EventEmitter {
class Chrome extends Event {
/**
*
* @param {*} ws WebSocket实例
* @param {*} ignoreHTTPSErrors 是否忽略https错误
* @param {Object} ws WebSocket实例
* @param {Boolean} ignoreHTTPSErrors 是否忽略https错误
*/

@@ -22,13 +16,5 @@ constructor(ws, ignoreHTTPSErrors = true, emulate = {}) {

this.emulate = emulate
this.id = 1 // 消息自增id
this.callbacks = new Map() // WebSocket异步消息队列
this.sessionCallbacks = new Map() // 带session的异步消息队列
this.targets = new Map() // target信息列表
this.pages = new Map() // page信息列表
this.page = undefined // 当前活跃状态的page
}
/**
* 创建消息实例
*/
async run() {

@@ -38,159 +24,10 @@

// 首次加载时等待标签就绪
let waitTargetCreated = promise()
// 启用Target监听(全局)
// 启用全局Target事件监听
this.send('Target.setDiscoverTargets', { discover: true });
this.on('Target.targetCreated', async ({ targetInfo }) => {
let { type, targetId } = targetInfo
// 只有第首次打开标签时type为page,访问内页和加载框架时均为iframe类型
if (type === 'page') {
// 为每个target绑定session
let { sessionId } = await this.send('Target.attachToTarget', { targetId })
let callbacks = this.sessionCallbacks
this.page = new Page(this.send.bind(this), { ...targetInfo, callbacks, sessionId })
let page = this.page
// 使用默认仿真配置
if (this.emulate) {
await page.emulate(this.emulate)
}
// 通过chrome.newPage()创建的target需要触发Promise结束等待
let newPage = this.pages.get(targetId)
if (newPage) {
newPage.resolve(page)
}
this.pages.set(targetId, page)
this.keyboard = page.keyboard
this.mouse = page.mouse
this.touch = page.touch
let { frameTree } = await page.send('Page.getFrameTree')
page.mainFrame = frameTree.frame
// page.send('Network.enable')
if (this.ignoreHTTPSErrors) {
page.send('Security.enable')
}
page.send('Page.enable')
page.send('Runtime.enable')
waitTargetCreated.resolve()
}
});
this.on('Target.targetInfoChanged', async function ({ targetInfo }) {
if (targetInfo.type === 'page') {
if (this.page) {
let { frameTree } = await this.page.send('Page.getFrameTree')
if (frameTree) {
this.page.mainFrame = frameTree.frame
}
}
}
});
this.on('Target.detachedFromTarget', ({ targetId }) => {
// 删除标签后将最后一个标签置于活跃状态
if (this.pages.delete(targetId)) {
let tid
let keys = this.pages.keys()
for (let id of keys) { tid = id }
this.page = this.pages.get(tid)
if (this.page) {
this.page.send('Page.bringToFront')
}
}
});
this.on('Target.targetCrashed', ({ targetId }) => {
this.pages.delete(targetId)
});
// 等待this.page初始标签准备就绪
await waitTargetCreated
await this.waitTargetCreated
}
/**
* 发送消息,不包含session
* @param {String} method 方法名
* @param {Object} params 参数
*/
send(method = '', params = {}) {
let id = this.id++
let message = JSON.stringify({ id, method, params });
debugSend(id, method, message)
this.ws.send(message);
return new Promise((resolve, reject) => {
this.callbacks.set(id, { resolve, reject, method, error: new Error() });
}).catch(error => {
signale.error(`mid:${id},${error.message}`)
})
}
/**
* 接收消息(异步)
*/
onMessage(data) {
const object = JSON.parse(data);
// 带id为主动消息
if (object.id) {
debugMessage(object.id, object.result)
const callback = this.callbacks.get(object.id);
if (callback) {
this.callbacks.delete(object.id);
if (object.error) {
callback.reject(object.error)
} else {
callback.resolve(object.result);
}
}
}
// 不带id为被动消息
else {
debugMessageNoId(object.method, object.params)
if (object.method === 'Target.receivedMessageFromTarget') {
this.page.emit(object.method, object.params)
} else {
this.emit(object.method, object.params)
}
}
}
/**
* 获取Chrome版本信息

@@ -205,13 +42,18 @@ */

* 创建新标签,如果返回空值表示创建失败
* @param {*} url 新标签的初始url
* @param {String} url 新标签的初始url
*/
async newPage(url = 'chrome://newtab/') {
async newPage(url = 'about:blank') {
let { targetId } = await this.send('Target.createTarget', { url })
// 通过Target.targetCreated事件解除Promise等待
return await new Promise((resolve, reject) => {
this.pages.set(targetId, { resolve, reject })
let awaitLoading = new zPromise({ time: 8000, message: '网页加载超时' })
this.pages.set(targetId, { awaitLoading })
await awaitLoading.catch(info => {
signale.warn(info)
})
return this.pages.get(targetId)
}

@@ -218,0 +60,0 @@ /**

@@ -0,80 +1,121 @@

const { sleep, signale } = require('./helper');
/**
* 用于实现可追溯的远程elment,实际上是devtools保存了注入函数的执行结果并生成查询id
* 通过状态跟踪,可以在已有远程结果基于上做增量操作,避免了代码的重复提交和执行
*/
class Element {
/**
*
* @param {Function} send
* @param {Object} info 匹配远程element对象描述信息
*/
constructor(page, info) {
constructor(send) {
this.page = page
this.bounding
this.selector
if (info) {
Object.assign(this, info)
}
}
/**
* 聚焦input元素
* @param {*} selector
* 注入包含参数的JS代码,并获取返回值
* @param {Function} func
* @param {*} args
*/
async focus() {
async evaluate(options = {}) {
return await this.evaluate(function (selector) {
let input = document.querySelector(selector)
input.focus()
}, this.selector)
let { func, returnByValue = false, args = [] } = options
let objectId = this.objectId
if (objectId) {
args.unshift({ objectId })
}
let { result } = await this.page.send('Runtime.callFunctionOn', {
executionContextId: this.page.contextId,
functionDeclaration: func.toString(),
arguments: args,
returnByValue,
awaitPromise: true,
userGesture: true
})
if (result.className === 'TypeError') {
signale.error(result.description)
return
}
return result
}
/**
* 通过选择器获取元素坐标
* @param {String} selector CSS选择器
* CSS单选迭代选择器
* @param {String} selector
*/
async getBoundingRect() {
async $(selector) {
return await this.evaluate(function (selector) {
if (this.objectId) {
let element = document.querySelector(selector)
if (element) {
let bounding = element.getBoundingClientRect()
let { x, y, width, height } = bounding
return { x, y, width, height }
// 子查询
let result = await this.evaluate({
func: (element, selector) => element.querySelector(selector),
args: [{ value: selector }]
})
if (result.objectId) {
// 创建新的子节点实例
return new Element(this.page, result)
}
}, this.selector)
} else {
}
/**
* 点击元素
* @param {String} selector CSS选择器
*/
async click() {
let result = await this.evaluate({
func: selector => document.querySelector(selector),
args: [{ value: selector }]
})
if (!this.bounding) {
this.bounding = await this.getBoundingRect(this.selector)
}
if (result.objectId) {
if (this.bounding) {
// 定位到元素中心
let { x, y, width, height } = bounding
x = x + width / 2
y = y + height / 2
this.mouse.click(x, y)
Object.assign(this, result)
return this
}
}
return this
}
/**
* 表单输入
* @param {String} text
* 聚焦input元素
* @param {*} selector
*/
async type(text, options) {
async focus() {
await this.keyboard.type(text, options)
return await this.evaluate({
func: element => element.focus()
})
return this
}
/**
* 显示被隐藏的元素
* @param {String} text
* 通过选择器获取元素坐标
* @param {String} selector CSS选择器
*/
async display() {
return this
async getBoundingRect() {
return await this.evaluate({
func: element => {
let bounding = element.getBoundingClientRect()
let { x, y, width, height } = bounding
return { x, y, width, height }
},
returnByValue: true
})
}
}
module.exports = Page
module.exports = Element
const signale = require('signale')
const zPromise = require('zpromise')
module.exports = {
signale,
zPromise,
assert(value, message) {

@@ -12,28 +14,2 @@ if (!value)

},
/**
* 带有超时功能的简化Promise包装器
* @param {Number} time 超时报错,单位ms
*/
promise(time) {
let callback
let promise = new Promise((resolve, reject) => {
callback = { resolve, reject }
})
if (time) {
let timeId = setTimeout(() => {
callback.reject(new Error(`Promise等待超超过${time}ms`))
}, time);
promise.resolve = function (data) {
clearTimeout(timeId)
callback.resolve(data)
}
promise.reject = function (data) {
clearTimeout(timeId)
callback.reject(data)
}
} else {
Object.assign(promise, callback)
}
return promise
}
};
const { assert, sleep } = require('./helper');
const keyDefinitions = require('./USKeyboardLayout');
const keyDefinitions = require('./USKeyboard');
class Keyboard {
/**
* @param {Function} send
*/
constructor(send) {
this.send = send;
this._modifiers = 0;
this._pressedKeys = new Set();
}
/**
* @param {Function} send
*/
constructor(page) {
this.page = page
this.send = page.send.bind(page);
this.modifiers = 0;
this.pressedKeys = new Set();
}
/**
* @param {string} key
* @param {{text: string}=} options
*/
async down(key, options = { text: undefined }) {
const description = this._keyDescriptionForString(key);
/**
* @param {string} key
* @param {{text: string}=} options
*/
async down(key, options = { text: undefined }) {
const description = this.keyDescriptionForString(key);
const autoRepeat = this._pressedKeys.has(description.code);
this._pressedKeys.add(description.code);
this._modifiers |= this._modifierBit(description.key);
const autoRepeat = this.pressedKeys.has(description.code);
this.pressedKeys.add(description.code);
this.modifiers |= this.modifierBit(description.key);
const text = options.text === undefined ? description.text : options.text;
await this.send('Input.dispatchKeyEvent', {
type: text ? 'keyDown' : 'rawKeyDown',
modifiers: this._modifiers,
windowsVirtualKeyCode: description.keyCode,
code: description.code,
key: description.key,
text: text,
unmodifiedText: text,
autoRepeat,
location: description.location,
isKeypad: description.location === 3
});
}
const text = options.text === undefined ? description.text : options.text;
await this.send('Input.dispatchKeyEvent', {
type: text ? 'keyDown' : 'rawKeyDown',
modifiers: this.modifiers,
windowsVirtualKeyCode: description.keyCode,
code: description.code,
key: description.key,
text: text,
unmodifiedText: text,
autoRepeat,
location: description.location,
isKeypad: description.location === 3
});
}
/**
* @param {string} key
* @return {number}
*/
_modifierBit(key) {
if (key === 'Alt')
return 1;
if (key === 'Control')
return 2;
if (key === 'Meta')
return 4;
if (key === 'Shift')
return 8;
return 0;
}
/**
* @param {string} key
* @return {number}
*/
modifierBit(key) {
if (key === 'Alt')
return 1;
if (key === 'Control')
return 2;
if (key === 'Meta')
return 4;
if (key === 'Shift')
return 8;
return 0;
}
/**
* @param {string} keyString
* @return {KeyDescription}
*/
_keyDescriptionForString(keyString) {
const shift = this._modifiers & 8;
const description = {
key: '',
keyCode: 0,
code: '',
text: '',
location: 0
};
/**
* @param {string} keyString
* @return {KeyDescription}
*/
keyDescriptionForString(keyString) {
const shift = this.modifiers & 8;
const description = {
key: '',
keyCode: 0,
code: '',
text: '',
location: 0
};
const definition = keyDefinitions[keyString];
assert(definition, `Unknown key: "${keyString}"`);
const definition = keyDefinitions[keyString];
assert(definition, `Unknown key: "${keyString}"`);
if (definition.key)
description.key = definition.key;
if (shift && definition.shiftKey)
description.key = definition.shiftKey;
if (definition.key)
description.key = definition.key;
if (shift && definition.shiftKey)
description.key = definition.shiftKey;
if (definition.keyCode)
description.keyCode = definition.keyCode;
if (shift && definition.shiftKeyCode)
description.keyCode = definition.shiftKeyCode;
if (definition.keyCode)
description.keyCode = definition.keyCode;
if (shift && definition.shiftKeyCode)
description.keyCode = definition.shiftKeyCode;
if (definition.code)
description.code = definition.code;
if (definition.code)
description.code = definition.code;
if (definition.location)
description.location = definition.location;
if (definition.location)
description.location = definition.location;
if (description.key.length === 1)
description.text = description.key;
if (description.key.length === 1)
description.text = description.key;
if (definition.text)
description.text = definition.text;
if (shift && definition.shiftText)
description.text = definition.shiftText;
if (definition.text)
description.text = definition.text;
if (shift && definition.shiftText)
description.text = definition.shiftText;
// if any modifiers besides shift are pressed, no text should be sent
if (this._modifiers & ~8)
description.text = '';
// if any modifiers besides shift are pressed, no text should be sent
if (this.modifiers & ~8)
description.text = '';
return description;
}
return description;
}
/**
* @param {string} key
*/
async up(key) {
const description = this._keyDescriptionForString(key);
/**
* @param {string} key
*/
async up(key) {
const description = this.keyDescriptionForString(key);
this._modifiers &= ~this._modifierBit(description.key);
this._pressedKeys.delete(description.code);
await this.send('Input.dispatchKeyEvent', {
type: 'keyUp',
modifiers: this._modifiers,
key: description.key,
windowsVirtualKeyCode: description.keyCode,
code: description.code,
location: description.location
});
}
this.modifiers &= ~this.modifierBit(description.key);
this.pressedKeys.delete(description.code);
await this.send('Input.dispatchKeyEvent', {
type: 'keyUp',
modifiers: this.modifiers,
key: description.key,
windowsVirtualKeyCode: description.keyCode,
code: description.code,
location: description.location
});
}
/**
* @param {string} char
*/
async sendCharacter(char) {
await this.send('Input.dispatchKeyEvent', {
type: 'char',
modifiers: this._modifiers,
text: char,
key: char,
unmodifiedText: char
});
}
/**
* @param {string} char
*/
async sendCharacter(char) {
await this.send('Input.dispatchKeyEvent', {
type: 'char',
modifiers: this.modifiers,
text: char,
key: char,
unmodifiedText: char
});
}
/**
* @param {string} text
* @param {{delay: (number|undefined)}=} options
*/
async type(text, options = {}) {
let delay = 30;
if (options.delay) {
delay = options.delay;
}
for (const char of text) {
if (keyDefinitions[char])
await this.press(char, { delay });
else
await this.sendCharacter(char);
if (delay)
await new Promise(f => setTimeout(f, delay));
}
}
/**
* @param {string} text
* @param {{delay: (number|undefined)}=} options
*/
async type(text, options = {}) {
let delay = 30;
if (options.delay) {
delay = options.delay;
}
for (const char of text) {
if (keyDefinitions[char])
await this.press(char, { delay });
else
await this.sendCharacter(char);
if (delay)
await new Promise(f => setTimeout(f, delay));
}
}
/**
* @param {string} key
* @param {!Object=} options
*/
async press(key, options) {
await this.down(key, options);
if (options && options.delay)
await new Promise(f => setTimeout(f, options.delay));
await this.up(key);
}
/**
* @param {string} key
* @param {!Object=} options
*/
async press(key, options) {
await this.down(key, options);
if (options && options.delay)
await new Promise(f => setTimeout(f, options.delay));
await this.up(key);
}
}
class Mouse {
/**
* @param {Function} send
*/
constructor(send, evaluate) {
this.send = send;
this.evaluate = evaluate;
this._x = 0;
this._y = 0;
this._button = 'none';
}
/**
* @param {Function} send
*/
constructor(page) {
this.page = page;
this.send = page.send.bind(page);
this.x = 0;
this.y = 0;
this.button = 'none';
}
/**
* 将steps默认值改为20,原值是1
* @param {number} x
* @param {number} y
* @param {Object=} options
* @return {!Promise}
*/
async move(x, y, options = {}) {
const fromX = this.x, fromY = this.y;
this.x = x;
this.y = y;
let { steps = 20 } = options
for (let i = 1; i <= steps; i++) {
await this.send('Input.dispatchMouseEvent', {
type: 'mouseMoved',
button: this.button,
x: fromX + (this.x - fromX) * (i / steps),
y: fromY + (this.y - fromY) * (i / steps)
});
}
}
/**
*
* @param {number} x
* @param {number} y
* @param {!Object=} options
*/
async click(x, y, options = {}) {
await this.move(x, y, options);
await this.down(options);
if (typeof options.delay === 'number')
await new Promise(f => setTimeout(f, options.delay));
await this.up(options);
}
/**
* 将steps默认值改为20,原值是1
* @param {number} x
* @param {number} y
* @param {Object=} options
* @return {!Promise}
*/
async move(x, y, options = {}) {
const fromX = this._x, fromY = this._y;
this._x = x;
this._y = y;
let { steps = 20 } = options
for (let i = 1; i <= steps; i++) {
/**
* @param {!Object=} options
*/
async down(options = {}) {
this.button = (options.button || 'left');
await this.send('Input.dispatchMouseEvent', {
type: 'mouseMoved',
button: this._button,
x: fromX + (this._x - fromX) * (i / steps),
y: fromY + (this._y - fromY) * (i / steps)
type: 'mousePressed',
button: this.button,
x: this.x,
y: this.y,
clickCount: (options.clickCount || 1)
});
}
}
/**
*
* @param {number} x
* @param {number} y
* @param {!Object=} options
*/
async click(x, y, options = {}) {
await this.move(x, y, options);
await this.down(options);
if (typeof options.delay === 'number')
await new Promise(f => setTimeout(f, options.delay));
await this.up(options);
}
}
/**
* @param {!Object=} options
*/
async down(options = {}) {
this._button = (options.button || 'left');
await this.send('Input.dispatchMouseEvent', {
type: 'mousePressed',
button: this._button,
x: this._x,
y: this._y,
clickCount: (options.clickCount || 1)
});
}
/**
* @param {!Object=} options
*/
async up(options = {}) {
this.button = 'none';
await this.send('Input.dispatchMouseEvent', {
type: 'mouseReleased',
button: (options.button || 'left'),
x: this.x,
y: this.y,
clickCount: (options.clickCount || 1)
});
}
/**
* @param {!Object=} options
*/
async up(options = {}) {
this._button = 'none';
await this.send('Input.dispatchMouseEvent', {
type: 'mouseReleased',
button: (options.button || 'left'),
x: this._x,
y: this._y,
clickCount: (options.clickCount || 1)
});
}
/**
* 新增 相对于窗口可视区滚动至指定坐标,目前仅支持纵向滚动
* @param {number} x 相对于窗口的横向偏移量
* @param {number} y 相对于窗口的纵向偏移量
*/
async scroll(x = 0, y = 0, step = 20) {
/**
* 新增 相对于窗口可视区滚动至指定坐标,目前仅支持纵向滚动
* @param {number} x 相对于窗口的横向偏移量
* @param {number} y 相对于窗口的纵向偏移量
*/
async scroll(x = 0, y = 0, step = 20) {
if (y < 0) step = -step
let count = y / step
for (let i = 0; i <= count; i++) {
await this.send('Input.dispatchMouseEvent', {
type: 'mouseWheel',
x: this.x,
y: this.y, // 鼠标在屏幕上的坐标
deltaX: 0,
deltaY: step // 滚动距离
});
await sleep(20);
}
if (y < 0) step = -step
let count = y / step
for (let i = 0; i <= count; i++) {
await this.send('Input.dispatchMouseEvent', {
type: 'mouseWheel',
x: this._x,
y: this._y, // 鼠标在屏幕上的坐标
deltaX: 0,
deltaY: step // 滚动距离
});
await sleep(20);
}
// 滚动后需要短暂停留,以消除惯性
await sleep(1000)
// 滚动后需要短暂停留,以消除惯性
await sleep(1000)
}
}
}
class Touch {
/**
* @param {Function} send 发送消息
*/
constructor(send, evaluate) {
this.send = send;
this.evaluate = evaluate;
}
/**
* @param {Function} send 发送消息
*/
constructor(page) {
this.page = page;
this.send = page.send.bind(page);
}
/**
* @param {number} x
* @param {number} y
*/
async tap(x, y) {
// Touches appear to be lost during the first frame after navigation.
// This waits a frame before sending the tap.
// @see https://crbug.com/613219
await this.send('Runtime.evaluate', {
expression: 'new Promise(x => requestAnimationFrame(() => requestAnimationFrame(x)))',
awaitPromise: true
});
/**
* @param {number} x
* @param {number} y
*/
async tap(x, y) {
// Touches appear to be lost during the first frame after navigation.
// This waits a frame before sending the tap.
// @see https://crbug.com/613219
await this.send('Runtime.evaluate', {
expression: 'new Promise(x => requestAnimationFrame(() => requestAnimationFrame(x)))',
awaitPromise: true
});
const touchPoints = [{ x: Math.round(x), y: Math.round(y) }];
await this.send('Input.dispatchTouchEvent', {
type: 'touchStart',
touchPoints
});
await this.send('Input.dispatchTouchEvent', {
type: 'touchEnd',
touchPoints: []
});
}
const touchPoints = [{ x: Math.round(x), y: Math.round(y) }];
await this.send('Input.dispatchTouchEvent', {
type: 'touchStart',
touchPoints
});
await this.send('Input.dispatchTouchEvent', {
type: 'touchEnd',
touchPoints: []
});
}
/**
* 单点滑动手势
* @param {Object} options
* @param {Object.start} 滑动起始坐标
* @param {Object.end} 滑动结束坐标
* @param {Object.steps} 步长
* @param {Object.delay} 发送move事件的间隔时间
*/
async slide({ start, end, steps = 50, delay = 150 }) {
/**
* 单点滑动手势
* @param {Object} options
* @param {Object.start} 滑动起始坐标
* @param {Object.end} 滑动结束坐标
* @param {Object.steps} 步长
* @param {Object.delay} 发送move事件的间隔时间
*/
async slide({ start, end, steps = 50, delay = 150 }) {
let { x: startX, y: startY } = start
let { x: endX, y: endY } = end
let { x: startX, y: startY } = start
let { x: endX, y: endY } = end
await this.send('Runtime.evaluate', {
expression: 'new Promise(x => requestAnimationFrame(() => requestAnimationFrame(x)))',
awaitPromise: true
});
await this.send('Runtime.evaluate', {
expression: 'new Promise(x => requestAnimationFrame(() => requestAnimationFrame(x)))',
awaitPromise: true
});
await this.send('Input.dispatchTouchEvent', {
type: 'touchStart',
touchPoints: [{ x: Math.round(startX), y: Math.round(startY) }]
});
let stepX = (endX - startX) / steps
let stepY = (endY - startY) / steps
for (let i = 1; i <= steps; i++) {
await this.send('Input.dispatchTouchEvent', {
type: 'touchMove',
touchPoints: [{ x: startX += stepX, y: startY += stepY }]
type: 'touchStart',
touchPoints: [{ x: Math.round(startX), y: Math.round(startY) }]
});
await sleep(8);
}
// 触点释放前的停留时间
await sleep(delay);
let stepX = (endX - startX) / steps
let stepY = (endY - startY) / steps
await this.send('Input.dispatchTouchEvent', {
type: 'touchEnd',
touchPoints: []
});
for (let i = 1; i <= steps; i++) {
await this.send('Input.dispatchTouchEvent', {
type: 'touchMove',
touchPoints: [{ x: startX += stepX, y: startY += stepY }]
});
await sleep(8);
}
}
// 触点释放前的停留时间
await sleep(delay);
/**
* 通过touch滚动至页面指定坐标
* @param {Number} x 横坐标
* @param {Number} y 纵坐标
* @param {Object} options 选项
* @param {Number}} options.interval 多次滚动的间隔时间,ms
*/
async scroll(x, y, options = {}) {
await this.send('Input.dispatchTouchEvent', {
type: 'touchEnd',
touchPoints: []
});
let { interval = 2000 } = options
// 滑动后需要短暂停留,以消除惯性
await sleep(1000)
// { scrollX, scrollY, innerWidth, innerHeight }
// await this.send('Runtime.evaluate', {
// expression: (function () {
// let { scrollX, scrollY, innerWidth, innerHeight } = window
// return { scrollX, scrollY, innerWidth, innerHeight }
// }).toString(),
// // awaitPromise: true
// });
}
/**
* 获取浏览器窗口和屏幕信息
*/
async windowInfo() {
// 获取当前浏览器滚动条位置
let { scrollX, scrollY, innerWidth, innerHeight } = await this.evaluate(() => {
let { scrollX, scrollY, innerWidth, innerHeight } = window
return { scrollX, scrollY, innerWidth, innerHeight }
});
let func = (function () {
let { scrollX, scrollY, innerWidth, innerHeight, screen: { width, height } } = window
return { scrollX, scrollY, innerWidth, innerHeight, width, height }
}).toString()
let totalX = x - scrollX
let totalY = y - scrollY
let { result } = await this.send('Runtime.evaluate', {
expression: `(${func})()`,
returnByValue: true,
awaitPromise: true,
userGesture: true,
// contextId: this.contextId,
})
let centerX = Math.round(innerWidth / 2)
let centerY = Math.round(innerHeight / 2)
return result.value
if (totalY > centerY) {
totalY -= centerY
} else {
return
}
}
/**
* 通过touch滚动至页面指定坐标
* @param {Number} x 横坐标
* @param {Number} y 纵坐标
* @param {Object} options 选项
* @param {Number}} options.interval 多次滚动的间隔时间,单位ms
*/
async scroll(x, y, options = {}) {
let plusX = 0
let plusY = 0
let { interval = 2000 } = options
// 分多次发送滑动事件
while (totalY > plusY) {
// 获取当前浏览器滚动条位置
let { scrollX, scrollY, innerWidth, innerHeight } = await this.windowInfo();
// 模拟随机坐标,让每次的滑动轨迹都不一样
let startX = Math.round(innerWidth * (0.3 + Math.random() * 0.4))
let startY = Math.round(innerHeight * (0.6 + Math.random() * 0.2))
let endX = Math.round(startX + Math.random() * 0.1)
let endY = Math.round(innerHeight * (0.2 + Math.random() * 0.2))
let totalX = x - scrollX
let totalY = y - scrollY
plusX += startX - endX
plusY += startY - endY
let centerX = Math.round(innerWidth / 2)
let centerY = Math.round(innerHeight / 2)
// 末端补齐
if (totalY < plusY) {
endY = startX + (plusY - totalY)
if (totalY > centerY) {
totalY -= centerY
} else {
return
}
let start = { x: startX, y: startY }
let end = { x: endX, y: endY }
let plusX = 0
let plusY = 0
await this.slide({ start, end });
await sleep(interval)
// 分多次发送滑动事件
while (totalY > plusY) {
}
// 模拟随机坐标,让每次的滑动轨迹都不一样
let startX = Math.round(innerWidth * (0.3 + Math.random() * 0.4))
let startY = Math.round(innerHeight * (0.6 + Math.random() * 0.2))
let endX = Math.round(startX + Math.random() * 0.1)
let endY = Math.round(innerHeight * (0.2 + Math.random() * 0.2))
}
plusX += startX - endX
plusY += startY - endY
// 末端补齐
if (totalY < plusY) {
endY = startX + (plusY - totalY)
}
let start = { x: startX, y: startY }
let end = { x: endX, y: endY }
await this.slide({ start, end });
await sleep(interval)
}
}
}
module.exports = { Keyboard, Mouse, Touch };

@@ -1,74 +0,19 @@

const EventEmitter = require('events');
const debug = require('debug');
const Message = require('./Message');
const Elment = require('./Elment');
const { Keyboard, Mouse, Touch } = require('./Input');
const { sleep, signale } = require('./helper');
const debugSend = debug('chrome:send:page');
class Page extends Message {
class Page extends EventEmitter {
constructor(chrome, { targetId }) {
constructor(send, { callbacks, targetId, sessionId }) {
super()
this.id = 1 // 消息自增id
this.chromeSend = send
this.callbacks = callbacks // WebSocket异步消息队列
this.chrome = chrome
this.targetId = targetId
this.sessionId = sessionId
this.mainFrame = {} // 页面主框架信息
this.contextId = 1 // 执行上下文id
this.keyboard = new Keyboard(this.send.bind(this))
this.mouse = new Mouse(this.send.bind(this), this.evaluate.bind(this))
this.touch = new Touch(this.send.bind(this), this.evaluate.bind(this))
this.keyboard = new Keyboard(this)
this.mouse = new Mouse(this)
this.touch = new Touch(this)
// session嵌套消息
this.on('Target.receivedMessageFromTarget', ({ message }) => {
message = JSON.parse(message)
if (message.id) {
const callback = this.callbacks.get(message.id);
if (callback) {
this.callbacks.delete(message.id);
if (message.error) {
callback.reject(message)
} else {
callback.resolve(message.result);
}
}
} else {
this.emit(message.method, message.params)
}
});
// 创建上下文时,获取contextId
this.on('Runtime.executionContextCreated', ({ context }) => {
if (context.auxData.frameId === this.mainFrame.id) {
this.contextId = context.id
}
});
// 安全证书错误
// 由于没有返回关联id,导致线程阻塞,需要强制调用Promise解除阻塞
this.on('Security.certificateError', async (data) => {
for (let item of this.callbacks.values()) {
item.reject(data)
}
});
this.on('Page.javascriptDialogOpening', async data => {
await this.send('Page.handleJavaScriptDialog', { accept: false });
signale.success(`关闭${data.type}对话框`, data.message)
});
// this.on('Page.lifecycleEvent', event => {
// // console.log(event)
// });
// this.on('Runtime.executionContextDestroyed', ({ executionContextId }) => {
// console.log('Runtime.executionContextDestroyed', executionContextId)
// });
// this.on('Runtime.executionContextsCleared', () => {
// });
}

@@ -105,3 +50,3 @@ /**

*/
async goto(url = 'chrome://newtab/') {
async goto(url = 'about:blank') {

@@ -112,31 +57,10 @@ return await this.send('Page.navigate', { url });

/**
* 发送带session的嵌套消息
* @param {*} method
* @param {*} params
* 注入包含参数的JS代码,并获取返回值
* @param {Function} func
* @param {*} args
*/
async send(method = '', params = {}) {
async evaluate(func, ...args) {
let id = this.id++
func = func.toString()
debugSend(id, method, params)
let message = JSON.stringify({ id, method, params });
this.chromeSend("Target.sendMessageToTarget", { sessionId: this.sessionId, message })
return new Promise((resolve, reject) => {
this.callbacks.set(id, { resolve, reject, method, error: new Error() });
}).catch(error => {
error.suffix = error.code
signale.error(error)
})
}
/**
* 注入包含参数的JS代码,并获取返回值
* @param {Function} pageFunction
* @param {*} args
*/
async evaluate(pageFunction, ...args) {
args = args.map(function (value) {

@@ -146,6 +70,7 @@ return { value }

// 相比Runtime.evaluate,Runtime.callFunctionOn是转为函数而设计
let { result } = await this.send('Runtime.callFunctionOn', {
functionDeclaration: pageFunction.toString(),
executionContextId: this.contextId,
functionDeclaration: func,
arguments: args,
executionContextId: this.contextId,
returnByValue: true,

@@ -164,11 +89,17 @@ awaitPromise: true,

}
// /**
// * 查找并缓存元素
// * @param {String} selector CSS选择器
// */
// async $(selector) {
/**
* 查找并缓存元素
* @param {String} selector CSS选择器
*/
async $(selector) {
// this.selector = selector
let element = new Elment(this)
// }
let el = element.$(selector)
if (el) {
return el
}
}
/**

@@ -205,2 +136,10 @@ * 聚焦input元素

/**
* 获取网页标题
*/
async title() {
return await this.evaluate(() => document.title)
}
/**
* 点击元素

@@ -228,6 +167,4 @@ * @param {String} selector CSS选择器

*/
async type(selector, text, options) {
async type(text, options) {
await this.focus(selector)
await sleep(600)

@@ -234,0 +171,0 @@

{
"name": "auto-chrome",
"version": "0.1.0",
"version": "0.2.0",
"description": "使用Node.js操作Chrome或Chromium,高仿真的用户行为模拟器",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"dev": "node test/",
"test": "set debut=chrome:message:other & node test"
},

@@ -14,4 +15,5 @@ "author": "",

"signale": "^1.2.1",
"ws": "^6.0.0"
"ws": "^6.0.0",
"zpromise": "^1.1.3"
}
}

@@ -24,5 +24,3 @@ # auto-chrome

* 隐藏了taget的概念,只需要直观的面对浏览器和标签和网页即可。
## Install

@@ -42,17 +40,17 @@

## chrome devtools术语
* `Chrome` 表示浏览器实例
* `Target` 表示浏览器中的多种对象,可以是browser、page、iframe、other类型其中之一。当type为page类型时,targetId对应于主框架的frame id。
* `Session` session机制用于创建多个会话,可以为每个Target绑定独立的session,也可以让多个Target共享同一个session。
* `Target` 表示浏览器中的不同对象,包含browser、page、iframe、other类型。devtools为每个target生成targetId,用于区分不同的目标。
* `Page` 浏览器标签,Chrome中允许打开多个Page,但始终只有一个Page处于激活状态。
* `Page` 表示浏览器标签,单个Chrome中允许包含多个Page,同一个时间点上始终只有一个Page处于激活状态。
* `Runtime` JavaScript运行时,用于向网页注入JS代码实现对DOM的操作。
* `Frame` 表示Target中的框架,主Frame中允许包含多个子Frame
* `Frame` 网页中的框架,主Frame中允许包含多个子Frame。
* `Context` 为了区分同一个Page中多个不同的网页、域名、框架,因此需要为这些对象分配唯一上下文。同一个域下的网页contextId从1开始递增,切换域时contextId初始化重新从1开始计数
* `Context` JavaScript运行时所处的的上下文,由于页面内可能包含Frame,每个Frame拥有独立的运行时,因此需要生成唯一contextId来区分它们。
* `Runtime` JavaScript运行时,通过向网页注入js代码实现对dom的操作

@@ -64,35 +62,35 @@

* options `Object` 全局实例配置选项,优先级低于page
* `options` *Object* 全局实例配置选项,优先级低于page
* executablePath `String` Chrome程序执行路径
* `executablePath` *String* Chrome程序执行路径
* args[ars, ...] `Array` Chrome启动参数数组
* `args[ars, ...]` *Array* Chrome启动参数数组
* ars `String` Chrome启动参数
* `ars` *String* Chrome启动参数
* userDataDir `String` 用户配置文件路径
* `userDataDir` *String* 用户配置文件路径
* emulate `Object` 设备仿真,该配置对于初始标签不太凑效,可能由于初始targetCreated事件并没有被捕获。
* `emulate` *Object* 设备仿真,该配置对于初始标签不太凑效,可能由于初始targetCreated事件并没有被捕获。
* mobile `Boolean` 移动设备,默认false
* `mobile` *Boolean* 移动设备,默认false
* hasTouch `Boolean` 启用触控,默认false
* `hasTouch` *Boolean* 启用触控,默认false
* width `Number` 屏幕宽度,默认自适应屏幕宽度
* `width` *Number* 屏幕宽度,默认自适应屏幕宽度
* width `Number` 屏幕高度,默认自适应屏幕高度
* `width` *Number* 屏幕高度,默认自适应屏幕高度
* geolocation `Object` 地理位置
* `geolocation` *Object* 地理位置,使用Google地图坐标
* longitude `Number` 经度
* `longitude` *Number* 经度
* latitude `Number` 纬度
* `latitude` *Number* 纬度
* accuracy `Number` 精准度
* `accuracy` *Number* 精准度
* headless `Boolean` 隐藏执行模式,默认false
* `headless` *Boolean* 隐藏执行模式,默认false
* devtools `Boolean` 为每个page自动打开devtools,默认false
* `devtools` *Boolean* 为每个page自动打开devtools,默认false
* ignoreHTTPSErrors `Boolean` 忽略https错误,默认true
* `ignoreHTTPSErrors` *Boolean* 忽略https错误,默认true

@@ -121,7 +119,7 @@ #### chrome.mouse

* url `String` 打开网页地址,缺省时打开空白网页
* `url` *String* 打开网页地址,缺省时打开空白网页
#### chrome.closePage(pageId)
* pageId `String` 要删除page的id
* `pageId` *String* 要删除page的id

@@ -132,5 +130,5 @@ #### chrome.send(method, params)

* method `String` 方法名
* `method` *String* 方法名
* params `Object` 参数
* `params` *Object* 参数

@@ -147,11 +145,11 @@ #### chrome.close()

鼠标
鼠标实例
#### page.keyboard
键盘
键盘实例
#### page.touch
触控设备
触控设备实例

@@ -162,19 +160,19 @@ #### page.emulate(options)

* options `Object` 选项
* `options` *Object* 选项
* mobile `Boolean` 移动设备
* `mobile` *Boolean* 移动设备
* hasTouch `Boolean` 启用触控
* `hasTouch` *Boolean* 启用触控
* width `Number` 屏幕宽度
* `width` *Number* 屏幕宽度
* width `Number` 屏幕高度
* `width` *Number* 屏幕高度
* geolocation `Object` 地理位置
* `geolocation` *Object* 地理位置
* longitude `Number` 经度
* `longitude` *Number* 经度
* latitude `Number` 纬度
* `latitude` *Number* 纬度
* accuracy `Number` 精准度
* `accuracy` *Number* 精准度

@@ -189,5 +187,5 @@ #### page.goto(url)

* pageFunction `Function` 注入函数
* `pageFunction` *Function* 注入函数
* arg `*` 可序列化参数,不支持函数
* `arg` *\** 可序列化参数,不支持函数

@@ -198,3 +196,3 @@ ### page.focus(selector)

* selector `String` CSS选择器
* `selector` *String* CSS选择器

@@ -205,3 +203,3 @@ ### page.getBoundingRect(selector)

* selector `String` CSS选择器
* `selector` *String* CSS选择器

@@ -212,3 +210,3 @@ ### page.click(selector)

* selector `String` CSS选择器
* `selector` *String* CSS选择器

@@ -219,9 +217,9 @@ ### page.type(selector, text, options)

* selector `String` CSS选择器
* `selector` *String* CSS选择器
* text `String` 输入文本
* `text` *String* 输入文本
* options `Object` 配置信息
* `options` *Object* 配置信息
* options.delay `Number` 输入间隔时间,ms
* `options.delay` *Number* 输入间隔时间,ms

@@ -232,3 +230,3 @@ ### page.scroll(selector)

* selector `String` CSS选择器
* `selector` *String* CSS选择器

@@ -239,5 +237,5 @@ #### page.send(method, params)

* method `String` 方法名
* `method` *String* 方法名
* params `Object` 参数
* `params` *Object* 参数

@@ -249,9 +247,9 @@

* selector `String` CSS选择器字符串
* `selector` *String* CSS选择器字符串
* options `Object` 选项
* `options` *Object* 选项
* steps `Number` touchmove的触发次数,默认50
* `steps` *Number* touchmove的触发次数,默认50
* interval `Number` 连续滑动的时间间隔,默认2000,单位ms
* `interval` *Number* 连续滑动的时间间隔,默认2000,单位ms

@@ -264,3 +262,27 @@ #### page.close()

### class: Element
用于实现可追溯的远程elment,实际上是devtools保存了注入函数的执行结果并生成查询id,通过状态追踪就可以在已有远程结果基于上做增量操作,避免了代码的重复提交和重复执行。
对于大的对象或者DOM对象,直接返回它们的代价非常高的,而且由于数据的动态性和实时性,这使得缓存策略并不好使。
#### elment.$(selector)
* `selector` *String*
* `return` *Object* Elment实例
选择元素,并生成远程引用对象
#### elment.focus()
聚焦元素
#### elment.getBoundingRect()
通过getBoundingClientRect函数获取元素大小、坐标信息
### class: Mouse

@@ -274,5 +296,5 @@

* options `Object`
* `options` *Object*
* steps `Number` mousemoved事件的触发次数,默认20
* `steps` *Number* mousemoved事件的触发次数,默认20

@@ -284,5 +306,5 @@

* options `Object` 选项
* `options` *Object* 选项
* steps `Number` 触发mousemoved事件的次数,默认值20
* `steps` *Number* 触发mousemoved事件的次数,默认值20

@@ -294,7 +316,7 @@

* x `Number` 横向坐标,0
* `x` *Number* 横向坐标,0
* y `Number` 纵向坐标
* `y` *Number* 纵向坐标
* step `Number` 步长
* `step` *Number* 步长

@@ -309,17 +331,17 @@

* start `Object` 起始坐标
* `start` *Object* 起始坐标
* x `Number` touchstart x坐标
* `x` *Number* touchstart x坐标
* y `Number` touchstart y坐标
* `y` *Number* touchstart y坐标
* end `Object` 结束坐标
* `end` *Object* 结束坐标
* x `Number` touchend x坐标
* `x` *Number* touchend x坐标
* y `Number` touchend y坐标
* `y` *Number* touchend y坐标
* steps `Number` touchmove的触发次数
* `steps` *Number* touchmove的触发次数
* delay `Number` 触点释放前的停留时间,用于滑动惯性控制
* `delay` *Number* 触点释放前的停留时间,用于滑动惯性控制

@@ -331,8 +353,8 @@

* x `Number` 目标x坐标
* `x` *Number* 目标x坐标
* y `Number` 目标y坐标
* `y` *Number* 目标y坐标
* options `Object`
* `options` *Object*
* interval `Number` 连续滑动的时间间隔,默认2000,单位ms
* `interval` *Number* 连续滑动的时间间隔,默认2000,单位ms

@@ -42,2 +42,4 @@ const autoChrome = require('../../')

run()
run().catch(function(error){
console.error(error)
})

@@ -14,14 +14,8 @@ const autoChrome = require('..')

await chrome.page.goto('http://www.runoob.com/')
await chrome.newPage('http://v.baidu.com/')
await sleep(3000)
console.log(333)
await chrome.page.type('.search-desktop .placeholder', 'hellow word')
await sleep(500)
await chrome.keyboard.press("Enter")
}
run()

@@ -18,4 +18,6 @@ const autoChrome = require('../../')

await chrome.page.type('#input', 'hellow word')
await chrome.page.focus('#input')
await chrome.page.type('hellow word')
await sleep(500)

@@ -22,0 +24,0 @@

@@ -60,15 +60,15 @@ const autoChrome = require('../../')

// 横向
// await page.touch.slide({ start: { x: 700, y: 100 }, end: { x: 50, y: 100 }, steps: 20 })
// await page.touch.slide({
// start: { x: 700, y: 100 },
// end: { x: 50, y: 100 },
// steps: 20
// })
// 纵向
// await page.touch.slide({ start: { x: 250, y: 500 }, end: { x: 250, y: 100 }, steps: 50 })
// await page.touch.slide({
// start: { x: 250, y: 500 },
// end: { x: 250, y: 100 },
// steps: 50
// })
// await page.$touchScroll('#taget', { steps: 50 })
// let { left, top } = await page.evaluate(async element => {
// let taget = document.getElementById('taget')
// let { top, left } = taget.getBoundingClientRect()
// return { left, top }
// });
await chrome.page.touchScroll('#taget')

@@ -78,2 +78,4 @@

run()
run().catch(function(error){
console.error(error)
})
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc