This commit is contained in:
Rafee 2024-04-17 20:20:00 -04:00
Родитель e1ba825198
Коммит 4d13f77f12
62 изменённых файлов: 1296 добавлений и 660 удалений

5
.github/workflows/playwright.yml поставляемый
Просмотреть файл

@ -1,4 +1,4 @@
name: Relay e2e Tests
name: Relay e2e tests
on:
schedule:
- cron: '0 8 * * *'
@ -17,7 +17,7 @@ on:
jobs:
relaye2e:
name: 'Relay Application e2e Tests'
name: 'Relay e2e all tests'
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
@ -53,3 +53,4 @@ jobs:
name: playwright-report
path: playwright-report/
retention-days: 5

63
.github/workflows/relay_e2e_health.yml поставляемый Normal file
Просмотреть файл

@ -0,0 +1,63 @@
name: Relay e2e health check
on:
schedule:
- cron: '0 8 * * *'
workflow_dispatch:
inputs:
environment:
description: 'Environment to run the e2e against'
required: true
default: 'stage'
type: choice
options:
- stage
- prod
- dev
push:
branches:
- "e2e-tests-enhancement-work"
pull_request:
branches:
- "main"
paths:
- ".github/workflows/relay_e2e_health.yml"
jobs:
relay_health_check:
name: 'Relay e2e health check'
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4.0.2
with:
node-version-file: 'frontend/package.json'
cache: 'npm'
- name: Install Node dependencies
run: npm install
- name: Install Playwright Browsers
run: |
npm install -D @playwright/test --with-deps
npx playwright install
- name: Run Playwright tests
run: |
commandenv="${{ inputs.environment != null && inputs.environment || 'stage' }}"
E2E_TEST_ENV=$commandenv npx playwright test --grep "@health_check"
env:
E2E_TEST_ENV: ${{ inputs.environment != null && inputs.environment || 'stage' }}
E2E_TEST_ACCOUNT_FREE: ${{ secrets.E2E_TEST_ACCOUNT_FREE }}
E2E_TEST_ACCOUNT_PASSWORD: ${{ secrets.E2E_TEST_ACCOUNT_PASSWORD }}
E2E_TEST_ACCOUNT_PREMIUM: ${{ secrets.E2E_TEST_ACCOUNT_PREMIUM }}
E2E_TEST_BASE_URL: ${{ secrets.E2E_TEST_BASE_URL }}
- name: Upload html report as artifact to troubleshoot failures.
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 5

Просмотреть файл

@ -32,20 +32,49 @@ npx playwright install
### 5. Run Tests
If you are running only free specs, this following will suffice.
```
create/update a .env file with the following:
E2E_TEST_ACCOUNT_PASSWORD=<arbitrary password>
```
If you are running premium tests as well, you will need the following.
```
create/update a .env file with the following:
E2E_TEST_ACCOUNT_PREMIUM = <your_premium_account_email>
E2E_TEST_ACCOUNT_PASSWORD=<your_premium_account_password>
```
Any free account created during the initial setup of tests will also use `E2E_TEST_ACCOUNT_PASSWORD`. If you do not want to use a personal premium account, reach out to Luke for `relay-team` premium account details.
### 6. Run Tests
```
npm run test:e2e
```
By default, `npm run test:e2e` will run the tests on https://stage.fxprivaterelay.nonprod.cloudops.mozgcp.net/.
By default, `npm run test:e2e` will run the tests on https://stage.fxprivaterelay.nonprod.cloudops.mozgcp.net/.
You can also run tests locally, on our dev server (https://dev.fxprivaterelay.nonprod.cloudops.mozgcp.net/), and in production (https://relay.firefox.com/). You can find the commands [here](https://github.com/mozilla/fx-private-relay/blob/main/package.json#L26-L31). To view the tests live in the browser, you can add `--headed` to the end of the command. See https://playwright.dev/docs/test-cli for more flags.
You can also run tests locally, on our dev server (https://dev.fxprivaterelay.nonprod.cloudops.mozgcp.net/), and in production (https://relay.firefox.com/). You can find the commands [here](https://github.com/mozilla/fx-private-relay/blob/main/package.json#L26-L31), or you can run `E2E_TEST_ENV=<env (prod, dev, stage)> npx playwright test`. To view the tests live in the browser, you can add `--headed` to the end of the command. See https://playwright.dev/docs/test-cli for more flags.
[![Relay e2e Tests](https://github.com/mozilla/fx-private-relay/actions/workflows/playwright.yml/badge.svg)](https://github.com/mozilla/fx-private-relay/actions/workflows/playwright.yml)
Our github actions workflows can be found here, [![Relay e2e Tests](https://github.com/mozilla/fx-private-relay/actions/workflows/playwright.yml/badge.svg)](https://github.com/mozilla/fx-private-relay/actions/workflows/playwright.yml). You can run the tests against different branches.
### 7. Screenshots
When you run tests for the first time locally, you will run into an error for the tests that rely on screenshots, stating that a snapshot does not exist. Here is an example.
```
Error: A snapshot doesn't exist at example.spec.ts-snapshots/example-test-1-chromium-darwin.png, writing actual.
```
This is because playwright needs to create an image initially. On the following runs, it will compare that a screenshot of the respective element matches the one added initially. Do not push your local images into the repo, the only ones that are needed for CI end in `linux`.
### 8. Linting
To lint the files, run the following in the root directory (it is recommended to run this after any changes to the test suite):
`npx prettier --write e2e-tests/*`

Просмотреть файл

@ -1,95 +1,114 @@
import { APIRequestContext, Page, request } from '@playwright/test';
import { APIRequestContext, Page, request } from "@playwright/test";
export const ENV_EMAIL_DOMAINS = {
stage: '@mozmail.fxprivaterelay.nonprod.cloudops.mozgcp.net',
prod: '@mozmail.com',
dev: '@mozmail.dev.fxprivaterelay.nonprod.cloudops.mozgcp.net',
local: '@mozmail.com'
}
stage: "@mozmail.fxprivaterelay.nonprod.cloudops.mozgcp.net",
prod: "@mozmail.com",
dev: "@mozmail.dev.fxprivaterelay.nonprod.cloudops.mozgcp.net",
local: "@mozmail.com",
};
export const ENV_URLS = {
stage: 'https://stage.fxprivaterelay.nonprod.cloudops.mozgcp.net',
prod: 'https://relay.firefox.com',
dev: 'https://dev.fxprivaterelay.nonprod.cloudops.mozgcp.net',
local: process.env.SITE_ORIGIN
}
stage: "https://stage.fxprivaterelay.nonprod.cloudops.mozgcp.net",
prod: "https://relay.firefox.com",
dev: "https://dev.fxprivaterelay.nonprod.cloudops.mozgcp.net",
local: process.env.SITE_ORIGIN,
};
export const getVerificationCode = async (testEmail: string, page: Page, attempts = 10) => {
export const getVerificationCode = async (
testEmail: string,
page: Page,
attempts = 10,
) => {
if (attempts === 0) {
throw new Error('Unable to retrieve restmail data');
throw new Error("Unable to retrieve restmail data");
}
const context = await request.newContext();
const res = await context.get(
`http://restmail.net/mail/${testEmail}`,
{
failOnStatusCode: false
}
);
const res = await context.get(`http://restmail.net/mail/${testEmail}`, {
failOnStatusCode: false,
});
const resJson = await res.json();
if (resJson.length) {
const verificationCode = resJson[0].headers['x-verify-short-code']
const verificationCode = resJson[0].headers["x-verify-short-code"];
return verificationCode;
}
await page.waitForTimeout(1000);
await page.waitForTimeout(2000);
return getVerificationCode(testEmail, page, attempts - 1);
}
};
export const deleteEmailAddressMessages = async (req: APIRequestContext, testEmail: string) => {
export const deleteEmailAddressMessages = async (
req: APIRequestContext,
testEmail: string,
) => {
try {
await req.delete(`http://restmail.net/mail/${testEmail}`);
} catch (err) {
console.error('ERROR DELETE RESTMAIL EMAIL', err);
console.error("ERROR DELETE RESTMAIL EMAIL", err);
}
};
const setYourPassword = async (page: Page) => {
await page.locator('#password').fill(process.env.E2E_TEST_ACCOUNT_PASSWORD as string)
await page.locator('#vpassword').fill(process.env.E2E_TEST_ACCOUNT_PASSWORD as string)
await page.locator('#age').fill('31');
await page.locator('button:has-text("Create account")').click({force: true})
await page.waitForTimeout(500)
await checkAuthState(page)
}
await page
.locator("#password")
.fill(process.env.E2E_TEST_ACCOUNT_PASSWORD as string);
await page
.locator("#vpassword")
.fill(process.env.E2E_TEST_ACCOUNT_PASSWORD as string);
await page.locator("#age").fill("31");
await page
.locator('button:has-text("Create account")')
.click({ force: true });
await page.waitForTimeout(2000);
await checkAuthState(page);
};
const enterConfirmationCode = async (page: Page) => {
const maybeVerificationCodeInput = 'div.card input'
await page.waitForSelector(maybeVerificationCodeInput, { timeout: 2000 })
const confirmButton = page.locator('#submit-btn')
const verificationCode = await getVerificationCode(process.env.E2E_TEST_ACCOUNT_FREE as string, page)
await page.locator(maybeVerificationCodeInput).fill(verificationCode)
await confirmButton.click({force: true})
await page.waitForTimeout(500)
await checkAuthState(page)
}
const maybeVerificationCodeInput = "div.card input";
await page.waitForSelector(maybeVerificationCodeInput, { timeout: 2000 });
const confirmButton = page.locator("#submit-btn");
const verificationCode = await getVerificationCode(
process.env.E2E_TEST_ACCOUNT_FREE as string,
page,
);
await page.locator(maybeVerificationCodeInput).fill(verificationCode);
await confirmButton.click({ force: true });
await page.waitForTimeout(2000);
await checkAuthState(page);
};
const signIn = async (page: Page) => {
const signInButton = '//button[@id="use-logged-in"]'
await page.waitForSelector(signInButton, { timeout: 2000 })
await page.locator(signInButton).click({force: true})
await page.waitForTimeout(500)
await checkAuthState(page)
}
const signInButton = page.getByRole("button", { name: "Sign in" });
await signInButton.waitFor({ timeout: 2000 });
await page.waitForLoadState("networkidle");
await page.waitForLoadState("domcontentloaded");
await page.getByRole("button", { name: "Sign in" }).click({ force: true });
await page.waitForTimeout(2000);
await checkAuthState(page);
};
const enterYourEmail = async (page: Page) => {
const maybeEmailInput = 'input[name="email"]'
await page.waitForSelector(maybeEmailInput, { timeout: 2000 })
const signInButton = page.locator('#submit-btn')
await page.locator(maybeEmailInput).fill(process.env.E2E_TEST_ACCOUNT_FREE as string)
await signInButton.click({force: true})
await page.waitForTimeout(500)
await checkAuthState(page)
}
const maybeEmailInput = 'input[name="email"]';
await page.waitForSelector(maybeEmailInput, { timeout: 2000 });
const signInButton = page.locator("#submit-btn");
await page
.locator(maybeEmailInput)
.fill(process.env.E2E_TEST_ACCOUNT_FREE as string);
await signInButton.click({ force: true });
await page.waitForTimeout(500);
await checkAuthState(page);
};
const enterYourPassword = async (page: Page) => {
await page.locator('#password').fill(process.env.E2E_TEST_ACCOUNT_PASSWORD as string)
await page
.locator("#password")
.fill(process.env.E2E_TEST_ACCOUNT_PASSWORD as string);
// using force here due to fxa issue with playwright
await page.locator('#submit-btn').click()
await page.waitForTimeout(500)
await checkAuthState(page)
}
await page.locator("#submit-btn").click();
await page.waitForTimeout(500);
await checkAuthState(page);
};
export const generateRandomEmail = async () => {
return `${Date.now()}_tstact@restmail.net`;
@ -98,46 +117,68 @@ export const generateRandomEmail = async () => {
export const setEnvVariables = async (email: string) => {
// set env variables
// stage will currently be the default
process.env['E2E_TEST_ENV'] = process.env.E2E_TEST_ENV as string ?? 'stage';
process.env['E2E_TEST_ACCOUNT_FREE'] = email;
process.env['E2E_TEST_BASE_URL'] = ENV_URLS[process.env.E2E_TEST_ENV as string] ?? ENV_URLS.stage
}
process.env["E2E_TEST_ENV"] = (process.env.E2E_TEST_ENV as string) ?? "stage";
process.env["E2E_TEST_ACCOUNT_FREE"] = email;
process.env["E2E_TEST_BASE_URL"] =
ENV_URLS[process.env.E2E_TEST_ENV as string] ?? ENV_URLS.stage;
};
interface DefaultScreenshotOpts {
animations?: "disabled" | "allow" | undefined
animations?: "disabled" | "allow" | undefined;
maxDiffPixelRatio?: number | undefined;
}
export const defaultScreenshotOpts: Partial<DefaultScreenshotOpts> = {
animations: 'disabled',
maxDiffPixelRatio: 0.04
animations: "disabled",
maxDiffPixelRatio: 0.04,
};
export const forceNonReactLink = async (page: Page) => {
/**
* There is a small chance you are redirected to an FxA auth page with the parameter showReactApp=true.
* This causes the page to look different, and our selectors for the auth page to become flaky because id's are missing.
*/
const url = new URL(page.url());
if (url.searchParams.get('showReactApp') === 'true') {
url.searchParams.set('showReactApp', 'false');
await page.goto(url.toString());
}
}
export const checkAuthState = async (page: Page) => {
try {
const authStateTitleString = await page.locator('h1').first()?.textContent({ timeout: 4000 })
await page.waitForLoadState("networkidle");
await page.waitForLoadState("domcontentloaded");
const authStateTitleString = await page
.locator("h1")
.first()
?.textContent({ timeout: 5000 });
const checkIfTitleContains = (potentialTitle: string) => {
return authStateTitleString?.includes(potentialTitle)
}
return authStateTitleString?.includes(potentialTitle);
};
await forceNonReactLink(page);
switch (true) {
case checkIfTitleContains('Enter your email'):
await enterYourEmail(page)
case checkIfTitleContains("Enter your email"):
await enterYourEmail(page);
break;
case checkIfTitleContains('Enter your password'):
await enterYourPassword(page)
case checkIfTitleContains("Enter your password"):
await enterYourPassword(page);
break;
case checkIfTitleContains('Set your password'):
await setYourPassword(page)
case checkIfTitleContains("Set your password"):
await setYourPassword(page);
break;
case checkIfTitleContains('Enter confirmation code'):
await enterConfirmationCode(page)
case checkIfTitleContains("Enter confirmation code"):
await enterConfirmationCode(page);
break;
case checkIfTitleContains('Sign in'):
await signIn(page)
case checkIfTitleContains("Sign in"):
await signIn(page);
break;
default:
break;
}
} catch {}
}
};

