Skip to content

misc: Assertion dropdown UI update #31598

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
May 5, 2025
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"devDependencies": {
"@cypress-design/icon-registry": "^1.5.1",
"@cypress-design/vue-button": "^1.6.0",
"@cypress-design/vue-icon": "^1.6.0",
"@cypress-design/vue-icon": "^1.18.0",
"@cypress-design/vue-spinner": "^1.0.0",
"@cypress-design/vue-statusicon": "^1.0.0",
"@cypress-design/vue-tabs": "^1.2.2",
Expand Down
58 changes: 58 additions & 0 deletions packages/app/src/runner/dom.cy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { getOrCreateHelperDom } from './dom'
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function seemed to be completely untested, so I added a test for this. Many of these runner 'helper' files are untested actually so not a lot of conventions to pull from.


describe('dom utilities', () => {
describe('getOrCreateHelperDom', () => {
let body: HTMLBodyElement
const className = 'test-helper'
const css = 'test-css'

beforeEach(() => {
// Create a fresh body element for each test
body = document.createElement('body')
document.body = body
})

afterEach(() => {
// Clean up after each test
const containers = body.querySelectorAll(`.${className}`)

containers.forEach((container) => container.remove())
})

it('should create new helper DOM elements when none exist', () => {
const result = getOrCreateHelperDom({ body, className, css })

// Verify container was created
expect(result.container).to.exist
expect(result.container.classList.contains(className)).to.be.true
expect(result.container.style.all).to.equal('initial')
expect(result.container.style.position).to.equal('static')

// Verify shadow root was created
expect(result.shadowRoot).to.exist
expect(result.shadowRoot!.mode).to.equal('open')

// Verify vue container was created
expect(result.vueContainer).to.exist
expect(result.vueContainer.classList.contains('vue-container')).to.be.true

// Verify style was added
const style = result.shadowRoot!.querySelector('style')

expect(style).to.exist
expect(style!.innerHTML).to.equal(css)
})

it('should return existing helper DOM elements when they exist', () => {
// First call to create elements
const firstResult = getOrCreateHelperDom({ body, className, css })

// Second call to get existing elements
const secondResult = getOrCreateHelperDom({ body, className, css })

// Verify we got the same elements back
expect(secondResult.container).to.equal(firstResult.container)
expect(secondResult.vueContainer).to.equal(firstResult.vueContainer)
})
})
})
2 changes: 2 additions & 0 deletions packages/app/src/runner/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ export function getOrCreateHelperDom ({ body, className, css }) {

container.classList.add(className)

// NOTE: This is needed to prevent the container from inheriting styles from the body of the AUT
container.style.all = 'initial'
container.style.position = 'static'

body.appendChild(container)
Expand Down
97 changes: 69 additions & 28 deletions packages/app/src/runner/studio/AssertionOptions.ce.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,32 @@
<div
ref="popper"
class="assertion-options"
data-cy="assertion-options"
>
<div
v-for="{ name, value } in options"
:key="`${name}${value}`"
v-for="option in options"
:key="getOptionKey(option)"
class="assertion-option"
@click.stop="() => onClick(name, value)"
data-cy="assertion-option"
tabindex="0"
role="button"
@keydown.enter="handleOptionClick(option)"
@keydown.space="handleOptionClick(option)"
@click.stop="handleOptionClick(option)"
>
<span
v-if="name"
v-if="option.name"
class="assertion-option-name"
data-cy="assertion-option-name"
>
{{ truncate(name) }}:{{ ' ' }}
{{ truncate(option.name) }}:{{ ' ' }}
</span>
<span
v-else
class="assertion-option-value"
data-cy="assertion-option-value"
>
{{ typeof value === 'string' && truncate(value) }}
{{ typeof option.value === 'string' && truncate(option.value) }}
</span>
</div>
</div>
Expand All @@ -30,45 +38,60 @@ import { createPopper } from '@popperjs/core'
import { onMounted, ref, nextTick, Ref } from 'vue'
import type { AssertionOption } from './types'

const props = defineProps<{
interface Props {
type: string
options: AssertionOption[]
}>()
}

const props = defineProps<Props>()

const emit = defineEmits<{
(eventName: 'addAssertion', value: { type: string, name: string, value: string })
(eventName: 'setPopperElement', value: HTMLElement)
}>()

const truncate = (str: string) => {
if (str && str.length > 80) {
return `${str.substr(0, 77)}...`
const popper: Ref<HTMLElement | null> = ref(null)

const TRUNCATE_LENGTH = 80
const TRUNCATE_SUFFIX = '...'

const truncate = (str: string): string => {
if (!str || str.length <= TRUNCATE_LENGTH) {
return str
}

return str
return `${str.substring(0, TRUNCATE_LENGTH - TRUNCATE_SUFFIX.length)}${TRUNCATE_SUFFIX}`
}

const popper: Ref<HTMLElement | null> = ref(null)
const getOptionKey = (option: AssertionOption): string => {
return `${option.name}${option.value}`
}

onMounted(() => {
nextTick(() => {
const popperEl = popper.value as HTMLElement
const reference = popperEl.parentElement as HTMLElement
const handleOptionClick = (option: AssertionOption): void => {
emit('addAssertion', {
type: props.type,
name: option.name || '',
value: String(option.value || ''),
})
}

createPopper(reference, popperEl, {
placement: 'right-start',
})
const initializePopper = (): void => {
const popperEl = popper.value as HTMLElement
const reference = popperEl.parentElement as HTMLElement

emit('setPopperElement', popperEl)
createPopper(reference, popperEl, {
placement: 'right-start',
})
})

const onClick = (name, value) => {
emit('addAssertion', { type: props.type, name, value })
emit('setPopperElement', popperEl)
}

onMounted(() => {
nextTick(initializePopper)
})
</script>

<style lang="scss">
<style scoped lang="scss">
@import './assertions-style.scss';

.assertion-options {
Expand All @@ -79,17 +102,35 @@ const onClick = (name, value) => {
overflow: hidden;
overflow-wrap: break-word;
position: absolute;
right: 8px;
border-radius: 4px;

.assertion-option {
font-size: 14px;
cursor: pointer;
padding: 0.4rem 0.6rem;
border: 1px solid transparent;

&:first-of-type {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}

&:last-of-type {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}

&:hover {
background-color: #e9ecef;
background-color: $gray-1000;
border: 1px solid $gray-950;
}

.assertion-option-value {
font-weight: 600;
&:focus {
background-color: $gray-950;
color: $indigo-300;
outline: none;
@include box-shadow;
}
}
}
Expand Down
52 changes: 30 additions & 22 deletions packages/app/src/runner/studio/AssertionType.ce.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
<template>
<div
:class="['assertion-type', { 'single-assertion': !hasOptions }]"
tabindex="0"
role="button"
:aria-expanded="isOpen"
:aria-haspopup="hasOptions"
@click.stop="onClick"
@mouseover.stop="onOpen"
@mouseout.stop="onClose"
@focus="onOpen"
@blur="onClose"
@keydown.enter="onClick"
@keydown.space="onClick"
>
<div class="assertion-type-text">
<span>
Expand All @@ -13,24 +21,13 @@
v-if="hasOptions"
class="dropdown-arrow"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="12"
height="12"
fill="currentColor"
viewBox="0 0 16 16"
>
<path
fillRule="evenodd"
d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"
/>
</svg>
<IconChevronRightMedium />
</span>
</div>
<AssertionOptions
v-if="hasOptions && isOpen"
:type="type"
:options="options"
:options="options || []"
@set-popper-element="setPopperElement"
@add-assertion="addAssertion"
/>
Expand All @@ -40,10 +37,12 @@
<script lang="ts" setup>
import { Ref, ref } from 'vue'
import AssertionOptions from './AssertionOptions.ce.vue'
import { IconChevronRightMedium } from '@cypress-design/vue-icon'
import type { AssertionType } from './types'

const props = defineProps<{
type: string
options: any
type: AssertionType['type']
options: AssertionType['options']
}>()

const emit = defineEmits<{
Expand All @@ -58,7 +57,7 @@ const onOpen = () => {
isOpen.value = true
}

const onClose = (e: MouseEvent) => {
const onClose = (e: MouseEvent | FocusEvent) => {
if (e.relatedTarget instanceof Element &&
popperElement.value && popperElement.value.contains(e.relatedTarget)) {
return
Expand All @@ -82,38 +81,47 @@ const addAssertion = ({ type, name, value }) => {
}
</script>

<style lang="scss">
<style scoped lang="scss">
@import './assertions-style.scss';

.assertion-type {
color: #202020;
cursor: default;
font-size: 14px;
padding: 0.4rem 0.4rem 0.4rem 0.7rem;
position: static;
outline: none;
border-radius: 4px;
border: 1px solid transparent;

&:first-of-type {
padding-top: 0.5rem;
}

&:last-of-type {
border-bottom-left-radius: $border-radius;
border-bottom-right-radius: $border-radius;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
padding-bottom: 0.5rem;
}

&:hover {
background-color: #e9ecef;
background-color: $gray-1000;
border: 1px solid $gray-950;
}

&:focus {
color: $indigo-300;
outline: none;
@include box-shadow;
}

&.single-assertion {
cursor: pointer;
font-weight: 600;
}

.assertion-type-text {
align-items: center;
display: flex;
cursor: pointer;

.dropdown-arrow {
margin-left: auto;
Expand Down
Loading
Loading