feat: add spec watcher to data-context (#19583)

* feat: add spec watcher to data-context

* fix typo

* attempt to fix flake

* fix path.join

* if this fixes it I'm going to be upset

* remove unused code

* address comments
This commit is contained in:
Zachary Williams
2022-01-09 20:30:38 -06:00
committed by GitHub
parent 1c17a0ff54
commit f953c59e9a
18 changed files with 111 additions and 100 deletions
@@ -6,7 +6,10 @@ describe('App: Index', () => {
beforeEach(() => {
cy.scaffoldProject('non-existent-spec')
cy.openProject('non-existent-spec')
cy.withCtx(async (ctx, o) => {
cy.withCtx(async (ctx, { testState }) => {
testState.newFilePath = 'cypress/integration/new-file.spec.js'
await ctx.actions.file.removeFileInProject(testState.newFilePath)
await ctx.actions.file.removeFileInProject('cypress/integration/spec.js')
})
@@ -32,6 +35,30 @@ describe('App: Index', () => {
})
})
context('with specs', () => {
it('refreshes spec list on spec changes', () => {
cy.get('[data-testid="create-spec-page-title"]').should('be.visible')
cy.withCtx(async (ctx, { testState }) => {
await ctx.actions.file.writeFileInProject(testState.newFilePath, '')
})
cy.wait(1000)
cy.withCtx(async (ctx, { testState }) => {
expect(ctx.project.specs).have.length(1)
const addedSpec = ctx.project.specs.find((spec) => spec.absolute.includes(testState.newFilePath))
expect(addedSpec).not.be.equal(undefined)
})
// Hack due to ctx.emitter.toApp() not triggering a refresh in e2e test
// TODO: Figure out why emitter doesn't work in e2e tests
cy.visitApp()
cy.findByTestId('spec-item').should('contain', 'new-file')
})
})
context('scaffold example specs', () => {
const assertSpecs = (createdSpecs: FoundSpec[]) => cy.wrap(createdSpecs).each((spec: FoundSpec) => cy.contains(spec.baseName).scrollIntoView().should('be.visible'))
@@ -53,9 +80,7 @@ describe('App: Index', () => {
cy.contains(defaultMessages.createSpec.e2e.importFromScaffold.specsAddedButton).click()
cy.visitApp().then(() => {
assertSpecs(createdSpecs)
})
cy.visitApp().then(() => assertSpecs(createdSpecs))
})
})
})
@@ -27,6 +27,9 @@ export interface ProjectApiShape {
clearProjectPreferences(projectTitle: string): Promise<unknown>
clearAllProjectPreferences(): Promise<unknown>
closeActiveProject(): Promise<unknown>
getDevServer (): {
updateSpecs: (specs: FoundSpec[]) => void
}
}
export class ProjectActions {
@@ -404,7 +407,7 @@ export class ProjectActions {
assert(projectRoot, `Cannot create spec without currentProject.`)
const integrationFolder = 'cypress/integration' || this.ctx.currentProject
const integrationFolder = path.join(projectRoot, 'cypress', 'e2e')
const results = await codeGenerator(
{ templateDir: templates['scaffoldIntegration'], target: integrationFolder },
@@ -432,9 +435,6 @@ export class ProjectActions {
throw Error('Could not find specPattern for project')
}
// created new specs - find and cache them!
this.ctx.project.setSpecs(await this.ctx.project.findSpecs(projectRoot, 'e2e', specPattern))
return withFileParts
}
}
@@ -7,7 +7,7 @@
* states that exist, and how they are managed.
*/
import { ChildProcess, ForkOptions, fork } from 'child_process'
import chokidar from 'chokidar'
import chokidar, { FSWatcher } from 'chokidar'
import path from 'path'
import inspector from 'inspector'
import _ from 'lodash'
@@ -857,6 +857,7 @@ export class ProjectLifecycleManager {
addWatcher (file: string | string[]) {
const w = chokidar.watch(file, {
ignoreInitial: true,
cwd: this.projectRoot,
})
this.watchers.add(w)
@@ -864,6 +865,17 @@ export class ProjectLifecycleManager {
return w
}
closeWatcher (watcherToClose: FSWatcher) {
for (const watcher of this.watchers.values()) {
if (watcher === watcherToClose) {
watcher.close()
this.watchers.delete(watcher)
return
}
}
}
registerEvent (event: string, callback: Function) {
debug(`register event '${event}'`)
@@ -1,9 +1,11 @@
import os from 'os'
import { FrontendFramework, FRONTEND_FRAMEWORKS, ResolvedFromConfig, RESOLVED_FROM, SpecFileWithExtension, STORYBOOK_GLOB, FoundSpec } from '@packages/types'
import { scanFSForAvailableDependency } from 'create-cypress-tests'
import { debounce } from 'lodash'
import path from 'path'
import Debug from 'debug'
import commonPathPrefix from 'common-path-prefix'
import type { FSWatcher } from 'chokidar'
const debug = Debug('cypress:data-context')
import assert from 'assert'
@@ -95,6 +97,7 @@ export function transformSpec ({
}
export class ProjectDataSource {
private _specWatcher: FSWatcher | null = null
private _specs: FoundSpec[] = []
constructor (private ctx: DataContext) {}
@@ -142,6 +145,39 @@ export class ProjectDataSource {
return matched
}
startSpecWatcher (projectRoot: string, testingType: Cypress.TestingType, specPattern: string | string[]) {
this.stopSpecWatcher()
const currentProject = this.ctx.currentProject
if (!currentProject) {
throw new Error('Cannot start spec watcher without current project')
}
const onSpecsChanged = debounce(async () => {
const specs = await this.findSpecs(projectRoot, testingType, specPattern)
this.setSpecs(specs)
if (testingType === 'component') {
this.api.getDevServer().updateSpecs(specs)
}
this.ctx.emitter.toApp()
})
this._specWatcher = this.ctx.lifecycleManager.addWatcher(specPattern)
this._specWatcher.on('add', onSpecsChanged)
this._specWatcher.on('unlink', onSpecsChanged)
}
stopSpecWatcher () {
if (!this._specWatcher) {
return
}
this.ctx.lifecycleManager.closeWatcher(this._specWatcher)
}
async getCurrentSpecByAbsolute (absolute: string) {
return this.ctx.project.specs.find((x) => x.absolute === absolute)
}
+2 -3
View File
@@ -22,7 +22,7 @@ const _serveNonProxiedError = (res: Response) => {
})
}
export interface ServeOptions extends Pick<InitializeRoutes, 'getSpec' | 'config' | 'getCurrentBrowser' | 'getRemoteState' | 'specsStore' | 'exit'> {
export interface ServeOptions extends Pick<InitializeRoutes, 'getSpec' | 'config' | 'getCurrentBrowser' | 'getRemoteState' | 'exit'> {
testingType: Cypress.TestingType
}
@@ -47,7 +47,7 @@ export const runner = {
return _serveNonProxiedError(res)
}
let { config, getRemoteState, getCurrentBrowser, getSpec, specsStore, exit } = options
let { config, getRemoteState, getCurrentBrowser, getSpec, exit } = options
config = _.clone(config)
// at any given point, rather than just arbitrarily modifying it.
@@ -72,7 +72,6 @@ export const runner = {
config.platform = os.platform() as PlatformName
config.arch = os.arch()
config.spec = spec ? { ...spec, name: spec.baseName } : null
config.specs = specsStore.specFiles
config.browser = getCurrentBrowser()
config.exit = exit ?? true
+4
View File
@@ -28,6 +28,7 @@ import * as savedState from './saved_state'
import appData from './util/app_data'
import plugins from './plugins'
import browsers from './browsers'
import devServer from './plugins/dev-server'
const { getBrowsers, ensureAndGetByNameOrPath } = browserUtils
@@ -116,6 +117,9 @@ export function makeDataContext (options: MakeDataContextOptions): DataContext {
closeActiveProject () {
return openProject.closeActiveProject()
},
getDevServer () {
return devServer
},
},
electronApi: {
openExternal (url: string) {
+1
View File
@@ -278,6 +278,7 @@ export class OpenProject {
const specs = await this._ctx.project.findSpecs(path, testingType, specPattern)
this._ctx.actions.project.setSpecs(specs)
this._ctx.project.startSpecWatcher(path, testingType, specPattern)
await this.projectBase.open()
} catch (err: any) {
+21 -52
View File
@@ -26,7 +26,6 @@ import { fs } from './util/fs'
import devServer from './plugins/dev-server'
import preprocessor from './plugins/preprocessor'
import { SpecsStore } from './specs-store'
import { checkSupportFile } from './project_utils'
import type { FoundBrowser, OpenProjectLaunchOptions, FoundSpec } from '@packages/types'
import { DataContext, getCtx } from '@packages/data-context'
@@ -173,11 +172,7 @@ export class ProjectBase<TServer extends Server> extends EE {
this._server = this.createServer(this.testingType)
const {
specsStore,
startSpecWatcher,
ctDevServerPort,
} = await this.initializeSpecStore(cfg)
const { ctDevServerPort } = await this.initializeSpecsAndDevServer(cfg)
if (this.testingType === 'component') {
cfg.baseUrl = `http://localhost:${ctDevServerPort}`
@@ -192,7 +187,6 @@ export class ProjectBase<TServer extends Server> extends EE {
shouldCorrelatePreRequests: this.shouldCorrelatePreRequests,
testingType: this.testingType,
SocketCtor: this.testingType === 'e2e' ? SocketE2E : SocketCt,
specsStore,
})
this.ctx.setAppServerPort(port)
@@ -255,13 +249,6 @@ export class ProjectBase<TServer extends Server> extends EE {
return
}
// start watching specs
// whenever a spec file is added or removed, we notify the
// <SpecList>
// This is only used for CT right now by general users.
// It is is used with E2E if the CypressInternal_UseInlineSpecList flag is true.
startSpecWatcher()
if (!cfg.experimentalInteractiveRunEvents) {
return
}
@@ -335,12 +322,28 @@ export class ProjectBase<TServer extends Server> extends EE {
options.onError(err)
}
async initializeSpecStore (updatedConfig: Cfg): Promise<{
specsStore: SpecsStore
async initializeSpecsAndDevServer (updatedConfig: Cfg): Promise<{
ctDevServerPort: number | undefined
startSpecWatcher: () => void
}> {
return this.initSpecStore({ specs: this.ctx.project.specs, config: updatedConfig })
const specs = this.ctx.project.specs || []
let ctDevServerPort: number | undefined
if (!this.ctx.currentProject) {
throw new Error('Cannot set specs without current project')
}
updatedConfig.specs = specs
if (this.testingType === 'component' && !this.options.skipPluginInitializeForTesting) {
const { port } = await this.startCtDevServer(specs, updatedConfig)
ctDevServerPort = port
}
return {
ctDevServerPort,
}
}
async startCtDevServer (specs: Cypress.Cypress['spec'][], config: any) {
@@ -359,40 +362,6 @@ export class ProjectBase<TServer extends Server> extends EE {
return { port: devServerOptions.port }
}
async initSpecStore ({
specs,
config,
}: {
specs: FoundSpec[]
config: Cfg
}) {
const specsStore = new SpecsStore()
const startSpecWatcher = () => {
if (this.testingType === 'component') {
// ct uses the dev-server to build and bundle the speces.
// send new files to dev server
devServer.updateSpecs(specs)
}
}
let ctDevServerPort: number | undefined
if (this.testingType === 'component' && !this.options.skipPluginInitializeForTesting) {
const { port } = await this.startCtDevServer(specs, config)
ctDevServerPort = port
}
specsStore.storeSpecFiles(specs)
return {
specsStore,
ctDevServerPort,
startSpecWatcher,
}
}
initializeReporter ({
report,
reporter,
-6
View File
@@ -5,7 +5,6 @@ import { ErrorRequestHandler, Router } from 'express'
import send from 'send'
import { getPathToDist } from '@packages/resolve-dist'
import type { SpecsStore } from './specs-store'
import type { Browser } from './browsers/types'
import type { NetworkProxy } from '@packages/proxy'
import type { Cfg } from './project-base'
@@ -19,7 +18,6 @@ const debug = Debug('cypress:server:routes')
export interface InitializeRoutes {
ctx: DataContext
specsStore: SpecsStore
config: Cfg
getSpec: () => FoundSpec | null
getCurrentBrowser: () => Browser
@@ -37,7 +35,6 @@ export const createCommonRoutes = ({
testingType,
getSpec,
getCurrentBrowser,
specsStore,
getRemoteState,
nodeProxy,
ctx,
@@ -48,7 +45,6 @@ export const createCommonRoutes = ({
...options.config,
testingType,
browser: options.getCurrentBrowser?.(),
specs: options.specsStore?.specFiles,
} as Cfg
if (testingType === 'e2e') {
@@ -79,7 +75,6 @@ export const createCommonRoutes = ({
const options = makeServeConfig({
config,
getCurrentBrowser,
specsStore,
})
res.json(options)
@@ -139,7 +134,6 @@ export const createCommonRoutes = ({
getSpec,
getCurrentBrowser,
getRemoteState,
specsStore,
exit,
})
}
-3
View File
@@ -1,7 +1,6 @@
import Debug from 'debug'
import _ from 'lodash'
import send from 'send'
import type { SpecsStore } from '@packages/server/lib/specs-store'
import { getPathToIndex, getPathToDist } from '@packages/resolve-dist'
import type { Cfg } from '@packages/server/lib/project-base'
import type { Browser } from '@packages/server/lib/browsers/types'
@@ -9,7 +8,6 @@ import type { Browser } from '@packages/server/lib/browsers/types'
interface ServeOptions {
config: Cfg
getCurrentBrowser: () => Browser
specsStore: SpecsStore
}
const debug = Debug('cypress:server:runner-ct')
@@ -25,7 +23,6 @@ export const makeServeConfig = (options) => {
const config = {
...options.config,
browser: options.getCurrentBrowser(),
specs: options.specsStore.specFiles,
} as Cfg
// TODO: move the component file watchers in here
-4
View File
@@ -25,7 +25,6 @@ import origin from './util/origin'
import { allowDestroy, DestroyableHttpServer } from './util/server_destroy'
import { SocketAllowed } from './util/socket_allowed'
import { createInitialWorkers } from '@packages/rewriter'
import type { SpecsStore } from './specs-store'
import type { Cfg } from './project-base'
import type { Browser } from '@packages/server/lib/browsers/types'
import { InitializeRoutes, createCommonRoutes } from './routes'
@@ -97,7 +96,6 @@ export type WarningErr = Record<string, any>
export interface OpenServerOptions {
SocketCtor: typeof SocketE2E | typeof SocketCt
specsStore: SpecsStore
testingType: Cypress.TestingType
onError: any
onWarning: any
@@ -177,7 +175,6 @@ export abstract class ServerBase<TSocket extends SocketE2E | SocketCt> {
onError,
onWarning,
shouldCorrelatePreRequests,
specsStore,
testingType,
SocketCtor,
exit,
@@ -217,7 +214,6 @@ export abstract class ServerBase<TSocket extends SocketE2E | SocketCt> {
const routeOptions: InitializeRoutes = {
ctx: this.ctx,
config,
specsStore,
getRemoteState,
nodeProxy: this.nodeProxy,
networkProxy: this._networkProxy!,
-12
View File
@@ -1,12 +0,0 @@
import type { FoundSpec } from '@packages/types'
type SpecFile = Cypress.Cypress['spec']
type SpecFiles = SpecFile[]
export class SpecsStore {
specFiles: SpecFiles = []
storeSpecFiles (specFiles: FoundSpec[]) {
this.specFiles = specFiles
}
}
@@ -21,7 +21,6 @@ const EventSource = require('eventsource')
const config = require(`../../lib/config`)
const { ServerE2E } = require(`../../lib/server-e2e`)
const ProjectBase = require(`../../lib/project-base`).ProjectBase
const { SpecsStore } = require(`../../lib/specs-store`)
const pluginsModule = require(`../../lib/plugins`)
const preprocessor = require(`../../lib/plugins/preprocessor`)
const resolve = require(`../../lib/util/resolve`)
@@ -167,7 +166,6 @@ describe('Routes', () => {
SocketCtor: SocketE2E,
getSpec: () => spec,
getCurrentBrowser: () => null,
specsStore: new SpecsStore({}, 'e2e'),
createRoutes,
testingType: 'e2e',
exit: false,
@@ -9,7 +9,6 @@ const httpsServer = require(`@packages/https-proxy/test/helpers/https_server`)
const config = require(`../../lib/config`)
const { ServerE2E } = require(`../../lib/server-e2e`)
const { SocketE2E } = require(`../../lib/socket-e2e`)
const { SpecsStore } = require(`../../lib/specs-store`)
const Fixtures = require('@tooling/system-tests/lib/fixtures')
const { createRoutes } = require(`../../lib/routes`)
@@ -87,7 +86,6 @@ describe('Server', () => {
this.server.open(cfg, {
SocketCtor: SocketE2E,
createRoutes,
specsStore: new SpecsStore({}, 'e2e'),
testingType: 'e2e',
})
.spread(async (port) => {
@@ -10,7 +10,6 @@ const httpsServer = require(`@packages/https-proxy/test/helpers/https_server`)
const config = require(`../../lib/config`)
const { ServerE2E } = require(`../../lib/server-e2e`)
const { SocketE2E } = require(`../../lib/socket-e2e`)
const { SpecsStore } = require(`../../lib/specs-store`)
const { Automation } = require(`../../lib/automation`)
const Fixtures = require('@tooling/system-tests/lib/fixtures')
const { createRoutes } = require(`../../lib/routes`)
@@ -44,7 +43,6 @@ describe('Web Sockets', () => {
return this.server.open(this.cfg, {
SocketCtor: SocketE2E,
createRoutes,
specsStore: new SpecsStore({}, 'e2e'),
testingType: 'e2e',
})
.then(async () => {
@@ -17,7 +17,6 @@ const performance = require('@tooling/system-tests/lib/performance')
const Promise = require('bluebird')
const sanitizeFilename = require('sanitize-filename')
const { createRoutes } = require(`../../lib/routes`)
const { SpecsStore } = require(`../../lib/specs-store`)
process.env.CYPRESS_INTERNAL_ENV = 'development'
@@ -363,7 +362,6 @@ describe('Proxy Performance', function () {
return cyServer.open(config, {
SocketCtor: SocketE2E,
createRoutes,
specsStore: new SpecsStore({}, 'e2e'),
testingType: 'e2e',
})
}),
-3
View File
@@ -11,7 +11,6 @@ const config = require(`../../lib/config`)
const { SocketE2E } = require(`../../lib/socket-e2e`)
const { ServerE2E } = require(`../../lib/server-e2e`)
const { Automation } = require(`../../lib/automation`)
const { SpecsStore } = require(`../../lib/specs-store`)
const exec = require(`../../lib/exec`)
const preprocessor = require(`../../lib/plugins/preprocessor`)
const { fs } = require(`../../lib/util/fs`)
@@ -53,7 +52,6 @@ describe('lib/socket', () => {
this.server.open(this.cfg, {
SocketCtor: SocketE2E,
createRoutes,
specsStore: new SpecsStore({}, 'e2e'),
testingType: 'e2e',
})
.then(() => {
@@ -570,7 +568,6 @@ describe('lib/socket', () => {
return this.server.open(this.cfg, {
SocketCtor: SocketE2E,
createRoutes,
specsStore: new SpecsStore({}, 'e2e'),
testingType: 'e2e',
})
.then(() => {
@@ -1,6 +1,7 @@
module.exports = {
'e2e': {
'supportFile': false,
specPattern: 'cypress/**/*.spec.js',
setupNodeEvents (on, config) {
on('file:preprocessor', () => '/does/not/exist.js')