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:
parent
fe39e42271
commit
423e799b5d
12 changed files with 443 additions and 114 deletions
|
@ -14,7 +14,7 @@
|
|||
"test:only": "NODE_OPTIONS=--experimental-vm-modules jest",
|
||||
"test": "pnpm build && pnpm test:api && pnpm test:ui",
|
||||
"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:report": "pnpm lint --format json --output-file report.json",
|
||||
"start": "pnpm test"
|
||||
|
@ -30,12 +30,11 @@
|
|||
"@silverhand/eslint-config": "4.0.1",
|
||||
"@silverhand/essentials": "^2.8.4",
|
||||
"@silverhand/ts-config": "4.0.0",
|
||||
"@types/expect-puppeteer": "^5.0.3",
|
||||
"@types/jest": "^29.4.0",
|
||||
"@types/jest-environment-puppeteer": "^5.0.3",
|
||||
"@types/node": "^18.11.18",
|
||||
"dotenv": "^16.0.0",
|
||||
"eslint": "^8.44.0",
|
||||
"expect-puppeteer": "^9.0.0",
|
||||
"got": "^13.0.0",
|
||||
"jest": "^29.5.0",
|
||||
"jest-puppeteer": "^9.0.0",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 logtoConsoleUrl = getEnv(
|
||||
|
@ -7,6 +7,7 @@ export const logtoConsoleUrl = getEnv(
|
|||
'http://localhost:3002'
|
||||
);
|
||||
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`;
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ import {
|
|||
} from '#src/helpers/sign-in-experience.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 () => {
|
||||
await enableAllPasswordSignInMethods({
|
||||
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 () => {
|
||||
await clearConnectorsByTypes([ConnectorType.Email, ConnectorType.Sms]);
|
||||
await setEmailConnector();
|
||||
|
|
|
@ -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).
|
||||
* Parallel execution will lead to errors.
|
||||
*/
|
||||
// Tip: See https://github.com/argos-ci/jest-puppeteer/blob/main/packages/expect-puppeteer/README.md
|
||||
// for convenient expect methods
|
||||
describe('smoke testing for the main flow', () => {
|
||||
it('should not explode', async () => {
|
||||
expect(true);
|
||||
describe('smoke testing on the demo app', () => {
|
||||
beforeAll(async () => {
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
143
packages/integration-tests/src/ui-helpers/expect-journey.ts
Normal file
143
packages/integration-tests/src/ui-helpers/expect-journey.ts
Normal 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.'
|
||||
);
|
||||
}
|
||||
}
|
86
packages/integration-tests/src/ui-helpers/expect-page.ts
Normal file
86
packages/integration-tests/src/ui-helpers/expect-page.ts
Normal 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);
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ import crypto from 'node:crypto';
|
|||
import path from 'node:path';
|
||||
|
||||
import { assert } from '@silverhand/essentials';
|
||||
import { type Page } from 'puppeteer';
|
||||
|
||||
export const generateName = () => 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
|
||||
* 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([
|
||||
action,
|
||||
page.waitForNavigation({ waitUntil: 'networkidle0' }),
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"extends": "@silverhand/ts-config/tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"isolatedModules": false,
|
||||
"allowJs": true,
|
||||
"outDir": "lib",
|
||||
|
|
|
@ -3,7 +3,7 @@ import { createRequire } from 'node:module';
|
|||
const require = createRequire(import.meta.url);
|
||||
const Sequencer = require('@jest/test-sequencer').default;
|
||||
|
||||
const bootstrapTestSuitePathSuffix = '/tests/ui/bootstrap.test.js';
|
||||
const bootstrapTestSuitePathSuffix = '/bootstrap.test.js';
|
||||
|
||||
class CustomSequencer extends Sequencer {
|
||||
sort(tests) {
|
||||
|
|
|
@ -37,7 +37,11 @@ const ErrorMessage = ({ error, className, children }: Props) => {
|
|||
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;
|
||||
|
|
105
pnpm-lock.yaml
105
pnpm-lock.yaml
|
@ -3500,15 +3500,9 @@ importers:
|
|||
'@silverhand/ts-config':
|
||||
specifier: 4.0.0
|
||||
version: 4.0.0(typescript@5.0.2)
|
||||
'@types/expect-puppeteer':
|
||||
specifier: ^5.0.3
|
||||
version: 5.0.3
|
||||
'@types/jest':
|
||||
specifier: ^29.4.0
|
||||
version: 29.4.0
|
||||
'@types/jest-environment-puppeteer':
|
||||
specifier: ^5.0.3
|
||||
version: 5.0.3
|
||||
'@types/node':
|
||||
specifier: ^18.11.18
|
||||
version: 18.11.18
|
||||
|
@ -3518,6 +3512,9 @@ importers:
|
|||
eslint:
|
||||
specifier: ^8.44.0
|
||||
version: 8.44.0
|
||||
expect-puppeteer:
|
||||
specifier: ^9.0.0
|
||||
version: 9.0.0
|
||||
got:
|
||||
specifier: ^13.0.0
|
||||
version: 13.0.0
|
||||
|
@ -6937,16 +6934,6 @@ packages:
|
|||
'@jest/types': 27.5.1
|
||||
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:
|
||||
resolution: {integrity: sha512-5FXw2+wD29YU1d4I2htpRX7jYnAyTRjP2CsXQdo9SAM8g3ifxWPSV0HnClSn71xwctr0U3oZIIH+dtbfmnbXVQ==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
|
@ -6974,18 +6961,6 @@ packages:
|
|||
- supports-color
|
||||
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:
|
||||
resolution: {integrity: sha512-9ARvuAAQcBwDAqOnglWq2zwNIRUDtk/SCkp/ToGEhFv5r86K21l+VEs0qNTaXtyiY0lEePl3kylijSYJQqdbDg==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
|
@ -9017,12 +8992,6 @@ packages:
|
|||
'@sinonjs/commons': 1.8.3
|
||||
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:
|
||||
resolution: {integrity: sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==}
|
||||
dependencies:
|
||||
|
@ -9471,13 +9440,6 @@ packages:
|
|||
'@types/node': 18.11.18
|
||||
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:
|
||||
resolution: {integrity: sha512-zeu3tpouA043RHxW0gzRxwCHchMgftE8GArRsvYT0ByDMbn19olQHx5jLue0LxWY6iYtXb7rXmuVtSkhy9YZvQ==}
|
||||
dependencies:
|
||||
|
@ -9561,14 +9523,6 @@ packages:
|
|||
'@types/istanbul-lib-report': 3.0.0
|
||||
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:
|
||||
resolution: {integrity: sha512-VaywcGQ9tPorCX/Jkkni7RWGFfI11whqzs8dvxF41P17Z+z872thvEvlIbznjPJ02kl1HMX3LmLOonsj2n7HeQ==}
|
||||
dependencies:
|
||||
|
@ -9735,12 +9689,6 @@ packages:
|
|||
resolution: {integrity: sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==}
|
||||
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:
|
||||
resolution: {integrity: sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==}
|
||||
dev: true
|
||||
|
@ -14410,18 +14358,6 @@ packages:
|
|||
- utf-8-validate
|
||||
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:
|
||||
resolution: {integrity: sha512-ExxuIK/+yQ+6PRGaHkKewYtg6hto2uGCgvKdb2nfJfKXgZ17DfXjvbZ+jA1Qt9A8EQSfPnt5FKIfnOO3u1h9qw==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
|
@ -14494,21 +14430,6 @@ packages:
|
|||
pretty-format: 29.5.0
|
||||
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:
|
||||
resolution: {integrity: sha512-Kijeg9Dag6CKtIDA7O21zNTACqD5MD/8HfIV8pdD94vFyFuer52SigdC3IQMhab3vACxXMiFk+yMHNdbqtyTGA==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
|
@ -14524,14 +14445,6 @@ packages:
|
|||
stack-utils: 2.0.5
|
||||
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:
|
||||
resolution: {integrity: sha512-GqOzvdWDE4fAV2bWQLQCkujxYWL7RxjCnj71b5VhDAGOevB3qj3Ovg26A5NI84ZpODxyzaozXLOh2NCgkbvyaw==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
|
@ -14701,18 +14614,6 @@ packages:
|
|||
react: 18.2.0
|
||||
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:
|
||||
resolution: {integrity: sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
|
|
Loading…
Reference in a new issue