Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Somewhat Off Topic - But - How To Set Value of OJ Combobox? #73

Open
brcolow opened this issue May 10, 2021 · 19 comments
Open

Somewhat Off Topic - But - How To Set Value of OJ Combobox? #73

brcolow opened this issue May 10, 2021 · 19 comments

Comments

@brcolow
Copy link

brcolow commented May 10, 2021

Sorry if this is off-topic.

I am attempting to automate input on a website that uses Oracle JET (version 4.1.0).

I am using Selenium to take user-like actions such as entering text in input fields, etc.

All is working well except for trying to set the value of an ojcombobox.

It is defined in the HTML thusly:

<div class="oj-combobox-choice" tabindex="-1" role="presentation" id="oj-combobox-choice-om_shipFromOverride">
   <input type="text" autocomplete="off" autocorrect="off" autocapitalize="none" spellcheck="false" class="oj-combobox-input oj-combobox-focused" role="combobox" aria-expanded="false" aria-autocomplete="list" id="oj-combobox-input-om_shipFromOverride" aria-owns="oj-listbox-results-om_shipFromOverride" aria-labelledby="oj-combobox-label-om_shipFromOverride">
   <abbr class="oj-combobox-clear-entry" role="presentation"></abbr>
   <span class="oj-combobox-divider" role="presentation"></span>
   <a class="oj-combobox-arrow oj-combobox-icon oj-component-icon oj-clickable-icon-nocontext oj-combobox-open-icon" role="button" aria-label="expand">
   </a>
</div>

It is constructed dynamically from a SQL result:

$("#orderBaseManager #om_shipFromOverride").ojCombobox({"options":locations});

I have tried a bunch of things, such as:

 js.executeScript(String.format("arguments[0].value = \"%s\"", SHIP_FROM),
              driver.findElement(By.id("oj-combobox-input-om_shipFromOverride")));

Which sets the value visually, but doesn't seem to actually trigger observables so the script on the page still believes it is not set.

I also tried sending the keys: "TAB, TAB, TAB, TAB... etc. ARROW_DOWN ARROW_DOWN ENTER" but this doesn't seem to do anything.

I am able to execute arbitrary Javascript using Selenium - so I was wondering if there is a way to programmatically make a selection for the combobox? Browsing the documentation I only saw a method for setting the initial value when it is instantiated.

Assistance would be greatly appreciated.

@peppertech
Copy link
Member

JET v4.1.0 is very old and it contains security vulnerabilities in multiple libraries that are distributed with JET (jQuery and Knockout to name two). I strongly recommend updating if at all possible.

Updating the value programmatically for oj-combobox is pretty simple in that you just update the observable that you have bound to the "value" property, or you can use setProperty on the component as well.

I'm making an assumption with the above comment, and that is that you are using the custom element syntax <oj-combobox ...> and not the really old jQueryUI syntax which used a Knockout custom binding called ojComponent.

As for test automation, we recommend the use of Selenium Web Driver. We are working on providing a set of Web Elements for the JET components that will make this a lot easier, but they won't be available until later this year unfortunately.

@brcolow
Copy link
Author

brcolow commented May 11, 2021

@peppertech Thanks so much for responding.

I am not the one who created the page. I am the one trying to automate third party input to it :).

I don't know what they used on the backend side of things ( or ojComponent) - I only have access to the front-end Javascript that the browser can see when the page is rendered.

I am using Selenium Web Driver, and that's what I am trying to automate - setting a value on the combobox.

I do see that they are including /js/libs/oj/v4.1.0/min/ojcomponentcore.js so maybe that is the older jquery syntax? Does this make a difference for how to set the value? Do you think you could show me the exact invocation for setting the value? Given that the combobox is instantiated thusly:

$("#orderBaseManager #om_shipFromOverride").ojCombobox({"options":locations});

Also it seems that the backing observable for the combobox is created thusly:

self.shipFromOverride = ko.observable(null);

I also see the following which looks to setup some type of event handler:

$("#om_shipFromOverride").ojCombobox({
	optionChange: function() {self.lookupShipFromRemarks();}
});

Thanks so much.

@brcolow
Copy link
Author

brcolow commented May 13, 2021

@peppertech Do you think you could take a second look? I really appreciate it.

@peppertech
Copy link
Member

Hi Michael, sorry for the delay. I've asked one of the test engineers to pop in and give some advice. I'm sure he'll be more knowledgeable than I am on how to use Selenium.
From a coding perspective, this link with show you the old API syntax when using jQuery. https://docs.oracle.com/middleware/jet320/jet/reference-jet/oj.ojCombobox.html#event:optionChange

