mirror of
https://github.com/cypress-io/cypress.git
synced 2026-01-24 07:59:03 -06:00
feat(app): render spec list, command log, iframe (#18372)
* wip * wip * wip * pre-bundle deps * wip * wip: spec * clear iframe * wip: it kind of works * wip, docs * wip * update runner API * wip * remove comments * do not render header * revert some stuff * remove test * update tests to work with react 17 * types * wip * revert react deps * move code around * add back hash change code * lint * comment * update readme * remove commet * revert yarn lock * remove unused file * Delete HelloWorld1.spec.tsx * Delete HelloWorld2.spec.tsx * update types * fix ui * fix bugs * fix test * remove dead code * more dead code Co-authored-by: Jessica Sachs <jess@jessicasachs.io>
This commit is contained in:
@@ -4,11 +4,25 @@ This is the front-end for the Cypress App.
|
||||
|
||||
## Development
|
||||
|
||||
1. Use existing project to get a server (for example `cd packages/runner-ct && yarn cypress:open`)
|
||||
2. It will open in a new browser on port 8080
|
||||
3. Do `yarn start`. It will start the front-end for the new Cypress app
|
||||
4. To back to the browser opened in step 2
|
||||
5. Visit http://localhost:8080/__vite__/ for the new front-end powered by Vite (currently running the RunnerCt)
|
||||
1. `yarn dev` (inside of `packages/app`)
|
||||
2. It will open launchpad
|
||||
3. Select Component Testing (current E2E is not fully working)
|
||||
3. Open chrome (or another browser)
|
||||
4. This launches the existing CT Runner. Change the URL to http://localhost:3000/__vite__/ (note the trailing `/`)
|
||||
5. It should show the new Vite powered app
|
||||
|
||||
## Using existing, Vite-incompatible modules
|
||||
|
||||
Some of our modules, like `@packages/reporter`, `@packages/driver` and `@packages/runner-shared` cannot be easily
|
||||
used with Vite due to circular dependencies and modules that do not have compatible ESM builds.
|
||||
|
||||
To work around this, when consuming existing code, it is bundled with webpack and made available under the
|
||||
`window.UnifiedRunner` namespace. It is injected via [`injectBundle`](./src/runner/injectBundle.ts).
|
||||
|
||||
To add more code to the bundle, add it in the bundle root, `@packages/runner-ct/src/main.tsx` and attach it to
|
||||
`window.UnifiedRunner`.
|
||||
|
||||
As a rule of thumb, avoid importing from the older, webpack based modules into this package. Instead, if you want to consume code from those older, webpack bundled modules, you should add them to the webpack root and consume them via `window.UnifiedRunner`. Ideally, update [`index.d.ts`](./index.d.ts) to add the types, as well.
|
||||
|
||||
### Icons
|
||||
|
||||
@@ -63,3 +77,4 @@ You can see this in the video with the Settings cog. It uses and `evenodd` fill
|
||||
## Diagram
|
||||
|
||||
]
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
describe('App', () => {
|
||||
it('resolves the home page', () => {
|
||||
cy.visit('http://localhost:5556')
|
||||
cy.get('[href="#/runs"]').click()
|
||||
cy.get('[href="#/settings"]').click()
|
||||
cy.get('[href="/__vite__/runner"]').click()
|
||||
cy.get('[href="/__vite__/settings"]').click()
|
||||
})
|
||||
})
|
||||
|
||||
78
packages/app/index.d.ts
vendored
Normal file
78
packages/app/index.d.ts
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
import type { Store } from './src/store'
|
||||
|
||||
interface ConnectionInfo {
|
||||
automationElement: '__cypress-string',
|
||||
randomString: string
|
||||
}
|
||||
|
||||
export {}
|
||||
|
||||
/**
|
||||
* The eventManager and driver are bundled separately
|
||||
* by webpack. We cannot import them because of
|
||||
* circular dependencies.
|
||||
* To work around this, we build the driver, eventManager
|
||||
* and some other dependencies using webpack, and consumed the dist'd
|
||||
* source code.
|
||||
*
|
||||
* This is attached to `window` under the `UnifiedRunner` namespace.
|
||||
*
|
||||
* For now, just declare the types that we need to give us type safety where possible.
|
||||
* Eventually, we should decouple the event manager and import it directly.
|
||||
*/
|
||||
declare global {
|
||||
interface Window {
|
||||
UnifiedRunner: {
|
||||
/**
|
||||
* decode config, which we receive as a base64 string
|
||||
* This comes from Driver.utils
|
||||
*/
|
||||
decodeBase64: (base64: string) => Record<string, unknown>
|
||||
|
||||
/**
|
||||
* Proxy event to the reporter via `Reporter.defaultEvents.emit`
|
||||
*/
|
||||
emit (evt: string, ...args: unknown[]): void
|
||||
|
||||
/**
|
||||
* This is the eventManager which orchestrates all the communication
|
||||
* between the reporter, driver, and server, as well as handle
|
||||
* setup, teardown and running of specs.
|
||||
*
|
||||
* It's only used on the "Runner" part of the unified runner.
|
||||
*/
|
||||
eventManager: {
|
||||
addGlobalListeners: (state: Store, connectionInfo: ConnectionInfo) => void
|
||||
setup: (config: Record<string, unknown>) => void
|
||||
initialize: ($autIframe: JQuery<HTMLIFrameElement>, config: Record<string, unknown>) => void
|
||||
teardown: (state: Store) => Promise<void>
|
||||
teardownReporter: () => Promise<void>
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/**
|
||||
* To ensure we are only a single copy of React
|
||||
* We get a reference to the copy of React (and React DOM)
|
||||
* that is used in the Reporter and Driver, which are bundled with
|
||||
* webpack.
|
||||
*
|
||||
* Unfortunately, attempting to have React in a project
|
||||
* using Vue causes mad conflicts because React'S JSX type
|
||||
* is ambient, so we cannot actually type it.
|
||||
*/
|
||||
React: any
|
||||
ReactDOM: any
|
||||
|
||||
/**
|
||||
* Any React components or general code needed from
|
||||
* runner-shared, reporter or driver are also bundled with
|
||||
* webpack and made available via the window.UnifedRunner namespace.
|
||||
*
|
||||
* We cannot import the correct types, because this causes the linter and type
|
||||
* checker to run on runner-shared and reporter, and it blows up.
|
||||
*/
|
||||
AutIframe: any
|
||||
Reporter: any
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,12 @@
|
||||
<template>
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive>
|
||||
<component :is="Component" />
|
||||
</keep-alive>
|
||||
<!--
|
||||
TODO(lachlan): put this back after doing proper cleanup when unmounting the runner page
|
||||
keep-alive works fine for Vue but has some weird effects on the React based Reporter
|
||||
For now it's way more simple to just unmount and remount the components when changing page.
|
||||
-->
|
||||
<!-- <keep-alive> -->
|
||||
<component :is="Component" />
|
||||
<!-- </keep-alive> -->
|
||||
</router-view>
|
||||
</template>
|
||||
|
||||
@@ -16,11 +16,11 @@
|
||||
name="fade"
|
||||
mode="out-in"
|
||||
>
|
||||
<keep-alive>
|
||||
<component
|
||||
:is="Component"
|
||||
/>
|
||||
</keep-alive>
|
||||
<!-- <keep-alive> -->
|
||||
<component
|
||||
:is="Component"
|
||||
/>
|
||||
<!-- </keep-alive> -->
|
||||
</transition>
|
||||
</router-view>
|
||||
</section>
|
||||
|
||||
@@ -16,15 +16,13 @@
|
||||
>
|
||||
<router-link
|
||||
v-for="item in navigation"
|
||||
v-slot="{ href, isActive }"
|
||||
v-slot="{ isActive }"
|
||||
:key="item.name"
|
||||
custom
|
||||
:to="item.href"
|
||||
>
|
||||
<SidebarNavigationRow
|
||||
:active="isActive"
|
||||
:icon="item.icon"
|
||||
:href="href"
|
||||
>
|
||||
{{ item.name }}
|
||||
</SidebarNavigationRow>
|
||||
@@ -43,7 +41,7 @@ import SettingsIcon from '~icons/cy/settings_x24'
|
||||
|
||||
const navigation = [
|
||||
{ name: 'Specs', icon: SpecsIcon, href: '/' },
|
||||
{ name: 'Runs', icon: CodeIcon, href: '/runs' },
|
||||
{ name: 'Runs', icon: CodeIcon, href: '/runner' },
|
||||
{ name: 'Settings', icon: SettingsIcon, href: '/settings' },
|
||||
]
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<template>
|
||||
<a
|
||||
href="#"
|
||||
<div
|
||||
:class="[active ? 'before:bg-jade-300' : 'before:bg-transparent']"
|
||||
class="w-full
|
||||
min-w-40px
|
||||
@@ -43,16 +42,16 @@
|
||||
<slot />
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FunctionalComponent, SVGAttributes } from 'vue'
|
||||
|
||||
withDefaults(defineProps <{
|
||||
icon?: FunctionalComponent<SVGAttributes, {}>
|
||||
icon: FunctionalComponent<SVGAttributes, {}>
|
||||
// Currently active row (generally the current route)
|
||||
active?: boolean
|
||||
active: boolean
|
||||
}>(), {
|
||||
active: false,
|
||||
icon: undefined,
|
||||
|
||||
@@ -1,9 +1,27 @@
|
||||
<template>
|
||||
<Specs />
|
||||
<div>
|
||||
<div v-if="query.fetching.value">
|
||||
Loading...
|
||||
</div>
|
||||
<div v-else-if="query.data.value?.app?.activeProject">
|
||||
<Specs :gql="query.data.value.app" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { gql, useQuery } from '@urql/vue'
|
||||
import Specs from './Specs.vue'
|
||||
import { IndexDocument } from '../generated/graphql'
|
||||
|
||||
gql`
|
||||
query Index {
|
||||
app {
|
||||
...Specs_Specs
|
||||
}
|
||||
}`
|
||||
|
||||
const query = useQuery({ query: IndexDocument })
|
||||
</script>
|
||||
|
||||
<route>
|
||||
|
||||
@@ -1,33 +1,49 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2>Runs Page</h2>
|
||||
<h4 v-if="!ready">
|
||||
Loading
|
||||
</h4>
|
||||
<h4 v-else>
|
||||
Error: TODO add gql backend for getting the base64Config into the browser
|
||||
</h4>
|
||||
<h2>Runner Page</h2>
|
||||
|
||||
<div v-once>
|
||||
<div :id="RUNNER_ID" />
|
||||
<div :id="REPORTER_ID" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { renderRunner } from '../../runner/renderRunner'
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from 'vue'
|
||||
import type { SpecFile } from '@packages/types/src'
|
||||
import { UnifiedRunnerAPI } from '../../runner'
|
||||
import { REPORTER_ID, RUNNER_ID } from '../../runner/utils'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const ready = ref(false)
|
||||
|
||||
// @ts-ignore
|
||||
window.__Cypress__ = true
|
||||
|
||||
renderRunner(() => {
|
||||
setTimeout(function () {
|
||||
// @ts-ignore
|
||||
window.Runner.start(document.getElementById('app'), '{{base64Config | safe}}')
|
||||
}, 0)
|
||||
|
||||
ready.value = true
|
||||
onMounted(() => {
|
||||
UnifiedRunnerAPI.initialize(executeSpec)
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
function executeSpec () {
|
||||
const absolute = route.hash.slice(1)
|
||||
|
||||
if (absolute) {
|
||||
// @ts-ignore
|
||||
execute({
|
||||
absolute,
|
||||
relative: `src/Basic.spec.tsx`,
|
||||
name: `Basic.spec.tsx`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const execute = (spec?: SpecFile) => {
|
||||
if (!spec) {
|
||||
return
|
||||
}
|
||||
|
||||
UnifiedRunnerAPI.executeSpec(spec)
|
||||
}
|
||||
</script>
|
||||
|
||||
<route>
|
||||
{
|
||||
name: "Runner"
|
||||
|
||||
@@ -1,36 +1,42 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2>Specs Page</h2>
|
||||
<template v-if="query.data.value?.app">
|
||||
<SpecsList :gql="query.data.value?.app" />
|
||||
</template>
|
||||
<p v-else>
|
||||
Loading...
|
||||
</p>
|
||||
</div>
|
||||
<h2>Specs Page</h2>
|
||||
|
||||
<SpecsList
|
||||
v-if="props.gql"
|
||||
:gql="props?.gql"
|
||||
/>
|
||||
|
||||
<p v-else>
|
||||
Loading...
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { gql } from '@urql/core'
|
||||
import { useQuery } from '@urql/vue'
|
||||
import { Specs_AppDocument } from '../generated/graphql'
|
||||
import { gql } from '@urql/vue'
|
||||
import SpecsList from '../specs/SpecsList.vue'
|
||||
import { REPORTER_ID, RUNNER_ID } from '../runner/utils'
|
||||
import type { Specs_SpecsFragment } from '../generated/graphql'
|
||||
|
||||
gql`
|
||||
query Specs_App {
|
||||
app {
|
||||
...SpecsList
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const query = useQuery({
|
||||
query: Specs_AppDocument,
|
||||
})
|
||||
fragment Specs_Specs on App {
|
||||
...Specs_SpecsList
|
||||
}`
|
||||
|
||||
const props = defineProps<{
|
||||
gql: Specs_SpecsFragment
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<route>
|
||||
{
|
||||
name: "Specs Page"
|
||||
}
|
||||
</route>
|
||||
|
||||
<style>
|
||||
iframe {
|
||||
border: 5px solid black;
|
||||
margin: 10px;
|
||||
background: lightgray;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createWebHashHistory, createRouter as _createRouter } from 'vue-router'
|
||||
import { createRouter as _createRouter, createWebHistory } from 'vue-router'
|
||||
import generatedRoutes from 'virtual:generated-pages'
|
||||
import { setupLayouts } from 'virtual:generated-layouts'
|
||||
|
||||
@@ -6,7 +6,7 @@ export const createRouter = () => {
|
||||
const routes = setupLayouts(generatedRoutes)
|
||||
|
||||
return _createRouter({
|
||||
history: createWebHashHistory(),
|
||||
history: createWebHistory('/__vite__/'),
|
||||
routes,
|
||||
})
|
||||
}
|
||||
|
||||
154
packages/app/src/runner/index.ts
Normal file
154
packages/app/src/runner/index.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
/// <reference types="../../index" />
|
||||
|
||||
/**
|
||||
* This is the seam between the new "unified app", built with
|
||||
* Vite and Vue, and the existing code, including:
|
||||
* - driver
|
||||
* - reporter
|
||||
* - event manager
|
||||
* - anything in runner-shared, such as AutIframe, etc.
|
||||
* which are built with React and bundle with webpack.
|
||||
*
|
||||
* The entry point for the webpack bundle is `runner-ct/main.tsx`.
|
||||
* Any time you need to consume some existing code, add it to the `window.UnifiedRunner`
|
||||
* namespace there, and access it with `window.UnifiedRunner`.
|
||||
*
|
||||
*/
|
||||
import { store, Store } from '../store'
|
||||
import { injectBundle } from './injectBundle'
|
||||
import type { SpecFile } from '@packages/types/src/spec'
|
||||
import { UnifiedReporterAPI } from './reporter'
|
||||
import { getRunnerElement } from './utils'
|
||||
|
||||
function empty (el: HTMLElement) {
|
||||
while (el.lastChild) {
|
||||
if (el && el.firstChild) {
|
||||
el.removeChild(el.firstChild)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const randomString = `${Math.random()}`
|
||||
|
||||
/**
|
||||
* One-time setup. Required `window.UnifiedRunner` to exist,
|
||||
* so this is passed as a callback to the `renderRunner` function,
|
||||
* which injects `UnifiedRunner` onto `window`.
|
||||
* Everything on `window.UnifiedRunner` is bundled using webpack.
|
||||
*
|
||||
* Creates Cypress instance, initializes various event buses to allow
|
||||
* for communication between driver, runner, reporter via event bus,
|
||||
* and server (via web socket).
|
||||
*/
|
||||
function setupRunner (done: () => void) {
|
||||
window.UnifiedRunner.eventManager.addGlobalListeners(store, {
|
||||
automationElement: '__cypress-string',
|
||||
randomString,
|
||||
})
|
||||
|
||||
done()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL for the spec. This is the URL of the AUT IFrame.
|
||||
*/
|
||||
function getSpecUrl (namespace: string, spec: SpecFile, prefix = '') {
|
||||
return spec ? `${prefix}/${namespace}/iframes/${spec.absolute}` : ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up the current Cypress instance and anything else prior to
|
||||
* running a new spec.
|
||||
* This should be called before you execute a spec,
|
||||
* or re-running the current spec.
|
||||
*/
|
||||
function teardownSpec (store: Store) {
|
||||
return window.UnifiedRunner.eventManager.teardown(store)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up a spec by creating a fresh AUT and initializing
|
||||
* Cypress on it.
|
||||
*
|
||||
*/
|
||||
function setupSpec (spec: SpecFile) {
|
||||
// @ts-ignore - TODO: figure out how to manage window.config.
|
||||
const config = window.config
|
||||
|
||||
// this is how the Cypress driver knows which spec to run.
|
||||
config.spec = spec
|
||||
|
||||
// creates a new instance of the Cypress driver for this spec,
|
||||
// initializes a bunch of listeners
|
||||
// watches spec file for changes.
|
||||
window.UnifiedRunner.eventManager.setup(config)
|
||||
|
||||
const $runnerRoot = getRunnerElement()
|
||||
|
||||
// clear AUT, if there is one.
|
||||
empty($runnerRoot)
|
||||
|
||||
// create root for new AUT
|
||||
const $container = document.createElement('div')
|
||||
|
||||
$runnerRoot.append($container)
|
||||
|
||||
// create new AUT
|
||||
const autIframe = new window.UnifiedRunner.AutIframe('Test Project')
|
||||
const $autIframe: JQuery<HTMLIFrameElement> = autIframe.create().appendTo($container)
|
||||
|
||||
const specSrc = getSpecUrl(config.namespace, spec)
|
||||
|
||||
autIframe.showInitialBlankContents()
|
||||
$autIframe.prop('src', specSrc)
|
||||
|
||||
// initialize Cypress (driver) with the AUT!
|
||||
window.UnifiedRunner.eventManager.initialize($autIframe, config)
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject the global `UnifiedRunner` via a <script src="..."> tag.
|
||||
* which includes the event manager and AutIframe constructor.
|
||||
* It is bundlded via webpack and consumed like a third party module.
|
||||
*
|
||||
* This only needs to happen once, prior to running the first spec.
|
||||
*/
|
||||
function initialize (ready: () => void) {
|
||||
injectBundle(() => setupRunner(ready))
|
||||
}
|
||||
|
||||
/**
|
||||
* This wraps all of the required interactions to run a spec.
|
||||
* Here are the things that happen:
|
||||
*
|
||||
* 1. set the current spec in the store. The Reporter, Driver etc
|
||||
* are all coupled to MobX tightly and require the MobX store containing
|
||||
* the current spec.
|
||||
*
|
||||
* 2. Reset the Reporter. We use the same instance of the Reporter,
|
||||
* but reset the internal state each time we run a spec.
|
||||
*
|
||||
* 3. Teardown spec. This does a few things, primaily stopping the current
|
||||
* spec run, which involves stopping the driver and runner.
|
||||
*
|
||||
* 4. Force the Reporter to re-render with the new spec we are executed.
|
||||
*
|
||||
* 5. Setup the spec. This involves a few things, see the `setupSpec` function's
|
||||
* description for more information.
|
||||
*/
|
||||
async function executeSpec (spec: SpecFile) {
|
||||
store.setSpec(spec)
|
||||
|
||||
await UnifiedReporterAPI.resetReporter()
|
||||
|
||||
await teardownSpec(store)
|
||||
|
||||
UnifiedReporterAPI.setupReporter()
|
||||
|
||||
return setupSpec(spec)
|
||||
}
|
||||
|
||||
export const UnifiedRunnerAPI = {
|
||||
initialize,
|
||||
executeSpec,
|
||||
}
|
||||
40
packages/app/src/runner/injectBundle.ts
Normal file
40
packages/app/src/runner/injectBundle.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
function injectReporterStyle () {
|
||||
const style = document.createElement('style')
|
||||
|
||||
style.innerText = `
|
||||
.reporter {
|
||||
min-height: 0;
|
||||
width: 300px;
|
||||
left: 750px;
|
||||
position: absolute;
|
||||
}
|
||||
`
|
||||
|
||||
document.head.appendChild(style)
|
||||
}
|
||||
|
||||
export async function injectBundle (ready: () => void) {
|
||||
const response = await window.fetch('/api')
|
||||
const data = await response.json()
|
||||
const script = document.createElement('script')
|
||||
|
||||
script.src = '/__cypress/runner/cypress_runner.js'
|
||||
script.type = 'text/javascript'
|
||||
|
||||
const link = document.createElement('link')
|
||||
|
||||
link.rel = 'stylesheet'
|
||||
link.href = '/__cypress/runner/cypress_runner.css'
|
||||
|
||||
document.head.appendChild(script)
|
||||
document.head.appendChild(link)
|
||||
|
||||
injectReporterStyle()
|
||||
|
||||
script.onload = () => {
|
||||
// @ts-ignore - just stick config on window until we figure out how we are
|
||||
// going to manage it
|
||||
window.config = window.UnifiedRunner.decodeBase64(data.base64Config)
|
||||
ready()
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
export function renderRunner (ready: () => void) {
|
||||
const script = document.createElement('script')
|
||||
|
||||
script.src = '/__cypress/runner/cypress_runner.js'
|
||||
script.type = 'text/javascript'
|
||||
|
||||
const link = document.createElement('link')
|
||||
|
||||
link.rel = 'stylesheet'
|
||||
link.href = '/__cypress/runner/cypress_runner.css'
|
||||
|
||||
document.head.appendChild(script)
|
||||
document.head.appendChild(link)
|
||||
|
||||
script.onload = () => {
|
||||
ready()
|
||||
}
|
||||
}
|
||||
59
packages/app/src/runner/reporter.ts
Normal file
59
packages/app/src/runner/reporter.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { store, Store } from '../store'
|
||||
import { getReporterElement } from './utils'
|
||||
|
||||
let hasInitializeReporter = false
|
||||
|
||||
async function unmountReporter () {
|
||||
// We do not need to unmount the reporter at any point right now,
|
||||
// but this will likely be useful for cleaning up at some point.
|
||||
window.UnifiedRunner.ReactDOM.unmountComponentAtNode(getReporterElement())
|
||||
}
|
||||
|
||||
async function resetReporter () {
|
||||
if (hasInitializeReporter) {
|
||||
await window.UnifiedRunner.eventManager.teardownReporter()
|
||||
}
|
||||
}
|
||||
|
||||
function setupReporter () {
|
||||
const $reporterRoot = getReporterElement()
|
||||
|
||||
renderReporter($reporterRoot, store, window.UnifiedRunner.eventManager)
|
||||
hasInitializeReporter = true
|
||||
}
|
||||
|
||||
function renderReporter (
|
||||
root: HTMLElement,
|
||||
store: Store,
|
||||
eventManager: typeof window.UnifiedRunner.eventManager,
|
||||
) {
|
||||
class EmptyHeader extends window.UnifiedRunner.React.Component {
|
||||
render () {
|
||||
return window.UnifiedRunner.React.createElement('div')
|
||||
}
|
||||
}
|
||||
|
||||
const reporter = window.UnifiedRunner.React.createElement(window.UnifiedRunner.Reporter, {
|
||||
runMode: 'single' as const,
|
||||
runner: eventManager.reporterBus,
|
||||
key: store.specRunId,
|
||||
spec: store.spec,
|
||||
specRunId: store.specRunId,
|
||||
error: null, // errorMessages.reporterError(props.state.scriptError, props.state.spec.relative),
|
||||
resetStatsOnSpecChange: true,
|
||||
experimentalStudioEnabled: false,
|
||||
// TODO: Are we re-skinning the Reporter header?
|
||||
// If so, with React or Vue?
|
||||
// For now, just render and empty div.
|
||||
renderReporterHeader: (props: null) => window.UnifiedRunner.React.createElement(EmptyHeader, props),
|
||||
})
|
||||
|
||||
window.UnifiedRunner.ReactDOM.render(reporter, root)
|
||||
}
|
||||
|
||||
export const UnifiedReporterAPI = {
|
||||
unmountReporter,
|
||||
setupReporter,
|
||||
hasInitializeReporter,
|
||||
resetReporter,
|
||||
}
|
||||
21
packages/app/src/runner/utils.ts
Normal file
21
packages/app/src/runner/utils.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export const RUNNER_ID = 'unified-runner'
|
||||
|
||||
export const REPORTER_ID = 'unified-reporter'
|
||||
|
||||
function getElementById (id: string) {
|
||||
const el = document.querySelector<HTMLElement>(`#${id}`)
|
||||
|
||||
if (!el) {
|
||||
throw Error(`Expected element with #${id} but did not find it.`)
|
||||
}
|
||||
|
||||
return el
|
||||
}
|
||||
|
||||
export function getRunnerElement () {
|
||||
return getElementById(RUNNER_ID)
|
||||
}
|
||||
|
||||
export function getReporterElement () {
|
||||
return getElementById(REPORTER_ID)
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import SpecsListHeader from './SpecsListHeader.vue'
|
||||
|
||||
describe('<SpecsListHeader />', () => {
|
||||
beforeEach(() => {
|
||||
cy.mount(() => (
|
||||
<div class="py-4 px-8">
|
||||
<SpecsListHeader modelValue='' />
|
||||
</div>
|
||||
))
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
import SpecsList from './SpecsList.vue'
|
||||
import { SpecsListFragmentDoc, SpecListRowFragment } from '../generated/graphql-test'
|
||||
import { Specs_SpecsListFragmentDoc, SpecListRowFragment } from '../generated/graphql-test'
|
||||
import { defaultMessages } from '@cy/i18n'
|
||||
|
||||
const rowSelector = '[data-testid=specs-list-row]'
|
||||
@@ -20,7 +20,7 @@ let specs: Array<SpecListRowFragment> = []
|
||||
|
||||
describe('<SpecsList />', { keystrokeDelay: 0 }, () => {
|
||||
beforeEach(() => {
|
||||
cy.mountFragment(SpecsListFragmentDoc, {
|
||||
cy.mountFragment(Specs_SpecsListFragmentDoc, {
|
||||
onResult: (ctx) => {
|
||||
specs = ctx.activeProject?.specs?.edges || []
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
v-slot="{ navigate }"
|
||||
:key="spec.node.id"
|
||||
:to="path(spec)"
|
||||
custom
|
||||
>
|
||||
<SpecsListRow
|
||||
:gql="spec"
|
||||
@@ -31,25 +30,32 @@ import SpecsListHeader from './SpecsListHeader.vue'
|
||||
import SpecsListRow from './SpecsListRow.vue'
|
||||
import { gql } from '@urql/vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import type { SpecsListFragment } from '../generated/graphql'
|
||||
import type { Specs_SpecsListFragment, SpecNode_SpecsListFragment } from '../generated/graphql'
|
||||
import { useI18n } from '@cy/i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
const path = (spec) => `/runner/tests/${spec.node.specType}/${spec.node.name}${spec.node.fileExtension}`
|
||||
const path = (spec: SpecNode_SpecsListFragment) => `/runner/#${spec.node.absolute}`
|
||||
|
||||
gql`
|
||||
fragment SpecsList on App {
|
||||
fragment SpecNode_SpecsList on SpecEdge {
|
||||
node {
|
||||
name
|
||||
specType
|
||||
absolute
|
||||
relative
|
||||
}
|
||||
...SpecListRow
|
||||
}
|
||||
`
|
||||
|
||||
gql`
|
||||
fragment Specs_SpecsList on App {
|
||||
activeProject {
|
||||
id
|
||||
projectRoot
|
||||
specs(first: 1) {
|
||||
specs(first: 10) {
|
||||
edges {
|
||||
node {
|
||||
name
|
||||
specType
|
||||
relative
|
||||
}
|
||||
...SpecListRow
|
||||
...SpecNode_SpecsList
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,14 +63,19 @@ fragment SpecsList on App {
|
||||
`
|
||||
|
||||
const props = defineProps<{
|
||||
gql: SpecsListFragment
|
||||
gql: Specs_SpecsListFragment
|
||||
}>()
|
||||
|
||||
const search = ref('')
|
||||
const specs = computed(() => props.gql.activeProject?.specs?.edges)
|
||||
|
||||
// If this search becomes any more complex, push it into the server
|
||||
const sortByGitStatus = (a, b) => a.node.gitInfo ? 1 : -1
|
||||
const sortByGitStatus = (
|
||||
a: SpecNode_SpecsListFragment,
|
||||
b: SpecNode_SpecsListFragment,
|
||||
) => {
|
||||
return a.node.gitInfo ? 1 : -1
|
||||
}
|
||||
const filteredSpecs = computed(() => {
|
||||
return specs.value?.filter((s) => {
|
||||
return s.node.relative.toLowerCase().includes(search.value.toLowerCase())
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import SpecsListHeader from './SpecsListHeader.vue'
|
||||
import { ref } from 'vue'
|
||||
import { defineComponent, ref, h } from 'vue'
|
||||
|
||||
const buttonSelector = '[data-testid=new-spec-button]'
|
||||
const inputSelector = 'input[type=search]'
|
||||
@@ -9,10 +9,19 @@ describe('<SpecsListHeader />', { keystrokeDelay: 0 }, () => {
|
||||
const search = ref('')
|
||||
const searchString = 'my/component.cy.tsx'
|
||||
|
||||
cy.mount(<SpecsListHeader modelValue={search.value} />)
|
||||
cy.mount(defineComponent({
|
||||
setup () {
|
||||
return () => h(SpecsListHeader, {
|
||||
modelValue: search.value,
|
||||
'onUpdate:modelValue': (val: string) => {
|
||||
search.value = val
|
||||
},
|
||||
})
|
||||
},
|
||||
}))
|
||||
.get(inputSelector)
|
||||
.type(searchString, { delay: 0 })
|
||||
.should(() => {
|
||||
.then(() => {
|
||||
expect(search.value).to.equal(searchString)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
class="flex-grow h-full min-w-200px"
|
||||
prefix-icon-classes="icon-light-gray-50 icon-dark-gray-500"
|
||||
:prefix-icon="IconMagnifyingGlass"
|
||||
:model-value="inputValue"
|
||||
:model-value="props.modelValue"
|
||||
:placeholder="t('specPage.searchPlaceholder')"
|
||||
@input="onInput"
|
||||
/>
|
||||
@@ -25,7 +25,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useVModel } from '@vueuse/core'
|
||||
import { useI18n } from '@cy/i18n'
|
||||
import Button from '@cy/components/Button.vue'
|
||||
import Input from '@cy/components/Input.vue'
|
||||
@@ -43,9 +42,9 @@ const emit = defineEmits<{
|
||||
(e: 'newSpec'): void
|
||||
}>()
|
||||
|
||||
const inputValue = useVModel(props, 'modelValue', emit)
|
||||
const onInput = (e: Event) => {
|
||||
const value = (e.target as HTMLInputElement).value
|
||||
|
||||
const onInput = (e) => {
|
||||
inputValue.value = (e as Event & { target: HTMLInputElement | null }).target?.value || ''
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
/>
|
||||
<div>
|
||||
<span class="font-medium text-gray-700 group-hocus:text-indigo-500">{{ spec.fileName }}</span>
|
||||
<span class="font-light text-gray-400 group-hocus:text-indigo-500">{{ spec.specFileExtension + spec.fileExtension }}</span>
|
||||
<span class="font-light text-gray-400 group-hocus:text-indigo-500">{{ spec.specFileExtension }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid git-info-row grid-cols-[16px,auto] items-center gap-9px">
|
||||
|
||||
5
packages/app/src/store.ts
Normal file
5
packages/app/src/store.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { BaseStore } from '@packages/runner-shared/src/store'
|
||||
|
||||
export class Store extends BaseStore {}
|
||||
|
||||
export const store = new Store()
|
||||
@@ -5,6 +5,7 @@
|
||||
"src/**/*.tsx",
|
||||
"cypress/**/*.ts",
|
||||
"cypress/**/*.tsx",
|
||||
"index.d.ts",
|
||||
"../frontend-shared/src/**/*.vue",
|
||||
"../frontend-shared/src/**/*.tsx",
|
||||
"../frontend-shared/cypress/**/*.ts"
|
||||
@@ -14,6 +15,7 @@
|
||||
"@cy/i18n": ["../frontend-shared/src/locales/i18n"],
|
||||
"@cy/components/*": ["../frontend-shared/src/components/*"],
|
||||
"@packages/*": ["../*"]
|
||||
}
|
||||
},
|
||||
"allowJs": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ export default makeConfig({
|
||||
include: [
|
||||
'@urql/core',
|
||||
'vue-i18n',
|
||||
'@cypress/vue',
|
||||
'@vue/test-utils',
|
||||
'vue-router',
|
||||
'@urql/devtools',
|
||||
'@urql/exchange-graphcache',
|
||||
|
||||
@@ -59,20 +59,6 @@ describe('runnables', () => {
|
||||
cy.percySnapshot()
|
||||
})
|
||||
|
||||
it('displays multi-spec reporters', () => {
|
||||
start({ runMode: 'multi', allSpecs: [
|
||||
{
|
||||
relative: 'fizz',
|
||||
},
|
||||
{
|
||||
relative: 'buzz',
|
||||
},
|
||||
] })
|
||||
|
||||
cy.contains('buzz').should('be.visible')
|
||||
cy.percySnapshot()
|
||||
})
|
||||
|
||||
it('displays bundle error if specified', () => {
|
||||
const error = {
|
||||
title: 'Oops...we found an error preparing this test file:',
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { action, runInAction } from 'mobx'
|
||||
import { observer } from 'mobx-react'
|
||||
import cs from 'classnames'
|
||||
import PropTypes from 'prop-types'
|
||||
import React, { Component } from 'react'
|
||||
import { render } from 'react-dom'
|
||||
// @ts-ignore
|
||||
@@ -19,14 +18,16 @@ import shortcuts from './lib/shortcuts'
|
||||
import Header, { ReporterHeaderProps } from './header/header'
|
||||
import Runnables from './runnables/runnables'
|
||||
|
||||
let runnerListenersAdded = false
|
||||
|
||||
interface BaseReporterProps {
|
||||
appState: AppState
|
||||
appState?: AppState
|
||||
className?: string
|
||||
runnablesStore: RunnablesStore
|
||||
runnablesStore?: RunnablesStore
|
||||
runner: Runner
|
||||
scroller: Scroller
|
||||
statsStore: StatsStore
|
||||
events: Events
|
||||
scroller?: Scroller
|
||||
statsStore?: StatsStore
|
||||
events?: Events
|
||||
error?: RunnablesErrorModel
|
||||
resetStatsOnSpecChange?: boolean
|
||||
renderReporterHeader?: (props: ReporterHeaderProps) => JSX.Element
|
||||
@@ -40,33 +41,9 @@ export interface SingleReporterProps extends BaseReporterProps{
|
||||
runMode: 'single'
|
||||
}
|
||||
|
||||
export interface MultiReporterProps extends BaseReporterProps{
|
||||
runMode: 'multi'
|
||||
allSpecs: Array<Cypress.Cypress['spec']>
|
||||
}
|
||||
|
||||
@observer
|
||||
class Reporter extends Component<SingleReporterProps | MultiReporterProps> {
|
||||
static propTypes = {
|
||||
error: PropTypes.shape({
|
||||
title: PropTypes.string.isRequired,
|
||||
link: PropTypes.string,
|
||||
callout: PropTypes.string,
|
||||
message: PropTypes.string.isRequired,
|
||||
}),
|
||||
runner: PropTypes.shape({
|
||||
emit: PropTypes.func.isRequired,
|
||||
on: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
spec: PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
relative: PropTypes.string.isRequired,
|
||||
absolute: PropTypes.string.isRequired,
|
||||
}),
|
||||
experimentalStudioEnabled: PropTypes.bool,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
class Reporter extends Component<SingleReporterProps> {
|
||||
static defaultProps: Partial<SingleReporterProps> = {
|
||||
runMode: 'single',
|
||||
appState,
|
||||
events,
|
||||
@@ -79,7 +56,6 @@ class Reporter extends Component<SingleReporterProps | MultiReporterProps> {
|
||||
const {
|
||||
appState,
|
||||
className,
|
||||
runMode,
|
||||
runnablesStore,
|
||||
scroller,
|
||||
error,
|
||||
@@ -90,29 +66,18 @@ class Reporter extends Component<SingleReporterProps | MultiReporterProps> {
|
||||
|
||||
return (
|
||||
<div className={cs(className, 'reporter', {
|
||||
multiSpecs: runMode === 'multi',
|
||||
'experimental-studio-enabled': experimentalStudioEnabled,
|
||||
'studio-active': appState.studioActive,
|
||||
})}>
|
||||
{renderReporterHeader({ appState, statsStore })}
|
||||
{this.props.runMode === 'single' ? (
|
||||
<Runnables
|
||||
appState={appState}
|
||||
error={error}
|
||||
runnablesStore={runnablesStore}
|
||||
scroller={scroller}
|
||||
spec={this.props.spec}
|
||||
/>
|
||||
) : this.props.allSpecs.map((spec) => (
|
||||
<Runnables
|
||||
key={spec.relative}
|
||||
appState={appState}
|
||||
error={error}
|
||||
runnablesStore={runnablesStore}
|
||||
scroller={scroller}
|
||||
spec={spec}
|
||||
/>
|
||||
))}
|
||||
<Runnables
|
||||
appState={appState}
|
||||
error={error}
|
||||
runnablesStore={runnablesStore}
|
||||
scroller={scroller}
|
||||
spec={this.props.spec}
|
||||
/>
|
||||
)
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -121,7 +86,6 @@ class Reporter extends Component<SingleReporterProps | MultiReporterProps> {
|
||||
// it never happens in normal e2e but can happen in component-testing mode
|
||||
componentDidUpdate (newProps: BaseReporterProps) {
|
||||
this.props.runnablesStore.setRunningSpec(this.props.spec.relative)
|
||||
|
||||
if (
|
||||
this.props.resetStatsOnSpecChange &&
|
||||
this.props.specRunId !== newProps.specRunId
|
||||
@@ -146,7 +110,10 @@ class Reporter extends Component<SingleReporterProps | MultiReporterProps> {
|
||||
statsStore,
|
||||
})
|
||||
|
||||
this.props.events.listen(runner)
|
||||
if (!runnerListenersAdded) {
|
||||
this.props.events.listen(runner)
|
||||
runnerListenersAdded = true
|
||||
}
|
||||
|
||||
shortcuts.start()
|
||||
EQ.init()
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
import '@packages/runner/src/main.scss'
|
||||
import './main.jsx'
|
||||
import './main.tsx'
|
||||
|
||||
@@ -58,7 +58,6 @@ const _defaults: Defaults = {
|
||||
export default class State extends BaseStore {
|
||||
defaults = _defaults
|
||||
|
||||
@observable isLoading = true
|
||||
@observable isRunning = false
|
||||
@observable waitingForInitialBuild = false
|
||||
|
||||
@@ -211,10 +210,6 @@ export default class State extends BaseStore {
|
||||
this.isSpecsListOpen = open
|
||||
}
|
||||
|
||||
@action setIsLoading (isLoading) {
|
||||
this.isLoading = isLoading
|
||||
}
|
||||
|
||||
@action updateReporterWidth (width: number) {
|
||||
this.reporterWidth = width
|
||||
}
|
||||
|
||||
@@ -1,23 +1,54 @@
|
||||
import { autorun, action, configure } from 'mobx'
|
||||
import React from 'react'
|
||||
import { render } from 'react-dom'
|
||||
import ReactDOM from 'react-dom'
|
||||
import $Cypress from '@packages/driver'
|
||||
const driverUtils = $Cypress.utils
|
||||
import { eventManager, AutIframe, Container } from '@packages/runner-shared'
|
||||
import defaultEvents from '@packages/reporter/src/lib/events'
|
||||
import { Reporter } from '@packages/reporter/src/main'
|
||||
|
||||
export function getSpecUrl (namespace: string, spec: FoundSpec, prefix = '') {
|
||||
return spec ? `${prefix}/${namespace}/iframes/${spec.absolute}` : ''
|
||||
}
|
||||
|
||||
const UnifiedRunner = {
|
||||
React,
|
||||
|
||||
ReactDOM,
|
||||
|
||||
Reporter,
|
||||
|
||||
AutIframe,
|
||||
|
||||
defaultEvents,
|
||||
|
||||
eventManager,
|
||||
|
||||
decodeBase64: (base64: string) => {
|
||||
return JSON.parse(driverUtils.decodeBase64Unicode(base64))
|
||||
},
|
||||
|
||||
emit (evt: string, ...args: unknown[]) {
|
||||
defaultEvents.emit(evt, ...args)
|
||||
},
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
window.UnifiedRunner = UnifiedRunner
|
||||
|
||||
/** This is the OG runner-ct */
|
||||
import 'regenerator-runtime/runtime'
|
||||
import type { FoundSpec } from '@packages/types/src/spec'
|
||||
|
||||
import { autorun, action, configure } from 'mobx'
|
||||
|
||||
import App from './app/RunnerCt'
|
||||
import State from './lib/state'
|
||||
import { Container, eventManager } from '@packages/runner-shared'
|
||||
import util from './lib/util'
|
||||
|
||||
// to support async/await
|
||||
import 'regenerator-runtime/runtime'
|
||||
|
||||
const driverUtils = $Cypress.utils
|
||||
|
||||
configure({ enforceActions: 'always' })
|
||||
|
||||
const Runner = {
|
||||
emit (evt, ...args) {
|
||||
const Runner: any = {
|
||||
emit (evt: string, ...args: unknown[]) {
|
||||
defaultEvents.emit(evt, ...args)
|
||||
},
|
||||
|
||||
@@ -78,9 +109,10 @@ const Runner = {
|
||||
/>
|
||||
)
|
||||
|
||||
render(container, el)
|
||||
ReactDOM.render(container, el)
|
||||
})()
|
||||
},
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
window.Runner = Runner
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
|
||||
export const automationElementId = '__cypress-string'
|
||||
export const automationElementId = '__cypress-string' as const
|
||||
|
||||
interface AutomationElementProps {
|
||||
randomString: string
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
export const automation = {
|
||||
export const automationStatus = ['CONNECTING', 'MISSING', 'CONNECTED', 'DISCONNECTED'] as const
|
||||
|
||||
export const automation: Record<string, typeof automationStatus[number]> = {
|
||||
CONNECTING: 'CONNECTING',
|
||||
MISSING: 'MISSING',
|
||||
CONNECTED: 'CONNECTED',
|
||||
DISCONNECTED: 'DISCONNECTED',
|
||||
}
|
||||
} as const
|
||||
|
||||
@@ -4,6 +4,7 @@ import Promise from 'bluebird'
|
||||
import { action } from 'mobx'
|
||||
|
||||
import { client } from '@packages/socket'
|
||||
import type { BaseStore } from './store'
|
||||
|
||||
import { studioRecorder } from './studio'
|
||||
import { automation } from './automation'
|
||||
@@ -11,6 +12,7 @@ import { logger } from './logger'
|
||||
import { selectorPlaygroundModel } from './selector-playground'
|
||||
|
||||
import $Cypress from '@packages/driver'
|
||||
import type { automationElementId } from './automation-element'
|
||||
|
||||
const $ = $Cypress.$
|
||||
const ws = client.connect({
|
||||
@@ -55,7 +57,7 @@ export const eventManager = {
|
||||
return Cypress
|
||||
},
|
||||
|
||||
addGlobalListeners (state, connectionInfo) {
|
||||
addGlobalListeners (state: BaseStore, connectionInfo: { automationElement: typeof automationElementId, randomString: string }) {
|
||||
const rerun = () => {
|
||||
if (!this) {
|
||||
// if the tests have been reloaded
|
||||
@@ -63,7 +65,7 @@ export const eventManager = {
|
||||
return
|
||||
}
|
||||
|
||||
return this._reRun(state)
|
||||
return this.runSpec(state)
|
||||
}
|
||||
|
||||
ws.emit('is:automation:client:connected', connectionInfo, action('automationEnsured', (isConnected) => {
|
||||
@@ -292,6 +294,7 @@ export const eventManager = {
|
||||
|
||||
const $window = $(window)
|
||||
|
||||
// TODO(lachlan): best place to do this?
|
||||
$window.on('hashchange', rerun)
|
||||
|
||||
// when we actually unload then
|
||||
@@ -331,7 +334,7 @@ export const eventManager = {
|
||||
window.Cypress = Cypress
|
||||
}
|
||||
|
||||
this._addListeners(Cypress)
|
||||
this._addListeners()
|
||||
|
||||
ws.emit('watch:test:file', config.spec)
|
||||
},
|
||||
@@ -342,7 +345,7 @@ export const eventManager = {
|
||||
return this.Cypress.isBrowser(browserName)
|
||||
},
|
||||
|
||||
initialize ($autIframe, config) {
|
||||
initialize ($autIframe: JQuery<HTMLIFrameElement>, config: Record<string, any>) {
|
||||
performance.mark('initialize-start')
|
||||
|
||||
return Cypress.initialize({
|
||||
@@ -525,8 +528,10 @@ export const eventManager = {
|
||||
ws.off()
|
||||
},
|
||||
|
||||
_reRun (state) {
|
||||
if (!Cypress) return
|
||||
async teardown (state: BaseStore) {
|
||||
if (!Cypress) {
|
||||
return
|
||||
}
|
||||
|
||||
state.setIsLoading(true)
|
||||
|
||||
@@ -536,26 +541,37 @@ export const eventManager = {
|
||||
|
||||
studioRecorder.setInactive()
|
||||
selectorPlaygroundModel.setOpen(false)
|
||||
|
||||
return this._restart()
|
||||
.then(() => {
|
||||
// this probably isn't 100% necessary
|
||||
// since Cypress will fall out of scope
|
||||
// but we want to be aggressive here
|
||||
// and force GC early and often
|
||||
Cypress.removeAllListeners()
|
||||
|
||||
localBus.emit('restart')
|
||||
})
|
||||
},
|
||||
|
||||
_restart () {
|
||||
teardownReporter () {
|
||||
return new Promise((resolve) => {
|
||||
reporterBus.once('reporter:restarted', resolve)
|
||||
reporterBus.emit('reporter:restart:test:run')
|
||||
})
|
||||
},
|
||||
|
||||
async _rerun () {
|
||||
await this.teardownReporter()
|
||||
|
||||
// this probably isn't 100% necessary
|
||||
// since Cypress will fall out of scope
|
||||
// but we want to be aggressive here
|
||||
// and force GC early and often
|
||||
Cypress.removeAllListeners()
|
||||
|
||||
localBus.emit('restart')
|
||||
},
|
||||
|
||||
async runSpec (state: BaseStore) {
|
||||
if (!Cypress) {
|
||||
return
|
||||
}
|
||||
|
||||
await this.teardown(state)
|
||||
|
||||
return this._rerun()
|
||||
},
|
||||
|
||||
_interceptStudio (displayProps) {
|
||||
if (studioRecorder.isActive) {
|
||||
displayProps.hookId = studioRecorder.hookId
|
||||
@@ -1,4 +1,4 @@
|
||||
import _ from 'lodash'
|
||||
import _, { DebouncedFunc } from 'lodash'
|
||||
import $Cypress from '@packages/driver'
|
||||
import * as blankContents from '../blank-contents'
|
||||
import { visitFailure } from '../visit-failure'
|
||||
@@ -11,14 +11,16 @@ import { studioRecorder } from '../studio'
|
||||
const $ = $Cypress.$
|
||||
|
||||
export class AutIframe {
|
||||
constructor (config) {
|
||||
this.config = config
|
||||
debouncedToggleSelectorPlayground: DebouncedFunc<(isEnabled: any) => void>
|
||||
$iframe?: JQuery<HTMLIFrameElement>
|
||||
|
||||
constructor (private projectName: string) {
|
||||
this.debouncedToggleSelectorPlayground = _.debounce(this.toggleSelectorPlayground, 300)
|
||||
}
|
||||
|
||||
create () {
|
||||
this.$iframe = $('<iframe>', {
|
||||
id: `Your App: '${this.config.projectName}'`,
|
||||
id: `Your App: '${this.projectName}'`,
|
||||
class: 'aut-iframe',
|
||||
})
|
||||
|
||||
@@ -42,7 +44,7 @@ export class AutIframe {
|
||||
}
|
||||
|
||||
_showContents (contents) {
|
||||
this._body().html(contents)
|
||||
this._body()?.html(contents)
|
||||
}
|
||||
|
||||
_contents () {
|
||||
@@ -50,15 +52,15 @@ export class AutIframe {
|
||||
}
|
||||
|
||||
_window () {
|
||||
return this.$iframe.prop('contentWindow')
|
||||
return this.$iframe?.prop('contentWindow')
|
||||
}
|
||||
|
||||
_document () {
|
||||
return this.$iframe.prop('contentDocument')
|
||||
return this.$iframe?.prop('contentDocument')
|
||||
}
|
||||
|
||||
_body () {
|
||||
return this._contents() && this._contents().find('body')
|
||||
return this._contents()?.find('body')
|
||||
}
|
||||
|
||||
detachDom = () => {
|
||||
@@ -1,22 +1,18 @@
|
||||
import { action, observable } from 'mobx'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { automation, automationStatus } from './automation'
|
||||
|
||||
export type RunMode = 'single' | 'multi'
|
||||
export type RunMode = 'single'
|
||||
|
||||
export abstract class BaseStore {
|
||||
@observable spec: Cypress.Cypress['spec'] | undefined
|
||||
@observable specs: Cypress.Cypress['spec'][] = []
|
||||
export class BaseStore {
|
||||
@observable spec: Cypress.Spec | undefined
|
||||
@observable specs: Cypress.Spec[] = []
|
||||
@observable specRunId: string | undefined
|
||||
/** @type {"single" | "multi"} */
|
||||
@observable runMode: RunMode = 'single'
|
||||
@observable multiSpecs: Cypress.Cypress['spec'][] = [];
|
||||
|
||||
@action setSingleSpec (spec: Cypress.Cypress['spec'] | undefined) {
|
||||
if (this.runMode === 'multi') {
|
||||
this.runMode = 'single'
|
||||
this.multiSpecs = []
|
||||
}
|
||||
@observable automation: typeof automationStatus[number] = automation.CONNECTING
|
||||
@observable isLoading = true
|
||||
|
||||
@action setSingleSpec (spec: Cypress.Spec | undefined) {
|
||||
this.setSpec(spec)
|
||||
}
|
||||
|
||||
@@ -25,7 +21,7 @@ export abstract class BaseStore {
|
||||
this.specRunId = nanoid()
|
||||
}
|
||||
|
||||
@action setSpecs (specs: Cypress.Cypress['spec'][]) {
|
||||
@action setSpecs (specs: Cypress.Spec[]) {
|
||||
this.specs = specs
|
||||
}
|
||||
|
||||
@@ -36,4 +32,8 @@ export abstract class BaseStore {
|
||||
this.spec = foundSpec
|
||||
}
|
||||
}
|
||||
|
||||
@action setIsLoading (isLoading) {
|
||||
this.isLoading = isLoading
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ const _defaults = {
|
||||
export default class State extends BaseStore {
|
||||
defaults = _defaults
|
||||
|
||||
@observable isLoading = true
|
||||
@observable isRunning = false
|
||||
|
||||
@observable messageTitle = _defaults.messageTitle
|
||||
@@ -109,10 +108,6 @@ export default class State extends BaseStore {
|
||||
}
|
||||
}
|
||||
|
||||
@action setIsLoading (isLoading) {
|
||||
this.isLoading = isLoading
|
||||
}
|
||||
|
||||
@action updateDimensions (width, height) {
|
||||
this.width = width
|
||||
this.height = height
|
||||
|
||||
@@ -43,6 +43,16 @@ export const createRoutesCT = ({
|
||||
|
||||
// TODO If prod, serve the build app files from app/dist
|
||||
|
||||
routesCt.get('/api', (req, res) => {
|
||||
const options = makeServeConfig({
|
||||
config,
|
||||
getCurrentBrowser,
|
||||
specsStore,
|
||||
})
|
||||
|
||||
res.json(options)
|
||||
})
|
||||
|
||||
routesCt.get('/__/api', (req, res) => {
|
||||
const options = makeServeConfig({
|
||||
config,
|
||||
|
||||
@@ -11,12 +11,18 @@ export type FindSpecs = {
|
||||
integrationFolder: Cypress.ResolvedConfigOptions['integrationFolder']
|
||||
} & CommonSearchOptions
|
||||
|
||||
export interface FoundSpec {
|
||||
// represents a spec file on file system
|
||||
export interface SpecFile {
|
||||
name: string
|
||||
baseName: string
|
||||
fileName: string
|
||||
relative: string
|
||||
absolute: string
|
||||
}
|
||||
|
||||
// represents a spec file on file system and
|
||||
// additional Cypress-specific information
|
||||
export interface FoundSpec extends SpecFile {
|
||||
specFileExtension: string
|
||||
fileExtension: string
|
||||
specType: Cypress.CypressSpecType
|
||||
|
||||
Reference in New Issue
Block a user