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