Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Feat e2e convert to pw suite coinmarket #16065

Merged
merged 14 commits into from
Dec 28, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/test-suite-desktop-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ jobs:
# CONTAINERS: "trezor-user-env-unix"
# - TEST_GROUP: "@group=passphrase"
# CONTAINERS: "trezor-user-env-unix"
# - TEST_GROUP: "@group=other"
# CONTAINERS: "trezor-user-env-unix"
- TEST_GROUP: "@group=other"
CONTAINERS: "trezor-user-env-unix"
- TEST_GROUP: "@group=wallet"
CONTAINERS: "trezor-user-env-unix"

Expand Down
6 changes: 4 additions & 2 deletions .github/workflows/test-suite-web-e2e-pw.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ on:
- ".github/workflows/release*"
- ".github/workflows/template*"
- ".github/actions/release*/**"
schedule:
- cron: "0 0 * * *"
Comment on lines +35 to +36
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Configures the Web tests to be run as nightly. Copied from desktop pipeline

workflow_dispatch:

env:
Expand Down Expand Up @@ -103,8 +105,8 @@ jobs:
# CONTAINERS: "trezor-user-env-unix"
# - TEST_GROUP: "@group=passphrase"
# CONTAINERS: "trezor-user-env-unix"
# - TEST_GROUP: "@group=other"
# CONTAINERS: "trezor-user-env-unix"
- TEST_GROUP: "@group=other"
CONTAINERS: "trezor-user-env-unix"
- TEST_GROUP: "@group=wallet"
CONTAINERS: "trezor-user-env-unix bitcoin-regtest"

Expand Down
8 changes: 7 additions & 1 deletion packages/suite-desktop-core/e2e/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,25 @@ const config: PlaywrightTestConfig = {
testDir: 'tests',
workers: 1, // to disable parallelism between test files
use: {
viewport: { width: 1280, height: 720 },
headless: process.env.HEADLESS === 'true',
trace: 'on',
video: 'on',
screenshot: 'on',
testIdAttribute: 'data-testid',
actionTimeout: 30000,
Copy link
Contributor Author

@Vere-Grey Vere-Grey Dec 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had some issue with traces and fails. Once I removed this forced timeout, it got fixed. Every action has its default timeout. So I suggest lets leave it to carefully chosen defaults by the playwright devs.

},
reportSlowTests: null,
reporter: process.env.GITHUB_ACTION
? [['list'], ['@currents/playwright'], ['html', { open: 'never' }]]
: [['list'], ['html', { open: 'never' }]],
timeout: process.env.GITHUB_ACTION ? timeoutCIRun : timeoutLocalRun,
outputDir: path.join(__dirname, 'test-results'),
snapshotPathTemplate: 'snapshots/{projectName}/{testFilePath}/{arg}{ext}',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By default snapshots are stored in folder with test file. Which can lead to cluttered test folders. So this configuration puts snapshot to their own Folder.
On the other hand I am not sure how easy will be to find corresponding snapshot here. We will see and improve based on our experience

expect: {
toHaveScreenshot: {
maxDiffPixelRatio: 0.025,
},
},
};

// eslint-disable-next-line import/no-default-export
Expand Down
Vere-Grey marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure whether the receiving address will stay same all the time :/

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions packages/suite-desktop-core/e2e/support/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { AnalyticsActions } from './pageActions/analyticsActions';
import { IndexedDbFixture } from './indexedDb';
import { RecoverActions } from './pageActions/recoverActions';
import { WordInputActions } from './pageActions/wordInputActions';
import { MarketActions } from './pageActions/marketActions';

