Selenium Tests for Checkout + other store operations (#1015)

This commit is contained in:
Andrew Camilleri 2019-09-10 10:03:24 +02:00 committed by Nicolas Dorier
parent b3298589c3
commit e6cfb6e851
10 changed files with 322 additions and 73 deletions

View file

@ -0,0 +1,176 @@
using System;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using BTCPayServer.Lightning;
using BTCPayServer.Tests.Logging;
using BTCPayServer.Views.Stores;
using NBitpayClient;
using OpenQA.Selenium;
using OpenQA.Selenium.Interactions;
using OpenQA.Selenium.Support.UI;
using Xunit;
using Xunit.Abstractions;
namespace BTCPayServer.Tests
{
[Trait("Selenium", "Selenium")]
public class CheckoutUITests
{
public CheckoutUITests(ITestOutputHelper helper)
{
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
Logs.LogProvider = new XUnitLogProvider(helper);
}
[Fact]
public void CanCreateInvoice()
{
using (var s = SeleniumTester.Create())
{
s.Start();
s.RegisterNewUser();
var store = s.CreateNewStore().storeName;
s.AddDerivationScheme();
s.CreateInvoice(store);
s.Driver.FindElement(By.ClassName("invoice-details-link")).Click();
s.Driver.AssertNoError();
s.Driver.Navigate().Back();
s.Driver.FindElement(By.ClassName("invoice-checkout-link")).Click();
Assert.NotEmpty(s.Driver.FindElements(By.Id("checkoutCtrl")));
s.Driver.Quit();
}
}
[Fact]
public async Task CanHandleRefundEmailForm()
{
using (var s = SeleniumTester.Create())
{
s.Start();
s.RegisterNewUser();
var store = s.CreateNewStore();
s.AddDerivationScheme("BTC");
var emailAlreadyThereInvoiceId =s.CreateInvoice(store.storeName, 100, "USD", "a@g.com");
s.GoToInvoiceCheckout(emailAlreadyThereInvoiceId);
s.Driver.AssertElementNotFound(By.Id("emailAddressFormInput"));
s.GoToHome();
var invoiceId = s.CreateInvoice(store.storeName);
s.GoToInvoiceCheckout(invoiceId);
Assert.True(s.Driver.FindElement(By.Id("emailAddressFormInput")).Displayed);
s.Driver.FindElement(By.Id("emailAddressFormInput")).SendKeys("xxx");
s.Driver.FindElement(By.Id("emailAddressForm")).FindElement(By.CssSelector("button.action-button"))
.Click();
Assert.True(s.Driver.FindElement(By.Id("emailAddressFormInput")).Displayed);
s.Driver.FindElement(By.Id("emailAddressFormInput")).SendKeys("@g.com");
s.Driver.FindElement(By.Id("emailAddressForm")).FindElement(By.CssSelector("button.action-button"))
.Click();
await Task.Delay(1000);
s.Driver.AssertElementNotFound(By.Id("emailAddressFormInput"));
s.Driver.Navigate().Refresh();
s.Driver.AssertElementNotFound(By.Id("emailAddressFormInput"));
}
}
[Fact]
public async Task CanUseLanguageDropdown()
{
using (var s = SeleniumTester.Create())
{
s.Start();
s.RegisterNewUser();
var store = s.CreateNewStore();
s.AddDerivationScheme("BTC");
var invoiceId = s.CreateInvoice(store.storeName);
s.GoToInvoiceCheckout(invoiceId);
Assert.True(s.Driver.FindElement(By.Id("DefaultLang")).FindElements(By.TagName("option")).Count > 1);
var payWithTextEnglish = s.Driver.FindElement(By.Id("pay-with-text")).Text;
var prettyDropdown = s.Driver.FindElement(By.Id("prettydropdown-DefaultLang"));
prettyDropdown.Click();
await Task.Delay(200);
prettyDropdown.FindElement(By.CssSelector("[data-value=\"da-DK\"]")).Click();
await Task.Delay(1000);
Assert.NotEqual(payWithTextEnglish, s.Driver.FindElement(By.Id("pay-with-text")).Text);
s.Driver.Navigate().GoToUrl(s.Driver.Url + "?lang=da-DK");
Assert.NotEqual(payWithTextEnglish, s.Driver.FindElement(By.Id("pay-with-text")).Text);
s.Driver.Quit();
}
}
[Fact]
public void CanUsePaymentMethodDropdown()
{
using (var s = SeleniumTester.Create())
{
s.Start();
s.RegisterNewUser();
var store = s.CreateNewStore();
s.AddDerivationScheme("BTC");
//check that there is no dropdown since only one payment method is set
var invoiceId = s.CreateInvoice(store.storeName, 10, "USD", "a@g.com");
s.GoToInvoiceCheckout(invoiceId);
s.Driver.FindElement(By.ClassName("payment__currencies_noborder"));
s.GoToHome();
s.GoToStore(store.storeId);
s.AddDerivationScheme("LTC");
s.AddLightningNode("BTC",LightningConnectionType.CLightning);
//there should be three now
invoiceId = s.CreateInvoice(store.storeName, 10, "USD", "a@g.com");
s.GoToInvoiceCheckout(invoiceId);
var currencyDropdownButton = s.Driver.FindElement(By.ClassName("payment__currencies"));
Assert.Contains("BTC", currencyDropdownButton.Text);
currencyDropdownButton.Click();
var elements = s.Driver.FindElement(By.ClassName("vex-content"))
.FindElements(By.ClassName("vexmenuitem"));
Assert.Equal(3, elements.Count);
elements.Single(element => element.Text.Contains("LTC")).Click();
currencyDropdownButton = s.Driver.FindElement(By.ClassName("payment__currencies"));
Assert.Contains("LTC", currencyDropdownButton.Text);
elements = s.Driver.FindElement(By.ClassName("vex-content"))
.FindElements(By.ClassName("vexmenuitem"));
elements.Single(element => element.Text.Contains("Lightning")).Click();
Assert.Contains("Lightning", currencyDropdownButton.Text);
s.Driver.Quit();
}
}
[Fact]
public void CanUseLightningSatsFeature()
{
//uncomment after https://github.com/btcpayserver/btcpayserver/pull/1014
// using (var s = SeleniumTester.Create())
// {
// s.Start();
// s.RegisterNewUser();
// var store = s.CreateNewStore();
// s.AddInternalLightningNode("BTC");
// s.GoToStore(store.storeId, StoreNavPages.Checkout);
// s.SetCheckbox(s, "LightningAmountInSatoshi", true);
// s.Driver.FindElement(By.Name("command")).Click();
// var invoiceId = s.CreateInvoice(store.storeName, 10, "USD", "a@g.com");
// s.GoToInvoiceCheckout(invoiceId);
// Assert.Contains("Sats", s.Driver.FindElement(By.ClassName("payment__currencies_noborder")).Text);
//
// }
}
}
}

View file

@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Tests.Logging;
using Microsoft.AspNetCore.Mvc;
@ -14,7 +12,6 @@ namespace BTCPayServer.Tests
public static void ScrollTo(this IWebDriver driver, By by)
{
var element = driver.FindElement(by);
((IJavaScriptExecutor)driver).ExecuteScript($"window.scrollBy({element.Location.X},{element.Location.Y});");
}
/// <summary>
/// Sometimes the chrome driver is fucked up and we need some magic to click on the element.
@ -70,5 +67,21 @@ namespace BTCPayServer.Tests
var vr = Assert.IsType<ViewResult>(result);
return Assert.IsType<T>(vr.Model);
}
public static IWebElement AssertElementNotFound(this IWebDriver driver, By by)
{
try
{
var webElement = driver.FindElement(by);
Assert.False(webElement.Displayed);
return webElement;
}
catch (NoSuchElementException)
{
}
return null;
}
}
}

