Skip to content
This repository was archived by the owner on Aug 10, 2022. It is now read-only.

Commit 484daa1

Browse files
authored
Merge pull request #10 from applitools/prepare-selenium-sdk
First working version of selenium sdk
2 parents 401e169 + d754248 commit 484daa1

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1598
-1074
lines changed

.editorconfig

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,7 @@ charset = utf-8
1414
[*.{py,rst,ini}]
1515
indent_style = space
1616
indent_size = 4
17-
max_line_length = 119
18-
19-
[*.py]
20-
max_line_length = 119
17+
max_line_length = 99
2118

2219
[*.rst]
2320
max_line_length = off

.travis.yml

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
language: python
2+
addons:
3+
apt:
4+
sources:
5+
- google-chrome
6+
packages:
7+
- google-chrome-stable
28
matrix:
39
include:
410
- python: 2.7
@@ -13,13 +19,25 @@ matrix:
1319
- python: 3.6
1420
env:
1521
- TOX_ENV=integration
22+
- python: 2.7
23+
env:
24+
- TOX_ENV=selenium
25+
- python: 3.6
26+
env:
27+
- TOX_ENV=selenium
1628
- python: 3.6
1729
env:
1830
- TOX_ENV=lint
1931
install:
20-
- pip install -U tox
32+
- npm install -g webdriver-manager
33+
- webdriver-manager update
34+
- pip install -U tox
2135
before_script:
22-
- export APPLITOOLS_BATCH_ID=`uuidgen -t`
23-
- echo $APPLITOOLS_BATCH_ID
36+
- export DISPLAY=:99.0
37+
- sh -e /etc/init.d/xvfb start
38+
- nohup webdriver-manager start --logging=ERROR &
39+
- sleep 10 # give webdriver some time to start
2440
script:
25-
- tox -e $TOX_ENV
41+
- export APPLITOOLS_BATCH_ID=`uuidgen -t`
42+
- echo $APPLITOOLS_BATCH_ID
43+
- tox -e $TOX_ENV

eyes_core/applitools/core/capture.py

Lines changed: 60 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,97 @@
11
import abc
22
import typing as tp
33

4-
import attr
5-
64
from .geometry import Point, Region
75
from .metadata import CoordinatesType
8-
from .utils import ABC, image_utils
6+
from .utils import ABC, image_utils, argument_guard
97

108
if tp.TYPE_CHECKING:
119
from PIL import Image
1210

1311
__all__ = ('EyesScreenshot',)
1412

1513

16-
@attr.s
1714
class EyesScreenshot(ABC):
1815
"""
1916
Base class for handling screenshots.
2017
"""
2118

22-
_image = attr.ib() # type: Image.Image
19+
def __init__(self, image):
20+
# type: (Image.Image) -> None
21+
self._image = image # type: Image.Image
2322

2423
@abc.abstractmethod
2524
def sub_screenshot(self, region, throw_if_clipped=False):
2625
# type: (Region, bool) -> Region
27-
pass
26+
"""
27+
Returns a part of the screenshot based on the given region.
28+
29+
:param region: The region for which we should get the sub screenshot.
30+
:param throw_if_clipped: Throw an EyesException if the region is not
31+
fully contained in the screenshot.
32+
:return: A screenshot instance containing the given region.
33+
"""
2834

2935
@abc.abstractmethod
3036
def convert_location(self, location, from_, to):
3137
# type: (Point, CoordinatesType, CoordinatesType) -> Point
32-
pass
38+
"""
39+
Converts a location's coordinates with the `from_` coordinates type
40+
to the `to` coordinates type.
41+
42+
:param location: The location which coordinates needs to be converted.
43+
:param from_: The current coordinates type for `location`.
44+
:param to: The target coordinates type for `location`.
45+
:return A new location which is the transformation of `location` to
46+
the `to` coordinates type.
47+
"""
3348

3449
@abc.abstractmethod
3550
def location_in_screenshot(self, location, coordinates_type):
3651
# type: (Point, CoordinatesType) -> Point
37-
pass
52+
"""
53+
Calculates the location in the screenshot of the location given as
54+
parameter.
55+
56+
:param location: The location as coordinates inside the current frame.
57+
:param coordinates_type: The coordinates type of `location`.
58+
:return: The corresponding location inside the screenshot,
59+
in screenshot as-is coordinates type.
60+
:raise: `OutOfBoundsException` If the location is
61+
not inside the frame's region in the screenshot.
62+
"""
3863

3964
@abc.abstractmethod
40-
def intersected_region(self, region, result_coordinate_types):
65+
def intersected_region(self, region, coordinates_type):
4166
# type: (Region, CoordinatesType) -> Region
42-
pass
67+
"""
68+
Get the intersection of the given region with the screenshot.
69+
70+
:param region: The region to intersect.
71+
:param coordinates_type: The coordinates type of `region`.
72+
:return The intersected region, in `coordinates_type` coordinates.
73+
"""
4374