type Fixtures = {
startEmulator: boolean;
Expand All @@ -44,6 +45,7 @@ type Fixtures = {
wordInputPage: WordInputActions;
analytics: AnalyticsFixture;
indexedDb: IndexedDbFixture;
marketPage: MarketActions;
};

const test = base.extend<Fixtures>({
Expand Down Expand Up @@ -177,6 +179,10 @@ const test = base.extend<Fixtures>({
const indexedDb = new IndexedDbFixture(page);
await use(indexedDb);
},
marketPage: async ({ page }, use) => {
const marketPage = new MarketActions(page);
await use(marketPage);
},
});

export { test };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { Locator, Page, expect } from '@playwright/test';
import { NetworkSymbol } from '@suite-common/wallet-config';

export class DashboardActions {
private readonly page: Page;
readonly dashboardMenuButton: Locator;
readonly discoveryHeader: Locator;
readonly discoveryBar: Locator;
Expand All @@ -19,8 +18,7 @@ export class DashboardActions {
readonly balanceOfNetwork = (symbol: NetworkSymbol) =>
this.page.getByTestId(`@wallet/coin-balance/value-${symbol}`);

constructor(page: Page) {
this.page = page;
constructor(private readonly page: Page) {
this.dashboardMenuButton = this.page.getByTestId('@suite/menu/suite-index');
this.discoveryHeader = this.page.getByRole('heading', { name: 'Dashboard' });
this.discoveryBar = this.page.getByTestId('@wallet/discovery-progress-bar');
Expand Down
112 changes: 112 additions & 0 deletions packages/suite-desktop-core/e2e/support/pageActions/marketActions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { Locator, Page, expect } from '@playwright/test';

import { TrezorUserEnvLink } from '@trezor/trezor-user-env-link';

import { WalletActions } from './walletActions';

export class MarketActions {
readonly offerSpinner: Locator;
readonly layout: Locator;
readonly form: Locator;
readonly bestOfferProvider: Locator;
readonly bestOfferAmount: Locator;
readonly buyBestOfferButton: Locator;
readonly youPayInput: Locator;
readonly buyOffersPage: Locator;
readonly compareButton: Locator;
readonly quotes: Locator;
readonly quoteOfProvider = (provider: string) =>
this.page.getByTestId(`@coinmarket/offers/quote-${provider}`);
readonly quoteProvider: Locator;
readonly quoteAmount: Locator;
readonly selectThisQuoteButton: Locator;
readonly modal: Locator;
readonly buyTermsConfirmButton: Locator;
readonly confirmOnTrezorButton: Locator;
readonly confirmOnDevicePrompt: Locator;
readonly tradeConfirmation: Locator;
readonly tradeConfirmationCryptoAmount: Locator;
readonly tradeConfirmationProvider: Locator;
readonly tradeConfirmationContinueButton: Locator;

constructor(private page: Page) {
this.offerSpinner = this.page.getByTestId('@coinmarket/offers/loading-spinner');
this.layout = this.page.getByTestId('@coinmarket');
this.form = this.page.getByTestId('@coinmarket/form');
this.bestOfferProvider = this.page.getByTestId('@coinmarket/offers/quote/provider');
this.bestOfferAmount = this.page.getByTestId('@coinmarket/form/offer/crypto-amount');
this.buyBestOfferButton = this.page.getByTestId('@coinmarket/form/buy-button');
this.youPayInput = this.page.getByTestId('@coinmarket/form/fiat-input');
this.buyOffersPage = this.page.getByTestId('@coinmarket/buy-offers');
this.compareButton = this.page.getByTestId('@coinmarket/form/compare-button');
this.quotes = this.page.getByTestId('@coinmarket/offers/quote');
this.quoteProvider = this.page.getByTestId('@coinmarket/offers/quote/provider');
this.quoteAmount = this.page.getByTestId('@coinmarket/offers/quote/crypto-amount');
this.selectThisQuoteButton = this.page.getByTestId(
'@coinmarket/offers/get-this-deal-button',
);
this.modal = this.page.getByTestId('@modal');
this.buyTermsConfirmButton = this.page.getByTestId(
'@coinmarket/buy/offers/buy-terms-confirm-button',
);
this.confirmOnTrezorButton = this.page.getByTestId(
'@coinmarket/offer/confirm-on-trezor-button',
);
this.confirmOnDevicePrompt = this.page.getByTestId('@prompts/confirm-on-device');
this.tradeConfirmation = this.page.getByTestId('@coinmarket/selected-offer');
this.tradeConfirmationCryptoAmount = this.page.getByTestId(
'@coinmarket/form/info/crypto-amount',
);
this.tradeConfirmationProvider = this.page.getByTestId('@coinmarket/form/info/provider');
this.tradeConfirmationContinueButton = this.page.getByTestId(
'@coinmarket/offer/continue-transaction-button',
);
}

openCoinMarket = async () => {
const walletPage = new WalletActions(this.page);
Vere-Grey marked this conversation as resolved.
Show resolved Hide resolved
await walletPage.accountMenuButton.click();
//TODO: #16073 We cannot set resolution for Electron. on CI button is hidden under dropdown due to a breakpoint
const isBuyButtonHidden = !(await walletPage.coinMarketBuyButton.isVisible());
if (isBuyButtonHidden) {
await walletPage.walletExtraDropDown.click();
await walletPage.coinMarketDropdownBuyButton.click();
} else {
await walletPage.coinMarketBuyButton.click();
}
};

waitForOffersSyncToFinish = async () => {
await expect(this.offerSpinner).toBeHidden({ timeout: 30000 });
};

setYouPayAmount = async (amount: string) => {
//Warning: the field is initialized empty and gets default value after the first offer sync
await expect(this.youPayInput).not.toHaveValue('');
await this.waitForOffersSyncToFinish();
await this.youPayInput.fill(amount);
//Warning: Bug #16054, as a workaround we wait for offer sync after setting the amount
await this.waitForOffersSyncToFinish();
};

confirmTrade = async () => {
await expect(this.modal).toBeVisible();
await this.buyTermsConfirmButton.click();
await this.confirmOnTrezorButton.click();
await expect(this.confirmOnDevicePrompt).toBeVisible();
await TrezorUserEnvLink.pressYes();
await expect(this.confirmOnDevicePrompt).not.toBeVisible();
};

readBestOfferValues = async () => {
const amount = await this.bestOfferAmount.textContent();
const provider = await this.bestOfferProvider.textContent();
if (!amount || !provider) {
throw new Error(
`Test was not able to extract amount or provider from the page. Amount: ${amount}, Provider: ${provider}`,
);
}

return { amount, provider };
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import { PlaywrightProjects } from '../../playwright.config';
import { AnalyticsActions } from './analyticsActions';

export class OnboardingActions {
readonly model: Model;
readonly testInfo: TestInfo;
readonly welcomeTitle: Locator;
readonly onboardingContinueButton: Locator;
readonly onboardingViewOnlySkipButton: Locator;
Expand All @@ -34,11 +32,9 @@ export class OnboardingActions {
constructor(
public page: Page,
private analyticsPage: AnalyticsActions,
model: Model,
testInfo: TestInfo,
private readonly model: Model,
private readonly testInfo: TestInfo,
) {
this.model = model;
this.testInfo = testInfo;
this.welcomeTitle = this.page.getByTestId('@welcome/title');
this.onboardingContinueButton = this.page.getByTestId('@onboarding/exit-app-button');
this.onboardingViewOnlySkipButton = this.page.getByTestId('@onboarding/viewOnly/skip');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,6 @@ const backgroundImages = {
};

export class SettingsActions {
private readonly page: Page;
private readonly apiURL: string;
private readonly TIMES_CLICK_TO_SET_DEBUG_MODE = 5;
readonly settingsMenuButton: Locator;
readonly settingsHeader: Locator;
Expand Down Expand Up @@ -75,9 +73,10 @@ export class SettingsActions {
this.page.getByTestId(`@settings/language-select/option/${language}`);
readonly pinInput = (index: number) => this.page.getByTestId(`@pin/input/${index}`);

constructor(page: Page, apiURL: string) {
this.page = page;
this.apiURL = apiURL;
constructor(
private readonly page: Page,
private readonly apiURL: string,
) {
this.settingsMenuButton = this.page.getByTestId('@suite/menu/settings');
this.settingsHeader = this.page.getByTestId('@settings/menu/title');
this.debugTabButton = this.page.getByTestId('@settings/menu/debug');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { capitalizeFirstLetter } from '@trezor/utils';
const anyTestIdEndingWithClose = '[data-testid$="close"]';

export class SuiteGuide {
private readonly page: Page;
readonly guideButton: Locator;
readonly supportAndFeedbackButton: Locator;
readonly bugFormButton: Locator;
Expand All @@ -27,8 +26,7 @@ export class SuiteGuide {
readonly feedbackSuccessToast: Locator;
readonly articleHeader: Locator;

constructor(page: Page) {
this.page = page;
constructor(private readonly page: Page) {
this.guideButton = this.page.getByTestId('@guide/button-open');
this.supportAndFeedbackButton = this.page.getByTestId('@guide/button-feedback');
this.bugFormButton = this.page.getByTestId('@guide/feedback/bug');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,18 @@ import { Locator, Page, expect } from '@playwright/test';
import { NetworkSymbol } from '@suite-common/wallet-config';

export class WalletActions {
private readonly page: Page;
readonly walletMenuButton: Locator;
readonly searchInput: Locator;
readonly accountChevron: Locator;
readonly cardanoAccountLabels: { [key: string]: Locator };
readonly walletStakingButton: Locator;
readonly stakeAddress: Locator;
readonly accountMenuButton: Locator;
readonly walletExtraDropDown: Locator;
readonly coinMarketBuyButton: Locator;
readonly coinMarketDropdownBuyButton: Locator;

constructor(page: Page) {
this.page = page;
constructor(private readonly page: Page) {
this.walletMenuButton = this.page.getByTestId('@suite/menu/wallet-index');
this.searchInput = this.page.getByTestId('@wallet/accounts/search-icon');
this.accountChevron = this.page.getByTestId('@account-menu/arrow');
Expand All @@ -23,6 +25,12 @@ export class WalletActions {
};
this.walletStakingButton = this.page.getByTestId('@wallet/menu/staking');
this.stakeAddress = this.page.getByTestId('@cardano/staking/address');
this.accountMenuButton = this.page.getByTestId('@account-menu/btc/normal/0');
this.walletExtraDropDown = this.page.getByTestId('@wallet/menu/extra-dropdown');
this.coinMarketBuyButton = this.page.getByTestId('@wallet/menu/wallet-coinmarket-buy');
this.coinMarketDropdownBuyButton = this.page
.getByRole('list')
.getByTestId('@wallet/menu/wallet-coinmarket-buy');
}

async filterTransactions(transaction: string) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { test, expect } from '../../support/fixtures';

const regexpBtcValue = /^\d+(\.\d+)? BTC$/;

test.describe('Coin market buy', { tag: ['@group=other'] }, () => {
test.use({ emulatorStartConf: { wipe: true } });
test.beforeEach(async ({ onboardingPage, dashboardPage, marketPage }) => {
await onboardingPage.completeOnboarding();
await dashboardPage.discoveryShouldFinish();
await marketPage.openCoinMarket();
});

test('Buy crypto from compared offers', async ({ marketPage }) => {
await test.step('Fill input amount and opens offer comparison', async () => {
await marketPage.setYouPayAmount('500');
await expect(marketPage.layout).toHaveScreenshot('buy-coins-layout.png', {
mask: [marketPage.bestOfferAmount, marketPage.bestOfferProvider],
});
await marketPage.compareButton.click();
});

await test.step('Check offers and chooses the first one', async () => {
// TOOD: #16041 Once solved, add verification of offer compare items
await expect(marketPage.buyOffersPage).toBeVisible();
expect(await marketPage.quotes.count()).toBeGreaterThan(1);
await marketPage.selectThisQuoteButton.first().click();
});

await test.step('Confirm trade and verifies confirmation summary', async () => {
await marketPage.confirmTrade();
await expect(marketPage.tradeConfirmation).toHaveScreenshot(
'compared-offers-buy-confirmation.png',
{
mask: [
marketPage.tradeConfirmationCryptoAmount,
marketPage.tradeConfirmationProvider,
],
},
);
// TOOD: #16041 Once solved, Assert mocked price
await expect(marketPage.tradeConfirmationCryptoAmount).toHaveText(regexpBtcValue);
});
await expect(marketPage.tradeConfirmationContinueButton).toBeEnabled();
});

test('Buy crypto from best offer', async ({ marketPage }) => {
await marketPage.setYouPayAmount('500');
const { amount, provider } = await marketPage.readBestOfferValues();
await marketPage.buyBestOfferButton.click();
await marketPage.confirmTrade();
await expect(marketPage.tradeConfirmation).toHaveScreenshot(
'best-offer-buy-confirmation.png',
{
mask: [
marketPage.tradeConfirmationCryptoAmount,
marketPage.tradeConfirmationProvider,
],
},
);
await expect(marketPage.tradeConfirmationCryptoAmount).toHaveText(amount);
await expect(marketPage.tradeConfirmationProvider).toHaveText(provider);
await expect(marketPage.tradeConfirmationContinueButton).toBeEnabled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,7 @@ const CoinmarketBuyOffersComponent = ({ selectedAccount }: UseCoinmarketProps) =
};

export const CoinmarketBuyOffers = () => (
<CoinmarketContainer SectionComponent={CoinmarketBuyOffersComponent} />
<span data-testid="@coinmarket/buy-offers">
<CoinmarketContainer SectionComponent={CoinmarketBuyOffersComponent} />
</span>
);
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const CoinmarketFormLayoutWrapper = styled.form`
`;

export const CoinmarketFormLayout = () => (
<Column gap={spacings.xxxxl}>
<Column gap={spacings.xxxxl} data-testid="@coinmarket/form">
<CoinmarketFormLayoutWrapper>
<Card>
<Column gap={spacings.lg}>
Expand Down
Loading
Loading