View file

@ -2,6 +2,8 @@ using System;
using BTCPayServer;
using System.Linq;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Text;
using NBitcoin;
@ -9,8 +11,16 @@ using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using Xunit;
using System.IO;
using System.Net.Http;
using System.Reflection;
using BTCPayServer.Tests.Logging;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Lightning;
using BTCPayServer.Lightning.CLightning;
using BTCPayServer.Views.Stores;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Tests
{
@ -28,18 +38,29 @@ namespace BTCPayServer.Tests
};
}
public void Start()
{
Server.Start();
ChromeOptions options = new ChromeOptions();
options.AddArguments("headless"); // Comment to view browser
options.AddArguments("window-size=1200x600"); // Comment to view browser
var isDebug = !Server.PayTester.InContainer;
if (!isDebug)
{
options.AddArguments("headless"); // Comment to view browser
options.AddArguments("window-size=1200x1000"); // Comment to view browser
}
options.AddArgument("shm-size=2g");
if (Server.PayTester.InContainer)
{
options.AddArgument("no-sandbox");
}
Driver = new ChromeDriver(Server.PayTester.InContainer ? "/usr/bin" : Directory.GetCurrentDirectory(), options);
if (isDebug)
{
//when running locally, depending on your resolution, the website may go into mobile responsive mode and screw with navigation of tests
Driver.Manage().Window.Maximize();
}
Logs.Tester.LogInformation("Selenium: Using chrome driver");
Logs.Tester.LogInformation("Selenium: Browsing to " + Server.PayTester.ServerUri);
Logs.Tester.LogInformation($"Selenium: Resolution {Driver.Manage().Window.Size}");
@ -78,15 +99,39 @@ namespace BTCPayServer.Tests
return (usr, Driver.FindElement(By.Id("Id")).GetAttribute("value"));
}
public void AddDerivationScheme(string derivationScheme = "xpub661MyMwAqRbcGABgHMUXDzPzH1tU7eZaAaJQXhDXsSxsqyQzQeU6kznNfSuAyqAK9UaWSaZaMFdNiY5BCF4zBPAzSnwfUAwUhwttuAKwfRX-[legacy]")
public void AddDerivationScheme(string cryptoCode = "BTC", string derivationScheme = "xpub661MyMwAqRbcGABgHMUXDzPzH1tU7eZaAaJQXhDXsSxsqyQzQeU6kznNfSuAyqAK9UaWSaZaMFdNiY5BCF4zBPAzSnwfUAwUhwttuAKwfRX-[legacy]")
{
Driver.FindElement(By.Id("ModifyBTC")).ForceClick();
Driver.FindElement(By.Id($"Modify{cryptoCode}")).ForceClick();
Driver.FindElement(By.ClassName("store-derivation-scheme")).SendKeys(derivationScheme);
Driver.FindElement(By.Id("Continue")).ForceClick();
Driver.FindElement(By.Id("Confirm")).ForceClick();
Driver.FindElement(By.Id("Save")).ForceClick();
return;
}
public void AddLightningNode(string cryptoCode, LightningConnectionType connectionType)
{
string connectionString = null;
if (connectionType == LightningConnectionType.Charge)
connectionString = "type=charge;server=" + Server.MerchantCharge.Client.Uri.AbsoluteUri;
else if (connectionType == LightningConnectionType.CLightning)
connectionString = "type=clightning;server=" + ((CLightningClient)Server.MerchantLightningD).Address.AbsoluteUri;
else if (connectionType == LightningConnectionType.LndREST)
connectionString = $"type=lnd-rest;server={Server.MerchantLnd.Swagger.BaseUrl};allowinsecure=true";
else
throw new NotSupportedException(connectionType.ToString());
Driver.FindElement(By.Id($"Modify-Lightning{cryptoCode}")).ForceClick();
Driver.FindElement(By.Name($"ConnectionString")).SendKeys(connectionString);
Driver.FindElement(By.Id($"save")).ForceClick();
}
public void AddInternalLightningNode(string cryptoCode)
{
Driver.FindElement(By.Id($"Modify-Lightning{cryptoCode}")).ForceClick();
Driver.FindElement(By.Id($"internal-ln-node-setter")).ForceClick();
Driver.FindElement(By.Id($"save")).ForceClick();
}
public void ClickOnAllSideMenus()
{
@ -100,26 +145,7 @@ namespace BTCPayServer.Tests
}
}
public string CreateInvoice(string random, string refundEmail = "")
{
Driver.FindElement(By.Id("Invoices")).Click();
Driver.FindElement(By.Id("CreateNewInvoice")).Click();
Driver.FindElement(By.CssSelector("input#Amount.form-control")).SendKeys("100");
Driver.FindElement(By.Name("StoreId")).SendKeys("Deriv" + random + Keys.Enter);
Driver.FindElement(By.Id("Create")).Click();
var statusElement = Driver.FindElement(By.ClassName("alert-success"));
var id = statusElement.Text.Split(" ")[1];
if (!string.IsNullOrEmpty(refundEmail))
{
GoToInvoiceCheckout(id);
Driver.FindElement(By.Id("emailAddressFormInput")).SendKeys(refundEmail);
Driver.FindElement(By.Id("emailAddressForm")).FindElement(By.CssSelector("button.action-button"))
.Click();
}
return id;
}
public void Dispose()
{
@ -159,16 +185,21 @@ namespace BTCPayServer.Tests
}
public void GoToStore(string storeId)
public void GoToStore(string storeId, StoreNavPages storeNavPage = StoreNavPages.Index)
{
Driver.FindElement(By.Id("Stores")).Click();
Driver.FindElement(By.Id($"update-store-{storeId}")).Click();
if (storeNavPage != StoreNavPages.Index)
{
Driver.FindElement(By.Id(storeNavPage.ToString())).Click();
}
}
public void GoToInvoiceCheckout(string invoiceId)
{
Driver.FindElement(By.Id("Invoices")).Click();
Driver.FindElement(By.Id($"invoice-checkout-{invoiceId}")).Click();
CheckForJSErrors();
}
@ -184,5 +215,61 @@ namespace BTCPayServer.Tests
{
SetCheckbox(s.Driver.FindElement(By.Name(inputName)), value);
}
public void GoToInvoices()
{
Driver.FindElement(By.Id("Invoices")).Click();
}
public void GoToCreateInvoicePage()
{
GoToInvoices();
Driver.FindElement(By.Id("CreateNewInvoice")).Click();
}
public string CreateInvoice(string store, decimal amount = 100, string currency = "USD", string refundEmail = "")
{
GoToInvoices();
Driver.FindElement(By.Id("CreateNewInvoice")).Click();
Driver.FindElement(By.Id("Amount")).SendKeys(amount.ToString(CultureInfo.InvariantCulture));
var currencyEl = Driver.FindElement(By.Id("Currency"));
currencyEl.Clear();
currencyEl.SendKeys(currency);
Driver.FindElement(By.Id("BuyerEmail")).SendKeys(refundEmail);
Driver.FindElement(By.Name("StoreId")).SendKeys(store + Keys.Enter);
Driver.FindElement(By.Id("Create")).ForceClick();
Assert.True(Driver.PageSource.Contains("just created!"), "Unable to create Invoice");
var statusElement = Driver.FindElement(By.ClassName("alert-success"));
var id = statusElement.Text.Split(" ")[1];
return id;
}
private void CheckForJSErrors()
{
//wait for seleniun update: https://stackoverflow.com/questions/57520296/selenium-webdriver-3-141-0-driver-manage-logs-availablelogtypes-throwing-syste
// var errorStrings = new List<string>
// {
// "SyntaxError",
// "EvalError",
// "ReferenceError",
// "RangeError",
// "TypeError",
// "URIError"
// };
//
// var jsErrors = Driver.Manage().Logs.GetLog(LogType.Browser).Where(x => errorStrings.Any(e => x.Message.Contains(e)));
//
// if (jsErrors.Any())
// {
// Logs.Tester.LogInformation("JavaScript error(s):" + Environment.NewLine + jsErrors.Aggregate("", (s, entry) => s + entry.Message + Environment.NewLine));
// }
// Assert.Empty(jsErrors);
}
}
}

