Skip to content

Commit

Permalink
test: E2E test for Notifications Settings Syncing (#30243)
Browse files Browse the repository at this point in the history
<!--
Please submit this PR as a draft initially.
Do not mark it as "Ready for review" until the template has been
completely filled out, and PR status checks have passed at least once.
-->

## **Description**

* E2E Tests for notifications Syncing
* Introduce a dataTestId prop to the NotificationsSettingsBox component
to enhance testing capabilities.
* Page objects for Notifications settings flow for validating and
interacting with notifications setting
* Updates unit and integration tests

## **Related issues**

Fixes:

## **Manual testing steps**

yarn build:test:webpack 
ENABLE_MV3=false yarn test:e2e:single
test/e2e/tests/notifications/enable-notifications.spec.ts
--browser=chrome

yarn build:test
yarn test:e2e:single
test/e2e/tests/notifications/enable-notifications-with-accounts-sync.spec.ts
--browser=chrome IS_ACCOUNT_SYNCING_ENABLED=true

## **Screenshots/Recordings**

<!-- If applicable, add screenshots and/or recordings to visualize the
before and after of your change. -->

### **Before**

<!-- [screenshots/recordings] -->

### **After**

<!-- [screenshots/recordings] -->

## **Pre-merge author checklist**

- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask
Extension Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I’ve included tests if applicable
- [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

## **Pre-merge reviewer checklist**

- [x] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [x] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.
  • Loading branch information
cmd-ob authored Feb 13, 2025
1 parent 901ce6a commit 9f07d45
Show file tree
Hide file tree
Showing 19 changed files with 766 additions and 153 deletions.
4 changes: 4 additions & 0 deletions privacy-snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"bafybeidxfmwycgzcp4v2togflpqh2gnibuexjy4m4qqwxp7nh3jx5zlh4y.ipfs.dweb.link",
"bridge.api.cx.metamask.io",
"bridge.dev-api.cx.metamask.io",
"cdn.contentful.com",
"cdn.segment.com",
"cdn.segment.io",
"cdnjs.cloudflare.com",
Expand Down Expand Up @@ -46,6 +47,7 @@
"metametrics.metamask.test",
"min-api.cryptocompare.com",
"nft.api.cx.metamask.io",
"notification.api.cx.metamask.io",
"oidc.api.cx.metamask.io",
"on-ramp-content.api.cx.metamask.io",
"on-ramp-content.uat-api.cx.metamask.io",
Expand All @@ -55,6 +57,7 @@
"price.api.cx.metamask.io",
"proxy.api.cx.metamask.io",
"proxy.dev-api.cx.metamask.io",
"push.api.cx.metamask.io",
"raw.githubusercontent.com",
"registry.npmjs.org",
"responsive-rpc.test",
Expand All @@ -76,6 +79,7 @@
"token.api.cx.metamask.io",
"tokens.api.cx.metamask.io",
"transaction.api.cx.metamask.io",
"trigger.api.cx.metamask.io",
"tx-sentinel-ethereum-mainnet.api.cx.metamask.io",
"unresponsive-rpc.test",
"unresponsive-rpc.url",
Expand Down
18 changes: 18 additions & 0 deletions test/e2e/page-objects/pages/header-navbar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ class HeaderNavbar {

private readonly networkPicker = '.mm-picker-network';

private readonly notificationsButton =
'[data-testid="notifications-menu-item"]';

private readonly firstTimeTurnOnNotificationsButton =
'[data-testid="turn-on-notifications-button"]';

constructor(driver: Driver) {
this.driver = driver;
}
Expand Down Expand Up @@ -88,6 +94,18 @@ class HeaderNavbar {
await this.driver.clickElement(this.switchNetworkDropDown);
}

async enableNotifications(): Promise<void> {
console.log('Enabling notifications for the first time');
await this.openThreeDotMenu();
await this.driver.clickElement(this.notificationsButton);
await this.driver.clickElement(this.firstTimeTurnOnNotificationsButton);
}

async goToNotifications(): Promise<void> {
console.log('Click notifications button');
await this.driver.clickElement(this.notificationsButton);
}

async check_currentSelectedNetwork(networkName: string): Promise<void> {
console.log(`Validate the Switch network to ${networkName}`);
await this.driver.waitForSelector(
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/page-objects/pages/home/homepage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ class HomePage {
await this.driver.wait(async () => {
const uiState = await getCleanAppState(this.driver);
return uiState.metamask.hasAccountSyncingSyncedAtLeastOnce === true;
}, 10000);
}, 30000); // Syncing can take some time so adding a longer timeout to reduce flakes
}

async check_ifBridgeButtonIsClickable(): Promise<boolean> {
Expand Down
47 changes: 47 additions & 0 deletions test/e2e/page-objects/pages/notifications-list-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Driver } from '../../webdriver/driver';

class NotificationsListPage {
private driver: Driver;

private readonly notificationsListPageTitle = {
text: 'Notifications',
tag: 'p',
};

private readonly notificationsSettingsButton =
'[data-testid="notifications-settings-button"]';

constructor(driver: Driver) {
this.driver = driver;
}

async check_pageIsLoaded(): Promise<void> {
try {
await this.driver.waitForMultipleSelectors([
this.notificationsListPageTitle,
this.notificationsSettingsButton,
]);
} catch (e) {
console.log(
'Timeout while waiting for Notifications list page to be loaded',
e,
);
throw e;
}
console.log('Notifications List page is loaded');
}

/**
* Navigates to the notifications settings page.
*
* This method clicks on the notifications settings button to navigate to the settings page.
*/
async goToNotificationsSettings(): Promise<void> {
console.log(
`On notifications list page, navigating to notifications settings`,
);
await this.driver.clickElement(this.notificationsSettingsButton);
}
}

export default NotificationsListPage;
168 changes: 168 additions & 0 deletions test/e2e/page-objects/pages/settings/notifications-settings-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { toChecksumHexAddress } from '@metamask/controller-utils';
import { Driver } from '../../../webdriver/driver';
import { shortenAddress } from '../../../../../ui/helpers/utils/util';

class NotificationsSettingsPage {
private driver: Driver;

private readonly notificationsSettingsPageTitle = {
text: 'Notifications',
tag: 'p',
};

private readonly allowNotificationsToggle =
'[data-testid="notifications-settings-allow-toggle-box"]';

private readonly allowNotificationsInput =
'[data-testid="notifications-settings-allow-toggle-input"]';

private readonly allowNotificationsAddressToggle = (
address: string,
elementType: 'input' | 'box',
) => {
const checksumAddress = toChecksumHexAddress(address.toLowerCase());
return `[data-testid="${shortenAddress(
checksumAddress,
)}-notifications-settings-toggle-${elementType}"]`;
};

private readonly allowProductAnnouncementToggle =
'[data-testid="product-announcements-toggle-box"]';

private readonly allowProductAnnouncementInput =
'[data-testid="product-announcements-toggle-input"]';

constructor(driver: Driver) {
this.driver = driver;
}

async check_pageIsLoaded(): Promise<void> {
try {
await this.driver.waitForMultipleSelectors([
this.notificationsSettingsPageTitle,
this.allowNotificationsToggle,
]);
} catch (e) {
console.log(
'Timeout while waiting for notifications settings page to be loaded',
e,
);
throw e;
}
console.log('Notifications Settings page is loaded');
}

/**
* Validates the state of any notification toggle.
*
* @param options - Configuration object
* @param options.toggleType - The type of toggle to check ('general' | 'product' | 'address')
* @param options.address - The ethereum address (required only when toggleType is 'address')
* @param options.expectedState - The expected state of the toggle ('enabled' or 'disabled')
* @throws {Error} If toggle state doesn't match expected state or if the toggle element cannot be found
*/
async check_notificationState({
toggleType,
address,
expectedState,
}: {
toggleType: 'general' | 'product' | 'address';
address?: string;
expectedState: 'enabled' | 'disabled';
}): Promise<void> {
let selector: string;
const description =
toggleType === 'address' ? `for address ${address}` : '';

switch (toggleType) {
case 'general':
selector = this.allowNotificationsInput;
break;
case 'product':
selector = this.allowProductAnnouncementInput;
break;
case 'address':
if (!address) {
throw new Error(
'Address is required when checking address notifications',
);
}
selector = this.allowNotificationsAddressToggle(address, 'input');
break;
default:
throw new Error(`Invalid toggle type: ${toggleType}`);
}

console.log(
`Checking if ${toggleType} notifications ${description} are ${expectedState}`,
);
const expectedValue = expectedState === 'enabled' ? 'true' : 'false';

try {
await this.driver.waitForElementToStopMoving(selector);
await this.driver.wait(async () => {
const toggle = await this.driver.findElement(selector);
return (await toggle.getAttribute('value')) === expectedValue;
});
console.log(
`Successfully verified ${toggleType} notifications ${description} to be ${expectedState}`,
);
} catch (error) {
throw new Error(
`Expected ${toggleType} notifications ${description} state to be: ${expectedState}`,
);
}
}

/**
* Clicks a notification toggle and verifies its new state.
*
* @param options - Configuration object
* @param options.toggleType - The type of toggle to click ('general' | 'product' | 'address')
* @param options.address - The ethereum address (required only when toggleType is 'address')
* @throws {Error} If toggle element cannot be found or if address is missing when required
*/
async clickNotificationToggle({
toggleType,
address,
}: {
toggleType: 'general' | 'product' | 'address';
address?: string;
}): Promise<void> {
let selector: string;

switch (toggleType) {
case 'general':
selector = this.allowNotificationsToggle;
console.log('Clicking general notifications toggle');
break;
case 'product':
selector = this.allowProductAnnouncementToggle;
console.log('Clicking product announcement toggle');
break;
case 'address':
if (!address) {
throw new Error(
'Address is required when toggling address notifications',
);
}
selector = this.allowNotificationsAddressToggle(address, 'box');
console.log(`Clicking notifications toggle for address ${address}`);
break;
default:
throw new Error(`Invalid toggle type: ${toggleType}`);
}

try {
await this.driver.waitForElementToStopMoving(selector);
await this.driver.clickElement(selector);
await this.driver.waitForElementToStopMoving(selector);
console.log(`Successfully clicked ${toggleType} notifications toggle`);
} catch (error) {
console.error(`Error clicking ${toggleType} notifications toggle`, error);
throw error;
}
}
}

export default NotificationsSettingsPage;
10 changes: 10 additions & 0 deletions test/e2e/page-objects/pages/settings/settings-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ class SettingsPage {
css: 'h3',
};

private readonly notificationsSettingsButton = {
text: 'Notifications',
css: '.tab-bar__tab__content__title',
};

constructor(driver: Driver) {
this.driver = driver;
}
Expand Down Expand Up @@ -88,6 +93,11 @@ class SettingsPage {
console.log('Navigating to Privacy & Security Settings page');
await this.driver.clickElement(this.privacySettingsButton);
}

async goToNotificationsSettings(): Promise<void> {
console.log('Navigating to Notifications Settings page');
await this.driver.clickElement(this.notificationsSettingsButton);
}
}

export default SettingsPage;
9 changes: 9 additions & 0 deletions test/e2e/tests/identity/account-syncing/mock-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ import { IDENTITY_TEAM_STORAGE_KEY } from '../constants';
import { createEncryptedResponse } from '../../../helpers/identity/user-storage/generateEncryptedData';
import { UserStorageAccount } from './helpers';

/**
* This array represents the accounts mock data before it is encrypted and sent to UserStorage.
* Each object within the array represents a UserStorageAccount, which includes properties such as:
* - v: The version of the User Storage.
* - a: The address of the account.
* - i: The id of the account.
* - n: The name of the account.
* - nlu: The name last updated timestamp of the account.
*/
export const accountsToMockForAccountsSync: UserStorageAccount[] = [
{
v: '1',
Expand Down
Loading

0 comments on commit 9f07d45

Please sign in to comment.