add bacon-ui test suite

This commit is contained in:
Laurent Sansonetti
2012-07-11 15:44:47 +02:00
parent 86cfd0a959
commit 7dbc26e610
8 changed files with 1024 additions and 0 deletions

9
test/bacon-ui/Rakefile Normal file
View File

@@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
$:.unshift("/Library/RubyMotion/lib")
require 'motion/project'
Motion::Project::App.setup do |app|
# Use `rake config' to see complete project settings.
app.name = 'MacBacon UI'
app.frameworks << 'CoreGraphics'
end

View File

@@ -0,0 +1,5 @@
class AppDelegate
def application(application, didFinishLaunchingWithOptions:launchOptions)
true
end
end

View File

@@ -0,0 +1,530 @@
# TODO make it pwetty
class TouchHighlightView < UIView
DIAMETER = 7
def self.forTouch(touch)
touch.window.addSubview(alloc.initWithTouch(touch)) if touch.window
end
def self.updateLocationForTouch(touch)
if highlight = highlightForTouch(touch)
highlight.updateLocation
end
end
def self.removeForTouch(touch)
performSelector('reallyRemoveForTouch:', withObject:touch, afterDelay:0.1)
end
def self.reallyRemoveForTouch(touch)
if highlight = highlightForTouch(touch)
highlight.removeFromSuperview
end
end
def self.highlightForTouch(touch)
# If the touch has no window anymore, then the window has already been destroyed.
touch.window.subviews.find { |v| v.is_a?(TouchHighlightView) && v.touch == touch } if touch.window
end
attr_reader :touch
def initWithTouch(touch)
@touch = touch
if initWithFrame(highlightFrame)
self.backgroundColor = UIColor.redColor
end
self
end
def highlightFrame
point = @touch.locationInView(nil)
offset = (DIAMETER-1)/2
CGRectMake(point.x-offset, point.y-offset, DIAMETER, DIAMETER)
end
def updateLocation
self.frame = highlightFrame
end
end
module UIApplicationExt
attr_accessor :logEvents
attr_accessor :highlightTouches
def sendEvent(event)
if @logEvents
NSLog(event.description)
end
# TODO this code breaks most gestures
if @highlightTouches
event.allTouches.each do |touch|
case touch.phase
when UITouchPhaseBegan
TouchHighlightView.forTouch(touch)
when UITouchPhaseMoved
TouchHighlightView.updateLocationForTouch(touch)
when UITouchPhaseStationary
# nothing
when UITouchPhaseEnded, UITouchPhaseCancelled
TouchHighlightView.removeForTouch(touch)
end
end
end
super
end
end
module RunLoopHelpers
extend self
MIN_INTERVAL = 0.01
# TODO this is already in the master branch, merge it!
def proper_wait(sec)
CFRunLoopRunInMode(KCFRunLoopDefaultMode, sec, false)
end
# Keeps trying the block until it returns a truthy value. Has a default
# `timeout` of 3 seconds.
#
# Returns the return value of the block.
def wait_till(timeout = Bacon::Functional.default_timeout)
result = nil
interval = MIN_INTERVAL
while interval < timeout
# First sleep for a bit, otherwise it can happen that even if a view
# (e.g. a button) is found, it won't properly respond to touches yet.
proper_wait(interval)
break if result = yield
interval *= 2
end
result
end
end
module UIViewExt
def viewByName(accessibilityLabel, timeout = Bacon::Functional.default_timeout)
RunLoopHelpers.wait_till(timeout) { _viewByName(accessibilityLabel) }
end
def viewsByClass(viewClass, timeout = Bacon::Functional.default_timeout)
views = RunLoopHelpers.wait_till(timeout) { v = _viewsByClass(viewClass); v if !v.empty? } || []
# sort by Y first, then X
views.sort_by { |v| v.convertPoint(v.bounds.origin, toView:nil).to_a.reverse }
end
def up(viewClass, timeout = Bacon::Functional.default_timeout)
RunLoopHelpers.wait_till(timeout) do
view = self
view = view.superview while view && !view.is_a?(viewClass)
view
end
end
private
def _viewByName(accessibilityLabel)
subviews.each do |subview|
if subview.accessibilityLabel == accessibilityLabel
return subview
elsif found = subview.send(:_viewByName, accessibilityLabel)
return found
end
end
nil
end
def _viewsByClass(viewClass)
result = []
subviews.each do |view|
result << view if view.is_a?(viewClass)
result.concat(view.send(:_viewsByClass, viewClass))
end
result
end
end
UIView.send(:include, UIViewExt)
module Bacon
class Context
# TODO
# * :navigation => true
# * :tab => true
def tests(controller_class, options = {})
@controller_class = controller_class
extend Bacon::Functional::API
extend Bacon::Functional::ContextExt
end
end
module Functional
module ContextExt
def self.extended(context)
context.before do
# Ensure a window exists and is on screen.
window
end
context.after do
# Disable logging events after each spec
app = UIApplication.sharedApplication
if app.respond_to?(:logEvents=)
app.logEvents = app.highlightTouches = false
end
# Remove window and ensure a new one will be created on the next run.
window.removeFromSuperview
proper_wait(0.3) # give objects a chance to do their cleanup, otherwise sometimes a segfault will occur
@window = nil
@controller = nil
end
end
def window
unless @window
@window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)
# On some iOS/simulator versions the background isn't cleared before
# a new window is shown. Make our windows black, just to make it a
# bit prettier.
@window.backgroundColor = UIColor.blackColor
@window.rootViewController = controller
end
@window.makeKeyAndVisible
@window
end
attr_accessor :controller
def controller
@controller ||= @controller_class.alloc.init
end
end
class << self
# The default timeout value, for the view finder methods, in seconds.
attr_accessor :default_timeout
# The default duration, for gestures, in seconds.
attr_accessor :default_duration
end
self.default_timeout = 3
self.default_duration = 0.25
module API
include RunLoopHelpers
attr_accessor :window
# Gets overriden by ContextExt#window when the spec context is configured
# to run against a specific controller.
def window
UIApplication.sharedApplication.keyWindow
end
def log_events!
app = UIApplication.sharedApplication
app.extend(UIApplicationExt) unless app.respond_to?(:logEvents)
app.logEvents = true
end
# Calling this will draw a red dot at each location a touch occurs.
#
# Note, however, that at this moment this *will* break most gestures. If
# you have a good idea of how to make this work then please file a ticket.
def highlight_touches!
app = UIApplication.sharedApplication
app.extend(UIApplicationExt) unless app.respond_to?(:logEvents)
app.highlightTouches = true
end
# Returns a list of points interpolated between `from` and `to`.
#
# The `from` and `to` points should be in window coordinates.
def linear_interpolate(from, to, number_of_points = nil)
number_of_points ||= 20
interval = 1.0 / number_of_points
points = Array.new(number_of_points-2) { |i| _linear_interpolate_point(from, to, (i+1)*interval) }
points.unshift(from)
points.push(to)
points
end
def view(label)
return label if label.is_a?(UIView)
window.viewByName(label) ||
raise(Bacon::Error.new(:error, "Unable to find a view with label `#{label}'"))
end
def views(view_class)
views = window.viewsByClass(view_class)
if views.empty?
raise(Bacon::Error.new(:error, "Unable to find any view of class `#{view_class.name}'"))
end
views
end
def rotate_device(options)
orientation = case options.values_at(:to, :button).compact
when [:portrait, :bottom], [:portrait]
UIInterfaceOrientationPortrait
when [:portrait, :top]
UIInterfaceOrientationPortraitUpsideDown
when [:landscape, :left], [:landscape]
UIInterfaceOrientationLandscapeLeft
when [:landscape, :right]
UIInterfaceOrientationLandscapeRight
end
if UIDevice.currentDevice.orientation != orientation
_event_generator.setOrientation(orientation)
proper_wait(0.6)
end
end
def accelerate(options)
duration = options[:duration] || Functional.default_duration
_event_generator.sendAccelerometerX(options[:x], Y:options[:y], Z:options[:z], duration:duration)
proper_wait(duration)
end
def shake
_event_generator.shake
proper_wait(MIN_INTERVAL)
end
def tap(label_or_view, options = {})
view = view(label_or_view)
taps = options[:times] || 1
touches = options[:touches] || 1
location = _coerce_location_to_point(view, options[:at], false) || view.superview.convertPoint(view.center, toView:nil)
location = view.superview.convertPoint(view.center, toView:window)
_event_generator.sendTaps(taps,
location:location,
withNumberOfTouches:touches,
inRect:window.frame)
proper_wait(taps * 0.4)
view
end
def flick(label_or_view, options)
view = view(label_or_view)
from, to = _extract_start_and_end_points(view, options)
duration = options[:duration] || Functional.default_duration
_event_generator.sendFlickWithStartPoint(from, endPoint:to, duration:duration)
proper_wait(duration)
view
end
def pinch_open(label_or_view, options = {})
view = view(label_or_view)
duration = options[:duration] || Functional.default_duration
options[:from] ||= :left unless options[:to]
from, to = _extract_start_and_end_points(view, options)
EventDispatcher.dispatch(duration) do
_event_generator.sendPinchOpenWithStartPoint(from, endPoint:to, duration:duration)
end
view
end
def pinch_close(label_or_view, options = {})
view = view(label_or_view)
duration = options[:duration] || Functional.default_duration
options[:from] ||= :right unless options[:to]
from, to = _extract_start_and_end_points(view, options)
EventDispatcher.dispatch(duration) do
_event_generator.sendPinchCloseWithStartPoint(from, endPoint:to, duration:duration)
end
view
end
# TODO add scroll helper? E.g. `scroll_down 'Scroll view'` would do `drag 'Scroll view', :from => :bottom`?
def drag(label_or_view, options)
view = view(label_or_view)
duration = options[:duration] || Functional.default_duration
touches = options[:touches] || 1
unless points = options[:points]
from, to = _extract_start_and_end_points(view, options)
points = linear_interpolate(from, to, options[:number_of_points])
end
pointer = Pointer.new(CGPoint.type, points.size)
points.each.with_index do |point, i|
pointer[i] = point
end
EventDispatcher.dispatch(duration) do
_event_generator.sendMultifingerDragWithPointArray(pointer, numPoints:points.size, duration:duration, numFingers:touches)
end
view
end
# TODO offset from the center in the same way that UIAutomation does (values between 0 and 1)
def rotate(label_or_view, options)
view = view(label_or_view)
center = view.superview.convertPoint(view.center, toView:nil)
angle = options[:radians] || (options[:degrees] && options[:degrees] * (Math::PI/180)) || Math::PI
touches = options[:touches] || 2
radius = (view.frame.size.width / 2.0)
duration = options[:duration] || Functional.default_duration
EventDispatcher.dispatch(duration) do
_event_generator.sendRotate(center, withRadius:radius, rotation:angle, duration:duration, touchCount:touches)
end
view
end
private
def _event_generator
UIASyntheticEvents.sharedEventGenerator
end
def _location_opposite(location)
case location
when :top_left
:bottom_right
when :top
:bottom
when :top_right
:bottom_left
when :right
:left
when :bottom_right
:top_left
when :bottom
:top
when :bottom_left
:top_right
when :left
:right
else
raise ArgumentError, "Invalid location value `#{location}'."
end
end
LOCATION_TO_POINT_INSET = 5
def _location_to_point(view, location, raise_if_invalid = true)
frame = view.frame
case location
when :top_left
CGPointMake(CGRectGetMinX(frame) + LOCATION_TO_POINT_INSET, CGRectGetMinY(frame) + LOCATION_TO_POINT_INSET)
when :top
CGPointMake(CGRectGetMidX(frame), CGRectGetMinY(frame) + LOCATION_TO_POINT_INSET)
when :top_right
CGPointMake(CGRectGetMaxX(frame) - LOCATION_TO_POINT_INSET, CGRectGetMinY(frame) + LOCATION_TO_POINT_INSET)
when :right
CGPointMake(CGRectGetMaxX(frame) - LOCATION_TO_POINT_INSET, CGRectGetMidY(frame))
when :bottom_right
CGPointMake(CGRectGetMaxX(frame) - LOCATION_TO_POINT_INSET, CGRectGetMaxY(frame) - LOCATION_TO_POINT_INSET)
when :bottom
CGPointMake(CGRectGetMidX(frame), CGRectGetMaxY(frame) - LOCATION_TO_POINT_INSET)
when :bottom_left
CGPointMake(CGRectGetMinX(frame) + LOCATION_TO_POINT_INSET, CGRectGetMaxY(frame) - LOCATION_TO_POINT_INSET)
when :left
CGPointMake(CGRectGetMinX(frame) + LOCATION_TO_POINT_INSET, CGRectGetMidY(frame))
else
raise ArgumentError, "Invalid location value `#{location}'." if raise_if_invalid
end
end
def _location_to_converted_point(view, location, raise_if_invalid = true)
if sv = view.superview
sv.convertPoint(_location_to_point(view, location, raise_if_invalid), toView:nil)
else
raise ArgumentError, "It is not possible to use the location constants on a view that has no superview."
end
end
def _linear_interpolate_point(a, b, alpha)
x = a.x + ((b.x-a.x) * alpha)
y = a.y + ((b.y-a.y) * alpha)
CGPointMake(x.round, y.round)
end
def _coerce_location_to_point(view, value, raise_if_invalid = true)
case value
when CGPoint then value
when Symbol then _location_to_converted_point(view, value, raise_if_invalid)
end
end
def _extract_point(view, options, from_or_to)
unless point = _coerce_location_to_point(view, options[from_or_to])
other = from_or_to == :from ? :to : :from
if options[other] && options[other].is_a?(Symbol)
point = _location_to_converted_point(view, _location_opposite(options[other]))
else
raise ArgumentError, "No :#{from_or_to} location given and unable to inflect from :#{other} location `#{options[opposite].inspect}'."
end
end
point
end
def _extract_start_and_end_points(view, options)
from = _extract_point(view, options, :from)
to = _extract_point(view, options, :to)
[from, to]
end
def _view(accessibilityLabel, error_message = nil)
return accessibilityLabel if accessibilityLabel.is_a?(UIView)
window.viewByName(accessibilityLabel) ||
raise(Bacon::Error.new(:error, error_message || "Unable to find a view with label `#{accessibilityLabel}'"))
end
# This class wraps a block that will be executed on a GCD queue, but will
# halt the main thread's call stack (while still handling the main thread's
# runloop) until the block has completely finished its work.
#
# This is only meant for those events that are continuous and need more
# time to be generated. E.g. a pinch gesture.
class EventDispatcher
include RunLoopHelpers
def self.dispatch(duration, &block)
new(duration, &block).call
end
attr_reader :done, :duration
def initialize(duration, &block)
@duration = duration
@done = false
@block = block
end
def call
group = Dispatch::Group.new
queue = Dispatch::Queue.concurrent
queue.async(group, &@block)
group.notify(queue) { @done = true }
# First wait the standard duration, then block additionally until the
# job has been actually finished.
#
# TODO have one mixin with wait helpers?
proper_wait(@duration)
proper_wait(MIN_INTERVAL) while !@done
end
end
end
end
end

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

