mirror of
https://github.com/cypress-io/cypress.git
synced 2026-02-21 14:41:00 -06:00
feat(app): work on routing and executing specs (#18605)
* wip: work on routing and running spec * removed old code * remove <keep-alive> * don't listen to hashchange event when in vite mode * renamed specs to specs-store, update activeSpec method + prop * switch from buttons to router-link * create dedicated "spec" route, rename old files, reimplement logic for activeSpec + initializing runner-ct bundle * just a simple example spec * cache shiki * add volar vue extension for vscode * fix TS linting * remove unused variables * remove unused code * rebuild schema * fixing tests * fix ts Co-authored-by: Brian Mann <brian.mann86@gmail.com>
This commit is contained in:
5
.vscode/extensions.json
vendored
5
.vscode/extensions.json
vendored
@@ -26,10 +26,13 @@
|
||||
// Name: Toggle Quotes
|
||||
// Description: Toggle cycle " -> ' -> `
|
||||
"britesnow.vscode-toggle-quotes",
|
||||
// Name: Volar
|
||||
// Description: Adds proper typescript support for Vue
|
||||
"johnsoncodehk.volar"
|
||||
],
|
||||
|
||||
// List of extensions recommended by VS Code that should not be recommended for Cypress contributors using VS Code:
|
||||
"unwantedRecommendations": [
|
||||
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
describe('App', () => {
|
||||
beforeEach(() => {
|
||||
cy.setupE2E('component-tests')
|
||||
cy.withCtx(async (ctx) => {
|
||||
// TODO: Why do we need this?
|
||||
await ctx.actions.app.refreshBrowsers()
|
||||
})
|
||||
|
||||
cy.initializeApp()
|
||||
})
|
||||
|
||||
it('resolves the home page', () => {
|
||||
cy.visitApp()
|
||||
cy.wait(1000)
|
||||
cy.get('[href="#/runner"]').click()
|
||||
cy.get('[href="#/runs"]').click()
|
||||
cy.get('[href="#/settings"]').click()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
"nanoid",
|
||||
"path",
|
||||
"pinia",
|
||||
"shiki",
|
||||
"socket.io-client",
|
||||
"vue",
|
||||
"vue-toastification",
|
||||
|
||||
5
packages/app/src/example.spec.ts
Normal file
5
packages/app/src/example.spec.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
describe('example', () => {
|
||||
it('does not do much', () => {
|
||||
expect(true).to.be.true
|
||||
})
|
||||
})
|
||||
@@ -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>
|
||||
<ModalManager v-if="modalStore.activeModalId" />
|
||||
|
||||
@@ -8,6 +8,12 @@ import { createI18n } from '@cy/i18n'
|
||||
import { createRouter } from './router/router'
|
||||
import { createPinia } from './store'
|
||||
|
||||
// set a global so we can run
|
||||
// conditional code in the vite branch
|
||||
// so that the existing runner code
|
||||
// @ts-ignore
|
||||
window.__vite__ = true
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(urql, makeUrqlClient('app'))
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
class="flex-1 px-2 mt-5 space-y-1 bg-gray-1000"
|
||||
aria-label="Sidebar"
|
||||
>
|
||||
<router-link
|
||||
<RouterLink
|
||||
v-for="item in navigation"
|
||||
v-slot="{ isActive }"
|
||||
:key="item.name"
|
||||
@@ -31,7 +31,7 @@
|
||||
>
|
||||
{{ item.name }}
|
||||
</SidebarNavigationRow>
|
||||
</router-link>
|
||||
</RouterLink>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
@@ -45,8 +45,9 @@ import SettingsIcon from '~icons/cy/settings_x24'
|
||||
import { useMainStore } from '../store'
|
||||
|
||||
const navigation = [
|
||||
{ name: 'Specs', icon: SpecsIcon, href: '/' },
|
||||
{ name: 'Runs', icon: CodeIcon, href: '/runner' },
|
||||
{ name: 'Home', icon: SpecsIcon, href: '/' },
|
||||
{ name: 'Specs', icon: CodeIcon, href: '/specs' },
|
||||
{ name: 'Runs', icon: CodeIcon, href: '/runs' },
|
||||
{ name: 'Settings', icon: SettingsIcon, href: '/settings' },
|
||||
{ name: 'New Spec', icon: SettingsIcon, href: '/newspec' },
|
||||
]
|
||||
|
||||
@@ -1,29 +1,7 @@
|
||||
<template>
|
||||
<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>
|
||||
<div>Home page</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>
|
||||
{
|
||||
name: "Home Page"
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
<template>
|
||||
<Runner
|
||||
v-if="query.data.value?.app && initialized"
|
||||
:gql="query.data.value.app"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { gql } from '@urql/core'
|
||||
import { useQuery } from '@urql/vue'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { Runner_AllDocument } from '../../generated/graphql'
|
||||
import { UnifiedRunnerAPI } from '../../runner'
|
||||
import Runner from '../../runs/Runner.vue'
|
||||
|
||||
gql`
|
||||
query Runner_All {
|
||||
app {
|
||||
...Specs_Runner
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const initialized = ref(false)
|
||||
|
||||
onMounted(async () => {
|
||||
await UnifiedRunnerAPI.initialize()
|
||||
initialized.value = true
|
||||
})
|
||||
|
||||
// network-only - we do not want to execute a stale spec
|
||||
const query = useQuery({
|
||||
query: Runner_AllDocument,
|
||||
requestPolicy: 'network-only',
|
||||
})
|
||||
</script>
|
||||
|
||||
<route>
|
||||
{
|
||||
name: "Runner"
|
||||
}
|
||||
</route>
|
||||
10
packages/app/src/pages/Spec.vue
Normal file
10
packages/app/src/pages/Spec.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<div>
|
||||
<SpecPageContainer />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import SpecPageContainer from '../spec/SpecPageContainer.vue'
|
||||
|
||||
</script>
|
||||
@@ -1,30 +1,9 @@
|
||||
<template>
|
||||
<h2>Specs Page</h2>
|
||||
|
||||
<SpecsList
|
||||
v-if="props.gql"
|
||||
:gql="props?.gql"
|
||||
/>
|
||||
|
||||
<p v-else>
|
||||
Loading...
|
||||
</p>
|
||||
<SpecsPageContainer />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
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`
|
||||
fragment Specs_Specs on App {
|
||||
...Specs_SpecsList
|
||||
}`
|
||||
|
||||
const props = defineProps<{
|
||||
gql: Specs_SpecsFragment
|
||||
}>()
|
||||
import SpecsPageContainer from '../specs/SpecsPageContainer.vue'
|
||||
</script>
|
||||
|
||||
<route>
|
||||
|
||||
@@ -12,9 +12,9 @@
|
||||
:key="route.path"
|
||||
class="text-left underline underline-2 underline-offset-1 underline-indigo-700 text-indigo-700 hover:text-indigo-500 hover:underline-indigo-500"
|
||||
>
|
||||
<router-link :to="route.path">
|
||||
<RouterLink :to="route.path">
|
||||
{{ route.name }}
|
||||
</router-link>
|
||||
</RouterLink>
|
||||
</li>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
23
packages/app/src/spec/SpecPageContainer.vue
Normal file
23
packages/app/src/spec/SpecPageContainer.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<div v-if="query.data.value?.app?.activeProject">
|
||||
<SpecRunnerContainer
|
||||
:gql="query.data.value.app"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { gql, useQuery } from '@urql/vue'
|
||||
import { SpecPageContainerDocument } from '../generated/graphql'
|
||||
import SpecRunnerContainer from './SpecRunnerContainer.vue'
|
||||
|
||||
gql`
|
||||
query SpecPageContainer {
|
||||
app {
|
||||
...SpecRunner
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const query = useQuery({ query: SpecPageContainerDocument })
|
||||
</script>
|
||||
@@ -4,10 +4,7 @@
|
||||
class="grid p-12 gap-8 h-full"
|
||||
>
|
||||
<div>
|
||||
<InlineSpecList
|
||||
:gql="props.gql"
|
||||
@selectSpec="selectSpec"
|
||||
/>
|
||||
<InlineSpecList :gql="props.gql" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -30,48 +27,32 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onBeforeUnmount, onMounted, reactive } from 'vue'
|
||||
import { UnifiedRunnerAPI } from '../runner'
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, watch } from 'vue'
|
||||
import { REPORTER_ID, RUNNER_ID, getRunnerElement, getReporterElement, empty } from '../runner/utils'
|
||||
import { gql } from '@urql/core'
|
||||
import type { Specs_RunnerFragment } from '../generated/graphql'
|
||||
import type { SpecRunnerFragment } from '../generated/graphql'
|
||||
import InlineSpecList from '../specs/InlineSpecList.vue'
|
||||
import { getMobxRunnerStore } from '../store'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { getMobxRunnerStore, useSpecStore } from '../store'
|
||||
import { UnifiedRunnerAPI } from '../runner'
|
||||
import type { BaseSpec } from '@packages/types'
|
||||
|
||||
gql`
|
||||
fragment CurrentSpec_Runner on Spec {
|
||||
id
|
||||
relative
|
||||
absolute
|
||||
name
|
||||
}
|
||||
`
|
||||
|
||||
gql`
|
||||
fragment Specs_Runner on App {
|
||||
fragment SpecRunner on App {
|
||||
...Specs_InlineSpecList
|
||||
activeProject {
|
||||
id
|
||||
projectRoot
|
||||
currentSpec {
|
||||
...CurrentSpec_Runner
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const runnerColumnWidth = 400
|
||||
|
||||
const store = getMobxRunnerStore()
|
||||
const mobxRunnerStore = getMobxRunnerStore()
|
||||
|
||||
const viewportDimensions = reactive({
|
||||
height: store.height,
|
||||
width: store.width,
|
||||
height: mobxRunnerStore.height,
|
||||
width: mobxRunnerStore.width,
|
||||
})
|
||||
|
||||
window.UnifiedRunner.MobX.reaction(
|
||||
() => [store.height, store.width],
|
||||
() => [mobxRunnerStore.height, mobxRunnerStore.width],
|
||||
([height, width]) => {
|
||||
viewportDimensions.height = height
|
||||
viewportDimensions.width = width
|
||||
@@ -87,48 +68,31 @@ const viewportStyle = computed(() => {
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
gql: Specs_RunnerFragment
|
||||
gql: SpecRunnerFragment
|
||||
activeSpec: BaseSpec
|
||||
}>()
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
async function selectSpec (relative: string) {
|
||||
router.push({ path: 'runner', query: { spec: relative } })
|
||||
const specToRun = props.gql.activeProject?.specs?.edges.find((x) => x.node.relative === relative)
|
||||
|
||||
if (!specToRun?.node) {
|
||||
return
|
||||
}
|
||||
|
||||
UnifiedRunnerAPI.executeSpec(specToRun.node)
|
||||
function runSpec () {
|
||||
UnifiedRunnerAPI.executeSpec(props.activeSpec)
|
||||
}
|
||||
|
||||
function executeSpec () {
|
||||
const relative = route.query.spec
|
||||
const spec = props.gql.activeProject?.specs?.edges.find((x) => x.node.relative === relative)?.node
|
||||
|
||||
if (!spec) {
|
||||
return
|
||||
}
|
||||
|
||||
UnifiedRunnerAPI.executeSpec(spec)
|
||||
}
|
||||
watch(() => props.activeSpec, (spec) => {
|
||||
runSpec()
|
||||
}, { immediate: true, flush: 'post' })
|
||||
|
||||
onMounted(() => {
|
||||
window.UnifiedRunner.eventManager.on('restart', () => {
|
||||
executeSpec()
|
||||
runSpec()
|
||||
})
|
||||
|
||||
executeSpec()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
// For now we clean up the AUT and Reporter every time we leave the route.
|
||||
// In the long term, we really should use <keep-alive> and maintain the state
|
||||
// For now, this is much more simple.
|
||||
// Clean up the AUT and Reporter every time we leave the route.
|
||||
empty(getRunnerElement())
|
||||
|
||||
// TODO: this should be handled by whoever starts it, reporter?
|
||||
window.UnifiedRunner.shortcuts.stop()
|
||||
|
||||
empty(getReporterElement())
|
||||
})
|
||||
|
||||
40
packages/app/src/spec/SpecRunnerContainer.vue
Normal file
40
packages/app/src/spec/SpecRunnerContainer.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div v-if="specStore.activeSpec">
|
||||
<SpecRunner
|
||||
v-if="initialized"
|
||||
:gql="props.gql"
|
||||
:active-spec="specStore.activeSpec"
|
||||
/>
|
||||
</div>
|
||||
<div v-else>
|
||||
Error, no spec matched!
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import type { SpecRunnerFragment } from '../generated/graphql'
|
||||
import { UnifiedRunnerAPI } from '../runner'
|
||||
import { useSpecStore } from '../store'
|
||||
import SpecRunner from './SpecRunner.vue'
|
||||
|
||||
const initialized = ref(false)
|
||||
const specStore = useSpecStore()
|
||||
const route = useRoute()
|
||||
|
||||
onMounted(async () => {
|
||||
await UnifiedRunnerAPI.initialize()
|
||||
initialized.value = true
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
gql: SpecRunnerFragment
|
||||
}>()
|
||||
|
||||
watch(() => route.query.file, (queryParam) => {
|
||||
const spec = props.gql.activeProject?.specs?.edges.find((x) => x.node.relative === queryParam)?.node
|
||||
|
||||
specStore.setActiveSpec(spec ?? null)
|
||||
}, { immediate: true, flush: 'post' })
|
||||
</script>
|
||||
@@ -1,23 +1,24 @@
|
||||
<template>
|
||||
<div>
|
||||
<button
|
||||
<RouterLink
|
||||
v-for="spec in specs"
|
||||
:key="spec.node.id"
|
||||
class="text-left grid grid-cols-[16px,auto,auto] items-center gap-10px"
|
||||
:class="{ 'border border-4 border-red-400': isCurrentSpec(spec.node.relative) }"
|
||||
@click.prevent="selectSpec(spec.node.relative)"
|
||||
:class="{ 'border-2 border-red-400': isCurrentSpec(spec) }"
|
||||
:to="{ path: 'spec', query: { file: spec.node.relative } }"
|
||||
>
|
||||
<SpecName :gql="spec.node" />
|
||||
</button>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { gql } from '@urql/vue'
|
||||
import type { Specs_InlineSpecListFragment } from '../generated/graphql'
|
||||
import type { SpecNode_InlineSpecListFragment, Specs_InlineSpecListFragment } from '../generated/graphql'
|
||||
import SpecName from './SpecName.vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useSpecStore } from '../store'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
gql`
|
||||
fragment SpecNode_InlineSpecList on SpecEdge {
|
||||
@@ -45,21 +46,17 @@ fragment Specs_InlineSpecList on App {
|
||||
}
|
||||
`
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'selectSpec', id: string): void
|
||||
}>()
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const isCurrentSpec = (relative: string) => relative === route.query.spec
|
||||
|
||||
async function selectSpec (id: string) {
|
||||
emit('selectSpec', id)
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
gql: Specs_InlineSpecListFragment
|
||||
}>()
|
||||
|
||||
const specStore = useSpecStore()
|
||||
|
||||
const isCurrentSpec = (spec: SpecNode_InlineSpecListFragment) => {
|
||||
return spec.node.relative === specStore.activeSpec?.relative
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const specs = computed(() => props.gql.activeProject?.specs?.edges || [])
|
||||
</script>
|
||||
|
||||
@@ -10,14 +10,14 @@
|
||||
<div>{{ t('specPage.componentSpecsHeader') }}</div>
|
||||
<div>{{ t('specPage.gitStatusHeader') }}</div>
|
||||
</div>
|
||||
<button
|
||||
<RouterLink
|
||||
v-for="spec in filteredSpecs"
|
||||
:key="spec.node.id"
|
||||
class="text-left"
|
||||
@click.prevent="selectSpec(spec.node.relative)"
|
||||
:to="{ path: 'spec', query: { file: spec.node.relative } }"
|
||||
>
|
||||
<SpecsListRow :gql="spec" />
|
||||
</button>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -25,11 +25,10 @@
|
||||
<script setup lang="ts">
|
||||
import SpecsListHeader from './SpecsListHeader.vue'
|
||||
import SpecsListRow from './SpecsListRow.vue'
|
||||
import { gql, useMutation } from '@urql/vue'
|
||||
import { gql } from '@urql/vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import type { Specs_SpecsListFragment, SpecNode_SpecsListFragment } from '../generated/graphql'
|
||||
import { useI18n } from '@cy/i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useModalStore } from '../store'
|
||||
|
||||
const modalStore = useModalStore()
|
||||
@@ -61,12 +60,6 @@ fragment Specs_SpecsList on App {
|
||||
}
|
||||
`
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
function selectSpec (relative: string) {
|
||||
router.push({ path: 'runner', query: { spec: relative } })
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
gql: Specs_SpecsListFragment
|
||||
}>()
|
||||
|
||||
27
packages/app/src/specs/SpecsPageContainer.vue
Normal file
27
packages/app/src/specs/SpecsPageContainer.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<div v-if="query.data.value?.app">
|
||||
<h2>Specs Page</h2>
|
||||
<SpecsList :gql="query.data.value.app" />
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
Loading...
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { gql, useQuery } from '@urql/vue'
|
||||
import SpecsList from './SpecsList.vue'
|
||||
import { SpecsPageContainerDocument } from '../generated/graphql'
|
||||
|
||||
gql`
|
||||
query SpecsPageContainer {
|
||||
app {
|
||||
...Specs_SpecsList
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const query = useQuery({ query: SpecsPageContainerDocument })
|
||||
|
||||
</script>
|
||||
@@ -5,6 +5,8 @@ export * from './main'
|
||||
|
||||
export * from './modals'
|
||||
|
||||
export * from './specs-store'
|
||||
|
||||
// Mobx Store Wrapper from @packages/runner-shared
|
||||
export * from './runner-store'
|
||||
|
||||
|
||||
22
packages/app/src/store/specs-store.ts
Normal file
22
packages/app/src/store/specs-store.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { BaseSpec } from '@packages/types/src'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export interface SpecState {
|
||||
activeSpec: BaseSpec | null
|
||||
}
|
||||
|
||||
export const useSpecStore = defineStore({
|
||||
id: 'spec',
|
||||
|
||||
state (): SpecState {
|
||||
return {
|
||||
activeSpec: null,
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
setActiveSpec (activeSpec: BaseSpec | null) {
|
||||
this.activeSpec = activeSpec
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -586,7 +586,6 @@ type ProjectPreferences {
|
||||
testingType: String
|
||||
}
|
||||
|
||||
|
||||
"""The root "Query" type containing all entry fields for our querying"""
|
||||
type Query {
|
||||
app: App!
|
||||
|
||||
@@ -306,7 +306,9 @@ export const eventManager = {
|
||||
const $window = $(window)
|
||||
|
||||
// TODO(lachlan): best place to do this?
|
||||
$window.on('hashchange', rerun)
|
||||
if (!window.__vite__) {
|
||||
$window.on('hashchange', rerun)
|
||||
}
|
||||
|
||||
// when we actually unload then
|
||||
// nuke all of the cookies again
|
||||
|
||||
Reference in New Issue
Block a user