Migrate CanManageWallet test to Playwright (#6797)

This commit is contained in:
Nicolas Dorier
2025-06-18 09:26:53 +09:00
committed by GitHub
parent 9a78d861f5
commit 2da91e6d41
5 changed files with 315 additions and 250 deletions

View File

@@ -1,11 +1,9 @@
using System;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Services.Wallets;
using BTCPayServer.Tests.Logging;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Playwright;
using NBXplorer.DerivationStrategy;

View File

@@ -50,7 +50,7 @@ namespace BTCPayServer.Tests
Browser = await playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions
{
Headless = Server.PayTester.InContainer,
SlowMo = Server.PayTester.InContainer ? 0 : 50, // Add slight delay, nicer during dev
SlowMo = 0 // 50 if you want to slow down
});
var context = await Browser.NewContextAsync();
Page = await context.NewPageAsync();
@@ -230,8 +230,9 @@ namespace BTCPayServer.Tests
var isImport = !string.IsNullOrEmpty(seed);
await GoToWalletSettings(cryptoCode);
// Replace previous wallet case
if (await Page.Locator("#ChangeWalletLink").IsVisibleAsync())
if (await Page.Locator("#ActionsDropdownToggle").IsVisibleAsync())
{
TestLogs.LogInformation($"Replacing the wallet");
await Page.ClickAsync("#ActionsDropdownToggle");
await Page.ClickAsync("#ChangeWalletLink");
await Page.FillAsync("#ConfirmInput", "REPLACE");
@@ -624,24 +625,25 @@ namespace BTCPayServer.Tests
{
await GoToWallet(walletId, navPages: WalletsNavPages.Transactions);
await Page.Locator("#WalletTransactions[data-loaded='true']").WaitForAsync(new() { State = WaitForSelectorState.Visible });
return new WalletTransactionsPMO(Page);
return new WalletTransactionsPMO(this);
}
#nullable enable
public class WalletTransactionsPMO(IPage page)
public class WalletTransactionsPMO(PlaywrightTester tester)
{
public Task SelectAll() => page.SetCheckedAsync(".mass-action-select-all", true);
private IPage Page => tester.Page;
public Task SelectAll() => Page.SetCheckedAsync(".mass-action-select-all", true);
public async Task Select(params uint256[] txs)
{
foreach (var txId in txs)
{
await page.SetCheckedAsync($"{TxRowSelector(txId)} .mass-action-select", true);
await Page.SetCheckedAsync($"{TxRowSelector(txId)} .mass-action-select", true);
}
}
public Task BumpFeeSelected() => page.ClickAsync("#BumpFee");
public Task BumpFeeSelected() => Page.ClickAsync("#BumpFee");
public Task BumpFee(uint256? txId = null) => page.ClickAsync($"{TxRowSelector(txId)} .bumpFee-btn");
public Task BumpFee(uint256? txId = null) => Page.ClickAsync($"{TxRowSelector(txId)} .bumpFee-btn");
static string TxRowSelector(uint256? txId = null) => txId is null ? ".transaction-row:first-of-type" : $".transaction-row[data-value=\"{txId}\"]";
public Task AssertHasLabels(string label) => AssertHasLabels(null, label);
@@ -653,23 +655,32 @@ namespace BTCPayServer.Tests
retry:
await WaitTransactionsLoaded();
var selector = $"{TxRowSelector(txId)} .transaction-label[data-value=\"{label}\"]";
if (await page.Locator(selector).IsVisibleAsync())
if (await Page.Locator(selector).IsVisibleAsync())
return;
if (tried > 5)
{
await page.Locator(selector).WaitForAsync();
try
{
await Page.Locator(selector).WaitForAsync();
}
catch
{
await tester.TakeScreenshot("AssertHasLabels.png");
throw;
}
return;
}
tried++;
await page.ReloadAsync();
await Page.ReloadAsync();
goto retry;
}
public Task WaitTransactionsLoaded() => page.Locator("#WalletTransactions[data-loaded='true']").WaitForAsync();
public Task WaitTransactionsLoaded() => Page.Locator("#WalletTransactions[data-loaded='true']").WaitForAsync();
public async Task AssertNotFound(uint256 txId)
{
Assert.False(await page.Locator(TxRowSelector(txId)).IsVisibleAsync());
Assert.False(await Page.Locator(TxRowSelector(txId)).IsVisibleAsync());
}
}
@@ -690,6 +701,8 @@ namespace BTCPayServer.Tests
public Task Sign() => page.ClickAsync("#SignTransaction");
public Task SetFeeRate(decimal val) => page.FillAsync("[name=\"FeeSatoshiPerByte\"]", val.ToString(CultureInfo.InvariantCulture));
public Task FillAmount(decimal amount) => page.FillAsync("[name='Outputs[0].Amount']", amount.ToString(CultureInfo.InvariantCulture));
}
public async Task MarkAsSettled()
@@ -698,5 +711,22 @@ namespace BTCPayServer.Tests
var client = await this.AsTestAccount().CreateClient();
await client.MarkInvoiceStatus(StoreId, txId, new() { Status = InvoiceStatus.Settled });
}
public WalletTransactionsPMO InWalletTransactions() => new WalletTransactionsPMO(this);
public WalletBroadcastPMO InBroadcast() => new WalletBroadcastPMO(Page);
public class WalletBroadcastPMO(IPage page)
{
public async Task AssertSending(BitcoinAddress destination, decimal amount)
{
await page.WaitForSelectorAsync($"td:text('{destination}')");
var amt = await page.WaitForSelectorAsync($"td:text('{destination}') >> .. >> :nth-child(3)");
var actual = (await amt!.TextContentAsync()).NormalizeWhitespaces();
var expected = ("-" + Money.Coins(amount).ToString() + " " + "BTC").NormalizeWhitespaces();
Assert.Equal(expected, actual);
}
public async Task Broadcast() => await page.ClickAsync("#BroadcastTransaction");
}
}
}

