auto-chrome
Advanced tools
Comparing version 1.0.7 to 1.1.0
@@ -39,2 +39,10 @@ ## 版本更新 | ||
* Chrome.close()改用关闭所有标签的方式优雅的退出浏览器。在实测中发现使用DevTools协议的"Browser.close"消息和childProcess.kill()均无法关闭浏览器。 | ||
* Chrome.close()改用关闭所有标签的方式优雅的退出浏览器。在实测中发现使用DevTools协议的"Browser.close"消息和childProcess.kill()均无法关闭浏览器。 | ||
### 1.1.0 | ||
* 使用ipc pipe通信替换ws WebSocket | ||
* 简化自动导航逻辑 | ||
* waitLoad()更名为autoNav() |
@@ -9,3 +9,3 @@ "use strict"; | ||
/** | ||
* @param {Object} args.ws WebSocket实例 | ||
* @param {Object} args.connection 进程连接实例 | ||
* @param {Boolean} args.process 子进程chrome实例 | ||
@@ -17,5 +17,5 @@ * @param {Boolean} args.options.ignoreHTTPSErrors 是否忽略https错误 | ||
const { ws, options, process } = args; | ||
const { connection, options, process } = args; | ||
super(ws); | ||
super(connection); | ||
@@ -45,6 +45,7 @@ this.options = options; | ||
// 启用目标监听 | ||
await this.send('Target.setDiscoverTargets', { discover: true }); | ||
// 如果浏览器默认标签未完成初始化,则等待标签初始标签准备就绪后再执行 | ||
await this.autoLoad(2000); | ||
await this.autoNav(); | ||
@@ -67,2 +68,3 @@ } | ||
const promise = this.closePageById(targetId); | ||
allPromise.push(promise); | ||
@@ -84,3 +86,3 @@ this.pages.delete(targetId); | ||
this.send('Target.createTarget', { url }), | ||
this.autoLoad(2000) | ||
this.autoNav() | ||
]); | ||
@@ -87,0 +89,0 @@ |
"use strict"; | ||
const logger = require('loggercc'); | ||
const EventEmitter = require('events'); | ||
const timerPromise = require('zpromise/timer.js'); | ||
const Page = require('./Page.js'); | ||
const helper = require('./helper.js'); | ||
const { timechain, restartPromise, sleep } = helper; | ||
const { timechain, sleep } = helper; | ||
class ChromeEvent { | ||
constructor(ws) { | ||
class ChromeEvent extends EventEmitter { | ||
constructor(connection) { | ||
this.ws = ws; | ||
super(); | ||
this.connection = connection; | ||
this.id = 1; // 消息自增id | ||
@@ -18,6 +21,2 @@ this.callbacks = new Map(); // WebSocket异步消息队列 | ||
this.newPagePromise = new restartPromise(3000, error => { | ||
logger.warn('newPagePromise等待超时'); | ||
}) | ||
} | ||
@@ -35,5 +34,6 @@ /** | ||
this.ws.send(message); // 发送消息 | ||
this.connection.send(message); // 发送消息 | ||
return new Promise((resolve, reject) => { | ||
this.callbacks.set(id, { | ||
@@ -44,6 +44,11 @@ resolve, | ||
}); | ||
timechain.set(reject, `${method}消息超时`); | ||
}).catch(error => { | ||
logger.error(error); | ||
return { error }; | ||
}); | ||
@@ -69,4 +74,6 @@ | ||
if (callback) { | ||
timechain.delete(callback.reject) | ||
timechain.delete(callback.reject); | ||
this.callbacks.delete(id); | ||
if (error) { | ||
@@ -77,2 +84,3 @@ callback.reject(error); | ||
} | ||
} | ||
@@ -89,11 +97,16 @@ | ||
if (method === 'Target.receivedMessageFromTarget') { | ||
const { targetId, message } = params; | ||
const page = this.pages.get(targetId); | ||
if (page) { | ||
page.message(message); | ||
} | ||
} else { | ||
if (this[method]) { | ||
this[method](params); | ||
} | ||
} | ||
@@ -105,77 +118,21 @@ | ||
/** | ||
* 等待页面装载 | ||
*/ | ||
async loadPage() { | ||
if (this.newPagePromise.state !== 'pending') { | ||
this.newPagePromise = this.newPagePromise.restart(); | ||
} | ||
await Promise.all([ | ||
this.newPagePromise, | ||
this.page.waitCtx(), | ||
this.page.waitLoad() | ||
]) | ||
} | ||
/** | ||
* 自动导航(主动监测) | ||
* @param {Number} delay 等待导航最大毫秒数 | ||
*/ | ||
async autoLoad(delay = 1200) { | ||
async autoNav(time = 3000) { | ||
const { page } = this; | ||
const { contextCreateTime } = this; | ||
if (page.sessionId) { | ||
// 每间隔200ms检查一次context是否更新 | ||
for (let i = 1; i <= (time / 200); i++) { | ||
const { contextId, ctxPromise, loadPromise, ctxTime } = page; | ||
await sleep(200); | ||
if (contextId) { | ||
if (contextCreateTime !== this.contextCreateTime) break; | ||
await Promise.all([ctxPromise, loadPromise]); | ||
} | ||
// 循环监测 | ||
for (let i = 1; i <= (delay / 100); i++) { | ||
await this.page.loadPromise; | ||
await sleep(100); | ||
await sleep(1000); | ||
// 原标签 | ||
if (page === this.page) { | ||
if (ctxTime !== page.ctxTime) { | ||
// 为url重定向保留时间 | ||
await sleep(800) | ||
await page.loadPromise | ||
return | ||
} | ||
} | ||
// 新标签 | ||
else { | ||
await this.autoLoad() | ||
return | ||
} | ||
} | ||
} else { | ||
await Promise.all([page.waitCtx(), page.waitLoad()]); | ||
} | ||
} else { | ||
await this.loadPage(); | ||
} | ||
} | ||
@@ -208,3 +165,3 @@ /** | ||
logger.success(`创建新标签`) | ||
logger.success(`创建新标签`); | ||
@@ -219,2 +176,4 @@ const { targetId } = targetInfo; | ||
this.emit('CreatedPage', page); | ||
// 通过给定targetId获取sessionId | ||
@@ -225,4 +184,2 @@ const { sessionId } = await this.send('Target.attachToTarget', { targetId }) | ||
this.keyboard = page.keyboard; | ||
this.mouse = page.mouse; | ||
@@ -232,2 +189,4 @@ | ||
this.keyboard = page.keyboard; | ||
const list = [ | ||
@@ -248,5 +207,3 @@ page.send('Page.enable'), | ||
if (this.disableDownload === true) { | ||
list.push(page.send('Page.setDownloadBehavior', { | ||
"behavior": "deny" | ||
})); | ||
list.push(page.send('Page.setDownloadBehavior', { "behavior": "deny" })); | ||
} | ||
@@ -256,4 +213,2 @@ | ||
this.newPagePromise.resolve(); | ||
} | ||
@@ -264,10 +219,8 @@ | ||
const { type, url, title } = targetInfo; | ||
// target为page类型 | ||
if (type === 'page') { | ||
if (targetInfo.type === 'page') { | ||
this.page.url = url; | ||
this.page.url = targetInfo.url; | ||
this.page.title = title; | ||
this.page.title = targetInfo.title; | ||
@@ -287,4 +240,27 @@ } | ||
} | ||
/** | ||
* 订阅事件并等待触发 | ||
* @param {String} name 事件名称 | ||
* @param {Function} callback 回调 | ||
* @param {Number} timer 等待超时时间 | ||
*/ | ||
async oncePromise(name, callback, timer = 3000) { | ||
const promise = new timerPromise(timer); | ||
this.once(name, async function (value) { | ||
if (callback) { | ||
await callback(value); | ||
} | ||
promise.resolve(value); | ||
}); | ||
return promise; | ||
} | ||
} | ||
module.exports = ChromeEvent; |
@@ -38,3 +38,3 @@ "use strict" | ||
return new Element(this.page, remote) | ||
return new Element(this.page, remote); | ||
@@ -52,6 +52,6 @@ } | ||
functionDeclaration: (element, selector) => { | ||
return element.querySelector(selector) | ||
return element.querySelector(selector); | ||
}, | ||
arguments: [{ objectId }, { value: selector }] | ||
}) | ||
}); | ||
@@ -94,3 +94,3 @@ const { className, description } = remote; | ||
if (className === 'TypeError') { | ||
logger.warn(new Error(description)) | ||
logger.warn(new Error(description)); | ||
return | ||
@@ -110,3 +110,5 @@ } | ||
for (const item of result) { | ||
const { enumerable, value } = item; | ||
if (enumerable === true) { | ||
@@ -116,2 +118,3 @@ const element = this.create(value); | ||
} | ||
} | ||
@@ -231,15 +234,15 @@ | ||
const result = await this.getBoundingRect() | ||
const result = await this.getBoundingRect(); | ||
let { x, y, height, innerHeight } = result; | ||
const { x, y, height, innerHeight } = result; | ||
let centreY = (innerHeight - height) / 2; | ||
const centreY = (innerHeight - height) / 2; | ||
y = y - centreY; | ||
const scrollY = y - centreY; | ||
await clicker.scroll(x, y) | ||
await clicker.scroll(x, scrollY); | ||
} else { | ||
await this.loopScroll() | ||
await this.loopScroll(); | ||
@@ -252,3 +255,3 @@ } | ||
* 在每个单次滚动后重新计数坐标,防止DOM更新后原坐标失效 | ||
* 当chrome不稳定导致事件未执行时,采用实时坐标提供了错误补救的机会 | ||
* 当chrome不稳定导致事件未执行时,采用实时坐标提供了纠正错误的机会 | ||
*/ | ||
@@ -266,9 +269,9 @@ async loopScroll() { | ||
// 模拟随机坐标,让每次的滑动轨迹都不一样 | ||
let startX = Math.round(innerWidth * (0.3 + Math.random() * 0.4)) | ||
let startY = Math.round(innerHeight * (0.8 + Math.random() * 0.15)) | ||
const startX = Math.round(innerWidth * (0.3 + Math.random() * 0.4)); | ||
const startY = Math.round(innerHeight * (0.8 + Math.random() * 0.15)); | ||
let endX = Math.round(startX + Math.random() * 0.1) | ||
let endY = Math.round(innerHeight * (0.1 + Math.random() * 0.15)) | ||
const endX = Math.round(startX + Math.random() * 0.1); | ||
let endY = Math.round(innerHeight * (0.1 + Math.random() * 0.15)); | ||
let moveY = startY - endY | ||
const moveY = startY - endY; | ||
@@ -293,3 +296,3 @@ // 末端补齐 | ||
await this.page.touch.slide({ start, end }) | ||
await this.page.touch.slide({ start, end }); | ||
@@ -299,3 +302,3 @@ // 分多次发送滑动事件 | ||
await this.loopScroll() | ||
await this.loopScroll(); | ||
@@ -312,3 +315,3 @@ } | ||
const result = await this.getBoundingRect() | ||
const result = await this.getBoundingRect(); | ||
@@ -321,6 +324,3 @@ let { x, y, width, height } = result; | ||
await Promise.all([ | ||
this.page.clicker.click(x, y), | ||
this.page.chrome.autoLoad(2000) | ||
]) | ||
await this.page.clicker.click(x, y); | ||
@@ -331,18 +331,23 @@ return result; | ||
/** | ||
* 低仿真点击元素,仅适用于PC端 | ||
* 高仿真点击元素 | ||
*/ | ||
async lowClick() { | ||
async clickNav() { | ||
const { objectId } = this; | ||
// await this.label() | ||
const click = this.frame.callFunctionOn({ | ||
functionDeclaration: element => { element.click(); }, | ||
arguments: [{ objectId }] | ||
}) | ||
const result = await this.getBoundingRect(); | ||
let { x, y, width, height } = result; | ||
// 定位到元素中心 | ||
x = x + width / 2; | ||
y = y + height / 2; | ||
await Promise.all([ | ||
click, | ||
this.page.chrome.autoLoad(2000) | ||
]); | ||
this.page.chrome.autoNav(3000), | ||
this.page.clicker.click(x, y) | ||
]) | ||
return result; | ||
} | ||
@@ -349,0 +354,0 @@ /** |
@@ -24,6 +24,6 @@ "use strict"; | ||
if (objectId === undefined) { | ||
throw new Error(`缺少objectId参数!`) | ||
throw new Error(`缺少objectId参数!`); | ||
} | ||
const { result } = await this.page.send('Runtime.callFunctionOn', { | ||
const { result, error } = await this.page.send('Runtime.callFunctionOn', { | ||
objectId, | ||
@@ -33,4 +33,9 @@ awaitPromise: true, | ||
...options | ||
}) | ||
}); | ||
if (error) { | ||
throw logger.error(new Error(error)); | ||
// throw new Error(error.message); | ||
} | ||
const { className, subtype, description } = result; | ||
@@ -54,3 +59,3 @@ | ||
let { result } = await this.page.send('Runtime.evaluate', { | ||
const { result } = await this.page.send('Runtime.evaluate', { | ||
returnByValue: false, | ||
@@ -62,13 +67,13 @@ awaitPromise: true, | ||
let { className, subtype, description } = result | ||
const { className, subtype, description } = result; | ||
if (className === 'TypeError' || subtype === 'error') { | ||
logger.error(new Error(description)) | ||
logger.error(new Error(description)); | ||
} | ||
return result | ||
return result; | ||
} catch (error) { | ||
logger.error(error) | ||
logger.error(error); | ||
@@ -80,2 +85,2 @@ } | ||
module.exports = Frame | ||
module.exports = Frame; |
"use strict"; | ||
const timeChain = require('timechain'); | ||
const timerPromise = require('zpromise/timer.js'); | ||
const restartPromise = require('zpromise/restart.js'); | ||
@@ -11,12 +9,5 @@ const timechain = new timeChain({ delay: 10000 }); | ||
timechain, | ||
timerPromise, | ||
restartPromise, | ||
assert(value, message) { | ||
if (!value) { | ||
throw new Error(message); | ||
} | ||
}, | ||
sleep(time = 0) { | ||
return new Promise(resolve => setTimeout(resolve, time)) | ||
return new Promise(resolve => setTimeout(resolve, time)); | ||
}, | ||
} |
@@ -5,16 +5,10 @@ "use strict"; | ||
const childProcess = require('child_process'); | ||
const readline = require('readline'); | ||
const WebSocket = require('ws'); | ||
const Connection = require('./Connection.js'); | ||
const Chrome = require('./Chrome.js'); | ||
const helper = require('./helper.js'); | ||
const { timerPromise } = helper; | ||
async function main(options) { | ||
const { args = [], userDataDir, profileDir, port = 9222, } = options; | ||
const { args = [], userDataDir, profileDir, } = options; | ||
const { headless, devtools, executablePath, } = options; | ||
args.push(`--remote-debugging-port=${port}`); | ||
if (userDataDir) { | ||
@@ -41,4 +35,9 @@ args.push(`--user-data-dir=${userDataDir}`); | ||
args.push('--remote-debugging-pipe'); | ||
// 启动浏览器 | ||
const chromeProcess = childProcess.execFile(executablePath, args); | ||
const chromeProcess = childProcess.spawn(executablePath, args, { | ||
env: process.env, | ||
stdio: ['ignore', 'ignore', 'ignore', 'pipe', 'pipe'] | ||
}); | ||
@@ -49,55 +48,8 @@ chromeProcess.on('error', error => { | ||
chromeProcess.on('message', message => { | ||
logger.info(message); | ||
}); | ||
const [, , , pipeWrite, pipeRead] = chromeProcess.stdio; | ||
const rl = readline.createInterface({ | ||
input: chromeProcess.stderr | ||
}); | ||
const connection = new Connection(pipeWrite, pipeRead); | ||
const linePromise = new timerPromise(30000); | ||
rl.on('line', function (data) { | ||
if (data.indexOf('ws://') >= 0) { | ||
const url = data.replace('DevTools listening on ', ''); | ||
linePromise.resolve(url); | ||
} | ||
}); | ||
// 获取webSocket连接地址 | ||
const webSocketUrl = await linePromise.catch(error => { | ||
throw error | ||
}) | ||
let ws; | ||
try { | ||
ws = new WebSocket(webSocketUrl, { perMessageDeflate: false }); | ||
} catch (error) { | ||
chromeProcess.kill(); | ||
throw error; | ||
} | ||
const awaitOpen = new timerPromise(30000); | ||
ws.on('open', awaitOpen.resolve); | ||
ws.on('error', awaitOpen.reject); | ||
await awaitOpen.then(function () { | ||
logger.success('Chrome连接成功'); | ||
}).catch(function (error) { | ||
logger.error(new Error('Chrome连接失败')); | ||
throw error; | ||
}) | ||
const chrome = new Chrome({ | ||
ws, | ||
connection, | ||
options, | ||
@@ -107,3 +59,3 @@ process: chromeProcess, | ||
ws.on('message', chrome.message.bind(chrome)); | ||
connection.bind(chrome); | ||
@@ -110,0 +62,0 @@ await chrome.init(); |
"use strict" | ||
const keyDefinitions = require('./USKeyboard.js'); | ||
const { assert, sleep } = require('./helper.js'); | ||
const { sleep } = require('./helper.js'); | ||
@@ -16,3 +16,3 @@ class Keyboard { | ||
this.pressedKeys = new Set(); | ||
} | ||
@@ -33,3 +33,3 @@ | ||
const text = options.text === undefined ? description.text : options.text; | ||
await this.send('Input.dispatchKeyEvent', { | ||
@@ -47,3 +47,3 @@ type: text ? 'keyDown' : 'rawKeyDown', | ||
}); | ||
} | ||
@@ -82,4 +82,7 @@ | ||
const definition = keyDefinitions[keyString]; | ||
assert(definition, `Unknown key: "${keyString}"`); | ||
if (definition === undefined) { | ||
throw new Error(`Unknown key: "${keyString}"`); | ||
} | ||
if (definition.key) | ||
@@ -98,12 +101,18 @@ description.key = definition.key; | ||
if (definition.location) | ||
if (definition.location) { | ||
description.location = definition.location; | ||
} | ||
if (description.key.length === 1) | ||
if (description.key.length === 1) { | ||
description.text = description.key; | ||
} | ||
if (definition.text) | ||
if (definition.text) { | ||
description.text = definition.text; | ||
if (shift && definition.shiftText) | ||
} | ||
if (shift && definition.shiftText) { | ||
description.text = definition.shiftText; | ||
} | ||
@@ -121,2 +130,3 @@ // if any modifiers besides shift are pressed, no text should be sent | ||
async up(key) { | ||
const description = this.keyDescriptionForString(key); | ||
@@ -134,2 +144,3 @@ | ||
}); | ||
} | ||
@@ -147,5 +158,6 @@ | ||
key: char, | ||
unmodifiedText: char | ||
unmodifiedText: char, | ||
isKeypad: true | ||
}); | ||
} | ||
@@ -155,17 +167,19 @@ | ||
* @param {string} text | ||
* @param {{delay: (number|undefined)}=} options | ||
*/ | ||
async type(text) { | ||
let chars = [] | ||
let chars = []; | ||
for (const char of text) { | ||
// 英文输入 | ||
if (keyDefinitions[char]) { | ||
if (chars.length) { | ||
await this.sendCharacter(chars.join('')); | ||
chars = [] | ||
chars = []; | ||
} | ||
await this.press(char, { delay: 20 }); // 键盘输入 | ||
} else { | ||
chars.push(char) | ||
} | ||
await sleep(100) | ||
// 非英文输入 | ||
else { | ||
chars.push(char); | ||
} | ||
await sleep(100); | ||
} | ||
@@ -184,4 +198,5 @@ // 直接赋值 | ||
await this.down(key, options); | ||
if (options && options.delay) | ||
if (options && options.delay) { | ||
await new Promise(f => setTimeout(f, options.delay)); | ||
} | ||
await this.up(key); | ||
@@ -188,0 +203,0 @@ } |
"use strict"; | ||
const logger = require('loggercc'); | ||
const { Mouse, Touch } = require('./Input.js'); | ||
const Mouse = require('./Mouse.js'); | ||
const Touch = require('./Touch.js'); | ||
const Keyboard = require('./Keyboard.js'); | ||
@@ -17,3 +18,3 @@ const Frame = require('./Frame.js'); | ||
this.chrome = chrome; | ||
this.ws = chrome.ws; | ||
this.connection = chrome.connection; | ||
this.targetId = targetId; | ||
@@ -70,4 +71,4 @@ | ||
const result = await Promise.all([ | ||
this.send('Page.navigate', { url }), | ||
this.chrome.autoLoad(2000) | ||
this.chrome.autoNav(), | ||
this.send('Page.navigate', { url }) | ||
]).catch(() => { | ||
@@ -108,8 +109,10 @@ logger.warn(`打开链接${url}超时`); | ||
const { absolute, relative } = options | ||
const { absolute, relative } = options; | ||
let entry; | ||
if (absolute) { | ||
var entry = entries[absolute] | ||
entry = entries[absolute]; | ||
} else if (relative) { | ||
var entry = entries[currentIndex + relative] | ||
entry = entries[currentIndex + relative]; | ||
} | ||
@@ -127,3 +130,3 @@ | ||
* @param {Function} func | ||
* @param {*} args | ||
* @param {Array} args | ||
*/ | ||
@@ -158,3 +161,3 @@ async run(functionDeclaration, ...args) { | ||
return await this.document.$$(selector) | ||
return await this.document.$$(selector); | ||
@@ -168,3 +171,3 @@ } | ||
return this.document.create(remote) | ||
return this.document.create(remote); | ||
@@ -186,3 +189,3 @@ } | ||
/** | ||
* 高仿真点击元素 | ||
* 点击元素 | ||
* @param {String} selector CSS选择器 | ||
@@ -204,6 +207,6 @@ */ | ||
/** | ||
* 低仿真点击元素,仅适用于PC端 | ||
* 点击元素,内置导航 | ||
* @param {String} selector CSS选择器 | ||
*/ | ||
async lowClick(selector) { | ||
async clickNav(selector) { | ||
@@ -214,3 +217,3 @@ const element = await this.$(selector); | ||
await element.lowClick(); | ||
await element.clickNav(); | ||
@@ -229,4 +232,4 @@ return element; | ||
await Promise.all([ | ||
this.keyboard.press("Enter"), | ||
this.chrome.autoLoad(2000) | ||
this.chrome.autoNav(3000), | ||
this.keyboard.press("Enter") | ||
]) | ||
@@ -299,3 +302,3 @@ | ||
await this.chrome.closePageById(this.targetId) | ||
await this.chrome.closePageById(this.targetId); | ||
@@ -302,0 +305,0 @@ } |
"use strict" | ||
const logger = require('loggercc'); | ||
const EventEmitter = require('events'); | ||
const timerPromise = require('zpromise/timer.js'); | ||
const restartPromise = require('zpromise/restart.js'); | ||
const helper = require('./helper.js'); | ||
const { timechain, restartPromise } = helper; | ||
const { timechain } = helper; | ||
@@ -11,5 +14,6 @@ /** | ||
*/ | ||
class PageEvent { | ||
class PageEvent extends EventEmitter { | ||
constructor() { | ||
constructor(chrome) { | ||
super(); | ||
@@ -21,32 +25,6 @@ this.id = 1 // 消息自增id | ||
this.ctxPromise = new restartPromise(5000, error => { | ||
logger.warn('ctxPromise等待超时'); | ||
}) | ||
this.loadPromise = new restartPromise(10000); | ||
this.loadPromise = new restartPromise(chrome.options.loadTimeout || 10000, error => { | ||
logger.warn('loadPromise等待超时'); | ||
}) | ||
} | ||
/** | ||
* 重置ctx | ||
*/ | ||
waitCtx() { | ||
this.ctxPromise = this.ctxPromise.restart(); | ||
return this.ctxPromise; | ||
} | ||
/** | ||
* 重置lode,等待页面完成加载 | ||
*/ | ||
waitLoad() { | ||
this.loadPromise = this.loadPromise.restart(); | ||
return this.loadPromise; | ||
} | ||
/** | ||
* 发送带session的嵌套消息 | ||
@@ -67,24 +45,28 @@ * @param {String} method 消息名称 | ||
const message = { | ||
id: 0, // id值可指定任意数值,不会建立响应关联,因此没有实际用途,但又不可缺省 | ||
method: "Target.sendMessageToTarget", | ||
params: { | ||
sessionId: this.sessionId, | ||
message: JSON.stringify(subMessage) | ||
'id': 0, // id值可指定任意数值,不会建立响应关联,因此没有实际用途,但又不可缺省 | ||
'method': "Target.sendMessageToTarget", | ||
'params': { | ||
'sessionId': this.sessionId, | ||
'message': JSON.stringify(subMessage) | ||
} | ||
} | ||
// 发送消息 | ||
this.ws.send(JSON.stringify(message)); | ||
this.connection.send(JSON.stringify(message)); // 发送消息 | ||
return new Promise((resolve, reject) => { | ||
this.callbacks.set(id, { resolve, reject, method }); | ||
timechain.set(reject, `“${method}”消息等待超时`); | ||
}).catch(error => { | ||
if (error instanceof Object) { | ||
logger.error(String(Object.values(error))); | ||
return { error } | ||
const errorInfo = String(Object.values(error)); | ||
logger.error(errorInfo); | ||
return { error: errorInfo }; | ||
} else { | ||
logger.error(String(error)); | ||
return { error } | ||
return { error }; | ||
} | ||
}) | ||
@@ -106,4 +88,4 @@ | ||
this.callbacks.delete(message.id); | ||
timechain.delete(callback.reject); | ||
this.callbacks.delete(message.id); | ||
@@ -135,14 +117,18 @@ if (message.error) { | ||
let { objectId } = await this.frame.evaluate({ expression: "document" }); | ||
const { origin, id } = context; | ||
const { objectId } = await this.frame.evaluate({ expression: "document" }); | ||
this.document.objectId = objectId; | ||
this.contextId = context.id; | ||
this.contextId = id; | ||
this.ctxTime = Date.now(); | ||
logger.log(`上下文切换为:${id}`); | ||
this.ctxPromise.resolve(); | ||
this.loadPromise = this.loadPromise.restart(); | ||
logger.log(`上下文切换为:${context.id}`); | ||
this.chrome.contextCreateTime = Date.now(); | ||
this.emit(`CreatedContext`, origin); | ||
} | ||
@@ -154,13 +140,2 @@ | ||
/** | ||
* 安全证书错误 | ||
* 由于没有返回关联id,导致线程阻塞,需要强制调用Promise解除阻塞 | ||
*/ | ||
'Security.certificateError'(data) { | ||
for (let item of this.callbacks.values()) { | ||
item.reject(data); | ||
} | ||
} | ||
/** | ||
* 框架加载完毕时触发,子框架优先级高于主框架 | ||
@@ -171,4 +146,5 @@ */ | ||
if (frameId === this.targetId) { | ||
logger.success('页面加载完毕'); | ||
this.loadPromise.resolve(); | ||
} | ||
@@ -182,12 +158,41 @@ | ||
await this.send('Page.handleJavaScriptDialog', { accept: false }) | ||
logger.log(`关闭${data.type}对话框`, data.message) | ||
await this.send('Page.handleJavaScriptDialog', { accept: false }); | ||
logger.log(`关闭${data.type}对话框`, data.message); | ||
} | ||
/** | ||
* 打开新窗口 | ||
* @param {*} param0 | ||
* 安全证书错误 | ||
* 由于没有返回关联id,导致线程阻塞,需要强制调用Promise解除阻塞 | ||
*/ | ||
'Page.windowOpen'() { } | ||
'DOM.getContentQuads'() { } | ||
'Security.certificateError'(data) { | ||
for (const item of this.callbacks.values()) { | ||
item.reject(data); | ||
} | ||
} | ||
/** | ||
* 订阅事件并等待触发 | ||
* @param {String} name 事件名称 | ||
* @param {Function} callback 回调 | ||
* @param {Number} timer 等待超时时间 | ||
*/ | ||
oncePromise(name, callback, timer = 10000) { | ||
const promise = new timerPromise(timer); | ||
this.once(name,async function (value) { | ||
if (callback) { | ||
await callback(value); | ||
} | ||
promise.resolve(); | ||
}); | ||
return promise; | ||
} | ||
} | ||
@@ -194,0 +199,0 @@ |
{ | ||
"name": "auto-chrome", | ||
"version": "1.0.7", | ||
"version": "1.1.0", | ||
"description": "使用Node.js操作Chrome或Chromium,高仿真的用户行为模拟器", | ||
@@ -14,6 +14,5 @@ "main": "lib/", | ||
"dependencies": { | ||
"loggercc": "^1.3.1", | ||
"timechain": "^1.3.2", | ||
"ws": "^6.1.2", | ||
"zpromise": "^1.5.0" | ||
"loggercc": "^1.3.2", | ||
"timechain": "^1.3.4", | ||
"zpromise": "^1.5.3" | ||
}, | ||
@@ -20,0 +19,0 @@ "directories": { |
@@ -62,3 +62,3 @@ # auto-chrome | ||
浏览器导航事件可分为可预测和不可预测两种,由于触发导航的方式非常多,通过鼠标、键盘、JS脚本等方式均可能触发未知的导航事件。如果导航切换时序不正确,会产生上下文消息错乱的bug。 | ||
浏览器导航事件可分为可预测和不可预测两种,由于触发导航的方式非常多,通过鼠标、键盘、JS脚本方式均可能触发未知的导航事件。如果导航切换时序不正确,会产生上下文消息错乱的bug。 | ||
@@ -73,13 +73,17 @@ 另外一种情况是由于url重定向,频繁的触发上下文切换,导致上下文错位。这种场景难以被察觉,也很难做预判。 | ||
对于不可预测导航,如按下Enter键、鼠标点击绑定了跳转事件的非链接元素触发导航行为。 | ||
* 键盘、鼠标事件绑定了跳转连接的非链接元素触发导航行为 | ||
autoChrome无法预判一个操作是否会触发导航,只能由用户来指定某个步骤是否需要等待导航结束后再继续执行。 | ||
* 导航可能在当前标签,也可能在新标签中加载 | ||
有时候会出现连用户自己也无法确定某个操作是否会触发导航,比如当点击一个动态元素时就会存在不确定性。 | ||
* 点击链接后出现一次或多次301重定向 | ||
以上情况中,无法准确预判一个操作是否会触发导航。autoChrome中通过循环探测的方式来实现自动导航,该方案的缺点是时效不高,如果是已知的静态导航,用户可以自定义导航行为。 | ||
有时候会出现连用户自己也无法确定某个操作是否会触发导航,比如当点击一个动态元素时会存在不确定性。 | ||
```js | ||
// 等待导航键盘示例代码 | ||
await Promise.all([ | ||
page.keyboard.press("Enter"), | ||
page.waitLoad() | ||
chrome.keyboard.press("Enter"), | ||
chrome.autoNav() | ||
]) | ||
@@ -90,3 +94,3 @@ | ||
page.click("#input"), | ||
page.waitLoad() | ||
chrome.autoNav() | ||
]) | ||
@@ -187,2 +191,9 @@ ``` | ||
### chrome.autoNav(time) | ||
* `time` *Number* 等待超时时间 | ||
循环监测,自动导航 | ||
### chrome.close() | ||
@@ -193,2 +204,3 @@ | ||
## class: Page | ||
@@ -262,2 +274,6 @@ | ||
### page.clickNav(selector) | ||
通过CSS选择器点击元素,内置导航 | ||
* `selector` *String* CSS选择器 | ||
@@ -308,7 +324,7 @@ | ||
### page.goBack() | ||
### page.prev() | ||
导航到上一个历史标签页 | ||
### page.goForward() | ||
### page.next() | ||
@@ -315,0 +331,0 @@ 导航到下一个历史标签页 |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
3
17
447
84670
1979
1
- Removedws@^6.1.2
- Removedasync-limiter@1.0.1(transitive)
- Removedws@6.2.3(transitive)
Updatedloggercc@^1.3.2
Updatedtimechain@^1.3.4
Updatedzpromise@^1.5.3