feat(tests): implement prod smoke tests in playwright

There's an ADR for why Playwright
This commit is contained in:
Danny Coates 2021-11-04 13:31:21 -07:00
Родитель f7b0e7d7c9
Коммит 833326ad59
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4C442633C62E00CB
54 изменённых файлов: 2818 добавлений и 50 удалений

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

@ -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

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

@ -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

6
.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",
}
]

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

@ -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

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

@ -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).

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

@ -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)}`);
}
}

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

@ -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`

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

@ -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<typeof createPages>;
export type TestOptions = {
pages: POMS;
credentials: Credentials;
};
export type WorkerOptions = { targetName: TargetName; target: ServerTarget };
export const test = base.extend<TestOptions, WorkerOptions>({
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);
}

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

@ -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.

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

@ -0,0 +1,27 @@
import AuthClient from 'fxa-auth-client';
import { EmailClient } from '../email';
type Resolved<T> = T extends PromiseLike<infer U> ? U : T;
export type Credentials = Resolved<ReturnType<AuthClient['signUp']>> & {
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<Credentials>;
}

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

@ -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 : {}),
};
}

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

@ -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';

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

@ -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;
}
}

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

@ -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');
}
}

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

@ -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<Credentials> {
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,
};
}
}

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

@ -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');
}
}

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

@ -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"
}
}

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

@ -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

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

@ -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),
};
}

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

@ -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 });
}
}

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

@ -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);
}
}

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

@ -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()
)
),
]);
}
}

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

@ -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' }),
]);
}
}

Двоичные данные
packages/functional-tests/pages/settings/avatar.png Normal file

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

После

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

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

@ -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]');
}
}

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

@ -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(),
]);
}
}

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

@ -0,0 +1,30 @@
import { ElementHandle, Page } from '@playwright/test';
export class ConnectedService {
name: string;
constructor(
readonly element: ElementHandle<HTMLElement | SVGElement>,
readonly page: Page
) {}
static async create(
element: ElementHandle<HTMLElement | SVGElement>,
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();
}
}

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

@ -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<string> {
// 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;
}
}

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

@ -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))
);
}
}

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

@ -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(),
]);
}
}

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

@ -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(),
]);
}
}

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

@ -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<string, UnitRow>();
private lazyRow<T extends UnitRow>(
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' }),
]);
}
}

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

@ -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' }),
]);
}
}

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

@ -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(),
]);
}
}

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

@ -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;
}
}

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

@ -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<string[]> {
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,
};
}
}

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

@ -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<TestOptions, WorkerOptions> = {
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;

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

@ -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

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

@ -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);
});
});

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

@ -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();
});
});

1
packages/functional-tests/types.d.ts поставляемый Normal file
Просмотреть файл

@ -0,0 +1 @@
type hexstring = string;

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

@ -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 = {

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

@ -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

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

@ -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';

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

@ -1,5 +1,9 @@
#!/bin/bash -ex
DIR=$(dirname "$0")
cd "$DIR/.."
yarn build
SKIP_PREFLIGHT_CHECK=true yarn test

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

@ -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);
});

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

@ -91,14 +91,14 @@ describe('UnitRow', () => {
<UnitRow
header="Display name"
headerValue={null}
noHeaderValueText="Not set"
noHeaderValueText="Not Set"
ctaText="Create"
route="/display_name"
/>
);
expect(screen.getByTestId('unit-row-header-value').textContent).toContain(
'Not set'
'Not Set'
);
expect(screen.getByTestId('unit-row-route').textContent).toContain(
'Create'

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

@ -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.

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

@ -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'));
});

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

@ -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}

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

@ -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

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

@ -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'));
});

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

@ -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,

608
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"