feat: rework selector playground ui (#20076)

This commit is contained in:
Zachary Williams
2022-02-10 12:45:53 -06:00
committed by GitHub
parent a015397f57
commit 87b7a6db66
12 changed files with 303 additions and 122 deletions
@@ -2,6 +2,7 @@ import 'regenerator-runtime/runtime'
import 'cypress-real-events/support'
import '@percy/cypress'
import './storybook'
import 'normalize.css/normalize.css'
// Need to register these once per app. Depending which components are consumed
// from @cypress/design-system, different icons are required.
-2
View File
@@ -4,8 +4,6 @@
@use 'derived/export.scss';
@use 'typography';
@use 'normalize.css/normalize.css';
// probably should leave this for the consumer to set?
body, html {
font-size: typography.text(m);
@@ -29,6 +29,29 @@ describe('Cypress In Cypress', { viewportWidth: 1200 }, () => {
.should('be.visible')
cy.percySnapshot('viewport info open')
cy.get('body').click()
cy.findByTestId('playground-activator').click()
cy.findByTestId('playground-selector').clear().type('#__cy_root')
cy.percySnapshot('cy.get selector')
cy.findByTestId('playground-num-elements').contains('1 Match')
cy.window().then((win) => cy.spy(win.console, 'log'))
cy.findByTestId('playground-print').click().window().then((win) => {
expect(win.console.log).to.have.been.calledWith('%cCommand: ', 'font-weight: bold', 'cy.get(\'#__cy_root\')')
})
cy.findByLabelText('Selector Methods').click()
cy.findByRole('menuitem', { name: 'cy.contains' }).click()
cy.findByTestId('playground-selector').clear().type('Component Test')
cy.percySnapshot('cy.contains selector')
cy.findByTestId('playground-num-elements').contains('1 Match')
})
it('navigation between specs and other parts of the app works', () => {
@@ -29,6 +29,29 @@ describe('Cypress In Cypress', { viewportWidth: 1200 }, () => {
.should('be.visible')
cy.percySnapshot('viewport info open')
cy.get('body').click()
cy.findByTestId('playground-activator').click()
cy.findByTestId('playground-selector').clear().type('li')
cy.percySnapshot('cy.get selector')
cy.findByTestId('playground-num-elements').contains('3 Matches')
cy.findByLabelText('Selector Methods').click()
cy.findByRole('menuitem', { name: 'cy.contains' }).click()
cy.findByTestId('playground-selector').clear().type('Item 1')
cy.percySnapshot('cy.contains selector')
cy.findByTestId('playground-num-elements').contains('1 Match')
cy.window().then((win) => cy.spy(win.console, 'log'))
cy.findByTestId('playground-print').click().window().then((win) => {
expect(win.console.log).to.have.been.calledWith('%cCommand: ', 'font-weight: bold', 'cy.contains(\'Item 1\')')
})
})
it('navigation between specs and other parts of the app works', () => {
+13 -17
View File
@@ -1,10 +1,10 @@
<template>
<div
id="spec-runner-header"
class="min-h-64px px-16px text-14px"
class="min-h-64px text-14px"
:style="{ width: `${props.width}px` }"
>
<div class="flex flex-grow flex-wrap py-16px gap-12px justify-end">
<div class="flex flex-wrap justify-end flex-grow p-16px gap-12px">
<!--
TODO: Studio. Out of scope for GA.
<Button
@@ -20,7 +20,7 @@
<div
v-if="props.gql.currentTestingType === 'e2e'"
data-cy="aut-url"
class="border rounded flex flex-grow border-1px border-gray-100 h-32px align-middle overflow-hidden"
class="flex flex-grow overflow-hidden align-middle border border-gray-100 rounded border-1px h-32px"
:class="{
'bg-gray-50': autStore.isLoadingUrl
}"
@@ -28,7 +28,7 @@
<Button
data-cy="playground-activator"
:disabled="isDisabled"
class="rounded-none border-r-1px border-gray-100 mr-12px"
class="border-gray-100 rounded-none border-r-1px mr-12px"
variant="text"
@click="togglePlayground"
>
@@ -45,7 +45,7 @@
<Button
data-cy="playground-activator"
:disabled="isDisabled"
class=" border-gray-100 mr-12px"
class="border-gray-100 mr-12px"
variant="outline"
@click="togglePlayground"
>
@@ -65,7 +65,7 @@
</template>
<template #default>
<div class="max-h-50vh overflow-auto">
<div class="overflow-auto max-h-50vh">
<VerticalBrowserListItems
:gql="props.gql"
:spec-path="activeSpecPath"
@@ -82,13 +82,13 @@
<span class="whitespace-nowrap">{{ autStore.viewportWidth }}x{{ autStore.viewportHeight }}</span>
<span
v-if="displayScale"
class="-ml-6px text-gray-500"
class="text-gray-500 -ml-6px"
>
({{ displayScale }})
</span>
</template>
<template #default>
<div class="max-h-50vw p-16px text-gray-600 leading-24px w-400px overflow-auto">
<div class="overflow-auto text-gray-600 max-h-50vw p-16px leading-24px w-400px">
<!-- TODO: This copy is a placeholder based on the old message for this, we should confirm the exact copy and then move to i18n -->
<p class="mb-16px">
The
@@ -103,7 +103,7 @@
</p>
<ShikiHighlight
class="rounded border-1 border-gray-200 mb-16px"
class="border-gray-200 rounded border-1 mb-16px"
lang="javascript"
:code="code"
/>
@@ -117,15 +117,11 @@
</SpecRunnerDropdown>
</div>
<div
<SelectorPlayground
v-if="selectorPlaygroundStore.show"
class="mt-8px"
>
<SelectorPlayground
:get-aut-iframe="getAutIframe"
:event-manager="eventManager"
/>
</div>
:get-aut-iframe="getAutIframe"
:event-manager="eventManager"
/>
</div>
</template>
@@ -20,7 +20,7 @@
v-if="props.gql.currentProject"
v-show="runnerUiStore.isSpecsListOpen"
id="inline-spec-list"
class="h-full bg-gray-1000 border-r-1 border-gray-900 force-dark"
class="h-full border-gray-900 bg-gray-1000 border-r-1 force-dark"
:class="{'pointer-events-none': isDragging}"
>
<InlineSpecList
@@ -11,10 +11,12 @@ describe('SelectorPlayground', () => {
return {
autIframe,
element: cy.mount(() => (
<SelectorPlayground
eventManager={eventManager}
getAutIframe={() => autIframe}
/>
<div class="py-64px">
<SelectorPlayground
eventManager={eventManager}
getAutIframe={() => autIframe}
/>
</div>
)),
}
}
@@ -23,7 +25,7 @@ describe('SelectorPlayground', () => {
const { autIframe } = mountSelectorPlayground()
cy.spy(autIframe, 'toggleSelectorHighlight')
cy.get('[data-cy="playground-method"]').should('contain', 'cy.get')
cy.get('[data-cy="selected-playground-method"]').should('contain', 'cy.get')
cy.get('[data-cy="playground-selector"]').should('have.value', 'body')
cy.percySnapshot()
@@ -53,27 +55,42 @@ describe('SelectorPlayground', () => {
cy.spy(autIframe, 'toggleSelectorHighlight')
expect(selectorPlaygroundStore.method).to.eq('get')
cy.get('[data-cy="playground-method"]').as('method')
cy.get('@method').contains('cy.get').click()
cy.get('li').contains('cy.contains').click().then(() => {
cy.get('[aria-label="Selector Methods"]').click()
cy.findByRole('menuitem', { name: 'cy.contains' }).click().then(() => {
expect(selectorPlaygroundStore.method).to.eq('contains')
expect(autIframe.toggleSelectorHighlight).to.have.been.called
cy.get('@method').contains('cy.contains')
})
cy.get('[data-cy="selected-playground-method"]').should('contain', 'cy.contains')
})
it('shows query and number of found elements', () => {
const selectorPlaygroundStore = useSelectorPlaygroundStore()
selectorPlaygroundStore.setNumElements(10)
selectorPlaygroundStore.setNumElements(0)
mountSelectorPlayground()
cy.get('[data-cy="playground-num-elements"]').contains('10')
cy.get('[data-cy="playground-num-elements"]').contains('No Matches')
cy.then(() => selectorPlaygroundStore.setNumElements(1))
cy.get('[data-cy="playground-num-elements"]').contains('1 Match')
cy.then(() => selectorPlaygroundStore.setNumElements(10))
cy.get('[data-cy="playground-num-elements"]').contains('10 Matches')
cy.percySnapshot()
cy.then(() => selectorPlaygroundStore.setValidity(false))
cy.get('[data-cy="playground-num-elements"]').contains('Invalid')
cy.percySnapshot('Invalid playground selector')
})
it('focuses and copies selector text', () => {
// TODO: UNIFY-999 Solve "write permission denied" error to test this in run mode
it.skip('focuses and copies selector text', () => {
const { autIframe } = mountSelectorPlayground()
cy.spy(autIframe, 'toggleSelectorHighlight')
@@ -83,10 +100,10 @@ describe('SelectorPlayground', () => {
cy.get('@copy').click()
cy.get('@copy').should('be.focused')
cy.spy(document, 'execCommand')
cy.get('[data-cy="playground-copy"]').click().then(() => {
expect(document.execCommand).to.be.calledWith('copy')
})
cy.spy(navigator.clipboard, 'writeText').as('clipboardSpy')
cy.get('[data-cy="playground-copy"]').click()
cy.get('[data-cy="playground-copy-tooltip"]').should('be.visible').contains('Copied to clipboard')
cy.get('@clipboardSpy').should('have.been.called')
})
it('prints nothing to console when no selected elements found', () => {
@@ -101,6 +118,8 @@ describe('SelectorPlayground', () => {
Yielded: 'Nothing',
})
})
cy.get('[data-cy="playground-print-tooltip"]').should('be.visible').contains('Printed to console')
})
it('prints elements when selected elements found', () => {
@@ -1,83 +1,129 @@
<template>
<div
id="selector-playground"
class="bg-white flex items-center"
class="border-t border-b bg-gray-50 border-gray-200 h-56px grid py-12px px-16px gap-12px grid-cols-[40px,1fr,auto] items-center "
>
<button
:class="{ 'bg-blue-100': selectorPlaygroundStore.isEnabled }"
class="rounded-md h-full px-8px"
class="flex items-center justify-center h-full text-white transition duration-150 border rounded-md outline-none w-40px hover:default-ring"
:class="[ selectorPlaygroundStore.isEnabled ? 'default-ring' : 'border-gray-200']"
data-cy="playground-toggle"
@click="toggleEnabled"
>
<Icon
:icon="IconCursorDefaultOutline"
height="18px"
width="18px"
/>
<i-cy-selector_x16 :class="{ 'icon-dark-indigo-500': selectorPlaygroundStore.isEnabled, 'icon-dark-gray-500': !selectorPlaygroundStore.isEnabled }" />
</button>
<div
class="flex h-full flex-1 mx-2 items-center"
class="relative flex items-center flex-1 w-full h-full"
@mouseover="setShowingHighlight"
>
<Select
:options="methods"
item-value="display"
data-cy="playground-method"
:model-value="{
value: selectorPlaygroundStore.method,
display: `cy.${selectorPlaygroundStore.method}`
}"
class="w-120px"
@update:model-value="({ value }) => selectorPlaygroundStore.setMethod(value)"
/>
<span class="mx-5px">('</span>
<input
ref="copyText"
v-model="selector"
class="border rounded-md border-gray-500 flex-1 py-8px px-1 pl-4 text-blue-500"
data-cy="playground-selector"
>
')
<Menu #="{ open }">
<MenuButton
:aria-label="t('runner.selectorPlayground.selectorMethodsLabel')"
class="flex items-center justify-center h-full text-white border border-gray-200 outline-none rounded-l-md w-40px hocus-default border-r-transparent"
@click.stop
>
<i-cy-chevron-down-small_x16
class="transition duration-300 transition-color"
:class="open ? 'icon-dark-indigo-500' : 'icon-dark-gray-500'"
/>
</MenuButton>
<MenuItems
class="absolute z-40 flex flex-col overflow-scroll text-white bg-gray-900 rounded outline-transparent top-34px"
>
<MenuItem
v-for="method in methods"
:key="method.display"
#="{ active }"
>
<button
:class="{ 'bg-gray-700': active }"
class="text-left border-b border-b-gray-800 py-8px px-16px"
@click="selectorPlaygroundStore.setMethod(method.value)"
>
{{ method.display }}
</button>
</MenuItem>
</MenuItems>
</Menu>
<code class="relative flex-1 h-full">
<span
ref="ghostLeft"
class="absolute inset-y-0 flex items-center text-gray-600 pointer-events-none pl-12px"
data-cy="selected-playground-method"
>
<span class="text-gray-800">cy</span>.<span class="text-purple-500">{{ selectorPlaygroundStore.method }}</span>(
</span>
<span
ref="ghostRight"
class="font-medium left-[-9999px] absolute inline-block"
>{{ selector.replace(/\s/g, '&nbsp;') }}</span>
<span
class="absolute inset-y-0 flex items-center text-gray-600 pointer-events-none"
:style="{left: inputRightOffset + 'px'}"
>)</span>
<input
ref="copyText"
v-model="selector"
data-cy="playground-selector"
:style="{paddingLeft: inputLeftOffset + 'px', paddingRight: matcherWidth + 32 + 24 + 'px'}"
class="w-full h-full font-medium text-indigo-500 border border-gray-200 outline-none rounded-r-md hocus-default overflow-ellipsis"
:class="{'hocus-default': selectorPlaygroundStore.isValid, 'hocus-error': !selectorPlaygroundStore.isValid}"
>
<div
ref="match"
class="absolute inset-y-0 right-0 flex items-center font-sans text-gray-600 border-l border-l-gray-200 my-6px px-16px"
data-cy="playground-num-elements"
>
<template v-if="!selectorPlaygroundStore.isValid">
<span class="text-error-400">{{ t('runner.selectorPlayground.invalidSelector') }}</span>
</template>
<template v-else>
{{ t('runner.selectorPlayground.matches', selectorPlaygroundStore.numElements) }}
</template>
</div>
</code>
</div>
<div
data-cy="playground-num-elements"
class="rounded-md flex h-full bg-gray-400 mx-1 text-white px-3 items-center"
>
{{ selectorPlaygroundStore.numElements }}
<div class="flex gap-12px">
<SelectorPlaygroundTooltip v-if="isSupported">
<Button
size="md"
variant="outline"
data-cy="playground-copy"
class="override-border"
@click="copyToClipboard"
>
<i-cy-copy-clipboard_x16 class="icon-dark-gray-500" />
</Button>
<template #popper>
<div
class="whitespace-nowrap"
data-cy="playground-copy-tooltip"
>
{{ t('runner.selectorPlayground.copyTooltip') }}
</div>
</template>
</SelectorPlaygroundTooltip>
<SelectorPlaygroundTooltip>
<Button
size="md"
variant="outline"
data-cy="playground-print"
class="override-border"
@click="printSelected"
>
<i-cy-technology-terminal_x16 class="icon-dark-gray-600" />
</Button>
<template #popper>
<div
class="whitespace-nowrap"
data-cy="playground-print-tooltip"
>
{{ t('runner.selectorPlayground.printTooltip') }}
</div>
</template>
</SelectorPlaygroundTooltip>
</div>
<div class="border rounded-md flex h-full divide-x-1 divide-gray-500 border-1 border-gray-500 mr-10px items-center">
<button
data-cy="playground-copy"
class="h-full px-8px"
@click="copySelector"
>
<Icon :icon="IconCopy" />
</button>
<button
data-cy="playground-print"
class="h-full px-8px"
@click="printSelected"
>
<Icon :icon="IconConsoleLine" />
</button>
</div>
<a
class="flex text-blue-500 items-center"
href="https://on.cypress.io/selector-playground"
target="_blank"
rel="noreferrer"
>
<span class="mr-5px">
<Icon :icon="IconHelpCircle" />
</span>
Learn more
</a>
</div>
</template>
@@ -86,13 +132,13 @@ import { computed, ref, watch } from 'vue'
import { useSelectorPlaygroundStore } from '../../store/selector-playground-store'
import type { AutIframe } from '../aut-iframe'
import type { EventManager } from '../event-manager'
import IconCopy from '~icons/mdi/content-copy'
import Icon from '@packages/frontend-shared/src/components/Icon.vue'
import IconCursorDefaultOutline from '~icons/mdi/cursor-default-outline'
import IconHelpCircle from '~icons/mdi/help-circle'
import SelectorPlaygroundSelectMethod from './SelectorPlaygroundSelectMethod.vue'
import Select from '@packages/frontend-shared/src/components/Select.vue'
import IconConsoleLine from '~icons/mdi/console-line'
import Button from '@packages/frontend-shared/src/components/Button.vue'
import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue'
import { useClipboard, useElementSize } from '@vueuse/core'
import SelectorPlaygroundTooltip from './SelectorPlaygroundTooltip.vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const props = defineProps<{
eventManager: EventManager
@@ -107,9 +153,11 @@ const methods = [
display: 'cy.contains',
value: 'contains',
},
]
] as const
const selectorPlaygroundStore = useSelectorPlaygroundStore()
const match = ref<HTMLDivElement>()
const { width: matcherWidth } = useElementSize(match)
const copyText = ref<HTMLInputElement>()
@@ -136,6 +184,23 @@ const selector = computed({
},
})
const inputSize = useElementSize(copyText)
// spooky
const ghostLeft = ref<HTMLSpanElement>()
const { width: ghostLeftWidth } = useElementSize(ghostLeft)
const inputLeftOffset = computed(() => ghostLeftWidth.value + 12)
const ghostRight = ref<HTMLSpanElement>()
const { width: ghostRightWidth } = useElementSize(ghostRight)
const inputRightOffset = computed(() => {
const leftOffset = inputLeftOffset.value
const combined = leftOffset + ghostRightWidth.value
const max = inputSize.width.value + leftOffset
return combined <= max ? combined : max
})
function setShowingHighlight () {
selectorPlaygroundStore.setShowingHighlight(true)
props.getAutIframe().toggleSelectorHighlight(true)
@@ -153,19 +218,9 @@ function printSelected () {
props.getAutIframe().printSelectorElementsToConsole()
}
function copySelector () {
try {
copyText.value?.select()
const successful = document.execCommand('copy')
if (successful) {
// Copied!
} else {
// Oops, unable to copy
}
} catch (e) {
// Oops, unable to copy
}
const { copy, isSupported } = useClipboard({ copiedDuring: 2000 })
const copyToClipboard = () => {
copy(selector.value)
}
</script>
@@ -173,4 +228,13 @@ function copySelector () {
#selector-playground {
height: 40px;
}
button.override-border {
@apply border-gray-200
}
button.override-border:hover {
@apply border-indigo-300
}
button.override-border:focus {
@apply border-indigo-300
}
</style>
@@ -0,0 +1,42 @@
<template>
<div
class="relative"
@click="start()"
>
<slot />
<div
ref="tooltip"
class="rounded flex bg-gray-900 text-center opacity-0 p-8px transform transition-opacity top-0 left-[50%] text-gray-300 translate-y-[calc(-100%-4px)] translate-x-[-50%] duration-250 absolute pointer-events-none"
role="tooltip"
:class="[tooltipClass, {'opacity-100': isPending}]"
>
<slot name="popper" />
</div>
</div>
</template>
<script lang="ts" setup>
import { useTimeoutFn } from '@vueuse/core'
import { computed, ref } from 'vue'
const tooltip = ref<HTMLDivElement>()
const tooltipClass = computed(() => {
if (!tooltip.value) return ''
const { right } = tooltip.value.getBoundingClientRect()
const outerWidth = window.outerWidth
return (right > outerWidth) ? 'tooltip-overflow' : ''
})
const { start, isPending } = useTimeoutFn(() => {}, 2000, { immediate: false })
</script>
<style scoped>
/* Created a class to override !important via greater specificity */
div.tooltip-overflow {
@apply left-auto right-0 translate-x-0
}
</style>
+4
View File
@@ -38,9 +38,13 @@ export const togglePlayground = (autIframe: AutIframe) => {
selectorPlaygroundStore.setShow(false)
autIframe.toggleSelectorPlayground(false)
selectorPlaygroundStore.setEnabled(false)
selectorPlaygroundStore.setShowingHighlight(false)
autIframe.toggleSelectorHighlight(false)
} else {
selectorPlaygroundStore.setShow(true)
autIframe.toggleSelectorPlayground(true)
selectorPlaygroundStore.setEnabled(true)
selectorPlaygroundStore.setShowingHighlight(true)
autIframe.toggleSelectorHighlight(true)
}
}
@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 5V4C14 2.89543 13.1046 2 12 2H4C2.89543 2 2 2.89543 2 4V12C2 13.1046 2.89543 14 4 14H5" stroke="#4956E3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon-dark"/>
<path d="M10 11L13 14L14 13L11 10L12.5 8.5L7 7L8.5 12.5L10 11Z" fill="#4956E3" stroke="#4956E3" stroke-width="2" stroke-linejoin="round" class="icon-dark"/>
</svg>

After

Width:  |  Height:  |  Size: 462 B

@@ -628,6 +628,13 @@
"defaultTitle": "DOM Snapshot",
"pinnedTitle": "Pinned",
"studioActiveError": "Cannot show Snapshot while creating commands in Studio"
},
"selectorPlayground": {
"matches": "No Matches | {n} Match | {n} Matches",
"copyTooltip": "Copied to clipboard",
"printTooltip": "Printed to console",
"invalidSelector": "Invalid",
"selectorMethodsLabel": "Selector Methods"
}
}
}