and here is a JET Cookbook demo using that same old jQuery syntax for setting up the component itself in the HTML and then defining the "val" variable as a Knockout observableArray. https://www.oracle.com/webfolder/technetwork/jet-320/jetCookbook.html?component=combobox&demo=single

To update the value, you would set update that observableArray

self.val(['new value'])

@brcolow
Copy link
Author

brcolow commented May 16, 2021

@peppertech Thanks for your feedback.

For some reason self.shipFromOverride is undefined. It seems that observables that are constructed with a null argument don't show up in the self object. Thus I am not able to call self.shipFromOverride.val(['Some Value']).

Again, this is how they setup the combobox - I am not able to change the page itself - only trying to automate it's entry. Is there a way to set the value by going through jQuery as $("#orderBaseManager #om_shipFromOverride") is defined - self.shipFromOverride is not even though it is defined thusly:

self.shipFromOverride = ko.observable(null);

Seems like the null argument is preventing it from being defined. When I use the JS console debugger self.shipFromOverride is undefined - even though it is instantiated as above): Uncaught TypeError: self.shipFromOverride is undefined.

The reason I think the null argument seems to be playing a role is the following observable is instantiated at the same time (next line): self.orderBaseXid = ko.observable(orderBaseRestApiJson.OrderBase.orderBaseXid); and self.orderBaseXid does show up in the self object.

@peppertech
Copy link
Member

For the Knockout observable issue, I would try initializing it with an empty string, or just leave it empty (it will default to undefined) and see if that works. I've never seen initializing with null, being and issue before, but the version of Knockout you are working against may very well have a bug in it. It's a bit old and insecure.

@peppertech
Copy link
Member

peppertech commented May 18, 2021

Well, doing a search of the Knockout repository issues, it appears that they did implement a breaking change in Knockout 3.0.0 where you can no longer initialize an observable or observableArray with null or undefined. The version of KO shipped with JET v4.1.0 was 3.4.0. Sooooo, I would initialize with an empty parens or array respectively if you don't know what value you want into there. Empty string will work for observable as well of course.
knockout/knockout#1054

@itlgit
Copy link
Member

itlgit commented May 18, 2021

I'm not sure on the DOM structure of the combobox for that version of JET, but have you tried calling sendKeys to the element and then tabbing out? That would normally trigger a "change" event on the element, which should cause the observable value to update. Something like
const input = await driver.findElement(By.id('oj-combobox-choice-om_shipFromOverride')) await input.sendKeys(SHIP_FROM, Keys.TAB)

@brcolow
Copy link
Author

brcolow commented May 19, 2021

@itlgit That works in the sense that it enters the text in the combobox, but it does not trigger the following event listener:

	    console.log({caller:"optionChange"});
	    $("#om_shipFromOverride").ojCombobox({
		    optionChange: function() {self.lookupShipFromRemarks();}
		    
		});

and thus the script does not think it is actually filled in and won't let the form be submitted.

@itlgit
Copy link
Member

itlgit commented May 20, 2021

Trying to understand from your sample, which element is id="om_shipFromOverride"? From your sample above, the outer-most "root" element is id="oj-combobox-choice-om_shipFromOverride" and the <input> element is id="oj-combobox-input-om_shipFromOverride"

Since typing the value into the input doesn't seem to trigger optionChange event, I wonder if clicking on the outer element to show the list, then sending it the arrow keys would do it? Or is that what you already tried?

@brcolow
Copy link
Author

brcolow commented May 20, 2021

