Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: restart dev-server on config change #21212

Merged
merged 10 commits into from
May 2, 2022
1 change: 1 addition & 0 deletions npm/vite-dev-server/src/devServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export async function devServer (config: ViteDevServerConfig): Promise<Cypress.R

return {
port,
// Close is for unit testing only. We kill this child process which will handle the closing of the server
close (cb) {
return server.close().then(() => cb?.()).catch(cb)
},
Expand Down
4 changes: 3 additions & 1 deletion npm/webpack-dev-server/src/devServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export function devServer (devServerConfig: WebpackDevServerConfig): Promise<Cyp

resolve({
port,
// Close is for unit testing only. We kill this child process which will handle the closing of the server
close: (done) => {
srv.close((err) => {
if (err) {
Expand All @@ -101,7 +102,8 @@ export function devServer (devServerConfig: WebpackDevServerConfig): Promise<Cyp

resolve({
port: result.server.options.port as number,
close: (done) => {
// Close is for unit testing only. We kill this child process which will handle the closing of the server
close: async (done) => {
debug('closing dev server')
result.server.stop().then(() => done?.()).catch(done).finally(() => {
debug('closed dev server')
Expand Down
262 changes: 149 additions & 113 deletions packages/app/cypress/e2e/cypress-in-cypress-component.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,156 +4,192 @@ import { getPathForPlatform } from '../../src/paths'
import { snapshotAUTPanel } from './support/snapshot-aut-panel'

describe('Cypress In Cypress CT', { viewportWidth: 1500, defaultCommandTimeout: 10000 }, () => {
beforeEach(() => {
cy.scaffoldProject('cypress-in-cypress')
cy.findBrowsers()
cy.openProject('cypress-in-cypress')
cy.startAppServer('component')
Comment on lines -10 to -11
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason we needed to move this from the beforeEach?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was another test that were duplicating the openProject and startAppServer call with different arguments, I'm not a fan of calling these more than once so I decided to move them out and into each test.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that's fine, I wonder if that's an indication we should have a second describe block then?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 8cabb60

})

it('test component', () => {
cy.visitApp()
cy.contains('TestComponent.spec').click()
cy.location().should((location) => {
expect(location.hash).to.contain('TestComponent.spec')
context('default config', () => {
beforeEach(() => {
cy.scaffoldProject('cypress-in-cypress')
cy.findBrowsers()
cy.openProject('cypress-in-cypress')
cy.startAppServer('component')
})

cy.get('[data-model-state="passed"]').should('contain', 'renders the test component')

cy.findByTestId('aut-url').should('not.exist')
cy.findByTestId('select-browser').click()

cy.contains('Canary').should('be.visible')
cy.findByTestId('viewport').click()
it('test component', () => {
cy.visitApp()
cy.contains('TestComponent.spec').click()
cy.location().should((location) => {
expect(location.hash).to.contain('TestComponent.spec')
})

snapshotAUTPanel('browsers open')
cy.contains('Canary').should('be.hidden')
cy.contains('The viewport determines the width and height of your application. By default the viewport will be 500px by 500px for Component Testing unless specified by a cy.viewport command.')
.should('be.visible')
cy.get('[data-model-state="passed"]').should('contain', 'renders the test component')

snapshotAUTPanel('viewport info open')
cy.findByTestId('aut-url').should('not.exist')
cy.findByTestId('select-browser').click()

cy.get('body').click()
cy.contains('Canary').should('be.visible')
cy.findByTestId('viewport').click()

cy.findByTestId('playground-activator').click()
cy.findByTestId('playground-selector').clear().type('[data-cy-root]')
snapshotAUTPanel('browsers open')
cy.contains('Canary').should('be.hidden')
cy.contains('The viewport determines the width and height of your application. By default the viewport will be 500px by 500px for Component Testing unless specified by a cy.viewport command.')
.should('be.visible')

snapshotAUTPanel('cy.get selector')
snapshotAUTPanel('viewport info open')

cy.findByTestId('playground-num-elements').contains('1 Match')
cy.get('body').click()

cy.window().then((win) => cy.spy(win.console, 'log'))
cy.findByTestId('playground-print').click().window().then((win) => {
expect(win.console.log).to.have.been.calledWith('%cCommand: ', 'font-weight: bold', 'cy.get(\'[data-cy-root]\')')
})
cy.findByTestId('playground-activator').click()
cy.findByTestId('playground-selector').clear().type('[data-cy-root]')

cy.findByLabelText('Selector Methods').click()
cy.findByRole('menuitem', { name: 'cy.contains' }).click()
snapshotAUTPanel('cy.get selector')

cy.findByTestId('playground-selector').clear().type('Component Test')
cy.findByTestId('playground-num-elements').contains('1 Match')

snapshotAUTPanel('cy.contains selector')
cy.window().then((win) => cy.spy(win.console, 'log'))
cy.findByTestId('playground-print').click().window().then((win) => {
expect(win.console.log).to.have.been.calledWith('%cCommand: ', 'font-weight: bold', 'cy.get(\'[data-cy-root]\')')
})

cy.findByTestId('playground-num-elements').contains('1 Match')
})
cy.findByLabelText('Selector Methods').click()
cy.findByRole('menuitem', { name: 'cy.contains' }).click()

it('navigation between specs and other parts of the app works', () => {
cy.visitApp()
cy.contains('TestComponent.spec').click()
cy.get('[data-model-state="passed"]').should('contain', 'renders the test component')

// go to Settings page and back to spec runner
cy.contains('a', 'Settings').click()
cy.contains(defaultMessages.settingsPage.device.title).should('be.visible')
cy.contains('a', 'Specs').click()
cy.contains('TestComponent.spec').click()
cy.get('[data-model-state="passed"]').should('contain', 'renders the test component')

// go to Runs page and back to spec runner
cy.contains('a', 'Runs').click()
cy.contains(defaultMessages.runs.connect.title).should('be.visible')
cy.contains('a', 'Specs').click()
cy.contains('TestComponent.spec').click()
cy.get('[data-model-state="passed"]').should('contain', 'renders the test component')
})
cy.findByTestId('playground-selector').clear().type('Component Test')

it('redirects to the specs list with error if a spec is not found', () => {
cy.visitApp()
const { noSpecErrorTitle, noSpecErrorIntro, noSpecErrorExplainer } = defaultMessages.specPage
const badFilePath = 'src/DoesNotExist.spec.js'

cy.visitApp(`/specs/runner?file=${badFilePath}`)
cy.contains(noSpecErrorTitle).should('be.visible')
cy.contains(noSpecErrorIntro).should('be.visible')
cy.contains(noSpecErrorExplainer).should('be.visible')
cy.contains(getPathForPlatform(badFilePath)).should('be.visible')
cy.location()
.its('href')
.should('eq', 'http://localhost:4455/__/#/specs')

// should clear after reload
cy.reload()
cy.contains(noSpecErrorTitle).should('not.exist')
})
snapshotAUTPanel('cy.contains selector')

it('redirects to the specs list with error if an open spec is not found when specs list updates', () => {
const { noSpecErrorTitle, noSpecErrorIntro, noSpecErrorExplainer } = defaultMessages.specPage

const goodFilePath = 'src/TestComponent.spec.jsx'
cy.findByTestId('playground-num-elements').contains('1 Match')
})

cy.visitApp(`/specs/runner?file=${goodFilePath}`)
it('navigation between specs and other parts of the app works', () => {
cy.visitApp()
cy.contains('TestComponent.spec').click()
cy.get('[data-model-state="passed"]').should('contain', 'renders the test component')

// go to Settings page and back to spec runner
cy.contains('a', 'Settings').click()
cy.contains(defaultMessages.settingsPage.device.title).should('be.visible')
cy.contains('a', 'Specs').click()
cy.contains('TestComponent.spec').click()
cy.get('[data-model-state="passed"]').should('contain', 'renders the test component')

// go to Runs page and back to spec runner
cy.contains('a', 'Runs').click()
cy.contains(defaultMessages.runs.connect.title).should('be.visible')
cy.contains('a', 'Specs').click()
cy.contains('TestComponent.spec').click()
cy.get('[data-model-state="passed"]').should('contain', 'renders the test component')
})

cy.contains('renders the test component').should('be.visible')
it('redirects to the specs list with error if a spec is not found', () => {
cy.visitApp()
const { noSpecErrorTitle, noSpecErrorIntro, noSpecErrorExplainer } = defaultMessages.specPage
const badFilePath = 'src/DoesNotExist.spec.js'

cy.withCtx((ctx, o) => {
ctx.actions.project.setSpecs(ctx.project.specs.filter((spec) => !spec.absolute.includes(o.path)))
}, { path: goodFilePath }).then(() => {
cy.visitApp(`/specs/runner?file=${badFilePath}`)
cy.contains(noSpecErrorTitle).should('be.visible')
cy.contains(noSpecErrorIntro).should('be.visible')
cy.contains(noSpecErrorExplainer).should('be.visible')
cy.contains(getPathForPlatform(goodFilePath)).should('be.visible')
cy.contains(getPathForPlatform(badFilePath)).should('be.visible')
cy.location()
.its('href')
.should('eq', 'http://localhost:4455/__/#/specs')

// should clear after reload
cy.reload()
cy.contains(noSpecErrorTitle).should('not.exist')
})
})

it('browser picker in runner calls mutation with current spec path', () => {
cy.visitApp()
cy.contains('TestComponent.spec').click()
cy.get('[data-model-state="passed"]').should('contain', 'renders the test component')
it('redirects to the specs list with error if an open spec is not found when specs list updates', () => {
const { noSpecErrorTitle, noSpecErrorIntro, noSpecErrorExplainer } = defaultMessages.specPage

const goodFilePath = 'src/TestComponent.spec.jsx'

cy.visitApp(`/specs/runner?file=${goodFilePath}`)

cy.withCtx((ctx, o) => {
o.sinon.stub(ctx.actions.app, 'setActiveBrowserById')
o.sinon.stub(ctx.actions.project, 'launchProject').resolves()
cy.contains('renders the test component').should('be.visible')

cy.withCtx((ctx, o) => {
ctx.actions.project.setSpecs(ctx.project.specs.filter((spec) => !spec.absolute.includes(o.path)))
}, { path: goodFilePath }).then(() => {
cy.contains(noSpecErrorTitle).should('be.visible')
cy.contains(noSpecErrorIntro).should('be.visible')
cy.contains(noSpecErrorExplainer).should('be.visible')
cy.contains(getPathForPlatform(goodFilePath)).should('be.visible')
cy.location()
.its('href')
.should('eq', 'http://localhost:4455/__/#/specs')
})
})

it('browser picker in runner calls mutation with current spec path', () => {
cy.visitApp()
cy.contains('TestComponent.spec').click()
cy.get('[data-model-state="passed"]').should('contain', 'renders the test component')

cy.withCtx((ctx, o) => {
o.sinon.stub(ctx.actions.app, 'setActiveBrowserById')
o.sinon.stub(ctx.actions.project, 'launchProject').resolves()
})

cy.get('[data-cy="select-browser"]')
.click()

cy.contains('Firefox')
.click()

cy.withCtx((ctx, o) => {
const browserId = (ctx.actions.app.setActiveBrowserById as SinonStub).args[0][0]
const genId = ctx.fromId(browserId, 'Browser')

expect(ctx.actions.app.setActiveBrowserById).to.have.been.calledWith(browserId)
expect(genId).to.eql('firefox-firefox-stable')
expect(ctx.actions.project.launchProject).to.have.been.calledWith(
ctx.coreData.currentTestingType, {}, o.sinon.match(new RegExp('cypress\-in\-cypress\/src\/TestComponent\.spec\.jsx$')),
)
})
})

cy.get('[data-cy="select-browser"]')
.click()
it('restarts dev server on config change', () => {
cy.visitApp()

cy.withCtx(async (ctx, { testState, sinon }) => {
sinon.stub(ctx._apis.projectApi.getDevServer(), 'close')
const devServerReady =
new Promise<void>((res) => {
ctx._apis.projectApi.getDevServer().emitter.on('dev-server:compile:success', () => res())
})

testState.originalCypressConfig = await ctx.file.readFileInProject('cypress.config.js')
const newCypressConfig = testState.originalCypressConfig.replace(`webpackConfig: require('./webpack.config.js')`, `webpackConfig: {}`)

await ctx.actions.file.writeFileInProject('cypress.config.js', newCypressConfig)
await devServerReady
})

cy.contains('Firefox')
.click()
cy.contains('TestComponent.spec').click()
cy.get('.failed > .num').should('contain', 1)

cy.withCtx((ctx, o) => {
const browserId = (ctx.actions.app.setActiveBrowserById as SinonStub).args[0][0]
const genId = ctx.fromId(browserId, 'Browser')
cy.withCtx(async (ctx, { testState }) => {
await ctx.actions.file.writeFileInProject('cypress.config.js', testState.originalCypressConfig)
})

expect(ctx.actions.app.setActiveBrowserById).to.have.been.calledWith(browserId)
expect(genId).to.eql('firefox-firefox-stable')
expect(ctx.actions.project.launchProject).to.have.been.calledWith(
ctx.coreData.currentTestingType, {}, o.sinon.match(new RegExp('cypress\-in\-cypress\/src\/TestComponent\.spec\.jsx$')),
)
cy.get('.passed > .num').should('contain', 1)
})
})

it('set the correct viewport values from CLI', () => {
cy.openProject('cypress-in-cypress', ['--config', 'viewportWidth=333,viewportHeight=333'])
cy.startAppServer('component')
context('custom config', () => {
beforeEach(() => {
cy.scaffoldProject('cypress-in-cypress')
cy.findBrowsers()
})

it('set the correct viewport values from CLI', () => {
cy.openProject('cypress-in-cypress', ['--config', 'viewportWidth=333,viewportHeight=333'])
cy.startAppServer('component')

cy.visitApp()
cy.contains('TestComponent.spec').click()
cy.visitApp()
cy.contains('TestComponent.spec').click()

cy.get('#unified-runner').should('have.css', 'width', '333px')
cy.get('#unified-runner').should('have.css', 'height', '333px')
cy.get('#unified-runner').should('have.css', 'width', '333px')
cy.get('#unified-runner').should('have.css', 'height', '333px')
})
})
})
3 changes: 1 addition & 2 deletions packages/app/cypress/e2e/specs_list_actual_git_repo.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ describe('Spec List - Git Status', () => {
beforeEach(() => {
cy.scaffoldProject('cypress-in-cypress')
.then((projectPath) => {
cy.task('initGitRepoForTestProject', projectPath)
cy.wait(500)
cy.openProject('cypress-in-cypress')
cy.startAppServer('e2e')
cy.task('initGitRepoForTestProject', projectPath)
cy.visitApp()
})
})
Expand Down
7 changes: 5 additions & 2 deletions packages/data-context/src/actions/ProjectActions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { CodeGenType, MutationSetProjectPreferencesArgs, NexusGenObjects, NexusGenUnions, TestingTypeEnum } from '@packages/graphql/src/gen/nxs.gen'
import type { InitializeProjectOptions, FoundBrowser, FoundSpec, LaunchOpts, OpenProjectLaunchOptions, Preferences, TestingType, ReceivedCypressOptions, AddProject } from '@packages/types'
import type { InitializeProjectOptions, FoundBrowser, FoundSpec, LaunchOpts, OpenProjectLaunchOptions, Preferences, TestingType, ReceivedCypressOptions, AddProject, FullConfig } from '@packages/types'
import execa from 'execa'
import path from 'path'
import assert from 'assert'
Expand Down Expand Up @@ -34,7 +34,10 @@ export interface ProjectApiShape {
getCurrentProjectSavedState(): {} | undefined
setPromptShown(slug: string): void
getDevServer (): {
updateSpecs: (specs: FoundSpec[]) => void
updateSpecs(specs: FoundSpec[]): void
start(options: {specs: Cypress.Spec[], config: FullConfig}): Promise<{port: number}>
close(): void
emitter: EventEmitter
}
isListening: (url: string) => Promise<void>
}
Expand Down
Loading