@letsrunit/playwright 0.4.2 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +78 -2
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/field/aria-select.ts +33 -0
- package/src/field/calendar.ts +2 -1
- package/src/field/index.ts +4 -0
- package/src/field/radio-group.ts +21 -2
- package/src/field/slider.ts +33 -0
- package/src/field/toggle.ts +15 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@letsrunit/playwright",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Playwright extensions and utilities for letsrunit",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"testing",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
},
|
|
45
45
|
"packageManager": "yarn@4.10.3",
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@letsrunit/utils": "0.
|
|
47
|
+
"@letsrunit/utils": "0.5.1",
|
|
48
48
|
"@playwright/test": "^1.57.0",
|
|
49
49
|
"case": "^1.6.3",
|
|
50
50
|
"diff": "^8.0.3",
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Loc, SetOptions, Value } from './types';
|
|
2
|
+
|
|
3
|
+
export async function selectAria({ el }: Loc, value: Value, options?: SetOptions): Promise<boolean> {
|
|
4
|
+
if (typeof value !== 'string' && typeof value !== 'number') return false;
|
|
5
|
+
|
|
6
|
+
const role = await el.getAttribute('role', options).catch(() => null);
|
|
7
|
+
if (role !== 'combobox') return false;
|
|
8
|
+
|
|
9
|
+
const ariaControls = await el.getAttribute('aria-controls', options).catch(() => null);
|
|
10
|
+
if (!ariaControls) return false;
|
|
11
|
+
|
|
12
|
+
const ariaExpanded = await el.getAttribute('aria-expanded', options).catch(() => null);
|
|
13
|
+
if (ariaExpanded !== 'true') await el.click(options);
|
|
14
|
+
|
|
15
|
+
const stringValue = String(value);
|
|
16
|
+
const listbox = el.page().locator(`#${ariaControls}`);
|
|
17
|
+
|
|
18
|
+
// 1. By value attribute
|
|
19
|
+
const byValue = listbox.locator(`[role="option"][value="${stringValue}"]`);
|
|
20
|
+
if ((await byValue.count()) >= 1) {
|
|
21
|
+
await byValue.first().click(options);
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 2. By accessible name (case-insensitive)
|
|
26
|
+
const byName = listbox.getByRole('option', { name: stringValue });
|
|
27
|
+
if ((await byName.count()) >= 1) {
|
|
28
|
+
await byName.first().click(options);
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return false;
|
|
33
|
+
}
|
package/src/field/calendar.ts
CHANGED
|
@@ -8,8 +8,9 @@ type MonthYear = { month: number; year: number };
|
|
|
8
8
|
|
|
9
9
|
async function getDialog(root: Locator, options?: SetOptions) {
|
|
10
10
|
const role = await root.getAttribute('role', options).catch(() => null);
|
|
11
|
+
const hasPopup = await root.getAttribute('aria-haspopup', options).catch(() => null);
|
|
11
12
|
|
|
12
|
-
if (role !== 'combobox') return null;
|
|
13
|
+
if (role !== 'combobox' && !hasPopup) return null;
|
|
13
14
|
|
|
14
15
|
const ariaControls = await root.getAttribute('aria-controls', options).catch(() => null);
|
|
15
16
|
if (!ariaControls) return null;
|
package/src/field/index.ts
CHANGED
|
@@ -7,10 +7,12 @@ import { setDateTextInput } from './date-text-input';
|
|
|
7
7
|
import { setNativeCheckbox } from './native-checkbox';
|
|
8
8
|
import { setNativeDate } from './native-date';
|
|
9
9
|
import { setNativeInput } from './native-input';
|
|
10
|
+
import { selectAria } from './aria-select';
|
|
10
11
|
import { selectNative } from './native-select';
|
|
11
12
|
import { setOtpValue } from './otp';
|
|
12
13
|
import { setRadioGroup } from './radio-group';
|
|
13
14
|
import { setSliderValue } from './slider';
|
|
15
|
+
import { setToggle } from './toggle';
|
|
14
16
|
import type { Loc, SetOptions, Value } from './types';
|
|
15
17
|
|
|
16
18
|
function toString(value: Value): string {
|
|
@@ -28,11 +30,13 @@ export async function setFieldValue(el: Locator, value: Value, options?: SetOpti
|
|
|
28
30
|
const setValue = chain(
|
|
29
31
|
// native
|
|
30
32
|
selectNative,
|
|
33
|
+
selectAria,
|
|
31
34
|
setNativeCheckbox,
|
|
32
35
|
setRadioGroup,
|
|
33
36
|
setNativeDate,
|
|
34
37
|
setNativeInput,
|
|
35
38
|
// aria / components
|
|
39
|
+
setToggle,
|
|
36
40
|
setDateTextInput,
|
|
37
41
|
setDateGroup,
|
|
38
42
|
setCalendarDate,
|
package/src/field/radio-group.ts
CHANGED
|
@@ -10,18 +10,37 @@ export async function setRadioGroup(
|
|
|
10
10
|
const stringValue = String(value);
|
|
11
11
|
if (stringValue.includes('\n') || stringValue.includes('"')) return false;
|
|
12
12
|
|
|
13
|
+
// 1. Native: exact value attribute
|
|
13
14
|
const radio = el.locator(`input[type=radio][value="${stringValue}"]`);
|
|
14
15
|
if ((await radio.count()) === 1) {
|
|
15
16
|
await radio.check(options);
|
|
16
17
|
return true;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
//
|
|
20
|
-
const radioByLabel = el.getByLabel(
|
|
20
|
+
// 2. Native: by label text
|
|
21
|
+
const radioByLabel = el.getByLabel(stringValue, { exact: true }).locator('input[type=radio]');
|
|
21
22
|
if ((await radioByLabel.count()) === 1) {
|
|
22
23
|
await radioByLabel.check(options);
|
|
23
24
|
return true;
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
// 3. ARIA: exact value attribute
|
|
28
|
+
const ariaRadio = el.locator(`[role="radio"][value="${stringValue}"]`);
|
|
29
|
+
if ((await ariaRadio.count()) >= 1) {
|
|
30
|
+
const item = ariaRadio.first();
|
|
31
|
+
const ariaChecked = await item.getAttribute('aria-checked', options).catch(() => null);
|
|
32
|
+
if (ariaChecked !== 'true') await item.click(options);
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 4. ARIA: by label text
|
|
37
|
+
const ariaRadioByLabel = el.getByLabel(stringValue, { exact: true }).locator('[role="radio"]');
|
|
38
|
+
if ((await ariaRadioByLabel.count()) >= 1) {
|
|
39
|
+
const item = ariaRadioByLabel.first();
|
|
40
|
+
const ariaChecked = await item.getAttribute('aria-checked', options).catch(() => null);
|
|
41
|
+
if (ariaChecked !== 'true') await item.click(options);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
26
45
|
return false;
|
|
27
46
|
}
|
package/src/field/slider.ts
CHANGED
|
@@ -18,13 +18,24 @@ export async function setSliderValue({ el, tag }: Loc, value: Value, options?: S
|
|
|
18
18
|
const { centerX, centerY } = await prepareMouse(slider, options);
|
|
19
19
|
const page = slider.page();
|
|
20
20
|
|
|
21
|
+
let unresponsive = false;
|
|
21
22
|
try {
|
|
22
23
|
const ratio = await calculateRatio(slider, initialValue, value, centerX, centerY, orientation, options);
|
|
23
24
|
await seekValue(slider, initialValue, value, centerX, centerY, orientation, ratio, options);
|
|
25
|
+
} catch (e) {
|
|
26
|
+
if (e instanceof Error && e.message === 'Slider appears to be disabled or unresponsive') {
|
|
27
|
+
unresponsive = true;
|
|
28
|
+
} else {
|
|
29
|
+
throw e;
|
|
30
|
+
}
|
|
24
31
|
} finally {
|
|
25
32
|
await page.mouse.up();
|
|
26
33
|
}
|
|
27
34
|
|
|
35
|
+
if (unresponsive) {
|
|
36
|
+
return setSliderByKeyboard(slider, initialValue, value, options);
|
|
37
|
+
}
|
|
38
|
+
|
|
28
39
|
return true;
|
|
29
40
|
}
|
|
30
41
|
|
|
@@ -130,3 +141,25 @@ async function moveMouse(page: any, centerX: number, centerY: number, orientatio
|
|
|
130
141
|
await page.mouse.move(centerX + distance, centerY);
|
|
131
142
|
}
|
|
132
143
|
}
|
|
144
|
+
|
|
145
|
+
async function setSliderByKeyboard(
|
|
146
|
+
slider: Locator,
|
|
147
|
+
initialValue: number,
|
|
148
|
+
targetValue: number,
|
|
149
|
+
options?: SetOptions,
|
|
150
|
+
): Promise<boolean> {
|
|
151
|
+
const stepStr = await slider.getAttribute('aria-valuestep', options).catch(() => null);
|
|
152
|
+
const step = stepStr ? parseFloat(stepStr) : 1;
|
|
153
|
+
const presses = Math.round(Math.abs(targetValue - initialValue) / step);
|
|
154
|
+
const key = targetValue > initialValue ? 'ArrowRight' : 'ArrowLeft';
|
|
155
|
+
|
|
156
|
+
await slider.focus(options);
|
|
157
|
+
const page = slider.page();
|
|
158
|
+
for (let i = 0; i < presses; i++) {
|
|
159
|
+
await page.keyboard.press(key);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const nowStr = await slider.getAttribute('aria-valuenow', options).catch(() => null);
|
|
163
|
+
const reached = nowStr !== null && Math.abs(parseFloat(nowStr) - targetValue) < 0.001;
|
|
164
|
+
return reached;
|
|
165
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Loc, SetOptions, Value } from './types';
|
|
2
|
+
|
|
3
|
+
export async function setToggle({ el }: Loc, value: Value, options?: SetOptions): Promise<boolean> {
|
|
4
|
+
if (typeof value !== 'boolean' && value !== null) return false;
|
|
5
|
+
|
|
6
|
+
const role = await el.getAttribute('role', options).catch(() => null);
|
|
7
|
+
if (role !== 'checkbox' && role !== 'switch') return false;
|
|
8
|
+
|
|
9
|
+
const ariaChecked = await el.getAttribute('aria-checked', options).catch(() => null);
|
|
10
|
+
const isChecked = ariaChecked === 'true';
|
|
11
|
+
|
|
12
|
+
if (Boolean(value) !== isChecked) await el.click(options);
|
|
13
|
+
|
|
14
|
+
return true;
|
|
15
|
+
}
|