View file

@ -179,8 +179,8 @@ namespace BTCPayServer.Tests
Assert.Contains(store, s.Driver.PageSource);
var storeUrl = s.Driver.Url;
s.ClickOnAllSideMenus();
CreateInvoice(s, store);
s.GoToInvoices();
s.CreateInvoice(store);
s.Driver.FindElement(By.ClassName("invoice-details-link")).Click();
var invoiceUrl = s.Driver.Url;
@ -216,37 +216,8 @@ namespace BTCPayServer.Tests
}
}
[Fact]
public void CanCreateInvoice()
{
using (var s = SeleniumTester.Create())
{
s.Start();
s.RegisterNewUser();
var store = s.CreateNewStore().storeName;
s.AddDerivationScheme();
CreateInvoice(s, store);
s.Driver.FindElement(By.ClassName("invoice-details-link")).Click();
s.Driver.AssertNoError();
s.Driver.Navigate().Back();
s.Driver.FindElement(By.ClassName("invoice-checkout-link")).Click();
Assert.NotEmpty(s.Driver.FindElements(By.Id("checkoutCtrl")));
s.Driver.Quit();
}
}
static void CreateInvoice(SeleniumTester s, string store)
{
s.Driver.FindElement(By.Id("Invoices")).Click();
s.Driver.FindElement(By.Id("CreateNewInvoice")).Click();
s.Driver.FindElement(By.CssSelector("input#Amount.form-control")).SendKeys("100");
s.Driver.FindElement(By.Name("StoreId")).SendKeys(store + Keys.Enter);
s.Driver.FindElement(By.Id("Create")).Click();
Assert.True(s.Driver.PageSource.Contains("just created!"), "Unable to create Invoice");
}
[Fact]
public void CanCreateAppPoS()
{
@ -335,7 +306,7 @@ namespace BTCPayServer.Tests
// to sign the transaction
var mnemonic = "usage fever hen zero slide mammal silent heavy donate budget pulse say brain thank sausage brand craft about save attract muffin advance illegal cabbage";
var root = new Mnemonic(mnemonic).DeriveExtKey();
s.AddDerivationScheme("ypub6WWc2gWwHbdnAAyJDnR4SPL1phRh7REqrPBfZeizaQ1EmTshieRXJC3Z5YoU4wkcdKHEjQGkh6AYEzCQC1Kz3DNaWSwdc1pc8416hAjzqyD");
s.AddDerivationScheme("BTC", "ypub6WWc2gWwHbdnAAyJDnR4SPL1phRh7REqrPBfZeizaQ1EmTshieRXJC3Z5YoU4wkcdKHEjQGkh6AYEzCQC1Kz3DNaWSwdc1pc8416hAjzqyD");
var tx = s.Server.ExplorerNode.SendToAddress(BitcoinAddress.Create("bcrt1qmxg8fgnmkp354vhe78j6sr4ut64tyz2xyejel4", Network.RegTest), Money.Coins(3.0m));
s.Server.ExplorerNode.Generate(1);

View file

@ -118,6 +118,7 @@ namespace BTCPayServer.Controllers
Description = settings.Description,
NotificationEmail = settings.NotificationEmail,
NotificationUrl = settings.NotificationUrl,
SearchTerm = $"storeid:{app.StoreDataId}",
RedirectAutomatically = settings.RedirectAutomatically.HasValue? settings.RedirectAutomatically.Value? "true": "false" : ""
};
if (HttpContext?.Request != null)