View File

@@ -0,0 +1,93 @@
class ScrollViewController < UIViewController
attr_reader :scrollView, :scrollViewRotation, :imageView
def loadView
@scrollViewRotation = 0
frame = UIScreen.mainScreen.applicationFrame
frame.origin = CGPointZero
self.view = UIView.alloc.initWithFrame(frame)
@scrollView = UIScrollView.alloc.initWithFrame(view.bounds)
@scrollView.backgroundColor = UIColor.redColor
@scrollView.delegate = self
@scrollView.maximumZoomScale = 3
view.addSubview(@scrollView)
@imageView = UIImageView.alloc.initWithImage(UIImage.imageNamed('We Need You.jpg'))
@imageView.frame = [[0, 0], @imageView.image.size]
@scrollView.accessibilityLabel = 'Scroll view'
@scrollView.contentSize = @imageView.image.size
@scrollView.addSubview(@imageView)
recognizer = UIRotationGestureRecognizer.alloc.initWithTarget(self, action:'handleRotation:')
@scrollView.addGestureRecognizer(recognizer)
end
def viewForZoomingInScrollView(sv)
@imageView
end
def handleRotation(recognizer)
@scrollViewRotation = recognizer.rotation
@scrollView.transform = CGAffineTransformMakeRotation(recognizer.rotation)
end
end
describe "Bacon::Functional::API, concerning continuous gestures" do
tests ScrollViewController
it "creates 'pinch open' and 'pinch close' gesture events" do
before = controller.scrollView.zoomScale
pinch_open 'Scroll view'
controller.scrollView.zoomScale.should > before
before = controller.scrollView.zoomScale
pinch_close 'Scroll view'
controller.scrollView.zoomScale.should < before
end
# TODO currently it doesn't rotate exactly 90 degreese. I think this has to
# do with the center point being off and the radius not being a perfect half
# of the total diameter.
it "creates rotate gesture events" do
before = controller.scrollViewRotation
rotate 'Scroll view', :degrees => 90
controller.scrollViewRotation.should > before
#before = controller.scrollView.transform
#rotate 'Scroll view', :degrees => 90, :duration => 5
#after = controller.scrollView.transform
#expected = CGAffineTransformRotate(before, Math::PI/2)
#CGAffineTransformEqualToTransform(after, expected).should == true
end
before do
pinch_open 'Scroll view'
end
it "drags from point A to point B" do
before = controller.scrollView.contentOffset
drag 'Scroll view', :from => CGPointMake(310, 100), :to => CGPointMake(5, 150)
controller.scrollView.contentOffset.x.should > before.x
controller.scrollView.contentOffset.y.should < before.y
end
it "drags along the specified list of points" do
view = controller.scrollView
before = view.contentOffset
drag 'Scroll view', :points => linear_interpolate(_location_to_point(view, :bottom_right), _location_to_point(view, :left))
view.contentOffset.x.should > before.x
view.contentOffset.y.should > before.y
end
it "drags with multiple fingers" do
controller.scrollView.panGestureRecognizer.minimumNumberOfTouches = 3
controller.scrollView.panGestureRecognizer.maximumNumberOfTouches = 3
before = controller.scrollView.contentOffset
drag 'Scroll view', :from => :right, :touches => 3
controller.scrollView.contentOffset.x.should > before.x
controller.scrollView.contentOffset.y.should == before.y
end
end

