mirror of
https://github.com/cypress-io/cypress.git
synced 2026-04-21 22:49:16 -05:00
feat: create the front end for runs in the launchpad (#17692)
* make sidebar item screen size smaller to zoom * create progressCircle element * fix configFile test * front end for runs * add useful helper for mounting fragment * types * update mount function * apply review comments Co-authored-by: Lachlan Miller <lachlan.miller.1990@outlook.com>
This commit is contained in:
committed by
GitHub
parent
7a59de1202
commit
da827512df
@@ -84,11 +84,71 @@ function mountFragment<Result, Variables, T extends TypedDocumentNode<Result, Va
|
||||
}).then(() => ctx)
|
||||
}
|
||||
|
||||
function mountFragmentList<Result, Variables, T extends TypedDocumentNode<Result, Variables>> (source: T[], options: MountFragmentConfig<T>): Cypress.Chainable<ClientTestContext> {
|
||||
const ctx = new ClientTestContext()
|
||||
|
||||
return mount(defineComponent({
|
||||
name: `mountFragmentList`,
|
||||
setup () {
|
||||
const getTypeCondition = (source: any) => (source.definitions[0] as any).typeCondition.name.value.toLowerCase()
|
||||
const frags = source.map((src) => {
|
||||
/**
|
||||
* generates something like
|
||||
* wizard {
|
||||
* ... MyFragment
|
||||
* }
|
||||
*
|
||||
* for each fragment passed in.
|
||||
*/
|
||||
const parent = getTypeCondition(src)
|
||||
|
||||
return `${parent} {
|
||||
...${(src.definitions[0] as FragmentDefinitionNode).name.value}
|
||||
}`
|
||||
})
|
||||
|
||||
const query = `
|
||||
query MountFragmentTest {
|
||||
${frags.join('\n')}
|
||||
}
|
||||
|
||||
${source.map(print)}
|
||||
`
|
||||
|
||||
const result = useQuery({
|
||||
query,
|
||||
})
|
||||
|
||||
return {
|
||||
gql: computed(() => result.data.value),
|
||||
}
|
||||
},
|
||||
render: (props) => {
|
||||
return props.gql ? options.render(props.gql) : h('div')
|
||||
},
|
||||
}), {
|
||||
global: {
|
||||
stubs: {
|
||||
transition: false,
|
||||
},
|
||||
plugins: [
|
||||
createI18n(),
|
||||
{
|
||||
install (app) {
|
||||
app.use(urql, testApolloClient({
|
||||
context: ctx,
|
||||
rootValue: options.type(ctx),
|
||||
}))
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}).then(() => ctx)
|
||||
}
|
||||
|
||||
Cypress.Commands.add('mountFragment', mountFragment)
|
||||
|
||||
Cypress.Commands.add('mountFragmentList', (source, options) => {
|
||||
return mountFragment(source, options, true)
|
||||
})
|
||||
Cypress.Commands.add('mountFragmentList', mountFragmentList)
|
||||
|
||||
type GetRootType<T> = T extends TypedDocumentNode<infer U, any>
|
||||
? U extends { __typename?: infer V }
|
||||
@@ -106,7 +166,7 @@ type MountFragmentConfig<T extends TypedDocumentNode> = {
|
||||
|
||||
type MountFragmentListConfig<T extends TypedDocumentNode> = {
|
||||
variables?: T['__variablesType']
|
||||
render: (frag: Exclude<T['__resultType'], undefined>[]) => JSX.Element
|
||||
render: (frag: Exclude<T['__resultType'], undefined>) => JSX.Element
|
||||
type: (ctx: ClientTestContext) => GetRootType<T>[]
|
||||
} & CyMountOptions<unknown>
|
||||
|
||||
@@ -127,8 +187,8 @@ declare global {
|
||||
/**
|
||||
* Mount helper for a component with a GraphQL fragment, as a list
|
||||
*/
|
||||
mountFragmentList<Result, Variables, T extends TypedDocumentNode<Result, Variables>>(
|
||||
fragment: T,
|
||||
mountFragmentList<Result, Variables, T extends TypedDocumentNode<Result, Variables>>(
|
||||
fragment: T[],
|
||||
config: MountFragmentListConfig<T>
|
||||
): Cypress.Chainable<ClientTestContext>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import ProgressCircle from './ProgressCircle.vue'
|
||||
|
||||
describe('<ProgressCircle />', { viewportWidth: 50, viewportHeight: 50 }, () => {
|
||||
it('playground', () => {
|
||||
cy.mount(() => (
|
||||
<ProgressCircle progress={57} radius={25} stroke={3} class="text-indigo-400" />
|
||||
))
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<svg
|
||||
:height="radius * 2"
|
||||
:width="radius * 2"
|
||||
>
|
||||
<circle
|
||||
stroke="#EBEBEB"
|
||||
fill="transparent"
|
||||
:stroke-width="stroke"
|
||||
:r="normalizedRadius"
|
||||
:cx="radius"
|
||||
:cy="radius"
|
||||
/>
|
||||
<circle
|
||||
stroke="currentColor"
|
||||
fill="transparent"
|
||||
:stroke-dasharray="circumference + ' ' + circumference"
|
||||
:style="{ strokeDashoffset }"
|
||||
:stroke-width="stroke"
|
||||
:r="normalizedRadius"
|
||||
:cx="radius"
|
||||
:cy="radius"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue"
|
||||
|
||||
const props = defineProps<{
|
||||
radius: number
|
||||
stroke: number
|
||||
progress: number
|
||||
}>()
|
||||
|
||||
const normalizedRadius = props.radius - props.stroke * 2;
|
||||
const circumference = normalizedRadius * 2 * Math.PI;
|
||||
|
||||
const strokeDashoffset = computed(() => {
|
||||
return circumference - props.progress / 100 * circumference;
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
svg{
|
||||
transform: rotate(-90deg);
|
||||
transform-origin: 50% 50%;
|
||||
}
|
||||
</style>
|
||||
@@ -2,7 +2,7 @@ import { ref } from 'vue'
|
||||
import SideBarItem from './SideBarItem.vue'
|
||||
import IconCoffee from 'virtual:vite-icons/mdi/coffee'
|
||||
|
||||
describe('<SideBarItem />', () => {
|
||||
describe('<SideBarItem />', { viewportWidth: 200, viewportHeight: 150 }, () => {
|
||||
it('playground', () => {
|
||||
const active = ref(0)
|
||||
const onClick = (value) => {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import RunCard from './RunCard.vue'
|
||||
|
||||
describe('<RunCard />', { viewportHeight: 400 }, () => {
|
||||
it('playground', () => {
|
||||
cy.mount(() => (
|
||||
<div class="bg-gray-100 h-screen p-3">
|
||||
<RunCard
|
||||
status="ko"
|
||||
name="Updating the hover state for the button component"
|
||||
branch="master"
|
||||
author="Ryan"
|
||||
timestamp={new Date().getTime()}
|
||||
results={{ pass: 5, fail: 0, skip: 0, flake: 2 }}/>
|
||||
<RunCard
|
||||
status="warn"
|
||||
name="Fixing broken tests"
|
||||
branch="master"
|
||||
author="Ryan"
|
||||
timestamp={new Date().getTime()}
|
||||
results={{ pass: 15, fail: 1, skip: 0, flake: 3 }}
|
||||
/>
|
||||
<RunCard
|
||||
status="ok"
|
||||
name="Adding a hover state to the button component"
|
||||
branch="master"
|
||||
author="Ryan"
|
||||
timestamp={new Date().getTime()}
|
||||
results={{ pass: 20, fail: 2, skip: 0, flake: 0 }}
|
||||
/>
|
||||
<RunCard
|
||||
status={25}
|
||||
name="In progress"
|
||||
branch="master"
|
||||
author="Bart"
|
||||
timestamp={new Date().getTime()}
|
||||
results={{ pass: 12, fail: 0, skip: 0, flake: 0 }}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<div class="h-18 border border-gray-200 rounded bg-white flex items-center mb-2 box-border">
|
||||
<div class="w-18 flex items-center justify-center">
|
||||
<RunIcon :status="props.status" />
|
||||
</div>
|
||||
<div class="pl-4 border-l border-gray-200 flex-grow">
|
||||
<h2 class="font-medium text-indigo-500 leading-4">{{ props.name }}</h2>
|
||||
<div class="flex">
|
||||
<span v-for="info in runInfo" class="flex items-center mr-3 mt-1">
|
||||
<component v-if="info.icon" :is="info.icon" class="mr-1 text-gray-500 text-sm" />
|
||||
<span class="text-gray-500 text-sm font-light">
|
||||
{{info.text}}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<RunResults v-bind="props.results" class="m-6 ml-0" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import RunIcon from './RunIcon.vue'
|
||||
import RunResults from './RunResults.vue'
|
||||
// bx:bx-user-circle
|
||||
import IconUserCircle from 'virtual:vite-icons/bx/bx-user-circle'
|
||||
// carbon:branch
|
||||
import IconBranch from 'virtual:vite-icons/carbon/branch'
|
||||
|
||||
const props = defineProps<{
|
||||
name: string
|
||||
status: "ok" | "ko" | "warn" | number
|
||||
author: string
|
||||
branch: string
|
||||
timestamp: number
|
||||
results: {
|
||||
flake: number
|
||||
skip: number
|
||||
pass: number
|
||||
fail: number
|
||||
}
|
||||
}>()
|
||||
|
||||
const runInfo = [{
|
||||
text: props.author,
|
||||
icon: IconUserCircle
|
||||
},
|
||||
{
|
||||
text: props.branch,
|
||||
icon: IconBranch
|
||||
},
|
||||
{
|
||||
text: new Date(props.timestamp).toLocaleTimeString()
|
||||
}]
|
||||
</script>
|
||||
@@ -0,0 +1,17 @@
|
||||
import RunIcon from './RunIcon.vue'
|
||||
|
||||
describe('<RunIcon />', { viewportWidth: 80, viewportHeight: 200 }, () => {
|
||||
it('playground', () => {
|
||||
cy.mount(() => (
|
||||
<div class="p-3 flex flex-col align-middle justify-center w-screen">
|
||||
<RunIcon status="ko"/>
|
||||
<hr/>
|
||||
<RunIcon status="ok"/>
|
||||
<hr/>
|
||||
<RunIcon status="warn"/>
|
||||
<hr/>
|
||||
<RunIcon status={45} />
|
||||
</div>
|
||||
))
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<IconPass v-if="props.status === 'ok'" class="text-green-500 text-xl" />
|
||||
<IconFail v-else-if="props.status === 'ko'" class="text-red-500 text-xl"/>
|
||||
<IconWarn v-else-if="props.status === 'warn'" class="text-orange-400 text-xl"/>
|
||||
<ProgressCircle v-else :progress="progress" :radius="12" :stroke="2" class="text-indigo-400"/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
// eva:checkmark-circle-2-fill
|
||||
import IconPass from 'virtual:vite-icons/eva/checkmark-circle-2-fill'
|
||||
// eva:close-circle-fill
|
||||
import IconFail from 'virtual:vite-icons/eva/close-circle-fill'
|
||||
// dashicons:warning
|
||||
import IconWarn from 'virtual:vite-icons/dashicons/warning'
|
||||
import ProgressCircle from "../components/progress/ProgressCircle.vue"
|
||||
|
||||
const props = defineProps<{
|
||||
status: "ok" | "ko" | "warn" | number
|
||||
}>()
|
||||
|
||||
const progress = typeof props.status === 'number' ? props.status : 0
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,9 @@
|
||||
import RunResults from './RunResults.vue'
|
||||
|
||||
const results = { pass: 5, fail: 0, skip: 0, flake: 2 }
|
||||
|
||||
describe('<RunResults />', () => {
|
||||
it('playground', () => {
|
||||
cy.mount(() => (<RunResults {...results}/>))
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div class="h-7 border border-gray-200 rounded flex text-gray-500" :class="class">
|
||||
<div class="flex items-center p-2">
|
||||
<IconFlake class="text-gray-400 text-sm mr-1" />
|
||||
{{props.flake}}
|
||||
</div>
|
||||
<div class="flex items-center p-2">
|
||||
<IconCancel class="text-gray-400 text-sm mr-1" />
|
||||
{{props.skip}}
|
||||
</div>
|
||||
<div class="flex items-center p-2">
|
||||
<IconPass class="text-green-600 text-xs mr-1" />
|
||||
{{props.pass}}
|
||||
</div>
|
||||
<div class="flex items-center p-2">
|
||||
<IconFail class="text-red-600 mr-1" />
|
||||
{{props.fail}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
// bi:check-lg
|
||||
import IconPass from 'virtual:vite-icons/bi/check-lg'
|
||||
// eva:close-fill
|
||||
import IconFail from 'virtual:vite-icons/eva/close-fill'
|
||||
// fa-solid:snowflake
|
||||
import IconFlake from 'virtual:vite-icons/fa-solid/snowflake'
|
||||
// line-md:cancel
|
||||
import IconCancel from 'virtual:vite-icons/line-md/cancel'
|
||||
|
||||
const props = defineProps<{
|
||||
flake: number
|
||||
skip: number
|
||||
pass: number
|
||||
fail: number
|
||||
class?: string
|
||||
}>()
|
||||
</script>
|
||||
@@ -0,0 +1,9 @@
|
||||
import RunsPage from './RunsPage.vue'
|
||||
|
||||
describe('<RunsPage />', () => {
|
||||
it('playground', () => {
|
||||
cy.mount(() => (
|
||||
<RunsPage />
|
||||
))
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<main class="min-w-650px max-w-800px">
|
||||
<RunCard v-for="run in runs" v-bind="run"/>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import RunCard from "./RunCard.vue";
|
||||
|
||||
// wire gql here
|
||||
const runs = []
|
||||
|
||||
</script>
|
||||
@@ -1,32 +1,37 @@
|
||||
import { ref } from 'vue'
|
||||
import { ConfigFileFragmentDoc } from '../generated/graphql'
|
||||
import {
|
||||
ConfigFileFragment,
|
||||
ConfigFileFragmentDoc,
|
||||
ProjectRootFragment,
|
||||
ProjectRootFragmentDoc,
|
||||
} from '../generated/graphql'
|
||||
import ConfigFile from './ConfigFile.vue'
|
||||
|
||||
describe('<ConfigFile />', () => {
|
||||
beforeEach(() => {
|
||||
const display = ref(false)
|
||||
|
||||
cy.mountFragment(ConfigFileFragmentDoc, {
|
||||
cy.mountFragmentList([
|
||||
ConfigFileFragmentDoc,
|
||||
ProjectRootFragmentDoc,
|
||||
], {
|
||||
type: (ctx) => {
|
||||
ctx.wizard.setFramework('nuxtjs')
|
||||
ctx.wizard.setFramework('cra')
|
||||
ctx.wizard.setBundler('webpack')
|
||||
|
||||
return ctx.wizard
|
||||
return [ctx.wizard, ctx.app]
|
||||
},
|
||||
render: (gqlVal) => (
|
||||
<div class="m-10">
|
||||
<button
|
||||
data-cy="show"
|
||||
onClick={() => {
|
||||
display.value = true
|
||||
}}
|
||||
class="hidden"
|
||||
></button>
|
||||
{display.value ? <ConfigFile gql={gqlVal} /> : undefined}
|
||||
</div>
|
||||
),
|
||||
})
|
||||
render: (gql) => {
|
||||
// @ts-ignore - TODO: fix types
|
||||
const wizard = gql.wizard as any as ConfigFileFragment
|
||||
// @ts-ignore - TODO: fix types
|
||||
const app = gql.app as any as ProjectRootFragment
|
||||
|
||||
cy.get('[data-cy="show"]').click({ force: true })
|
||||
return (
|
||||
<ConfigFile
|
||||
wizard={wizard}
|
||||
app={app}
|
||||
/>
|
||||
)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('playground', { viewportWidth: 1280, viewportHeight: 1024 }, () => {
|
||||
|
||||
@@ -85,7 +85,7 @@ mutation appCreateConfigFile($code: String!, $configFilename: String!) {
|
||||
|
||||
const props = defineProps<{
|
||||
wizard: ConfigFileFragment
|
||||
app: ProjectRootFragment | undefined
|
||||
app: ProjectRootFragment
|
||||
}>()
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,9 @@ describe('<InstallDependencies />', () => {
|
||||
|
||||
return ctx.wizard
|
||||
},
|
||||
render: (gqlVal) => <InstallDependencies gql={gqlVal} />,
|
||||
render: (gqlVal) => {
|
||||
return <InstallDependencies gql={gqlVal} />
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ export default defineComponent({
|
||||
return {
|
||||
loading: result.fetching,
|
||||
wizard: computed(() => result.data.value?.wizard),
|
||||
app: computed(() => result.data.value?.app)
|
||||
app: computed(() => result.data.value?.app!)
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user