From 441b7a80c780aa7a63da227af68181362afeb67c Mon Sep 17 00:00:00 2001 From: Celine Sarafa Date: Fri, 1 Mar 2024 14:45:58 +0100 Subject: [PATCH] Test/e2e phase 1 (#450) * chore: rename modalPage as it is not a modalPage * test: add updatePreferences test * chore: count subscribed dapps * test: add message test * chore: fix bugs in tests * chore: expose env in workflow * chore: pass duplex option for webkit * Update tests/shared/helpers/notifyServer.ts Co-authored-by: Ben Kremer * Apply suggestions from code review Co-authored-by: Chris Smith <1979423+chris13524@users.noreply.github.com> Co-authored-by: Ben Kremer * chore: update env vars * chore: remove need for clipboard, and stream in fetch * chore: remove unused prop * chore: wait for dapps to be subscribed * chore: disable webkit * chore: remove extra subscribe * chore: remove unhelpful logs * Update tests/shared/pages/InboxPage.ts Co-authored-by: Enes * Update tests/shared/pages/InboxPage.ts Co-authored-by: Enes * chore: remove empty space * chore: refactor waitForSubscriptions --------- Co-authored-by: Ben Kremer Co-authored-by: Chris Smith <1979423+chris13524@users.noreply.github.com> Co-authored-by: Enes --- .github/workflows/ui_test.yml | 2 + playwright.config.ts | 4 - tests/shared/constants/index.ts | 10 ++ tests/shared/fixtures/fixture.ts | 36 ++++-- tests/shared/helpers/notifyServer.ts | 53 +++++++++ tests/shared/pages/InboxPage.ts | 83 +++++++++++++- tests/shared/pages/SettingsPage.ts | 21 ++++ tests/shared/validators/ModalValidator.ts | 2 +- tests/subscribe.spec.ts | 131 ++++++++++++++++++++-- 9 files changed, 311 insertions(+), 31 deletions(-) create mode 100644 tests/shared/helpers/notifyServer.ts create mode 100644 tests/shared/pages/SettingsPage.ts diff --git a/.github/workflows/ui_test.yml b/.github/workflows/ui_test.yml index da8f8501..c5541366 100644 --- a/.github/workflows/ui_test.yml +++ b/.github/workflows/ui_test.yml @@ -29,6 +29,8 @@ jobs: env: VITE_PROJECT_ID: ${{ secrets.VITE_DEV_PROJECT_ID }} VITE_EXPLORER_API_URL: ${{ secrets.VITE_EXPLORER_API_URL }} + TEST_DAPP_PROJECT_ID: ${{ secrets.TEST_DAPP_PROJECT_ID }} + TEST_DAPP_PROJECT_SECRET: ${{ secrets.TEST_DAPP_PROJECT_SECRET }} VITE_CI: true - uses: actions/upload-artifact@v3 if: always() diff --git a/playwright.config.ts b/playwright.config.ts index 901a8c2a..7350c995 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -37,10 +37,6 @@ export default defineConfig({ name: 'firefox', use: { ...devices['Desktop Firefox'] } }, - { - name: 'webkit', - use: { ...devices['Desktop Safari'] } - } ], /* Run your local dev server before starting the tests */ diff --git a/tests/shared/constants/index.ts b/tests/shared/constants/index.ts index 27c19328..97179939 100644 --- a/tests/shared/constants/index.ts +++ b/tests/shared/constants/index.ts @@ -8,3 +8,13 @@ export const DEFAULT_SESSION_PARAMS: SessionParams = { optAccounts: ['1', '2'], accept: true } + +export const CUSTOM_TEST_DAPP = { + description: "Test description", + icons: ["https://i.imgur.com/q9QDRXc.png"], + name: "Notify Swift Integration Tests Prod", + appDomain: "wc-notify-swift-integration-tests-prod.pages.dev", + projectSecret: process.env['TEST_DAPP_PROJECT_SECRET'], + projectId: process.env['TEST_DAPP_PROJECT_ID'], + messageType: "f173f231-a45c-4dc0-aa5d-956eb04f7360" +} as const; diff --git a/tests/shared/fixtures/fixture.ts b/tests/shared/fixtures/fixture.ts index 08f28131..837cc8b1 100644 --- a/tests/shared/fixtures/fixture.ts +++ b/tests/shared/fixtures/fixture.ts @@ -1,24 +1,38 @@ import { test as base } from '@playwright/test' -import { ModalPage } from '../pages/InboxPage' -import { ModalValidator } from '../validators/ModalValidator' +import { InboxPage } from '../pages/InboxPage' +import { InboxValidator } from '../validators/ModalValidator' +import { SettingsPage } from '../pages/SettingsPage' +import { NotifyServer } from '../helpers/notifyServer' // Declare the types of fixtures to use export interface ModalFixture { - modalPage: ModalPage - modalValidator: ModalValidator + inboxPage: InboxPage + inboxValidator: InboxValidator + settingsPage: SettingsPage + notifyServer: NotifyServer library: string } export const test = base.extend({ - modalPage: async ({ page }, use) => { - const modalPage = new ModalPage(page) - await modalPage.load() - await use(modalPage) + inboxPage: async ({ page }, use) => { + const inboxPage = new InboxPage(page) + await inboxPage.load() + await use(inboxPage) }, - modalValidator: async ({ modalPage }, use) => { - const modalValidator = new ModalValidator(modalPage.page) + inboxValidator: async ({ inboxPage }, use) => { + const modalValidator = new InboxValidator(inboxPage.page) await use(modalValidator) - } + }, + // Have to pass same page object to maintain state between pages + settingsPage: async({ inboxPage }, use) => { + const settingsPage = new SettingsPage(inboxPage.page) + settingsPage.load() + use(settingsPage) + }, + notifyServer: async({}, use) => { + const notifyServer = new NotifyServer(); + use(notifyServer) + }, }) export { expect } from '@playwright/test' diff --git a/tests/shared/helpers/notifyServer.ts b/tests/shared/helpers/notifyServer.ts new file mode 100644 index 00000000..620636ce --- /dev/null +++ b/tests/shared/helpers/notifyServer.ts @@ -0,0 +1,53 @@ +import { expect } from "@playwright/test" + +export class NotifyServer { + private notifyBaseUrl = "https://notify.walletconnect.com" + + public async sendMessage({ + projectId, + projectSecret, + accounts, + url, + title, + body, + icon, + type + }: { + projectId: string, + projectSecret: string, + accounts: string[] + title: string, + body: string, + icon: string, + url: string + type: string + }) { + const request = JSON.stringify({ + accounts, + notification: { + title, + body, + icon, + url, + type + } + }) + + const fetchUrl = `${this.notifyBaseUrl}/${projectId}/notify` + + const headers = new Headers({ + Authorization: `Bearer ${projectSecret}`, + "Content-Type": "application/json" + }) + + const fetchResults = await fetch(fetchUrl, { + method: "POST", + headers, + body: request + }) + + console.log({fetchResultsStatus: fetchResults.status, fetchResults: await fetchResults.text()}) + + expect(fetchResults.status).toEqual(200) + } +} diff --git a/tests/shared/pages/InboxPage.ts b/tests/shared/pages/InboxPage.ts index fbff74f3..55a76dd5 100644 --- a/tests/shared/pages/InboxPage.ts +++ b/tests/shared/pages/InboxPage.ts @@ -2,7 +2,7 @@ import { type Locator, type Page, expect } from '@playwright/test' import { BASE_URL } from '../constants' -export class ModalPage { +export class InboxPage { private readonly baseURL = BASE_URL private readonly connectButton: Locator @@ -15,6 +15,13 @@ export class ModalPage { await this.page.goto(this.baseURL) } + async gotoDiscoverPage() { + await this.page.locator('.Sidebar__Navigation__Link[href="/notifications"]').click() + await this.page.getByText('Discover Apps').click(); + + await this.page.getByText('Discover Web3Inbox').isVisible(); + } + async copyConnectUriToClipboard() { await this.page.goto(this.baseURL) await this.connectButton.click() @@ -40,14 +47,32 @@ export class ModalPage { await this.page.locator('.NotificationPwaModal__close-button').first().click() } + async getAddress() { + await this.page.locator('.Avatar').first().click() + const address = await this.page.locator('wui-avatar').getAttribute('alt') + await this.page.locator('wui-icon[name=close]').first().click(); + + return address; + } + async subscribe(nth: number) { - await this.page.locator('.AppCard__body > .AppCard__body__subscribe').nth(nth).click() - await this.page.getByText('Subscribed to', { exact: false }).isVisible() + const appCard = this.page.locator('.AppCard__body').nth(nth) + await appCard.locator('.AppCard__body__subscribe').click() + + await appCard.locator('.AppCard__body__subscribed').getByText('Subscribed', { exact: false }).isVisible() } - async unsubscribe(nth: number) { + async navigateToNewSubscription(nth: number) { await this.page.getByRole('button', { name: 'Subscribed' }).nth(nth).click() await this.page.getByRole('button', { name: 'Subscribed' }).nth(nth).isHidden() + } + + async subscribeAndNavigateToDapp(nth: number) { + await this.subscribe(nth); + await this.navigateToNewSubscription(nth); + } + + async unsubscribe() { await this.page.locator('.AppNotificationsHeader__wrapper > .Dropdown').click() await this.page.getByRole('button', { name: 'Unsubscribe' }).click() await this.page.getByRole('button', { name: 'Unsubscribe' }).nth(1).click() @@ -55,6 +80,56 @@ export class ModalPage { await this.page.waitForTimeout(2000) } + async navigateToDappFromSidebar(nth: number) { + await this.page.locator('.AppSelector__notifications-link').nth(nth).click() + } + + async countSubscribedDapps() { + const notificationsCount = await this.page.locator('.AppSelector__notifications').count() + + return notificationsCount - 1; + } + + /** + * Waits for a specific number of dApps to be subscribed. + * + * @param {number} expectedCount - The expected number of dApps to wait for. + * @returns {Promise} + */ + async waitForSubscriptions(expectedCount: number): Promise { + // Wait for a function that checks the length of a list or a set of elements + // matching a certain condition to equal the expectedCount. + await this.page.waitForFunction(([className, count]) => { + const elements = document.getElementsByClassName(className)[1].children;; + return elements.length === count; + }, ['AppSelector__list', expectedCount] as const, { timeout: 5000 }); + } + + async updatePreferences() { + await this.page.locator('.AppNotificationsHeader__wrapper > .Dropdown').click() + await this.page.getByRole('button', { name: 'Preferences' }).click() + // Ensure the modal is visible + await this.page.getByText('Preferences').nth(1).isVisible() + await this.page.getByText('Preferences').nth(1).click() + + const firstCheckBoxIsChecked = await this.page.isChecked('.Toggle__checkbox:nth-of-type(1)') + await expect(this.page.locator('.Toggle__label').first()).toBeVisible() + + await this.page.locator('.Toggle').first().click() + + await this.page.getByRole('button', { name: 'Update' }).click() + + + await this.page.locator('.AppNotificationsHeader__wrapper > .Dropdown').click() + await this.page.getByRole('button', { name: 'Preferences' }).click() + + const firstCheckBoxIsCheckedAfterUpdating = await this.page.isChecked('.Toggle__checkbox:nth-of-type(1)') + + expect(firstCheckBoxIsChecked).not.toEqual(firstCheckBoxIsCheckedAfterUpdating) + + await this.page.locator('.PreferencesModal__close').click(); + } + async cancelSiwe() { await this.page.getByTestId('w3m-connecting-siwe-cancel').click() } diff --git a/tests/shared/pages/SettingsPage.ts b/tests/shared/pages/SettingsPage.ts new file mode 100644 index 00000000..cbb46e70 --- /dev/null +++ b/tests/shared/pages/SettingsPage.ts @@ -0,0 +1,21 @@ +import { type Locator, type Page, expect } from '@playwright/test' + +import { BASE_URL } from '../../shared/constants' + +export class SettingsPage { + private readonly baseURL = BASE_URL + + constructor(public readonly page: Page) {} + + async load() {} + + async goToNotificationSettings() { + await this.page.locator('.Sidebar__Navigation__Link[href="/settings"]').click() + } + + async displayCustomDapp(dappUrl: string) { + await this.page.getByPlaceholder('app.example.com').fill(dappUrl) + await this.page.getByRole('button', { name: "Save", exact: true}).click() + } + +} diff --git a/tests/shared/validators/ModalValidator.ts b/tests/shared/validators/ModalValidator.ts index 6405b8bc..b043219d 100644 --- a/tests/shared/validators/ModalValidator.ts +++ b/tests/shared/validators/ModalValidator.ts @@ -1,7 +1,7 @@ import { expect } from '@playwright/test' import type { Page } from '@playwright/test' -export class ModalValidator { +export class InboxValidator { constructor(public readonly page: Page) {} async expectConnected() { diff --git a/tests/subscribe.spec.ts b/tests/subscribe.spec.ts index aa792158..cb54d4cc 100644 --- a/tests/subscribe.spec.ts +++ b/tests/subscribe.spec.ts @@ -1,23 +1,23 @@ -import { DEFAULT_SESSION_PARAMS } from './shared/constants' -import { testWallet as test } from './shared/fixtures/wallet-fixture' +import { CUSTOM_TEST_DAPP, DEFAULT_SESSION_PARAMS } from './shared/constants' +import { expect, testWallet as test } from './shared/fixtures/wallet-fixture' -test.beforeEach(async ({ modalPage, walletPage, browserName }) => { +test.beforeEach(async ({ inboxPage, walletPage, browserName }) => { if (browserName === 'webkit') { // Clipboard doesn't work here. Remove this when we moved away from Clipboard in favor of links test.skip() } - await modalPage.copyConnectUriToClipboard() + await inboxPage.copyConnectUriToClipboard() await walletPage.connect() await walletPage.handleSessionProposal(DEFAULT_SESSION_PARAMS) }) -test.afterEach(async ({ modalValidator, walletValidator }) => { - await modalValidator.expectDisconnected() +test.afterEach(async ({ inboxValidator, walletValidator }) => { + await inboxValidator.expectDisconnected() await walletValidator.expectDisconnected() }) test('it should subscribe and unsubscribe', async ({ - modalPage, + inboxPage, walletPage, walletValidator, browserName @@ -26,10 +26,119 @@ test('it should subscribe and unsubscribe', async ({ // Clipboard doesn't work here. Remove this when we moved away from Clipboard in favor of links test.skip() } - await modalPage.promptSiwe() + await inboxPage.promptSiwe() await walletValidator.expectReceivedSign({}) await walletPage.handleRequest({ accept: true }) - await modalPage.rejectNotifications() - await modalPage.subscribe(0) - await modalPage.unsubscribe(0) + await inboxPage.rejectNotifications() + await inboxPage.subscribeAndNavigateToDapp(0) + await inboxPage.unsubscribe() +}) + +test('it should subscribe, update preferences and unsubscribe', async ({ + inboxPage, + walletPage, + walletValidator, + browserName +}) => { + if (browserName === 'webkit') { + // Clipboard doesn't work here. Remove this when we moved away from Clipboard in favor of links + test.skip() + } + await inboxPage.promptSiwe() + await walletValidator.expectReceivedSign({}) + await walletPage.handleRequest({ accept: true }) + await inboxPage.rejectNotifications() + await inboxPage.subscribeAndNavigateToDapp(0) + await inboxPage.updatePreferences() + await inboxPage.unsubscribe() +}) + +test('it should subscribe and unsubscribe to and from multiple dapps', async ({ + inboxPage, + walletPage, + walletValidator, + browserName +}) => { + if (browserName === 'webkit') { + // Clipboard doesn't work here. Remove this when we moved away from Clipboard in favor of links + test.skip() + } + await inboxPage.promptSiwe() + await walletValidator.expectReceivedSign({}) + await walletPage.handleRequest({ accept: true }) + await inboxPage.rejectNotifications() + await inboxPage.subscribe(0) + await inboxPage.subscribe(1) + + await inboxPage.waitForSubscriptions(2) + + // Wait for the 2 dapps to be subscribed to. + await inboxPage.page.waitForFunction(() => { + // Using 1 here since the first `AppSelector__list` is the one with `Discover Apps` + const apps = document.getElementsByClassName('AppSelector__list')[1].children.length; + return apps === 2; + }) + + expect(await inboxPage.countSubscribedDapps()).toEqual(2); + + await inboxPage.navigateToDappFromSidebar(0); + await inboxPage.unsubscribe() + expect(await inboxPage.countSubscribedDapps()).toEqual(1); + + // select 0 again since we unsubscribed from the second dapp + // so there is only one item + await inboxPage.navigateToDappFromSidebar(0); + await inboxPage.unsubscribe() +}) + + +test('it should subscribe, receive messages and unsubscribe', async ({ + inboxPage, + walletPage, + settingsPage, + walletValidator, + browserName, + notifyServer +}) => { + if (browserName === 'webkit') { + // Clipboard doesn't work here. Remove this when we moved away from Clipboard in favor of links + test.skip() + } + await inboxPage.promptSiwe() + await walletValidator.expectReceivedSign({}) + await walletPage.handleRequest({ accept: true }) + await inboxPage.rejectNotifications() + + await settingsPage.goToNotificationSettings() + await settingsPage.displayCustomDapp(CUSTOM_TEST_DAPP.appDomain) + + await inboxPage.gotoDiscoverPage() + + // Ensure the custom dapp is the one subscribed to + await inboxPage.page.getByText("Notify Swift", {exact: false}).waitFor({ state: 'visible' }) + + expect(await inboxPage.page.getByText("Notify Swift", {exact: false}).isVisible()).toEqual(true); + + await inboxPage.subscribeAndNavigateToDapp(0) + + if(!CUSTOM_TEST_DAPP.projectId || !(CUSTOM_TEST_DAPP.projectSecret)) { + throw new Error("TEST_DAPP_SECRET and TEST_DAPP_ID are required") + } + + const address = await inboxPage.getAddress() + + await notifyServer.sendMessage({ + accounts: [`eip155:1:${address}`], + body: "Test Body", + title: "Test Title", + type: CUSTOM_TEST_DAPP.messageType, + url: CUSTOM_TEST_DAPP.appDomain, + icon: CUSTOM_TEST_DAPP.icons[0], + projectId: CUSTOM_TEST_DAPP.projectId, + projectSecret: CUSTOM_TEST_DAPP.projectSecret, + }) + + await inboxPage.page.getByText("Test Body").waitFor({state: 'visible'}) + + expect(await inboxPage.page.getByText("Test Body").isVisible()).toEqual(true) })