зеркало из https://github.com/mozilla/fxa.git
feat(tests): implement prod smoke tests in playwright
There's an ADR for why Playwright
This commit is contained in:
Родитель
f7b0e7d7c9
Коммит
833326ad59
|
@ -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
|
||||
|
|
|
@ -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' }),
|
||||
]);
|
||||
}
|
||||
}
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 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();
|
||||
});
|
||||
});
|
|
@ -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
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"
|
||||
|
|
Загрузка…
Ссылка в новой задаче