Просмотреть файл

@ -1,28 +1,33 @@
import { LandingPage } from "../pages/landingPage";
import { AuthPage } from "../pages/authPage";
import { test as baseTest } from '@playwright/test'
import { test as baseTest } from "@playwright/test";
import { DashboardPage } from "../pages/dashboardPage";
import { SubscriptionPaymentPage } from "../pages/subscriptionPaymentPage";
import { MozillaMonitorPage } from "../pages/mozillaMonitorPage";
const test = baseTest.extend<{
landingPage: LandingPage;
authPage: AuthPage;
dashboardPage: DashboardPage;
subscriptionPage: SubscriptionPaymentPage
landingPage: LandingPage;
authPage: AuthPage;
dashboardPage: DashboardPage;
subscriptionPage: SubscriptionPaymentPage;
mozillaMonitorPage: MozillaMonitorPage;
}>({
authPage: async ({ page }, use) => {
await use(new AuthPage(page))
},
landingPage: async ({ page }, use) => {
await use(new LandingPage(page))
},
dashboardPage: async ({ page }, use) => {
await use(new DashboardPage(page))
},
subscriptionPage: async ({ page }, use) => {
await use(new SubscriptionPaymentPage(page))
},
})
authPage: async ({ page }, use) => {
await use(new AuthPage(page));
},
landingPage: async ({ page }, use) => {
await use(new LandingPage(page));
},
dashboardPage: async ({ page }, use) => {
await use(new DashboardPage(page));
},
subscriptionPage: async ({ page }, use) => {
await use(new SubscriptionPaymentPage(page));
},
mozillaMonitorPage: async ({ page }, use) => {
await use(new MozillaMonitorPage(page));
},
});
export default test;
export const expect = test.expect;
export const expect = test.expect;

Просмотреть файл

@ -1,32 +1,36 @@
import { ENV_URLS, getVerificationCode, setEnvVariables } from "./e2eTestUtils/helpers";
import {
ENV_URLS,
getVerificationCode,
setEnvVariables,
} from "./e2eTestUtils/helpers";
import { AuthPage } from "./pages/authPage";
import { LandingPage } from "./pages/landingPage";
const { chromium } = require('@playwright/test');
const { chromium } = require("@playwright/test");
async function globalSetup() {
// playwright setup
const browser = await chromium.launch();
const page = await browser.newPage();
// playwright setup
const browser = await chromium.launch();
const page = await browser.newPage();
// generate email and set env variables
const randomEmail = `${Date.now()}_tstact@restmail.net`
await setEnvVariables(randomEmail)
// generate email and set env variables
const randomEmail = `${Date.now()}_tstact@restmail.net`;
await setEnvVariables(randomEmail);
await page.goto(ENV_URLS[process.env.E2E_TEST_ENV as string])
const landingPage = new LandingPage(page);
await landingPage.goToSignUp()
await page.goto(ENV_URLS[process.env.E2E_TEST_ENV as string]);
const landingPage = new LandingPage(page);
await landingPage.goToSignUp();
// register user with generated email and set as env variable
const authPage = new AuthPage(page)
await authPage.signUp(randomEmail)
// register user with generated email and set as env variable
const authPage = new AuthPage(page);
await authPage.signUp(randomEmail);
// get verification code from restmail
const verificationCode = await getVerificationCode(randomEmail, page)
await authPage.enterVerificationCode(verificationCode)
// get verification code from restmail
const verificationCode = await getVerificationCode(randomEmail, page);
await authPage.enterVerificationCode(verificationCode);
await page.context().storageState({ path: 'state.json' });
await browser.close();
await page.context().storageState({ path: "state.json" });
await browser.close();
}
export default globalSetup;

Просмотреть файл

