From 459fb436ef9872e52d87f0821ff8eb1cb22f4ea6 Mon Sep 17 00:00:00 2001 From: Mabel Amaya Date: Tue, 20 May 2025 13:56:49 -0400 Subject: [PATCH 1/4] internal: allow studio panel to be draggable --- packages/app/src/runner/ResizablePanels.vue | 89 +++++++++++++------ .../app/src/runner/SpecRunnerOpenMode.vue | 6 +- packages/app/src/runner/runner-constants.ts | 1 + 3 files changed, 69 insertions(+), 27 deletions(-) diff --git a/packages/app/src/runner/ResizablePanels.vue b/packages/app/src/runner/ResizablePanels.vue index 2a8a8bc32f54..f43ceccea53b 100644 --- a/packages/app/src/runner/ResizablePanels.vue +++ b/packages/app/src/runner/ResizablePanels.vue @@ -4,7 +4,7 @@ id="resizable-panels-root" class="flex" :class="{ - 'select-none': panel1IsDragging || panel2IsDragging, + 'select-none': panel1IsDragging || panel2IsDragging || panel4IsDragging, }" @mouseup="handleMouseup" @mousemove="handleMousemove" @@ -14,7 +14,7 @@ v-show="showPanel1" data-cy="specs-list-panel" class="h-full shrink-0 z-20 relative" - :style="{width: `${panel1Width}px`}" + :style="{ width: `${panel1Width}px` }" > @@ -46,7 +46,7 @@
- + +
@@ -86,9 +89,11 @@ const props = withDefaults(defineProps<{ showPanel4?: boolean // studio in runner initialPanel1Width?: number initialPanel2Width?: number + initialPanel4Width?: number minPanel1Width?: number minPanel2Width?: number minPanel3Width?: number + minPanel4Width?: number maxTotalWidth?: number // windowWidth in runner offsetLeft?: number }>(), { @@ -97,24 +102,29 @@ const props = withDefaults(defineProps<{ showPanel4: false, initialPanel1Width: runnerConstants.defaultSpecListWidth, initialPanel2Width: runnerConstants.defaultReporterWidth, + initialPanel4Width: runnerConstants.defaultStudioWidth, minPanel1Width: 200, minPanel2Width: 220, minPanel3Width: 100, + minPanel4Width: 340, maxTotalWidth: window.innerWidth, offsetLeft: 0, }) const emit = defineEmits<{ (e: 'resizeEnd', value: DraggablePanel): void - (e: 'panelWidthUpdated', value: {panel: DraggablePanel, width: number}): void + (e: 'panelWidthUpdated', value: { panel: DraggablePanel, width: number }): void }>() const panel1HandleX = ref(props.initialPanel1Width) const panel2HandleX = ref(props.initialPanel2Width + props.initialPanel1Width) +const panel4HandleX = ref(props.initialPanel2Width + props.initialPanel1Width + props.initialPanel4Width) const panel1IsDragging = ref(false) const panel2IsDragging = ref(false) +const panel4IsDragging = ref(false) const cachedPanel1Width = ref(props.initialPanel1Width) // because panel 1 (the inline specs list) can be opened and closed in the UI, we cache the width const panel2Width = ref(props.initialPanel2Width) +const panel4Width = ref(props.initialPanel4Width) const handleMousedown = (panel: DraggablePanel, event: MouseEvent) => { if (panel === 'panel1') { @@ -122,10 +132,13 @@ const handleMousedown = (panel: DraggablePanel, event: MouseEvent) => { } else if (panel === 'panel2') { panel2IsDragging.value = true panel2HandleX.value = event.clientX + } else if (panel === 'panel4') { + panel4IsDragging.value = true + panel4HandleX.value = event.clientX } } const handleMousemove = (event: MouseEvent) => { - if (!panel1IsDragging.value && !panel2IsDragging.value) { + if (!panel1IsDragging.value && !panel2IsDragging.value && !panel4IsDragging.value) { // nothing is dragging, ignore mousemove return @@ -139,6 +152,15 @@ const handleMousemove = (event: MouseEvent) => { panel2HandleX.value = event.clientX panel2Width.value = event.clientX - props.offsetLeft - panel1Width.value emit('panelWidthUpdated', { panel: 'panel2', width: panel2Width.value }) + } else if (panel4IsDragging.value && isNewWidthAllowed(event.clientX, 'panel4')) { + panel4HandleX.value = event.clientX + // Calculate width from the right edge of the window + // so that when we drag the panel to the left, it grows + // and when we drag it to the right, it shrinks + const rightEdge = props.maxTotalWidth + props.offsetLeft + + panel4Width.value = rightEdge - event.clientX + emit('panelWidthUpdated', { panel: 'panel4', width: panel4Width.value }) } } const handleMouseup = () => { @@ -149,8 +171,15 @@ const handleMouseup = () => { return } - handleResizeEnd('panel2') - panel2IsDragging.value = false + if (panel2IsDragging.value) { + handleResizeEnd('panel2') + panel2IsDragging.value = false + } + + if (panel4IsDragging.value) { + handleResizeEnd('panel4') + panel4IsDragging.value = false + } } const maxPanel1Width = computed(() => { @@ -159,14 +188,6 @@ const maxPanel1Width = computed(() => { return props.maxTotalWidth - unavailableWidth }) -const panel4Width = computed(() => { - if (!props.showPanel4) { - return 0 - } - - return runnerConstants.defaultStudioWidth -}) - const panel1Width = computed(() => { if (!props.showPanel1) { return 0 @@ -192,6 +213,12 @@ const panel3width = computed(() => { return panel3SpaceAvailable < props.minPanel3Width ? minimumWithBuffer : panel3SpaceAvailable }) +const maxPanel4Width = computed(() => { + const unavailableWidth = panel1Width.value + panel2Width.value + props.minPanel3Width + + return props.maxTotalWidth - unavailableWidth +}) + function handleResizeEnd (panel: DraggablePanel) { emit('resizeEnd', panel) } @@ -212,15 +239,25 @@ function isNewWidthAllowed (mouseClientX: number, panel: DraggablePanel) { return result } - const newWidth = mouseClientX - props.offsetLeft - panel1Width.value + if (panel === 'panel2') { + const newWidth = mouseClientX - props.offsetLeft - panel1Width.value + + if (isMaxWidthSmall && newWidth > fallbackWidth) { + return true + } + + return panel2IsDragging.value && newWidth >= props.minPanel2Width && newWidth <= maxPanel2Width.value + } + + if (panel === 'panel4') { + const rightEdge = props.maxTotalWidth + props.offsetLeft + const newWidth = rightEdge - mouseClientX - if (isMaxWidthSmall && newWidth > fallbackWidth) { - return true + return panel4IsDragging.value && newWidth >= props.minPanel4Width && newWidth <= maxPanel4Width.value } - return panel2IsDragging.value && newWidth >= props.minPanel2Width && newWidth <= maxPanel2Width.value + return false } - watchEffect(() => { if (!props.showPanel1) { emit('panelWidthUpdated', { panel: 'panel1', width: 0 }) diff --git a/packages/app/src/runner/SpecRunnerOpenMode.vue b/packages/app/src/runner/SpecRunnerOpenMode.vue index cc5bd3b1de4a..b7a6a5aaf31a 100644 --- a/packages/app/src/runner/SpecRunnerOpenMode.vue +++ b/packages/app/src/runner/SpecRunnerOpenMode.vue @@ -26,9 +26,11 @@ :max-total-width="windowWidth - collapsedNavBarWidth" :initial-panel1-width="specsListWidthPreferences" :initial-panel2-width="reporterWidthPreferences" + :initial-panel4-width="studioWidthPreferences" :min-panel1-width="minWidths.specsList" :min-panel2-width="minWidths.reporter" :min-panel3-width="minWidths.aut" + :min-panel4-width="minWidths.studio" :show-panel1="runnerUiStore.isSpecsListOpen && !screenshotStore.isScreenshotting" :show-panel2="!screenshotStore.isScreenshotting && !hideCommandLog" :show-panel4="shouldShowStudioPanel" @@ -149,6 +151,7 @@ const { absoluteAutMinimum, absoluteSpecListMinimum, absoluteReporterMinimum, + absoluteStudioMinimum, collapsedNavBarWidth, } = runnerConstants @@ -278,7 +281,7 @@ const shouldShowStudioButton = computed(() => { }) const shouldShowStudioPanel = computed(() => { - return !!cloudStudioRequested.value && (studioStore.isLoading || studioStore.isActive) + return !!cloudStudioRequested.value && (studioStore.isLoading || studioStore.isActive) && !screenshotStore.isScreenshotting }) const hideCommandLog = runnerUiStore.hideCommandLog @@ -330,6 +333,7 @@ const minWidths = computed(() => { aut: getMinimum(absoluteAutMinimum, doesContentFit), specsList: getMinimum(absoluteSpecListMinimum, doesContentFit), reporter: getMinimum(absoluteReporterMinimum, doesContentFit), + studio: absoluteStudioMinimum, } }) diff --git a/packages/app/src/runner/runner-constants.ts b/packages/app/src/runner/runner-constants.ts index 1165cfaf6d8b..e80fb2cd09cd 100644 --- a/packages/app/src/runner/runner-constants.ts +++ b/packages/app/src/runner/runner-constants.ts @@ -6,5 +6,6 @@ export const runnerConstants = { absoluteAutMinimum: 100, absoluteSpecListMinimum: 50, absoluteReporterMinimum: 50, + absoluteStudioMinimum: 340, collapsedNavBarWidth: 64, } From e89d5a743c68411c2f579006219359a3b2cca954 Mon Sep 17 00:00:00 2001 From: Mabel Amaya Date: Tue, 20 May 2025 15:29:17 -0400 Subject: [PATCH 2/4] change width calculation when screen is very small so panel can still be draggable --- packages/app/src/runner/ResizablePanels.vue | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/app/src/runner/ResizablePanels.vue b/packages/app/src/runner/ResizablePanels.vue index f43ceccea53b..667ee33b07d6 100644 --- a/packages/app/src/runner/ResizablePanels.vue +++ b/packages/app/src/runner/ResizablePanels.vue @@ -253,6 +253,10 @@ function isNewWidthAllowed (mouseClientX: number, panel: DraggablePanel) { const rightEdge = props.maxTotalWidth + props.offsetLeft const newWidth = rightEdge - mouseClientX + if (isMaxWidthSmall && newWidth >= props.minPanel4Width) { + return true + } + return panel4IsDragging.value && newWidth >= props.minPanel4Width && newWidth <= maxPanel4Width.value } From 711b24ef952d6a394ce2f5fcc64cfa48b23d7d95 Mon Sep 17 00:00:00 2001 From: Mabel Amaya Date: Wed, 21 May 2025 13:10:38 -0400 Subject: [PATCH 3/4] add tests and fix panel 4 width when studio panel is not open --- .../app/src/runner/ResizablePanels.cy.tsx | 128 ++++++++++++++++++ packages/app/src/runner/ResizablePanels.vue | 16 ++- 2 files changed, 140 insertions(+), 4 deletions(-) diff --git a/packages/app/src/runner/ResizablePanels.cy.tsx b/packages/app/src/runner/ResizablePanels.cy.tsx index a60844cce61d..4bc664232db0 100644 --- a/packages/app/src/runner/ResizablePanels.cy.tsx +++ b/packages/app/src/runner/ResizablePanels.cy.tsx @@ -5,9 +5,11 @@ import { runnerConstants } from './runner-constants' // default values const defaultPanel1Width = runnerConstants.defaultSpecListWidth const defaultPanel2Width = runnerConstants.defaultReporterWidth +const defaultPanel4Width = runnerConstants.defaultStudioWidth const minPanel1Width = 100 const minPanel2Width = 100 const minPanel3Width = 500 +const minPanel4Width = runnerConstants.absoluteStudioMinimum // helpers const assertWidth = (panel: ResizablePanelName, width: number) => { @@ -38,9 +40,11 @@ describe('', { viewportWidth: 1500, defaultCommandTimeout: 40 v-slots={slotContents} initialPanel1Width={defaultPanel1Width} initialPanel2Width={defaultPanel2Width} + initialPanel4Width={defaultPanel4Width} minPanel1Width={minPanel1Width} minPanel2Width={minPanel2Width} minPanel3Width={minPanel3Width} + minPanel4Width={minPanel4Width} /> )) }) @@ -106,6 +110,130 @@ describe('', { viewportWidth: 1500, defaultCommandTimeout: 40 }) }) + describe('when panel 4 is shown', () => { + const minPanel3Width = 500 + + beforeEach(() => { + cy.mount(() => ( +
+
+ +
)) + }) + + it('the panels can be resized', () => { + assertWidth('panel1', defaultPanel1Width) + dragHandleToClientX('panel1', 500) + assertWidth('panel1', 500) + dragHandleToClientX('panel1', 400) + assertWidth('panel1', 400) + + assertWidth('panel2', defaultPanel2Width) + dragHandleToClientX('panel2', 800) + assertWidth('panel2', 400) + dragHandleToClientX('panel2', 700) + assertWidth('panel2', 300) + + assertWidth('panel4', defaultPanel4Width) + dragHandleToClientX('panel4', 1300) + assertWidth('panel4', 700) + dragHandleToClientX('panel4', 1500) + assertWidth('panel4', 500) + }) + + it('panel 1 can be resized between its minimum allowed width and maximum available space', () => { + // drag panel 1 to its minimum width and attempt to go below it + assertWidth('panel1', defaultPanel1Width) + dragHandleToClientX('panel1', 100) + dragHandleToClientX('panel1', 99) + assertWidth('panel1', minPanel1Width) + dragHandleToClientX('panel1', 50) + assertWidth('panel1', minPanel1Width) + + // drag panel 1 to the maximum space available and attempt to go above it + dragHandleToClientX('panel1', 710) + dragHandleToClientX('panel1', 800) + assertWidth('panel1', 710) + dragHandleToClientX('panel1', 900) + assertWidth('panel1', 710) + + // panel 2 was not reduced + assertWidth('panel2', defaultPanel2Width) + + // panel 3 reached its minimum allowed size + assertWidth('panel3', 500) + + // panel 4 was not reduced + assertWidth('panel4', defaultPanel4Width) + }) + + it('panel 2 can be resized between its minimum allowed width and maximum available space', () => { + // drag panel 2 to its minimum width and attempt to go below it + assertWidth('panel2', defaultPanel2Width) + dragHandleToClientX('panel2', 380) + dragHandleToClientX('panel2', 200) + assertWidth('panel2', minPanel2Width) + dragHandleToClientX('panel2', 180) + assertWidth('panel2', minPanel2Width) + + // drag panel 2 to the maximum space available and attempt to go above it + dragHandleToClientX('panel2', 1160) + dragHandleToClientX('panel2', 1200) + assertWidth('panel2', 880) + dragHandleToClientX('panel2', 1300) + assertWidth('panel2', 880) + + // panel 1 was not reduced + assertWidth('panel1', defaultPanel1Width) + + // panel 3 reached its minimum allowed size + assertWidth('panel3', minPanel3Width) + + // panel 4 was not reduced + assertWidth('panel4', defaultPanel4Width) + }) + + it('panel 4 can be resized between its minimum allowed width and maximum available space', () => { + // since its starting width is the same as its minimum width, + // drag panel 4 to a different width, then drag it to its minimum width and attempt to go below it + assertWidth('panel4', defaultPanel4Width) + dragHandleToClientX('panel4', 1400) + assertWidth('panel4', 600) + dragHandleToClientX('panel4', 1660) + dragHandleToClientX('panel4', 1800) + assertWidth('panel4', minPanel4Width) + dragHandleToClientX('panel4', 1900) + assertWidth('panel4', minPanel4Width) + + // drag panel 4 to the maximum space available and attempt to go above it + dragHandleToClientX('panel4', 1230) + dragHandleToClientX('panel4', 1100) + assertWidth('panel4', 770) + dragHandleToClientX('panel4', 900) + assertWidth('panel4', 770) + + // panel 1 was not reduced + assertWidth('panel1', defaultPanel1Width) + + // panel 2 was not reduced + assertWidth('panel2', defaultPanel2Width) + + // panel 3 reached its absolute minimum allowed size + assertWidth('panel3', minPanel3Width) + }) + }) + describe('when there is a side nav', () => { it('handles being offset by some distance on the left', () => { cy.mount(() => ( diff --git a/packages/app/src/runner/ResizablePanels.vue b/packages/app/src/runner/ResizablePanels.vue index 667ee33b07d6..dbe769dff330 100644 --- a/packages/app/src/runner/ResizablePanels.vue +++ b/packages/app/src/runner/ResizablePanels.vue @@ -123,8 +123,8 @@ const panel1IsDragging = ref(false) const panel2IsDragging = ref(false) const panel4IsDragging = ref(false) const cachedPanel1Width = ref(props.initialPanel1Width) // because panel 1 (the inline specs list) can be opened and closed in the UI, we cache the width +const cachedPanel4Width = ref(props.initialPanel4Width) const panel2Width = ref(props.initialPanel2Width) -const panel4Width = ref(props.initialPanel4Width) const handleMousedown = (panel: DraggablePanel, event: MouseEvent) => { if (panel === 'panel1') { @@ -159,7 +159,7 @@ const handleMousemove = (event: MouseEvent) => { // and when we drag it to the right, it shrinks const rightEdge = props.maxTotalWidth + props.offsetLeft - panel4Width.value = rightEdge - event.clientX + cachedPanel4Width.value = rightEdge - event.clientX emit('panelWidthUpdated', { panel: 'panel4', width: panel4Width.value }) } } @@ -183,7 +183,7 @@ const handleMouseup = () => { } const maxPanel1Width = computed(() => { - const unavailableWidth = panel2Width.value + props.minPanel3Width + const unavailableWidth = panel2Width.value + props.minPanel3Width + panel4Width.value return props.maxTotalWidth - unavailableWidth }) @@ -196,6 +196,14 @@ const panel1Width = computed(() => { return cachedPanel1Width.value }) +const panel4Width = computed(() => { + if (!props.showPanel4) { + return 0 + } + + return cachedPanel4Width.value +}) + const maxPanel2Width = computed(() => { const unavailableWidth = panel1Width.value + props.minPanel3Width + panel4Width.value @@ -272,7 +280,7 @@ watchEffect(() => { if (!props.showPanel4) { emit('panelWidthUpdated', { panel: 'panel4', width: 0 }) } else if (props.showPanel4) { - emit('panelWidthUpdated', { panel: 'panel4', width: panel4Width.value }) + emit('panelWidthUpdated', { panel: 'panel4', width: cachedPanel4Width.value }) } }) From 586a2a8de16929ea879388896eefd1fe98a7962e Mon Sep 17 00:00:00 2001 From: Mabel Amaya Date: Wed, 21 May 2025 14:43:56 -0400 Subject: [PATCH 4/4] remove unused variable --- packages/app/src/runner/ResizablePanels.cy.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/app/src/runner/ResizablePanels.cy.tsx b/packages/app/src/runner/ResizablePanels.cy.tsx index 4bc664232db0..840e973a9c44 100644 --- a/packages/app/src/runner/ResizablePanels.cy.tsx +++ b/packages/app/src/runner/ResizablePanels.cy.tsx @@ -111,8 +111,6 @@ describe('', { viewportWidth: 1500, defaultCommandTimeout: 40 }) describe('when panel 4 is shown', () => { - const minPanel3Width = 500 - beforeEach(() => { cy.mount(() => (