Skip to content

Commit

Permalink
LukeIS: switchTo support for iOS frame and window. There is no suppor…
Browse files Browse the repository at this point in the history
…t for multiple windows, so switching windows does nothing. UIWebView does not give direct access to the page's frame objects, it has to be done through JavaScript, so there is no cross domain frame switching support. Fixes issue 1371

r15656
  • Loading branch information
lukeis committed Jan 25, 2012
1 parent cae19df commit 93c33f3
Show file tree
Hide file tree
Showing 15 changed files with 220 additions and 68 deletions.
8 changes: 8 additions & 0 deletions iphone/iWebDriver.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@
40005B8B14AA80C6009763C2 /* DDFileLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = 40005B8314AA80A1009763C2 /* DDFileLogger.m */; };
40005B8C14AA80C6009763C2 /* DDLog.m in Sources */ = {isa = PBXBuildFile; fileRef = 40005B8514AA80A1009763C2 /* DDLog.m */; };
40005B8D14AA80C6009763C2 /* DDTTYLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = 40005B8714AA80A1009763C2 /* DDTTYLogger.m */; };
4003054714CA44F80080BF72 /* FrameContext.m in Sources */ = {isa = PBXBuildFile; fileRef = 4003054614CA44F80080BF72 /* FrameContext.m */; };
4003054814CA44F80080BF72 /* FrameContext.m in Sources */ = {isa = PBXBuildFile; fileRef = 4003054614CA44F80080BF72 /* FrameContext.m */; };
40597F1214AB78C80056C533 /* GCDAsyncSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 40005B7B14AA80A1009763C2 /* GCDAsyncSocket.m */; };
40597F1414AB78C80056C533 /* DDAbstractDatabaseLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = 40005B7F14AA80A1009763C2 /* DDAbstractDatabaseLogger.m */; };
40597F1614AB78C80056C533 /* DDASLLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = 40005B8114AA80A1009763C2 /* DDASLLogger.m */; };
Expand Down Expand Up @@ -256,6 +258,8 @@
40005B8514AA80A1009763C2 /* DDLog.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DDLog.m; sourceTree = "<group>"; };
40005B8614AA80A1009763C2 /* DDTTYLogger.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DDTTYLogger.h; sourceTree = "<group>"; };
40005B8714AA80A1009763C2 /* DDTTYLogger.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DDTTYLogger.m; sourceTree = "<group>"; };
4003054514CA44F80080BF72 /* FrameContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FrameContext.h; sourceTree = "<group>"; };
4003054614CA44F80080BF72 /* FrameContext.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FrameContext.m; sourceTree = "<group>"; };
409AF55C147BF8EB00554A6E /* NSObject+SBJson.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = "NSObject+SBJson.m"; path = "../third_party/objc/json-framework/Classes/NSObject+SBJson.m"; sourceTree = "<group>"; };
409AF55D147BF8EB00554A6E /* SBJsonParser.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = SBJsonParser.m; path = "../third_party/objc/json-framework/Classes/SBJsonParser.m"; sourceTree = "<group>"; };
409AF55E147BF8EB00554A6E /* SBJsonStreamParser.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = SBJsonStreamParser.m; path = "../third_party/objc/json-framework/Classes/SBJsonStreamParser.m"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -707,6 +711,8 @@
839049340F1ACEFD001A79B6 /* Attribute.m */,
73D14FCB1289B6540074E63B /* Database.h */,
73D14FCC1289B6540074E63B /* Database.m */,
4003054514CA44F80080BF72 /* FrameContext.h */,
4003054614CA44F80080BF72 /* FrameContext.m */,
);
name = "REST Service";
sourceTree = "<group>";
Expand Down Expand Up @@ -996,13 +1002,15 @@
64A2262E12C1828000537E73 /* NSData+Base64.m in Sources */,
648EB1FD137C63B800DEFD44 /* Css.m in Sources */,
40BE09AB147714580042C6DA /* Status.m in Sources */,
4003054714CA44F80080BF72 /* FrameContext.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
839988DC0EE6052D0074D106 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
4003054814CA44F80080BF72 /* FrameContext.m in Sources */,
40597F3414AB78E90056C533 /* HTTPAsyncFileResponse.m in Sources */,
40597F3614AB78E90056C533 /* HTTPDataResponse.m in Sources */,
40597F3814AB78E90056C533 /* HTTPDynamicFileResponse.m in Sources */,
Expand Down
1 change: 0 additions & 1 deletion iphone/src/objc/Attribute.m
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
#import "WebDriverResponse.h"
#import "HTTPVirtualDirectory+ExecuteScript.h"
#import "HTTPStaticResource.h"
#import "WebViewController.h"

