diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml
deleted file mode 100644
index e18764d9..00000000
--- a/.github/workflows/playwright.yml
+++ /dev/null
@@ -1,29 +0,0 @@
-name: Playwright Tests
-on:
- push:
- branches: [ main ]
- pull_request:
- branches: [ main ]
-jobs:
- test:
- timeout-minutes: 10
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v3
- - uses: actions/setup-node@v3
- with:
- node-version: 18
- - name: Install dependencies
- run: yarn
- - name: Install Playwright Browsers
- run: yarn playwright:install
- - name: Run Playwright tests
- run: yarn playwright:test
- env:
- VITE_PROJECT_ID: ${{ secrets.VITE_DEV_PROJECT_ID }}
- - uses: actions/upload-artifact@v3
- if: always()
- with:
- name: playwright-report
- path: playwright-report/
- retention-days: 30
diff --git a/.github/workflows/ui_test.yml b/.github/workflows/ui_test.yml
new file mode 100644
index 00000000..da8f8501
--- /dev/null
+++ b/.github/workflows/ui_test.yml
@@ -0,0 +1,44 @@
+name: Playwright Tests
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+concurrency:
+ # Support push/pr as event types with different behaviors each:
+ # 1. push: queue up builds
+ # 2. pr: only allow one run per PR
+ group: ${{ github.workflow }}-${{ github.event.type }}${{ github.event.pull_request.number }}
+ # If there is already a workflow running for the same pull request, cancel it
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
+jobs:
+ test:
+ timeout-minutes: 10
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-node@v3
+ with:
+ node-version: 18
+ - name: Install dependencies
+ run: yarn
+ - name: Install Playwright Browsers
+ run: yarn playwright:install
+ - name: Run Playwright tests
+ run: yarn playwright:test
+ env:
+ VITE_PROJECT_ID: ${{ secrets.VITE_DEV_PROJECT_ID }}
+ VITE_EXPLORER_API_URL: ${{ secrets.VITE_EXPLORER_API_URL }}
+ VITE_CI: true
+ - uses: actions/upload-artifact@v3
+ if: always()
+ with:
+ name: playwright-report
+ path: playwright-report/
+ retention-days: 7
+ - uses: actions/upload-artifact@v3
+ if: always()
+ with:
+ name: test-results
+ path: test-results/
+ retention-days: 7
diff --git a/package.json b/package.json
index 152a559f..bd8be5ef 100644
--- a/package.json
+++ b/package.json
@@ -14,7 +14,9 @@
"prettier": "prettier --check '**/*.{js,ts,jsx,tsx,scss}'",
"prettier:write": "prettier --write '**/*.{js,ts,jsx,tsx,scss}'",
"playwright:install": "playwright install --with-deps",
- "playwright:test": "playwright test"
+ "playwright:test": "playwright test",
+ "playwright:start": "VITE_CI=true yarn dev",
+ "playwright:debug": "playwright test --debug"
},
"dependencies": {
"@sentry/react": "^7.93.0",
diff --git a/playwright.config.ts b/playwright.config.ts
index 121b6501..901a8c2a 100644
--- a/playwright.config.ts
+++ b/playwright.config.ts
@@ -1,11 +1,5 @@
import { defineConfig, devices } from '@playwright/test'
-/**
- * Read environment variables from file.
- * https://github.com/motdotla/dotenv
- */
-// require('dotenv').config();
-
const baseURL = 'http://localhost:5173'
/**
@@ -18,20 +12,19 @@ export default defineConfig({
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
- retries: process.env.CI ? 2 : 0,
- /* Opt out of parallel tests on CI. */
- workers: process.env.CI ? 1 : undefined,
+ retries: 0,
+ /* Parallel tests currently blocked. */
+ workers: 1,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL,
-
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
-
- screenshot: 'only-on-failure'
+ screenshot: 'only-on-failure',
+ video: 'retain-on-failure'
},
/* Configure projects for major browsers */
@@ -40,41 +33,19 @@ export default defineConfig({
name: 'chromium',
use: { ...devices['Desktop Chrome'] }
},
-
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] }
},
-
{
name: 'webkit',
use: { ...devices['Desktop Safari'] }
}
-
- /* Test against mobile viewports. */
- // {
- // name: 'Mobile Chrome',
- // use: { ...devices['Pixel 5'] },
- // },
- // {
- // name: 'Mobile Safari',
- // use: { ...devices['iPhone 12'] },
- // },
-
- /* Test against branded browsers. */
- // {
- // name: 'Microsoft Edge',
- // use: { ...devices['Desktop Edge'], channel: 'msedge' },
- // },
- // {
- // name: 'Google Chrome',
- // use: { ...devices['Desktop Chrome'], channel: 'chrome' },
- // },
],
/* Run your local dev server before starting the tests */
webServer: {
- command: 'yarn dev',
+ command: 'yarn playwright:start',
url: baseURL,
reuseExistingServer: !process.env.CI
}
diff --git a/src/Modals.tsx b/src/Modals.tsx
index 6dc6c529..e49fc2be 100644
--- a/src/Modals.tsx
+++ b/src/Modals.tsx
@@ -10,6 +10,7 @@ import NotificationPwaModal from '@/components/utils/NotificationPwaModal'
import PwaModal from '@/components/utils/PwaModal'
import W3iContext from '@/contexts/W3iContext/context'
import { SignatureModal } from '@/pages/Login/SignatureModal'
+import { isCI } from '@/utils/env'
import { useModals } from '@/utils/hooks'
import { useNotificationPermissionState } from '@/utils/hooks/notificationHooks'
import {
@@ -96,7 +97,7 @@ export const Modals = () => {
{shouldShowPWAModal && }
- {isNotificationPwaModalOpen && }
+ {!isCI && isNotificationPwaModalOpen && }
{shouldShowChangeBrowserModal && }
diff --git a/src/components/settings/PrivacySettings/PrivacySettings.scss b/src/components/settings/PrivacySettings/PrivacySettings.scss
deleted file mode 100644
index 2b4171ef..00000000
--- a/src/components/settings/PrivacySettings/PrivacySettings.scss
+++ /dev/null
@@ -1,27 +0,0 @@
-.PrivacySettings {
- &__wrapper {
- display: flex;
- flex-direction: column;
-
- .SettingsItem__content {
- width: 100%;
- }
- }
-
- &__radios {
- padding: 0.625rem 1.75rem;
- display: flex;
- flex-direction: column;
- gap: 1.25rem;
-
- @media only screen and (max-width: 768px) {
- padding: 0.625rem 0rem;
- }
-
- &__subtitle {
- display: block;
- color: #788181;
- width: 75%;
- }
- }
-}
diff --git a/src/components/settings/PrivacySettings/index.tsx b/src/components/settings/PrivacySettings/index.tsx
deleted file mode 100644
index e183df40..00000000
--- a/src/components/settings/PrivacySettings/index.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import React from 'react'
-
-import MessageCheckmarkIcon from '@/components/general/Icon/MessageCheckmarkIcon'
-import ProfileCheckIcon from '@/components/general/Icon/ProfileCheckIcon'
-import ProfileDeclineIcon from '@/components/general/Icon/ProfileDeclineIcon'
-import ProfileLoadingIcon from '@/components/general/Icon/ProfileLoadingIcon'
-import Radio from '@/components/general/Radio'
-import Text from '@/components/general/Text'
-import MobileHeader from '@/components/layout/MobileHeader'
-
-import SettingsHeader from '../SettingsHeader'
-import SettingsItem from '../SettingsItem'
-import SettingsToggle from '../SettingsToggle/Index'
-
-import './PrivacySettings.scss'
-
-const radios = [
- {
- id: 'require-invite',
- label: 'Require new contacts to send me a chat invite',
- description: 'You will get notified about new contact requests and can accept or deny them.',
- icon:
- },
- {
- id: 'reject-new',
- label: 'Decline all chat invites from new contacts',
- description: 'New contacts will be added immediately and can send you messages right away.',
- icon:
- },
- {
- id: 'accept-new',
- label: 'Accept all chat invites from new contacts',
- description: 'Only you can invite others. Choose this option if you receive too many requests.',
- icon:
- }
-]
-
-const PrivacySettings: React.FC = () => {
- return (
-
-
-
-
-
- {/* }
- title="Send read receipts"
- subtitle="You must enable read receipts to see when others have read your messages."
- active={true}
- /> */}
-
-
-
- {radios.map(({ id, label, icon, description }) => (
- {}}
- />
- ))}
-
-
-
-
- )
-}
-
-export default PrivacySettings
diff --git a/src/routes.tsx b/src/routes.tsx
index b1de83c1..f1b6f0a7 100644
--- a/src/routes.tsx
+++ b/src/routes.tsx
@@ -45,7 +45,6 @@ const ConfiguredRoutes: React.FC = () => {
} />
} />
} />
- {/* } /> */}
) : null}
diff --git a/src/utils/env.ts b/src/utils/env.ts
new file mode 100644
index 00000000..3ba58151
--- /dev/null
+++ b/src/utils/env.ts
@@ -0,0 +1 @@
+export const isCI = import.meta.env.VITE_CI === 'true'
diff --git a/src/utils/sentry.ts b/src/utils/sentry.ts
index 220ae68f..ca88abf7 100644
--- a/src/utils/sentry.ts
+++ b/src/utils/sentry.ts
@@ -7,7 +7,7 @@ export const initSentry = () => {
new Sentry.BrowserTracing({
tracePropagationTargets: ['https://web3inbox-dev-hidden.vercel.app']
}),
- new Sentry.Replay()
+ new Sentry.Replay(),
],
tracesSampleRate: 0.2,
replaysSessionSampleRate: 0.1,
diff --git a/tests/README.md b/tests/README.md
new file mode 100644
index 00000000..dc5bae11
--- /dev/null
+++ b/tests/README.md
@@ -0,0 +1,20 @@
+# Functional Tests
+
+We use Playwright as our functional test runner. It's configured to try multiple permutations:
+
+- Browsers: Chromium/Firefox
+
+## Running Tests
+
+- `npx playwright test` to run in default mode (make sure your `.env` is set up)
+- `npm run playwright:debug` to step by step see what the tests are doing
+
+## Debugging
+
+For scenarios when tests pass locally but not remotely you can `await this.page.screenshot({ path: './screenshots/wallet.png' })` and the screenshot will be uploaded to GitHub Actions.
+
+## Running from GitHub Actions
+
+These tests can be run from GitHub Actions both from this and other repositories.
+
+You can tweak what's under test by setting the `BASE_URL` and `WALLET_URL` (default 'https://react-wallet.walletconnect.com/') environment variables.
diff --git a/tests/shared/constants/index.ts b/tests/shared/constants/index.ts
new file mode 100644
index 00000000..27c19328
--- /dev/null
+++ b/tests/shared/constants/index.ts
@@ -0,0 +1,10 @@
+import type { SessionParams } from '../types'
+
+// Allow localhost
+export const BASE_URL = process.env['BASE_URL'] || 'http://localhost:5173/'
+export const WALLET_URL = process.env['WALLET_URL'] || 'https://react-wallet.walletconnect.com/'
+export const DEFAULT_SESSION_PARAMS: SessionParams = {
+ reqAccounts: ['1', '2'],
+ optAccounts: ['1', '2'],
+ accept: true
+}
diff --git a/tests/shared/fixtures/fixture.ts b/tests/shared/fixtures/fixture.ts
new file mode 100644
index 00000000..08f28131
--- /dev/null
+++ b/tests/shared/fixtures/fixture.ts
@@ -0,0 +1,24 @@
+import { test as base } from '@playwright/test'
+
+import { ModalPage } from '../pages/InboxPage'
+import { ModalValidator } from '../validators/ModalValidator'
+
+// Declare the types of fixtures to use
+export interface ModalFixture {
+ modalPage: ModalPage
+ modalValidator: ModalValidator
+ library: string
+}
+
+export const test = base.extend({
+ modalPage: async ({ page }, use) => {
+ const modalPage = new ModalPage(page)
+ await modalPage.load()
+ await use(modalPage)
+ },
+ modalValidator: async ({ modalPage }, use) => {
+ const modalValidator = new ModalValidator(modalPage.page)
+ await use(modalValidator)
+ }
+})
+export { expect } from '@playwright/test'
diff --git a/tests/shared/fixtures/wallet-fixture.ts b/tests/shared/fixtures/wallet-fixture.ts
new file mode 100644
index 00000000..885be665
--- /dev/null
+++ b/tests/shared/fixtures/wallet-fixture.ts
@@ -0,0 +1,28 @@
+import { WalletPage } from '../pages/WalletPage'
+import { WalletValidator } from '../validators/WalletValidator'
+import { test as base } from './fixture'
+
+// Declare the types of fixtures to use
+interface ModalWalletFixture {
+ walletPage: WalletPage
+ walletValidator: WalletValidator
+}
+
+export const testWallet = base.extend({
+ walletPage: async ({ context, browserName }, use) => {
+ // WalletPage needs clipboard permissions with chromium to paste URI
+ if (browserName === 'chromium') {
+ await context.grantPermissions(['clipboard-read', 'clipboard-write'])
+ }
+
+ // Use a new page, to open alongside the modal
+ const walletPage = new WalletPage(await context.newPage())
+ await walletPage.load()
+ await use(walletPage)
+ },
+ walletValidator: async ({ walletPage }, use) => {
+ const walletValidator = new WalletValidator(walletPage.page)
+ await use(walletValidator)
+ }
+})
+export { expect } from '@playwright/test'
diff --git a/tests/shared/pages/DeviceRegistrationPage.ts b/tests/shared/pages/DeviceRegistrationPage.ts
new file mode 100644
index 00000000..6afd0b48
--- /dev/null
+++ b/tests/shared/pages/DeviceRegistrationPage.ts
@@ -0,0 +1,16 @@
+import type { Page } from '@playwright/test'
+
+export class DeviceRegistrationPage {
+ constructor(
+ public readonly page: Page,
+ public readonly url: string
+ ) {}
+
+ async load() {
+ await this.page.goto(this.url)
+ }
+
+ async approveDevice() {
+ await this.page.getByRole('button', { name: 'Approve' }).click()
+ }
+}
diff --git a/tests/shared/pages/InboxPage.ts b/tests/shared/pages/InboxPage.ts
new file mode 100644
index 00000000..fbff74f3
--- /dev/null
+++ b/tests/shared/pages/InboxPage.ts
@@ -0,0 +1,68 @@
+import { type Locator, type Page, expect } from '@playwright/test'
+
+import { BASE_URL } from '../constants'
+
+export class ModalPage {
+ private readonly baseURL = BASE_URL
+
+ private readonly connectButton: Locator
+
+ constructor(public readonly page: Page) {
+ this.connectButton = this.page.getByRole('button', { name: 'Connect Wallet' })
+ }
+
+ async load() {
+ await this.page.goto(this.baseURL)
+ }
+
+ async copyConnectUriToClipboard() {
+ await this.page.goto(this.baseURL)
+ await this.connectButton.click()
+ await this.page.getByTestId('wallet-selector-walletconnect').click()
+ await this.page.waitForTimeout(2000)
+ await this.page.getByTestId('copy-wc2-uri').click()
+ }
+
+ async disconnect() {
+ await this.page.getByTestId('account-button').click()
+ await this.page.getByTestId('disconnect-button').click()
+ }
+
+ async promptSiwe() {
+ await this.page.getByRole('button', { name: 'Sign in with wallet' }).click()
+ }
+
+ async rejectNotifications() {
+ // Allow for the modal to pop up
+ await this.page.waitForTimeout(4000)
+ const isVisible = (await this.page.locator('.NotificationPwaModal__close-button').count()) > 0
+ if (!isVisible) return
+ await this.page.locator('.NotificationPwaModal__close-button').first().click()
+ }
+
+ 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()
+ }
+
+ async unsubscribe(nth: number) {
+ await this.page.getByRole('button', { name: 'Subscribed' }).nth(nth).click()
+ await this.page.getByRole('button', { name: 'Subscribed' }).nth(nth).isHidden()
+ 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()
+ await this.page.getByText('Unsubscribed from', { exact: false }).isVisible()
+ await this.page.waitForTimeout(2000)
+ }
+
+ async cancelSiwe() {
+ await this.page.getByTestId('w3m-connecting-siwe-cancel').click()
+ }
+
+ async switchNetwork(network: string) {
+ await this.page.getByTestId('account-button').click()
+ await this.page.getByTestId('w3m-account-select-network').click()
+ await this.page.getByTestId(`w3m-network-switch-${network}`).click()
+ await this.page.getByTestId(`w3m-header-close`).click()
+ }
+}
diff --git a/tests/shared/pages/WalletPage.ts b/tests/shared/pages/WalletPage.ts
new file mode 100644
index 00000000..ba1c0360
--- /dev/null
+++ b/tests/shared/pages/WalletPage.ts
@@ -0,0 +1,58 @@
+/* eslint-disable no-await-in-loop */
+import type { Locator, Page } from '@playwright/test'
+
+import { WALLET_URL } from '../constants'
+import type { SessionParams } from '../types'
+
+export class WalletPage {
+ private readonly baseURL = WALLET_URL
+
+ private readonly gotoHome: Locator
+ private readonly vercelPreview: Locator
+
+ constructor(public readonly page: Page) {
+ this.gotoHome = this.page.getByTestId('wc-connect')
+ this.vercelPreview = this.page.locator('css=vercel-live-feedback')
+ }
+
+ async load() {
+ await this.page.goto(this.baseURL)
+ }
+
+ async connect() {
+ const isVercelPreview = (await this.vercelPreview.count()) > 0
+ if (isVercelPreview) {
+ await this.vercelPreview.evaluate((iframe: HTMLIFrameElement) => iframe.remove())
+ }
+ await this.gotoHome.click()
+ await this.page.getByTestId('uri-input').click()
+
+ // Paste clipboard
+ const isMac = process.platform === 'darwin'
+ const modifier = isMac ? 'Meta' : 'Control'
+ await this.page.keyboard.press(`${modifier}+KeyV`)
+ await this.page.getByTestId('uri-connect-button').click()
+ }
+
+ /**
+ * Handle a session proposal event in the wallet
+ * @param reqAccounts - required account numbers to select ex/ ['1', '2']
+ * @param optAccounts - optional account numbers to select ex/ ['1', '2']
+ * @param accept - accept or reject the session
+ */
+ async handleSessionProposal(opts: SessionParams) {
+ const variant = opts.accept ? `approve` : `reject`
+ // `.click` doesn't work here, so we use `.focus` and `Space`
+ await this.page.getByTestId(`session-${variant}-button`).isEnabled()
+ await this.page.getByTestId(`session-${variant}-button`).focus()
+ await this.page.keyboard.press('Space')
+ }
+
+ async handleRequest({ accept }: { accept: boolean }) {
+ const variant = accept ? `approve` : `reject`
+ // `.click` doesn't work here, so we use `.focus` and `Space`
+ await this.page.getByTestId(`session-${variant}-button`).isEnabled()
+ await this.page.getByTestId(`session-${variant}-button`).focus()
+ await this.page.keyboard.press('Space')
+ }
+}
diff --git a/tests/shared/types/index.ts b/tests/shared/types/index.ts
new file mode 100644
index 00000000..d9188ff7
--- /dev/null
+++ b/tests/shared/types/index.ts
@@ -0,0 +1,5 @@
+export interface SessionParams {
+ reqAccounts: string[]
+ optAccounts: string[]
+ accept: boolean
+}
diff --git a/tests/shared/validators/ModalValidator.ts b/tests/shared/validators/ModalValidator.ts
new file mode 100644
index 00000000..6405b8bc
--- /dev/null
+++ b/tests/shared/validators/ModalValidator.ts
@@ -0,0 +1,38 @@
+import { expect } from '@playwright/test'
+import type { Page } from '@playwright/test'
+
+export class ModalValidator {
+ constructor(public readonly page: Page) {}
+
+ async expectConnected() {
+ await expect(this.page.getByTestId('account-button')).toBeVisible()
+ }
+
+ async expectAuthenticated() {
+ await expect(this.page.getByTestId('w3m-authentication-status')).toContainText('authenticated')
+ }
+
+ async expectUnauthenticated() {
+ await expect(this.page.getByTestId('w3m-authentication-status')).toContainText(
+ 'unauthenticated'
+ )
+ }
+
+ async expectSignatureDeclined() {
+ await expect(this.page.getByText('Signature declined')).toBeVisible()
+ }
+
+ async expectDisconnected() {
+ await expect(this.page.getByTestId('account-button')).not.toBeVisible()
+ }
+
+ async expectAcceptedSign() {
+ // We use Chakra Toast and it's not quite straightforward to set the `data-testid` attribute on the toast element.
+ await expect(this.page.getByText('abc')).toBeVisible()
+ }
+
+ async expectRejectedSign() {
+ // We use Chakra Toast and it's not quite straightforward to set the `data-testid` attribute on the toast element.
+ await expect(this.page.getByText('abc')).toBeVisible()
+ }
+}
diff --git a/tests/shared/validators/WalletValidator.ts b/tests/shared/validators/WalletValidator.ts
new file mode 100644
index 00000000..2f7a0305
--- /dev/null
+++ b/tests/shared/validators/WalletValidator.ts
@@ -0,0 +1,25 @@
+import { expect } from '@playwright/test'
+import type { Locator, Page } from '@playwright/test'
+
+export class WalletValidator {
+ private readonly gotoSessions: Locator
+
+ constructor(public readonly page: Page) {
+ this.gotoSessions = this.page.getByTestId('sessions')
+ }
+
+ async expectConnected() {
+ await this.gotoSessions.click()
+ await expect(this.page.getByTestId('session-card')).toBeVisible()
+ }
+
+ async expectDisconnected() {
+ await this.gotoSessions.click()
+ await expect(this.page.getByTestId('session-card')).not.toBeVisible()
+ }
+
+ async expectReceivedSign({ chainName = 'Ethereum' }) {
+ await expect(this.page.getByTestId('session-approve-button')).toBeVisible()
+ await expect(this.page.getByTestId('request-details-chain')).toHaveText(chainName)
+ }
+}
diff --git a/tests/subscribe.spec.ts b/tests/subscribe.spec.ts
new file mode 100644
index 00000000..f0c9097e
--- /dev/null
+++ b/tests/subscribe.spec.ts
@@ -0,0 +1,36 @@
+import { DEFAULT_SESSION_PARAMS } from './shared/constants'
+import { testWallet as test } from './shared/fixtures/wallet-fixture'
+
+test.beforeEach(async ({ modalPage, walletPage, browserName }) => {
+ if (browserName === 'webkit') {
+ // Clipboard doesn't work here. Remove this when we moved away from Clipboard in favor of links
+ test.skip()
+ }
+ test.skip()
+ await modalPage.copyConnectUriToClipboard()
+ await walletPage.connect()
+ await walletPage.handleSessionProposal(DEFAULT_SESSION_PARAMS)
+})
+
+test.afterEach(async ({ modalValidator, walletValidator }) => {
+ await modalValidator.expectDisconnected()
+ await walletValidator.expectDisconnected()
+})
+
+test('it should subscribe and unsubscribe', async ({
+ modalPage,
+ 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 modalPage.promptSiwe()
+ await walletValidator.expectReceivedSign({})
+ await walletPage.handleRequest({ accept: true })
+ await modalPage.rejectNotifications()
+ await modalPage.subscribe(0)
+ await modalPage.unsubscribe(0)
+})