View File

@@ -1,18 +1,25 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Payments;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Views.Manage;
using BTCPayServer.Views.Server;
using BTCPayServer.Views.Stores;
using BTCPayServer.Views.Wallets;
using Microsoft.AspNetCore.Identity;
using Microsoft.Playwright;
using NBitcoin;
using NBitcoin.Payment;
using NBXplorer.Models;
using Newtonsoft.Json.Linq;
using Xunit;
using Xunit.Abstractions;
@@ -55,10 +62,10 @@ namespace BTCPayServer.Tests
await s.Page.SelectOptionAsync("#FormId", "Email");
await s.ClickPagePrimary();
await s.FindAlertMessage(partialText: "App updated");
var opening = s.Page.Context.WaitForPageAsync();
await s.Page.ClickAsync("#ViewApp");
var popOutPage = await s.Page.Context.WaitForPageAsync();
string invoiceId;
await using (var o = await s.SwitchPage(popOutPage))
await using (_ = await s.SwitchPage(opening))
{
await s.Page.Locator("button[type='submit']").First.ClickAsync();
await s.Page.FillAsync("[name='buyerEmail']", "aa@aa.com");
@@ -79,8 +86,9 @@ namespace BTCPayServer.Tests
await s.ClickPagePrimary();
await s.Page.Locator("a[id^='Edit-']").First.ClickAsync();
var editUrl = new Uri(s.Page.Url);
opening = s.Page.Context.WaitForPageAsync();
await s.Page.ClickAsync("#ViewPaymentRequest");
popOutPage = await s.Page.Context.WaitForPageAsync();
var popOutPage = await opening;
await popOutPage.ClickAsync("[data-test='form-button']");
Assert.Contains("Enter your email", await popOutPage.ContentAsync());
await popOutPage.FillAsync("input[name='buyerEmail']", "aa@aa.com");
@@ -581,5 +589,265 @@ namespace BTCPayServer.Tests
Assert.Equal(TimeSpan.FromMinutes(5), newStore.DisplayExpirationTimer);
Assert.Equal(TimeSpan.FromMinutes(15), newStore.InvoiceExpiration);
}
[Fact]
public async Task CanManageWallet()
{
await using var s = CreatePlaywrightTester();
await s.StartAsync();
await s.RegisterNewUser(true);
var (_, storeId) = await s.CreateNewStore();
const string cryptoCode = "BTC";
// 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
await s.GenerateWallet(cryptoCode, "", true);
//let's test quickly the wallet send page
await s.Page.ClickAsync($"#StoreNav-Wallet{cryptoCode}");
await s.Page.ClickAsync("#WalletNav-Send");
//you cannot use the Sign with NBX option without saving private keys when generating the wallet.
Assert.DoesNotContain("nbx-seed", await s.Page.ContentAsync());
Assert.Equal(0, await s.Page.Locator("#GoBack").CountAsync());
await s.Page.ClickAsync("#SignTransaction");
await s.Page.WaitForSelectorAsync("text=Destination Address field is required");
Assert.Equal(0, await s.Page.Locator("#GoBack").CountAsync());
await s.Page.ClickAsync("#CancelWizard");
await s.Page.ClickAsync("#WalletNav-Receive");
//generate a receiving address
await s.Page.WaitForSelectorAsync("#address-tab .qr-container");
Assert.True(await s.Page.Locator("#address-tab .qr-container").IsVisibleAsync());
// no previous page in the wizard, hence no back button
Assert.Equal(0, await s.Page.Locator("#GoBack").CountAsync());
var receiveAddr = await s.Page.Locator("#Address").GetAttributeAsync("data-text");
// Can add a label?
await TestUtils.EventuallyAsync(async () =>
{
await s.Page.ClickAsync("div.label-manager input");
await Task.Delay(500);
await s.Page.FillAsync("div.label-manager input", "test-label");
await s.Page.Keyboard.PressAsync("Enter");
await Task.Delay(500);
await s.Page.FillAsync("div.label-manager input", "label2");
await s.Page.Keyboard.PressAsync("Enter");
await Task.Delay(500);
});
await TestUtils.EventuallyAsync(async () =>
{
await s.Page.ReloadAsync();
await s.Page.WaitForSelectorAsync("[data-value='test-label']");
});
Assert.True(await s.Page.Locator("#address-tab .qr-container").IsVisibleAsync());
Assert.Equal(receiveAddr, await s.Page.Locator("#Address").GetAttributeAsync("data-text"));
await TestUtils.EventuallyAsync(async () =>
{
var content = await s.Page.ContentAsync();
Assert.Contains("test-label", content);
});
// Remove a label
await s.Page.WaitForSelectorAsync("[data-value='test-label']");
await s.Page.ClickAsync("[data-value='test-label']");
await Task.Delay(500);
await s.Page.EvaluateAsync(@"() => {
const l = document.querySelector('[data-value=""test-label""]');
l.click();
l.nextSibling.dispatchEvent(new KeyboardEvent('keydown', {'key': 'Delete', keyCode: 8}));
}");
await Task.Delay(500);
await s.Page.ReloadAsync();
Assert.DoesNotContain("test-label", await s.Page.ContentAsync());
Assert.Equal(0, await s.Page.Locator("#GoBack").CountAsync());
//send money to addr and ensure it changed
var sess = await s.Server.ExplorerClient.CreateWebsocketNotificationSessionAsync();
await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(receiveAddr!, Network.RegTest),
Money.Parse("0.1"));
await sess.WaitNext<NewTransactionEvent>(e => e.Outputs.FirstOrDefault()?.Address.ToString() == receiveAddr);
await Task.Delay(200);
await s.Page.ReloadAsync();
await s.Page.ClickAsync("button[value=generate-new-address]");
Assert.NotEqual(receiveAddr, await s.Page.Locator("#Address").GetAttributeAsync("data-text"));
receiveAddr = await s.Page.Locator("#Address").GetAttributeAsync("data-text");
await s.Page.ClickAsync("#CancelWizard");
// Check the label is applied to the tx
var wt = s.InWalletTransactions();
await wt.AssertHasLabels("label2");
//change the wallet and ensure old address is not there and generating a new one does not result in the prev one
await s.GenerateWallet(cryptoCode, "", true);
await s.GoToWallet(null, WalletsNavPages.Receive);
await s.Page.ClickAsync("button[value=generate-new-address]");
var newAddr = await s.Page.Locator("#Address").GetAttributeAsync("data-text");
Assert.NotEqual(receiveAddr, newAddr);
var invoiceId = await s.CreateInvoice(storeId);
var invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId);
var btc = PaymentTypes.CHAIN.GetPaymentMethodId("BTC");
var address = invoice.GetPaymentPrompt(btc)!.Destination;
//wallet should have been imported to bitcoin core wallet in watch only mode.
var result =
await s.Server.ExplorerNode.GetAddressInfoAsync(BitcoinAddress.Create(address, Network.RegTest));
Assert.True(result.IsWatchOnly);
await s.GoToStore(storeId);
var mnemonic = await s.GenerateWallet(cryptoCode, "", true, true);
//lets import and save private keys
invoiceId = await s.CreateInvoice(storeId);
invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId);
address = invoice.GetPaymentPrompt(btc)!.Destination;
result = await s.Server.ExplorerNode.GetAddressInfoAsync(
BitcoinAddress.Create(address, Network.RegTest));
//spendable from bitcoin core wallet!
Assert.False(result.IsWatchOnly);
var tx = await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(address, Network.RegTest),
Money.Coins(3.0m));
await s.Server.ExplorerNode.GenerateAsync(1);
await s.GoToStore(storeId);
await s.GoToWalletSettings();
var url = s.Page.Url;
await s.ClickOnAllSectionLinks("#Nav-Wallets");
// Make sure wallet info is correct
await s.GoToUrl(url);
await s.Page.WaitForSelectorAsync("#AccountKeys_0__MasterFingerprint");
Assert.Equal(mnemonic.DeriveExtKey().GetPublicKey().GetHDFingerPrint().ToString(),
await s.Page.Locator("#AccountKeys_0__MasterFingerprint").GetAttributeAsync("value"));
Assert.Equal("m/84'/1'/0'",
await s.Page.Locator("#AccountKeys_0__AccountKeyPath").GetAttributeAsync("value"));
// Make sure we can rescan, because we are admin!
await s.Page.ClickAsync("#ActionsDropdownToggle");
await s.Page.ClickAsync("#Rescan");
await s.Page.GetByText("The batch size make sure").WaitForAsync();
//
// Check the tx sent earlier arrived
wt = await s.GoToWalletTransactions();
await wt.WaitTransactionsLoaded();
await s.Page.Locator($"[data-text='{tx}']").WaitForAsync();
var walletTransactionUri = new Uri(s.Page.Url);
// Send to bob
var ws = await s.GoToWalletSend();
var bob = new Key().PubKey.Hash.GetAddress(Network.RegTest);
await ws.FillAddress(bob);
await ws.FillAmount(1);
await ws.Sign();
// Back button should lead back to the previous page inside the send wizard
var backUrl = await s.Page.Locator("#GoBack").GetAttributeAsync("href");
Assert.EndsWith($"/send?returnUrl={Uri.EscapeDataString(walletTransactionUri.AbsolutePath)}", backUrl);
// Cancel button should lead to the page that referred to the send wizard
var cancelUrl = await s.Page.Locator("#CancelWizard").GetAttributeAsync("href");
Assert.EndsWith(walletTransactionUri.AbsolutePath, cancelUrl);
// Broadcast
var wb = s.InBroadcast();
await wb.AssertSending(bob, 1.0m);
await wb.Broadcast();
Assert.Equal(walletTransactionUri.ToString(), s.Page.Url);
await s.Page.ClickAsync($"#StoreNav-Wallet{cryptoCode}");
await s.Page.ClickAsync("#WalletNav-Send");
var jack = new Key().PubKey.Hash.GetAddress(Network.RegTest);
await ws.FillAddress(jack);
await ws.FillAmount(0.01m);
await ws.Sign();
await wb.AssertSending(jack, 0.01m);
Assert.EndsWith("psbt/ready", s.Page.Url);
await wb.Broadcast();
await s.FindAlertMessage();
var bip21 = invoice.EntityToDTO(s.Server.PayTester.GetService<Dictionary<PaymentMethodId, IPaymentMethodBitpayAPIExtension>>(), s.Server.PayTester.GetService<CurrencyNameTable>()).CryptoInfo.First().PaymentUrls.BIP21;
//let's make bip21 more interesting
bip21 += "&label=Solid Snake&message=Snake? Snake? SNAAAAKE!";
var parsedBip21 = new BitcoinUrlBuilder(bip21, Network.RegTest);
await s.GoToWalletSend();
// ReSharper disable once AsyncVoidMethod
async void PasteBIP21(object sender, IDialog e)
{
await e.AcceptAsync(bip21);
}
s.Page.Dialog += PasteBIP21;
await s.Page.ClickAsync("#bip21parse");
s.Page.Dialog -= PasteBIP21;
await s.FindAlertMessage(StatusMessageModel.StatusSeverity.Info);
Assert.Equal(parsedBip21.Amount!.ToString(false),
await s.Page.Locator("#Outputs_0__Amount").GetAttributeAsync("value"));
Assert.Equal(parsedBip21.Address!.ToString(),
await s.Page.Locator("#Outputs_0__DestinationAddress").GetAttributeAsync("value"));
await s.Page.ClickAsync("#CancelWizard");
await s.GoToWalletSettings();
var settingsUri = new Uri(s.Page.Url);
await s.Page.ClickAsync("#ActionsDropdownToggle");
await s.Page.ClickAsync("#ViewSeed");
// Seed backup page
var recoveryPhrase = await s.Page.Locator("#RecoveryPhrase").First.GetAttributeAsync("data-mnemonic");
Assert.Equal(mnemonic.ToString(), recoveryPhrase);
Assert.Contains("The recovery phrase will also be stored on the server as a hot wallet.",
await s.Page.ContentAsync());
// No confirmation, just a link to return to the wallet
Assert.Equal(0, await s.Page.Locator("#confirm").CountAsync());
await s.Page.ClickAsync("#proceed");
Assert.Equal(settingsUri.ToString(), s.Page.Url);
// Once more, test the cancel link of the wallet send page leads back to the previous page
await s.Page.ClickAsync("#WalletNav-Send");
cancelUrl = await s.Page.Locator("#CancelWizard").GetAttributeAsync("href");
Assert.EndsWith(settingsUri.AbsolutePath, cancelUrl);
// no previous page in the wizard, hence no back button
Assert.Equal(0, await s.Page.Locator("#GoBack").CountAsync());
await s.Page.ClickAsync("#CancelWizard");
Assert.Equal(settingsUri.ToString(), s.Page.Url);
// Transactions list contains export, ensure functions are present.
await s.GoToWalletTransactions();
await s.Page.ClickAsync(".mass-action-select-all");
await s.Page.Locator("#BumpFee").WaitForAsync();
// JSON export
await s.Page.ClickAsync("#ExportDropdownToggle");
var opening = s.Page.Context.WaitForPageAsync();
await s.Page.ClickAsync("#ExportJSON");
await using (_ = await s.SwitchPage(opening))
{
await s.Page.WaitForLoadStateAsync();
Assert.Contains(s.WalletId.ToString(), s.Page.Url);
Assert.EndsWith("export?format=json", s.Page.Url);
Assert.Contains("\"Amount\": \"3.00000000\"", await s.Page.ContentAsync());
}
// CSV export
await s.Page.ClickAsync("#ExportDropdownToggle");
var download = await s.Page.RunAndWaitForDownloadAsync(async () =>
{
await s.Page.ClickAsync("#ExportCSV");
});
Assert.Contains(tx.ToString(), await File.ReadAllTextAsync(await download.PathAsync()));
// BIP-329 export
await s.Page.ClickAsync("#ExportDropdownToggle");
download = await s.Page.RunAndWaitForDownloadAsync(async () =>
{
await s.Page.ClickAsync("#ExportBIP329");
});
Assert.Contains(tx.ToString(), await File.ReadAllTextAsync(await download.PathAsync()));
}
}
}

