Skip to content

test: add ui test project #92

New issue

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

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

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8" ?>
<RunSettings>
<Playwright>
<BrowserName>chromium</BrowserName>
<LaunchOptions>
<SlowMo>0</SlowMo>
<Headless>false</Headless>
</LaunchOptions>
</Playwright>
<RunConfiguration>
<EnvironmentVariables>
<UserA_Username></UserA_Username>
<UserA_UserPassword></UserA_UserPassword>
<UserA_UserSeed></UserA_UserSeed>
<UserB_Username></UserB_Username>
<UserB_UserPassword></UserB_UserPassword>
<UserB_UserSeed></UserB_UserSeed>
<UserC_Username></UserC_Username>
<UserC_UserPassword></UserC_UserPassword>
<UserC_UserSeed></UserC_UserSeed>
</EnvironmentVariables>
</RunConfiguration>
</RunSettings>
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace RecordingBot.UiTests.PageObjects.Call.Page
{
public class CallPage
{
public const string CallOptionsBtn = "[data-tid='dropdown-calling-toggle-more-options-btn']";
public const string VideoCallBtn = "[data-tid='chat-call-video-button']";
public const string AudioCallBtn = "[data-tid='chat-call-audio-button']";
public const string HangUpBtn = "#hangup-button";

public const string CallToastCallingActions = "[data-testid='calling-actions']";
public const string CallToastAcceptVideo = CallToastCallingActions + " button:nth-child(1)";
public const string CallToastAcceptAudio = CallToastCallingActions + " button:nth-child(2)";
public const string CallToastHangUp = CallToastCallingActions + "button:nth-child(3)";

public const string CallComplianceToast = "[data-tid='ufd_ComplianceRecordingStartedByCurrentUser']";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace RecordingBot.UiTests.PageObjects.Login.Page
{
public class LoginPage
{
public const string UsernameInput = "//*[@id=\"i0116\"]";
public const string PasswordInput = "//*[@id=\"i0118\"]";
public const string TokenInput = "//*[@id=\"idTxtBx_SAOTCC_OTC\"]";
public const string TokenSubmitBtn = "//*[@id=\"idSubmit_SAOTCC_Continue\"]";
public const string SubmitBtn = "//*[@id=\"idSIButton9\"]";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using Microsoft.Playwright;
using RecordingBot.UiTests.Shared.Models;
using RecordingBot.UiTests.PageObjects.Login.Page;
using OtpNet;

namespace RecordingBot.UiTests.PageObjects.Login.Steps
{
public class LoginSteps
{
public static async Task LoginPerson(IPage page, Person person)
{
await page.GotoAsync("https://teams.microsoft.com/");

if (!string.IsNullOrWhiteSpace(person.Username))
{
var usernameInput = page.Locator(LoginPage.UsernameInput);
await usernameInput.ClickAsync();
await usernameInput.FillAsync(person.Username);
await page.Locator(LoginPage.SubmitBtn).ClickAsync();
}

if (!string.IsNullOrWhiteSpace(person.Password))
{
var passwordInput = page.Locator(LoginPage.PasswordInput);
await passwordInput.ClickAsync();
await passwordInput.FillAsync(person.Password);
await page.Locator(LoginPage.SubmitBtn).ClickAsync();
}

if (!string.IsNullOrWhiteSpace(person.Seed))
{
var tokenInput = page.Locator(LoginPage.TokenInput);

var totp = new Totp(Base32Encoding.ToBytes(person.Seed), totpSize: 6 );
string otpCode = totp.ComputeTotp();

if (!string.IsNullOrWhiteSpace(otpCode))
{
await tokenInput.ClickAsync();
await tokenInput.FillAsync(otpCode);
await page.Locator(LoginPage.TokenSubmitBtn).ClickAsync();
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace RecordingBot.UiTests.PageObjects.Teams.Page
{
public class CalendarPage
{
public const string StartMeetingBtn = "//*[@id=\"app\"]/div/div/div/div[5]/div/div/div[3]/button[2]";
public const string MeetNowFlyoutBtn = "[data-tid='meet_now_calendar_flyout_start_meeting_button']";
public const string JoinBtn = "[data-tid='prejoin-join-button']";
public const string InviteDismissBtn = "[data-tid='share_meeting_invite_dialog_dismiss_button']";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace RecordingBot.UiTests.PageObjects.Teams.Page
{
public class SearchPage
{
public const string TabBarPeople = "[data-tid='people-tab']";
public const string ContentAreaPerson = "[data-tid='app-layout-area--main'] ul > li.ms-FocusZone:first-child";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace RecordingBot.UiTests.PageObjects.Teams.Page
{
public class TeamsPage
{
public const string Search = "[data-tid='AUTOSUGGEST_INPUT']";
public const string Calendar = "//*[@id=\"ef56c0de-36fc-4ef8-b417-3d82ba9d073c\"]";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Microsoft.Playwright;
using RecordingBot.UiTests.PageObjects.Teams.Page;

namespace RecordingBot.UiTests.PageObjects.Teams.Steps
{
public class CalendarSteps
{
public static async Task CreateAudioCall(IPage page)
{
await page.Locator(TeamsPage.Calendar).ClickAsync();
await page.Locator(CalendarPage.StartMeetingBtn).ClickAsync();
await page.Locator(CalendarPage.MeetNowFlyoutBtn).ClickAsync();
await page.Locator(CalendarPage.JoinBtn).ClickAsync();
await page.Locator(CalendarPage.InviteDismissBtn).ClickAsync();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using Microsoft.Playwright;
using RecordingBot.UiTests.Shared.Models;
using RecordingBot.UiTests.PageObjects.Teams.Page;

namespace RecordingBot.UiTests.PageObjects.Teams.Steps
{
public class TeamsSteps
{
public static async Task SearchPersonAndOpenChat(IPage page, Person person)
{
var searchInput = page.Locator(TeamsPage.Search);

if (!string.IsNullOrWhiteSpace(person.Username))
{
await searchInput.ClickAsync();
await searchInput.FillAsync(person.Username);
await searchInput.PressAsync("Enter");
}

await page.ClickAsync(SearchPage.TabBarPeople);
await page.ClickAsync(SearchPage.ContentAreaPerson);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Introduction
This project aims to provide automated end to end tests for a compliance recording bot between two or more users participating in a call.
It's purpose is to ensure that a compliance recording bot joins a call and the call participants receives a visual notification that a records has started.

## End-To-End Framework
As a end-to-end testing framework playwright.net is used. (Read more [Playwright](https://playwright.dev/dotnet/))

## Framework
As a framework to write and execute tests, nUnit is used. (Read more [NUnit](https://nunit.org/))

## Otp
Otp.Net is used to generate one time passwords and optain a code for login. (Read more [Otp.Net](https://www.nuget.org/packages/Otp.NET))


# Getting Started
To start using this project, you need to provide at least two users that are registered for using teams.
Therefore you need to provide the following information in the .runsettings file:
```json
{
"RunConfiguration": {
"EnvironmentVariables": {
"UserA_Username": "",
"UserA_UserPassword": "",
"UserA_UserSeed": "",
"UserB_Username": "",
"UserB_UserPassword": "",
"UserB_UserSeed": "",
"UserC_Username": "",
"UserC_UserPassword": "",
"UserC_UserSeed": ""
}
}
}
```

Furthermore you should adjust the launch options to your needs in the .runsettings file.
Locally its a good idea execute the tests not in headless mode to see the test running, but if you consider to run the tests in a pipeline you should keep it headless:
```json
{
"RunConfiguration": {
"EnvironmentVariables": {
"LaunchOptions": {
"headless": false,
"slowMo": 0
}
}
}
}
```

# Contribute
TODO:
Furthermore the login and some other locators uses xPath to find the elements. This should be changed to use the id or data-tid once it is provided.
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="Microsoft.Playwright.NUnit" Version="1.27.1" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit.Analyzers" Version="3.9.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="Otp.NET" Version="1.4.0" />
</ItemGroup>

<ItemGroup>
<Using Include="Microsoft.Playwright.NUnit" />
<Using Include="NUnit.Framework" />
<Using Include="System.Text.RegularExpressions" />
<Using Include="System.Threading.Tasks" />
</ItemGroup>

<ItemGroup>
<Folder Include="Properties\" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace RecordingBot.UiTests.Shared.Models
{
public abstract class Person
{
/// <summary>
/// Username that is used to login
/// </summary>
/// <remarks>Example: Max.Mustermann@musterpage.com</remarks>
public abstract string Username { get; set; }
/// <summary>
/// Password that is used to login
/// </summary>
/// <remarks>Example: YourPassword</remarks>
public abstract string Password { get; set; }

/// <summary>
/// Seed that is used to calculate token for login when 2fa is enabled
/// </summary>
/// <remarks>Example: YourSeed</remarks>
public abstract string Seed { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using RecordingBot.UiTests.Shared.Models;
using System.Reflection.Metadata.Ecma335;

namespace RecordingBot.UiTests.Shared.Users
{
public class UserA : Person
{
public override string Username { get; set; } = Environment.GetEnvironmentVariable("UserA_Username") ?? throw new Exception("Please provide environment variable UserA_Username");
public override string Password { get; set; } = Environment.GetEnvironmentVariable("UserA_UserPassword") ?? throw new Exception("Please provide environment variable UserA_UserPassword");
public override string Seed { get; set; } = Environment.GetEnvironmentVariable("UserA_UserSeed") ?? throw new Exception("Please provide environment variable UserA_UserSeed");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using RecordingBot.UiTests.Shared.Models;

namespace RecordingBot.UiTests.Shared.Users
{
public class UserB : Person
{
public override string Username { get; set; } = Environment.GetEnvironmentVariable("UserB_Username") ?? throw new Exception("Please provide environment variable UserB_Username");
public override string Password { get; set; } = Environment.GetEnvironmentVariable("UserB_UserPassword") ?? throw new Exception("Please provide environment variable UserB_UserPassword");
public override string Seed { get; set; } = Environment.GetEnvironmentVariable("UserB_UserSeed") ?? throw new Exception("Please provide environment variable UserB_UserSeed");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using RecordingBot.UiTests.Shared.Models;

namespace RecordingBot.UiTests.Shared.Users
{
public class UserC : Person
{
public override string Username { get; set; } = Environment.GetEnvironmentVariable("UserC_Username") ?? throw new Exception("Please provide environment variable UserC_Username");
public override string Password { get; set; } = Environment.GetEnvironmentVariable("UserC_UserPassword") ?? throw new Exception("Please provide environment variable UserC_UserPassword");
public override string Seed { get; set; } = Environment.GetEnvironmentVariable("UserC_UserSeed") ?? throw new Exception("Please provide environment variable UserC_UserSeed");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using Microsoft.Playwright;
using RecordingBot.UiTests.PageObjects.Call.Page;
using RecordingBot.UiTests.PageObjects.Login.Steps;
using RecordingBot.UiTests.PageObjects.Teams.Steps;
using RecordingBot.UiTests.Shared.Users;

namespace RecordingBot.UiTests.Tests.Call;

[TestFixture]
[Category("CallComplianceBotOnline")]
[Description("Automated E2E-Tests for a call with joined compliance bot")]
public class CallComplianceBotOnlineTests : PageTest
{
public override BrowserNewContextOptions ContextOptions()
{
return new BrowserNewContextOptions
{
Permissions = ["microphone", "camera"]
};
}

[Test]
[Description("PersonA creates a meeting. Compliance bot starts recording call")]
public async Task AudioCall_Should_DisplayRecordingComplianceToast_When_PersonACreatesMeeting()
{
var user = new UserA();
var page = Page;

await LoginSteps.LoginPerson(page, user);
await CalendarSteps.CreateAudioCall(page);

await VerifyRecordingToast(page);
await HangUpCall(page);
}

private async Task VerifyRecordingToast(IPage page)
{
await Expect(page.Locator(CallPage.CallComplianceToast)).ToBeVisibleAsync();
}

private static async Task HangUpCall(IPage page)
{
await page.Locator(CallPage.HangUpBtn).ClickAsync();
}
}
Loading
Loading