4475
def convert_region_location(self, region, from_, to):
4576
# type: (Region, CoordinatesType, CoordinatesType) -> Region
46-
assert region is not None
47-
assert isinstance(region, Region)
48-
assert from_ is not None
49-
assert to is not None
50-
51-
if region.is_empty:
77+
"""
78+
Converts a region's location coordinates with the `from_`
79+
coordinates type to the `to` coordinates type.
80+
81+
:param region: The region which location's coordinates needs to be converted.
82+
:param from_: The current coordinates type for `region`.
83+
:param to: The target coordinates type for `region`.
84+
:return: A new region which is the transformation of `region` to
85+
the `to` coordinates type.
86+
"""
87+
argument_guard.not_none(region)
88+
argument_guard.is_a(region, Region)
89+
if region.is_size_empty:
5290
return Region.create_empty_region()
5391

92+
argument_guard.not_none(from_)
93+
argument_guard.not_none(to)
94+
5495
updated_location = self.convert_location(region.location, from_, to)
5596
return Region(updated_location.x, updated_location.y, region.width, region.height)
5697

@@ -59,13 +100,9 @@ def image_region(self):
59100
# type: () -> Region
60101
return Region(0, 0, self._image.width, self._image.height, CoordinatesType.SCREENSHOT_AS_IS)
61102

62-
@staticmethod
63-
def from_region(region):
64-
# type: (Region) -> Image.Image
65-
return Image.new('RGBA', (region.width, region.height))
66-
67-
def get_bytes(self):
68-
# type: () -> bytes
103+
@property
104+
def bytes(self):
105+
# type: () -> __builtins__.bytes
69106
"""
70107
Returns the bytes of the screenshot.
71108

eyes_core/applitools/core/eyes_base.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,6 @@ def __init__(self, server_url=DEFAULT_EYES_SERVER):
9898
# If true, Eyes will treat new tests the same as failed tests.
9999
self.fail_on_new_test = False # type: bool
100100

101-
# The number of milliseconds to wait before each time a screenshot is taken.
102-
self.wait_before_screenshots = EyesBase._DEFAULT_WAIT_BEFORE_SCREENSHOTS # type: int
103-
104101
# If true, we will send full DOM to the server for analyzing
105102
self.send_dom = False # type: bool
106103

@@ -131,7 +128,7 @@ def set_viewport_size(self, size):
131128
"""
132129

133130
@abc.abstractmethod
134-
def _assign_viewport_size(self):
131+
def _ensure_viewport_size(self):
135132
# type: () -> None
136133
"""
137134
Assign the viewport size we need to be in the default content frame.
@@ -150,10 +147,6 @@ def _environment(self):
150147
def _inferred_environment(self):
151148
pass
152149

153-
@property
154-
def _seconds_to_wait_screenshot(self):
155-
return self.wait_before_screenshots / 1000.0
156-
157150
@property
158151
def match_level(self):
159152
# type: () -> tp.Text
@@ -430,7 +423,7 @@ def _create_start_info(self):
430423
def _start_session(self):
431424
# type: () -> None
432425
logger.debug("_start_session()")
433-
self._assign_viewport_size()
426+
self._ensure_viewport_size()
434427

435428
# initialization of Eyes parameters if empty from ENV variables
436429
if not self.branch_name:

eyes_core/applitools/core/geometry.py

Lines changed: 38 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import typing as tp
55
from collections import OrderedDict
66

7+
from .utils import argument_guard
78
from .metadata import CoordinatesType
89
from .errors import EyesError
910

@@ -13,7 +14,24 @@
1314
__all__ = ('Point', 'Region',)
1415

1516

16-
class Point(object):
17+
class DictAccessMixin(object):
18+
def __getitem__(self, item):
19+
if item not in self.__slots__:
20+
raise KeyError
21+
return getattr(self, item)
22+
23+
24+
class StateMixin(object):
25+
def __getstate__(self):
26+
return OrderedDict([(name, getattr(self, name)) for name in self.__slots__])
27+
28+
# Required is required in order for jsonpickle to work on this object.
29+
# noinspection PyMethodMayBeStatic
30+
def __setstate__(self, state):
31+
raise EyesError('Cannot create Point instance from dict!')
32+
33+
34+
class Point(DictAccessMixin, StateMixin):
1735
"""
1836
A point with the coordinates (x,y).
1937
"""
@@ -24,15 +42,6 @@ def __init__(self, x=0, y=0):
2442
self.x = int(round(x))
2543
self.y = int(round(y))
2644

27-
def __getstate__(self):
28-
return OrderedDict([("x", self.x),
29-
("y", self.y)])
30-
31-
# Required is required in order for jsonpickle to work on this object.
32-
# noinspection PyMethodMayBeStatic
33-
def __setstate__(self, state):
34-
raise EyesError('Cannot create Point instance from dict!')
35-
3645
def __add__(self, other):
3746
return Point(self.x + other.x, self.y + other.y)
3847

@@ -164,11 +173,10 @@ def rotate_about(self, p, theta):
164173
return result
165174

166175
def scale(self, scale_ratio):
167-
return Point(int(math.ceil(self.x * scale_ratio)),
168-
int(math.ceil(self.y * scale_ratio)))
176+
return Point(int(math.ceil(self.x * scale_ratio)), int(math.ceil(self.y * scale_ratio)))
169177

170178

171-
class Region(object):
179+
class Region(DictAccessMixin, StateMixin):
172180
"""
173181
A rectangle identified by left,top, width, height.
174182
"""
@@ -182,15 +190,6 @@ def __init__(self, left=0, top=0, width=0, height=0, coordinates_type=Coordinate
182190
self.height = int(round(height))
183191
self.coordinates_type = coordinates_type
184192

185-
def __getstate__(self):
186-
return OrderedDict([("top", self.top), ("left", self.left), ("width", self.width),
187-
("height", self.height), ("coordinatesType", self.coordinates_type)])
188-
189-
# Required is required in order for jsonpickle to work on this object.
190-
# noinspection PyMethodMayBeStatic
191-
def __setstate__(self, state):
192-
raise EyesError('Cannot create Region instance from dict!')
193-
194193
@classmethod
195194
def create_empty_region(cls):
196195
return cls(0, 0, 0, 0)
@@ -228,10 +227,11 @@ def location(self):
228227
return Point(self.left, self.top)
229228

230229
@location.setter
231-
def location(self, p):
230+
def location(self, point):
232231
# type: (Point) -> None
233232
"""Sets the top left corner of the region"""
234-
self.left, self.top = p.x, p.y
233+
argument_guard.not_none(point)
234+
self.left, self.top = point.x, point.y
235235

236236
@property
237237
def bottom_right(self):
@@ -265,8 +265,12 @@ def is_same(self, other):
265265
:return: Whether or not the rectangles have same coordinates.
266266
:rtype: bool
267267
"""
268-
return (self.left == other.left and self.top == other.top and self.width == other.width
269-
and self.height == other.height)
268+
return (
269+
self.left == other.left
270+
and self.top == other.top
271+
and self.width == other.width
272+
and self.height == other.height
273+
)
270274

