Skip to content

Commit 8139dee

Browse files
authored
Compose action (#53)
* proper handling of exceptions. * action-router model * Now we throw errors and action is more error-prone. * compose action * Fix everything... * Working service! * Docs * Fixes after review * Fixes after review
1 parent b37e0ae commit 8139dee

30 files changed

+406
-290
lines changed

README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,51 @@ Example request body:
149149
}
150150
```
151151

152+
### **/compose**
153+
154+
This POST method allows to combine several puppeteer actions into one.
155+
Note that the method does not expect nested composite actions inside its body.
156+
157+
Example request body:
158+
```json5
159+
{
160+
"actions": [
161+
{
162+
"endpoint": "goto",
163+
"body": {
164+
"url": "<URL>",
165+
"harRecording": false,
166+
},
167+
},
168+
{
169+
"endpoint": "click",
170+
"body": {
171+
"selector": "<SELECTOR>",
172+
},
173+
},
174+
{
175+
"endpoint": "click",
176+
"body": {
177+
"selector": "<SELECTOR>",
178+
},
179+
},
180+
{
181+
"endpoint": "scroll",
182+
"body": {},
183+
},
184+
{
185+
"endpoint": "screenshot",
186+
"body": {
187+
"options": {
188+
"full_page": true,
189+
"type": "jpeg",
190+
},
191+
},
192+
}
193+
],
194+
}
195+
```
196+
152197
### **/scroll**
153198

154199
This POST method allows to scroll page to the first element that is matched by selector and returns page result.

actions/action.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
const exceptions = require('../helpers/exceptions');
2+
const utils = require('../helpers/utils'); // For usage inside user's action(page, request) function
3+
4+
/**
5+
* Content-Type: application/javascript
6+
* body = js function as pattern:
7+
* async function action(page, request) {
8+
* ...
9+
* some actions with page in puppeteer syntax
10+
* ...
11+
* return {
12+
* context_id: page.browserContext().id,
13+
* page_id: page.target()._targetId,
14+
* html: await page.content(),
15+
* cookies: await page.cookies()
16+
* };
17+
* };
18+
*/
19+
exports.action = async function action(page, request) {
20+
eval(request.body.toString());
21+
22+
// check action function existence
23+
if (!(typeof action === "function" && action.length >= 1)) {
24+
throw new exceptions.IncorrectArgumentError("Invalid action function.\n" +
25+
"Valid action function: \"async function action(page, request) " +
26+
"{ ... some actions with request and page in puppeteer " +
27+
"syntax};\"");
28+
}
29+
30+
return {
31+
data: await action(page, request)
32+
}
33+
}

actions/click.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
const utils = require('../helpers/utils');
2+
3+
const DEFAULT_TIMEOUT = 1000; // 1 second
4+
5+
/*
6+
* body = {
7+
* "selector": "", // <string> A selector to search for element to click. If there are multiple elements satisfying the selector, the first will be clicked.
8+
* "clickOptions": {
9+
* "button", // <"left"|"right"|"middle"> Defaults to left.
10+
* "clickCount", // <number> defaults to 1.
11+
* "delay" // <number> Time to wait between mousedown and mouseup in milliseconds. Defaults to 0.
12+
* },
13+
* "waitOptions": {...}, // same as in goto action, defaults to 1s timeout
14+
* "navigationOptions": {...} // same as in goto action
15+
* }
16+
*/
17+
exports.click = async function click(page, request) {
18+
await page.hover(request.body.selector);
19+
if (request.body.navigationOptions) {
20+
await Promise.all([
21+
page.waitForNavigation(request.body.navigationOptions),
22+
page.click(request.body.selector, request.body.clickOptions),
23+
]);
24+
} else {
25+
await page.click(request.body.selector, request.body.clickOptions);
26+
}
27+
const waitOptions = request.body.waitOptions || { timeout: DEFAULT_TIMEOUT };
28+
return await utils.getContents(page, waitOptions);
29+
}

actions/compose.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
endpoint2action = {
2+
action: require("./action").action,
3+
click: require("./click").click,
4+
fill_form: require("./fill_form").fillForm,
5+
back: require("./goback").goBack,
6+
forward: require("./goforward").goForward,
7+
goto: require("./goto").goto,
8+
har: require("./har").har,
9+
mhtml: require("./mhtml").captureSnapshot,
10+
recaptcha_solver: require("./recaptcha_solver").recaptchaSolver,
11+
screenshot: require("./screenshot").screenshot,
12+
scroll: require("./scroll").scroll,
13+
}
14+
15+
async function compose(page, request) {
16+
const originalClosePage = request.query.closePage;
17+
const originalBody = structuredClone(request.body);
18+
19+
request.query.closePage = false;
20+
delete request.body["actions"];
21+
22+
let response;
23+
try {
24+
for (const action of originalBody["actions"]) {
25+
request.body = action["body"];
26+
response = await endpoint2action[action["endpoint"]](page, request);
27+
}
28+
} finally {
29+
request.query.closePage = originalClosePage;
30+
request.body = originalBody;
31+
}
32+
33+
return response;
34+
}
35+
exports.compose = compose;

actions/fill_form.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
const utils = require('../helpers/utils');
2+
3+
/*
4+
* body = {
5+
* "inputMapping": { A dictionary where each key is a CSS selector, and each value is another dictionary containing details about the input for that element:
6+
* "selector": <string> The CSS selector for the input element (used as the key).
7+
* "value": <string> The text to be inputted into the element.
8+
* "delay": <number> A delay (in milliseconds) between each keystroke when inputting the text. Defaults to 0 if not provided.
9+
* },
10+
* "submitButton": <string> The CSS selector for the form's submit button. If provided, the button will be clicked after filling in the form.
11+
* }
12+
*/
13+
exports.fillForm = async function fillForm(page, request) {
14+
const inputMapping = request.body.inputMapping;
15+
const submitButton = request.body.submitButton;
16+
17+
for (const [selector, params] of Object.entries(inputMapping)) {
18+
const value = params.value;
19+
const delay = params.delay || 0;
20+
await page.type(selector, value, { delay });
21+
}
22+
23+
if (submitButton) {
24+
await page.click(submitButton);
25+
}
26+
27+
return await utils.getContents(page);
28+
29+
}

actions/goback.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
const utils = require('../helpers/utils');
2+
3+
exports.goBack = async function goBack(page, request) {
4+
await page.goBack(request.body.navigationOptions);
5+
return await utils.getContents(page, request.body.waitOptions);
6+
}

actions/goforward.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
const utils = require('../helpers/utils');
2+
3+
exports.goForward = async function goForward(page, request) {
4+
await page.goForward(request.body.navigationOptions);
5+
return await utils.getContents(page, request.body.waitOptions);
6+
}

actions/goto.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
const utils = require('../helpers/utils');
2+
3+
/*
4+
* body = {
5+
* "url": <string> URL to navigate page to. The url should include scheme, e.g. https://.
6+
* "navigationOptions": { Navigation parameters which might have the following properties:
7+
* "timeout": <number> Maximum navigation time in milliseconds, defaults to 30 seconds, pass 0 to disable timeout. The default value can be changed by using the page.setDefaultNavigationTimeout(timeout) or page.setDefaultTimeout(timeout) methods.
8+
* "waitUntil": <string|Array<string>> When to consider navigation succeeded, defaults to load. Given an array of event strings, navigation is considered to be successful after all events have been fired. Events can be either:
9+
* load - consider navigation to be finished when the load event is fired.
10+
* domcontentloaded - consider navigation to be finished when the DOMContentLoaded event is fired.
11+
* networkidle0 - consider navigation to be finished when there are no more than 0 network connections for at least 500 ms.
12+
* networkidle2 - consider navigation to be finished when there are no more than 2 network connections for at least 500 ms.
13+
* "referer" <string> Referer header value. If provided it will take preference over the referer header value set by page.setExtraHTTPHeaders().
14+
* },
15+
* "waitOptions": {
16+
* "timeout": <number> Wait for given timeout in milliseconds
17+
* "selector": <string> Wait for element by selector (see https://pptr.dev/api/puppeteer.page.waitforselector)
18+
* "xpath": <string> Wait for element by xpath (see https://pptr.dev/api/puppeteer.page.waitforxpath)
19+
* "options": <object> Options to wait for elements (see https://pptr.dev/api/puppeteer.waitforselectoroptions)
20+
* },
21+
* "harRecording": true,
22+
* }
23+
*/
24+
exports.goto = async function goto(page, request) {
25+
await page.goto(request.body.url, request.body.navigationOptions);
26+
return await utils.getContents(page, request.body.waitOptions);
27+
}

actions/har.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
const exceptions = require("../helpers/exceptions");
2+
3+
exports.har = async function har(page, request) {
4+
if (!(page.harWriter)){
5+
throw new exceptions.NoHarWriterError();
6+
}
7+
8+
return {
9+
har: JSON.stringify(await page.harWriter.stop()) // TODO: do we really need JSON.stringify?
10+
};
11+
}

actions/mhtml.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/*
2+
* Captures mhtml snapshot of a page
3+
*/
4+
exports.captureSnapshot = async function captureSnapshot(page, request) {
5+
const cdpSession = await page.target().createCDPSession();
6+
const { data } = await cdpSession.send('Page.captureSnapshot', { format: 'mhtml' });
7+
await cdpSession.detach()
8+
return {
9+
mhtml: data,
10+
};
11+
}

0 commit comments

Comments
 (0)