@implementation Attribute

Expand Down
26 changes: 26 additions & 0 deletions iphone/src/objc/FrameContext.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//
// FrameContext.h
// iWebDriver
//
// Created by Luke Inman-Semerau on 1/20/12.
// Copyright (c) 2012 Software Freedom Conservancy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#import <Foundation/Foundation.h>

@interface FrameContext : NSMutableArray

+(FrameContext*)sharedInstance;

@end
39 changes: 39 additions & 0 deletions iphone/src/objc/FrameContext.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// FrameContext.m
// iWebDriver
//
// Created by Luke Inman-Semerau on 1/20/12.
// Copyright (c) 2012 Software Freedom Conservancy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#import "FrameContext.h"

@implementation FrameContext

// Apologies to all those who know Obj-C way better.
// This was to have a global variable that's an NSMutableArray
// I would much prefer this to be on the Session object and then fetched
// via WebViewController, but alas WVC doesn't have access to the Session
// object and it would be a significant effort to make it available.
static FrameContext *singleton = nil;

+(FrameContext*) sharedInstance {
if (singleton == nil) {
singleton = [[NSMutableArray alloc] init];
}

return singleton;
}

@end
10 changes: 10 additions & 0 deletions iphone/src/objc/Session.m
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,11 @@ - (id) initWithSessionRootAndSessionId:(SessionRoot*)root
[self setResourceToViewMethodGET:@selector(windowHandles)
POST:NULL
withName:@"window_handles"];

// switch to window
[self setResourceToViewMethodGET:NULL
POST:@selector(window:)
withName:@"window"];

// HTML5 Local WebStorage
[self setResource:[Storage storageWithType:LOCAL_STORAGE]
Expand Down Expand Up @@ -166,6 +171,11 @@ - (id) initWithSessionRootAndSessionId:(SessionRoot*)root
[self setResource:[Timeouts timeoutsForSession:self]
withName:@"timeouts"];

// switch to frame
[self setResourceToViewMethodGET:NULL
POST:@selector(frame:)
withName:@"frame"];

[self cleanSessionStatus];

return self;
Expand Down
3 changes: 3 additions & 0 deletions iphone/src/objc/WebDriverResource.m
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,9 @@ - (WebDriverResponse *)createResponseFromSelector:(SEL)selector
}

NSDictionary *arguments = [self getArgumentDictionaryFromData:theData];
if (arguments != nil) {
[arguments setValue:session_ forKey:@"sessionId"];
}

[[MainViewController sharedInstance]
describeLastAction:NSStringFromSelector(selector)];
Expand Down
2 changes: 2 additions & 0 deletions iphone/src/objc/WebViewController.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@
- (void)back:(NSDictionary*)ignored;
- (void)refresh:(NSDictionary*)ignored;

- (void)frame:(NSDictionary*)frameTarget;

// Evaluate a javascript string and return the result.
// Arguments can be passed in in NSFormatter (printf) style.
//
Expand Down
120 changes: 109 additions & 11 deletions iphone/src/objc/WebViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,24 @@
// See the License for the specific language governing permissions and
// limitations under the License.

#import "WebViewController.h"
#import <objc/runtime.h>
#import <QuartzCore/QuartzCore.h>
#import <QuartzCore/CATransaction.h>

#import "errorcodes.h"
#import "FrameContext.h"
#import "GeoLocation.h"
#import "HTTPServerController.h"
#import "NSObject+SBJson.h"
#import "NSException+WebDriver.h"
#import "NSURLRequest+IgnoreSSL.h"
#import "RootViewController.h"
#import "UIResponder+SimulateTouch.h"
#import "WebDriverResponse.h"
#import "WebDriverPreferences.h"
#import "WebDriverRequestFetcher.h"
#import "WebDriverUtilities.h"
#import "NSObject+SBJson.h"
#import <objc/runtime.h>
#import "RootViewController.h"
#import <QuartzCore/QuartzCore.h>
#import <QuartzCore/CATransaction.h>
#import "GeoLocation.h"
#import "errorcodes.h"
#import "WebViewController.h"