View File

@@ -0,0 +1,99 @@
class DeviceSpecController < UIViewController
def loadView
frame = UIScreen.mainScreen.applicationFrame
frame.origin = CGPointZero
self.view = UIImageView.alloc.initWithFrame(frame)
view.image = UIImage.imageNamed('We Need You.jpg')
end
def shouldAutorotateToInterfaceOrientation(orientation)
true
end
# This is all for `shake` support
attr_accessor :shaked
def shaked?
@shaked
end
def viewWillAppear(animated)
super
becomeFirstResponder
end
def viewDidDisappear(animated)
super
resignFirstResponder
end
def canBecomeFirstResponder
true
end
def motionEnded(motion, withEvent:event)
@shaked = motion == UIEventSubtypeMotionShake
end
# Accelerometer support
def enableAccelerometer=(flag)
UIAccelerometer.sharedAccelerometer.delegate = (flag == true ? self : nil)
end
attr_reader :accelerationData
def accelerometer(accelerometer, didAccelerate:acceleration)
@accelerationData = acceleration
end
end
describe "Bacon::Functional::API, concerning device events" do
tests DeviceSpecController
after do
rotate_device :to => :portrait
end
it "changes device orientation" do
rotate_device :to => :landscape, :button => :right
controller.interfaceOrientation.should == UIInterfaceOrientationLandscapeRight
rotate_device :to => :landscape, :button => :left
controller.interfaceOrientation.should == UIInterfaceOrientationLandscapeLeft
rotate_device :to => :portrait, :button => :bottom
controller.interfaceOrientation.should == UIInterfaceOrientationPortrait
rotate_device :to => :portrait, :button => :top
controller.interfaceOrientation.should == UIInterfaceOrientationPortraitUpsideDown
end
it "has default orientations for portrait and landscape for when the :button option is omitted" do
rotate_device :to => :landscape
controller.interfaceOrientation.should == UIInterfaceOrientationLandscapeLeft
rotate_device :to => :portrait
controller.interfaceOrientation.should == UIInterfaceOrientationPortrait
end
it "creates a shake motion gesture (for undo support, for instance)" do
shake
controller.should.be.shaked
end
it "sends accelerometer events" do
with_accelerometer do
accelerate :x => 0.5, :y => 0.5, :z => 0.5
end
controller.accelerationData.x.should == 0.5
controller.accelerationData.y.should == 0.5
controller.accelerationData.z.should == 0.5
end
def with_accelerometer
controller.enableAccelerometer = true
yield
ensure
controller.enableAccelerometer = false
end
end

