mirror of
https://github.com/cypress-io/cypress.git
synced 2026-04-24 07:59:12 -05:00
Merge branch 'develop' into release/15.0.0
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Vendored
-1
@@ -3324,7 +3324,6 @@ declare namespace Cypress {
|
||||
spec: Cypress['spec'] | null
|
||||
specs: Array<Cypress['spec']>
|
||||
isDefaultProtocolEnabled: boolean
|
||||
isStudioProtocolEnabled: boolean
|
||||
hideCommandLog: boolean
|
||||
hideRunnerUi: boolean
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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"
|
||||
>×</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;
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
]
|
||||
|
||||
@@ -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,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
@@ -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 {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+37
-18
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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}`
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,7 +176,6 @@ const omitConfigKeys = [
|
||||
'state',
|
||||
'supportFolder',
|
||||
'isDefaultProtocolEnabled',
|
||||
'isStudioProtocolEnabled',
|
||||
'hideCommandLog',
|
||||
'hideRunnerUi',
|
||||
]
|
||||
|
||||
@@ -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' })
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
+147
-22
@@ -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
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
+9
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user