mirror of
https://github.com/zhigang1992/RubyMotion.git
synced 2026-05-16 18:09:31 +08:00
integrate spec/ui work
This commit is contained in:
@@ -36,8 +36,7 @@ task :bridgesupport_files do
|
||||
sdk_frameworks = "#{sdk_path}/System/Library/Frameworks"
|
||||
mkdir_p "#{sdk_version}/BridgeSupport"
|
||||
|
||||
rm_bs = "#{sdk_version}/BridgeSupport/RubyMotion.bridgesupport"
|
||||
cp 'RubyMotion.bridgesupport', rm_bs
|
||||
Dir.glob('*.bridgesupport').each { |bs| cp bs, "#{sdk_version}/BridgeSupport" }
|
||||
|
||||
next if ENV['SDK_BETA']
|
||||
|
||||
|
||||
3065
data/UIAutomation.bridgesupport
Normal file
3065
data/UIAutomation.bridgesupport
Normal file
File diff suppressed because it is too large
Load Diff
@@ -162,7 +162,6 @@ module Motion; module Project;
|
||||
app_objs = objs
|
||||
if config.spec_mode
|
||||
# Build spec files too, but sequentially.
|
||||
objs << build_file.call(File.expand_path(File.join(File.dirname(__FILE__), '../spec.rb')))
|
||||
spec_objs = config.spec_files.map { |path| build_file.call(path) }
|
||||
objs += spec_objs
|
||||
end
|
||||
@@ -196,11 +195,31 @@ EOS
|
||||
@interface SpecLauncher : NSObject
|
||||
@end
|
||||
|
||||
#include <dlfcn.h>
|
||||
|
||||
@implementation SpecLauncher
|
||||
|
||||
+ (id)launcher
|
||||
{
|
||||
[UIApplication sharedApplication];
|
||||
|
||||
// Enable simulator accessibility.
|
||||
// Thanks http://www.stewgleadow.com/blog/2011/10/14/enabling-accessibility-for-ios-applications/
|
||||
NSString *simulatorRoot = [[[NSProcessInfo processInfo] environment] objectForKey:@"IPHONE_SIMULATOR_ROOT"];
|
||||
if (simulatorRoot != nil) {
|
||||
void *appSupportLibrary = dlopen([[simulatorRoot stringByAppendingPathComponent:@"/System/Library/PrivateFrameworks/AppSupport.framework/AppSupport"] fileSystemRepresentation], RTLD_LAZY);
|
||||
CFStringRef (*copySharedResourcesPreferencesDomainForDomain)(CFStringRef domain) = (CFStringRef (*)(CFStringRef)) dlsym(appSupportLibrary, "CPCopySharedResourcesPreferencesDomainForDomain");
|
||||
|
||||
if (copySharedResourcesPreferencesDomainForDomain != NULL) {
|
||||
CFStringRef accessibilityDomain = copySharedResourcesPreferencesDomainForDomain(CFSTR("com.apple.Accessibility"));
|
||||
|
||||
if (accessibilityDomain != NULL) {
|
||||
CFPreferencesSetValue(CFSTR("ApplicationAccessibilityEnabled"), kCFBooleanTrue, accessibilityDomain, kCFPreferencesAnyUser, kCFPreferencesAnyHost);
|
||||
CFRelease(accessibilityDomain);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SpecLauncher *launcher = [[self alloc] init];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:launcher selector:@selector(appLaunched:) name:UIApplicationDidBecomeActiveNotification object:nil];
|
||||
return launcher;
|
||||
|
||||
@@ -289,7 +289,9 @@ EOS
|
||||
end
|
||||
deps << framework
|
||||
end
|
||||
deps.uniq.select { |dep| File.exist?(File.join(datadir, 'BridgeSupport', dep + '.bridgesupport')) }
|
||||
deps = deps.uniq.select { |dep| File.exist?(File.join(datadir, 'BridgeSupport', dep + '.bridgesupport')) }
|
||||
deps << 'UIAutomation' if spec_mode
|
||||
deps
|
||||
end
|
||||
end
|
||||
|
||||
@@ -311,7 +313,10 @@ EOS
|
||||
end
|
||||
|
||||
def spec_files
|
||||
Dir.glob(File.join(specs_dir, '**', '*.rb'))
|
||||
@spec_files ||= begin
|
||||
# core library + core helpers + project helpers + project specs
|
||||
Dir.chdir(File.join(File.dirname(__FILE__), '..')) { (['spec.rb'] + Dir.glob('spec/helpers/*.rb')).map { |x| File.expand_path(x) } } + Dir.glob(File.join(specs_dir, 'helpers', '*.rb')) + Dir.glob(File.join(specs_dir, '*.rb'))
|
||||
end
|
||||
end
|
||||
|
||||
def motiondir
|
||||
@@ -389,7 +394,9 @@ EOS
|
||||
end
|
||||
|
||||
def common_flags(platform)
|
||||
"#{arch_flags(platform)} -isysroot \"#{sdk(platform)}\" -miphoneos-version-min=#{deployment_target} -F#{sdk(platform)}/System/Library/Frameworks"
|
||||
cflags = "#{arch_flags(platform)} -isysroot \"#{sdk(platform)}\" -miphoneos-version-min=#{deployment_target} -F#{sdk(platform)}/System/Library/Frameworks"
|
||||
cflags << " -F#{sdk(platform)}/Developer/Library/PrivateFrameworks" if spec_mode # For UIAutomation
|
||||
cflags
|
||||
end
|
||||
|
||||
def cflags(platform, cplusplus)
|
||||
|
||||
537
lib/motion/spec/helpers/ui.rb
Normal file
537
lib/motion/spec/helpers/ui.rb
Normal file
@@ -0,0 +1,537 @@
|
||||
# macbacon-ui extensions.
|
||||
#
|
||||
# Copyright (C) 2012 Eloy Durán eloy.de.enige@gmail.com
|
||||
#
|
||||
# Bacon is freely distributable under the terms of an MIT-style license.
|
||||
# See COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
# 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
|
||||
Reference in New Issue
Block a user