Angular选择自定义控件和SeleniumWebdriver



我是一名开发人员,与我所在组织的BQA小组合作,协助他们实现测试自动化。我们有一个用Angular开发的大型复杂网站(我想是版本7-10(,它的开发已经外包给了另一家大公司。我正在尝试使用Visual Studio和带有NUnit的C#来实现Selenium Web驱动程序。如果说我在自动化方面最大的麻烦是自定义实现下拉列表的话。

我知道一点JavaScript,但一直在努力赶上ES5、ES6、NodeJS、TypeScript、Angular等。然而,我对C#做得更好。

我可以很容易地使用Selenium中的内置函数来选择这些列表中的选项。但是,这些值永远不会在服务器端注册,如果它们被标记为强制值,则通常会触发警告。

var driver = new ChromeDriver();
driver.FindElement(By.XPath("//app-select[@id='myId']")).Click();
new SelectElement(driver.FindElement(By.XPath("//app-select[@id='myId']//select"))).SelectByText("Some Option");

甚至。。。

var js = (IJavaScriptExecutor)driver;
var appSelect = new SelectElement(driver.FindElement(By.XPath("//app-select[@id='myId']//select")));
foreach(var opt in appSelect.Options)
{
if(opt.Text = "Some Option")
{
js.ExecuteScript("arguments[0].selected=true", opt);    // Works like SelectByText().
js.ExecuteScript("arguments[0].input", appSelect);      // These don't seem to ...
js.ExecuteScript("arguments[0].change", appSelect);     // ... make any kind of difference.
break;
}
}

我可以访问我们网站的代码库。所以,我可以检查这个自定义组件,但我不太了解Angular。我怀疑有一个服务器端会话变量(NgModel?(没有更新。我不知道是否可以使用客户端JavaScript来触发更新并使脚本正常运行。

任何建议都将不胜感激!

这是组件的角度代码。

=====select.component.html=====

<select #innerSelect [attr.id]="uid" class="select" [value]="value" [disabled]="disabled" (blur)="onBlur()"
(input)="inputChanged($event)">
<ng-content></ng-content>
</select>
<div class="arrow-icon" aria-hidden="true"></div>

=====选择组件.ts=====

import { Component, ViewEncapsulation, forwardRef, Output, EventEmitter, Input, OnInit, Renderer2, ElementRef, ViewChild, AfterViewInit, OnDestroy } from '@angular/core';
import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms';
import { ContentObserver } from '@angular/cdk/observers';
import { Subscription } from 'rxjs';
let nextUniqueId = 0;
/** A wrapper for a native select component. This allows us to render out a drop down arrow that is inline with branding. */
@Component({
selector: 'app-select',
templateUrl: './select.component.html',
styleUrls: ['./select.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => SelectComponent),
multi: true
}
],
encapsulation: ViewEncapsulation.None
})
export class SelectComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnDestroy {
/** When dataType is set to "number",. value will be converted to a number on get. */
@Input() dataType;
/** Whether the component is disabled. */
@Input() disabled = false;
/** Value of the control. */
@Input()
get value(): any { return this._value; }
set value(newValue: any) {
if (newValue !== this._value) {
this.writeValue(newValue);
this._value = newValue;
}
}
private _value: any;
/** Unique id of the element. */
@Input()
get uid(): string { return this._uid; }
set uid(value: string) {
this._uid = value || this._defaultId;
}
private _uid: string;
/** tracks changes to the content of the inner select (options) */
private contentChanges: Subscription = null;
/** appAutoFocus directive does not work here, so we implement autoFocus ourselves */
@Input() autoFocus = false;
/**
* Event that emits whenever the raw value of the select changes. This is here primarily
* to facilitate the two-way binding for the 'value' input.
*/
@Output() readonly valueChange: EventEmitter<any> = new EventEmitter<any>();
@ViewChild('innerSelect', { static: true }) innerSelect: ElementRef;
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/** Unique id for this input. */
// tslint:disable-next-line:member-ordering
private _defaultId = `app-select-${nextUniqueId++}`;
/** View -> model callback called when value changes */
onChange: (value: any) => void = () => { };
/** View -> model callback called when select has been touched */
onTouched = () => { };
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
constructor(private el: ElementRef, private renderer: Renderer2, private obs: ContentObserver) {
// Force setter to be called in case id was not specified.
this.uid = this.uid;
}
ngOnInit() {
this.autoFocus = this.el.nativeElement.hasAttribute('autoFocus');
}
ngAfterViewInit() {
// Watch for changes in the content of the select control.
// This is in place for situations where the list of options comes in *after* the value of the select has been set.
// If that occurs, re-set the value of the select to force the browser to pick the right option in the drop down list.
this.contentChanges = this.obs.observe(this.innerSelect.nativeElement).subscribe((event: MutationRecord[]) => {
this.innerSelect.nativeElement.value = this._value;
});
if (this.autoFocus === true) {
setTimeout(() => {
this.innerSelect.nativeElement.focus();
}, 200);
}
if (this.el.nativeElement.hasAttribute('id')) {
console.warn('app-select has an "id". Use "uid" to set the id of the inner select element.');
}
}
ngOnDestroy() {
this.contentChanges.unsubscribe();
}
/**
* Sets the select's value. Part of the ControlValueAccessor interface
* required to integrate with Angular's core forms API.
*/
writeValue(value: any): void {
this._value = value;
}
/**
* Saves a callback function to be invoked when the select's value
* changes from user input. Part of the ControlValueAccessor interface
* required to integrate with Angular's core forms API.
*/
registerOnChange(fn: (value: any) => void): void {
this.onChange = fn;
}
/**
* Saves a callback function to be invoked when the select is blurred
* by the user. Part of the ControlValueAccessor interface required
* to integrate with Angular's core forms API.
*/
registerOnTouched(fn: () => {}): void {
this.onTouched = fn;
}
/**
* Disables the select. Part of the ControlValueAccessor interface required
* to integrate with Angular's core forms API.
*/
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
onBlur() {
this.onTouched();
}
inputChanged(event) {
const targetValue = event.target.value;
if (targetValue && targetValue.toLowerCase() === 'null') {
this.value = null;
} else if (this.dataType === 'number') {
// if number is specified, attempt to convert the value
this.value = +targetValue;
} else if (this.dataType === 'string') {
// if string is specified, attempt to convert the value
this.value = '' + targetValue;
} else if (targetValue === '') {
// treat empty string as null
this.value = null;
} else if (!isNaN(targetValue)) {
// if no dataType is specified, attempt to determine if the value is a number
this.value = +targetValue;
} else {
// otherwise use the value as is (should be a string)
this.value = targetValue;
}
this.onChange(this.value);
}
}

=====select.component.scss=====

app-select {
position: relative;
display: block;
width: 19em; // default drop-down width?  
select {
display: inline-block;
position: relative;
width: 100%;
padding: .37em .4em;
padding-right: 44px;
font-size: 1.125em;
color: #222;
border: 1px solid #000;
border-radius: 4px;
background: white;
height: 38px;
background: linear-gradient(to left, #ffcd41 0, #ffcd41 21px, #484848 21px, white 22px);
box-shadow: 0px 0px 2px 0px rgba(0, 0, 0, 0.5);
transition: border linear 0.2s, box-shadow linear 0.2s;
text-overflow: ellipsis;
option {
color: #222;
font-size: .9em;
padding: .2em;
&.default {
font-weight: 600;
color: black;
}
&.legacy {
color: #333;
}
}
optgroup {
&.legacy {
background: #eeeeee;
color: #333;
}
}
&[readonly] {
border-color: #bbb;
color: black;
background-color: #f7f7f7;
-webkit-appearance: none; // hide drop down arrow
}
&.ng-dirty {
color: black;
}
&[disabled]:not([readonly]) {
background: #E6E6E6;
opacity: 1;
}
}
.arrow-icon {
display: block;
position: absolute;
right: 1px;
top: 1px;
bottom: 1px;
width: 44px;
background: #ffcd41;
border-left: 1px solid #000;
pointer-events: none;
border-radius: 0 4px 4px 0;
padding-top: 7px;
padding-left: 11px;
font-size: 1.4em;
line-height: normal;
&:before {
font-family: "custom" !important;
content: "71";
}
}
.select[disabled]:not([readonly])+.arrow-icon {
background-color: #E6E6E6;
}
.select:focus+.arrow-icon {
right: 2px;
top: 2px;
bottom: 2px;
width: 43px;
padding-top: 6px;
}
.select:focus-within {
box-shadow: inset 0 0 0 1px #000;
}
}

经过更多的研究,我发现了dispatchEvent((函数。

这是我自己的问题的解决方案:

var driver = new ChromeDriver();
var js = (IJavaScriptExecutor)driver;
var select = baseDriver.FindElement(By.XPath("//select"));
string script = "const evt = new Event('input', {'bubbles':true, 'cancelable':true});" +
"arguments[0].dispatchEvent(evt);";
js.ExecuteScript(script, select);

此外,这仍然有效(JavaScript部分(:

const evt = document.createEvent("HTMLEvents");
evt.initEvent("input", true, true);
arguments[0].dispatchEvent(evt);

我希望有人觉得这很有用。

相关内容

  • 没有找到相关文章

最新更新