diff --git a/samples/v1.0/Elements/Action.ShowCard.json b/samples/v1.0/Elements/Action.ShowCard.json
index b22e3f7321..2795e882da 100644
--- a/samples/v1.0/Elements/Action.ShowCard.json
+++ b/samples/v1.0/Elements/Action.ShowCard.json
@@ -23,7 +23,10 @@
"actions": [
{
"type": "Action.Submit",
- "title": "Neat!"
+ "title": "Neat!",
+ "data": {
+ "neat": "true"
+ }
}
]
}
diff --git a/samples/v1.0/Elements/Column.SelectAction.json b/samples/v1.0/Elements/Column.SelectAction.json
index 8b00d3bcab..7512e694ec 100644
--- a/samples/v1.0/Elements/Column.SelectAction.json
+++ b/samples/v1.0/Elements/Column.SelectAction.json
@@ -43,7 +43,7 @@
],
"selectAction": {
"type": "Action.OpenUrl",
- "tooltip": "cool link",
+ "tooltip": "another cool link",
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
}
},
@@ -61,7 +61,7 @@
],
"selectAction": {
"type": "Action.OpenUrl",
- "tooltip": "cool link",
+ "tooltip": "a third cool link",
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
}
}
diff --git a/source/nodejs/adaptivecards-designer-app/dist/index.html b/source/nodejs/adaptivecards-designer-app/dist/index.html
index aa0058069c..6218117768 100644
--- a/source/nodejs/adaptivecards-designer-app/dist/index.html
+++ b/source/nodejs/adaptivecards-designer-app/dist/index.html
@@ -1 +1,12 @@
-
Adaptive Cards Designer
\ No newline at end of file
+
+
+
+
+ Adaptive Cards Designer
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/source/nodejs/adaptivecards-ui-testapp/index.html b/source/nodejs/adaptivecards-ui-testapp/index.html
index f9e3aef7da..6fd5a4ea7d 100644
--- a/source/nodejs/adaptivecards-ui-testapp/index.html
+++ b/source/nodejs/adaptivecards-ui-testapp/index.html
@@ -20,7 +20,12 @@ Adaptive Cards UI Test App
-
+
diff --git a/source/nodejs/adaptivecards-ui-testapp/src/adaptivecards-ui-testapp.css b/source/nodejs/adaptivecards-ui-testapp/src/adaptivecards-ui-testapp.css
index 1bdfad060f..b7f2635e1d 100644
--- a/source/nodejs/adaptivecards-ui-testapp/src/adaptivecards-ui-testapp.css
+++ b/source/nodejs/adaptivecards-ui-testapp/src/adaptivecards-ui-testapp.css
@@ -16,12 +16,12 @@ body {
}
.main {
- width: 80vw;
+ width: 60vw;
height: 100vh;
}
.testCasesDiv {
- width: 20vw;
+ width: 40vw;
height: 100vh;
}
@@ -31,13 +31,24 @@ body {
height: 80vh;
}
-#retrievedInputsDiv
+.inputsDiv
{
+ display: flex;
width: 100vw;
height: 20vh;
background-color: rgb(240, 238, 238);
}
+#hideInputsDiv
+{
+ width: 10vw;
+}
+
+#retrievedInputsDiv
+{
+ width: 90vw;
+}
+
#renderedCardSpace
{
width: 600px;
diff --git a/source/nodejs/adaptivecards-ui-testapp/src/adaptivecards-ui-testapp.ts b/source/nodejs/adaptivecards-ui-testapp/src/adaptivecards-ui-testapp.ts
index e7a408a316..66ffa09532 100644
--- a/source/nodejs/adaptivecards-ui-testapp/src/adaptivecards-ui-testapp.ts
+++ b/source/nodejs/adaptivecards-ui-testapp/src/adaptivecards-ui-testapp.ts
@@ -15,6 +15,11 @@ function renderTestCasesList() {
});
}
+function hideInputsDiv() {
+ const retrievedInputsDiv = document.getElementById("retrievedInputsDiv");
+ retrievedInputsDiv.style.visibility = "hidden";
+}
+
window.onload = () => {
renderTestCasesList();
@@ -47,4 +52,7 @@ window.onload = () => {
};
renderCard(card, cardRenderedCallback);
+
+ const hideInputsButton = document.getElementById("hideInputsBtn");
+ hideInputsButton.addEventListener("click", (e: Event) => { hideInputsDiv() });
}
diff --git a/source/nodejs/adaptivecards-ui-testapp/src/file-retriever-utils.ts b/source/nodejs/adaptivecards-ui-testapp/src/file-retriever-utils.ts
index 060504c6ab..ccee0274c6 100644
--- a/source/nodejs/adaptivecards-ui-testapp/src/file-retriever-utils.ts
+++ b/source/nodejs/adaptivecards-ui-testapp/src/file-retriever-utils.ts
@@ -1,9 +1,21 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
-const v10TestCases: string[] = ['Scenarios/ActivityUpdate'];
+const v10TestCases: string[] = [
+ 'Scenarios/ActivityUpdate',
+ 'Elements/Container.SelectAction',
+ 'Elements/Column.SelectAction',
+ 'Elements/ColumnSet.SelectAction',
+ 'Elements/Image.SelectAction',
+ 'Elements/Action.ShowCard'];
const v11TestCases: string[] = [];
const v12TestCases: string[] = [];
-const v13TestCases: string[] = ['Elements/Input.Text.ErrorMessage'];
+const v13TestCases: string[] = [
+ 'Elements/Input.Text.ErrorMessage',
+ 'Elements/Input.Number.ErrorMessage',
+ 'Elements/Input.ChoiceSet.ErrorMessage',
+ 'Elements/Input.Time.ErrorMessage',
+ 'Elements/Input.Toggle.ErrorMessage'
+];
const v14TestCases: string[] = [];
const v15TestCases: string[] = [];
const v16TestCases: string[] = [
diff --git a/source/nodejs/adaptivecards-ui-testapp/src/rendering-utils.ts b/source/nodejs/adaptivecards-ui-testapp/src/rendering-utils.ts
index 682e0b6777..4e0768b2ab 100644
--- a/source/nodejs/adaptivecards-ui-testapp/src/rendering-utils.ts
+++ b/source/nodejs/adaptivecards-ui-testapp/src/rendering-utils.ts
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
import { getTestCasesList } from "./file-retriever-utils";
-import { Action, AdaptiveCard, ExecuteAction, HostConfig, IMarkdownProcessingResult, Input, OpenUrlAction, SerializationContext, SubmitAction, Version, Versions } from "adaptivecards";
+import { Action, AdaptiveCard, ExecuteAction, HostConfig, IMarkdownProcessingResult, Input, OpenUrlAction, PropertyBag, SerializationContext, SubmitAction, Version, Versions } from "adaptivecards";
import * as Remarkable from "remarkable";
export function listAllFiles(): HTMLLIElement[] {
@@ -33,8 +33,8 @@ export function listAllFiles(): HTMLLIElement[] {
}
export async function readAndRenderCard(fileName: string, callbackFunction: Function) {
- const retrievedInputsDiv: HTMLElement = document.getElementById("renderedCardSpace");
- retrievedInputsDiv.style.visibility = "hidden";
+ const renderedCardSpaceDiv: HTMLElement = document.getElementById("renderedCardSpace");
+ renderedCardSpaceDiv.style.visibility = "hidden";
const response = await fetch(`samples/${fileName}`);
@@ -59,8 +59,6 @@ export async function readAndRenderCard(fileName: string, callbackFunction: Func
}
renderCard(jsonToRender, callbackFunction);
-
- retrievedInputsDiv.style.visibility = "visible";
}
export function renderCard(cardJson: any, callbackFunction: Function): void {
@@ -89,6 +87,16 @@ export function renderCard(cardJson: any, callbackFunction: Function): void {
inputsMap[input.id] = input.value;
});
+ if (actionType === SubmitAction.JsonTypeName)
+ {
+ const submitAction = action as SubmitAction;
+
+ for (const [key, value] of Object.entries(submitAction.data))
+ {
+ inputsMap[key] = value;
+ }
+ }
+
inputsAsJson = JSON.stringify(inputsMap);
}
else if (actionType === OpenUrlAction.JsonTypeName) {
@@ -98,6 +106,7 @@ export function renderCard(cardJson: any, callbackFunction: Function): void {
const retrievedInputsDiv: HTMLElement = document.getElementById("retrievedInputsDiv");
retrievedInputsDiv.innerHTML = inputsAsJson;
+ retrievedInputsDiv.style.visibility = "visible";
};
// For markdown support you need a third-party library
@@ -122,4 +131,5 @@ export function cardRenderedCallback(renderedCard: HTMLElement) {
const renderedCardDiv = document.getElementById("renderedCardSpace");
renderedCardDiv.innerHTML = "";
renderedCardDiv.appendChild(renderedCard);
+ renderedCardDiv.style.visibility = "visible";
}
diff --git a/source/nodejs/tests/ui-tests/src/card-element-utils.ts b/source/nodejs/tests/ui-tests/src/card-element-utils.ts
new file mode 100644
index 0000000000..67e3a6dd66
--- /dev/null
+++ b/source/nodejs/tests/ui-tests/src/card-element-utils.ts
@@ -0,0 +1,500 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+import { WebElement, By, Locator } from "selenium-webdriver";
+import { ByExtended, XpathBuilder } from "./selenium-utils";
+import { TestUtils } from "./test-utils";
+import { WaitUtils } from "./wait-utils";
+import * as Assert from "assert";
+
+export abstract class ACElement {
+ private _id: string;
+ private _underlyingElement?: WebElement = undefined;
+ private _container?: ACContainer;
+
+ protected constructor(underlyingElement: WebElement | string, container?: ACContainer) {
+ if (typeof underlyingElement === "string") {
+ this._id = underlyingElement;
+ } else {
+ this._underlyingElement = underlyingElement;
+ }
+
+ this._container = container;
+ }
+
+ public get id(): string {
+ return this._id;
+ }
+
+ public get underlyingElement(): WebElement | undefined {
+ return this._underlyingElement;
+ }
+
+ protected set underlyingElement(underlyingElement: WebElement | undefined) {
+ this._underlyingElement = underlyingElement;
+ }
+
+ public get container(): ACContainer | undefined {
+ return this._container;
+ }
+
+ abstract ensureUnderlyingElement(className?: string): Promise;
+
+ elementWasFound(): boolean {
+ return (this.underlyingElement !== undefined);
+ }
+
+ async getCssPropertyValue(propertyName: string): Promise {
+ if (this.elementWasFound()) {
+ return await TestUtils.getInstance().getCssPropertyValueForElement(this.underlyingElement!, propertyName);
+ } else {
+ return undefined;
+ }
+ }
+
+ async elementIsVisible(): Promise {
+ return (await this.underlyingElement?.getAttribute("visible") === "true") || (await this.underlyingElement!.isDisplayed());
+ }
+
+ async elementIsCssVisible(): Promise {
+ return (await this.getCssPropertyValue("visibility") === "visible");
+ }
+
+ async getChildrenHtml(): Promise {
+ return (await this.underlyingElement!.getAttribute("innerHtml"));
+ }
+
+ async getHtml(): Promise {
+ return (await this.underlyingElement!.getAttribute("outerHtml"));
+ }
+}
+
+export abstract class ACActionableElement extends ACElement {
+ async click()
+ {
+ await TestUtils.getInstance().clearInputs();
+ await this.underlyingElement?.click();
+ }
+}
+
+export class Input extends ACElement {
+ protected div: WebElement;
+ protected label?: WebElement;
+ protected errorMessage?: WebElement;
+
+ async setData(data: string): Promise {
+ await this.underlyingElement?.click();
+ await this.underlyingElement?.sendKeys(data);
+ }
+
+ override async ensureUnderlyingElement(className?: string): Promise {
+ this.div = await TestUtils.getInstance().getInputContainer(this.id, this.container);
+ this.underlyingElement = await TestUtils.getInstance().getInput(className!, this.div);
+
+ await this.getLabel();
+ }
+
+ hasLabel(): boolean {
+ return (this.label !== undefined);
+ }
+
+ async getLabel(): Promise {
+ await this.ensureLabel();
+ return await this.label?.getText();
+ }
+
+ async getErrorMessage(): Promise {
+ await this.ensureErrorMessage();
+ return this.errorMessage?.getText();
+ }
+
+ async isRequired(): Promise {
+ return await this.classListContains(this.underlyingElement!, "ac-input-required");
+ }
+
+ async validationFailed(): Promise {
+ return await this.classListContains(this.underlyingElement!, "ac-input-validation-failed");
+ }
+
+ async classListContains(element: WebElement, classToQuery: string): Promise {
+ return (await element.getAttribute("class")).split(" ").includes(classToQuery);
+ }
+
+ async ensureLabel(): Promise {
+ const htmlInputId: string = await this.underlyingElement!.getAttribute("id");
+ const labels: WebElement[] = await this.div.findElements(By.xpath(new XpathBuilder().setTagName("label").addAttributeEquals("for", htmlInputId).buildXpath()));
+ this.label = (labels.length > 0) ? labels[0] : undefined;
+ }
+
+ async ensureErrorMessage(): Promise {
+ if (await this.validationFailed()) {
+ this.errorMessage = await this.div.findElement(By.id(await this.getErrorMessageId()));
+ }
+ }
+
+ protected async getErrorMessageId(): Promise {
+ const ariaLabeledBy = await this.underlyingElement!.getAttribute("aria-labelledby");
+
+ let labels: string[] = ariaLabeledBy.split(" ");
+ Assert.strictEqual(labels.length, 2, `Labels contains more than two labels ${labels}`);
+
+ labels.splice(labels.indexOf(await this.label!.getAttribute("id")), 1);
+ Assert.strictEqual(labels.length, 1, `Labels contains more than one label ${labels}`);
+
+ return labels[0];
+ }
+
+ async getChildrenHtml(): Promise {
+ return (await this.div.getAttribute("innerHtml"));
+ }
+
+ async getHtml(): Promise {
+ return (await this.div.getAttribute("outerHtml"));
+ }
+}
+
+export class ACTypeableInput extends Input {
+ async isFocused(): Promise {
+ if (this.elementWasFound()) {
+ const inputId: string = await this.underlyingElement!.getAttribute("id");
+ const activeElementId: string = await TestUtils.getInstance().driver.switchTo().activeElement().getAttribute("id");
+
+ return (inputId === activeElementId);
+ }
+
+ return false;
+ }
+}
+
+export class ACInputText extends ACTypeableInput {
+ static async getInputWithId(id: string, container?: ACContainer): Promise {
+ const input = new ACInputText(id, container);
+ await input.ensureUnderlyingElement("ac-textInput");
+ return input;
+ }
+}
+
+export class ACInputDate extends ACTypeableInput {
+ static async getInputWithId(id: string, container?: ACContainer): Promise {
+ const input = new ACInputDate(id, container);
+ await input.ensureUnderlyingElement("ac-dateInput");
+ return input;
+ }
+
+ async setDate(year: number, month: number, day: number) {
+ await this.setData(month.toString().padStart(2, "0") + day.toString().padStart(2, "0") + year.toString());
+ }
+}
+
+export class ACInputTime extends ACTypeableInput {
+ static async getInputWithId(id: string, container?: ACContainer): Promise {
+ const input = new ACInputTime(id, container);
+ await input.ensureUnderlyingElement("ac-timeInput");
+ return input;
+ }
+
+ async setTime(hour: number, minute: number) {
+ const meridian: string = (hour >= 12) ? "PM" : "AM";
+ await this.setData(hour.toString().padStart(2, "0") + minute.toString().padStart(2, "0") + meridian);
+ }
+}
+
+export class ACInputNumber extends ACTypeableInput {
+ static async getInputWithId(id: string, container?: ACContainer): Promise {
+ const input = new ACInputNumber(id, container);
+ await input.ensureUnderlyingElement("ac-numberInput");
+ return input;
+ }
+}
+
+export class ACInputChoiceSet extends Input
+{
+ isExpanded: boolean = false;
+ isMultiSelect: boolean = false;
+ underlyingExpandedElements: WebElement[];
+
+ private constructor(id: string, isExpanded: boolean, isMultiSelect: boolean, container?: ACContainer) {
+ super(id, container);
+
+ this.isExpanded = isExpanded;
+ this.isMultiSelect = isMultiSelect;
+ }
+
+ override async ensureUnderlyingElement(className?: string): Promise {
+ this.div = await TestUtils.getInstance().getInputContainer(this.id, this.container);
+
+ if (!this.isExpanded) {
+ await this.getCompactChoiceSet(this.id, this.container);
+ }
+ else {
+ await this.getExpandedChoiceSet(this.id, this.container);
+ }
+ }
+
+ private async getCompactChoiceSet(id: string, container?: ACContainer): Promise {
+ this.underlyingElement = await this.div.findElement(By.className("ac-choiceSetInput-compact"));
+ }
+
+ private async getExpandedChoiceSet(id: string, container?: ACContainer): Promise {
+ if (this.isMultiSelect) {
+ this.underlyingElement = await this.div.findElement(ByExtended.containsClass("ac-choiceSetInput-multiSelect"));
+ } else {
+ this.underlyingElement = await this.div.findElement(ByExtended.containsClass("ac-choiceSetInput-expanded"));
+ }
+ this.underlyingExpandedElements = await TestUtils.getInstance().getElementsWithName(id, container);
+ }
+
+ static async getInputWithId(id: string, isExpanded: boolean, isMultiSelect: boolean, container?: ACContainer): Promise {
+ const input = new ACInputChoiceSet(id, isExpanded, isMultiSelect, container);
+ await input.ensureUnderlyingElement();
+ return input;
+ }
+
+ override async setData(data: string): Promise {
+
+ if (this.elementWasFound()) {
+ if (this.isExpanded) {
+ if (this.isMultiSelect) {
+ const options: string[] = data.split(",");
+ for (const choice of this.underlyingExpandedElements)
+ {
+ const choiceValue = await choice.getAttribute("aria-label");
+ const choiceIsSelected: boolean = await choice.isSelected();
+
+ // if the choice has to be selected and it's not selected, click it
+ // or if the choice must not be selected but is selected, click it
+ if ((options.includes(choiceValue) && !choiceIsSelected) ||
+ (!options.includes(choiceValue) && choiceIsSelected)) {
+ await choice.click();
+ }
+ }
+ }
+ else
+ {
+ for (const choice of this.underlyingExpandedElements)
+ {
+ const choiceValue = await choice.getAttribute("aria-label");
+ if (choiceValue === data) {
+ await choice.click();
+ }
+ }
+ }
+
+ } else {
+ let underlyingElement: WebElement = this.underlyingElement!;
+ await underlyingElement.click();
+
+ let option = await underlyingElement.findElement(By.xpath(`//*[@aria-label='${data}']`));
+ await option.click();
+ }
+ }
+ }
+
+ override elementWasFound(): boolean {
+ if (this.isExpanded) {
+ return this.underlyingExpandedElements.length > 0;
+ }
+ else {
+ return super.elementWasFound();
+ }
+ }
+
+ override async getErrorMessageId(): Promise {
+ if (this.isExpanded) {
+ const ariaLabeledBy = await this.underlyingExpandedElements[0].getAttribute("aria-labelledby");
+ const choiceId = await this.underlyingExpandedElements[0].getAttribute("id");
+
+ let labels: string[] = ariaLabeledBy.split(" ");
+ Assert.strictEqual(labels.length, 3, `Labels contains more than three labels ${labels}`);
+
+ labels.splice(labels.indexOf(await this.label!.getAttribute("id")), 1);
+ labels.splice(labels.indexOf(await this.getIdOfLabel(choiceId)), 1);
+ Assert.strictEqual(labels.length, 1, `Labels contains more than one label ${labels}`);
+
+ return labels[0];
+ } else {
+ return super.getErrorMessageId();
+ }
+ }
+
+ private async getIdOfLabel(choiceId: string): Promise {
+ const label: WebElement = await this.div.findElement(By.xpath(new XpathBuilder().setTagName("label").addAttributeEquals("for", choiceId).buildXpath()));
+ return await label.getAttribute("id");
+ }
+}
+
+export class ACInputToggle extends Input {
+ static async getInputWithId(id: string, container?: ACContainer): Promise {
+ const input = new ACInputToggle(id, container);
+ await input.ensureUnderlyingElement("ac-toggleInput");
+ return input;
+ }
+
+ override async setData(data: string): Promise {
+ if (this.elementWasFound()) {
+ if (data === "set") {
+ await this.set();
+ }
+ else if (data === "unset") {
+ await this.unset();
+ }
+ else {
+ await this.toggle();
+ }
+ }
+ }
+
+ async set(): Promise {
+ const isInputSelected: boolean = await this.underlyingElement!.isSelected();
+
+ if (!isInputSelected) {
+ await this.toggle();
+ }
+ }
+
+ async unset(): Promise {
+ const isInputSelected: boolean = await this.underlyingElement!.isSelected();
+
+ if (isInputSelected) {
+ await this.toggle();
+ }
+ }
+
+ async toggle(): Promise {
+ await this.underlyingElement?.click();
+ }
+}
+
+export class ACContainer extends ACActionableElement {
+ static async getContainer(id: string): Promise {
+ return new ACContainer(id);
+ }
+
+ static async getContainerWithAction(tooltip: string, parentContainer?: ACContainer): Promise {
+ const container = new ACContainer(tooltip, parentContainer);
+ await container.ensureUnderlyingElement();
+ return container;
+ }
+
+ override async ensureUnderlyingElement(className?: string): Promise {
+ this.underlyingElement = await TestUtils.getInstance().tryGetContainerWithAction(this.id, this.container);
+ }
+}
+
+// Currently Column and ColumnSet behave just as container (except columnset having a different class name),
+// leaving the empty classes for code clarity when used
+export class ACColumn extends ACContainer {}
+
+export class ACColumnSet extends ACContainer {}
+
+export class ACCard extends ACContainer {
+
+ private index: number = 0;
+
+ constructor(underlyingElement: string, index?: number) {
+ super(underlyingElement);
+
+ if (index !== undefined) {
+ this.index = index;
+ }
+ }
+
+ static async getCard(index?: number): Promise {
+ const card = new ACCard("", index);
+ await card.ensureUnderlyingElement("ac-adaptiveCard");
+ return card;
+ }
+
+ override async ensureUnderlyingElement(className?: string): Promise {
+ const cardList = await TestUtils.getInstance().getElementsWithClass(className!);
+ this.underlyingElement = cardList[this.index];
+ }
+}
+
+export class ACImage extends ACActionableElement {
+ static async getImage(title: string, container?: ACContainer): Promise
+ {
+ const image = new ACImage(title, container);
+ await image.ensureUnderlyingElement("ac-image");
+ return image;
+ }
+
+ override async ensureUnderlyingElement(className?: string): Promise {
+ this.underlyingElement = await TestUtils.getInstance().getImageWithTitle(this.id, className!, this.container);
+ }
+
+ async getSrc(): Promise {
+ return await this.underlyingElement!.getAttribute("src");
+ }
+}
+
+export class ACAction extends ACActionableElement {
+ static async clickOnActionWithTitle(title: string): Promise
+ {
+ let action: ACAction = await this.getActionWithTitle(title);
+ await action.click();
+ }
+
+ static async getActionWithTitle(title: string): Promise
+ {
+ let action = new ACAction(title);
+ await action.ensureUnderlyingElement();
+ return action;
+ }
+
+ override async ensureUnderlyingElement(className?: string): Promise {
+ this.underlyingElement = await TestUtils.getInstance().tryGetActionWithTitle(this.id);
+ }
+}
+
+// As currently there's only one carousel per card is supported then this tests are simpler
+export class ACCarousel {
+
+ private constructor(){}
+
+ static async clickOnLeftArrow()
+ {
+ const leftArrow = await TestUtils.getInstance().driver.findElement(By.className("ac-carousel-left"));
+ await leftArrow.click();
+ }
+
+ static async clickOnRightArrow()
+ {
+ const leftArrow = await TestUtils.getInstance().driver.findElement(By.className("ac-carousel-right"));
+ await leftArrow.click();
+ }
+
+ static async isPageVisible(pageId: string): Promise
+ {
+ let anyPageIsVisible = false;
+
+ // Due to how the swiper library is made, the first and last pages are duplicated, if we were to use the regular
+ // getElementWithId method we would retrieve the duplicated slide, so we have to get the second element with that id
+ const pages = await TestUtils.getInstance().getElementsWithId(pageId);
+
+ for (const page of pages)
+ {
+ const pageVisibility = await TestUtils.getInstance().getCssPropertyValueForElement(page, "visibility");
+ anyPageIsVisible = anyPageIsVisible || (pageVisibility === "visible")
+ }
+
+ return anyPageIsVisible;
+ }
+
+ static async getPageDirection(pageId: string): Promise
+ {
+ const pageElement = await TestUtils.getInstance().getElementWithId(pageId);
+ // The actual direction is provided to a id-less div under the page, so
+ // we query for that value
+ const pageContainer = await pageElement.findElement(By.xpath("./*"));
+ const pageDirection = await pageContainer.getAttribute("dir");
+
+ return pageDirection;
+ }
+
+ // This is a helper method to handle the odd scenario of the carousel not behaving correctly
+ // when clicking an arrow while the carousel is moving
+ static async waitForAnimationsToEnd(): Promise
+ {
+ await WaitUtils.waitFor(1000);
+ }
+}
\ No newline at end of file
diff --git a/source/nodejs/tests/ui-tests/src/selenium-utils.ts b/source/nodejs/tests/ui-tests/src/selenium-utils.ts
new file mode 100644
index 0000000000..7af2d1bf30
--- /dev/null
+++ b/source/nodejs/tests/ui-tests/src/selenium-utils.ts
@@ -0,0 +1,89 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+import { By } from "selenium-webdriver";
+
+export class ByExtended extends By {
+ static ariaLabel(ariaLabel: string, tagName?: string): By {
+ return this.getXpathProperty("aria-label", ariaLabel);
+ }
+
+ static ariaDescription(ariaDescription: string, tagName?: string): By {
+ return this.getXpathProperty("aria-description", ariaDescription);
+ }
+
+ static title(title: string, tagName?: string): By {
+ return this.getXpathProperty("title", title);
+ }
+
+ static containsClass(className: string, tagName?: string): By {
+ return By.xpath(new XpathBuilder().addAttributeContains(className, "class").buildXpath());
+ }
+
+ static getXpathProperty(propertyName: string, propertyValue: string, tagName?:string): By {
+ const tag = tagName? tagName: '*';
+ return By.xpath(new XpathBuilder().setTagName(tag).addAttributeEquals(propertyName, propertyValue).buildXpath());
+ }
+}
+
+export class XpathBuilder {
+ private tagName?: string;
+ private expressions: string[] = [];
+
+ setTagName(tagName: string): XpathBuilder {
+ this.tagName = tagName;
+ return this;
+ }
+
+ addAttributeEquals(attribute: string, value: string): XpathBuilder {
+ return this.addEquality(value, attribute);
+ }
+
+ addTextEquals(value: string): XpathBuilder {
+ return this.addEquality(value);
+ }
+
+ addAttributeStartsWith(value: string, attribute?:"string"): XpathBuilder {
+ return this.addFunction("starts-with", value, attribute);
+ }
+
+ addAttributeContains(value: string, attribute?: string): XpathBuilder {
+ return this.addFunction("contains", value, attribute);
+ }
+
+ private addEquality(value: string, attribute?: string): XpathBuilder {
+ const attributeName = (attribute !== undefined) ? "@" + attribute : "text()";
+ this.expressions.push(`${attributeName}='${value}'`);
+ return this;
+ }
+
+ private addFunction(functionName: string, value: string, attribute?: string): XpathBuilder {
+ const attributeName = (attribute !== undefined) ? "@" + attribute : "text()";
+ this.expressions.push(`${functionName}(${attributeName},'${value}')`);
+ return this;
+ }
+
+ private concatenateExpressions(): string {
+ let concatenatedExpressions = "";
+ let isFirst = true;
+
+ for (const expression of this.expressions)
+ {
+ if (!isFirst) {
+ concatenatedExpressions += " and ";
+ }
+ concatenatedExpressions += expression;
+
+ isFirst = false;
+ }
+
+ return concatenatedExpressions;
+ }
+
+ buildXpath(): string {
+ const tagName = (this.tagName !== undefined) ? this.tagName : "*";
+ const expressions = this.concatenateExpressions();
+ return `//${tagName}[${expressions}]`;
+ }
+
+}
\ No newline at end of file
diff --git a/source/nodejs/tests/ui-tests/src/test-utils.ts b/source/nodejs/tests/ui-tests/src/test-utils.ts
new file mode 100644
index 0000000000..05ba66c59d
--- /dev/null
+++ b/source/nodejs/tests/ui-tests/src/test-utils.ts
@@ -0,0 +1,164 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+import { Builder, By, Capabilities, Locator, until, WebDriver, WebElement } from "selenium-webdriver";
+import * as Assert from "assert";
+import { WaitUntil, WaitUtils } from "./wait-utils";
+import { ACCard, ACContainer } from "./card-element-utils";
+import { ByExtended } from "./selenium-utils";
+
+export class TestUtils {
+
+ private _driver: WebDriver;
+ private hideInputsButton: WebElement | undefined = undefined;
+ private inputs: any = undefined;
+
+ private static instance : TestUtils;
+
+ private constructor()
+ {
+ this._driver = new Builder().withCapabilities(Capabilities.edge()).build();
+ }
+
+ async initializeDriver()
+ {
+ await TestUtils.instance.driver.get("http://127.0.0.1:8080/");
+ }
+
+ async stopDriver()
+ {
+ if (this.driver) {
+ await this.driver.quit();
+ }
+ }
+
+ static getInstance()
+ {
+ if (TestUtils.instance == undefined) {
+ TestUtils.instance = new TestUtils();
+ }
+ return TestUtils.instance;
+ }
+
+ get driver(): WebDriver {
+ return this._driver;
+ }
+
+ async goToTestCase(testCaseName: string): Promise {
+ const elementLinkText: WebElement = await this.driver.findElement(By.id(testCaseName));
+ await elementLinkText.click();
+
+ const renderedCardContainer: WebElement = await this.getElementWithId("renderedCardSpace");
+
+ await this.driver.wait(until.elementIsVisible(renderedCardContainer), WaitUtils.timeoutForCardRendering);
+
+ return await ACCard.getCard();
+ }
+
+ async getHideInputsButton(): Promise {
+ if (this.hideInputsButton === undefined)
+ {
+ this.hideInputsButton = await this.getElementWithId("hideInputsBtn");
+ }
+
+ return this.hideInputsButton!;
+ }
+
+ async clearInputs(): Promise {
+ this.inputs = undefined;
+ await this.hideInputsDiv();
+ }
+
+ async hideInputsDiv(): Promise {
+ const hideInputsButton = await this.getHideInputsButton();
+ await hideInputsButton.click();
+ }
+
+ async getInputFor(inputId: string): Promise {
+ if (this.inputs === undefined) {
+ await this.getRetrievedInputs();
+ }
+
+ return this.inputs[inputId];
+ }
+
+ async getUrlInRetrievedInputs(): Promise {
+ return await this.getInputFor("url");
+ }
+
+ async getRetrievedInputs(): Promise {
+ if (this.inputs === undefined) {
+ const retrievedInputsDiv: WebElement = await this.getElementWithId("retrievedInputsDiv");
+
+ let successFunction = async (params: any) => {
+ let visibility = await TestUtils.getInstance().getCssPropertyValueForElement(retrievedInputsDiv, "visibility");
+ return (visibility === "visible");
+ };
+
+ await WaitUntil.customWait(WaitUtils.timeoutForCardRendering, successFunction, retrievedInputsDiv);
+
+ const retrievedInputs: string = await retrievedInputsDiv.getText();
+
+ this.inputs = JSON.parse(retrievedInputs)
+ }
+ }
+
+ getRoot(container?: ACContainer): WebElement | WebDriver {
+ if (container !== undefined && container.elementWasFound()) {
+ return container.underlyingElement!;
+ }
+ else {
+ return this.driver;
+ }
+ }
+
+ async tryGetActionWithTitle(actionTitle: string): Promise {
+ const buttonList: WebElement[] = await this.driver.findElements(ByExtended.ariaLabel(actionTitle));
+ return (buttonList.length > 0) ? buttonList[0] : undefined;
+ }
+
+ async tryGetContainerWithAction(tooltip: string, container?: ACContainer): Promise {
+ const containerList: WebElement[] = await this.getRoot(container).findElements(ByExtended.ariaDescription(tooltip));
+ return (containerList.length > 0) ? containerList[0] : undefined;
+ }
+
+ async getImageWithTitle(title: string, className: string, container?: ACContainer): Promise {
+ return await this.getRoot(container).findElement(ByExtended.title(title));
+ }
+
+ async getActionWithTitle(actionTitle: string, container?: ACContainer): Promise {
+ return await this.getRoot(container).findElement(ByExtended.title(actionTitle));
+ }
+
+ async getInputContainer(inputId: string, container?: ACContainer): Promise {
+ return await this.getRoot(container).findElement(By.id(inputId));
+ }
+
+ async getInput(className: string, container: WebElement): Promise {
+ return await container.findElement(By.className(className));
+ }
+
+ async getElementsWithClass(className: string, container?: ACContainer): Promise {
+ return await this.getRoot(container).findElements(By.className(className));
+ }
+
+ async assertElementWithIdDoesNotExist(id: string): Promise {
+ const elementList = await this.getElementsWithId(id);
+ Assert.strictEqual(elementList.length, 0);
+ }
+
+ async getElementsWithId(id: string): Promise {
+ return this.driver.findElements(By.id(id));
+ }
+
+ async getElementWithId(id: string, container?: ACContainer): Promise {
+ return await this.getRoot(container).findElement(By.id(id));
+ }
+
+ async getCssPropertyValueForElement(element: WebElement, cssProperty: string): Promise {
+ return element.getCssValue(cssProperty);
+ }
+
+ async getElementsWithName(id: string, container?: ACContainer): Promise {
+ return await this.getRoot(container).findElements(By.name(id));
+ }
+}
diff --git a/source/nodejs/tests/ui-tests/src/testUtils.ts b/source/nodejs/tests/ui-tests/src/testUtils.ts
deleted file mode 100644
index 9d878efbdf..0000000000
--- a/source/nodejs/tests/ui-tests/src/testUtils.ts
+++ /dev/null
@@ -1,108 +0,0 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT License.
-import * as Webdriver from "selenium-webdriver";
-import * as Assert from "assert";
-
-export class TestUtils {
- driver: Webdriver.WebDriver;
- timeoutForCardRendering: number = 10000;
-
- constructor(driver: Webdriver.WebDriver) {
- this.driver = driver;
- }
-
- async goToTestCase(testCaseName: string): Promise {
- const elementLinkText: Webdriver.WebElement = await this.driver.findElement(Webdriver.By.id(testCaseName));
- await elementLinkText.click();
-
- const renderedCardContainer: Webdriver.WebElement = await this.getElementWithId("renderedCardSpace");
-
- await this.driver.wait(Webdriver.until.elementIsVisible(renderedCardContainer), this.timeoutForCardRendering);
- }
-
- async tryGetActionWithTitle(actionTitle: string): Promise {
- const buttonList: Webdriver.WebElement[] = await this.driver.findElements(Webdriver.By.xpath(`//*[@aria-label='${actionTitle}']`));
-
- return (buttonList.length > 0) ? buttonList[0] : null;
- }
-
- async getActionWithTitle(actionTitle: string): Promise {
- const button: Webdriver.WebElement = await this.driver.findElement(Webdriver.By.xpath(`//*[@title='${actionTitle}']`));
-
- return button;
- }
-
- async clickOnActionWithTitle(actionTitle: string): Promise {
- const button: Webdriver.WebElement = await this.getActionWithTitle(actionTitle);
- await button.click();
- }
-
- async getRetrievedInputs(): Promise {
- const retrievedInputsDiv: Webdriver.WebElement = await this.driver.findElement(Webdriver.By.id("retrievedInputsDiv"));
- const retrievedInputsText: string = await retrievedInputsDiv.getText();
-
- return retrievedInputsText;
- }
-
- async getInputFor(inputId: string): Promise {
- const retrievedInputsAsJsonString: string = await this.getRetrievedInputs();
- const retrievedInputs: any = JSON.parse(retrievedInputsAsJsonString);
-
- return retrievedInputs[inputId];
- }
-
- async getInput(inputId: string, className: string): Promise {
- const inputDiv: Webdriver.WebElement = await this.driver.findElement(Webdriver.By.id(inputId));
- const input = await inputDiv.findElement(Webdriver.By.className(className));
-
- return input;
- }
-
- async setValueOnInput(inputId: string, className: string, value: string) {
- const input: Webdriver.WebElement = await this.getInput(inputId, className);
- await input.click();
- await input.sendKeys(value);
- }
-
- async setDateOnDateInput(dateInputId: string, value: string): Promise {
- await this.setValueOnInput(dateInputId, "ac-dateInput", value);
- }
-
- async setTextOnTextInput(textInputId: string, value: string): Promise {
- await this.setValueOnInput(textInputId, "ac-textInput", value);
- }
-
- async getTextInput(textInputId: string): Promise {
- const textInput: Webdriver.WebElement = await this.getInput(textInputId, "ac-textInput");
-
- return textInput;
- }
-
- // Await for x miliseconds
- async delay(miliseconds: number): Promise {
- return new Promise(res => setTimeout(res, miliseconds));
- }
-
- async assertElementWithIdDoesNotExist(id: string): Promise {
- const elementList = await this.driver.findElements(Webdriver.By.id(id));
- Assert.strictEqual(0, elementList.length);
- }
-
- async getElementsWithId(id: string): Promise {
- return this.driver.findElements(Webdriver.By.id(id));
- }
-
- async getElementWithId(id: string): Promise {
- return this.driver.findElement(Webdriver.By.id(id));
- }
-
- async getCssPropertyValueForElement(element: Webdriver.WebElement, cssProperty: string): Promise {
- return element.getCssValue(cssProperty);
- }
-
- async getCssPropertyValueForElementWithId(id: string, cssProperty: string): Promise {
- const element: Webdriver.WebElement = await this.getElementWithId(id);
-
- return this.getCssPropertyValueForElement(element, cssProperty);
- }
-}
diff --git a/source/nodejs/tests/ui-tests/src/ui-tests.test.ts b/source/nodejs/tests/ui-tests/src/ui-tests.test.ts
index 1890fdcd4c..37e055e795 100644
--- a/source/nodejs/tests/ui-tests/src/ui-tests.test.ts
+++ b/source/nodejs/tests/ui-tests/src/ui-tests.test.ts
@@ -1,20 +1,17 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
import * as Assert from "assert";
-import { assert } from "console";
-import * as Webdriver from "selenium-webdriver";
-import * as TestUtils from "./testUtils";
+import { ACAction, ACCarousel, ACColumn, ACColumnSet, ACContainer, ACInputChoiceSet, ACInputDate, ACInputNumber, ACInputText, ACInputTime, ACInputToggle, ACImage, ACElement } from "./card-element-utils";
+import { TestUtils } from "./test-utils";
+import { WaitUtils } from "./wait-utils";
describe("Mock function", function() {
- let driver: Webdriver.WebDriver;
- let testUtils: TestUtils.TestUtils;
+ let utils: TestUtils;
// This is a constant value for the wait time between pressing an action and retrieving
// the input value. Only use this if you see some test flakiness. Value is given in ms
- const delayForInputRetrieval: number = 500;
- const delayForCarouselArrows: number = 1000;
- const delayForCarouselTimer: number = 5500;
- const timeOutValueForCarousel: number = 9000;
+ const delayForCarouselTimer: number = 6000;
+ const timeOutValueForCarousel: number = 30000;
const timeOutValueForSuddenJumpTest: number = 20000;
// Timeout of 10 minutes for the dev server to start up in the CI jobs, the dev-server
@@ -23,184 +20,336 @@ describe("Mock function", function() {
const timeoutForServerStartupInCIBuild: number = 600000;
beforeAll(async() => {
- driver = new Webdriver.Builder().withCapabilities(Webdriver.Capabilities.edge()).build();
- await driver.get("http://127.0.0.1:8080/");
-
- testUtils = new TestUtils.TestUtils(driver);
+ utils = TestUtils.getInstance();
+ await utils.initializeDriver();
}, timeoutForServerStartupInCIBuild);
afterAll(async() => {
- if (driver) {
- await driver.quit();
- }
+ await utils.stopDriver();
});
test("Test ActivityUpdate submit", (async() => {
- await testUtils.goToTestCase("v1.0/ActivityUpdate");
+ await utils.goToTestCase("v1.0/ActivityUpdate");
- await testUtils.clickOnActionWithTitle("Set due date");
+ await ACAction.clickOnActionWithTitle("Set due date");
- await testUtils.setDateOnDateInput("dueDate", "02041993");
- await testUtils.setTextOnTextInput("comment", "A comment");
+ let dueDateInput = await ACInputDate.getInputWithId("dueDate");
+ await dueDateInput.setDate(1993, 2, 4);
- await testUtils.clickOnActionWithTitle("OK");
+ let commentInput = await ACInputText.getInputWithId("comment");
+ await commentInput.setData("A comment");
- const dueDateRetrievedValue: string = await testUtils.getInputFor("dueDate");
- Assert.strictEqual("1993-02-04", dueDateRetrievedValue);
+ await ACAction.clickOnActionWithTitle("OK");
- const commentRetrievedValue: string = await testUtils.getInputFor("comment");
- Assert.strictEqual("A comment", commentRetrievedValue);
+ Assert.strictEqual(await utils.getInputFor("dueDate"), "1993-02-04");
+ Assert.strictEqual(await utils.getInputFor("comment"), "A comment");
}));
test("Test TextInput get focus on invalid submit", (async() => {
- await testUtils.goToTestCase("v1.3/Input.Text.ErrorMessage");
+ await utils.goToTestCase("v1.3/Input.Text.ErrorMessage");
+
+ const commentInput = await ACInputText.getInputWithId("id1");
+
+ Assert.strictEqual(await commentInput.getLabel(), "Required Input.Text *");
+ Assert.strictEqual(await commentInput.isRequired(), true);
+ Assert.strictEqual(await commentInput.getErrorMessage(), undefined);
+ Assert.strictEqual(await commentInput.isFocused(), false);
+
+ await ACAction.clickOnActionWithTitle("Submit");
+
+ Assert.strictEqual(await commentInput.getErrorMessage(), "This is a required input");
+ Assert.strictEqual(await commentInput.isFocused(), true);
+ }));
+
+ test("Test interaction with Input.Number", (async() => {
+ await utils.goToTestCase("v1.3/Input.Number.ErrorMessage");
+
+ let input1 = await ACInputNumber.getInputWithId("input1");
+ await input1.setData("1");
+
+ let input2 = await ACInputNumber.getInputWithId("input2");
+ await input2.setData("5");
+
+ let input3 = await ACInputNumber.getInputWithId("input3");
+ await input3.setData("10");
+
+ let input4 = await ACInputNumber.getInputWithId("input4");
+ await input4.setData("50");
+
+ await ACAction.clickOnActionWithTitle("Submit");
+
+ Assert.strictEqual(await utils.getInputFor("input1"), "1");
+ Assert.strictEqual(await utils.getInputFor("input2"), "5");
+ Assert.strictEqual(await utils.getInputFor("input3"), "10");
+ Assert.strictEqual(await utils.getInputFor("input4"), "50");
+ }));
+
+ test("Input.ChoiceSet: Test input interaction", (async() => {
+ await utils.goToTestCase("v1.3/Input.ChoiceSet.ErrorMessage");
+
+ let compactChoiceSet = await ACInputChoiceSet.getInputWithId("requiredCompactId", false, false);
+ await compactChoiceSet.setData("Option 1");
+
+ let expandedChoiceSet = await ACInputChoiceSet.getInputWithId("requiredExpandedId", true, false);
+ await expandedChoiceSet.setData("Option 2");
- await testUtils.clickOnActionWithTitle("Submit");
+ let multiselectChoiceSet = await ACInputChoiceSet.getInputWithId("requiredMultiselectId", true, true);
+ await multiselectChoiceSet.setData("Option 1,Option 2");
- const firstInput: Webdriver.WebElement = await testUtils.getTextInput("id1");
+ await ACAction.clickOnActionWithTitle("OK");
- const firstInputId: string = await firstInput.getAttribute("id");
- const activeElementId: string = await driver.switchTo().activeElement().getAttribute("id");
+ Assert.strictEqual(await utils.getInputFor("requiredCompactId"), "1");
+ Assert.strictEqual(await utils.getInputFor("requiredExpandedId"), "2");
+ Assert.strictEqual(await utils.getInputFor("requiredMultiselectId"), "1,2");
+ }));
+
+ test("Input.ChoiceSet: Test compact required validation", (async() => {
+ await utils.goToTestCase("v1.3/Input.ChoiceSet.ErrorMessage");
+
+ const compactChoiceSet = await ACInputChoiceSet.getInputWithId("requiredCompactId", false, false);
+ Assert.strictEqual(await compactChoiceSet.getLabel(), "Required Input.ChoiceSet label (compact) *");
+ Assert.strictEqual(await compactChoiceSet.isRequired(), true);
+ Assert.strictEqual(await compactChoiceSet.getErrorMessage(), undefined);
- Assert.strictEqual(firstInputId, activeElementId);
+ await ACAction.clickOnActionWithTitle("OK");
+
+ Assert.strictEqual(await compactChoiceSet.validationFailed(), true);
+ Assert.strictEqual(await compactChoiceSet.getErrorMessage(), "This is a required input");
}));
- test("Test actions are rendered and active below carousel", (async() => {
- await testUtils.goToTestCase("v1.6/Carousel.HostConfig");
+ test("Input.ChoiceSet: Test expanded required validation", (async() => {
+ await utils.goToTestCase("v1.3/Input.ChoiceSet.ErrorMessage");
- await testUtils.clickOnActionWithTitle("See more");
+ const expandedChoiceSet = await ACInputChoiceSet.getInputWithId("requiredExpandedId", true, false);
+ Assert.strictEqual(await expandedChoiceSet.getLabel(), "Required Input.ChoiceSet label (expanded) *");
+ Assert.strictEqual(await expandedChoiceSet.isRequired(), true);
+ Assert.strictEqual(await expandedChoiceSet.getErrorMessage(), undefined);
- await testUtils.delay(delayForInputRetrieval);
+ await ACAction.clickOnActionWithTitle("OK");
- const url: string = await testUtils.getInputFor("url");
- Assert.strictEqual("https://adaptivecards.io", url);
+ Assert.strictEqual(await expandedChoiceSet.validationFailed(), true);
+ Assert.strictEqual(await expandedChoiceSet.getErrorMessage(), "This is a required input");
}));
- test("Test page limit is honoured", (async() => {
- await testUtils.goToTestCase("v1.6/Carousel.HostConfig");
+ test("Input.ChoiceSet: Test multiselect required validation", (async() => {
+ await utils.goToTestCase("v1.3/Input.ChoiceSet.ErrorMessage");
+
+ const multiselectChoiceSet = await ACInputChoiceSet.getInputWithId("requiredMultiselectId", true, true);
+ Assert.strictEqual(await multiselectChoiceSet.getLabel(), "Required Input.ChoiceSet label (expanded, multiselect) *");
+ Assert.strictEqual(await multiselectChoiceSet.isRequired(), true);
+ Assert.strictEqual(await multiselectChoiceSet.getErrorMessage(), undefined);
- await testUtils.assertElementWithIdDoesNotExist("page10");
+ await ACAction.clickOnActionWithTitle("OK");
+
+ Assert.strictEqual(await multiselectChoiceSet.validationFailed(), true);
+ Assert.strictEqual(await multiselectChoiceSet.getErrorMessage(), "This is a required input");
}));
- test("Unsupported elements are not rendered", (async() => {
- await testUtils.goToTestCase("v1.6/Carousel.ForbiddenElements");
+ test("Test interaction with Input.Time", (async() => {
+ await utils.goToTestCase("v1.3/Input.Time.ErrorMessage");
+
+ let timeInput1 = await ACInputTime.getInputWithId("input1");
+ await timeInput1.setTime(1, 9);
+
+ let timeInput4 = await ACInputTime.getInputWithId("input4");
+ await timeInput4.setTime(14, 30);
+
+ await ACAction.clickOnActionWithTitle("OK");
- await testUtils.assertElementWithIdDoesNotExist("id1");
- await testUtils.assertElementWithIdDoesNotExist("id2");
- await testUtils.assertElementWithIdDoesNotExist("id3");
- await testUtils.assertElementWithIdDoesNotExist("id4");
- await testUtils.assertElementWithIdDoesNotExist("id5");
- await testUtils.assertElementWithIdDoesNotExist("id6");
- await testUtils.assertElementWithIdDoesNotExist("id7");
+ Assert.strictEqual(await utils.getInputFor("input1"), "01:09");
+ Assert.strictEqual(await utils.getInputFor("input4"), "14:30");
}));
- test("Verify left and right buttons in carousel work", (async() => {
- await testUtils.goToTestCase("v1.6/Carousel.ScenarioCards");
+ test("Test interaction with Input.Toggle", (async() => {
+ await utils.goToTestCase("v1.3/Input.Toggle.ErrorMessage");
- let firstCarouselPageVisibility: string = await testUtils.getCssPropertyValueForElementWithId("firstCarouselPage", "visibility");
- Assert.strictEqual("visible", firstCarouselPageVisibility);
+ let toggleInput = await ACInputToggle.getInputWithId("input2");
+ await toggleInput.set();
- const rightArrow = await testUtils.driver.findElement(Webdriver.By.className("ac-carousel-right"));
- await rightArrow.click();
+ await ACAction.clickOnActionWithTitle("OK");
- await testUtils.delay(delayForCarouselArrows);
+ Assert.strictEqual(await utils.getInputFor("input2"), "true");
+ }));
+
+ test("Action.ShowCard: Test hidden card is shown", (async() => {
+ await utils.goToTestCase("v1.0/Action.ShowCard");
+
+ await ACAction.clickOnActionWithTitle("Action.ShowCard");
- const secondCarouselPageVisibility: string =
- await testUtils.getCssPropertyValueForElementWithId("theSecondCarouselPage", "visibility");
- Assert.strictEqual("visible", secondCarouselPageVisibility);
+ const neatAction: ACAction = await ACAction.getActionWithTitle("Neat!");
+ const neatActionIsVisible = async (params: any) => {
+ let elements = params as ACElement[];
+ return (await elements[0].elementIsVisible()) || (await elements[0].elementIsCssVisible());
+ };
- const leftArrow = await testUtils.driver.findElement(Webdriver.By.className("ac-carousel-left"));
- await leftArrow.click();
+ await WaitUtils.waitUntilPredicateIsTrue([neatAction], neatActionIsVisible);
- await testUtils.delay(delayForCarouselArrows);
+ await neatAction.click();
- firstCarouselPageVisibility = await testUtils.getCssPropertyValueForElementWithId("firstCarouselPage", "visibility");
- Assert.strictEqual("visible", firstCarouselPageVisibility);
+ Assert.strictEqual(await utils.getInputFor("neat"), "true");
}));
- test("Unsupported actions are not rendered", (async() => {
- await testUtils.goToTestCase("v1.6/Carousel.ForbiddenActions");
+ test("Image: Test select action can be clicked", (async() => {
+ await utils.goToTestCase("v1.0/Image.SelectAction");
- const showCardAction = await testUtils.tryGetActionWithTitle("Action.ShowCard");
- Assert.strictEqual(null, showCardAction);
+ let image = await ACImage.getImage("cool link");
+ await image.click();
+
+ Assert.strictEqual(await utils.getUrlInRetrievedInputs(), "https://www.youtube.com/watch?v=dQw4w9WgXcQ");
+ }));
- const toggleVisibilityAction = await testUtils.tryGetActionWithTitle("Action.ToggleVisibility");
- Assert.strictEqual(null, toggleVisibilityAction);
+ test("Column: Test select action can be clicked", (async() => {
+ await utils.goToTestCase("v1.0/Column.SelectAction");
+
+ let firstColumn = await ACColumn.getContainerWithAction("cool link");
+ await firstColumn.click();
+
+ Assert.strictEqual(await utils.getUrlInRetrievedInputs(), "https://www.youtube.com/watch?v=dQw4w9WgXcQ");
+ }));
+
+ test("ColumnSet: Test select action can be clicked", (async() => {
+ await utils.goToTestCase("v1.0/ColumnSet.SelectAction");
+
+ let secondColumnSet = await ACColumnSet.getContainerWithAction("Remodel your kitchen with our new cabinet styles!");
+ await secondColumnSet.click();
+
+ Assert.strictEqual(await utils.getUrlInRetrievedInputs(), "https://www.AdaptiveCards.io");
+ }));
+
+ test("Container: Test select action can be clicked", (async() => {
+ await utils.goToTestCase("v1.0/Container.SelectAction");
+
+ let submitContainer = await ACContainer.getContainerWithAction("Submit action");
+ await submitContainer.click();
+
+ Assert.strictEqual(await utils.getInputFor("info"), "My submit action data");
+
+ let emphasisContainer = await ACContainer.getContainerWithAction("Go to a different url");
+ await emphasisContainer.click();
+
+ Assert.strictEqual(await utils.getUrlInRetrievedInputs(), "https://msn.com");
+ }));
+
+ test("Carousel: Test actions are rendered and active", (async() => {
+ await utils.goToTestCase("v1.6/Carousel.HostConfig");
+
+ await ACAction.clickOnActionWithTitle("See more");
+
+ Assert.strictEqual(await utils.getUrlInRetrievedInputs(), "https://adaptivecards.io");
+ }));
+
+ test("Carousel: Test page limit is honoured", (async() => {
+ await utils.goToTestCase("v1.6/Carousel.HostConfig");
+
+ await utils.assertElementWithIdDoesNotExist("page10");
+ }));
+
+ test("Carousel: Unsupported elements are not rendered", (async() => {
+ await utils.goToTestCase("v1.6/Carousel.ForbiddenElements");
+
+ await utils.assertElementWithIdDoesNotExist("id1");
+ await utils.assertElementWithIdDoesNotExist("id2");
+ await utils.assertElementWithIdDoesNotExist("id3");
+ await utils.assertElementWithIdDoesNotExist("id4");
+ await utils.assertElementWithIdDoesNotExist("id5");
+ await utils.assertElementWithIdDoesNotExist("id6");
+ await utils.assertElementWithIdDoesNotExist("id7");
+ }));
+
+ test("Carousel: Verify left and right buttons work", (async() => {
+ await utils.goToTestCase("v1.6/Carousel.ScenarioCards");
+
+ let firstPageIsVisible = await ACCarousel.isPageVisible("firstCarouselPage");
+ Assert.strictEqual(firstPageIsVisible, true);
+
+ await ACCarousel.clickOnRightArrow();
+
+ await WaitUtils.waitUntilElementIsCssVisible("theSecondCarouselPage", delayForCarouselTimer);
+
+ let secondPageIsVisible = await ACCarousel.isPageVisible("theSecondCarouselPage");
+ Assert.strictEqual(secondPageIsVisible, true);
+
+ await ACCarousel.waitForAnimationsToEnd();
+ await ACCarousel.clickOnLeftArrow();
+
+ await WaitUtils.waitUntilElementIsCssVisible("firstCarouselPage");
+
+ firstPageIsVisible = await ACCarousel.isPageVisible("firstCarouselPage");
+ Assert.strictEqual(firstPageIsVisible, true);
+ }), timeOutValueForSuddenJumpTest);
+
+ test("Carousel: Unsupported actions are not rendered", (async() => {
+ await utils.goToTestCase("v1.6/Carousel.ForbiddenActions");
+
+ let showCardAction = await ACAction.getActionWithTitle("Action.ShowCard");
+ Assert.strictEqual(showCardAction.elementWasFound(), false);
+
+ let toggleVisibilityAction = await ACAction.getActionWithTitle("Action.ToggleVisibility");
+ Assert.strictEqual(toggleVisibilityAction.elementWasFound(), false);
}));
// Giving this test 7 seconds to run
- test("Test autoplay is disabled", (async() => {
- await testUtils.goToTestCase("v1.6/Carousel.ScenarioCards");
+ test("Carousel: Test autoplay is disabled", (async() => {
+ await utils.goToTestCase("v1.6/Carousel.ScenarioCards");
- let firstCarouselPageVisibility: string = await testUtils.getCssPropertyValueForElementWithId("firstCarouselPage", "visibility");
- Assert.strictEqual("visible", firstCarouselPageVisibility);
+ let firstPageIsVisible = await ACCarousel.isPageVisible("firstCarouselPage");
+ Assert.strictEqual(firstPageIsVisible, true);
// Await for 5 seconds and verify no change happened
- await testUtils.delay(5000);
+ await WaitUtils.waitFor(5000);
- firstCarouselPageVisibility = await testUtils.getCssPropertyValueForElementWithId("firstCarouselPage", "visibility");
- Assert.strictEqual("visible", firstCarouselPageVisibility);
+ firstPageIsVisible = await ACCarousel.isPageVisible("firstCarouselPage");
+ Assert.strictEqual(firstPageIsVisible, true);
}), 7000);
// Giving this test 9 seconds to run
- test("Test autoplay is applied", (async() => {
- await testUtils.goToTestCase("v1.6/Carousel.ScenarioCards.Timer");
-
- let firstCarouselPageVisibility: string = await testUtils.getCssPropertyValueForElementWithId("firstCarouselPage", "visibility");
+ test("Carousel: Test autoplay is applied", (async() => {
+ await utils.goToTestCase("v1.6/Carousel.ScenarioCards.Timer");
- Assert.strictEqual("visible", firstCarouselPageVisibility);
+ let firstPageIsVisible = await ACCarousel.isPageVisible("firstCarouselPage");
+ Assert.strictEqual(firstPageIsVisible, true);
// Await for 5 seconds and verify the first page is now hidden
- await testUtils.delay(7000);
+ await WaitUtils.waitUntilElementIsNotVisible("firstCarouselPage");
- firstCarouselPageVisibility = await testUtils.getCssPropertyValueForElementWithId("firstCarouselPage", "visibility");
- Assert.strictEqual("hidden", firstCarouselPageVisibility);
+ firstPageIsVisible = await ACCarousel.isPageVisible("firstCarouselPage");
+ Assert.strictEqual(firstPageIsVisible, false);
- const secondCarouselPageVisibility: string =
- await testUtils.getCssPropertyValueForElementWithId("theSecondCarouselPage", "visibility");
- Assert.strictEqual("visible", secondCarouselPageVisibility);
+ let secondPageIsVisible = await ACCarousel.isPageVisible("theSecondCarouselPage");
+ Assert.strictEqual(secondPageIsVisible, true);
}), timeOutValueForCarousel);
- test("Test click on navigation does not cause sudden jump", (async() => {
- await testUtils.goToTestCase("v1.6/Carousel");
+ test("Carousel: Test click on navigation does not cause sudden jump", (async() => {
+ await utils.goToTestCase("v1.6/Carousel");
- let firstCarouselPageVisibility: string = await testUtils.getCssPropertyValueForElementWithId("firstCarouselPage", "visibility");
-
- Assert.strictEqual("visible", firstCarouselPageVisibility);
+ let firstPageIsVisible = await ACCarousel.isPageVisible("firstCarouselPage");
+ Assert.strictEqual(firstPageIsVisible, true);
// wait for 2 pages to turn
- await testUtils.delay(delayForCarouselTimer * 2);
-
- firstCarouselPageVisibility = await testUtils.getCssPropertyValueForElementWithId("firstCarouselPage", "visibility");
- Assert.strictEqual("hidden", firstCarouselPageVisibility);
+ await WaitUtils.waitUntilElementIsCssVisible("last-carousel-page", delayForCarouselTimer * 2);
- // Due to how the swiper library is made, the first and last pages are duplicated, if we were to use the regular
- // getElementWithId method we would retrieve the duplicated slide, so we have to get the second element with that id
+ firstPageIsVisible = await ACCarousel.isPageVisible("firstCarouselPage");
+ Assert.strictEqual(firstPageIsVisible, false);
- const lastPages = await testUtils.getElementsWithId("last-carousel-page");
- Assert.strictEqual(lastPages.length, 2);
-
- const thirdCarouselPageVisibility: string =
- await testUtils.getCssPropertyValueForElement(lastPages[1], "visibility");
- Assert.strictEqual("visible", thirdCarouselPageVisibility);
+ let lastPageIsVisible = await ACCarousel.isPageVisible("last-carousel-page");
+ Assert.strictEqual(lastPageIsVisible, true);
// cause the page to go the 2nd page
- const leftArrow = await testUtils.driver.findElement(Webdriver.By.className("ac-carousel-left"));
- await leftArrow.click();
- await testUtils.delay(delayForCarouselArrows);
+ await ACCarousel.waitForAnimationsToEnd();
+ await ACCarousel.clickOnLeftArrow();
+
+ await WaitUtils.waitUntilElementIsCssVisible("theSecondCarouselPage");
// make sure firstCarouselPage is hidden
- firstCarouselPageVisibility = await testUtils.getCssPropertyValueForElementWithId("firstCarouselPage", "visibility");
- Assert.strictEqual("hidden", firstCarouselPageVisibility);
+ firstPageIsVisible = await ACCarousel.isPageVisible("firstCarouselPage");
+ Assert.strictEqual(firstPageIsVisible, false);
}), timeOutValueForSuddenJumpTest);
- test("Test rtl on carousel", (async() => {
- await testUtils.goToTestCase("v1.6/Carousel.rtl");
+ test("Carousel: Test rtl", (async() => {
+ await utils.goToTestCase("v1.6/Carousel.rtl");
for (const page of [["firstCarouselPage", "rtl"], ["secondCarouselPage", "ltr"], ["thirdCarouselPage", "rtl"]]){
- const pageElement = await testUtils.getElementWithId(page[0]);
- const pageContainer = pageElement.findElement(Webdriver.By.xpath("./*"));
- const pageDirection: string = await pageContainer.getAttribute("dir");
+ const pageDirection = await ACCarousel.getPageDirection(page[0])
Assert.strictEqual(pageDirection, page[1]);
}
diff --git a/source/nodejs/tests/ui-tests/src/wait-utils.ts b/source/nodejs/tests/ui-tests/src/wait-utils.ts
new file mode 100644
index 0000000000..0ed111a0dc
--- /dev/null
+++ b/source/nodejs/tests/ui-tests/src/wait-utils.ts
@@ -0,0 +1,68 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+import { until, WebElement } from "selenium-webdriver";
+import { TestUtils } from "./test-utils";
+import { ACElement } from "./card-element-utils";
+
+export class WaitUntil {
+
+ static async customWait(timeoutMs: number, successFunction: Function, params: any) : Promise
+ {
+ let milisPassed: number = 0;
+
+ while (milisPassed < timeoutMs) {
+ const successFunctionResult = await successFunction(params);
+ if (successFunctionResult === true)
+ {
+ break;
+ }
+ milisPassed += 100;
+ await WaitUtils.waitFor(100);
+ }
+ }
+}
+
+export class WaitUtils {
+
+ static timeoutForCardRendering: number = 10000;
+
+ // Await for x miliseconds
+ static async waitFor(miliseconds: number): Promise {
+ return new Promise(res => setTimeout(res, miliseconds));
+ }
+
+ static async waitUntilPredicateIsTrue(elements: ACElement[], successPredicate: Function, timeoutInMs?: number) {
+ const timeOut = (timeoutInMs !== undefined) ? timeoutInMs : WaitUtils.timeoutForCardRendering;
+ await WaitUntil.customWait(timeOut, successPredicate, elements);
+ }
+
+ static async waitUntilElementIsVisible(id: string) {
+ const elementToWaitFor: WebElement = await TestUtils.getInstance().getElementWithId(id);
+ await TestUtils.getInstance().driver.wait(until.elementIsVisible(elementToWaitFor), WaitUtils.timeoutForCardRendering);
+ }
+
+ static async waitUntilElementIsNotVisible(id: string) {
+ const elementToWaitFor: WebElement = await TestUtils.getInstance().getElementWithId(id);
+ await TestUtils.getInstance().driver.wait(until.elementIsNotVisible(elementToWaitFor), WaitUtils.timeoutForCardRendering);
+ }
+
+ static async waitUntilElementIsCssVisible(id: string, timeoutInMs?: number) {
+ const elementsToWaitFor: WebElement[] = await TestUtils.getInstance().getElementsWithId(id);
+ const timeOut = timeoutInMs !== undefined ? timeoutInMs : WaitUtils.timeoutForCardRendering;
+
+ let successFunction = async (params: any) => {
+ let elementIsVisible: boolean = false;
+
+ for (let element of elementsToWaitFor)
+ {
+ let visibility = await TestUtils.getInstance().getCssPropertyValueForElement(element, "visibility");
+ elementIsVisible = elementIsVisible || (visibility === "visible");
+ }
+
+ return elementIsVisible;
+ };
+
+ await WaitUntil.customWait(timeOut, successFunction, elementsToWaitFor);
+ }
+}
\ No newline at end of file