@itlgit That's a good question. I don't see that in the HTML anywhere, but that's how they reference the combobox. Here are all appearances of that string in their scripts:

	var validationMessages = [];
	if (inspirage.isShipFromOverrideRequired) {
	    if (! self.shipFromOverride()) {
		var message = "Ship From Override is required";
		validationMessages.push({message:message, selector:"#om_shipFromOverride"});		
	    }
			// set combo box here om_shipFromOverride
			$("#orderBaseManager #om_shipFromOverride").ojCombobox({"options":locations});
	if (false) {
	    $("#om_shipFromOverrideDiv").focusout(function() {
		    self.lookupShipFromRemarks();
		});
	}

	if (true) {
	    console.log({caller:"optionChange"});
	    $("#om_shipFromOverride").ojCombobox({
		    optionChange: function() {self.lookupShipFromRemarks();}
		    
		});
	}

Indeed, trying to click on the outer element and then sending arrow keys was the first thing I tried.

I realize we may be at an impasse because of how this page was coded - it's quite annoying.

Appreciate all your assistance.

@itlgit
Copy link
Member

itlgit commented May 21, 2021

Does document.querySelector('#om_shipFromOverride') return the root element? If so, can you share the full DOM tree for that element?

@brcolow
Copy link
Author

brcolow commented May 21, 2021

@itlgit It does, good idea.

<div class="oj-flex-item inspirage-writeable">
	<div class="oj-combobox oj-component oj-enabled oj-form-control" id="ojChoiceId_om_shipFromOverride" style="max-width:40em">
		<div class="oj-combobox-choice" tabindex="-1" role="presentation" id="oj-combobox-choice-om_shipFromOverride">
			<input type="text" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" class="oj-combobox-input" role="combobox" aria-expanded="false" aria-autocomplete="list" id="oj-combobox-input-om_shipFromOverride" aria-owns="oj-listbox-results-om_shipFromOverride" aria-labelledby="oj-combobox-label-om_shipFromOverride"> <abbr class="oj-combobox-clear-entry" role="presentation"></abbr> <span class="oj-combobox-divider" role="presentation"></span>
			<a class="oj-combobox-arrow oj-combobox-icon oj-component-icon oj-clickable-icon-nocontext oj-combobox-open-icon" role="button" aria-label="expand"></a>
		</div>
		<div class="oj-listbox-drop" style="display:none" role="presentation" data-oj-containerid="ojChoiceId_om_shipFromOverride">
			<ul class="oj-listbox-results" role="listbox" aria-label="Ship From Override" id="oj-listbox-results-om_shipFromOverride"> </ul>
		</div>
		<input id="om_shipFromOverride" data-bind="ojComponent: {component: 'ojCombobox', 					  rootAttributes: {style:'max-width:40em'}, value:shipFromOverride}" tabindex="-1" aria-labelledby="oj-combobox-label-om_shipFromOverride" aria-hidden="true" class="oj-component-initnode" style="display: none;">
	</div>
	<!-- <oj-combobox id="om_shipFromOverride" style="max-width:40em" value="[[shipFromOverride]]" on-value-changed="[[lookupShipFromRemarks]]"></oj-combobox> -->
</div>

Is that full enough? Or do you need more DOM?

Thank you so much.

@itlgit
Copy link
Member

itlgit commented Jun 1, 2021

Sorry for the late reply. Can you share what the live DOM looks like after the component has been created? The original HTML
<input id="om_shipFromOverride" data-bind="...>
Is the placeholder element where the component will be created, but the real DOM structure will be different after the components is created.

@brcolow
Copy link
Author

brcolow commented Jun 1, 2021

@itlgit No problem. Thanks for your help.

It seems that the real DOM structure is only created when I interact with the input box (such as typing a character). It does not seem to do it just on page load which may be a problem for automating in this case. But after some interaction here is the result:

I took a screenshot because copy/paste is selecting the old DOM structure.

cvp

@itlgit
Copy link
Member

itlgit commented Jun 2, 2021

Ok, so #om_shipFromOverride is the original node on which the jQuery component was created, but it's no longer relevant (it's display:none) because, presumably, the component root is now #ojChoiceId_om_shipFromoverride, and the new, effective <input> element is #oj-combobox-input-om_shipFromOverride.

So you've tried send the "value" property via JS, calling sendKeys to send the keystrokes to the input--neither have triggered the optionChange event.

Does the combobox have a expand/drop-down arrow that shows a list of options when clicked? If so, you could try targeting that with your test, click it, then click the appropriate item from the list that displays.

@brcolow
Copy link
Author

brcolow commented Jun 2, 2021

That's one of the things I tried. It does not seem possible to click on the arrow dropdown box, which happens to be:

<a class="oj-combobox-arrow oj-combobox-icon oj-component-icon oj-clickable-icon-nocontext oj-combobox-open-icon" role="button" aria-label="expand"></a>

I don't mean from Selenium, either. Trying to do it just from the developer tools console also doesn't work:

document.querySelector('#oj-combobox-choice-om_shipFromOverride > a:nth-child(4)').click()

I made sure that the selector is infact selecting the drop down arrow:

cvp2

No matter what I try this continues to stump me.

@itlgit
Copy link
Member

itlgit commented Jun 2, 2021

Does Selenium give you any error when you try to click it, or it just does nothing? I'm assuming actually clicking it with your mouse works and shows the list...

@brcolow
Copy link
Author

brcolow commented Jun 2, 2021

Selenium and the Javascript console both do nothing. Actually clicking it with my mouse does work :).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants