Skip to content

Refactor scroll utils and improve test coverage

What does this MR do and why?

  • Refactors scroll_utils.js to improve maintainability and testability.
  • Introduces getScrollingElement to abstract away the difference between window and other scroll containers.
  • Updates scrollToTargetOnResize to use the scroll_utils functions.
  • Adds more unit tests for scroll_utils.js.

References

Screenshots or screen recordings

No changes expected.

How to set up and validate locally

I created a tester component to run manual sanity checks, the following diff adds it to the job page to test scrolling:

Click to expand
diff --git a/app/assets/javascripts/ci/job_details/components/scroll_tester.vue b/app/assets/javascripts/ci/job_details/components/scroll_tester.vue
new file mode 100644
index 000000000000..080c23b192ac
--- /dev/null
+++ b/app/assets/javascripts/ci/job_details/components/scroll_tester.vue
@@ -0,0 +1,68 @@
+<script>
+/* eslint-disable */
+import {
+  getScrollingElement,
+  isScrolledToBottom,
+  isScrolledToTop,
+  scrollDown,
+  scrollUp,
+  smoothScrollTop,
+  scrollTo,
+  scrollToElement,
+} from '~/lib/utils/scroll_utils';
+import { scrollToTargetOnResize } from '~/lib/utils/resize_observer';
+
+export default {
+  name: 'ScrollTester',
+  methods: {
+    getScrollingElement() {
+      console.log('getScrollingElement()', getScrollingElement());
+    },
+    isScrolledToBottom(contextElement) {
+      console.log('isScrolledToBottom()', { contextElement }, isScrolledToBottom(contextElement));
+    },
+    isScrolledToTop(contextElement) {
+      console.log('isScrolledToTop(c)', { contextElement }, isScrolledToTop(contextElement));
+    },
+    scrollDown,
+    scrollUp,
+    smoothScrollTop,
+    scrollTo,
+    scrollToElement,
+    scrollToElementInSidebar() {
+      scrollToElement(
+        '[title="build-job-with-a-long-name-that-that-may-or-may-not-overflow 4/20 - passed"]',
+        { parent: '.sidebar-container' },
+      );
+    },
+  },
+  data() {
+    return { scrollToTargetOnResizeCleanup: null };
+  },
+  mounted() {
+    this.scrollToTargetOnResizeCleanup = scrollToTargetOnResize({ targetId: 'L101' });
+  },
+};
+</script>
+<template>
+  <div class="gl-fixed gl-bottom-[100px] gl-z-9999 gl-w-1/2 gl-bg-white gl-p-5">
+    <button @click="getScrollingElement()">getScrollContainer</button>
+    <button @click="isScrolledToBottom()">isScrolledToBottom</button>
+    <button @click="isScrolledToBottom(null)">isScrolledToBottom (window)</button>
+    <button @click="isScrolledToTop()">isScrolledToTop</button>
+    <button @click="scrollDown()">scrollDown</button>
+    <button @click="scrollUp()">scrollUp</button>
+    <button @click="smoothScrollTop()">smoothScrollTop</button>
+    <button @click="scrollTo({ top: 1000 })">scrollTo 1000px</button>
+    <button @click="scrollToElement('#L101')">scrollToElement L101</button>
+    <button @click="scrollToElementInSidebar()">scrollToElement sidebar 4/20</button>
+    <button
+      @click="
+        scrollToTargetOnResizeCleanup();
+        scrollToTargetOnResizeCleanup = null;
+      "
+    >
+      scrollToTargetOnResize is {{ scrollToTargetOnResizeCleanup ? 'active for line L101' : 'off' }}
+    </button>
+  </div>
+</template>
diff --git a/app/assets/javascripts/ci/job_details/job_app.vue b/app/assets/javascripts/ci/job_details/job_app.vue
index 0aa23e44011f..634b9583692e 100644
--- a/app/assets/javascripts/ci/job_details/job_app.vue
+++ b/app/assets/javascripts/ci/job_details/job_app.vue
@@ -19,6 +19,7 @@ import JobHeader from './components/job_header.vue';
 import StuckBlock from './components/stuck_block.vue';
 import UnmetPrerequisitesBlock from './components/unmet_prerequisites_block.vue';
 import Sidebar from './components/sidebar/sidebar.vue';
+import ScrollTester from './components/scroll_tester.vue';
 
 const STATIC_PANEL_WRAPPER_SELECTOR = '.js-static-panel-inner';
 
@@ -39,6 +40,7 @@ export default {
     GlLoadingIcon,
     SharedRunner: () => import('ee_component/ci/runner/components/shared_runner_limit_block.vue'),
     GlAlert,
+    ScrollTester,
   },
   directives: {
     SafeHtml,
@@ -235,6 +237,8 @@ export default {
     <gl-loading-icon v-if="isLoading" size="lg" class="gl-mt-6" />
 
     <template v-else-if="shouldRenderContent">
+      <scroll-tester />
+
       <div class="build-page" data-testid="job-content">
         <!-- Header Section -->
         <header>

This is the result of my manual tests

New UI

2025-10-22_15.54.27

Old UI

2025-10-22_15.52.50

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Related to #577063

Edited by Miguel Rincon

Merge request reports

Loading