feat: setting up e2e open mode testing framework (#18472)

This commit is contained in:
Tim Griesser
2021-10-14 11:10:26 -04:00
committed by GitHub
parent b7d8a60908
commit bdfc4a7b80
48 changed files with 608 additions and 132 deletions

View File

@@ -6,7 +6,7 @@
**/build
**/cypress/fixtures
**/dist
**/dist-test
**/dist-*
**/node_modules
**/support/fixtures/*
!**/support/fixtures/projects

3
.gitignore vendored
View File

@@ -50,6 +50,9 @@ packages/example/app
packages/example/build
packages/example/cypress/integration
# from frontend-shared
packages/frontend-shared/cypress/e2e/.projects
# from server
packages/server/.cy
packages/server/.projects

View File

@@ -21,6 +21,7 @@
"check-ts": "gulp checkTs",
"clean-deps": "find . -depth -name node_modules -type d -exec rm -rf {} \\;",
"clean-untracked-files": "git clean -d -f",
"codegen": "yarn gulp codegen",
"debug": "yarn gulp debug",
"precypress:open": "yarn ensure-deps",
"cypress:open": "cypress open --dev --global",
@@ -117,6 +118,7 @@
"@types/react": "16.9.50",
"@types/react-dom": "16.9.8",
"@types/request-promise": "4.1.45",
"@types/rimraf": "^3.0.2",
"@types/send": "^0.17.1",
"@types/sinon-chai": "3.2.3",
"@types/through2": "^2.0.36",

View File

@@ -1,4 +1,5 @@
{
"$schema": "../../cli/schema/cypress.schema.json",
"projectId": "sehy69",
"viewportWidth": 800,
"viewportHeight": 850,

View File

@@ -1,39 +0,0 @@
let GQL_PORT
let SERVER_PORT
describe('App', () => {
beforeEach(() => {
cy.withCtx(async (ctx) => {
await ctx.dispose()
await ctx.actions.project.setActiveProject(ctx.launchArgs.projectRoot)
ctx.actions.wizard.setTestingType('e2e')
await ctx.actions.project.initializeActiveProject({
skipPluginIntializeForTesting: true,
})
await ctx.actions.project.launchProject('e2e', {
skipBrowserOpenForTest: true,
})
return [
ctx.gqlServerPort,
ctx.appServerPort,
]
}).then(([gqlPort, serverPort]) => {
GQL_PORT = gqlPort
SERVER_PORT = serverPort
})
})
it('resolves the home page', () => {
cy.visit(`dist/index.html?serverPort=${SERVER_PORT}&gqlPort=${GQL_PORT}`)
cy.get('[href="#/runner"]').click()
cy.get('[href="#/settings"]').click()
})
it('resolves the home page, with a different server port?', () => {
cy.visit(`dist/index.html?serverPort=${SERVER_PORT}&gqlPort=${GQL_PORT}`)
cy.get('[href="#/runner"]').click()
cy.get('[href="#/settings"]').click()
})
})

View File

@@ -0,0 +1,13 @@
describe('App', () => {
beforeEach(() => {
cy.setupE2E('component-tests')
cy.initializeApp()
})
it('resolves the home page', () => {
cy.visitApp()
cy.wait(1000)
cy.get('[href="#/runner"]').click()
cy.get('[href="#/settings"]').click()
})
})

View File

@@ -9,8 +9,9 @@
"clean-deps": "rimraf node_modules",
"test": "echo 'ok'",
"cypress:launch": "cross-env TZ=America/New_York node ../../scripts/cypress open --project ${PWD}",
"cypress:open": "yarn gulp cyOpenAppE2E",
"cypress:run:e2e": "yarn gulp cyRunAppE2E",
"cypress:open": "cross-env TZ=America/New_York node ../../scripts/cypress open --project ${PWD}",
"cypress:run:ct": "cross-env TZ=America/New_York node ../../scripts/cypress run-ct --project ${PWD}",
"cypress:run:e2e": "cross-env TZ=America/New_York node ../../scripts/cypress run --project ${PWD}",
"debug": "gulp debug --project ${PWD}",
"dev": "gulp dev --project ${PWD}",
"start": "echo \"run 'yarn dev' from the root\" && exit 1",

View File

@@ -1,5 +1,5 @@
import type { DataContext } from '.'
import { AppActions, ProjectActions, StorybookActions, WizardActions } from './actions'
import { AppActions, FileActions, ProjectActions, StorybookActions, WizardActions } from './actions'
import { AuthActions } from './actions/AuthActions'
import { DevActions } from './actions/DevActions'
import { cached } from './util'
@@ -7,6 +7,11 @@ import { cached } from './util'
export class DataActions {
constructor (private ctx: DataContext) {}
@cached
get file () {
return new FileActions(this.ctx)
}
@cached
get dev () {
return new DevActions(this.ctx)

View File

@@ -1,4 +1,5 @@
import type { LaunchArgs, OpenProjectLaunchOptions, PlatformName } from '@packages/types'
import path from 'path'
import type { AppApiShape, ProjectApiShape } from './actions'
import type { NexusGenAbstractTypeMembers } from '@packages/graphql/src/gen/nxs.gen'
import type { AuthApiShape } from './actions/AuthActions'
@@ -45,6 +46,11 @@ export class DataContext extends DataContextShell {
return fsExtra
}
@cached
get path () {
return path
}
constructor (private config: DataContextConfig) {
super(config)
this._coreData = config.coreData ?? makeCoreData()
@@ -193,7 +199,9 @@ export class DataContext extends DataContextShell {
console.error(e)
}
async dispose () {
async destroy () {
super.destroy()
return Promise.all([
this.util.disposeLoaders(),
this.actions.project.clearActiveProject(),

View File

@@ -1,4 +1,6 @@
import { EventEmitter } from 'events'
import type { Server } from 'http'
import type { AddressInfo } from 'net'
import { DataEmitterActions } from './actions/DataEmitterActions'
import { cached } from './util/cached'
@@ -9,6 +11,7 @@ export interface DataContextShellConfig {
// Used in places where we have to create a "shell" data context,
// for non-unified parts of the codebase
export class DataContextShell {
private _gqlServer?: Server
private _appServerPort: number | undefined
private _gqlServerPort: number | undefined
@@ -18,8 +21,9 @@ export class DataContextShell {
this._appServerPort = port
}
setGqlServerPort (port: number | undefined) {
this._gqlServerPort = port
setGqlServer (srv: Server) {
this._gqlServer = srv
this._gqlServerPort = (srv.address() as AddressInfo).port
}
get appServerPort () {
@@ -40,4 +44,8 @@ export class DataContextShell {
busApi: this.shellConfig.rootBus,
}
}
destroy () {
this._gqlServer?.close()
}
}

View File

@@ -1,5 +1,18 @@
import path from 'path'
import type { DataContext } from '..'
export class FileActions {
constructor (private ctx: DataContext) {}
async writeFileInProject (relativePath: string, data: any) {
if (!this.ctx.activeProject) {
throw new Error(`Cannot write file in project without active project`)
}
await this.ctx.fs.writeFile(
path.join(this.ctx.activeProject?.projectRoot, relativePath),
data,
)
}
}

View File

@@ -30,9 +30,14 @@ export class ProjectActions {
}
async clearActiveProject () {
this.ctx.appData.activeProject = null
await this.api.closeActiveProject()
return this.api.closeActiveProject()
// TODO(tim): Improve general state management w/ immutability (immer) & updater fn
this.ctx.coreData.app.isInGlobalMode = true
this.ctx.coreData.app.activeProject = null
this.ctx.coreData.app.activeTestingType = null
this.ctx.coreData.wizard.history = ['welcome']
this.ctx.coreData.wizard.currentStep = 'welcome'
}
private get projects () {

View File

@@ -0,0 +1,9 @@
import type { DataContext } from '..'
export class SettingsDataSource {
constructor (private ctx: DataContext) {}
readSettingsForProject (projectRoot: string) {
this.ctx.util.assertAbsolute(projectRoot)
}
}

View File

@@ -18,6 +18,12 @@ export class UtilDataSource {
return vals.map((v) => v.status === 'fulfilled' ? v.value : this.ensureError(v.reason))
}
assertAbsolute (val: string) {
if (!this.ctx.path.isAbsolute(val)) {
throw new Error(`Expected ${val} to be an absolute path`)
}
}
ensureError (val: any): Error {
return val instanceof Error ? val : new Error(val)
}

View File

@@ -6,6 +6,7 @@ export * from './BrowserDataSource'
export * from './FileDataSource'
export * from './GitDataSource'
export * from './ProjectDataSource'
export * from './SettingsDataSource'
export * from './StorybookDataSource'
export * from './UtilDataSource'
export * from './WizardDataSource'

View File

@@ -1,37 +1,92 @@
import type { DataContext } from '@packages/data-context'
import * as inspector from 'inspector'
import sinonChai from '@cypress/sinon-chai'
import sinon from 'sinon'
import rimraf from 'rimraf'
import util from 'util'
// require'd so we don't conflict with globals loaded in @packages/types
const chai = require('chai')
const chaiAsPromised = require('chai-as-promised')
const chaiSubset = require('chai-subset')
const { expect } = chai
chai.use(chaiAsPromised)
chai.use(chaiSubset)
chai.use(sinonChai)
import path from 'path'
import type { WithCtxInjected, WithCtxOptions } from './support/e2eSupport'
import { e2eProjectDirs } from './support/e2eProjectDirs'
export async function e2ePluginSetup (projectRoot: string, on: Cypress.PluginEvents, config: Cypress.PluginConfigOptions) {
process.env.CYPRESS_INTERNAL_E2E_TESTING_SELF = 'true'
// require'd so we don't import the types from @packages/server which would
// pollute strict type checking
const { runInternalServer } = require('@packages/server/lib/modes/internal-server')
const Fixtures = require('../../../server/test/support/helpers/fixtures')
const tmpDir = path.join(__dirname, '.projects')
process.env.CYPRESS_INTERNAL_E2E_TESTING_SELF = 'true'
const { serverPortPromise, ctx } = runInternalServer({
projectRoot,
}) as {ctx: DataContext, serverPortPromise: Promise<number>}
await util.promisify(rimraf)(tmpDir)
Fixtures.setTmpDir(tmpDir)
interface WithCtxObj {
fn: string
options: WithCtxOptions
activeTestId: string
}
let ctx: DataContext
let serverPortPromise: Promise<number>
let currentTestId: string | undefined
let testState: Record<string, any> = {}
on('task', {
async withCtx (fnString: string) {
await serverPortPromise
setupE2E (projectName: string) {
Fixtures.scaffoldProject(projectName)
return new Function('ctx', `return (${fnString})(ctx)`).call(undefined, ctx)
return null
},
async resetCtxState () {
return ctx.dispose()
},
async visitLaunchpad () {
async withCtx (obj: WithCtxObj) {
// Ensure we spin up a completely isolated server/state for each test
if (obj.activeTestId !== currentTestId) {
ctx?.destroy()
currentTestId = obj.activeTestId
testState = {};
({ serverPortPromise, ctx } = runInternalServer({
projectRoot: null,
}) as {ctx: DataContext, serverPortPromise: Promise<number>})
},
async visitApp () {
await serverPortPromise
}
},
getGraphQLPort () {
return serverPortPromise
},
getAppServerPort () {
return ctx.appServerPort ?? null
const options: WithCtxInjected = {
...obj.options,
testState,
require,
process,
projectDir (projectName) {
if (!e2eProjectDirs.includes(projectName)) {
throw new Error(`${projectName} is not a fixture project`)
}
return path.join(tmpDir, projectName)
},
}
const val = await Promise.resolve(new Function('ctx', 'options', 'chai', 'expect', 'sinon', `
return (${obj.fn})(ctx, options, chai, expect, sinon)
`).call(undefined, ctx, options, chai, expect, sinon))
return val || null
},
})
return config
return {
...config,
env: {
e2e_isDebugging: Boolean(inspector.url()),
},
}
}

View File

@@ -0,0 +1,87 @@
/* eslint-disable */
// Auto-generated by gulpE2ETestScaffold.ts
export const e2eProjectDirs = [
'browser-extensions',
'busted-support-file',
'chrome-browser-preferences',
'component-tests',
'config-with-custom-file-js',
'config-with-custom-file-ts',
'config-with-invalid-browser',
'config-with-invalid-viewport',
'config-with-js',
'config-with-short-timeout',
'config-with-ts',
'cookies',
'default-layout',
'downloads',
'e2e',
'empty-folders',
'failures',
'firefox-memory',
'fixture-subfolder-of-integration',
'folder-same-as-fixture',
'hooks-after-rerun',
'ids',
'integration-outside-project-root',
'issue-8111-iframe-input',
'max-listeners',
'multiple-task-registrations',
'no-scaffolding',
'no-server',
'non-existent-spec',
'non-proxied',
'odd-directory-name',
'plugin-after-screenshot',
'plugin-after-spec-deletes-video',
'plugin-before-browser-launch-deprecation',
'plugin-browser',
'plugin-config',
'plugin-config-version',
'plugin-empty',
'plugin-event-deprecated',
'plugin-extension',
'plugin-filter-browsers',
'plugin-retries',
'plugin-returns-bad-config',
'plugin-returns-empty-browsers-list',
'plugin-returns-invalid-browser',
'plugin-run-event-throws',
'plugin-run-events',
'plugin-validation-error',
'plugins-absolute-path',
'plugins-async-error',
'plugins-root-async-error',
'pristine',
'read-only-project-root',
'record',
'remote-debugging-disconnect',
'remote-debugging-port-removed',
'retries-2',
'same-fixtures-integration-folders',
'screen-size',
'server',
'shadow-dom-global-inclusion',
'studio',
'studio-no-source-maps',
'system-node',
'task-not-registered',
'todos',
'ts-installed',
'ts-proj',
'ts-proj-custom-names',
'ts-proj-esmoduleinterop-true',
'ts-proj-tsconfig-in-plugins',
'ts-proj-with-module-esnext',
'ts-proj-with-paths',
'uncaught-support-file',
'unify-onboarding',
'unify-plugin-errors',
'various-file-types',
'webpack-preprocessor',
'webpack-preprocessor-awesome-typescript-loader',
'webpack-preprocessor-ts-loader',
'webpack-preprocessor-ts-loader-compiler-options',
'working-preprocessor',
'yarn-v2-pnp'
] as const

View File

@@ -1,17 +1,125 @@
import '@testing-library/cypress/add-commands'
import type { DataContext } from '@packages/data-context'
import { e2eProjectDirs } from './e2eProjectDirs'
const SIXTY_SECONDS = 60 * 1000
const NO_TIMEOUT = 1000 * 1000
const FOUR_SECONDS = 4 * 1000
export type ProjectFixture = typeof e2eProjectDirs[number]
export interface WithCtxOptions extends Cypress.Loggable, Cypress.Timeoutable {
projectName?: ProjectFixture
[key: string]: any
}
export interface WithCtxInjected extends WithCtxOptions {
require: typeof require
process: typeof process
testState: Record<string, any>
projectDir(projectName: ProjectFixture): string
}
declare global {
namespace Cypress {
interface Chainable {
withCtx(fn: (ctx: DataContext) => any): Chainable
/**
* Calls a function block with the "ctx" object from the server,
* and an object containing any options passed into the server context
* and some helper properties:
*
*
* You cannot access any variables outside of the function scope,
* however we do provide expect, chai, sinon
*/
withCtx: typeof withCtx
/**
* Takes the name of a "system" test directory, and mounts the project within open mode
*/
setupE2E: typeof setupE2E
initializeApp: typeof initializeApp
visitApp(href?: string): Chainable<string>
visitLaunchpad(href?: string): Chainable<string>
}
}
}
Cypress.Commands.add('withCtx', (fn) => {
const _log = Cypress.log({
beforeEach(() => {
// Reset the ports so we know we need to call "setupE2E" before each test
Cypress.env('e2e_serverPort', undefined)
Cypress.env('e2e_gqlPort', undefined)
})
// function setup
function setupE2E (projectName?: ProjectFixture) {
if (projectName && !e2eProjectDirs.includes(projectName)) {
throw new Error(`Unknown project ${projectName}`)
}
if (projectName) {
cy.task('setupE2E', projectName, { log: false })
}
return cy.withCtx(async (ctx, o) => {
if (o.projectName) {
await ctx.actions.project.setActiveProject(o.projectDir(o.projectName))
}
return [
ctx.gqlServerPort,
ctx.appServerPort,
]
}, { projectName, log: false }).then(([gqlPort, serverPort]) => {
Cypress.env('e2e_gqlPort', gqlPort)
Cypress.env('e2e_serverPort', serverPort)
})
}
function initializeApp (mode: 'component' | 'e2e' = 'e2e') {
return cy.withCtx(async (ctx, o) => {
ctx.actions.wizard.setTestingType(o.mode)
await ctx.actions.project.initializeActiveProject({
skipPluginIntializeForTesting: true,
})
await ctx.actions.project.launchProject(o.mode, {
skipBrowserOpenForTest: true,
})
return ctx.appServerPort
}, { log: false, mode }).then((serverPort) => {
Cypress.env('e2e_serverPort', serverPort)
})
}
function visitApp () {
const { e2e_serverPort, e2e_gqlPort } = Cypress.env()
if (!e2e_gqlPort) {
throw new Error(`Missing gqlPort - did you forget to call cy.setupE2E(...) ?`)
}
if (!e2e_serverPort) {
throw new Error(`Missing serverPort - did you forget to call cy.initializeApp(...) ?`)
}
return cy.visit(`dist-app/index.html?gqlPort=${e2e_gqlPort}&serverPort=${e2e_serverPort}`)
}
function visitLaunchpad (hash?: string) {
const { e2e_gqlPort } = Cypress.env()
if (!e2e_gqlPort) {
throw new Error(`Missing gqlPort - did you forget to call cy.setupE2E(...) ?`)
}
cy.visit(`dist-launchpad/index.html?gqlPort=${e2e_gqlPort}`)
}
const pageLoadId = `uid${Math.random()}`
function withCtx<T extends Partial<WithCtxOptions>> (fn: (ctx: DataContext, o: T & WithCtxInjected) => any, opts: T = {} as T): Cypress.Chainable {
const _log = opts.log === false ? { end () {} } : Cypress.log({
name: 'withCtx',
message: '(view in console)',
consoleProps () {
@@ -21,7 +129,20 @@ Cypress.Commands.add('withCtx', (fn) => {
},
})
cy.task('withCtx', fn.toString(), { timeout: SIXTY_SECONDS, log: false }).then(() => {
const { log, timeout, ...rest } = opts
return cy.task('withCtx', {
fn: fn.toString(),
options: rest,
// @ts-expect-error
activeTestId: `${pageLoadId}-${Cypress.mocha.getRunner().test.id ?? Cypress.currentTest.title}`,
}, { timeout: timeout ?? Cypress.env('e2e_isDebugging') ? NO_TIMEOUT : FOUR_SECONDS, log }).then(() => {
_log.end()
})
})
}
Cypress.Commands.add('visitApp', visitApp)
Cypress.Commands.add('visitLaunchpad', visitLaunchpad)
Cypress.Commands.add('initializeApp', initializeApp)
Cypress.Commands.add('setupE2E', setupE2E)
Cypress.Commands.add('withCtx', withCtx)

View File

@@ -12,6 +12,7 @@ import { client } from '@packages/socket/lib/browser'
import { cacheExchange as graphcacheExchange } from '@urql/exchange-graphcache'
import { pubSubExchange } from './urqlExchangePubsub'
import { namedRouteExchange } from './urqlExchangeNamedRoute'
const GQL_PORT_MATCH = /gqlPort=(\d+)/.exec(window.location.search)
const SERVER_PORT_MATCH = /serverPort=(\d+)/.exec(window.location.search)
@@ -69,6 +70,7 @@ export function makeUrqlClient (target: 'launchpad' | 'app'): Client {
}),
// https://formidable.com/open-source/urql/docs/graphcache/errors/
makeCacheExchange(),
namedRouteExchange,
// TODO(tim): add this when we want to use the socket as the GraphQL
// transport layer for all operations
// target === 'launchpad' ? fetchExchange : socketExchange(io),

View File

@@ -0,0 +1,19 @@
import { Exchange, getOperationName } from '@urql/core'
import { map, pipe } from 'wonka'
export const namedRouteExchange: Exchange = ({ client, forward }) => {
return (ops$) => {
return forward(pipe(
ops$,
map((o) => {
return {
...o,
context: {
...o.context,
url: `${o.context.url}/${o.kind}-${getOperationName(o.query)}`,
},
}
}),
))
}
}

View File

@@ -56,8 +56,8 @@ export const mutation = mutationType({
t.nonNull.field('clearActiveProject', {
type: 'Query',
resolve: (root, args, ctx) => {
ctx.actions.project.clearActiveProject()
resolve: async (root, args, ctx) => {
await ctx.actions.project.clearActiveProject()
return {}
},

View File

@@ -78,4 +78,8 @@ export const Project = objectType({
resolve: (source, args, ctx) => ctx.storybook.loadStorybookInfo(),
})
},
sourceType: {
module: __dirname,
export: 'ProjectShape',
},
})

View File

@@ -9,7 +9,7 @@ import { parse } from 'graphql'
const SHOW_GRAPHIQL = process.env.CYPRESS_INTERNAL_ENV !== 'production'
export function addGraphQLHTTP (app: ReturnType<typeof express>, context: DataContext) {
app.use('/graphql', graphqlHTTP((req, res, params) => {
app.use('/graphql/:operationName?', graphqlHTTP((req, res, params) => {
const ctx = SHOW_GRAPHIQL ? maybeProxyContext(params, context) : context
return {

View File

@@ -1,4 +1,5 @@
{
"$schema": "../../cli/schema/cypress.schema.json",
"projectId": "sehy69",
"viewportWidth": 800,
"viewportHeight": 850,
@@ -19,6 +20,8 @@
"pluginsFile": "cypress/component/plugins/index.js"
},
"e2e": {
"supportFile": false
"supportFile": "cypress/e2e/support/e2eSupport.ts",
"integrationFolder": "cypress/e2e/integration",
"pluginsFile": "cypress/e2e/plugins/index.ts"
}
}

View File

@@ -0,0 +1,21 @@
describe('Onboarding Flow', () => {
beforeEach(() => {
cy.setupE2E('unify-onboarding')
})
it('can scaffold a project in e2e mode', () => {
cy.visitLaunchpad()
cy.get('[data-cy-testingType=component]').click()
cy.get('[data-cy=select-framework]').click()
cy.get('[data-cy-framework=vue]').click()
cy.get('[data-cy=select-framework]').should('contain', 'Vue')
cy.get('[data-cy=select-bundler]').click()
cy.get('[data-cy-bundler=webpack]').click()
cy.get('[data-cy=select-bundler]').should('contain', 'Webpack')
cy.reload()
cy.get('[data-cy=select-framework]').should('contain', 'Vue')
cy.get('[data-cy=select-bundler]').should('contain', 'Webpack')
cy.findByText('Next Step').click()
cy.get('h1').should('contain', 'Dependencies')
})
})

View File

@@ -0,0 +1,20 @@
describe('Launchpad: Open Mode', () => {
beforeEach(() => {
cy.setupE2E()
cy.visitLaunchpad()
})
it('Shows the open page', () => {
cy.get('h1').should('contain', 'Cypress')
})
it('allows adding a project', () => {
cy.withCtx(async (ctx, o) => {
await ctx.actions.project.setActiveProject(o.projectDir('todos'))
ctx.emitter.toLaunchpad()
})
cy.get('h1').should('contain', 'Welcome to Cypress')
cy.findByText('Choose which method of testing you would like to set up first.')
})
})

View File

@@ -0,0 +1,16 @@
describe('Plugin error handling', () => {
it('it handles a plugin error', () => {
cy.setupE2E('unify-plugin-errors')
cy.visitLaunchpad()
// TODO(alejandro): use this to test against error flow
cy.get('[data-cy-testingType=e2e]').click()
cy.wait(2000)
cy.withCtx((ctx) => {
ctx.actions.file.writeFileInProject('cypress/plugins/index.js', `module.exports = (on, config) => {}`)
})
cy.reload()
cy.wait(2000)
})
})

View File

@@ -1,5 +1,5 @@
/// <reference types="cypress" />
const { monorepoPaths } = require('../../../../scripts/gulp/monorepoPaths')
const { monorepoPaths } = require('../../../../../scripts/gulp/monorepoPaths')
import { e2ePluginSetup } from '@packages/frontend-shared/cypress/e2e/e2ePluginSetup'
// ***********************************************************

View File

@@ -0,0 +1,2 @@
/// <reference path="../../../../frontend-shared/cypress/e2e/support/e2eSupport.ts" />
require('../../../../frontend-shared/cypress/e2e/support/e2eSupport')

View File

@@ -1,14 +0,0 @@
let GQL_PORT
describe('Launchpad', () => {
before(() => {
cy.task('getGraphQLPort').then((port) => {
GQL_PORT = port
})
})
it('resolves the home page', () => {
cy.visit(`dist/index.html?gqlPort=${GQL_PORT}`)
cy.get('h1').should('contain', 'Welcome')
})
})

View File

@@ -10,9 +10,9 @@
"test": "yarn cypress:run:ct && yarn types",
"windi": "yarn windicss-analysis",
"cypress:launch": "cross-env TZ=America/New_York node ../../scripts/cypress open --project ${PWD}",
"cypress:open": "yarn gulp cyOpenLaunchpadE2E",
"cypress:open": "cross-env TZ=America/New_York node ../../scripts/cypress open --project ${PWD}",
"cypress:run:ct": "cross-env TZ=America/New_York node ../../scripts/cypress run-ct --project ${PWD}",
"cypress:run:e2e": "yarn gulp cyRunLaunchpadE2E",
"cypress:run:e2e": "cross-env TZ=America/New_York node ../../scripts/cypress run --project ${PWD}",
"dev": "yarn gulp dev --project ${PWD}",
"start": "echo 'run yarn dev from the root' && exit 1",
"watch": "echo 'run yarn dev from the root' && exit 1"

View File

@@ -21,6 +21,7 @@
w-full
focus:border-indigo-600 focus:outline-transparent
"
data-cy="select-bundler"
:class="disabledClass
+ (isOpen ? ' border-indigo-600' : ' border-gray-200')
+ (props.disabled ? ' bg-gray-100 text-gray-800' : '')"
@@ -68,6 +69,7 @@
:key="opt.type"
focus="1"
class="cursor-pointer flex items-center py-1 px-2 hover:bg-gray-10"
:data-cy-bundler="opt.type"
@click="selectOption(opt.type)"
>
<img

View File

@@ -47,6 +47,7 @@
:key="opt.id"
focus="1"
class="cursor-pointer flex items-center py-1 px-2 hover:bg-gray-10"
:data-cy-framework="opt.type"
@click="selectOption(opt.type)"
>
<img

View File

@@ -3,6 +3,7 @@
<TestingTypeCard
v-if="ct"
:id="ct.type"
:data-cy-testingType="ct.type"
:title="ct.title"
:description="firstTimeCT ? ct.description : 'LAUNCH'"
:configured="!firstTimeCT"
@@ -15,6 +16,7 @@
<TestingTypeCard
v-if="e2e"
:id="e2e.type"
:data-cy-testingType="e2e.type"
:title="e2e.title"
:description="firstTimeE2E ? e2e.description : 'LAUNCH'"
:configured="!firstTimeE2E"

View File

@@ -4,31 +4,8 @@ import type { AddressInfo } from 'net'
import type { DataContext } from '@packages/data-context'
import pDefer from 'p-defer'
import cors from 'cors'
import type { Server } from 'http'
import { SocketIOServer } from '@packages/socket'
let graphqlServer: Server | undefined
export async function closeGraphQLServer () {
if (!graphqlServer) {
return
}
const dfd = pDefer()
graphqlServer.close((err) => {
if (err) {
dfd.reject()
}
dfd.resolve()
})
graphqlServer = undefined
dfd.promise
}
export async function makeGraphQLServer (ctx: DataContext) {
const dfd = pDefer<number>()
const app = express()
@@ -39,7 +16,7 @@ export async function makeGraphQLServer (ctx: DataContext) {
// it's not jammed into the projects
addGraphQLHTTP(app, ctx)
const srv = graphqlServer = app.listen(() => {
const srv = app.listen(() => {
const port = (srv.address() as AddressInfo).port
const endpoint = `http://localhost:${port}/graphql`
@@ -51,6 +28,8 @@ export async function makeGraphQLServer (ctx: DataContext) {
ctx.debug(`GraphQL Server at ${endpoint}`)
ctx.setGqlServer(srv)
dfd.resolve(port)
})

View File

@@ -23,9 +23,5 @@ export function runInternalServer (options) {
const serverPortPromise = makeGraphQLServer(ctx)
serverPortPromise.then((port) => {
ctx.setGqlServerPort(port)
})
return { ctx, bus, serverPortPromise }
}

View File

@@ -16,7 +16,7 @@ import type { LaunchOpts, LaunchArgs, OpenProjectLaunchOptions, FoundBrowser } f
import { fs } from './util/fs'
import path from 'path'
import os from 'os'
import { closeGraphQLServer } from './gui/makeGraphQLServer'
import type { DataContextShell } from '@packages/data-context/src/DataContextShell'
const debug = Debug('cypress:server:open_project')
@@ -408,12 +408,15 @@ export class OpenProject {
this.stopSpecsWatcher()
return Promise.all([
closeGraphQLServer(),
this._ctx?.destroy(),
this.closeOpenProjectAndBrowsers(),
]).then(() => null)
}
_ctx?: DataContextShell
async create (path: string, args: LaunchArgs, options: OpenProjectLaunchOptions, browsers: FoundBrowser[] = []) {
this._ctx = options.ctx
debug('open_project create %s', path)
_.defaults(options, {

View File

@@ -94,10 +94,6 @@ module.exports = {
// allow overriding the app_data folder
let folder = env.CYPRESS_KONFIG_ENV || env.CYPRESS_INTERNAL_ENV
if (env.CYPRESS_INTERNAL_E2E_TESTING_SELF) {
folder = `${folder}-e2e-test`
}
const p = path.join(ELECTRON_APP_DATA_PATH, 'cy', folder, ...paths)
log('path: %s', p)

View File

@@ -0,0 +1,3 @@
describe('unify-plugin-errors', () => {
it('ok', () => {})
})

View File

@@ -0,0 +1,5 @@
module.exports = async function () {
await new Promise((resolve) => setTimeout(resolve, 1000))
throw new Error('Error Loading Plugin!!!')
}

View File

@@ -4,7 +4,7 @@ const chokidar = require('chokidar')
const root = path.join(__dirname, '..', '..', '..')
const projects = path.join(root, 'test', 'support', 'fixtures', 'projects')
const tmpDir = path.join(root, '.projects')
let tmpDir = path.join(root, '.projects')
// copy contents instead of deleting+creating new file, which can cause
// filewatchers to lose track of toFile.
@@ -22,6 +22,14 @@ const copyContents = (fromFile, toFile) => {
}
module.exports = {
setTmpDir (dir) {
tmpDir = dir
},
scaffoldProject (project) {
return fs.copySync(path.join(projects, project), path.join(tmpDir, project))
},
// copies all of the project fixtures
// to the tmpDir .projects in the root
scaffold () {

View File

@@ -11,7 +11,7 @@ import gulp from 'gulp'
import { autobarrelWatcher } from './tasks/gulpAutobarrel'
import { startCypressWatch, openCypressLaunchpad, openCypressApp, runCypressLaunchpad, wrapRunWithExit, runCypressApp, killExistingCypress } from './tasks/gulpCypress'
import { graphqlCodegen, graphqlCodegenWatch, nexusCodegen, nexusCodegenWatch, generateFrontendSchema, syncRemoteGraphQL } from './tasks/gulpGraphql'
import { viteApp, viteCleanApp, viteCleanLaunchpad, viteLaunchpad, viteBuildApp, viteBuildAndWatchApp, viteBuildLaunchpad, viteBuildAndWatchLaunchpad } from './tasks/gulpVite'
import { viteApp, viteCleanApp, viteCleanLaunchpad, viteLaunchpad, viteBuildApp, viteBuildAndWatchApp, viteBuildLaunchpad, viteBuildAndWatchLaunchpad, symlinkViteProjects } from './tasks/gulpVite'
import { checkTs } from './tasks/gulpTsc'
import { makePathMap } from './utils/makePathMap'
import { makePackage } from './tasks/gulpMakePackage'
@@ -19,6 +19,7 @@ import { setGulpGlobal } from './gulpConstants'
import { exitAfterAll } from './tasks/gulpRegistry'
import { execSync } from 'child_process'
import { webpackRunner } from './tasks/gulpWebpack'
import { e2eTestScaffold, e2eTestScaffoldWatch } from './tasks/gulpE2ETestScaffold'
/**------------------------------------------------------------------------
* Local Development Workflow
@@ -46,6 +47,9 @@ gulp.task(
// ... and generate the correct GraphQL types for the frontend
graphqlCodegenWatch,
// ... and generate the patsh for the e2e support file watching
e2eTestScaffoldWatch,
),
)
@@ -71,6 +75,7 @@ gulp.task(
viteApp,
viteLaunchpad,
webpackRunner,
e2eTestScaffoldWatch,
),
// And we're finally ready for electron, watching for changes in
@@ -123,6 +128,7 @@ gulp.task('buildProd',
viteBuildApp,
viteBuildLaunchpad,
),
symlinkViteProjects,
))
gulp.task(
@@ -133,6 +139,19 @@ gulp.task(
),
)
gulp.task('watchForE2E', gulp.series(
'codegen',
gulp.parallel(
gulp.series(
viteBuildAndWatchLaunchpad,
viteBuildAndWatchApp,
),
webpackRunner,
),
symlinkViteProjects,
e2eTestScaffold,
))
/**------------------------------------------------------------------------
* Launchpad Testing
* This task builds and hosts the launchpad as if it was a static website.
@@ -176,10 +195,12 @@ const cyOpenLaunchpad = gulp.series(
// This watches for changes and is not the same things as statically
// building the app for production.
gulp.parallel(
viteBuildApp,
viteBuildAndWatchLaunchpad,
viteBuildApp,
),
symlinkViteProjects,
// 2. Start the REAL (dev) Cypress App, which will launch in open mode.
openCypressLaunchpad,
)
@@ -196,6 +217,8 @@ const cyOpenApp = gulp.series(
webpackRunner,
),
symlinkViteProjects,
// 2. Start the REAL (dev) Cypress App, which will launch in open mode.
openCypressApp,
)
@@ -242,6 +265,7 @@ gulp.task(makePackage)
* here for debugging, e.g. `yarn gulp syncRemoteGraphQL`
*------------------------------------------------------------------------**/
gulp.task(symlinkViteProjects)
gulp.task(syncRemoteGraphQL)
gulp.task(generateFrontendSchema)
gulp.task(makePathMap)
@@ -265,6 +289,8 @@ gulp.task('debugCypressLaunchpad', gulp.series(
openCypressLaunchpad,
))
gulp.task(e2eTestScaffoldWatch)
gulp.task(e2eTestScaffold)
gulp.task(startCypressWatch)
gulp.task(openCypressApp)
gulp.task(openCypressLaunchpad)

View File

@@ -0,0 +1,52 @@
import chokidar from 'chokidar'
import path from 'path'
import fs from 'fs-extra'
import { monorepoPaths } from '../monorepoPaths'
const PROJECT_FIXTURE_DIRECTORY = 'test/support/fixtures/projects'
const DIR_PATH = path.join(monorepoPaths.pkgServer, PROJECT_FIXTURE_DIRECTORY)
const OUTPUT_PATH = path.join(monorepoPaths.pkgFrontendShared, 'cypress/e2e/support/e2eProjectDirs.ts')
export async function e2eTestScaffold () {
const possibleDirectories = await fs.readdir(DIR_PATH)
const dirs = await Promise.all(possibleDirectories.map(async (dir) => {
const fullPath = path.join(DIR_PATH, dir)
const stat = await fs.stat(fullPath)
if (stat.isDirectory()) {
return fullPath
}
}))
const allDirs = dirs.filter((dir) => dir) as string[]
await fs.writeFile(
OUTPUT_PATH,
`/* eslint-disable */
// Auto-generated by ${path.basename(__filename)}
export const e2eProjectDirs = [
${allDirs
.map((dir) => ` '${path.basename(dir)}'`).join(',\n')}
] as const
`,
)
return allDirs
}
export async function e2eTestScaffoldWatch () {
const fixtureWatcher = chokidar.watch(PROJECT_FIXTURE_DIRECTORY, {
cwd: monorepoPaths.pkgServer,
// ignoreInitial: true,
depth: 0,
})
fixtureWatcher.on('unlinkDir', () => {
e2eTestScaffold()
})
fixtureWatcher.on('addDir', () => {
e2eTestScaffold()
})
}

View File

@@ -72,6 +72,27 @@ function spawnViteDevServer (
* * viteBuildLaunchpad
*------------------------------------------------------------------------**/
export async function symlinkViteProjects () {
await Promise.all([
spawned('cmd-symlink', 'ln -s ../app/dist dist-app', {
cwd: monorepoPaths.pkgLaunchpad,
waitForExit: true,
}).catch((e) => {}),
spawned('cmd-symlink', 'ln -s dist dist-app', {
cwd: monorepoPaths.pkgApp,
waitForExit: true,
}).catch((e) => {}),
spawned('cmd-symlink', 'ln -s dist dist-launchpad', {
cwd: monorepoPaths.pkgLaunchpad,
waitForExit: true,
}).catch((e) => {}),
spawned('cmd-symlink', 'ln -s ../launchpad/dist dist-launchpad', {
cwd: monorepoPaths.pkgApp,
waitForExit: true,
}).catch((e) => {}),
])
}
export function viteBuildApp () {
return spawned('vite:build-app', `yarn vite build`, {
cwd: monorepoPaths.pkgApp,

View File

@@ -7,6 +7,7 @@ import { prefixLog, prefixStream } from './prefixStream'
import { addChildProcess } from '../tasks/gulpRegistry'
export type AllSpawnableApps =
| `cmd-${string}`
| `vite-${string}`
| `vite:build-${string}`
| `serve:${string}`

View File

@@ -9045,6 +9045,14 @@
resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d"
integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==
"@types/rimraf@^3.0.2":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@types/rimraf/-/rimraf-3.0.2.tgz#a63d175b331748e5220ad48c901d7bbf1f44eef8"
integrity sha512-F3OznnSLAUxFrCEu/L5PY8+ny8DtcFRjx7fZZ9bycvXRi3KPTRS9HOitGZwvPg0juRhXFWIeKX58cnX5YqLohQ==
dependencies:
"@types/glob" "*"
"@types/node" "*"
"@types/semver@7.3.4":
version "7.3.4"
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.4.tgz#43d7168fec6fa0988bb1a513a697b29296721afb"