feat(humanize): add Playwright-style actionability checks (#228)

* feat(humanize): add Playwright-style actionability checks to all interaction methods

Humanized locator/page methods now perform pre-action validation matching
Playwright's native behavior: attached, visible, enabled, editable, stable,
and receives-pointer-events checks with retry loop and backoff.

- New error hierarchy: ActionabilityError base with ElementNotAttachedError,
  ElementNotVisibleError, ElementNotStableError, ElementNotEnabledError,
  ElementNotEditableError, ElementNotReceivingEventsError
- force=True parameter skips all actionability checks (matches Playwright)
- Shared deadline across all steps (checks + scroll + stable + pointer)
- Post-scroll stability check only runs when scroll actually happened
- Chained methods (type/fill/check/uncheck/press) skip inner click checks
  but still run pointer-events check at actual click coordinates
- Frame methods now forward kwargs (force, timeout, human_config)
- Locator patches forward force via _forward_kwargs
- Python sync + async, JS/TS implementation

* fix(humanize): forward human_config in all chained methods, use evaluate args in handle pointer checks

- Add human_config=kwargs.get("human_config") to check/uncheck/select_option/press inner calls (sync+async+JS)
- Convert check_pointer_events_handle from f-string interpolation to evaluate args pattern (sync+async+JS)

* fix(humanize): strip custom kwargs before forwarding to Playwright select_option

originals.select_option(**kwargs) passes human_config/force to Playwright
which rejects unknown kwargs with TypeError.
This commit is contained in:
Cloak-HQ
2026-05-15 20:57:17 +02:00
committed by GitHub
parent 6f4f92e7c7
commit b0ea580cba
15 changed files with 1704 additions and 181 deletions
+10 -2
View File
@@ -44,9 +44,14 @@ function buildMockPage(overrides: Record<string, any> = {}): any {
const makeLocator = () => {
const loc: any = {
boundingBox: vi.fn(async () => ({ x: 100, y: 100, width: 200, height: 30 })),
boundingBox: vi.fn(async () => ({ x: 100, y: 300, width: 200, height: 30 })),
scrollIntoViewIfNeeded: vi.fn(async () => {}),
isChecked: overrides.isChecked ?? vi.fn(async () => false),
waitFor: vi.fn(async () => {}),
isVisible: vi.fn(async () => true),
isEnabled: vi.fn(async () => true),
isEditable: vi.fn(async () => true),
evaluate: vi.fn(async () => ({ hit: true })),
};
loc.first = vi.fn(() => loc);
return loc;
@@ -687,6 +692,9 @@ describe("isInputElement stealth integration via patchPage", () => {
}
if (method === "Runtime.evaluate") {
stealthEvaluateCalls.push(params.expression);
if (params.expression.includes("elementFromPoint")) {
return { result: { value: { hit: true } } };
}
return { result: { value: false } }; // not an input
}
return {};
@@ -696,7 +704,7 @@ describe("isInputElement stealth integration via patchPage", () => {
const page = buildMockPage({
evaluate: vi.fn(async (...args: any[]) => {
evaluateCalls.push(args);
return false;
return { hit: true };
}),
});
page.context = vi.fn(() => ({