auto-chrome
Advanced tools
Comparing version 0.1.0 to 0.2.0
@@ -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 | ||
} | ||
}; |
704
lib/Input.js
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 }; |
141
lib/Page.js
@@ -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" | ||
} | ||
} |
174
README.md
@@ -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) | ||
}) |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No tests
QualityPackage does not have any tests. This is a strong signal of a poorly maintained or low quality package.
Found 1 instance in 1 package
558276
41
2543
2
339
1
4
+ Addedzpromise@^1.1.3
+ Addedzpromise@1.5.4(transitive)