View file

@ -38,7 +38,7 @@
<div class="order-details">
<div class="currency-selection">
<div class="single-item-order__left">
<div style="font-weight: 600;">
<div style="font-weight: 600;" id="pay-with-text">
{{$t("Pay with")}}
</div>
</div>

View file

@ -84,6 +84,7 @@
<select asp-for="DefaultLang"
class="cmblang reverse invisible"
onkeypress="if(event.keyCode==13){ $(this).click();}"
onchange="changeLanguage($(this).val())"
asp-items="@langService.GetLanguages().Select((language) => new SelectListItem(language.DisplayName,language.Code, false))"></select>

View file

@ -77,7 +77,7 @@
@if (Model.InternalLightningNode != null)
{
<p class="form-text text-muted">
You can use the internal lightning node by <a href="#" onclick="$('#lightningurl').val('@Model.InternalLightningNode'); return false;">clicking here</a>
You can use the internal lightning node by <a href="#" id="internal-ln-node-setter" onclick="$('#lightningurl').val('@Model.InternalLightningNode'); return false;">clicking here</a>
</p>
}
</div>
@ -85,7 +85,7 @@
<label asp-for="Enabled"></label>
<input asp-for="Enabled" type="checkbox" class="form-check" />
</div>
<button name="command" type="submit" value="save" class="btn btn-primary">Submit</button>
<button id="save" name="command" type="submit" value="save" class="btn btn-primary">Submit</button>
<button name="command" type="submit" value="test" class="btn btn-secondary">Test connection</button>
<a
class="btn btn-secondary"

