UNPKG

7.23 kBJavaScriptView Raw
1const debug = require('debug')('Actions');
2const merge = require('lodash.merge');
3const Storage = require('./Storage');
4const actionsFactory = require('./actions/actionsFactory');
5const wait = require('./tools/wait');
6
7class Actions {
8 /**
9 * @param {Object} options
10 * @param {AbstractEnvironment} options.environment
11 * @param {Parser} options.parser
12 * @param {Storage} options.storage
13 */
14 constructor(options) {
15 this._env = options.environment;
16 this._parser = options.parser;
17 this._storage = options.storage || new Storage();
18 }
19
20 /**
21 * Perform parsing rule
22 * @param {Rule} rule
23 * @param {string?} parentSelector
24 * @returns {Promise}
25 */
26 async performForRule(rule, parentSelector) {
27 const actions = rule.actions;
28 const possibleErrors = rule.catchError || {};
29
30 if (!actions) {
31 return Promise.resolve();
32 }
33
34 try {
35 return this.performActions(actions, parentSelector);
36 } catch (e) {
37 debug('Catching possible errors %o', possibleErrors);
38 if (!(e instanceof Error) || !possibleErrors[e.name]) {
39 debug('Handler for %o not found', e);
40 throw e;
41 }
42
43 return this._handleError(possibleErrors[e.name], args);
44 }
45 }
46
47 /**
48 * Handle action error
49 * @param {object} handlerOptions
50 * @param {number} handlerOptions.handler Handler name
51 * @param {number} handlerOptions.attempts Number of attempts before cancel with error
52 * @param {number} handlerOptions.__attempt Current attempt number
53 * @param actionArgs
54 * @return {Promise}
55 * @private
56 */
57 async _handleError(handlerOptions, actionArgs) {
58 debug('Handle error with rules %o', handlerOptions);
59 switch (handlerOptions.handler) {
60 case 'repeat':
61 handlerOptions.__attempt = handlerOptions.__attempt || 0;
62 if (++handlerOptions.__attempt > handlerOptions.attempts) {
63 throw new Error('Max attempts limit exceeded');
64 }
65 const result = await this.performForRule(...actionArgs);
66 delete handlerOptions.__attempt;
67 return result;
68
69 default:
70 throw new Error('Unknown handler ' + handlerOptions.handler);
71 }
72 }
73
74 /**
75 * Perform parsing rule
76 * @param {Rule} rule
77 * @param {string} parentSelector
78 * @returns {Promise}
79 */
80 async performPostActionsForRule(rule, parentSelector) {
81 const actions = rule.postActions;
82
83 if (!actions) {
84 return Promise.resolve();
85 }
86
87 return this.performActions(actions, parentSelector);
88 }
89
90 /**
91 * Perform array of actions
92 * @param {Array} actions
93 * @param {string} [parentSelector]
94 * @returns {Promise}
95 */
96 async performActions(actions, parentSelector) {
97 if (!Array.isArray(actions)) {
98 throw new Error('actions must be an Array');
99 }
100
101 debug('Perform actions %o', actions);
102
103 if (!parentSelector) {
104 parentSelector = 'body';
105 debug('Parent scope switched to %s', parentSelector);
106 }
107
108 return actions.reduce(async (promise, action) => {
109 if (action.once && action.__done) {
110 return promise;
111 }
112
113 const prevResult = await promise;
114 const result = await this.performAction(action, parentSelector, prevResult);
115 action.__done = true;
116 return result;
117 }, Promise.resolve());
118 }
119
120 /**
121 * @param {ActionOptions} action
122 * @param {string} parentSelector
123 * @param {?*} prevResult
124 * @returns {Promise}
125 */
126 async performAction(action, parentSelector, prevResult) {
127 const selector = (action.parentScope || parentSelector || '') + ' ' + (action.scope || '');
128 debug('Perform action %o for generated selector %s', action, selector);
129
130 let waitForPromise = Promise.resolve();
131 if (action.waitForPage || action.type === 'back') {
132 waitForPromise = this.performAction({
133 type: 'waitForPage',
134 timeout: action.waitForPageTimeout
135 }, parentSelector, prevResult);
136 }
137
138 if (action.waitForQuery) {
139 const waitAction = merge({}, action.waitForQuery, {
140 type: this.TYPES.WAIT_FOR_QUERY
141 });
142 waitForPromise = this.performAction(waitAction, parentSelector, prevResult);
143 }
144
145 if (action.waitFor) {
146 let waitFor = typeof action.waitFor === 'string' ?
147 { type: action.waitFor } : action.waitFor;
148 waitFor = merge({}, waitFor, {
149 type: `waitFor${waitFor.type.charAt(0).toUpperCase() + waitFor.type.slice(1)}`,
150 });
151 waitForPromise = this.performAction(waitFor, parentSelector, prevResult);
152 }
153
154 if (action.cases && action.type !== 'cases') {
155 waitForPromise = this.performAction({
156 type: 'cases',
157 cases: action.cases,
158 }, parentSelector, prevResult);
159 }
160
161 // mutation for if-then-else action
162 if (action.conditions) {
163 action.type = 'condition';
164 }
165
166 const actionInstance = this._createInstance(action, selector, parentSelector, prevResult);
167
168 if (!actionInstance) {
169 Promise.reject(new Error('Unknown action type: ' + action.type));
170 return;
171 }
172
173 let result = await actionInstance.perform();
174 const actionResult = await waitForPromise;
175
176 // mutation for transform action
177 if (action.transform) {
178 result = this._parser.transform(result, action.transform);
179 }
180
181 // mutation for set action
182 if (action.set) {
183 this._storage.set(action.set, result);
184 }
185
186 return actionResult || result;
187 }
188
189 /**
190 * @param action
191 * @param selector
192 * @param parentSelector
193 * @param prevResult
194 * @return {Action}
195 * @private
196 */
197 _createInstance(action, selector, parentSelector, prevResult) {
198 return actionsFactory.createAction({
199 selector,
200 actionOptions: action,
201 parentSelector,
202 prevResult,
203 env: this._env,
204 parser: this._parser,
205 actions: this
206 });
207 }
208
209 /**
210 * Add custom action
211 * @param {string} type
212 * @param {Function} action
213 */
214 addAction(type, action) {
215 actionsFactory.addAction(type, action);
216 }
217
218 async click(selector) {
219 return this.performAction({
220 type: 'click',
221 scope: selector
222 }, '');
223 }
224
225 /**
226 * Perform page scroll-down
227 * @param {number} interval
228 * @returns {Promise}
229 */
230 async scroll(interval) {
231 debug('scroll %s px', interval);
232 return this._env.evaluateJs(interval, /* istanbul ignore next */ function (interval) {
233 document.body.scrollTop += interval;
234 });
235 }
236}
237
238module.exports = Actions;