271275
def is_same_size(self, other):
272276
# type: (Region) -> bool
@@ -291,13 +295,15 @@ def clip_negative_location(self):
291295
self.left = max(self.left, 0)
292296
self.top = max(self.top, 0)
293297

298+
@property
294299
def is_size_empty(self):
295300
# type: () -> bool
296301
"""
297302
:return: true if the region's size is 0, false otherwise.
298303
"""
299304
return self.width <= 0 or self.height <= 0
300305

306+
@property
301307
def is_empty(self):
302308
# type: () -> bool
303309
"""
@@ -323,8 +329,8 @@ def overlaps(self, other):
323329
"""
324330
Return true if a rectangle overlaps this rectangle.
325331
"""
326-
return ((self.left <= other.left <= self.right or other.left <= self.left <= other.right)
327-
and (self.top <= other.top <= self.bottom or other.top <= self.top <= other.bottom))
332+
return ((self.left <= other.left <= self.right or other.left <= self.left <= other.right) and (
333+
self.top <= other.top <= self.bottom or other.top <= self.top <= other.bottom))
328334

329335
def intersect(self, other):
330336
# type: (Region) -> None
@@ -362,8 +368,7 @@ def get_sub_regions(self, max_sub_region_size):
362368
current_height = current_bottom - current_top
363369
current_width = current_right - current_left
364370

365-
sub_regions.append(Region(current_left, current_top, current_width,
366-
current_height))
371+
sub_regions.append(Region(current_left, current_top, current_width, current_height))
367372

368373
current_left += max_sub_region_size["width"]
369374

@@ -378,17 +383,11 @@ def middle_offset(self):
378383

379384
def offset(self, dx, dy):
380385
location = self.location.offset(dx, dy)
381-
return Region(left=location.x, top=location.y,
382-
width=self.size['width'],
383-
height=self.size['height'])
386+
return Region(left=location.x, top=location.y, width=self.size['width'], height=self.size['height'])
384387

385388
def scale(self, scale_ratio):
386-
return Region(
387-
left=int(math.ceil(self.left * scale_ratio)),
388-
top=int(math.ceil(self.top * scale_ratio)),
389-
width=int(math.ceil(self.width * scale_ratio)),
390-
height=int(math.ceil(self.height * scale_ratio))
391-
)
389+
return Region(left=int(math.ceil(self.left * scale_ratio)), top=int(math.ceil(self.top * scale_ratio)),
390+
width=int(math.ceil(self.width * scale_ratio)), height=int(math.ceil(self.height * scale_ratio)))
392391

393392
def __str__(self):
394393
return "(%s, %s) %s x %s" % (self.left, self.top, self.width, self.height)

0 commit comments

Comments
 (0)