1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01:00

test: add interactive demo guide e2e test (#3656)

This PR revamps e2e tests, while adding a new one for the interactive
demo guide:
 - Bumps Cypress from `9.7.0` to `12.11.0`;
 - Bumps Cypress GH action from `v2` to `v5`;
 - Makes any adjustments needed;
 - Fixes a lot of issues identified with existing tests;
- Adds new `demo.spec.ts` e2e test that covers the entire demo guide
flow;

**Note:** Currently does not include `demo.spec.ts` in the GH action, as
it
[fails](https://github.com/Unleash/unleash/actions/runs/4896839575/jobs/8744137231?pr=3656)
on step 2.13 (last step of "user-specific" topic). It runs perfectly
fine locally, though.

Might be placebo, but in general tests seem less flaky now and they may
even be faster (especially when not adding the `demo` one, which would
always take a long time).
This commit is contained in:
Nuno Góis 2023-05-08 09:16:18 +01:00 committed by GitHub
parent dc6a158794
commit edefa6fc7e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 288 additions and 92 deletions

View File

@ -22,7 +22,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v3
- name: Run Cypress
uses: cypress-io/github-action@v2
uses: cypress-io/github-action@v5
with:
working-directory: frontend
env: AUTH_USER=admin,AUTH_PASSWORD=unleash4all

View File

@ -0,0 +1,28 @@
import path from 'path';
import { defineConfig } from 'cypress';
import vitePreprocessor from 'cypress-vite';
export default defineConfig({
projectId: 'tc2qff',
defaultCommandTimeout: 12000,
screenshotOnRunFailure: false,
video: false,
e2e: {
specPattern: '**/*.spec.ts',
setupNodeEvents(on, config) {
on(
'file:preprocessor',
vitePreprocessor({
configFile: path.resolve(__dirname, './vite.config.ts'),
mode: 'development',
})
);
on('task', {
log(message) {
console.log(message);
return null;
},
});
},
},
});

View File

@ -1,7 +0,0 @@
{
"projectId": "tc2qff",
"defaultCommandTimeout": 12000,
"screenshotOnRunFailure": false,
"video": false,
"experimentalSessionAndOrigin": true
}

View File

@ -12,6 +12,12 @@ declare namespace Cypress {
email: string;
password: string;
}
interface IEnvironment {
name: string;
type: 'development' | 'preproduction' | 'test' | 'production';
}
interface Chainable {
runBefore(): Chainable;
@ -48,19 +54,14 @@ declare namespace Cypress {
// STRATEGY
addUserIdStrategyToFeature_UI(
featureName: string,
strategyId: string,
projectName?: string
): Chainable;
addFlexibleRolloutStrategyToFeature_UI(
options: AddFlexibleRolloutStrategyOptions
): Chainable;
updateFlexibleRolloutStrategy_UI(
featureToggleName: string,
strategyId: string
);
updateFlexibleRolloutStrategy_UI(featureToggleName: string);
deleteFeatureStrategy_UI(
featureName: string,
strategyId: string,
shouldWait?: boolean,
projectName?: string
): Chainable;
@ -73,9 +74,20 @@ declare namespace Cypress {
role: number,
projectName?: string
): Chainable;
createProject_API(name: string): Chainable;
createProject_API(
name: string,
options?: Partial<Cypress.RequestOptions>
): Chainable;
deleteProject_API(name: string): Chainable;
createFeature_API(name: string, projectName?: string): Chainable;
createFeature_API(
name: string,
projectName?: string,
options?: Partial<Cypress.RequestOptions>
): Chainable;
deleteFeature_API(name: string): Chainable;
createEnvironment_API(
environment: IEnvironment,
options?: Partial<Cypress.RequestOptions>
): Chainable;
}
}

View File

@ -0,0 +1,130 @@
///<reference path="../../global.d.ts" />
import { TOPICS } from '../../../src/component/demo/demo-topics';
describe('demo', () => {
const baseUrl = Cypress.config().baseUrl;
const randomId = String(Math.random()).split('.')[1];
before(() => {
cy.runBefore();
cy.login_UI();
const optionsIgnore409 = { failOnStatusCode: false };
cy.createEnvironment_API(
{
name: 'dev',
type: 'development',
},
optionsIgnore409
);
cy.createProject_API('demo-app', optionsIgnore409);
cy.createFeature_API('demoApp.step1', 'demo-app', optionsIgnore409);
cy.createFeature_API('demoApp.step2', 'demo-app', optionsIgnore409);
cy.createFeature_API('demoApp.step3', 'demo-app', optionsIgnore409);
cy.createFeature_API('demoApp.step4', 'demo-app', optionsIgnore409);
});
beforeEach(() => {
cy.login_UI();
cy.visit('/projects');
if (document.querySelector("[data-testid='CLOSE_SPLASH']")) {
cy.get("[data-testid='CLOSE_SPLASH']").click();
}
cy.intercept('GET', `${baseUrl}/api/admin/ui-config`, req => {
req.headers['cache-control'] =
'no-cache, no-store, must-revalidate';
req.on('response', res => {
if (res.body) {
res.body.flags = {
...res.body.flags,
demo: true,
};
}
});
});
});
afterEach(() => {
cy.intercept('GET', `${baseUrl}/api/admin/ui-config`).as('uiConfig');
});
after(() => {
cy.request({
method: 'DELETE',
url: `${baseUrl}/api/admin/projects/demo-app/features/demoApp.step1`,
});
cy.request({
method: 'DELETE',
url: `${baseUrl}/api/admin/projects/demo-app/features/demoApp.step2`,
});
cy.request({
method: 'DELETE',
url: `${baseUrl}/api/admin/projects/demo-app/features/demoApp.step3`,
});
cy.request({
method: 'DELETE',
url: `${baseUrl}/api/admin/projects/demo-app/features/demoApp.step4`,
});
cy.request({
method: 'POST',
url: `${baseUrl}/api/admin/projects/demo-app/delete`,
body: {
features: [
'demoApp.step1',
'demoApp.step2',
'demoApp.step3',
'demoApp.step4',
],
},
});
});
it('can complete the demo', () => {
cy.get('[data-testid="DEMO_START_BUTTON"]').click();
for (let topic = 0; topic < TOPICS.length; topic++) {
const currentTopic = TOPICS[topic];
for (let step = 0; step < currentTopic.steps.length; step++) {
const currentStep = currentTopic.steps[step];
cy.task(
'log',
`Testing topic #${topic + 1} "${
currentTopic.title
}", step #${step + 1}...`
);
if (!currentStep.optional) {
cy.wait(2000);
if (currentStep.nextButton) {
if (currentStep.focus) {
if (currentStep.focus === true) {
cy.get(currentStep.target as string)
.first()
.type(randomId, { force: true });
} else {
cy.get(currentStep.target as string)
.first()
.find(currentStep.focus)
.first()
.type(randomId, { force: true });
}
}
cy.get('[data-testid="DEMO_NEXT_BUTTON"]').click({
force: true,
});
} else {
cy.get(currentStep.target as string)
.first()
.click({
force: true,
});
}
}
}
}
});
});

View File

@ -6,7 +6,6 @@ describe('feature', () => {
const variant1 = 'variant1';
const variant2 = 'variant2';
let strategyId = '';
before(() => {
cy.runBefore();
@ -40,26 +39,20 @@ describe('feature', () => {
);
});
it('can add, update and delete a gradual rollout strategy to the development environment', async () => {
it('can add, update and delete a gradual rollout strategy to the development environment', () => {
cy.addFlexibleRolloutStrategyToFeature_UI({
featureToggleName,
}).then(value => {
strategyId = value;
cy.updateFlexibleRolloutStrategy_UI(
featureToggleName,
strategyId
).then(() =>
cy.deleteFeatureStrategy_UI(featureToggleName, strategyId)
}).then(() => {
cy.updateFlexibleRolloutStrategy_UI(featureToggleName).then(() =>
cy.deleteFeatureStrategy_UI(featureToggleName)
);
});
});
it('can add a userId strategy to the development environment', () => {
cy.addUserIdStrategyToFeature_UI(featureToggleName, strategyId).then(
value => {
cy.deleteFeatureStrategy_UI(featureToggleName, value, false);
}
);
cy.addUserIdStrategyToFeature_UI(featureToggleName).then(() => {
cy.deleteFeatureStrategy_UI(featureToggleName, false);
});
});
it('can add variants to the development environment', () => {

View File

@ -34,7 +34,7 @@ describe('imports', () => {
it('can import data', () => {
cy.visit('/projects/default');
cy.get("[data-testid='IMPORT_BUTTON']").click();
cy.get("[data-testid='IMPORT_BUTTON']").click({ force: true });
const exportText = {
features: [
@ -114,6 +114,9 @@ describe('imports', () => {
// cy.contains('Import completed');
cy.visit(`/projects/default/features/${randomFeatureName}`);
cy.wait(500);
cy.get(
"[data-testid='feature-toggle-status'] input[type='checkbox']:checked"
)

View File

@ -1,22 +0,0 @@
/// <reference types="cypress" />
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
/**
* @type {Cypress.PluginConfig}
*/
// eslint-disable-next-line no-unused-vars
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
}

View File

@ -6,11 +6,12 @@ const password = Cypress.env(`AUTH_PASSWORD`) + '_A';
const PROJECT_MEMBER = 5;
export const createFeature_API = (
featureName: string,
projectName?: string
projectName?: string,
options?: Partial<Cypress.RequestOptions>
): Chainable<any> => {
const project = projectName || 'default';
return cy.request({
url: `/api/admin/projects/${project}/features`,
url: `${baseUrl}/api/admin/projects/${project}/features`,
method: 'POST',
body: {
name: `${featureName}`,
@ -18,6 +19,7 @@ export const createFeature_API = (
type: 'release',
impressionData: false,
},
...options,
});
};
@ -32,9 +34,12 @@ export const deleteFeature_API = (name: string): Chainable<any> => {
});
};
export const createProject_API = (project: string): Chainable<any> => {
export const createProject_API = (
project: string,
options?: Partial<Cypress.RequestOptions>
): Chainable<any> => {
return cy.request({
url: `/api/admin/projects`,
url: `${baseUrl}/api/admin/projects`,
method: 'POST',
body: {
id: project,
@ -42,6 +47,7 @@ export const createProject_API = (project: string): Chainable<any> => {
description: project,
impressionData: false,
},
...options,
});
};
@ -101,3 +107,20 @@ export const addUserToProject_API = (
}
);
};
interface IEnvironment {
name: string;
type: 'development' | 'preproduction' | 'test' | 'production';
}
export const createEnvironment_API = (
environment: IEnvironment,
options?: Partial<Cypress.RequestOptions>
): Chainable<any> => {
return cy.request({
url: `${baseUrl}/api/admin/environments`,
method: 'POST',
body: environment,
...options,
});
};

View File

@ -5,6 +5,9 @@ import AddStrategyOptions = Cypress.AddFlexibleRolloutStrategyOptions;
const AUTH_USER = Cypress.env('AUTH_USER');
const AUTH_PASSWORD = Cypress.env('AUTH_PASSWORD');
const ENTERPRISE = Boolean(Cypress.env('ENTERPRISE'));
let strategyId: string | undefined;
const disableActiveSplashScreens = () => {
return cy.visit(`/splash/operators`);
};
@ -90,19 +93,14 @@ export const createProject_UI = (
export const createSegment_UI = (segmentName: string): Chainable<any> => {
cy.get("[data-testid='NAVIGATE_TO_CREATE_SEGMENT']").click();
let segmentId;
cy.intercept('POST', '/api/admin/segments', req => {
req.continue(res => {
segmentId = res.body.id;
});
}).as('createSegment');
cy.intercept('POST', '/api/admin/segments').as('createSegment');
cy.get("[data-testid='SEGMENT_NAME_ID']").type(segmentName);
cy.get("[data-testid='SEGMENT_DESC_ID']").type('hello-world');
cy.get("[data-testid='SEGMENT_NEXT_BTN_ID']").click();
cy.get("[data-testid='SEGMENT_CREATE_BTN_ID']").click();
cy.wait('@createSegment');
return cy.wrap(segmentId);
return cy.wait('@createSegment');
};
export const deleteSegment_UI = (segmentName: string): Chainable<any> => {
@ -121,7 +119,7 @@ export const addFlexibleRolloutStrategyToFeature_UI = (
const defaultStickiness = stickiness || 'default';
cy.visit(`/projects/default/features/${featureToggleName}`);
let strategyId;
cy.intercept(
'POST',
`/api/admin/projects/${projectName}/features/${featureToggleName}/environments/development/strategies`,
@ -154,13 +152,11 @@ export const addFlexibleRolloutStrategyToFeature_UI = (
cy.get('[data-testid=DIALOGUE_CONFIRM_ID]').click();
}
cy.get(`[data-testid=STRATEGY_FORM_SUBMIT_ID]`).first().click();
cy.wait('@addStrategyToFeature');
return cy.wrap(strategyId);
return cy.wait('@addStrategyToFeature');
};
export const updateFlexibleRolloutStrategy_UI = (
featureToggleName: string,
strategyId: string,
projectName?: string
) => {
const project = projectName || 'default';
@ -183,7 +179,6 @@ export const updateFlexibleRolloutStrategy_UI = (
.clear()
.type('new-group-id');
cy.get(`[data-testid=STRATEGY_FORM_SUBMIT_ID]`).first().click();
cy.intercept(
'PUT',
`/api/admin/projects/${project}/features/${featureToggleName}/environments/*/strategies/${strategyId}`,
@ -203,12 +198,13 @@ export const updateFlexibleRolloutStrategy_UI = (
});
}
).as('updateStrategy');
cy.get(`[data-testid=STRATEGY_FORM_SUBMIT_ID]`).first().click();
return cy.wait('@updateStrategy');
};
export const deleteFeatureStrategy_UI = (
featureToggleName: string,
strategyId: string,
shouldWait?: boolean,
projectName?: string
): Chainable<any> => {
@ -225,7 +221,7 @@ export const deleteFeatureStrategy_UI = (
).as('deleteUserStrategy');
cy.visit(`/projects/${project}/features/${featureToggleName}`);
cy.get('[data-testid=FEATURE_ENVIRONMENT_ACCORDION_development]').click();
cy.get('[data-testid=STRATEGY_FORM_REMOVE_ID]').click();
cy.get('[data-testid=STRATEGY_FORM_REMOVE_ID]').first().click();
if (!shouldWait) return cy.get('[data-testid=DIALOGUE_CONFIRM_ID]').click();
else cy.get('[data-testid=DIALOGUE_CONFIRM_ID]').click();
return cy.wait('@deleteUserStrategy');
@ -254,7 +250,7 @@ export const addUserIdStrategyToFeature_UI = (
.type('user2')
.type('{enter}');
cy.get('[data-testid=ADD_TO_STRATEGY_INPUT_LIST]').click();
let strategyId;
cy.intercept(
'POST',
`/api/admin/projects/default/features/${featureToggleName}/environments/*/strategies`,
@ -276,8 +272,7 @@ export const addUserIdStrategyToFeature_UI = (
).as('addStrategyToFeature');
cy.get(`[data-testid=STRATEGY_FORM_SUBMIT_ID]`).first().click();
cy.wait('@addStrategyToFeature');
return cy.wrap(strategyId);
return cy.wait('@addStrategyToFeature');
};
export const addVariantsToFeature_UI = (

View File

@ -24,6 +24,7 @@ import {
deleteFeature_API,
deleteProject_API,
updateUserPassword_API,
createEnvironment_API,
//@ts-ignore
} from './API';
@ -35,6 +36,7 @@ Cypress.Commands.add('deleteFeature_API', deleteFeature_API);
Cypress.Commands.add('deleteProject_API', deleteProject_API);
Cypress.Commands.add('logout_UI', logout_UI);
Cypress.Commands.add('createProject_UI', createProject_UI);
Cypress.Commands.add('createProject_API', createProject_API);
Cypress.Commands.add('createUser_API', createUser_API);
Cypress.Commands.add('addUserToProject_API', addUserToProject_API);
Cypress.Commands.add('updateUserPassword_API', updateUserPassword_API);
@ -55,3 +57,4 @@ Cypress.Commands.add(
'updateFlexibleRolloutStrategy_UI',
updateFlexibleRolloutStrategy_UI
);
Cypress.Commands.add('createEnvironment_API', createEnvironment_API);

View File

@ -0,0 +1,20 @@
// ***********************************************************
// This example support/e2e.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')

View File

@ -67,7 +67,8 @@
"classnames": "2.3.2",
"copy-to-clipboard": "3.3.3",
"countries-and-timezones": "^3.4.0",
"cypress": "9.7.0",
"cypress": "12.11.0",
"cypress-vite": "^1.4.0",
"date-fns": "2.29.3",
"date-fns-tz": "^2.0.0",
"debounce": "1.2.1",

View File

@ -57,6 +57,7 @@ export const DemoDialogFinish = ({
variant="contained"
color="primary"
onClick={onClose}
data-testid="DEMO_FINISH_BUTTON"
>
Continue
</StyledButton>

View File

@ -100,6 +100,7 @@ export const DemoDialogWelcome = ({
variant="contained"
color="primary"
onClick={onStart}
data-testid="DEMO_START_BUTTON"
>
Try Unleash demo
</StyledStartButton>

View File

@ -134,6 +134,7 @@ export const DemoStepTooltip = ({
onClick={() => onNext(stepIndex)}
variant="contained"
sx={{ alignSelf: 'flex-end' }}
data-testid="DEMO_NEXT_BUTTON"
>
{topic === topics.length - 1 &&
stepIndex ===
@ -189,6 +190,7 @@ export const DemoStepTooltip = ({
onClick={() => onNext(stepIndex)}
variant="contained"
sx={{ alignSelf: 'flex-end' }}
data-testid="DEMO_NEXT_BUTTON"
>
{topic === topics.length - 1 &&
stepIndex === topics[topic].steps.length - 1

View File

@ -3772,10 +3772,10 @@ commander@^2.20.3:
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
commander@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae"
integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==
commander@^6.2.1:
version "6.2.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==
common-tags@^1.8.0:
version "1.8.2"
@ -3919,10 +3919,18 @@ cuid@^2.1.8:
resolved "https://registry.yarnpkg.com/cuid/-/cuid-2.1.8.tgz#cbb88f954171e0d5747606c0139fb65c5101eac0"
integrity sha512-xiEMER6E7TlTPnDxrM4eRiC6TRgjNX9xzEZ5U/Se2YJKr7Mq4pJn/2XEHjl3STcSh96GmkHPcBXLES8M29wyyg==
cypress@9.7.0:
version "9.7.0"
resolved "https://registry.yarnpkg.com/cypress/-/cypress-9.7.0.tgz#bf55b2afd481f7a113ef5604aa8b693564b5e744"
integrity sha512-+1EE1nuuuwIt/N1KXRR2iWHU+OiIt7H28jJDyyI4tiUftId/DrXYEwoDa5+kH2pki1zxnA0r6HrUGHV5eLbF5Q==
cypress-vite@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/cypress-vite/-/cypress-vite-1.4.0.tgz#4d2889d62c11aed7188b1082af16210240aac2e6"
integrity sha512-BHmOku8q6nRtDGPiBcE7zcAZs56/OsiX5SFoldHEMSQ+I6nnPUU2tcrRNeRsCArONQAvwTu2Da7R/rFGA/DSEg==
dependencies:
chokidar "^3.5.3"
debug "^4.3.4"
cypress@12.11.0:
version "12.11.0"
resolved "https://registry.yarnpkg.com/cypress/-/cypress-12.11.0.tgz#b46dc6a1d0387f59a4b5c6a18cc03884fd61876e"
integrity sha512-TJE+CCWI26Hwr5Msb9GpQhFLubdYooW0fmlPwTsfiyxmngqc7+SZGLPeIkj2dTSSZSEtpQVzOzvcnzH0o8G7Vw==
dependencies:
"@cypress/request" "^2.88.10"
"@cypress/xvfb" "^1.2.4"
@ -3938,12 +3946,12 @@ cypress@9.7.0:
check-more-types "^2.24.0"
cli-cursor "^3.1.0"
cli-table3 "~0.6.1"
commander "^5.1.0"
commander "^6.2.1"
common-tags "^1.8.0"
dayjs "^1.10.4"
debug "^4.3.2"
debug "^4.3.4"
enquirer "^2.3.6"
eventemitter2 "^6.4.3"
eventemitter2 "6.4.7"
execa "4.1.0"
executable "^4.1.1"
extract-zip "2.0.1"
@ -3956,7 +3964,7 @@ cypress@9.7.0:
listr2 "^3.8.3"
lodash "^4.17.21"
log-symbols "^4.0.0"
minimist "^1.2.6"
minimist "^1.2.8"
ospath "^1.2.2"
pretty-bytes "^5.6.0"
proxy-from-env "1.0.0"
@ -5117,10 +5125,10 @@ event-target-shim@^5.0.0:
resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
eventemitter2@^6.4.3:
version "6.4.9"
resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.9.tgz#41f2750781b4230ed58827bc119d293471ecb125"
integrity sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==
eventemitter2@6.4.7:
version "6.4.7"
resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.7.tgz#a7f6c4d7abf28a14c1ef3442f21cb306a054271d"
integrity sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==
eventemitter3@^4.0.0:
version "4.0.7"
@ -7046,6 +7054,11 @@ minimist@^1.2.6:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18"
integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==
minimist@^1.2.8:
version "1.2.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
mlly@^1.1.1, mlly@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.2.0.tgz#f0f6c2fc8d2d12ea6907cd869066689b5031b613"