Skip to content

Commit 68c77bf

Browse files
added capability to perform soft validation of element condition validations (#153)
1 parent cd32844 commit 68c77bf

File tree

8 files changed

+464
-438
lines changed

8 files changed

+464
-438
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how
1414

1515
:microscope: - experimental
1616

17+
18+
## [2.6.0]
19+
- :rocket: added capability to perform soft validation of element condition validations
20+
```gherkin
21+
Scenario Outline: wait for condition
22+
Then I expect 'Element' to be softly present
23+
```
24+
1725
## [2.5.0]
1826
Breaking change:
1927
- :rocket: added step `I will wait for alert/dialog`

package-lock.json

Lines changed: 399 additions & 383 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@qavajs/steps-playwright",
3-
"version": "2.5.0",
3+
"version": "2.6.0",
44
"description": "steps to interact with playwright",
55
"main": "./index.js",
66
"scripts": {
@@ -27,23 +27,24 @@
2727
"homepage": "https://github.com/qavajs/steps-playwright#readme",
2828
"devDependencies": {
2929
"@cucumber/cucumber": "^11.2.0",
30-
"@qavajs/core": "^2.4.0",
3130
"@qavajs/console-formatter": "^1.0.0",
31+
"@qavajs/core": "^2.5.0",
3232
"@qavajs/html-formatter": "^0.18.1",
3333
"@qavajs/memory": "^1.10.2",
34+
"@qavajs/validation": "^1.2.1",
3435
"@qavajs/webstorm-adapter": "^8.0.0",
35-
"@types/chai": "^4.3.20",
36+
"@types/chai": "^5.2.2",
3637
"@types/express": "^5.0.1",
37-
"@vitest/coverage-v8": "^3.1.1",
38-
"@vitest/ui": "^3.1.1",
39-
"electron": "^35.1.5",
38+
"@types/node": "^22.15.12",
39+
"@vitest/coverage-v8": "^3.1.3",
40+
"@vitest/ui": "^3.1.3",
41+
"electron": "^36.1.0",
4042
"express": "^5.1.0",
4143
"ts-node": "^10.9.2",
4244
"typescript": "^5.8.3",
43-
"vitest": "^3.1.1",
44-
"@qavajs/validation": "^1.1.1"
45+
"vitest": "^3.1.3"
4546
},
4647
"dependencies": {
47-
"@playwright/test": "^1.51.1"
48+
"@playwright/test": "^1.52.0"
4849
}
4950
}

src/conditionWait.ts

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import { Locator, expect } from '@playwright/test';
2-
import { throwTimeoutError } from './utils/utils';
2+
3+
class SoftAssertionError extends Error {
4+
name = 'SoftAssertionError';
5+
}
36

47
export const conditionValidations = {
58
PRESENT: 'present',
6-
// CLICKABLE: 'clickable',
9+
CLICKABLE: 'clickable',
710
VISIBLE: 'visible',
811
INVISIBLE: 'invisible',
912
IN_VIEWPORT: 'in viewport',
@@ -13,57 +16,58 @@ export const conditionValidations = {
1316

1417
const notClause = '(not )?';
1518
const toBeClause = 'to (?:be )?';
19+
const softClause = '(softly )?'
1620
const validationClause = `(${Object.values(conditionValidations).join('|')})`;
1721

18-
export const conditionWaitExtractRegexp = new RegExp(`^${notClause}${toBeClause}${validationClause}$`);
19-
export const conditionWaitRegexp = new RegExp(`(${notClause}${toBeClause}${validationClause})`);
22+
export const conditionWaitExtractRegexp = new RegExp(`^${notClause}${toBeClause}${softClause}${validationClause}$`);
2023

24+
const makeExpect = (element: Locator, reverse: boolean, message: string) => {
25+
const eMessage = expect(element, message);
26+
return reverse ? eMessage.not : eMessage;
27+
}
2128
const waits = {
2229
[conditionValidations.PRESENT]: (
2330
element: Locator,
2431
reverse: boolean,
2532
timeout: number,
2633
timeoutMsg: string
27-
) => element.waitFor({ state: reverse ? 'detached' : 'attached', timeout }),
34+
) => makeExpect(element, reverse, timeoutMsg).toBeAttached({ timeout }),
2835
[conditionValidations.VISIBLE]: (
2936
element: Locator,
3037
reverse: boolean,
3138
timeout: number,
3239
timeoutMsg: string
33-
) => element.waitFor({ state: reverse ? 'hidden' : 'visible', timeout }),
40+
) => makeExpect(element, reverse, timeoutMsg).toBeVisible({ timeout }),
3441
[conditionValidations.INVISIBLE]: (
3542
element: Locator,
3643
reverse: boolean,
3744
timeout: number,
3845
timeoutMsg: string
39-
) => element.waitFor({ state: reverse ? 'visible' : 'hidden', timeout }),
46+
) => makeExpect(element, reverse, timeoutMsg).toBeHidden({ timeout }),
4047
[conditionValidations.IN_VIEWPORT]: (
4148
element: Locator,
4249
reverse: boolean,
4350
timeout: number,
4451
timeoutMsg: string
45-
) => throwTimeoutError(() => expect(async () => {
46-
const e = reverse ? expect(element).not : expect(element);
47-
await e.toBeInViewport();
48-
}).toPass({ timeout }), timeoutMsg),
52+
) => makeExpect(element, reverse, timeoutMsg).toBeInViewport({ timeout }),
4953
[conditionValidations.ENABLED]: (
5054
element: Locator,
5155
reverse: boolean,
5256
timeout: number,
5357
timeoutMsg: string
54-
) => throwTimeoutError(() => expect(async () => {
55-
const e = reverse ? expect(element).not : expect(element);
56-
await e.toBeEnabled();
57-
}).toPass({ timeout }), timeoutMsg),
58+
) => makeExpect(element, reverse, timeoutMsg).toBeEnabled({ timeout }),
5859
[conditionValidations.DISABLED]: (
5960
element: Locator,
6061
reverse: boolean,
6162
timeout: number,
6263
timeoutMsg: string
63-
) => throwTimeoutError(() => expect(async () => {
64-
const e = reverse ? expect(element).not : expect(element);
65-
await e.toBeDisabled();
66-
}).toPass({ timeout }), timeoutMsg)
64+
) => makeExpect(element, reverse, timeoutMsg).toBeDisabled({ timeout }),
65+
[conditionValidations.CLICKABLE]: (
66+
element: Locator,
67+
reverse: boolean,
68+
timeout: number,
69+
timeoutMsg: string
70+
) => makeExpect(element, reverse, timeoutMsg).toBeEnabled({ timeout }),
6771
}
6872
/**
6973
* Wait for condition
@@ -78,8 +82,22 @@ export async function conditionWait(
7882
validationType: string,
7983
timeout: number = 10000,
8084
reverse: boolean = false
81-
) {
85+
): Promise<void> {
8286
const timeoutMsg: string = `Element is${reverse ? '' : ' not'} ${validationType}`;
8387
const waitFn = waits[validationType];
8488
await waitFn(element, reverse, timeout, timeoutMsg);
8589
}
90+
91+
export function getConditionWait(condition: string): Function {
92+
const match = condition.match(conditionWaitExtractRegexp) as RegExpMatchArray;
93+
if (!match) throw new Error(`${condition} wait is not implemented`);
94+
const [_, reverse, soft, validation] = match;
95+
return async function (element: Locator, timeout: number) {
96+
try {
97+
await conditionWait(element, validation, timeout, Boolean(reverse))
98+
} catch (error) {
99+
if (soft && error instanceof Error) throw new SoftAssertionError(error.message, { cause: error });
100+
throw error;
101+
}
102+
}
103+
}

src/types.ts

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,5 @@
11
import { defineParameterType } from '@cucumber/cucumber';
2-
import { conditionWait, conditionWaitExtractRegexp } from './conditionWait';
3-
import { type Locator } from '@playwright/test';
4-
5-
export function getConditionWait(condition: string): Function {
6-
const match = condition.match(conditionWaitExtractRegexp) as RegExpMatchArray;
7-
if (!match) throw new Error(`${condition} wait is not implemented`);
8-
const [_, reverse, validation] = match;
9-
return async function (element: Locator, timeout: number) {
10-
await conditionWait(element, validation, timeout, Boolean(reverse))
11-
}
12-
}
2+
import { getConditionWait } from './conditionWait';
133

144
function transformString(fn: (value: string) => any) {
155
return function (s1: string, s2: string) {
@@ -31,7 +21,7 @@ defineParameterType({
3121

3222
defineParameterType({
3323
name: 'playwrightCondition',
34-
regexp: /((not )?to (?:be )?(present|clickable|visible|invisible|enabled|disabled|in viewport))/,
24+
regexp: /((not )?to (?:be )?(?:softly )?(present|clickable|visible|invisible|enabled|disabled|in viewport))/,
3525
transformer: getConditionWait,
3626
useForSnippets: false
3727
});

src/utils/utils.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -56,17 +56,6 @@ export function equalOrIncludes(value: string | string[], argument: string) {
5656
: value === argument;
5757
}
5858

59-
export async function throwTimeoutError(fn: Function, message: string) {
60-
try {
61-
await fn()
62-
} catch (err: any) {
63-
if (err.message.includes('exceeded while waiting on the predicate')) {
64-
throw new Error(message);
65-
}
66-
throw err
67-
}
68-
}
69-
7059
/**
7160
* Parse 'x, y' string to coordinates object
7261
* @param {string} coords - 'x, y' string

test-e2e/features/pollValidations.feature

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@ Feature: waits
77
Then I expect '<element>' <condition>
88

99
Examples:
10-
| element | condition |
11-
| Present Element | to be present |
10+
| element | condition |
11+
| Present Element | to be present |
12+
| Detach Element | not to be present |
13+
| Not Existing Element | not to be present |
14+
| Visible Element | to be visible |
15+
| Visible Element | to be clickable |
1216

1317
Scenario Outline: wait for text (<condition>)
1418
Then I expect text of 'Loading' <condition> '<expectation>'

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"test/**/*.spec.ts"
77
],
88
"compilerOptions": {
9-
"target": "es2018",
9+
"target": "es2022",
1010
"module": "node16",
1111
"moduleResolution": "node16",
1212
"outDir": "./lib",

0 commit comments

Comments
 (0)