diff --git a/api/element.go b/api/element.go index 0e542f0..6f929bf 100644 --- a/api/element.go +++ b/api/element.go @@ -1,6 +1,7 @@ package api import ( + "encoding/base64" "errors" "path" "strings" @@ -145,3 +146,24 @@ func (e *Element) GetLocation() (x, y int, err error) { func round(number float64) int { return int(number + 0.5) } + +func (e *Element) GetRect() (x, y, width, height int, err error) { + var rect struct { + X float64 `json:"x"` + Y float64 `json:"y"` + Height float64 `json:"height"` + Width float64 `json:"width"` + } + if err := e.Send("GET", "rect", nil, &rect); err != nil { + return 0, 0, 0, 0, err + } + return round(rect.X), round(rect.Y), round(rect.Width), round(rect.Height), nil +} + +func (e *Element) GetScreenshot() ([]byte, error) { + var base64Image string + if err := e.Send("GET", "screenshot", nil, &base64Image); err != nil { + return nil, err + } + return base64.StdEncoding.DecodeString(base64Image) +} diff --git a/api/element_test.go b/api/element_test.go index 8342273..c653502 100644 --- a/api/element_test.go +++ b/api/element_test.go @@ -386,4 +386,65 @@ var _ = Describe("Element", func() { }) }) }) + + Describe("#GetRect", func() { + It("should successfully send a GET request to the rect endpoint", func() { + _, _, _, _, err := element.GetRect() + Expect(err).NotTo(HaveOccurred()) + Expect(bus.SendCall.Method).To(Equal("GET")) + Expect(bus.SendCall.Endpoint).To(Equal("element/some-id/rect")) + }) + + It("should return the rounded rect of the element", func() { + bus.SendCall.Result = `{"x": 100.7, "y": 200, "height": 55.05, "width": 33}` + x, y, width, height, err := element.GetRect() + Expect(err).NotTo(HaveOccurred()) + Expect(x).To(Equal(101)) + Expect(y).To(Equal(200)) + Expect(width).To(Equal(33)) + Expect(height).To(Equal(55)) + }) + + Context("when the bus indicates a failure", func() { + It("should return an error indicating the bus failed to retrieve the rect", func() { + bus.SendCall.Err = errors.New("some error") + _, _, _, _, err := element.GetRect() + Expect(err).To(MatchError("some error")) + }) + }) + }) + + Describe("#GetScreenshot", func() { + It("should successfully send a GET request to the screenshot endpoint", func() { + _, err := element.GetScreenshot() + Expect(err).NotTo(HaveOccurred()) + Expect(bus.SendCall.Method).To(Equal("GET")) + Expect(bus.SendCall.Endpoint).To(Equal("element/some-id/screenshot")) + }) + + Context("when the image is valid base64", func() { + It("should return the decoded image", func() { + bus.SendCall.Result = `"c29tZS1wbmc="` + image, err := element.GetScreenshot() + Expect(err).NotTo(HaveOccurred()) + Expect(string(image)).To(Equal("some-png")) + }) + }) + + Context("when the image is not valid base64", func() { + It("should return an error", func() { + bus.SendCall.Result = `"..."` + _, err := element.GetScreenshot() + Expect(err).To(MatchError("illegal base64 data at input byte 0")) + }) + }) + + Context("when the bus indicates a failure", func() { + It("should return an error", func() { + bus.SendCall.Err = errors.New("some error") + _, err := element.GetScreenshot() + Expect(err).To(MatchError("some error")) + }) + }) + }) }) diff --git a/injector_test.go b/injector_test.go index 1feae03..c220611 100644 --- a/injector_test.go +++ b/injector_test.go @@ -1,15 +1,18 @@ package agouti -import "github.com/sclevine/agouti/internal/target" +import ( + "github.com/sclevine/agouti/internal/crop" + "github.com/sclevine/agouti/internal/target" +) -func NewTestSelection(session apiSession, elements elementRepository, firstSelector string) *Selection { +func NewTestSelection(session apiSession, elements elementRepository, firstSelector string, cropper crop.Cropper) *Selection { selector := target.Selector{Type: target.CSS, Value: firstSelector, Single: true} - return &Selection{selectable{session, target.Selectors{selector}}, elements} + return &Selection{selectable{session, target.Selectors{selector}}, elements, cropper} } -func NewTestMultiSelection(session apiSession, elements elementRepository, firstSelector string) *MultiSelection { +func NewTestMultiSelection(session apiSession, elements elementRepository, firstSelector string, cropper crop.Cropper) *MultiSelection { selector := target.Selector{Type: target.CSS, Value: firstSelector} - selection := Selection{selectable{session, target.Selectors{selector}}, elements} + selection := Selection{selectable{session, target.Selectors{selector}}, elements, cropper} return &MultiSelection{selection} } diff --git a/internal/crop/LICENSE b/internal/crop/LICENSE new file mode 100644 index 0000000..5412782 --- /dev/null +++ b/internal/crop/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2014 Olivier Amblet + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/internal/crop/README.md b/internal/crop/README.md new file mode 100644 index 0000000..f165d77 --- /dev/null +++ b/internal/crop/README.md @@ -0,0 +1,4 @@ +## Crop + +This is a modified version of https://github.com/oliamb/cutter. +License is detailed in LICENSE. diff --git a/internal/crop/crop.go b/internal/crop/crop.go new file mode 100644 index 0000000..4e5a601 --- /dev/null +++ b/internal/crop/crop.go @@ -0,0 +1,62 @@ +package crop + +import ( + "image" + "image/draw" +) + +// An interface that is +// image.Image + SubImage method. +type subImageSupported interface { + SubImage(r image.Rectangle) image.Image +} + +// Cropper is the interface used to crop images +type Cropper interface { + Crop(img image.Image, width, height int, anchor image.Point) (image.Image, error) +} + +// CropperFunc exposes a Crop function that calls itself. +// It implements Cropper. +type CropperFunc func(img image.Image, width, height int, anchor image.Point) (image.Image, error) + +// Crop calls the CropperFunc +func (c CropperFunc) Crop(img image.Image, width, height int, anchor image.Point) (image.Image, error) { + return c(img, width, height, anchor) +} + +// Crop retrieves an image that is a +// cropped copy of the original img. +func Crop(img image.Image, width, height int, anchor image.Point) (image.Image, error) { + maxBounds := maxBounds(anchor, img.Bounds()) + size := computeSize(maxBounds, image.Point{width, height}) + cr := computedCropArea(anchor, img.Bounds(), size) + cr = img.Bounds().Intersect(cr) + + if dImg, ok := img.(subImageSupported); ok { + return dImg.SubImage(cr), nil + } + return cropWithCopy(img, cr) +} + +func cropWithCopy(img image.Image, cr image.Rectangle) (image.Image, error) { + result := image.NewRGBA(cr) + draw.Draw(result, cr, img, cr.Min, draw.Src) + return result, nil +} + +func maxBounds(anchor image.Point, bounds image.Rectangle) image.Rectangle { + return image.Rect(anchor.X, anchor.Y, bounds.Max.X, bounds.Max.Y) +} + +// computeSize retrieve the effective size of the cropped image. +func computeSize(bounds image.Rectangle, ratio image.Point) image.Point { + return image.Point{ratio.X, ratio.Y} +} + +// computedCropArea retrieve the theorical crop area. +func computedCropArea(anchor image.Point, bounds image.Rectangle, size image.Point) (r image.Rectangle) { + min := bounds.Min + rMin := image.Point{min.X + anchor.X, min.Y + anchor.Y} + return image.Rect(rMin.X, rMin.Y, rMin.X+size.X, rMin.Y+size.Y) +} diff --git a/internal/crop/crop_suite_test.go b/internal/crop/crop_suite_test.go new file mode 100644 index 0000000..410205d --- /dev/null +++ b/internal/crop/crop_suite_test.go @@ -0,0 +1,13 @@ +package crop_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestCrop(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Crop Suite") +} diff --git a/internal/crop/crop_test.go b/internal/crop/crop_test.go new file mode 100644 index 0000000..099940e --- /dev/null +++ b/internal/crop/crop_test.go @@ -0,0 +1,34 @@ +package crop + +import ( + "image" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Crop", func() { + It("crops the image", func() { + r, err := Crop(getImage(), 512, 400, image.Point{}) + Expect(err).NotTo(HaveOccurred()) + Expect(r.Bounds().Dx()).To(Equal(512)) + Expect(r.Bounds().Dy()).To(Equal(400)) + Expect(r.Bounds().Min.X).To(Equal(0)) + Expect(r.Bounds().Min.Y).To(Equal(0)) + }) + + Context("when a different anchor point is used", func() { + It("crops the image", func() { + r, err := Crop(getImage(), 512, 400, image.Point{X: 100, Y: 50}) + Expect(err).NotTo(HaveOccurred()) + Expect(r.Bounds().Dx()).To(Equal(512)) + Expect(r.Bounds().Dy()).To(Equal(400)) + Expect(r.Bounds().Min.X).To(Equal(100)) + Expect(r.Bounds().Min.Y).To(Equal(50)) + }) + }) +}) + +func getImage() image.Image { + return image.NewGray(image.Rect(0, 0, 1600, 1437)) +} diff --git a/internal/element/repository.go b/internal/element/repository.go index 4749ddf..8074b17 100644 --- a/internal/element/repository.go +++ b/internal/element/repository.go @@ -34,6 +34,8 @@ type Element interface { Value(text string) error Submit() error GetLocation() (x, y int, err error) + GetRect() (x, y, width, height int, err error) + GetScreenshot() ([]byte, error) } func (e *Repository) GetAtLeastOne() ([]Element, error) { diff --git a/internal/mocks/crop.go b/internal/mocks/crop.go new file mode 100644 index 0000000..b69a15d --- /dev/null +++ b/internal/mocks/crop.go @@ -0,0 +1,20 @@ +package mocks + +import "image" + +type Cropper struct { + Image image.Image + Width int + Height int + Anchor image.Point + ReturnImage image.Image + Err error +} + +func (c *Cropper) Crop(img image.Image, width, height int, anchor image.Point) (image.Image, error) { + c.Image = img + c.Width = width + c.Height = height + c.Anchor = anchor + return c.ReturnImage, c.Err +} diff --git a/internal/mocks/element.go b/internal/mocks/element.go index e405fac..0e3c1c0 100644 --- a/internal/mocks/element.go +++ b/internal/mocks/element.go @@ -87,6 +87,19 @@ type Element struct { ReturnY int Err error } + + GetRectCall struct { + ReturnX int + ReturnY int + ReturnHeight int + ReturnWidth int + Err error + } + + GetScreenshotCall struct { + ReturnImage []byte + Err error + } } func (e *Element) GetElement(selector api.Selector) (*api.Element, error) { @@ -161,3 +174,11 @@ func (e *Element) IsEqualTo(other *api.Element) (bool, error) { func (e *Element) GetLocation() (x, y int, err error) { return e.GetLocationCall.ReturnX, e.GetLocationCall.ReturnY, e.GetLocationCall.Err } + +func (e *Element) GetRect() (x, y, width, height int, err error) { + return e.GetRectCall.ReturnX, e.GetRectCall.ReturnY, e.GetRectCall.ReturnWidth, e.GetRectCall.ReturnHeight, e.GetRectCall.Err +} + +func (e *Element) GetScreenshot() (s []byte, err error) { + return e.GetScreenshotCall.ReturnImage, e.GetScreenshotCall.Err +} diff --git a/multiselection_test.go b/multiselection_test.go index 6f656af..ede8613 100644 --- a/multiselection_test.go +++ b/multiselection_test.go @@ -18,7 +18,7 @@ var _ = Describe("MultiSelection", func() { BeforeEach(func() { bus = &mocks.Bus{} session = &api.Session{Bus: bus} - selection = NewTestMultiSelection(session, nil, "#selector") + selection = NewTestMultiSelection(session, nil, "#selector", nil) }) Describe("#At", func() { diff --git a/selection.go b/selection.go index 1a32096..58eb0da 100644 --- a/selection.go +++ b/selection.go @@ -1,9 +1,15 @@ package agouti import ( + "bytes" "fmt" + "image" + "image/png" + "io/ioutil" + "path/filepath" "github.com/sclevine/agouti/api" + "github.com/sclevine/agouti/internal/crop" "github.com/sclevine/agouti/internal/element" "github.com/sclevine/agouti/internal/target" ) @@ -24,6 +30,7 @@ import ( type Selection struct { selectable elements elementRepository + cropper crop.Cropper } type elementRepository interface { @@ -39,6 +46,7 @@ func newSelection(session apiSession, selectors target.Selectors) *Selection { Client: session, Selectors: selectors, }, + crop.CropperFunc(crop.Crop), } } @@ -115,3 +123,58 @@ func (s *Selection) MouseToElement() error { return nil } + +// Screenshot takes a screenshot of exactly one element +// and saves it to the provided filename. +// The provided filename may be an absolute or relative path. +func (s *Selection) Screenshot(filename string) error { + selectedElement, err := s.elements.GetExactlyOne() + if err != nil { + return fmt.Errorf("failed to select element from %s: %s", s, err) + } + + absFilePath, err := filepath.Abs(filename) + if err != nil { + return fmt.Errorf("failed to find absolute path for filename: %s", err) + } + + screenshot, err := selectedElement.GetScreenshot() + if err != nil { + // Fallback to getting full size screenshot and cropping + data, err := s.session.GetScreenshot() + if err != nil { + return fmt.Errorf("failed to retrieve screenshot: %s", err) + } + + img, err := png.Decode(bytes.NewBuffer(data)) + if err != nil { + return fmt.Errorf("failed to decode screenshot: %s", err) + } + + x, y, width, height, err := selectedElement.GetRect() + if err != nil { + return fmt.Errorf("failed to retrieve bounds for selection: %s", err) + } + + croppedImg, err := s.cropper.Crop( + img, width, height, + image.Point{X: x, Y: y}) + if err != nil { + return fmt.Errorf("failed to crop screenshot: %s", err) + } + + b := new(bytes.Buffer) + err = png.Encode(b, croppedImg) + if err != nil { + return fmt.Errorf("failed to encode screenshot: %s", err) + } + + screenshot = b.Bytes() + } + + if err := ioutil.WriteFile(absFilePath, screenshot, 0666); err != nil { + return fmt.Errorf("failed to save screenshot: %s", err) + } + + return nil +} diff --git a/selection_actions_test.go b/selection_actions_test.go index fad22cc..6a99aed 100644 --- a/selection_actions_test.go +++ b/selection_actions_test.go @@ -27,7 +27,7 @@ var _ = Describe("Selection Actions", func() { firstElement = &mocks.Element{} secondElement = &mocks.Element{} elementRepository = &mocks.ElementRepository{} - selection = NewTestMultiSelection(session, elementRepository, "#selector") + selection = NewTestMultiSelection(session, elementRepository, "#selector", nil) elementRepository.GetAtLeastOneCall.ReturnElements = []element.Element{firstElement, secondElement} }) diff --git a/selection_frames_test.go b/selection_frames_test.go index 3362490..e921053 100644 --- a/selection_frames_test.go +++ b/selection_frames_test.go @@ -21,7 +21,7 @@ var _ = Describe("Selection Frames", func() { BeforeEach(func() { session = &mocks.Session{} elementRepository = &mocks.ElementRepository{} - selection = NewTestSelection(session, elementRepository, "#selector") + selection = NewTestSelection(session, elementRepository, "#selector", nil) }) Describe("#SwitchToFrame", func() { diff --git a/selection_properties_test.go b/selection_properties_test.go index 5a92910..5e97e5a 100644 --- a/selection_properties_test.go +++ b/selection_properties_test.go @@ -26,7 +26,7 @@ var _ = Describe("Selection Properties", func() { firstElement = &mocks.Element{} secondElement = &mocks.Element{} elementRepository = &mocks.ElementRepository{} - selection = NewTestMultiSelection(session, elementRepository, "#selector") + selection = NewTestMultiSelection(session, elementRepository, "#selector", nil) }) Describe("#Text", func() { diff --git a/selection_test.go b/selection_test.go index b2e0543..a0f7e42 100644 --- a/selection_test.go +++ b/selection_test.go @@ -1,7 +1,13 @@ package agouti_test import ( + "bytes" "errors" + "image" + "image/png" + "io/ioutil" + "os" + "path/filepath" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" @@ -12,6 +18,15 @@ import ( "github.com/sclevine/agouti/internal/mocks" ) +var minimalpng = []byte{0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, + 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, + 0x00, 0x00, 0x1f, 0x15, 0xc4, 0x89, 0x00, 0x00, 0x00, 0x11, + 0x49, 0x44, 0x41, 0x54, 0x78, 0x9c, 0x62, 0x62, 0x60, 0x60, + 0x60, 0x00, 0x04, 0x00, 0x00, 0xff, 0xff, 0x00, 0x0f, 0x00, + 0x03, 0xfe, 0x8f, 0xeb, 0xcf, 0x00, 0x00, 0x00, 0x00, 0x49, + 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82} + var _ = Describe("Selection", func() { var ( firstElement *mocks.Element @@ -25,7 +40,7 @@ var _ = Describe("Selection", func() { Describe("#String", func() { It("should return a string representation of the selection", func() { - selection := NewTestMultiSelection(nil, nil, "#selector") + selection := NewTestMultiSelection(nil, nil, "#selector", nil) Expect(selection.AllByXPath("#subselector").String()).To(Equal("selection 'CSS: #selector | XPath: #subselector'")) }) }) @@ -38,7 +53,7 @@ var _ = Describe("Selection", func() { BeforeEach(func() { elementRepository = &mocks.ElementRepository{} - selection = NewTestSelection(nil, elementRepository, "#selector") + selection = NewTestSelection(nil, elementRepository, "#selector", nil) }) It("should return a []*api.Elements retrieved from the element repository", func() { @@ -64,7 +79,7 @@ var _ = Describe("Selection", func() { BeforeEach(func() { elementRepository = &mocks.ElementRepository{} - selection = NewTestMultiSelection(nil, elementRepository, "#selector") + selection = NewTestMultiSelection(nil, elementRepository, "#selector", nil) elementRepository.GetCall.ReturnElements = []element.Element{firstElement, secondElement} }) @@ -92,11 +107,11 @@ var _ = Describe("Selection", func() { BeforeEach(func() { firstElementRepository = &mocks.ElementRepository{} firstElementRepository.GetExactlyOneCall.ReturnElement = firstElement - firstSelection = NewTestSelection(nil, firstElementRepository, "#first_selector") + firstSelection = NewTestSelection(nil, firstElementRepository, "#first_selector", nil) secondElementRepository = &mocks.ElementRepository{} secondElementRepository.GetExactlyOneCall.ReturnElement = secondElement - secondSelection = NewTestSelection(nil, secondElementRepository, "#second_selector") + secondSelection = NewTestSelection(nil, secondElementRepository, "#second_selector", nil) }) It("should compare the selection elements for equality", func() { @@ -116,7 +131,7 @@ var _ = Describe("Selection", func() { Context("when the provided object is a *MultiSelection", func() { It("should not fail", func() { - multiSelection := NewTestMultiSelection(nil, secondElementRepository, "#multi_selector") + multiSelection := NewTestMultiSelection(nil, secondElementRepository, "#multi_selector", nil) Expect(firstSelection.EqualsElement(multiSelection)).To(BeFalse()) Expect(firstElement.IsEqualToCall.Element).To(ExactlyEqual(secondElement)) }) @@ -165,7 +180,7 @@ var _ = Describe("Selection", func() { elementRepository = &mocks.ElementRepository{} elementRepository.GetExactlyOneCall.ReturnElement = secondElement session = &mocks.Session{} - selection = NewTestSelection(session, elementRepository, "#selector") + selection = NewTestSelection(session, elementRepository, "#selector", nil) }) It("should successfully instruct the session to move the mouse over the selection", func() { @@ -190,4 +205,106 @@ var _ = Describe("Selection", func() { }) }) }) + + Describe("#Screenshot", func() { + var ( + selection *Selection + session *mocks.Session + cropper *mocks.Cropper + firstElement *mocks.Element + elementRepository *mocks.ElementRepository + ) + + BeforeEach(func() { + firstElement = &mocks.Element{} + elementRepository = &mocks.ElementRepository{} + elementRepository.GetExactlyOneCall.ReturnElement = firstElement + session = &mocks.Session{} + cropper = &mocks.Cropper{} + selection = NewTestSelection(session, elementRepository, "#selector", cropper) + }) + + It("should successfully return the screenshot", func() { + firstElement.GetScreenshotCall.ReturnImage = []byte("some-image") + filename, _ := filepath.Abs(".test.screenshot.png") + Expect(selection.Screenshot(".test.screenshot.png")).To(Succeed()) + defer os.Remove(filename) + result, _ := ioutil.ReadFile(filename) + Expect(string(result)).To(Equal("some-image")) + }) + + Context("when a new screenshot file cannot be saved", func() { + It("should return an error", func() { + err := selection.Screenshot("") + Expect(err.Error()).To(ContainSubstring("failed to save screenshot: open")) + }) + }) + + Context("when the element repository fails to return exactly one element", func() { + It("should return an error", func() { + elementRepository.GetExactlyOneCall.Err = errors.New("some error") + err := selection.Screenshot(".test.screenshot.png") + Expect(err).To(MatchError("failed to select element from selection 'CSS: #selector [single]': some error")) + }) + }) + + Context("when the selection fails to retrieve a screenshot", func() { + BeforeEach(func() { + firstElement.GetScreenshotCall.Err = errors.New("some error") + }) + + It("should fall back to using the session screenshot and cropping", func() { + session.GetScreenshotCall.ReturnImage = minimalpng + cropper.ReturnImage, _ = png.Decode(bytes.NewBuffer(minimalpng)) + filename, _ := filepath.Abs(".test.screenshot.png") + Expect(selection.Screenshot(".test.screenshot.png")).To(Succeed()) + defer os.Remove(filename) + result, _ := ioutil.ReadFile(filename) + Expect(result).To(Equal(minimalpng)) + }) + + Context("and the session fails to retrieve a screenshot", func() { + It("should return an error", func() { + session.GetScreenshotCall.Err = errors.New("some error") + err := selection.Screenshot(".test.screenshot.png") + Expect(err).To(MatchError("failed to retrieve screenshot: some error")) + }) + }) + + Context("and the session screenshot cannot be decoded", func() { + It("should return an error", func() { + session.GetScreenshotCall.ReturnImage = []byte("some-image") + err := selection.Screenshot(".test.screenshot.png") + Expect(err).To(MatchError("failed to decode screenshot: png: invalid format: not a PNG file")) + }) + }) + + Context("and the selections bounding rectangle cannot be retrieved", func() { + It("should return an error", func() { + session.GetScreenshotCall.ReturnImage = minimalpng + firstElement.GetRectCall.Err = errors.New("some error") + err := selection.Screenshot(".test.screenshot.png") + Expect(err).To(MatchError("failed to retrieve bounds for selection: some error")) + }) + }) + + Context("and the image cannot be cropped", func() { + It("should return an error", func() { + session.GetScreenshotCall.ReturnImage = minimalpng + cropper.Err = errors.New("some error") + err := selection.Screenshot(".test.screenshot.png") + Expect(err).To(MatchError("failed to crop screenshot: some error")) + }) + }) + + Context("and the image cannot be encoded", func() { + It("should return an error", func() { + session.GetScreenshotCall.ReturnImage = minimalpng + cropper.ReturnImage = image.Rectangle{} + err := selection.Screenshot(".test.screenshot.png") + Expect(err).To(MatchError("failed to encode screenshot: png: invalid format: invalid image size: 0x0")) + }) + }) + }) + }) })