@@ -134,7 +134,11 @@ def click( # noqa: C901
134134
135135
136136def type_text (
137- browser : SentienceBrowser , element_id : int , text : str , take_snapshot : bool = False
137+ browser : SentienceBrowser ,
138+ element_id : int ,
139+ text : str ,
140+ take_snapshot : bool = False ,
141+ delay_ms : float = 0 ,
138142) -> ActionResult :
139143 """
140144 Type text into an element (focus then input)
@@ -144,9 +148,16 @@ def type_text(
144148 element_id: Element ID from snapshot
145149 text: Text to type
146150 take_snapshot: Whether to take snapshot after action
151+ delay_ms: Delay between keystrokes in milliseconds for human-like typing (default: 0)
147152
148153 Returns:
149154 ActionResult
155+
156+ Example:
157+ >>> # Type instantly (default behavior)
158+ >>> type_text(browser, element_id, "Hello World")
159+ >>> # Type with human-like delay (~10ms between keystrokes)
160+ >>> type_text(browser, element_id, "Hello World", delay_ms=10)
150161 """
151162 if not browser .page :
152163 raise RuntimeError ("Browser not started. Call browser.start() first." )
@@ -177,8 +188,8 @@ def type_text(
177188 error = {"code" : "focus_failed" , "reason" : "Element not found" },
178189 )
179190
180- # Type using Playwright keyboard
181- browser .page .keyboard .type (text )
191+ # Type using Playwright keyboard with optional delay between keystrokes
192+ browser .page .keyboard .type (text , delay = delay_ms )
182193
183194 duration_ms = int ((time .time () - start_time ) * 1000 )
184195 url_after = browser .page .url
@@ -242,6 +253,94 @@ def press(browser: SentienceBrowser, key: str, take_snapshot: bool = False) -> A
242253 )
243254
244255
256+ def scroll_to (
257+ browser : SentienceBrowser ,
258+ element_id : int ,
259+ behavior : str = "smooth" ,
260+ block : str = "center" ,
261+ take_snapshot : bool = False ,
262+ ) -> ActionResult :
263+ """
264+ Scroll an element into view
265+
266+ Scrolls the page so that the specified element is visible in the viewport.
267+ Uses the element registry to find the element and scrollIntoView() to scroll it.
268+
269+ Args:
270+ browser: SentienceBrowser instance
271+ element_id: Element ID from snapshot to scroll into view
272+ behavior: Scroll behavior - 'smooth', 'instant', or 'auto' (default: 'smooth')
273+ block: Vertical alignment - 'start', 'center', 'end', or 'nearest' (default: 'center')
274+ take_snapshot: Whether to take snapshot after action
275+
276+ Returns:
277+ ActionResult
278+
279+ Example:
280+ >>> snap = snapshot(browser)
281+ >>> button = find(snap, 'role=button[name="Submit"]')
282+ >>> if button:
283+ >>> # Scroll element into view with smooth animation
284+ >>> scroll_to(browser, button.id)
285+ >>> # Scroll instantly to top of viewport
286+ >>> scroll_to(browser, button.id, behavior='instant', block='start')
287+ """
288+ if not browser .page :
289+ raise RuntimeError ("Browser not started. Call browser.start() first." )
290+
291+ start_time = time .time ()
292+ url_before = browser .page .url
293+
294+ # Scroll element into view using the element registry
295+ scrolled = browser .page .evaluate (
296+ """
297+ (args) => {
298+ const el = window.sentience_registry[args.id];
299+ if (el && el.scrollIntoView) {
300+ el.scrollIntoView({
301+ behavior: args.behavior,
302+ block: args.block,
303+ inline: 'nearest'
304+ });
305+ return true;
306+ }
307+ return false;
308+ }
309+ """ ,
310+ {"id" : element_id , "behavior" : behavior , "block" : block },
311+ )
312+
313+ if not scrolled :
314+ return ActionResult (
315+ success = False ,
316+ duration_ms = int ((time .time () - start_time ) * 1000 ),
317+ outcome = "error" ,
318+ error = {"code" : "scroll_failed" , "reason" : "Element not found or not scrollable" },
319+ )
320+
321+ # Wait a bit for scroll to complete (especially for smooth scrolling)
322+ wait_time = 500 if behavior == "smooth" else 100
323+ browser .page .wait_for_timeout (wait_time )
324+
325+ duration_ms = int ((time .time () - start_time ) * 1000 )
326+ url_after = browser .page .url
327+ url_changed = url_before != url_after
328+
329+ outcome = "navigated" if url_changed else "dom_updated"
330+
331+ snapshot_after : Snapshot | None = None
332+ if take_snapshot :
333+ snapshot_after = snapshot (browser )
334+
335+ return ActionResult (
336+ success = True ,
337+ duration_ms = duration_ms ,
338+ outcome = outcome ,
339+ url_changed = url_changed ,
340+ snapshot_after = snapshot_after ,
341+ )
342+
343+
245344def _highlight_rect (
246345 browser : SentienceBrowser , rect : dict [str , float ], duration_sec : float = 2.0
247346) -> None :
@@ -553,7 +652,11 @@ async def click_async(
553652
554653
555654async def type_text_async (
556- browser : AsyncSentienceBrowser , element_id : int , text : str , take_snapshot : bool = False
655+ browser : AsyncSentienceBrowser ,
656+ element_id : int ,
657+ text : str ,
658+ take_snapshot : bool = False ,
659+ delay_ms : float = 0 ,
557660) -> ActionResult :
558661 """
559662 Type text into an element (async)
@@ -563,9 +666,16 @@ async def type_text_async(
563666 element_id: Element ID from snapshot
564667 text: Text to type
565668 take_snapshot: Whether to take snapshot after action
669+ delay_ms: Delay between keystrokes in milliseconds for human-like typing (default: 0)
566670
567671 Returns:
568672 ActionResult
673+
674+ Example:
675+ >>> # Type instantly (default behavior)
676+ >>> await type_text_async(browser, element_id, "Hello World")
677+ >>> # Type with human-like delay (~10ms between keystrokes)
678+ >>> await type_text_async(browser, element_id, "Hello World", delay_ms=10)
569679 """
570680 if not browser .page :
571681 raise RuntimeError ("Browser not started. Call await browser.start() first." )
@@ -596,8 +706,8 @@ async def type_text_async(
596706 error = {"code" : "focus_failed" , "reason" : "Element not found" },
597707 )
598708
599- # Type using Playwright keyboard
600- await browser .page .keyboard .type (text )
709+ # Type using Playwright keyboard with optional delay between keystrokes
710+ await browser .page .keyboard .type (text , delay = delay_ms )
601711
602712 duration_ms = int ((time .time () - start_time ) * 1000 )
603713 url_after = browser .page .url
@@ -663,6 +773,94 @@ async def press_async(
663773 )
664774
665775
776+ async def scroll_to_async (
777+ browser : AsyncSentienceBrowser ,
778+ element_id : int ,
779+ behavior : str = "smooth" ,
780+ block : str = "center" ,
781+ take_snapshot : bool = False ,
782+ ) -> ActionResult :
783+ """
784+ Scroll an element into view (async)
785+
786+ Scrolls the page so that the specified element is visible in the viewport.
787+ Uses the element registry to find the element and scrollIntoView() to scroll it.
788+
789+ Args:
790+ browser: AsyncSentienceBrowser instance
791+ element_id: Element ID from snapshot to scroll into view
792+ behavior: Scroll behavior - 'smooth', 'instant', or 'auto' (default: 'smooth')
793+ block: Vertical alignment - 'start', 'center', 'end', or 'nearest' (default: 'center')
794+ take_snapshot: Whether to take snapshot after action
795+
796+ Returns:
797+ ActionResult
798+
799+ Example:
800+ >>> snap = await snapshot_async(browser)
801+ >>> button = find(snap, 'role=button[name="Submit"]')
802+ >>> if button:
803+ >>> # Scroll element into view with smooth animation
804+ >>> await scroll_to_async(browser, button.id)
805+ >>> # Scroll instantly to top of viewport
806+ >>> await scroll_to_async(browser, button.id, behavior='instant', block='start')
807+ """
808+ if not browser .page :
809+ raise RuntimeError ("Browser not started. Call await browser.start() first." )
810+
811+ start_time = time .time ()
812+ url_before = browser .page .url
813+
814+ # Scroll element into view using the element registry
815+ scrolled = await browser .page .evaluate (
816+ """
817+ (args) => {
818+ const el = window.sentience_registry[args.id];
819+ if (el && el.scrollIntoView) {
820+ el.scrollIntoView({
821+ behavior: args.behavior,
822+ block: args.block,
823+ inline: 'nearest'
824+ });
825+ return true;
826+ }
827+ return false;
828+ }
829+ """ ,
830+ {"id" : element_id , "behavior" : behavior , "block" : block },
831+ )
832+
833+ if not scrolled :
834+ return ActionResult (
835+ success = False ,
836+ duration_ms = int ((time .time () - start_time ) * 1000 ),
837+ outcome = "error" ,
838+ error = {"code" : "scroll_failed" , "reason" : "Element not found or not scrollable" },
839+ )
840+
841+ # Wait a bit for scroll to complete (especially for smooth scrolling)
842+ wait_time = 500 if behavior == "smooth" else 100
843+ await browser .page .wait_for_timeout (wait_time )
844+
845+ duration_ms = int ((time .time () - start_time ) * 1000 )
846+ url_after = browser .page .url
847+ url_changed = url_before != url_after
848+
849+ outcome = "navigated" if url_changed else "dom_updated"
850+
851+ snapshot_after : Snapshot | None = None
852+ if take_snapshot :
853+ snapshot_after = await snapshot_async (browser )
854+
855+ return ActionResult (
856+ success = True ,
857+ duration_ms = duration_ms ,
858+ outcome = outcome ,
859+ url_changed = url_changed ,
860+ snapshot_after = snapshot_after ,
861+ )
862+
863+
666864async def _highlight_rect_async (
667865 browser : AsyncSentienceBrowser , rect : dict [str , float ], duration_sec : float = 2.0
668866) -> None :
0 commit comments