View File

@@ -1493,238 +1493,6 @@ namespace BTCPayServer.Tests
}
}
[Fact(Timeout = TestTimeout)]
public async Task CanManageWallet()
{
using var s = CreateSeleniumTester();
await s.StartAsync();
s.RegisterNewUser(true);
(_, string storeId) = s.CreateNewStore();
const string cryptoCode = "BTC";
// 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(cryptoCode, "", true);
//let's test quickly the wallet send page
s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click();
s.Driver.FindElement(By.Id("WalletNav-Send")).Click();
//you cannot use the Sign with NBX option without saving private keys when generating the wallet.
Assert.DoesNotContain("nbx-seed", s.Driver.PageSource);
Assert.True(s.Driver.ElementDoesNotExist(By.Id("GoBack")));
s.Driver.FindElement(By.Id("SignTransaction")).Click();
Assert.Contains("Destination Address field is required", s.Driver.PageSource);
Assert.True(s.Driver.ElementDoesNotExist(By.Id("GoBack")));
s.Driver.FindElement(By.Id("CancelWizard")).Click();
s.Driver.FindElement(By.Id("WalletNav-Receive")).Click();
//generate a receiving address
Assert.True(s.Driver.FindElement(By.CssSelector("#address-tab .qr-container")).Displayed);
// no previous page in the wizard, hence no back button
Assert.True(s.Driver.ElementDoesNotExist(By.Id("GoBack")));
var receiveAddr = s.Driver.FindElement(By.Id("Address")).GetAttribute("data-text");
// Can add a label?
await TestUtils.EventuallyAsync(async () =>
{
s.Driver.WaitForElement(By.CssSelector("div.label-manager input")).Click();
await Task.Delay(500);
s.Driver.WaitForElement(By.CssSelector("div.label-manager input")).SendKeys("test-label" + Keys.Enter);
await Task.Delay(500);
s.Driver.WaitForElement(By.CssSelector("div.label-manager input")).SendKeys("label2" + Keys.Enter);
});
TestUtils.Eventually(() =>
{
s.Driver.Navigate().Refresh();
Assert.NotNull(s.Driver.FindElement(By.CssSelector("[data-value='test-label']")));
});
Assert.True(s.Driver.FindElement(By.CssSelector("#address-tab .qr-container")).Displayed);
Assert.Equal(receiveAddr, s.Driver.FindElement(By.Id("Address")).GetAttribute("data-text"));
TestUtils.Eventually(() =>
{
Assert.Contains("test-label", s.Driver.PageSource);
});
// Remove a label
s.Driver.WaitForElement(By.CssSelector("[data-value='test-label']")).Click();
await Task.Delay(500);
s.Driver.ExecuteJavaScript("var l=document.querySelector('[data-value=\"test-label\"]');l.click();l.nextSibling.dispatchEvent(new KeyboardEvent('keydown', {'key': 'Delete', keyCode: 8}));");
await Task.Delay(500);
await s.Driver.Navigate().RefreshAsync();
Assert.DoesNotContain("test-label", s.Driver.PageSource);
Assert.True(s.Driver.ElementDoesNotExist(By.Id("GoBack")));
//send money to addr and ensure it changed
var sess = await s.Server.ExplorerClient.CreateWebsocketNotificationSessionAsync();
await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(receiveAddr, Network.RegTest),
Money.Parse("0.1"));
await sess.WaitNext<NewTransactionEvent>(e => e.Outputs.FirstOrDefault()?.Address.ToString() == receiveAddr);
await Task.Delay(200);
s.Driver.Navigate().Refresh();
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
Assert.NotEqual(receiveAddr, s.Driver.FindElement(By.Id("Address")).GetAttribute("data-text"));
receiveAddr = s.Driver.FindElement(By.Id("Address")).GetAttribute("data-text");
s.Driver.FindElement(By.Id("CancelWizard")).Click();
// Check the label is applied to the tx
s.Driver.WaitWalletTransactionsLoaded();
// Sometimes this fails in local, but not CI
Assert.Equal("label2", s.Driver.FindElement(By.XPath("//*[@id=\"WalletTransactionsList\"]//*[contains(@class, 'transaction-label')]")).Text);
//change the wallet and ensure old address is not there and generating a new one does not result in the prev one
s.GenerateWallet(cryptoCode, "", true);
s.GoToWallet(null, WalletsNavPages.Receive);
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
Assert.NotEqual(receiveAddr, s.Driver.FindElement(By.Id("Address")).GetAttribute("data-text"));
var invoiceId = s.CreateInvoice(storeId);
var invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId);
var btc = PaymentTypes.CHAIN.GetPaymentMethodId("BTC");
var address = invoice.GetPaymentPrompt(btc).Destination;
//wallet should have been imported to bitcoin core wallet in watch only mode.
var result =
await s.Server.ExplorerNode.GetAddressInfoAsync(BitcoinAddress.Create(address, Network.RegTest));
Assert.True(result.IsWatchOnly);
s.GoToStore(storeId);
var mnemonic = s.GenerateWallet(cryptoCode, "", true, true);
//lets import and save private keys
invoiceId = s.CreateInvoice(storeId);
invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId);
address = invoice.GetPaymentPrompt(btc).Destination;
result = await s.Server.ExplorerNode.GetAddressInfoAsync(
BitcoinAddress.Create(address, Network.RegTest));
//spendable from bitcoin core wallet!
Assert.False(result.IsWatchOnly);
var tx = s.Server.ExplorerNode.SendToAddress(BitcoinAddress.Create(address, Network.RegTest),
Money.Coins(3.0m));
await s.Server.ExplorerNode.GenerateAsync(1);
s.GoToStore(storeId);
s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click();
s.ClickOnAllSectionLinks();
// Make sure wallet info is correct
s.GoToWalletSettings(cryptoCode);
Assert.Contains(mnemonic.DeriveExtKey().GetPublicKey().GetHDFingerPrint().ToString(),
s.Driver.FindElement(By.Id("AccountKeys_0__MasterFingerprint")).GetAttribute("value"));
Assert.Contains("m/84'/1'/0'",
s.Driver.FindElement(By.Id("AccountKeys_0__AccountKeyPath")).GetAttribute("value"));
// Make sure we can rescan, because we are admin!
s.Driver.FindElement(By.Id("ActionsDropdownToggle")).Click();
s.Driver.FindElement(By.Id("Rescan")).Click();
Assert.Contains("The batch size make sure", s.Driver.PageSource);
// Check the tx sent earlier arrived
s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click();
s.Driver.WaitWalletTransactionsLoaded();
s.Driver.FindElement(By.CssSelector($"[data-text='{tx}']"));
var walletTransactionUri = new Uri(s.Driver.Url);
// Send to bob
s.Driver.FindElement(By.Id("WalletNav-Send")).Click();
var bob = new Key().PubKey.Hash.GetAddress(Network.RegTest);
SetTransactionOutput(s, 0, bob, 1);
s.Driver.FindElement(By.Id("SignTransaction")).Click();
// Back button should lead back to the previous page inside the send wizard
var backUrl = s.Driver.FindElement(By.Id("GoBack")).GetAttribute("href");
Assert.EndsWith($"/send?returnUrl={Uri.EscapeDataString(walletTransactionUri.AbsolutePath)}", backUrl);
// Cancel button should lead to the page that referred to the send wizard
var cancelUrl = s.Driver.FindElement(By.Id("CancelWizard")).GetAttribute("href");
Assert.EndsWith(walletTransactionUri.AbsolutePath, cancelUrl);
// Broadcast
Assert.Contains(bob.ToString(), s.Driver.PageSource);
Assert.Contains("1.00000000", s.Driver.PageSource);
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click();
Assert.Equal(walletTransactionUri.ToString(), s.Driver.Url);
s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click();
s.Driver.FindElement(By.Id("WalletNav-Send")).Click();
var jack = new Key().PubKey.Hash.GetAddress(Network.RegTest);
SetTransactionOutput(s, 0, jack, 0.01m);
s.Driver.FindElement(By.Id("SignTransaction")).Click();
s.Driver.WaitForElement(By.CssSelector("button[value=broadcast]"));
Assert.Contains(jack.ToString(), s.Driver.PageSource);
Assert.Contains("0.01000000", s.Driver.PageSource);
Assert.EndsWith("psbt/ready", s.Driver.Url);
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).Click();
Assert.Equal(walletTransactionUri.ToString(), s.Driver.Url);
var bip21 = invoice.EntityToDTO(s.Server.PayTester.GetService<Dictionary<PaymentMethodId, IPaymentMethodBitpayAPIExtension>>(), s.Server.PayTester.GetService<CurrencyNameTable>()).CryptoInfo.First().PaymentUrls.BIP21;
//let's make bip21 more interesting
bip21 += "&label=Solid Snake&message=Snake? Snake? SNAAAAKE!";
var parsedBip21 = new BitcoinUrlBuilder(bip21, Network.RegTest);
s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click();
s.Driver.FindElement(By.Id("WalletNav-Send")).Click();
s.Driver.FindElement(By.Id("bip21parse")).Click();
s.Driver.SwitchTo().Alert().SendKeys(bip21);
s.Driver.SwitchTo().Alert().Accept();
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.Driver.FindElement(By.Id("CancelWizard")).Click();
s.GoToWalletSettings(cryptoCode);
var settingsUri = new Uri(s.Driver.Url);
s.Driver.FindElement(By.Id("ActionsDropdownToggle")).Click();
s.Driver.FindElement(By.Id("ViewSeed")).Click();
// Seed backup page
var recoveryPhrase = s.Driver.FindElements(By.Id("RecoveryPhrase")).First()
.GetAttribute("data-mnemonic");
Assert.Equal(mnemonic.ToString(), recoveryPhrase);
Assert.Contains("The recovery phrase will also be stored on the server as a hot wallet.",
s.Driver.PageSource);
// No confirmation, just a link to return to the wallet
Assert.Empty(s.Driver.FindElements(By.Id("confirm")));
s.Driver.FindElement(By.Id("proceed")).Click();
Assert.Equal(settingsUri.ToString(), s.Driver.Url);
// Once more, test the cancel link of the wallet send page leads back to the previous page
s.Driver.FindElement(By.Id("WalletNav-Send")).Click();
cancelUrl = s.Driver.FindElement(By.Id("CancelWizard")).GetAttribute("href");
Assert.EndsWith(settingsUri.AbsolutePath, cancelUrl);
// no previous page in the wizard, hence no back button
Assert.True(s.Driver.ElementDoesNotExist(By.Id("GoBack")));
s.Driver.FindElement(By.Id("CancelWizard")).Click();
Assert.Equal(settingsUri.ToString(), s.Driver.Url);
// Transactions list contains export, ensure functions are present.
s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click();
s.Driver.FindElement(By.ClassName("mass-action-select-all")).Click();
s.Driver.FindElement(By.Id("BumpFee"));
// JSON export
s.Driver.FindElement(By.Id("ExportDropdownToggle")).Click();
s.Driver.FindElement(By.Id("ExportJSON")).Click();
Thread.Sleep(1000);
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
Assert.Contains(s.WalletId.ToString(), s.Driver.Url);
Assert.EndsWith("export?format=json", s.Driver.Url);
Assert.Contains("\"Amount\": \"3.00000000\"", s.Driver.PageSource);
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
// CSV export
s.Driver.FindElement(By.Id("ExportDropdownToggle")).Click();
s.Driver.FindElement(By.Id("ExportCSV")).Click();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
// BIP-329 export
s.Driver.FindElement(By.Id("ExportDropdownToggle")).Click();
s.Driver.FindElement(By.Id("ExportBIP329")).Click();
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
}
[Fact]
[Trait("Selenium", "Selenium")]
[Trait("Lightning", "Lightning")]

View File

@@ -1,4 +1,5 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=BIP/@EntryIndexedValue">BIP</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=BTC/@EntryIndexedValue">BTC</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=CPFP/@EntryIndexedValue">CPFP</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=HWI/@EntryIndexedValue">HWI</s:String>