View file

@ -120,7 +120,7 @@
{
<a asp-action="WalletTransactions" asp-controller="Wallets" asp-route-walletId="@scheme.WalletId">Wallet</a><span> - </span>
}
<a asp-action="AddDerivationScheme" asp-route-cryptoCode="@scheme.Crypto" id="ModifyBTC">Modify</a>
<a asp-action="AddDerivationScheme" asp-route-cryptoCode="@scheme.Crypto" id="@($"Modify{scheme.Crypto}")">Modify</a>
</td>
</tr>
}
@ -162,7 +162,7 @@
<span class="fa fa-times"></span>
}
</td>
<td style="text-align:right"><a asp-action="AddLightningNode" asp-route-cryptoCode="@scheme.CryptoCode">Modify</a></td>
<td style="text-align:right"><a asp-action="AddLightningNode" asp-route-cryptoCode="@scheme.CryptoCode" id="@($"Modify-Lightning{scheme.CryptoCode}")">Modify</a></td>
</tr>
}
</tbody>

View file

@ -1,9 +1,9 @@
<div class="nav flex-column nav-pills">
<a class="nav-link @ViewData.IsActivePage(StoreNavPages.Index)" asp-action="UpdateStore">General settings</a>
<a class="nav-link @ViewData.IsActivePage(StoreNavPages.Rates)" asp-action="Rates">Rates</a>
<a class="nav-link @ViewData.IsActivePage(StoreNavPages.Checkout)" asp-action="CheckoutExperience">Checkout experience</a>
<a class="nav-link @ViewData.IsActivePage(StoreNavPages.Tokens)" asp-action="ListTokens">Access Tokens</a>
<a class="nav-link @ViewData.IsActivePage(StoreNavPages.Users)" asp-action="StoreUsers">Users</a>
<a class="nav-link @ViewData.IsActivePage(StoreNavPages.PayButton)" asp-action="PayButton">Pay Button</a>
<a id="@(nameof(StoreNavPages.Index))"class="nav-link @ViewData.IsActivePage(StoreNavPages.Index)" asp-action="UpdateStore">General settings</a>
<a id="@(nameof(StoreNavPages.Rates))"class="nav-link @ViewData.IsActivePage(StoreNavPages.Rates)" asp-action="Rates">Rates</a>
<a id="@(nameof(StoreNavPages.Checkout))"class="nav-link @ViewData.IsActivePage(StoreNavPages.Checkout)" asp-action="CheckoutExperience">Checkout experience</a>
<a id="@(nameof(StoreNavPages.Tokens))"class="nav-link @ViewData.IsActivePage(StoreNavPages.Tokens)" asp-action="ListTokens">Access Tokens</a>
<a id="@(nameof(StoreNavPages.Users))"class="nav-link @ViewData.IsActivePage(StoreNavPages.Users)" asp-action="StoreUsers">Users</a>
<a id="@(nameof(StoreNavPages.PayButton))"class="nav-link @ViewData.IsActivePage(StoreNavPages.PayButton)" asp-action="PayButton">Pay Button</a>
</div>