View File

@@ -0,0 +1,141 @@
class SmallControlsViewController < UIViewController
attr_reader :tappableView, :switch, :lateKumbayaButton
def loadView
frame = UIScreen.mainScreen.applicationFrame
frame.origin = CGPointZero
self.view = UIView.alloc.initWithFrame(frame)
#view.userInteractionEnabled = true
view.accessibilityLabel = 'Container view'
buttonMargin, buttonWidth, buttonHeight = 20, 150, 34
buttonY = buttonMargin
button = UIButton.buttonWithType(UIButtonTypeRoundedRect)
button.setTitle("Kumbaya!", forState:UIControlStateNormal)
button.frame = CGRectMake((frame.size.width - buttonWidth) / 2, buttonY, buttonWidth, buttonHeight)
button.addTarget(self, action:'buttonTapped=:', forControlEvents:UIControlEventTouchUpInside)
view.addSubview(button)
buttonY += buttonHeight + buttonMargin
@lateKumbayaButton = UIButton.buttonWithType(UIButtonTypeRoundedRect)
@lateKumbayaButton.setTitle("Late Kumbaya!", forState:UIControlStateNormal)
@lateKumbayaButton.frame = CGRectMake((frame.size.width - buttonWidth) / 2, buttonY, buttonWidth, buttonHeight)
@lateKumbayaButton.addTarget(self, action:'buttonTapped=:', forControlEvents:UIControlEventTouchUpInside)
# Delay adding the view so we can test that the `view` method retries until it's found
view.performSelector('addSubview:', withObject:@lateKumbayaButton, afterDelay:1)
switchY = buttonY + buttonHeight + buttonMargin
@switch = UISwitch.alloc.initWithFrame(CGRectMake((frame.size.width - buttonWidth) / 2, switchY, buttonWidth, buttonHeight))
@switch.accessibilityLabel = 'Switch control'
@switch.on = false
view.addSubview(@switch)
# Nested tap gesture recognizers
tappableViewContainerY = switchY + buttonHeight + buttonMargin
container = UIView.alloc.initWithFrame(CGRectMake(10, tappableViewContainerY, buttonWidth + 50, buttonHeight + 50))
container.backgroundColor = UIColor.greenColor
view.addSubview(container)
tappableViewY = buttonMargin
@tappableView = UILabel.alloc.initWithFrame(CGRectMake(10, tappableViewY, buttonWidth, buttonHeight))
@tappableView.userInteractionEnabled = true
@tappableView.backgroundColor = UIColor.blueColor
@tappableView.accessibilityLabel = 'Tappable view'
@tappableView.textAlignment = UITextAlignmentCenter
@tappableView.text = 'Taps: 0'
container.addSubview(@tappableView)
previous_recognizer = nil
3.downto(1) do |taps|
2.downto(1) do |touches|
recognizer = UITapGestureRecognizer.alloc.initWithTarget(self, action:'handleTap:')
recognizer.numberOfTapsRequired = taps
recognizer.numberOfTouchesRequired = touches
@tappableView.addGestureRecognizer(recognizer)
recognizer.requireGestureRecognizerToFail(previous_recognizer) if previous_recognizer
previous_recognizer = recognizer
end
end
end
#def viewWillAppear(animated)
#super
##self.navigationItem.rightBarButtonItem = UIBarButtonItem.alloc.initWithBarButtonSystemItem(UIBarButtonSystemItemDone, target:self, action:'bla:')
#end
attr_accessor :buttonTapped
# RM TODO: Assertion failed: (i != sel_to_attr.end()), function rb_attr_generic_getter, file vm.cpp, line 2753.
#alias_method :buttonTapped?, :buttonTapped
def buttonTapped?; @buttonTapped; end
attr_reader :tapRecognizer
attr_reader :tappedLocationInWindow
def handleTap(recognizer)
@tappableView.text = "Taps: #{recognizer.numberOfTapsRequired}"
@tapRecognizer = recognizer
@tappedLocationInWindow = recognizer.locationInView(nil)
end
end
describe "Bacon::Functional::API, concerning one-shot gestures" do
tests SmallControlsViewController
it "flicks a switch" do
flick "Switch control", :from => :left
controller.switch.isOn.should == true
flick "Switch control", :to => :left
controller.switch.isOn.should == false
end
it "by default taps at the center of a view" do
highlight_touches!
view = tap("Tappable view")
controller.tappedLocationInWindow.should == view.superview.convertPoint(view.center, toView:nil)
end
#it "taps at a specific point in window coordinates" do
#view = controller.tappableView
#point = CGPointMake(view.frame.origin.x + 10, view.frame.origin.y + 10)
#point = view.superview.convertPoint(point, toView:nil)
#tap "Tappable view", :at => point
#controller.tappedLocationInWindow.should == point
#end
it "taps buttons" do
tap "Kumbaya!"
controller.buttonTapped.currentTitle.should == "Kumbaya!"
# this one actually shows up a second later
tap "Late Kumbaya!"
controller.buttonTapped.currentTitle.should == "Late Kumbaya!"
end
it "recognizes a single tap" do
tap "Tappable view"
controller.tapRecognizer.numberOfTapsRequired.should == 1
controller.tapRecognizer.numberOfTouchesRequired.should == 1
tap "Tappable view", :touches => 2
controller.tapRecognizer.numberOfTapsRequired.should == 1
controller.tapRecognizer.numberOfTouchesRequired.should == 2
end
it "recognizes a double tap" do
tap "Tappable view", :times => 2
controller.tapRecognizer.numberOfTapsRequired.should == 2
controller.tapRecognizer.numberOfTouchesRequired.should == 1
tap "Tappable view", :times => 2, :touches => 2
controller.tapRecognizer.numberOfTapsRequired.should == 2
controller.tapRecognizer.numberOfTouchesRequired.should == 2
end
it "recognizes a triple tap" do
tap "Tappable view", :times => 3
controller.tapRecognizer.numberOfTapsRequired.should == 3
controller.tapRecognizer.numberOfTouchesRequired.should == 1
tap "Tappable view", :times => 3, :touches => 2
controller.tapRecognizer.numberOfTapsRequired.should == 3
controller.tapRecognizer.numberOfTouchesRequired.should == 2
end
end

