Run JS in iOS webView every time loading a new page - javascript

For example, I want to run this part of code every time user click a new link in my UIWebView,
NSString* js =
#"var meta = document.createElement('meta'); "
"meta.setAttribute( 'name', 'viewport' ); "
"meta.setAttribute( 'content', 'width = device-width;initial-scale=1.0; maximum-scale=1.0' ); "
"document.getElementsByTagName('head')[0].appendChild(meta)";
[webView stringByEvaluatingJavaScriptFromString: js];
how can I do that? which API should I look at?
And I want this JS run in new page. How should I determine if the new page is fully loaded? –

There are two things likely to go wrong in this scenario: 1) your Js throws an exception and 2) the page reloads after your Js has run, and therefore reset the effects of your script.
In my experience, when a Javascript doesn't seem to run in a UIWebView, it's almost always the Javascript that is throwing an exception.
Try surrounding the script in try { ... } catch { ... } to capture the exception:
NSString* js =
#"try {"
"var meta = document.createElement('meta'); "
// [rest of the script...]
"document.getElementsByTagName('head')[0].appendChild(meta)";
" } catch (exc) { "
" window.ERR = exc.toString(); "
" } "
Now you can set a breakpoint in Xcode after your call to stringByEvaluatingJavaScriptFromString:. An error message might be available if you run
po [webView stringByEvaluatingJavaScriptFromString:#"window.ERR"]
In your debug prompt.
A good idea when you try out Javascript on your UIWebView is to first do exploratory
work in something like the Chrome Developer Tools to weed out the worst errors (e.g. syntax errors). If your code works there, the next step is to connect to the view using
Safaris Web inspector for iOS and see if it still does what it should.
#nilveryboring's point that you add scripts in webViewDidFinishLoading rather than in shouldStartLoadWithRequest is correct. In shouldStartLoadWithRequest the page hasn't been loaded yet, and any state you introduce at that point will be discarded by the loading page.

You can detect when the user clicks a link with:
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
Once you have detected that he has clicked on a link, perform your js.
More on it in the Docs.

Related

Open a Browser Window with Javascript on Xamarin.Forms

I have a Xamarin.Forms app. It includes a button like this:
<Button x:Name="Buy_Button" Text="Satın Al" FontAttributes="Bold" TextColor="#e2e2e2" BackgroundColor="#2A52BE" FontFamily="Segoe UI" Grid.Column="2" Grid.ColumnSpan="1" Grid.RowSpan="1" CornerRadius="5" VerticalOptions="Start" HorizontalOptions="Center" FontSize="15.667" Grid.Row="0" Margin="0,10,10,0" Clicked="Buy_Button_ClickedAsync" CommandParameter="{Binding Buy_URL}" />
I'm sending a URL link to click event for opening specific web page. Code is:
private async void Buy_Button_ClickedAsync(object sender, EventArgs e)
{
Button btn = (Button)sender; // Coming button from click event handler.
var buylink = btn.CommandParameter.ToString(); // Get the CommandParameter.
// await DisplayAlert("Satın alma linki", buylink, "Anladım"); // Show the link.
try // Uwp & iOS & Android
{
await Browser.OpenAsync(new Uri(buylink), BrowserLaunchMode.SystemPreferred); // Open url in-app browser for iOS & Android- native in UWP
}
catch (NotImplementedInReferenceAssemblyException ex) //Wasm falls here because lack of Xamarin.Essentials.
{
// await DisplayAlert("Hata", ex.Message, "Anladım"); // Show the info about exception.
// Jint - nt is a Javascript interpreter for .NET which provides full ECMA 5.1 compliance and can run on any .NET platform.
//Because it doesn't generate any .NET bytecode nor use the DLR it runs relatively small scripts faster.
//https://github.com/sebastienros/jint
var engine = new Engine();
engine.SetValue("log", new Action<object>(Console.WriteLine));
engine.Execute(#"function openurl() { log('" + buylink + "'); }; openurl(); ");
}
}
In UWP, Xamarin.iOS and Xamarin. Android this code is running via Xamarin.Esssentials:
await Browser.OpenAsync(new Uri(buylink), BrowserLaunchMode.SystemPreferred); // Open url in-app browser for iOS & Android- native in UWP
However, my Xamarin.Forms app projected to WebAssembly code with Uno Platform, so this code block not running. As a result. I install Jint to Xamarin.Forms app. This catch block prints the link to Browser console, but no window.open function track in API reference:
catch (NotImplementedInReferenceAssemblyException ex) //Wasm falls here because lack of Xamarin.Essentials.
{
// await DisplayAlert("Hata", ex.Message, "Anladım"); // Show the info about exception.
// Jint - nt is a Javascript interpreter for .NET which provides full ECMA 5.1 compliance and can run on any .NET platform.
//Because it doesn't generate any .NET bytecode nor use the DLR it runs relatively small scripts faster.
//https://github.com/sebastienros/jint
var engine = new Engine();
engine.SetValue("log", new Action<object>(Console.WriteLine));
engine.Execute(#"function openurl() { log('" + buylink + "'); }; openurl(); ");
}
}
How can I open WebBrowser page on WASM via Javascript form Xamarin.Forms C# code? Thanks.
2 things:
1. Use the browser!
On Wasm, you're running in a webassembly environment, which is running in a javascript virtual machine (that's not totally accurate, but close enough for my point). That means you can directly invoke the javascript of the running environment (browser).
Making a call to native javascript...
WebAssemblyRuntime
.InvokeJS("(function(){location.href=\"https://www.wikipedia.com/\";})();");
In your case, since you want to open a browser window, it's required to use this approach, because Jint can't access anything from the browser itself.
2. You can still call Jint anyway (but not to open a new window)
If you still want to call code using Jint (because you can!!), you need to exclude the Jint.dll assembly from the linking process. Probably because it's using reflection to operate. Again, it won't work to open a window as you're asking, but if you need to call Jint for any other reason, it will work as on other platforms!
Add this to your LinkerConfig.xml (in the Wasm project):
<assembly fullname="Jint" />
Also... You gave me an idea and I did something cool with Jint...
I put the entire solution there: https://github.com/carldebilly/TestJint
It works, even on Wasm:
Interesting code:
https://github.com/carldebilly/TestJint/blob/master/TestJint/TestJint.Shared/MainPage.xaml.cs#L18-L40
private void BtnClick(object sender, RoutedEventArgs e)
{
void Log(object o)
{
output.Text = o?.ToString() ?? "<null>";
}
var engine = new Engine()
.SetValue("log", new Action<object>(Log));
engine.Execute(#"
function hello() {
log('Hello World ' + new Date());
};
hello();
");
#if __WASM__
output2.Text =
WebAssemblyRuntime.InvokeJS("(function(){return 'Hello World ' + new Date();})();");
#else
output2.Text = "Not supported on this platform.";
#endif
}
Final Note
On UWP/WinUI XAML, you can directly put a <Hyperlink /> in your XAML. I'm not familiar enough with Xamarin Forms to know if there's an equivalent.
I am using Device.OpenUri and it works in WASM with Xamarin.Forms
Device.OpenUri(new Uri("https://www.bing.com"));

Synchronization problems with automated testing Angular website with Selenium and testing issues with Internet Explorer.

I am trying to write an automated test program for one of my website using Selenium WEbDriver. I am having some problems when doing the test on Internet Explorer. The website that I am trying to test is built in AngularJS. I will explain my problems in detail.
Here, is the code that waits until Angular has finished processing.
private static ExpectedCondition angularHasFinishedProcessing() {
return (ExpectedCondition<Boolean>) driver -> {
String hasAngularFinishedScript = "var callback = arguments[arguments.length - 1];\n" +
"var el = document.querySelector('html');\n" +
"if (!window.angular) {\n" +
" callback('false')\n" +
"}\n" +
"if (angular.getTestability) {\n" +
" angular.getTestability(el).whenStable(function(){callback('true')});\n" +
"} else {\n" +
" if (!angular.element(el).injector()) {\n" +
" callback('false')\n" +
" }\n" +
" var browser = angular.element(el).injector().get('$browser');\n" +
" browser.notifyWhenNoOutstandingRequests(function(){callback('true')});\n" +
"}";
JavascriptExecutor javascriptExecutor = (JavascriptExecutor) driver;
assert javascriptExecutor != null;
String isProcessingFinished = javascriptExecutor.executeAsyncScript(hasAngularFinishedScript).toString();
return Boolean.valueOf(isProcessingFinished);
};
}
private void waitForAngular() {
WebDriverWait wait = new WebDriverWait(driver, 15, 100);
wait.until(angularHasFinishedProcessing());
}
And here is the code that instantiates WebDriver for Internet Explorer.
System.setProperty("webdriver.ie.driver",
new File("H:/libraries/webdrivers/IEDriverServer.exe").getAbsolutePath());
DesiredCapabilities d = DesiredCapabilities.internetExplorer();
// To bypasse the Protected Mode settings of IE
d.setCapability(InternetExplorerDriver.INTRODUCE_FLAKINESS_BY_IGNORING_SECURITY_DOMAINS, true);
driver = new InternetExplorerDriver(d);
driver.manage().timeouts().setScriptTimeout(30, TimeUnit.SECONDS);
driver.manage().window().maximize();
driver.get("https://arandomangularjsapp.com"); // Let's suppose this
I had to bypass the Protected Mode settings (shown in above code) because I was constantly getting this Exception
Caused by: org.openqa.selenium.WebDriverException: Unexpected error launching Internet
Explorer. Protected Mode must be set to the same value (enabled or disabled) for all
zones. (WARNING: The server did not provide any stacktrace information)
though I made sure that protected mode was enabled and same values were set for all zones.
Now comes the real issue I am having. Look at this block of code,
waitForAngular();
WebElement el = driver.findElement(By.xpath("//div[#class='events-list__event-info' and #id='48040']" +
"//following-sibling::div[#class='events-list__event-buy']/a"));
if(driver.toString().toUpperCase().contains("INTERNETEXPLORER"))
el.sendKeys(Keys.ENTER);
else
el.click();
waitForAngular();
webElement = driver.findElement(By.xpath("(//div[#class='ticket u-cf ng-scope'])[1]//select"));
select = new Select(webElement);
select.selectByIndex(1);
My first question, the way I am trying to wait for Angular app to get ready by calling waitForAngular(), is it a good way? In my program I even had to use Thread.sleep() so many times in order to let elements to properly render before I could invoke actions on them. It would be great if you guys can suggest me a proper way to use Selenium WebDriver along with Angular App.
Now let's talk about my second issue. I had to completely disable Protected Mode from Internet Explorer's options in order to run the program. Otherwise, I would get this exception,
Caused by: org.openqa.selenium.JavascriptException: JavaScript error in async script. (WARNING: The server did not provide any stacktrace information)
I get this exception when trying to execute waitForAngular(). What could be causing this? Is there any way by which I could keep the Protected Mode enabled and still be able to execute that script inside angularHasFinishedProcessing() method?
Finally, my third problem is related to click action not being triggered. Forget about the previous issues for a second. I disabled IE's Protected Mode, so my program starts IE without throwing any exception. The program successfully finds the first element as shown in the code above. But, the click action is not triggered, browser thus doesn't navigate to next page, and the program fails to find the second element. As a workaround I even tried el.sendKeys(Keys.ENTER); but it didn't work.** So, am I having this problem because I bypassed/disabled Protected Mode? Or, is there something else that I am not being able to see?**
I run tests on IE, Opera, Chrome and Firefox and the problem seems to occur only in IE. Any sort of help, suggestions or guidelines are highly appreciated.
Thank You.

script to extract information from a webpage in a browser

Background
I have a network performance testing tool which after every test,displays all its results in a new results page. Below is my workflow to find the final value.(Details not relevant here but the html elements i am looking for might
1.Go to a section called "Data frame" ( which has a table with timestamp,transmit,recieve and percentage as columns) and get the time stamp when percentage first drops below 99.9
Here is a snippet of the "Data Frame" table
Data frame snippet
2.Go to another section called "Data throughput" ( table with timestamp, throughput as columns) and get the maximum value of throughput before the previously found timestamp.
After every test, i have to do this manually, but there should be someway to automate this.
Question
1.Is it possible to write a script ( maybe in javascript ) to automate my workflow for every new results page?
example algorithm
section = webpage.section_with_title("Data frames")
for each row in section
find first my_timestamp with percent < 99.9
done
section2 = webpage.section_with_title("Data Throughput")
for each row in section2
find max row.throughput if row.timestamp < my_timestamp
don
2.Once i write the script, how do i execute it in the web page?
3.Can i leverage the firefox/chrome developer console in some way ?
Note:I develop mostly in C,C++ and ruby with very basic experience in HTML and CSS. I have barely used javascript, but i have a vague idea that the browsers use it to handle the content of every page.
My thought process was to somehow execute the script through firefox console, and it will provide me with the results... is that difficult?
edit: since i am not familiar with the web development terms, ill put an a different choice of words...
1.i open up the results page
2.i open the console window of firefox/chrome
3.run my script(dont know what language).
4.i should get the result in the console. i do not want to modify the results page. i just need the result displayed to me(in the console, or to a text file).
Yes it's possible to do what you think using Javascript. Please look up XMLHttpRequest
Here is a way for you to start (in Java):
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.firefox.FirefoxDriver;
public class YourClass
{
private WebDriver webDriver = null;
public void open() throws Exception
{
webDriver = new FirefoxDriver();
}
public void close() throws Exception
{
webDriver.quit();
}
public void doStuff() throws Exception
{
WebElement element = webDriver.findElement(By.id(...));
...
// Alternatively, it might be easier to parse the entire
// page-source instead of searching elements in the DOM
String pageSource = webDriver.getPageSource();
...
}
}
You will need to download the following JAR files and add them in your project's class-path:
selenium-java-2.xx.0.jar (currently xx=39)
selenium-server-standalone-2.xx.0.jar (currently xx=39)

Chrome app: accessing sandboxed iframe from parent window

I'm using knockoutjs in my google chrome app. To be able to use knockout, I have to define the real application.html as sandox page and include it as an iframe in a dummy container. Application structure is as follows:
- container.html
|
+-- application.html as iframe
|
+-knockout and application.js
Iframe is defined as follows:
<iframe src="application.html" frameborder="0"
sandbox="allow-same-origin allow-scripts" ></iframe>
Running
document.getElementsByTagName("iframe")[0]
in inspect tool on container.html throws following error.
Sandbox access violation: Blocked a frame at "chrome-extension://hllbklabnppjkmnngfanldbllljfeaia"
from accessing a frame at "chrome-extension://hllbklabnppjkmnngfanldbllljfeaia".
The frame being accessed is sandboxed and lacks the "allow-same-origin" flag.
How can i access the iframed document from it's parent?
Do something like this:
manifest.json
"sandbox": {
"pages": ["my_ui.html"]
}
my_ui.html
<script type="text/javascript" src="knockout-1.2.3.4.js"></script>
<script type="text/javascript" src="my_ui.js"></script>
my_ui.js
this.onSomethingChange = function() {
window.top.postMessage(
{ command: 'please-do-something', myArgument: this.myArgument() }, '*');
};
container.html
<script type="text/javascript" src="container.js"></script>
<iframe id="knockoutFrame" src="my_ui.html"></iframe>
container.js
window.addEventListener('message', function(event) {
var kocw = document.getElementById('knockoutFrame').contentWindow;
var anotherContentWindow = // etc.
var dest;
if (event.source == kocw) {
// The knockout iframe sent us a message. So we'll forward it to our
// app code.
dest = anotherContentWindow;
}
if (event.source == anotherContentWindow) {
// Our app code is responding to the knockout message (or initiating
// a conversation with that iframe). Forward it to the knockout code.
dest = kocw;
}
if (dest == null) {
console.log('huh?');
}
// This makes container.js like a gatekeeper, bouncing valid messages between
// the sandboxed page and the other page in your app. You should do
// better validation here, making sure the command is real, the source
// is as expected for the kind of command, etc.
dest.postMessage(event.data, '*');
}
Your statement "I have to define the real application.html as sandbox page and include it as an iframe in a dummy container" is probably not what you wanted. The idea is to sandbox the smallest possible thing, message out to the gatekeeper page that validates the messages, and have the gatekeeper forward the narrow messages to your un-sandboxed application logic. If you just stuff everything into the sandbox, you're defeating the purpose of the sandbox.
Disclaimer: I haven't carefully examined this code from a security perspective. You'll want to assume that hostile messages are coming from the sandbox (or from elsewhere, for that matter), and do what you can to address that threat.
Found out the culprit. This is my proxy.js, which is included by the container.html, used as a bridge to transfer the messages between the application iframe and the background.js. Following part is the one that listens for the messages originated from the iframe.
window.addEventListener("message",
function(evt){
console.log(evt); <= this is the problem
var iframe = document.getElementById("application").contentWindow; <= not this one
if (evt.source == iframe) {
return chrome.runtime.sendMessage(null, evt.data);
}
}
);
I didn't think that console.log would be the causing the problem. Instead i was suspecting from the document.getElem.. line. Because trying to run that code in inspect window of the application was throwing the same error.
But it seems that console.log (console seems to belong to container.html's scope) accesses some internals of event object that are not meant to be accessible out of the iframe's scope(which explains why i get the same error in inspect console). Removing the console.log line solved this problem for me.

Issuing a synchronous HTTP GET request or invoking shell script in JavaScript from iOS UIAutomation

I am trying to use Apple's UIAutomation to write unit tests for an iOS Application that has a server-side component. In order to setup the test server in various states (as well as simulate two clients communicating through my server), I would like to issue HTTP get requests from within my javascript-based test.
Can anyone provide an example of how to either issue HTTP GET requests directly from within UIAutomation javascript tests, or how to invoke a shell script from within my UIAutomation javascript tests?
FWIW, most of the core objects made available by all browsers are missing within the UIAutomation runtime. Try to use XMLHTTPRequest for example and you will get an exception reporting that it cannot find the variable.
Thanks!
Folks,
I was able to work around this by sending HTTP requests to the iOS client to process and return the results in a UIAlertView. Note that all iOS code modifications are wrapped in #if DEBUG conditional compilation directives.
First, setup your client to send out notifications in the event of a device shake. Read this post for more information.
Next, in your iOS main app delegate add this code:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(deviceShakenShowDebug:)
name:#"DeviceShaken"
object:nil];
Then add a method that looks something like this:
- (void) deviceShakenShowDebug:(id)sender
{
if (!self.textFieldEnterDebugArgs)
{
self.textFieldEnterDebugArgs = [[[UITextField alloc] initWithFrame:CGRectMake(0, 0, 260.0, 25.0)] autorelease];
self.textFieldEnterDebugArgs.accessibilityLabel = #"AlertDebugArgsField";
self.textFieldEnterDebugArgs.isAccessibilityElement = YES;
[self.textFieldEnterDebugArgs setBackgroundColor:[UIColor whiteColor]];
[self.tabBarController.selectedViewController.view addSubview:self.textFieldEnterDebugArgs];
[self.tabBarController.selectedViewController.view bringSubviewToFront:self.textFieldEnterDebugArgs];
}
else
{
if ([self.textFieldEnterDebugArgs.text length] > 0)
{
if ([self.textFieldEnterDebugArgs.text hasPrefix:#"http://"])
{
[self doDebugHttpRequest:self.textFieldEnterDebugArgs.text];
}
}
}
}
- (void)requestDidFinishLoad:(TTURLRequest*)request
{
NSString *response = [[[NSString alloc] initWithData:((TTURLDataResponse*)request.response).data
encoding:NSUTF8StringEncoding] autorelease];
UIAlertView *resultAlert =
[[[UIAlertView alloc] initWithTitle:NSLocalizedString(#"Request Loaded",#"")
message:response
delegate:nil
cancelButtonTitle:NSLocalizedString(#"OK",#"")
otherButtonTitles:nil] autorelease];
resultAlert.accessibilityLabel = #"AlertDebugResult";
[resultAlert show];
}
This code will add a UITextField to the very top view controller after a shake, slapped right above any navigation bar or other UI element. UIAutomation, or you the user, can manually enter a URL into this UITextField. When you shake the device again, if the text begins with "http" it will issue an HTTP request in code (exercise for the reader to implement doDebugHttpRequest).
Then, in my UIAutomation JavaScript file, I have defined the following two functions:
function httpGet(url, delayInSec) {
if (!delayInSec) delay = 1;
var alertDebugResultSeen = false;
var httpResponseValue = null;
UIATarget.onAlert = function onAlert(alert) {
httpResponseValue = alert.staticTexts().toArray()[1].name();
alert.buttons()[0].tap();
alertDebugResultSeen = true;
}
var target = UIATarget.localTarget();
var application = target.frontMostApp();
target.shake(); // bring up the input field
application.mainWindow().textFields()["AlertDebugArgsField"].setValue(url);
target.shake(); // send back to be processed
target.delay(delayInSec);
assertTrue(alertDebugResultSeen);
return httpResponseValue;
}
function httpGetJSON(url, delayInSec) {
var response = httpGet(url, delayInSec);
return eval('(' + response + ')');
}
Now, in my javascript file, I can call
httpGet('http://localhost:3000/do_something')
and it will execute an HTTP request. If I want JSON data back from the server, I call
var jsonResponse = httpGetJSON('http://localhost:3000/do_something')
If I know it is going to be a long-running call, I call
var jsonResponse = httpGetJSON('http://localhost:3000/do_something', 10 /* timeout */)
I've been using this approach successfully now for several weeks.
Try performTaskWithPathArgumentsTimeout
UIATarget.host().performTaskWithPathArgumentsTimeout("/usr/bin/curl", "http://google.com", 30);
Just a small correction. The answer that suggests using UIATarget.host().performTaskWithPathArgumentsTimeout is an easy way to make a request on a URL in iOS 5.0+, but the syntax of the example is incorrect. Here is the correct way to make this call:
UIATarget.host().performTaskWithPathArgumentsTimeout("/usr/bin/curl", ["http://google.com"], 30);
The "[" around the "args" param is important, and the test will die with an exception similar to the following if you forget the brackets:
Error: -[__NSCFString count]: unrecognized selector sent to instance
Here is a fully working example that hits google.com and logs all the output:
var result = UIATarget.host().performTaskWithPathArgumentsTimeout("/usr/bin/curl", ["http://www.google.com"], 30);
UIALogger.logDebug("exitCode: " + result.exitCode);
UIALogger.logDebug("stdout: " + result.stdout);
UIALogger.logDebug("stderr: " + result.stderr);
+1 for creative use of "shake()". However, that's not an option for some projects, especially those that actually use the shake feature.
Think outside the box. Do the fetching with something else (Python, Ruby, node.js, bash+wget, etc). Then, you can use the pre-canned response and auto-generate the ui-test.js on the fly by including that dynamically generated json payload as the "sample data" into the test. Then you simply execute the test.
In my opinion, the test is the test, leave that alone. The test data you are using, if it's that dynamic, it ought to be separated from the test itself. By doing it this way of fetching / generating JSON, and referencing it from the test, you can update that JSON however often you like, either immediately right before every test, or on a set interval like when you know the server has been updated. I'm not sure you would want to generate it while the test is running, that seems like it would create problems. Taking it to the next level, you could get fancy and use functions that calculate what values ought to be based on other values, and expose them as "dynamic properties" of the data, rather than that math being inside the test, but at that point I think the discussion is more of an academic one rather than the practical one of how.
Apple has recently updated UIAutomation to include a new UIAHost element for performing a task on the Host that is running the instance of Instruments that is executing the tests.

Categories