@ -1,57 +1,70 @@
import { Locator, Page } from "@playwright/test";
import { forceNonReactLink } from "../e2eTestUtils/helpers";
export class AuthPage {
readonly page: Page
readonly emailInputField: Locator
readonly passwordInputField: Locator
readonly passwordConfirmInputField: Locator
readonly ageInputField: Locator
readonly continueButton: Locator
readonly createAccountButton: Locator
readonly verifyCodeInputField: Locator
readonly confirmCodeButton: Locator
readonly page: Page;
readonly emailInputField: Locator;
readonly passwordInputField: Locator;
readonly passwordConfirmInputField: Locator;
readonly ageInputField: Locator;
readonly continueButton: Locator;
readonly createAccountButton: Locator;
readonly verifyCodeInputField: Locator;
readonly confirmCodeButton: Locator;
constructor(page: Page){
this.page = page;
this.emailInputField = page.locator('input[name="email"]');
this.passwordInputField = process.env["E2E_TEST_ENV"] === "prod" ? page.locator('#password') : page.getByTestId('new-password-input-field');
this.passwordConfirmInputField = process.env["E2E_TEST_ENV"] === "prod" ? page.locator('#vpassword') : page.getByTestId('verify-password-input-field');
this.ageInputField = process.env["E2E_TEST_ENV"] === "prod" ? page.locator('#age') : page.getByTestId('age-input-field');
this.continueButton = page.locator('#submit-btn');
this.createAccountButton = page.getByRole('button', { name: 'Create account' });
this.verifyCodeInputField = process.env["E2E_TEST_ENV"] === "prod" ? page.locator('div.card input') : page.getByTestId('confirm-signup-code-input-field');
this.confirmCodeButton = page.getByRole('button', { name: 'Confirm' });
}
constructor(page: Page) {
this.page = page;
this.emailInputField = page.locator('input[name="email"]');
this.passwordInputField = page.locator("#password");
this.passwordConfirmInputField = page.locator("#vpassword");
this.ageInputField = page.locator("#age");
this.continueButton = page.locator("#submit-btn");
this.createAccountButton = page.getByRole("button", {
name: "Create account",
});
this.verifyCodeInputField = page.locator("div.card input");
this.confirmCodeButton = page.getByRole("button", { name: "Confirm" });
}
async continue() {
await this.continueButton.click();
}
async continue() {
await this.continueButton.click();
}
async enterVerificationCode(code: string){
await this.verifyCodeInputField.fill(code);
await this.confirmCodeButton.click();
}
async enterVerificationCode(code: string) {
await this.verifyCodeInputField.fill(code);
await this.confirmCodeButton.click();
}
async enterEmail(email: string) {
await this.emailInputField.fill(email);
await this.continue();
}
async enterEmail(email: string) {
await forceNonReactLink(this.page);
await this.emailInputField.fill(email);
await this.continue();
}
async enterPassword() {
await this.passwordInputField.fill(process.env.E2E_TEST_ACCOUNT_PASSWORD as string);
await this.continue();
}
async enterPassword() {
await forceNonReactLink(this.page);
await this.passwordInputField.fill(
process.env.E2E_TEST_ACCOUNT_PASSWORD as string,
);
await this.continue();
}
async login(email: string) {
await this.enterEmail(email);
await this.enterPassword();
}
async login(email: string) {
await forceNonReactLink(this.page);
await this.enterEmail(email);
await this.enterPassword();
}
async signUp(email: string){
await this.enterEmail(email)
await this.passwordInputField.fill(process.env.E2E_TEST_ACCOUNT_PASSWORD as string);
await this.passwordConfirmInputField.fill(process.env.E2E_TEST_ACCOUNT_PASSWORD as string);
await this.ageInputField.type("31");
await this.createAccountButton.click()
}
async signUp(email: string) {
await forceNonReactLink(this.page);
await this.enterEmail(email);
await this.passwordInputField.fill(
process.env.E2E_TEST_ACCOUNT_PASSWORD as string,
);
await this.passwordConfirmInputField.fill(
process.env.E2E_TEST_ACCOUNT_PASSWORD as string,
);
await this.ageInputField.type("31");
await this.createAccountButton.click();
}
}

Просмотреть файл