View File

@@ -0,0 +1,147 @@
describe "Bacon::Functional::API, concerning location helpers" do
extend Bacon::Functional::API
def inset
Bacon::Functional::API::LOCATION_TO_POINT_INSET
end
it "returns the points corresponding to the view" do
view = UIView.alloc.initWithFrame(CGRectMake(100, 100, 100, 100))
_location_to_point(view, :top_left).should == CGPointMake(100+inset, 100+inset)
_location_to_point(view, :top).should == CGPointMake(150, 100+inset)
_location_to_point(view, :top_right).should == CGPointMake(200-inset, 100+inset)
_location_to_point(view, :right).should == CGPointMake(200-inset, 150)
_location_to_point(view, :bottom_right).should == CGPointMake(200-inset, 200-inset)
_location_to_point(view, :bottom).should == CGPointMake(150, 200-inset)
_location_to_point(view, :bottom_left).should == CGPointMake(100+inset, 200-inset)
_location_to_point(view, :left).should == CGPointMake(100+inset, 150)
end
it "returns the opposite of a location" do
_location_opposite(:top_left).should == :bottom_right
_location_opposite(:top).should == :bottom
_location_opposite(:top_right).should == :bottom_left
_location_opposite(:right).should == :left
_location_opposite(:bottom_right).should == :top_left
_location_opposite(:bottom).should == :top
_location_opposite(:bottom_left).should == :top_right
_location_opposite(:left).should == :right
end
end
class ContainerView < UIView
end
class SimpleViewController < UIViewController
attr_reader :purpleView, :blueView, :redView
def loadView
frame = UIScreen.mainScreen.applicationFrame
frame.origin = CGPointZero
self.view = ContainerView.alloc.initWithFrame(frame)
view.accessibilityLabel = 'Container view'
@purpleView = UIView.alloc.initWithFrame(CGRectMake(100, 100, 100, 100))
@purpleView.backgroundColor = UIColor.purpleColor
@purpleView.accessibilityLabel = 'Purple view'
view.addSubview(@purpleView)
@blueView = UIView.alloc.initWithFrame(CGRectMake(25, 25, 50, 50))
@blueView.backgroundColor = UIColor.blueColor
@blueView.accessibilityLabel = 'Blue view'
@purpleView.performSelector('addSubview:', withObject:@blueView, afterDelay:0.5)
@redView = UIView.alloc.initWithFrame(CGRectMake(110, 25, 75, 75))
@redView.backgroundColor = UIColor.redColor
@redView.accessibilityLabel = 'Red view'
view.addSubview(@redView)
end
end
describe "UIView extensions" do
tests SimpleViewController
it "returns the first subview with the specified accessibility label" do
window.viewByName('Container view', 1).should == controller.view
controller.view.viewByName('Purple view', 1).should == controller.purpleView
controller.blueView.viewByName('Purple view', 1).should == nil
end
it "keeps trying to find a view by accessibility label during the timeout" do
# This button shows up 0.5 second after the other views
window.viewByName('Blue view', 0.1).should == nil
window.viewByName('Blue view', 0.6).should == controller.blueView
end
it "looks through all superviews, until it finds a matching class" do
controller.purpleView.up(UIButton, 0.1).should == nil
controller.purpleView.up(ContainerView, 0.6).should == controller.view
end
it "keeps trying to look through all superviews, until it finds a matching class, during the timeout" do
controller.blueView.up(ContainerView, 0.1).should == nil
controller.blueView.up(ContainerView, 0.6).should == controller.view
end
it "returns all views of a specific class and sorts them top-left to bottom-right" do
window.viewsByClass(UIButton, 0.1).should == []
window.viewsByClass(ContainerView, 0.1).should == [controller.view]
window.viewsByClass(UIView, 0.1).should == [
controller.view,
controller.redView,
controller.purpleView
]
end
it "keeps trying to find a view by class during the timeout" do
controller.purpleView.viewsByClass(UIView, 0.1).should == []
controller.purpleView.viewsByClass(UIView, 0.6).should == [controller.blueView]
end
end
describe "Bacon::Functional::API, concerning device events" do
tests SimpleViewController
it "finds a view by its accessibility label" do
view('Purple view').should == controller.purpleView
view('Blue view').should == controller.blueView
end
it "finds views by class" do
views(ContainerView).should == [controller.view]
end
it "returns a view immediately if given instead of an accessibility label" do
view = controller.purpleView
# This will raise if the #view helper would not actually return the view immediately
def window.viewByName(accessibilityLabel)
raise 'Oh noes!'
end
view(view).should == view
end
it "raises if no view by label could be found after the `timeout` passes" do
start = Time.now.to_i
e = catch_bacon_error { view('Does not exist') }
Time.now.to_i.should >= (start + 3)
e.message.should == "Unable to find a view with label `Does not exist'"
end
it "raises if no views by class could be found after the `timeout` passes" do
start = Time.now.to_i
e = catch_bacon_error { views(UITableView) }
Time.now.to_i.should >= (start + 3)
e.message.should == "Unable to find any view of class `UITableView'"
end
def catch_bacon_error
e = nil
begin
yield
rescue Bacon::Error => e
end
e.should.not == nil
e.count_as.should == :error
e
end
end