static const NSString* kGeoLocationKey = @"location";
static const NSString* kGeoLongitudeKey = @"longitude";
Expand Down Expand Up @@ -60,9 +63,9 @@ - (void)viewDidLoad {
// Creating a new session if auto-create is enabled
if ([[RootViewController sharedInstance] isAutoCreateSession]) {
[[HTTPServerController sharedInstance]
httpResponseForQuery:@"/hub/session"
httpResponseForQuery:@"/wd/hub/session"
method:@"POST"
withData:[@"{\"browserName\":\"firefox\",\"platform\":\"ANY\","
withData:[@"{\"browserName\":\"safari\",\"platform\":\"iOS\","
"\"javascriptEnabled\":false,\"version\":\"\"}"
dataUsingEncoding:NSASCIIStringEncoding]];
}
Expand Down Expand Up @@ -259,6 +262,9 @@ - (void)setURL:(NSDictionary *)urlMap {
[self performSelectorOnView:@selector(loadRequest:)
withObject:url
waitUntilLoad:YES];
// setting the URL happens on the main container, all
// switch_to's are reset.
[[FrameContext sharedInstance] removeAllObjects];
}

- (void)back:(NSDictionary*)ignored {
Expand Down Expand Up @@ -292,6 +298,69 @@ -(NSArray*)windowHandles {
return [[NSArray alloc] initWithObjects:@"1", nil];
}

-(void)window:(NSDictionary*)ignored {
// window switching isn't supported
return;
}

- (void)frame:(NSDictionary*)frameTarget {
NSObject* ID = [frameTarget objectForKey:@"id"];
NSString* frameIndex = nil;
if ([ID isKindOfClass:[NSNull class]]) {
// Switch to default content
[self describeLastAction:@"switch frame to top"];
[[FrameContext sharedInstance] removeAllObjects];
return;
} else if ([ID isKindOfClass:[NSDictionary class]]) {
// A WebElement was passed in
[self describeLastAction:@"switching frame to provided web element"];

// need to make a separate call to 'execute_script'
// in order to get the benefits of mapping the webelement to an actual
// dom element. Seemingly the easiest way is to spoof another external
// call and process the result
WebDriverResponse* response = (WebDriverResponse*)
[[HTTPServerController sharedInstance]
httpResponseForQuery:[[NSString alloc] initWithFormat:@"/wd/hub/session/%@/execute", [frameTarget objectForKey:@"sessionId"]]
method:@"POST"
// example of what the data needs to look like
// {"sessionId": "%@", "args": [{"ELEMENT": "%@"}], "script": "return arguments[0]"}
withData:[[[NSDictionary dictionaryWithObjectsAndKeys:
[[NSArray alloc] initWithObjects:ID, nil], @"args",
@"return (function(vs,v){for(var i=0;i<vs.length;i++){if(vs[i]==v)return String(i);}return '';})(window.frames,arguments[0].contentWindow)", @"script", nil] JSONRepresentation]
dataUsingEncoding:NSASCIIStringEncoding]
];

frameIndex = [response value];

} else {
// We should have a string here which will either be the frame name,
// frame index or the id of the element. Let's try frame name / index first.
[self describeLastAction:@"switching frame by index/name"];
frameIndex = [self jsEval:[[NSString alloc]
initWithFormat:@"(function(vs,v){for(var i=0;i<vs.length;i++){if(vs[i]==v)return String(i);}return '';})(window.frames,window.frames['%@'])",
ID]];
if ([frameIndex isEqual:nil] || [frameIndex isEqualToString:@""]) {
// couldn't find the frame by name or index
// try to see if there's a frame with an id
[self describeLastAction:@"switching frame by id"];
frameIndex = [self jsEval:[[NSString alloc]
initWithFormat:@"(function(vs,v){for(var i=0;i<vs.length;i++){if(vs[i]==v)return String(i);}return '';})(window.frames,document.getElementById('%@').contentWindow)",
ID]];
}
}
if (![frameIndex isEqual:nil] && ![frameIndex isEqualToString:@""]) {
[[FrameContext sharedInstance] addObject:frameIndex];
} else {
[self describeLastAction:@"switch frame could not find frame"];
// NoSuchFrame Exception is 8
// according to http://code.google.com/p/selenium/wiki/JsonWireProtocol#Response_Status_Codes
@throw [NSException webDriverExceptionWithMessage:
[NSString stringWithFormat:@"Could not find frame '%@' in the current window", ID]
andStatusCode:8];
}
}

- (id)visible {
// The WebView is always visible.
return [NSNumber numberWithBool:YES];
Expand Down Expand Up @@ -340,6 +409,33 @@ - (NSString *)jsEval:(NSString *)format, ... {
autorelease];
va_end(argList);

if ([[FrameContext sharedInstance] count] > 0) {
// check first is the frames still exist, if any of them are gone
// automatically reset to default content
[self performSelectorOnMainThread:@selector(jsEvalInternal:)
withObject:[NSString stringWithFormat:@"(function(){var w=window;var frameIndexes=%@;for(var i=0;i<frameIndexes.length;i++){if(!(w=w.frames[frameIndexes[i]]))return true;};return false;})()",
[[FrameContext sharedInstance] JSONRepresentation]]
waitUntilDone:YES];
if ([[[lastJSResult_ copy] autorelease] isEqualToString:@"true"]) {
// this means one of the frames no longer exists, switching back to default content.
[[FrameContext sharedInstance] removeAllObjects];
} else {
script = [[NSString alloc]
initWithFormat:@"(function(){var win=(function(){var w=window;var frameIndexes=%@;for(var i=0;i<frameIndexes.length;i++){if(!(w=w.frames[frameIndexes[i]]))return window;};return w;})(); return win.eval('%@');})()",
[[FrameContext sharedInstance] JSONRepresentation],
// [script JSONRepresentation]
// It would have been so nice to just use the JSON serializer
// but as luck would have it, it fails to convert most of our
// atomized javascript code.
// So, resorting to a string replacement to escape certain characters
// to coerce into a javascript string for eval.
[[[[script stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"]
stringByReplacingOccurrencesOfString:@"'" withString:@"\\'"]
stringByReplacingOccurrencesOfString:@"\\\\x" withString:@"\\x"]
stringByReplacingOccurrencesOfString:@"\n" withString:@""]
];
}
}
[self performSelectorOnMainThread:@selector(jsEvalInternal:)
withObject:script
waitUntilDone:YES];
Expand All @@ -348,7 +444,9 @@ - (NSString *)jsEval:(NSString *)format, ... {
}

- (NSString *)currentTitle {
return [self jsEval:@"document.title"];
// always return the 'visible' title which is on the top window
// not in the frameset
return [self jsEval:@"window.top.document.title"];
}

- (NSString *)source {
Expand Down
31 changes: 1 addition & 30 deletions java/client/src/org/openqa/selenium/iphone/IPhoneDriver.java
Original file line number Diff line number Diff line change
Expand Up @@ -100,36 +100,7 @@ public TargetLocator switchTo() {
return new IPhoneTargetLocator();
}

private class IPhoneTargetLocator implements TargetLocator {

public WebDriver frame(int frameIndex) {
// is this even possible to do on the iphone?
throw new UnsupportedOperationException(
"Frame switching is not supported on the iPhone");
}

public WebDriver frame(String frameName) {
// is this even possible to do on the iphone?
throw new UnsupportedOperationException(
"Frame switching is not supported on the iPhone");
}

public WebDriver frame(WebElement frameElement) {
// is this even possible to do on the iphone?
throw new UnsupportedOperationException(
"Frame switching is not supported on the iPhone");
}

public WebDriver window(String windowName) {
throw new UnsupportedOperationException(
"Window switching is unsupported on the iPhone");
}

public WebDriver defaultContent() {
// The iphone driver does not support frame switching, so we're always
// focused on the default content.
return IPhoneDriver.this;
}
private class IPhoneTargetLocator extends RemoteTargetLocator {

public WebElement activeElement() {
return (WebElement) executeScript("return document.activeElement || document.body;");
Expand Down
Loading

0 comments on commit 93c33f3

Please sign in to comment.