Merge branch 'develop' into release/15.0.0

This commit is contained in:
Jennifer Shehane
2025-05-05 09:36:42 -04:00
committed by GitHub
94 changed files with 3418 additions and 1243 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
version: 2.1
chrome-stable-version: &chrome-stable-version "135.0.7049.114"
chrome-beta-version: &chrome-beta-version "136.0.7103.33"
chrome-beta-version: &chrome-beta-version "136.0.7103.48"
firefox-stable-version: &firefox-stable-version "137.0"
orbs:
+10
View File
@@ -21,8 +21,18 @@ _Released 07/01/2025 (PENDING)_
_Released 5/6/2025 (PENDING)_
**Performance:**
- Ensure the previous pausing event handlers are removed before new ones are added. Addressed in [#31596](https://github.com/cypress-io/cypress/pull/31596).
**Bugfixes:**
- Fixed an issue where the configuration setting `trashAssetsBeforeRuns=false` was ignored for assets in the `videosFolder`. These assets were incorrectly deleted before running tests with `cypress run`. Addresses [#8280](https://github.com/cypress-io/cypress/issues/8280).
- Fixed a potential hang condition when navigating to `about:blank`. Addressed in [#31634](https://github.com/cypress-io/cypress/pull/31634).
**Misc:**
- The Assertions menu when you right click in `experimentalStudio` tests now displays in dark mode. Addresses [#10621](https://github.com/cypress-io/cypress-services/issues/10621). Addressed in [#31598](https://github.com/cypress-io/cypress/pull/31598).
- The URL in the Cypress App no longer displays a white background when the URL is loading. Fixes [#31556](https://github.com/cypress-io/cypress/issues/31556).
## 14.3.2
-1
View File
@@ -3324,7 +3324,6 @@ declare namespace Cypress {
spec: Cypress['spec'] | null
specs: Array<Cypress['spec']>
isDefaultProtocolEnabled: boolean
isStudioProtocolEnabled: boolean
hideCommandLog: boolean
hideRunnerUi: boolean
}
+2
View File
@@ -19,7 +19,9 @@ For general contributor information, check out [`CONTRIBUTING.md`](../CONTRIBUTI
* [Error handling](./error-handling.md)
* [GraphQL Subscriptions - Overview and Test Guide](./graphql-subscriptions.md)
* [Patching packages](./patch-package.md)
* [Protocol development](./protocol-development.md)
* [Release process](./release-process.md)
* [Studio development](./studio-development.md)
* [Testing other projects](./testing-other-projects.md)
* [Testing strategy and style guide (draft)](./testing-strategy-and-styleguide.md)
* [Writing cross-platform JavaScript](./writing-cross-platform-javascript.md)
+78 -3
View File
@@ -1,14 +1,33 @@
# Studio Development
In production, the code used to facilitate Studio functionality will be retrieved from the Cloud. However, in order to develop locally, developers will:
In production, the code used to facilitate Studio functionality will be retrieved from the Cloud. While Studio is still in its early stages it is hidden behind an environment variable: `CYPRESS_ENABLE_CLOUD_STUDIO` but can also be run against local cloud Studio code via the environment variable: `CYPRESS_LOCAL_STUDIO_PATH`.
To run against locally developed Studio:
- Clone the `cypress-services` repo (this requires that you be a member of the Cypress organization)
- Run `yarn`
- Run `yarn watch` in `app/studio`
- Set `CYPRESS_LOCAL_STUDIO_PATH` to the path to the `cypress-services/app/studio/dist/development` directory
- Run `yarn watch` in `app/packages/studio`
- Set:
- `CYPRESS_INTERNAL_ENV=<environment>` (e.g. `staging` or `production` if you want to hit those deployments of `cypress-services` or `development` if you want to hit a locally running version of `cypress-services`)
- `CYPRESS_LOCAL_STUDIO_PATH` to the path to the `cypress-services/app/packages/studio/dist/development` directory
To run against a deployed version of studio:
- Set:
- `CYPRESS_INTERNAL_ENV=<environment>` (e.g. `staging` or `production` if you want to hit those deployments of `cypress-services` or `development` if you want to hit a locally running version of `cypress-services`)
- `CYPRESS_ENABLE_CLOUD_STUDIO=true`
Regardless of running against local or deployed studio:
- Clone the `cypress` repo
- Run `yarn`
- Run `yarn cypress:open`
- Log In to the Cloud via the App
- Ensure the project has been setup in the `Cypress (staging)` if in staging environment or `Cypress Internal Org` if in production environment and has a `projectId` that represents that. If developing against locally running `cypress-services`, ensure that the project has the feature `studio-ai` enabled for it.
- Open a project that has `experimentalStudio: true` set in the `e2e` config of the `cypress.config.js|ts` file.
- Click to 'Add Commands to Test' after hovering over a test command.
Note: When using the `CYPRESS_LOCAL_STUDIO_PATH` environment variable or when running the Cypress app via the locally cloned repository, we bypass our error reporting and instead log errors to the browser or node console.
## Types
@@ -23,3 +42,59 @@ or to reference a local `cypress_services` repo:
```sh
CYPRESS_LOCAL_STUDIO_PATH=<path-to-cypress-services/app/studio/dist/development-directory> yarn gulp downloadStudioTypes
```
## Testing
### Unit/Component Testing
The code that supports cloud Studio and lives in the `cypress` monorepo is unit and component tested in a similar fashion to the rest of the code in the repo. See the [contributing guide](https://github.com/cypress-io/cypress/blob/ad353fcc0f7fdc51b8e624a2a1ef4e76ef9400a0/CONTRIBUTING.md?plain=1#L366) for more specifics.
The code that supports cloud Studio and lives in the `cypress-services` monorepo has unit and component tests that live alongside the code in that monorepo.
### Cypress in Cypress Testing
Several helpers are provided to facilitate testing cloud Studio using Cypress in Cypress tests. The [helper file](https://github.com/cypress-io/cypress/blob/ad353fcc0f7fdc51b8e624a2a1ef4e76ef9400a0/packages/app/cypress/e2e/studio/helper.ts) provides a method, `launchStudio` that:
1. Loads a project (by default [`experimental-studio`](https://github.com/cypress-io/cypress/tree/develop/system-tests/projects/experimental-studio)).
2. Navigates to the appropriate spec (by default `specName.cy.js`).
3. Enters Studio either by creating a new test or entering from an existing test via the `createNewTest` parameter
4. Waits for the test to finish executing again in Studio mode.
The above steps actually download the studio code from the cloud and use it for the test. Note that `experimental-studio` is set up to be a `canary` project so it will always get the latest and greatest of the cloud Studio code, whether or not it has been fully promoted to production. Note that this means that if you are writing Cypress in Cypress tests that depend on new functionality delivered from the cloud, the Cypress in Cypress tests cannot be merged until the code lands and is built in the cloud. Local development is still possible however by setting `process.env.CYPRESS_LOCAL_STUDIO_PATH` to your local studio path where we enable studio [here](https://github.com/cypress-io/cypress/blob/develop/packages/frontend-shared/cypress/e2e/e2ePluginSetup.ts#L424).
In order to properly engage with Studio AI, we choose to simulate the cloud interactions that enable it via something like:
```js
cy.mockNodeCloudRequest({
url: '/studio/testgen/n69px6/enabled',
method: 'get',
body: { enabled: true },
})
```
To ensure that we get the same results from our Studio AI calls every time, we simulate them via something like:
```js
const aiOutput = 'cy.get(\'button\').should(\'have.text\', \'Increment\')'
cy.mockNodeCloudStreamingRequest({
url: '/studio/testgen/n69px6/generate',
method: 'post',
body: { recommendations: [{ content: aiOutput }] },
})
```
The above two helpers actually mock out the Node requests so we still test the interface between the browser and node with these tests.
Also, since protocol does not work properly on the inner Cypress of Cypress in Cypress tests, we choose to create a dummy protocol which means we need to provide a simulated CDP full snapshot that will be sent to AI via something like:
```js
cy.mockStudioFullSnapshot({
id: 1,
nodeType: 1,
nodeName: 'div',
localName: 'div',
nodeValue: 'div',
children: [],
shadowRoots: [],
})
```
@@ -214,8 +214,7 @@ describe('Cypress In Cypress E2E', { viewportWidth: 1500, defaultCommandTimeout:
cy.specsPageIsVisible()
cy.contains('withFailure.spec').click()
cy.contains('[aria-controls=reporter-inline-specs-list]', 'Specs')
// A bit of a hack, but our cy-in-cy test needs to wait for the reporter to fully render before pressing the "f" key to expand the "Search specs" menu.
// Otherwise, the "f" keypress happens before the event is registered, which causes the "Search Specs" menu to not expand.
cy.get('[data-cy="runnable-header"]').should('be.visible')
cy.get('body').type('f')
cy.contains('Search specs')
@@ -11,6 +11,7 @@ describe('Reporter Header', () => {
})
it('selects the correct spec in the Specs List', () => {
cy.get('[data-cy="runnable-header"]').should('be.visible')
cy.get('body').type('f')
cy.get('[data-selected-spec="true"]').should('contain', 'dom-content').should('have.length', '1')
@@ -19,6 +20,7 @@ describe('Reporter Header', () => {
// TODO: Reenable as part of https://github.com/cypress-io/cypress/issues/23902
it.skip('filters the list of specs when searching for specs', () => {
cy.get('[data-cy="runnable-header"]').should('be.visible')
cy.get('body').type('f')
cy.findByTestId('specs-list-panel').within(() => {
@@ -1,11 +1,11 @@
import { loadSpec } from './support/spec-loader'
import { loadSpec, shouldHaveTestResults } from './support/spec-loader'
describe('event-manager', () => {
it('emits the cypress:created event when spec is rerun', () => {
// load the spec initially
loadSpec({
filePath: 'hooks/basic.cy.js',
passCount: 1,
passCount: 2,
})
cy.window().then((win) => {
@@ -26,4 +26,28 @@ describe('event-manager', () => {
cy.wrap(() => eventReceived).invoke('call').should('be.true')
})
})
it('clears the pause listeners when the spec is rerun', () => {
loadSpec({
filePath: 'hooks/basic.cy.js',
passCount: 2,
})
cy.window().then((win) => {
const eventManager = win.getEventManager()
cy.wrap(() => eventManager.reporterBus.listeners('runner:next').length).invoke('call').should('equal', 1)
// trigger a rerun
cy.get('.restart').click()
shouldHaveTestResults({
passCount: 2,
failCount: 0,
pendingCount: 0,
})
cy.wrap(() => eventManager.reporterBus.listeners('runner:next').length).invoke('call').should('equal', 1)
})
})
})
@@ -57,6 +57,7 @@ describe('plugin events', () => {
})
})
cy.get('[data-cy="runnable-header"]').should('be.visible')
cy.get('body').type('f')
cy.get('div[title="run_events_spec_2.cy.js"]').click()
cy.waitForSpecToFinish({
@@ -772,6 +772,7 @@ describe('runner/cypress sessions.open_mode.spec', () => {
})
it('persists global session and does not persists spec session when selecting a different spec', () => {
cy.get('[data-cy="runnable-header"]').should('be.visible')
cy.get('body').type('f')
cy.get('div[title="blank_session.cy.js"]').click()
@@ -27,6 +27,7 @@ describe('App: Spec List (Component)', () => {
it('highlights the currently running spec', () => {
cy.contains('fails').click()
cy.contains('[aria-controls=reporter-inline-specs-list]', 'Specs')
cy.get('[data-cy="runnable-header"]').should('be.visible')
cy.get('body').type('f')
cy.get('[data-selected-spec="true"]').should('contain', 'fails')
cy.get('[data-selected-spec="false"]').should('contain', 'foo')
@@ -121,6 +121,7 @@ describe('App: Spec List (E2E)', () => {
cy.contains('[aria-controls=reporter-inline-specs-list]', 'Specs')
cy.findByText('Your tests are loading...').should('not.be.visible')
cy.get('[data-cy="runnable-header"]').should('be.visible')
cy.get('body').type('f')
cy.get('[data-selected-spec="true"]').contains('dom-content.spec.js')
@@ -136,8 +137,6 @@ describe('App: Spec List (E2E)', () => {
cy.findByText('Your tests are loading...').should('not.be.visible')
cy.contains('[aria-controls=reporter-inline-specs-list]', 'Specs')
// A bit of a hack, but our cy-in-cy test needs to wait for the reporter to fully render before pressing the "f" key to expand the "Search specs" menu.
// Otherwise, the "f" keypress happens before the event is registered, which causes the "Search Specs" menu to not expand.
cy.get('[data-cy="runnable-header"]').should('be.visible')
// open the inline spec list
cy.get('body').type('f')
+110 -1
View File
@@ -56,11 +56,52 @@ describe('studio functionality', () => {
cy.window().then((win) => {
expect(win.Cypress.config('isDefaultProtocolEnabled')).to.be.false
expect(win.Cypress.config('isStudioProtocolEnabled')).to.be.true
expect(win.Cypress.state('isProtocolEnabled')).to.be.true
})
})
it('loads the studio UI correctly when studio bundle is taking too long to load', () => {
loadProjectAndRunSpec({ enableCloudStudio: false })
cy.window().then(() => {
cy.withCtx((ctx) => {
// Mock the studioLifecycleManager.getStudio method to return a hanging promise
if (ctx.coreData.studioLifecycleManager) {
const neverResolvingPromise = new Promise<null>(() => {})
ctx.coreData.studioLifecycleManager.getStudio = () => neverResolvingPromise
ctx.coreData.studioLifecycleManager.isStudioReady = () => false
}
})
})
cy.contains('visits a basic html page')
.closest('.runnable-wrapper')
.findByTestId('launch-studio')
.click()
cy.waitForSpecToFinish()
// Verify the cloud studio panel is not present
cy.findByTestId('studio-panel').should('not.exist')
cy.get('[data-cy="loading-studio-panel"]').should('not.exist')
cy.get('[data-cy="hook-name-studio commands"]').should('exist')
cy.getAutIframe().within(() => {
cy.get('#increment').realClick()
})
cy.findByTestId('hook-name-studio commands').closest('.hook-studio').within(() => {
cy.get('.command').should('have.length', 2)
cy.get('.command-name-get').should('contain.text', '#increment')
cy.get('.command-name-click').should('contain.text', 'click')
})
cy.get('button').contains('Save Commands').should('not.be.disabled')
})
it('does not display Studio button when not using cloud studio', () => {
loadProjectAndRunSpec({ })
@@ -126,6 +167,74 @@ describe('studio functionality', () => {
cy.percySnapshot()
})
it('opens a cloud studio session with AI enabled', () => {
cy.mockNodeCloudRequest({
url: '/studio/testgen/n69px6/enabled',
method: 'get',
body: { enabled: true },
})
const aiOutput = 'cy.get(\'button\').should(\'have.text\', \'Increment\')'
cy.mockNodeCloudStreamingRequest({
url: '/studio/testgen/n69px6/generate',
method: 'post',
body: { recommendations: [{ content: aiOutput }] },
})
cy.mockStudioFullSnapshot({
id: 1,
nodeType: 1,
nodeName: 'div',
localName: 'div',
nodeValue: 'div',
children: [],
shadowRoots: [],
})
const deferred = pDefer()
loadProjectAndRunSpec({ enableCloudStudio: true })
cy.findByTestId('studio-panel').should('not.exist')
cy.intercept('/cypress/e2e/index.html', () => {
// wait for the promise to resolve before responding
// this will ensure the studio panel is loaded before the test finishes
return deferred.promise
}).as('indexHtml')
cy.contains('visits a basic html page')
.closest('.runnable-wrapper')
.findByTestId('launch-studio')
.click()
// regular studio is not loaded until after the test finishes
cy.get('[data-cy="hook-name-studio commands"]').should('not.exist')
// cloud studio is loaded immediately
cy.findByTestId('studio-panel').then(() => {
// check for the loading panel from the app first
cy.get('[data-cy="loading-studio-panel"]').should('be.visible')
// we've verified the studio panel is loaded, now resolve the promise so the test can finish
deferred.resolve()
})
cy.wait('@indexHtml')
// Studio re-executes spec before waiting for commands - wait for the spec to finish executing.
cy.waitForSpecToFinish()
// Verify the studio panel is still open
cy.findByTestId('studio-panel')
cy.get('[data-cy="hook-name-studio commands"]')
// Verify that AI is enabled
cy.get('[data-cy="ai-status-text"]').should('contain.text', 'Enabled')
// Verify that the AI output is correct
cy.get('[data-cy="studio-ai-output-textarea"]').should('contain.text', aiOutput)
})
})
it('updates an existing test with an action', () => {
@@ -195,6 +195,7 @@ e2e: {
cy.waitForSpecToFinish()
cy.get('[data-model-state="passed"]').should('contain', 'renders the test content')
cy.get('[data-cy="runnable-header"]').should('be.visible')
cy.get('body').type('f')
cy.get('[data-cy="spec-file-item"]')
.should('have.length', 28)
@@ -221,6 +222,7 @@ e2e: {
cy.waitForSpecToFinish()
cy.get('[data-model-state="passed"]').should('contain', 'renders the test content')
cy.get('[data-cy="runnable-header"]').should('be.visible')
cy.get('body').type('f')
cy.get('[data-cy="spec-file-item"]')
.should('have.length', 28)
@@ -297,6 +299,7 @@ e2e: {
cy.waitForSpecToFinish()
cy.get('[data-model-state="passed"]').should('contain', 'renders the test content')
cy.get('[data-cy="runnable-header"]').should('be.visible')
cy.get('body').type('f')
cy.get('[data-cy="spec-file-item"]')
.should('have.length', 28)
+1 -1
View File
@@ -24,7 +24,7 @@
"devDependencies": {
"@cypress-design/icon-registry": "^1.5.1",
"@cypress-design/vue-button": "^1.6.0",
"@cypress-design/vue-icon": "^1.6.0",
"@cypress-design/vue-icon": "^1.18.0",
"@cypress-design/vue-spinner": "^1.0.0",
"@cypress-design/vue-statusicon": "^1.0.0",
"@cypress-design/vue-tabs": "^1.2.2",
@@ -248,11 +248,11 @@ const studioStatus = computed(() => {
})
const shouldShowStudioButton = computed(() => {
return !!props.gql.studio && !studioStore.isOpen
return !!props.gql.studio && studioStatus.value === 'ENABLED' && !studioStore.isOpen
})
const shouldShowStudioPanel = computed(() => {
return studioStatus.value === 'INITIALIZED' && (studioStore.isLoading || studioStore.isActive)
return studioStatus.value === 'ENABLED' && (studioStore.isLoading || studioStore.isActive)
})
const hideCommandLog = runnerUiStore.hideCommandLog
+2 -2
View File
@@ -129,8 +129,6 @@ export class AutIframe {
return
}
this.$iframe[0].src = 'about:blank'
this.$iframe.one('load', () => {
if (testIsolation) {
this._showTestIsolationBlankPage()
@@ -140,6 +138,8 @@ export class AutIframe {
resolve()
})
this.$iframe[0].src = 'about:blank'
})
}
+58
View File
@@ -0,0 +1,58 @@
import { getOrCreateHelperDom } from './dom'
describe('dom utilities', () => {
describe('getOrCreateHelperDom', () => {
let body: HTMLBodyElement
const className = 'test-helper'
const css = 'test-css'
beforeEach(() => {
// Create a fresh body element for each test
body = document.createElement('body')
document.body = body
})
afterEach(() => {
// Clean up after each test
const containers = body.querySelectorAll(`.${className}`)
containers.forEach((container) => container.remove())
})
it('should create new helper DOM elements when none exist', () => {
const result = getOrCreateHelperDom({ body, className, css })
// Verify container was created
expect(result.container).to.exist
expect(result.container.classList.contains(className)).to.be.true
expect(result.container.style.all).to.equal('initial')
expect(result.container.style.position).to.equal('static')
// Verify shadow root was created
expect(result.shadowRoot).to.exist
expect(result.shadowRoot!.mode).to.equal('open')
// Verify vue container was created
expect(result.vueContainer).to.exist
expect(result.vueContainer.classList.contains('vue-container')).to.be.true
// Verify style was added
const style = result.shadowRoot!.querySelector('style')
expect(style).to.exist
expect(style!.innerHTML).to.equal(css)
})
it('should return existing helper DOM elements when they exist', () => {
// First call to create elements
const firstResult = getOrCreateHelperDom({ body, className, css })
// Second call to get existing elements
const secondResult = getOrCreateHelperDom({ body, className, css })
// Verify we got the same elements back
expect(secondResult.container).to.equal(firstResult.container)
expect(secondResult.vueContainer).to.equal(firstResult.vueContainer)
})
})
})
+2
View File
@@ -30,6 +30,8 @@ export function getOrCreateHelperDom ({ body, className, css }) {
container.classList.add(className)
// NOTE: This is needed to prevent the container from inheriting styles from the body of the AUT
container.style.all = 'initial'
container.style.position = 'static'
body.appendChild(container)
+12 -2
View File
@@ -449,10 +449,20 @@ export class EventManager {
this.studioStore.setup(config)
const isDefaultProtocolEnabled = Cypress.config('isDefaultProtocolEnabled')
const isStudioProtocolEnabled = Cypress.config('isStudioProtocolEnabled')
const isStudioInScope = this.studioStore.isActive || this.studioStore.isLoading
Cypress.state('isProtocolEnabled', isDefaultProtocolEnabled || (isStudioProtocolEnabled && isStudioInScope))
if (isStudioInScope && !isDefaultProtocolEnabled) {
await new Promise<void>((resolve) => {
this.ws.emit('studio:protocol:enabled', ({ studioProtocolEnabled }) => {
Cypress.state('isProtocolEnabled', studioProtocolEnabled)
resolve()
})
})
} else {
Cypress.state('isProtocolEnabled', isDefaultProtocolEnabled)
}
this._addListeners()
}
@@ -75,7 +75,6 @@ export const addCaptureProtocolListeners = (Cypress: Cypress.Cypress) => {
}
Cypress.on('viewport:changed', viewportChangedHandler)
// @ts-expect-error
Cypress.primaryOriginCommunicator.on('viewport:changed', viewportChangedHandler)
Cypress.on('test:before:run:async', async (attributes) => {
+54 -20
View File
@@ -1,12 +1,18 @@
export const handlePausing = (getCypress, reporterBus) => {
const Cypress = getCypress()
// tracks whether the cy.pause() was called from the primary driver
// (value === null) or from a cross-origin spec bridge (value is the origin
// matching that spec bridge)
let sendEventsToOrigin = null
// tracks whether the cy.pause() was called from the primary driver
// (value === null) or from a cross-origin spec bridge (value is the origin
reporterBus.on('runner:next', () => {
const Cypress = getCypress()
import type EventEmitter from 'events'
// matching that spec bridge)
let sendEventsToOrigin: string | null = null
type GetCypressFunction = () => Cypress.Cypress
class PauseHandlers {
constructor (private getCypress: GetCypressFunction, private reporterBus: EventEmitter) {}
nextHandler = () => {
const Cypress = this.getCypress()
if (!Cypress) return
@@ -17,10 +23,10 @@ export const handlePausing = (getCypress, reporterBus) => {
} else {
Cypress.emit('resume:next')
}
})
}
reporterBus.on('runner:resume', () => {
const Cypress = getCypress()
resumeHandler = () => {
const Cypress = this.getCypress()
if (!Cypress) return
@@ -34,17 +40,45 @@ export const handlePausing = (getCypress, reporterBus) => {
// pause sequence is over - reset this for subsequent pauses
sendEventsToOrigin = null
})
}
// from the primary driver
Cypress.on('paused', (nextCommandName) => {
reporterBus.emit('paused', nextCommandName)
})
pausedHandler = (nextCommandName: string) => {
this.reporterBus.emit('paused', nextCommandName)
}
// from a cross-origin spec bridge
Cypress.primaryOriginCommunicator.on('paused', ({ nextCommandName, origin }) => {
crossOriginPausedHandler = ({ nextCommandName, origin }: { nextCommandName: string, origin: string }) => {
sendEventsToOrigin = origin
this.reporterBus.emit('paused', nextCommandName)
}
reporterBus.emit('paused', nextCommandName)
})
removeListeners = () => {
const Cypress = this.getCypress()
this.reporterBus.removeListener('runner:next', this.nextHandler)
this.reporterBus.removeListener('runner:resume', this.resumeHandler)
Cypress.removeListener('paused', this.pausedHandler)
Cypress.primaryOriginCommunicator.removeListener('paused', this.crossOriginPausedHandler)
}
addListeners = () => {
const Cypress = this.getCypress()
this.reporterBus.on('runner:next', this.nextHandler)
this.reporterBus.on('runner:resume', this.resumeHandler)
Cypress.on('paused', this.pausedHandler)
Cypress.primaryOriginCommunicator.on('paused', this.crossOriginPausedHandler)
}
}
let currentHandlers: PauseHandlers | null = null
export const handlePausing = (getCypress: GetCypressFunction, reporterBus: EventEmitter) => {
// Remove existing handlers if they exist
if (currentHandlers) {
currentHandlers.removeListeners()
}
// Create new handlers
currentHandlers = new PauseHandlers(getCypress, reporterBus)
currentHandlers.addListeners()
}
@@ -85,7 +85,15 @@ describe('SelectorPlayground', () => {
cy.get('[data-cy="playground-num-elements"]').contains('Invalid')
})
it('focuses and copies selector text', () => {
it('focuses playground selector', () => {
mountSelectorPlayground()
cy.get('[data-cy="playground-selector"]').as('copy').clear().type('.foo-bar')
cy.get('@copy').click()
cy.get('@copy').should('be.focused')
})
it('copies selector text', () => {
const copyStub = cy.stub()
cy.stubMutationResolver(Clipboard_CopyToClipboardDocument, (defineResult, { text }) => {
@@ -100,18 +108,13 @@ describe('SelectorPlayground', () => {
cy.spy(autIframe, 'toggleSelectorHighlight')
cy.get('[data-cy="playground-selector"]').as('copy').clear().type('.foo-bar')
cy.get('@copy').click()
cy.get('@copy').should('be.focused')
cy.get('[data-cy="playground-copy"]').trigger('mouseenter')
cy.get('[data-cy="selector-playground-tooltip"]').should('be.visible').contains('Copy to clipboard')
cy.get('[data-cy="playground-copy"]').click()
cy.get('[data-cy="selector-playground-tooltip"]').should('be.visible').contains('Copied!')
cy.wrap(copyStub).should('have.been.calledWith', 'cy.get(\'.foo-bar\')')
cy.wrap(copyStub).should('have.been.calledWith', 'cy.get(\'body\')')
})
it('prints elements when selected elements found', () => {
@@ -2,24 +2,32 @@
<div
ref="popper"
class="assertion-options"
data-cy="assertion-options"
>
<div
v-for="{ name, value } in options"
:key="`${name}${value}`"
v-for="option in options"
:key="getOptionKey(option)"
class="assertion-option"
@click.stop="() => onClick(name, value)"
data-cy="assertion-option"
tabindex="0"
role="button"
@keydown.enter="handleOptionClick(option)"
@keydown.space="handleOptionClick(option)"
@click.stop="handleOptionClick(option)"
>
<span
v-if="name"
v-if="option.name"
class="assertion-option-name"
data-cy="assertion-option-name"
>
{{ truncate(name) }}:{{ ' ' }}
{{ truncate(option.name) }}:{{ ' ' }}
</span>
<span
v-else
class="assertion-option-value"
data-cy="assertion-option-value"
>
{{ typeof value === 'string' && truncate(value) }}
{{ typeof option.value === 'string' && truncate(option.value) }}
</span>
</div>
</div>
@@ -30,45 +38,60 @@ import { createPopper } from '@popperjs/core'
import { onMounted, ref, nextTick, Ref } from 'vue'
import type { AssertionOption } from './types'
const props = defineProps<{
interface Props {
type: string
options: AssertionOption[]
}>()
}
const props = defineProps<Props>()
const emit = defineEmits<{
(eventName: 'addAssertion', value: { type: string, name: string, value: string })
(eventName: 'setPopperElement', value: HTMLElement)
}>()
const truncate = (str: string) => {
if (str && str.length > 80) {
return `${str.substr(0, 77)}...`
}
return str
}
const popper: Ref<HTMLElement | null> = ref(null)
onMounted(() => {
nextTick(() => {
const popperEl = popper.value as HTMLElement
const reference = popperEl.parentElement as HTMLElement
const TRUNCATE_LENGTH = 80
const TRUNCATE_SUFFIX = '...'
createPopper(reference, popperEl, {
placement: 'right-start',
})
const truncate = (str: string): string => {
if (!str || str.length <= TRUNCATE_LENGTH) {
return str
}
emit('setPopperElement', popperEl)
})
})
const onClick = (name, value) => {
emit('addAssertion', { type: props.type, name, value })
return `${str.substring(0, TRUNCATE_LENGTH - TRUNCATE_SUFFIX.length)}${TRUNCATE_SUFFIX}`
}
const getOptionKey = (option: AssertionOption): string => {
return `${option.name}${option.value}`
}
const handleOptionClick = (option: AssertionOption): void => {
emit('addAssertion', {
type: props.type,
name: option.name || '',
value: String(option.value || ''),
})
}
const initializePopper = (): void => {
const popperEl = popper.value as HTMLElement
const reference = popperEl.parentElement as HTMLElement
createPopper(reference, popperEl, {
placement: 'right-start',
})
emit('setPopperElement', popperEl)
}
onMounted(() => {
nextTick(initializePopper)
})
</script>
<style lang="scss">
<style scoped lang="scss">
@import './assertions-style.scss';
.assertion-options {
@@ -79,17 +102,35 @@ const onClick = (name, value) => {
overflow: hidden;
overflow-wrap: break-word;
position: absolute;
right: 8px;
border-radius: 4px;
.assertion-option {
font-size: 14px;
cursor: pointer;
padding: 0.4rem 0.6rem;
border: 1px solid transparent;
&:hover {
background-color: #e9ecef;
&:first-of-type {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.assertion-option-value {
font-weight: 600;
&:last-of-type {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
&:hover {
background-color: $gray-1000;
border: 1px solid $gray-950;
}
&:focus {
background-color: $gray-950;
color: $indigo-300;
outline: none;
@include box-shadow;
}
}
}
@@ -1,9 +1,17 @@
<template>
<div
:class="['assertion-type', { 'single-assertion': !hasOptions }]"
tabindex="0"
role="button"
:aria-expanded="isOpen"
:aria-haspopup="hasOptions"
@click.stop="onClick"
@mouseover.stop="onOpen"
@mouseout.stop="onClose"
@focus="onOpen"
@blur="onClose"
@keydown.enter="onClick"
@keydown.space="onClick"
>
<div class="assertion-type-text">
<span>
@@ -13,24 +21,13 @@
v-if="hasOptions"
class="dropdown-arrow"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="12"
height="12"
fill="currentColor"
viewBox="0 0 16 16"
>
<path
fillRule="evenodd"
d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"
/>
</svg>
<IconChevronRightMedium />
</span>
</div>
<AssertionOptions
v-if="hasOptions && isOpen"
:type="type"
:options="options"
:options="options || []"
@set-popper-element="setPopperElement"
@add-assertion="addAssertion"
/>
@@ -40,10 +37,12 @@
<script lang="ts" setup>
import { Ref, ref } from 'vue'
import AssertionOptions from './AssertionOptions.ce.vue'
import { IconChevronRightMedium } from '@cypress-design/vue-icon'
import type { AssertionType } from './types'
const props = defineProps<{
type: string
options: any
type: AssertionType['type']
options: AssertionType['options']
}>()
const emit = defineEmits<{
@@ -58,7 +57,7 @@ const onOpen = () => {
isOpen.value = true
}
const onClose = (e: MouseEvent) => {
const onClose = (e: MouseEvent | FocusEvent) => {
if (e.relatedTarget instanceof Element &&
popperElement.value && popperElement.value.contains(e.relatedTarget)) {
return
@@ -82,38 +81,47 @@ const addAssertion = ({ type, name, value }) => {
}
</script>
<style lang="scss">
<style scoped lang="scss">
@import './assertions-style.scss';
.assertion-type {
color: #202020;
cursor: default;
font-size: 14px;
padding: 0.4rem 0.4rem 0.4rem 0.7rem;
position: static;
outline: none;
border-radius: 4px;
border: 1px solid transparent;
&:first-of-type {
padding-top: 0.5rem;
}
&:last-of-type {
border-bottom-left-radius: $border-radius;
border-bottom-right-radius: $border-radius;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
padding-bottom: 0.5rem;
}
&:hover {
background-color: #e9ecef;
background-color: $gray-1000;
border: 1px solid $gray-950;
}
&:focus {
color: $indigo-300;
outline: none;
@include box-shadow;
}
&.single-assertion {
cursor: pointer;
font-weight: 600;
}
.assertion-type-text {
align-items: center;
display: flex;
cursor: pointer;
.dropdown-arrow {
margin-left: auto;
@@ -8,23 +8,39 @@
ref="assertionsMenu"
class="assertions-menu"
>
<div class="header">
<div
class="header"
data-cy="assertions-menu-header"
>
<div class="title">
<span>Add Assertion</span>
<IconActionTap
size="16"
stroke-color="gray-500"
fill-color="gray-900"
/>
<span>Assert</span>
</div>
<div class="close-wrapper">
<a
data-cy="assertions-menu-close"
tabindex="0"
role="button"
class="close"
@keydown.enter="onClose"
@keydown.space="onClose"
@click.stop="onClose"
>&times;</a>
>
<IconActionDeleteSmall />
</a>
</div>
</div>
<div
class="subtitle"
data-cy="assertions-subtitle"
>
expect
Expect
{{ ' ' }}
<code>
<code class="code">
{{ tagName }}
</code>
{{ ' ' }}
@@ -49,6 +65,7 @@ import { createPopper } from '@popperjs/core'
import AssertionType from './AssertionType.ce.vue'
import _ from 'lodash'
import { nextTick, onMounted, Ref, ref, StyleValue } from 'vue'
import { IconActionDeleteSmall, IconActionTap } from '@cypress-design/vue-icon'
import type { PossibleAssertions, AddAssertion, AssertionArgs } from './types'
const props = defineProps <{
@@ -98,9 +115,19 @@ onMounted(() => {
})
</script>
<style lang="scss">
<style scoped lang="scss">
@import "./assertions-style.scss";
// NOTE: This is needed because the icon component css is not imported in this component
.icon-dark-gray-500 {
fill: $gray-500;
}
// NOTE: This is needed because the icon component css is not imported in this component
.icon-light-gray-900 {
fill: $gray-900;
}
.highlight {
background: rgba(159, 196, 231, 0.6);
border: solid 2px #9FC4E7;
@@ -110,52 +137,76 @@ onMounted(() => {
.assertions-menu {
@include menu-style;
font-family: 'Helvetica Neue', 'Arial', sans-serif;
font-weight: normal;
font-family: $font-system;
z-index: 2147483647;
width: 175px;
width: 225px;
position: absolute;
color: $gray-300;
.header {
align-items: center;
background: #07b282;
border-top-left-radius: $border-radius;
border-top-right-radius: $border-radius;
color: #fff;
background: $gray-1100;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
color: $gray-300;
display: flex;
padding: 0.5rem 0.7rem;
padding: 8px;
border-bottom: 1px solid $gray-900;
font-weight: 500;
.title {
font-size: 14px;
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
padding: 0.4rem 0.6rem;
span {
font-size: 14px;
font-weight: 500;
color: $gray-100;
}
}
.close-wrapper {
margin-left: auto;
margin-top: -2.5px;
margin-right: 8px;
.close {
font-size: 18px;
font-weight: 500;
&:hover, &:focus, &:active {
cursor: pointer;
color: #eee;
}
&:focus {
outline-color: #9aa2fc;
}
}
}
}
.subtitle {
border-bottom: 1px solid #c4c4c4;
color: #6b6b6b;
font-size: 13px;
font-style: italic;
font-weight: 400;
padding: 0.5rem 0.7rem;
border-bottom: 1px solid $gray-900;
padding: 14px 9px;
margin: 0 8px;
color: $gray-500;
font-size: 14px;
}
code {
font-weight: 600;
}
.code {
font-size: 12px;
font-weight: 500;
color: $white;
border-radius: 4px;
border: 1px solid $gray-900;
line-height: 20px;
padding: 4px;
}
.assertions-list {
padding: 8px;
}
}
</style>
@@ -0,0 +1,88 @@
import AssertionsMenu from './AssertionsMenu.ce.vue'
import AssertionType from './AssertionType.ce.vue'
import AssertionOptions from './AssertionOptions.ce.vue'
import type { PossibleAssertions, AddAssertion } from './types'
// Add styles to the document
const styleElement = document.createElement('style')
styleElement.textContent = `${AssertionsMenu.styles}\n${AssertionType.styles}\n${AssertionOptions.styles}`
document.head.appendChild(styleElement)
describe('AssertionsMenu', () => {
const mockPossibleAssertions: PossibleAssertions = [
{
type: 'have.text',
options: [
{ value: 'Test Element' },
],
},
{
type: 'have.attr',
options: [
{ name: 'aria-label', value: 'Hello World' },
{ name: 'name', value: 'foo' },
{ name: 'id', value: 'bar' },
],
},
{
type: 'be.visible',
options: [],
},
]
let mockAddAssertion: AddAssertion
let mockCloseMenu: () => void
let defaultProps: any
beforeEach(() => {
mockAddAssertion = cy.stub()
mockCloseMenu = cy.stub()
// Create a real jQuery element
const $el = Cypress.$('<div class="test-element">Test Element</div>').appendTo('body')
defaultProps = {
jqueryElement: $el,
possibleAssertions: mockPossibleAssertions,
addAssertion: mockAddAssertion,
closeMenu: mockCloseMenu,
}
cy.viewport(500, 500)
cy.mount(AssertionsMenu, {
props: defaultProps,
})
})
it('renders the menu with correct title and tag name', () => {
cy.get('[data-cy="assertions-menu-header"]')
.should('be.visible')
.and('contain', 'Assert')
cy.get('[data-cy="assertions-subtitle"]')
.should('be.visible')
.and('contain', 'Expect')
.and('contain', 'div')
})
it('is tabbable', () => {
cy.get('[data-cy="assertion-options"]').should('not.exist')
cy.press('Tab') // close
cy.get('[data-cy="assertions-menu-close"]').should('be.focused')
cy.press('Tab') // first assertion type
cy.press('Tab') // first assertion option
cy.get('[data-cy="assertion-options"]').should('contain', 'Test Element')
})
it('calls addAssertion when clicking a single assertion', () => {
cy.get('.assertion-type.single-assertion').click()
cy.wrap(mockAddAssertion).should('have.been.calledWith', Cypress.sinon.match.any, 'be.visible')
})
it('calls closeMenu when clicking the close button', () => {
cy.get('[data-cy="assertions-menu-close"]').click()
cy.wrap(mockCloseMenu).should('have.been.called')
})
})
@@ -1,8 +1,15 @@
$border-radius: 4px;
@mixin box-shadow {
border: 1px solid #9aa2fc;
box-shadow: 0 0 0 3px rgba(154, 162, 252, 0.35);
-webkit-box-shadow: 0 0 0 3px rgba(154, 162, 252, 0.35);
-moz-box-shadow: 0 0 0 3px rgba(154, 162, 252, 0.35);
}
@mixin menu-style {
background: #fff;
background: $gray-1100;
border: 1px solid #DDD;
box-shadow: 2px 5px 12px rgba(0, 0, 0, 0.2);
border-radius: $border-radius;
border-radius: 4px;
@include box-shadow;
}
+91 -86
View File
@@ -5,14 +5,7 @@ import AssertionOptions from './AssertionOptions.ce.vue'
import { getOrCreateHelperDom, getSelectorHighlightStyles } from '../dom'
import type { PossibleAssertions, AddAssertion } from './types'
function getStudioAssertionsMenuDom (body) {
return getOrCreateHelperDom({
body,
className: '__cypress-studio-assertions-menu',
css: `${AssertionsMenu.styles}\n${AssertionType.styles}\n${AssertionOptions.styles}`,
})
}
// Types
interface StudioAssertionsMenuArgs {
$el: JQuery<HTMLElement>
$body: JQuery<HTMLElement>
@@ -23,33 +16,84 @@ interface StudioAssertionsMenuArgs {
}
}
export function openStudioAssertionsMenu ({ $el, $body, props }: StudioAssertionsMenuArgs) {
const { vueContainer } = getStudioAssertionsMenuDom($body.get(0))
vueContainerListeners(vueContainer)
const selectorHighlightStyles = getSelectorHighlightStyles([$el.get(0)])[0]
mountAssertionsMenu(vueContainer, $el, props.possibleAssertions, props.addAssertion, props.closeMenu, selectorHighlightStyles)
interface EventTarget extends HTMLElement {
className: string
}
export function closeStudioAssertionsMenu ($body) {
const { container } = getStudioAssertionsMenuDom($body.get(0))
unmountAssertionsMenu()
container.remove()
}
// Constants
const EVENT_THROTTLE_MS = 100
// State
let app: App<Element> | null = null
let lastTarget: EventTarget | null = null
let lastTimeStamp = -1
const mountAssertionsMenu = (
// Helper functions
function getStudioAssertionsMenuDom (body: HTMLElement) {
return getOrCreateHelperDom({
body,
className: '__cypress-studio-assertions-menu',
css: `${AssertionsMenu.styles}\n${AssertionType.styles}\n${AssertionOptions.styles}`,
})
}
function classIncludes (el: EventTarget, className: string): boolean {
return typeof el.className === 'string' && el.className.includes(className)
}
function shouldThrottleEvent (e: MouseEvent): boolean {
return lastTarget === e.target && lastTimeStamp - e.timeStamp < EVENT_THROTTLE_MS
}
function dispatchEventToTarget (e: MouseEvent, targetClass: string): void {
const paths = e.composedPath()
for (const el of paths) {
if (classIncludes(el as EventTarget, targetClass)) {
el.dispatchEvent(new MouseEvent(e.type, e))
break
}
}
}
// Event handlers
function setupVueContainerListeners (vueContainer: HTMLElement): void {
vueContainer.addEventListener('click', (e) => {
const paths = e.composedPath()
for (const el of paths) {
if (classIncludes(el as EventTarget, 'single-assertion') ||
classIncludes(el as EventTarget, 'assertion-option') ||
(el instanceof HTMLElement && el.tagName === 'A' && classIncludes(el, 'close'))) {
el.dispatchEvent(new MouseEvent('click', e))
break
}
}
})
vueContainer.addEventListener('mouseover', (e) => {
dispatchEventToTarget(e, 'assertion-type')
})
vueContainer.addEventListener('mouseout', (e) => {
if (shouldThrottleEvent(e)) return
lastTarget = e.target as EventTarget
lastTimeStamp = e.timeStamp
dispatchEventToTarget(e, 'assertion-type')
})
}
// Component mounting
function mountAssertionsMenu (
container: Element,
jqueryElement: any,
possibleAssertions: any[],
addAssertion: any,
closeMenu: any,
jqueryElement: JQuery<HTMLElement>,
possibleAssertions: PossibleAssertions,
addAssertion: AddAssertion,
closeMenu: () => void,
highlightStyle: StyleValue,
) => {
): void {
app = createApp(AssertionsMenu, {
jqueryElement,
possibleAssertions,
@@ -61,73 +105,34 @@ const mountAssertionsMenu = (
app.mount(container)
}
const unmountAssertionsMenu = () => {
function unmountAssertionsMenu (): void {
if (app) {
app.unmount()
app = null
}
}
// TODO: remove these.
// For some reason, the root div of our AssertionsMenu app usually gets
// all the events and does not distribute the events to the children.
// So, we're manually distributing the events.
// But it causes duplicated events are sent to the same object, so we're filtering them.
// I failed to prove it's our problem or Vue's problem.
let lastTarget = null
let lastTimeStamp = -1
// Public API
export function openStudioAssertionsMenu ({ $el, $body, props }: StudioAssertionsMenuArgs): void {
const { vueContainer } = getStudioAssertionsMenuDom($body.get(0))
function vueContainerListeners (vueContainer) {
vueContainer.addEventListener('click', (e) => {
const paths = e.composedPath()
setupVueContainerListeners(vueContainer)
for (let i = 0; i < paths.length; i++) {
const el = paths[i] as HTMLElement
const selectorHighlightStyles = getSelectorHighlightStyles([$el.get(0)])[0]
if (classIncludes(el, 'single-assertion') ||
classIncludes(el, 'assertion-option') ||
(el.tagName === 'A' && classIncludes(el, 'close'))) {
el.dispatchEvent(new MouseEvent('click', e))
break
}
}
})
vueContainer.addEventListener('mouseover', (e) => {
const paths = e.composedPath()
for (let i = 0; i < paths.length; i++) {
const el = paths[i] as HTMLElement
if (classIncludes(el, 'assertion-type')) {
el.dispatchEvent(new MouseEvent('mouseover', e))
break
}
}
})
vueContainer.addEventListener('mouseout', (e) => {
// Sometimes, there is maximum call stack size exceeded error.
if (lastTarget === e.target && lastTimeStamp - e.timeStamp < 100) {
return
}
lastTarget = e.target
lastTimeStamp = e.timeStamp
const paths = e.composedPath()
for (let i = 0; i < paths.length; i++) {
const el = paths[i] as HTMLElement
if (classIncludes(el, 'assertion-type')) {
el.dispatchEvent(new MouseEvent('mouseout', e))
break
}
}
})
mountAssertionsMenu(
vueContainer,
$el,
props.possibleAssertions,
props.addAssertion,
props.closeMenu,
selectorHighlightStyles,
)
}
function classIncludes (el, className) {
return typeof el.className === 'string' && el.className.includes(className)
export function closeStudioAssertionsMenu ($body: JQuery<HTMLElement>): void {
const { container } = getStudioAssertionsMenuDom($body.get(0))
unmountAssertionsMenu()
container.remove()
}
+15 -5
View File
@@ -3,15 +3,25 @@ export interface AssertionOption {
value?: string | number | string[]
}
export type PossibleAssertions = Array<{ type: string, options?: AssertionOption[] }>
export interface AssertionType {
type: string
options?: AssertionOption[]
}
export type PossibleAssertions = AssertionType[]
// Single argument assertion: ['be.visible']
type AssertionArgs_1 = [string]
export type AssertionArgs_1 = [string]
// Two argument assertion: ['have.text', '<some text>']
type AssertionArgs_2 = [string, string]
export type AssertionArgs_2 = [string, string]
// Three argument assertion: ['have.attr', 'href', '<some value>']
type AssertionArgs_3 = [string, string, string]
export type AssertionArgs_3 = [string, string, string]
export type AssertionArgs = AssertionArgs_1 | AssertionArgs_2 | AssertionArgs_3
export type AddAssertion = ($el: HTMLElement | JQuery<HTMLElement>, ...args: AssertionArgs) => void
export type AddAssertion = (
$el: HTMLElement | JQuery<HTMLElement>,
...args: AssertionArgs
) => void
+36 -6
View File
@@ -1,8 +1,10 @@
export interface StudioPanelProps {
canAccessStudioAI: boolean
onStudioPanelClose: () => void
useStudioEventManager?: StudioEventManagerShape
onStudioPanelClose?: () => void
useRunnerStatus?: RunnerStatusShape
useTestContentRetriever?: TestContentRetrieverShape
useStudioAIStream?: StudioAIStreamShape
useCypress?: CypressShape
}
export type StudioPanelShape = (props: StudioPanelProps) => JSX.Element
@@ -18,21 +20,49 @@ CyEventEmitter & {
state: (key: string) => any
}
export interface StudioEventManagerProps {
Cypress: CypressInternal
export interface TestBlock {
content: string
testBodyPosition: {
contentStart: number
contentEnd: number
indentation: number
}
}
export type RunnerStatus = 'running' | 'finished'
export type StudioEventManagerShape = (props: StudioEventManagerProps) => {
export interface RunnerStatusProps {
Cypress: CypressInternal
}
export interface CypressProps {
Cypress: CypressInternal
}
export type CypressShape = (props: CypressProps) => {
currentCypress: CypressInternal
}
export type RunnerStatusShape = (props: RunnerStatusProps) => {
runnerStatus: RunnerStatus
testBlock: string | null
}
export interface StudioAIStreamProps {
canAccessStudioAI: boolean
AIOutputRef: { current: HTMLTextAreaElement | null }
runnerStatus: RunnerStatus
testCode?: string
isCreatingNewTest: boolean
}
export type StudioAIStreamShape = (props: StudioAIStreamProps) => void
export interface TestContentRetrieverProps {
Cypress: CypressInternal
}
export type TestContentRetrieverShape = (props: TestContentRetrieverProps) => {
isLoading: boolean
testBlock: TestBlock | null
isCreatingNewTest: boolean
}
@@ -105,7 +105,6 @@ exports['config/src/index .getDefaultValues returns list of public config keys 1
'socketIoCookie': '__socket',
'socketIoRoute': '/__socket',
'isDefaultProtocolEnabled': false,
'isStudioProtocolEnabled': false,
'hideCommandLog': false,
'hideRunnerUi': false,
}
@@ -197,7 +196,6 @@ exports['config/src/index .getDefaultValues returns list of public config keys f
'socketIoCookie': '__socket',
'socketIoRoute': '/__socket',
'isDefaultProtocolEnabled': false,
'isStudioProtocolEnabled': false,
'hideCommandLog': false,
'hideRunnerUi': false,
}
-5
View File
@@ -593,11 +593,6 @@ const runtimeOptions: Array<RuntimeConfigOption> = [
defaultValue: false,
validation: validate.isBoolean,
isInternal: true,
}, {
name: 'isStudioProtocolEnabled',
defaultValue: false,
validation: validate.isBoolean,
isInternal: true,
}, {
name: 'hideCommandLog',
defaultValue: false,
@@ -1,4 +1,4 @@
import { FoundBrowser, Editor, AllowedState, AllModeOptions, TestingType, BrowserStatus, PACKAGE_MANAGERS, AuthStateName, MIGRATION_STEPS, MigrationStep, StudioManagerShape } from '@packages/types'
import { FoundBrowser, Editor, AllowedState, AllModeOptions, TestingType, BrowserStatus, PACKAGE_MANAGERS, AuthStateName, MIGRATION_STEPS, MigrationStep, StudioLifecycleManagerShape } from '@packages/types'
import { WizardBundler, CT_FRAMEWORKS, resolveComponentFrameworkDefinition, ErroredFramework } from '@packages/scaffold-config'
import type { NexusGenObjects } from '@packages/graphql/src/gen/nxs.gen'
// tslint:disable-next-line no-implicit-dependencies - electron dep needs to be defined
@@ -164,7 +164,7 @@ export interface CoreDataShape {
cloudProject: CloudDataShape
eventCollectorSource: EventCollectorSource | null
didBrowserPreviouslyHaveUnexpectedExit: boolean
studio: StudioManagerShape | null
studioLifecycleManager?: StudioLifecycleManagerShape
}
/**
@@ -246,7 +246,7 @@ export function makeCoreData (modeOptions: Partial<AllModeOptions> = {}): CoreDa
},
eventCollectorSource: null,
didBrowserPreviouslyHaveUnexpectedExit: false,
studio: null,
studioLifecycleManager: undefined,
}
async function machineId (): Promise<string | null> {
@@ -57,7 +57,6 @@ export class HtmlDataSource {
'namespace',
'socketIoRoute',
'isDefaultProtocolEnabled',
'isStudioProtocolEnabled',
'hideCommandLog',
'hideRunnerUi',
]
+1 -2
View File
@@ -25,8 +25,7 @@ const omitConfigReadOnlyDifferences = (objectLikeConfig: Cypress.ObjectLike) =>
return
}
if ((overrideLevels === 'never' && configKey !== 'isDefaultProtocolEnabled') ||
(overrideLevels === 'never' && configKey !== 'isStudioProtocolEnabled')) {
if ((overrideLevels === 'never' && configKey !== 'isDefaultProtocolEnabled')) {
delete objectLikeConfig[configKey]
}
})
+7
View File
@@ -7,6 +7,12 @@ declare namespace Cypress {
interface Cypress {
runner: any
state: State
emit: import('eventemitter2').EventEmitter2['emit']
removeListener: import('eventemitter2').EventEmitter2['removeListener']
primaryOriginCommunicator: import('eventemitter2').EventEmitter2 & {
toSpecBridge: (origin: string, event: string, data?: any, responseEvent?: string) => void
userInvocationStack?: string
}
}
interface Actions {
@@ -23,6 +29,7 @@ declare namespace Cypress {
(action: 'test:after:run:async', fn: (attributes: ObjectLike, test: Mocha.Test) => void)
(action: 'cy:protocol-snapshot', fn: () => void)
(action: 'test:before:after:run:async', fn: (attributes: ObjectLike, test: Mocha.Test, options: ObjectLike) => void | Promise<any>): Cypress
(action: 'paused', fn: (nextCommandName: string) => void)
}
interface Backend {
+1 -5
View File
@@ -51,10 +51,7 @@ declare namespace Cypress {
}
sinon: sinon.SinonApi
utils: CypressUtils
state: State
events: Events
emit: (event: string, payload?: any) => void
primaryOriginCommunicator: import('../src/cross-origin/communicator').PrimaryOriginCommunicator
specBridgeCommunicator: import('../src/cross-origin/communicator').SpecBridgeCommunicator
mocha: $Mocha
configure: (config: Cypress.ObjectLike) => void
@@ -80,7 +77,6 @@ declare namespace Cypress {
interface TestConfigOverrides extends Cypress.TestConfigOverrides {
isDefaultProtocolEnabled?: boolean
isStudioProtocolEnabled?: boolean
}
interface ResolvedConfigOptions {
@@ -97,7 +93,7 @@ declare namespace Cypress {
(action: 'before:stability:release', fn: () => void)
(action: '_log:added', fn: (attributes: ObjectLike, log: Cypress.Log) => void): Cypress
(action: '_log:changed', fn: (attributes: ObjectLike, log: Cypress.Log) => void): Cypress
(action: 'paused', fn: (nextCommandName: string) => void)
(action: 'resume:all', fn: () => void)
}
interface Backend {
+1 -1
View File
@@ -15,7 +15,7 @@
},
"devDependencies": {
"cross-env": "6.0.3",
"cypress-example-kitchensink": "3.1.2",
"cypress-example-kitchensink": "4.0.0",
"gh-pages": "5.0.0",
"gulp": "4.0.2",
"gulp-clean": "0.4.0",
@@ -24,20 +24,24 @@ import path from 'path'
import execa from 'execa'
import _ from 'lodash'
import type { CyTaskResult, OpenGlobalModeOptions, RemoteGraphQLBatchInterceptor, RemoteGraphQLInterceptor, ResetOptionsResult, WithCtxInjected, WithCtxOptions } from '../support/e2e'
import type { CyTaskResult, OpenGlobalModeOptions, RemoteGraphQLBatchInterceptor, RemoteGraphQLInterceptor, ResetOptionsResult, WithCtxInjected, WithCtxOptions, MockNodeCloudRequestOptions, MockNodeCloudStreamingRequestOptions } from '../support/e2e'
import { fixtureDirs } from '@tooling/system-tests'
import * as inspector from 'inspector'
// tslint:disable-next-line: no-implicit-dependencies - requires cypress
import sinonChai from '@cypress/sinon-chai'
import sinon from 'sinon'
import fs from 'fs-extra'
import nock from 'nock'
import { CYPRESS_REMOTE_MANIFEST_URL, NPM_CYPRESS_REGISTRY_URL } from '@packages/types'
import { CloudQuery } from '@packages/graphql/test/stubCloudTypes'
import pDefer from 'p-defer'
import { Readable } from 'stream'
const pkg = require('@packages/root')
const dummyProtocolPath = path.join(__dirname, '../fixtures/dummy-protocol.js')
export interface InternalOpenProjectCapabilities {
cloudStudio: boolean
}
@@ -196,6 +200,8 @@ async function makeE2ETasks () {
*/
__internal__afterEach () {
delete process.env.CYPRESS_ENABLE_CLOUD_STUDIO
delete process.env.CYPRESS_IN_CYPRESS_MOCK_FULL_SNAPSHOT
nock.cleanAll()
return null
},
@@ -422,6 +428,9 @@ async function makeE2ETasks () {
async __internal_openProject ({ argv, projectName, capabilities }: InternalOpenProjectArgs): Promise<ResetOptionsResult> {
if (capabilities.cloudStudio) {
process.env.CYPRESS_ENABLE_CLOUD_STUDIO = 'true'
// Cypress in Cypress testing breaks pretty heavily in terms of the inner Cypress's protocol. For now, we essentially
// disable the protocol by using a dummy protocol that does nothing and allowing tests to mock studio full snapshots as needed.
process.env.CYPRESS_LOCAL_PROTOCOL_PATH = dummyProtocolPath
}
let projectMatched = false
@@ -463,6 +472,52 @@ async function makeE2ETasks () {
e2eServerPort: ctx.coreData.servers.appServerPort,
}
},
__internal_mockStudioFullSnapshot (fullSnapshot: Record<string, any>) {
// This is the outlet to provide a mock full snapshot for studio tests.
// This is necessary because protocol does not capture things properly in the inner Cypress
// when running in Cypress in Cypress.
process.env.CYPRESS_IN_CYPRESS_MOCK_FULL_SNAPSHOT = JSON.stringify(fullSnapshot)
return null
},
__internal_mockNodeCloudRequest ({ url, method, body }: MockNodeCloudRequestOptions) {
const nocked = nock('https://cloud.cypress.io', {
allowUnmocked: true,
})
nocked[method](url).reply(200, body)
return null
},
__internal_mockNodeCloudStreamingRequest ({ url, method, body }: MockNodeCloudStreamingRequestOptions) {
const nocked = nock('https://cloud.cypress.io', {
allowUnmocked: true,
})
// This format is exactly what is expected by our cloud streaming requests (currently just our
// interactions with studio AI). Note that this does not replicate how the event streaming
// works exactly, but it is good enough for these Cypress in Cypress purposes and we test
// the full functionality with all edge cases alongside the Studio cloud code.
nocked[method](url).reply(200, () => {
const stream = new Readable({
read () {
this.push('event: chunk\n')
this.push(`data: ${JSON.stringify(body)}\n\n`)
this.push('event: end\n')
this.push('data: \n\n')
this.push(null)
},
})
return stream
}, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
})
return null
},
async __internal_withCtx (obj: WithCtxObj): Promise<CyTaskResult<any>> {
const options: WithCtxInjected = {
...obj.options,
@@ -0,0 +1,58 @@
const { Readable } = require('stream')
class AppCaptureProtocol {
uploadStallSamplingInterval () {
return 0
}
cdpReconnect () {
return Promise.resolve()
}
responseEndedWithEmptyBody (options) {
return
}
responseStreamTimedOut (options) {
return
}
getDbMetadata () {
return {
offset: 0,
size: 0,
}
}
responseStreamReceived (options) {
return Readable.from([])
}
beforeSpec ({ workingDirectory, archivePath, dbPath, db }) {
}
addRunnables (runnables) {
}
commandLogAdded (log) {
}
commandLogChanged (log) {
}
viewportChanged (input) {
}
urlChanged (input) {
}
beforeTest (test) {
return Promise.resolve()
}
preAfterTest (test, options) {
return Promise.resolve()
}
afterTest (test) {
return Promise.resolve()
}
afterSpec () {
return Promise.resolve({ durations: {} })
}
connectToBrowser (cdpClient) {
return Promise.resolve()
}
pageLoading (input) {
}
resetTest (testId) {
}
}
module.exports = { AppCaptureProtocol }
@@ -12,6 +12,7 @@ import type sinon from 'sinon'
import type pDefer from 'p-defer'
import 'cypress-plugin-tab'
import type { Response } from 'cross-fetch'
import type nock from 'nock'
import type { E2ETaskMap, InternalOpenProjectCapabilities } from '../e2e/e2ePluginSetup'
import { installCustomPercyCommand } from './customPercyCommand'
@@ -80,6 +81,18 @@ export interface FindBrowsersOptions {
filter?(browser: Browser): boolean
}
export interface MockNodeCloudRequestOptions {
url: string
method: string
body: nock.Body
}
export interface MockNodeCloudStreamingRequestOptions {
url: string
method: string
body: nock.Body
}
export interface ValidateExternalLinkOptions {
/**
* The user-visible descriptor for the link. If omitted, the href
@@ -184,6 +197,20 @@ declare global {
* Get the AUT <iframe>. Useful for Cypress in Cypress tests.
*/
getAutIframe(): Chainable<JQuery<HTMLIFrameElement>>
/**
* Mocks a studio full snapshot as if it were captured in the inner Cypress's protocol.
* This is necessary because protocol does not capture things properly in the inner Cypress
* when running in Cypress in Cypress.
*/
mockStudioFullSnapshot(fullSnapshot: Record<string, any>): void
/**
* Mocks a node cloud request
*/
mockNodeCloudRequest(options: { url: string, method: string, body: nock.Body }): void
/**
* Mocks a node cloud streaming request
*/
mockNodeCloudStreamingRequest(options: { url: string, method: string, body: nock.Body }): void
}
}
@@ -260,6 +287,24 @@ function openProject (projectName: WithPrefix<ProjectFixtureDir>, argv: string[]
})
}
function mockStudioFullSnapshot (fullSnapshot: Record<string, any>) {
return logInternal({ name: 'mockStudioFullSnapshot' }, () => {
return taskInternal('__internal_mockStudioFullSnapshot', fullSnapshot)
})
}
function mockNodeCloudRequest (options: MockNodeCloudRequestOptions) {
return logInternal({ name: 'mockNodeCloudRequest' }, () => {
return taskInternal('__internal_mockNodeCloudRequest', options)
})
}
function mockNodeCloudStreamingRequest (options: MockNodeCloudStreamingRequestOptions) {
return logInternal({ name: 'mockNodeCloudStreamingRequest' }, () => {
return taskInternal('__internal_mockNodeCloudStreamingRequest', options)
})
}
function startAppServer (mode: 'component' | 'e2e' = 'e2e', options: { skipMockingPrompts: boolean } = { skipMockingPrompts: false }) {
const { name, family } = Cypress.browser
@@ -602,6 +647,9 @@ Cypress.Commands.add('remoteGraphQLInterceptBatched', remoteGraphQLInterceptBatc
Cypress.Commands.add('findBrowsers', findBrowsers)
Cypress.Commands.add('tabUntil', tabUntil)
Cypress.Commands.add('validateExternalLink', { prevSubject: ['optional', 'element'] }, validateExternalLink)
Cypress.Commands.add('mockStudioFullSnapshot', mockStudioFullSnapshot)
Cypress.Commands.add('mockNodeCloudRequest', mockNodeCloudRequest)
Cypress.Commands.add('mockNodeCloudStreamingRequest', mockNodeCloudStreamingRequest)
installCustomPercyCommand({
elementOverrides: {
+2 -1
View File
@@ -19,7 +19,7 @@
"tslint": "tslint --config ../ts/tslint.json --project ."
},
"dependencies": {
"@cypress-design/vue-icon": "^1.6.0",
"@cypress-design/vue-icon": "^1.18.0",
"@intlify/unplugin-vue-i18n": "4.0.0",
"@packages/data-context": "0.0.0-development",
"@urql/devtools": "2.0.3",
@@ -89,6 +89,7 @@
"lodash": "4.17.21",
"markdown-it": "13.0.1",
"modern-normalize": "1.1.0",
"nock": "13.2.9",
"p-defer": "^3.0.0",
"patch-package": "8.0.0",
"postcss": "^8.4.22",
+1
View File
@@ -2379,6 +2379,7 @@ type Studio {
}
enum StudioStatusType {
ENABLED
INITIALIZED
IN_ERROR
NOT_INITIALIZED
@@ -12,6 +12,7 @@ import { ErrorWrapper } from './gql-ErrorWrapper'
import { CachedUser } from './gql-CachedUser'
import { Cohort } from './gql-Cohorts'
import { Studio } from './gql-Studio'
import type { StudioStatusType } from '@packages/data-context/src/gen/graphcache-config.gen'
export const Query = objectType({
name: 'Query',
@@ -105,7 +106,17 @@ export const Query = objectType({
t.field('studio', {
type: Studio,
description: 'Data pertaining to studio and the studio manager that is loaded from the cloud',
resolve: (source, args, ctx) => ctx.coreData.studio,
resolve: async (source, args, ctx) => {
const isStudioReady = ctx.coreData.studioLifecycleManager?.isStudioReady()
if (!isStudioReady) {
return { status: 'INITIALIZED' as StudioStatusType }
}
const studio = await ctx.coreData.studioLifecycleManager?.getStudio()
return studio ? { status: studio.status } : null
},
})
t.nonNull.field('localSettings', {
+1
View File
@@ -8,6 +8,7 @@ $gray-600: #747994;
$gray-700: #5a5f7a;
$gray-800: #434861;
$gray-900: #2e3247;
$gray-950: #25283C;
$gray-1000: #1b1e2e;
$gray-1100: #161827;
@@ -0,0 +1,182 @@
import type { StudioManager } from './cloud/studio'
import { ProtocolManager } from './cloud/protocol'
import { getAndInitializeStudioManager } from './cloud/api/studio/get_and_initialize_studio_manager'
import Debug from 'debug'
import type { CloudDataSource } from '@packages/data-context/src/sources'
import type { Cfg } from './project-base'
import _ from 'lodash'
import type { DataContext } from '@packages/data-context'
import api from './cloud/api'
import { reportStudioError } from './cloud/api/studio/report_studio_error'
import { CloudRequest } from './cloud/api/cloud_request'
import { isRetryableError } from './cloud/network/is_retryable_error'
import { asyncRetry } from './util/async_retry'
import { postStudioSession } from './cloud/api/studio/post_studio_session'
const debug = Debug('cypress:server:studio-lifecycle-manager')
const routes = require('./cloud/routes')
export class StudioLifecycleManager {
private studioManagerPromise?: Promise<StudioManager | null>
private studioManager?: StudioManager
private listeners: ((studioManager: StudioManager) => void)[] = []
/**
* Initialize the studio manager and possibly set up protocol.
* Also registers this instance in the data context.
* @param projectId The project ID
* @param cloudDataSource The cloud data source
* @param cfg The project configuration
* @param debugData Debug data for the configuration
* @param ctx Data context to register this instance with
*/
initializeStudioManager ({
projectId,
cloudDataSource,
cfg,
debugData,
ctx,
}: {
projectId?: string
cloudDataSource: CloudDataSource
cfg: Cfg
debugData: any
ctx: DataContext
}): void {
debug('Initializing studio manager')
const studioManagerPromise = this.createStudioManager({
projectId,
cloudDataSource,
cfg,
debugData,
}).catch(async (error) => {
debug('Error during studio manager setup: %o', error)
const cloudEnv = (process.env.CYPRESS_CONFIG_ENV || process.env.CYPRESS_INTERNAL_ENV || 'production') as 'development' | 'staging' | 'production'
const cloudUrl = ctx.cloud.getCloudUrl(cloudEnv)
const cloudHeaders = await ctx.cloud.additionalHeaders()
reportStudioError({
cloudApi: {
cloudUrl,
cloudHeaders,
CloudRequest,
isRetryableError,
asyncRetry,
},
studioHash: projectId,
projectSlug: cfg.projectId,
error,
studioMethod: 'initializeStudioManager',
studioMethodArgs: [],
})
// Clean up any registered listeners
this.listeners = []
return null
})
this.studioManagerPromise = studioManagerPromise
// Register this instance in the data context
ctx.update((data) => {
data.studioLifecycleManager = this
})
}
isStudioReady (): boolean {
return !!this.studioManager
}
async getStudio () {
if (!this.studioManagerPromise) {
throw new Error('Studio manager has not been initialized')
}
return await this.studioManagerPromise
}
private async createStudioManager ({
projectId,
cloudDataSource,
cfg,
debugData,
}: {
projectId?: string
cloudDataSource: CloudDataSource
cfg: Cfg
debugData: any
}): Promise<StudioManager> {
const studioSession = await postStudioSession({
projectId,
})
const studioManager = await getAndInitializeStudioManager({
studioUrl: studioSession.studioUrl,
projectId,
cloudDataSource,
})
if (studioManager.status === 'ENABLED') {
debug('Cloud studio is enabled - setting up protocol')
const protocolManager = new ProtocolManager()
const script = await api.getCaptureProtocolScript(studioSession.protocolUrl)
await protocolManager.prepareProtocol(script, {
runId: 'studio',
projectId: cfg.projectId,
testingType: cfg.testingType,
cloudApi: {
url: routes.apiUrl,
retryWithBackoff: api.retryWithBackoff,
requestPromise: api.rp,
},
projectConfig: _.pick(cfg, ['devServerPublicPathRoute', 'port', 'proxyUrl', 'namespace']),
mountVersion: api.runnerCapabilities.protocolMountVersion,
debugData,
mode: 'studio',
})
studioManager.protocolManager = protocolManager
} else {
debug('Cloud studio is not enabled - skipping protocol setup')
}
debug('Studio is ready')
this.studioManager = studioManager
this.callRegisteredListeners()
return studioManager
}
private callRegisteredListeners () {
if (!this.studioManager) {
throw new Error('Studio manager has not been initialized')
}
const studioManager = this.studioManager
debug('Calling all studio ready listeners')
this.listeners.forEach((listener) => {
listener(studioManager)
})
this.listeners = []
}
/**
* Register a listener that will be called when the studio is ready
* @param listener Function to call when studio is ready
*/
registerStudioReadyListener (listener: (studioManager: StudioManager) => void): void {
// if there is already a studio manager, call the listener immediately
if (this.studioManager) {
debug('Studio ready - calling listener immediately')
listener(this.studioManager)
} else {
debug('Studio not ready - registering studio ready listener')
this.listeners.push(listener)
}
}
}
@@ -1,21 +1,25 @@
import path from 'path'
import os from 'os'
import { ensureDir, copy, readFile } from 'fs-extra'
import { StudioManager } from '../studio'
import { StudioManager } from '../../studio'
import tar from 'tar'
import { verifySignatureFromFile } from '../encryption'
import { verifySignatureFromFile } from '../../encryption'
import crypto from 'crypto'
import fs from 'fs'
import fetch from 'cross-fetch'
import { agent } from '@packages/network'
import { asyncRetry, linearDelay } from '../../util/async_retry'
import { isRetryableError } from '../network/is_retryable_error'
import { PUBLIC_KEY_VERSION } from '../constants'
import { CloudRequest } from './cloud_request'
import { asyncRetry, linearDelay } from '../../../util/async_retry'
import { isRetryableError } from '../../network/is_retryable_error'
import { PUBLIC_KEY_VERSION } from '../../constants'
import { CloudRequest } from '../cloud_request'
import type { CloudDataSource } from '@packages/data-context/src/sources'
interface Options {
studioUrl: string
projectId?: string
}
const pkg = require('@packages/root')
const routes = require('../routes')
const _delay = linearDelay(500)
@@ -24,11 +28,11 @@ export const studioPath = path.join(os.tmpdir(), 'cypress', 'studio')
const bundlePath = path.join(studioPath, 'bundle.tar')
const serverFilePath = path.join(studioPath, 'server', 'index.js')
const downloadStudioBundleToTempDirectory = async (projectId?: string): Promise<void> => {
const downloadStudioBundleToTempDirectory = async ({ studioUrl, projectId }: Options): Promise<void> => {
let responseSignature: string | null = null
await (asyncRetry(async () => {
const response = await fetch(routes.apiRoutes.studio() as string, {
const response = await fetch(studioUrl, {
// @ts-expect-error - this is supported
agent,
method: 'GET',
@@ -90,7 +94,7 @@ const getTarHash = (): Promise<string> => {
})
}
export const retrieveAndExtractStudioBundle = async ({ projectId }: { projectId?: string } = {}): Promise<{ studioHash: string | undefined }> => {
export const retrieveAndExtractStudioBundle = async ({ studioUrl, projectId }: Options): Promise<{ studioHash: string | undefined }> => {
// First remove studioPath to ensure we have a clean slate
await fs.promises.rm(studioPath, { recursive: true, force: true })
await ensureDir(studioPath)
@@ -106,7 +110,7 @@ export const retrieveAndExtractStudioBundle = async ({ projectId }: { projectId?
return { studioHash: undefined }
}
await downloadStudioBundleToTempDirectory(projectId)
await downloadStudioBundleToTempDirectory({ studioUrl, projectId })
const studioHash = await getTarHash()
@@ -118,20 +122,22 @@ export const retrieveAndExtractStudioBundle = async ({ projectId }: { projectId?
return { studioHash }
}
export const getAndInitializeStudioManager = async ({ projectId, cloudDataSource }: { projectId?: string, cloudDataSource: CloudDataSource }): Promise<StudioManager> => {
export const getAndInitializeStudioManager = async ({ studioUrl, projectId, cloudDataSource }: { studioUrl: string, projectId?: string, cloudDataSource: CloudDataSource }): Promise<StudioManager> => {
let script: string
const cloudEnv = (process.env.CYPRESS_CONFIG_ENV || process.env.CYPRESS_INTERNAL_ENV || 'production') as 'development' | 'staging' | 'production'
const cloudUrl = cloudDataSource.getCloudUrl(cloudEnv)
const cloudHeaders = await cloudDataSource.additionalHeaders()
let studioHash: string | undefined
try {
const { studioHash } = await retrieveAndExtractStudioBundle({ projectId })
({ studioHash } = await retrieveAndExtractStudioBundle({ studioUrl, projectId }))
script = await readFile(serverFilePath, 'utf8')
const studioManager = new StudioManager()
const cloudEnv = (process.env.CYPRESS_INTERNAL_ENV || 'production') as 'development' | 'staging' | 'production'
const cloudUrl = cloudDataSource.getCloudUrl(cloudEnv)
const cloudHeaders = await cloudDataSource.additionalHeaders()
await studioManager.setup({
script,
studioPath,
@@ -144,6 +150,7 @@ export const getAndInitializeStudioManager = async ({ projectId, cloudDataSource
isRetryableError,
asyncRetry,
},
shouldEnableStudio: !!(process.env.CYPRESS_ENABLE_CLOUD_STUDIO || process.env.CYPRESS_LOCAL_STUDIO_PATH),
})
return studioManager
@@ -156,7 +163,19 @@ export const getAndInitializeStudioManager = async ({ projectId, cloudDataSource
actualError = error
}
return StudioManager.createInErrorManager(actualError)
return StudioManager.createInErrorManager({
cloudApi: {
cloudUrl,
cloudHeaders,
CloudRequest,
isRetryableError,
asyncRetry,
},
studioHash,
projectSlug: projectId,
error: actualError,
studioMethod: 'getAndInitializeStudioManager',
})
} finally {
await fs.promises.rm(bundlePath, { force: true })
}
@@ -0,0 +1,45 @@
import { asyncRetry, linearDelay } from '../../../util/async_retry'
import { isRetryableError } from '../../network/is_retryable_error'
import fetch from 'cross-fetch'
import os from 'os'
import { agent } from '@packages/network'
const pkg = require('@packages/root')
const routes = require('../../routes') as typeof import('../../routes')
interface GetStudioSessionOptions {
projectId?: string
}
const _delay = linearDelay(500)
export const postStudioSession = async ({ projectId }: GetStudioSessionOptions) => {
return await (asyncRetry(async () => {
const response = await fetch(routes.apiRoutes.studioSession(), {
// @ts-expect-error - this is supported
agent,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-os-name': os.platform(),
'x-cypress-version': pkg.version,
},
body: JSON.stringify({ projectSlug: projectId, studioMountVersion: 1, protocolMountVersion: 2 }),
})
if (!response.ok) {
throw new Error('Failed to create studio session')
}
const data = await response.json()
return {
studioUrl: data.studioUrl,
protocolUrl: data.protocolUrl,
}
}, {
maxAttempts: 3,
retryDelay: _delay,
shouldRetry: isRetryableError,
}))()
}
@@ -0,0 +1,107 @@
import type { StudioCloudApi } from '@packages/types/src/studio/studio-server-types'
import Debug from 'debug'
import { stripPath } from '../../strip_path'
const debug = Debug('cypress:server:cloud:api:studio:report_studio_errors')
export interface ReportStudioErrorOptions {
cloudApi: StudioCloudApi
studioHash: string | undefined
projectSlug: string | undefined
error: unknown
studioMethod: string
studioMethodArgs?: unknown[]
}
interface StudioError {
name: string
stack: string
message: string
studioMethod: string
studioMethodArgs?: string
}
interface StudioErrorPayload {
studioHash: string | undefined
projectSlug: string | undefined
errors: StudioError[]
}
export function reportStudioError ({
cloudApi,
studioHash,
projectSlug,
error,
studioMethod,
studioMethodArgs,
}: ReportStudioErrorOptions): void {
debug('Error reported:', error)
// When developing locally, do not send to Sentry, but instead log to console.
if (
process.env.CYPRESS_LOCAL_STUDIO_PATH ||
process.env.NODE_ENV === 'development'
) {
// eslint-disable-next-line no-console
console.error(`Error in ${studioMethod}:`, error)
return
}
let errorObject: Error
if (!(error instanceof Error)) {
errorObject = new Error(String(error))
} else {
errorObject = error
}
let studioMethodArgsString: string | undefined
if (studioMethodArgs) {
try {
studioMethodArgsString = JSON.stringify({
args: studioMethodArgs,
})
} catch (e: unknown) {
studioMethodArgsString = `Unknown args: ${e}`
}
}
try {
const payload: StudioErrorPayload = {
studioHash,
projectSlug,
errors: [{
name: stripPath(errorObject.name ?? `Unknown name`),
stack: stripPath(errorObject.stack ?? `Unknown stack`),
message: stripPath(errorObject.message ?? `Unknown message`),
studioMethod,
studioMethodArgs: studioMethodArgsString,
}],
}
cloudApi.CloudRequest.post(
`${cloudApi.cloudUrl}/studio/errors`,
payload,
{
headers: {
'Content-Type': 'application/json',
...cloudApi.cloudHeaders,
},
},
).catch((e: unknown) => {
debug(
`Error calling StudioManager.reportError: %o, original error %o`,
e,
error,
)
})
} catch (e: unknown) {
debug(
`Error calling StudioManager.reportError: %o, original error %o`,
e,
error,
)
}
}
+1 -12
View File
@@ -4,18 +4,7 @@ const pkg = require('@packages/root')
const api = require('./api').default
const user = require('./user')
const system = require('../util/system')
// strip everything but the file name to remove any sensitive
// data in the path
const pathRe = /'?((\/|\\+|[a-z]:\\)[^\s']+)+'?/ig
const pathSepRe = /[\/\\]+/
const stripPath = (text) => {
return (text || '').replace(pathRe, (path) => {
const fileName = _.last(path.split(pathSepRe)) || ''
return `<stripped-path>${fileName}`
})
}
const { stripPath } = require('./strip_path')
export = {
getErr (err: Error) {
+1 -2
View File
@@ -16,8 +16,7 @@ const CLOUD_ENDPOINTS = {
instanceStdout: 'instances/:id/stdout',
instanceArtifacts: 'instances/:id/artifacts',
captureProtocolErrors: 'capture-protocol/errors',
captureProtocolCurrent: 'capture-protocol/script/current.js',
studio: 'studio/bundle/current.tgz',
studioSession: 'studio/session',
studioErrors: 'studio/errors',
exceptions: 'exceptions',
telemetry: 'telemetry',
+14
View File
@@ -0,0 +1,14 @@
import { last } from 'lodash'
// strip everything but the file name to remove any sensitive
// data in the path
const pathRe = /'?((\/|\\+|[a-z]:\\)[^\s']+)+'?/ig
const pathSepRe = /[\/\\]+/
export const stripPath = (text: string) => {
return (text || '').replace(pathRe, (path) => {
const fileName = last(path.split(pathSepRe)) || ''
return `<stripped-path>${fileName}`
})
}
+39 -44
View File
@@ -1,13 +1,10 @@
import type { StudioErrorReport, StudioManagerShape, StudioStatus, StudioServerDefaultShape, StudioServerShape, ProtocolManagerShape, StudioCloudApi, StudioAIInitializeOptions } from '@packages/types'
import type { StudioManagerShape, StudioStatus, StudioServerDefaultShape, StudioServerShape, ProtocolManagerShape, StudioCloudApi, StudioAIInitializeOptions, StudioEvent } from '@packages/types'
import type { Router } from 'express'
import type { Socket } from 'socket.io'
import fetch from 'cross-fetch'
import pkg from '@packages/root'
import os from 'os'
import { agent } from '@packages/network'
import Debug from 'debug'
import { requireScript } from './require_script'
import path from 'path'
import { reportStudioError, ReportStudioErrorOptions } from './api/studio/report_studio_error'
interface StudioServer { default: StudioServerDefaultShape }
@@ -17,40 +14,45 @@ interface SetupOptions {
studioHash?: string
projectSlug?: string
cloudApi: StudioCloudApi
shouldEnableStudio: boolean
}
const debug = Debug('cypress:server:studio')
const routes = require('./routes')
export class StudioManager implements StudioManagerShape {
status: StudioStatus = 'NOT_INITIALIZED'
isProtocolEnabled: boolean = false
protocolManager: ProtocolManagerShape | undefined
private _studioServer: StudioServerShape | undefined
private _studioHash: string | undefined
static createInErrorManager (error: Error): StudioManager {
static createInErrorManager ({ cloudApi, studioHash, projectSlug, error, studioMethod, studioMethodArgs }: ReportStudioErrorOptions): StudioManager {
const manager = new StudioManager()
manager.status = 'IN_ERROR'
manager.reportError(error).catch(() => { })
reportStudioError({
cloudApi,
studioHash,
projectSlug,
error,
studioMethod,
studioMethodArgs,
})
return manager
}
async setup ({ script, studioPath, studioHash, projectSlug, cloudApi }: SetupOptions): Promise<void> {
async setup ({ script, studioPath, studioHash, projectSlug, cloudApi, shouldEnableStudio }: SetupOptions): Promise<void> {
const { createStudioServer } = requireScript<StudioServer>(script).default
this._studioServer = await createStudioServer({
studioHash,
studioPath,
projectSlug,
cloudApi,
betterSqlite3Path: path.dirname(require.resolve('better-sqlite3/package.json')),
})
this._studioHash = studioHash
this.status = 'INITIALIZED'
this.status = shouldEnableStudio ? 'ENABLED' : 'INITIALIZED'
}
initializeRoutes (router: Router): void {
@@ -59,6 +61,15 @@ export class StudioManager implements StudioManagerShape {
}
}
async captureStudioEvent (event: StudioEvent): Promise<void> {
if (this._studioServer) {
// this request is not essential - we don't want studio to error out if a telemetry request fails
return (await this.invokeAsync('captureStudioEvent', { isEssential: false }, event))
}
return Promise.resolve()
}
addSocketListeners (socket: Socket): void {
if (this._studioServer) {
this.invokeSync('addSocketListeners', { isEssential: true }, socket)
@@ -77,32 +88,11 @@ export class StudioManager implements StudioManagerShape {
await this.invokeAsync('destroy', { isEssential: true })
}
private async reportError (error: Error): Promise<void> {
reportError (error: unknown, studioMethod: string, ...studioMethodArgs: unknown[]): void {
try {
const payload: StudioErrorReport = {
studioHash: this._studioHash,
errors: [{
name: error.name ?? `Unknown name`,
stack: error.stack ?? `Unknown stack`,
message: error.message ?? `Unknown message`,
}],
}
const body = JSON.stringify(payload)
await fetch(routes.apiRoutes.studioErrors() as string, {
// @ts-expect-error - this is supported
agent,
method: 'POST',
body,
headers: {
'Content-Type': 'application/json',
'x-cypress-version': pkg.version,
'x-os-name': os.platform(),
'x-arch': os.arch(),
},
})
this._studioServer?.reportError(error, studioMethod, ...studioMethodArgs)
} catch (e) {
// If we fail to report the error, we shouldn't try and report it again
debug(`Error calling StudioManager.reportError: %o, original error %o`, e, error)
}
}
@@ -129,13 +119,16 @@ export class StudioManager implements StudioManagerShape {
}
this.status = 'IN_ERROR'
// Call and forget this, we don't want to block the main thread
this.reportError(actualError).catch(() => { })
this.reportError(actualError, method, ...args)
}
}
get isProtocolEnabled () {
return !!this.protocolManager
}
/**
* Abstracts invoking a synchronous method on the StudioServer instance, so we can handle
* Abstracts invoking an asynchronous method on the StudioServer instance, so we can handle
* errors in a uniform way
*/
private async invokeAsync <K extends StudioServerAsyncMethods> (method: K, { isEssential }: { isEssential: boolean }, ...args: Parameters<StudioServerShape[K]>): Promise<ReturnType<StudioServerShape[K]> | undefined> {
@@ -155,11 +148,13 @@ export class StudioManager implements StudioManagerShape {
actualError = error
}
this.status = 'IN_ERROR'
// Call and forget this, we don't want to block the main thread
this.reportError(actualError).catch(() => { })
// only set error state if this request is essential
if (isEssential) {
this.status = 'IN_ERROR'
}
this.reportError(actualError, method, ...args)
// TODO: Figure out errors
return undefined
}
}
-1
View File
@@ -176,7 +176,6 @@ const omitConfigKeys = [
'state',
'supportFolder',
'isDefaultProtocolEnabled',
'isStudioProtocolEnabled',
'hideCommandLog',
'hideRunnerUi',
]
+30 -7
View File
@@ -13,7 +13,7 @@ import Reporter from '../reporter'
import browserUtils from '../browsers'
import { openProject } from '../open_project'
import * as videoCapture from '../video_capture'
import { fs } from '../util/fs'
import { fs, getPath } from '../util/fs'
import runEvents from '../plugins/run_events'
import env from '../util/env'
import trash from '../util/trash'
@@ -23,6 +23,7 @@ import chromePolicyCheck from '../util/chrome_policy_check'
import type { SpecWithRelativeRoot, SpecFile, TestingType, OpenProjectLaunchOpts, FoundBrowser, BrowserVideoController, VideoRecording, ProcessOptions, ProtocolManagerShape, AutomationCommands } from '@packages/types'
import type { Cfg, ProjectBase } from '../project-base'
import type { Browser } from '../browsers/types'
import type { Data } from '../util/fs'
import * as printResults from '../util/print-run'
import { telemetry } from '@packages/telemetry'
import { CypressRunResult, createPublicBrowser, createPublicConfig, createPublicRunResults, createPublicSpec, createPublicSpecResults } from './results'
@@ -223,15 +224,30 @@ async function trashAssets (config: Cfg) {
}
}
async function startVideoRecording (options: { previous?: VideoRecording, project: Project, spec: SpecWithRelativeRoot, videosFolder: string }): Promise<VideoRecording> {
async function startVideoRecording (options: { previous?: VideoRecording, project: Project, spec: SpecWithRelativeRoot, videosFolder: string, overwrite: boolean }): Promise<VideoRecording> {
if (!options.videosFolder) throw new Error('Missing videoFolder for recording')
function videoPath (suffix: string) {
return path.join(options.videosFolder, options.spec.relativeToCommonRoot + suffix)
async function videoPath (ext: string, suffix: string = '') {
const specPath = options.spec.relativeToCommonRoot + suffix
// tslint:disable-next-line
const data: Data = {
name: specPath,
startTime: new Date(), // needed for ts-lint
viewport: {
width: 0,
height: 0,
},
specName: '', // this is optional, the getPath will pick up from specPath
testFailure: false, // this is only applicable for screenshot, not for video
testAttemptIndex: 0,
titles: [],
}
return getPath(data, ext, options.videosFolder, options.overwrite)
}
const videoName = videoPath('.mp4')
const compressedVideoName = videoPath('-compressed.mp4')
const videoName = await videoPath('mp4')
const compressedVideoName = await videoPath('mp4', '-compressed')
const outputDir = path.dirname(videoName)
@@ -332,6 +348,13 @@ async function compressRecording (options: { quiet: boolean, videoCompression: n
if (options.videoCompression === false || options.videoCompression === 0) {
debug('skipping compression')
// the getSafePath used to get the compressedVideoName creates the file
// in order to check if the path is safe or not. So here, if the compressed
// file exists, we remove it as compression is not enabled
if (fs.existsSync(options.processOptions.compressedVideoName)) {
await fs.remove(options.processOptions.compressedVideoName)
}
return
}
@@ -932,7 +955,7 @@ async function runSpec (config, spec: SpecWithRelativeRoot, options: { project:
async function getVideoRecording () {
if (!options.video) return undefined
const opts = { project, spec, videosFolder: options.videosFolder }
const opts = { project, spec, videosFolder: options.videosFolder, overwrite: options.config.trashAssetsBeforeRuns }
telemetry.startSpan({ name: 'video:capture' })
+84 -56
View File
@@ -16,21 +16,19 @@ import * as savedState from './saved_state'
import { SocketCt } from './socket-ct'
import { SocketE2E } from './socket-e2e'
import { ensureProp } from './util/class-helpers'
import system from './util/system'
import type { BannersState, FoundBrowser, FoundSpec, OpenProjectLaunchOptions, ProtocolManagerShape, ReceivedCypressOptions, ResolvedConfigurationOptions, TestingType, VideoRecording, AutomationCommands } from '@packages/types'
import { BannersState, FoundBrowser, FoundSpec, OpenProjectLaunchOptions, ProtocolManagerShape, ReceivedCypressOptions, ResolvedConfigurationOptions, TestingType, VideoRecording, AutomationCommands, StudioMetricsTypes } from '@packages/types'
import { DataContext, getCtx } from '@packages/data-context'
import { createHmac } from 'crypto'
import ProtocolManager from './cloud/protocol'
import { ServerBase } from './server-base'
import type Protocol from 'devtools-protocol'
import type { ServiceWorkerClientEvent } from '@packages/proxy/lib/http/util/service-worker-manager'
import { getAndInitializeStudioManager } from './cloud/api/get_and_initialize_studio_manager'
import api from './cloud/api'
import type { StudioManager } from './cloud/studio'
import { v4 } from 'uuid'
const routes = require('./cloud/routes')
import { StudioLifecycleManager } from './StudioLifecycleManager'
import { reportStudioError } from './cloud/api/studio/report_studio_error'
import { CloudRequest } from './cloud/api/cloud_request'
import { isRetryableError } from './cloud/network/is_retryable_error'
import { asyncRetry } from './util/async_retry'
export interface Cfg extends ReceivedCypressOptions {
projectId?: string
@@ -39,7 +37,6 @@ export interface Cfg extends ReceivedCypressOptions {
fileServerFolder?: Cypress.ResolvedConfigOptions['fileServerFolder']
testingType: TestingType
isDefaultProtocolEnabled?: boolean
isStudioProtocolEnabled?: boolean
hideCommandLog?: boolean
hideRunnerUi?: boolean
exit?: boolean
@@ -160,41 +157,16 @@ export class ProjectBase extends EE {
this._server = new ServerBase(cfg)
let studioManager: StudioManager | null
if (!cfg.isTextTerminal) {
const studioLifecycleManager = new StudioLifecycleManager()
if (process.env.CYPRESS_ENABLE_CLOUD_STUDIO || process.env.CYPRESS_LOCAL_STUDIO_PATH) {
studioManager = await getAndInitializeStudioManager({
studioLifecycleManager.initializeStudioManager({
projectId: cfg.projectId,
cloudDataSource: this.ctx.cloud,
cfg,
debugData: this.configDebugData,
ctx: this.ctx,
})
this.ctx.update((data) => {
data.studio = studioManager
})
if (studioManager.status === 'INITIALIZED') {
const protocolManager = new ProtocolManager()
const protocolUrl = routes.apiRoutes.captureProtocolCurrent()
const script = await api.getCaptureProtocolScript(protocolUrl)
await protocolManager.prepareProtocol(script, {
runId: 'studio',
projectId: cfg.projectId,
testingType: cfg.testingType,
cloudApi: {
url: routes.apiUrl,
retryWithBackoff: api.retryWithBackoff,
requestPromise: api.rp,
},
projectConfig: _.pick(cfg, ['devServerPublicPathRoute', 'port', 'proxyUrl', 'namespace']),
mountVersion: api.runnerCapabilities.protocolMountVersion,
debugData: this.configDebugData,
mode: 'studio',
})
studioManager.protocolManager = protocolManager
studioManager.isProtocolEnabled = true
}
}
const [port, warning] = await this._server.open(cfg, {
@@ -286,7 +258,7 @@ export class ProjectBase extends EE {
// if we're in studio mode, we need to close the protocol manager
// to ensure the config is initialized properly on browser relaunch
if (this.getConfig().isStudioProtocolEnabled) {
if (this.ctx.coreData.studioLifecycleManager) {
this.protocolManager?.close()
this.protocolManager = undefined
}
@@ -431,31 +403,75 @@ export class ProjectBase extends EE {
closeExtraTargets: this.closeExtraTargets,
onStudioInit: async () => {
if (this.spec && this.ctx.coreData.studio?.protocolManager) {
const canAccessStudioAI = await this.ctx.coreData.studio.canAccessStudioAI(this.browser) ?? false
const isStudioReady = this.ctx.coreData.studioLifecycleManager?.isStudioReady()
if (!isStudioReady) {
debug('User entered studio mode before cloud studio was initialized')
const cloudEnv = (process.env.CYPRESS_CONFIG_ENV || process.env.CYPRESS_INTERNAL_ENV || 'production') as 'development' | 'staging' | 'production'
const cloudUrl = this.ctx.cloud.getCloudUrl(cloudEnv)
const cloudHeaders = await this.ctx.cloud.additionalHeaders()
reportStudioError({
cloudApi: {
cloudUrl,
cloudHeaders,
CloudRequest,
isRetryableError,
asyncRetry,
},
studioHash: this.id,
projectSlug: this.cfg.projectId,
error: new Error('User entered studio before cloud studio was initialized'),
studioMethod: 'onStudioInit',
studioMethodArgs: [],
})
return { canAccessStudioAI: false }
}
const studio = await this.ctx.coreData.studioLifecycleManager?.getStudio()
try {
studio?.captureStudioEvent({
type: StudioMetricsTypes.STUDIO_STARTED,
machineId: await this.ctx.coreData.machineId,
projectId: this.cfg.projectId,
browser: this.browser ? {
name: this.browser.name,
family: this.browser.family,
channel: this.browser.channel,
version: this.browser.version,
} : undefined,
cypressVersion: pkg.version,
})
} catch (error) {
debug('Error capturing studio event:', error)
}
if (this.spec && studio?.protocolManager) {
const canAccessStudioAI = await studio?.canAccessStudioAI(this.browser) ?? false
if (!canAccessStudioAI) {
return { canAccessStudioAI }
}
this.ctx.coreData.studio.protocolManager.setupProtocol()
this.ctx.coreData.studio.protocolManager.beforeSpec({
this.protocolManager = studio.protocolManager
this.protocolManager.setupProtocol()
this.protocolManager.beforeSpec({
...this.spec,
instanceId: v4(),
})
await browsers.connectProtocolToBrowser({ browser: this.browser, foundBrowsers: this.options.browsers, protocolManager: this.ctx.coreData.studio.protocolManager })
await browsers.connectProtocolToBrowser({ browser: this.browser, foundBrowsers: this.options.browsers, protocolManager: studio.protocolManager })
if (!this.ctx.coreData.studio.protocolManager.dbPath) {
if (!studio.protocolManager.dbPath) {
debug('Protocol database path is not set after initializing protocol manager')
return { canAccessStudioAI: false }
}
this.protocolManager = this.ctx.coreData.studio.protocolManager
await this.ctx.coreData.studio.initializeStudioAI({
protocolDbPath: this.ctx.coreData.studio.protocolManager.dbPath,
await studio.initializeStudioAI({
protocolDbPath: studio.protocolManager.dbPath,
})
return { canAccessStudioAI: true }
@@ -467,11 +483,22 @@ export class ProjectBase extends EE {
},
onStudioDestroy: async () => {
if (this.ctx.coreData.studio?.protocolManager) {
const isStudioReady = await this.ctx.coreData.studioLifecycleManager?.isStudioReady()
if (!isStudioReady) {
debug('Studio is not ready - skipping destroy')
return
}
const studio = await this.ctx.coreData.studioLifecycleManager?.getStudio()
await studio?.destroy()
if (this.protocolManager) {
await browsers.closeProtocolConnection({ browser: this.browser, foundBrowsers: this.options.browsers })
this.protocolManager?.close()
this.protocolManager = undefined
await this.ctx.coreData.studio.destroy()
}
},
@@ -623,8 +650,10 @@ export class ProjectBase extends EE {
const isDefaultProtocolEnabled = this._protocolManager?.isProtocolEnabled ?? false
// hide the runner if explicitly requested or if the protocol is enabled outside of studio and the runner is not explicitly enabled
const hideRunnerUi = this.options?.args?.runnerUi === false || (isDefaultProtocolEnabled && !this.ctx.coreData.studio && !this.options?.args?.runnerUi)
const hideRunnerUi = (
this.options?.args?.runnerUi === false ||
(isDefaultProtocolEnabled && this._cfg.isTextTerminal && !this.options?.args?.runnerUi)
)
// hide the command log if explicitly requested or if we are hiding the runner
const hideCommandLog = this._cfg.env?.NO_COMMAND_LOG === 1 || hideRunnerUi
@@ -636,7 +665,6 @@ export class ProjectBase extends EE {
testingType: this.ctx.coreData.currentTestingType ?? 'e2e',
specs: [],
isDefaultProtocolEnabled,
isStudioProtocolEnabled: this.ctx.coreData.studio?.isProtocolEnabled ?? false,
hideCommandLog,
hideRunnerUi,
}
+9 -9
View File
@@ -109,16 +109,16 @@ export const createCommonRoutes = ({
router.get('/__cypress-studio/*', async (req, res) => {
await networkProxy.handleHttpRequest(req, res)
})
// We need to handle the case where the studio is not defined or loaded properly.
// Module federation still tries to load the dynamic asset, but since we do not
// have anything to load, we return a blank file.
} else if (!getCtx().coreData.studio || getCtx().coreData.studio?.status === 'IN_ERROR') {
router.get('/__cypress-studio/app-studio.js', (req, res) => {
res.setHeader('Content-Type', 'application/javascript')
res.status(200).send('')
})
} else {
getCtx().coreData.studio?.initializeRoutes(router)
// express matches routes in order. since this callback executes after the
// router has already been defined, we need to create a new router to use
// for the studio routes
const studioRouter = Router()
router.use('/', studioRouter)
getCtx().coreData.studioLifecycleManager?.registerStudioReadyListener((studio) => {
studio.initializeRoutes(studioRouter)
})
}
router.get(`/${config.namespace}/tests`, (req, res, next) => {
+6 -131
View File
@@ -1,54 +1,20 @@
import _ from 'lodash'
import Debug from 'debug'
import mime from 'mime'
import path from 'path'
import Promise from 'bluebird'
import dataUriToBuffer from 'data-uri-to-buffer'
import Jimp from 'jimp'
import sizeOf from 'image-size'
import colorString from 'color-string'
import sanitize from 'sanitize-filename'
import * as plugins from './plugins'
import { fs } from './util/fs'
import { fs, getPath } from './util/fs'
import type { Data, ScreenshotsFolder } from './util/fs'
let debug = Debug('cypress:server:screenshot')
const RUNNABLE_SEPARATOR = ' -- '
const pathSeparatorRe = /[\\\/]/g
// internal id incrementor
let __ID__: string | null = null
type ScreenshotsFolder = string | false | undefined
interface Clip {
x: number
y: number
width: number
height: number
}
// TODO: This is likely not representative of the entire Type and should be updated
interface Data {
specName: string
name: string
startTime: Date
viewport: {
width: number
height: number
}
titles?: string[]
testFailure?: boolean
overwrite?: boolean
simple?: boolean
current?: number
total?: number
testAttemptIndex?: number
appOnly?: boolean
hideRunnerUi?: boolean
clip?: Clip
userClip?: Clip
}
// TODO: This is likely not representative of the entire Type and should be updated
interface Details {
image: any
@@ -61,7 +27,10 @@ interface Details {
interface SavedDetails {
size?: string
takenAt?: Date
dimensions?: string
dimensions?: {
width: number
height: number
}
multipart?: any
pixelRatio?: number
name?: any
@@ -70,14 +39,6 @@ interface SavedDetails {
path?: string
}
// many filesystems limit filename length to 255 bytes/characters, so truncate the filename to
// the smallest common denominator of safe filenames, which is 255 bytes. when ENAMETOOLONG
// errors are encountered, `maxSafeBytes` will be decremented to at most `MIN_PREFIX_BYTES`, at
// which point the latest ENAMETOOLONG error will be emitted.
// @see https://en.wikipedia.org/wiki/Comparison_of_file_systems#Limits
let maxSafeBytes = Number(process.env.CYPRESS_MAX_SAFE_FILENAME_BYTES) || 254
const MIN_PREFIX_BYTES = 64
// TODO: when we parallelize these builds we'll need
// a semaphore to access the file system when we write
// screenshots since its possible two screenshots with
@@ -361,92 +322,6 @@ const getDimensions = function (details) {
return pick(details.image.bitmap)
}
const ensureSafePath = function (withoutExt: string, extension: string, overwrite: Data['overwrite'], num = 0) {
const suffix = `${(num && !overwrite) ? ` (${num})` : ''}.${extension}`
const maxSafePrefixBytes = maxSafeBytes - suffix.length
const filenameBuf = Buffer.from(path.basename(withoutExt))
if (filenameBuf.byteLength > maxSafePrefixBytes) {
const truncated = filenameBuf.slice(0, maxSafePrefixBytes).toString()
withoutExt = path.join(path.dirname(withoutExt), truncated)
}
const fullPath = [withoutExt, suffix].join('')
debug('ensureSafePath %o', { withoutExt, extension, num, maxSafeBytes, maxSafePrefixBytes })
return fs.pathExists(fullPath)
.then((found) => {
if (found && !overwrite) {
return ensureSafePath(withoutExt, extension, overwrite, num + 1)
}
// path does not exist, attempt to create it to check for an ENAMETOOLONG error
// @ts-expect-error
return fs.outputFileAsync(fullPath, '')
.then(() => fullPath)
.catch((err) => {
debug('received error when testing path %o', { err, fullPath, maxSafePrefixBytes, maxSafeBytes })
if (err.code === 'ENAMETOOLONG' && maxSafePrefixBytes >= MIN_PREFIX_BYTES) {
maxSafeBytes -= 1
return ensureSafePath(withoutExt, extension, overwrite, num)
}
throw err
})
})
}
const sanitizeToString = (title: string | null | undefined) => {
// test titles may be values which aren't strings like
// null or undefined - so convert before trying to sanitize
return sanitize(_.toString(title))
}
const getPath = function (data: Data, ext, screenshotsFolder: ScreenshotsFolder, overwrite: Data['overwrite']) {
let names
const specNames = (data.specName || '')
.split(pathSeparatorRe)
if (data.name) {
// @ts-expect-error
names = data.name.split(pathSeparatorRe).map(sanitize)
} else {
names = _
.chain(data.titles)
.map(sanitizeToString)
.join(RUNNABLE_SEPARATOR)
// @ts-expect-error - this shouldn't be necessary, but it breaks if you remove it
.concat([])
.value()
}
const index = names.length - 1
// append (failed) to the last name
if (data.testFailure) {
names[index] = `${names[index]} (failed)`
}
if (data.testAttemptIndex && data.testAttemptIndex > 0) {
names[index] = `${names[index]} (attempt ${data.testAttemptIndex + 1})`
}
let withoutExt
if (screenshotsFolder) {
withoutExt = path.join(screenshotsFolder, ...specNames, ...names)
} else {
withoutExt = path.join(...specNames, ...names)
}
return ensureSafePath(withoutExt, ext, overwrite)
}
const getPathToScreenshot = function (data: Data, details: Details, screenshotsFolder: ScreenshotsFolder) {
const ext = mime.getExtension(getType(details))
+20 -1
View File
@@ -405,7 +405,9 @@ export class SocketBase {
return socket.emit('dev-server:on-spec-updated')
})
getCtx().coreData.studio?.addSocketListeners(socket)
getCtx().coreData.studioLifecycleManager?.registerStudioReadyListener((studio) => {
studio.addSocketListeners(socket)
})
socket.on('studio:init', async (cb) => {
try {
@@ -417,6 +419,23 @@ export class SocketBase {
}
})
socket.on('studio:protocol:enabled', async (cb) => {
try {
const ctx = await getCtx()
const isStudioReady = ctx.coreData.studioLifecycleManager?.isStudioReady()
if (!isStudioReady) {
return cb({ studioProtocolEnabled: false })
}
const studio = await ctx.coreData.studioLifecycleManager?.getStudio()
cb({ studioProtocolEnabled: studio?.isProtocolEnabled })
} catch (error) {
cb({ error: errors.cloneErr(error) })
}
})
socket.on('studio:destroy', async (cb) => {
try {
await options.onStudioDestroy()
+125
View File
@@ -2,6 +2,20 @@
import Bluebird from 'bluebird'
import fsExtra from 'fs-extra'
import sanitize from 'sanitize-filename'
import path from 'path'
import _ from 'lodash'
const RUNNABLE_SEPARATOR = ' -- '
const pathSeparatorRe = /[\\\/]/g
// many filesystems limit filename length to 255 bytes/characters, so truncate the filename to
// the smallest common denominator of safe filenames, which is 255 bytes. when ENAMETOOLONG
// errors are encountered, `maxSafeBytes` will be decremented to at most `MIN_PREFIX_BYTES`, at
// which point the latest ENAMETOOLONG error will be emitted.
// @see https://en.wikipedia.org/wiki/Comparison_of_file_systems#Limits
let maxSafeBytes = Number(process.env.CYPRESS_MAX_SAFE_FILENAME_BYTES) || 254
const MIN_PREFIX_BYTES = 64
type Promisified<T extends (...args: any) => any>
= (...params: Parameters<T>) => Bluebird<ReturnType<T>>
@@ -12,6 +26,117 @@ interface PromisifiedFsExtra {
readFileAsync: Promisified<typeof fsExtra.readFileSync>
writeFileAsync: Promisified<typeof fsExtra.writeFileSync>
pathExistsAsync: Promisified<typeof fsExtra.pathExistsSync>
outputFileAsync: Promisified<typeof fsExtra.outputFileSync>
}
interface Clip {
x: number
y: number
width: number
height: number
}
export type ScreenshotsFolder = string | false | undefined
// TODO: This is likely not representative of the entire Type and should be updated
export interface Data {
specName: string
name: string
startTime: Date
viewport: {
width: number
height: number
}
titles?: string[]
testFailure?: boolean
overwrite?: boolean
simple?: boolean
current?: number
total?: number
testAttemptIndex?: number
appOnly?: boolean
hideRunnerUi?: boolean
clip?: Clip
userClip?: Clip
}
export const fs = Bluebird.promisifyAll(fsExtra) as PromisifiedFsExtra & typeof fsExtra
const ensureSafePath = async function (withoutExt: string, extension: string | null, overwrite: boolean | undefined, num: number = 0): Promise<string> {
const suffix = `${(num && !overwrite) ? ` (${num})` : ''}.${extension}`
const maxSafePrefixBytes = maxSafeBytes - suffix.length
const filenameBuf = Buffer.from(path.basename(withoutExt))
if (filenameBuf.byteLength > maxSafePrefixBytes) {
const truncated = filenameBuf.slice(0, maxSafePrefixBytes).toString()
withoutExt = path.join(path.dirname(withoutExt), truncated)
}
const fullPath = [withoutExt, suffix].join('')
return fs.pathExists(fullPath)
.then((found) => {
if (found && !overwrite) {
return ensureSafePath(withoutExt, extension, overwrite, num + 1)
}
// path does not exist, attempt to create it to check for an ENAMETOOLONG error
return fs.outputFileAsync(fullPath, '')
.then(() => fullPath)
.catch((err) => {
if (err.code === 'ENAMETOOLONG' && maxSafePrefixBytes >= MIN_PREFIX_BYTES) {
maxSafeBytes -= 1
return ensureSafePath(withoutExt, extension, overwrite, num)
}
throw err
})
})
}
const sanitizeToString = (title: any, idx: number, arr: Array<string>) => {
// test titles may be values which aren't strings like
// null or undefined - so convert before trying to sanitize
return sanitize(_.toString(title))
}
export const getPath = async function (data: Data, ext: string | null, screenshotsFolder: ScreenshotsFolder, overwrite: boolean | undefined): Promise<string> {
let names
const specNames = (data.specName || '')
.split(pathSeparatorRe)
if (data.name) {
names = data.name.split(pathSeparatorRe).map(sanitizeToString)
} else {
// we put this in array so to match with type of the if branch above
names = [_
.chain(data.titles)
.map(sanitizeToString)
.join(RUNNABLE_SEPARATOR)
.value()]
}
const index = names.length - 1
// append '(failed)' to the last name
if (data.testFailure) {
names[index] = `${names[index]} (failed)`
}
if (data.testAttemptIndex && data.testAttemptIndex > 0) {
names[index] = `${names[index]} (attempt ${data.testAttemptIndex + 1})`
}
let withoutExt
if (screenshotsFolder) {
withoutExt = path.join(screenshotsFolder, ...specNames, ...names)
} else {
withoutExt = path.join(...specNames, ...names)
}
return await ensureSafePath(withoutExt, ext, overwrite)
}
@@ -17,6 +17,10 @@ class StudioServer implements StudioServerShape {
return Promise.resolve()
}
reportError (error: Error, method: string, ...args: any[]): void {
// This is a test implementation that does nothing
}
destroy (): Promise<void> {
return Promise.resolve()
}
@@ -24,6 +28,10 @@ class StudioServer implements StudioServerShape {
addSocketListeners (socket: Socket): void {
// This is a test implementation that does nothing
}
captureStudioEvent (event: StudioEvent): Promise<void> {
return Promise.resolve()
}
}
const studioServerDefault: StudioServerDefaultShape = {
@@ -0,0 +1,341 @@
import { sinon } from '../spec_helper'
import { expect } from 'chai'
import { StudioManager } from '../../lib/cloud/studio'
import { StudioLifecycleManager } from '../../lib/StudioLifecycleManager'
import type { DataContext } from '@packages/data-context'
import type { Cfg } from '../../lib/project-base'
import type { CloudDataSource } from '@packages/data-context/src/sources'
import * as getAndInitializeStudioManagerModule from '../../lib/cloud/api/studio/get_and_initialize_studio_manager'
import * as reportStudioErrorPath from '../../lib/cloud/api/studio/report_studio_error'
import ProtocolManager from '../../lib/cloud/protocol'
const api = require('../../lib/cloud/api').default
import * as postStudioSessionModule from '../../lib/cloud/api/studio/post_studio_session'
// Helper to wait for next tick in event loop
const nextTick = () => new Promise((resolve) => process.nextTick(resolve))
describe('StudioLifecycleManager', () => {
let studioLifecycleManager: StudioLifecycleManager
let mockStudioManager: StudioManager
let mockCtx: DataContext
let mockCloudDataSource: CloudDataSource
let mockCfg: Cfg
let postStudioSessionStub: sinon.SinonStub
let getAndInitializeStudioManagerStub: sinon.SinonStub
let getCaptureProtocolScriptStub: sinon.SinonStub
let prepareProtocolStub: sinon.SinonStub
let reportStudioErrorStub: sinon.SinonStub
beforeEach(() => {
studioLifecycleManager = new StudioLifecycleManager()
mockStudioManager = {
addSocketListeners: sinon.stub(),
canAccessStudioAI: sinon.stub().resolves(true),
status: 'INITIALIZED',
} as unknown as StudioManager
mockCtx = {
update: sinon.stub(),
coreData: {},
cloud: {
getCloudUrl: sinon.stub().returns('https://cloud.cypress.io'),
additionalHeaders: sinon.stub().resolves({ 'Authorization': 'Bearer test-token' }),
},
} as unknown as DataContext
mockCloudDataSource = {} as CloudDataSource
mockCfg = {
projectId: 'abc123',
testingType: 'e2e',
projectRoot: '/test/project',
port: 8888,
proxyUrl: 'http://localhost:8888',
devServerPublicPathRoute: '/__cypress/src',
namespace: '__cypress',
} as unknown as Cfg
postStudioSessionStub = sinon.stub(postStudioSessionModule, 'postStudioSession')
postStudioSessionStub.resolves({
studioUrl: 'https://cloud.cypress.io/studio/bundle/abc.tgz',
protocolUrl: 'https://cloud.cypress.io/capture-protocol/script/def.js',
})
getAndInitializeStudioManagerStub = sinon.stub(getAndInitializeStudioManagerModule, 'getAndInitializeStudioManager')
getAndInitializeStudioManagerStub.resolves(mockStudioManager)
getCaptureProtocolScriptStub = sinon.stub(api, 'getCaptureProtocolScript').resolves('console.log("hello")')
prepareProtocolStub = sinon.stub(ProtocolManager.prototype, 'prepareProtocol').resolves()
reportStudioErrorStub = sinon.stub(reportStudioErrorPath, 'reportStudioError')
})
afterEach(() => {
sinon.restore()
})
describe('initializeStudioManager', () => {
it('initializes the studio manager and registers it in the data context', async () => {
studioLifecycleManager.initializeStudioManager({
projectId: 'test-project-id',
cloudDataSource: mockCloudDataSource,
cfg: mockCfg,
debugData: {},
ctx: mockCtx,
})
const studioReadyPromise = new Promise((resolve) => {
studioLifecycleManager?.registerStudioReadyListener((studioManager) => {
resolve(studioManager)
})
})
await studioReadyPromise
expect(mockCtx.update).to.be.calledOnce
expect(studioLifecycleManager.isStudioReady()).to.be.true
})
it('sets up protocol if studio is enabled', async () => {
mockStudioManager.status = 'ENABLED'
studioLifecycleManager.initializeStudioManager({
projectId: 'abc123',
cloudDataSource: mockCloudDataSource,
cfg: mockCfg,
debugData: {},
ctx: mockCtx,
})
const studioReadyPromise = new Promise((resolve) => {
studioLifecycleManager?.registerStudioReadyListener((studioManager) => {
resolve(studioManager)
})
})
await studioReadyPromise
expect(postStudioSessionStub).to.be.calledWith({
projectId: 'abc123',
})
expect(getCaptureProtocolScriptStub).to.be.calledWith('https://cloud.cypress.io/capture-protocol/script/def.js')
expect(prepareProtocolStub).to.be.calledWith('console.log("hello")', {
runId: 'studio',
projectId: 'abc123',
testingType: 'e2e',
cloudApi: {
url: 'http://localhost:1234/',
retryWithBackoff: api.retryWithBackoff,
requestPromise: api.rp,
},
projectConfig: {
devServerPublicPathRoute: '/__cypress/src',
namespace: '__cypress',
port: 8888,
proxyUrl: 'http://localhost:8888',
},
mountVersion: 2,
debugData: {},
mode: 'studio',
})
})
it('handles errors during initialization and reports them', async () => {
const error = new Error('Test error')
const listener1 = sinon.stub()
const listener2 = sinon.stub()
// Register listeners that should be cleaned up
studioLifecycleManager.registerStudioReadyListener(listener1)
studioLifecycleManager.registerStudioReadyListener(listener2)
// @ts-ignore - accessing private property for testing
expect(studioLifecycleManager['listeners'].length).to.equal(2)
getAndInitializeStudioManagerStub.rejects(error)
const reportErrorPromise = new Promise<void>((resolve) => {
reportStudioErrorStub.callsFake(() => {
resolve()
return undefined
})
})
// Should not throw
studioLifecycleManager.initializeStudioManager({
projectId: 'test-project-id',
cloudDataSource: mockCloudDataSource,
cfg: mockCfg,
debugData: {},
ctx: mockCtx,
})
await reportErrorPromise
expect(mockCtx.update).to.be.calledOnce
// @ts-ignore - accessing private property for testing
const studioPromise = studioLifecycleManager.studioManagerPromise
expect(studioPromise).to.not.be.null
expect(reportStudioErrorStub).to.be.calledOnce
expect(reportStudioErrorStub).to.be.calledWithMatch({
cloudApi: sinon.match.object,
studioHash: 'test-project-id',
projectSlug: 'abc123',
error: sinon.match.instanceOf(Error).and(sinon.match.has('message', 'Test error')),
studioMethod: 'initializeStudioManager',
studioMethodArgs: [],
})
// @ts-ignore - accessing private property for testing
expect(studioLifecycleManager['listeners'].length).to.equal(0)
expect(listener1).not.to.be.called
expect(listener2).not.to.be.called
if (studioPromise) {
const result = await studioPromise
expect(result).to.be.null
}
})
})
describe('isStudioReady', () => {
it('returns false when studio manager has not been initialized', () => {
expect(studioLifecycleManager.isStudioReady()).to.be.false
})
it('returns true when studio has been initialized', async () => {
// @ts-ignore - accessing private property for testing
studioLifecycleManager.studioManager = mockStudioManager
expect(studioLifecycleManager.isStudioReady()).to.be.true
})
})
describe('getStudio', () => {
it('throws an error when studio manager is not initialized', async () => {
try {
await studioLifecycleManager.getStudio()
expect.fail('Expected method to throw')
} catch (error) {
expect(error.message).to.equal('Studio manager has not been initialized')
}
})
it('returns the studio manager when initialized', async () => {
// @ts-ignore - accessing private property for testing
studioLifecycleManager.studioManagerPromise = Promise.resolve(mockStudioManager)
const result = await studioLifecycleManager.getStudio()
expect(result).to.equal(mockStudioManager)
})
})
describe('registerStudioReadyListener', () => {
it('registers a listener that will be called when studio is ready', () => {
const listener = sinon.stub()
studioLifecycleManager.registerStudioReadyListener(listener)
// @ts-ignore - accessing private property for testing
expect(studioLifecycleManager['listeners']).to.include(listener)
})
it('calls listener immediately if studio is already ready', async () => {
const listener = sinon.stub()
// @ts-ignore - accessing private property for testing
studioLifecycleManager.studioManager = mockStudioManager
// @ts-ignore - accessing private property for testing
studioLifecycleManager['studioReady'] = true
await Promise.resolve()
studioLifecycleManager.registerStudioReadyListener(listener)
await Promise.resolve()
await Promise.resolve()
await nextTick()
expect(listener).to.be.calledWith(mockStudioManager)
})
it('does not call listener if studio manager is null', async () => {
const listener = sinon.stub()
// @ts-ignore - accessing private property for testing
studioLifecycleManager.studioManager = null
// @ts-ignore - accessing private property for testing
studioLifecycleManager['studioReady'] = true
studioLifecycleManager.registerStudioReadyListener(listener)
// Give enough time for any promises to resolve
await Promise.resolve()
await Promise.resolve()
await nextTick()
expect(listener).not.to.be.called
})
it('adds multiple listeners to the list', () => {
const listener1 = sinon.stub()
const listener2 = sinon.stub()
studioLifecycleManager.registerStudioReadyListener(listener1)
studioLifecycleManager.registerStudioReadyListener(listener2)
// @ts-ignore - accessing private property for testing
expect(studioLifecycleManager['listeners']).to.include(listener1)
// @ts-ignore - accessing private property for testing
expect(studioLifecycleManager['listeners']).to.include(listener2)
})
it('cleans up listeners after calling them when studio becomes ready', async () => {
const listener1 = sinon.stub()
const listener2 = sinon.stub()
studioLifecycleManager.registerStudioReadyListener(listener1)
studioLifecycleManager.registerStudioReadyListener(listener2)
// @ts-ignore - accessing private property for testing
expect(studioLifecycleManager['listeners'].length).to.equal(2)
const listenersCalledPromise = Promise.all([
new Promise<void>((resolve) => {
listener1.callsFake(() => resolve())
}),
new Promise<void>((resolve) => {
listener2.callsFake(() => resolve())
}),
])
studioLifecycleManager.initializeStudioManager({
projectId: 'test-project-id',
cloudDataSource: mockCloudDataSource,
cfg: mockCfg,
debugData: {},
ctx: mockCtx,
})
await listenersCalledPromise
await nextTick()
expect(listener1).to.be.calledWith(mockStudioManager)
expect(listener2).to.be.calledWith(mockStudioManager)
// @ts-ignore - accessing private property for testing
expect(studioLifecycleManager['listeners'].length).to.equal(0)
})
})
})
@@ -1,13 +1,13 @@
import { Readable, Writable } from 'stream'
import { proxyquire, sinon } from '../../../spec_helper'
import { HttpError } from '../../../../lib/cloud/network/http_error'
import { CloudRequest } from '../../../../lib/cloud/api/cloud_request'
import { isRetryableError } from '../../../../lib/cloud/network/is_retryable_error'
import { asyncRetry } from '../../../../lib/util/async_retry'
import { proxyquire, sinon } from '../../../../spec_helper'
import { HttpError } from '../../../../../lib/cloud/network/http_error'
import { CloudRequest } from '../../../../../lib/cloud/api/cloud_request'
import { isRetryableError } from '../../../../../lib/cloud/network/is_retryable_error'
import { asyncRetry } from '../../../../../lib/util/async_retry'
import { CloudDataSource } from '@packages/data-context/src/sources'
describe('getAndInitializeStudioManager', () => {
let getAndInitializeStudioManager: typeof import('@packages/server/lib/cloud/api/get_and_initialize_studio_manager').getAndInitializeStudioManager
let getAndInitializeStudioManager: typeof import('@packages/server/lib/cloud/api/studio/get_and_initialize_studio_manager').getAndInitializeStudioManager
let rmStub: sinon.SinonStub = sinon.stub()
let ensureStub: sinon.SinonStub = sinon.stub()
let copyStub: sinon.SinonStub = sinon.stub()
@@ -20,8 +20,10 @@ describe('getAndInitializeStudioManager', () => {
let createInErrorManagerStub: sinon.SinonStub = sinon.stub()
let tmpdir: string = '/tmp'
let studioManagerSetupStub: sinon.SinonStub = sinon.stub()
let originalEnv: NodeJS.ProcessEnv
beforeEach(() => {
originalEnv = { ...process.env }
rmStub = sinon.stub()
ensureStub = sinon.stub()
copyStub = sinon.stub()
@@ -34,7 +36,7 @@ describe('getAndInitializeStudioManager', () => {
createInErrorManagerStub = sinon.stub()
studioManagerSetupStub = sinon.stub()
getAndInitializeStudioManager = (proxyquire('../lib/cloud/api/get_and_initialize_studio_manager', {
getAndInitializeStudioManager = (proxyquire('../lib/cloud/api/studio/get_and_initialize_studio_manager', {
fs: {
promises: {
rm: rmStub.resolves(),
@@ -54,10 +56,10 @@ describe('getAndInitializeStudioManager', () => {
tar: {
extract: extractStub.resolves(),
},
'../encryption': {
'../../encryption': {
verifySignatureFromFile: verifySignatureFromFileStub,
},
'../studio': {
'../../studio': {
StudioManager: class StudioManager {
static createInErrorManager = createInErrorManagerStub
setup = (...options) => studioManagerSetupStub(...options)
@@ -67,13 +69,105 @@ describe('getAndInitializeStudioManager', () => {
'@packages/root': {
version: '1.2.3',
},
}) as typeof import('@packages/server/lib/cloud/api/get_and_initialize_studio_manager')).getAndInitializeStudioManager
}) as typeof import('@packages/server/lib/cloud/api/studio/get_and_initialize_studio_manager')).getAndInitializeStudioManager
})
afterEach(() => {
process.env = originalEnv
sinon.restore()
})
describe('Studio status based on environment variables', () => {
let mockGetCloudUrl: sinon.SinonStub
let mockAdditionalHeaders: sinon.SinonStub
let cloud: CloudDataSource
let writeStream: Writable
let readStream: Readable
beforeEach(() => {
readStream = Readable.from('console.log("studio script")')
writeStream = new Writable({
write: (chunk, encoding, callback) => {
callback()
},
})
createWriteStreamStub.returns(writeStream)
createReadStreamStub.returns(Readable.from('tar contents'))
mockGetCloudUrl = sinon.stub()
mockAdditionalHeaders = sinon.stub()
cloud = {
getCloudUrl: mockGetCloudUrl,
additionalHeaders: mockAdditionalHeaders,
} as unknown as CloudDataSource
mockGetCloudUrl.returns('http://localhost:1234')
mockAdditionalHeaders.resolves({
a: 'b',
c: 'd',
})
crossFetchStub.resolves({
body: readStream,
headers: {
get: (header) => {
if (header === 'x-cypress-signature') {
return '159'
}
},
},
})
verifySignatureFromFileStub.resolves(true)
})
it('sets status to ENABLED when CYPRESS_ENABLE_CLOUD_STUDIO is set', async () => {
process.env.CYPRESS_ENABLE_CLOUD_STUDIO = '1'
delete process.env.CYPRESS_LOCAL_STUDIO_PATH
await getAndInitializeStudioManager({ studioUrl: 'http://localhost:1234/studio/bundle/abc.tgz', projectId: '12345', cloudDataSource: cloud })
expect(studioManagerSetupStub).to.be.calledWith(sinon.match({
shouldEnableStudio: true,
}))
})
it('sets status to ENABLED when CYPRESS_LOCAL_STUDIO_PATH is set', async () => {
delete process.env.CYPRESS_ENABLE_CLOUD_STUDIO
process.env.CYPRESS_LOCAL_STUDIO_PATH = '/path/to/studio'
await getAndInitializeStudioManager({ studioUrl: 'http://localhost:1234/studio/bundle/abc.tgz', projectId: '12345', cloudDataSource: cloud })
expect(studioManagerSetupStub).to.be.calledWith(sinon.match({
shouldEnableStudio: true,
}))
})
it('sets status to INITIALIZED when neither env variable is set', async () => {
delete process.env.CYPRESS_ENABLE_CLOUD_STUDIO
delete process.env.CYPRESS_LOCAL_STUDIO_PATH
await getAndInitializeStudioManager({ studioUrl: 'http://localhost:1234/studio/bundle/abc.tgz', projectId: '12345', cloudDataSource: cloud })
expect(studioManagerSetupStub).to.be.calledWith(sinon.match({
shouldEnableStudio: false,
}))
})
it('sets status to ENABLED when both env variables are set', async () => {
process.env.CYPRESS_ENABLE_CLOUD_STUDIO = '1'
process.env.CYPRESS_LOCAL_STUDIO_PATH = '/path/to/studio'
await getAndInitializeStudioManager({ studioUrl: 'http://localhost:1234/studio/bundle/abc.tgz', projectId: '12345', cloudDataSource: cloud })
expect(studioManagerSetupStub).to.be.calledWith(sinon.match({
shouldEnableStudio: true,
}))
})
})
describe('CYPRESS_LOCAL_STUDIO_PATH is set', () => {
beforeEach(() => {
process.env.CYPRESS_LOCAL_STUDIO_PATH = '/path/to/studio'
@@ -94,6 +188,7 @@ describe('getAndInitializeStudioManager', () => {
})
await getAndInitializeStudioManager({
studioUrl: 'http://localhost:1234/studio/bundle/abc.tgz',
projectId: '12345',
cloudDataSource: cloud,
})
@@ -170,12 +265,12 @@ describe('getAndInitializeStudioManager', () => {
const projectId = '12345'
await getAndInitializeStudioManager({ projectId, cloudDataSource: cloud })
await getAndInitializeStudioManager({ studioUrl: 'http://localhost:1234/studio/bundle/abc.tgz', projectId, cloudDataSource: cloud })
expect(rmStub).to.be.calledWith('/tmp/cypress/studio')
expect(ensureStub).to.be.calledWith('/tmp/cypress/studio')
expect(crossFetchStub).to.be.calledWith('http://localhost:1234/studio/bundle/current.tgz', {
expect(crossFetchStub).to.be.calledWith('http://localhost:1234/studio/bundle/abc.tgz', {
agent: sinon.match.any,
method: 'GET',
headers: {
@@ -237,12 +332,12 @@ describe('getAndInitializeStudioManager', () => {
const projectId = '12345'
await getAndInitializeStudioManager({ projectId, cloudDataSource: cloud })
await getAndInitializeStudioManager({ studioUrl: 'http://localhost:1234/studio/bundle/abc.tgz', projectId, cloudDataSource: cloud })
expect(rmStub).to.be.calledWith('/tmp/cypress/studio')
expect(ensureStub).to.be.calledWith('/tmp/cypress/studio')
expect(crossFetchStub).to.be.calledWith('http://localhost:1234/studio/bundle/current.tgz', {
expect(crossFetchStub).to.be.calledWith('http://localhost:1234/studio/bundle/abc.tgz', {
agent: sinon.match.any,
method: 'GET',
headers: {
@@ -294,13 +389,13 @@ describe('getAndInitializeStudioManager', () => {
const projectId = '12345'
await getAndInitializeStudioManager({ projectId, cloudDataSource: cloud })
await getAndInitializeStudioManager({ studioUrl: 'http://localhost:1234/studio/bundle/abc.tgz', projectId, cloudDataSource: cloud })
expect(rmStub).to.be.calledWith('/tmp/cypress/studio')
expect(ensureStub).to.be.calledWith('/tmp/cypress/studio')
expect(crossFetchStub).to.be.calledThrice
expect(crossFetchStub).to.be.calledWith('http://localhost:1234/studio/bundle/current.tgz', {
expect(crossFetchStub).to.be.calledWith('http://localhost:1234/studio/bundle/abc.tgz', {
agent: sinon.match.any,
method: 'GET',
headers: {
@@ -314,7 +409,19 @@ describe('getAndInitializeStudioManager', () => {
encrypt: 'signed',
})
expect(createInErrorManagerStub).to.be.calledWithMatch(sinon.match.instanceOf(AggregateError))
expect(createInErrorManagerStub).to.be.calledWithMatch({
error: sinon.match.instanceOf(AggregateError),
cloudApi: {
cloudUrl: 'http://localhost:1234',
cloudHeaders: {
a: 'b',
c: 'd',
},
},
studioHash: undefined,
projectSlug: '12345',
studioMethod: 'getAndInitializeStudioManager',
})
})
it('throws an error and returns a studio manager in error state if the signature verification fails', async () => {
@@ -346,13 +453,13 @@ describe('getAndInitializeStudioManager', () => {
const projectId = '12345'
await getAndInitializeStudioManager({ projectId, cloudDataSource: cloud })
await getAndInitializeStudioManager({ studioUrl: 'http://localhost:1234/studio/bundle/abc.tgz', projectId, cloudDataSource: cloud })
expect(rmStub).to.be.calledWith('/tmp/cypress/studio')
expect(ensureStub).to.be.calledWith('/tmp/cypress/studio')
expect(writeResult).to.eq('console.log("studio script")')
expect(crossFetchStub).to.be.calledWith('http://localhost:1234/studio/bundle/current.tgz', {
expect(crossFetchStub).to.be.calledWith('http://localhost:1234/studio/bundle/abc.tgz', {
agent: sinon.match.any,
method: 'GET',
headers: {
@@ -367,7 +474,16 @@ describe('getAndInitializeStudioManager', () => {
})
expect(verifySignatureFromFileStub).to.be.calledWith('/tmp/cypress/studio/bundle.tar', '159')
expect(createInErrorManagerStub).to.be.calledWithMatch(sinon.match.instanceOf(Error).and(sinon.match.has('message', 'Unable to verify studio signature')))
expect(createInErrorManagerStub).to.be.calledWithMatch({
error: sinon.match.instanceOf(Error).and(sinon.match.has('message', 'Unable to verify studio signature')),
cloudApi: {
cloudUrl: 'http://localhost:1234',
cloudHeaders: { a: 'b', c: 'd' },
},
studioHash: undefined,
projectSlug: '12345',
studioMethod: 'getAndInitializeStudioManager',
})
})
it('throws an error if there is no signature in the response headers', async () => {
@@ -393,11 +509,20 @@ describe('getAndInitializeStudioManager', () => {
const projectId = '12345'
await getAndInitializeStudioManager({ projectId, cloudDataSource: cloud })
await getAndInitializeStudioManager({ studioUrl: 'http://localhost:1234/studio/bundle/abc.tgz', projectId, cloudDataSource: cloud })
expect(rmStub).to.be.calledWith('/tmp/cypress/studio')
expect(ensureStub).to.be.calledWith('/tmp/cypress/studio')
expect(createInErrorManagerStub).to.be.calledWithMatch(sinon.match.instanceOf(Error).and(sinon.match.has('message', 'Unable to get studio signature')))
expect(createInErrorManagerStub).to.be.calledWithMatch({
error: sinon.match.instanceOf(Error).and(sinon.match.has('message', 'Unable to get studio signature')),
cloudApi: {
cloudUrl: 'http://localhost:1234',
cloudHeaders: { a: 'b', c: 'd' },
},
studioHash: undefined,
projectSlug: '12345',
studioMethod: 'getAndInitializeStudioManager',
})
})
})
})
@@ -0,0 +1,80 @@
import { SystemError } from '../../../../../lib/cloud/network/system_error'
import { proxyquire } from '../../../../spec_helper'
import os from 'os'
import { agent } from '@packages/network'
import pkg from '@packages/root'
describe('postStudioSession', () => {
let postStudioSession: typeof import('@packages/server/lib/cloud/api/studio/post_studio_session').postStudioSession
let crossFetchStub: sinon.SinonStub = sinon.stub()
beforeEach(() => {
crossFetchStub.reset()
postStudioSession = (proxyquire('@packages/server/lib/cloud/api/studio/post_studio_session', {
'cross-fetch': crossFetchStub,
}) as typeof import('@packages/server/lib/cloud/api/studio/post_studio_session')).postStudioSession
})
it('should post a studio session', async () => {
crossFetchStub.resolves({
ok: true,
json: () => {
return Promise.resolve({
studioUrl: 'http://localhost:1234/studio/bundle/abc.tgz',
protocolUrl: 'http://localhost:1234/capture-protocol/script/def.js',
})
},
})
const result = await postStudioSession({
projectId: '12345',
})
expect(result).to.deep.equal({
studioUrl: 'http://localhost:1234/studio/bundle/abc.tgz',
protocolUrl: 'http://localhost:1234/capture-protocol/script/def.js',
})
expect(crossFetchStub).to.have.been.calledOnce
expect(crossFetchStub).to.have.been.calledWith(
'http://localhost:1234/studio/session',
{
method: 'POST',
agent,
headers: {
'Content-Type': 'application/json',
'x-os-name': os.platform(),
'x-cypress-version': pkg.version,
},
body: JSON.stringify({ projectSlug: '12345', studioMountVersion: 1, protocolMountVersion: 2 }),
},
)
})
it('should throw immediately if the response is not ok', async () => {
crossFetchStub.resolves({
ok: false,
json: () => {
return Promise.resolve({
error: 'Failed to create studio session',
})
},
})
await expect(postStudioSession({
projectId: '12345',
})).to.be.rejectedWith('Failed to create studio session')
expect(crossFetchStub).to.have.been.calledOnce
})
it('should throw an error if we receive a retryable error more than twice', async () => {
crossFetchStub.rejects(new SystemError(new Error('Failed to create studio session'), 'http://localhost:1234/studio/session'))
await expect(postStudioSession({
projectId: '12345',
})).to.be.rejected
expect(crossFetchStub).to.have.been.calledThrice
})
})
@@ -0,0 +1,240 @@
import { expect } from 'chai'
import { sinon } from '../../../../spec_helper'
import { reportStudioError } from '@packages/server/lib/cloud/api/studio/report_studio_error'
describe('lib/cloud/api/studio/report_studio_error', () => {
let cloudRequestStub: sinon.SinonStub
let cloudApi: any
beforeEach(() => {
cloudRequestStub = sinon.stub()
cloudApi = {
cloudUrl: 'http://localhost:1234',
cloudHeaders: { 'x-cypress-version': '1.2.3' },
CloudRequest: {
post: cloudRequestStub,
},
}
})
afterEach(() => {
sinon.restore()
})
describe('reportStudioError', () => {
it('logs error when CYPRESS_LOCAL_STUDIO_PATH is set', () => {
sinon.stub(console, 'error')
process.env.CYPRESS_LOCAL_STUDIO_PATH = '/path/to/studio'
const error = new Error('test error')
reportStudioError({
cloudApi,
studioHash: 'abc123',
projectSlug: 'test-project',
error,
studioMethod: 'testMethod',
})
// eslint-disable-next-line no-console
expect(console.error).to.have.been.calledWith(
'Error in testMethod:',
error,
)
})
it('logs error when NODE_ENV is development', () => {
sinon.stub(console, 'error')
process.env.NODE_ENV = 'development'
const error = new Error('test error')
reportStudioError({
cloudApi,
studioHash: 'abc123',
projectSlug: 'test-project',
error,
studioMethod: 'testMethod',
})
// eslint-disable-next-line no-console
expect(console.error).to.have.been.calledWith(
'Error in testMethod:',
error,
)
})
it('converts non-Error objects to Error', () => {
const error = 'string error'
reportStudioError({
cloudApi,
studioHash: 'abc123',
projectSlug: 'test-project',
error,
studioMethod: 'testMethod',
})
expect(cloudRequestStub).to.be.calledWithMatch(
'http://localhost:1234/studio/errors',
{
studioHash: 'abc123',
projectSlug: 'test-project',
errors: [{
name: 'Error',
message: 'string error',
stack: sinon.match((stack) => stack.includes('<stripped-path>report_studio_error_spec.ts')),
studioMethod: 'testMethod',
studioMethodArgs: undefined,
}],
},
{
headers: {
'Content-Type': 'application/json',
'x-cypress-version': '1.2.3',
},
},
)
})
it('handles Error objects correctly', () => {
const error = new Error('test error')
error.stack = 'test stack'
reportStudioError({
cloudApi,
studioHash: 'abc123',
projectSlug: 'test-project',
error,
studioMethod: 'testMethod',
})
expect(cloudRequestStub).to.be.calledWithMatch(
'http://localhost:1234/studio/errors',
{
studioHash: 'abc123',
projectSlug: 'test-project',
errors: [{
name: 'Error',
message: 'test error',
stack: 'test stack',
studioMethod: 'testMethod',
studioMethodArgs: undefined,
}],
},
{
headers: {
'Content-Type': 'application/json',
'x-cypress-version': '1.2.3',
},
},
)
})
it('includes studioMethodArgs when provided', () => {
const error = new Error('test error')
const args = ['arg1', { key: 'value' }]
reportStudioError({
cloudApi,
studioHash: 'abc123',
projectSlug: 'test-project',
error,
studioMethod: 'testMethod',
studioMethodArgs: args,
})
expect(cloudRequestStub).to.be.calledWithMatch(
'http://localhost:1234/studio/errors',
{
studioHash: 'abc123',
projectSlug: 'test-project',
errors: [{
name: 'Error',
message: 'test error',
stack: sinon.match((stack) => stack.includes('<stripped-path>report_studio_error_spec.ts')),
studioMethod: 'testMethod',
studioMethodArgs: JSON.stringify({ args }),
}],
},
{
headers: {
'Content-Type': 'application/json',
'x-cypress-version': '1.2.3',
},
},
)
})
it('handles errors in JSON.stringify for studioMethodArgs', () => {
const error = new Error('test error')
const circularObj: any = {}
circularObj.self = circularObj
reportStudioError({
cloudApi,
studioHash: 'abc123',
projectSlug: 'test-project',
error,
studioMethod: 'testMethod',
studioMethodArgs: [circularObj],
})
expect(cloudRequestStub).to.be.calledWithMatch(
'http://localhost:1234/studio/errors',
{
studioHash: 'abc123',
projectSlug: 'test-project',
errors: [{
name: 'Error',
message: 'test error',
stack: sinon.match((stack) => stack.includes('<stripped-path>report_studio_error_spec.ts')),
studioMethod: 'testMethod',
studioMethodArgs: sinon.match(/Unknown args/),
}],
},
{
headers: {
'Content-Type': 'application/json',
'x-cypress-version': '1.2.3',
},
},
)
})
it('handles errors in CloudRequest.post', () => {
const error = new Error('test error')
const postError = new Error('post error')
cloudRequestStub.rejects(postError)
reportStudioError({
cloudApi,
studioHash: 'abc123',
projectSlug: 'test-project',
error,
studioMethod: 'testMethod',
})
// Just verify the post was called, don't check debug output
expect(cloudRequestStub).to.be.called
})
it('handles errors in payload construction', () => {
const error = new Error('test error')
sinon.stub(JSON, 'stringify').throws(new Error('JSON error'))
reportStudioError({
cloudApi,
studioHash: 'abc123',
projectSlug: 'test-project',
error,
studioMethod: 'testMethod',
})
// Just verify the post was called, don't check debug output
expect(cloudRequestStub).to.be.called
})
})
})
+33 -64
View File
@@ -6,8 +6,6 @@ import esbuild from 'esbuild'
import type { StudioManager as StudioManagerShape } from '@packages/server/lib/cloud/studio'
import os from 'os'
const pkg = require('@packages/root')
const { outputFiles: [{ contents: stubStudioRaw }] } = esbuild.buildSync({
entryPoints: [path.join(__dirname, '..', '..', 'support', 'fixtures', 'cloud', 'studio', 'test-studio.ts')],
bundle: true,
@@ -18,15 +16,17 @@ const { outputFiles: [{ contents: stubStudioRaw }] } = esbuild.buildSync({
const stubStudio = new TextDecoder('utf-8').decode(stubStudioRaw)
describe('lib/cloud/studio', () => {
let stubbedCrossFetch: sinon.SinonStub
let studioManager: StudioManagerShape
let studio: StudioServerShape
let StudioManager: typeof import('@packages/server/lib/cloud/studio').StudioManager
let reportStudioError: sinon.SinonStub
beforeEach(async () => {
stubbedCrossFetch = sinon.stub()
reportStudioError = sinon.stub()
StudioManager = (proxyquire('../lib/cloud/studio', {
'cross-fetch': stubbedCrossFetch,
'./api/studio/report_studio_error': {
reportStudioError,
},
}) as typeof import('@packages/server/lib/cloud/studio')).StudioManager
studioManager = new StudioManager()
@@ -36,6 +36,7 @@ describe('lib/cloud/studio', () => {
studioHash: 'abcdefg',
projectSlug: '1234',
cloudApi: {} as any,
shouldEnableStudio: true,
})
studio = (studioManager as any)._studioServer
@@ -53,30 +54,12 @@ describe('lib/cloud/studio', () => {
const error = new Error('foo')
sinon.stub(studio, 'initializeRoutes').throws(error)
sinon.stub(studio, 'reportError')
studioManager.initializeRoutes({} as any)
expect(studioManager.status).to.eq('IN_ERROR')
expect(stubbedCrossFetch).to.be.calledWithMatch(sinon.match((url: string) => url.endsWith('/studio/errors')), {
agent: sinon.match.any,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-cypress-version': pkg.version,
'x-os-name': 'darwin',
'x-arch': 'x64',
},
body: sinon.match((body) => {
const parsedBody = JSON.parse(body)
expect(parsedBody.studioHash).to.eq('abcdefg')
expect(parsedBody.errors[0].name).to.eq(error.name)
expect(parsedBody.errors[0].stack).to.eq(error.stack)
expect(parsedBody.errors[0].message).to.eq(error.message)
return true
}),
})
expect(studio.reportError).to.be.calledWithMatch(error, 'initializeRoutes', {})
})
})
@@ -85,58 +68,44 @@ describe('lib/cloud/studio', () => {
const error = new Error('foo')
sinon.stub(studio, 'canAccessStudioAI').throws(error)
sinon.stub(studio, 'reportError')
await studioManager.canAccessStudioAI({} as any)
expect(studioManager.status).to.eq('IN_ERROR')
expect(stubbedCrossFetch).to.be.calledWithMatch(sinon.match((url: string) => url.endsWith('/studio/errors')), {
agent: sinon.match.any,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-cypress-version': pkg.version,
'x-os-name': 'darwin',
'x-arch': 'x64',
},
body: sinon.match((body) => {
const parsedBody = JSON.parse(body)
expect(studio.reportError).to.be.calledWithMatch(error, 'canAccessStudioAI', {})
})
expect(parsedBody.studioHash).to.eq('abcdefg')
expect(parsedBody.errors[0].name).to.eq(error.name)
expect(parsedBody.errors[0].stack).to.eq(error.stack)
expect(parsedBody.errors[0].message).to.eq(error.message)
it('does not set state IN_ERROR when a non-essential async method fails', async () => {
const error = new Error('foo')
return true
}),
})
sinon.stub(studio, 'captureStudioEvent').throws(error)
await studioManager.captureStudioEvent({} as any)
expect(studioManager.status).to.eq('ENABLED')
})
})
describe('createInErrorManager', () => {
it('creates a studio manager in error state', () => {
const manager = StudioManager.createInErrorManager(new Error('foo'))
const error = new Error('foo')
const manager = StudioManager.createInErrorManager({
error,
cloudApi: {} as any,
studioHash: 'abcdefg',
projectSlug: '1234',
studioMethod: 'initializeRoutes',
})
expect(manager.status).to.eq('IN_ERROR')
expect(stubbedCrossFetch).to.be.calledWithMatch(sinon.match((url: string) => url.endsWith('/studio/errors')), {
agent: sinon.match.any,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-cypress-version': pkg.version,
'x-os-name': 'darwin',
'x-arch': 'x64',
},
body: sinon.match((body) => {
const parsedBody = JSON.parse(body)
expect(parsedBody.studioHash).to.be.undefined
expect(parsedBody.errors[0].name).to.eq('Error')
expect(parsedBody.errors[0].stack).to.be.a('string')
expect(parsedBody.errors[0].message).to.eq('foo')
return true
}),
expect(reportStudioError).to.be.calledWithMatch({
error,
cloudApi: {} as any,
studioHash: 'abcdefg',
projectSlug: '1234',
studioMethod: 'initializeRoutes',
studioMethodArgs: undefined,
})
})
})
+211 -254
View File
@@ -1,4 +1,5 @@
require('../spec_helper')
require('../../lib/environment')
const path = require('path')
const chokidar = require('chokidar')
@@ -13,10 +14,10 @@ const savedState = require(`../../lib/saved_state`)
const runEvents = require(`../../lib/plugins/run_events`)
const system = require(`../../lib/util/system`)
const { getCtx } = require(`../../lib/makeDataContext`)
const studio = require('../../lib/cloud/api/get_and_initialize_studio_manager')
const api = require('../../lib/cloud/api').default
const { ProtocolManager } = require('../../lib/cloud/protocol')
const studio = require('../../lib/cloud/api/studio/get_and_initialize_studio_manager')
const browsers = require('../../lib/browsers')
const { StudioLifecycleManager } = require('../../lib/StudioLifecycleManager')
const { StudioManager } = require('../../lib/cloud/studio')
let ctx
@@ -293,9 +294,28 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
expect(config.hideRunnerUi).to.be.false
})
it('returns false if cloud studio is enabled', function () {
it('returns true if runnerUi arg is not set and protocol is enabled', function () {
this.project.protocolManager = { isProtocolEnabled: true }
this.project.ctx.coreData.studio = { isStudioProtocolEnabled: true }
this.project.cfg.isTextTerminal = true
const config = this.project.getConfig()
expect(config.hideRunnerUi).to.be.true
})
it('returns false if runnerUi arg is not set and protocol is not enabled', function () {
this.project.protocolManager = { isProtocolEnabled: false }
this.project.cfg.isTextTerminal = true
const config = this.project.getConfig()
expect(config.hideRunnerUi).to.be.false
})
it('returns false if runnerUi arg is set to true and protocol is enabled', function () {
this.project.protocolManager = { isProtocolEnabled: true }
this.project.options.args.runnerUi = true
this.project.cfg.isTextTerminal = true
const config = this.project.getConfig()
@@ -312,30 +332,14 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
expect(config.hideCommandLog).to.be.true
})
it('returns true if runnerUi arg is not set and protocol is enabled', function () {
it('returns true if in run mode and protocol is enabled', function () {
this.project.protocolManager = { isProtocolEnabled: true }
this.project.cfg.isTextTerminal = true
const config = this.project.getConfig()
expect(config.hideRunnerUi).to.be.true
})
it('returns false if runnerUi arg is not set and protocol is not enabled', function () {
this.project.protocolManager = { isProtocolEnabled: false }
const config = this.project.getConfig()
expect(config.hideRunnerUi).to.be.false
})
it('returns false if runnerUi arg is set to true and protocol is enabled', function () {
this.project.protocolManager = { isProtocolEnabled: false }
this.project.options.args.runnerUi = true
const config = this.project.getConfig()
expect(config.hideRunnerUi).to.be.false
})
})
})
@@ -447,110 +451,6 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
})
})
it('gets studio manager for the project id if CYPRESS_ENABLE_CLOUD_STUDIO is set', async function () {
process.env.CYPRESS_ENABLE_CLOUD_STUDIO = '1'
sinon.stub(api, 'getCaptureProtocolScript').resolves('console.log("hello")')
sinon.stub(ProtocolManager.prototype, 'prepareProtocol').resolves()
this.config.testingType = 'e2e'
await this.project.open()
expect(studio.getAndInitializeStudioManager).to.be.calledWith({
projectId: 'abc123',
cloudDataSource: ctx.cloud,
})
expect(ctx.coreData.studio).to.eq(this.testStudioManager)
expect(api.getCaptureProtocolScript).to.be.calledWith('http://localhost:1234/capture-protocol/script/current.js')
expect(ProtocolManager.prototype.prepareProtocol).to.be.calledWith('console.log("hello")', {
runId: 'studio',
projectId: 'abc123',
testingType: 'e2e',
cloudApi: {
url: 'http://localhost:1234/',
retryWithBackoff: api.retryWithBackoff,
requestPromise: api.rp,
},
projectConfig: {
devServerPublicPathRoute: '/__cypress/src',
namespace: '__cypress',
port: 8888,
proxyUrl: 'http://localhost:8888',
},
mountVersion: 2,
debugData: {},
mode: 'studio',
})
})
it('gets studio manager for the project id if CYPRESS_ENABLE_CLOUD_STUDIO is set but does not create the protocol manager if it is in error', async function () {
process.env.CYPRESS_ENABLE_CLOUD_STUDIO = '1'
this.testStudioManager.status = 'IN_ERROR'
sinon.stub(api, 'getCaptureProtocolScript').resolves('console.log("hello")')
sinon.stub(ProtocolManager.prototype, 'prepareProtocol').resolves()
this.config.testingType = 'e2e'
await this.project.open()
expect(studio.getAndInitializeStudioManager).to.be.calledWith({
projectId: 'abc123',
cloudDataSource: ctx.cloud,
})
expect(ctx.coreData.studio).to.eq(this.testStudioManager)
expect(api.getCaptureProtocolScript).not.to.be.called
expect(ProtocolManager.prototype.prepareProtocol).not.to.be.called
})
it('gets studio manager for the project id if CYPRESS_LOCAL_STUDIO_PATH is set', async function () {
process.env.CYPRESS_LOCAL_STUDIO_PATH = '/path/to/app/studio'
sinon.stub(api, 'getCaptureProtocolScript').resolves('console.log("hello")')
sinon.stub(ProtocolManager.prototype, 'prepareProtocol').resolves()
this.config.testingType = 'e2e'
await this.project.open()
expect(studio.getAndInitializeStudioManager).to.be.calledWith({
projectId: 'abc123',
cloudDataSource: ctx.cloud,
})
expect(ctx.coreData.studio).to.eq(this.testStudioManager)
expect(api.getCaptureProtocolScript).to.be.calledWith('http://localhost:1234/capture-protocol/script/current.js')
expect(ProtocolManager.prototype.prepareProtocol).to.be.calledWith('console.log("hello")', {
runId: 'studio',
projectId: 'abc123',
testingType: 'e2e',
cloudApi: {
url: 'http://localhost:1234/',
retryWithBackoff: api.retryWithBackoff,
requestPromise: api.rp,
},
projectConfig: {
devServerPublicPathRoute: '/__cypress/src',
namespace: '__cypress',
port: 8888,
proxyUrl: 'http://localhost:8888',
},
mountVersion: 2,
debugData: {},
mode: 'studio',
})
})
it('does not get studio manager if neither CYPRESS_ENABLE_CLOUD_STUDIO nor CYPRESS_LOCAL_STUDIO_PATH is set', async function () {
await this.project.open()
expect(studio.getAndInitializeStudioManager).not.to.be.called
expect(ctx.coreData.studio).to.be.null
})
describe('saved state', function () {
beforeEach(function () {
this._time = 1609459200000
@@ -674,33 +574,56 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
it('resets server + automation', function () {
this.project._cfg = {}
this.project.ctx.coreData.studio = {
isStudioProtocolEnabled: false,
// Create proper structure for ctx and coreData
this.project.ctx = this.project.ctx || {}
this.project.ctx.coreData = this.project.ctx.coreData || {}
this.project.ctx.coreData.studioLifecycleManager = {
isStudioReady: sinon.stub().returns(true),
getStudio: sinon.stub().resolves({
isProtocolEnabled: false,
}),
}
let protocolManagerValue
sinon.stub(this.project, 'protocolManager').get(() => protocolManagerValue).set((val) => {
protocolManagerValue = val
})
this.project.reset()
expect(this.project._automation.reset).to.be.calledOnce
expect(this.project.server.reset).to.be.calledOnce
})
it('resets server + automation with studio protocol enabled', function () {
// Set up minimal test structure
this.project._cfg = {}
this.project.ctx.coreData.studio = {
isProtocolEnabled: true,
}
this.project._protocolManager = { close: sinon.stub() }
const mockClose = sinon.stub()
const mockSetProtocolManager = sinon.stub()
const studioLifecycleManager = new StudioLifecycleManager()
sinon.stub(this.project, 'protocolManager').get(() => ({ close: mockClose })).set(mockSetProtocolManager)
this.project.ctx = this.project.ctx || {}
this.project.ctx.coreData = this.project.ctx.coreData || {}
this.project.ctx.coreData.studioLifecycleManager = studioLifecycleManager
const studio = { isProtocolEnabled: true }
studioLifecycleManager.isStudioReady = sinon.stub().returns(true)
sinon.stub(studioLifecycleManager, 'getStudio').resolves(studio)
let protocolManagerValue = this.project._protocolManager
sinon.stub(this.project, 'protocolManager').get(() => protocolManagerValue).set((val) => {
protocolManagerValue = val
})
// Call reset
this.project.reset()
expect(this.project._automation.reset).to.be.calledOnce
// Verify expected behaviors
expect(this.project._automation.reset).to.be.calledOnce
expect(this.project.server.reset).to.be.calledOnce
expect(mockClose).to.be.calledOnce
expect(mockSetProtocolManager).to.be.calledWith(undefined)
})
})
@@ -708,7 +631,7 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
beforeEach(function () {
this.project = new ProjectBase({ projectRoot: '/_test-output/path/to/project-e2e', testingType: 'e2e' })
this.project.watchers = {}
this.project._server = { close () {}, startWebsockets: sinon.stub() }
this.project._server = { close () {}, startWebsockets: sinon.stub(), setProtocolManager: sinon.stub() }
sinon.stub(ProjectBase.prototype, 'open').resolves()
})
@@ -738,21 +661,44 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
const mockSetupProtocol = sinon.stub()
const mockBeforeSpec = sinon.stub()
const mockAccessStudioAI = sinon.stub().resolves(true)
const mockSetProtocolDbPath = sinon.stub()
const mockInitializeStudioAI = sinon.stub().resolves()
const mockCaptureStudioEvent = sinon.stub().resolves()
this.project.spec = {}
this.project.ctx.coreData.studio = {
canAccessStudioAI: mockAccessStudioAI,
protocolManager: {
setupProtocol: mockSetupProtocol,
beforeSpec: mockBeforeSpec,
dbPath: 'test-db-path',
},
setProtocolDbPath: mockSetProtocolDbPath,
initializeStudioAI: mockInitializeStudioAI,
this.project._cfg = this.project._cfg || {}
this.project._cfg.projectId = 'test-project-id'
this.project.ctx.coreData.user = { email: 'test@example.com' }
this.project.ctx.coreData.machineId = Promise.resolve('test-machine-id')
const studioManager = new StudioManager()
studioManager.canAccessStudioAI = mockAccessStudioAI
studioManager.captureStudioEvent = mockCaptureStudioEvent
studioManager.protocolManager = {
setupProtocol: mockSetupProtocol,
beforeSpec: mockBeforeSpec,
db: { test: 'db' },
dbPath: 'test-db-path',
}
const studioLifecycleManager = new StudioLifecycleManager()
this.project.ctx.coreData.studioLifecycleManager = studioLifecycleManager
// Set up the studio manager promise directly
studioLifecycleManager.studioManagerPromise = Promise.resolve(studioManager)
studioLifecycleManager.isStudioReady = sinon.stub().returns(true)
// Create a browser object
this.project.browser = {
name: 'chrome',
family: 'chromium',
}
this.project.options = { browsers: [this.project.browser] }
sinon.stub(browsers, 'closeProtocolConnection').resolves()
sinon.stub(browsers, 'connectProtocolToBrowser').resolves()
sinon.stub(this.project, 'protocolManager').get(() => {
return this.project['_protocolManager']
@@ -760,18 +706,6 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
this.project['_protocolManager'] = protocolManager
})
this.project.browser = {
name: 'chrome',
family: 'chromium',
channel: 'stable',
}
this.project.options.browsers = [{
name: 'chrome',
family: 'chromium',
channel: 'stable',
}]
let studioInitPromise
this.project.server.startWebsockets.callsFake(async (automation, config, callbacks) => {
@@ -783,90 +717,70 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
const { canAccessStudioAI } = await studioInitPromise
expect(canAccessStudioAI).to.be.true
expect(mockCaptureStudioEvent).to.be.calledWith({
type: 'studio:started',
machineId: 'test-machine-id',
projectId: 'test-project-id',
browser: {
name: 'chrome',
family: 'chromium',
channel: undefined,
version: undefined,
},
cypressVersion: pkg.version,
})
expect(mockSetupProtocol).to.be.calledOnce
expect(mockBeforeSpec).to.be.calledOnce
expect(mockAccessStudioAI).to.be.calledWith({
family: 'chromium',
name: 'chrome',
channel: 'stable',
})
expect(browsers.connectProtocolToBrowser).to.be.calledWith({
browser: this.project.browser,
foundBrowsers: this.project.options.browsers,
protocolManager: this.project.ctx.coreData.studio.protocolManager,
protocolManager: studioManager.protocolManager,
})
expect(this.project['_protocolManager']).to.eq(this.project.ctx.coreData.studio.protocolManager)
expect(mockInitializeStudioAI).to.be.calledWith({
protocolDbPath: 'test-db-path',
})
})
it('handles case where protocol manager db path is not set', async function () {
const mockSetupProtocol = sinon.stub()
const mockBeforeSpec = sinon.stub()
const mockAccessStudioAI = sinon.stub().resolves(true)
const mockSetProtocolDbPath = sinon.stub()
const mockInitializeStudioAI = sinon.stub().resolves()
this.project.spec = {}
this.project.ctx.coreData.studio = {
canAccessStudioAI: mockAccessStudioAI,
protocolManager: {
setupProtocol: mockSetupProtocol,
beforeSpec: mockBeforeSpec,
dbPath: null,
},
setProtocolDbPath: mockSetProtocolDbPath,
initializeStudioAI: mockInitializeStudioAI,
}
sinon.stub(browsers, 'connectProtocolToBrowser').resolves()
this.project.browser = {
name: 'chrome',
family: 'chromium',
channel: 'stable',
}
this.project.options.browsers = [{
name: 'chrome',
family: 'chromium',
channel: 'stable',
}]
let studioInitPromise
this.project.server.startWebsockets.callsFake(async (automation, config, callbacks) => {
studioInitPromise = callbacks.onStudioInit()
})
this.project.startWebsockets({}, {})
const { canAccessStudioAI } = await studioInitPromise
expect(canAccessStudioAI).to.be.false
expect(this.project['_protocolManager']).to.be.undefined
expect(this.project['_protocolManager']).to.eq(studioManager.protocolManager)
})
it('passes onStudioInit callback with AI enabled but no protocol manager', async function () {
const mockSetupProtocol = sinon.stub()
const mockBeforeSpec = sinon.stub()
const mockAccessStudioAI = sinon.stub().resolves(true)
const mockCaptureStudioEvent = sinon.stub().resolves()
this.project.spec = {}
this.project.ctx.coreData.studio = {
canAccessStudioAI: mockAccessStudioAI,
}
this.project._cfg = this.project._cfg || {}
this.project._cfg.projectId = 'test-project-id'
this.project.ctx.coreData.user = { email: 'test@example.com' }
this.project.ctx.coreData.machineId = Promise.resolve('test-machine-id')
const studioManager = new StudioManager()
studioManager.canAccessStudioAI = mockAccessStudioAI
studioManager.captureStudioEvent = mockCaptureStudioEvent
const studioLifecycleManager = new StudioLifecycleManager()
this.project.ctx.coreData.studioLifecycleManager = studioLifecycleManager
studioLifecycleManager.studioManagerPromise = Promise.resolve(studioManager)
studioLifecycleManager.isStudioReady = sinon.stub().returns(true)
// Create a browser object
this.project.browser = {
name: 'chrome',
family: 'chromium',
channel: 'stable',
}
this.project.options = { browsers: [this.project.browser] }
sinon.stub(browsers, 'closeProtocolConnection').resolves()
sinon.stub(browsers, 'connectProtocolToBrowser').resolves()
sinon.stub(this.project, 'protocolManager').get(() => {
return this.project['_protocolManager']
@@ -885,6 +799,18 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
const { canAccessStudioAI } = await studioInitPromise
expect(canAccessStudioAI).to.be.false
expect(mockCaptureStudioEvent).to.be.calledWith({
type: 'studio:started',
machineId: 'test-machine-id',
projectId: 'test-project-id',
browser: {
name: 'chrome',
family: 'chromium',
channel: undefined,
version: undefined,
},
cypressVersion: pkg.version,
})
expect(mockSetupProtocol).not.to.be.called
expect(mockBeforeSpec).not.to.be.called
@@ -898,21 +824,41 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
const mockSetupProtocol = sinon.stub()
const mockBeforeSpec = sinon.stub()
const mockAccessStudioAI = sinon.stub().resolves(false)
const mockCaptureStudioEvent = sinon.stub().resolves()
this.project.spec = {}
this.project.ctx.coreData.studio = {
canAccessStudioAI: mockAccessStudioAI,
protocolManager: {
setupProtocol: mockSetupProtocol,
beforeSpec: mockBeforeSpec,
},
this.project._cfg = this.project._cfg || {}
this.project._cfg.projectId = 'test-project-id'
this.project.ctx.coreData.user = { email: 'test@example.com' }
this.project.ctx.coreData.machineId = Promise.resolve('test-machine-id')
const studioManager = new StudioManager()
studioManager.canAccessStudioAI = mockAccessStudioAI
studioManager.captureStudioEvent = mockCaptureStudioEvent
studioManager.protocolManager = {
setupProtocol: mockSetupProtocol,
beforeSpec: mockBeforeSpec,
}
const studioLifecycleManager = new StudioLifecycleManager()
this.project.ctx.coreData.studioLifecycleManager = studioLifecycleManager
studioLifecycleManager.studioManagerPromise = Promise.resolve(studioManager)
studioLifecycleManager.isStudioReady = sinon.stub().returns(true)
this.project.browser = {
name: 'chrome',
family: 'chromium',
}
this.project.options = { browsers: [this.project.browser] }
sinon.stub(browsers, 'closeProtocolConnection').resolves()
sinon.stub(browsers, 'connectProtocolToBrowser').resolves()
sinon.stub(this.project, 'protocolManager').get(() => {
return this.project['_protocolManager']
@@ -931,6 +877,19 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
const { canAccessStudioAI } = await studioInitPromise
expect(canAccessStudioAI).to.be.false
expect(mockCaptureStudioEvent).to.be.calledWith({
type: 'studio:started',
machineId: 'test-machine-id',
projectId: 'test-project-id',
browser: {
name: 'chrome',
family: 'chromium',
channel: undefined,
version: undefined,
},
cypressVersion: pkg.version,
})
expect(mockSetupProtocol).not.to.be.called
expect(mockBeforeSpec).not.to.be.called
expect(browsers.connectProtocolToBrowser).not.to.be.called
@@ -938,51 +897,49 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
})
it('passes onStudioDestroy callback', async function () {
const mockClose = sinon.stub()
const mockDestroy = sinon.stub().resolves()
// Set up minimal required properties
this.project.ctx = this.project.ctx || {}
this.project.ctx.coreData = this.project.ctx.coreData || {}
this.project.ctx.coreData.studio = {
protocolManager: {},
destroy: mockDestroy,
// Create a studio manager with minimal properties
const protocolManager = { close: sinon.stub().resolves() }
const studioManager = {
destroy: sinon.stub().resolves(),
protocolManager,
}
sinon.stub(browsers, 'closeProtocolConnection').resolves()
this.project.ctx.coreData.studioLifecycleManager = {
getStudio: sinon.stub().resolves(studioManager),
isStudioReady: sinon.stub().resolves(true),
}
sinon.stub(this.project, 'protocolManager').get(() => {
return {
close: mockClose,
}
}).set((protocolManager) => {
this.project['_protocolManager'] = protocolManager
})
this.project['_protocolManager'] = protocolManager
// Create a browser object
this.project.browser = {
name: 'chrome',
family: 'chromium',
}
this.project.options.browsers = [{
name: 'chrome',
family: 'chromium',
}]
this.project.options = { browsers: [this.project.browser] }
let studioDestroyPromise
sinon.stub(browsers, 'closeProtocolConnection').resolves()
this.project.server.startWebsockets.callsFake(async (automation, config, callbacks) => {
studioDestroyPromise = callbacks.onStudioDestroy()
// Modify the startWebsockets stub to track the callbacks
const callbackPromise = new Promise((resolve) => {
this.project.server.startWebsockets.callsFake(async (automation, config, callbacks) => {
await callbacks.onStudioDestroy()
resolve()
})
})
this.project.startWebsockets({}, {})
await studioDestroyPromise
await callbackPromise
expect(browsers.closeProtocolConnection).to.be.calledWith({
browser: this.project.browser,
foundBrowsers: this.project.options.browsers,
})
expect(mockClose).to.be.calledOnce
expect(mockDestroy).to.be.calledOnce
expect(studioManager.destroy).to.have.been.calledOnce
expect(browsers.closeProtocolConnection).to.have.been.calledOnce
expect(protocolManager.close).to.have.been.calledOnce
expect(this.project['_protocolManager']).to.be.undefined
})
})
+100 -37
View File
@@ -49,9 +49,9 @@ describe('lib/routes', () => {
function setupCommonRoutes () {
const router = {
get: sinon.stub(),
post: () => {},
all: () => {},
use: sinon.stub(),
post: sinon.stub(),
all: sinon.stub(),
use: sinon.spy(),
}
const Router = sinon.stub().returns(router)
@@ -70,6 +70,8 @@ describe('lib/routes', () => {
it('sends 301 if a chrome https upgrade is detected for /', () => {
const { router } = setupCommonRoutes()
const middleware = router.use.args.find((args) => args[0] === '/')?.[1]
const req = {
hostname: 'foobar.com',
path: '/',
@@ -84,7 +86,7 @@ describe('lib/routes', () => {
res.status.returns(res)
router.use.withArgs('/').yield(req, res, next)
middleware(req, res, next)
expect(res.status).to.be.calledWith(301)
expect(res.redirect).to.be.calledWith('http://foobar.com/')
@@ -93,6 +95,8 @@ describe('lib/routes', () => {
it('sends 301 if a chrome https upgrade is detected for /__/', () => {
const { router } = setupCommonRoutes()
const middleware = router.use.args.find((args) => args[0] === '/')?.[1]
const req = {
hostname: 'foobar.com',
path: '/__/',
@@ -107,29 +111,52 @@ describe('lib/routes', () => {
res.status.returns(res)
router.use.withArgs('/').yield(req, res, next)
middleware(req, res, next)
expect(res.status).to.be.calledWith(301)
expect(res.redirect).to.be.calledWith('http://foobar.com/__/')
})
it('is a noop if not a matching route', () => {
it('is a noop if path is neither / nor /__/', () => {
const { router } = setupCommonRoutes()
const middleware = router.use.args.find((args) => args[0] === '/')?.[1]
const req = {
hostname: 'foobar.com',
path: '/other-route',
proxiedUrl: 'https://foobar.com/other-route',
path: '/something-else',
proxiedUrl: 'https://foobar.com/something-else',
protocol: 'https',
}
const res = {
status: sinon.stub().throws('res.status() should not be called'),
redirect: sinon.stub(),
}
const next = sinon.stub()
res.status.returns(res)
middleware(req, res, next)
router.use.withArgs('/').yield(req, res, next)
expect(next).to.be.called
})
it('is a noop if protocol is not https', () => {
const { router } = setupCommonRoutes()
const middleware = router.use.args.find((args) => args[0] === '/')?.[1]
const req = {
hostname: 'foobar.com',
path: '/',
proxiedUrl: 'http://foobar.com/',
protocol: 'http',
}
const res = {
status: sinon.stub().throws('res.status() should not be called'),
redirect: sinon.stub(),
}
const next = sinon.stub()
middleware(req, res, next)
expect(next).to.be.called
})
@@ -139,6 +166,8 @@ describe('lib/routes', () => {
const { router } = setupCommonRoutes()
const middleware = router.use.args.find((args) => args[0] === '/')?.[1]
const req = {
hostname: 'foobar.com',
path: '/',
@@ -147,12 +176,11 @@ describe('lib/routes', () => {
}
const res = {
status: sinon.stub().throws('res.status() should not be called'),
redirect: sinon.stub(),
}
const next = sinon.stub()
res.status.returns(res)
router.use.withArgs('/').yield(req, res, next)
middleware(req, res, next)
expect(next).to.be.called
})
@@ -160,6 +188,8 @@ describe('lib/routes', () => {
it('is a noop if primary hostname and request hostname do not match', () => {
const { router } = setupCommonRoutes()
const middleware = router.use.args.find((args) => args[0] === '/')?.[1]
const req = {
hostname: 'other.com',
path: '/',
@@ -168,12 +198,11 @@ describe('lib/routes', () => {
}
const res = {
status: sinon.stub().throws('res.status() should not be called'),
redirect: sinon.stub(),
}
const next = sinon.stub()
res.status.returns(res)
router.use.withArgs('/').yield(req, res, next)
middleware(req, res, next)
expect(next).to.be.called
})
@@ -189,6 +218,8 @@ describe('lib/routes', () => {
const { router } = setupCommonRoutes()
const middleware = router.use.args.find((args) => args[0] === '/')?.[1]
const req = {
hostname: 'foobar.com',
path: '/',
@@ -197,59 +228,91 @@ describe('lib/routes', () => {
}
const res = {
status: sinon.stub().throws('res.status() should not be called'),
redirect: sinon.stub(),
}
const next = sinon.stub()
res.status.returns(res)
router.use.withArgs('/').yield(req, res, next)
middleware(req, res, next)
expect(next).to.be.called
})
it('initializes routes on studio if present', () => {
getCtx().coreData.studio = {
const studioManager = {
status: 'INITIALIZED',
initializeRoutes: sinon.stub(),
isProtocolEnabled: false,
captureStudioEvent: sinon.stub(),
canAccessStudioAI: sinon.stub(),
setProtocolDb: sinon.stub(),
addSocketListeners: sinon.stub(),
}
const studioLifecycleManager = {
registerStudioReadyListener: sinon.stub().callsFake((callback) => {
callback(studioManager)
return () => {}
}),
}
getCtx().coreData.studioLifecycleManager = studioLifecycleManager as any
const { router } = setupCommonRoutes()
expect(getCtx().coreData.studio.initializeRoutes).to.be.calledWith(router)
expect(studioManager.initializeRoutes).to.be.calledWith(router)
})
it('initializes a dummy route for studio if studio is not present', () => {
const { router } = setupCommonRoutes()
delete getCtx().coreData.studioLifecycleManager
const req = {
path: '/__cypress-studio/app-studio.js',
protocol: 'https',
const studioRouter = {
get: sinon.stub(),
post: sinon.stub(),
all: sinon.stub(),
use: sinon.stub(),
}
const res = {
setHeader: sinon.stub(),
status: sinon.stub(),
send: sinon.stub(),
const router = {
get: sinon.stub(),
post: sinon.stub(),
all: sinon.stub(),
use: sinon.stub().withArgs('/').returns(studioRouter),
}
const next = sinon.stub().throws('next() should not be called')
res.status.returns(res)
const Router = sinon.stub()
router.get.withArgs('/__cypress-studio/app-studio.js').yield(req, res, next)
Router.onFirstCall().returns(router)
Router.onSecondCall().returns(studioRouter)
expect(res.setHeader).to.be.calledWith('Content-Type', 'application/javascript')
expect(res.status).to.be.calledWith(200)
expect(res.send).to.be.calledWith('')
const { createCommonRoutes } = proxyquire('../../lib/routes', {
'express': { Router },
})
createCommonRoutes(routeOptions)
expect(router.use).to.have.been.calledWith('/')
expect(Router).to.have.been.calledTwice
expect(getCtx().coreData.studioLifecycleManager).to.be.undefined
})
it('does not initialize routes on studio if status is in error', () => {
getCtx().coreData.studio = {
const studioManager = {
status: 'IN_ERROR',
initializeRoutes: sinon.stub(),
}
const studioLifecycleManager = {
registerStudioReadyListener: sinon.stub().returns(() => {}),
}
getCtx().coreData.studioLifecycleManager = studioLifecycleManager as any
setupCommonRoutes()
expect(getCtx().coreData.studio.initializeRoutes).not.to.be.called
expect(studioManager.initializeRoutes).not.to.be.called
})
})
})
+22 -5
View File
@@ -78,8 +78,15 @@ describe('lib/socket', () => {
addSocketListeners: sinon.stub(),
}
// Set the studio in the context
ctx.coreData.studio = mockStudio
const studioLifecycleManager = {
registerStudioReadyListener: sinon.stub().callsFake((callback) => {
callback(mockStudio)
return () => {}
}),
}
ctx.coreData.studioLifecycleManager = studioLifecycleManager
this.server.startWebsockets(this.automation, this.cfg, this.options)
this.socket = this.server._socket
@@ -308,9 +315,19 @@ describe('lib/socket', () => {
context('studio.addSocketListeners', () => {
it('calls addSocketListeners on studio when socket connects', function () {
// The socket connection was already established in the beforeEach so
// we can just verify that addSocketListeners was called
expect(ctx.coreData.studio.addSocketListeners).to.be.called
// Verify that registerStudioReadyListener was called
expect(ctx.coreData.studioLifecycleManager.registerStudioReadyListener).to.be.called
// Check that the callback was called with the mock studio object
const registerStudioReadyListenerCallback = ctx.coreData.studioLifecycleManager.registerStudioReadyListener.firstCall.args[0]
expect(registerStudioReadyListenerCallback).to.be.a('function')
// Verify the mock studio's addSocketListeners was called by the callback
const mockStudio = { addSocketListeners: sinon.stub() }
registerStudioReadyListenerCallback(mockStudio)
expect(mockStudio.addSocketListeners).to.be.called
})
})
+9 -2
View File
@@ -1,9 +1,9 @@
import type { ProtocolManagerShape } from '../protocol'
import type { StudioServerShape } from './studio-server-types'
import type { StudioServerShape, StudioEvent } from './studio-server-types'
export * from './studio-server-types'
export const STUDIO_STATUSES = ['NOT_INITIALIZED', 'INITIALIZED', 'IN_ERROR'] as const
export const STUDIO_STATUSES = ['NOT_INITIALIZED', 'INITIALIZED', 'ENABLED', 'IN_ERROR'] as const
export type StudioStatus = typeof STUDIO_STATUSES[number]
@@ -11,6 +11,13 @@ export interface StudioManagerShape extends StudioServerShape {
status: StudioStatus
isProtocolEnabled: boolean
protocolManager?: ProtocolManagerShape
captureStudioEvent: (event: StudioEvent) => Promise<void>
}
export interface StudioLifecycleManagerShape {
getStudio: () => Promise<StudioManagerShape | null>
isStudioReady: () => boolean
registerStudioReadyListener: (listener: (studioManager: StudioManagerShape) => void) => void
}
export type StudioErrorReport = {
@@ -4,6 +4,26 @@ import type { Router } from 'express'
import type { AxiosInstance } from 'axios'
import type { Socket } from 'socket.io'
export const StudioMetricsTypes = {
STUDIO_STARTED: 'studio:started',
} as const
export type StudioMetricsType =
(typeof StudioMetricsTypes)[keyof typeof StudioMetricsTypes]
export interface StudioEvent {
type: StudioMetricsType
machineId: string | null
projectId?: string
browser?: {
name: string
family: string
channel?: string
version?: string
}
cypressVersion?: string
}
interface RetryOptions {
maxAttempts: number
retryDelay?: (attempt: number) => number
@@ -25,6 +45,7 @@ type AsyncRetry = <TArgs extends any[], TResult>(
) => (...args: TArgs) => Promise<TResult>
export interface StudioServerOptions {
studioHash?: string
studioPath: string
projectSlug?: string
cloudApi: StudioCloudApi
@@ -40,7 +61,13 @@ export interface StudioServerShape {
canAccessStudioAI(browser: Cypress.Browser): Promise<boolean>
addSocketListeners(socket: Socket): void
initializeStudioAI(options: StudioAIInitializeOptions): Promise<void>
reportError(
error: unknown,
studioMethod: string,
...studioMethodArgs: unknown[]
): void
destroy(): Promise<void>
captureStudioEvent(event: StudioEvent): Promise<void>
}
export interface StudioServerDefaultShape {
+1 -5
View File
@@ -95,9 +95,7 @@ module.exports = async function (params) {
const cloudApiFileSource = await getProtocolFileSource(cloudApiFilePath)
const cloudProtocolFilePath = path.join(CY_ROOT_DIR, 'packages/server/lib/cloud/protocol.ts')
const cloudProtocolFileSource = await getProtocolFileSource(cloudProtocolFilePath)
const projectBaseFilePath = path.join(CY_ROOT_DIR, 'packages/server/lib/project-base.ts')
const projectBaseFileSource = await getStudioFileSource(projectBaseFilePath)
const getAndInitializeStudioManagerFilePath = path.join(CY_ROOT_DIR, 'packages/server/lib/cloud/api/get_and_initialize_studio_manager.ts')
const getAndInitializeStudioManagerFilePath = path.join(CY_ROOT_DIR, 'packages/server/lib/cloud/api/studio/get_and_initialize_studio_manager.ts')
const getAndInitializeStudioManagerFileSource = await getStudioFileSource(getAndInitializeStudioManagerFilePath)
await Promise.all([
@@ -105,7 +103,6 @@ module.exports = async function (params) {
fs.writeFile(cloudEnvironmentFilePath, cloudEnvironmentFileSource),
fs.writeFile(cloudApiFilePath, cloudApiFileSource),
fs.writeFile(cloudProtocolFilePath, cloudProtocolFileSource),
fs.writeFile(projectBaseFilePath, projectBaseFileSource),
fs.writeFile(getAndInitializeStudioManagerFilePath, getAndInitializeStudioManagerFileSource),
fs.writeFile(path.join(outputFolder, 'index.js'), binaryEntryPointSource),
])
@@ -119,7 +116,6 @@ module.exports = async function (params) {
validateCloudEnvironmentFile(cloudEnvironmentFilePath),
validateProtocolFile(cloudApiFilePath),
validateProtocolFile(cloudProtocolFilePath),
validateStudioFile(projectBaseFilePath),
validateStudioFile(getAndInitializeStudioManagerFilePath),
])
@@ -2,10 +2,13 @@ process.env.CYPRESS_INTERNAL_ENV = process.env.CYPRESS_INTERNAL_ENV ?? 'producti
import path from 'path'
import fs from 'fs-extra'
import { retrieveAndExtractStudioBundle, studioPath } from '@packages/server/lib/cloud/api/get_and_initialize_studio_manager'
import { retrieveAndExtractStudioBundle, studioPath } from '@packages/server/lib/cloud/api/studio/get_and_initialize_studio_manager'
import { postStudioSession } from '@packages/server/lib/cloud/api/studio/post_studio_session'
export const downloadStudioTypes = async (): Promise<void> => {
await retrieveAndExtractStudioBundle({ projectId: 'ypt4pf' })
const studioSession = await postStudioSession({ projectId: 'ypt4pf' })
await retrieveAndExtractStudioBundle({ studioUrl: studioSession.studioUrl, projectId: 'ypt4pf' })
await fs.copyFile(
path.join(studioPath, 'app', 'types.ts'),
+5 -5
View File
@@ -10,23 +10,23 @@ These tests run in CI in Electron, Chrome, Firefox, and WebKit under the `system
## Running System Tests
```bash
yarn test # runs all tests
yarn test-system # runs all tests
## or use globbing to find spec in folders as defined in "glob-in-dir" param in package.json
yarn test screenshot*element # runs screenshot_element_capture_spec.js
yarn test screenshot # runs screenshot_element_capture_spec.js, screenshot_fullpage_capture_spec.js, ..., etc.
yarn test-system screenshot*element # runs screenshot_element_capture_spec.js
yarn test-system screenshot # runs screenshot_element_capture_spec.js, screenshot_fullpage_capture_spec.js, ..., etc.
```
To keep the browser open after a spec run (for easier debugging and iterating on specs), you can pass the `--no-exit` flag to the test command. Live reloading due to spec changes should also work:
```sh
yarn test go_spec.js --browser chrome --no-exit
yarn test-system go_spec.js --browser chrome --no-exit
```
To debug the Cypress process under test, you can pass `--cypress-inspect-brk`:
```sh
yarn test go_spec.js --browser chrome --no-exit --cypress-inspect-brk
yarn test-system go_spec.js --browser chrome --no-exit --cypress-inspect-brk
```
## Developing Tests
@@ -0,0 +1,6 @@
module.exports = {
e2e: {
supportFile: false,
video: true,
},
}
@@ -0,0 +1,13 @@
// here the delays are just so there is something in the screenshots and recordings.
describe('spec1', () => {
it('testCase1', () => {
cy.wait(500)
assert(false)
})
it('testCase2', () => {
cy.wait(500)
assert(true)
})
})
@@ -0,0 +1,13 @@
// here the delays are just so there is something in the screenshots and recordings.
describe('spec2', () => {
it('testCase1', () => {
cy.wait(500)
assert(false)
})
it('testCase2', () => {
cy.wait(500)
assert(true)
})
})
+92
View File
@@ -0,0 +1,92 @@
const { fs } = require('@packages/server/lib/util/fs')
const Fixtures = require('../lib/fixtures')
const systemTests = require('../lib/system-tests').default
const PROJECT_NAME = 'issue-8280-retain-video'
describe('e2e issue 8280', () => {
systemTests.setup()
// https://github.com/cypress-io/cypress/issues/8280
it('should retain the videos from previous runs if trashAssetsBeforeRuns=false', async function () {
// first run
await systemTests.exec(this, {
project: PROJECT_NAME,
snapshot: false,
expectedExitCode: 2,
processEnv: {
'CYPRESS_trashAssetsBeforeRuns': 'false',
},
})
// second run
await systemTests.exec(this, {
project: PROJECT_NAME,
snapshot: false,
expectedExitCode: 2,
processEnv: {
'CYPRESS_trashAssetsBeforeRuns': 'false',
},
})
const spec1Screenshots = await fs.readdir(Fixtures.projectPath(`${PROJECT_NAME}/cypress/screenshots/spec1.cy.js`))
expect(spec1Screenshots.length).to.eq(2)
expect(spec1Screenshots).to.include('spec1 -- testCase1 (failed).png')
expect(spec1Screenshots).to.include('spec1 -- testCase1 (failed) (1).png')
const spec2Screenshots = await fs.readdir(Fixtures.projectPath(`${PROJECT_NAME}/cypress/screenshots/spec2.cy.js`))
expect(spec2Screenshots.length).to.eq(2)
expect(spec2Screenshots).to.include('spec2 -- testCase1 (failed).png')
expect(spec2Screenshots).to.include('spec2 -- testCase1 (failed) (1).png')
const videos = await fs.readdir(Fixtures.projectPath(`${PROJECT_NAME}/cypress/videos`))
expect(videos.length).to.eq(4)
expect(videos).to.include('spec1.cy.js.mp4')
expect(videos).to.include('spec1.cy.js (1).mp4')
expect(videos).to.include('spec2.cy.js.mp4')
expect(videos).to.include('spec2.cy.js (1).mp4')
})
// if trash assets = true, then there will be no retention of screenshots or videos
it('should not retain the videos from previous runs if trashAssetsBeforeRuns=true', async function () {
// first run
await systemTests.exec(this, {
project: PROJECT_NAME,
snapshot: false,
expectedExitCode: 2,
processEnv: {
'CYPRESS_trashAssetsBeforeRuns': 'true',
},
})
// second run
await systemTests.exec(this, {
project: PROJECT_NAME,
snapshot: false,
expectedExitCode: 2,
processEnv: {
'CYPRESS_trashAssetsBeforeRuns': 'true',
},
})
const spec1Screenshots = await fs.readdir(Fixtures.projectPath(`${PROJECT_NAME}/cypress/screenshots/spec1.cy.js`))
expect(spec1Screenshots.length).to.eq(1)
expect(spec1Screenshots).to.include('spec1 -- testCase1 (failed).png')
const spec2Screenshots = await fs.readdir(Fixtures.projectPath(`${PROJECT_NAME}/cypress/screenshots/spec2.cy.js`))
expect(spec2Screenshots.length).to.eq(1)
expect(spec2Screenshots).to.include('spec2 -- testCase1 (failed).png')
const videos = await fs.readdir(Fixtures.projectPath(`${PROJECT_NAME}/cypress/videos`))
expect(videos.length).to.eq(2)
expect(videos).to.include('spec1.cy.js.mp4')
expect(videos).to.include('spec2.cy.js.mp4')
})
})
+1 -1
View File
@@ -93,7 +93,7 @@ necessary consequence taken.
The possible consequences affect the module we verified in the following manner:
- Defer: we need to _defer_ the module in order to prevent it from loading
- NoRewrite: we should not _rewrite_ the module as it results in invalid code
- Norewrite: we should not _rewrite_ the module as it results in invalid code
- None: no consequence, i.e. a light weight warning for informative purposes only
Once we have done this for all leaves, the doctor finds all modules that only depend on those and
+27 -40
View File
@@ -1,10 +1,5 @@
{
"norewrite": [
"./ci-info/index.js",
"./evil-dns/evil-dns.js",
"./get-stream/buffer-stream.js",
"./graceful-fs/polyfills.js",
"./lockfile/lockfile.js",
"./node_modules/@babel/traverse/lib/index.js",
"./node_modules/@babel/traverse/lib/path/comments.js",
"./node_modules/@babel/traverse/lib/path/conversion.js",
@@ -15,28 +10,31 @@
"./node_modules/@colors/colors/lib/system/supports-colors.js",
"./node_modules/@cspotcode/source-map-support/source-map-support.js",
"./node_modules/@cypress/commit-info/node_modules/debug/src/node.js",
"./node_modules/@cypress/commit-info/node_modules/get-stream/buffer-stream.js",
"./node_modules/@cypress/get-windows-proxy/node_modules/debug/src/node.js",
"./node_modules/@cypress/get-windows-proxy/src/registry.js",
"./node_modules/body-parser/node_modules/debug/src/node.js",
"./node_modules/chalk/node_modules/supports-color/index.js",
"./node_modules/chrome-remote-interface/node_modules/ws/lib/websocket.js",
"./node_modules/ci-info/index.js",
"./node_modules/coffeescript/lib/coffeescript/helpers.js",
"./node_modules/compression/node_modules/debug/src/node.js",
"./node_modules/debug/src/node.js",
"./node_modules/evil-dns/evil-dns.js",
"./node_modules/express/node_modules/debug/src/node.js",
"./node_modules/finalhandler/node_modules/debug/src/node.js",
"./node_modules/firefox-profile/node_modules/jsonfile/index.js",
"./node_modules/flatted/cjs/index.js",
"./node_modules/front-matter/node_modules/js-yaml/lib/js-yaml/type/js/function.js",
"./node_modules/fs-extra/node_modules/jsonfile/index.js",
"./node_modules/get-package-info/node_modules/debug/src/node.js",
"./node_modules/get-stream/buffer-stream.js",
"./node_modules/graceful-fs/polyfills.js",
"./node_modules/jose/dist/node/cjs/runtime/verify.js",
"./node_modules/js-yaml/lib/js-yaml/type/js/function.js",
"./node_modules/jsonfile/index.js",
"./node_modules/lockfile/lockfile.js",
"./node_modules/make-dir/index.js",
"./node_modules/minimatch/minimatch.js",
"./node_modules/mocha-7.2.0/node_modules/debug/src/node.js",
"./node_modules/mocha-7.2.0/node_modules/glob/node_modules/minimatch/minimatch.js",
"./node_modules/mocha-junit-reporter/node_modules/debug/src/node.js",
"./node_modules/mocha/node_modules/debug/src/node.js",
"./node_modules/morgan/node_modules/debug/src/node.js",
"./node_modules/prettier/index.js",
@@ -46,24 +44,23 @@
"./node_modules/prettier/parser-meriyah.js",
"./node_modules/prettier/parser-typescript.js",
"./node_modules/prettier/third-party.js",
"./node_modules/process-nextick-args/index.js",
"./node_modules/react-docgen/dist/FileState.js",
"./node_modules/run-applescript/node_modules/get-stream/buffer-stream.js",
"./node_modules/send/node_modules/debug/src/node.js",
"./node_modules/shell-env/node_modules/get-stream/buffer-stream.js",
"./node_modules/signal-exit/index.js",
"./node_modules/stream-parser/node_modules/debug/src/node.js",
"./node_modules/tcp-port-used/node_modules/debug/src/node.js",
"./node_modules/trash/node_modules/make-dir/index.js",
"./packages/data-context/node_modules/debug/src/node.js",
"./packages/data-context/node_modules/minimatch/minimatch.js",
"./packages/graphql/node_modules/debug/src/node.js",
"./node_modules/ws/lib/websocket.js",
"./packages/data-context/node_modules/get-stream/buffer-stream.js",
"./packages/https-proxy/lib/ca.js",
"./packages/net-stubbing/node_modules/debug/src/node.js",
"./packages/network/node_modules/minimatch/minimatch.js",
"./packages/proxy/lib/http/util/prerequests.ts",
"./packages/server/lib/browsers/index.ts",
"./packages/server/lib/browsers/utils.ts",
"./packages/server/lib/capture.js",
"./packages/server/lib/cloud/exception.ts",
"./packages/server/lib/errors.ts",
"./packages/server/lib/modes/record.js",
"./packages/server/lib/modes/run.ts",
"./packages/server/lib/open_project.ts",
"./packages/server/lib/project-base.ts",
@@ -71,12 +68,13 @@
"./packages/server/lib/util/process_profiler.ts",
"./packages/server/lib/util/suppress_warnings.js",
"./packages/server/node_modules/axios/lib/adapters/http.js",
"./packages/server/node_modules/glob/node_modules/minimatch/minimatch.js",
"./packages/server/node_modules/body-parser/node_modules/debug/src/node.js",
"./packages/server/node_modules/get-stream/buffer-stream.js",
"./packages/server/node_modules/graceful-fs/polyfills.js",
"./packages/server/node_modules/mocha/node_modules/debug/src/node.js",
"./process-nextick-args/index.js",
"./signal-exit/index.js",
"./ws/lib/websocket.js"
"./packages/socket/node_modules/socket.io-parser/node_modules/debug/src/node.js",
"./packages/socket/node_modules/socket.io/node_modules/debug/src/node.js",
"./packages/socket/node_modules/socket.io/node_modules/ws/lib/websocket.js"
],
"deferred": [
"./node_modules/@ampproject/remapping/dist/remapping.umd.js",
@@ -145,7 +143,6 @@
"./node_modules/@babel/types/lib/builders/typescript/createTSUnionType.js",
"./node_modules/@babel/types/lib/builders/validateNode.js",
"./node_modules/@babel/types/lib/converters/ensureBlock.js",
"./node_modules/@babel/types/lib/converters/gatherSequenceExpressions.js",
"./node_modules/@babel/types/lib/converters/toBlock.js",
"./node_modules/@babel/types/lib/converters/toComputedKey.js",
"./node_modules/@babel/types/lib/converters/toSequenceExpression.js",
@@ -163,7 +160,6 @@
"./node_modules/@cypress/commit-info/node_modules/debug/src/browser.js",
"./node_modules/@cypress/commit-info/node_modules/debug/src/index.js",
"./node_modules/@cypress/commit-info/node_modules/execa/lib/errname.js",
"./node_modules/@cypress/commit-info/node_modules/get-stream/buffer-stream.js",
"./node_modules/@cypress/commit-info/node_modules/semver/semver.js",
"./node_modules/@cypress/get-windows-proxy/node_modules/debug/src/browser.js",
"./node_modules/@cypress/get-windows-proxy/node_modules/debug/src/index.js",
@@ -233,7 +229,6 @@
"./node_modules/chrome-remote-interface/node_modules/ws/lib/constants.js",
"./node_modules/chrome-remote-interface/node_modules/ws/lib/receiver.js",
"./node_modules/chrome-remote-interface/node_modules/ws/lib/websocket-server.js",
"./node_modules/chrome-remote-interface/node_modules/ws/lib/websocket.js",
"./node_modules/coffeescript/lib/coffeescript/coffeescript.js",
"./node_modules/coffeescript/lib/coffeescript/index.js",
"./node_modules/coffeescript/lib/coffeescript/nodes.js",
@@ -260,7 +255,6 @@
"./node_modules/encoding/node_modules/iconv-lite/encodings/internal.js",
"./node_modules/encoding/node_modules/iconv-lite/lib/index.js",
"./node_modules/esutils/lib/code.js",
"./node_modules/evil-dns/evil-dns.js",
"./node_modules/express-graphql/index.js",
"./node_modules/express-graphql/node_modules/depd/index.js",
"./node_modules/express-graphql/node_modules/http-errors/index.js",
@@ -292,7 +286,6 @@
"./node_modules/front-matter/index.js",
"./node_modules/front-matter/node_modules/js-yaml/lib/js-yaml/loader.js",
"./node_modules/front-matter/node_modules/js-yaml/lib/js-yaml/schema/default_full.js",
"./node_modules/front-matter/node_modules/js-yaml/lib/js-yaml/type/js/function.js",
"./node_modules/fs-extra/lib/fs/index.js",
"./node_modules/fs-extra/lib/index.js",
"./node_modules/fs-extra/lib/json/index.js",
@@ -337,7 +330,6 @@
"./node_modules/jose/dist/node/cjs/runtime/webcrypto.js",
"./node_modules/jsbn/index.js",
"./node_modules/json3/lib/json3.js",
"./node_modules/lockfile/lockfile.js",
"./node_modules/lodash/isBuffer.js",
"./node_modules/lodash/lodash.js",
"./node_modules/make-dir/node_modules/semver/semver.js",
@@ -466,7 +458,6 @@
"./node_modules/picomatch/lib/picomatch.js",
"./node_modules/pidusage/lib/stats.js",
"./node_modules/pinkie/index.js",
"./node_modules/process-nextick-args/index.js",
"./node_modules/pseudomap/map.js",
"./node_modules/pumpify/index.js",
"./node_modules/queue/index.js",
@@ -475,7 +466,6 @@
"./node_modules/react-docgen/dist/importer/fsImporter.js",
"./node_modules/react-docgen/dist/main.js",
"./node_modules/react-docgen/dist/utils/expressionTo.js",
"./node_modules/react-docgen/dist/utils/getMemberExpressionValuePath.js",
"./node_modules/react-docgen/dist/utils/getMemberValuePath.js",
"./node_modules/react-docgen/dist/utils/getPropertyName.js",
"./node_modules/react-docgen/dist/utils/getPropertyValuePath.js",
@@ -510,7 +500,6 @@
"./node_modules/resolve/lib/homedir.js",
"./node_modules/resolve/lib/sync.js",
"./node_modules/run-applescript/node_modules/execa/lib/errname.js",
"./node_modules/run-applescript/node_modules/get-stream/buffer-stream.js",
"./node_modules/run-applescript/node_modules/semver/semver.js",
"./node_modules/safe-buffer/index.js",
"./node_modules/safer-buffer/safer.js",
@@ -520,7 +509,6 @@
"./node_modules/send/node_modules/debug/src/browser.js",
"./node_modules/send/node_modules/debug/src/index.js",
"./node_modules/shell-env/node_modules/execa/lib/errname.js",
"./node_modules/shell-env/node_modules/get-stream/buffer-stream.js",
"./node_modules/shell-env/node_modules/semver/semver.js",
"./node_modules/signal-exit/signals.js",
"./node_modules/simple-git/dist/cjs/index.js",
@@ -599,7 +587,6 @@
"./node_modules/ws/lib/receiver.js",
"./node_modules/ws/lib/validation.js",
"./node_modules/ws/lib/websocket-server.js",
"./node_modules/ws/lib/websocket.js",
"./node_modules/xdg-basedir/index.js",
"./node_modules/xml2js/lib/xml2js.js",
"./packages/config/index.js",
@@ -621,7 +608,6 @@
"./packages/data-context/node_modules/fs-extra/lib/json/index.js",
"./packages/data-context/node_modules/fs-extra/lib/json/jsonfile.js",
"./packages/data-context/node_modules/fs-extra/lib/path-exists/index.js",
"./packages/data-context/node_modules/get-stream/buffer-stream.js",
"./packages/data-context/node_modules/readdirp/index.js",
"./packages/data-context/src/DataActions.ts",
"./packages/data-context/src/DataContext.ts",
@@ -757,9 +743,9 @@
"./packages/server/lib/browsers/memory/index.ts",
"./packages/server/lib/cache.ts",
"./packages/server/lib/cloud/api/cloud_request.ts",
"./packages/server/lib/cloud/api/get_and_initialize_studio_manager.ts",
"./packages/server/lib/cloud/api/index.ts",
"./packages/server/lib/cloud/api/put_protocol_artifact.ts",
"./packages/server/lib/cloud/api/studio/get_and_initialize_studio_manager.ts",
"./packages/server/lib/cloud/artifacts/protocol_artifact.ts",
"./packages/server/lib/cloud/artifacts/screenshot_artifact.ts",
"./packages/server/lib/cloud/artifacts/video_artifact.ts",
@@ -799,13 +785,13 @@
"./packages/server/node_modules/body-parser/node_modules/debug/src/browser.js",
"./packages/server/node_modules/body-parser/node_modules/debug/src/index.js",
"./packages/server/node_modules/body-parser/node_modules/debug/src/node.js",
"./packages/server/node_modules/chownr/chownr.js",
"./packages/server/node_modules/cross-fetch/node_modules/node-fetch/lib/index.js",
"./packages/server/node_modules/cross-spawn/node_modules/semver/semver.js",
"./packages/server/node_modules/duplexify/index.js",
"./packages/server/node_modules/execa/lib/errname.js",
"./packages/server/node_modules/fs-minipass/index.js",
"./packages/server/node_modules/fs-minipass/node_modules/minipass/index.js",
"./packages/server/node_modules/get-stream/buffer-stream.js",
"./packages/server/node_modules/glob/glob.js",
"./packages/server/node_modules/glob/sync.js",
"./packages/server/node_modules/graceful-fs/graceful-fs.js",
@@ -859,7 +845,6 @@
"./packages/socket/lib/socket.ts",
"./packages/socket/node_modules/socket.io-parser/node_modules/debug/src/browser.js",
"./packages/socket/node_modules/socket.io-parser/node_modules/debug/src/index.js",
"./packages/socket/node_modules/socket.io-parser/node_modules/debug/src/node.js",
"./packages/socket/node_modules/socket.io/dist/broadcast-operator.js",
"./packages/socket/node_modules/socket.io/dist/index.js",
"./packages/socket/node_modules/socket.io/dist/namespace.js",
@@ -868,7 +853,6 @@
"./packages/socket/node_modules/socket.io/dist/typed-events.js",
"./packages/socket/node_modules/socket.io/node_modules/debug/src/browser.js",
"./packages/socket/node_modules/socket.io/node_modules/debug/src/index.js",
"./packages/socket/node_modules/socket.io/node_modules/debug/src/node.js",
"./packages/socket/node_modules/socket.io/node_modules/engine.io/lib/server.js",
"./packages/socket/node_modules/socket.io/node_modules/engine.io/lib/socket.js",
"./packages/socket/node_modules/socket.io/node_modules/engine.io/lib/transport.js",
@@ -880,7 +864,6 @@
"./packages/socket/node_modules/socket.io/node_modules/ws/lib/constants.js",
"./packages/socket/node_modules/socket.io/node_modules/ws/lib/receiver.js",
"./packages/socket/node_modules/socket.io/node_modules/ws/lib/websocket-server.js",
"./packages/socket/node_modules/socket.io/node_modules/ws/lib/websocket.js",
"./packages/ts/register.js",
"./packages/types/index.js",
"./tooling/v8-snapshot/dist/setup/v8-snapshot-entry-cy-in-cy.js"
@@ -1013,6 +996,7 @@
"./node_modules/@babel/types/lib/comments/removeComments.js",
"./node_modules/@babel/types/lib/constants/generated/index.js",
"./node_modules/@babel/types/lib/constants/index.js",
"./node_modules/@babel/types/lib/converters/gatherSequenceExpressions.js",
"./node_modules/@babel/types/lib/converters/toBindingIdentifierName.js",
"./node_modules/@babel/types/lib/converters/toExpression.js",
"./node_modules/@babel/types/lib/converters/toIdentifier.js",
@@ -1629,7 +1613,6 @@
"./node_modules/chrome-remote-interface/node_modules/ws/lib/sender.js",
"./node_modules/chrome-remote-interface/node_modules/ws/lib/stream.js",
"./node_modules/chrome-remote-interface/node_modules/ws/lib/validation.js",
"./node_modules/ci-info/index.js",
"./node_modules/ci-info/vendors.json",
"./node_modules/cli-table3/index.js",
"./node_modules/cli-table3/src/cell.js",
@@ -3138,6 +3121,7 @@
"./node_modules/react-docgen/dist/utils/getClassMemberValuePath.js",
"./node_modules/react-docgen/dist/utils/getFlowType.js",
"./node_modules/react-docgen/dist/utils/getMemberExpressionRoot.js",
"./node_modules/react-docgen/dist/utils/getMemberExpressionValuePath.js",
"./node_modules/react-docgen/dist/utils/getMembers.js",
"./node_modules/react-docgen/dist/utils/getMethodDocumentation.js",
"./node_modules/react-docgen/dist/utils/getNameOrValue.js",
@@ -3325,7 +3309,6 @@
"./node_modules/side-channel-map/index.js",
"./node_modules/side-channel-weakmap/index.js",
"./node_modules/side-channel/index.js",
"./node_modules/signal-exit/index.js",
"./node_modules/simple-swizzle/index.js",
"./node_modules/simple-swizzle/node_modules/is-arrayish/index.js",
"./node_modules/slash/index.js",
@@ -3879,6 +3862,7 @@
"./packages/scaffold-config/src/index.ts",
"./packages/scaffold-config/src/supportFile.ts",
"./packages/server/config/app.json",
"./packages/server/lib/StudioLifecycleManager.ts",
"./packages/server/lib/automation/automation.ts",
"./packages/server/lib/automation/automation_not_implemented.ts",
"./packages/server/lib/automation/commands/key_press.ts",
@@ -3905,6 +3889,8 @@
"./packages/server/lib/cloud/api/axios_middleware/logging.ts",
"./packages/server/lib/cloud/api/axios_middleware/transform_error.ts",
"./packages/server/lib/cloud/api/scrub_url.ts",
"./packages/server/lib/cloud/api/studio/post_studio_session.ts",
"./packages/server/lib/cloud/api/studio/report_studio_error.ts",
"./packages/server/lib/cloud/artifacts/artifact.ts",
"./packages/server/lib/cloud/artifacts/file_upload_strategy.ts",
"./packages/server/lib/cloud/artifacts/print_protocol_upload_error.ts",
@@ -3920,6 +3906,7 @@
"./packages/server/lib/cloud/network/system_error.ts",
"./packages/server/lib/cloud/protocol.ts",
"./packages/server/lib/cloud/require_script.ts",
"./packages/server/lib/cloud/strip_path.ts",
"./packages/server/lib/cloud/studio.ts",
"./packages/server/lib/cloud/upload/send_file.ts",
"./packages/server/lib/cloud/upload/stream_activity_monitor.ts",
@@ -4230,5 +4217,5 @@
"./tooling/v8-snapshot/cache/darwin/snapshot-entry.js"
],
"deferredHashFile": "yarn.lock",
"deferredHash": "cd76680a6f1bcefa099431a7c1e265c393113c83aabf100326aa163035f57483"
"deferredHash": "5d74de9bbedf1995fdb2501108e4358ecb4d72771a17ba6ac4b53a1900edb977"
}
+27 -40
View File
@@ -1,10 +1,5 @@
{
"norewrite": [
"./ci-info/index.js",
"./evil-dns/evil-dns.js",
"./get-stream/buffer-stream.js",
"./graceful-fs/polyfills.js",
"./lockfile/lockfile.js",
"./node_modules/@babel/traverse/lib/index.js",
"./node_modules/@babel/traverse/lib/path/comments.js",
"./node_modules/@babel/traverse/lib/path/conversion.js",
@@ -15,28 +10,31 @@
"./node_modules/@colors/colors/lib/system/supports-colors.js",
"./node_modules/@cspotcode/source-map-support/source-map-support.js",
"./node_modules/@cypress/commit-info/node_modules/debug/src/node.js",
"./node_modules/@cypress/commit-info/node_modules/get-stream/buffer-stream.js",
"./node_modules/@cypress/get-windows-proxy/node_modules/debug/src/node.js",
"./node_modules/@cypress/get-windows-proxy/src/registry.js",
"./node_modules/body-parser/node_modules/debug/src/node.js",
"./node_modules/chalk/node_modules/supports-color/index.js",
"./node_modules/chrome-remote-interface/node_modules/ws/lib/websocket.js",
"./node_modules/ci-info/index.js",
"./node_modules/coffeescript/lib/coffeescript/helpers.js",
"./node_modules/compression/node_modules/debug/src/node.js",
"./node_modules/debug/src/node.js",
"./node_modules/evil-dns/evil-dns.js",
"./node_modules/express/node_modules/debug/src/node.js",
"./node_modules/finalhandler/node_modules/debug/src/node.js",
"./node_modules/firefox-profile/node_modules/jsonfile/index.js",
"./node_modules/flatted/cjs/index.js",
"./node_modules/front-matter/node_modules/js-yaml/lib/js-yaml/type/js/function.js",
"./node_modules/fs-extra/node_modules/jsonfile/index.js",
"./node_modules/get-package-info/node_modules/debug/src/node.js",
"./node_modules/get-stream/buffer-stream.js",
"./node_modules/graceful-fs/polyfills.js",
"./node_modules/jose/dist/node/cjs/runtime/verify.js",
"./node_modules/js-yaml/lib/js-yaml/type/js/function.js",
"./node_modules/jsonfile/index.js",
"./node_modules/lockfile/lockfile.js",
"./node_modules/make-dir/index.js",
"./node_modules/minimatch/minimatch.js",
"./node_modules/mocha-7.2.0/node_modules/debug/src/node.js",
"./node_modules/mocha-7.2.0/node_modules/glob/node_modules/minimatch/minimatch.js",
"./node_modules/mocha-junit-reporter/node_modules/debug/src/node.js",
"./node_modules/mocha/node_modules/debug/src/node.js",
"./node_modules/morgan/node_modules/debug/src/node.js",
"./node_modules/prettier/index.js",
@@ -46,24 +44,23 @@
"./node_modules/prettier/parser-meriyah.js",
"./node_modules/prettier/parser-typescript.js",
"./node_modules/prettier/third-party.js",
"./node_modules/process-nextick-args/index.js",
"./node_modules/react-docgen/dist/FileState.js",
"./node_modules/run-applescript/node_modules/get-stream/buffer-stream.js",
"./node_modules/send/node_modules/debug/src/node.js",
"./node_modules/shell-env/node_modules/get-stream/buffer-stream.js",
"./node_modules/signal-exit/index.js",
"./node_modules/stream-parser/node_modules/debug/src/node.js",
"./node_modules/tcp-port-used/node_modules/debug/src/node.js",
"./node_modules/trash/node_modules/make-dir/index.js",
"./packages/data-context/node_modules/debug/src/node.js",
"./packages/data-context/node_modules/minimatch/minimatch.js",
"./packages/graphql/node_modules/debug/src/node.js",
"./node_modules/ws/lib/websocket.js",
"./packages/data-context/node_modules/get-stream/buffer-stream.js",
"./packages/https-proxy/lib/ca.js",
"./packages/net-stubbing/node_modules/debug/src/node.js",
"./packages/network/node_modules/minimatch/minimatch.js",
"./packages/proxy/lib/http/util/prerequests.ts",
"./packages/server/lib/browsers/index.ts",
"./packages/server/lib/browsers/utils.ts",
"./packages/server/lib/capture.js",
"./packages/server/lib/cloud/exception.ts",
"./packages/server/lib/errors.ts",
"./packages/server/lib/modes/record.js",
"./packages/server/lib/modes/run.ts",
"./packages/server/lib/open_project.ts",
"./packages/server/lib/project-base.ts",
@@ -71,12 +68,13 @@
"./packages/server/lib/util/process_profiler.ts",
"./packages/server/lib/util/suppress_warnings.js",
"./packages/server/node_modules/axios/lib/adapters/http.js",
"./packages/server/node_modules/glob/node_modules/minimatch/minimatch.js",
"./packages/server/node_modules/body-parser/node_modules/debug/src/node.js",
"./packages/server/node_modules/get-stream/buffer-stream.js",
"./packages/server/node_modules/graceful-fs/polyfills.js",
"./packages/server/node_modules/mocha/node_modules/debug/src/node.js",
"./process-nextick-args/index.js",
"./signal-exit/index.js",
"./ws/lib/websocket.js"
"./packages/socket/node_modules/socket.io-parser/node_modules/debug/src/node.js",
"./packages/socket/node_modules/socket.io/node_modules/debug/src/node.js",
"./packages/socket/node_modules/socket.io/node_modules/ws/lib/websocket.js"
],
"deferred": [
"./node_modules/@ampproject/remapping/dist/remapping.umd.js",
@@ -145,7 +143,6 @@
"./node_modules/@babel/types/lib/builders/typescript/createTSUnionType.js",
"./node_modules/@babel/types/lib/builders/validateNode.js",
"./node_modules/@babel/types/lib/converters/ensureBlock.js",
"./node_modules/@babel/types/lib/converters/gatherSequenceExpressions.js",
"./node_modules/@babel/types/lib/converters/toBlock.js",
"./node_modules/@babel/types/lib/converters/toComputedKey.js",
"./node_modules/@babel/types/lib/converters/toSequenceExpression.js",
@@ -163,7 +160,6 @@
"./node_modules/@cypress/commit-info/node_modules/debug/src/browser.js",
"./node_modules/@cypress/commit-info/node_modules/debug/src/index.js",
"./node_modules/@cypress/commit-info/node_modules/execa/lib/errname.js",
"./node_modules/@cypress/commit-info/node_modules/get-stream/buffer-stream.js",
"./node_modules/@cypress/commit-info/node_modules/semver/semver.js",
"./node_modules/@cypress/get-windows-proxy/node_modules/debug/src/browser.js",
"./node_modules/@cypress/get-windows-proxy/node_modules/debug/src/index.js",
@@ -233,7 +229,6 @@
"./node_modules/chrome-remote-interface/node_modules/ws/lib/constants.js",
"./node_modules/chrome-remote-interface/node_modules/ws/lib/receiver.js",
"./node_modules/chrome-remote-interface/node_modules/ws/lib/websocket-server.js",
"./node_modules/chrome-remote-interface/node_modules/ws/lib/websocket.js",
"./node_modules/coffeescript/lib/coffeescript/coffeescript.js",
"./node_modules/coffeescript/lib/coffeescript/index.js",
"./node_modules/coffeescript/lib/coffeescript/nodes.js",
@@ -260,7 +255,6 @@
"./node_modules/encoding/node_modules/iconv-lite/encodings/internal.js",
"./node_modules/encoding/node_modules/iconv-lite/lib/index.js",
"./node_modules/esutils/lib/code.js",
"./node_modules/evil-dns/evil-dns.js",
"./node_modules/express-graphql/index.js",
"./node_modules/express-graphql/node_modules/depd/index.js",
"./node_modules/express-graphql/node_modules/http-errors/index.js",
@@ -292,7 +286,6 @@
"./node_modules/front-matter/index.js",
"./node_modules/front-matter/node_modules/js-yaml/lib/js-yaml/loader.js",
"./node_modules/front-matter/node_modules/js-yaml/lib/js-yaml/schema/default_full.js",
"./node_modules/front-matter/node_modules/js-yaml/lib/js-yaml/type/js/function.js",
"./node_modules/fs-extra/lib/fs/index.js",
"./node_modules/fs-extra/lib/index.js",
"./node_modules/fs-extra/lib/json/index.js",
@@ -336,7 +329,6 @@
"./node_modules/jose/dist/node/cjs/runtime/webcrypto.js",
"./node_modules/jsbn/index.js",
"./node_modules/json3/lib/json3.js",
"./node_modules/lockfile/lockfile.js",
"./node_modules/lodash/isBuffer.js",
"./node_modules/lodash/lodash.js",
"./node_modules/make-dir/node_modules/semver/semver.js",
@@ -465,7 +457,6 @@
"./node_modules/picomatch/lib/picomatch.js",
"./node_modules/pidusage/lib/stats.js",
"./node_modules/pinkie/index.js",
"./node_modules/process-nextick-args/index.js",
"./node_modules/pseudomap/map.js",
"./node_modules/pumpify/index.js",
"./node_modules/queue/index.js",
@@ -474,7 +465,6 @@
"./node_modules/react-docgen/dist/importer/fsImporter.js",
"./node_modules/react-docgen/dist/main.js",
"./node_modules/react-docgen/dist/utils/expressionTo.js",
"./node_modules/react-docgen/dist/utils/getMemberExpressionValuePath.js",
"./node_modules/react-docgen/dist/utils/getMemberValuePath.js",
"./node_modules/react-docgen/dist/utils/getPropertyName.js",
"./node_modules/react-docgen/dist/utils/getPropertyValuePath.js",
@@ -509,7 +499,6 @@
"./node_modules/resolve/lib/homedir.js",
"./node_modules/resolve/lib/sync.js",
"./node_modules/run-applescript/node_modules/execa/lib/errname.js",
"./node_modules/run-applescript/node_modules/get-stream/buffer-stream.js",
"./node_modules/run-applescript/node_modules/semver/semver.js",
"./node_modules/safe-buffer/index.js",
"./node_modules/safer-buffer/safer.js",
@@ -519,7 +508,6 @@
"./node_modules/send/node_modules/debug/src/browser.js",
"./node_modules/send/node_modules/debug/src/index.js",
"./node_modules/shell-env/node_modules/execa/lib/errname.js",
"./node_modules/shell-env/node_modules/get-stream/buffer-stream.js",
"./node_modules/shell-env/node_modules/semver/semver.js",
"./node_modules/signal-exit/signals.js",
"./node_modules/simple-git/dist/cjs/index.js",
@@ -598,7 +586,6 @@
"./node_modules/ws/lib/receiver.js",
"./node_modules/ws/lib/validation.js",
"./node_modules/ws/lib/websocket-server.js",
"./node_modules/ws/lib/websocket.js",
"./node_modules/xdg-basedir/index.js",
"./node_modules/xml2js/lib/xml2js.js",
"./packages/config/index.js",
@@ -620,7 +607,6 @@
"./packages/data-context/node_modules/fs-extra/lib/json/index.js",
"./packages/data-context/node_modules/fs-extra/lib/json/jsonfile.js",
"./packages/data-context/node_modules/fs-extra/lib/path-exists/index.js",
"./packages/data-context/node_modules/get-stream/buffer-stream.js",
"./packages/data-context/node_modules/readdirp/index.js",
"./packages/data-context/src/DataActions.ts",
"./packages/data-context/src/DataContext.ts",
@@ -756,9 +742,9 @@
"./packages/server/lib/browsers/memory/index.ts",
"./packages/server/lib/cache.ts",
"./packages/server/lib/cloud/api/cloud_request.ts",
"./packages/server/lib/cloud/api/get_and_initialize_studio_manager.ts",
"./packages/server/lib/cloud/api/index.ts",
"./packages/server/lib/cloud/api/put_protocol_artifact.ts",
"./packages/server/lib/cloud/api/studio/get_and_initialize_studio_manager.ts",
"./packages/server/lib/cloud/artifacts/protocol_artifact.ts",
"./packages/server/lib/cloud/artifacts/screenshot_artifact.ts",
"./packages/server/lib/cloud/artifacts/video_artifact.ts",
@@ -798,13 +784,13 @@
"./packages/server/node_modules/body-parser/node_modules/debug/src/browser.js",
"./packages/server/node_modules/body-parser/node_modules/debug/src/index.js",
"./packages/server/node_modules/body-parser/node_modules/debug/src/node.js",
"./packages/server/node_modules/chownr/chownr.js",
"./packages/server/node_modules/cross-fetch/node_modules/node-fetch/lib/index.js",
"./packages/server/node_modules/cross-spawn/node_modules/semver/semver.js",
"./packages/server/node_modules/duplexify/index.js",
"./packages/server/node_modules/execa/lib/errname.js",
"./packages/server/node_modules/fs-minipass/index.js",
"./packages/server/node_modules/fs-minipass/node_modules/minipass/index.js",
"./packages/server/node_modules/get-stream/buffer-stream.js",
"./packages/server/node_modules/glob/glob.js",
"./packages/server/node_modules/glob/sync.js",
"./packages/server/node_modules/graceful-fs/graceful-fs.js",
@@ -858,7 +844,6 @@
"./packages/socket/lib/socket.ts",
"./packages/socket/node_modules/socket.io-parser/node_modules/debug/src/browser.js",
"./packages/socket/node_modules/socket.io-parser/node_modules/debug/src/index.js",
"./packages/socket/node_modules/socket.io-parser/node_modules/debug/src/node.js",
"./packages/socket/node_modules/socket.io/dist/broadcast-operator.js",
"./packages/socket/node_modules/socket.io/dist/index.js",
"./packages/socket/node_modules/socket.io/dist/namespace.js",
@@ -867,7 +852,6 @@
"./packages/socket/node_modules/socket.io/dist/typed-events.js",
"./packages/socket/node_modules/socket.io/node_modules/debug/src/browser.js",
"./packages/socket/node_modules/socket.io/node_modules/debug/src/index.js",
"./packages/socket/node_modules/socket.io/node_modules/debug/src/node.js",
"./packages/socket/node_modules/socket.io/node_modules/engine.io/lib/server.js",
"./packages/socket/node_modules/socket.io/node_modules/engine.io/lib/socket.js",
"./packages/socket/node_modules/socket.io/node_modules/engine.io/lib/transport.js",
@@ -879,7 +863,6 @@
"./packages/socket/node_modules/socket.io/node_modules/ws/lib/constants.js",
"./packages/socket/node_modules/socket.io/node_modules/ws/lib/receiver.js",
"./packages/socket/node_modules/socket.io/node_modules/ws/lib/websocket-server.js",
"./packages/socket/node_modules/socket.io/node_modules/ws/lib/websocket.js",
"./packages/ts/register.js",
"./packages/types/index.js",
"./tooling/v8-snapshot/dist/setup/v8-snapshot-entry-cy-in-cy.js"
@@ -1012,6 +995,7 @@
"./node_modules/@babel/types/lib/comments/removeComments.js",
"./node_modules/@babel/types/lib/constants/generated/index.js",
"./node_modules/@babel/types/lib/constants/index.js",
"./node_modules/@babel/types/lib/converters/gatherSequenceExpressions.js",
"./node_modules/@babel/types/lib/converters/toBindingIdentifierName.js",
"./node_modules/@babel/types/lib/converters/toExpression.js",
"./node_modules/@babel/types/lib/converters/toIdentifier.js",
@@ -1630,7 +1614,6 @@
"./node_modules/chrome-remote-interface/node_modules/ws/lib/sender.js",
"./node_modules/chrome-remote-interface/node_modules/ws/lib/stream.js",
"./node_modules/chrome-remote-interface/node_modules/ws/lib/validation.js",
"./node_modules/ci-info/index.js",
"./node_modules/ci-info/vendors.json",
"./node_modules/cli-table3/index.js",
"./node_modules/cli-table3/src/cell.js",
@@ -3139,6 +3122,7 @@
"./node_modules/react-docgen/dist/utils/getClassMemberValuePath.js",
"./node_modules/react-docgen/dist/utils/getFlowType.js",
"./node_modules/react-docgen/dist/utils/getMemberExpressionRoot.js",
"./node_modules/react-docgen/dist/utils/getMemberExpressionValuePath.js",
"./node_modules/react-docgen/dist/utils/getMembers.js",
"./node_modules/react-docgen/dist/utils/getMethodDocumentation.js",
"./node_modules/react-docgen/dist/utils/getNameOrValue.js",
@@ -3328,7 +3312,6 @@
"./node_modules/side-channel-map/index.js",
"./node_modules/side-channel-weakmap/index.js",
"./node_modules/side-channel/index.js",
"./node_modules/signal-exit/index.js",
"./node_modules/simple-swizzle/index.js",
"./node_modules/simple-swizzle/node_modules/is-arrayish/index.js",
"./node_modules/slash/index.js",
@@ -3882,6 +3865,7 @@
"./packages/scaffold-config/src/index.ts",
"./packages/scaffold-config/src/supportFile.ts",
"./packages/server/config/app.json",
"./packages/server/lib/StudioLifecycleManager.ts",
"./packages/server/lib/automation/automation.ts",
"./packages/server/lib/automation/automation_not_implemented.ts",
"./packages/server/lib/automation/commands/key_press.ts",
@@ -3908,6 +3892,8 @@
"./packages/server/lib/cloud/api/axios_middleware/logging.ts",
"./packages/server/lib/cloud/api/axios_middleware/transform_error.ts",
"./packages/server/lib/cloud/api/scrub_url.ts",
"./packages/server/lib/cloud/api/studio/post_studio_session.ts",
"./packages/server/lib/cloud/api/studio/report_studio_error.ts",
"./packages/server/lib/cloud/artifacts/artifact.ts",
"./packages/server/lib/cloud/artifacts/file_upload_strategy.ts",
"./packages/server/lib/cloud/artifacts/print_protocol_upload_error.ts",
@@ -3923,6 +3909,7 @@
"./packages/server/lib/cloud/network/system_error.ts",
"./packages/server/lib/cloud/protocol.ts",
"./packages/server/lib/cloud/require_script.ts",
"./packages/server/lib/cloud/strip_path.ts",
"./packages/server/lib/cloud/studio.ts",
"./packages/server/lib/cloud/upload/send_file.ts",
"./packages/server/lib/cloud/upload/stream_activity_monitor.ts",
@@ -4233,5 +4220,5 @@
"./tooling/v8-snapshot/cache/linux/snapshot-entry.js"
],
"deferredHashFile": "yarn.lock",
"deferredHash": "cd76680a6f1bcefa099431a7c1e265c393113c83aabf100326aa163035f57483"
"deferredHash": "5d74de9bbedf1995fdb2501108e4358ecb4d72771a17ba6ac4b53a1900edb977"
}
+27 -40
View File
@@ -1,10 +1,5 @@
{
"norewrite": [
"./ci-info/index.js",
"./evil-dns/evil-dns.js",
"./get-stream/buffer-stream.js",
"./graceful-fs/polyfills.js",
"./lockfile/lockfile.js",
"./node_modules/@babel/traverse/lib/index.js",
"./node_modules/@babel/traverse/lib/path/comments.js",
"./node_modules/@babel/traverse/lib/path/conversion.js",
@@ -15,28 +10,31 @@
"./node_modules/@colors/colors/lib/system/supports-colors.js",
"./node_modules/@cspotcode/source-map-support/source-map-support.js",
"./node_modules/@cypress/commit-info/node_modules/debug/src/node.js",
"./node_modules/@cypress/commit-info/node_modules/get-stream/buffer-stream.js",
"./node_modules/@cypress/get-windows-proxy/node_modules/debug/src/node.js",
"./node_modules/@cypress/get-windows-proxy/src/registry.js",
"./node_modules/body-parser/node_modules/debug/src/node.js",
"./node_modules/chalk/node_modules/supports-color/index.js",
"./node_modules/chrome-remote-interface/node_modules/ws/lib/websocket.js",
"./node_modules/ci-info/index.js",
"./node_modules/coffeescript/lib/coffeescript/helpers.js",
"./node_modules/compression/node_modules/debug/src/node.js",
"./node_modules/debug/src/node.js",
"./node_modules/evil-dns/evil-dns.js",
"./node_modules/express/node_modules/debug/src/node.js",
"./node_modules/finalhandler/node_modules/debug/src/node.js",
"./node_modules/firefox-profile/node_modules/jsonfile/index.js",
"./node_modules/flatted/cjs/index.js",
"./node_modules/front-matter/node_modules/js-yaml/lib/js-yaml/type/js/function.js",
"./node_modules/fs-extra/node_modules/jsonfile/index.js",
"./node_modules/get-package-info/node_modules/debug/src/node.js",
"./node_modules/get-stream/buffer-stream.js",
"./node_modules/graceful-fs/polyfills.js",
"./node_modules/jose/dist/node/cjs/runtime/verify.js",
"./node_modules/js-yaml/lib/js-yaml/type/js/function.js",
"./node_modules/jsonfile/index.js",
"./node_modules/lockfile/lockfile.js",
"./node_modules/make-dir/index.js",
"./node_modules/minimatch/minimatch.js",
"./node_modules/mocha-7.2.0/node_modules/debug/src/node.js",
"./node_modules/mocha-7.2.0/node_modules/glob/node_modules/minimatch/minimatch.js",
"./node_modules/mocha-junit-reporter/node_modules/debug/src/node.js",
"./node_modules/mocha/node_modules/debug/src/node.js",
"./node_modules/morgan/node_modules/debug/src/node.js",
"./node_modules/prettier/index.js",
@@ -46,24 +44,23 @@
"./node_modules/prettier/parser-meriyah.js",
"./node_modules/prettier/parser-typescript.js",
"./node_modules/prettier/third-party.js",
"./node_modules/process-nextick-args/index.js",
"./node_modules/react-docgen/dist/FileState.js",
"./node_modules/run-applescript/node_modules/get-stream/buffer-stream.js",
"./node_modules/send/node_modules/debug/src/node.js",
"./node_modules/shell-env/node_modules/get-stream/buffer-stream.js",
"./node_modules/signal-exit/index.js",
"./node_modules/stream-parser/node_modules/debug/src/node.js",
"./node_modules/tcp-port-used/node_modules/debug/src/node.js",
"./node_modules/trash/node_modules/make-dir/index.js",
"./packages/data-context/node_modules/debug/src/node.js",
"./packages/data-context/node_modules/minimatch/minimatch.js",
"./packages/graphql/node_modules/debug/src/node.js",
"./node_modules/ws/lib/websocket.js",
"./packages/data-context/node_modules/get-stream/buffer-stream.js",
"./packages/https-proxy/lib/ca.js",
"./packages/net-stubbing/node_modules/debug/src/node.js",
"./packages/network/node_modules/minimatch/minimatch.js",
"./packages/proxy/lib/http/util/prerequests.ts",
"./packages/server/lib/browsers/index.ts",
"./packages/server/lib/browsers/utils.ts",
"./packages/server/lib/capture.js",
"./packages/server/lib/cloud/exception.ts",
"./packages/server/lib/errors.ts",
"./packages/server/lib/modes/record.js",
"./packages/server/lib/modes/run.ts",
"./packages/server/lib/open_project.ts",
"./packages/server/lib/project-base.ts",
@@ -71,12 +68,13 @@
"./packages/server/lib/util/process_profiler.ts",
"./packages/server/lib/util/suppress_warnings.js",
"./packages/server/node_modules/axios/lib/adapters/http.js",
"./packages/server/node_modules/glob/node_modules/minimatch/minimatch.js",
"./packages/server/node_modules/body-parser/node_modules/debug/src/node.js",
"./packages/server/node_modules/get-stream/buffer-stream.js",
"./packages/server/node_modules/graceful-fs/polyfills.js",
"./packages/server/node_modules/mocha/node_modules/debug/src/node.js",
"./process-nextick-args/index.js",
"./signal-exit/index.js",
"./ws/lib/websocket.js"
"./packages/socket/node_modules/socket.io-parser/node_modules/debug/src/node.js",
"./packages/socket/node_modules/socket.io/node_modules/debug/src/node.js",
"./packages/socket/node_modules/socket.io/node_modules/ws/lib/websocket.js"
],
"deferred": [
"./node_modules/@ampproject/remapping/dist/remapping.umd.js",
@@ -145,7 +143,6 @@
"./node_modules/@babel/types/lib/builders/typescript/createTSUnionType.js",
"./node_modules/@babel/types/lib/builders/validateNode.js",
"./node_modules/@babel/types/lib/converters/ensureBlock.js",
"./node_modules/@babel/types/lib/converters/gatherSequenceExpressions.js",
"./node_modules/@babel/types/lib/converters/toBlock.js",
"./node_modules/@babel/types/lib/converters/toComputedKey.js",
"./node_modules/@babel/types/lib/converters/toSequenceExpression.js",
@@ -163,7 +160,6 @@
"./node_modules/@cypress/commit-info/node_modules/debug/src/browser.js",
"./node_modules/@cypress/commit-info/node_modules/debug/src/index.js",
"./node_modules/@cypress/commit-info/node_modules/execa/lib/errname.js",
"./node_modules/@cypress/commit-info/node_modules/get-stream/buffer-stream.js",
"./node_modules/@cypress/commit-info/node_modules/semver/semver.js",
"./node_modules/@cypress/get-windows-proxy/node_modules/debug/src/browser.js",
"./node_modules/@cypress/get-windows-proxy/node_modules/debug/src/index.js",
@@ -235,7 +231,6 @@
"./node_modules/chrome-remote-interface/node_modules/ws/lib/constants.js",
"./node_modules/chrome-remote-interface/node_modules/ws/lib/receiver.js",
"./node_modules/chrome-remote-interface/node_modules/ws/lib/websocket-server.js",
"./node_modules/chrome-remote-interface/node_modules/ws/lib/websocket.js",
"./node_modules/coffeescript/lib/coffeescript/coffeescript.js",
"./node_modules/coffeescript/lib/coffeescript/index.js",
"./node_modules/coffeescript/lib/coffeescript/nodes.js",
@@ -262,7 +257,6 @@
"./node_modules/encoding/node_modules/iconv-lite/encodings/internal.js",
"./node_modules/encoding/node_modules/iconv-lite/lib/index.js",
"./node_modules/esutils/lib/code.js",
"./node_modules/evil-dns/evil-dns.js",
"./node_modules/express-graphql/index.js",
"./node_modules/express-graphql/node_modules/depd/index.js",
"./node_modules/express-graphql/node_modules/http-errors/index.js",
@@ -294,7 +288,6 @@
"./node_modules/front-matter/index.js",
"./node_modules/front-matter/node_modules/js-yaml/lib/js-yaml/loader.js",
"./node_modules/front-matter/node_modules/js-yaml/lib/js-yaml/schema/default_full.js",
"./node_modules/front-matter/node_modules/js-yaml/lib/js-yaml/type/js/function.js",
"./node_modules/fs-extra/lib/fs/index.js",
"./node_modules/fs-extra/lib/index.js",
"./node_modules/fs-extra/lib/json/index.js",
@@ -338,7 +331,6 @@
"./node_modules/jose/dist/node/cjs/runtime/webcrypto.js",
"./node_modules/jsbn/index.js",
"./node_modules/json3/lib/json3.js",
"./node_modules/lockfile/lockfile.js",
"./node_modules/lodash/isBuffer.js",
"./node_modules/lodash/lodash.js",
"./node_modules/make-dir/node_modules/semver/semver.js",
@@ -467,7 +459,6 @@
"./node_modules/picomatch/lib/picomatch.js",
"./node_modules/pidusage/lib/stats.js",
"./node_modules/pinkie/index.js",
"./node_modules/process-nextick-args/index.js",
"./node_modules/pseudomap/map.js",
"./node_modules/pumpify/index.js",
"./node_modules/queue/index.js",
@@ -476,7 +467,6 @@
"./node_modules/react-docgen/dist/importer/fsImporter.js",
"./node_modules/react-docgen/dist/main.js",
"./node_modules/react-docgen/dist/utils/expressionTo.js",
"./node_modules/react-docgen/dist/utils/getMemberExpressionValuePath.js",
"./node_modules/react-docgen/dist/utils/getMemberValuePath.js",
"./node_modules/react-docgen/dist/utils/getPropertyName.js",
"./node_modules/react-docgen/dist/utils/getPropertyValuePath.js",
@@ -513,7 +503,6 @@
"./node_modules/resolve/lib/homedir.js",
"./node_modules/resolve/lib/sync.js",
"./node_modules/run-applescript/node_modules/execa/lib/errname.js",
"./node_modules/run-applescript/node_modules/get-stream/buffer-stream.js",
"./node_modules/run-applescript/node_modules/semver/semver.js",
"./node_modules/safe-buffer/index.js",
"./node_modules/safer-buffer/safer.js",
@@ -523,7 +512,6 @@
"./node_modules/send/node_modules/debug/src/browser.js",
"./node_modules/send/node_modules/debug/src/index.js",
"./node_modules/shell-env/node_modules/execa/lib/errname.js",
"./node_modules/shell-env/node_modules/get-stream/buffer-stream.js",
"./node_modules/shell-env/node_modules/semver/semver.js",
"./node_modules/signal-exit/signals.js",
"./node_modules/simple-git/dist/cjs/index.js",
@@ -603,7 +591,6 @@
"./node_modules/ws/lib/receiver.js",
"./node_modules/ws/lib/validation.js",
"./node_modules/ws/lib/websocket-server.js",
"./node_modules/ws/lib/websocket.js",
"./node_modules/xdg-basedir/index.js",
"./node_modules/xml2js/lib/xml2js.js",
"./packages/config/index.js",
@@ -625,7 +612,6 @@
"./packages/data-context/node_modules/fs-extra/lib/json/index.js",
"./packages/data-context/node_modules/fs-extra/lib/json/jsonfile.js",
"./packages/data-context/node_modules/fs-extra/lib/path-exists/index.js",
"./packages/data-context/node_modules/get-stream/buffer-stream.js",
"./packages/data-context/node_modules/readdirp/index.js",
"./packages/data-context/src/DataActions.ts",
"./packages/data-context/src/DataContext.ts",
@@ -761,9 +747,9 @@
"./packages/server/lib/browsers/memory/index.ts",
"./packages/server/lib/cache.ts",
"./packages/server/lib/cloud/api/cloud_request.ts",
"./packages/server/lib/cloud/api/get_and_initialize_studio_manager.ts",
"./packages/server/lib/cloud/api/index.ts",
"./packages/server/lib/cloud/api/put_protocol_artifact.ts",
"./packages/server/lib/cloud/api/studio/get_and_initialize_studio_manager.ts",
"./packages/server/lib/cloud/artifacts/protocol_artifact.ts",
"./packages/server/lib/cloud/artifacts/screenshot_artifact.ts",
"./packages/server/lib/cloud/artifacts/video_artifact.ts",
@@ -803,13 +789,13 @@
"./packages/server/node_modules/body-parser/node_modules/debug/src/browser.js",
"./packages/server/node_modules/body-parser/node_modules/debug/src/index.js",
"./packages/server/node_modules/body-parser/node_modules/debug/src/node.js",
"./packages/server/node_modules/chownr/chownr.js",
"./packages/server/node_modules/cross-fetch/node_modules/node-fetch/lib/index.js",
"./packages/server/node_modules/cross-spawn/node_modules/semver/semver.js",
"./packages/server/node_modules/duplexify/index.js",
"./packages/server/node_modules/execa/lib/errname.js",
"./packages/server/node_modules/fs-minipass/index.js",
"./packages/server/node_modules/fs-minipass/node_modules/minipass/index.js",
"./packages/server/node_modules/get-stream/buffer-stream.js",
"./packages/server/node_modules/glob/glob.js",
"./packages/server/node_modules/glob/sync.js",
"./packages/server/node_modules/graceful-fs/graceful-fs.js",
@@ -864,7 +850,6 @@
"./packages/socket/lib/socket.ts",
"./packages/socket/node_modules/socket.io-parser/node_modules/debug/src/browser.js",
"./packages/socket/node_modules/socket.io-parser/node_modules/debug/src/index.js",
"./packages/socket/node_modules/socket.io-parser/node_modules/debug/src/node.js",
"./packages/socket/node_modules/socket.io/dist/broadcast-operator.js",
"./packages/socket/node_modules/socket.io/dist/index.js",
"./packages/socket/node_modules/socket.io/dist/namespace.js",
@@ -873,7 +858,6 @@
"./packages/socket/node_modules/socket.io/dist/typed-events.js",
"./packages/socket/node_modules/socket.io/node_modules/debug/src/browser.js",
"./packages/socket/node_modules/socket.io/node_modules/debug/src/index.js",
"./packages/socket/node_modules/socket.io/node_modules/debug/src/node.js",
"./packages/socket/node_modules/socket.io/node_modules/engine.io/lib/server.js",
"./packages/socket/node_modules/socket.io/node_modules/engine.io/lib/socket.js",
"./packages/socket/node_modules/socket.io/node_modules/engine.io/lib/transport.js",
@@ -885,7 +869,6 @@
"./packages/socket/node_modules/socket.io/node_modules/ws/lib/constants.js",
"./packages/socket/node_modules/socket.io/node_modules/ws/lib/receiver.js",
"./packages/socket/node_modules/socket.io/node_modules/ws/lib/websocket-server.js",
"./packages/socket/node_modules/socket.io/node_modules/ws/lib/websocket.js",
"./packages/ts/register.js",
"./packages/types/index.js",
"./tooling/v8-snapshot/dist/setup/v8-snapshot-entry-cy-in-cy.js"
@@ -1018,6 +1001,7 @@
"./node_modules/@babel/types/lib/comments/removeComments.js",
"./node_modules/@babel/types/lib/constants/generated/index.js",
"./node_modules/@babel/types/lib/constants/index.js",
"./node_modules/@babel/types/lib/converters/gatherSequenceExpressions.js",
"./node_modules/@babel/types/lib/converters/toBindingIdentifierName.js",
"./node_modules/@babel/types/lib/converters/toExpression.js",
"./node_modules/@babel/types/lib/converters/toIdentifier.js",
@@ -1634,7 +1618,6 @@
"./node_modules/chrome-remote-interface/node_modules/ws/lib/sender.js",
"./node_modules/chrome-remote-interface/node_modules/ws/lib/stream.js",
"./node_modules/chrome-remote-interface/node_modules/ws/lib/validation.js",
"./node_modules/ci-info/index.js",
"./node_modules/ci-info/vendors.json",
"./node_modules/cli-table3/index.js",
"./node_modules/cli-table3/src/cell.js",
@@ -3143,6 +3126,7 @@
"./node_modules/react-docgen/dist/utils/getClassMemberValuePath.js",
"./node_modules/react-docgen/dist/utils/getFlowType.js",
"./node_modules/react-docgen/dist/utils/getMemberExpressionRoot.js",
"./node_modules/react-docgen/dist/utils/getMemberExpressionValuePath.js",
"./node_modules/react-docgen/dist/utils/getMembers.js",
"./node_modules/react-docgen/dist/utils/getMethodDocumentation.js",
"./node_modules/react-docgen/dist/utils/getNameOrValue.js",
@@ -3330,7 +3314,6 @@
"./node_modules/side-channel-map/index.js",
"./node_modules/side-channel-weakmap/index.js",
"./node_modules/side-channel/index.js",
"./node_modules/signal-exit/index.js",
"./node_modules/simple-swizzle/index.js",
"./node_modules/simple-swizzle/node_modules/is-arrayish/index.js",
"./node_modules/slash/index.js",
@@ -3883,6 +3866,7 @@
"./packages/scaffold-config/src/index.ts",
"./packages/scaffold-config/src/supportFile.ts",
"./packages/server/config/app.json",
"./packages/server/lib/StudioLifecycleManager.ts",
"./packages/server/lib/automation/automation.ts",
"./packages/server/lib/automation/automation_not_implemented.ts",
"./packages/server/lib/automation/commands/key_press.ts",
@@ -3909,6 +3893,8 @@
"./packages/server/lib/cloud/api/axios_middleware/logging.ts",
"./packages/server/lib/cloud/api/axios_middleware/transform_error.ts",
"./packages/server/lib/cloud/api/scrub_url.ts",
"./packages/server/lib/cloud/api/studio/post_studio_session.ts",
"./packages/server/lib/cloud/api/studio/report_studio_error.ts",
"./packages/server/lib/cloud/artifacts/artifact.ts",
"./packages/server/lib/cloud/artifacts/file_upload_strategy.ts",
"./packages/server/lib/cloud/artifacts/print_protocol_upload_error.ts",
@@ -3924,6 +3910,7 @@
"./packages/server/lib/cloud/network/system_error.ts",
"./packages/server/lib/cloud/protocol.ts",
"./packages/server/lib/cloud/require_script.ts",
"./packages/server/lib/cloud/strip_path.ts",
"./packages/server/lib/cloud/studio.ts",
"./packages/server/lib/cloud/upload/send_file.ts",
"./packages/server/lib/cloud/upload/stream_activity_monitor.ts",
@@ -4233,5 +4220,5 @@
"./tooling/v8-snapshot/cache/win32/snapshot-entry.js"
],
"deferredHashFile": "yarn.lock",
"deferredHash": "c72da791c55e07d52fa01f0d6d748df4ecb47d8a6b966dc2edc430cbcb2137a7"
"deferredHash": "f63d2697a50b3b6e3bf056a593ed344e0f266da7bd49e6c4a63288bc0d61a585"
}
+14 -13
View File
@@ -29,6 +29,7 @@
},
"devDependencies": {
"@tooling/system-tests": "0.0.0-development",
"chai-as-promised": "7.1.1",
"cpr": "^3.0.1",
"mocha": "7.0.1",
"snap-shot-it": "7.9.10",
@@ -40,19 +41,19 @@
],
"types": "src/v8-snapshot.ts",
"optionalDependencies": {
"@cypress/snapbuild-android-arm64": "1.0.3",
"@cypress/snapbuild-darwin-64": "1.0.3",
"@cypress/snapbuild-darwin-arm64": "1.0.3",
"@cypress/snapbuild-freebsd-64": "1.0.3",
"@cypress/snapbuild-freebsd-arm64": "1.0.3",
"@cypress/snapbuild-linux-32": "1.0.3",
"@cypress/snapbuild-linux-64": "1.0.3",
"@cypress/snapbuild-linux-arm": "1.0.3",
"@cypress/snapbuild-linux-arm64": "1.0.3",
"@cypress/snapbuild-linux-mips64le": "1.0.3",
"@cypress/snapbuild-linux-ppc64le": "1.0.3",
"@cypress/snapbuild-windows-32": "1.0.3",
"@cypress/snapbuild-windows-64": "1.0.3"
"@cypress/snapbuild-android-arm64": "1.0.4",
"@cypress/snapbuild-darwin-64": "1.0.4",
"@cypress/snapbuild-darwin-arm64": "1.0.4",
"@cypress/snapbuild-freebsd-64": "1.0.4",
"@cypress/snapbuild-freebsd-arm64": "1.0.4",
"@cypress/snapbuild-linux-32": "1.0.4",
"@cypress/snapbuild-linux-64": "1.0.4",
"@cypress/snapbuild-linux-arm": "1.0.4",
"@cypress/snapbuild-linux-arm64": "1.0.4",
"@cypress/snapbuild-linux-mips64le": "1.0.4",
"@cypress/snapbuild-linux-ppc64le": "1.0.4",
"@cypress/snapbuild-windows-32": "1.0.4",
"@cypress/snapbuild-windows-64": "1.0.4"
},
"nx": {
"implicitDependencies": [
@@ -1,11 +1,54 @@
import debug from 'debug'
import fs from 'fs'
import path from 'path'
import { SnapshotDoctor } from './snapshot-doctor'
import { doesDependencyMatchForceNorewriteEntry, SnapshotDoctor } from './snapshot-doctor'
import { canAccess, createHashForFile, matchFileHash } from '../utils'
const logInfo = debug('cypress:snapgen:info')
interface ErrorOnInvalidForceNorewriteOpts {
forceNorewrite: Set<string>
inputs: Record<string, { fileInfo: { fullPath: string } }>
nodeModulesOnly: boolean
}
/**
* Filters out the wildcard force no rewrite modules
* @param norewrite - The set of calculated no rewrite modules in the project
*/
function filterForceNorewrite (norewrite: string[]) {
return norewrite.filter((dependency) => !dependency.startsWith('./*'))
}
/**
* Throws an error if a force no rewrite module is not found in the project
* @param norewrite - The set of force no rewrite modules
* @param inputs - The inputs from the esbuild bundle which are actually in the project
*/
function errorOnInvalidForceNorewrite (opts: ErrorOnInvalidForceNorewriteOpts) {
const inputsKeys = Object.keys(opts.inputs)
const invalidForceNorewrites: string[] = []
Array.from(opts.forceNorewrite).forEach((dependency) => {
if (opts.nodeModulesOnly && !dependency.startsWith('node_modules') && !dependency.startsWith('*')) {
return
}
const includedInInputs = inputsKeys.some((key) => {
return doesDependencyMatchForceNorewriteEntry(key, dependency)
})
if (!includedInInputs) {
invalidForceNorewrites.push(dependency)
}
})
if (invalidForceNorewrites.length > 0) {
throw new Error(`Force no rewrite dependencies not found in project: ${invalidForceNorewrites.join(', ')}`)
}
}
export async function determineDeferred (
bundlerPath: string,
projectBaseDir: string,
@@ -13,7 +56,7 @@ export async function determineDeferred (
cacheDir: string,
opts: {
nodeModulesOnly: boolean
forceNoRewrite: Set<string>
forceNorewrite: Set<string>
nodeEnv: string
cypressInternalEnv: string
integrityCheckSource: string | undefined
@@ -50,22 +93,22 @@ export async function determineDeferred (
}
})
let nodeModulesNoRewrite: string[] = []
let projectNoRewrite: string[] = []
let currentNoRewrite = opts.nodeModulesOnly ? nodeModulesNoRewrite : norewrite
let nodeModulesNorewrite: string[] = []
let projectNorewrite: string[] = []
let currentNorewrite = opts.nodeModulesOnly ? nodeModulesNorewrite : norewrite
norewrite.forEach((dependency) => {
if (dependency.includes('node_modules')) {
nodeModulesNoRewrite.push(dependency)
nodeModulesNorewrite.push(dependency)
} else {
projectNoRewrite.push(dependency)
projectNorewrite.push(dependency)
}
})
if (res.match && opts.nodeModulesOnly) {
const combined: Set<string> = new Set([
...currentNoRewrite,
...opts.forceNoRewrite,
...currentNorewrite,
...opts.forceNorewrite,
])
return {
@@ -86,8 +129,8 @@ export async function determineDeferred (
nodeModulesOnly: opts.nodeModulesOnly,
previousDeferred: currentDeferred,
previousHealthy: currentHealthy,
previousNoRewrite: currentNoRewrite,
forceNoRewrite: opts.forceNoRewrite,
previousNorewrite: currentNorewrite,
forceNorewrite: opts.forceNorewrite,
nodeEnv: opts.nodeEnv,
cypressInternalEnv: opts.cypressInternalEnv,
supportTypeScript: opts.nodeModulesOnly,
@@ -98,11 +141,20 @@ export async function determineDeferred (
deferred: updatedDeferred,
norewrite: updatedNorewrite,
healthy: updatedHealthy,
meta: esbuildMeta,
} = await doctor.heal()
errorOnInvalidForceNorewrite({
forceNorewrite: opts.forceNorewrite,
inputs: esbuildMeta.inputs,
nodeModulesOnly: opts.nodeModulesOnly,
})
const deferredHashFile = path.relative(projectBaseDir, hashFilePath)
const filteredNorewrite = filterForceNorewrite(updatedNorewrite)
const updatedMeta = {
norewrite: opts.nodeModulesOnly ? [...updatedNorewrite, ...projectNoRewrite] : updatedNorewrite,
norewrite: opts.nodeModulesOnly ? [...filteredNorewrite, ...projectNorewrite] : filteredNorewrite,
deferred: opts.nodeModulesOnly ? [...updatedDeferred, ...projectDeferred] : updatedDeferred,
healthy: opts.nodeModulesOnly ? [...updatedHealthy, ...projectHealthy] : updatedHealthy,
deferredHashFile,
@@ -122,7 +174,7 @@ export async function determineDeferred (
}
return {
norewrite: updatedNorewrite,
norewrite: filteredNorewrite,
deferred: updatedDeferred,
healthy: updatedHealthy,
}
@@ -30,8 +30,8 @@ const logError = debug('cypress:snapgen:error')
*
* @property previousDeferred See {@link GenerationOpts} previousDeferred
* @property previousHealthy See {@link GenerationOpts} previousHealthy
* @property previousNoRewrite See {@link GenerationOpts} previousNoRewrite
* @property forceNoRewrite See {@link GenerationOpts} forceNoRewrite
* @property previousNorewrite See {@link GenerationOpts} previousNorewrite
* @property forceNorewrite See {@link GenerationOpts} forceNorewrite
*/
export type SnapshotDoctorOpts = Omit<
CreateSnapshotScriptOpts,
@@ -44,8 +44,22 @@ export type SnapshotDoctorOpts = Omit<
> & {
previousDeferred: Set<string>
previousHealthy: Set<string>
previousNoRewrite: Set<string>
forceNoRewrite: Set<string>
previousNorewrite: Set<string>
forceNorewrite: Set<string>
}
/**
* Checks if a dependency matches a force no rewrite entry
* @param dependency - The dependency to check
* @param forceNorewrite - The force no rewrite entry
* @returns true if the dependency matches the force no rewrite entry, false otherwise
*/
export const doesDependencyMatchForceNorewriteEntry = (dependency: string, forceNorewrite: string) => {
// The force no rewrite file follows a convention where we try
// and match all possible node_modules paths if the force no
// rewrite entry starts with "*/". If it does not
// start with "*" then it is an exact match.
return (forceNorewrite.startsWith('*/') && dependency.endsWith(forceNorewrite.slice(2))) || dependency === forceNorewrite
}
/**
@@ -241,7 +255,7 @@ function unpathify (keys: Set<string>) {
* ```ts
* previousDeferred: Set<string>
* previousHealthy: Set<string>
* previousNoRewrite: Set<string>
* previousNorewrite: Set<string>
* ```
*
* ## Snapshot Doctor Steps
@@ -261,8 +275,8 @@ export class SnapshotDoctor {
private readonly nodeModulesOnly: boolean
private readonly previousDeferred: Set<string>
private readonly previousHealthy: Set<string>
private readonly previousNoRewrite: Set<string>
private readonly forceNoRewrite: Set<string>
private readonly previousNorewrite: Set<string>
private readonly forceNorewrite: Set<string>
private readonly nodeEnv: string
private readonly cypressInternalEnv: string
private readonly _scriptProcessor: AsyncScriptProcessor
@@ -283,8 +297,8 @@ export class SnapshotDoctor {
this.nodeModulesOnly = opts.nodeModulesOnly
this.previousDeferred = unpathify(opts.previousDeferred)
this.previousHealthy = unpathify(opts.previousHealthy)
this.previousNoRewrite = unpathify(opts.previousNoRewrite)
this.forceNoRewrite = unpathify(opts.forceNoRewrite)
this.previousNorewrite = unpathify(opts.previousNorewrite)
this.forceNorewrite = unpathify(opts.forceNorewrite)
this.nodeEnv = opts.nodeEnv
this.cypressInternalEnv = opts.cypressInternalEnv
this.integrityCheckSource = opts.integrityCheckSource
@@ -328,21 +342,21 @@ export class SnapshotDoctor {
const filteredPreviousHealthy = filterStaleImports(this.previousHealthy)
const filteredPreviousDeferred = filterStaleImports(this.previousDeferred)
const filteredPreviousNoRewrite = filterStaleImports(this.previousNoRewrite)
const filteredPreviousNorewrite = filterStaleImports(this.previousNorewrite)
// 3. Initialize the heal state with data from previous runs that was
// provided to us
// forceNoRewrite is provided for modules which we manually determined
// forceNorewrite is provided for modules which we manually determined
// to result in invalid/problematic code when rewritten
const healState = new HealState(
meta,
filteredPreviousHealthy,
filteredPreviousDeferred,
new Set([...filteredPreviousNoRewrite, ...this.forceNoRewrite]),
new Set([...filteredPreviousNorewrite, ...this.forceNorewrite]),
)
// 4. Generate the initial bundle and warnings using what was done previously
const { warnings, bundle } = await this._createScript(new Set(filteredPreviousDeferred), new Set([...filteredPreviousNoRewrite, ...this.forceNoRewrite]))
const { warnings, bundle } = await this._createScript(new Set(filteredPreviousDeferred), new Set([...filteredPreviousNorewrite, ...this.forceNorewrite]))
// 5. Process the initial bundle in order to detect issues during
// verification
@@ -456,7 +470,7 @@ export class SnapshotDoctor {
logError('Encountered warning triggering defer: %s', s)
healState.needDefer.add(warning.location.file)
break
case WarningConsequence.NoRewrite:
case WarningConsequence.Norewrite:
logError('Encountered warning triggering no-rewrite: %s', s)
healState.needNorewrite.add(warning.location.file)
break
@@ -492,6 +506,17 @@ export class SnapshotDoctor {
) {
// 5. Process the module verification in parallel
const promises = nextStage.map(async (key): Promise<void> => {
const includedInForceNorewrite = [...this.forceNorewrite].find((forceNorewrite) => {
return doesDependencyMatchForceNorewriteEntry(key, forceNorewrite)
})
if (includedInForceNorewrite) {
healState.needNorewrite.add(key)
logDebug('Not rewriting "%s" as it is a force no rewrite module', key)
return
}
logDebug('Testing entry in isolation "%s"', key)
// 5.1. The script processor distributes processing modules across
// multiple worker threads
@@ -515,6 +540,7 @@ export class SnapshotDoctor {
case 'completed': {
healState.healthy.add(key)
logDebug('Verified as healthy "%s"', key)
break
}
case 'failed:assembleScript':
@@ -533,9 +559,10 @@ export class SnapshotDoctor {
case WarningConsequence.Defer: {
logInfo('Deferring "%s"', key)
healState.needDefer.add(key)
break
}
case WarningConsequence.NoRewrite: {
case WarningConsequence.Norewrite: {
logInfo(
'Not rewriting "%s" as it results in incorrect code',
key,
@@ -48,13 +48,13 @@ export type WarningsProcessHistory = {
* The consequence of a specific type of warning.
*
* - Defer: we need to defer the module in order to prevent it from loading
* - NoRewrite: we should not rewrite the module as it results in invalid code
* - Norewrite: we should not rewrite the module as it results in invalid code
* - None: no consequence, i.e. a light weight warning for informative purposes only
* @category snapshot
*/
export enum WarningConsequence {
Defer,
NoRewrite,
Norewrite,
None,
}
@@ -157,7 +157,7 @@ export class WarningsProcessor {
// prettier-ignore
const consequence =
text.includes(SNAPSHOT_REWRITE_FAILURE)
|| REFERENCE_ERROR_NOREWRITE.test(text) ? WarningConsequence.NoRewrite
|| REFERENCE_ERROR_NOREWRITE.test(text) ? WarningConsequence.Norewrite
: text.includes(SNAPSHOT_CACHE_FAILURE)
|| REFERENCE_ERROR_DEFER.test(text) ? WarningConsequence.Defer
: WarningConsequence.None
@@ -185,7 +185,7 @@ export class WarningsProcessor {
return x
}
case WarningConsequence.NoRewrite: {
case WarningConsequence.Norewrite: {
if (norewrite.has(x.location.file)) return null
return x
@@ -31,7 +31,7 @@ const logError = debug('cypress:snapgen:error')
*
* @property nodeModulesOnly if `true` only node modules will be included in the snapshot and app modules are omitted
*
* @property forceNoRewrite relative paths to modules that we know will cause
* @property forceNorewrite relative paths to modules that we know will cause
* problems when rewritten and we manually want to exclude them from snapshot
* bundler rewrites
*
@@ -82,7 +82,7 @@ export type GenerationOpts = {
cacheDir: string
snapshotBinDir: string
nodeModulesOnly: boolean
forceNoRewrite?: string[]
forceNorewrite?: string[]
resolverMap?: Record<string, string>
flags: Flag
nodeEnv: string
@@ -137,8 +137,8 @@ export class SnapshotGenerator {
private readonly electronVersion: string
/** See {@link GenerationOpts} nodeModulesOnly */
private readonly nodeModulesOnly: boolean
/** See {@link GenerationOpts} forceNoRewrite */
private readonly forceNoRewrite: Set<string>
/** See {@link GenerationOpts} forceNorewrite */
private readonly forceNorewrite: Set<string>
/** See {@link GenerationOpts} nodeEnv */
private readonly nodeEnv: string
/** See {@link GenerationOpts} cypressInternalEnv */
@@ -200,7 +200,7 @@ export class SnapshotGenerator {
const {
cacheDir,
nodeModulesOnly,
forceNoRewrite,
forceNorewrite,
flags: mode,
nodeEnv,
cypressInternalEnv,
@@ -226,7 +226,7 @@ export class SnapshotGenerator {
this.electronVersion = resolveElectronVersion(projectBaseDir)
this.nodeModulesOnly = nodeModulesOnly
this.forceNoRewrite = new Set(forceNoRewrite)
this.forceNorewrite = new Set(forceNorewrite)
this.nodeEnv = nodeEnv
this.cypressInternalEnv = cypressInternalEnv
this._flags = new GeneratorFlags(mode)
@@ -243,7 +243,7 @@ export class SnapshotGenerator {
cacheDir,
snapshotScriptPath: this.snapshotScriptPath,
nodeModulesOnly: this.nodeModulesOnly,
forceNoRewrite: this.forceNoRewrite.size,
forceNorewrite: this.forceNorewrite.size,
auxiliaryData: auxiliaryDataKeys,
})
}
@@ -287,7 +287,7 @@ export class SnapshotGenerator {
this.cacheDir,
{
nodeModulesOnly: this.nodeModulesOnly,
forceNoRewrite: this.forceNoRewrite,
forceNorewrite: this.forceNorewrite,
nodeEnv: this.nodeEnv,
cypressInternalEnv: this.cypressInternalEnv,
integrityCheckSource: this.integrityCheckSource,
@@ -393,7 +393,7 @@ export class SnapshotGenerator {
this.cacheDir,
{
nodeModulesOnly: this.nodeModulesOnly,
forceNoRewrite: this.forceNoRewrite,
forceNorewrite: this.forceNorewrite,
nodeEnv: this.nodeEnv,
cypressInternalEnv: this.cypressInternalEnv,
integrityCheckSource: this.integrityCheckSource,
@@ -1,60 +1,39 @@
/**
* These modules are force no rewritten because they are rewritten in a way that
* breaks the snapshot but is not detected automatically by the snapshot builder.
* When run through the snapshot generator, these strings
* will be compared to the file's path and if they match, the given
* file will be marked as force no rewritten. If we want to match the full path, we
* should include the full relative path with respect to the project base (e.g.
* packages/https-proxy/lib/ca.js). For files where we want to match multiple hoisted
* locations, we should specify the dependency starting with `* /` (e.g.
* `* /node_modules/signal-exit/index.js`)
*/
export default [
// recursion due to process.emit overwrites which is incorrectly rewritten
'signal-exit/index.js',
// recursion due to process.{chdir,cwd} overwrites which are incorrectly rewritten
'graceful-fs/polyfills.js',
'*/node_modules/signal-exit/index.js',
// wx is rewritten to __get_wx__ but not available for Node.js > 0.6
'lockfile/lockfile.js',
'*/node_modules/lockfile/lockfile.js',
// rewrites dns.lookup which conflicts with our rewrite
'evil-dns/evil-dns.js',
'*/node_modules/evil-dns/evil-dns.js',
// `address instanceof (__get_URL2__())` -- right hand side not an object
// even though function is in scope
'ws/lib/websocket.js',
'*/node_modules/ws/lib/websocket.js',
// defers PassThroughStream which is then not accepted as a constructor
'get-stream/buffer-stream.js',
'*/node_modules/get-stream/buffer-stream.js',
// deferring should be fine as it just reexports `process` which in the
// case of cache is the stub
'process-nextick-args/index.js',
'*/node_modules/process-nextick-args/index.js',
// Has issues depending on the architecture due to how it handles errors
'node_modules/@cypress/get-windows-proxy/src/registry.js',
'*/node_modules/@cypress/get-windows-proxy/src/registry.js',
// results in recursive call to __get_fs2__
'packages/https-proxy/lib/ca.js',
// TODO: Figure out why these don't properly get flagged as norewrite: https://github.com/cypress-io/cypress/issues/23986
'node_modules/@cspotcode/source-map-support/source-map-support.js',
'packages/server/lib/modes/record.js',
'*/node_modules/@cspotcode/source-map-support/source-map-support.js',
'packages/server/lib/modes/run.ts',
'node_modules/debug/src/node.js',
'node_modules/body-parser/node_modules/debug/src/node.js',
'node_modules/compression/node_modules/debug/src/node.js',
'node_modules/express/node_modules/debug/src/node.js',
'node_modules/finalhandler/node_modules/debug/src/node.js',
'node_modules/get-package-info/node_modules/debug/src/node.js',
'node_modules/mocha-junit-reporter/node_modules/debug/src/node.js',
'node_modules/mocha/node_modules/debug/src/node.js',
'node_modules/morgan/node_modules/debug/src/node.js',
'node_modules/send/node_modules/debug/src/node.js',
'node_modules/stream-parser/node_modules/debug/src/node.js',
'node_modules/@cypress/commit-info/node_modules/debug/src/node.js',
'node_modules/@cypress/get-windows-proxy/node_modules/debug/src/node.js',
'node_modules/mocha-7.2.0/node_modules/debug/src/node.js',
'node_modules/tcp-port-used/node_modules/debug/src/node.js',
'packages/data-context/node_modules/debug/src/node.js',
'packages/graphql/node_modules/debug/src/node.js',
'packages/net-stubbing/node_modules/debug/src/node.js',
'packages/server/node_modules/mocha/node_modules/debug/src/node.js',
'node_modules/minimatch/minimatch.js',
'node_modules/mocha-7.2.0/node_modules/glob/node_modules/minimatch/minimatch.js',
'packages/data-context/node_modules/minimatch/minimatch.js',
'packages/network/node_modules/minimatch/minimatch.js',
'packages/server/node_modules/glob/node_modules/minimatch/minimatch.js',
'node_modules/js-yaml/lib/js-yaml/type/js/function.js',
'*/node_modules/debug/src/node.js',
'*/node_modules/minimatch/minimatch.js',
'*/node_modules/js-yaml/lib/js-yaml/type/js/function.js',
'packages/server/lib/open_project.ts',
'packages/server/lib/project-base.ts',
'packages/server/lib/socket-ct.ts',
@@ -62,15 +41,15 @@ export default [
'packages/server/lib/cloud/exception.ts',
'packages/server/lib/errors.ts',
'packages/server/lib/util/process_profiler.ts',
'node_modules/prettier/index.js',
'node_modules/prettier/parser-babel.js',
'node_modules/prettier/parser-espree.js',
'node_modules/prettier/parser-flow.js',
'node_modules/prettier/parser-meriyah.js',
'node_modules/prettier/parser-typescript.js',
'node_modules/prettier/third-party.js',
'ci-info/index.js',
'node_modules/@babel/traverse/lib/index.js',
'node_modules/@babel/types/lib/definitions/index.js',
'packages/server/node_modules/axios/lib/adapters/http.js',
'*/node_modules/prettier/index.js',
'*/node_modules/prettier/parser-babel.js',
'*/node_modules/prettier/parser-espree.js',
'*/node_modules/prettier/parser-flow.js',
'*/node_modules/prettier/parser-meriyah.js',
'*/node_modules/prettier/parser-typescript.js',
'*/node_modules/prettier/third-party.js',
'*/node_modules/ci-info/index.js',
'*/node_modules/@babel/traverse/lib/index.js',
'*/node_modules/@babel/types/lib/definitions/index.js',
'*/node_modules/axios/lib/adapters/http.js',
]
@@ -2,7 +2,7 @@ import path from 'path'
import { SnapshotGenerator } from '../generator/snapshot-generator'
import { prettyPrintError } from '../utils'
import fs from 'fs-extra'
import forceNoRewrite from './force-no-rewrite'
import forceNorewrite from './force-no-rewrite'
import type { SnapshotConfig } from './config'
const debug = require('debug')
@@ -34,7 +34,7 @@ function getSnapshotGenerator ({
cacheDir: snapshotCacheDir,
nodeModulesOnly,
resolverMap,
forceNoRewrite,
forceNorewrite,
minify,
integrityCheckSource,
useExistingSnapshotScript,
@@ -1,4 +1,13 @@
exports.healthy = require('./healthy')
exports.deferred = require('./deferred')
exports.intermediate = require('./intermediate-healthy')
exports.norewrite = require('./norewrite')
exports.forceNorewriteNested = require('./packages/server/node_modules/force-no-rewrite')
exports.absoluteNorewrite = require('./absolute-path/force-no-rewrite')
exports.absoluteNorewriteNotAbsolute = require('./node_modules/absolute-path/force-no-rewrite')
@@ -0,0 +1 @@
module.exports = 1
+32 -3
View File
@@ -3,10 +3,13 @@ import { readBundleResult, readSnapshotResult } from '../utils/bundle'
import { SnapshotGenerator } from '../../src/generator/snapshot-generator'
import { Flag } from '../../src/generator/snapshot-generator-flags'
import { electronExecutable } from '../utils/consts'
import { expect, assert } from 'chai'
import { expect, assert, use } from 'chai'
import { promisify } from 'util'
import { exec as execOrig } from 'child_process'
import fs from 'fs-extra'
import chaiAsPromised from 'chai-as-promised'
use(chaiAsPromised)
const exec = promisify(execOrig)
@@ -287,6 +290,7 @@ describe('doctor', () => {
let generator = new SnapshotGenerator(projectBaseDir, snapshotEntryFile, {
cacheDir,
nodeModulesOnly: false,
forceNorewrite: ['*/node_modules/force-no-rewrite.js', 'absolute-path/force-no-rewrite.js'],
})
// Set up project to use an intermediate healthy dependency and snapshot
@@ -296,6 +300,7 @@ describe('doctor', () => {
const intermediateHealthy = await fs.readFile(path.join(templateDir, 'intermediate-healthy.js'))
const intermediateDeferred = await fs.readFile(path.join(templateDir, 'intermediate-deferred.js'))
const norewrite = await fs.readFile(path.join(templateDir, 'leaf-norewrite.js'))
const forceNorewrite = await fs.readFile(path.join(templateDir, 'force-no-rewrite.js'))
await fs.writeFile(path.join(projectBaseDir, 'entry.js'), initialEntry)
await fs.writeFile(path.join(projectBaseDir, 'healthy.js'), healthy)
@@ -303,6 +308,12 @@ describe('doctor', () => {
await fs.writeFile(path.join(projectBaseDir, 'intermediate-healthy.js'), intermediateHealthy)
await fs.writeFile(path.join(projectBaseDir, 'intermediate-deferred.js'), intermediateDeferred)
await fs.writeFile(path.join(projectBaseDir, 'norewrite.js'), norewrite)
await fs.ensureDir(path.join(projectBaseDir, 'packages', 'server', 'node_modules'))
await fs.writeFile(path.join(projectBaseDir, 'packages', 'server', 'node_modules', 'force-no-rewrite.js'), forceNorewrite)
await fs.ensureDir(path.join(projectBaseDir, 'absolute-path'))
await fs.writeFile(path.join(projectBaseDir, 'absolute-path', 'force-no-rewrite.js'), forceNorewrite)
await fs.ensureDir(path.join(projectBaseDir, 'node_modules', 'absolute-path'))
await fs.writeFile(path.join(projectBaseDir, 'node_modules', 'absolute-path', 'force-no-rewrite.js'), forceNorewrite)
await generator.createScript()
await generator.makeAndInstallSnapshot()
@@ -310,7 +321,9 @@ describe('doctor', () => {
expect(metadata).to.deep.equal({
norewrite: [
'./absolute-path/force-no-rewrite.js',
'./norewrite.js',
'./packages/server/node_modules/force-no-rewrite.js',
],
deferred: [
'./deferred.js',
@@ -319,6 +332,7 @@ describe('doctor', () => {
'./entry.js',
'./healthy.js',
'./intermediate-healthy.js',
'./node_modules/absolute-path/force-no-rewrite.js',
],
})
@@ -327,7 +341,7 @@ describe('doctor', () => {
nodeModulesOnly: false,
previousDeferred: metadata.deferred,
previousHealthy: metadata.healthy,
previousNoRewrite: metadata.norewrite,
previousNorewrite: metadata.norewrite,
})
// Switch project to use an intermediate deferred dependency and re-snapshot
@@ -355,6 +369,21 @@ describe('doctor', () => {
})
})
it('throws an error if a force no rewrite module is not found in the project', async () => {
const projectBaseDir = path.join(__dirname, '..', 'fixtures', 'iterative', 'project')
const cacheDir = path.join(projectBaseDir, 'cache')
const snapshotEntryFile = path.join(projectBaseDir, 'entry.js')
await fs.remove(cacheDir)
let generator = new SnapshotGenerator(projectBaseDir, snapshotEntryFile, {
cacheDir,
nodeModulesOnly: false,
forceNorewrite: ['force-no-rewrite.js'],
})
await expect(generator.createScript()).to.be.rejectedWith('Force no rewrite dependencies not found in project: force-no-rewrite.js')
})
// TODO: We still have a hole where a file moves from healthy to deferred or norewrite. This doesn't happen very frequently and can be solved later:
// https://github.com/cypress-io/cypress/issues/23690
it.skip('snapshots an entry, typescripts an intermediate file, and snapshots again', async () => {
@@ -404,7 +433,7 @@ describe('doctor', () => {
nodeModulesOnly: false,
previousDeferred: metadata.deferred,
previousHealthy: metadata.healthy,
previousNoRewrite: metadata.norewrite,
previousNorewrite: metadata.norewrite,
})
// Then create the snapshot with a intermediate deferred file
+80 -80
View File
@@ -2642,10 +2642,10 @@
resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-3.0.3.tgz#a5502c8539265fecbd873c1e395a890339f119c2"
integrity sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==
"@cypress-design/color-constants@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@cypress-design/color-constants/-/color-constants-1.0.0.tgz#a8373c5eeeefc9b2040710b9159c96e6e2ac09b3"
integrity sha512-ZGSfWR5zSQQkc6+k0xiRiuCdxUzoPYOwdIOHIK6M0PqhPg3rmP455M752U082jbIsQvoea4k9CNE053oOna4Zw==
"@cypress-design/color-constants@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@cypress-design/color-constants/-/color-constants-1.1.0.tgz#f09370d802d6eaddda03a22d0307da1327c37f6f"
integrity sha512-a6M416WSQ3Yh5u3O2tw3h7DnHcus/hrMTWI8yFwp6Wl1KlyJlHSDcKZmotFqLfjFPgkzu9u7hFSnRA4y7WqZJA==
dependencies:
"@tailwindcss/container-queries" "^0.1.1"
lodash "^4.17.21"
@@ -2681,24 +2681,24 @@
tailwindcss "^3.4.3"
tailwindcss-hocus "^0.0.7"
"@cypress-design/icon-registry@^1.0.0", "@cypress-design/icon-registry@^1.5.1", "@cypress-design/icon-registry@^1.6.0":
version "1.6.0"
resolved "https://registry.yarnpkg.com/@cypress-design/icon-registry/-/icon-registry-1.6.0.tgz#6367e40722cbf191a0086419ba06de18e7e79f46"
integrity sha512-erJVhL+0gk2oL+fCnBy3hhcs7xFM1abPLZzmIupOtoyb/PB5DtpHtGi3QEF2MBu1UcMjRhuihF6VO3rqZhkn2Q==
"@cypress-design/icon-registry@^1.0.0", "@cypress-design/icon-registry@^1.18.0", "@cypress-design/icon-registry@^1.5.1":
version "1.18.0"
resolved "https://registry.yarnpkg.com/@cypress-design/icon-registry/-/icon-registry-1.18.0.tgz#799c5ac8f8362aebdcf2181119c3205136ca5ab9"
integrity sha512-4goChP9rWVq7F/+c36JyJ4quvHSyI6gkjJ/IKFqncNwkC3gvVeJ4GQX2mqQJCQ+z0Er+2Mmzcw7JiVo1GpbJlg==
dependencies:
"@cypress-design/color-constants" "^1.0.0"
"@cypress-design/color-constants" "^1.1.0"
"@cypress-design/vue-button@^1.1.0", "@cypress-design/vue-button@^1.6.0":
version "1.6.0"
resolved "https://registry.yarnpkg.com/@cypress-design/vue-button/-/vue-button-1.6.0.tgz#e7266dfe11c31628ef3a979fffcf041b141e39c3"
integrity sha512-RnV2suh3BJg3fb7hc1gdh58p13yxBHHm9fmmE6AkHR3OBRAbbd5oqQmZWwuBuWteyj2gVFRgTRYHPzOZBm9wEg==
"@cypress-design/vue-icon@^1.0.0", "@cypress-design/vue-icon@^1.6.0":
version "1.6.0"
resolved "https://registry.yarnpkg.com/@cypress-design/vue-icon/-/vue-icon-1.6.0.tgz#d67e6cf241363f648896717efc456da28590cd0f"
integrity sha512-XQjGWTw31ENMh0npWynR71F56zzqgPrryjUFi2b7lremzHzwHV8KaonKNVVSzbJnDOyI50VCns70Rhd8yS6VNw==
"@cypress-design/vue-icon@^1.0.0", "@cypress-design/vue-icon@^1.18.0":
version "1.18.0"
resolved "https://registry.yarnpkg.com/@cypress-design/vue-icon/-/vue-icon-1.18.0.tgz#deecb084252271903de8e22cac3e29ad9f108e72"
integrity sha512-EPMCzy2d5t+wB2sayKdjHJsTeCJe94B9ulX+USYUw/fOhxIKfOBFR48KkfZHBMIYtIDV5iNHxSKvnLFPDYiuAg==
dependencies:
"@cypress-design/icon-registry" "^1.6.0"
"@cypress-design/icon-registry" "^1.18.0"
"@cypress-design/vue-spinner@^1.0.0":
version "1.0.0"
@@ -2813,70 +2813,70 @@
resolved "https://registry.yarnpkg.com/@cypress/sinon-chai/-/sinon-chai-2.9.1.tgz#1705c0341bc286740979b1b1cac89b7f5d34d6bc"
integrity sha512-qwFQ1urghF3mv7CFSDw/LEqIQP12qqKLuW7p6mXR92HP5fPNlgNiZVITWVsupDg7JpOEKfeRTVearo9mkk/5eg==
"@cypress/snapbuild-android-arm64@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@cypress/snapbuild-android-arm64/-/snapbuild-android-arm64-1.0.3.tgz#bc1da7ad2972107f6b527d3f2056b723d51e60f3"
integrity sha512-f5Ze/2RKW7WdBvZFL9EIz1hLHx+03hamvn+DFotbRDa4uCd3z5mWnWqRGRj2asVyE/HyZAsuVcQ6u4DkSkkpeQ==
"@cypress/snapbuild-android-arm64@1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@cypress/snapbuild-android-arm64/-/snapbuild-android-arm64-1.0.4.tgz#b55b4c7040eefd270d15a7259bf7a1141c5b8c8b"
integrity sha512-op8n069RDxn5Y8DEAlcryp331z7xvpIiCNHjlLj7nLW5llxEN7sqXuLEu4sxHGE+1iNPyiKMUsTIrlYfZ8oTxg==
"@cypress/snapbuild-darwin-64@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@cypress/snapbuild-darwin-64/-/snapbuild-darwin-64-1.0.3.tgz#ae49b42d10091a555e47f1946016e7c2015859f6"
integrity sha512-0UMy6Aua3ObLmF23lIligk2EokHsy+gAxMbumn8+N2blEo4VELVzopS44DoFngdXz/C5XjY+fcTZ8UzKQtQazg==
"@cypress/snapbuild-darwin-64@1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@cypress/snapbuild-darwin-64/-/snapbuild-darwin-64-1.0.4.tgz#b9da7a93da60b7bb2a5080d83ec2866da44ec7a8"
integrity sha512-Bv2NJL7AI13reotJqHazPTCQG/p/07JS4sshX+1qN6aR4syeYZXGGt5/BWb3FKdu95ttPTV7n31u50gJ1oPGQA==
"@cypress/snapbuild-darwin-arm64@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@cypress/snapbuild-darwin-arm64/-/snapbuild-darwin-arm64-1.0.3.tgz#c0e9349d4a9e4748d2f7ea69b59f0ddf2cf62a25"
integrity sha512-IndicX9hwIzxn8cuw4u731/FimmP0cNnEVnceXPkleeZr5B/RKKn+YUhUUio9E0FpFH9+GvYWZ0ggFPzigX6Gw==
"@cypress/snapbuild-darwin-arm64@1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@cypress/snapbuild-darwin-arm64/-/snapbuild-darwin-arm64-1.0.4.tgz#cbd6cd8392c5001e7653fdd6f0a1b4c32ac2f187"
integrity sha512-FOL2MSzVVmblBmxy2ix7jLlDMNvkc/6CFAxeMaQs5k3yS/ISJVLF8+hxDeJ5okTAQmktm2hxCuvqNtIfXOBrig==
"@cypress/snapbuild-freebsd-64@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@cypress/snapbuild-freebsd-64/-/snapbuild-freebsd-64-1.0.3.tgz#2e2812015409db54dc1cf8f350f53bebf0678f6d"
integrity sha512-hXTOK4NQdtJJfjz+rgYUlxpV423yOjk2AJ5f2Waa+b/fYZJLTfowMFIAb3K6AUBfhC2o4MP2fftlQJQGuw5+WQ==
"@cypress/snapbuild-freebsd-64@1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@cypress/snapbuild-freebsd-64/-/snapbuild-freebsd-64-1.0.4.tgz#df651fd8d1f5752f26aa67ffd8bbab89b23a6ca6"
integrity sha512-r7duu+fdCnXematAOkkNn5eiH9pe1kYZhT9+hX1/EV5svyrKYYzudpOSqsANSM8mefECvw1CNXf2pkdr1eQwUQ==
"@cypress/snapbuild-freebsd-arm64@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@cypress/snapbuild-freebsd-arm64/-/snapbuild-freebsd-arm64-1.0.3.tgz#1342cb6d86ce7b503ac24badab1b57e83505d7d2"
integrity sha512-b/LoH2LJuWD7DyCH3Mig0Q6DX4cmNp986LEKevm8s/kBVeeF3+rHPQy+XNRA8wdvEqGoS/OHaxY0dOIwtwmIrA==
"@cypress/snapbuild-freebsd-arm64@1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@cypress/snapbuild-freebsd-arm64/-/snapbuild-freebsd-arm64-1.0.4.tgz#513405fe56b5e6f4ece42efa65cf3312e9a2b7d7"
integrity sha512-sIqLvNypokGS/Hkod0/O48PHoMI5ozr1Wbkdo+Y+gC3/B1mO8tzfXY921cthXDFmvOijRrx/G/zyYIDK0uKZbQ==
"@cypress/snapbuild-linux-32@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@cypress/snapbuild-linux-32/-/snapbuild-linux-32-1.0.3.tgz#9231a8271e5c077428f76aef5bf627ede8efc52b"
integrity sha512-pEM2iE+6/omz24Tld5N+nWfIZ2BRxhO4PSQCtpEtIWPnRefHcixA0kxjHlRuDJZUfiQy9k5c1Syubri2RCcvCg==
"@cypress/snapbuild-linux-32@1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@cypress/snapbuild-linux-32/-/snapbuild-linux-32-1.0.4.tgz#1bc5927089c5ebb081a2ae7771401ca5c057e2f7"
integrity sha512-1wlEexRcQUJokvOvJ5eob5dqc09LaDas7pYAIO6Ry7npVCR+IkK6fFxSUhlS2wnLH0aYzODWPYxvpYid5t2UzQ==
"@cypress/snapbuild-linux-64@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@cypress/snapbuild-linux-64/-/snapbuild-linux-64-1.0.3.tgz#c3ce77734c62e2902a19a2b27abe5cce93941530"
integrity sha512-r8b7sUFSQcZx+8Nmgw69aZR/aFDfprdma23/JHddBna9rxjB22QMstEBrVffbkSOmblNWG4ZFuD0Xvv9NtH/5w==
"@cypress/snapbuild-linux-64@1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@cypress/snapbuild-linux-64/-/snapbuild-linux-64-1.0.4.tgz#1ee178d43a27b9f7f48ce6c5cbf225f905df0762"
integrity sha512-T1eYg1AXq2ene6StFqoU+6C+oKGmq0ivf2imOs6b+oFEn5i50fs48lVsRH0H79Ny12AFEXoBU+jtBkeKyUPKsw==
"@cypress/snapbuild-linux-arm64@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@cypress/snapbuild-linux-arm64/-/snapbuild-linux-arm64-1.0.3.tgz#3d22a8ba186aa639ee7c9a830ace2959fd93fc71"
integrity sha512-yaz37dQ0lAzuX9Yr9mNvl/jJMibotGQ6FrnQM9iyfTPGKQpnoeqB4UczLiWMk29MCbhmVIzxNKHJojyxixBEpQ==
"@cypress/snapbuild-linux-arm64@1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@cypress/snapbuild-linux-arm64/-/snapbuild-linux-arm64-1.0.4.tgz#118cd5da1dbad714ca64e95cf35cf7e6404b4fe5"
integrity sha512-c04PE2GAWt4tey4ixGY4YIiMj++PrPid9geVijpjk1vZgeKtAC20TTMZ+8BDT9+n3aZuk5tqGHh+woH8ZqEutQ==
"@cypress/snapbuild-linux-arm@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@cypress/snapbuild-linux-arm/-/snapbuild-linux-arm-1.0.3.tgz#7bf1c8f6fea7f224b7c594dbd79fb056876779b8"
integrity sha512-wMBaqwDABeTnByR0bYxeSCF//hFZKcVFkyRKugFbGAYMLAqD9FwHymbi9qe4qpe0OXYDTVOj9kHcrArb7HFqrw==
"@cypress/snapbuild-linux-arm@1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@cypress/snapbuild-linux-arm/-/snapbuild-linux-arm-1.0.4.tgz#79ce571e618a9052b19dc8d202f15a5277e1b360"
integrity sha512-R9hjOdd4km0hpY0mVaG7sA9wR2O32cGAUVNRsK14Wq2SwDA4HBLwmTmOZu01t78Gm/4WGvr7PAQWY4Lx3Yb7FA==
"@cypress/snapbuild-linux-mips64le@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@cypress/snapbuild-linux-mips64le/-/snapbuild-linux-mips64le-1.0.3.tgz#848f86c3a0477772d3097065294af70767b2e3c2"
integrity sha512-3sQR9tkLnpqFEWLgxVzngn1ZHmoj0UwBpvAN1QpMAMwTV0gWqYEhpR3V1GJDIj/OHR4z6g2/pjSmgUbtRhTWYg==
"@cypress/snapbuild-linux-mips64le@1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@cypress/snapbuild-linux-mips64le/-/snapbuild-linux-mips64le-1.0.4.tgz#038f60623ea3d4032682c77e7cd143ad2d5f7b3f"
integrity sha512-d58+E+DkV9MT4EtYEgBvxWwg1sPwMLXg3qjJykxc7Q6udEBLm8CJSBP9o3KSYqQ2WCAaJBiMBn4cJFWRUR37Ww==
"@cypress/snapbuild-linux-ppc64le@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@cypress/snapbuild-linux-ppc64le/-/snapbuild-linux-ppc64le-1.0.3.tgz#907e16b69bb4bced63d7e6f78b378ff31033f883"
integrity sha512-VdzAq0H1zGahueO3cOvzueKkBm2Gs4fHnz0gDykr2EIx21iBbpkGB0WHwyV0DbW1bXJbx12LNIjwrguUkkakag==
"@cypress/snapbuild-linux-ppc64le@1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@cypress/snapbuild-linux-ppc64le/-/snapbuild-linux-ppc64le-1.0.4.tgz#fd4f0a844f384f5bd3153209b0ed16be86ce2587"
integrity sha512-J9RWsf884YwE8NVruVN5lDuZZscRCc0+1ZwkUQIafz+/68KCfn6/YxBgCXC0kwUfTffQsVlq0m7/yaInE/1ReA==
"@cypress/snapbuild-windows-32@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@cypress/snapbuild-windows-32/-/snapbuild-windows-32-1.0.3.tgz#fd1159602d1f6d028e7e84046d3b6bbd07c69f54"
integrity sha512-14QbmvHyHl+2fUGs0sW0mRqhj34tc3MnrbUX+XV/SLFu3I09zg+mBn4kx7h881h9tf42o7aRj+Z6LuG7SZ2jRw==
"@cypress/snapbuild-windows-32@1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@cypress/snapbuild-windows-32/-/snapbuild-windows-32-1.0.4.tgz#ab148a83f6e31a738d65a6f321139fb6e69cea0c"
integrity sha512-kVA3jOY53EiJsBIT2E2hdNneRJhUPE6YP5PopTuVQ+xh9ms0rD3mmK/XEyDwF/AzPhQht6GxKPyIPSJwmaRJ0Q==
"@cypress/snapbuild-windows-64@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@cypress/snapbuild-windows-64/-/snapbuild-windows-64-1.0.3.tgz#92b9602a288c2cc961dc4f759049e9a3275cac9d"
integrity sha512-+7YH4+iex1BbqOKXKkJSvsr108wCAW4LsPZjMjpC+Ft0dIhJ8+MEpljpwCpUl3spjHp+e7qJXIOfgvFJ5fAJZw==
"@cypress/snapbuild-windows-64@1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@cypress/snapbuild-windows-64/-/snapbuild-windows-64-1.0.4.tgz#37854c0d1262b98913d3b6aa99f5343724e80d2b"
integrity sha512-sXqQ2+6xTVZVvKnK0WTNkczDUFRXeE9Cmv0nhfVYXpFNVhhLcUJm748haTMzeBtLh40o9BzvN/sFqkQwoCsI9w==
"@cypress/unique-selector@0.0.5":
version "0.0.5"
@@ -13324,10 +13324,10 @@ cross-spawn@^6.0.0, cross-spawn@^6.0.5:
shebang-command "^1.2.0"
which "^1.2.9"
cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3, cross-spawn@^7.0.6:
version "7.0.6"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
dependencies:
path-key "^3.1.0"
shebang-command "^2.0.0"
@@ -13502,12 +13502,12 @@ cypress-each@^1.11.0:
resolved "https://registry.yarnpkg.com/cypress-each/-/cypress-each-1.11.0.tgz#013c9b43a950f157bcf082d4bd0bb424fb370441"
integrity sha512-zeqeQkppPL6BKLIJdfR5IUoZRrxRudApJapnFzWCkkrmefQSqdlBma2fzhmniSJ3TRhxe5xpK3W3/l8aCrHvwQ==
cypress-example-kitchensink@3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/cypress-example-kitchensink/-/cypress-example-kitchensink-3.1.2.tgz#ddf8d6c21ccc8b6e0fe47c814299f02873e817fe"
integrity sha512-yKJeqNGGqZt313lwiFZxfLjpJWpaC3LO7+IpN/xocA15G6uqQ3etxEbCOg/NfmmskP9sm5NukXJ03MbeKvkTxA==
cypress-example-kitchensink@4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/cypress-example-kitchensink/-/cypress-example-kitchensink-4.0.0.tgz#a47566a06ef9273c379649406698a5bfa8022311"
integrity sha512-Ot0MlAHrtcx3Snd0JHxQFIEOoCgP/MwKflPlTqqqX3JbQOhT0WtS24+iUF/hJplrkOz/2iM17d/rmd+U3f8A2A==
dependencies:
npm-run-all2 "7.0.1"
npm-run-all2 "7.0.2"
serve "14.2.4"
cypress-expect@^2.5.3:
@@ -23579,13 +23579,13 @@ npm-registry-fetch@^18.0.0, npm-registry-fetch@^18.0.1:
npm-package-arg "^12.0.0"
proc-log "^5.0.0"
npm-run-all2@7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/npm-run-all2/-/npm-run-all2-7.0.1.tgz#7a20f65d072db4a880802d4ba5cd19566daef752"
integrity sha512-Adbv+bJQ8UTAM03rRODqrO5cx0YU5KCG2CvHtSURiadvdTjjgGJXdbc1oQ9CXBh9dnGfHSoSB1Web/0Dzp6kOQ==
npm-run-all2@7.0.2:
version "7.0.2"
resolved "https://registry.yarnpkg.com/npm-run-all2/-/npm-run-all2-7.0.2.tgz#26155c140b5e3f1155efd7f5d67212c8027b397c"
integrity sha512-7tXR+r9hzRNOPNTvXegM+QzCuMjzUIIq66VDunL6j60O4RrExx32XUhlrS7UK4VcdGw5/Wxzb3kfNcFix9JKDA==
dependencies:
ansi-styles "^6.2.1"
cross-spawn "^7.0.3"
cross-spawn "^7.0.6"
memorystream "^0.3.1"
minimatch "^9.0.0"
pidtree "^0.6.0"