diff --git a/.circleci/base-install.sh b/.circleci/base-install.sh index 799f6b298a..00038ece6c 100755 --- a/.circleci/base-install.sh +++ b/.circleci/base-install.sh @@ -1,13 +1,8 @@ #!/bin/bash -ex -MODULE=$1 DIR=$(dirname "$0") cd "$DIR/.." yarn install --immutable node .circleci/modules-to-test.js | tee packages/test.list -if ([[ "$MODULE" == "many" ]] && grep -e '.' packages/test.list > /dev/null) || - grep -e "$MODULE" -e 'all' packages/test.list > /dev/null; then - ./.circleci/assert-branch.sh - ./_scripts/create-version-json.sh -fi +./_scripts/create-version-json.sh diff --git a/.circleci/config.yml b/.circleci/config.yml index be2871d3fd..5fc48d245f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -259,21 +259,65 @@ jobs: - store_test_results: path: artifacts/tests + # This job is manually triggered for now. see .circleci/README.md + production-smoke-tests: + resource_class: medium+ + docker: + - image: mcr.microsoft.com/playwright:focal + steps: + - checkout + # use a separate cache until this is on main + - restore_cache: + keys: + # prefer the exact hash + - fxa-yarn-cache-pw-{{ checksum "yarn.lock" }} + # any cache to start with is better than nothing + - fxa-yarn-cache-pw- + - run: ./.circleci/base-install.sh + - save_cache: + key: fxa-yarn-cache-pw-{{ checksum "yarn.lock" }} + paths: + - .yarn/cache + - .yarn/build-state.yml + - .yarn/install-state.gz + - run: + name: Running smoke tests + command: yarn workspace functional-tests test-production + - store_artifacts: + path: artifacts + - store_test_results: + path: artifacts/tests + playwright-functional-tests: - resource_class: medium + resource_class: medium+ docker: - image: mcr.microsoft.com/playwright:focal - image: redis - image: memcached - image: circleci/mysql:5.7.27 environment: - NODE_ENV: development + NODE_ENV: dev steps: - - base-install: - package: fxa-settings + # needed by check-mysql.sh + - run: apt-get install -y netcat + - checkout + # use a separate cache until this is on main + - restore_cache: + keys: + # prefer the exact hash + - fxa-yarn-cache-pw-{{ checksum "yarn.lock" }} + # any cache to start with is better than nothing + - fxa-yarn-cache-pw- + - run: ./.circleci/base-install.sh + - save_cache: + key: fxa-yarn-cache-pw-{{ checksum "yarn.lock" }} + paths: + - .yarn/cache + - .yarn/build-state.yml + - .yarn/install-state.gz - run: name: Running playwright tests - command: ./packages/fxa-settings/scripts/playwright-tests.sh + command: ./packages/functional-tests/scripts/test-ci.sh - store_artifacts: path: artifacts - store_test_results: @@ -349,6 +393,12 @@ workflows: ignore: main tags: ignore: /.*/ + - playwright-functional-tests: + filters: + branches: + ignore: main + tags: + ignore: /.*/ - test-email-service: # since email-service is expensive # to build and rarely changes diff --git a/.vscode/launch.json b/.vscode/launch.json index 297315bc30..2befc86799 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -22,10 +22,10 @@ "env": { "DEBUG": "1" }, - "program": "${workspaceFolder}/node_modules/folio/cli.js", - "args": ["--config=${workspaceFolder}/packages/fxa-settings/fnl/config.ts", "--timeout=0"], + "program": "${workspaceFolder}/node_modules/@playwright/test/cli.js", + "args": ["test","--config=${workspaceFolder}/packages/functional-tests/playwright.config.ts", "--project=local"], "autoAttachChildProcesses": true, - "cwd":"${workspaceFolder}", + "cwd":"${workspaceFolder}/packages/functional-tests", "request": "launch", } ] diff --git a/_scripts/check-url.sh b/_scripts/check-url.sh index c5d120e0cb..0641087834 100755 --- a/_scripts/check-url.sh +++ b/_scripts/check-url.sh @@ -3,7 +3,7 @@ RETRY=60 for i in $(eval echo "{1..$RETRY}"); do if [ "$(curl -s -o /dev/null --silent -w "%{http_code}" http://$1)" == "${2:-200}" ]; then - echo "took $i seconds" + echo "took $SECONDS seconds" exit 0 else if [ "$i" -lt $RETRY ]; then @@ -11,5 +11,5 @@ for i in $(eval echo "{1..$RETRY}"); do fi fi done - +echo "giving up after $SECONDS seconds" exit 1 diff --git a/packages/functional-tests/README.md b/packages/functional-tests/README.md new file mode 100644 index 0000000000..5a2a33afa7 --- /dev/null +++ b/packages/functional-tests/README.md @@ -0,0 +1,117 @@ +# Firefox Accounts - functional test suite + +## Playwright Test + +The suite uses [Playwright Test](https://playwright.dev/docs/intro). Also check out the [API reference](https://playwright.dev/docs/api/class-test). + +## Target environments + +The environments that this suite may run against are: + +- local +- stage +- production + +Each has its own named script in [package.json](./package.json) or you can use `--project` when running `playwright test` manually. They are implemented in `lib/targets`. + +### Running the tests + +- `yarn test` will run the tests against your localhost using the default configuration. +- `yarn playwright test` will let you set any of the [cli options](https://playwright.dev/docs/test-cli#reference) +- You can also add cli options after any of the npm scripts + +### Specifying a target in tests + +Some tests only work with certain targets. The content-server mocha tests for example will only work on `local`. Use [annotations](https://playwright.dev/docs/test-annotations#annotations) and [TestInfo](https://playwright.dev/docs/api/class-testinfo) to determine when a test should run. + +Example: + +```ts +test('mocha tests', async ({ target, page }, info) => { + test.skip(info.project.name !== 'local', 'mocha tests are local only'); + //... +}); +``` + +## Fixtures + +We have a standard [fixture](https://playwright.dev/docs/test-fixtures) for the most common kind of tests. + +It's job is to: + +- Connect to the target environment +- Create and verify an account for each test +- Create the POMs +- Destroy the account after each test + +Use this fixture in test files like so: + +```ts +// tests/example.spec.ts +import { test } from '../lib/fixtures/standard'; +``` + +Other fixtures may be added as needed. + +## Page Object Models (POMs) + +To keep the tests readable and high-level we use the [page object model](https://playwright.dev/docs/test-pom) pattern. Pages are organized by url "route" when possible in the `pages` directory, and made available to tests in the fixture as `pages`. + +Example: + +```ts +test('create an account', async ({ pages: { login } }) => { + // login is a POM at pages/login.ts + await login.goto(); + //... +}); +``` + +For guidance on writing POMs there's [pages/README.md](./pages/README.md) + +## Group by severity + +Test cases are grouped by [severity](https://wiki.mozilla.org/BMO/UserGuide/BugFields#bug_severity) (1-4) so that it's easy to identify the impact of a failure. + +Use `test.describe('severity-#', ...)` to designate the severity for a group of tests. For example: + +```ts +test.describe('severity-1', () => { + test('create an account', async ({ pages: { login } }) => { + //... + }); +}); +``` + +Avoid adding extra steps or assertions to a test for lower severity issues. For example, checking the placement of a tooltip for a validation error on the change password form should be a separate lower severity test from the S1 test that ensures the password can be changed at all. It's tempting to check all the functionality for a component in one test, since we're already there, but it makes high severity tests run slower and harder to determine the actual severity when a test fails. Grouping related assertions at the same severity level within a single test is ok. We want to be able to run S1 tests and not have them fail for an S4 assertion. + +If you're unsure about which severity to group a test in use `severity-na` and we'll triage those periodically. + +### Running tests by severity + +To run only the tests from a particular severity use the `--grep` cli option. + +Examples: + +- Just S1 + - `--grep=severity-1` +- S1 and S2 + - `--grep="severity-(1|2)"` +- All except NA + - `--grep-invert=severity-na` + +## Debugging + +Playwright Test offers great debugging features. + +### --debug option + +Add the `--debug` option when you run the tests an it will run with the [Playwright Inspector](https://playwright.dev/docs/inspector), letting you step through the tests and show interactions in the browser. + +### VSCode debugging + +There's a `Functional Tests` launch target in the root `.vscode/launch.json`. Set any breakpoints in you tests and run them from the Debug panel. The browser will run in "headed" mode and you can step through the test. + +### Traces + +We record traces for failed tests locally and in CI. On CircleCI they are in the test artifacts. For more read the [Trace Viewer docs](https://playwright.dev/docs/trace-viewer). diff --git a/packages/functional-tests/lib/email.ts b/packages/functional-tests/lib/email.ts new file mode 100644 index 0000000000..731ba9d9d0 --- /dev/null +++ b/packages/functional-tests/lib/email.ts @@ -0,0 +1,105 @@ +import got from 'got'; + +function wait() { + return new Promise((r) => setTimeout(r, 50)); +} + +function toUsername(emailAddress: string) { + return emailAddress.split('@')[0]; +} + +export enum EmailType { + subscriptionReactivation, + subscriptionUpgrade, + subscriptionDowngrade, + subscriptionPaymentExpired, + subscriptionsPaymentExpired, + subscriptionPaymentProviderCancelled, + subscriptionsPaymentProviderCancelled, + subscriptionPaymentFailed, + subscriptionAccountDeletion, + subscriptionCancellation, + subscriptionSubsequentInvoice, + subscriptionFirstInvoice, + downloadSubscription, + lowRecoveryCodes, + newDeviceLogin, + passwordChanged, + passwordChangeRequired, + passwordReset, + passwordResetAccountRecovery, + passwordResetRequired, + postChangePrimary, + postRemoveSecondary, + postVerify, + postVerifySecondary, + postAddTwoStepAuthentication, + postRemoveTwoStepAuthentication, + postAddAccountRecovery, + postRemoveAccountRecovery, + postConsumeRecoveryCode, + postNewRecoveryCodes, + recovery, + unblockCode, + verify, + verifySecondaryCode, + verifyShortCode, + verifyLogin, + verifyLoginCode, + verifyPrimary, + verifySecondary, + verificationReminderFirst, + verificationReminderSecond, + cadReminderFirst, + cadReminderSecond, +} + +export enum EmailHeader { + verifyCode = 'x-verify-code', + shortCode = 'x-verify-short-code', + unblockCode = 'x-unblock-code', + signinCode = 'x-signin-verify-code', + recoveryCode = 'x-recovery-code', + uid = 'x-uid', + serviceId = 'x-service-id', + link = 'x-link', + templateName = 'x-template-name', + templateVersion = 'x-template-version', +} + +export class EmailClient { + static emailFromTestTitle(title: string) { + return `${title + .match(/(\w+)/g) + .join('_') + .substr(0, 20) + .toLowerCase()}_${Math.floor(Math.random() * 10000)}@restmail.net`; + } + constructor(private readonly host: string = 'http://restmail.net') {} + + async waitForEmail( + emailAddress: string, + type: EmailType, + header?: EmailHeader, + timeout: number = 15000 + ) { + const expires = Date.now() + timeout; + while (Date.now() < expires) { + const mail = (await got( + `${this.host}/mail/${toUsername(emailAddress)}` + ).json()) as any[]; + const msg = mail.find( + (m) => m.headers[EmailHeader.templateName] === EmailType[type] + ); + if (msg) { + return header ? msg.headers[header] : msg; + } + await wait(); + } + throw new Error('EmailTimeout'); + } + + async clear(emailAddress: string) { + await got.delete(`${this.host}/mail/${toUsername(emailAddress)}`); + } +} diff --git a/packages/functional-tests/lib/fixtures/README.md b/packages/functional-tests/lib/fixtures/README.md new file mode 100644 index 0000000000..82ade8a14e --- /dev/null +++ b/packages/functional-tests/lib/fixtures/README.md @@ -0,0 +1,11 @@ +# Fixtures + +Fixtures make writing tests nicer so that they have less boilerplate. + +## Standard + +The standard fixure provides + +- `target`: the [Target](../targets/README.md) object +- `credentials`: provides the `email` and `password` of a fresh account created and logged in for each test +- `pages`: the set of all Page Object Models defined in `pages/index.ts` diff --git a/packages/functional-tests/lib/fixtures/standard.ts b/packages/functional-tests/lib/fixtures/standard.ts new file mode 100644 index 0000000000..697ff68401 --- /dev/null +++ b/packages/functional-tests/lib/fixtures/standard.ts @@ -0,0 +1,114 @@ +import { Browser, test as base, expect } from '@playwright/test'; +import { TargetName, ServerTarget, create, Credentials } from '../targets'; +import { EmailClient } from '../email'; +import { create as createPages } from '../../pages'; +import { BaseTarget } from '../targets/base'; +import { getCode } from 'fxa-settings/src/lib/totp'; + +export { expect }; +export type POMS = ReturnType; + +export type TestOptions = { + pages: POMS; + credentials: Credentials; +}; +export type WorkerOptions = { targetName: TargetName; target: ServerTarget }; + +export const test = base.extend({ + targetName: ['local', { scope: 'worker' }], + + target: [ + async ({ targetName }, use) => { + const target = create(targetName); + await use(target); + }, + { scope: 'worker', auto: true }, + ], + + credentials: async ({ target }, use, testInfo) => { + const email = EmailClient.emailFromTestTitle(testInfo.title); + const password = 'asdzxcasd'; + await target.email.clear(email); + let credentials: Credentials; + try { + credentials = await target.createAccount(email, password); + } catch (e) { + await target.auth.accountDestroy(email, password); + credentials = await target.createAccount(email, password); + } + + await use(credentials); + + //teardown + await target.email.clear(credentials.email); + try { + await target.auth.accountDestroy(credentials.email, credentials.password); + } catch (error: any) { + if (error.message === 'Unverified session') { + // If totp was enabled we'll need a verified session to destroy the account + if (credentials.secret) { + // we don't know if the original session still exists + // the test may have called signOut() + const { sessionToken } = await target.auth.signIn( + credentials.email, + credentials.password + ); + credentials.sessionToken = sessionToken; + await target.auth.verifyTotpCode( + sessionToken, + await getCode(credentials.secret) + ); + await target.auth.accountDestroy( + credentials.email, + credentials.password, + {}, + sessionToken + ); + } else { + throw error; + } + } else if (error.message !== 'Unknown account') { + throw error; + } + //s'ok + } + }, + + pages: async ({ target, page }, use) => { + const pages = createPages(page, target); + await use(pages); + }, + + storageState: async ({ target, credentials }, use) => { + // This is to store our session without logging in through the ui + await use({ + cookies: [], + origins: [ + { + origin: target.contentServerUrl, + localStorage: [ + { + name: '__fxa_storage.currentAccountUid', + value: JSON.stringify(credentials.uid), + }, + { + name: '__fxa_storage.accounts', + value: JSON.stringify({ + [credentials.uid]: { + sessionToken: credentials.sessionToken, + uid: credentials.uid, + }, + }), + }, + ], + }, + ], + }); + }, +}); + +export async function newPages(browser: Browser, target: BaseTarget) { + const context = await browser.newContext(); + const page = await context.newPage(); + return createPages(page, target); +} diff --git a/packages/functional-tests/lib/targets/README.md b/packages/functional-tests/lib/targets/README.md new file mode 100644 index 0000000000..16dac473fc --- /dev/null +++ b/packages/functional-tests/lib/targets/README.md @@ -0,0 +1,6 @@ +# Targets + +Targets provide the implementation details for running the tests against +different server environments, like localhost or production. Anything +that needs different or special handling depending on what target +environment it runs against should be implemented here. diff --git a/packages/functional-tests/lib/targets/base.ts b/packages/functional-tests/lib/targets/base.ts new file mode 100644 index 0000000000..7c22e63aba --- /dev/null +++ b/packages/functional-tests/lib/targets/base.ts @@ -0,0 +1,27 @@ +import AuthClient from 'fxa-auth-client'; +import { EmailClient } from '../email'; + +type Resolved = T extends PromiseLike ? U : T; +export type Credentials = Resolved> & { + email: string; + password: string; + secret?: string; +}; + +export abstract class BaseTarget { + readonly auth: AuthClient; + readonly email: EmailClient; + abstract readonly contentServerUrl: string; + abstract readonly relierUrl: string; + + constructor(readonly authServerUrl: string, emailUrl?: string) { + this.auth = new AuthClient(authServerUrl); + this.email = new EmailClient(emailUrl); + } + + get baseUrl() { + return this.contentServerUrl; + } + + abstract createAccount(email: string, password: string): Promise; +} diff --git a/packages/functional-tests/lib/targets/firefoxUserPrefs.ts b/packages/functional-tests/lib/targets/firefoxUserPrefs.ts new file mode 100644 index 0000000000..de90bd3fda --- /dev/null +++ b/packages/functional-tests/lib/targets/firefoxUserPrefs.ts @@ -0,0 +1,68 @@ +const CONFIGS = { + local: { + auth: 'http://localhost:9000/v1', + content: 'http://localhost:3030/', + token: 'http://localhost:5000/token/1.0/sync/1.5', + oauth: 'http://localhost:9000/v1', + profile: 'http://localhost:1111/v1', + }, + stage: { + auth: 'https://api-accounts.stage.mozaws.net/v1', + content: 'https://accounts.stage.mozaws.net/', + token: 'https://token.stage.mozaws.net/1.0/sync/1.5', + oauth: 'https://oauth.stage.mozaws.net/v1', + profile: 'https://profile.stage.mozaws.net/v1', + }, + production: { + auth: 'https://api.accounts.firefox.com/v1', + content: 'https://accounts.firefox.com/', + token: 'https://token.services.mozilla.com/1.0/sync/1.5', + oauth: 'https://oauth.accounts.firefox.com/v1', + profile: 'https://profile.accounts.firefox.com/v1', + }, +} as const; + +export function getFirefoxUserPrefs( + target: 'local' | 'stage' | 'production', + debug?: boolean +) { + const fxaEnv = CONFIGS[target]; + const debugOptions = { + 'devtools.debugger.remote-enabled': true, + 'devtools.chrome.enabled': true, + 'devtools.debugger.prompt-connection': false, + 'identity.fxaccounts.log.appender.dump': 'Debug', + 'identity.fxaccounts.loglevel': 'Debug', + 'services.sync.log.appender.file.logOnSuccess': true, + 'services.sync.log.appender.console': 'Debug', + 'services.sync.log.appender.dump': 'Debug', + }; + return { + 'browser.tabs.remote.separatePrivilegedMozillaWebContentProcess': + target !== 'production', + 'browser.tabs.remote.separatePrivilegedContentProcess': + target !== 'production', + 'identity.fxaccounts.auth.uri': fxaEnv.auth, + 'identity.fxaccounts.allowHttp': target === 'local', + 'identity.fxaccounts.remote.root': fxaEnv.content, + 'identity.fxaccounts.remote.force_auth.uri': + fxaEnv.content + 'force_auth?service=sync&context=fx_desktop_v3', + 'identity.fxaccounts.remote.signin.uri': + fxaEnv.content + 'signin?service=sync&context=fx_desktop_v3', + 'identity.fxaccounts.remote.signup.uri': + fxaEnv.content + 'signup?service=sync&context=fx_desktop_v3', + 'identity.fxaccounts.remote.webchannel.uri': fxaEnv.content, + 'identity.fxaccounts.remote.oauth.uri': fxaEnv.oauth, + 'identity.fxaccounts.remote.profile.uri': fxaEnv.profile, + 'identity.fxaccounts.settings.uri': + fxaEnv.content + 'settings?service=sync&context=fx_desktop_v3', + // for some reason there are 2 settings for the token server + 'identity.sync.tokenserver.uri': fxaEnv.token, + 'services.sync.tokenServerURI': fxaEnv.token, + 'identity.fxaccounts.contextParam': 'fx_desktop_v3', + 'browser.newtabpage.activity-stream.fxaccounts.endpoint': fxaEnv.content, + // allow webchannel url, strips slash from content-server origin. + 'webchannel.allowObject.urlWhitelist': fxaEnv.content.slice(0, -1), + ...(debug ? debugOptions : {}), + }; +} diff --git a/packages/functional-tests/lib/targets/index.ts b/packages/functional-tests/lib/targets/index.ts new file mode 100644 index 0000000000..1c2c20ae6f --- /dev/null +++ b/packages/functional-tests/lib/targets/index.ts @@ -0,0 +1,24 @@ +import { BaseTarget } from './base'; +import { LocalTarget } from './local'; +import { StageTarget } from './stage'; +import { ProductionTarget } from './production'; + +export const TargetNames = [ + LocalTarget.target, + StageTarget.target, + ProductionTarget.target, +] as const; +export type TargetName = typeof TargetNames[number]; + +const targets = { + [LocalTarget.target]: LocalTarget, + [StageTarget.target]: StageTarget, + [ProductionTarget.target]: ProductionTarget, +}; + +export function create(name: TargetName): BaseTarget { + return new targets[name](); +} + +export { BaseTarget as ServerTarget }; +export { Credentials } from './base'; diff --git a/packages/functional-tests/lib/targets/local.ts b/packages/functional-tests/lib/targets/local.ts new file mode 100644 index 0000000000..ddb96f7f1f --- /dev/null +++ b/packages/functional-tests/lib/targets/local.ts @@ -0,0 +1,26 @@ +import { TargetName } from '.'; +import { BaseTarget, Credentials } from './base'; + +export class LocalTarget extends BaseTarget { + static readonly target = 'local'; + readonly name: TargetName = LocalTarget.target; + readonly contentServerUrl = 'http://localhost:3030'; + readonly relierUrl = 'http://localhost:8080'; + + constructor() { + super('http://localhost:9000', 'http://localhost:9001'); + } + + async createAccount(email: string, password: string) { + const result = await this.auth.signUp(email, password, { + lang: 'en', + preVerified: 'true', + }); + await this.auth.deviceRegister(result.sessionToken, 'playwright', 'tester'); + return { + email, + password, + ...result, + } as Credentials; + } +} diff --git a/packages/functional-tests/lib/targets/production.ts b/packages/functional-tests/lib/targets/production.ts new file mode 100644 index 0000000000..f8a01d9acb --- /dev/null +++ b/packages/functional-tests/lib/targets/production.ts @@ -0,0 +1,13 @@ +import { TargetName } from '.'; +import { RemoteTarget } from './remote'; + +export class ProductionTarget extends RemoteTarget { + static readonly target = 'production'; + readonly name: TargetName = ProductionTarget.target; + readonly contentServerUrl = 'https://accounts.firefox.com'; + readonly relierUrl = 'https://123done.org'; + + constructor() { + super('https://api.accounts.firefox.com'); + } +} diff --git a/packages/functional-tests/lib/targets/remote.ts b/packages/functional-tests/lib/targets/remote.ts new file mode 100644 index 0000000000..c4d258a061 --- /dev/null +++ b/packages/functional-tests/lib/targets/remote.ts @@ -0,0 +1,21 @@ +import { BaseTarget, Credentials } from './base'; +import { EmailHeader, EmailType } from '../email'; + +export abstract class RemoteTarget extends BaseTarget { + async createAccount(email: string, password: string): Promise { + const creds = await this.auth.signUp(email, password); + const code = await this.email.waitForEmail( + email, + EmailType.verify, + EmailHeader.verifyCode + ); + await this.auth.verifyCode(creds.uid, code); + await this.email.clear(email); + await this.auth.deviceRegister(creds.sessionToken, 'playwright', 'tester'); + return { + email, + password, + ...creds, + }; + } +} diff --git a/packages/functional-tests/lib/targets/stage.ts b/packages/functional-tests/lib/targets/stage.ts new file mode 100644 index 0000000000..955b8d7e80 --- /dev/null +++ b/packages/functional-tests/lib/targets/stage.ts @@ -0,0 +1,13 @@ +import { TargetName } from '.'; +import { RemoteTarget } from './remote'; + +export class StageTarget extends RemoteTarget { + static readonly target = 'stage'; + readonly name: TargetName = StageTarget.target; + readonly contentServerUrl = 'https://accounts.stage.mozaws.net'; + readonly relierUrl = 'https://stage-123done.herokuapp.com'; + + constructor() { + super('https://api-accounts.stage.mozaws.net'); + } +} diff --git a/packages/functional-tests/package.json b/packages/functional-tests/package.json new file mode 100644 index 0000000000..ece0db3177 --- /dev/null +++ b/packages/functional-tests/package.json @@ -0,0 +1,22 @@ +{ + "name": "functional-tests", + "version": "1.218.5", + "private": true, + "scripts": { + "test": "playwright test --project=local", + "test-local": "playwright test --project=local", + "test-stage": "playwright test --project=stage", + "test-production": "playwright test --project=production" + }, + "devDependencies": { + "@playwright/test": "^1.16.3", + "@types/upng-js": "^2", + "fxa-auth-client": "workspace:*", + "fxa-content-server": "workspace:*", + "fxa-payments-server": "workspace:*", + "fxa-settings": "workspace:*", + "jsqr": "^1.4.0", + "playwright": "^1.16.3", + "upng-js": "^2.1.0" + } +} diff --git a/packages/functional-tests/pages/README.md b/packages/functional-tests/pages/README.md new file mode 100644 index 0000000000..314993e6b1 --- /dev/null +++ b/packages/functional-tests/pages/README.md @@ -0,0 +1,11 @@ +# Page Object Models + +Page Object Models (POMs) do the heavy lifting of performing actions and getting +data on a page. If React Components are on one side of the browser coin POMs are +on the other. + +## Organization + +TBD + +## Tips diff --git a/packages/functional-tests/pages/index.ts b/packages/functional-tests/pages/index.ts new file mode 100644 index 0000000000..335bfd31cd --- /dev/null +++ b/packages/functional-tests/pages/index.ts @@ -0,0 +1,30 @@ +import { Page } from '@playwright/test'; +import { BaseTarget } from '../lib/targets/base'; +import { ChangePasswordPage } from './settings/changePassword'; +import { DeleteAccountPage } from './settings/deleteAccount'; +import { DisplayNamePage } from './settings/displayName'; +import { LoginPage } from './login'; +import { RecoveryKeyPage } from './settings/recoveryKey'; +import { RelierPage } from './relier'; +import { SecondaryEmailPage } from './settings/secondaryEmail'; +import { SettingsPage } from './settings'; +import { SubscribePage } from './products'; +import { TotpPage } from './settings/totp'; +import { AvatarPage } from './settings/avatar'; + +export function create(page: Page, target: BaseTarget) { + return { + page, + avatar: new AvatarPage(page, target), + changePassword: new ChangePasswordPage(page, target), + deleteAccount: new DeleteAccountPage(page, target), + displayName: new DisplayNamePage(page, target), + login: new LoginPage(page, target), + secondaryEmail: new SecondaryEmailPage(page, target), + settings: new SettingsPage(page, target), + subscribe: new SubscribePage(page, target), + recoveryKey: new RecoveryKeyPage(page, target), + relier: new RelierPage(page, target), + totp: new TotpPage(page, target), + }; +} diff --git a/packages/functional-tests/pages/layout.ts b/packages/functional-tests/pages/layout.ts new file mode 100644 index 0000000000..02cd03ad1e --- /dev/null +++ b/packages/functional-tests/pages/layout.ts @@ -0,0 +1,24 @@ +import { Page } from '@playwright/test'; +import { BaseTarget } from '../lib/targets/base'; + +export abstract class BaseLayout { + readonly path?: string; + + constructor(public page: Page, protected readonly target: BaseTarget) {} + + protected get baseUrl() { + return this.target.baseUrl; + } + + get url() { + return `${this.baseUrl}/${this.path}`; + } + + goto(waitUntil: 'networkidle' | 'domcontentloaded' | 'load' = 'load') { + return this.page.goto(this.url, { waitUntil }); + } + + screenshot() { + return this.page.screenshot({ fullPage: true }); + } +} diff --git a/packages/functional-tests/pages/login.ts b/packages/functional-tests/pages/login.ts new file mode 100644 index 0000000000..50f3d51046 --- /dev/null +++ b/packages/functional-tests/pages/login.ts @@ -0,0 +1,101 @@ +import { EmailHeader, EmailType } from '../lib/email'; +import { BaseLayout } from './layout'; +import { getCode } from 'fxa-settings/src/lib/totp'; + +export class LoginPage extends BaseLayout { + readonly path = ''; + + async login(email: string, password: string, recoveryCode?: string) { + await this.setEmail(email); + await this.page.click('button[type=submit]'); + await this.setPassword(password); + await this.submit(); + if (recoveryCode) { + await this.clickUseRecoveryCode(); + await this.setCode(recoveryCode); + await this.submit(); + } + } + + setEmail(email: string) { + return this.page.fill('input[type=email]', email); + } + + setPassword(password: string) { + return this.page.fill('input[type=password]', password); + } + + async clickUseRecoveryCode() { + return this.page.click('#use-recovery-code-link'); + } + + async setCode(code: string) { + return this.page.fill('input[type=text]', code); + } + + async unblock(email: string) { + const code = await this.target.email.waitForEmail( + email, + EmailType.unblockCode, + EmailHeader.unblockCode + ); + await this.setCode(code); + await this.submit(); + } + + async submit() { + return Promise.all([ + this.page.click('button[type=submit]'), + this.page.waitForNavigation({ waitUntil: 'networkidle' }), + ]); + } + + async clickForgotPassword() { + return Promise.all([ + this.page.click('a[href="/reset_password"]'), + this.page.waitForNavigation({ waitUntil: 'networkidle' }), + ]); + } + + async clickDontHaveRecoveryKey() { + return Promise.all([ + this.page.click('a.lost-recovery-key'), + this.page.waitForNavigation(), + ]); + } + + setRecoveryKey(key: string) { + return this.page.fill('input[type=text]', key); + } + + async setNewPassword(password: string) { + await this.page.fill('#password', password); + await this.page.fill('#vpassword', password); + await this.submit(); + } + + async setTotp(secret: string) { + const code = await getCode(secret); + await this.page.fill('input[type=number]', code); + await this.submit(); + } + + async useCredentials(credentials: any) { + await this.goto(); + return this.page.evaluate((creds) => { + localStorage.setItem( + '__fxa_storage.accounts', + JSON.stringify({ + [creds.uid]: { + sessionToken: creds.sessionToken, + uid: creds.uid, + }, + }) + ); + localStorage.setItem( + '__fxa_storage.currentAccountUid', + JSON.stringify(creds.uid) + ); + }, credentials); + } +} diff --git a/packages/functional-tests/pages/products/index.ts b/packages/functional-tests/pages/products/index.ts new file mode 100644 index 0000000000..35fbc909ae --- /dev/null +++ b/packages/functional-tests/pages/products/index.ts @@ -0,0 +1,29 @@ +import { BaseLayout } from '../layout'; + +export class SubscribePage extends BaseLayout { + setFullName(name: string = 'Cave Johnson') { + return this.page.fill('[data-testid="name"]', name); + } + + async setCreditCardInfo() { + const frame = this.page.frame({ url: /elements-inner-card/ }); + await frame.fill('.InputElement[name=cardnumber]', '4242424242424242'); + await frame.fill('.InputElement[name=exp-date]', '555'); + await frame.fill('.InputElement[name=cvc]', '333'); + await frame.fill('.InputElement[name=postal]', '66666'); + await this.page.check('input[type=checkbox]'); + } + + submit() { + return Promise.all([ + this.page.click('button[type=submit]'), + this.page.waitForResponse( + (r) => + r.request().method() === 'GET' && + /\/mozilla-subscriptions\/customer\/billing-and-subscriptions$/.test( + r.request().url() + ) + ), + ]); + } +} diff --git a/packages/functional-tests/pages/relier.ts b/packages/functional-tests/pages/relier.ts new file mode 100644 index 0000000000..c5a7c59cd2 --- /dev/null +++ b/packages/functional-tests/pages/relier.ts @@ -0,0 +1,39 @@ +import { BaseLayout } from './layout'; + +export class RelierPage extends BaseLayout { + goto(query?: string) { + const url = query + ? `${this.target.relierUrl}?${query}` + : this.target.relierUrl; + return this.page.goto(url); + } + + isLoggedIn() { + return this.page.isVisible('#loggedin', { timeout: 1000 }); + } + + isPro() { + return this.page.isVisible('.pro-status', { timeout: 1000 }); + } + + async signOut() { + await Promise.all([ + this.page.click('#logout'), + this.page.waitForResponse(/\/api\/logout/), + ]); + } + + clickEmailFirst() { + return Promise.all([ + this.page.click('button.email-first-button'), + this.page.waitForNavigation(), + ]); + } + + async clickSubscribe() { + await Promise.all([ + this.page.click('a[data-currency=usd]'), + this.page.waitForNavigation({ waitUntil: 'load' }), + ]); + } +} diff --git a/packages/functional-tests/pages/settings/avatar.png b/packages/functional-tests/pages/settings/avatar.png new file mode 100644 index 0000000000..f236744ecc Binary files /dev/null and b/packages/functional-tests/pages/settings/avatar.png differ diff --git a/packages/functional-tests/pages/settings/avatar.ts b/packages/functional-tests/pages/settings/avatar.ts new file mode 100644 index 0000000000..190d6d6c87 --- /dev/null +++ b/packages/functional-tests/pages/settings/avatar.ts @@ -0,0 +1,40 @@ +import { SettingsLayout } from './layout'; + +export class AvatarPage extends SettingsLayout { + readonly path = 'settings/avatar'; + + async clickAddPhoto() { + const [filechooser] = await Promise.all([ + this.page.waitForEvent('filechooser'), + this.page.click('[data-testid=add-photo-btn]'), + ]); + return filechooser; + } + + clickTakePhoto() { + return this.page.click('[data-testid=take-photo-btn]'); + } + + async clickRemove() { + await Promise.all([ + this.page.click('[data-testid=remove-photo-btn]'), + this.page.waitForNavigation(), + ]); + // HACK we don't really have a good way to distinguish + // between monogram avatars and user set images + // and if we return directly after navigation + // react may not have updated the image yet + await this.page.waitForSelector('img[src*="avatar"]'); + } + + clickSave() { + return Promise.all([ + this.page.click('[data-testid=save-button]'), + this.page.waitForNavigation(), + ]); + } + + clickCancel() { + return this.page.click('[data-testid=close-button]'); + } +} diff --git a/packages/functional-tests/pages/settings/changePassword.ts b/packages/functional-tests/pages/settings/changePassword.ts new file mode 100644 index 0000000000..4a77f42775 --- /dev/null +++ b/packages/functional-tests/pages/settings/changePassword.ts @@ -0,0 +1,30 @@ +import { SettingsLayout } from './layout'; + +export class ChangePasswordPage extends SettingsLayout { + readonly path = 'settings/change_password'; + + setCurrentPassword(password: string) { + return this.page.fill( + '[data-testid=current-password-input-field]', + password + ); + } + + setNewPassword(password: string) { + return this.page.fill('[data-testid=new-password-input-field]', password); + } + + setConfirmPassword(password: string) { + return this.page.fill( + '[data-testid=verify-password-input-field]', + password + ); + } + + submit() { + return Promise.all([ + this.page.click('button[type=submit]'), + this.page.waitForNavigation(), + ]); + } +} diff --git a/packages/functional-tests/pages/settings/components/connectedService.ts b/packages/functional-tests/pages/settings/components/connectedService.ts new file mode 100644 index 0000000000..fd61c11050 --- /dev/null +++ b/packages/functional-tests/pages/settings/components/connectedService.ts @@ -0,0 +1,30 @@ +import { ElementHandle, Page } from '@playwright/test'; + +export class ConnectedService { + name: string; + constructor( + readonly element: ElementHandle, + readonly page: Page + ) {} + + static async create( + element: ElementHandle, + page: Page + ) { + const service = new ConnectedService(element, page); + service.name = await service.getName(); + return service; + } + + async getName() { + const p = await this.element.waitForSelector('[data-testid=service-name]'); + return p.innerText(); + } + + async signout() { + const button = await this.element.waitForSelector( + '[data-testid=connected-service-sign-out]' + ); + return button.click(); + } +} diff --git a/packages/functional-tests/pages/settings/components/dataTrio.ts b/packages/functional-tests/pages/settings/components/dataTrio.ts new file mode 100644 index 0000000000..9ea804d7d9 --- /dev/null +++ b/packages/functional-tests/pages/settings/components/dataTrio.ts @@ -0,0 +1,44 @@ +import { Page } from '@playwright/test'; + +export class DataTrioComponent { + constructor(readonly page: Page) {} + + async clickDownload() { + const [download] = await Promise.all([ + this.page.waitForEvent('download'), + this.page.click('[data-testid=databutton-download]'), + ]); + return download; + } + + async clickCopy(): Promise { + // override writeText so we can capture the value + await this.page.evaluate(() => { + //@ts-ignore + window.clipboardText = null; + //@ts-ignore + navigator.clipboard.writeText = (text) => (window.clipboardText = text); + }); + await this.page.click('[data-testid=databutton-copy]'); + //@ts-ignore + return this.page.evaluate(() => window.clipboardText); + } + + async clickPrint() { + // override the print function + // so that we can test that it was called + await this.page.context().addInitScript(() => { + //@ts-ignore window.printed + window.print = () => (window.printed = true); + window.close = () => {}; + }); + const [printPage] = await Promise.all([ + this.page.context().waitForEvent('page'), + this.page.click('[data-testid=databutton-print]'), + ]); + //@ts-ignore window.printed + const printed = await printPage.evaluate(() => window.printed); + await printPage.close(); + return printed; + } +} diff --git a/packages/functional-tests/pages/settings/components/unitRow.ts b/packages/functional-tests/pages/settings/components/unitRow.ts new file mode 100644 index 0000000000..9fbe273c29 --- /dev/null +++ b/packages/functional-tests/pages/settings/components/unitRow.ts @@ -0,0 +1,116 @@ +import { Page } from '@playwright/test'; +import { ConnectedService } from './connectedService'; + +export class UnitRow { + constructor(readonly page: Page, readonly id: string) {} + + protected clickCta() { + return Promise.all([ + this.page.click(`[data-testid=${this.id}-unit-row-route]`), + this.page.waitForNavigation(), + ]); + } + + protected clickShowModal() { + return this.page.click(`[data-testid=${this.id}-unit-row-modal]`); + } + + protected clickShowSecondaryModal() { + return this.page.click(`[data-testid=${this.id}-secondary-unit-row-modal]`); + } + + statusText() { + return this.page.innerText( + `[data-testid=${this.id}-unit-row-header-value]` + ); + } + + clickRefresh() { + return this.page.click(`[data-testid=${this.id}-refresh]`); + } + + async screenshot() { + const el = await this.page.waitForSelector( + `[data-testid=${this.id}-unit-row]` + ); + return el.screenshot(); + } +} + +export class AvatarRow extends UnitRow { + async isDefault() { + const el = await this.page.$('[data-testid=avatar-nondefault]'); + if (!el) { + return true; + } + const src = await el.getAttribute('src'); + return src.includes('/avatar/'); + } + + clickAdd() { + return this.clickCta(); + } + + clickChange() { + return this.clickCta(); + } +} + +export class DisplayNameRow extends UnitRow { + clickAdd() { + return this.clickCta(); + } +} + +export class PasswordRow extends UnitRow { + clickChange() { + return this.clickCta(); + } +} + +export class PrimaryEmailRow extends UnitRow {} + +export class SecondaryEmailRow extends UnitRow { + clickAdd() { + return this.clickCta(); + } + clickMakePrimary() { + return this.page.click('[data-testid=secondary-email-make-primary]'); + } + clickDelete() { + return this.page.click('[data-testid=secondary-email-delete]'); + } +} + +export class RecoveryKeyRow extends UnitRow { + clickCreate() { + return this.clickCta(); + } + clickRemove() { + return this.clickShowModal(); + } +} + +export class TotpRow extends UnitRow { + clickAdd() { + return this.clickCta(); + } + clickChange() { + return this.clickShowModal(); + } + clickDisable() { + return this.page.click( + `[data-testid=two-step-disable-button-unit-row-modal]` + ); + } +} + +export class ConnectedServicesRow extends UnitRow { + async services() { + await this.page.waitForSelector('#service'); + const elements = await this.page.$$('#service'); + return Promise.all( + elements.map((el) => ConnectedService.create(el, this.page)) + ); + } +} diff --git a/packages/functional-tests/pages/settings/deleteAccount.ts b/packages/functional-tests/pages/settings/deleteAccount.ts new file mode 100644 index 0000000000..e3b78da0d4 --- /dev/null +++ b/packages/functional-tests/pages/settings/deleteAccount.ts @@ -0,0 +1,29 @@ +import { SettingsLayout } from './layout'; + +export class DeleteAccountPage extends SettingsLayout { + readonly path = 'settings/delete_account'; + + async checkAllBoxes() { + await this.page.waitForSelector(':nth-match(input[type=checkbox], 4)'); + for (let i = 1; i < 5; i++) { + await this.page.click( + `:nth-match(label[data-testid=checkbox-container], ${i})` + ); + } + } + + clickContinue() { + return this.page.click('button[data-testid=continue-button]'); + } + + setPassword(password: string) { + return this.page.fill('input[type=password]', password); + } + + submit() { + return Promise.all([ + this.page.click('button[type=submit]'), + this.page.waitForNavigation(), + ]); + } +} diff --git a/packages/functional-tests/pages/settings/displayName.ts b/packages/functional-tests/pages/settings/displayName.ts new file mode 100644 index 0000000000..b25d5e9b27 --- /dev/null +++ b/packages/functional-tests/pages/settings/displayName.ts @@ -0,0 +1,20 @@ +import { SettingsLayout } from './layout'; + +export class DisplayNamePage extends SettingsLayout { + readonly path = 'settings/display_name'; + + displayName() { + return this.page.$eval('input[type=text]', (el: any) => el.value); + } + + setDisplayName(name: string) { + return this.page.fill('input[type=text]', name); + } + + submit() { + return Promise.all([ + this.page.click('button[type=submit]'), + this.page.waitForNavigation(), + ]); + } +} diff --git a/packages/functional-tests/pages/settings/index.ts b/packages/functional-tests/pages/settings/index.ts new file mode 100644 index 0000000000..3f88207b0b --- /dev/null +++ b/packages/functional-tests/pages/settings/index.ts @@ -0,0 +1,82 @@ +import { Page } from '@playwright/test'; +import { + AvatarRow, + ConnectedServicesRow, + DisplayNameRow, + PasswordRow, + PrimaryEmailRow, + RecoveryKeyRow, + SecondaryEmailRow, + TotpRow, + UnitRow, +} from './components/unitRow'; +import { SettingsLayout } from './layout'; + +export class SettingsPage extends SettingsLayout { + readonly path = 'settings'; + private rows = new Map(); + + private lazyRow( + id: string, + RowType: { new (page: Page, id: string): T } + ): T { + if (!this.rows.has(id)) { + this.rows.set(id, new RowType(this.page, id)); + } + return this.rows.get(id) as T; + } + + get avatar() { + return this.lazyRow('avatar', AvatarRow); + } + + get displayName() { + return this.lazyRow('display-name', DisplayNameRow); + } + + get password() { + return this.lazyRow('password', PasswordRow); + } + + get primaryEmail() { + return this.lazyRow('primary-email', PrimaryEmailRow); + } + + get secondaryEmail() { + return this.lazyRow('secondary-email', SecondaryEmailRow); + } + + get recoveryKey() { + return this.lazyRow('recovery-key', RecoveryKeyRow); + } + + get totp() { + return this.lazyRow('two-step', TotpRow); + } + + get connectedServices() { + return this.lazyRow('connected-services', ConnectedServicesRow); + } + + clickDeleteAccount() { + return Promise.all([ + this.page.click('[data-testid=settings-delete-account]'), + this.page.waitForNavigation(), + ]); + } + + async clickEmailPreferences() { + const [emailPage] = await Promise.all([ + this.page.context().waitForEvent('page'), + this.page.click('[data-testid=nav-link-newsletters]'), + ]); + return emailPage; + } + + clickPaidSubscriptions() { + return Promise.all([ + this.page.click('[data-testid=nav-link-subscriptions]'), + this.page.waitForNavigation({ waitUntil: 'networkidle' }), + ]); + } +} diff --git a/packages/functional-tests/pages/settings/layout.ts b/packages/functional-tests/pages/settings/layout.ts new file mode 100644 index 0000000000..db226a336d --- /dev/null +++ b/packages/functional-tests/pages/settings/layout.ts @@ -0,0 +1,59 @@ +import { BaseLayout } from '../layout'; + +export abstract class SettingsLayout extends BaseLayout { + get bentoMenu() { + return this.page.locator('[data-testid="drop-down-bento-menu"]'); + } + + get avatarMenu() { + return this.page.locator('[data-testid=drop-down-avatar-menu]'); + } + + goto() { + return super.goto('networkidle'); + } + + alertBarText() { + return this.page.innerText('[data-testid=alert-bar-content]'); + } + + async waitForAlertBar() { + return this.page.waitForSelector('[data-testid=alert-bar-content]'); + } + + closeAlertBar() { + return this.page.click('[data-testid=alert-bar-dismiss]'); + } + + clickModalConfirm() { + return this.page.click('[data-testid=modal-confirm]'); + } + + async clickHelp() { + const [helpPage] = await Promise.all([ + this.page.context().waitForEvent('page'), + this.page.click('[data-testid=header-sumo-link]'), + ]); + return helpPage; + } + + clickBentoIcon() { + return this.page.click('[data-testid="drop-down-bento-menu-toggle"]'); + } + + clickAvatarIcon() { + return this.page.click('[data-testid=drop-down-avatar-menu-toggle]'); + } + + clickSignOut() { + return this.page.click('[data-testid=avatar-menu-sign-out]'); + } + + async signOut() { + await this.clickAvatarIcon(); + await Promise.all([ + this.clickSignOut(), + this.page.waitForURL(this.target.baseUrl, { waitUntil: 'networkidle' }), + ]); + } +} diff --git a/packages/functional-tests/pages/settings/recoveryKey.ts b/packages/functional-tests/pages/settings/recoveryKey.ts new file mode 100644 index 0000000000..6d5dbafd42 --- /dev/null +++ b/packages/functional-tests/pages/settings/recoveryKey.ts @@ -0,0 +1,32 @@ +import { SettingsLayout } from './layout'; +import { DataTrioComponent } from './components/dataTrio'; + +export class RecoveryKeyPage extends SettingsLayout { + readonly path = 'settings/account_recovery'; + + get dataTrio() { + return new DataTrioComponent(this.page); + } + + getKey() { + return this.page.innerText('[data-testid=datablock] span'); + } + + setPassword(password: string) { + return this.page.fill('input[type=password]', password); + } + + submit() { + return Promise.all([ + this.page.click('button[type=submit]'), + this.page.waitForResponse(/recoveryKey$/), + ]); + } + + clickClose() { + return Promise.all([ + this.page.click('[data-testid=close-button]'), + this.page.waitForNavigation(), + ]); + } +} diff --git a/packages/functional-tests/pages/settings/secondaryEmail.ts b/packages/functional-tests/pages/settings/secondaryEmail.ts new file mode 100644 index 0000000000..24f2b92841 --- /dev/null +++ b/packages/functional-tests/pages/settings/secondaryEmail.ts @@ -0,0 +1,35 @@ +import { EmailHeader, EmailType } from '../../lib/email'; +import { SettingsLayout } from './layout'; + +export class SecondaryEmailPage extends SettingsLayout { + readonly path = 'settings/emails'; + + setEmail(email: string) { + return this.page.fill('input[type=email]', email); + } + + setVerificationCode(code: string) { + return this.page.fill('input[type=text]', code); + } + + submit() { + return Promise.all([ + this.page.click('button[type=submit]'), + this.page.waitForNavigation(), + ]); + } + + async addAndVerify(email: string) { + await this.target.email.clear(email); + await this.setEmail(email); + await this.submit(); + const code = await this.target.email.waitForEmail( + email, + EmailType.verifySecondaryCode, + EmailHeader.verifyCode + ); + await this.setVerificationCode(code); + await this.submit(); + return code; + } +} diff --git a/packages/functional-tests/pages/settings/totp.ts b/packages/functional-tests/pages/settings/totp.ts new file mode 100644 index 0000000000..4fbb9deb1b --- /dev/null +++ b/packages/functional-tests/pages/settings/totp.ts @@ -0,0 +1,76 @@ +import jsQR from 'jsqr'; +import UPNG from 'upng-js'; +import { SettingsLayout } from './layout'; +import { getCode } from 'fxa-settings/src/lib/totp'; +import { DataTrioComponent } from './components/dataTrio'; +import { Credentials } from '../../lib/targets'; + +export class TotpPage extends SettingsLayout { + readonly path = 'settings/two_step_authentication'; + + get dataTrio() { + return new DataTrioComponent(this.page); + } + + async useQRCode() { + const qr = await this.page.waitForSelector('[data-testid="2fa-qr-code"]'); + const png = await qr.screenshot(); + const img = UPNG.decode(png); + const { data } = jsQR( + new Uint8ClampedArray(UPNG.toRGBA8(img)[0]), + img.width, + img.height + ); + const secret = new URL(data).searchParams.get('secret'); + const code = await getCode(secret); + await this.page.fill('input[type=text]', code); + return secret; + } + + async useManualCode() { + await this.page.click('[data-testid=cant-scan-code]'); + const secret = ( + await this.page.innerText('[data-testid=manual-code]') + ).replace(/\s/g, ''); + const code = await getCode(secret); + await this.page.fill('input[type=text]', code); + return secret; + } + + submit() { + return this.page.click('button[type=submit]'); + } + + clickClose() { + return Promise.all([ + this.page.click('[data-testid=close-button]'), + this.page.waitForNavigation(), + ]); + } + + async getRecoveryCodes(): Promise { + await this.page.waitForSelector('[data-testid=datablock]'); + return this.page.$$eval('[data-testid=datablock] span', (elements) => + elements.map((el) => (el as HTMLElement).innerText) + ); + } + + setRecoveryCode(code: string) { + return this.page.fill('[data-testid=recovery-code-input-field]', code); + } + + async enable(credentials: Credentials, method: 'qr' | 'manual' = 'manual') { + const secret = + method === 'qr' ? await this.useQRCode() : await this.useManualCode(); + await this.submit(); + const recoveryCodes = await this.getRecoveryCodes(); + await this.submit(); + await this.setRecoveryCode(recoveryCodes[0]); + await this.submit(); + credentials.secret = secret; + return { + secret, + recoveryCodes, + }; + } +} diff --git a/packages/functional-tests/playwright.config.ts b/packages/functional-tests/playwright.config.ts new file mode 100644 index 0000000000..25d6b6d61f --- /dev/null +++ b/packages/functional-tests/playwright.config.ts @@ -0,0 +1,51 @@ +import { PlaywrightTestConfig } from '@playwright/test'; +import path from 'path'; +import { TargetNames } from './lib/targets'; +import { TestOptions, WorkerOptions } from './lib/fixtures/standard'; +import { getFirefoxUserPrefs } from './lib/targets/firefoxUserPrefs'; + +const CI = !!process.env.CI; + +// The DEBUG env is used to debug without the playwright inspector, like in vscode +// see .vscode/launch.json +const DEBUG = !!process.env.DEBUG; + +const config: PlaywrightTestConfig = { + outputDir: path.resolve(__dirname, '../../artifacts/functional'), + forbidOnly: CI, + retries: CI ? 1 : 0, + testDir: 'tests', + use: { + viewport: { width: 1280, height: 720 }, + }, + projects: TargetNames.map((name) => ({ + name, + use: { + browserName: 'firefox', + targetName: name, + launchOptions: { + args: DEBUG ? ['-start-debugger-server'] : undefined, + firefoxUserPrefs: getFirefoxUserPrefs(name, DEBUG), + headless: !DEBUG, + }, + trace: CI ? 'on-first-retry' : 'retain-on-failure', + }, + })), + reporter: CI + ? [ + ['line'], + [ + 'junit', + { + outputFile: path.resolve( + __dirname, + '../../artifacts/tests/test-results.xml' + ), + }, + ], + ] + : 'list', + workers: CI ? 1 : undefined, +}; + +export default config; diff --git a/packages/functional-tests/scripts/test-ci.sh b/packages/functional-tests/scripts/test-ci.sh new file mode 100755 index 0000000000..76b68e2ead --- /dev/null +++ b/packages/functional-tests/scripts/test-ci.sh @@ -0,0 +1,36 @@ +#!/bin/bash -ex + +DIR=$(dirname "$0") + +cd "$DIR/../../../" + +mkdir -p ~/.pm2/logs +mkdir -p artifacts/tests +node ./packages/db-migrations/bin/patcher.mjs + +yarn workspaces foreach \ + --verbose \ + --topological-dev \ + --include 123done \ + --include browserid-verifier \ + --include fxa-auth-server \ + --include fxa-content-server \ + --include fxa-graphql-api \ + --include fxa-payments-server \ + --include fxa-profile-server \ + --include fxa-react \ + --include fxa-settings \ + --include fxa-shared \ + --include fxa-support-panel \ + run start > ~/.pm2/logs/startup.log + +# ensure payments-server is ready +_scripts/check-url.sh localhost:3031/__lbheartbeat__ +# ensure content-server is ready +_scripts/check-url.sh localhost:3030/bundle/app.bundle.js +# ensure settings is ready +_scripts/check-url.sh localhost:3030/settings/static/js/bundle.js + +npx pm2 ls + +yarn workspace functional-tests test diff --git a/packages/functional-tests/tests/misc.spec.ts b/packages/functional-tests/tests/misc.spec.ts new file mode 100644 index 0000000000..738dde4de5 --- /dev/null +++ b/packages/functional-tests/tests/misc.spec.ts @@ -0,0 +1,70 @@ +import { test, expect } from '../lib/fixtures/standard'; + +test.describe('severity-1', () => { + test('subscribe and login to product', async ({ + pages: { relier, login, subscribe }, + }, { project }) => { + test.skip(project.name === 'production', 'prod needs a valid credit card'); + test.fixme(project.name === 'local', 'needs correct product'); + test.slow(); + await relier.goto(); + await relier.clickSubscribe(); + await subscribe.setFullName(); + await subscribe.setCreditCardInfo(); + await subscribe.submit(); + await relier.goto(); + await relier.clickEmailFirst(); + await login.submit(); + expect(await relier.isPro()).toBe(true); + }); + + test('content-server mocha tests', async ({ target, page }, { project }) => { + test.skip(project.name !== 'local', 'mocha tests are local only'); + test.slow(); + await page.goto(`${target.contentServerUrl}/tests/index.html`, { + waitUntil: 'networkidle', + }); + await page.evaluate(() => + globalThis.runner.on('end', () => (globalThis.done = true)) + ); + await page.waitForFunction(() => globalThis.done, {}, { timeout: 0 }); + const failures = await page.evaluate(() => globalThis.runner.failures); + expect(failures).toBe(0); + }); + + test('change email and unblock', async ({ + credentials, + pages: { page, login, settings, secondaryEmail }, + }) => { + await settings.goto(); + await settings.secondaryEmail.clickAdd(); + const newEmail = `blocked${Math.floor( + Math.random() * 100000 + )}@restmail.net`; + await secondaryEmail.addAndVerify(newEmail); + await settings.secondaryEmail.clickMakePrimary(); + credentials.email = newEmail; + await settings.signOut(); + await login.login(credentials.email, credentials.password); + await login.unblock(newEmail); + expect(page.url()).toBe(settings.url); + }); + + test('prompt=consent', async ({ + credentials, + pages: { page, relier, login }, + }, { project }) => { + test.skip(project.name === 'production', 'no 123done relier in prod'); + await relier.goto(); + await relier.clickEmailFirst(); + await login.login(credentials.email, credentials.password); + expect(await relier.isLoggedIn()).toBe(true); + await relier.signOut(); + await relier.goto('prompt=consent'); + await relier.clickEmailFirst(); + await login.submit(); + expect(page.url()).toMatch(/signin_permissions/); + await login.submit(); + expect(await relier.isLoggedIn()).toBe(true); + }); +}); diff --git a/packages/functional-tests/tests/prod.smoke.spec.ts b/packages/functional-tests/tests/prod.smoke.spec.ts new file mode 100644 index 0000000000..2f6c5001ce --- /dev/null +++ b/packages/functional-tests/tests/prod.smoke.spec.ts @@ -0,0 +1,576 @@ +import { test, expect, newPages } from '../lib/fixtures/standard'; +import { EmailHeader, EmailType } from '../lib/email'; + +test.describe('severity-1', () => { + // https://testrail.stage.mozaws.net/index.php?/cases/view/1293471 + test('signin to sync and disconnect #1293471', async ({ + target, + page, + credentials, + pages: { login, settings }, + }) => { + test.fixme( + true, + '(Invalid parameter in request body) response to attachedClientDisconnect mutation' + ); + await page.goto( + target.contentServerUrl + + '?context=fx_desktop_v3&entrypoint=fxa%3Aenter_email&service=sync&action=email' + ); + await login.login(credentials.email, credentials.password); + await settings.goto(); + const services = await settings.connectedServices.services(); + const sync = services.find((s) => s.name !== 'playwright'); + await sync.signout(); + await page.click('text=Rather not say >> input[name="reason"]'); + await settings.clickModalConfirm(); + // The sync row should be removed but isn't + }); + + // https://testrail.stage.mozaws.net/index.php?/cases/view/1293385 + test('change password #1293385', async ({ + pages: { settings, changePassword, login }, + credentials, + }) => { + const newPassword = credentials.password + '2'; + await settings.goto(); + await settings.password.clickChange(); + await changePassword.setCurrentPassword(credentials.password); + await changePassword.setNewPassword(newPassword); + await changePassword.setConfirmPassword(newPassword); + await changePassword.submit(); + await settings.signOut(); + credentials.password = newPassword; + await login.login(credentials.email, credentials.password); + const primaryEmail = await settings.primaryEmail.statusText(); + expect(primaryEmail).toEqual(credentials.email); + }); + + // https://testrail.stage.mozaws.net/index.php?/cases/view/1293389 + test('forgot password #1293389', async ({ + target, + credentials, + page, + pages: { settings, login }, + }, { project }) => { + test.slow(project.name !== 'local', 'email delivery can be slow'); + await settings.goto(); + await settings.signOut(); + await login.setEmail(credentials.email); + await login.submit(); + await login.clickForgotPassword(); + await login.setEmail(credentials.email); + await login.submit(); + const link = await target.email.waitForEmail( + credentials.email, + EmailType.recovery, + EmailHeader.link + ); + await page.goto(link, { waitUntil: 'networkidle' }); + await login.setNewPassword(credentials.password); + expect(page.url()).toMatch(settings.url); + }); + + // https://testrail.stage.mozaws.net/index.php?/cases/view/1293431 + test('forgot password has recovery key but skip using it #1293431', async ({ + target, + credentials, + page, + pages: { settings, login, recoveryKey }, + }, { project }) => { + test.slow(project.name !== 'local', 'email delivery can be slow'); + await settings.goto(); + await settings.recoveryKey.clickCreate(); + await recoveryKey.setPassword(credentials.password); + await recoveryKey.submit(); + await settings.signOut(); + await login.setEmail(credentials.email); + await login.submit(); + await login.clickForgotPassword(); + await login.setEmail(credentials.email); + await login.submit(); + const link = await target.email.waitForEmail( + credentials.email, + EmailType.recovery, + EmailHeader.link + ); + await page.goto(link, { waitUntil: 'networkidle' }); + await login.clickDontHaveRecoveryKey(); + await login.setNewPassword(credentials.password); + await settings.waitForAlertBar(); + const status = await settings.recoveryKey.statusText(); + expect(status).toEqual('Not Set'); + }); + + // https://testrail.stage.mozaws.net/index.php?/cases/view/1293421 + // https://testrail.stage.mozaws.net/index.php?/cases/view/1293429 + test('add and remove recovery key #1293421 #1293429', async ({ + credentials, + pages: { settings, recoveryKey }, + }) => { + await settings.goto(); + let status = await settings.recoveryKey.statusText(); + expect(status).toEqual('Not Set'); + await settings.recoveryKey.clickCreate(); + await recoveryKey.setPassword(credentials.password); + await recoveryKey.submit(); + await recoveryKey.clickClose(); + status = await settings.recoveryKey.statusText(); + expect(status).toEqual('Enabled'); + await settings.recoveryKey.clickRemove(); + await settings.clickModalConfirm(); + await settings.waitForAlertBar(); + status = await settings.recoveryKey.statusText(); + expect(status).toEqual('Not Set'); + }); + + // https://testrail.stage.mozaws.net/index.php?/cases/view/1293432 + // https://testrail.stage.mozaws.net/index.php?/cases/view/1293433 + test('use recovery key #1293432 #1293433', async ({ + credentials, + target, + pages: { page, login, recoveryKey, settings }, + }, { project }) => { + test.slow(project.name !== 'local', 'email delivery can be slow'); + await settings.goto(); + await settings.recoveryKey.clickCreate(); + await recoveryKey.setPassword(credentials.password); + await recoveryKey.submit(); + const key = await recoveryKey.getKey(); + await settings.signOut(); + await login.setEmail(credentials.email); + await login.submit(); + await login.clickForgotPassword(); + await login.setEmail(credentials.email); + await login.submit(); + const link = await target.email.waitForEmail( + credentials.email, + EmailType.recovery, + EmailHeader.link + ); + await page.goto(link, { waitUntil: 'networkidle' }); + await login.setRecoveryKey(key); + await login.submit(); + credentials.password = credentials.password + '_new'; + await login.setNewPassword(credentials.password); + await settings.waitForAlertBar(); + await settings.signOut(); + await login.login(credentials.email, credentials.password); + let status = await settings.recoveryKey.statusText(); + expect(status).toEqual('Not Set'); + await settings.recoveryKey.clickCreate(); + await recoveryKey.setPassword(credentials.password); + await recoveryKey.submit(); + await recoveryKey.clickClose(); + status = await settings.recoveryKey.statusText(); + expect(status).toEqual('Enabled'); + }); + + // https://testrail.stage.mozaws.net/index.php?/cases/view/1293446 + // https://testrail.stage.mozaws.net/index.php?/cases/view/1293452 + test('add and remove totp #1293446 #1293452', async ({ + credentials, + pages: { settings, totp }, + }) => { + await settings.goto(); + let status = await settings.totp.statusText(); + expect(status).toEqual('Not Set'); + await settings.totp.clickAdd(); + await totp.enable(credentials); + await settings.waitForAlertBar(); + status = await settings.totp.statusText(); + expect(status).toEqual('Enabled'); + await settings.totp.clickDisable(); + await settings.clickModalConfirm(); + await settings.waitForAlertBar(); + status = await settings.totp.statusText(); + expect(status).toEqual('Not Set'); + }); + + // https://testrail.stage.mozaws.net/index.php?/cases/view/1293445 + test('totp use QR code #1293445', async ({ + credentials, + pages: { settings, totp }, + }) => { + await settings.goto(); + let status = await settings.totp.statusText(); + expect(status).toEqual('Not Set'); + await settings.totp.clickAdd(); + await totp.enable(credentials, 'qr'); + await settings.waitForAlertBar(); + status = await settings.totp.statusText(); + expect(status).toEqual('Enabled'); + }); + + // https://testrail.stage.mozaws.net/index.php?/cases/view/1293459 + test('add TOTP and login #1293459', async ({ + credentials, + pages: { login, settings, totp }, + }) => { + await settings.goto(); + await settings.totp.clickAdd(); + await totp.enable(credentials); + await settings.signOut(); + await login.login(credentials.email, credentials.password); + await login.setTotp(credentials.secret); + const status = await settings.totp.statusText(); + expect(status).toEqual('Enabled'); + }); + + // https://testrail.stage.mozaws.net/index.php?/cases/view/1293402 + // https://testrail.stage.mozaws.net/index.php?/cases/view/1293406 + test('change email and login #1293402 #1293406', async ({ + credentials, + target, + pages: { login, settings, secondaryEmail }, + }, { project }) => { + test.slow(project.name !== 'local', 'email delivery can be slow'); + await settings.goto(); + await settings.secondaryEmail.clickAdd(); + const newEmail = credentials.email.replace(/(\w+)/, '$1_alt'); + await secondaryEmail.addAndVerify(newEmail); + await settings.waitForAlertBar(); + await settings.secondaryEmail.clickMakePrimary(); + credentials.email = newEmail; + await settings.signOut(); + await login.login(credentials.email, credentials.password); + const primary = await settings.primaryEmail.statusText(); + expect(primary).toEqual(newEmail); + }); + + // https://testrail.stage.mozaws.net/index.php?/cases/view/1293450 + test('can change recovery codes #1293450', async ({ + credentials, + page, + pages: { settings, totp, login }, + }) => { + await settings.goto(); + await settings.totp.clickAdd(); + const { recoveryCodes } = await totp.enable(credentials); + await settings.totp.clickChange(); + await settings.clickModalConfirm(); + const newCodes = await totp.getRecoveryCodes(); + for (const code of recoveryCodes) { + expect(newCodes).not.toContain(code); + } + await settings.signOut(); + await login.login(credentials.email, credentials.password, newCodes[0]); + expect(page.url()).toMatch(settings.url); + }); + + // https://testrail.stage.mozaws.net/index.php?/cases/view/1293460 + test('can get new recovery codes via email #1293460', async ({ + target, + credentials, + pages: { page, login, settings, totp }, + }, { project }) => { + test.slow(project.name !== 'local', 'non-local use more codes'); + await settings.goto(); + await settings.totp.clickAdd(); + const { recoveryCodes } = await totp.enable(credentials); + await settings.signOut(); + for (let i = 0; i < recoveryCodes.length - 3; i++) { + await login.login( + credentials.email, + credentials.password, + recoveryCodes[i] + ); + await settings.signOut(); + } + await login.login( + credentials.email, + credentials.password, + recoveryCodes[recoveryCodes.length - 1] + ); + const link = await target.email.waitForEmail( + credentials.email, + EmailType.lowRecoveryCodes, + EmailHeader.link + ); + await page.goto(link, { waitUntil: 'networkidle' }); + const newCodes = await totp.getRecoveryCodes(); + expect(newCodes.length).toEqual(recoveryCodes.length); + }); + + // https://testrail.stage.mozaws.net/index.php?/cases/view/1293493 + test('delete account #1293493', async ({ + credentials, + pages: { login, settings, deleteAccount, page }, + }) => { + await settings.goto(); + await settings.clickDeleteAccount(); + await deleteAccount.checkAllBoxes(); + await deleteAccount.clickContinue(); + await deleteAccount.setPassword(credentials.password); + await deleteAccount.submit(); + const success = await page.waitForSelector('.success'); + expect(await success.isVisible()).toBeTruthy(); + }); + + // https://testrail.stage.mozaws.net/index.php?/cases/view/1293461 + test('delete account with totp enabled #1293461', async ({ + credentials, + page, + pages: { settings, totp, login, deleteAccount }, + }) => { + await settings.goto(); + await settings.totp.clickAdd(); + const { secret } = await totp.enable(credentials); + await settings.signOut(); + await login.login(credentials.email, credentials.password); + await login.setTotp(secret); + await settings.clickDeleteAccount(); + await deleteAccount.checkAllBoxes(); + await deleteAccount.clickContinue(); + await deleteAccount.setPassword(credentials.password); + await deleteAccount.submit(); + const success = await page.waitForSelector('.success'); + expect(await success.isVisible()).toBeTruthy(); + }); +}); + +test.describe('severity-2', () => { + // https://testrail.stage.mozaws.net/index.php?/cases/view/1293371 + // https://testrail.stage.mozaws.net/index.php?/cases/view/1293373 + test('set/unset the display name #1293371 #1293373', async ({ + pages: { settings, displayName }, + }) => { + await settings.goto(); + expect(await settings.displayName.statusText()).toEqual('None'); + await settings.displayName.clickAdd(); + await displayName.setDisplayName('me'); + await displayName.submit(); + expect(await settings.displayName.statusText()).toEqual('me'); + await settings.displayName.clickAdd(); + await displayName.setDisplayName(''); + await displayName.submit(); + expect(await settings.displayName.statusText()).toEqual('None'); + }); + + // https://testrail.stage.mozaws.net/index.php?/cases/view/1293475 + test('disconnect RP #1293475', async ({ + browser, + credentials, + pages, + target, + }, { project }) => { + test.skip(project.name === 'production', 'no 123done in production'); + const [a, b] = [pages, await newPages(browser, target)]; + await b.relier.goto(); + await b.relier.clickEmailFirst(); + await b.login.login(credentials.email, credentials.password); + + await a.settings.goto(); + + let services = await a.settings.connectedServices.services(); + expect(services.length).toEqual(3); + const relier = services[2]; + await relier.signout(); + await a.settings.waitForAlertBar(); + services = await a.settings.connectedServices.services(); + expect(services.length).toEqual(2); + }); + + // https://testrail.stage.mozaws.net/index.php?/cases/view/1293504 + test('settings help link #1293504', async ({ pages: { settings } }) => { + await settings.goto(); + const helpPage = await settings.clickHelp(); + expect(helpPage.url()).toMatch('https://support.mozilla.org'); + }); + + // https://testrail.stage.mozaws.net/index.php?/cases/view/1293498 + test('settings avatar drop-down #1293498', async ({ + target, + credentials, + page, + pages: { settings }, + }) => { + await settings.goto(); + await settings.clickAvatarIcon(); + await expect(settings.avatarMenu).toBeVisible(); + await expect(settings.avatarMenu).toContainText(credentials.email); + await page.keyboard.press('Escape'); + await expect(settings.avatarMenu).toBeHidden(); + await settings.signOut(); + }); + + // https://testrail.stage.mozaws.net/index.php?/cases/view/1293407 + test('removing secondary emails #1293407', async ({ + credentials, + pages: { settings, secondaryEmail }, + }, { project }) => { + test.slow(project.name !== 'local', 'email delivery can be slow'); + await settings.goto(); + await settings.secondaryEmail.clickAdd(); + const newEmail = credentials.email.replace(/(\w+)/, '$1_alt'); + await secondaryEmail.addAndVerify(newEmail); + await settings.waitForAlertBar(); + await settings.closeAlertBar(); + await settings.secondaryEmail.clickDelete(); + await settings.waitForAlertBar(); + expect(await settings.alertBarText()).toMatch('successfully deleted'); + await settings.secondaryEmail.clickAdd(); + await secondaryEmail.setEmail(newEmail); + await secondaryEmail.submit(); + // skip verification + await settings.goto(); + expect(await settings.secondaryEmail.statusText()).toMatch('UNVERIFIED'); + await settings.secondaryEmail.clickDelete(); + await settings.waitForAlertBar(); + expect(await settings.alertBarText()).toMatch('successfully deleted'); + }); + + // https://testrail.stage.mozaws.net/index.php?/cases/view/1293513 + // https://testrail.stage.mozaws.net/index.php?/cases/view/1293517 + test('upload avatar #1293513 #1293517', async ({ + pages: { settings, avatar }, + }) => { + await settings.goto(); + await settings.avatar.clickAdd(); + const filechooser = await avatar.clickAddPhoto(); + await filechooser.setFiles('./pages/settings/avatar.png'); + await avatar.clickSave(); + expect(await settings.avatar.isDefault()).toBeFalsy(); + await settings.avatar.clickChange(); + await avatar.clickRemove(); + expect(await settings.avatar.isDefault()).toBeTruthy(); + }); + + // https://testrail.stage.mozaws.net/index.php?/cases/view/1293480 + test('edit email communication preferences #1293480', async ({ + credentials, + pages: { settings, login }, + }, { project }) => { + test.skip(project.name !== 'production', 'uses prod email prefs'); + + await settings.goto(); + const emailPage = await settings.clickEmailPreferences(); + login.page = emailPage; + await login.setPassword(credentials.password); + await login.submit(); + expect(emailPage.url()).toMatch('https://www.mozilla.org/en-US/newsletter'); + // TODO change prefs and save + }); +}); + +test.describe('severity-3', () => { + // https://testrail.stage.mozaws.net/index.php?/cases/view/1293501 + test('settings bento menu #1293501', async ({ + page, + pages: { settings }, + }) => { + await settings.goto(); + await settings.clickBentoIcon(); + await expect(settings.bentoMenu).toBeVisible(); + await page.keyboard.press('Escape'); + await expect(settings.bentoMenu).toBeHidden(); + }); + + // https://testrail.stage.mozaws.net/index.php?/cases/view/1293423 + // https://testrail.stage.mozaws.net/index.php?/cases/view/1293440 + test('settings data-trio component works #1293423 #1293440', async ({ + credentials, + pages: { settings, recoveryKey }, + }) => { + await settings.goto(); + await settings.recoveryKey.clickCreate(); + await recoveryKey.setPassword(credentials.password); + await recoveryKey.submit(); + const dl = await recoveryKey.dataTrio.clickDownload(); + expect(dl.suggestedFilename()).toBe(`${credentials.email} Firefox.txt`); + const clipboard = await recoveryKey.dataTrio.clickCopy(); + expect(clipboard).toEqual(await recoveryKey.getKey()); + const printed = await recoveryKey.dataTrio.clickPrint(); + expect(printed).toBe(true); + }); + + // https://testrail.stage.mozaws.net/index.php?/cases/view/1293362 + // https://testrail.stage.mozaws.net/index.php?/cases/view/1293469 + test('can login to addons #1293362 #1293469', async ({ + credentials, + page, + pages: { login, settings }, + }, { project }) => { + test.skip(project.name !== 'production', 'uses prod addons site'); + await page.goto('https://addons.mozilla.org/en-US/firefox/'); + await Promise.all([page.click('text=Log in'), page.waitForNavigation()]); + await login.login(credentials.email, credentials.password); + expect(page.url()).toMatch( + 'https://addons.mozilla.org/en-US/firefox/users/edit' + ); + await page.click('text=Delete My Profile'); + await page.click('button.Button--alert[type=submit]'); + await page.waitForURL('https://addons.mozilla.org/en-US/firefox'); + await settings.goto(); + const services = await settings.connectedServices.services(); + const names = services.map((s) => s.name); + expect(names).toContainEqual('Add-ons'); + }); + + // https://testrail.stage.mozaws.net/index.php?/cases/view/1293352 + test('can login to pocket #1293352', async ({ + credentials, + page, + pages: { login }, + }, { project }) => { + test.fixme(true, 'pocket logout hangs after link clicked'); + test.skip(project.name !== 'production', 'uses prod pocket'); + await page.goto('https://getpocket.com/login'); + await Promise.all([ + page.click('a:has-text("Continue with Firefox")'), + page.waitForNavigation(), + ]); + await login.login(credentials.email, credentials.password); + expect(page.url()).toMatch('https://getpocket.com/my-list'); + await page.click('[aria-label="Open Account Menu"]'); + await page.click('a:has-text("Log out")'); + }); + + // https://testrail.stage.mozaws.net/index.php?/cases/view/1293364 + test('can login to monitor #1293364', async ({ + credentials, + page, + pages: { login }, + }, { project }) => { + test.skip(project.name !== 'production', 'uses prod monitor'); + await page.goto('https://monitor.firefox.com'); + await Promise.all([ + page.click('text=Sign Up for Alerts'), + page.waitForNavigation(), + ]); + await login.login(credentials.email, credentials.password); + expect(page.url()).toMatch('https://monitor.firefox.com/user/dashboard'); + await page.click('[aria-label="Open Firefox Account navigation"]'); + await Promise.all([ + page.click('text=Sign Out'), + page.waitForNavigation({ waitUntil: 'networkidle' }), + ]); + await expect(page.locator('#sign-in-btn')).toBeVisible(); + }); + + // https://testrail.stage.mozaws.net/index.php?/cases/view/1293360 + test('can login to SUMO #1293360', async ({ + credentials, + page, + pages: { login }, + }, { project }) => { + test.skip(project.name !== 'production', 'uses prod monitor'); + test.slow(); + + await page.goto('https://support.mozilla.org/en-US/'); + + await Promise.all([ + page.click('text=Sign In/Up'), + page.waitForNavigation(), + ]); + await Promise.all([ + page.click('text=Continue with Firefox Accounts'), + page.waitForNavigation(), + ]); + await login.login(credentials.email, credentials.password); + + await page.hover('a[href="/en-US/users/auth"]'); + await page.click('text=Sign Out'); + await expect(page.locator('text=Sign In/Up').first()).toBeVisible(); + }); +}); diff --git a/packages/functional-tests/types.d.ts b/packages/functional-tests/types.d.ts new file mode 100644 index 0000000000..7b26d42910 --- /dev/null +++ b/packages/functional-tests/types.d.ts @@ -0,0 +1 @@ +type hexstring = string; diff --git a/packages/fxa-auth-client/lib/client.ts b/packages/fxa-auth-client/lib/client.ts index c14174c28a..a3f8ba983c 100644 --- a/packages/fxa-auth-client/lib/client.ts +++ b/packages/fxa-auth-client/lib/client.ts @@ -36,6 +36,13 @@ export interface MetricsContext { utmTerm?: string; } +export type VerificationMethod = + | 'email' + | 'email-otp' + | 'email-2fa' + | 'email-captcha' + | 'totp-2fa'; + function langHeader(lang?: string) { return new Headers( lang @@ -221,7 +228,13 @@ export default class AuthClient { verificationMethod?: string; metricsContext?: MetricsContext; } = {} - ) { + ): Promise<{ + uid: hexstring; + authAt: number; + sessionToken: hexstring; + keyFetchToken?: hexstring; + verificationMethod?: VerificationMethod; + }> { const credentials = await crypto.getCredentials(email, password); const payloadOptions = ({ keys, lang, ...rest }: any) => rest; const payload = { diff --git a/packages/fxa-content-server/app/tests/test_start.js b/packages/fxa-content-server/app/tests/test_start.js index a4dbdec58c..e9d69aa327 100644 --- a/packages/fxa-content-server/app/tests/test_start.js +++ b/packages/fxa-content-server/app/tests/test_start.js @@ -2,6 +2,8 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +/* globals globalThis */ + mocha.setup('bdd'); mocha.timeout(20000); @@ -278,7 +280,7 @@ const runTests = function () { }); var runner = mocha.run(); - + globalThis.runner = runner; /** * Monkey patch runner.fail to clean the stack trace. Using * `runner.on('fail', ..` does not work because the callback diff --git a/packages/fxa-content-server/app/tests/webpack.js b/packages/fxa-content-server/app/tests/webpack.js index 3d576737b1..fd04ee4dcd 100644 --- a/packages/fxa-content-server/app/tests/webpack.js +++ b/packages/fxa-content-server/app/tests/webpack.js @@ -2,5 +2,4 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -/* eslint-disable */ -import Tests from './test_start'; +import './test_start'; diff --git a/packages/fxa-settings/scripts/test-ci.sh b/packages/fxa-settings/scripts/test-ci.sh index 3ca9e9a489..88176870c0 100755 --- a/packages/fxa-settings/scripts/test-ci.sh +++ b/packages/fxa-settings/scripts/test-ci.sh @@ -1,5 +1,9 @@ #!/bin/bash -ex +DIR=$(dirname "$0") + +cd "$DIR/.." + yarn build SKIP_PREFLIGHT_CHECK=true yarn test diff --git a/packages/fxa-settings/src/components/Security/index.test.tsx b/packages/fxa-settings/src/components/Security/index.test.tsx index fbc5a285b6..0dd4a6af86 100644 --- a/packages/fxa-settings/src/components/Security/index.test.tsx +++ b/packages/fxa-settings/src/components/Security/index.test.tsx @@ -23,7 +23,7 @@ describe('Security', () => { expect(await screen.findByText('rk-header')).toBeTruthy; expect(await screen.findByText('tfa-row-header')).toBeTruthy; - const result = await screen.findAllByText('Not set'); + const result = await screen.findAllByText('Not Set'); expect(result).toHaveLength(2); }); diff --git a/packages/fxa-settings/src/components/UnitRow/index.test.tsx b/packages/fxa-settings/src/components/UnitRow/index.test.tsx index 6b8a02b84c..03d29ff21a 100644 --- a/packages/fxa-settings/src/components/UnitRow/index.test.tsx +++ b/packages/fxa-settings/src/components/UnitRow/index.test.tsx @@ -91,14 +91,14 @@ describe('UnitRow', () => { ); expect(screen.getByTestId('unit-row-header-value').textContent).toContain( - 'Not set' + 'Not Set' ); expect(screen.getByTestId('unit-row-route').textContent).toContain( 'Create' diff --git a/packages/fxa-settings/src/components/UnitRowRecoveryKey/en-US.ftl b/packages/fxa-settings/src/components/UnitRowRecoveryKey/en-US.ftl index 1887ddad67..66abfe8e9c 100644 --- a/packages/fxa-settings/src/components/UnitRowRecoveryKey/en-US.ftl +++ b/packages/fxa-settings/src/components/UnitRowRecoveryKey/en-US.ftl @@ -2,7 +2,7 @@ rk-header = Recovery key rk-enabled = Enabled -rk-not-set = Not set +rk-not-set = Not Set rk-action-create = Create rk-action-remove = Remove rk-cannot-refresh = Sorry, there was a problem refreshing the recovery key. diff --git a/packages/fxa-settings/src/components/UnitRowRecoveryKey/index.test.tsx b/packages/fxa-settings/src/components/UnitRowRecoveryKey/index.test.tsx index be30110a9a..02e5cbff6a 100644 --- a/packages/fxa-settings/src/components/UnitRowRecoveryKey/index.test.tsx +++ b/packages/fxa-settings/src/components/UnitRowRecoveryKey/index.test.tsx @@ -45,7 +45,7 @@ describe('UnitRowRecoveryKey', () => { ).toContain('rk-header'); expect( screen.getByTestId('recovery-key-unit-row-header-value').textContent - ).toContain('Not set'); + ).toContain('Not Set'); expect( screen.getByTestId('recovery-key-unit-row-route').textContent ).toContain('Create'); @@ -65,7 +65,7 @@ describe('UnitRowRecoveryKey', () => { }); expect( screen.getByTestId('recovery-key-unit-row-header-value') - ).toHaveTextContent('Not set'); + ).toHaveTextContent('Not Set'); await act(async () => { fireEvent.click(screen.getByTestId('recovery-key-refresh')); }); diff --git a/packages/fxa-settings/src/components/UnitRowRecoveryKey/index.tsx b/packages/fxa-settings/src/components/UnitRowRecoveryKey/index.tsx index a513d6e31a..27418cdeb1 100644 --- a/packages/fxa-settings/src/components/UnitRowRecoveryKey/index.tsx +++ b/packages/fxa-settings/src/components/UnitRowRecoveryKey/index.tsx @@ -45,7 +45,7 @@ export const UnitRowRecoveryKey = () => { headerValue={ recoveryKey ? l10n.getString('rk-enabled', null, 'Enabled') - : l10n.getString('rk-not-set', null, 'Not set') + : l10n.getString('rk-not-set', null, 'Not Set') } route={recoveryKey ? undefined : `${HomePath}/account_recovery`} revealModal={recoveryKey ? revealModal : undefined} diff --git a/packages/fxa-settings/src/components/UnitRowTwoStepAuth/en-US.ftl b/packages/fxa-settings/src/components/UnitRowTwoStepAuth/en-US.ftl index 24e3883edd..b6498184b5 100644 --- a/packages/fxa-settings/src/components/UnitRowTwoStepAuth/en-US.ftl +++ b/packages/fxa-settings/src/components/UnitRowTwoStepAuth/en-US.ftl @@ -3,7 +3,7 @@ tfa-row-header = Two-step authentication tfa-row-disabled = Two-step authentication disabled. tfa-row-enabled = Enabled -tfa-row-not-set = Not set +tfa-row-not-set = Not Set tfa-row-action-add = Add tfa-row-action-disable = Disable diff --git a/packages/fxa-settings/src/components/UnitRowTwoStepAuth/index.test.tsx b/packages/fxa-settings/src/components/UnitRowTwoStepAuth/index.test.tsx index 2da50b4ae7..ee6bdd9ee9 100644 --- a/packages/fxa-settings/src/components/UnitRowTwoStepAuth/index.test.tsx +++ b/packages/fxa-settings/src/components/UnitRowTwoStepAuth/index.test.tsx @@ -62,7 +62,7 @@ describe('UnitRowTwoStepAuth', () => { ).toContain('tfa-row-header'); expect( screen.getByTestId('two-step-unit-row-header-value').textContent - ).toContain('Not set'); + ).toContain('Not Set'); expect(screen.getByTestId('two-step-unit-row-route').textContent).toContain( 'Add' ); @@ -80,7 +80,7 @@ describe('UnitRowTwoStepAuth', () => { ); expect( screen.getByTestId('two-step-unit-row-header-value') - ).toHaveTextContent('Not set'); + ).toHaveTextContent('Not Set'); await act(async () => { fireEvent.click(screen.getByTestId('two-step-refresh')); }); diff --git a/packages/fxa-settings/src/components/UnitRowTwoStepAuth/index.tsx b/packages/fxa-settings/src/components/UnitRowTwoStepAuth/index.tsx index ca3d2483ce..785cb2dbc6 100644 --- a/packages/fxa-settings/src/components/UnitRowTwoStepAuth/index.tsx +++ b/packages/fxa-settings/src/components/UnitRowTwoStepAuth/index.tsx @@ -23,11 +23,8 @@ export const UnitRowTwoStepAuth = () => { totp: { exists, verified }, } = account; const [modalRevealed, revealModal, hideModal] = useBooleanState(); - const [ - secondaryModalRevealed, - revealSecondaryModal, - hideSecondaryModal, - ] = useBooleanState(); + const [secondaryModalRevealed, revealSecondaryModal, hideSecondaryModal] = + useBooleanState(); const { l10n } = useLocalization(); const disableTwoStepAuth = useCallback(async () => { @@ -73,7 +70,7 @@ export const UnitRowTwoStepAuth = () => { } : { headerValue: null, - noHeaderValueText: l10n.getString('tfa-row-not-set', null, 'Not set'), + noHeaderValueText: l10n.getString('tfa-row-not-set', null, 'Not Set'), ctaText: l10n.getString('tfa-row-action-add', null, 'Add'), secondaryCtaText: undefined, revealSecondaryModal: undefined, diff --git a/yarn.lock b/yarn.lock index 20cf7929f1..634033f40d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -315,6 +315,15 @@ __metadata: languageName: node linkType: hard +"@babel/code-frame@npm:^7.16.0": + version: 7.16.0 + resolution: "@babel/code-frame@npm:7.16.0" + dependencies: + "@babel/highlight": ^7.16.0 + checksum: 8961d0302ec6b8c2e9751a11e06a17617425359fd1645e4dae56a90a03464c68a0916115100fbcd030961870313f21865d0b85858360a2c68aabdda744393607 + languageName: node + linkType: hard + "@babel/compat-data@npm:^7.12.1, @babel/compat-data@npm:^7.13.11, @babel/compat-data@npm:^7.14.7, @babel/compat-data@npm:^7.15.0": version: 7.15.0 resolution: "@babel/compat-data@npm:7.15.0" @@ -322,6 +331,13 @@ __metadata: languageName: node linkType: hard +"@babel/compat-data@npm:^7.16.0": + version: 7.16.0 + resolution: "@babel/compat-data@npm:7.16.0" + checksum: 2befa4ba145e3acdce3e160dcad0917a073f12d238bde295c37676e7a1d164630848926034df2dfde244cef6a190b25350ffac0b4215c37123787f67aea80e71 + languageName: node + linkType: hard + "@babel/core@npm:7.12.3": version: 7.12.3 resolution: "@babel/core@npm:7.12.3" @@ -416,6 +432,29 @@ __metadata: languageName: node linkType: hard +"@babel/core@npm:7.16.0, @babel/core@npm:^7.14.8": + version: 7.16.0 + resolution: "@babel/core@npm:7.16.0" + dependencies: + "@babel/code-frame": ^7.16.0 + "@babel/generator": ^7.16.0 + "@babel/helper-compilation-targets": ^7.16.0 + "@babel/helper-module-transforms": ^7.16.0 + "@babel/helpers": ^7.16.0 + "@babel/parser": ^7.16.0 + "@babel/template": ^7.16.0 + "@babel/traverse": ^7.16.0 + "@babel/types": ^7.16.0 + convert-source-map: ^1.7.0 + debug: ^4.1.0 + gensync: ^1.0.0-beta.2 + json5: ^2.1.2 + semver: ^6.3.0 + source-map: ^0.5.0 + checksum: a140f669daa90c774016a76b1f85641975333c1c219ae0a8e65d8b4c316836e918276e0dfd55613b14f8e578406a92393d4368a63bdd5d0708122976ee2ee8e3 + languageName: node + linkType: hard + "@babel/generator@npm:^7.12.1, @babel/generator@npm:^7.12.11, @babel/generator@npm:^7.12.5, @babel/generator@npm:^7.15.0": version: 7.15.0 resolution: "@babel/generator@npm:7.15.0" @@ -438,6 +477,17 @@ __metadata: languageName: node linkType: hard +"@babel/generator@npm:^7.16.0": + version: 7.16.0 + resolution: "@babel/generator@npm:7.16.0" + dependencies: + "@babel/types": ^7.16.0 + jsesc: ^2.5.1 + source-map: ^0.5.0 + checksum: 9ff53e0db72a225c8783c4a277698b4efcead750542ebb9cff31732ba62d092090715a772df10a323446924712f6928ad60c03db4e7051bed3a9701b552d51fb + languageName: node + linkType: hard + "@babel/helper-annotate-as-pure@npm:^7.14.5": version: 7.14.5 resolution: "@babel/helper-annotate-as-pure@npm:7.14.5" @@ -447,6 +497,15 @@ __metadata: languageName: node linkType: hard +"@babel/helper-annotate-as-pure@npm:^7.16.0": + version: 7.16.0 + resolution: "@babel/helper-annotate-as-pure@npm:7.16.0" + dependencies: + "@babel/types": ^7.16.0 + checksum: 0db76106983e10ffc482c5f01e89c3b4687d2474bea69c44470b2acb6bd37f362f9057d6e69c617255390b5d0063d9932a931e83c3e130445b688ca1fcdb5bcd + languageName: node + linkType: hard + "@babel/helper-builder-binary-assignment-operator-visitor@npm:^7.14.5": version: 7.14.5 resolution: "@babel/helper-builder-binary-assignment-operator-visitor@npm:7.14.5" @@ -485,6 +544,20 @@ __metadata: languageName: node linkType: hard +"@babel/helper-compilation-targets@npm:^7.16.0": + version: 7.16.0 + resolution: "@babel/helper-compilation-targets@npm:7.16.0" + dependencies: + "@babel/compat-data": ^7.16.0 + "@babel/helper-validator-option": ^7.14.5 + browserslist: ^4.16.6 + semver: ^6.3.0 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 81117682e84107a4fbfe619a53c232f1c79d769adae32f0b16b5114377bd4b04ad1741d96f6c155dab78ef9c084aec0e6b835a44598f32a404fb72db915f4acd + languageName: node + linkType: hard + "@babel/helper-create-class-features-plugin@npm:^7.12.1, @babel/helper-create-class-features-plugin@npm:^7.13.11, @babel/helper-create-class-features-plugin@npm:^7.14.5, @babel/helper-create-class-features-plugin@npm:^7.15.0": version: 7.15.0 resolution: "@babel/helper-create-class-features-plugin@npm:7.15.0" @@ -501,6 +574,22 @@ __metadata: languageName: node linkType: hard +"@babel/helper-create-class-features-plugin@npm:^7.16.0": + version: 7.16.0 + resolution: "@babel/helper-create-class-features-plugin@npm:7.16.0" + dependencies: + "@babel/helper-annotate-as-pure": ^7.16.0 + "@babel/helper-function-name": ^7.16.0 + "@babel/helper-member-expression-to-functions": ^7.16.0 + "@babel/helper-optimise-call-expression": ^7.16.0 + "@babel/helper-replace-supers": ^7.16.0 + "@babel/helper-split-export-declaration": ^7.16.0 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 0f7d1b8d413e5fbd719c95e22e3b59749b4c6c652f20e0fa1fa954112145a134c22709f1325574632d7262aeeeaaf4fc7c2eb8117e0d521e42b36d05c3e5a885 + languageName: node + linkType: hard + "@babel/helper-create-regexp-features-plugin@npm:^7.14.5": version: 7.14.5 resolution: "@babel/helper-create-regexp-features-plugin@npm:7.14.5" @@ -580,6 +669,17 @@ __metadata: languageName: node linkType: hard +"@babel/helper-function-name@npm:^7.16.0": + version: 7.16.0 + resolution: "@babel/helper-function-name@npm:7.16.0" + dependencies: + "@babel/helper-get-function-arity": ^7.16.0 + "@babel/template": ^7.16.0 + "@babel/types": ^7.16.0 + checksum: 8c02371d28678f3bb492e69d4635b2fe6b1c5a93ce129bf883f1fafde2005f4dbc0e643f52103ca558b698c0774bfb84a93f188d71db1c077f754b6220629b92 + languageName: node + linkType: hard + "@babel/helper-get-function-arity@npm:^7.14.5": version: 7.14.5 resolution: "@babel/helper-get-function-arity@npm:7.14.5" @@ -598,6 +698,15 @@ __metadata: languageName: node linkType: hard +"@babel/helper-get-function-arity@npm:^7.16.0": + version: 7.16.0 + resolution: "@babel/helper-get-function-arity@npm:7.16.0" + dependencies: + "@babel/types": ^7.16.0 + checksum: 1a68322c7b5fdffb1b51df32f7a53b1ff2268b5b99d698f0a1a426dcb355482a44ef3dae982a507907ba975314638dabb6d77ac1778098bdbe99707e6c29cae8 + languageName: node + linkType: hard + "@babel/helper-hoist-variables@npm:^7.14.5": version: 7.14.5 resolution: "@babel/helper-hoist-variables@npm:7.14.5" @@ -616,6 +725,15 @@ __metadata: languageName: node linkType: hard +"@babel/helper-hoist-variables@npm:^7.16.0": + version: 7.16.0 + resolution: "@babel/helper-hoist-variables@npm:7.16.0" + dependencies: + "@babel/types": ^7.16.0 + checksum: 2ee5b400c267c209a53c90eea406a8f09c30d4d7a2b13e304289d858a2e34a99272c062cfad6dad63705662943951c42ff20042ef539b2d3c4f8743183a28954 + languageName: node + linkType: hard + "@babel/helper-member-expression-to-functions@npm:^7.15.0": version: 7.15.0 resolution: "@babel/helper-member-expression-to-functions@npm:7.15.0" @@ -634,6 +752,15 @@ __metadata: languageName: node linkType: hard +"@babel/helper-member-expression-to-functions@npm:^7.16.0": + version: 7.16.0 + resolution: "@babel/helper-member-expression-to-functions@npm:7.16.0" + dependencies: + "@babel/types": ^7.16.0 + checksum: 58ef8e3a4af0c1dc43a2011f43f25502877ac1c5aa9a4a6586f0265ab857b65831f60560044bc9380df43c91ac21cad39a84095b91764b433d1acf18d27e38d6 + languageName: node + linkType: hard + "@babel/helper-module-imports@npm:^7.0.0, @babel/helper-module-imports@npm:^7.12.1, @babel/helper-module-imports@npm:^7.12.13, @babel/helper-module-imports@npm:^7.14.5": version: 7.14.5 resolution: "@babel/helper-module-imports@npm:7.14.5" @@ -652,6 +779,15 @@ __metadata: languageName: node linkType: hard +"@babel/helper-module-imports@npm:^7.16.0": + version: 7.16.0 + resolution: "@babel/helper-module-imports@npm:7.16.0" + dependencies: + "@babel/types": ^7.16.0 + checksum: 8e1eb9ac39440e52080b87c78d8d318e7c93658bdd0f3ce0019c908de88cbddafdc241f392898c0b0ba81fc52c8c6d2f9cc1b163ac5ed2a474d49b11646b7516 + languageName: node + linkType: hard + "@babel/helper-module-transforms@npm:^7.12.1, @babel/helper-module-transforms@npm:^7.14.5, @babel/helper-module-transforms@npm:^7.15.0": version: 7.15.0 resolution: "@babel/helper-module-transforms@npm:7.15.0" @@ -684,6 +820,22 @@ __metadata: languageName: node linkType: hard +"@babel/helper-module-transforms@npm:^7.16.0": + version: 7.16.0 + resolution: "@babel/helper-module-transforms@npm:7.16.0" + dependencies: + "@babel/helper-module-imports": ^7.16.0 + "@babel/helper-replace-supers": ^7.16.0 + "@babel/helper-simple-access": ^7.16.0 + "@babel/helper-split-export-declaration": ^7.16.0 + "@babel/helper-validator-identifier": ^7.15.7 + "@babel/template": ^7.16.0 + "@babel/traverse": ^7.16.0 + "@babel/types": ^7.16.0 + checksum: a3d0e5556f26ebdf2ae422af3b9a1ba1848fead891f46bcd1c6a4be88ad8e9f348140f81d1843a3481574be1643a9c79b01469231f5b5801f5d5e691efdd11f3 + languageName: node + linkType: hard + "@babel/helper-optimise-call-expression@npm:^7.14.5": version: 7.14.5 resolution: "@babel/helper-optimise-call-expression@npm:7.14.5" @@ -702,6 +854,15 @@ __metadata: languageName: node linkType: hard +"@babel/helper-optimise-call-expression@npm:^7.16.0": + version: 7.16.0 + resolution: "@babel/helper-optimise-call-expression@npm:7.16.0" + dependencies: + "@babel/types": ^7.16.0 + checksum: 121ae6054fcec76ed2c4dd83f0281b901c1e3cfac1bbff79adc3667983903ad1030a0ad9a8bea58e52b225e13881cf316f371c65276976e7a6762758a98be8f6 + languageName: node + linkType: hard + "@babel/helper-plugin-utils@npm:7.10.4": version: 7.10.4 resolution: "@babel/helper-plugin-utils@npm:7.10.4" @@ -751,6 +912,18 @@ __metadata: languageName: node linkType: hard +"@babel/helper-replace-supers@npm:^7.16.0": + version: 7.16.0 + resolution: "@babel/helper-replace-supers@npm:7.16.0" + dependencies: + "@babel/helper-member-expression-to-functions": ^7.16.0 + "@babel/helper-optimise-call-expression": ^7.16.0 + "@babel/traverse": ^7.16.0 + "@babel/types": ^7.16.0 + checksum: 61f04bbe05ff0987d5a8d5253cb101d47004a27951d6c5cd95457e30fcb3adaca85f0bcaa7f31f4d934f22386b935ac7281398c68982d4a4768769d95c028460 + languageName: node + linkType: hard + "@babel/helper-simple-access@npm:^7.14.8": version: 7.14.8 resolution: "@babel/helper-simple-access@npm:7.14.8" @@ -769,6 +942,15 @@ __metadata: languageName: node linkType: hard +"@babel/helper-simple-access@npm:^7.16.0": + version: 7.16.0 + resolution: "@babel/helper-simple-access@npm:7.16.0" + dependencies: + "@babel/types": ^7.16.0 + checksum: 2d7155f318411788b42d2f4a3d406de12952ad620d0bd411a0f3b5803389692ad61d9e7fab5f93b23ad3d8a09db4a75ca9722b9873a606470f468bc301944af6 + languageName: node + linkType: hard + "@babel/helper-skip-transparent-expression-wrappers@npm:^7.12.1, @babel/helper-skip-transparent-expression-wrappers@npm:^7.14.5": version: 7.14.5 resolution: "@babel/helper-skip-transparent-expression-wrappers@npm:7.14.5" @@ -796,6 +978,15 @@ __metadata: languageName: node linkType: hard +"@babel/helper-split-export-declaration@npm:^7.16.0": + version: 7.16.0 + resolution: "@babel/helper-split-export-declaration@npm:7.16.0" + dependencies: + "@babel/types": ^7.16.0 + checksum: 8bd87b5ea2046b145f0f55bc75cbdb6df69eaeb32919ee3c1c758757025aebca03e567a4d48389eb4f16a55021adb6ed8fa58aa771e164b15fa5e0a0722f771d + languageName: node + linkType: hard + "@babel/helper-validator-identifier@npm:^7.14.5, @babel/helper-validator-identifier@npm:^7.14.9": version: 7.14.9 resolution: "@babel/helper-validator-identifier@npm:7.14.9" @@ -851,6 +1042,17 @@ __metadata: languageName: node linkType: hard +"@babel/helpers@npm:^7.16.0": + version: 7.16.0 + resolution: "@babel/helpers@npm:7.16.0" + dependencies: + "@babel/template": ^7.16.0 + "@babel/traverse": ^7.16.0 + "@babel/types": ^7.16.0 + checksum: 88d37c414dfb8815d5966774f9d65c9378fe9fd2e7e70f5c1c13e0611eca41b7114e9ffa8b37a69682c1a31a83dc7302e92e759b515220fea16c8e642282375a + languageName: node + linkType: hard + "@babel/highlight@npm:^7.0.0, @babel/highlight@npm:^7.10.4, @babel/highlight@npm:^7.14.5": version: 7.14.5 resolution: "@babel/highlight@npm:7.14.5" @@ -862,6 +1064,17 @@ __metadata: languageName: node linkType: hard +"@babel/highlight@npm:^7.16.0": + version: 7.16.0 + resolution: "@babel/highlight@npm:7.16.0" + dependencies: + "@babel/helper-validator-identifier": ^7.15.7 + chalk: ^2.0.0 + js-tokens: ^4.0.0 + checksum: abf244c48fcff20ec87830e8b99c776f4dcdd9138e63decc195719a94148da35339639e0d8045eb9d1f3e67a39ab90a9c3f5ce2d579fb1a0368d911ddf29b4e5 + languageName: node + linkType: hard + "@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.12.11, @babel/parser@npm:^7.12.3, @babel/parser@npm:^7.12.7, @babel/parser@npm:^7.14.5, @babel/parser@npm:^7.15.0, @babel/parser@npm:^7.7.0": version: 7.15.0 resolution: "@babel/parser@npm:7.15.0" @@ -880,6 +1093,15 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.16.0": + version: 7.16.2 + resolution: "@babel/parser@npm:7.16.2" + bin: + parser: ./bin/babel-parser.js + checksum: e8ceef8214adf55beaae80fff1647ae6535e17af592749a6f3fd3ed1081bbb1474fd88bf4d3470ec8bc0082843a6d23445e9e08b03e91831458acc6fd37d7bc9 + languageName: node + linkType: hard + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:^7.14.5": version: 7.14.5 resolution: "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:7.14.5" @@ -1423,6 +1645,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-syntax-typescript@npm:^7.16.0": + version: 7.16.0 + resolution: "@babel/plugin-syntax-typescript@npm:7.16.0" + dependencies: + "@babel/helper-plugin-utils": ^7.14.5 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 2da3bdd031230e515615fe39c50d40064d04f64f1d2b60113adff2c112a27e4f9425425e604297d5c2af2b635e7980f3677e434dfeb1d7320ad2cd1ffc8e8c2a + languageName: node + linkType: hard + "@babel/plugin-transform-arrow-functions@npm:^7.12.1, @babel/plugin-transform-arrow-functions@npm:^7.14.5": version: 7.14.5 resolution: "@babel/plugin-transform-arrow-functions@npm:7.14.5" @@ -1639,6 +1872,20 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-modules-commonjs@npm:^7.14.5": + version: 7.16.0 + resolution: "@babel/plugin-transform-modules-commonjs@npm:7.16.0" + dependencies: + "@babel/helper-module-transforms": ^7.16.0 + "@babel/helper-plugin-utils": ^7.14.5 + "@babel/helper-simple-access": ^7.16.0 + babel-plugin-dynamic-import-node: ^2.3.3 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: a7e43670f503b31d6ad42977ddefb7bffc23f700a24252859652aa03efd666698567b0817060dd6f84a6cd23e7aac7464bc0dc7f7f929cad212263abcac9d470 + languageName: node + linkType: hard + "@babel/plugin-transform-modules-systemjs@npm:^7.12.1, @babel/plugin-transform-modules-systemjs@npm:^7.14.5": version: 7.14.5 resolution: "@babel/plugin-transform-modules-systemjs@npm:7.14.5" @@ -1920,6 +2167,19 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-typescript@npm:^7.16.0": + version: 7.16.1 + resolution: "@babel/plugin-transform-typescript@npm:7.16.1" + dependencies: + "@babel/helper-create-class-features-plugin": ^7.16.0 + "@babel/helper-plugin-utils": ^7.14.5 + "@babel/plugin-syntax-typescript": ^7.16.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 1b1efe62e8de828d52b996429718663705cbefb9a7382d2849725b6318051fcbe9671e9e8f761a94fddf46ea159810c97d1b6282c644f69c98ebf5d4d2687ef6 + languageName: node + linkType: hard + "@babel/plugin-transform-unicode-escapes@npm:^7.12.1, @babel/plugin-transform-unicode-escapes@npm:^7.14.5": version: 7.14.5 resolution: "@babel/plugin-transform-unicode-escapes@npm:7.14.5" @@ -2187,6 +2447,19 @@ __metadata: languageName: node linkType: hard +"@babel/preset-typescript@npm:^7.14.5": + version: 7.16.0 + resolution: "@babel/preset-typescript@npm:7.16.0" + dependencies: + "@babel/helper-plugin-utils": ^7.14.5 + "@babel/helper-validator-option": ^7.14.5 + "@babel/plugin-transform-typescript": ^7.16.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 9b22316e96a34836c113f60c49d58023c8ba4219bcb0843a7685c04511486cf7c610e0d30551a1417809e2fd039884c847f6ede46abe2b8d520140e15fb36aaf + languageName: node + linkType: hard + "@babel/register@npm:^7.12.1": version: 7.15.3 resolution: "@babel/register@npm:7.15.3" @@ -2276,6 +2549,17 @@ __metadata: languageName: node linkType: hard +"@babel/template@npm:^7.16.0": + version: 7.16.0 + resolution: "@babel/template@npm:7.16.0" + dependencies: + "@babel/code-frame": ^7.16.0 + "@babel/parser": ^7.16.0 + "@babel/types": ^7.16.0 + checksum: 940f105cc6a6aee638cd8cfae80b8b80811e0ddd53b6a11f3a68431ebb998564815fb26511b5d9cb4cff66ea67130ba7498555ee015375d32f5f89ceaa6662ea + languageName: node + linkType: hard + "@babel/traverse@npm:^7.1.0, @babel/traverse@npm:^7.12.1, @babel/traverse@npm:^7.12.11, @babel/traverse@npm:^7.12.9, @babel/traverse@npm:^7.13.0, @babel/traverse@npm:^7.14.5, @babel/traverse@npm:^7.14.8, @babel/traverse@npm:^7.15.0, @babel/traverse@npm:^7.7.0": version: 7.15.0 resolution: "@babel/traverse@npm:7.15.0" @@ -2310,6 +2594,23 @@ __metadata: languageName: node linkType: hard +"@babel/traverse@npm:^7.16.0": + version: 7.16.0 + resolution: "@babel/traverse@npm:7.16.0" + dependencies: + "@babel/code-frame": ^7.16.0 + "@babel/generator": ^7.16.0 + "@babel/helper-function-name": ^7.16.0 + "@babel/helper-hoist-variables": ^7.16.0 + "@babel/helper-split-export-declaration": ^7.16.0 + "@babel/parser": ^7.16.0 + "@babel/types": ^7.16.0 + debug: ^4.1.0 + globals: ^11.1.0 + checksum: 83f634019a705d7ecd5c0f89a7c2cbd292c98a2ecc8a61faeeb48507bf23d81a79c808eb9d50337b48ed51a26929a75601d006cd4e537b1ec090d0ea2502b317 + languageName: node + linkType: hard + "@babel/types@npm:^7.0.0, @babel/types@npm:^7.12.1, @babel/types@npm:^7.12.11, @babel/types@npm:^7.12.6, @babel/types@npm:^7.12.7, @babel/types@npm:^7.14.5, @babel/types@npm:^7.14.8, @babel/types@npm:^7.15.0, @babel/types@npm:^7.3.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.7.0, @babel/types@npm:^7.8.3": version: 7.15.0 resolution: "@babel/types@npm:7.15.0" @@ -2330,6 +2631,16 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.16.0": + version: 7.16.0 + resolution: "@babel/types@npm:7.16.0" + dependencies: + "@babel/helper-validator-identifier": ^7.15.7 + to-fast-properties: ^2.0.0 + checksum: 5b483da5c6e6f2394fba7ee1da8787a0c9cddd33491271c4da702e49e6faf95ce41d7c8bf9a4ee47f2ef06bdb35096f4d0f6ae4b5bea35ebefe16309d22344b7 + languageName: node + linkType: hard + "@base2/pretty-print-object@npm:1.0.0": version: 1.0.0 resolution: "@base2/pretty-print-object@npm:1.0.0" @@ -4418,6 +4729,49 @@ __metadata: languageName: node linkType: hard +"@playwright/test@npm:^1.16.3": + version: 1.16.3 + resolution: "@playwright/test@npm:1.16.3" + dependencies: + "@babel/code-frame": ^7.14.5 + "@babel/core": ^7.14.8 + "@babel/plugin-proposal-class-properties": ^7.14.5 + "@babel/plugin-proposal-dynamic-import": ^7.14.5 + "@babel/plugin-proposal-export-namespace-from": ^7.14.5 + "@babel/plugin-proposal-logical-assignment-operators": ^7.14.5 + "@babel/plugin-proposal-nullish-coalescing-operator": ^7.14.5 + "@babel/plugin-proposal-numeric-separator": ^7.14.5 + "@babel/plugin-proposal-optional-chaining": ^7.14.5 + "@babel/plugin-proposal-private-methods": ^7.14.5 + "@babel/plugin-proposal-private-property-in-object": ^7.14.5 + "@babel/plugin-syntax-async-generators": ^7.8.4 + "@babel/plugin-syntax-json-strings": ^7.8.3 + "@babel/plugin-syntax-object-rest-spread": ^7.8.3 + "@babel/plugin-syntax-optional-catch-binding": ^7.8.3 + "@babel/plugin-transform-modules-commonjs": ^7.14.5 + "@babel/preset-typescript": ^7.14.5 + colors: ^1.4.0 + commander: ^8.2.0 + debug: ^4.1.1 + expect: =27.2.5 + jest-matcher-utils: =27.2.5 + jpeg-js: ^0.4.2 + minimatch: ^3.0.3 + ms: ^2.1.2 + open: ^8.3.0 + pirates: ^4.0.1 + pixelmatch: ^5.2.1 + playwright-core: =1.16.3 + pngjs: ^5.0.0 + rimraf: ^3.0.2 + source-map-support: ^0.4.18 + stack-utils: ^2.0.3 + bin: + playwright: cli.js + checksum: 363f2f214da1aa5ad0e6f5a3affb0a4d33a43bfc5a628e3c58816ef363a0dbae2400e36334142c0d65e1bbde1f622a032e5b55e92e541b1e7e7c82ae8c7d835c + languageName: node + linkType: hard + "@pm2/agent@npm:~2.0.0": version: 2.0.0 resolution: "@pm2/agent@npm:2.0.0" @@ -8546,6 +8900,13 @@ __metadata: languageName: node linkType: hard +"@types/upng-js@npm:^2": + version: 2.1.2 + resolution: "@types/upng-js@npm:2.1.2" + checksum: 1a9c4384e27760226985d7589d7412ec2f85e287c3100b072dfbd59079444e4f54f6d5634e860812d26ea4f90c6827eca1b1caec6b90ae49aeb9b29902434eb9 + languageName: node + linkType: hard + "@types/uuid@npm:^7.0.2": version: 7.0.3 resolution: "@types/uuid@npm:7.0.3" @@ -8660,6 +9021,15 @@ __metadata: languageName: node linkType: hard +"@types/yauzl@npm:^2.9.1": + version: 2.9.2 + resolution: "@types/yauzl@npm:2.9.2" + dependencies: + "@types/node": "*" + checksum: dfb49abe82605615712fc694eaa4f7068fe30aa03f38c085e2c2e74408beaad30471d36da9654a811482ece2ea4405575fd99b19c0aa327ed2a9736b554bbf43 + languageName: node + linkType: hard + "@types/yoga-layout@npm:1.9.1": version: 1.9.1 resolution: "@types/yoga-layout@npm:1.9.1" @@ -9603,7 +9973,7 @@ __metadata: languageName: node linkType: hard -"agent-base@npm:6, agent-base@npm:^6.0.0": +"agent-base@npm:6, agent-base@npm:^6.0.0, agent-base@npm:^6.0.2": version: 6.0.2 resolution: "agent-base@npm:6.0.2" dependencies: @@ -14124,6 +14494,13 @@ __metadata: languageName: node linkType: hard +"colors@npm:^1.4.0": + version: 1.4.0 + resolution: "colors@npm:1.4.0" + checksum: 98aa2c2418ad87dedf25d781be69dc5fc5908e279d9d30c34d8b702e586a0474605b3a189511482b9d5ed0d20c867515d22749537f7bc546256c6014f3ebdcec + languageName: node + linkType: hard + "combine-source-map@npm:^0.8.0, combine-source-map@npm:~0.8.0": version: 0.8.0 resolution: "combine-source-map@npm:0.8.0" @@ -14224,6 +14601,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:^8.2.0": + version: 8.3.0 + resolution: "commander@npm:8.3.0" + checksum: 0f82321821fc27b83bd409510bb9deeebcfa799ff0bf5d102128b500b7af22872c0c92cb6a0ebc5a4cf19c6b550fba9cedfa7329d18c6442a625f851377bacf0 + languageName: node + linkType: hard + "commander@npm:~2.8.1": version: 2.8.1 resolution: "commander@npm:2.8.1" @@ -15813,6 +16197,13 @@ __metadata: languageName: node linkType: hard +"define-lazy-prop@npm:^2.0.0": + version: 2.0.0 + resolution: "define-lazy-prop@npm:2.0.0" + checksum: 0115fdb065e0490918ba271d7339c42453d209d4cb619dfe635870d906731eff3e1ade8028bb461ea27ce8264ec5e22c6980612d332895977e89c1bbc80fcee2 + languageName: node + linkType: hard + "define-properties@npm:^1.1.2, define-properties@npm:^1.1.3": version: 1.1.3 resolution: "define-properties@npm:1.1.3" @@ -18083,6 +18474,20 @@ __metadata: languageName: node linkType: hard +"expect@npm:=27.2.5": + version: 27.2.5 + resolution: "expect@npm:27.2.5" + dependencies: + "@jest/types": ^27.2.5 + ansi-styles: ^5.0.0 + jest-get-type: ^27.0.6 + jest-matcher-utils: ^27.2.5 + jest-message-util: ^27.2.5 + jest-regex-util: ^27.0.6 + checksum: c9be6ec30d19f69c6b838c379e102c156b3ce231e0e3bfc7928eb7a239e5d2a8ed3a43ded4856ad6b3f2f83944561455ad3cf4dfc5322e7d962f2eddc67941c7 + languageName: node + linkType: hard + "expect@npm:^26.6.0, expect@npm:^26.6.2": version: 26.6.2 resolution: "expect@npm:26.6.2" @@ -18249,6 +18654,23 @@ __metadata: languageName: node linkType: hard +"extract-zip@npm:^2.0.1": + version: 2.0.1 + resolution: "extract-zip@npm:2.0.1" + dependencies: + "@types/yauzl": ^2.9.1 + debug: ^4.1.1 + get-stream: ^5.1.0 + yauzl: ^2.10.0 + dependenciesMeta: + "@types/yauzl": + optional: true + bin: + extract-zip: cli.js + checksum: 8cbda9debdd6d6980819cc69734d874ddd71051c9fe5bde1ef307ebcedfe949ba57b004894b585f758b7c9eeeea0e3d87f2dda89b7d25320459c2c9643ebb635 + languageName: node + linkType: hard + "extsprintf@npm:1.3.0": version: 1.3.0 resolution: "extsprintf@npm:1.3.0" @@ -19478,6 +19900,22 @@ fsevents@~2.1.1: languageName: node linkType: hard +"functional-tests@workspace:packages/functional-tests": + version: 0.0.0-use.local + resolution: "functional-tests@workspace:packages/functional-tests" + dependencies: + "@playwright/test": ^1.16.3 + "@types/upng-js": ^2 + fxa-auth-client: "workspace:*" + fxa-content-server: "workspace:*" + fxa-payments-server: "workspace:*" + fxa-settings: "workspace:*" + jsqr: ^1.4.0 + playwright: ^1.16.3 + upng-js: ^2.1.0 + languageName: unknown + linkType: soft + "functions-have-names@npm:^1.2.0": version: 1.2.1 resolution: "functions-have-names@npm:1.2.1" @@ -20321,7 +20759,7 @@ fsevents@~2.1.1: languageName: node linkType: hard -"fxa-payments-server@workspace:packages/fxa-payments-server": +"fxa-payments-server@workspace:*, fxa-payments-server@workspace:packages/fxa-payments-server": version: 0.0.0-use.local resolution: "fxa-payments-server@workspace:packages/fxa-payments-server" dependencies: @@ -24035,6 +24473,15 @@ fsevents@~2.1.1: languageName: node linkType: hard +"is-docker@npm:^2.1.1": + version: 2.2.1 + resolution: "is-docker@npm:2.2.1" + bin: + is-docker: cli.js + checksum: 3fef7ddbf0be25958e8991ad941901bf5922ab2753c46980b60b05c1bf9c9c2402d35e6dc32e4380b980ef5e1970a5d9d5e5aa2e02d77727c3b6b5e918474c56 + languageName: node + linkType: hard + "is-dom@npm:^1.0.0": version: 1.1.0 resolution: "is-dom@npm:1.1.0" @@ -25000,7 +25447,7 @@ fsevents@~2.1.1: languageName: node linkType: hard -"jest-diff@npm:^27.3.1": +"jest-diff@npm:^27.2.5, jest-diff@npm:^27.3.1": version: 27.3.1 resolution: "jest-diff@npm:27.3.1" dependencies: @@ -25121,7 +25568,7 @@ fsevents@~2.1.1: languageName: node linkType: hard -"jest-get-type@npm:^27.3.1": +"jest-get-type@npm:^27.0.6, jest-get-type@npm:^27.3.1": version: 27.3.1 resolution: "jest-get-type@npm:27.3.1" checksum: b0b8db1d770c6332b4189bbf4073184489acbb1095410cf53add033daf911577ee6bd1c4f8d747dd2f3d63de42f7eb15c5527fc7288a2855a046f4a8957cd902 @@ -25249,6 +25696,18 @@ fsevents@~2.1.1: languageName: node linkType: hard +"jest-matcher-utils@npm:=27.2.5": + version: 27.2.5 + resolution: "jest-matcher-utils@npm:27.2.5" + dependencies: + chalk: ^4.0.0 + jest-diff: ^27.2.5 + jest-get-type: ^27.0.6 + pretty-format: ^27.2.5 + checksum: 92f285c8e2a50f2b6761a1d81db98858416b6ccb6559c9ce954ef9cad6b76729ac18b8c1e98e2e81e1a55fca4dc9d8571d5dfbc2161583ed5716119e35b2a089 + languageName: node + linkType: hard + "jest-matcher-utils@npm:^26.6.0, jest-matcher-utils@npm:^26.6.2": version: 26.6.2 resolution: "jest-matcher-utils@npm:26.6.2" @@ -25261,7 +25720,7 @@ fsevents@~2.1.1: languageName: node linkType: hard -"jest-matcher-utils@npm:^27.3.1": +"jest-matcher-utils@npm:^27.2.5, jest-matcher-utils@npm:^27.3.1": version: 27.3.1 resolution: "jest-matcher-utils@npm:27.3.1" dependencies: @@ -25290,7 +25749,7 @@ fsevents@~2.1.1: languageName: node linkType: hard -"jest-message-util@npm:^27.3.1": +"jest-message-util@npm:^27.2.5, jest-message-util@npm:^27.3.1": version: 27.3.1 resolution: "jest-message-util@npm:27.3.1" dependencies: @@ -25875,6 +26334,13 @@ fsevents@~2.1.1: languageName: node linkType: hard +"jpeg-js@npm:^0.4.2": + version: 0.4.3 + resolution: "jpeg-js@npm:0.4.3" + checksum: 9e5bacc9135efa7da340b62e81fa56fab0c8516ef617228758132af5b7d31b516cc6e1500cdffb82d3161629be341be980099f2b37eb76b81e26db6e3e848c77 + languageName: node + linkType: hard + "jquery-modal@git://github.com/mozilla-fxa/jquery-modal.git#0576775d1b4590314b114386019f4c7421c77503": version: 0.7.1 resolution: "jquery-modal@git://github.com/mozilla-fxa/jquery-modal.git#commit=0576775d1b4590314b114386019f4c7421c77503" @@ -26436,7 +26902,7 @@ fsevents@~2.1.1: languageName: node linkType: hard -"jsqr@npm:1.4.0": +"jsqr@npm:1.4.0, jsqr@npm:^1.4.0": version: 1.4.0 resolution: "jsqr@npm:1.4.0" checksum: 7c572971f90c42772e30d152bde63b84edf1164bde80c53942e6b2068ea31caf00ad704aa46cacc9e71645f52dbeddebc6e84ba15e883c678ee93cde690de339 @@ -28407,7 +28873,7 @@ fsevents@~2.1.1: languageName: node linkType: hard -"minimatch@npm:2 || 3, minimatch@npm:3.0.4, minimatch@npm:^3.0.2, minimatch@npm:^3.0.4, minimatch@npm:~3.0.2, minimatch@npm:~3.0.4": +"minimatch@npm:2 || 3, minimatch@npm:3.0.4, minimatch@npm:^3.0.2, minimatch@npm:^3.0.3, minimatch@npm:^3.0.4, minimatch@npm:~3.0.2, minimatch@npm:~3.0.4": version: 3.0.4 resolution: "minimatch@npm:3.0.4" dependencies: @@ -30475,6 +30941,17 @@ fsevents@~2.1.1: languageName: node linkType: hard +"open@npm:^8.3.0": + version: 8.4.0 + resolution: "open@npm:8.4.0" + dependencies: + define-lazy-prop: ^2.0.0 + is-docker: ^2.1.1 + is-wsl: ^2.2.0 + checksum: e9545bec64cdbf30a0c35c1bdc310344adf8428a117f7d8df3c0af0a0a24c513b304916a6d9b11db0190ff7225c2d578885080b761ed46a3d5f6f1eebb98b63c + languageName: node + linkType: hard + "opencollective-postinstall@npm:^2.0.2": version: 2.0.2 resolution: "opencollective-postinstall@npm:2.0.2" @@ -31491,6 +31968,17 @@ fsevents@~2.1.1: languageName: node linkType: hard +"pixelmatch@npm:^5.2.1": + version: 5.2.1 + resolution: "pixelmatch@npm:5.2.1" + dependencies: + pngjs: ^4.0.1 + bin: + pixelmatch: bin/pixelmatch + checksum: 0ec7a87168e51b80812d1c39fe1a278e2266dc1e9c426418c2a9d7f0c6465de3c03c51dbf7e6b97c5ba72a043ec3fb576571cdde1f88b12ef0851bf9bfd16da0 + languageName: node + linkType: hard + "pkg-dir@npm:^2.0.0": version: 2.0.0 resolution: "pkg-dir@npm:2.0.0" @@ -31552,6 +32040,43 @@ fsevents@~2.1.1: languageName: node linkType: hard +"playwright-core@npm:=1.16.3": + version: 1.16.3 + resolution: "playwright-core@npm:1.16.3" + dependencies: + commander: ^8.2.0 + debug: ^4.1.1 + extract-zip: ^2.0.1 + https-proxy-agent: ^5.0.0 + jpeg-js: ^0.4.2 + mime: ^2.4.6 + pngjs: ^5.0.0 + progress: ^2.0.3 + proper-lockfile: ^4.1.1 + proxy-from-env: ^1.1.0 + rimraf: ^3.0.2 + socks-proxy-agent: ^6.1.0 + stack-utils: ^2.0.3 + ws: ^7.4.6 + yauzl: ^2.10.0 + yazl: ^2.5.1 + bin: + playwright: cli.js + checksum: b37e5abadb22096f84515fa9307587747a65c2b465b10b0688ae228aff5537eb5faa88ee9d1cd1225ff9270747b6c9b72a76a008cfb670b8df939b078f3d29b9 + languageName: node + linkType: hard + +"playwright@npm:^1.16.3": + version: 1.16.3 + resolution: "playwright@npm:1.16.3" + dependencies: + playwright-core: =1.16.3 + bin: + playwright: cli.js + checksum: c1fe9255e3023195ca94c7a693b8816f9f13ce5901599806ac161df8da193f2bbb016fa452c8f750602845e7ce517129ea639feae0ea182d0ec77e4cb6fc9030 + languageName: node + linkType: hard + "please-upgrade-node@npm:^3.2.0": version: 3.2.0 resolution: "please-upgrade-node@npm:3.2.0" @@ -31692,6 +32217,20 @@ fsevents@~2.1.1: languageName: node linkType: hard +"pngjs@npm:^4.0.1": + version: 4.0.1 + resolution: "pngjs@npm:4.0.1" + checksum: 9497e08a6c2d850630ba7c8d3738fd36c9db1af7ee8b8c2d4b664e450807a280936dfa1489deb60e6943b968bedd58c9aa93def25a765579d745ea44467fc47f + languageName: node + linkType: hard + +"pngjs@npm:^5.0.0": + version: 5.0.0 + resolution: "pngjs@npm:5.0.0" + checksum: 04e912cc45fb9601564e2284efaf0c5d20d131d9b596244f8a6789fc6cdb6b18d2975a6bbf7a001858d7e159d5c5c5dd7b11592e97629b7137f7f5cef05904c8 + languageName: node + linkType: hard + "pngparse@npm:2.0.1": version: 2.0.1 resolution: "pngparse@npm:2.0.1" @@ -32949,7 +33488,7 @@ fsevents@~2.1.1: languageName: node linkType: hard -"pretty-format@npm:^27.3.1": +"pretty-format@npm:^27.2.5, pretty-format@npm:^27.3.1": version: 27.3.1 resolution: "pretty-format@npm:27.3.1" dependencies: @@ -33028,7 +33567,7 @@ fsevents@~2.1.1: languageName: node linkType: hard -"progress@npm:^2.0.0": +"progress@npm:^2.0.0, progress@npm:^2.0.3": version: 2.0.3 resolution: "progress@npm:2.0.3" checksum: f67403fe7b34912148d9252cb7481266a354bd99ce82c835f79070643bb3c6583d10dbcfda4d41e04bbc1d8437e9af0fb1e1f2135727878f5308682a579429b7 @@ -33122,6 +33661,17 @@ fsevents@~2.1.1: languageName: node linkType: hard +"proper-lockfile@npm:^4.1.1": + version: 4.1.2 + resolution: "proper-lockfile@npm:4.1.2" + dependencies: + graceful-fs: ^4.2.4 + retry: ^0.12.0 + signal-exit: ^3.0.2 + checksum: 00078ee6a61c216a56a6140c7d2a98c6c733b3678503002dc073ab8beca5d50ca271de4c85fca13b9b8ee2ff546c36674d1850509b84a04a5d0363bcb8638939 + languageName: node + linkType: hard + "property-information@npm:^5.0.0, property-information@npm:^5.3.0": version: 5.6.0 resolution: "property-information@npm:5.6.0" @@ -33195,7 +33745,7 @@ fsevents@~2.1.1: languageName: node linkType: hard -"proxy-from-env@npm:^1.0.0": +"proxy-from-env@npm:^1.0.0, proxy-from-env@npm:^1.1.0": version: 1.1.0 resolution: "proxy-from-env@npm:1.1.0" checksum: ed7fcc2ba0a33404958e34d95d18638249a68c430e30fcb6c478497d72739ba64ce9810a24f53a7d921d0c065e5b78e3822759800698167256b04659366ca4d4 @@ -36713,6 +37263,17 @@ resolve@^2.0.0-next.3: languageName: node linkType: hard +"socks-proxy-agent@npm:^6.1.0": + version: 6.1.0 + resolution: "socks-proxy-agent@npm:6.1.0" + dependencies: + agent-base: ^6.0.2 + debug: ^4.3.1 + socks: ^2.6.1 + checksum: 32ea0d62c848b5c246955e8d6c34832fe6cd8c5f3b66f5ace3a9bd7387bafae3e67d96474d41291723ba7135e2da46d65e008a8a35a793dfa5cb0f4ac05429df + languageName: node + linkType: hard + "socks@npm:^2.3.3": version: 2.4.1 resolution: "socks@npm:2.4.1" @@ -36723,6 +37284,16 @@ resolve@^2.0.0-next.3: languageName: node linkType: hard +"socks@npm:^2.6.1": + version: 2.6.1 + resolution: "socks@npm:2.6.1" + dependencies: + ip: ^1.1.5 + smart-buffer: ^4.1.0 + checksum: 2ca9d616e424f645838ebaabb04f85d94ea999e0f8393dc07f86c435af22ed88cb83958feeabd1bb7bc537c635ed47454255635502c6808a6df61af1f41af750 + languageName: node + linkType: hard + "sort-keys@npm:^1.0.0": version: 1.1.2 resolution: "sort-keys@npm:1.1.2" @@ -36798,7 +37369,7 @@ resolve@^2.0.0-next.3: languageName: node linkType: hard -"source-map-support@npm:^0.4.15": +"source-map-support@npm:^0.4.15, source-map-support@npm:^0.4.18": version: 0.4.18 resolution: "source-map-support@npm:0.4.18" dependencies: @@ -39789,7 +40360,7 @@ typescript@^4.3.5: languageName: node linkType: hard -"upng-js@npm:2.1.0": +"upng-js@npm:2.1.0, upng-js@npm:^2.1.0": version: 2.1.0 resolution: "upng-js@npm:2.1.0" dependencies: @@ -41797,7 +42368,7 @@ typescript@^4.3.5: languageName: node linkType: hard -"yauzl@npm:^2.4.2": +"yauzl@npm:^2.10.0, yauzl@npm:^2.4.2": version: 2.10.0 resolution: "yauzl@npm:2.10.0" dependencies: @@ -41807,6 +42378,15 @@ typescript@^4.3.5: languageName: node linkType: hard +"yazl@npm:^2.5.1": + version: 2.5.1 + resolution: "yazl@npm:2.5.1" + dependencies: + buffer-crc32: ~0.2.3 + checksum: daec5154b5485d8621bfea359e905ddca0b2f068430a4aa0a802bf5d67391157a383e0c2767acccbf5964264851da643bc740155a9458e2d8dce55b94c1cc2ed + languageName: node + linkType: hard + "yn@npm:3.1.1": version: 3.1.1 resolution: "yn@npm:3.1.1"