diff --git a/BTCPayServer.Tests/AltcoinTests/AltcoinTests.cs b/BTCPayServer.Tests/AltcoinTests/AltcoinTests.cs index 9e2ffac22..e28c7d436 100644 --- a/BTCPayServer.Tests/AltcoinTests/AltcoinTests.cs +++ b/BTCPayServer.Tests/AltcoinTests/AltcoinTests.cs @@ -479,21 +479,21 @@ namespace BTCPayServer.Tests //there should be three now invoiceId = s.CreateInvoice(store.storeName, 10, "USD", "a@g.com"); s.GoToInvoiceCheckout(invoiceId); - var currencyDropdownButton = s.Driver.WaitForElement(By.ClassName("payment__currencies")); + 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.WaitForElement(By.ClassName("payment__currencies")); + currencyDropdownButton = s.Driver.FindElement(By.ClassName("payment__currencies")); Assert.Contains("LTC", currencyDropdownButton.Text); currencyDropdownButton.Click(); elements = s.Driver.FindElement(By.ClassName("vex-content")).FindElements(By.ClassName("vexmenuitem")); elements.Single(element => element.Text.Contains("Lightning")).Click(); - currencyDropdownButton = s.Driver.WaitForElement(By.ClassName("payment__currencies")); + currencyDropdownButton = s.Driver.FindElement(By.ClassName("payment__currencies")); Assert.Contains("Lightning", currencyDropdownButton.Text); s.Driver.Quit(); diff --git a/BTCPayServer.Tests/AltcoinTests/EthereumTests.cs b/BTCPayServer.Tests/AltcoinTests/EthereumTests.cs index c5f2b19fe..08550e154 100644 --- a/BTCPayServer.Tests/AltcoinTests/EthereumTests.cs +++ b/BTCPayServer.Tests/AltcoinTests/EthereumTests.cs @@ -35,7 +35,7 @@ namespace BTCPayServer.Tests InitialData = new[] {new KeyValuePair("chains", "usdt20"),} }) }); - + var networkProvider = config.ConfigureNetworkProvider(); Assert.NotNull(networkProvider.GetNetwork("ETH")); Assert.NotNull(networkProvider.GetNetwork("USDT20")); @@ -60,7 +60,7 @@ namespace BTCPayServer.Tests web3Link.Click(); s.Driver.FindElement(By.Id("Web3ProviderUrl")).SendKeys("https://ropsten-rpc.linkpool.io"); s.Driver.FindElement(By.Id("saveButton")).Click(); - s.AssertHappyMessage(); + s.FindAlertMessage(); TestUtils.Eventually(() => { s.Driver.Navigate().Refresh(); @@ -76,17 +76,17 @@ namespace BTCPayServer.Tests s.SetCheckbox(s.Driver.FindElement(By.Id("StoreSeed")), true); s.SetCheckbox(s.Driver.FindElement(By.Id("Enabled")), true); s.Driver.FindElement(By.Id("SaveButton")).Click(); - s.AssertHappyMessage(); + s.FindAlertMessage(); s.Driver.FindElement(By.Id("ModifyUSDT20")).Click(); s.Driver.FindElement(By.Id("Seed")).SendKeys(seed.ToString()); s.SetCheckbox(s.Driver.FindElement(By.Id("StoreSeed")), true); s.SetCheckbox(s.Driver.FindElement(By.Id("Enabled")), true); s.Driver.FindElement(By.Id("SaveButton")).Click(); - s.AssertHappyMessage(); + s.FindAlertMessage(); var invoiceId = s.CreateInvoice(store.storeName, 10); s.GoToInvoiceCheckout(invoiceId); - var currencyDropdownButton = s.Driver.WaitForElement(By.ClassName("payment__currencies")); + var currencyDropdownButton = s.Driver.FindElement(By.ClassName("payment__currencies")); Assert.Contains("ETH", currencyDropdownButton.Text); s.Driver.FindElement(By.Id("copy-tab")).Click(); diff --git a/BTCPayServer.Tests/ApiKeysTests.cs b/BTCPayServer.Tests/ApiKeysTests.cs index 043dbaae8..d10f51d6b 100644 --- a/BTCPayServer.Tests/ApiKeysTests.cs +++ b/BTCPayServer.Tests/ApiKeysTests.cs @@ -65,7 +65,7 @@ namespace BTCPayServer.Tests s.SetCheckbox(s, "btcpay.store.canmodifystoresettings", true); s.SetCheckbox(s, "btcpay.user.canviewprofile", true); s.Driver.FindElement(By.Id("Generate")).Click(); - var superApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text; + var superApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text; //this api key has access to everything await TestApiAgainstAccessToken(superApiKey, tester, user, Policies.CanModifyServerSettings, Policies.CanModifyStoreSettings, Policies.CanViewProfile); @@ -74,7 +74,7 @@ namespace BTCPayServer.Tests s.Driver.FindElement(By.Id("AddApiKey")).Click(); s.SetCheckbox(s, "btcpay.server.canmodifyserversettings", true); s.Driver.FindElement(By.Id("Generate")).Click(); - var serverOnlyApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text; + var serverOnlyApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text; await TestApiAgainstAccessToken(serverOnlyApiKey, tester, user, Policies.CanModifyServerSettings); @@ -82,7 +82,7 @@ namespace BTCPayServer.Tests s.Driver.FindElement(By.Id("AddApiKey")).Click(); s.SetCheckbox(s, "btcpay.store.canmodifystoresettings", true); s.Driver.FindElement(By.Id("Generate")).Click(); - var allStoreOnlyApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text; + var allStoreOnlyApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text; await TestApiAgainstAccessToken(allStoreOnlyApiKey, tester, user, Policies.CanModifyStoreSettings); @@ -94,13 +94,13 @@ namespace BTCPayServer.Tests var storeId = option.GetAttribute("value"); option.Click(); s.Driver.FindElement(By.Id("Generate")).Click(); - var selectiveStoreApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text; + var selectiveStoreApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text; await TestApiAgainstAccessToken(selectiveStoreApiKey, tester, user, Permission.Create(Policies.CanModifyStoreSettings, storeId).ToString()); s.Driver.FindElement(By.Id("AddApiKey")).Click(); s.Driver.FindElement(By.Id("Generate")).Click(); - var noPermissionsApiKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text; + var noPermissionsApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text; await TestApiAgainstAccessToken(noPermissionsApiKey, tester, user); await Assert.ThrowsAnyAsync(async () => @@ -188,7 +188,7 @@ namespace BTCPayServer.Tests checkbox.Click(); } s.Driver.FindElement(By.Id("Generate")).Click(); - var allAPIKey = s.AssertHappyMessage().FindElement(By.TagName("code")).Text; + var allAPIKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text; var apikeydata = await TestApiAgainstAccessToken(allAPIKey, $"api/v1/api-keys/current", tester.PayTester.HttpClient); Assert.Equal(checkedPermissionCount, apikeydata.Permissions.Length); } diff --git a/BTCPayServer.Tests/BTCPayServer.Tests.csproj b/BTCPayServer.Tests/BTCPayServer.Tests.csproj index abd425e8e..87929b282 100644 --- a/BTCPayServer.Tests/BTCPayServer.Tests.csproj +++ b/BTCPayServer.Tests/BTCPayServer.Tests.csproj @@ -23,7 +23,7 @@ - + all diff --git a/BTCPayServer.Tests/CheckoutUITests.cs b/BTCPayServer.Tests/CheckoutUITests.cs index e41104dab..d0c1b0369 100644 --- a/BTCPayServer.Tests/CheckoutUITests.cs +++ b/BTCPayServer.Tests/CheckoutUITests.cs @@ -1,13 +1,10 @@ using System; -using System.Linq; using System.Threading.Tasks; -using BTCPayServer.Lightning; using BTCPayServer.Payments; using BTCPayServer.Tests.Logging; using BTCPayServer.Views.Stores; using NBitcoin; using OpenQA.Selenium; -using OpenQA.Selenium.Support.UI; using Xunit; using Xunit.Abstractions; @@ -36,7 +33,7 @@ namespace BTCPayServer.Tests s.AddDerivationScheme("BTC"); s.GoToStore(store.storeId, StoreNavPages.Checkout); s.Driver.FindElement(By.Id("RequiresRefundEmail")).Click(); - s.Driver.FindElement(By.Name("command")).ForceClick(); + s.Driver.FindElement(By.Name("command")).Click(); var emailAlreadyThereInvoiceId = s.CreateInvoice(store.storeName, 100, "USD", "a@g.com"); s.GoToInvoiceCheckout(emailAlreadyThereInvoiceId); @@ -119,7 +116,7 @@ namespace BTCPayServer.Tests s.SetCheckbox(s, "LightningAmountInSatoshi", true); var command = s.Driver.FindElement(By.Name("command")); - command.ForceClick(); + 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); @@ -166,30 +163,4 @@ namespace BTCPayServer.Tests } } } - - public static class SeleniumExtensions - { - /// - /// Utility method to wait until timeout for element to be present (optionally displayed) - /// - /// Wait context - /// How we search for element - /// Flag to wait for element to be displayed or just present - /// How long to wait for element to be present/displayed - /// Element we were waiting for - public static IWebElement WaitForElement(this IWebDriver context, By by, bool displayed = true, uint timeout = 3) - { - var wait = new DefaultWait(context); - wait.Timeout = TimeSpan.FromSeconds(timeout); - wait.IgnoreExceptionTypes(typeof(NoSuchElementException)); - return wait.Until(ctx => - { - var elem = ctx.FindElement(by); - if (displayed && !elem.Displayed) - return null; - - return elem; - }); - } - } } diff --git a/BTCPayServer.Tests/Extensions.cs b/BTCPayServer.Tests/Extensions.cs index 4976f59e6..83cd75179 100644 --- a/BTCPayServer.Tests/Extensions.cs +++ b/BTCPayServer.Tests/Extensions.cs @@ -13,24 +13,17 @@ namespace BTCPayServer.Tests { public static class Extensions { - private static readonly JsonSerializerSettings jsonSettings = new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }; - public static string ToJson(this object o) + private static readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }; + public static string ToJson(this object o) => JsonConvert.SerializeObject(o, Formatting.None, JsonSettings); + + public static void LogIn(this SeleniumTester s, string email) { - var res = JsonConvert.SerializeObject(o, Formatting.None, jsonSettings); - return res; - } - public static void ScrollTo(this IWebDriver driver, By by) - { - var element = driver.FindElement(by); - } - /// - /// Sometimes the chrome driver is fucked up and we need some magic to click on the element. - /// - /// - public static void ForceClick(this IWebElement element) - { - element.SendKeys(Keys.Return); + s.Driver.FindElement(By.Id("Email")).SendKeys(email); + s.Driver.FindElement(By.Id("Password")).SendKeys("123456"); + s.Driver.FindElement(By.Id("LoginButton")).Click(); + s.Driver.AssertNoError(); } + public static void AssertNoError(this IWebDriver driver) { try @@ -57,14 +50,18 @@ namespace BTCPayServer.Tests builder.AppendLine($"[{entry.Level}]: {entry.Message}"); } } - catch { } - builder.AppendLine($"---------"); + catch + { + // ignored + } + + builder.AppendLine("---------"); } Logs.Tester.LogInformation(builder.ToString()); builder = new StringBuilder(); - builder.AppendLine($"Selenium [Sources]:"); + builder.AppendLine("Selenium [Sources]:"); builder.AppendLine(driver.PageSource); - builder.AppendLine($"---------"); + builder.AppendLine("---------"); Logs.Tester.LogInformation(builder.ToString()); throw; } diff --git a/BTCPayServer.Tests/PayJoinTests.cs b/BTCPayServer.Tests/PayJoinTests.cs index f9d8b3c0a..e31d92e5d 100644 --- a/BTCPayServer.Tests/PayJoinTests.cs +++ b/BTCPayServer.Tests/PayJoinTests.cs @@ -25,6 +25,7 @@ using NBXplorer.DerivationStrategy; using NBXplorer.Models; using Newtonsoft.Json.Linq; using OpenQA.Selenium; +using OpenQA.Selenium.Support.UI; using Xunit; using Xunit.Abstractions; @@ -239,6 +240,7 @@ namespace BTCPayServer.Tests s.SetCheckbox(s, "PayJoinEnabled", true); s.Driver.FindElement(By.Id("Save")).Click(); Assert.True(s.Driver.FindElement(By.Id("PayJoinEnabled")).Selected); + var sender = s.CreateNewStore(); var senderSeed = s.GenerateWallet("BTC", "", true, true, format); var senderWalletId = new WalletId(sender.storeId, "BTC"); @@ -257,16 +259,17 @@ namespace BTCPayServer.Tests s.Driver.SwitchTo().Alert().Accept(); Assert.False(string.IsNullOrEmpty(s.Driver.FindElement(By.Id("PayJoinBIP21")) .GetAttribute("value"))); - s.Driver.ScrollTo(By.Id("SendMenu")); - s.Driver.FindElement(By.Id("SendMenu")).ForceClick(); - s.Driver.FindElement(By.CssSelector("button[value=nbx-seed]")).Click(); + s.Driver.FindElement(By.Id("SendMenu")).Click(); + var nbxSeedButton = s.Driver.FindElement(By.CssSelector("button[value=nbx-seed]")); + new WebDriverWait(s.Driver, SeleniumTester.ImplicitWait).Until(d=> nbxSeedButton.Enabled); + nbxSeedButton.Click(); await s.Server.WaitForEvent(() => { - s.Driver.FindElement(By.CssSelector("button[value=payjoin]")).ForceClick(); + s.Driver.FindElement(By.CssSelector("button[value=payjoin]")).Click(); return Task.CompletedTask; }); //no funds in receiver wallet to do payjoin - s.AssertHappyMessage(StatusMessageModel.StatusSeverity.Warning); + s.FindAlertMessage(StatusMessageModel.StatusSeverity.Warning); await TestUtils.EventuallyAsync(async () => { var invoice = await s.Server.PayTester.GetService().GetInvoice(invoiceId); @@ -294,15 +297,14 @@ namespace BTCPayServer.Tests .GetAttribute("value"))); s.Driver.FindElement(By.Id("FeeSatoshiPerByte")).Clear(); s.Driver.FindElement(By.Id("FeeSatoshiPerByte")).SendKeys("2"); - s.Driver.ScrollTo(By.Id("SendMenu")); - s.Driver.FindElement(By.Id("SendMenu")).ForceClick(); + s.Driver.FindElement(By.Id("SendMenu")).Click(); s.Driver.FindElement(By.CssSelector("button[value=nbx-seed]")).Click(); var txId = await s.Server.WaitForEvent(() => { - s.Driver.FindElement(By.CssSelector("button[value=payjoin]")).ForceClick(); + s.Driver.FindElement(By.CssSelector("button[value=payjoin]")).Click(); return Task.CompletedTask; }); - s.AssertHappyMessage(StatusMessageModel.StatusSeverity.Success); + s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success); await TestUtils.EventuallyAsync(async () => { var invoice = await invoiceRepository.GetInvoice(invoiceId); diff --git a/BTCPayServer.Tests/README.md b/BTCPayServer.Tests/README.md index c41b5e301..dd66ebc7f 100644 --- a/BTCPayServer.Tests/README.md +++ b/BTCPayServer.Tests/README.md @@ -57,14 +57,20 @@ The `./docker-lightning-channel-teardown.sh` script closes any existing lightnin ## FAQ -`docker-compose up dev` failed or tests are not passing, what should I do? +### `docker-compose up dev` failed or tests are not passing, what should I do? 1. Run `docker-compose down --v` (this will reset your test environment) 2. Run `docker-compose pull` (this will ensure you have the lastest images) 3. Run again with `docker-compose up dev` -How to run the Altcoin environment? +### How to run the Altcoin environment? `docker-compose -f docker-compose.altcoins.yml up dev` If you still have issues, try to restart docker. + +### How to run the Selenium test with a browser? + +Run `dotnet user-secrets set RunSeleniumInBrowser true` to run tests in browser. + +To switch back to headless mode (recommended) you can run `dotnet user-secrets remove RunSeleniumInBrowser`. diff --git a/BTCPayServer.Tests/SeleniumTester.cs b/BTCPayServer.Tests/SeleniumTester.cs index 4f8053d94..e34f43dca 100644 --- a/BTCPayServer.Tests/SeleniumTester.cs +++ b/BTCPayServer.Tests/SeleniumTester.cs @@ -3,23 +3,20 @@ using System.Globalization; using System.IO; using System.Linq; using System.Runtime.CompilerServices; -using System.Threading; using System.Threading.Tasks; -using BTCPayServer; using BTCPayServer.Abstractions.Models; using BTCPayServer.Lightning; using BTCPayServer.Lightning.CLightning; -using BTCPayServer.Models; using BTCPayServer.Services; using BTCPayServer.Tests.Logging; using BTCPayServer.Views.Manage; using BTCPayServer.Views.Server; using BTCPayServer.Views.Stores; using BTCPayServer.Views.Wallets; +using Microsoft.Extensions.Configuration; using NBitcoin; using OpenQA.Selenium; using OpenQA.Selenium.Chrome; -using OpenQA.Selenium.Interactions; using Xunit; namespace BTCPayServer.Tests @@ -28,64 +25,59 @@ namespace BTCPayServer.Tests { public IWebDriver Driver { get; set; } public ServerTester Server { get; set; } + public WalletId WalletId { get; set; } - public static SeleniumTester Create([CallerMemberNameAttribute] string scope = null, bool newDb = false) - { - var server = ServerTester.Create(scope, newDb); - return new SeleniumTester() - { - Server = server - }; - } + public string StoreId { get; set; } + + public static SeleniumTester Create([CallerMemberNameAttribute] string scope = null, bool newDb = false) => + new SeleniumTester { Server = ServerTester.Create(scope, newDb) }; public async Task StartAsync() { await Server.StartAsync(); - ChromeOptions options = new ChromeOptions(); + + var windowSize = (Width: 1200, Height: 1000); + var builder = new ConfigurationBuilder(); + builder.AddUserSecrets("AB0AC1DD-9D26-485B-9416-56A33F268117"); + var config = builder.Build(); + + // Run `dotnet user-secrets set RunSeleniumInBrowser true` to run tests in browser + var runInBrowser = config["RunSeleniumInBrowser"] == "true"; + // Reset this using `dotnet user-secrets remove RunSeleniumInBrowser` + + var options = new ChromeOptions(); if (Server.PayTester.InContainer) { // this must be first option https://stackoverflow.com/questions/53073411/selenium-webdriverexceptionchrome-failed-to-start-crashed-as-google-chrome-is#comment102570662_53073789 options.AddArgument("no-sandbox"); } - - var isDebug = !Server.PayTester.InContainer; - if (!isDebug) + if (!runInBrowser) { - options.AddArguments("headless"); // Comment to view browser - options.AddArguments("window-size=1200x1000"); // Comment to view browser + options.AddArguments("headless"); } + options.AddArguments($"window-size={windowSize.Width}x{windowSize.Height}"); options.AddArgument("shm-size=2g"); Driver = new ChromeDriver(Server.PayTester.InContainer ? "/usr/bin" : Directory.GetCurrentDirectory(), options); - if (isDebug) + + if (runInBrowser) { - //when running locally, depending on your resolution, the website may go into mobile responsive mode and screw with navigation of tests + // ensure maximized window size Driver.Manage().Window.Maximize(); } - Logs.Tester.LogInformation("Selenium: Using chrome driver"); - Logs.Tester.LogInformation("Selenium: Browsing to " + Server.PayTester.ServerUri); + + Logs.Tester.LogInformation($"Selenium: Using {Driver.GetType()}"); + Logs.Tester.LogInformation($"Selenium: Browsing to {Server.PayTester.ServerUri}"); Logs.Tester.LogInformation($"Selenium: Resolution {Driver.Manage().Window.Size}"); Driver.Manage().Timeouts().ImplicitWait = ImplicitWait; GoToRegister(); Driver.AssertNoError(); } - internal IWebElement AssertHappyMessage(StatusMessageModel.StatusSeverity severity = StatusMessageModel.StatusSeverity.Success) - { - using var cts = new CancellationTokenSource(20_000); - while (!cts.IsCancellationRequested) - { - var result = Driver.FindElements(By.ClassName($"alert-{StatusMessageModel.ToString(severity)}")).Where(el => el.Displayed); - if (result.Any()) - return result.First(); - Thread.Sleep(100); - } - Logs.Tester.LogInformation(this.Driver.PageSource); - Assert.True(false, $"Should have shown {severity} message"); - return null; - } + internal IWebElement FindAlertMessage(StatusMessageModel.StatusSeverity severity = StatusMessageModel.StatusSeverity.Success) => + Driver.FindElement(By.ClassName($"alert-{StatusMessageModel.ToString(severity)}")); - public static readonly TimeSpan ImplicitWait = TimeSpan.FromSeconds(10); + public static readonly TimeSpan ImplicitWait = TimeSpan.FromSeconds(5); public string Link(string relativeLink) { return Server.PayTester.ServerUri.AbsoluteUri.WithoutEndingSlash() + relativeLink.WithStartingSlash(); @@ -93,8 +85,9 @@ namespace BTCPayServer.Tests public void GoToRegister() { - Driver.Navigate().GoToUrl(this.Link("/Account/Register")); + Driver.Navigate().GoToUrl(Link("/Account/Register")); } + public string RegisterNewUser(bool isAdmin = false) { var usr = RandomUtils.GetUInt256().ToString().Substring(64 - 20) + "@a.com"; @@ -111,30 +104,29 @@ namespace BTCPayServer.Tests public (string storeName, string storeId) CreateNewStore() { - var usr = "Store" + RandomUtils.GetUInt64().ToString(); Driver.FindElement(By.Id("Stores")).Click(); Driver.FindElement(By.Id("CreateStore")).Click(); - Driver.FindElement(By.Id("Name")).SendKeys(usr); + var name = "Store" + RandomUtils.GetUInt64(); + Driver.FindElement(By.Id("Name")).SendKeys(name); Driver.FindElement(By.Id("Create")).Click(); StoreId = Driver.FindElement(By.Id("Id")).GetAttribute("value"); - return (usr, StoreId); + return (name, StoreId); } - public string StoreId { get; set; } public Mnemonic GenerateWallet(string cryptoCode = "BTC", string seed = "", bool importkeys = false, bool privkeys = false, ScriptPubKeyType format = ScriptPubKeyType.Segwit) { - Driver.FindElement(By.Id($"Modify{cryptoCode}")).ForceClick(); - Driver.FindElement(By.Id("import-from-btn")).ForceClick(); - Driver.FindElement(By.Id("nbxplorergeneratewalletbtn")).ForceClick(); - Driver.WaitForElement(By.Id("ExistingMnemonic")).SendKeys(seed); - SetCheckbox(Driver.WaitForElement(By.Id("SavePrivateKeys")), privkeys); - SetCheckbox(Driver.WaitForElement(By.Id("ImportKeysToRPC")), importkeys); - Driver.WaitForElement(By.Id("ScriptPubKeyType")).Click(); - Driver.WaitForElement(By.CssSelector($"#ScriptPubKeyType option[value={format}]")).Click(); + Driver.FindElement(By.Id($"Modify{cryptoCode}")).Click(); + Driver.FindElement(By.Id("import-from-btn")).Click(); + Driver.FindElement(By.Id("nbxplorergeneratewalletbtn")).Click(); + Driver.FindElement(By.Id("ExistingMnemonic")).SendKeys(seed); + SetCheckbox(Driver.FindElement(By.Id("SavePrivateKeys")), privkeys); + SetCheckbox(Driver.FindElement(By.Id("ImportKeysToRPC")), importkeys); + Driver.FindElement(By.Id("ScriptPubKeyType")).Click(); + Driver.FindElement(By.CssSelector($"#ScriptPubKeyType option[value={format}]")).Click(); Logs.Tester.LogInformation("Trying to click btn-generate"); - Driver.WaitForElement(By.Id("btn-generate")).ForceClick(); + Driver.FindElement(By.Id("btn-generate")).Click(); // Seed backup page - AssertHappyMessage(); + FindAlertMessage(); if (string.IsNullOrEmpty(seed)) { seed = Driver.FindElements(By.Id("recovery-phrase")).First().GetAttribute("data-mnemonic"); @@ -146,19 +138,19 @@ namespace BTCPayServer.Tests WalletId = new WalletId(StoreId, cryptoCode); return new Mnemonic(seed); } - public WalletId WalletId { get; set; } + public void AddDerivationScheme(string cryptoCode = "BTC", string derivationScheme = "xpub661MyMwAqRbcGABgHMUXDzPzH1tU7eZaAaJQXhDXsSxsqyQzQeU6kznNfSuAyqAK9UaWSaZaMFdNiY5BCF4zBPAzSnwfUAwUhwttuAKwfRX-[legacy]") { - Driver.FindElement(By.Id($"Modify{cryptoCode}")).ForceClick(); + Driver.FindElement(By.Id($"Modify{cryptoCode}")).Click(); Driver.FindElement(By.ClassName("store-derivation-scheme")).SendKeys(derivationScheme); - Driver.FindElement(By.Id("Continue")).ForceClick(); - Driver.FindElement(By.Id("Confirm")).ForceClick(); - AssertHappyMessage(); + Driver.FindElement(By.Id("Continue")).Click(); + Driver.FindElement(By.Id("Confirm")).Click(); + FindAlertMessage(); } public void AddLightningNode(string cryptoCode, LightningConnectionType connectionType) { - string connectionString = null; + string connectionString; if (connectionType == LightningConnectionType.Charge) connectionString = $"type=charge;server={Server.MerchantCharge.Client.Uri.AbsoluteUri};allowinsecure=true"; else if (connectionType == LightningConnectionType.CLightning) @@ -168,16 +160,16 @@ namespace BTCPayServer.Tests else throw new NotSupportedException(connectionType.ToString()); - Driver.FindElement(By.Id($"Modify-Lightning{cryptoCode}")).ForceClick(); + Driver.FindElement(By.Id($"Modify-Lightning{cryptoCode}")).Click(); Driver.FindElement(By.Name($"ConnectionString")).SendKeys(connectionString); - Driver.FindElement(By.Id($"save")).ForceClick(); + Driver.FindElement(By.Id($"save")).Click(); } 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(); + Driver.FindElement(By.Id($"Modify-Lightning{cryptoCode}")).Click(); + Driver.FindElement(By.Id($"internal-ln-node-setter")).Click(); + Driver.FindElement(By.Id($"save")).Click(); } public void ClickOnAllSideMenus() @@ -193,21 +185,23 @@ namespace BTCPayServer.Tests } } - - public void Dispose() { if (Driver != null) { try { - Driver.Close(); + Driver.Quit(); } - catch { } + catch + { + // ignored + } + Driver.Dispose(); } - if (Server != null) - Server.Dispose(); + + Server?.Dispose(); } internal void AssertNotFound() @@ -241,6 +235,7 @@ namespace BTCPayServer.Tests { 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(); @@ -271,14 +266,7 @@ namespace BTCPayServer.Tests public void SetCheckbox(SeleniumTester s, string checkboxId, bool value) { - SetCheckbox(s.Driver.WaitForElement(By.Id(checkboxId)), value); - } - - public void ScrollToElement(IWebElement element) - { - Actions actions = new Actions(Driver); - actions.MoveToElement(element); - actions.Perform(); + SetCheckbox(s.Driver.FindElement(By.Id(checkboxId)), value); } public void GoToInvoices() @@ -300,16 +288,10 @@ namespace BTCPayServer.Tests Driver.Navigate().GoToUrl(new Uri(Server.PayTester.ServerUri, "Account/Login")); } - public void GoToCreateInvoicePage() - { - GoToInvoices(); - Driver.FindElement(By.Id("CreateNewInvoice")).Click(); - } - public string CreateInvoice(string storeName, decimal amount = 100, string currency = "USD", string refundEmail = "") { GoToInvoices(); - Driver.FindElement(By.Id("CreateNewInvoice")).Click(); // ocassionally gets stuck for some reason, tried force click and wait for element + 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(); @@ -318,7 +300,7 @@ namespace BTCPayServer.Tests Driver.FindElement(By.Name("StoreId")).SendKeys(storeName); Driver.FindElement(By.Id("Create")).Click(); - AssertHappyMessage(); + FindAlertMessage(); var statusElement = Driver.FindElement(By.ClassName("alert-success")); var id = statusElement.Text.Split(" ")[1]; return id; @@ -331,7 +313,7 @@ namespace BTCPayServer.Tests Driver.FindElement(By.Id("generateButton")).Click(); var addressStr = Driver.FindElement(By.Id("address")).GetProperty("value"); var address = BitcoinAddress.Create(addressStr, ((BTCPayNetwork)Server.NetworkProvider.GetNetwork(walletId.CryptoCode)).NBitcoinNetwork); - for (int i = 0; i < coins; i++) + for (var i = 0; i < coins; i++) { await Server.ExplorerNode.SendToAddressAsync(address, Money.Coins(denomination)); } @@ -344,19 +326,15 @@ namespace BTCPayServer.Tests .GetAttribute("href"); Assert.Contains($"{PayjoinClient.BIP21EndpointKey}", bip21); - GoToWallet(walletId, WalletsNavPages.Send); + GoToWallet(walletId); Driver.FindElement(By.Id("bip21parse")).Click(); Driver.SwitchTo().Alert().SendKeys(bip21); Driver.SwitchTo().Alert().Accept(); - Driver.ScrollTo(By.Id("SendMenu")); - Driver.FindElement(By.Id("SendMenu")).ForceClick(); + Driver.FindElement(By.Id("SendMenu")).Click(); Driver.FindElement(By.CssSelector("button[value=nbx-seed]")).Click(); - Driver.FindElement(By.CssSelector("button[value=broadcast]")).ForceClick(); + Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click(); } - - - private void CheckForJSErrors() { //wait for seleniun update: https://stackoverflow.com/questions/57520296/selenium-webdriver-3-141-0-driver-manage-logs-availablelogtypes-throwing-syste @@ -402,7 +380,6 @@ namespace BTCPayServer.Tests { Driver.FindElement(By.Id($"Server-{navPages}")).Click(); } - } public void GoToInvoice(string id) diff --git a/BTCPayServer.Tests/SeleniumTests.cs b/BTCPayServer.Tests/SeleniumTests.cs index 399f1edc7..4ed8668ab 100644 --- a/BTCPayServer.Tests/SeleniumTests.cs +++ b/BTCPayServer.Tests/SeleniumTests.cs @@ -3,12 +3,10 @@ using System.Globalization; using System.Linq; using System.Text; using System.Text.RegularExpressions; -using System.Threading; using System.Threading.Tasks; using BTCPayServer.Abstractions.Models; using BTCPayServer.Client.Models; using BTCPayServer.Data; -using BTCPayServer.Models; using BTCPayServer.Services.Wallets; using BTCPayServer.Tests.Logging; using BTCPayServer.Views.Server; @@ -17,23 +15,21 @@ using Microsoft.EntityFrameworkCore; using NBitcoin; using NBitcoin.DataEncoders; using NBitcoin.Payment; -using NBitpayClient; using Newtonsoft.Json.Linq; using OpenQA.Selenium; using OpenQA.Selenium.Support.Extensions; using OpenQA.Selenium.Support.UI; -using Org.BouncyCastle.Ocsp; using Renci.SshNet.Security.Cryptography; using Xunit; using Xunit.Abstractions; -using Xunit.Sdk; namespace BTCPayServer.Tests { [Trait("Selenium", "Selenium")] public class ChromeTests { - public const int TestTimeout = TestUtils.TestTimeout; + private const int TestTimeout = TestUtils.TestTimeout; + public ChromeTests(ITestOutputHelper helper) { Logs.Tester = new XUnitLog(helper) { Name = "Tests" }; @@ -85,7 +81,7 @@ namespace BTCPayServer.Tests Assert.Contains(passEl.Text, "hellorockstar", StringComparison.OrdinalIgnoreCase); s.Driver.FindElement(By.Id("delete")).Click(); s.Driver.FindElement(By.Id("continue")).Click(); - s.AssertHappyMessage(); + s.FindAlertMessage(); seedEl = s.Driver.FindElement(By.Id("SeedTextArea")); Assert.Contains("Seed removed", seedEl.Text, StringComparison.OrdinalIgnoreCase); } @@ -143,15 +139,15 @@ namespace BTCPayServer.Tests //let's test invite link s.Logout(); s.GoToRegister(); - var newAdminUser = s.RegisterNewUser(true); + s.RegisterNewUser(true); s.GoToServer(ServerNavPages.Users); s.Driver.FindElement(By.Id("CreateUser")).Click(); var usr = RandomUtils.GetUInt256().ToString().Substring(64 - 20) + "@a.com"; s.Driver.FindElement(By.Id("Email")).SendKeys(usr); s.Driver.FindElement(By.Id("Save")).Click(); - var url = s.AssertHappyMessage().FindElement(By.TagName("a")).Text; - ; + var url = s.FindAlertMessage().FindElement(By.TagName("a")).Text; + s.Logout(); s.Driver.Navigate().GoToUrl(url); Assert.Equal("hidden", s.Driver.FindElement(By.Id("Email")).GetAttribute("type")); @@ -160,7 +156,7 @@ namespace BTCPayServer.Tests s.Driver.FindElement(By.Id("Password")).SendKeys("123456"); s.Driver.FindElement(By.Id("ConfirmPassword")).SendKeys("123456"); s.Driver.FindElement(By.Id("SetPassword")).Click(); - s.AssertHappyMessage(); + s.FindAlertMessage(); s.Driver.FindElement(By.Id("Email")).SendKeys(usr); s.Driver.FindElement(By.Id("Password")).SendKeys("123456"); s.Driver.FindElement(By.Id("LoginButton")).Click(); @@ -170,23 +166,16 @@ namespace BTCPayServer.Tests } } - static void LogIn(SeleniumTester s, string email) - { - s.Driver.FindElement(By.Id("Email")).SendKeys(email); - s.Driver.FindElement(By.Id("Password")).SendKeys("123456"); - s.Driver.FindElement(By.Id("LoginButton")).Click(); - s.Driver.AssertNoError(); - } [Fact(Timeout = TestTimeout)] public async Task CanUseSSHService() { using (var s = SeleniumTester.Create()) { await s.StartAsync(); - var alice = s.RegisterNewUser(isAdmin: true); + s.RegisterNewUser(isAdmin: true); s.Driver.Navigate().GoToUrl(s.Link("/server/services")); Assert.Contains("server/services/ssh", s.Driver.PageSource); - using (var client = await s.Server.PayTester.GetService().SSHSettings.ConnectAsync()) + using (var client = await s.Server.PayTester.GetService().SSHSettings.ConnectAsync()) { var result = await client.RunBash("echo hello"); Assert.Equal(string.Empty, result.Error); @@ -197,7 +186,7 @@ namespace BTCPayServer.Tests s.Driver.AssertNoError(); s.Driver.FindElement(By.Id("SSHKeyFileContent")).Clear(); s.Driver.FindElement(By.Id("SSHKeyFileContent")).SendKeys("tes't\r\ntest2"); - s.Driver.FindElement(By.Id("submit")).ForceClick(); + s.Driver.FindElement(By.Id("submit")).Click(); s.Driver.AssertNoError(); var text = s.Driver.FindElement(By.Id("SSHKeyFileContent")).Text; @@ -207,7 +196,7 @@ namespace BTCPayServer.Tests Assert.True(s.Driver.PageSource.Contains("authorized_keys has been updated", StringComparison.OrdinalIgnoreCase)); s.Driver.FindElement(By.Id("SSHKeyFileContent")).Clear(); - s.Driver.FindElement(By.Id("submit")).ForceClick(); + s.Driver.FindElement(By.Id("submit")).Click(); text = s.Driver.FindElement(By.Id("SSHKeyFileContent")).Text; Assert.DoesNotContain("test2", text); @@ -220,12 +209,12 @@ namespace BTCPayServer.Tests using (var s = SeleniumTester.Create()) { await s.StartAsync(); - var alice = s.RegisterNewUser(isAdmin: true); + s.RegisterNewUser(isAdmin: true); s.Driver.Navigate().GoToUrl(s.Link("/server/emails")); if (s.Driver.PageSource.Contains("Configured")) { s.Driver.FindElement(By.CssSelector("button[value=\"ResetPassword\"]")).Submit(); - s.AssertHappyMessage(); + s.FindAlertMessage(); } CanSetupEmailCore(s); s.CreateNewStore(); @@ -234,33 +223,13 @@ namespace BTCPayServer.Tests } } - private static void CanSetupEmailCore(SeleniumTester s) - { - s.Driver.FindElement(By.ClassName("dropdown-toggle")).Click(); - s.Driver.FindElement(By.ClassName("dropdown-item")).Click(); - s.Driver.FindElement(By.Id("Settings_Login")).SendKeys("test@gmail.com"); - s.Driver.FindElement(By.CssSelector("button[value=\"Save\"]")).Submit(); - s.AssertHappyMessage(); - s.Driver.FindElement(By.Id("Settings_Password")).SendKeys("mypassword"); - s.Driver.FindElement(By.CssSelector("button[value=\"Save\"]")).Submit(); - Assert.Contains("Configured", s.Driver.PageSource); - s.Driver.FindElement(By.Id("Settings_Login")).SendKeys("test_fix@gmail.com"); - s.Driver.FindElement(By.CssSelector("button[value=\"Save\"]")).Submit(); - Assert.Contains("Configured", s.Driver.PageSource); - Assert.Contains("test_fix", s.Driver.PageSource); - s.Driver.FindElement(By.CssSelector("button[value=\"ResetPassword\"]")).Submit(); - s.AssertHappyMessage(); - Assert.DoesNotContain("Configured", s.Driver.PageSource); - Assert.Contains("test_fix", s.Driver.PageSource); - } - [Fact(Timeout = TestTimeout)] public async Task CanUseDynamicDns() { using (var s = SeleniumTester.Create()) { await s.StartAsync(); - var alice = s.RegisterNewUser(isAdmin: true); + s.RegisterNewUser(isAdmin: true); s.Driver.Navigate().GoToUrl(s.Link("/server/services")); Assert.Contains("Dynamic DNS", s.Driver.PageSource); @@ -337,22 +306,22 @@ namespace BTCPayServer.Tests s.ClickOnAllSideMenus(); s.GoToInvoices(); var invoiceId = s.CreateInvoice(storeData.storeName); - s.AssertHappyMessage(); + s.FindAlertMessage(); s.Driver.FindElement(By.ClassName("invoice-details-link")).Click(); var invoiceUrl = s.Driver.Url; //let's test archiving an invoice Assert.DoesNotContain("Archived", s.Driver.FindElement(By.Id("btn-archive-toggle")).Text); s.Driver.FindElement(By.Id("btn-archive-toggle")).Click(); - s.AssertHappyMessage(); Assert.Contains("Archived", s.Driver.FindElement(By.Id("btn-archive-toggle")).Text); //check that it no longer appears in list s.GoToInvoices(); + Assert.DoesNotContain(invoiceId, s.Driver.PageSource); //ok, let's unarchive and see that it shows again s.Driver.Navigate().GoToUrl(invoiceUrl); s.Driver.FindElement(By.Id("btn-archive-toggle")).Click(); - s.AssertHappyMessage(); + s.FindAlertMessage(); Assert.DoesNotContain("Archived", s.Driver.FindElement(By.Id("btn-archive-toggle")).Text); s.GoToInvoices(); Assert.Contains(invoiceId, s.Driver.PageSource); @@ -374,14 +343,14 @@ namespace BTCPayServer.Tests s.Logout(); // Let's add Bob as a guest to alice's store - LogIn(s, alice); + s.LogIn(alice); s.Driver.Navigate().GoToUrl(storeUrl + "/users"); s.Driver.FindElement(By.Id("Email")).SendKeys(bob + Keys.Enter); Assert.Contains("User added successfully", s.Driver.PageSource); s.Logout(); // Bob should not have access to store, but should have access to invoice - LogIn(s, bob); + s.LogIn(bob); s.Driver.Navigate().GoToUrl(storeUrl); Assert.Contains("ReturnUrl", s.Driver.Url); s.Driver.Navigate().GoToUrl(invoiceUrl); @@ -389,7 +358,7 @@ namespace BTCPayServer.Tests // Alice should be able to delete the store s.Logout(); - LogIn(s, alice); + s.LogIn(alice); s.Driver.FindElement(By.Id("Stores")).Click(); // there shouldn't be any hints now @@ -412,17 +381,17 @@ namespace BTCPayServer.Tests s.Driver.Navigate().GoToUrl(s.Link("/api-access-request")); Assert.Contains("ReturnUrl", s.Driver.Url); s.GoToRegister(); - var alice = s.RegisterNewUser(); - var store = s.CreateNewStore().storeName; + s.RegisterNewUser(); + s.CreateNewStore(); s.AddDerivationScheme(); s.Driver.FindElement(By.Id("Tokens")).Click(); s.Driver.FindElement(By.Id("CreateNewToken")).Click(); s.Driver.FindElement(By.Id("RequestPairing")).Click(); - string pairingCode = AssertUrlHasPairingCode(s); + var pairingCode = AssertUrlHasPairingCode(s); s.Driver.FindElement(By.Id("ApprovePairing")).Click(); - s.AssertHappyMessage(); + s.FindAlertMessage(); Assert.Contains(pairingCode, s.Driver.PageSource); var client = new NBitpayClient.Bitpay(new Key(), s.Server.PayTester.ServerUri); @@ -454,15 +423,6 @@ namespace BTCPayServer.Tests } } - private static string AssertUrlHasPairingCode(SeleniumTester s) - { - var regex = Regex.Match(new Uri(s.Driver.Url, UriKind.Absolute).Query, "pairingCode=([^&]*)"); - Assert.True(regex.Success, $"{s.Driver.Url} does not match expected regex"); - var pairingCode = regex.Groups[1].Value; - return pairingCode; - } - - [Fact(Timeout = TestTimeout)] public async Task CanCreateAppPoS() { @@ -470,17 +430,17 @@ namespace BTCPayServer.Tests { await s.StartAsync(); s.RegisterNewUser(); - var store = s.CreateNewStore(); + var (storeName, _) = s.CreateNewStore(); s.Driver.FindElement(By.Id("Apps")).Click(); s.Driver.FindElement(By.Id("CreateNewApp")).Click(); s.Driver.FindElement(By.Name("Name")).SendKeys("PoS" + Guid.NewGuid()); - s.Driver.FindElement(By.Id("SelectedAppType")).SendKeys("PointOfSale" + Keys.Enter); - s.Driver.FindElement(By.Id("SelectedStore")).SendKeys(store + Keys.Enter); + s.Driver.FindElement(By.Id("SelectedAppType")).SendKeys("PointOfSale"); + s.Driver.FindElement(By.Id("SelectedStore")).SendKeys(storeName); s.Driver.FindElement(By.Id("Create")).Click(); - s.Driver.FindElement(By.Id("DefaultView")).SendKeys("Cart" + Keys.Enter); - s.Driver.FindElement(By.Id("SaveSettings")).ForceClick(); - s.Driver.FindElement(By.Id("ViewApp")).ForceClick(); + s.Driver.FindElement(By.Id("DefaultView")).SendKeys("Cart"); + s.Driver.FindElement(By.Id("SaveSettings")).Click(); + s.Driver.FindElement(By.Id("ViewApp")).Click(); var posBaseUrl = s.Driver.Url.Replace("/Cart", ""); Assert.True(s.Driver.PageSource.Contains("Tea shop"), "Unable to create PoS"); @@ -491,36 +451,32 @@ namespace BTCPayServer.Tests s.Driver.Url = posBaseUrl + "/cart"; Assert.True(s.Driver.PageSource.Contains("Cart"), "Cart PoS not showing correct view"); - - s.Driver.Quit(); } } [Fact(Timeout = TestTimeout)] - public async Task CanCreateAppCF() + public async Task CanCreateCrowdfundingApp() { using (var s = SeleniumTester.Create()) { await s.StartAsync(); s.RegisterNewUser(); - var store = s.CreateNewStore(); + var (storeName, _) = s.CreateNewStore(); s.AddDerivationScheme(); s.Driver.FindElement(By.Id("Apps")).Click(); s.Driver.FindElement(By.Id("CreateNewApp")).Click(); s.Driver.FindElement(By.Name("Name")).SendKeys("CF" + Guid.NewGuid()); - s.Driver.FindElement(By.Id("SelectedAppType")).SendKeys("Crowdfund" + Keys.Enter); - s.Driver.FindElement(By.Id("SelectedStore")).SendKeys(store + Keys.Enter); + s.Driver.FindElement(By.Id("SelectedAppType")).SendKeys("Crowdfund"); + s.Driver.FindElement(By.Id("SelectedStore")).SendKeys(storeName); s.Driver.FindElement(By.Id("Create")).Click(); s.Driver.FindElement(By.Id("Title")).SendKeys("Kukkstarter"); s.Driver.FindElement(By.CssSelector("div.note-editable.card-block")).SendKeys("1BTC = 1BTC"); s.Driver.FindElement(By.Id("TargetCurrency")).SendKeys("JPY"); s.Driver.FindElement(By.Id("TargetAmount")).SendKeys("700"); - s.Driver.FindElement(By.Id("SaveSettings")).ForceClick(); - s.Driver.FindElement(By.Id("ViewApp")).ForceClick(); - s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last()); - Assert.True(s.Driver.PageSource.Contains("Currently Active!"), "Unable to create CF"); - s.Driver.Quit(); + s.Driver.FindElement(By.Id("SaveSettings")).Click(); + s.Driver.FindElement(By.Id("ViewApp")).Click(); + Assert.Equal("Currently Active!", s.Driver.FindElement(By.CssSelector(".h6.text-muted")).Text); } } @@ -539,11 +495,10 @@ namespace BTCPayServer.Tests s.Driver.FindElement(By.Id("Title")).SendKeys("Pay123"); s.Driver.FindElement(By.Id("Amount")).SendKeys("700"); s.Driver.FindElement(By.Id("Currency")).SendKeys("BTC"); - s.Driver.FindElement(By.Id("SaveButton")).ForceClick(); - s.Driver.FindElement(By.Name("ViewAppButton")).SendKeys(Keys.Return); + s.Driver.FindElement(By.Id("SaveButton")).Click(); + s.Driver.FindElement(By.Name("ViewAppButton")).Click(); s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last()); - Assert.True(s.Driver.PageSource.Contains("Amount due"), "Unable to create Payment Request"); - s.Driver.Quit(); + Assert.Equal("Amount due", s.Driver.FindElement(By.CssSelector("[data-test='amount-due-title']")).Text); } } @@ -554,8 +509,8 @@ namespace BTCPayServer.Tests using (var s = SeleniumTester.Create()) { await s.StartAsync(); - var userId = s.RegisterNewUser(true); - var storeId = s.CreateNewStore().storeId; + s.RegisterNewUser(true); + var (_, storeId) = s.CreateNewStore(); s.GenerateWallet("BTC", "", false, true); var walletId = new WalletId(storeId, "BTC"); s.GoToWallet(walletId, WalletsNavPages.Receive); @@ -582,10 +537,10 @@ namespace BTCPayServer.Tests coin => coin.OutPoint == spentOutpoint); }); await s.Server.ExplorerNode.GenerateAsync(1); - s.GoToWallet(walletId, WalletsNavPages.Send); + s.GoToWallet(walletId); s.Driver.FindElement(By.Id("advancedSettings")).Click(); s.Driver.FindElement(By.Id("toggleInputSelection")).Click(); - s.Driver.WaitForElement(By.Id(spentOutpoint.ToString())); + s.Driver.FindElement(By.Id(spentOutpoint.ToString())); Assert.Equal("true", s.Driver.FindElement(By.Name("InputSelection")).GetAttribute("value").ToLowerInvariant()); var el = s.Driver.FindElement(By.Id(spentOutpoint.ToString())); s.Driver.FindElement(By.Id(spentOutpoint.ToString())).Click(); @@ -596,8 +551,8 @@ namespace BTCPayServer.Tests SetTransactionOutput(s, 0, bob, 0.3m); s.Driver.FindElement(By.Id("SendMenu")).Click(); s.Driver.FindElement(By.Id("spendWithNBxplorer")).Click(); - s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).ForceClick(); - var happyElement = s.AssertHappyMessage(); + s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click(); + var happyElement = s.FindAlertMessage(); var happyText = happyElement.Text; var txid = Regex.Match(happyText, @"\((.*)\)").Groups[1].Value; @@ -614,11 +569,11 @@ namespace BTCPayServer.Tests { await s.StartAsync(); s.RegisterNewUser(true); - var store = s.CreateNewStore(); - s.GoToStore(store.storeId, Views.Stores.StoreNavPages.Webhooks); + var (storeName, storeId) = s.CreateNewStore(); + s.GoToStore(storeId, Views.Stores.StoreNavPages.Webhooks); Logs.Tester.LogInformation("Let's create two webhooks"); - for (int i = 0; i < 2; i++) + for (var i = 0; i < 2; i++) { s.Driver.FindElement(By.Id("CreateWebhook")).Click(); s.Driver.FindElement(By.Name("PayloadUrl")).SendKeys($"http://127.0.0.1/callback{i}"); @@ -636,7 +591,7 @@ namespace BTCPayServer.Tests s.Driver.FindElement(By.Id("continue")).Click(); deletes = s.Driver.FindElements(By.LinkText("Delete")); Assert.Single(deletes); - s.AssertHappyMessage(); + s.FindAlertMessage(); Logs.Tester.LogInformation("Let's try to update one of them"); s.Driver.FindElement(By.LinkText("Modify")).Click(); @@ -648,7 +603,7 @@ namespace BTCPayServer.Tests s.Driver.FindElement(By.Name("Secret")).Clear(); s.Driver.FindElement(By.Name("Secret")).SendKeys("HelloWorld"); s.Driver.FindElement(By.Name("update")).Click(); - s.AssertHappyMessage(); + s.FindAlertMessage(); s.Driver.FindElement(By.LinkText("Modify")).Click(); foreach (var value in Enum.GetValues(typeof(WebhookEventType))) { @@ -664,13 +619,13 @@ namespace BTCPayServer.Tests Assert.DoesNotContain($"value=\"InvoiceReceivedPayment\" checked", s.Driver.PageSource); s.Driver.FindElement(By.Name("update")).Click(); - s.AssertHappyMessage(); + s.FindAlertMessage(); Assert.Contains(server.ServerUri.AbsoluteUri, s.Driver.PageSource); Logs.Tester.LogInformation("Let's see if we can generate an event"); - s.GoToStore(store.storeId); + s.GoToStore(storeId); s.AddDerivationScheme(); - s.CreateInvoice(store.storeName); + s.CreateInvoice(storeName); var request = await server.GetNextRequest(); var headers = request.Request.Headers; var actualSig = headers["BTCPay-Sig"].First(); @@ -681,21 +636,22 @@ namespace BTCPayServer.Tests server.Done(); Logs.Tester.LogInformation("Let's make a failed event"); - s.CreateInvoice(store.storeName); + s.CreateInvoice(storeName); request = await server.GetNextRequest(); request.Response.StatusCode = 404; server.Done(); // The delivery is done asynchronously, so small wait here await Task.Delay(500); - s.GoToStore(store.storeId, Views.Stores.StoreNavPages.Webhooks); + s.GoToStore(storeId, Views.Stores.StoreNavPages.Webhooks); s.Driver.FindElement(By.LinkText("Modify")).Click(); var elements = s.Driver.FindElements(By.ClassName("redeliver")); // One worked, one failed s.Driver.FindElement(By.ClassName("fa-times")); s.Driver.FindElement(By.ClassName("fa-check")); elements[0].Click(); - s.AssertHappyMessage(); + + s.FindAlertMessage(); request = await server.GetNextRequest(); request.Response.StatusCode = 404; server.Done(); @@ -708,32 +664,22 @@ namespace BTCPayServer.Tests CanBrowseContent(s); var element = s.Driver.FindElement(By.ClassName("redeliver")); element.Click(); - s.AssertHappyMessage(); + + s.FindAlertMessage(); request = await server.GetNextRequest(); request.Response.StatusCode = 404; server.Done(); Logs.Tester.LogInformation("Let's see if we can delete store with some webhooks inside"); - s.GoToStore(store.storeId); + s.GoToStore(storeId); s.Driver.ExecuteJavaScript("window.scrollBy(0,1000);"); s.Driver.FindElement(By.Id("danger-zone-expander")).Click(); s.Driver.FindElement(By.Id("delete-store")).Click(); s.Driver.FindElement(By.Id("continue")).Click(); - s.AssertHappyMessage(); + s.FindAlertMessage(); } } - private static void CanBrowseContent(SeleniumTester s) - { - s.Driver.FindElement(By.ClassName("delivery-content")).Click(); - var windows = s.Driver.WindowHandles; - Assert.Equal(2, windows.Count); - s.Driver.SwitchTo().Window(windows[1]); - JObject.Parse(s.Driver.FindElement(By.TagName("body")).Text); - s.Driver.Close(); - s.Driver.SwitchTo().Window(windows[0]); - } - [Fact(Timeout = TestTimeout)] public async Task CanManageWallet() { @@ -745,15 +691,14 @@ namespace BTCPayServer.Tests // In this test, we try to spend from a manual seed. We import the xpub 49'/0'/0', then try to use the seed // to sign the transaction - s.GenerateWallet("BTC", "", true, false); + s.GenerateWallet("BTC", "", true); //let's test quickly the receive wallet page s.Driver.FindElement(By.Id("Wallets")).Click(); s.Driver.FindElement(By.LinkText("Manage")).Click(); - s.Driver.FindElement(By.Id("WalletSend")).Click(); - s.Driver.ScrollTo(By.Id("SendMenu")); - s.Driver.FindElement(By.Id("SendMenu")).ForceClick(); + s.Driver.FindElement(By.Id("SendMenu")).Click(); + //you cant use the Sign with NBX option without saving private keys when generating the wallet. Assert.DoesNotContain("nbx-seed", s.Driver.PageSource); @@ -764,7 +709,7 @@ namespace BTCPayServer.Tests var receiveAddr = s.Driver.FindElement(By.Id("address")).GetAttribute("value"); //unreserve s.Driver.FindElement(By.CssSelector("button[value=unreserve-current-address]")).Click(); - //generate it again, should be the same one as before as nothign got used in the meantime + //generate it again, should be the same one as before as nothing got used in the meantime s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click(); Assert.True(s.Driver.FindElement(By.ClassName("qr-container")).Displayed); Assert.Equal(receiveAddr, s.Driver.FindElement(By.Id("address")).GetAttribute("value")); @@ -784,11 +729,12 @@ namespace BTCPayServer.Tests //change the wallet and ensure old address is not there and generating a new one does not result in the prev one s.GoToStore(storeId.storeId); - s.GenerateWallet("BTC", "", true, false); + s.GenerateWallet("BTC", "", true); s.Driver.FindElement(By.Id("Wallets")).Click(); s.Driver.FindElement(By.LinkText("Manage")).Click(); s.Driver.FindElement(By.Id("WalletReceive")).Click(); s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click(); + Assert.NotEqual(receiveAddr, s.Driver.FindElement(By.Id("address")).GetAttribute("value")); var invoiceId = s.CreateInvoice(storeId.storeName); @@ -810,7 +756,7 @@ namespace BTCPayServer.Tests //spendable from bitcoin core wallet! Assert.False(result.IsWatchOnly); var tx = s.Server.ExplorerNode.SendToAddress(BitcoinAddress.Create(address, Network.RegTest), Money.Coins(3.0m)); - s.Server.ExplorerNode.Generate(1); + await s.Server.ExplorerNode.GenerateAsync(1); s.Driver.FindElement(By.Id("Wallets")).Click(); s.Driver.FindElement(By.LinkText("Manage")).Click(); @@ -818,16 +764,17 @@ namespace BTCPayServer.Tests s.ClickOnAllSideMenus(); // Make sure we can rescan, because we are admin! - s.Driver.FindElement(By.Id("WalletRescan")).ForceClick(); + s.Driver.FindElement(By.Id("WalletRescan")).Click(); Assert.Contains("The batch size make sure", s.Driver.PageSource); // We setup the fingerprint and the account key path - s.Driver.FindElement(By.Id("WalletSettings")).ForceClick(); + s.Driver.FindElement(By.Id("WalletSettings")).Click(); // s.Driver.FindElement(By.Id("AccountKeys_0__MasterFingerprint")).SendKeys("8bafd160"); // s.Driver.FindElement(By.Id("AccountKeys_0__AccountKeyPath")).SendKeys("m/49'/0'/0'" + Keys.Enter); // Check the tx sent earlier arrived - s.Driver.FindElement(By.Id("WalletTransactions")).ForceClick(); + s.Driver.FindElement(By.Id("WalletTransactions")).Click(); + var walletTransactionLink = s.Driver.Url; Assert.Contains(tx.ToString(), s.Driver.PageSource); @@ -838,17 +785,16 @@ namespace BTCPayServer.Tests s.Driver.FindElement(By.Id("WalletSend")).Click(); var bob = new Key().PubKey.Hash.GetAddress(Network.RegTest); SetTransactionOutput(s, 0, bob, 1); - s.Driver.ScrollTo(By.Id("SendMenu")); - s.Driver.FindElement(By.Id("SendMenu")).ForceClick(); + s.Driver.FindElement(By.Id("SendMenu")).Click(); s.Driver.FindElement(By.CssSelector("button[value=seed]")).Click(); // Input the seed - s.Driver.FindElement(By.Id("SeedOrKey")).SendKeys(signingSource.ToString() + Keys.Enter); + s.Driver.FindElement(By.Id("SeedOrKey")).SendKeys(signingSource + Keys.Enter); // Broadcast Assert.Contains(bob.ToString(), s.Driver.PageSource); Assert.Contains("1.00000000", s.Driver.PageSource); - s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).ForceClick(); + s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click(); Assert.Equal(walletTransactionLink, s.Driver.Url); } @@ -860,18 +806,17 @@ namespace BTCPayServer.Tests var jack = new Key().PubKey.Hash.GetAddress(Network.RegTest); SetTransactionOutput(s, 0, jack, 0.01m); - s.Driver.ScrollTo(By.Id("SendMenu")); - s.Driver.FindElement(By.Id("SendMenu")).ForceClick(); + s.Driver.FindElement(By.Id("SendMenu")).Click(); s.Driver.FindElement(By.CssSelector("button[value=nbx-seed]")).Click(); Assert.Contains(jack.ToString(), s.Driver.PageSource); Assert.Contains("0.01000000", s.Driver.PageSource); - s.Driver.FindElement(By.CssSelector("button[value=analyze-psbt]")).ForceClick(); + s.Driver.FindElement(By.CssSelector("button[value=analyze-psbt]")).Click(); Assert.EndsWith("psbt", s.Driver.Url); - s.Driver.FindElement(By.CssSelector("#OtherActions")).ForceClick(); - s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).ForceClick(); + s.Driver.FindElement(By.CssSelector("#OtherActions")).Click(); + s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click(); Assert.EndsWith("psbt/ready", s.Driver.Url); - s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).ForceClick(); + s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click(); Assert.Equal(walletTransactionLink, s.Driver.Url); var bip21 = invoice.EntityToDTO().CryptoInfo.First().PaymentUrls.BIP21; @@ -884,14 +829,14 @@ namespace BTCPayServer.Tests s.Driver.FindElement(By.Id("bip21parse")).Click(); s.Driver.SwitchTo().Alert().SendKeys(bip21); s.Driver.SwitchTo().Alert().Accept(); - s.AssertHappyMessage(StatusMessageModel.StatusSeverity.Info); + s.FindAlertMessage(StatusMessageModel.StatusSeverity.Info); Assert.Equal(parsedBip21.Amount.ToString(false), s.Driver.FindElement(By.Id($"Outputs_0__Amount")).GetAttribute("value")); Assert.Equal(parsedBip21.Address.ToString(), s.Driver.FindElement(By.Id($"Outputs_0__DestinationAddress")).GetAttribute("value")); s.GoToWallet(new WalletId(storeId.storeId, "BTC"), WalletsNavPages.Settings); var walletUrl = s.Driver.Url; - s.Driver.FindElement(By.Id("SettingsMenu")).ForceClick(); + s.Driver.FindElement(By.Id("SettingsMenu")).Click(); s.Driver.FindElement(By.CssSelector("button[value=view-seed]")).Click(); // Seed backup page @@ -905,18 +850,6 @@ namespace BTCPayServer.Tests Assert.Equal(walletUrl, s.Driver.Url); } } - void SetTransactionOutput(SeleniumTester s, int index, BitcoinAddress dest, decimal amount, bool subtract = false) - { - s.Driver.FindElement(By.Id($"Outputs_{index}__DestinationAddress")).SendKeys(dest.ToString()); - var amountElement = s.Driver.FindElement(By.Id($"Outputs_{index}__Amount")); - amountElement.Clear(); - amountElement.SendKeys(amount.ToString()); - var checkboxElement = s.Driver.FindElement(By.Id($"Outputs_{index}__SubtractFeesFromOutput")); - if (checkboxElement.Selected != subtract) - { - checkboxElement.Click(); - } - } [Fact] [Trait("Selenium", "Selenium")] @@ -926,45 +859,47 @@ namespace BTCPayServer.Tests { await s.StartAsync(); s.RegisterNewUser(true); - var receiver = s.CreateNewStore(); - var receiverSeed = s.GenerateWallet("BTC", "", true, true, ScriptPubKeyType.Segwit); + s.CreateNewStore(); + s.GenerateWallet("BTC", "", true, true); + await s.Server.ExplorerNode.GenerateAsync(1); await s.FundStoreWallet(denomination: 50.0m); s.GoToWallet(navPages: WalletsNavPages.PullPayments); s.Driver.FindElement(By.Id("NewPullPayment")).Click(); s.Driver.FindElement(By.Id("Name")).SendKeys("PP1"); s.Driver.FindElement(By.Id("Amount")).Clear(); - s.Driver.FindElement(By.Id("Amount")).SendKeys("99.0" + Keys.Enter); + s.Driver.FindElement(By.Id("Amount")).SendKeys("99.0");; + s.Driver.FindElement(By.Id("Create")).Click(); s.Driver.FindElement(By.LinkText("View")).Click(); - Thread.Sleep(1000); s.GoToWallet(navPages: WalletsNavPages.PullPayments); + s.Driver.FindElement(By.Id("NewPullPayment")).Click(); s.Driver.FindElement(By.Id("Name")).SendKeys("PP2"); s.Driver.FindElement(By.Id("Amount")).Clear(); - s.Driver.FindElement(By.Id("Amount")).SendKeys("100.0" + Keys.Enter); + s.Driver.FindElement(By.Id("Amount")).SendKeys("100.0"); + s.Driver.FindElement(By.Id("Create")).Click(); + // This should select the first View, ie, the last one PP2 s.Driver.FindElement(By.LinkText("View")).Click(); - - Thread.Sleep(1000); var address = await s.Server.ExplorerNode.GetNewAddressAsync(); s.Driver.FindElement(By.Id("Destination")).SendKeys(address.ToString()); s.Driver.FindElement(By.Id("ClaimedAmount")).Clear(); s.Driver.FindElement(By.Id("ClaimedAmount")).SendKeys("15" + Keys.Enter); - s.AssertHappyMessage(); + s.FindAlertMessage(); // We should not be able to use an address already used s.Driver.FindElement(By.Id("Destination")).SendKeys(address.ToString()); s.Driver.FindElement(By.Id("ClaimedAmount")).Clear(); s.Driver.FindElement(By.Id("ClaimedAmount")).SendKeys("20" + Keys.Enter); - s.AssertHappyMessage(StatusMessageModel.StatusSeverity.Error); + s.FindAlertMessage(StatusMessageModel.StatusSeverity.Error); address = await s.Server.ExplorerNode.GetNewAddressAsync(); s.Driver.FindElement(By.Id("Destination")).Clear(); s.Driver.FindElement(By.Id("Destination")).SendKeys(address.ToString()); s.Driver.FindElement(By.Id("ClaimedAmount")).Clear(); s.Driver.FindElement(By.Id("ClaimedAmount")).SendKeys("20" + Keys.Enter); - s.AssertHappyMessage(); + s.FindAlertMessage(); Assert.Contains("Awaiting Approval", s.Driver.PageSource); var viewPullPaymentUrl = s.Driver.Url; @@ -982,11 +917,11 @@ namespace BTCPayServer.Tests Assert.DoesNotContain("No payout waiting for approval", s.Driver.PageSource); s.Driver.FindElement(By.Id("selectAllCheckbox")).Click(); s.Driver.FindElement(By.Id("payCommand")).Click(); - s.Driver.ScrollTo(By.Id("SendMenu")); - s.Driver.FindElement(By.Id("SendMenu")).ForceClick(); + s.Driver.FindElement(By.Id("SendMenu")).Click(); s.Driver.FindElement(By.CssSelector("button[value=nbx-seed]")).Click(); - s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).ForceClick(); - s.AssertHappyMessage(); + s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click(); + + s.FindAlertMessage(); TestUtils.Eventually(() => { @@ -1027,5 +962,57 @@ namespace BTCPayServer.Tests }); } } + + private static void CanBrowseContent(SeleniumTester s) + { + s.Driver.FindElement(By.ClassName("delivery-content")).Click(); + var windows = s.Driver.WindowHandles; + Assert.Equal(2, windows.Count); + s.Driver.SwitchTo().Window(windows[1]); + JObject.Parse(s.Driver.FindElement(By.TagName("body")).Text); + s.Driver.Close(); + s.Driver.SwitchTo().Window(windows[0]); + } + + private static void CanSetupEmailCore(SeleniumTester s) + { + s.Driver.FindElement(By.ClassName("dropdown-toggle")).Click(); + s.Driver.FindElement(By.ClassName("dropdown-item")).Click(); + s.Driver.FindElement(By.Id("Settings_Login")).SendKeys("test@gmail.com"); + s.Driver.FindElement(By.CssSelector("button[value=\"Save\"]")).Submit(); + s.FindAlertMessage(); + s.Driver.FindElement(By.Id("Settings_Password")).SendKeys("mypassword"); + s.Driver.FindElement(By.CssSelector("button[value=\"Save\"]")).Submit(); + Assert.Contains("Configured", s.Driver.PageSource); + s.Driver.FindElement(By.Id("Settings_Login")).SendKeys("test_fix@gmail.com"); + s.Driver.FindElement(By.CssSelector("button[value=\"Save\"]")).Submit(); + Assert.Contains("Configured", s.Driver.PageSource); + Assert.Contains("test_fix", s.Driver.PageSource); + s.Driver.FindElement(By.CssSelector("button[value=\"ResetPassword\"]")).Submit(); + s.FindAlertMessage(); + Assert.DoesNotContain("Configured", s.Driver.PageSource); + Assert.Contains("test_fix", s.Driver.PageSource); + } + + private static string AssertUrlHasPairingCode(SeleniumTester s) + { + var regex = Regex.Match(new Uri(s.Driver.Url, UriKind.Absolute).Query, "pairingCode=([^&]*)"); + Assert.True(regex.Success, $"{s.Driver.Url} does not match expected regex"); + var pairingCode = regex.Groups[1].Value; + return pairingCode; + } + + private void SetTransactionOutput(SeleniumTester s, int index, BitcoinAddress dest, decimal amount, bool subtract = false) + { + s.Driver.FindElement(By.Id($"Outputs_{index}__DestinationAddress")).SendKeys(dest.ToString()); + var amountElement = s.Driver.FindElement(By.Id($"Outputs_{index}__Amount")); + amountElement.Clear(); + amountElement.SendKeys(amount.ToString(CultureInfo.InvariantCulture)); + var checkboxElement = s.Driver.FindElement(By.Id($"Outputs_{index}__SubtractFeesFromOutput")); + if (checkboxElement.Selected != subtract) + { + checkboxElement.Click(); + } + } } } diff --git a/BTCPayServer/Views/AppsPublic/Crowdfund/MinimalCrowdfund.cshtml b/BTCPayServer/Views/AppsPublic/Crowdfund/MinimalCrowdfund.cshtml index 6bb6bb74e..7dcc07508 100644 --- a/BTCPayServer/Views/AppsPublic/Crowdfund/MinimalCrowdfund.cshtml +++ b/BTCPayServer/Views/AppsPublic/Crowdfund/MinimalCrowdfund.cshtml @@ -15,19 +15,19 @@ @Model.Title @if (!Model.Started && Model.StartDate.HasValue) { - + Starts @Model.StartDate.Value.Subtract(DateTime.Now.ToUniversalTime()) } else if (Model.Started && !Model.Ended && Model.EndDate.HasValue) { - + Ends @Model.EndDate.Value.Subtract(DateTime.Now.ToUniversalTime()) } else if (Model.Started && !Model.Ended && !Model.EndDate.HasValue) { - + Currently Active! } diff --git a/BTCPayServer/Views/PaymentRequest/ViewPaymentRequest.cshtml b/BTCPayServer/Views/PaymentRequest/ViewPaymentRequest.cshtml index 16410d04a..54c245af3 100644 --- a/BTCPayServer/Views/PaymentRequest/ViewPaymentRequest.cshtml +++ b/BTCPayServer/Views/PaymentRequest/ViewPaymentRequest.cshtml @@ -216,7 +216,7 @@
@Model.AmountDueFormatted
-
Amount due
+
Amount due
diff --git a/BTCPayServer/Views/Stores/AddDerivationScheme.cshtml b/BTCPayServer/Views/Stores/AddDerivationScheme.cshtml index 2c847a420..06ec4a313 100644 --- a/BTCPayServer/Views/Stores/AddDerivationScheme.cshtml +++ b/BTCPayServer/Views/Stores/AddDerivationScheme.cshtml @@ -230,6 +230,5 @@ $("#qr-import-form").submit(); },"scanqrModal"); }); - } diff --git a/BTCPayServer/Views/Wallets/WalletPSBT.cshtml b/BTCPayServer/Views/Wallets/WalletPSBT.cshtml index 733bf2995..ee3f28567 100644 --- a/BTCPayServer/Views/Wallets/WalletPSBT.cshtml +++ b/BTCPayServer/Views/Wallets/WalletPSBT.cshtml @@ -86,17 +86,15 @@ - - + hljs.initHighlightingOnLoad(); + + $(function (){ + initQRShow("Scan PSBT", @Json.Serialize(Model.PSBTHex), "scan-qr-modal"); + initCameraScanningApp("Scan PSBT", function (data){ + $("textarea[name=PSBT]").val(data); + $("#Decode").click(); + }, "scanModal"); + }); + } diff --git a/BTCPayServer/wwwroot/js/wallet/wallet-camera-scanner.js b/BTCPayServer/wwwroot/js/wallet/wallet-camera-scanner.js index 899b64786..07a9eeb1d 100644 --- a/BTCPayServer/wwwroot/js/wallet/wallet-camera-scanner.js +++ b/BTCPayServer/wwwroot/js/wallet/wallet-camera-scanner.js @@ -1,5 +1,5 @@ $(function () { - initCameraScanningApp("Scan address/ payment link", function(data){ + initCameraScanningApp("Scan address/ payment link", function(data) { $("#BIP21").val(data); $("form").submit(); },"scanModal");