mirror of
https://github.com/zhigang1992/RubyMotion.git
synced 2026-06-11 08:14:16 +08:00
add bacon-ui test suite
This commit is contained in:
9
test/bacon-ui/Rakefile
Normal file
9
test/bacon-ui/Rakefile
Normal 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
|
||||
5
test/bacon-ui/app/app_delegate.rb
Normal file
5
test/bacon-ui/app/app_delegate.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
class AppDelegate
|
||||
def application(application, didFinishLaunchingWithOptions:launchOptions)
|
||||
true
|
||||
end
|
||||
end
|
||||
530
test/bacon-ui/app/macbacon_ui.rb
Normal file
530
test/bacon-ui/app/macbacon_ui.rb
Normal 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
|
||||
BIN
test/bacon-ui/resources/We Need You.jpg
Normal file
BIN
test/bacon-ui/resources/We Need You.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 116 KiB |
93
test/bacon-ui/spec/continuous_gestures_spec.rb
Normal file
93
test/bacon-ui/spec/continuous_gestures_spec.rb
Normal 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
|
||||
99
test/bacon-ui/spec/device_events_spec.rb
Normal file
99
test/bacon-ui/spec/device_events_spec.rb
Normal 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
|
||||
141
test/bacon-ui/spec/simple_gestures_spec.rb
Normal file
141
test/bacon-ui/spec/simple_gestures_spec.rb
Normal 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
|
||||
147
test/bacon-ui/spec/view_spec.rb
Normal file
147
test/bacon-ui/spec/view_spec.rb
Normal 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
|
||||
Reference in New Issue
Block a user