Skip to content

Commit e437f3d

Browse files
authored
Support conditional actions with expectations (#1144)
* Stub out expectations with actions * Add failSilently flag * Change test for failSilently * Ensure actions only run for successful expectations, update tests * Localize type alterations * Rewrite logic, make execution of steps async, catch secondary errors and add test to validate * Remove silly error message
1 parent fae9dce commit e437f3d

File tree

9 files changed

+223
-10
lines changed

9 files changed

+223
-10
lines changed

injected/integration-test/broker-protection.spec.js

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { test } from '@playwright/test'
1+
import { test, expect } from '@playwright/test'
22
import { BrokerProtectionPage } from './page-objects/broker-protection.js'
33

44
test.describe('Broker Protection communications', () => {
@@ -490,6 +490,56 @@ test.describe('Broker Protection communications', () => {
490490
})
491491
})
492492

493+
test('expectation with actions', async ({ page }, workerInfo) => {
494+
const dbp = BrokerProtectionPage.create(page, workerInfo)
495+
await dbp.enabled()
496+
await dbp.navigatesTo('expectation-actions.html')
497+
await dbp.receivesAction('expectation-actions.json')
498+
const response = await dbp.waitForMessage('actionCompleted')
499+
500+
dbp.isSuccessMessage(response)
501+
await page.waitForURL(url => url.hash === '#1', { timeout: 2000 })
502+
})
503+
504+
test('expectation fails when failSilently is not present', async ({ page }, workerInfo) => {
505+
const dbp = BrokerProtectionPage.create(page, workerInfo)
506+
await dbp.enabled()
507+
await dbp.navigatesTo('expectation-actions.html')
508+
await dbp.receivesAction('expectation-actions-fail.json')
509+
510+
const response = await dbp.waitForMessage('actionCompleted')
511+
dbp.isErrorMessage(response)
512+
513+
const currentUrl = page.url()
514+
expect(currentUrl).not.toContain('#')
515+
})
516+
517+
test('expectation succeeds when failSilently is present', async ({ page }, workerInfo) => {
518+
const dbp = BrokerProtectionPage.create(page, workerInfo)
519+
await dbp.enabled()
520+
await dbp.navigatesTo('expectation-actions.html')
521+
await dbp.receivesAction('expectation-actions-fail-silently.json')
522+
523+
const response = await dbp.waitForMessage('actionCompleted')
524+
dbp.isSuccessMessage(response)
525+
526+
const currentUrl = page.url()
527+
expect(currentUrl).not.toContain('#')
528+
})
529+
530+
test('expectation succeeds but subaction fails should throw error', async ({ page }, workerInfo) => {
531+
const dbp = BrokerProtectionPage.create(page, workerInfo)
532+
await dbp.enabled()
533+
await dbp.navigatesTo('expectation-actions.html')
534+
await dbp.receivesAction('expectation-actions-subaction-fail.json')
535+
536+
const response = await dbp.waitForMessage('actionCompleted')
537+
dbp.isErrorMessage(response)
538+
539+
const currentUrl = page.url()
540+
expect(currentUrl).not.toContain('#')
541+
})
542+
493543
test.describe('retrying', () => {
494544
test('retrying a click', async ({ page }, workerInfo) => {
495545
const dbp = BrokerProtectionPage.create(page, workerInfo)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"state": {
3+
"action": {
4+
"actionType": "expectation",
5+
"id": "2",
6+
"expectations": [
7+
{
8+
"type": "text",
9+
"selector": "body",
10+
"expect": "How old is Jane Doe?",
11+
"failSilently": true
12+
}
13+
],
14+
"actions": [
15+
{
16+
"actionType": "click",
17+
"elements": [
18+
{
19+
"type": "button",
20+
"selector": ".view-more"
21+
}
22+
]
23+
}
24+
]
25+
}
26+
}
27+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"state": {
3+
"action": {
4+
"actionType": "expectation",
5+
"id": "2",
6+
"retry": {
7+
"environment": "web",
8+
"interval": { "ms": 1000 },
9+
"maxAttempts": 1
10+
},
11+
"expectations": [
12+
{
13+
"type": "text",
14+
"selector": "body",
15+
"expect": "How old is Jane Doe?"
16+
}
17+
],
18+
"actions": [
19+
{
20+
"actionType": "click",
21+
"elements": [
22+
{
23+
"type": "button",
24+
"selector": ".view-more"
25+
}
26+
]
27+
}
28+
]
29+
}
30+
}
31+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"state": {
3+
"action": {
4+
"actionType": "expectation",
5+
"id": "2",
6+
"retry": {
7+
"environment": "web",
8+
"interval": { "ms": 1000 },
9+
"maxAttempts": 1
10+
},
11+
"expectations": [
12+
{
13+
"type": "text",
14+
"selector": "body",
15+
"expect": "How old is John Doe?"
16+
}
17+
],
18+
"actions": [
19+
{
20+
"actionType": "click",
21+
"elements": [
22+
{
23+
"type": "button",
24+
"selector": ".view-less"
25+
}
26+
]
27+
}
28+
]
29+
}
30+
}
31+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"state": {
3+
"action": {
4+
"actionType": "expectation",
5+
"id": "2",
6+
"expectations": [
7+
{
8+
"type": "text",
9+
"selector": "body",
10+
"expect": "How old is John Doe?"
11+
}
12+
],
13+
"actions": [
14+
{
15+
"actionType": "click",
16+
"elements": [
17+
{
18+
"type": "button",
19+
"selector": ".view-more"
20+
}
21+
]
22+
}
23+
]
24+
}
25+
}
26+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width">
6+
<title>Broker Protection</title>
7+
</head>
8+
<body>
9+
<div class="result">
10+
How old is John Doe?
11+
<a href="#" class="view-more" data-id="1">View More</a>
12+
</div>
13+
<script>
14+
document.querySelector('.view-more').addEventListener('click', function(e) {
15+
e.preventDefault();
16+
const id = this.getAttribute('data-id');
17+
18+
window.location.hash = id;
19+
this.removeEventListener('click', arguments.callee);
20+
})
21+
</script>
22+
</body>
23+
</html>

injected/src/features/broker-protection/actions/expectation.js

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,48 @@
11
import { getElement } from '../utils.js'
22
import { ErrorResponse, SuccessResponse } from '../types.js'
3+
import { execute } from '../execute.js'
34

45
/**
56
* @param {Record<string, any>} action
6-
* @param {Document | HTMLElement} root
7-
* @return {import('../types.js').ActionResponse}
7+
* @param {Record<string, any>} userData
8+
* @param {Document} root
9+
* @return {Promise<import('../types.js').ActionResponse>}
810
*/
9-
export function expectation (action, root = document) {
11+
export async function expectation (action, userData, root = document) {
1012
const results = expectMany(action.expectations, root)
1113

12-
const errors = results.filter(x => x.result === false).map(x => {
13-
if ('error' in x) return x.error
14-
return 'unknown error'
15-
})
14+
// filter out good results + silent failures, leaving only fatal errors
15+
const errors = results
16+
.filter((x, index) => {
17+
if (x.result === true) return false
18+
if (action.expectations[index].failSilently) return false
19+
return true
20+
}).map((x) => {
21+
return 'error' in x ? x.error : 'unknown error'
22+
})
1623

1724
if (errors.length > 0) {
1825
return new ErrorResponse({ actionID: action.id, message: errors.join(', ') })
1926
}
2027

28+
// only run later actions if every expectation was met
29+
const runActions = results.every(x => x.result === true)
30+
const secondaryErrors = []
31+
32+
if (action.actions?.length && runActions) {
33+
for (const subAction of action.actions) {
34+
const result = await execute(subAction, userData, root)
35+
36+
if ('error' in result) {
37+
secondaryErrors.push(result.error)
38+
}
39+
}
40+
41+
if (secondaryErrors.length > 0) {
42+
return new ErrorResponse({ actionID: action.id, message: secondaryErrors.join(', ') })
43+
}
44+
}
45+
2146
return new SuccessResponse({ actionID: action.id, actionType: action.actionType, response: null })
2247
}
2348

injected/src/features/broker-protection/execute.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export async function execute (action, inputData, root = document) {
2525
case 'click':
2626
return click(action, data(action, inputData, 'userProfile'), root)
2727
case 'expectation':
28-
return expectation(action, root)
28+
return await expectation(action, data(action, inputData, 'userProfile'), root)
2929
case 'fillForm':
3030
return fillForm(action, data(action, inputData, 'extractedProfile'), root)
3131
case 'getCaptchaInfo':

injected/src/features/broker-protection/types.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* @typedef {SuccessResponse | ErrorResponse} ActionResponse
33
* @typedef {{ result: true } | { result: false; error: string }} BooleanResult
4-
* @typedef {{type: "element" | "text" | "url"; selector: string; parent?: string; expect?: string}} Expectation
4+
* @typedef {{type: "element" | "text" | "url"; selector: string; parent?: string; expect?: string; failSilently?: boolean}} Expectation
55
*/
66

77
/**

0 commit comments

Comments
 (0)