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:
Lachlan Miller
2021-10-25 20:03:53 +10:00
committed by GitHub
parent 48a2ffa2f4
commit dfb3e84bfd
22 changed files with 206 additions and 191 deletions

View File

@@ -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": [
]
}
}

View File

@@ -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()
})
})

View File

@@ -89,6 +89,7 @@
"nanoid",
"path",
"pinia",
"shiki",
"socket.io-client",
"vue",
"vue-toastification",

View File

@@ -0,0 +1,5 @@
describe('example', () => {
it('does not do much', () => {
expect(true).to.be.true
})
})

View File

@@ -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" />

View File

@@ -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'))

View File

@@ -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' },
]

View File

@@ -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"

View File

@@ -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>

View File

@@ -0,0 +1,10 @@
<template>
<div>
<SpecPageContainer />
</div>
</template>
<script lang="ts" setup>
import SpecPageContainer from '../spec/SpecPageContainer.vue'
</script>

View File

@@ -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>

View File

@@ -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>

View 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>

View File

@@ -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())
})

View 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>

View File

@@ -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>

View File

@@ -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
}>()

View 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>

View File

@@ -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'

View 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
},
},
})

View File

@@ -586,7 +586,6 @@ type ProjectPreferences {
testingType: String
}
"""The root "Query" type containing all entry fields for our querying"""
type Query {
app: App!

View File

@@ -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