@ -1,243 +1,416 @@
import { Locator, Page } from "@playwright/test";
import { Locator, Page, expect } from "@playwright/test";
import { checkAuthState, getVerificationCode } from "../e2eTestUtils/helpers";
export class DashboardPage {
readonly page: Page
readonly header: Locator
readonly homeButton: Locator
readonly FAQButton: Locator
readonly newsButton: Locator
readonly toastCloseButton: string
readonly userMenuPopUp: Locator
readonly userMenuLetter: Locator
readonly getMoreProtectionButton: Locator
readonly userMenuPopEmail: Locator
readonly upgradeButton: Locator
readonly upgradeNowButton: Locator
readonly userMenuButton: Locator
readonly signOutButton: Locator
readonly signOutToastAlert: Locator
readonly bottomUgradeBanner: Locator
readonly relayExtensionBanner: Locator
readonly dashBoardWithoutMasks: Locator
readonly dashBoardWithoutMasksEmail: Locator
readonly generateNewMaskButton: Locator
readonly emailsForwardedAmount: Locator
readonly emailsBlockedAmount: Locator
readonly emailMasksUsedAmount: Locator
readonly maskCard: Locator
readonly maskCardString: string
readonly maskCardExpanded: Locator
readonly maskCardExpandButton: Locator
readonly maskCardHeader: Locator
readonly maskCardForwardEmail: Locator
readonly maskCardCreatedDate: Locator
readonly maskCardGeneratedEmail: Locator
readonly maskCardForwardedAmount: Locator
readonly maskCardRepliesAmount: Locator
readonly maskCardBlockedAmount: Locator
readonly maskCardDeleteButton: Locator
readonly maskCardCancelButton: Locator
readonly dashboardPageWithoutHeader: Locator
readonly maskCardDeleteDialogModal: Locator
readonly maskCardDeleteDialogModalEmailString: Locator
readonly maskCardDeleteDialogModalGeneratedEmail: Locator
readonly maskCardDeleteConfirmationCheckbox: Locator
readonly maskCardFinalDeleteButton: Locator
readonly maxMaskLimitButton: Locator
readonly page: Page;
readonly header: Locator;
readonly homeButton: Locator;
readonly FAQButton: Locator;
readonly newsButton: Locator;
readonly userMenuPopUp: Locator;
readonly userMenuLetter: Locator;
readonly getMoreProtectionButton: Locator;
readonly userMenuPopEmail: Locator;
readonly upgradeButton: Locator;
readonly upgradeNowButton: Locator;
readonly userMenuButton: Locator;
readonly signOutButton: Locator;
readonly signOutToastAlert: Locator;
readonly bottomUgradeBanner: Locator;
readonly relayExtensionBanner: Locator;
readonly dashBoardWithoutMasks: Locator;
readonly dashBoardWithoutMasksEmail: Locator;
readonly generateNewMaskButton: Locator;
readonly emailsForwardedAmount: Locator;
readonly emailsBlockedAmount: Locator;
readonly emailMasksUsedAmount: Locator;
readonly maskCard: Locator;
readonly maskCardString: string;
readonly maskCardExpanded: Locator;
readonly maskCardExpandButton: Locator;
readonly maskCardHeader: Locator;
readonly maskCardGeneratedEmail: Locator;
readonly maskCardForwardedAmount: Locator;
readonly maskCardRepliesAmount: Locator;
readonly maskCardBlockedAmount: Locator;
readonly maskCardDeleteButton: Locator;
readonly maskCardCancelButton: Locator;
readonly dashboardPageWithoutHeader: Locator;
readonly maskCardDeleteDialogModal: Locator;
readonly maskCardDeleteDialogModalGeneratedEmail: Locator;
readonly maskCardFinalDeleteButton: Locator;
readonly maxMaskLimitButton: Locator;
readonly maxMaskBannerText: Locator;
readonly generateNewMaskPremiumButton: Locator;
readonly premiumRandomMask: Locator;
readonly closeCornerUpsell: Locator;
readonly blockPromotions: Locator;
readonly blockAll: Locator;
readonly blockLevelAllLabel: Locator;
readonly blockLevelPromosLabel: Locator;
readonly premiumDomainMask: Locator;
readonly customMaskInput: Locator;
readonly generateCustomMaskConfirm: Locator;
readonly customMaskSuccessHeader: Locator;
readonly customMaskDoneButton: Locator;
readonly maskCardBottomMeta: Locator;
readonly maskCardTrackersCount: Locator;
constructor(page: Page){
this.page = page;
constructor(page: Page) {
this.page = page;
// dashboard header elements
this.header = page.locator('div header').first();
this.FAQButton = page.locator('header >> text=FAQ')
this.newsButton = page.locator('header >> text=News')
this.homeButton = page.locator('header >> text=Email Masks')
this.userMenuButton = page.locator('//div[starts-with(@class, "UserMenu_wrapper")]')
this.userMenuPopUp = page.locator('//ul[starts-with(@class, "UserMenu_popup")]')
this.userMenuLetter = page.locator('//div[starts-with(@class, "UserMenu_wrapper")]')
this.userMenuPopEmail = page.locator('//span[starts-with(@class, "UserMenu_account")]/b')
this.toastCloseButton = '//div[starts-with(@class, "Layout_close")]'
this.signOutButton = page.locator('button:has-text("Sign Out")').first()
this.signOutToastAlert = page.locator('//div[@class="Toastify__toast-body"]')
// dashboard header elements
this.header = page.locator("div header").first();
this.FAQButton = page.getByText("FAQ").first();
this.newsButton = page.getByText("News");
this.homeButton = page.getByRole("link", { name: "Email masks" });
this.userMenuButton = page.locator(
'//div[starts-with(@class, "UserMenu_wrapper")]',
);
this.userMenuPopUp = page.locator(
'//ul[starts-with(@class, "UserMenu_popup")]',
);
this.userMenuLetter = page.locator(
'//div[starts-with(@class, "UserMenu_wrapper")]',
);
this.userMenuPopEmail = page.locator(
'//span[starts-with(@class, "UserMenu_account")]/b',
);
this.signOutButton = page.locator('button:has-text("Sign Out")').first();
this.signOutToastAlert = page.locator(
'//div[@class="Toastify__toast-body"]',
);
// dashboard elements
this.upgradeNowButton = page.locator('a:has-text("Upgrade Now")')
this.upgradeButton = page.locator('a:has-text("Upgrade")').first()
this.getMoreProtectionButton = page.locator(':has-text("Get more protection")')
this.dashboardPageWithoutHeader = page.locator('//main[starts-with(@class, "profile_profile-wrapper")]')
this.emailsForwardedAmount = page.locator('(//dd[starts-with(@class, "profile_value")])[3]')
this.emailsBlockedAmount = page.locator('(//dd[starts-with(@class, "profile_value")])[2]')
this.emailMasksUsedAmount = page.locator('(//dd[starts-with(@class, "profile_value")])[1]')
this.generateNewMaskButton = page.locator('button:has-text("Generate new mask")')
this.maxMaskLimitButton = page.locator('//div[starts-with(@class, "AliasList_controls")]//a[starts-with(@class, "Button_button")]')
this.bottomUgradeBanner = page.locator('//div[starts-with(@class, "profile_bottom-banner-wrapper")]')
this.relayExtensionBanner = page.locator('//section[starts-with(@class, "profile_banners-wrapper")]/div')
this.dashBoardWithoutMasks = page.locator('//section[starts-with(@class, "Onboarding_wrapper")]')
this.dashBoardWithoutMasksEmail = page.locator('//section[starts-with(@class, "profile_no-premium-header")]')
// dashboard elements
this.upgradeNowButton = page.locator('a:has-text("Upgrade Now")');
this.upgradeButton = page.locator('a:has-text("Upgrade")').first();
this.getMoreProtectionButton = page.locator(
':has-text("Get more protection")',
);
this.dashboardPageWithoutHeader = page.locator(
'//main[starts-with(@class, "profile_profile-wrapper")]',
);
this.emailsForwardedAmount = page.locator(
'(//dd[starts-with(@class, "profile_value")])[3]',
);
this.emailsBlockedAmount = page.locator(
'(//dd[starts-with(@class, "profile_value")])[2]',
);
this.emailMasksUsedAmount = page.locator(
'(//dd[starts-with(@class, "profile_value")])[1]',
);
this.generateNewMaskButton = page.getByTitle("Generate new mask");
this.generateNewMaskPremiumButton = page.locator(
"button:has-text('Generate new mask')",
);
this.maxMaskLimitButton = page.getByText("Get unlimited email masks");
this.maxMaskBannerText = page.locator(
'//p[starts-with(@class, "profile_upsell-banner-description")]',
);
this.premiumRandomMask = page.locator('//li[@data-key="random"]');
this.premiumDomainMask = page.locator('//li[@data-key="custom"]');
this.closeCornerUpsell = page.locator(
'//button[starts-with(@class, "CornerNotification_close-button")]',
);
this.bottomUgradeBanner = page.locator(
'//div[starts-with(@class, "profile_bottom-banner-wrapper")]',
);
this.relayExtensionBanner = page.locator(
'//div[contains(@class, "is-hidden-with-addon")]',
);
this.dashBoardWithoutMasks = page.locator(
'//section[starts-with(@class, "Onboarding_wrapper")]',
);
this.dashBoardWithoutMasksEmail = page.locator(
'//section[starts-with(@class, "profile_no-premium-header")]',
);
// mask card elements
this.maskCard = page.getByRole('button', { name: 'Generate new mask' })
this.maskCardString = '//div[starts-with(@class, "MaskCard_card")]'
this.maskCardExpanded = page.locator('//button[starts-with(@class, "MaskCard_expand")]')
this.maskCardExpandButton = page.locator('//button[starts-with(@class, "MaskCard_expand")]')
this.maskCardHeader = page.locator('//div[starts-with(@class, "MaskCard_summary")]')
this.maskCardGeneratedEmail = page.locator('//button[starts-with(@class, "MaskCard_copy")]/samp').first()
this.maskCardForwardEmail = page.locator('//div[starts-with(@class, "Alias_forward-target")]')
this.maskCardCreatedDate = page.locator('//div[starts-with(@class, "Alias_date-created")]')
this.maskCardForwardedAmount = page.locator('//div[contains(@class, "MaskCard_forwarded")]/dd').first()
this.maskCardRepliesAmount = page.locator('(//span[contains(@class, "Alias_blocked-stat")])[2]')
this.maskCardBlockedAmount = page.locator('(//span[contains(@class, "Alias_blocked-stat")])[1]')
this.maskCardDeleteButton = page.locator('button:has-text("Delete")')
this.maskCardCancelButton = page.locator('button:has-text("Cancel")')
this.maskCardDeleteDialogModal = page.locator('//div[starts-with(@class, "AliasDeletionButton_dialog-wrapper")]')
this.maskCardDeleteDialogModalEmailString = page.locator('//div[starts-with(@class, "AliasDeletionButton_dialog-wrapper")]//strong')
this.maskCardDeleteDialogModalGeneratedEmail = page.locator('//div[starts-with(@class, "AliasDeletionButton_dialog-wrapper")]//samp')
this.maskCardDeleteConfirmationCheckbox = page.locator('#confirmDeletion')
this.maskCardFinalDeleteButton = page.locator('//button[contains(@class, "Button_is-destructive")]')
// mask card elements
this.maskCard = page.getByRole("button", { name: "Generate new mask" });
this.maskCardString = '//div[starts-with(@class, "MaskCard_card")]';
this.maskCardExpanded = page.locator(
'//button[starts-with(@class, "MaskCard_expand")]',
);
this.maskCardExpandButton = page.locator(
'//button[starts-with(@class, "MaskCard_expand")]',
);
this.maskCardHeader = page.locator(
'//div[starts-with(@class, "MaskCard_summary")]',
);
this.maskCardGeneratedEmail = page
.locator('//button[starts-with(@class, "MaskCard_copy")]/samp')
.first();
this.maskCardBottomMeta = page.locator(
'//div[starts-with(@class, "MaskCard_meta")]',
);
this.maskCardForwardedAmount = page
.locator('//div[contains(@class, "MaskCard_forwarded")]/dd')
.first();
this.maskCardTrackersCount = page
.locator('//div[contains(@class, "MaskCard_trackers-removed-stat")]/dd')
.first();
this.maskCardRepliesAmount = page.locator(
'(//span[contains(@class, "Alias_blocked-stat")])[2]',
);
this.maskCardBlockedAmount = page.locator(
'(//span[contains(@class, "Alias_blocked-stat")])[1]',
);
this.maskCardDeleteButton = page.locator('button:has-text("Delete")');
this.maskCardCancelButton = page.locator('button:has-text("Cancel")');
this.maskCardDeleteDialogModal = page.locator(
'//div[starts-with(@class, "AliasDeletionButtonPermanent_dialog-wrapper")]',
);
this.maskCardDeleteDialogModalGeneratedEmail = page.locator(
'//div[starts-with(@class, "AliasDeletionButtonPermanent_dialog-wrapper")]//samp',
);
this.maskCardFinalDeleteButton = page.locator(
'//button[contains(@class, "Button_is-destructive")]',
);
this.blockPromotions = page
.locator(
'//div[starts-with(@class, "MaskCard_block-level-segmented-control")]',
)
.first()
.getByText("Promotions")
.first();
this.blockLevelPromosLabel = page
.locator('//div[starts-with(@class, "MaskCard_block-level-label")]')
.getByText("Blocking promo emails")
.first();
this.blockLevelAllLabel = page
.locator('//div[starts-with(@class, "MaskCard_block-level-label")]')
.getByText("Blocking all emails")
.first();
this.blockAll = page
.locator(
'//div[starts-with(@class, "MaskCard_block-level-segmented-control")]',
)
.first()
.getByText("All")
.first();
this.customMaskInput = page.getByPlaceholder("Enter text");
this.generateCustomMaskConfirm = page.getByRole("button", {
name: "Generate mask",
});
this.customMaskSuccessHeader = page.getByRole("heading", {
name: "Success!",
});
this.customMaskDoneButton = page.getByRole("button", { name: "Done" });
}
async open() {
await this.page.goto("/accounts/profile/");
}
async skipOnboarding() {
const onboardingElem = this.page.getByRole("button", { name: "Skip" });
if (await onboardingElem.isVisible({ timeout: 6000 })) {
await onboardingElem.click();
}
}
async generatePremiumDomainMask(numberOfMasks = 1) {
if (numberOfMasks === 0) {
return;
}
await this.generateNewMaskPremiumButton.click();
await this.premiumDomainMask.click();
await this.customMaskInput.fill(`${Date.now()}`);
await this.generateCustomMaskConfirm.click();
expect(await this.customMaskSuccessHeader.textContent()).toContain(
"Success",
);
await this.customMaskDoneButton.click();
await this.generatePremiumDomainMask(numberOfMasks - 1);
}
async generateMask(numberOfMasks = 1, isPremium = false) {
// check if max number of masks have been created
if (numberOfMasks === 0) {
return;
}
const generateMaskBtn = isPremium
? this.generateNewMaskPremiumButton
: this.generateNewMaskButton;
// generate a new mask and confirm
await generateMaskBtn.click();
const randomMaskShown = await this.premiumRandomMask.isVisible();
if (randomMaskShown) {
await this.premiumRandomMask.click();
}
await this.page.waitForSelector(this.maskCardString, { timeout: 3000 });
// randomize between 1.5-2.5 secs between each generate to deal with issue of multiple quick clicks
await this.page.waitForTimeout(Math.random() * 2500 + 1500);
await this.generateMask(numberOfMasks - 1, isPremium);
}
async upgrade() {
await Promise.all([
this.page.waitForURL(/.*\/accounts\/profile\/.*/),
this.upgradeButton.click(),
]);
}
async upgradeNow() {
await Promise.all([this.upgradeNowButton.click()]);
}
async maybeDeleteMasks(clearAll = true, numberOfMasks = 1) {
let isExpanded = false;
try {
numberOfMasks = await this.page.locator(this.maskCardString).count();
} catch (err) {}
// check number of masks available
if (numberOfMasks === 0) {
return;
}
async open() {
await this.page.goto('/accounts/profile/');
if (await this.closeCornerUpsell.isVisible()) {
await this.closeCornerUpsell.click();
}
async generateMask(numberOfMasks = 1){
// check if max number of masks have been created
if(numberOfMasks === 0){
return
}
// if clear all, check if there's an expanded mask card
if (clearAll) {
try {
await this.page.waitForSelector(this.maskCardString, { timeout: 3000 });
} catch (error) {
console.error("There are no masks to delete");
return;
}
// generate a new mask and confirm
await this.generateNewMaskButton.click()
await this.page.waitForSelector(this.maskCardString, { timeout: 3000 })
// randomize between 1.5-2.5 secs between each generate to deal with issue of multiple quick clicks
await this.page.waitForTimeout((Math.random() * 2500) + 1500)
await this.generateMask(numberOfMasks - 1)
try {
isExpanded = await this.page
.getByRole("button", { expanded: true })
.first()
.isVisible();
} catch {}
}
async upgrade(){
await Promise.all([
this.page.waitForURL(/.*\/accounts\/profile\/.*/),
this.upgradeButton.click()
]);
// locate mask expand button only if mask is not already expanded
if (numberOfMasks && !isExpanded) {
try {
await this.maskCardExpanded.first().click();
} catch {}
}
async upgradeNow(){
await Promise.all([
this.page.waitForNavigation(),
this.upgradeNowButton.click()
]);
// delete flow
if (numberOfMasks) {
const currentMaskCardDeleteButton = this.page
.locator('button:has-text("Delete")')
.first();
await currentMaskCardDeleteButton.click();
await this.maskCardFinalDeleteButton.click();
}
async maybeCloseToaster(){
try {
await this.page.waitForSelector(this.toastCloseButton, { timeout: 2000 })
await this.page.locator(this.toastCloseButton).click()
} catch (error) {
console.error('No Toaster, please proceed')
}
// wait for 500 ms and run flow again with the next masks
await this.page.waitForTimeout(500);
await this.maybeDeleteMasks(true, numberOfMasks - 1);
}
async generateOneRandomMask() {
// reset data
await this.open();
await checkAuthState(this.page);
await this.skipOnboarding();
await this.maybeDeleteMasks();
// create mask and use generated mask email to test email forwarding feature
await this.generateMask(1);
const generatedMaskEmail = await this.maskCardGeneratedEmail.textContent();
return generatedMaskEmail;
}
async checkForwardedEmailCount(attempts = 10) {
if (attempts === 0) {
throw new Error("Email forwarded count did not update");
}
async maybeDeleteMasks(clearAll = true, numberOfMasks = 1){
let isExpanded = false
await this.forceRefresh();
try {
numberOfMasks = await this.page.locator(this.maskCardString).count()
} catch(err){}
// check number of masks available
if(numberOfMasks === 0){
return
}
// if clear all, check if there's an expanded mask card
if(clearAll){
try {
await this.page.waitForSelector(this.maskCardString, { timeout: 3000 })
} catch (error) {
console.error('There are no masks to delete')
return
}
try {
isExpanded = await this.page.getByRole('button', { expanded: true }).first().isVisible()
} catch {}
}
// locate mask expand button only if mask is not already expanded
if(numberOfMasks && !isExpanded){
try {
await this.maskCardExpanded.first().click()
} catch {}
}
// delete flow
if(numberOfMasks){
const currentMaskCardDeleteButton = this.page.locator('button:has-text("Delete")').first()
await currentMaskCardDeleteButton.click()
await this.maskCardDeleteConfirmationCheckbox.click()
await this.maskCardFinalDeleteButton.click()
}
// wait for 500 ms and run flow again with the next masks
await this.page.waitForTimeout(500)
await this.maybeDeleteMasks(true, numberOfMasks - 1)
// check if card is expanded
if (
!(await this.page
.getByRole("button", { expanded: true })
.first()
.isVisible())
) {
await this.maskCardExpanded.first().click();
}
async sendMaskEmail(){
// reset data
await this.open()
await checkAuthState(this.page)
await this.maybeDeleteMasks()
// create mask and use generated mask email to test email forwarding feature
await this.generateMask(1)
const generatedMaskEmail = await this.maskCardGeneratedEmail.textContent()
// TODO: Replace with a page under control of Relay team
await this.page.goto("https://monitor.firefox.com/", { waitUntil: 'networkidle' })
await this.page.locator('#scan-email-address').fill(generatedMaskEmail as string)
await this.page.locator('button.primary').click()
await this.page.waitForURL('**/scan**')
await this.page.getByRole('link', {name: 'Get alerts about new breaches'}).click()
await this.page.locator('input[name=email]').fill(generatedMaskEmail as string)
await this.page.locator('#submit-btn').click()
await this.page.locator('#password').fill(process.env.E2E_TEST_ACCOUNT_PASSWORD as string);
await this.page.locator('#vpassword').fill(process.env.E2E_TEST_ACCOUNT_PASSWORD as string);
await this.page.locator('#age').fill('31');
await this.page.locator('#submit-btn').click()
await this.page.waitForURL('**/confirm_signup_code**')
// verification email from fxa to generatedMaskEmail should be forwarded to E2E_TEST_ACCOUNT_FREE
await getVerificationCode(process.env.E2E_TEST_ACCOUNT_FREE as string, this.page)
// check the forward emails count, if not 0, return the current value
const forwardCount = await this.maskCardForwardedAmount.textContent();
if (forwardCount !== "0") {
return forwardCount;
}
async checkForwardedEmailCount(attempts = 10) {
if (attempts === 0) {
throw new Error('Email forwarded count did not update');
}
return this.checkForwardedEmailCount(attempts - 1);
}
// force a re-request of relayaddresses
await this.FAQButton.click()
await this.homeButton.click()
// check if card is expanded
if(!(await this.page.getByRole('button', { expanded: true }).first().isVisible())){
await this.maskCardExpanded.first().click()
}
// check the forward emails count, if not 0, return the current value
const forwardCount = await this.maskCardForwardedAmount.textContent()
if(forwardCount !== "0"){
return forwardCount;
}
await this.page.waitForTimeout(1000)
return this.checkForwardedEmailCount(attempts - 1)
async setTrackersRemovalOpt() {
await this.page.goto("/accounts/settings/");
const trackersBox = this.page.locator("#tracker-removal");
if (!(await trackersBox.isChecked())) {
await trackersBox.check();
}
await this.page.waitForTimeout(1000);
await this.page.getByRole("button", { name: "Save" }).click();
}
async signUpForPageWithTrackers(mask: string) {
await this.page.goto("https://pages.developmentthatpays.com/cheatsheets/scrum-kanban");
await this.page.getByPlaceholder("First Name").fill("relay-testing");
await this.page.getByPlaceholder("Email Address").fill(mask);
await this.page
.getByRole("button", { name: "let me have that cheat sheet!" })
.click();
await this.page.waitForTimeout(5000);
const captchaShown = await this.page.isVisible(".seva-overlay");
if (captchaShown) {
throw new Error("Unable to continue test, captcha was shown");
}
await this.page.waitForURL("**/scrum-vs-kanban/");
await this.open();
}
async forceRefresh() {
// force a re-request of relayaddresses
await this.FAQButton.click();
await this.homeButton.click();
}
async checkTrackersCount(attempts = 10) {
if (attempts === 0) {
throw new Error("Email trackers count did not update");
}
await this.forceRefresh();
// check if card is expanded
if (
!(await this.page
.getByRole("button", { expanded: true })
.first()
.isVisible())
) {
await this.maskCardExpanded.first().click();
}
// check the forward emails count, if not 0, return the current value
const trackersCount = await this.maskCardTrackersCount.textContent();
if (trackersCount !== "0") {
return trackersCount;
}
this.page.waitForTimeout(500);
return this.checkTrackersCount(attempts - 1);
}
}

Просмотреть файл

@ -1,71 +1,130 @@
import { Locator, Page } from "@playwright/test";
export class LandingPage {
readonly page: Page
readonly header: Locator
readonly FAQButton: Locator
readonly homeButton: Locator
readonly signUpButton: Locator
readonly subscriptionTitle: Locator
readonly planPricingSignUpButton: Locator
readonly signInButton: Locator
readonly firefoxAppsServices: Locator
readonly firefoxAppsServicesHeading: Locator
readonly firefoxLogo: Locator
readonly page: Page;
readonly header: Locator;
readonly FAQButton: Locator;
readonly homeButton: Locator;
readonly signUpButton: Locator;
readonly subscriptionTitle: Locator;
readonly planMatrixDesktop: Locator;
readonly planMatrixMobile: Locator;
yearlyEmailsPlan: Locator;
monthlyEmailsPlan: Locator;
emailsPlanSubmit: Locator;
yearlyEmailsPhonesBundle: Locator;
monthlyEmailsPhonesBundle: Locator;
emailsPhonesBundleSubmit: Locator;
vpnBundleSubmit: Locator;
readonly signInButton: Locator;
readonly firefoxAppsServices: Locator;
readonly firefoxAppsServicesHeading: Locator;
readonly firefoxLogo: Locator;
readonly heroSection: Locator;
constructor(page: Page){
this.page = page
this.header = page.locator('#overlayProvider header')
this.FAQButton = page.getByRole('link', { name: 'FAQ', exact: true })
this.homeButton = page.getByRole('link', { name: 'Home', exact: true })
this.signUpButton = page.locator('a:has-text("Sign Up")').first()
this.planPricingSignUpButton = page.locator('//a[contains(@class, "Plans_premium-plan")]/div')
this.subscriptionTitle = page.locator('[data-testid="subscription-create-title"]')
this.signInButton = page.locator('a:has-text("Sign In")')
this.firefoxAppsServices = page.getByRole('button', { name: 'Firefox apps and services' })
this.firefoxAppsServicesHeading = page.getByRole('heading', { name: 'Firefox is tech that fights for your online privacy.' })
this.firefoxLogo = page.locator('//a[starts-with(@class, "Layout_logo")]')
}
constructor(page: Page) {
this.page = page;
this.heroSection = page.locator("#hero");
this.header = page.locator("#overlayProvider header");
this.FAQButton = page.getByRole("link", { name: "FAQ", exact: true });
this.homeButton = page.getByRole("link", { name: "Home", exact: true });
this.signUpButton = page.locator('a:has-text("Sign Up")').first();
this.planMatrixDesktop = page.locator(
'//table[starts-with(@class, "PlanMatrix_desktop")]',
);
this.planMatrixMobile = page.locator(
'//div[starts-with(@class, "PlanMatrix_mobile")]',
);
this.subscriptionTitle = page.locator(
'[data-testid="subscription-create-title"]',
);
this.signInButton = page.locator('a:has-text("Sign In")');
this.firefoxAppsServices = page.getByRole("button", {
name: "Firefox apps and services",
});
this.firefoxAppsServicesHeading = page.getByRole("heading", {
name: "Firefox is tech that fights for your online privacy.",
});
this.firefoxLogo = page.locator('//a[starts-with(@class, "Layout_logo")]');
this.setPlanElements();
}
async open(){
await this.page.goto(process.env.E2E_TEST_BASE_URL as string)
}
async setPlanElements() {
const isDesktop = await this.planMatrixDesktop.isVisible();
const currPlanMatrix = isDesktop
? this.planMatrixDesktop
: this.planMatrixMobile;
this.yearlyEmailsPlan = currPlanMatrix
.locator('[id*="tab-yearly"]')
.first();
this.monthlyEmailsPlan = currPlanMatrix
.locator('[id*="tab-monthly"]')
.first();
this.emailsPlanSubmit = currPlanMatrix.getByText("Sign Up").first();
this.yearlyEmailsPhonesBundle = currPlanMatrix
.locator('[id*="tab-yearly"]')
.nth(1);
this.monthlyEmailsPhonesBundle = currPlanMatrix
.locator('[id*="tab-monthly"]')
.nth(1);
this.emailsPhonesBundleSubmit = currPlanMatrix.getByText("Sign Up").nth(1);
this.vpnBundleSubmit = currPlanMatrix.getByText("Sign Up").nth(2);
}
async goHome(){
await Promise.all([
this.page.waitForLoadState("networkidle"),
this.homeButton.click()
]);
}
async open() {
await this.page.goto(process.env.E2E_TEST_BASE_URL as string);
}
async goToFAQ(){
await Promise.all([
this.page.waitForURL(/faq/),
this.FAQButton.click()
]);
}
async goHome() {
await Promise.all([
this.page.waitForLoadState("networkidle"),
this.homeButton.click(),
]);
}
async goToSignUp(){
await this.signUpButton.click()
}
async goToFAQ() {
await Promise.all([this.page.waitForURL(/faq/), this.FAQButton.click()]);
}
async selectPricingPlanSignUp(){
await Promise.all([
this.page.waitForNavigation(),
this.planPricingSignUpButton.click()
]);
}
async goToSignUp() {
await this.signUpButton.click();
}
async goToSignIn(){
await this.signInButton.click()
}
async selectYearlyEmailsPlan() {
await this.yearlyEmailsPlan.click();
await this.emailsPlanSubmit.click();
}
async openFirefoxAppsServices(){
await this.page.waitForLoadState("networkidle")
await this.firefoxAppsServices.click({ force: true })
}
async selectMonthlyEmailsPlan() {
await this.monthlyEmailsPlan.click();
await this.emailsPlanSubmit.click();
}
async clickFirefoxLogo(){
await this.firefoxLogo.click()
}
}
async selectYearlyPhonesEmailsBundle() {
await this.yearlyEmailsPhonesBundle.click();
await this.emailsPhonesBundleSubmit.click();
}
async selectMonthlyPhonesEmailsBundle() {
await this.monthlyEmailsPhonesBundle.click();
await this.emailsPhonesBundleSubmit.click();
}
async selectVpnBundlePlan() {
await this.vpnBundleSubmit.click();
}
async goToSignIn() {
await this.signInButton.click();
await this.page.waitForURL("**/oauth/**");
}
async openFirefoxAppsServices() {
await this.page.waitForLoadState("networkidle");
await this.firefoxAppsServices.click({ force: true });
}
async clickFirefoxLogo() {
await this.firefoxLogo.click();
}
}

Просмотреть файл

@ -0,0 +1,47 @@
import { Page, Locator } from "@playwright/test";
import { getVerificationCode } from "../e2eTestUtils/helpers";
export class MozillaMonitorPage {
readonly page: Page;
readonly monitorSignUpInput: Locator;
readonly monitorSignUpButton: Locator;
constructor(page: Page) {
this.page = page;
this.monitorSignUpInput = page
.locator("//form[contains(@class, 'SignUpForm_form')]/input")
.first();
this.monitorSignUpButton = page
.locator("button.Button_primary___XZsP")
.first();
}
async signupWithMask(randomMask: string | null) {
if (randomMask === null) {
return new Error("Mask could not be created.");
}
await this.page.goto("https://monitor.mozilla.org/", {
waitUntil: "networkidle",
});
await this.monitorSignUpInput.fill(randomMask as string);
await this.monitorSignUpButton.click();
await this.page.waitForURL("**/oauth/signup**");
await this.page
.locator("#password")
.fill(process.env.E2E_TEST_ACCOUNT_PASSWORD as string);
await this.page
.locator("#vpassword")
.fill(process.env.E2E_TEST_ACCOUNT_PASSWORD as string);
await this.page.locator("#age").fill("31");
await this.page.locator("#submit-btn").click();
await this.page.waitForURL("**/confirm_signup_code**");
// verification email from fxa to generatedMaskEmail should be forwarded to E2E_TEST_ACCOUNT_FREE
await getVerificationCode(
process.env.E2E_TEST_ACCOUNT_FREE as string,
this.page,
);
}
}

Просмотреть файл

@ -1,29 +1,46 @@
import { Locator, Page } from "@playwright/test";
export class SubscriptionPaymentPage {
readonly page: Page
readonly paypalButton: Locator
readonly paymentDiv: Locator
readonly productDetails: Locator
readonly discountForm: Locator
readonly paymentNameField: Locator
readonly cardNumberField: Locator
readonly cardExpiryField: Locator
readonly cardCvcField: Locator
readonly postalCodeField: Locator
readonly authorizationCheckbox: Locator
readonly page: Page;
readonly paypalButton: Locator;
readonly paymentDiv: Locator;
readonly productDetails: Locator;
readonly discountForm: Locator;
readonly paymentNameField: Locator;
readonly cardNumberField: Locator;
readonly cardExpiryField: Locator;
readonly cardCvcField: Locator;
readonly postalCodeField: Locator;
readonly authorizationCheckbox: Locator;
readonly subscriptionTitle: Locator;
readonly subscriptionType: Locator;
readonly planDetails: Locator;
readonly planType: Locator;
constructor(page: Page) {
this.page = page
this.authorizationCheckbox = page.locator('[data-testid="confirm"]')
this.paypalButton = page.locator('[data-testid="pay-with-other"]')
this.paymentDiv = page.locator('[data-testid="subscription-create"]')
this.productDetails = page.locator('.plan-details-component-inner')
this.discountForm = page.locator('[data-testid="coupon-component"]')
this.paymentNameField = page.locator('[data-testid="name"]')
this.cardNumberField = page.locator('[data-elements-stable-field-name="cardNumber"]')
this.cardExpiryField = page.locator('[data-elements-stable-field-name="cardExpiry"]')
this.cardCvcField = page.locator('[data-elements-stable-field-name="cardCvc"]')
this.postalCodeField = page.locator('[data-elements-stable-field-name="postalCode"]')
}
}
constructor(page: Page) {
this.page = page;
this.authorizationCheckbox = page.locator('[data-testid="confirm"]');
this.paypalButton = page.locator('[data-testid="pay-with-other"]');
this.paymentDiv = page.locator('[data-testid="subscription-create"]');
this.productDetails = page.locator(".plan-details-component-inner");
this.discountForm = page.locator('[data-testid="coupon-component"]');
this.paymentNameField = page.locator('[data-testid="name"]');
this.cardNumberField = page.locator(
'[data-elements-stable-field-name="cardNumber"]',
);
this.cardExpiryField = page.locator(
'[data-elements-stable-field-name="cardExpiry"]',
);
this.cardCvcField = page.locator(
'[data-elements-stable-field-name="cardCvc"]',
);
this.postalCodeField = page.locator(
'[data-elements-stable-field-name="postalCode"]',
);
this.subscriptionTitle = page.locator(
'[data-testid="subscription-create-title"]',
);
this.planDetails = page.locator("#plan-details-product");
this.planType = page.locator(".plan-details-description");
}
}

Просмотреть файл

@ -1,46 +1,177 @@
import test, { expect } from '../fixtures/basePages'
import { checkAuthState } from '../e2eTestUtils/helpers';
import test, { expect } from "../fixtures/basePages";
import { checkAuthState } from "../e2eTestUtils/helpers";
test.describe.configure({ mode: 'parallel' });
test.skip(({ browserName }) => browserName !== 'firefox', 'firefox only e2e!');
test.fixme('Relay e2e function email forwarding', () => {
// use stored authenticated state
test.use({ storageState: 'state.json' })
test.describe.configure({ mode: "parallel" });
test.describe("FxA auth, random mask generation, and email forwarding @health_check", () => {
test.skip(
process.env.E2E_TEST_ENV === "prod",
"This test only works on stage/dev environments",
);
// use stored authenticated state
test.use({ storageState: "state.json" });
test.beforeEach(async ({ dashboardPage }) => {
await dashboardPage.sendMaskEmail()
});
test('Check that the user can use the masks on websites and receive emails sent to the masks, C1553068, C1553065, C1811801', async ({
dashboardPage,
page
}) => {
// This tests creates a new Mozilla Account with a new mask, to have
// the signup confirmation email show up in the forwarded email count.
// This is a pretty slow process:
test.slow()
await expect(async () => {
await dashboardPage.open()
await checkAuthState(page)
const forwardedEmailCount = await dashboardPage.checkForwardedEmailCount()
expect(forwardedEmailCount).toEqual('1')
}).toPass()
})
})
test.fixme('Relay e2e auth flows', () => {
// pricing table is being updated -- will update this test afterwords
test.beforeEach(async ({ landingPage }) => {
await landingPage.open()
test.beforeEach(async ({ dashboardPage, mozillaMonitorPage }) => {
const randomMask = await dashboardPage.generateOneRandomMask();
await mozillaMonitorPage.signupWithMask(randomMask);
});
test('Verify that the "Sign Up" button works correctly, C1818792', async ({
landingPage
test("Check that the user can use the masks on websites and receive emails sent to the masks, C1553068, C1553065, C1811801", async ({
dashboardPage,
page,
}) => {
await landingPage.selectPricingPlanSignUp()
// This tests creates a new Mozilla Account with a new mask, to have
// the signup confirmation email show up in the forwarded email count.
// This is a pretty slow process:
await expect(async () => {
await dashboardPage.open();
await checkAuthState(page);
await dashboardPage.skipOnboarding();
const forwardedEmailCount =
await dashboardPage.checkForwardedEmailCount();
expect(forwardedEmailCount).toEqual("1");
}).toPass();
});
});
test.describe("Email forwarding and trackers removal", () => {
test.skip(
({ browserName }) => browserName !== "firefox",
"firefox only test",
);
test.use({ storageState: "state.json" });
let initialTrackersCount;
test.beforeEach(async ({ dashboardPage }) => {
const randomMask = await dashboardPage.generateOneRandomMask();
initialTrackersCount =
await dashboardPage.maskCardTrackersCount.textContent();
// Set trackers removed option
await dashboardPage.setTrackersRemovalOpt();
try {
await dashboardPage.signUpForPageWithTrackers(randomMask as string);
} catch (e) {
test.skip(true, e.message);
}
});
test("Verify that an email with trackers is forwarded, and trackers are detected", async ({
dashboardPage,
}) => {
const newTrackersCount = await dashboardPage.checkTrackersCount();
expect(parseInt(newTrackersCount)).toBeGreaterThan(
parseInt(initialTrackersCount),
);
});
});
test.describe("Subscription flows @health_check", () => {
/**
* Verifies that all plans correctly redirect to its corresponding subscriptions page.
*/
test.beforeEach(async ({ landingPage }) => {
await landingPage.open();
});
test('Verify that the yearly emails plan "Sign Up" button works correctly, C1818792', async ({
landingPage,
subscriptionPage,
}) => {
await landingPage.selectYearlyEmailsPlan();
const expectedPlanDetails =
process.env["E2E_TEST_ENV"] === "prod"
? "Relay Premium"
: "Relay Premium (stage)";
// verify redirect to subscription page
expect(await landingPage.subscriptionTitle.textContent()).toContain('Set up your subscription')
})
})
expect(await subscriptionPage.subscriptionTitle.textContent()).toContain(
"Set up your subscription",
);
expect(await subscriptionPage.planDetails.textContent()).toEqual(
expectedPlanDetails,
);
expect(await subscriptionPage.planType.textContent()).toContain("yearly");
});
test('Verify that the monthly emails plan "Sign Up" button works correctly, C1818792', async ({
landingPage,
subscriptionPage,
}) => {
await landingPage.selectMonthlyEmailsPlan();
const expectedPlanDetails =
process.env["E2E_TEST_ENV"] === "prod"
? "Relay Premium"
: "Relay Premium (stage)";
// verify redirect to subscription page
expect(await subscriptionPage.subscriptionTitle.textContent()).toContain(
"Set up your subscription",
);
expect(await subscriptionPage.planDetails.textContent()).toEqual(
expectedPlanDetails,
);
expect(await subscriptionPage.planType.textContent()).toContain("monthly");
});
test('Verify that the yearly emails and phones bundle plan "Sign Up" button works correctly, C1818792', async ({
landingPage,
subscriptionPage,
}) => {
await landingPage.selectYearlyPhonesEmailsBundle();
const expectedPlanDetails =
process.env["E2E_TEST_ENV"] === "prod"
? "Relay Premium: Phone Number & Email Address Masking"
: "Relay Email & Phone Protection (stage)";
// verify redirect to subscription page
expect(await subscriptionPage.subscriptionTitle.textContent()).toContain(
"Set up your subscription",
);
expect(await subscriptionPage.planDetails.textContent()).toEqual(
expectedPlanDetails,
);
expect(await subscriptionPage.planType.textContent()).toContain("yearly");
});
test('Verify that the monthly emails and phones bundle plan "Sign Up" button works correctly, C1818792', async ({
landingPage,
subscriptionPage,
}) => {
await landingPage.selectMonthlyPhonesEmailsBundle();
const expectedPlanDetails =
process.env["E2E_TEST_ENV"] === "prod"
? "Relay Premium: Phone Number & Email Address Masking"
: "Relay Email & Phone Protection (stage)";
// verify redirect to subscription page
expect(await subscriptionPage.subscriptionTitle.textContent()).toContain(
"Set up your subscription",
);
expect(await subscriptionPage.planDetails.textContent()).toEqual(
expectedPlanDetails,
);
expect(await subscriptionPage.planType.textContent()).toContain("monthly");
});
test('Verify that the VPN bundle "Sign Up" button works correctly, C1818792', async ({
landingPage,
subscriptionPage,
}) => {
await landingPage.selectVpnBundlePlan();
const expectedPlanDetails =
process.env["E2E_TEST_ENV"] === "prod"
? "Mozilla VPN & Firefox Relay"
: "Firefox Relay & Mozilla VPN (Stage)";
// verify redirect to subscription page
expect(await subscriptionPage.subscriptionTitle.textContent()).toContain(
"Set up your subscription",
);
expect(await subscriptionPage.planDetails.textContent()).toEqual(
expectedPlanDetails,
);
expect(await subscriptionPage.planType.textContent()).toContain("yearly");
});
});

Просмотреть файл

@ -1,85 +1,105 @@
import test, { expect } from '../fixtures/basePages'
import { checkAuthState, defaultScreenshotOpts } from '../e2eTestUtils/helpers';
import test, { expect } from "../fixtures/basePages";
import { checkAuthState, defaultScreenshotOpts } from "../e2eTestUtils/helpers";
// using logged in state outside of describe block will cover state for all tests in file
test.use({ storageState: 'state.json' })
test.describe.fixme('Free - General Functionalities, Desktop', () => {
test.use({ storageState: "state.json" });
test.describe("Free - General Functionalities, Desktop", () => {
test.beforeEach(async ({ dashboardPage, page }) => {
await dashboardPage.open()
await checkAuthState(page)
await dashboardPage.maybeDeleteMasks()
await dashboardPage.open();
await checkAuthState(page);
await dashboardPage.skipOnboarding();
await dashboardPage.maybeDeleteMasks();
});
test('Check the free user can only create 5 masks, C1553067', async ({ dashboardPage, page }) => {
test("Check the free user can only create 5 masks, C1553067 @health_check", async ({
dashboardPage,
page,
}) => {
// Generating five masks takes a while:
test.slow()
await expect(async () => {
await dashboardPage.generateMask(5)
expect(await page.locator(dashboardPage.maskCardString).count() === 5)
}).toPass()
await dashboardPage.generateMask(5);
expect((await page.locator(dashboardPage.maskCardString).count()) === 5);
}).toPass();
// After five times, user cannot add other masks anymore
expect(await dashboardPage.maxMaskLimitButton.textContent()).toContain(
"Get unlimited email masks",
);
expect(await dashboardPage.maxMaskBannerText.innerText()).toContain(
"mask limit",
);
});
});
// After five times, the button becomes greyed-out and the user cannot add other masks anymore (TODO: for a free user from a country where Premium is NOT available).
expect(await dashboardPage.maxMaskLimitButton.textContent()).toContain('Get unlimited email masks')
})
})
test.describe.fixme('Free - General Functionalities, Desktop - Visual Regression', () => {
test.skip(({ browserName }) => browserName !== 'firefox', 'firefox only image comparisons!');
test.describe("Free - General Functionalities, Desktop - Visual Regression", () => {
test.skip(
({ browserName }) => browserName !== "firefox",
"firefox only image comparisons!",
);
test.beforeEach(async ({ dashboardPage, page }) => {
await dashboardPage.open()
await checkAuthState(page)
await dashboardPage.maybeDeleteMasks()
await dashboardPage.open();
await checkAuthState(page);
await dashboardPage.skipOnboarding();
await dashboardPage.maybeDeleteMasks();
});
test('Verify that the Header is displayed correctly for a Free user that is logged in, C1812639', async ({ dashboardPage }) => {
await dashboardPage.maybeCloseToaster()
test("Verify that the Header is displayed correctly for a Free user that is logged in, C1812639", async ({
dashboardPage,
}) => {
await expect(dashboardPage.header).toHaveScreenshot(
`${process.env.E2E_TEST_ENV}-dashboardHeader.png`,
{...defaultScreenshotOpts, mask: [dashboardPage.userMenuLetter] }
{ ...defaultScreenshotOpts },
);
})
});
test.fixme('Verify that the "Profile" button and its options work correctly, C1812641', async ({ dashboardPage }) => {
await dashboardPage.relayExtensionBanner.scrollIntoViewIfNeeded()
test("Verify that the extension upgrade banner is shown, C1812641", async ({
dashboardPage,
}) => {
await dashboardPage.relayExtensionBanner.scrollIntoViewIfNeeded();
await expect(dashboardPage.relayExtensionBanner).toHaveScreenshot(
`${process.env.E2E_TEST_ENV}-relayExtensionBanner.png`,
defaultScreenshotOpts
defaultScreenshotOpts,
);
});
await dashboardPage.bottomUgradeBanner.scrollIntoViewIfNeeded()
await expect(dashboardPage.bottomUgradeBanner).toHaveScreenshot(
`${process.env.E2E_TEST_ENV}-bottomUgradeBanner.png`,
defaultScreenshotOpts
test("Verify that opened mask cards are displayed correctly to a Free user, C1553070", async ({
dashboardPage,
page,
}) => {
await expect(async () => {
await dashboardPage.generateMask(1);
expect((await page.locator(dashboardPage.maskCardString).count()) === 1);
}).toPass();
await expect(page.locator(dashboardPage.maskCardString)).toHaveScreenshot(
`${process.env.E2E_TEST_ENV}-maskCard.png`,
{
...defaultScreenshotOpts,
mask: [
dashboardPage.maskCardGeneratedEmail,
dashboardPage.maskCardBottomMeta,
],
},
);
})
});
test('Verify that opened mask cards are displayed correctly to a Free user, C1553070', async ({ dashboardPage, page }) => {
test("Check that the user can delete an mask, and is prompted to confirm before they delete, C1553071 @health_check", async ({
dashboardPage,
page,
}) => {
await expect(async () => {
await dashboardPage.generateMask(1)
expect(await page.locator(dashboardPage.maskCardString).count() === 1)
}).toPass()
await dashboardPage.generateMask(1);
expect((await page.locator(dashboardPage.maskCardString).count()) === 1);
await dashboardPage.maskCardDeleteButton.click();
}).toPass();
await expect(page.locator(dashboardPage.maskCardString)).toHaveScreenshot(`${process.env.E2E_TEST_ENV}-maskCard.png`,
{...defaultScreenshotOpts, mask: [
dashboardPage.maskCardForwardEmail,
dashboardPage.maskCardGeneratedEmail,
dashboardPage.maskCardCreatedDate
]});
})
test.skip('Check that the user can delete an mask, and is prompted to confirm before they delete, C1553071', async ({ dashboardPage, page }) => {
await expect(async () => {
await dashboardPage.generateMask(1)
expect(await page.locator(dashboardPage.maskCardString).count() === 1)
await dashboardPage.maskCardDeleteButton.click()
}).toPass()
await expect(dashboardPage.maskCardDeleteDialogModal).toHaveScreenshot(`${process.env.E2E_TEST_ENV}-maskCardDeleteDialogModal.png`,
{...defaultScreenshotOpts, mask: [
dashboardPage.maskCardDeleteDialogModalEmailString,
dashboardPage.maskCardDeleteDialogModalGeneratedEmail
]});
})
})
await expect(dashboardPage.maskCardDeleteDialogModal).toHaveScreenshot(
`${process.env.E2E_TEST_ENV}-maskCardDeleteDialogModal.png`,
{
...defaultScreenshotOpts,
mask: [dashboardPage.maskCardDeleteDialogModalGeneratedEmail],
},
);
});
});

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 48 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 47 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 14 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 38 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 37 KiB

После

Ширина:  |  Высота:  |  Размер: 36 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 42 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 39 KiB

После

Ширина:  |  Высота:  |  Размер: 28 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 17 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 16 KiB

После

Ширина:  |  Высота:  |  Размер: 21 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 48 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 47 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 13 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 36 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 35 KiB

После

Ширина:  |  Высота:  |  Размер: 36 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 46 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 43 KiB

После

Ширина:  |  Высота:  |  Размер: 28 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 22 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 22 KiB

После

Ширина:  |  Высота:  |  Размер: 21 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 15 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 48 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 47 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 15 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 53 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 36 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 46 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 44 KiB

После

Ширина:  |  Высота:  |  Размер: 28 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 61 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 16 KiB

После

Ширина:  |  Высота:  |  Размер: 21 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 16 KiB

Просмотреть файл

@ -1,49 +1,67 @@
import test, { expect } from '../fixtures/basePages'
import { defaultScreenshotOpts } from '../e2eTestUtils/helpers';
import test, { expect } from "../fixtures/basePages";
import { defaultScreenshotOpts } from "../e2eTestUtils/helpers";
test.describe('Firefox Relay - Landing Page - Visual Regression', () => {
test.skip(({ browserName }) => browserName !== 'firefox', 'firefox only image comparisons!');
test.describe("Firefox Relay - Landing Page - Visual Regression @health_check", () => {
test.skip(
({ browserName }) => browserName !== "firefox",
"firefox only image comparisons!",
);
test.beforeEach(async ({ landingPage }) => {
await landingPage.open()
await landingPage.open();
});
test('Verify that the header is displayed correctly for a user that is NOT logged in, C1812637', async ({ landingPage }) => {
test("Verify that the header is displayed correctly for a user that is NOT logged in, C1812637", async ({
landingPage,
}) => {
await expect(landingPage.header).toHaveScreenshot(
`${process.env.E2E_TEST_ENV}-landingHeader.png`,
defaultScreenshotOpts
defaultScreenshotOpts,
);
});
test("Verify home page is not malformed", async ({ landingPage }) => {
await expect(landingPage.heroSection).toHaveScreenshot(
`${process.env.E2E_TEST_ENV}-landingHero.png`,
defaultScreenshotOpts,
);
});
});
test.describe('Check header buttons and their redirects, C1812638', () => {
test.describe("Check header buttons and their redirects, C1812638 @health_check", () => {
test.beforeEach(async ({ landingPage }) => {
await landingPage.open()
await landingPage.open();
});
test('Verify home FAQ button redirect', async ({ landingPage }) => {
const FAQRedirectLink = await landingPage.FAQButton.getAttribute('href')
expect(FAQRedirectLink).toEqual('/faq/')
})
test("Verify home FAQ button redirect", async ({ landingPage }) => {
const FAQRedirectLink = await landingPage.FAQButton.getAttribute("href");
expect(FAQRedirectLink).toEqual("/faq/");
});
test('Verify home button redirect', async ({ landingPage }) => {
const homeRedirectLink = await landingPage.homeButton.getAttribute('href')
expect(homeRedirectLink).toEqual('/')
})
test("Verify home button redirect", async ({ landingPage }) => {
const homeRedirectLink = await landingPage.homeButton.getAttribute("href");
expect(homeRedirectLink).toEqual("/");
});
test('Verify home firefox logo redirect', async ({ landingPage }) => {
const firefoxLogoRedirectLink = await landingPage.firefoxLogo.getAttribute('href')
expect(firefoxLogoRedirectLink).toEqual('/')
})
test("Verify home firefox logo redirect", async ({ landingPage }) => {
const firefoxLogoRedirectLink =
await landingPage.firefoxLogo.getAttribute("href");
expect(firefoxLogoRedirectLink).toEqual("/");
});
test('Verify sign in button authentication flow, C1818784', async ({ landingPage, authPage }) => {
await landingPage.goToSignIn()
expect(authPage.emailInputField.isVisible()).toBeTruthy()
})
test("Verify sign in button authentication flow, C1818784", async ({
landingPage,
authPage,
}) => {
await landingPage.goToSignIn();
expect(authPage.emailInputField.isVisible()).toBeTruthy();
});
test('Verify sign up button authentication flow, C1818782', async ({ landingPage, authPage }) => {
await landingPage.goToSignUp()
expect(authPage.emailInputField.isVisible()).toBeTruthy()
})
});
test("Verify sign up button authentication flow, C1818782", async ({
landingPage,
authPage,
}) => {
await landingPage.goToSignUp();
expect(authPage.emailInputField.isVisible()).toBeTruthy();
});
});

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 32 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 30 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 10 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 113 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 32 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 30 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 10 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 113 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 32 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 30 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 12 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 113 KiB

Просмотреть файл

@ -1,20 +1,50 @@
import test, { expect } from '../fixtures/basePages'
import test, { expect } from "../fixtures/basePages";
test.describe.skip('Premium - General Functionalities, Desktop', () => {
test.describe("Premium - General Functionalities, Desktop", () => {
test.beforeEach(async ({ landingPage, authPage, dashboardPage }) => {
await landingPage.open()
await landingPage.goToSignIn()
await authPage.login(process.env.E2E_TEST_ACCOUNT_PREMIUM as string)
await dashboardPage.maybeDeleteMasks()
await landingPage.open();
await landingPage.goToSignIn();
await authPage.login(process.env.E2E_TEST_ACCOUNT_PREMIUM as string);
const totalMasks = await dashboardPage.emailMasksUsedAmount.textContent();
await dashboardPage.maybeDeleteMasks(true, parseInt(totalMasks as string));
});
test('Check the premium user can more than 5 masks', async ({ dashboardPage }) => {
await dashboardPage.generateMask(6)
await expect.poll(async () => {
return await dashboardPage.emailMasksUsedAmount.textContent()
}, {
intervals: [1_000]
}).toContain('6');
})
})
test("Verify that a premium user can make more than 5 masks @health_check", async ({
dashboardPage,
}) => {
await dashboardPage.generateMask(6, true);
await expect
.poll(
async () => {
return await dashboardPage.emailMasksUsedAmount.textContent();
},
{
intervals: [1_000],
},
)
.toContain("6");
});
test("Verify that a user can click the mask blocking options", async ({
dashboardPage,
}) => {
await dashboardPage.generateMask(1, true);
await dashboardPage.blockPromotions.click();
expect(await dashboardPage.blockLevelPromosLabel.textContent()).toContain(
"Blocking promo emails",
);
await dashboardPage.blockAll.click();
expect(await dashboardPage.blockLevelAllLabel.textContent()).toContain(
"Blocking all emails",
);
});
test("Verify that a premium user can generate a custom mask @health_check", async ({
dashboardPage,
}) => {
// When there are zero masks, a random mask must be generated first
await dashboardPage.generateMask();
await dashboardPage.generatePremiumDomainMask();
});
});

Просмотреть файл

@ -1,51 +1,36 @@
import test, { expect } from '../fixtures/basePages'
import { checkAuthState, defaultScreenshotOpts } from '../e2eTestUtils/helpers';
import test, { expect } from "../fixtures/basePages";
import { checkAuthState, defaultScreenshotOpts } from "../e2eTestUtils/helpers";
// using logged in state outside of describe block will cover state for all tests in file
test.use({ storageState: 'state.json' })
test.describe.configure({ mode: 'parallel' });
test.describe('Premium Relay - Purchase Premium Flow, Desktop', () => {
test.skip(({ browserName }) => browserName === 'firefox', 'FxA Issue with with authentication');
test.use({ storageState: "state.json" });
test.describe.configure({ mode: "parallel" });
test.describe("Premium Relay - Purchase Premium Flow, Desktop", () => {
test.beforeEach(async ({ dashboardPage, page }) => {
await dashboardPage.open()
await checkAuthState(page)
await dashboardPage.open();
await checkAuthState(page);
});
test('Verify that the "Upgrade" button redirects correctly, C1812640, 1808503', async ({ dashboardPage, page }) => {
await dashboardPage.upgrade()
expect(page.url()).toContain('/premium/')
})
})
test.describe.skip(() => {
// TODO: add flow for stage only
// run this test only on stage as only stage will accept test cards
test.skip(() => process.env.E2E_TEST_ENV !== 'stage', 'run only on stage');
test.beforeEach(async ({ dashboardPage, page }) => {
await dashboardPage.open()
await checkAuthState(page)
test('Verify that the "Upgrade" button redirects correctly, C1812640, 1808503', async ({
dashboardPage,
page,
}) => {
await dashboardPage.upgrade();
expect(page.url()).toContain("/premium/");
});
})
test.describe.skip('Premium Relay - Purchase Premium Flow, Desktop - Visual Regression', () => {
test.skip(({ browserName }) => browserName !== 'firefox', 'firefox only image comparisons!');
});
test.describe("Premium Relay - Purchase Premium Flow, Desktop - Visual Regression", () => {
test.beforeEach(async ({ dashboardPage, page }) => {
await dashboardPage.open()
await checkAuthState(page)
await dashboardPage.open();
await checkAuthState(page);
await dashboardPage.skipOnboarding();
});
test('Verify that the subscription page is displayed correctly, C1553108', async ({ subscriptionPage, dashboardPage, page }) => {
await dashboardPage.upgradeNow()
expect(page.url()).toContain('subscriptions')
await expect(subscriptionPage.paypalButton).toBeVisible()
// will uncomment in a fast follow update
// await expect(subscriptionPage.productDetails).toHaveScreenshot(
// `${process.env.E2E_TEST_ENV}-productDetails.png`,
// defaultScreenshotOpts
// );
})
})
test("Verify that the subscription page is displayed correctly, C1553108", async ({
dashboardPage,
page,
}) => {
await dashboardPage.upgradeNow();
expect(page.url()).toContain("premium");
});
});

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 32 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 32 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 23 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 22 KiB

Просмотреть файл

@ -15,14 +15,14 @@ const config: PlaywrightTestConfig = {
/* Add location of specs. */
testDir: 'e2e-tests/specs',
/* Maximum time one test can run for. */
timeout: 60_000,
/* Maximum time one test can run for. 3 minutes */
timeout: 60 * 1000,
/* Global setup */
globalSetup: require.resolve('./e2e-tests/global-setup.ts'),
/* Max time in milliseconds the whole test suite can to prevent CI breaking. */
globalTimeout: 360_000,
/* Max time in milliseconds the whole test suite can to prevent CI breaking. 15 minutes */
globalTimeout: 60 * 15 * 1000,
// adding missing snapshots for later comparison
updateSnapshots: 'missing',
@ -35,7 +35,7 @@ const config: PlaywrightTestConfig = {
timeout: 5_000
},
/* Run tests in files in parallel */
// fullyParallel: true,
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */