0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-30 20:33:54 -05:00

refactor(test): add flows integration tests

This commit is contained in:
Gao Sun 2023-09-12 18:42:15 +08:00
parent fe39e42271
commit 423e799b5d
No known key found for this signature in database
GPG key ID: 13EBE123E4773688
12 changed files with 443 additions and 114 deletions

View file

@ -14,7 +14,7 @@
"test:only": "NODE_OPTIONS=--experimental-vm-modules jest", "test:only": "NODE_OPTIONS=--experimental-vm-modules jest",
"test": "pnpm build && pnpm test:api && pnpm test:ui", "test": "pnpm build && pnpm test:api && pnpm test:ui",
"test:api": "pnpm test:only -i ./lib/tests/api/", "test:api": "pnpm test:only -i ./lib/tests/api/",
"test:ui": "pnpm test:only -i --config=jest.config.ui.js ./lib/tests/console/ ./lib/tests/flows/", "test:ui": "pnpm test:only -i --config=jest.config.ui.js ./lib/tests/flows/",
"lint": "eslint --ext .ts src", "lint": "eslint --ext .ts src",
"lint:report": "pnpm lint --format json --output-file report.json", "lint:report": "pnpm lint --format json --output-file report.json",
"start": "pnpm test" "start": "pnpm test"
@ -30,12 +30,11 @@
"@silverhand/eslint-config": "4.0.1", "@silverhand/eslint-config": "4.0.1",
"@silverhand/essentials": "^2.8.4", "@silverhand/essentials": "^2.8.4",
"@silverhand/ts-config": "4.0.0", "@silverhand/ts-config": "4.0.0",
"@types/expect-puppeteer": "^5.0.3",
"@types/jest": "^29.4.0", "@types/jest": "^29.4.0",
"@types/jest-environment-puppeteer": "^5.0.3",
"@types/node": "^18.11.18", "@types/node": "^18.11.18",
"dotenv": "^16.0.0", "dotenv": "^16.0.0",
"eslint": "^8.44.0", "eslint": "^8.44.0",
"expect-puppeteer": "^9.0.0",
"got": "^13.0.0", "got": "^13.0.0",
"jest": "^29.5.0", "jest": "^29.5.0",
"jest-puppeteer": "^9.0.0", "jest-puppeteer": "^9.0.0",

View file

@ -1,5 +1,5 @@
import { SignInIdentifier, demoAppApplicationId } from '@logto/schemas'; import { SignInIdentifier, demoAppApplicationId } from '@logto/schemas';
import { getEnv } from '@silverhand/essentials'; import { appendPath, getEnv } from '@silverhand/essentials';
export const logtoUrl = getEnv('INTEGRATION_TESTS_LOGTO_URL', 'http://localhost:3001'); export const logtoUrl = getEnv('INTEGRATION_TESTS_LOGTO_URL', 'http://localhost:3001');
export const logtoConsoleUrl = getEnv( export const logtoConsoleUrl = getEnv(
@ -7,6 +7,7 @@ export const logtoConsoleUrl = getEnv(
'http://localhost:3002' 'http://localhost:3002'
); );
export const logtoCloudUrl = getEnv('INTEGRATION_TESTS_LOGTO_CLOUD_URL', 'http://localhost:3003'); export const logtoCloudUrl = getEnv('INTEGRATION_TESTS_LOGTO_CLOUD_URL', 'http://localhost:3003');
export const demoAppUrl = appendPath(new URL(logtoUrl), 'demo-app');
export const discoveryUrl = `${logtoUrl}/oidc/.well-known/openid-configuration`; export const discoveryUrl = `${logtoUrl}/oidc/.well-known/openid-configuration`;

View file

@ -24,7 +24,7 @@ import {
} from '#src/helpers/sign-in-experience.js'; } from '#src/helpers/sign-in-experience.js';
import { generateNewUserProfile, generateNewUser } from '#src/helpers/user.js'; import { generateNewUserProfile, generateNewUser } from '#src/helpers/user.js';
describe('Register with username and password', () => { describe('register with username and password', () => {
it('register with username and password', async () => { it('register with username and password', async () => {
await enableAllPasswordSignInMethods({ await enableAllPasswordSignInMethods({
identifiers: [SignInIdentifier.Username], identifiers: [SignInIdentifier.Username],
@ -51,7 +51,7 @@ describe('Register with username and password', () => {
}); });
}); });
describe('Register with passwordless identifier', () => { describe('register with passwordless identifier', () => {
beforeAll(async () => { beforeAll(async () => {
await clearConnectorsByTypes([ConnectorType.Email, ConnectorType.Sms]); await clearConnectorsByTypes([ConnectorType.Email, ConnectorType.Sms]);
await setEmailConnector(); await setEmailConnector();

View file

@ -1,11 +1,60 @@
import { SignInMode, SignInIdentifier, ConnectorType } from '@logto/schemas';
import { updateSignInExperience } from '#src/api/sign-in-experience.js';
import { demoAppUrl } from '#src/constants.js';
import { clearConnectorsByTypes } from '#src/helpers/connector.js';
import ExpectJourney from '#src/ui-helpers/expect-journey.js';
const credentials = {
username: 'test_bootstrap',
pwnedPassword: 'test_password',
password: 'test_passWorD_not_PWNED:-)',
};
/** /**
* NOTE: This test suite assumes test cases will run sequentially (which is Jest default). * NOTE: This test suite assumes test cases will run sequentially (which is Jest default).
* Parallel execution will lead to errors. * Parallel execution will lead to errors.
*/ */
// Tip: See https://github.com/argos-ci/jest-puppeteer/blob/main/packages/expect-puppeteer/README.md // Tip: See https://github.com/argos-ci/jest-puppeteer/blob/main/packages/expect-puppeteer/README.md
// for convenient expect methods // for convenient expect methods
describe('smoke testing for the main flow', () => { describe('smoke testing on the demo app', () => {
it('should not explode', async () => { beforeAll(async () => {
expect(true); await clearConnectorsByTypes([ConnectorType.Email, ConnectorType.Sms]);
await updateSignInExperience({
signInMode: SignInMode.SignInAndRegister,
signUp: {
identifiers: [SignInIdentifier.Username],
password: true,
verify: false,
},
signIn: {
methods: [
{
identifier: SignInIdentifier.Username,
password: true,
verificationCode: false,
isPasswordPrimary: true,
},
],
},
passwordPolicy: {},
});
});
it('should be able to create a new account with a credential preset', async () => {
const journey = new ExpectJourney(await browser.newPage());
// Open the demo app and navigate to the register page
await journey.startWith(demoAppUrl, 'register');
await journey.toFillInput('identifier', credentials.username, { submit: true });
// Simple password tests
journey.toBeAt('register/password');
await journey.toFillPasswords(
[credentials.pwnedPassword, 'simple password'],
credentials.password
);
await journey.verifyThenEnd();
}); });
}); });

View file

@ -0,0 +1,141 @@
/* Test the sign-in with different password policies. */
import { ConnectorType, SignInIdentifier, SignInMode } from '@logto/schemas';
import { updateSignInExperience } from '#src/api/sign-in-experience.js';
import { demoAppUrl } from '#src/constants.js';
import { clearConnectorsByTypes, setEmailConnector } from '#src/helpers/connector.js';
import ExpectJourney from '#src/ui-helpers/expect-journey.js';
import { waitFor } from '#src/utils.js';
describe('password policy', () => {
const username = 'test_pass_policy_30';
const emailName = 'a_good_foo_30';
const email = emailName + '@bar.com';
const invalidPasswords: Array<[string, string | RegExp]> = [
['123', 'minimum length'],
['12345678', 'at least 3 types'],
['123456aA', 'simple password'],
['defghiZ@', 'sequential characters'],
['TTTTTT@z', 'repeated characters'],
];
beforeAll(async () => {
await clearConnectorsByTypes([ConnectorType.Email, ConnectorType.Sms]);
await setEmailConnector();
await updateSignInExperience({
signInMode: SignInMode.SignInAndRegister,
signUp: {
identifiers: [SignInIdentifier.Username],
password: true,
verify: false,
},
signIn: {
methods: [
{
identifier: SignInIdentifier.Username,
password: true,
verificationCode: false,
isPasswordPrimary: true,
},
{
identifier: SignInIdentifier.Email,
password: true,
verificationCode: true,
isPasswordPrimary: true,
},
],
},
passwordPolicy: {
length: { min: 8, max: 32 },
characterTypes: { min: 3 },
rejects: {
pwned: true,
repetitionAndSequence: true,
userInfo: true,
words: [username],
},
},
});
});
it('should work for username + password', async () => {
const journey = new ExpectJourney(await browser.newPage(), { forgotPassword: true });
// Open the demo app and navigate to the register page
await journey.startWith(demoAppUrl, 'register');
await journey.toFillInput('identifier', username, { submit: true });
// Password tests
journey.toBeAt('register/password');
await journey.toFillPasswords(
...invalidPasswords,
[username + 'A', /product context .* personal information/],
username + 'ABCD_ok'
);
await journey.verifyThenEnd();
});
it('should work for email + password', async () => {
// Enable email verification and make password primary
await updateSignInExperience({
signUp: {
identifiers: [SignInIdentifier.Email],
password: true,
verify: true,
},
});
const journey = new ExpectJourney(await browser.newPage(), { forgotPassword: true });
// Open the demo app and navigate to the register page
await journey.startWith(demoAppUrl, 'register');
// Complete verification code flow
await journey.toFillInput('identifier', email, { submit: true });
await journey.toCompleteVerification('register');
// Wait for the password page to load
await waitFor(100);
journey.toBeAt('continue/password');
await journey.toFillPasswords(
...invalidPasswords,
[emailName, 'personal information'],
emailName + 'ABCD@# $'
);
await journey.verifyThenEnd();
});
it('should work for forgot password', async () => {
const journey = new ExpectJourney(await browser.newPage(), { forgotPassword: true });
// Open the demo app and navigate to the register page
await journey.startWith(demoAppUrl, 'sign-in');
// Click the forgot password link
await journey.toFillInput('identifier', email, { submit: true });
await journey.toClick('a', 'Forgot your password');
// Submit to continue
await journey.toClickSubmit();
// Complete verification code flow
await journey.toCompleteVerification('forgot-password');
// Wait for the password page to load
await waitFor(100);
journey.toBeAt('forgot-password/reset');
await journey.toFillPasswords(
...invalidPasswords,
[emailName, 'personal information'],
[emailName + 'ABCD@# $', 'be the same as'],
emailName + 'ABCD135'
);
await journey.waitForToast(/password changed/i);
await journey.toFillInput('identifier', email, { submit: true });
await journey.toFillInput('password', emailName + 'ABCD135', { submit: true });
await journey.verifyThenEnd();
});
});

View file

@ -0,0 +1,143 @@
import { appendPath } from '@silverhand/essentials';
import { logtoUrl } from '#src/constants.js';
import { readVerificationCode } from '#src/helpers/index.js';
import ExpectPage from './expect-page.js';
const demoAppUrl = appendPath(new URL(logtoUrl), 'demo-app');
/** Remove the query string together with the `?` from a URL string. */
const stripQuery = (url: string) => url.split('?')[0];
export type JourneyType = 'sign-in' | 'register' | 'continue' | 'forgot-password';
export type JourneyPath =
| JourneyType
| `${JourneyType}/password`
| `${JourneyType}/verify`
| `${JourneyType}/verification-code`
| `forgot-password/reset`;
export type ExpectJourneyOptions = {
/** The URL of the journey endpoint. */
endpoint?: URL;
/**
* Whether the forgot password flow is enabled.
*
* @default false
*/
forgotPassword?: boolean;
};
type OngoingJourney = {
type: JourneyType;
initialUrl: URL;
};
/**
* A class that provides:
*
* - A set of methods to navigate to a specific page for a journey.
* - A set of methods to assert the state of a journey and its side effects.
*/
export default class ExpectJourney extends ExpectPage {
readonly options: Required<ExpectJourneyOptions>;
protected get journeyType() {
if (this.#ongoing === undefined) {
return this.throwNoOngoingJourneyError();
}
return this.#ongoing.type;
}
#ongoing?: OngoingJourney;
constructor(thePage = global.page, options: ExpectJourneyOptions = {}) {
super(thePage);
this.options = {
endpoint: new URL(logtoUrl),
forgotPassword: false,
...options,
};
}
async startWith(initialUrl = demoAppUrl, type: JourneyType = 'sign-in') {
await this.toStart(initialUrl);
this.toBeAt('sign-in');
if (type === 'register') {
await this.toClick('a', 'Create account');
this.toBeAt('register');
}
this.#ongoing = { type, initialUrl };
}
async verifyThenEnd() {
if (this.#ongoing === undefined) {
return this.throwNoOngoingJourneyError();
}
this.toMatchUrl(this.#ongoing.initialUrl);
await this.toClick('div[role=button]', /sign out/i);
this.#ongoing = undefined;
await this.page.close();
}
toBeAt(mode: JourneyPath) {
const stripped = stripQuery(this.page.url());
expect(stripped).toBe(this.buildJourneyUrl(mode).href);
}
async toCompleteVerification(type: JourneyType) {
this.toBeAt(`${type}/verification-code`);
const { code } = await readVerificationCode();
for (const [index, char] of code.split('').entries()) {
// eslint-disable-next-line no-await-in-loop
await this.toFillInput(`passcode_${index}`, char);
}
}
async toFillPasswords(
...passwords: Array<string | [password: string, errorMessage: string | RegExp]>
) {
for (const element of passwords) {
const [password, errorMessage] = Array.isArray(element) ? element : [element, undefined];
// eslint-disable-next-line no-await-in-loop
await this.toFillForm(
this.options.forgotPassword
? { newPassword: password }
: {
newPassword: password,
confirmPassword: password,
},
{ submit: true, shouldNavigate: errorMessage === undefined }
);
if (errorMessage === undefined) {
break;
} else {
// Reject the password and assert the error message
// eslint-disable-next-line no-await-in-loop
await this.toMatchAlert(
typeof errorMessage === 'string' ? new RegExp(errorMessage, 'i') : errorMessage
);
}
}
}
/** Build a full journey URL from a pathname. */
protected buildJourneyUrl(pathname = '') {
return appendPath(this.options.endpoint, pathname);
}
protected throwNoOngoingJourneyError() {
return this.throwError(
'The journey has not started yet. Use `startWith` to start the journey.'
);
}
}

View file

@ -0,0 +1,86 @@
import { type ElementHandle, type Page } from 'puppeteer';
import { expectNavigation } from '#src/utils.js';
class ExpectPageError extends Error {
constructor(
message: string,
public readonly page: Page
) {
super(message);
}
}
/**
* A class that provides a set of methods to assert the state of page tests and its side effects.
*/
export default class ExpectPage {
constructor(public readonly page = global.page) {}
async toStart(initialUrl: URL) {
await expectNavigation(this.page.goto(initialUrl.href), this.page);
await expect(this.page).toMatchElement('#app');
}
async toClick(selector: string, text?: string | RegExp, shouldNavigate = true) {
const clicked = expect(this.page).toClick(selector, { text });
return shouldNavigate ? expectNavigation(clicked, this.page) : clicked;
}
async toClickSubmit(shouldNavigate = true) {
return this.toClick('button[type=submit]', undefined, shouldNavigate);
}
async toSubmit(shouldNavigate = true) {
const form = await expect(this.page).toMatchElement('form');
// eslint-disable-next-line no-restricted-syntax
const submitted = (form as ElementHandle<HTMLFormElement>).evaluate((form) => {
form.submit();
});
return shouldNavigate ? expectNavigation(submitted, this.page) : submitted;
}
async toFillInput(
name: string,
value: string,
options?: { submit: true; shouldNavigate?: boolean }
) {
await expect(this.page).toFill(`input[name=${name}]`, value);
if (options?.submit) {
await this.toClickSubmit(options.shouldNavigate);
}
}
async toFillForm(
values: Record<string, string>,
options?: { submit: true; shouldNavigate?: boolean }
) {
await expect(this.page).toFillForm('form', values);
if (options?.submit) {
await this.toClickSubmit(options.shouldNavigate);
}
}
async toMatchAlert(text?: string | RegExp): Promise<ElementHandle> {
return expect(this.page).toMatchElement('*[role=alert]', { text });
}
toMatchUrl(url: URL | string) {
expect(this.page.url()).toBe(typeof url === 'string' ? url : url.href);
}
async waitForToast(text: string | RegExp) {
const toast = await expect(this.page).toMatchElement(`.ReactModal__Content[class*=toast]`, {
text,
});
// Remove immediately to prevent waiting for the toast to disappear and matching the same toast again
await toast.evaluate((element) => {
element.remove();
});
}
protected throwError(message: string): never {
throw new ExpectPageError(message, this.page);
}
}

View file

@ -2,6 +2,7 @@ import crypto from 'node:crypto';
import path from 'node:path'; import path from 'node:path';
import { assert } from '@silverhand/essentials'; import { assert } from '@silverhand/essentials';
import { type Page } from 'puppeteer';
export const generateName = () => crypto.randomUUID(); export const generateName = () => crypto.randomUUID();
export const generateUserId = () => crypto.randomUUID(); export const generateUserId = () => crypto.randomUUID();
@ -70,7 +71,10 @@ export const appendPathname = (pathname: string, baseUrl: URL) =>
* useful for actions that trigger navigation, such as clicking a link or * useful for actions that trigger navigation, such as clicking a link or
* submitting a form. * submitting a form.
*/ */
export const expectNavigation = async <T>(action: Promise<T>): Promise<T> => { export const expectNavigation = async <T>(
action: Promise<T>,
page: Page = global.page
): Promise<T> => {
const [result] = await Promise.all([ const [result] = await Promise.all([
action, action,
page.waitForNavigation({ waitUntil: 'networkidle0' }), page.waitForNavigation({ waitUntil: 'networkidle0' }),

View file

@ -1,6 +1,7 @@
{ {
"extends": "@silverhand/ts-config/tsconfig.base", "extends": "@silverhand/ts-config/tsconfig.base",
"compilerOptions": { "compilerOptions": {
"rootDir": "src",
"isolatedModules": false, "isolatedModules": false,
"allowJs": true, "allowJs": true,
"outDir": "lib", "outDir": "lib",

View file

@ -3,7 +3,7 @@ import { createRequire } from 'node:module';
const require = createRequire(import.meta.url); const require = createRequire(import.meta.url);
const Sequencer = require('@jest/test-sequencer').default; const Sequencer = require('@jest/test-sequencer').default;
const bootstrapTestSuitePathSuffix = '/tests/ui/bootstrap.test.js'; const bootstrapTestSuitePathSuffix = '/bootstrap.test.js';
class CustomSequencer extends Sequencer { class CustomSequencer extends Sequencer {
sort(tests) { sort(tests) {

View file

@ -37,7 +37,11 @@ const ErrorMessage = ({ error, className, children }: Props) => {
return t(error.code, { ...error.data }); return t(error.code, { ...error.data });
}; };
return <div className={classNames(styles.error, className)}>{getMessage()}</div>; return (
<div role="alert" className={classNames(styles.error, className)}>
{getMessage()}
</div>
);
}; };
export default ErrorMessage; export default ErrorMessage;

View file

@ -3500,15 +3500,9 @@ importers:
'@silverhand/ts-config': '@silverhand/ts-config':
specifier: 4.0.0 specifier: 4.0.0
version: 4.0.0(typescript@5.0.2) version: 4.0.0(typescript@5.0.2)
'@types/expect-puppeteer':
specifier: ^5.0.3
version: 5.0.3
'@types/jest': '@types/jest':
specifier: ^29.4.0 specifier: ^29.4.0
version: 29.4.0 version: 29.4.0
'@types/jest-environment-puppeteer':
specifier: ^5.0.3
version: 5.0.3
'@types/node': '@types/node':
specifier: ^18.11.18 specifier: ^18.11.18
version: 18.11.18 version: 18.11.18
@ -3518,6 +3512,9 @@ importers:
eslint: eslint:
specifier: ^8.44.0 specifier: ^8.44.0
version: 8.44.0 version: 8.44.0
expect-puppeteer:
specifier: ^9.0.0
version: 9.0.0
got: got:
specifier: ^13.0.0 specifier: ^13.0.0
version: 13.0.0 version: 13.0.0
@ -6937,16 +6934,6 @@ packages:
'@jest/types': 27.5.1 '@jest/types': 27.5.1
dev: true dev: true
/@jest/environment@27.5.1:
resolution: {integrity: sha512-/WQjhPJe3/ghaol/4Bq480JKXV/Rfw8nQdN7f41fM8VDHLcxKXou6QyXAh3EFr9/bVG3x74z1NWDkP87EiY8gA==}
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
dependencies:
'@jest/fake-timers': 27.5.1
'@jest/types': 27.5.1
'@types/node': 18.11.18
jest-mock: 27.5.1
dev: true
/@jest/environment@29.5.0: /@jest/environment@29.5.0:
resolution: {integrity: sha512-5FXw2+wD29YU1d4I2htpRX7jYnAyTRjP2CsXQdo9SAM8g3ifxWPSV0HnClSn71xwctr0U3oZIIH+dtbfmnbXVQ==} resolution: {integrity: sha512-5FXw2+wD29YU1d4I2htpRX7jYnAyTRjP2CsXQdo9SAM8g3ifxWPSV0HnClSn71xwctr0U3oZIIH+dtbfmnbXVQ==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@ -6974,18 +6961,6 @@ packages:
- supports-color - supports-color
dev: true dev: true
/@jest/fake-timers@27.5.1:
resolution: {integrity: sha512-/aPowoolwa07k7/oM3aASneNeBGCmGQsc3ugN4u6s4C/+s5M64MFo/+djTdiwcbQlRfFElGuDXWzaWj6QgKObQ==}
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
dependencies:
'@jest/types': 27.5.1
'@sinonjs/fake-timers': 8.1.0
'@types/node': 18.11.18
jest-message-util: 27.5.1
jest-mock: 27.5.1
jest-util: 27.5.1
dev: true
/@jest/fake-timers@29.5.0: /@jest/fake-timers@29.5.0:
resolution: {integrity: sha512-9ARvuAAQcBwDAqOnglWq2zwNIRUDtk/SCkp/ToGEhFv5r86K21l+VEs0qNTaXtyiY0lEePl3kylijSYJQqdbDg==} resolution: {integrity: sha512-9ARvuAAQcBwDAqOnglWq2zwNIRUDtk/SCkp/ToGEhFv5r86K21l+VEs0qNTaXtyiY0lEePl3kylijSYJQqdbDg==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@ -9017,12 +8992,6 @@ packages:
'@sinonjs/commons': 1.8.3 '@sinonjs/commons': 1.8.3
dev: true dev: true
/@sinonjs/fake-timers@8.1.0:
resolution: {integrity: sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==}
dependencies:
'@sinonjs/commons': 1.8.3
dev: true
/@sinonjs/fake-timers@9.1.2: /@sinonjs/fake-timers@9.1.2:
resolution: {integrity: sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==} resolution: {integrity: sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==}
dependencies: dependencies:
@ -9471,13 +9440,6 @@ packages:
'@types/node': 18.11.18 '@types/node': 18.11.18
dev: true dev: true
/@types/expect-puppeteer@5.0.3:
resolution: {integrity: sha512-NIqATm95VmFbc2s9v1L3yj9ZS9/rCrtptSgBsvW8mcw2KFpLFQqXPyEbo0Vju1eiBieI38jRGWgpbVuUKfQVoQ==}
dependencies:
'@types/jest': 29.4.0
'@types/puppeteer': 5.4.6
dev: true
/@types/express-serve-static-core@4.17.26: /@types/express-serve-static-core@4.17.26:
resolution: {integrity: sha512-zeu3tpouA043RHxW0gzRxwCHchMgftE8GArRsvYT0ByDMbn19olQHx5jLue0LxWY6iYtXb7rXmuVtSkhy9YZvQ==} resolution: {integrity: sha512-zeu3tpouA043RHxW0gzRxwCHchMgftE8GArRsvYT0ByDMbn19olQHx5jLue0LxWY6iYtXb7rXmuVtSkhy9YZvQ==}
dependencies: dependencies:
@ -9561,14 +9523,6 @@ packages:
'@types/istanbul-lib-report': 3.0.0 '@types/istanbul-lib-report': 3.0.0
dev: true dev: true
/@types/jest-environment-puppeteer@5.0.3:
resolution: {integrity: sha512-vWGfeb+0TOPZy7+VscKURWzE5lzYjclSWLxtjVpDAYcjUv8arAS1av06xK3mpgeNCDVx7XvavD8Elq1a4w9wIA==}
dependencies:
'@jest/types': 27.5.1
'@types/puppeteer': 5.4.6
jest-environment-node: 27.5.1
dev: true
/@types/jest@29.4.0: /@types/jest@29.4.0:
resolution: {integrity: sha512-VaywcGQ9tPorCX/Jkkni7RWGFfI11whqzs8dvxF41P17Z+z872thvEvlIbznjPJ02kl1HMX3LmLOonsj2n7HeQ==} resolution: {integrity: sha512-VaywcGQ9tPorCX/Jkkni7RWGFfI11whqzs8dvxF41P17Z+z872thvEvlIbznjPJ02kl1HMX3LmLOonsj2n7HeQ==}
dependencies: dependencies:
@ -9735,12 +9689,6 @@ packages:
resolution: {integrity: sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==} resolution: {integrity: sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==}
dev: true dev: true
/@types/puppeteer@5.4.6:
resolution: {integrity: sha512-98Kghehs7+/GD9b56qryhqdqVCXUTbetTv3PlvDnmFRTHQH0j9DIp1f7rkAW3BAj4U3yoeSEQnKgdW8bDq0Y0Q==}
dependencies:
'@types/node': 18.11.18
dev: true
/@types/qs@6.9.7: /@types/qs@6.9.7:
resolution: {integrity: sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==} resolution: {integrity: sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==}
dev: true dev: true
@ -14410,18 +14358,6 @@ packages:
- utf-8-validate - utf-8-validate
dev: true dev: true
/jest-environment-node@27.5.1:
resolution: {integrity: sha512-Jt4ZUnxdOsTGwSRAfKEnE6BcwsSPNOijjwifq5sDFSA2kesnXTvNqKHYgM0hDq3549Uf/KzdXNYn4wMZJPlFLw==}
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
dependencies:
'@jest/environment': 27.5.1
'@jest/fake-timers': 27.5.1
'@jest/types': 27.5.1
'@types/node': 18.11.18
jest-mock: 27.5.1
jest-util: 27.5.1
dev: true
/jest-environment-node@29.5.0: /jest-environment-node@29.5.0:
resolution: {integrity: sha512-ExxuIK/+yQ+6PRGaHkKewYtg6hto2uGCgvKdb2nfJfKXgZ17DfXjvbZ+jA1Qt9A8EQSfPnt5FKIfnOO3u1h9qw==} resolution: {integrity: sha512-ExxuIK/+yQ+6PRGaHkKewYtg6hto2uGCgvKdb2nfJfKXgZ17DfXjvbZ+jA1Qt9A8EQSfPnt5FKIfnOO3u1h9qw==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@ -14494,21 +14430,6 @@ packages:
pretty-format: 29.5.0 pretty-format: 29.5.0
dev: true dev: true
/jest-message-util@27.5.1:
resolution: {integrity: sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==}
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
dependencies:
'@babel/code-frame': 7.22.5
'@jest/types': 27.5.1
'@types/stack-utils': 2.0.1
chalk: 4.1.2
graceful-fs: 4.2.11
micromatch: 4.0.5
pretty-format: 27.5.1
slash: 3.0.0
stack-utils: 2.0.5
dev: true
/jest-message-util@29.5.0: /jest-message-util@29.5.0:
resolution: {integrity: sha512-Kijeg9Dag6CKtIDA7O21zNTACqD5MD/8HfIV8pdD94vFyFuer52SigdC3IQMhab3vACxXMiFk+yMHNdbqtyTGA==} resolution: {integrity: sha512-Kijeg9Dag6CKtIDA7O21zNTACqD5MD/8HfIV8pdD94vFyFuer52SigdC3IQMhab3vACxXMiFk+yMHNdbqtyTGA==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@ -14524,14 +14445,6 @@ packages:
stack-utils: 2.0.5 stack-utils: 2.0.5
dev: true dev: true
/jest-mock@27.5.1:
resolution: {integrity: sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==}
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
dependencies:
'@jest/types': 27.5.1
'@types/node': 18.11.18
dev: true
/jest-mock@29.5.0: /jest-mock@29.5.0:
resolution: {integrity: sha512-GqOzvdWDE4fAV2bWQLQCkujxYWL7RxjCnj71b5VhDAGOevB3qj3Ovg26A5NI84ZpODxyzaozXLOh2NCgkbvyaw==} resolution: {integrity: sha512-GqOzvdWDE4fAV2bWQLQCkujxYWL7RxjCnj71b5VhDAGOevB3qj3Ovg26A5NI84ZpODxyzaozXLOh2NCgkbvyaw==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@ -14701,18 +14614,6 @@ packages:
react: 18.2.0 react: 18.2.0
dev: true dev: true
/jest-util@27.5.1:
resolution: {integrity: sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==}
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
dependencies:
'@jest/types': 27.5.1
'@types/node': 18.11.18
chalk: 4.1.2
ci-info: 3.8.0
graceful-fs: 4.2.11
picomatch: 2.3.1
dev: true
/jest-util@29.5.0: /jest-util@29.5.0:
resolution: {integrity: sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==} resolution: {integrity: sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}