Merge branch '10.0-release' into 10.0-release-merge-develop-03-22

This commit is contained in:
Barthélémy Ledoux
2022-03-23 16:20:14 -04:00
committed by GitHub
35 changed files with 760 additions and 158 deletions
@@ -16,19 +16,23 @@ describe('Cypress In Cypress - run mode', { viewportWidth: 1200 }, () => {
cy.findByTestId('aut-url').should('be.visible')
cy.findByTestId('playground-activator').should('not.exist')
cy.findByLabelText('Stats').within(() => {
cy.get('.passed .num', { timeout: 10000 }).should('have.text', '1')
})
// confirm expected content is rendered
cy.contains('1000x660').should('be.visible')
cy.contains('71%').should('be.visible')
cy.contains('Chrome 1').should('be.visible')
cy.contains('http://localhost:4455/cypress/e2e/dom-content.html').should('be.visible')
cy.percySnapshot()
// confirm no interactions are implemented
cy.findByTestId('viewport').click()
cy.contains('The viewport determines').should('not.exist')
cy.contains('Chrome 1').click()
cy.contains('Firefox').should('not.exist')
cy.percySnapshot()
})
it('component testing run mode spec runner header is correct', () => {
@@ -46,17 +50,21 @@ describe('Cypress In Cypress - run mode', { viewportWidth: 1200 }, () => {
cy.findByTestId('aut-url').should('not.exist')
cy.findByTestId('playground-activator').should('not.exist')
cy.findByLabelText('Stats').within(() => {
cy.get('.passed .num', { timeout: 10000 }).should('have.text', '1')
})
// confirm expected content is rendered
cy.contains('500x500').should('be.visible')
cy.contains('Chrome 1').should('be.visible')
cy.percySnapshot()
// confirm no interactions are implemented
cy.findByTestId('viewport').click()
cy.contains('The viewport determines').should('not.exist')
cy.contains('Chrome 1').click()
cy.contains('Firefox').should('not.exist')
cy.percySnapshot()
})
})
@@ -1,13 +1,9 @@
// Takes percy snapshot with navigation/AUT hidden and run duration mocked
export const snapshotReporter = () => {
// @ts-ignore
cy.percySnapshot({
width: 450,
elementOverrides: {
'.cy-tooltip': true,
'.runnable-header .duration': ($el) => {
$el.text('XX:XX')
},
'[data-cy=sidebar]': ($el) => {
$el.attr('style', 'display: none !important')
},
+28
View File
@@ -91,6 +91,34 @@ describe('App: Runs', { viewportWidth: 1200 }, () => {
cy.get('button').get('[aria-label="Close"').click()
cy.get('[aria-modal="true"]').should('not.exist')
})
it('opens create Org modal after clicking Connect Project button and refetch data from the cloud', () => {
cy.scaffoldProject('component-tests')
cy.openProject('component-tests', ['--config-file', 'cypressWithoutProjectId.config.js'])
cy.startAppServer('component')
cy.loginUser()
cy.visitApp()
cy.remoteGraphQLIntercept(async (obj) => {
if ((obj.operationName === 'CheckCloudOrganizations_cloudViewerChange_cloudViewer' || obj.operationName === 'Runs_cloudViewer') && obj.callCount < 2) {
if (obj.result.data?.cloudViewer?.organizations?.nodes) {
obj.result.data.cloudViewer.organizations.nodes = []
}
}
return obj.result
})
cy.get('[href="#/runs"]').click()
cy.findByText(defaultMessages.runs.connect.buttonProject).click()
cy.get('[aria-modal="true"]').should('exist')
cy.contains('button', defaultMessages.runs.connect.modal.createOrg.refreshButton).click()
cy.findByText(defaultMessages.runs.connect.modal.selectProject.manageOrgs)
})
})
context('Runs - Connect Project', () => {
+78 -31
View File
@@ -178,21 +178,6 @@ describe('App: Index', () => {
cy.percySnapshot('Invalid spec error')
//Shows extension warning
cy.get('@enterSpecInput').clear().type(getPathForPlatform('cypress/e2e/MyTest.spec.j'))
cy.intercept('mutation-EmptyGenerator_MatchSpecFile', (req) => {
if (req.body.variables.specFile === getPathForPlatform('cypress/e2e/MyTest.spec.jx')) {
req.on('before:response', (res) => {
res.body.data.matchesSpecPattern = true
})
}
})
cy.get('@enterSpecInput').type('x')
cy.contains(defaultMessages.createSpec.e2e.importEmptySpec.specExtensionWarning)
cy.percySnapshot('Non-recommended spec pattern warning')
cy.contains('span', '{filename}.cy.jx')
// Create spec
cy.get('@enterSpecInput').clear().type(getPathForPlatform('cypress/e2e/MyTest.cy.js'))
cy.contains('button', defaultMessages.createSpec.createSpec).should('not.be.disabled').click()
@@ -282,21 +267,6 @@ describe('App: Index', () => {
cy.percySnapshot('Invalid spec error')
//Shows extension warning
cy.get('@enterSpecInput').clear().type(getPathForPlatform('cypress/e2e/MyTest.spec.t'))
cy.intercept('mutation-EmptyGenerator_MatchSpecFile', (req) => {
if (req.body.variables.specFile === getPathForPlatform('cypress/e2e/MyTest.spec.tx')) {
req.on('before:response', (res) => {
res.body.data.matchesSpecPattern = true
})
}
})
cy.get('@enterSpecInput').type('x')
cy.contains(defaultMessages.createSpec.e2e.importEmptySpec.specExtensionWarning)
cy.percySnapshot('Non-recommended spec pattern warning')
cy.contains('span', '{filename}.cy.tx')
// Create spec
cy.get('@enterSpecInput').clear().type(getPathForPlatform('cypress/e2e/MyTest.cy.ts'))
cy.contains('button', defaultMessages.createSpec.createSpec).should('not.be.disabled').click()
@@ -350,7 +320,7 @@ describe('App: Index', () => {
cy.findByTestId('file-match-indicator').should('contain', '0 Matches')
cy.findByRole('button', { name: 'cypress.config.js' })
cy.findByTestId('spec-pattern').should('contain', 'src/**/*.cy.{js,jsx}')
cy.findByTestId('spec-pattern').should('contain', 'src/**/*.{cy,spec}.{js,jsx}')
cy.contains('button', defaultMessages.createSpec.updateSpecPattern)
cy.findByRole('button', { name: 'New Spec', exact: false })
@@ -383,6 +353,83 @@ describe('App: Index', () => {
.and('contain', defaultMessages.createSpec.e2e.importEmptySpec.header)
})
})
context('scaffold empty spec', () => {
it('should generate empty spec', () => {
cy.findByRole('button', { name: 'New Spec', exact: false }).click()
cy.findByRole('dialog', { name: defaultMessages.createSpec.newSpecModalTitle }).within(() => {
cy.findAllByTestId('card').eq(0)
.and('contain', defaultMessages.createSpec.e2e.importFromScaffold.header)
cy.findAllByTestId('card').eq(1)
.and('contain', defaultMessages.createSpec.e2e.importEmptySpec.header)
})
cy.contains('Create new empty spec').click()
cy.findAllByLabelText(defaultMessages.createSpec.e2e.importEmptySpec.inputPlaceholder)
.as('enterSpecInput')
cy.get('@enterSpecInput').invoke('val').should('eq', getPathForPlatform('src/filename.cy.js'))
cy.contains(defaultMessages.createSpec.e2e.importEmptySpec.invalidSpecWarning).should('not.exist')
cy.get('@enterSpecInput').clear()
cy.contains(defaultMessages.createSpec.e2e.importEmptySpec.invalidSpecWarning).should('not.exist')
// Shows entered file does not match spec pattern
cy.get('@enterSpecInput').type(getPathForPlatform('cypress/e2e/no-match'))
cy.contains(defaultMessages.createSpec.e2e.importEmptySpec.invalidSpecWarning)
cy.contains('button', defaultMessages.createSpec.createSpec).should('be.disabled')
cy.percySnapshot('Invalid spec error')
// Create spec
cy.get('@enterSpecInput').clear().type(getPathForPlatform('src/MyTest.cy.js'))
cy.contains('button', defaultMessages.createSpec.createSpec).should('not.be.disabled').click()
cy.contains('h2', defaultMessages.createSpec.successPage.header)
cy.get('[data-cy="file-row"]').contains(getPathForPlatform('src/MyTest.cy.js')).click()
cy.get('pre').should('contain', 'describe(\'MyTest.cy.js\'')
cy.percySnapshot('Generator success')
cy.get('[aria-label="Close"]').click()
cy.visitApp().get('[data-cy="specs-list-row"]').contains('MyTest.cy.js')
})
})
it('shows extension warning', () => {
cy.findByRole('button', { name: 'New Spec', exact: false }).click()
cy.findByRole('dialog', { name: defaultMessages.createSpec.newSpecModalTitle }).within(() => {
cy.findAllByTestId('card').eq(0)
.and('contain', defaultMessages.createSpec.e2e.importFromScaffold.header)
cy.findAllByTestId('card').eq(1)
.and('contain', defaultMessages.createSpec.e2e.importEmptySpec.header)
})
cy.contains('Create new empty spec').click()
cy.findAllByLabelText(defaultMessages.createSpec.e2e.importEmptySpec.inputPlaceholder)
.as('enterSpecInput')
cy.get('@enterSpecInput').clear().type(getPathForPlatform('src/e2e/MyTest.spec.j'))
cy.intercept('mutation-EmptyGenerator_MatchSpecFile', (req) => {
if (req.body.variables.specFile === getPathForPlatform('src/e2e/MyTest.spec.jx')) {
req.on('before:response', (res) => {
res.body.data.matchesSpecPattern = true
})
}
})
cy.get('@enterSpecInput').type('x')
cy.contains(defaultMessages.createSpec.e2e.importEmptySpec.specExtensionWarning)
cy.percySnapshot('Non-recommended spec pattern warning')
cy.contains('span', '{filename}.cy.jx')
})
})
context('pristine app', () => {
@@ -0,0 +1,51 @@
import defaultMessages from '@packages/frontend-shared/src/locales/en-US.json'
import type { SinonStub } from 'sinon'
describe('App: Runs', { viewportWidth: 1200 }, () => {
beforeEach(() => {
cy.scaffoldProject('component-tests')
cy.openProject('component-tests')
cy.startAppServer('component')
})
context('Runs - Connect Org', () => {
it('opens create Org modal after clicking Connect Project button', () => {
cy.scaffoldProject('component-tests')
cy.openProject('component-tests', ['--config-file', 'cypressWithoutProjectId.config.js'])
cy.startAppServer('component')
cy.loginUser()
cy.visitApp()
cy.remoteGraphQLIntercept(async (obj) => {
if ((obj.operationName === 'CheckCloudOrganizations_cloudViewerChange_cloudViewer' || obj.operationName === 'Runs_cloudViewer') && obj.callCount < 2) {
if (obj.result.data?.cloudViewer?.organizations?.nodes) {
obj.result.data.cloudViewer.organizations.nodes = []
}
}
return obj.result
})
cy.get('[href="#/runs"]').click()
cy.findByText(defaultMessages.runs.connect.buttonProject).click()
cy.get('[aria-modal="true"]').should('exist')
cy.findByText(defaultMessages.runs.connect.modal.createOrg.button).click()
cy.withRetryableCtx((ctx) => {
expect((ctx.actions.electron.openExternal as SinonStub).lastCall.lastArg).to.eq(`http://dummy.cypress.io/organizations/create?port=${process.env.CYPRESS_INTERNAL_GRAPHQL_PORT}`)
})
cy.contains('button', defaultMessages.runs.connect.modal.createOrg.waitingButton).should('be.visible')
cy.contains('a', defaultMessages.links.needHelp).should('have.attr', 'href', 'https://on.cypress.io/adding-new-project')
cy.withCtx(async (ctx) => {
await ctx.util.fetch(`http://127.0.0.1:${ctx.gqlServerPort}/cloud-notification?operationName=orgCreated`)
})
cy.findByText(defaultMessages.runs.connect.modal.selectProject.manageOrgs)
})
})
})
@@ -27,8 +27,9 @@ import { ref } from 'vue'
import SelectCloudProjectModal from './SelectCloudProjectModal.vue'
import CreateCloudOrgModal from './CreateCloudOrgModal.vue'
import NeedManualUpdateModal from './NeedManualUpdateModal.vue'
import { gql } from '@urql/vue'
import { gql, useSubscription } from '@urql/vue'
import type { CloudConnectModalsFragment } from '../../generated/graphql'
import { CheckCloudOrganizationsDocument } from '../../generated/graphql'
gql`
fragment CloudConnectModals on Query {
@@ -44,6 +45,16 @@ fragment CloudConnectModals on Query {
}
`
gql`
subscription CheckCloudOrganizations {
cloudViewerChange {
...CloudConnectModals
}
}
`
useSubscription({ query: CheckCloudOrganizationsDocument })
const emit = defineEmits<{
(event: 'success'): void
(event: 'cancel'): void
@@ -9,11 +9,12 @@
<p class=" mb-16px text-gray-700">
{{ t('runs.connect.modal.createOrg.description') }}
</p>
<div @click="startPolling()">
<div @click="startWaitingOrgToBeCreated()">
<ExternalLink
class="border rounded mx-auto border-gray-100 py-4px px-16px text-indigo-500 inline-block"
:href="createOrgUrl"
:prefix-icon="OrganizationIcon"
:include-graphql-port="true"
prefix-icon-class="icon-light-transparent icon-dark-white"
>
{{ t('runs.connect.modal.createOrg.button') }}
@@ -23,7 +24,7 @@
<template #footer>
<div class="flex gap-16px">
<Button
v-if="polling"
v-if="waitingOrgToBeCreated"
size="lg"
variant="pending"
>
@@ -37,7 +38,7 @@
<Button
v-else
size="lg"
@click="startPolling()"
@click="refetch()"
>
{{ t('runs.connect.modal.createOrg.refreshButton') }}
</Button>
@@ -54,16 +55,16 @@
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue'
import { computed, onBeforeUnmount, ref } from 'vue'
import { gql, useQuery } from '@urql/vue'
import { useIntervalFn } from '@vueuse/core'
import StandardModal from '@cy/components/StandardModal.vue'
import Button from '@cy/components/Button.vue'
import ExternalLink from '@cy/gql-components/ExternalLink.vue'
import OrganizationIcon from '~icons/cy/office-building_x16.svg'
import type { CreateCloudOrgModalFragment } from '../../generated/graphql'
import { CheckCloudOrganizationsDocument } from '../../generated/graphql'
import { CloudOrganizationsCheckDocument } from '../../generated/graphql'
import { useI18n } from '@cy/i18n'
import { useDebounceFn } from '@vueuse/core'
const { t } = useI18n()
@@ -84,15 +85,8 @@ fragment CreateCloudOrgModal on CloudUser {
`
gql`
query CheckCloudOrganizations {
cloudViewer {
id
organizations (first:1) {
nodes {
id
}
}
}
query CloudOrganizationsCheck {
...CloudConnectModals
}
`
@@ -101,31 +95,28 @@ const props = defineProps<{
}>()
const query = useQuery({
query: CheckCloudOrganizationsDocument,
query: CloudOrganizationsCheckDocument,
requestPolicy: 'network-only',
pause: true,
})
const polling = ref(false)
const refetch = useDebounceFn(() => query.executeQuery(), 1000)
const { pause, resume } = useIntervalFn(() => {
if (props?.gql?.organizationControl?.nodes?.length || 0 > 0) {
pause()
} else {
query.executeQuery()
}
}, 5000)
const waitingOrgToBeCreated = ref(false)
function startPolling () {
if (!polling.value) {
resume()
polling.value = true
}
let timer
setTimeout(() => {
pause()
polling.value = false
function startWaitingOrgToBeCreated () {
waitingOrgToBeCreated.value = true
timer = setTimeout(() => {
waitingOrgToBeCreated.value = false
}, 60000)
}
onBeforeUnmount(() => {
window.clearTimeout(timer)
})
const createOrgUrl = computed(() => props.gql.createCloudOrganizationUrl || '#')
</script>
@@ -37,6 +37,7 @@ describe('<CreateSpecModal />', () => {
},
specs: [],
fileExtensionToUse: 'js',
defaultSpecFileName: 'cypress/e2e/filename.cy.js',
},
}}
show={show.value}
@@ -106,6 +107,7 @@ describe('playground', () => {
},
specs: [],
fileExtensionToUse: 'js',
defaultSpecFileName: 'cypress/e2e/filename.cy.js',
},
}}
show={show.value}
+4 -1
View File
@@ -70,6 +70,7 @@ fragment CreateSpecModal on Query {
currentProject {
id
fileExtensionToUse
defaultSpecFileName
...EmptyGenerator
...ComponentGeneratorStepOne_codeGenGlob
...StoryGeneratorStepOne_codeGenGlob
@@ -99,7 +100,9 @@ const helpLink = computed(() => {
const specFileName = computed(() => {
const extension = props.gql.currentProject?.fileExtensionToUse ?? 'js'
return getPathForPlatform(`cypress/e2e/filename.cy.${extension}`)
const fileName = props.gql.currentProject?.defaultSpecFileName ?? `cypress/e2e/filename.cy.${extension}`
return getPathForPlatform(fileName)
})
const codeGenGlob = computed(() => {
+4
View File
@@ -38,9 +38,12 @@
"isbinaryfile": "^4.0.8",
"launch-editor": "2.2.1",
"lodash": "4.17.21",
"micromatch": "4.0.4",
"node-machine-id": "1.1.12",
"p-defer": "^3.0.0",
"parse-glob": "3.0.4",
"prettier": "2.5.1",
"randexp": "0.5.3",
"randomstring": "1.1.5",
"resolve-from": "^5.0.0",
"stringify-object": "^3.0.0",
@@ -63,6 +66,7 @@
"@types/ejs": "^3.1.0",
"@types/fs-extra": "^8.0.1",
"@types/mocha": "^8.0.3",
"@types/parse-glob": "3.0.29",
"@types/prettier": "2.4.3",
"@types/stringify-object": "^3.0.0",
"mocha": "7.0.1",
@@ -21,6 +21,13 @@ abstract class DataEmitterEvents {
this._emit('devChange')
}
/**
* Emitted when we have a notification from the cloud to refresh the data
*/
cloudViewerChange () {
this._emit('cloudViewerChange')
}
browserStatusChange () {
this._emit('browserStatusChange')
}
@@ -5,7 +5,7 @@ import debugLib from 'debug'
import type { DataContext } from '..'
import pDefer from 'p-defer'
import getenv from 'getenv'
import { pipe, subscribe, toPromise } from 'wonka'
import { pipe, subscribe, toPromise, take } from 'wonka'
import type { DocumentNode, OperationTypeNode } from 'graphql'
import {
createClient,
@@ -129,7 +129,10 @@ export class CloudDataSource {
return dfd.promise
}
return pipe(executingQuery, toPromise).then((data) => {
// take(1) completes the stream immediately after the first value was emitted
// avoiding it to hang forever on query operations
// https://github.com/FormidableLabs/urql/issues/298
return pipe(executingQuery, take(1), toPromise).then((data) => {
debug('executeRemoteGraphQL toPromise res %o', data)
if (data.error) {
@@ -8,6 +8,9 @@ import path from 'path'
import Debug from 'debug'
import commonPathPrefix from 'common-path-prefix'
import type { FSWatcher } from 'chokidar'
import parseGlob from 'parse-glob'
import mm from 'micromatch'
import RandExp from 'randexp'
const debug = Debug('cypress:data-context')
import assert from 'assert'
@@ -101,6 +104,62 @@ export function transformSpec ({
}
}
export function getDefaultSpecFileName (specPattern: string, fileExtensionToUse?: 'js' | 'ts') {
function replaceWildCard (s: string, fallback: string) {
return s.replace(/\*/g, fallback)
}
const parsedGlob = parseGlob(specPattern)
if (!parsedGlob.is.glob) {
return specPattern
}
let dirname = parsedGlob.path.dirname
if (dirname.startsWith('**')) {
dirname = dirname.replace('**', 'cypress')
}
const splittedDirname = dirname.split('/').filter((s) => s !== '**').map((x) => replaceWildCard(x, 'e2e')).join('/')
const fileName = replaceWildCard(parsedGlob.path.filename, 'filename')
const extnameWithoutExt = parsedGlob.path.extname.replace(parsedGlob.path.ext, '')
let extname = replaceWildCard(extnameWithoutExt, 'cy')
if (extname.startsWith('.')) {
extname = extname.substr(1)
}
if (extname.endsWith('.')) {
extname = extname.slice(0, -1)
}
const basename = [fileName, extname, parsedGlob.path.ext].filter(Boolean).join('.')
const glob = splittedDirname + basename
const globWithoutBraces = mm.braces(glob, { expand: true })
let finalGlob = globWithoutBraces[0]
if (fileExtensionToUse) {
const filteredGlob = mm(globWithoutBraces, `*.${fileExtensionToUse}`, { basename: true })
if (filteredGlob?.length) {
finalGlob = filteredGlob[0]
}
}
if (!finalGlob) {
return
}
const randExp = new RandExp(finalGlob.replace(/\./g, '\\.'))
return randExp.gen()
}
export class ProjectDataSource {
private _specWatcher: FSWatcher | null = null
private _specs: FoundSpec[] = []
@@ -204,6 +263,37 @@ export class ProjectDataSource {
this._specWatcher.on('unlink', onSpecsChanged)
}
async defaultSpecFileName () {
const defaultFileName = 'cypress/e2e/filename.cy.js'
try {
if (!this.ctx.currentProject || !this.ctx.coreData.currentTestingType) {
return null
}
let specPatternSet: string | undefined
const { specPattern = [] } = await this.ctx.project.specPatternsForTestingType(this.ctx.currentProject, this.ctx.coreData.currentTestingType)
if (Array.isArray(specPattern)) {
specPatternSet = specPattern[0]
}
if (!specPatternSet) {
return defaultFileName
}
const specFileName = getDefaultSpecFileName(specPatternSet, this.ctx.lifecycleManager.fileExtensionToUse)
if (!specFileName) {
return defaultFileName
}
return specFileName
} catch {
return defaultFileName
}
}
async matchesSpecPattern (specFile: string): Promise<boolean> {
if (!this.ctx.currentProject || !this.ctx.coreData.currentTestingType) {
return false
@@ -1,5 +1,5 @@
import { expect } from 'chai'
import { matchedSpecs, transformSpec, SpecWithRelativeRoot, BrowserApiShape } from '../../../src/sources'
import { matchedSpecs, transformSpec, SpecWithRelativeRoot, BrowserApiShape, getDefaultSpecFileName } from '../../../src/sources'
import path from 'path'
import { DataContext } from '../../../src'
import { graphqlSchema } from '@packages/graphql/src/schema'
@@ -189,3 +189,152 @@ describe('findSpecs', () => {
expect(specs).to.have.length(3)
})
})
describe('getDefaultSpecFileName', () => {
context('dirname', () => {
it('returns pattern without change if it is do not a glob', () => {
const specPattern = 'cypress/e2e/foo.spec.ts'
const defaultFileName = getDefaultSpecFileName(specPattern)
expect(defaultFileName).to.eq(specPattern)
})
it('remove ** from glob if it is not in the beginning', () => {
const defaultFileName = getDefaultSpecFileName('cypress/**/foo.spec.ts')
expect(defaultFileName).to.eq('cypress/foo.spec.ts')
})
it('replace ** for cypress if it starts with **', () => {
const defaultFileName = getDefaultSpecFileName('**/e2e/foo.spec.ts')
expect(defaultFileName).to.eq('cypress/e2e/foo.spec.ts')
})
it('replace ** for cypress if it starts with ** and omit extra **', () => {
const defaultFileName = getDefaultSpecFileName('**/**/foo.spec.ts')
expect(defaultFileName).to.eq('cypress/foo.spec.ts')
})
it('selects first option if there are multiples possibilities of values', () => {
const defaultFileName = getDefaultSpecFileName('{cypress,tests}/{integration,e2e}/foo.spec.ts')
expect(defaultFileName).to.eq('cypress/integration/foo.spec.ts')
})
})
context('filename', () => {
it('replace * for filename', () => {
const defaultFileName = getDefaultSpecFileName('cypress/e2e/*.spec.ts')
expect(defaultFileName).to.eq('cypress/e2e/filename.spec.ts')
})
it('selects first option if there are multiples possibilities of values', () => {
const defaultFileName = getDefaultSpecFileName('cypress/e2e/{foo,filename}.spec.ts')
expect(defaultFileName).to.eq('cypress/e2e/foo.spec.ts')
})
})
context('test extension', () => {
it('replace * for filename', () => {
const defaultFileName = getDefaultSpecFileName('cypress/e2e/filename.*.ts')
expect(defaultFileName).to.eq('cypress/e2e/filename.cy.ts')
})
it('selects first option if there are multiples possibilities of values', () => {
const defaultFileName = getDefaultSpecFileName('cypress/e2e/filename.{spec,cy}.ts')
expect(defaultFileName).to.eq('cypress/e2e/filename.spec.ts')
})
})
context('lang extension', () => {
it('if project use TS, set TS as extension if it exists in the glob', () => {
const defaultFileName = getDefaultSpecFileName('cypress/e2e/filename.cy.ts', 'ts')
expect(defaultFileName).to.eq('cypress/e2e/filename.cy.ts')
})
it('if project use TS, set TS as extension if it exists in the options of extensions', () => {
const defaultFileName = getDefaultSpecFileName('cypress/e2e/filename.cy.{js,ts,tsx}', 'ts')
expect(defaultFileName).to.eq('cypress/e2e/filename.cy.ts')
})
it('if project use TS, do not set TS as extension if it do not exists in the options of extensions', () => {
const defaultFileName = getDefaultSpecFileName('cypress/e2e/filename.cy.{js,jsx}', 'ts')
expect(defaultFileName).to.eq('cypress/e2e/filename.cy.js')
})
it('selects first option if there are multiples possibilities of values', () => {
const defaultFileName = getDefaultSpecFileName('cypress/e2e/filename.cy.{ts,js}')
expect(defaultFileName).to.eq('cypress/e2e/filename.cy.ts')
})
})
context('extra cases', () => {
it('creates specName for tests/*.js', () => {
const defaultFileName = getDefaultSpecFileName('tests/*.js')
expect(defaultFileName).to.eq('tests/filename.js')
})
it('creates specName for src/*-test.js', () => {
const defaultFileName = getDefaultSpecFileName('src/*-test.js')
expect(defaultFileName).to.eq('src/filename-test.js')
})
it('creates specName for src/*.foo.bar.js', () => {
const defaultFileName = getDefaultSpecFileName('src/*.foo.bar.js')
expect(defaultFileName).to.eq('src/filename.foo.bar.js')
})
it('creates specName for src/prefix.*.test.js', () => {
const defaultFileName = getDefaultSpecFileName('src/prefix.*.test.js')
expect(defaultFileName).to.eq('src/prefix.cy.test.js')
})
it('creates specName for src/*/*.test.js', () => {
const defaultFileName = getDefaultSpecFileName('src/*/*.test.js')
expect(defaultFileName).to.eq('src/e2e/filename.test.js')
})
it('creates specName for src-*/**/*.test.js', () => {
const defaultFileName = getDefaultSpecFileName('src-*/**/*.test.js')
expect(defaultFileName).to.eq('src-e2e/filename.test.js')
})
it('creates specName for src/*.test.(js|jsx)', () => {
const defaultFileName = getDefaultSpecFileName('src/*.test.(js|jsx)')
const possiblesFileNames = ['src/filename.test.jsx', 'src/filename.test.js']
expect(possiblesFileNames.includes(defaultFileName)).to.eq(true)
})
it('creates specName for (src|components)/**/*.test.js', () => {
const defaultFileName = getDefaultSpecFileName('(src|components)/**/*.test.js')
const possiblesFileNames = ['src/filename.test.js', 'components/filename.test.js']
expect(possiblesFileNames.includes(defaultFileName)).to.eq(true)
})
it('creates specName for e2e/**/*.cy.{js,jsx,ts,tsx}', () => {
const defaultFileName = getDefaultSpecFileName('e2e/**/*.cy.{js,jsx,ts,tsx}')
expect(defaultFileName).to.eq('e2e/filename.cy.js')
})
})
})
@@ -157,6 +157,8 @@ async function makeE2ETasks () {
sinon.stub(ctx.actions.electron, 'openExternal')
sinon.stub(ctx.actions.electron, 'showItemInFolder')
const operationCount: Record<string, number> = {}
sinon.stub(ctx.util, 'fetch').get(() => {
return async (url: RequestInfo, init?: RequestInit) => {
if (String(url).endsWith('/test-runner-graphql')) {
@@ -164,6 +166,8 @@ async function makeE2ETasks () {
const document = parse(query)
const operationName = getOperationName(document)
operationCount[operationName ?? 'unknown'] = operationCount[operationName ?? 'unknown'] ?? 0
let result = await execute({
operationName,
document,
@@ -175,6 +179,8 @@ async function makeE2ETasks () {
},
})
operationCount[operationName ?? 'unknown']++
if (remoteGraphQLIntercept) {
try {
result = await remoteGraphQLIntercept({
@@ -183,6 +189,7 @@ async function makeE2ETasks () {
document,
query,
result,
callCount: operationCount[operationName ?? 'unknown'],
})
} catch (e) {
const err = e as Error
@@ -45,6 +45,7 @@ export interface RemoteGraphQLInterceptPayload {
variables: Record<string, any>
document: DocumentNode
result: ExecutionResult
callCount: number
}
export type RemoteGraphQLInterceptor = (obj: RemoteGraphQLInterceptPayload) => ExecutionResult | Promise<ExecutionResult>
@@ -524,6 +525,12 @@ Cypress.Commands.add('findBrowsers', findBrowsers)
Cypress.Commands.add('tabUntil', tabUntil)
Cypress.Commands.add('validateExternalLink', { prevSubject: ['optional', 'element'] }, validateExternalLink)
installCustomPercyCommand()
installCustomPercyCommand({
elementOverrides: {
'.runnable-header .duration': ($el) => {
$el.text('XX:XX')
},
},
})
addNetworkCommands()
@@ -1,4 +1,3 @@
import '@percy/cypress'
import '../../src/styles/shared.scss'
import type { ComponentPublicInstance } from 'vue'
-1
View File
@@ -59,7 +59,6 @@
"patch-package": "6.4.7",
"rimraf": "3.0.2",
"shiki": "^0.9.12",
"spin.js": "^4.1.1",
"unplugin-icons": "0.13.2",
"unplugin-vue-components": "^0.15.4",
"vite": "2.9.0-beta.3",
@@ -0,0 +1,11 @@
import Spinner from './Spinner.vue'
describe('<Spinner />', { viewportHeight: 200, viewportWidth: 200 }, () => {
it('renders', () => {
cy.mount(() => (
<Spinner />
))
cy.percySnapshot()
})
})
@@ -1,23 +1,68 @@
<template>
<div
id="vue-spinner"
data-e2e="spin"
/>
</template>
<div class="flex top-0 right-0 bottom-0 left-0 absolute items-center justify-center">
<svg
width="48"
height="48"
viewBox="0 0 48 48"
fill="none"
class="spinner"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<mask id="y_and_circle">
<path
d="M37.3061 16.0737L32.658 27.8353L27.979 16.0737H24.1514L30.7133 32.1268L26.0446 43.449L25.828 43.963C25.6788 44.3224 25.3533 44.5752 24.9776 44.6273C24.6537 44.6423 24.3277 44.65 24 44.65C23.9323 44.65 23.8647 44.6497 23.7972 44.649C12.4859 44.5402 3.35 35.337 3.35 24C3.35 12.5953 12.5953 3.35 24 3.35C35.4047 3.35 44.65 12.5953 44.65 24C44.65 32.5719 39.4271 39.924 31.99 43.0474L30.7772 45.9958C30.5987 46.43 30.3755 46.8377 30.1141 47.2142C40.4075 44.5105 48 35.1419 48 24C48 10.7452 37.2548 0 24 0C10.7452 0 0 10.7452 0 24C0 37.1872 10.6357 47.8902 23.7972 47.9992C23.8247 47.9994 25.0196 47.9794 25.0177 47.9794C26.736 47.9075 28.2717 46.8308 28.9276 45.235L29.4694 43.9179L40.9228 16.0737H37.3061Z"
fill="white"
/>
</mask>
</defs>
<script lang="ts" setup>
import { Spinner } from 'spin.js'
import { onMounted } from 'vue'
<path
d="M16.877 19.0494C18.819 19.0494 20.401 20.085 21.2139 21.8915L21.2782 22.0329L24.5398 20.9253L24.4704 20.7583C23.2074 17.6823 20.2981 15.7704 16.877 15.7704C14.4719 15.7704 12.5169 16.5414 10.9015 18.1243C9.29635 19.697 8.48353 21.6757 8.48353 24.0064C8.48353 26.3166 9.29635 28.285 10.9015 29.8577C12.5169 31.4407 14.4719 32.2116 16.877 32.2116C20.2981 32.2116 23.2074 30.2997 24.4704 27.2263L24.5398 27.0593L21.273 25.9491L21.2113 26.0956C20.4833 27.8713 18.8628 28.9326 16.877 28.9326C15.5239 28.9326 14.3818 28.4598 13.4763 27.5295C12.5606 26.5864 12.0976 25.4018 12.0976 24.009C12.0976 22.6059 12.5503 21.4444 13.4763 20.4576C14.3792 19.5222 15.5239 19.0494 16.877 19.0494Z"
/>
<rect
x="0"
y="0"
width="48"
height="48"
mask="url(#y_and_circle)"
/>
<circle
cx="24"
cy="24"
r="23"
stroke-width="5"
mask="url(#y_and_circle)"
/>
</svg>
</div>
</template>.
onMounted(() => {
const target = document.getElementById('vue-spinner')
<style scoped>
if (target) {
new Spinner({ color: '#888', scale: 0.5 }).spin(target)
.spinner circle {
stroke-dasharray: 30 115;
stroke-dashoffset: 145;
animation: dash linear 1s infinite;
@apply stroke-jade-300;
}
.spinner > path,
.spinner > rect {
@apply fill-gray-1000;
}
@keyframes dash {
20% {
stroke-dasharray: 45 100;
}
})
</script>
<style>
@import 'spin.js/spin.css';
67% {
stroke-dasharray: 10 135;
stroke-dashoffset: 50;
}
to {
stroke-dashoffset: 0;
stroke-dasharray: 30 115;
}
}
</style>
@@ -25,10 +25,12 @@ import { useExternalLink } from '../gql-components/useExternalLink'
const props = withDefaults(defineProps<{
href?: string
useDefaultHocus?: boolean
includeGraphqlPort?: boolean
}>(), {
useDefaultHocus: true,
href: '',
includeGraphqlPort: false,
})
const open = useExternalLink(props.href)
const open = useExternalLink(props.href, props.includeGraphqlPort)
</script>
@@ -4,12 +4,12 @@ import type { MaybeRef } from '@vueuse/core'
import { unref } from 'vue'
gql`
mutation ExternalLink_OpenExternal ($url: String!) {
openExternal(url: $url)
mutation ExternalLink_OpenExternal ($url: String!, $includeGraphqlPort: Boolean) {
openExternal(url: $url, includeGraphqlPort: $includeGraphqlPort)
}
`
export const useExternalLink = ($href?: MaybeRef<string>) => {
export const useExternalLink = ($href?: MaybeRef<string>, includeGraphqlPort: boolean = false) => {
const openExternalMutation = useMutation(ExternalLink_OpenExternalDocument)
return (href?: string) => {
@@ -19,6 +19,6 @@ export const useExternalLink = ($href?: MaybeRef<string>) => {
return new Error(`Cannot open external link. Possible urls passed in were ${{ localHref: href, initialHref: unref($href) }}`)
}
return openExternalMutation.executeMutation({ url: resolvedHref })
return openExternalMutation.executeMutation({ url: resolvedHref, includeGraphqlPort })
}
}
+7 -1
View File
@@ -388,6 +388,9 @@ type CurrentProject implements Node & ProjectLike {
"""The mode the interactive runner was launched in"""
currentTestingType: TestingTypeEnum
"""Default spec file name for spec creation"""
defaultSpecFileName: String
"""File extension to use based on if the project has typescript or not"""
fileExtensionToUse: FileExtensionEnum
@@ -1001,7 +1004,7 @@ type Mutation {
"""Open a path in preferred IDE"""
openDirectoryInIDE(path: String!): Boolean
openExternal(url: String!): Boolean
openExternal(includeGraphqlPort: Boolean, url: String!): Boolean
"""Open a file on specified line and column in preferred IDE"""
openFileInIDE(input: FileDetailsInput!): Boolean
@@ -1253,6 +1256,9 @@ type Subscription {
"""Status of the currently opened browser"""
browserStatusChange: CurrentProject
""""""
cloudViewerChange: Query
"""Issued for internal development changes"""
devChange: DevState
+23
View File
@@ -46,6 +46,29 @@ export async function makeGraphQLServer () {
})
})
app.get('/cloud-notification', (req, res) => {
const ctx = getCtx()
const operationName = req.query.operationName
if (!operationName || Array.isArray(operationName)) {
res.sendStatus(200)
return
}
switch (operationName) {
case 'orgCreated':
ctx.emitter.cloudViewerChange()
break
default:
break
}
res.sendStatus(200)
})
app.use('/__launchpad/graphql/:operationName?', graphQLHTTP)
function makeProxy (): express.Handler {
@@ -129,6 +129,13 @@ export const CurrentProject = objectType({
},
})
t.string('defaultSpecFileName', {
description: 'Default spec file name for spec creation',
resolve: (source, args, ctx) => {
return ctx.project.defaultSpecFileName()
},
})
t.nonNull.list.nonNull.field('specs', {
description: 'A list of specs for the currently open testing type of a project',
type: Spec,
@@ -82,9 +82,16 @@ export const mutation = mutationType({
type: 'Boolean',
args: {
url: nonNull(stringArg()),
includeGraphqlPort: booleanArg(),
},
resolve: (_, args, ctx) => {
ctx.actions.electron.openExternal(args.url)
let url = args.url
if (args.includeGraphqlPort && process.env.CYPRESS_INTERNAL_GRAPHQL_PORT) {
url = `${args.url}?port=${process.env.CYPRESS_INTERNAL_GRAPHQL_PORT}`
}
ctx.actions.electron.openExternal(url)
return true
},
@@ -17,6 +17,17 @@ export const Subscription = subscriptionType({
resolve: (source, args, ctx) => ctx.coreData.dev,
})
t.field('cloudViewerChange', {
type: Query,
description: '',
subscribe: (source, args, ctx) => ctx.emitter.subscribeTo('cloudViewerChange'),
resolve: (source, args, ctx) => {
return {
requestPolicy: 'network-only',
}
},
})
t.field('browserStatusChange', {
type: CurrentProject,
description: 'Status of the currently opened browser',
@@ -7,7 +7,7 @@ import type { DataContext } from '@packages/data-context'
* @returns
*/
export const remoteSchemaExecutor = async (obj: Record<string, any>) => {
const { document: _document, operationType, variables, context: _context } = obj
const { document: _document, operationType, variables, context: _context, rootValue } = obj
const document: DocumentNode = _document
const context: DataContext = _context
@@ -21,6 +21,7 @@ export const remoteSchemaExecutor = async (obj: Record<string, any>) => {
document,
variables,
query: print(document),
requestPolicy: rootValue?.requestPolicy,
})
context.debug('executorResult %o', executorResult)
@@ -24,6 +24,7 @@ export const remoteSchemaWrapped = wrapSchema({
info,
operationName: getOperationName(info),
transformedSchema,
rootValue: _parent,
})
}
},
@@ -1,4 +1,5 @@
{
"parser": "@typescript-eslint/parser",
"extends": [
"plugin:cypress/recommended",
"plugin:@cypress/dev/react"
@@ -1,8 +1,61 @@
require('@percy/cypress')
const _ = require('lodash')
/// <reference types="cypress" />
import '@percy/cypress'
import type { SnapshotOptions } from '@percy/core'
export interface CustomSnapshotOptions extends SnapshotOptions{
/**
* width of the snapshot taken from the left edge of the viewport
* @default - The test's viewportWidth
*/
width?: number
/**
* height of the snapshot taken from the top edge of the viewport
* @default - The test's viewportHeight
*/
height?: number
/**
* The desired snapshot overrides. These will be merged with and take
* precedence over the global override defined when the command was installed.
* @example
* ```ts
* {
* '.element-to-hide': true,
* '#element-to-replace-content': ($el) => { $el.text('new content') },
* }
* ```
*/
elementOverrides?: Record<string, ((el$: JQuery) => void) | true>
}
interface SnapshotMutationOptions{
log?: string
defaultWidth: number
defaultHeight: number
snapshotWidth: number
snapshotHeight: number
snapshotElementOverrides: NonNullable<CustomSnapshotOptions['elementOverrides']>
}
declare global {
namespace Cypress {
interface Chainable{
/**
* A custom Percy command that allows for additional mutations prior to snapshot generation. Mutations will be
* reset after snapshot generation so that the AUT is not polluted after the command executes.
*/
percySnapshot(options?: CustomSnapshotOptions): Chainable<() => void>
/**
* A custom Percy command that allows for additional mutations prior to snapshot generation. Mutations will be
* reset after snapshot generation so that the AUT is not polluted after the command executes.
*/
percySnapshot(name?: string, options?: CustomSnapshotOptions): Chainable<() => void>
}
}
}
class ElementOverrideManager {
mutationStack = undefined
private mutationStack: Array<MutationRecord> | undefined = undefined
/**
* overrides are defined in selector/override pairs.
@@ -17,19 +70,24 @@ class ElementOverrideManager {
* }
* }
*/
performOverrides (cy, overrides) {
performOverrides (cy: Cypress.cy, overrides: NonNullable<CustomSnapshotOptions['elementOverrides']>) {
const observer = new MutationObserver((mutations) => {
this.mutationStack ??= []
this.mutationStack.push(...mutations)
})
observer.observe(cy.$$('html')[0], { childList: true, subtree: true, attributes: true, attributeOldValue: true })
observer.observe(cy.$$('html')[0], {
childList: true,
subtree: true,
attributes: true,
attributeOldValue: true,
})
_.each(overrides, (v, k) => {
Object.entries(overrides).forEach(([k, v]) => {
// eslint-disable-next-line cypress/no-assigning-return-values
const $el = cy.$$(k)
if (_.isFunction(v)) {
if (typeof v === 'function') {
v($el)
return
@@ -46,7 +104,9 @@ class ElementOverrideManager {
}
resetOverrides () {
_.forEachRight(this.mutationStack, ({
if (!this.mutationStack) return
[...this.mutationStack].reverse().forEach(({
type,
target,
attributeName,
@@ -54,8 +114,14 @@ class ElementOverrideManager {
addedNodes,
removedNodes,
}) => {
if (type === 'attributes') {
target.setAttribute(attributeName, oldValue)
if (type === 'attributes' && attributeName) {
let targetElement = target as HTMLElement
if (!oldValue) {
targetElement.removeAttribute(attributeName)
} else {
targetElement.setAttribute(attributeName, oldValue)
}
return
}
@@ -94,22 +160,21 @@ const applySnapshotMutations = ({
snapshotElementOverrides,
defaultWidth,
defaultHeight,
}) => {
let elementOverrideManager
if (Object.keys(snapshotElementOverrides).length) {
elementOverrideManager = new ElementOverrideManager()
}: SnapshotMutationOptions): Cypress.Chainable<() => void> => {
if (!Object.keys(snapshotElementOverrides).length) {
return cy.then(() => () => {})
}
const elementOverrideManager = new ElementOverrideManager()
return cy.viewport(snapshotWidth, snapshotHeight, { log: false })
.then(() => {
if (elementOverrideManager) {
elementOverrideManager.performOverrides(cy, snapshotElementOverrides)
}
elementOverrideManager.performOverrides(cy, snapshotElementOverrides)
if (log) {
Cypress.log({
message: log,
// @ts-ignore
snapshot: true,
end: true,
})
@@ -118,15 +183,13 @@ const applySnapshotMutations = ({
return () => {
cy.viewport(defaultWidth, defaultHeight, { log: false })
.then(() => {
if (elementOverrideManager) {
elementOverrideManager.resetOverrides()
}
elementOverrideManager.resetOverrides()
})
}
})
}
export const installCustomPercyCommand = ({ before, elementOverrides } = {}) => {
export const installCustomPercyCommand = ({ before, elementOverrides }: {before?: () => void, elementOverrides?: CustomSnapshotOptions['elementOverrides'] } = {}) => {
/**
* A custom Percy command that allows for additional mutations prior to snapshot generation. Mutations will be
* reset after snapshot generation so that the AUT is not polluted after the command executes.
@@ -138,28 +201,27 @@ export const installCustomPercyCommand = ({ before, elementOverrides } = {}) =>
* @param {Object} options.elementOverrides The desired snapshot overrides. These will be merged with and take
* precedence over the global override defined when the command was installed.
*/
const customPercySnapshot = (origFn, name, options = {}) => {
if (_.isObject(name)) {
const customPercySnapshot = (percySnapshot: (name?: string, options?: SnapshotOptions) => Cypress.Chainable<any>, name?: string, options: CustomSnapshotOptions = {}) => {
if (name && typeof name === 'object') {
options = name
name = null
name = undefined
}
/**
* @type {Mocha.Test}
*/
const test = cy.state('test')
// @ts-ignore
const test: Mocha.Test = cy.state('test')
const titlePath = test.titlePath()
const screenshotName = titlePath.concat(name).filter(Boolean).join(' > ')
const screenshotName = name ? titlePath.concat(name).filter(Boolean).join(' > ') : ''
// viewport data is read from test state rather than config to ensure that
// the snapshot is presented at the test's expected size.
const { viewportWidth, viewportHeight } = cy.state()
const snapshotWidth = !_.isNil(options.width) ? options.width : viewportWidth
const snapshotHeight = !_.isNil(options.height) ? options.height : viewportHeight
// @ts-ignore
const { viewportWidth, viewportHeight } = cy.state() as Cypress.Config
const snapshotWidth = options.width ? options.width : viewportWidth
const snapshotHeight = options.height ? options.height : viewportHeight
const snapshotMutationOptions = {
const snapshotMutationOptions: SnapshotMutationOptions = {
defaultWidth: viewportWidth,
defaultHeight: viewportHeight,
snapshotWidth,
@@ -181,7 +243,7 @@ export const installCustomPercyCommand = ({ before, elementOverrides } = {}) =>
}).then((reset) => reset())
}
if (_.isFunction(before)) {
if (before && typeof before === 'function') {
before()
}
@@ -198,11 +260,14 @@ export const installCustomPercyCommand = ({ before, elementOverrides } = {}) =>
.then((reset) => {
// Wrap in cy.then here to ensure that the original command is
// enqueued appropriately.
cy.then(() => {
return origFn(screenshotName, {
cy
.then(() => {
return percySnapshot(screenshotName, {
...options,
widths: [snapshotWidth],
})
}).then(() => {
})
.then(() => {
return reset()
})
.then(() => {
+2
View File
@@ -22,6 +22,8 @@
"@cypress/react-tooltip": "^0.5.3",
"@cypress/webpack-preprocessor": "0.0.0-development",
"@fortawesome/fontawesome-free": "6.0.0",
"@percy/core": "^1.0.0-beta.48",
"@percy/cypress": "^3.1.0",
"@reach/dialog": "0.10.5",
"@reach/visually-hidden": "0.10.4",
"babel-loader": "8.1.0",
+1 -1
View File
@@ -12,7 +12,7 @@ Can't run because no spec files were found.
We searched for specs matching this glob pattern:
> /foo/bar/.projects/no-specs-custom-pattern/src/**/*.cy.{js,jsx}
> /foo/bar/.projects/no-specs-custom-pattern/src/**/*.{cy,spec}.{js,jsx}
`
@@ -11,6 +11,6 @@ module.exports = {
},
e2e: {
supportFile: false,
specPattern: 'src/**/*.cy.{js,jsx}',
specPattern: 'src/**/*.{cy,spec}.{js,jsx}',
},
}
+32 -14
View File
@@ -9977,6 +9977,11 @@
resolved "https://registry.yarnpkg.com/@types/overlayscrollbars/-/overlayscrollbars-1.12.0.tgz#98456caceca8ad73bd5bb572632a585074e70764"
integrity sha512-h/pScHNKi4mb+TrJGDon8Yb06ujFG0mSg12wIO0sWMUF3dQIe2ExRRdNRviaNt9IjxIiOfnRr7FsQAdHwK4sMg==
"@types/parse-glob@3.0.29":
version "3.0.29"
resolved "https://registry.yarnpkg.com/@types/parse-glob/-/parse-glob-3.0.29.tgz#6a40ec7ebd2418ee69ee397e48e42169268a10bf"
integrity sha1-akDsfr0kGO5p7jl+SOQhaSaKEL8=
"@types/parse-json@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
@@ -19590,6 +19595,11 @@ downshift@^6.0.15:
prop-types "^15.7.2"
react-is "^17.0.2"
drange@^1.0.2:
version "1.1.1"
resolved "https://registry.yarnpkg.com/drange/-/drange-1.1.1.tgz#b2aecec2aab82fcef11dbbd7b9e32b83f8f6c0b8"
integrity sha512-pYxfDYpued//QpnLIm4Avk7rsNtAtQkUES2cwAYSvD/wd2pKD71gN2Ebj3e7klzXwjocvE8c5vx/1fxwpqmSxA==
dts-critic@latest:
version "3.3.4"
resolved "https://registry.yarnpkg.com/dts-critic/-/dts-critic-3.3.4.tgz#c15b7d4724190b8afaca7646f38271332f46dad7"
@@ -30110,6 +30120,14 @@ microevent.ts@~0.1.1:
resolved "https://registry.yarnpkg.com/microevent.ts/-/microevent.ts-0.1.1.tgz#70b09b83f43df5172d0205a63025bce0f7357fa0"
integrity sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g==
micromatch@4.0.4, micromatch@^4.0.0, micromatch@^4.0.2, micromatch@^4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9"
integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==
dependencies:
braces "^3.0.1"
picomatch "^2.2.3"
micromatch@^2.3.7:
version "2.3.11"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565"
@@ -30148,14 +30166,6 @@ micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4, micromatch@^3.1.9:
snapdragon "^0.8.1"
to-regex "^3.0.2"
micromatch@^4.0.0, micromatch@^4.0.2, micromatch@^4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9"
integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==
dependencies:
braces "^3.0.1"
picomatch "^2.2.3"
miller-rabin@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d"
@@ -33321,7 +33331,7 @@ parse-github-repo-url@^1.3.0:
resolved "https://registry.yarnpkg.com/parse-github-repo-url/-/parse-github-repo-url-1.4.1.tgz#9e7d8bb252a6cb6ba42595060b7bf6df3dbc1f50"
integrity sha1-nn2LslKmy2ukJZUGC3v23z28H1A=
parse-glob@^3.0.4:
parse-glob@3.0.4, parse-glob@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c"
integrity sha1-ssN2z7EfNVE7rdFz7wu246OIORw=
@@ -35866,6 +35876,14 @@ randexp@0.4.6:
discontinuous-range "1.0.0"
ret "~0.1.10"
randexp@0.5.3:
version "0.5.3"
resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.5.3.tgz#f31c2de3148b30bdeb84b7c3f59b0ebb9fec3738"
integrity sha512-U+5l2KrcMNOUPYvazA3h5ekF80FHTUG+87SEAmHZmolh1M+i/WyTCxVzmi+tidIa1tM4BSe8g2Y/D3loWDjj+w==
dependencies:
drange "^1.0.2"
ret "^0.2.0"
random-bytes@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b"
@@ -37896,6 +37914,11 @@ restore-cursor@^3.1.0:
onetime "^5.1.0"
signal-exit "^3.0.2"
ret@^0.2.0:
version "0.2.2"
resolved "https://registry.yarnpkg.com/ret/-/ret-0.2.2.tgz#b6861782a1f4762dce43402a71eb7a283f44573c"
integrity sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==
ret@~0.1.10:
version "0.1.15"
resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
@@ -39908,11 +39931,6 @@ speed-measure-webpack-plugin@1.4.2:
dependencies:
chalk "^4.1.0"
spin.js@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/spin.js/-/spin.js-4.1.1.tgz#567464a08620541e523da856cb5f67af2d0f48ad"
integrity sha512-3cjbjZBw8TmZmvzcmlXqArUpefJ1vGgQZ+dh1CdyDyxZZNxNmw+2Dq5jyoP/OCqQP+z78rWgSJX9m3uMuGaxxw==
split-ca@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz#6c83aff3692fa61256e0cd197e05e9de157691a6"