• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2024 The Chromium Authors
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5from __future__ import annotations
6
7import datetime as dt
8from typing import TYPE_CHECKING, Optional, Tuple, Type
9
10from crossbench.action_runner.action.action import ACTION_TIMEOUT, ActionT
11from crossbench.action_runner.action.action_type import ActionType
12from crossbench.action_runner.action.base_input_source import InputSourceAction
13from crossbench.benchmarks.loading.input_source import InputSource
14from crossbench.benchmarks.loading.point import Point
15from crossbench.parse import DurationParser, NumberParser, ObjectParser
16
17if TYPE_CHECKING:
18  from crossbench.action_runner.base import ActionRunner
19  from crossbench.config import ConfigParser
20  from crossbench.runner.run import Run
21  from crossbench.types import JsonDict
22
23
24class ClickAction(InputSourceAction):
25  TYPE: ActionType = ActionType.CLICK
26
27  @classmethod
28  def config_parser(cls: Type[ActionT]) -> ConfigParser[ActionT]:
29    parser = super().config_parser()
30    parser.add_argument("selector", type=ObjectParser.non_empty_str)
31    parser.add_argument("required", type=ObjectParser.bool, default=False)
32    parser.add_argument(
33        "scroll_into_view", type=ObjectParser.bool, default=False)
34    parser.add_argument("x", type=NumberParser.positive_zero_int)
35    parser.add_argument("y", type=NumberParser.positive_zero_int)
36    parser.add_argument(
37        "duration",
38        type=DurationParser.positive_or_zero_duration,
39        default=dt.timedelta())
40    return parser
41
42  def __init__(self,
43               source: InputSource,
44               duration: dt.timedelta = dt.timedelta(),
45               selector: Optional[str] = None,
46               required: bool = False,
47               scroll_into_view: bool = False,
48               x: Optional[int] = None,
49               y: Optional[int] = None,
50               timeout: dt.timedelta = ACTION_TIMEOUT,
51               index: int = 0):
52    # TODO: convert to custom selector object.
53    self._selector = selector
54    self._required: bool = required
55    self._scroll_into_view: bool = scroll_into_view
56    self._coordinates: Optional[Point] = None
57    if x is not None and y is not None:
58      self._coordinates = Point(x, y)
59    super().__init__(source, duration, timeout, index)
60
61  @property
62  def selector(self) -> Optional[str]:
63    return self._selector
64
65  @property
66  def required(self) -> bool:
67    return self._required
68
69  @property
70  def scroll_into_view(self) -> bool:
71    return self._scroll_into_view
72
73  @property
74  def coordinates(self) -> Optional[Point]:
75    return self._coordinates
76
77  def run_with(self, run: Run, action_runner: ActionRunner) -> None:
78    action_runner.click(run, self)
79
80  def validate(self) -> None:
81    super().validate()
82
83    if self._selector and self._coordinates:
84      raise ValueError("Only one is allowed: either selector or coordinates")
85
86    if not self._selector and not self._coordinates:
87      raise ValueError("Either selector or coordinates are required")
88
89    if self._input_source is InputSource.JS and self._coordinates:
90      raise ValueError("X,Y Coordinates cannot be used with JS click source.")
91
92    if self._required and self._coordinates:
93      raise ValueError("'required' is not compatible with coordinates")
94
95    if self._scroll_into_view and self._coordinates:
96      raise ValueError("'scroll_into_view' is not compatible with coordinates")
97
98  def validate_duration(self) -> None:
99    # A click action is allowed to have a zero duration.
100    return
101
102  def supported_input_sources(self) -> Tuple[InputSource, ...]:
103    return (InputSource.JS, InputSource.TOUCH, InputSource.MOUSE)
104
105  def to_json(self) -> JsonDict:
106    details = super().to_json()
107
108    if self._selector:
109      details["selector"] = self._selector
110      details["required"] = self._required
111      details["scroll_into_view"] = self._scroll_into_view
112    else:
113      assert self._coordinates
114      details["x"] = self._coordinates.x
115      details["y"] = self._coordinates.y
116    return details
117