Skip to content

Commit 53f7aae

Browse files
misc: Assertion dropdown UI update (#31598)
* Refactor existing mounter of assertions - make a CT test for it * update more styles + add a11y features * bump vue-icons * Finish last styles and icons * Fix logic * bump shared package icons * fix test * update css to be scoped + set all css to initial to clear AUT styles * add test for getOrCreateHelperDom function * update tabbing test * add changelog * update changelog to include addressed in
1 parent acaaf30 commit 53f7aae

14 files changed

+456
-184
lines changed

cli/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ _Released 5/6/2025 (PENDING)_
1414

1515
**Misc:**
1616

17+
- The Assertions menu when you right click in `experimentalStudio` tests now displays in dark mode. Addresses [#10621](https://github.com/cypress-io/cypress-services/issues/10621). Addressed in [#31598](https://github.com/cypress-io/cypress/pull/31598).
1718
- The URL in the Cypress App no longer displays a white background when the URL is loading. Fixes [#31556](https://github.com/cypress-io/cypress/issues/31556).
1819

1920
## 14.3.2

packages/app/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
"devDependencies": {
2525
"@cypress-design/icon-registry": "^1.5.1",
2626
"@cypress-design/vue-button": "^1.6.0",
27-
"@cypress-design/vue-icon": "^1.6.0",
27+
"@cypress-design/vue-icon": "^1.18.0",
2828
"@cypress-design/vue-spinner": "^1.0.0",
2929
"@cypress-design/vue-statusicon": "^1.0.0",
3030
"@cypress-design/vue-tabs": "^1.2.2",

packages/app/src/runner/dom.cy.tsx

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { getOrCreateHelperDom } from './dom'
2+
3+
describe('dom utilities', () => {
4+
describe('getOrCreateHelperDom', () => {
5+
let body: HTMLBodyElement
6+
const className = 'test-helper'
7+
const css = 'test-css'
8+
9+
beforeEach(() => {
10+
// Create a fresh body element for each test
11+
body = document.createElement('body')
12+
document.body = body
13+
})
14+
15+
afterEach(() => {
16+
// Clean up after each test
17+
const containers = body.querySelectorAll(`.${className}`)
18+
19+
containers.forEach((container) => container.remove())
20+
})
21+
22+
it('should create new helper DOM elements when none exist', () => {
23+
const result = getOrCreateHelperDom({ body, className, css })
24+
25+
// Verify container was created
26+
expect(result.container).to.exist
27+
expect(result.container.classList.contains(className)).to.be.true
28+
expect(result.container.style.all).to.equal('initial')
29+
expect(result.container.style.position).to.equal('static')
30+
31+
// Verify shadow root was created
32+
expect(result.shadowRoot).to.exist
33+
expect(result.shadowRoot!.mode).to.equal('open')
34+
35+
// Verify vue container was created
36+
expect(result.vueContainer).to.exist
37+
expect(result.vueContainer.classList.contains('vue-container')).to.be.true
38+
39+
// Verify style was added
40+
const style = result.shadowRoot!.querySelector('style')
41+
42+
expect(style).to.exist
43+
expect(style!.innerHTML).to.equal(css)
44+
})
45+
46+
it('should return existing helper DOM elements when they exist', () => {
47+
// First call to create elements
48+
const firstResult = getOrCreateHelperDom({ body, className, css })
49+
50+
// Second call to get existing elements
51+
const secondResult = getOrCreateHelperDom({ body, className, css })
52+
53+
// Verify we got the same elements back
54+
expect(secondResult.container).to.equal(firstResult.container)
55+
expect(secondResult.vueContainer).to.equal(firstResult.vueContainer)
56+
})
57+
})
58+
})

packages/app/src/runner/dom.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ export function getOrCreateHelperDom ({ body, className, css }) {
3030

3131
container.classList.add(className)
3232

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

3537
body.appendChild(container)

packages/app/src/runner/studio/AssertionOptions.ce.vue

Lines changed: 69 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,32 @@
22
<div
33
ref="popper"
44
class="assertion-options"
5+
data-cy="assertion-options"
56
>
67
<div
7-
v-for="{ name, value } in options"
8-
:key="`${name}${value}`"
8+
v-for="option in options"
9+
:key="getOptionKey(option)"
910
class="assertion-option"
10-
@click.stop="() => onClick(name, value)"
11+
data-cy="assertion-option"
12+
tabindex="0"
13+
role="button"
14+
@keydown.enter="handleOptionClick(option)"
15+
@keydown.space="handleOptionClick(option)"
16+
@click.stop="handleOptionClick(option)"
1117
>
1218
<span
13-
v-if="name"
19+
v-if="option.name"
1420
class="assertion-option-name"
21+
data-cy="assertion-option-name"
1522
>
16-
{{ truncate(name) }}:{{ ' ' }}
23+
{{ truncate(option.name) }}:{{ ' ' }}
1724
</span>
1825
<span
1926
v-else
2027
class="assertion-option-value"
28+
data-cy="assertion-option-value"
2129
>
22-
{{ typeof value === 'string' && truncate(value) }}
30+
{{ typeof option.value === 'string' && truncate(option.value) }}
2331
</span>
2432
</div>
2533
</div>
@@ -30,45 +38,60 @@ import { createPopper } from '@popperjs/core'
3038
import { onMounted, ref, nextTick, Ref } from 'vue'
3139
import type { AssertionOption } from './types'
3240
33-
const props = defineProps<{
41+
interface Props {
3442
type: string
3543
options: AssertionOption[]
36-
}>()
44+
}
45+
46+
const props = defineProps<Props>()
3747
3848
const emit = defineEmits<{
3949
(eventName: 'addAssertion', value: { type: string, name: string, value: string })
4050
(eventName: 'setPopperElement', value: HTMLElement)
4151
}>()
4252
43-
const truncate = (str: string) => {
44-
if (str && str.length > 80) {
45-
return `${str.substr(0, 77)}...`
53+
const popper: Ref<HTMLElement | null> = ref(null)
54+
55+
const TRUNCATE_LENGTH = 80
56+
const TRUNCATE_SUFFIX = '...'
57+
58+
const truncate = (str: string): string => {
59+
if (!str || str.length <= TRUNCATE_LENGTH) {
60+
return str
4661
}
4762
48-
return str
63+
return `${str.substring(0, TRUNCATE_LENGTH - TRUNCATE_SUFFIX.length)}${TRUNCATE_SUFFIX}`
4964
}
5065
51-
const popper: Ref<HTMLElement | null> = ref(null)
66+
const getOptionKey = (option: AssertionOption): string => {
67+
return `${option.name}${option.value}`
68+
}
5269
53-
onMounted(() => {
54-
nextTick(() => {
55-
const popperEl = popper.value as HTMLElement
56-
const reference = popperEl.parentElement as HTMLElement
70+
const handleOptionClick = (option: AssertionOption): void => {
71+
emit('addAssertion', {
72+
type: props.type,
73+
name: option.name || '',
74+
value: String(option.value || ''),
75+
})
76+
}
5777
58-
createPopper(reference, popperEl, {
59-
placement: 'right-start',
60-
})
78+
const initializePopper = (): void => {
79+
const popperEl = popper.value as HTMLElement
80+
const reference = popperEl.parentElement as HTMLElement
6181
62-
emit('setPopperElement', popperEl)
82+
createPopper(reference, popperEl, {
83+
placement: 'right-start',
6384
})
64-
})
6585
66-
const onClick = (name, value) => {
67-
emit('addAssertion', { type: props.type, name, value })
86+
emit('setPopperElement', popperEl)
6887
}
88+
89+
onMounted(() => {
90+
nextTick(initializePopper)
91+
})
6992
</script>
7093

71-
<style lang="scss">
94+
<style scoped lang="scss">
7295
@import './assertions-style.scss';
7396
7497
.assertion-options {
@@ -79,17 +102,35 @@ const onClick = (name, value) => {
79102
overflow: hidden;
80103
overflow-wrap: break-word;
81104
position: absolute;
105+
right: 8px;
106+
border-radius: 4px;
82107
83108
.assertion-option {
109+
font-size: 14px;
84110
cursor: pointer;
85111
padding: 0.4rem 0.6rem;
112+
border: 1px solid transparent;
113+
114+
&:first-of-type {
115+
border-top-left-radius: 4px;
116+
border-top-right-radius: 4px;
117+
}
118+
119+
&:last-of-type {
120+
border-bottom-left-radius: 4px;
121+
border-bottom-right-radius: 4px;
122+
}
86123
87124
&:hover {
88-
background-color: #e9ecef;
125+
background-color: $gray-1000;
126+
border: 1px solid $gray-950;
89127
}
90128
91-
.assertion-option-value {
92-
font-weight: 600;
129+
&:focus {
130+
background-color: $gray-950;
131+
color: $indigo-300;
132+
outline: none;
133+
@include box-shadow;
93134
}
94135
}
95136
}

packages/app/src/runner/studio/AssertionType.ce.vue

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
<template>
22
<div
33
:class="['assertion-type', { 'single-assertion': !hasOptions }]"
4+
tabindex="0"
5+
role="button"
6+
:aria-expanded="isOpen"
7+
:aria-haspopup="hasOptions"
48
@click.stop="onClick"
59
@mouseover.stop="onOpen"
610
@mouseout.stop="onClose"
11+
@focus="onOpen"
12+
@blur="onClose"
13+
@keydown.enter="onClick"
14+
@keydown.space="onClick"
715
>
816
<div class="assertion-type-text">
917
<span>
@@ -13,24 +21,13 @@
1321
v-if="hasOptions"
1422
class="dropdown-arrow"
1523
>
16-
<svg
17-
xmlns="http://www.w3.org/2000/svg"
18-
width="12"
19-
height="12"
20-
fill="currentColor"
21-
viewBox="0 0 16 16"
22-
>
23-
<path
24-
fillRule="evenodd"
25-
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"
26-
/>
27-
</svg>
24+
<IconChevronRightMedium />
2825
</span>
2926
</div>
3027
<AssertionOptions
3128
v-if="hasOptions && isOpen"
3229
:type="type"
33-
:options="options"
30+
:options="options || []"
3431
@set-popper-element="setPopperElement"
3532
@add-assertion="addAssertion"
3633
/>
@@ -40,10 +37,12 @@
4037
<script lang="ts" setup>
4138
import { Ref, ref } from 'vue'
4239
import AssertionOptions from './AssertionOptions.ce.vue'
40+
import { IconChevronRightMedium } from '@cypress-design/vue-icon'
41+
import type { AssertionType } from './types'
4342
4443
const props = defineProps<{
45-
type: string
46-
options: any
44+
type: AssertionType['type']
45+
options: AssertionType['options']
4746
}>()
4847
4948
const emit = defineEmits<{
@@ -58,7 +57,7 @@ const onOpen = () => {
5857
isOpen.value = true
5958
}
6059
61-
const onClose = (e: MouseEvent) => {
60+
const onClose = (e: MouseEvent | FocusEvent) => {
6261
if (e.relatedTarget instanceof Element &&
6362
popperElement.value && popperElement.value.contains(e.relatedTarget)) {
6463
return
@@ -82,38 +81,47 @@ const addAssertion = ({ type, name, value }) => {
8281
}
8382
</script>
8483

85-
<style lang="scss">
84+
<style scoped lang="scss">
8685
@import './assertions-style.scss';
8786
8887
.assertion-type {
89-
color: #202020;
9088
cursor: default;
9189
font-size: 14px;
9290
padding: 0.4rem 0.4rem 0.4rem 0.7rem;
9391
position: static;
92+
outline: none;
93+
border-radius: 4px;
94+
border: 1px solid transparent;
9495
9596
&:first-of-type {
9697
padding-top: 0.5rem;
9798
}
9899
99100
&:last-of-type {
100-
border-bottom-left-radius: $border-radius;
101-
border-bottom-right-radius: $border-radius;
101+
border-bottom-left-radius: 4px;
102+
border-bottom-right-radius: 4px;
102103
padding-bottom: 0.5rem;
103104
}
104105
105106
&:hover {
106-
background-color: #e9ecef;
107+
background-color: $gray-1000;
108+
border: 1px solid $gray-950;
109+
}
110+
111+
&:focus {
112+
color: $indigo-300;
113+
outline: none;
114+
@include box-shadow;
107115
}
108116
109117
&.single-assertion {
110118
cursor: pointer;
111-
font-weight: 600;
112119
}
113120
114121
.assertion-type-text {
115122
align-items: center;
116123
display: flex;
124+
cursor: pointer;
117125
118126
.dropdown-arrow {
119127
margin-left: auto;

0 commit comments

Comments
 (0)