write to Java console when my Javascript callback

2019-09-02 14:47发布

I have learned how to create Javascript callback functions and I have a basic understanding of 'functional programming' since it seems easy enough. I am however, new to javascript and it's syntax and I can't find a good way to test said syntax while in my IntelliJ IDE.

What is it you're doing?

I'm creating a Selenium based tool to click on a webelement, wait for it to reload the page, become stale or wait for a timeout. The reason I'm doing this is to classify webelements into three categories: causes a page reload, becomes stale, doesn't change. To do this I've made a simple javascript script with the JavascriptExecutor that comes with Java. Most of my code is in java and that is the language I am proficient in. I want to learn how to use javascript with java to do the things I want with web pages.

Ok but what specifically is the problem?

I have a javascript callback function:

function test(callback) {callback();} 
function Return() {SeleniumTest.isPageReloaded.JavascriptWorking} 
window.addEventListener('onload', test(Return));

which is executed inside a Javascript Executor like so:

System.setProperty("webdriver.chrome.driver", 
"C:\\chromedriver_win32\\chromedriver.exe");
WebDriver driver = new ChromeDriver();
String script = "function test(callback) {callback();}" +
                    "function Return()" + 
                    "{SeleniumTest.isPageReloaded.JavascriptWorking}" +
                    "window.addEventListener('onload', test(Return));";
JavascriptExecutor js = (JavascriptExecutor)driver;
js.executeScript(script);

Which is basically the Javascript script from before, executing as a String. As you can see I am attempting to call a java class. SeleniumTest is my package, isPageReloaded is the current class and JavascriptWorking is a static method within that class. That static method looks like this:

public static void JavascriptWorking(){
    System.out.println("Javascript ran here");
}

Its meant to be a simple way to get something from the javascript to my java code. The reason I tried it this way is because I read this:

https://documentation.progress.com/output/ua/OpenEdge_latest/index.html#page/bpm-appdev/invoking-java-methods-in-javascript.html

But then I realized that it wouldn't work and I dug deeper. I read that Javascript and Java are seperated by server and client and I gained some insight from this question:

calling java methods in javascript code

However I'm not 100% sure this is accurate to my case since the Javascript I'm executing isn't coming from the webpage I'm testing, Rather I made it myself inside the java code as a String. Additionally I'm still highly confused on if the answer to that question actually applies to me. There is only one and it basically just says, 'install some stuff because java is clientside and javascript is serverside'. I (kindof) understand what those terms mean but I'm not sure that the javascript I made in my class would be considered 'server-side' in fact it would seem to not be that way. What I need is clarification on A: is the javascript I'm running/creating in my java code actually serverside? B: if yes then can someone give me a basic rundown on how I would go about calling java code from the server? does this require permissions? I assume I have to communicate with said server so does that mean I use GET and POSt requests? C: If the Javascript Isn't server side then it must be clientside and I should be able to call it pretty easily right? How do I do this?

Show us what exactly you want

I want to be able to run:

System.setProperty("webdriver.chrome.driver", 
"C:\\chromedriver_win32\\chromedriver.exe");
WebDriver driver = new ChromeDriver();
String script = "function test(callback) {callback();}" +
                    "function Return()" + 
"{//insert callback code here}" +
                    "window.addEventListener('onload', test(Return));";
JavascriptExecutor js = (JavascriptExecutor)driver;
js.executeScript(script);

and get either a static java method ran, something printed to console, or some other means that links the javascript code to the javacode. So for example if I inserted the correct code to call my static method:

SeleniumTest.IsPageReloaded.JavascriptWorking

(which again looks like):

public static void JavascriptWorking(){
    System.out.println("Javascript ran here");
}

Then I'd want to see "Javascript ran here" on my java console. The driver being used is interchangebale, I just used chrome first because its fast. All that this needs is an enclosing main class and It ((should)) be runnable but no promises.

The purpose is to get something in java that I can then use as a flag to know that my asynchronous javascript is done in java and I can continue on with program execution. I can get the async javascript part and I understand it, I just need a link back to my java code.

Possible Solutions

I've been told that the common way to provide a flag for your java code is to create a certain webelement on the page with javascript and test for it in java (hence the link). I don't feel like adding to the webpages I test because I want to test them without actually editing/changing them. I'm generally open to other simple solutions but the biggest thing I need is clarification on the whole clientside serverside issue because its specific to my setup (Selenium java -> javascript -> java) where most questions only cover (javascript -> java) or vice versa.

1条回答
聊天终结者
2楼-- · 2019-09-02 15:43

The link you mentioned about JS invoking Java is for a specific application, that is meant to do that. Not saying it is impossible (I wrote FF plugins based on similar principle), but it is not applicable in this case. It also requires special application support (by default Javascript executed in browser is heavily sandboxed - it can't access anything out of its own scope. Invoking other apps on its own is a big no.).

The scripts you are injecting are always client side, they are executed only in the browser, that is isolated from the java code itself. With that said nothing is impossible.

Would like to mention two interesting features of the Selenium library that can come handy for you.

  1. You mention a magic term many times "async Javascript execution" - and as I can see you are implementing your own version of executeAsyncScript. Webdriver does provide this method out of the box, pretty much for the purpose you want to use it with.

When you use executeScript, it will return pretty much immediately once it finished - in your case it will just inject your listener with your code, and then it returns. Using executeAsyncScript you can get a callback - just what you are doing. When calling executeAsyncScript, a default callback method is added to your code as the last argument, that needs to be called by your JS code for the method to return.

A simple example:

String script = "var callback = arguments[arguments.length - 1];" + //the last argument is the callback function
                "var classToCall = 'SeleniumTest.IsPageReloaded';" +  //the classname you want to return to call from Java in case of success)
                "window.addEventListener('onload', callback(classToCall));"; 
//you can give any supported return value to the callback function. Here I assume that you want to call a static method. This is the class name that can be used later.
try {
    JavascriptExecutor js = (JavascriptExecutor)driver;
    //classToCall has the value we passed to the callback function
    String classToCall = js.executeAsyncScript(script);
} catch (ScriptTimeoutException e) {
    System.err.println("Uhhh... this failed I guess");
    e.printStackTrace();
}

The executeAsyncScript does not return until the callback is called - to avoid infinite hangs, you can set the WebDriver.Timeouts.setScriptTimeout property to control this. If the script takes longer, JavascriptExecutor will throw an exception. Once returned, you can instantiate the returned class, and print like

Class clazz = Class.forName(classToCall); //it is only necessary if the classname is dynamic. If it is the same always, you can just go ahead with that.
((IsPageReloaded)clazz.newInstance()).JavascriptWorking();

Of course you can return a more complex datastructure also from the JS where you specify the method name also, but using reflection is really offtopic here.

  1. Take a look at EventFiringWebdriver. This is a useful class that makes use of WebDriverEventListener to create custom Webdriver wrappers, with hooks on many events, allowing you to execute custom code before/after clicking, before/after pageload... and beside some others more importantly before/after executing javascript in the webdriver. You could leverage this to always call the same code around javascript execution - just create your own WebDriverEventListener.

You can find more info on the js executor here, and on WebDriverEventListener here.

查看更多
登录 后发表回答