From 494da5670513a70b83b88b02ea65d948d46a33cd Mon Sep 17 00:00:00 2001
From: Paul Gascou-Vaillancourt <pgascouvaillancourt@gitlab.com>
Date: Fri, 17 Jun 2022 14:04:23 +0000
Subject: [PATCH] Enable prefer-dom-node-dataset ESLint rule

This enables the unicorn/prefer-dom-node-dataset ESLint rule to prevent
using attributes APIs when manipulating data- attributes.
---
 .eslintrc.yml                                  |  2 ++
 .../behaviors/markdown/render_kroki.js         |  4 ++--
 .../behaviors/markdown/render_math.js          |  4 ++--
 .../blob/blob_line_permalink_updater.js        |  5 +++--
 app/assets/javascripts/blob/viewer/index.js    | 17 +++++++++--------
 app/assets/javascripts/breadcrumb.js           |  2 +-
 .../javascripts/code_navigation/utils/index.js |  4 ++--
 .../deprecated_jquery_dropdown/render.js       |  8 ++++----
 app/assets/javascripts/diff.js                 |  3 ++-
 .../available_dropdown_mappings.js             |  4 ++--
 .../filtered_search/dropdown_hint.js           |  4 ++--
 .../filtered_search/dropdown_operator.js       |  2 +-
 .../filtered_search/dropdown_user.js           |  4 ++--
 .../filtered_search/dropdown_utils.js          |  2 ++
 .../filtered_search/droplab/drop_down.js       |  4 ++--
 .../filtered_search/filtered_search_manager.js |  2 +-
 .../image_diff/helpers/dom_helper.js           |  2 +-
 .../issues/create_merge_request_dropdown.js    |  5 +----
 .../issues/show/components/description.vue     |  2 +-
 app/assets/javascripts/lazy_loader.js          |  9 +++++----
 .../confirm_via_gl_modal.js                    |  2 +-
 .../members/components/table/role_dropdown.vue |  2 +-
 .../pages/shared/nav/sidebar_tracking.js       | 10 +++++-----
 .../pages/users/activity_calendar.js           |  2 +-
 .../commits/components/author_select.vue       |  2 +-
 .../javascripts/sidebar/mount_sidebar.js       |  7 +++++--
 app/assets/javascripts/terraform/index.js      |  2 +-
 .../javascripts/whats_new/components/app.vue   |  2 +-
 .../whats_new/utils/notification.js            |  2 +-
 .../analytics/productivity_analytics/index.js  |  8 ++++----
 .../javascripts/event_tracking/navbar.js       |  2 +-
 .../init_arkose_labs_script_spec.js            |  2 +-
 .../components/table_header_spec.js            |  4 +---
 .../compliance_frameworks/init_form_spec.js    | 10 +++++-----
 ee/spec/frontend/members/index_spec.js         |  2 +-
 ee/spec/frontend/status_checks/mount_spec.js   |  4 ++--
 .../__helpers__/init_vue_mr_page_helper.js     | 18 +++++++++---------
 .../matchers/to_have_sprite_icon.js            |  2 +-
 spec/frontend/admin/users/index_spec.js        |  8 ++++----
 .../two_factor_auth/index_spec.js              |  4 ++--
 .../blob/components/table_contents_spec.js     |  4 ++--
 spec/frontend/blob/viewer/index_spec.js        |  6 +++---
 .../components/lock_popovers_spec.js           | 10 +++++-----
 .../code_navigation/store/actions_spec.js      | 12 ++++++------
 spec/frontend/confirm_modal_spec.js            |  6 +++---
 .../helpers/startup_css_helper_spec.js         |  7 ++++---
 .../create_merge_request_dropdown_spec.js      |  4 ++--
 .../frontend/labels/delete_label_modal_spec.js |  6 +++---
 spec/frontend/lazy_loader_spec.js              |  4 ++--
 spec/frontend/members/index_spec.js            |  2 +-
 spec/frontend/members/utils_spec.js            |  2 +-
 spec/frontend/notebook/cells/markdown_spec.js  |  4 ++--
 spec/frontend/notes/stores/actions_spec.js     | 10 +++++-----
 spec/frontend/performance_bar/index_spec.js    | 10 +++++-----
 spec/frontend/search_autocomplete_spec.js      |  2 +-
 spec/frontend/user_popovers_spec.js            |  2 +-
 spec/frontend/users_select/test_helper.js      |  8 ++++----
 .../components/states/mr_widget_merged_spec.js |  4 +---
 .../vue_mr_widget/mr_widget_options_spec.js    |  2 +-
 .../ide/helpers/ide_helper.js                  |  4 ++--
 60 files changed, 149 insertions(+), 144 deletions(-)

diff --git a/.eslintrc.yml b/.eslintrc.yml
index 97c24d99871ce..7505d864e6fcd 100644
--- a/.eslintrc.yml
+++ b/.eslintrc.yml
@@ -114,6 +114,8 @@ rules:
       message: 'Migrate to GlSkeletonLoader, or import GlDeprecatedSkeletonLoading.'
   # See https://gitlab.com/gitlab-org/gitlab/-/issues/360551
   vue/multi-word-component-names: off
+  unicorn/prefer-dom-node-dataset:
+    - error
 overrides:
   - files:
     - '{,ee/,jh/}spec/frontend*/**/*'
diff --git a/app/assets/javascripts/behaviors/markdown/render_kroki.js b/app/assets/javascripts/behaviors/markdown/render_kroki.js
index abe71694d735f..5fd910dd6cca7 100644
--- a/app/assets/javascripts/behaviors/markdown/render_kroki.js
+++ b/app/assets/javascripts/behaviors/markdown/render_kroki.js
@@ -55,8 +55,8 @@ export function renderKroki(krokiImages) {
 
     // A single Kroki image is processed multiple times for some reason,
     // so this condition ensures we only create one alert per Kroki image
-    if (!parent.hasAttribute('data-kroki-processed')) {
-      parent.setAttribute('data-kroki-processed', 'true');
+    if (!Object.prototype.hasOwnProperty.call(parent.dataset, 'krokiProcessed')) {
+      parent.dataset.krokiProcessed = 'true';
       parent.after(createAlert(krokiImage));
     }
   });
diff --git a/app/assets/javascripts/behaviors/markdown/render_math.js b/app/assets/javascripts/behaviors/markdown/render_math.js
index fd1a99acf9927..af7aac4cf36dd 100644
--- a/app/assets/javascripts/behaviors/markdown/render_math.js
+++ b/app/assets/javascripts/behaviors/markdown/render_math.js
@@ -112,7 +112,7 @@ class SafeMathRenderer {
 
       try {
         displayContainer.innerHTML = this.katex.renderToString(text, {
-          displayMode: el.getAttribute('data-math-style') === 'display',
+          displayMode: el.dataset.mathStyle === 'display',
           throwOnError: true,
           maxSize: 20,
           maxExpand: 20,
@@ -145,7 +145,7 @@ class SafeMathRenderer {
     this.elements.forEach((el) => {
       const placeholder = document.createElement('span');
       placeholder.style.display = 'none';
-      placeholder.setAttribute('data-math-style', el.getAttribute('data-math-style'));
+      placeholder.dataset.mathStyle = el.dataset.mathStyle;
       placeholder.textContent = el.textContent;
       el.parentNode.replaceChild(placeholder, el);
       this.queue.push(placeholder);
diff --git a/app/assets/javascripts/blob/blob_line_permalink_updater.js b/app/assets/javascripts/blob/blob_line_permalink_updater.js
index a3dd241604d58..0a5bcf326a1af 100644
--- a/app/assets/javascripts/blob/blob_line_permalink_updater.js
+++ b/app/assets/javascripts/blob/blob_line_permalink_updater.js
@@ -9,10 +9,11 @@ const updateLineNumbersOnBlobPermalinks = (linksToUpdate) => {
 
     [].concat(Array.prototype.slice.call(linksToUpdate)).forEach((permalinkButton) => {
       const baseHref =
-        permalinkButton.getAttribute('data-original-href') ||
+        permalinkButton.dataset.originalHref ||
         (() => {
           const href = permalinkButton.getAttribute('href');
-          permalinkButton.setAttribute('data-original-href', href);
+          // eslint-disable-next-line no-param-reassign
+          permalinkButton.dataset.originalHref = href;
           return href;
         })();
       permalinkButton.setAttribute('href', `${baseHref}${hashUrlString}`);
diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js
index a6eed4ecae333..a0d4f7ef4f20f 100644
--- a/app/assets/javascripts/blob/viewer/index.js
+++ b/app/assets/javascripts/blob/viewer/index.js
@@ -36,19 +36,19 @@ const loadRichBlobViewer = (type) => {
 
 const loadViewer = (viewerParam) => {
   const viewer = viewerParam;
-  const url = viewer.getAttribute('data-url');
+  const { url } = viewer.dataset;
 
-  if (!url || viewer.getAttribute('data-loaded') || viewer.getAttribute('data-loading')) {
+  if (!url || viewer.dataset.loaded || viewer.dataset.loading) {
     return Promise.resolve(viewer);
   }
 
-  viewer.setAttribute('data-loading', 'true');
+  viewer.dataset.loading = 'true';
 
   return axios.get(url).then(({ data }) => {
     viewer.innerHTML = data.html;
 
     window.requestIdleCallback(() => {
-      viewer.removeAttribute('data-loading');
+      delete viewer.dataset.loading;
     });
 
     return viewer;
@@ -108,7 +108,7 @@ export class BlobViewer {
 
   switchToInitialViewer() {
     const initialViewer = this.$fileHolder[0].querySelector('.blob-viewer:not(.hidden)');
-    let initialViewerName = initialViewer.getAttribute('data-type');
+    let initialViewerName = initialViewer.dataset.type;
 
     if (this.switcher && window.location.hash.indexOf('#L') === 0) {
       initialViewerName = 'simple';
@@ -138,12 +138,12 @@ export class BlobViewer {
 
     e.preventDefault();
 
-    this.switchToViewer(target.getAttribute('data-viewer'));
+    this.switchToViewer(target.dataset.viewer);
   }
 
   toggleCopyButtonState() {
     if (!this.copySourceBtn) return;
-    if (this.simpleViewer.getAttribute('data-loaded')) {
+    if (this.simpleViewer.dataset.loaded) {
       this.copySourceBtnTooltip.setAttribute('title', __('Copy file contents'));
       this.copySourceBtn.classList.remove('disabled');
     } else if (this.activeViewer === this.simpleViewer) {
@@ -199,7 +199,8 @@ export class BlobViewer {
           this.$fileHolder.trigger('highlight:line');
           handleLocationHash();
 
-          viewer.setAttribute('data-loaded', 'true');
+          // eslint-disable-next-line no-param-reassign
+          viewer.dataset.loaded = 'true';
           this.toggleCopyButtonState();
           eventHub.$emit('showBlobInteractionZones', viewer.dataset.path);
         });
diff --git a/app/assets/javascripts/breadcrumb.js b/app/assets/javascripts/breadcrumb.js
index b9d3742974ce3..113840dbc52a3 100644
--- a/app/assets/javascripts/breadcrumb.js
+++ b/app/assets/javascripts/breadcrumb.js
@@ -5,7 +5,7 @@ export const addTooltipToEl = (el) => {
 
   if (textEl && textEl.scrollWidth > textEl.offsetWidth) {
     el.setAttribute('title', el.textContent);
-    el.setAttribute('data-container', 'body');
+    el.dataset.container = 'body';
     el.classList.add('has-tooltip');
   }
 };
diff --git a/app/assets/javascripts/code_navigation/utils/index.js b/app/assets/javascripts/code_navigation/utils/index.js
index 0d72153d8fe82..46038df2f864e 100644
--- a/app/assets/javascripts/code_navigation/utils/index.js
+++ b/app/assets/javascripts/code_navigation/utils/index.js
@@ -32,8 +32,8 @@ export const addInteractionClass = ({ path, d, wrapTextNodes }) => {
     });
 
     if (el && !isTextNode(el)) {
-      el.setAttribute('data-char-index', d.start_char);
-      el.setAttribute('data-line-index', d.start_line);
+      el.dataset.charIndex = d.start_char;
+      el.dataset.lineIndex = d.start_line;
       el.classList.add('cursor-pointer', 'code-navigation', 'js-code-navigation');
       el.closest('.line').classList.add('code-navigation-line');
     }
diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/render.js b/app/assets/javascripts/deprecated_jquery_dropdown/render.js
index 37287b9d98184..f10c2d82b61df 100644
--- a/app/assets/javascripts/deprecated_jquery_dropdown/render.js
+++ b/app/assets/javascripts/deprecated_jquery_dropdown/render.js
@@ -107,10 +107,10 @@ function createLink(data, selected, options, index) {
   }
 
   if (options.trackSuggestionClickedLabel) {
-    link.setAttribute('data-track-action', 'click_text');
-    link.setAttribute('data-track-label', options.trackSuggestionClickedLabel);
-    link.setAttribute('data-track-value', index);
-    link.setAttribute('data-track-property', slugify(data.category || 'no-category'));
+    link.dataset.trackAction = 'click_text';
+    link.dataset.trackLabel = options.trackSuggestionClickedLabel;
+    link.dataset.trackValue = index;
+    link.dataset.trackProperty = slugify(data.category || 'no-category');
   }
 
   link.classList.toggle('is-active', selected);
diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js
index a12829f8420b9..9f3fb7151509d 100644
--- a/app/assets/javascripts/diff.js
+++ b/app/assets/javascripts/diff.js
@@ -26,7 +26,8 @@ export default class Diff {
       FilesCommentButton.init($diffFile);
 
     const firstFile = $('.files').first().get(0);
-    const canCreateNote = firstFile && firstFile.hasAttribute('data-can-create-note');
+    const canCreateNote =
+      firstFile && Object.prototype.hasOwnProperty.call(firstFile.dataset, 'canCreateNote');
     $diffFile.each((index, file) => initImageDiffHelper.initImageDiff(file, canCreateNote));
 
     if (!isBound) {
diff --git a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
index b57db73a86e03..3913e4e8d812f 100644
--- a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
+++ b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
@@ -197,10 +197,10 @@ export default class AvailableDropdownMappings {
   }
 
   getGroupId() {
-    return this.filteredSearchInput.getAttribute('data-group-id') || '';
+    return this.filteredSearchInput.dataset.groupId || '';
   }
 
   getProjectId() {
-    return this.filteredSearchInput.getAttribute('data-project-id') || '';
+    return this.filteredSearchInput.dataset.projectId || '';
   }
 }
diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js b/app/assets/javascripts/filtered_search/dropdown_hint.js
index 9d29782c9a7ba..10c3a6a36d59c 100644
--- a/app/assets/javascripts/filtered_search/dropdown_hint.js
+++ b/app/assets/javascripts/filtered_search/dropdown_hint.js
@@ -25,9 +25,9 @@ export default class DropdownHint extends FilteredSearchDropdown {
     const { selected } = e.detail;
 
     if (selected.tagName === 'LI') {
-      if (selected.hasAttribute('data-value')) {
+      if (Object.prototype.hasOwnProperty.call(selected.dataset, 'value')) {
         this.dismissDropdown();
-      } else if (selected.getAttribute('data-action') === 'submit') {
+      } else if (selected.dataset.action === 'submit') {
         this.dismissDropdown();
         this.dispatchFormSubmitEvent();
       } else {
diff --git a/app/assets/javascripts/filtered_search/dropdown_operator.js b/app/assets/javascripts/filtered_search/dropdown_operator.js
index fb9f25a8c4501..f3f159ab98880 100644
--- a/app/assets/javascripts/filtered_search/dropdown_operator.js
+++ b/app/assets/javascripts/filtered_search/dropdown_operator.js
@@ -23,7 +23,7 @@ export default class DropdownOperator extends FilteredSearchDropdown {
     const { selected } = e.detail;
 
     if (selected.tagName === 'LI') {
-      if (selected.hasAttribute('data-value')) {
+      if (Object.prototype.hasOwnProperty.call(selected.dataset, 'value')) {
         const name = FilteredSearchVisualTokens.getLastTokenPartial();
         const operator = selected.dataset.value;
 
diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js b/app/assets/javascripts/filtered_search/dropdown_user.js
index 9a23ff25eacee..26507a85fa80b 100644
--- a/app/assets/javascripts/filtered_search/dropdown_user.js
+++ b/app/assets/javascripts/filtered_search/dropdown_user.js
@@ -31,11 +31,11 @@ export default class DropdownUser extends DropdownAjaxFilter {
   }
 
   getGroupId() {
-    return this.input.getAttribute('data-group-id');
+    return this.input.dataset.groupId;
   }
 
   getProjectId() {
-    return this.input.getAttribute('data-project-id');
+    return this.input.dataset.projectId;
   }
 
   projectOrGroupId() {
diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js b/app/assets/javascripts/filtered_search/dropdown_utils.js
index c98d1f8e06417..22e1604871a53 100644
--- a/app/assets/javascripts/filtered_search/dropdown_utils.js
+++ b/app/assets/javascripts/filtered_search/dropdown_utils.js
@@ -87,6 +87,7 @@ export default class DropdownUtils {
   }
 
   static setDataValueIfSelected(filter, operator, selected) {
+    // eslint-disable-next-line unicorn/prefer-dom-node-dataset
     const dataValue = selected.getAttribute('data-value');
 
     if (dataValue) {
@@ -96,6 +97,7 @@ export default class DropdownUtils {
         tokenValue: dataValue,
         clicked: true,
         options: {
+          // eslint-disable-next-line unicorn/prefer-dom-node-dataset
           capitalizeTokenValue: selected.hasAttribute('data-capitalize'),
         },
       });
diff --git a/app/assets/javascripts/filtered_search/droplab/drop_down.js b/app/assets/javascripts/filtered_search/droplab/drop_down.js
index 05b741af19152..398a7b266773b 100644
--- a/app/assets/javascripts/filtered_search/droplab/drop_down.js
+++ b/app/assets/javascripts/filtered_search/droplab/drop_down.js
@@ -165,8 +165,8 @@ class DropDown {
     images.forEach((image) => {
       const img = image;
 
-      img.src = img.getAttribute('data-src');
-      img.removeAttribute('data-src');
+      img.src = img.dataset.src;
+      delete img.dataset.src;
     });
   }
 }
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index 07f2c75f00a3a..ac2cf27e87327 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -814,7 +814,7 @@ export default class FilteredSearchManager {
   getUsernameParams() {
     const usernamesById = {};
     try {
-      const attribute = this.filteredSearchInput.getAttribute('data-username-params');
+      const attribute = this.filteredSearchInput.dataset.usernameParams;
       JSON.parse(attribute).forEach((user) => {
         usernamesById[user.id] = user.username;
       });
diff --git a/app/assets/javascripts/image_diff/helpers/dom_helper.js b/app/assets/javascripts/image_diff/helpers/dom_helper.js
index 3468a629f5ac0..180e927a3e789 100644
--- a/app/assets/javascripts/image_diff/helpers/dom_helper.js
+++ b/app/assets/javascripts/image_diff/helpers/dom_helper.js
@@ -6,7 +6,7 @@ export function setPositionDataAttribute(el, options) {
 
   const positionObject = { ...JSON.parse(position), x, y, width, height };
 
-  el.setAttribute('data-position', JSON.stringify(positionObject));
+  el.dataset.position = JSON.stringify(positionObject);
 }
 
 export function updateDiscussionAvatarBadgeNumber(discussionEl, newBadgeNumber) {
diff --git a/app/assets/javascripts/issues/create_merge_request_dropdown.js b/app/assets/javascripts/issues/create_merge_request_dropdown.js
index c5f31081625f2..edf3789e6dc91 100644
--- a/app/assets/javascripts/issues/create_merge_request_dropdown.js
+++ b/app/assets/javascripts/issues/create_merge_request_dropdown.js
@@ -82,10 +82,7 @@ export default class CreateMergeRequestDropdown {
     this.init();
 
     if (isConfidentialIssue()) {
-      this.createMergeRequestButton.setAttribute(
-        'data-dropdown-trigger',
-        '#create-merge-request-dropdown',
-      );
+      this.createMergeRequestButton.dataset.dropdownTrigger = '#create-merge-request-dropdown';
       initConfidentialMergeRequest();
     }
   }
diff --git a/app/assets/javascripts/issues/show/components/description.vue b/app/assets/javascripts/issues/show/components/description.vue
index b8b7cbb5bdce4..892c631f8eae7 100644
--- a/app/assets/javascripts/issues/show/components/description.vue
+++ b/app/assets/javascripts/issues/show/components/description.vue
@@ -379,7 +379,7 @@ export default {
     },
     setActiveTask(el) {
       const { parentElement } = el;
-      const lineNumbers = parentElement.getAttribute('data-sourcepos').match(/\b\d+(?=:)/g);
+      const lineNumbers = parentElement.dataset.sourcepos.match(/\b\d+(?=:)/g);
       this.activeTask = {
         title: parentElement.innerText,
         lineNumberStart: lineNumbers[0],
diff --git a/app/assets/javascripts/lazy_loader.js b/app/assets/javascripts/lazy_loader.js
index 2b4dd205cf1c9..ba8010823775b 100644
--- a/app/assets/javascripts/lazy_loader.js
+++ b/app/assets/javascripts/lazy_loader.js
@@ -127,7 +127,7 @@ export default class LazyLoader {
 
     // Loading Images which are in the current viewport or close to them
     this.lazyImages = this.lazyImages.filter((selectedImage) => {
-      if (selectedImage.getAttribute('data-src')) {
+      if (selectedImage.dataset.src) {
         const imgBoundRect = selectedImage.getBoundingClientRect();
         const imgTop = scrollTop + imgBoundRect.top;
         const imgBound = imgTop + imgBoundRect.height;
@@ -156,16 +156,17 @@ export default class LazyLoader {
   }
 
   static loadImage(img) {
-    if (img.getAttribute('data-src')) {
+    if (img.dataset.src) {
       img.setAttribute('loading', 'lazy');
-      let imgUrl = img.getAttribute('data-src');
+      let imgUrl = img.dataset.src;
       // Only adding width + height for avatars for now
       if (imgUrl.indexOf('/avatar/') > -1 && imgUrl.indexOf('?') === -1) {
         const targetWidth = img.getAttribute('width') || img.width;
         imgUrl += `?width=${targetWidth}`;
       }
       img.setAttribute('src', imgUrl);
-      img.removeAttribute('data-src');
+      // eslint-disable-next-line no-param-reassign
+      delete img.dataset.src;
       img.classList.remove('lazy');
       img.classList.add('js-lazy-loaded');
       img.classList.add('qa-js-lazy-loaded');
diff --git a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js
index 173116062c9ac..2dc479db80af6 100644
--- a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js
+++ b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js
@@ -56,7 +56,7 @@ export function confirmAction(
 export function confirmViaGlModal(message, element) {
   const primaryBtnConfig = {};
 
-  const confirmBtnVariant = element.getAttribute('data-confirm-btn-variant');
+  const { confirmBtnVariant } = element.dataset;
 
   if (confirmBtnVariant) {
     primaryBtnConfig.primaryBtnVariant = confirmBtnVariant;
diff --git a/app/assets/javascripts/members/components/table/role_dropdown.vue b/app/assets/javascripts/members/components/table/role_dropdown.vue
index fa895cf24c476..6cd8bf57313ec 100644
--- a/app/assets/javascripts/members/components/table/role_dropdown.vue
+++ b/app/assets/javascripts/members/components/table/role_dropdown.vue
@@ -41,7 +41,7 @@ export default {
     const dropdownToggle = this.$refs.glDropdown.$el.querySelector('.dropdown-toggle');
 
     if (dropdownToggle) {
-      dropdownToggle.setAttribute('data-qa-selector', 'access_level_dropdown');
+      dropdownToggle.dataset.qaSelector = 'access_level_dropdown';
     }
   },
   methods: {
diff --git a/app/assets/javascripts/pages/shared/nav/sidebar_tracking.js b/app/assets/javascripts/pages/shared/nav/sidebar_tracking.js
index 79ce1a37d2111..47aae36ecbb97 100644
--- a/app/assets/javascripts/pages/shared/nav/sidebar_tracking.js
+++ b/app/assets/javascripts/pages/shared/nav/sidebar_tracking.js
@@ -1,6 +1,6 @@
 function onSidebarLinkClick() {
   const setDataTrackAction = (element, action) => {
-    element.setAttribute('data-track-action', action);
+    element.dataset.trackAction = action;
   };
 
   const setDataTrackExtra = (element, value) => {
@@ -12,10 +12,10 @@ function onSidebarLinkClick() {
       ? SIDEBAR_COLLAPSED
       : SIDEBAR_EXPANDED;
 
-    element.setAttribute(
-      'data-track-extra',
-      JSON.stringify({ sidebar_display: sidebarCollapsed, menu_display: value }),
-    );
+    element.dataset.trackExtra = JSON.stringify({
+      sidebar_display: sidebarCollapsed,
+      menu_display: value,
+    });
   };
 
   const EXPANDED = 'Expanded';
diff --git a/app/assets/javascripts/pages/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js
index 996e12bc105c5..94506d33b3351 100644
--- a/app/assets/javascripts/pages/users/activity_calendar.js
+++ b/app/assets/javascripts/pages/users/activity_calendar.js
@@ -298,7 +298,7 @@ export default class ActivityCalendar {
             .querySelector(this.activitiesContainer)
             .querySelectorAll('.js-localtime')
             .forEach((el) => {
-              el.setAttribute('title', formatDate(el.getAttribute('data-datetime')));
+              el.setAttribute('title', formatDate(el.dataset.datetime));
             });
         })
         .catch(() =>
diff --git a/app/assets/javascripts/projects/commits/components/author_select.vue b/app/assets/javascripts/projects/commits/components/author_select.vue
index c8a0a3417f3a7..884ef7321445c 100644
--- a/app/assets/javascripts/projects/commits/components/author_select.vue
+++ b/app/assets/javascripts/projects/commits/components/author_select.vue
@@ -57,7 +57,7 @@ export default {
 
     if (authorParam) {
       commitsSearchInput.setAttribute('disabled', true);
-      commitsSearchInput.setAttribute('data-toggle', 'tooltip');
+      commitsSearchInput.dataset.toggle = 'tooltip';
       commitsSearchInput.setAttribute('title', tooltipMessage);
       this.currentAuthor = authorParam;
     }
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 351bb50d9417d..bb40ac1443819 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -119,7 +119,7 @@ function mountAssigneesComponentDeprecated(mediator) {
           issuableIid: String(iid),
           projectPath: fullPath,
           field: el.dataset.field,
-          signedIn: el.hasAttribute('data-signed-in'),
+          signedIn: Object.prototype.hasOwnProperty.call(el.dataset, 'signedIn'),
           issuableType:
             isInIssuePage() || isInIncidentPage() || isInDesignPage()
               ? IssuableType.Issue
@@ -149,7 +149,10 @@ function mountAssigneesComponent() {
     },
     provide: {
       canUpdate: editable,
-      directlyInviteMembers: el.hasAttribute('data-directly-invite-members'),
+      directlyInviteMembers: Object.prototype.hasOwnProperty.call(
+        el.dataset,
+        'directlyInviteMembers',
+      ),
     },
     render: (createElement) =>
       createElement('sidebar-assignees-widget', {
diff --git a/app/assets/javascripts/terraform/index.js b/app/assets/javascripts/terraform/index.js
index 571177986d2f5..2d70ccfac4dc4 100644
--- a/app/assets/javascripts/terraform/index.js
+++ b/app/assets/javascripts/terraform/index.js
@@ -39,7 +39,7 @@ export default () => {
       return createElement(TerraformList, {
         props: {
           emptyStateImage,
-          terraformAdmin: el.hasAttribute('data-terraform-admin'),
+          terraformAdmin: Object.prototype.hasOwnProperty.call(el.dataset, 'terraformAdmin'),
         },
       });
     },
diff --git a/app/assets/javascripts/whats_new/components/app.vue b/app/assets/javascripts/whats_new/components/app.vue
index b74dba686ade0..0c55cc2f8a6ce 100644
--- a/app/assets/javascripts/whats_new/components/app.vue
+++ b/app/assets/javascripts/whats_new/components/app.vue
@@ -33,7 +33,7 @@ export default {
     this.fetchFreshItems();
 
     const body = document.querySelector('body');
-    const namespaceId = body.getAttribute('data-namespace-id');
+    const { namespaceId } = body.dataset;
 
     this.track('click_whats_new_drawer', { label: 'namespace_id', value: namespaceId });
   },
diff --git a/app/assets/javascripts/whats_new/utils/notification.js b/app/assets/javascripts/whats_new/utils/notification.js
index 66ee3b1a97126..41aff202f48c6 100644
--- a/app/assets/javascripts/whats_new/utils/notification.js
+++ b/app/assets/javascripts/whats_new/utils/notification.js
@@ -1,6 +1,6 @@
 export const STORAGE_KEY = 'display-whats-new-notification';
 
-export const getVersionDigest = (appEl) => appEl.getAttribute('data-version-digest');
+export const getVersionDigest = (appEl) => appEl.dataset.versionDigest;
 
 export const setNotification = (appEl) => {
   const versionDigest = getVersionDigest(appEl);
diff --git a/ee/app/assets/javascripts/analytics/productivity_analytics/index.js b/ee/app/assets/javascripts/analytics/productivity_analytics/index.js
index 795bd8b7b8159..003052a99db5b 100644
--- a/ee/app/assets/javascripts/analytics/productivity_analytics/index.js
+++ b/ee/app/assets/javascripts/analytics/productivity_analytics/index.js
@@ -123,14 +123,14 @@ export default () => {
         const labelsEndpoint = getLabelsEndpoint(groupNamespace, projectNamespace);
         const milestonesEndpoint = getMilestonesEndpoint(groupNamespace, projectNamespace);
 
-        filteredSearchInput.setAttribute('data-group-id', groupId);
+        filteredSearchInput.dataset.groupId = groupId;
 
         if (projectId) {
-          filteredSearchInput.setAttribute('data-project-id', projectId);
+          filteredSearchInput.dataset.projectId = projectId;
         }
 
-        filteredSearchInput.setAttribute('data-labels-endpoint', labelsEndpoint);
-        filteredSearchInput.setAttribute('data-milestones-endpoint', milestonesEndpoint);
+        filteredSearchInput.dataset.labelsEndpoint = labelsEndpoint;
+        filteredSearchInput.dataset.milestonesEndpoint = milestonesEndpoint;
         filterManager = new FilteredSearchProductivityAnalytics({ isGroup: false });
         filterManager.setup();
       },
diff --git a/ee/app/assets/javascripts/event_tracking/navbar.js b/ee/app/assets/javascripts/event_tracking/navbar.js
index 3a41d254c0760..ec8becc7d2125 100644
--- a/ee/app/assets/javascripts/event_tracking/navbar.js
+++ b/ee/app/assets/javascripts/event_tracking/navbar.js
@@ -33,7 +33,7 @@ export default function trackNavbarEvents() {
       const parentDropdown = e.currentTarget.closest('li.dropdown');
 
       Tracking.event(TRACKING_CATEGORY, 'activate_form_input', {
-        label: `${parentDropdown.getAttribute('data-track-label')}_search`,
+        label: `${parentDropdown.dataset.trackLabel}_search`,
         property: '',
         value: '',
       });
diff --git a/ee/spec/frontend/arkose_labs/init_arkose_labs_script_spec.js b/ee/spec/frontend/arkose_labs/init_arkose_labs_script_spec.js
index 7a3017a0458da..321f263b10668 100644
--- a/ee/spec/frontend/arkose_labs/init_arkose_labs_script_spec.js
+++ b/ee/spec/frontend/arkose_labs/init_arkose_labs_script_spec.js
@@ -37,7 +37,7 @@ describe('initArkoseLabsScript', () => {
     expect(scriptTag.getAttribute('src')).toBe(
       `https://${TEST_DOMAIN}/v2/${TEST_PUBLIC_KEY}/api.js`,
     );
-    expect(scriptTag.getAttribute('data-callback')).toBe(EXPECTED_CALLBACK_NAME);
+    expect(scriptTag.dataset.callback).toBe(EXPECTED_CALLBACK_NAME);
   });
 
   it('when callback is called, cleans up the global object and resolves the Promise', () => {
diff --git a/ee/spec/frontend/group_member_contributions/components/table_header_spec.js b/ee/spec/frontend/group_member_contributions/components/table_header_spec.js
index e8302e5dce6c4..c857da257790e 100644
--- a/ee/spec/frontend/group_member_contributions/components/table_header_spec.js
+++ b/ee/spec/frontend/group_member_contributions/components/table_header_spec.js
@@ -86,9 +86,7 @@ describe('TableHeaderComponent', () => {
 
       expect(headerItemEl).not.toBeNull();
       expect(headerItemEl.innerText.trim()).toBe('Name');
-      expect(headerItemEl.querySelector('svg').getAttribute('data-testid')).toBe(
-        'chevron-lg-up-icon',
-      );
+      expect(headerItemEl.querySelector('svg').dataset.testid).toBe('chevron-lg-up-icon');
     });
   });
 });
diff --git a/ee/spec/frontend/groups/settings/compliance_frameworks/init_form_spec.js b/ee/spec/frontend/groups/settings/compliance_frameworks/init_form_spec.js
index aea3a43f5fa9d..d8e77ed1017f4 100644
--- a/ee/spec/frontend/groups/settings/compliance_frameworks/init_form_spec.js
+++ b/ee/spec/frontend/groups/settings/compliance_frameworks/init_form_spec.js
@@ -19,13 +19,13 @@ describe('createComplianceFrameworksFormApp', () => {
 
   const setUpDocument = (id = null) => {
     el = document.createElement('div');
-    el.setAttribute('data-group-edit-path', groupEditPath);
-    el.setAttribute('data-group-path', groupPath);
-    el.setAttribute('data-pipeline-configuration-full-path-enabled', 'true');
+    el.dataset.groupEditPath = groupEditPath;
+    el.dataset.groupPath = groupPath;
+    el.dataset.pipelineConfigurationFullPathEnabled = 'true';
 
     if (id) {
-      el.setAttribute('data-graphql-field-name', graphqlFieldName);
-      el.setAttribute('data-framework-id', id);
+      el.dataset.graphqlFieldName = graphqlFieldName;
+      el.dataset.frameworkId = id;
     }
 
     document.body.appendChild(el);
diff --git a/ee/spec/frontend/members/index_spec.js b/ee/spec/frontend/members/index_spec.js
index 49ea38e49ab5d..c562251f58aa6 100644
--- a/ee/spec/frontend/members/index_spec.js
+++ b/ee/spec/frontend/members/index_spec.js
@@ -14,7 +14,7 @@ describe('initMembersApp', () => {
 
   beforeEach(() => {
     el = document.createElement('div');
-    el.setAttribute('data-members-data', dataAttribute);
+    el.dataset.membersData = dataAttribute;
   });
 
   afterEach(() => {
diff --git a/ee/spec/frontend/status_checks/mount_spec.js b/ee/spec/frontend/status_checks/mount_spec.js
index 87b9624b6572c..81bbdb6c1d399 100644
--- a/ee/spec/frontend/status_checks/mount_spec.js
+++ b/ee/spec/frontend/status_checks/mount_spec.js
@@ -15,8 +15,8 @@ describe('mountStatusChecks', () => {
 
   const setUpDocument = () => {
     el = document.createElement('div');
-    el.setAttribute('data-project-id', projectId);
-    el.setAttribute('data-status-checks-path', statusChecksPath);
+    el.dataset.projectId = projectId;
+    el.dataset.statusChecksPath = statusChecksPath;
 
     document.body.appendChild(el);
 
diff --git a/spec/frontend/__helpers__/init_vue_mr_page_helper.js b/spec/frontend/__helpers__/init_vue_mr_page_helper.js
index ee01e9e6268c6..6b719a324802f 100644
--- a/spec/frontend/__helpers__/init_vue_mr_page_helper.js
+++ b/spec/frontend/__helpers__/init_vue_mr_page_helper.js
@@ -13,16 +13,16 @@ export default function initVueMRPage() {
   const diffsAppProjectPath = 'testproject';
   const mrEl = document.createElement('div');
   mrEl.className = 'merge-request fixture-mr';
-  mrEl.setAttribute('data-mr-action', 'diffs');
+  mrEl.dataset.mrAction = 'diffs';
   mrTestEl.appendChild(mrEl);
 
   const mrDiscussionsEl = document.createElement('div');
   mrDiscussionsEl.id = 'js-vue-mr-discussions';
-  mrDiscussionsEl.setAttribute('data-current-user-data', JSON.stringify(userDataMock));
-  mrDiscussionsEl.setAttribute('data-noteable-data', JSON.stringify(noteableDataMock));
-  mrDiscussionsEl.setAttribute('data-notes-data', JSON.stringify(notesDataMock));
-  mrDiscussionsEl.setAttribute('data-noteable-type', 'merge-request');
-  mrDiscussionsEl.setAttribute('data-is-locked', 'false');
+  mrDiscussionsEl.dataset.currentUserData = JSON.stringify(userDataMock);
+  mrDiscussionsEl.dataset.noteableData = JSON.stringify(noteableDataMock);
+  mrDiscussionsEl.dataset.notesData = JSON.stringify(notesDataMock);
+  mrDiscussionsEl.dataset.noteableType = 'merge-request';
+  mrDiscussionsEl.dataset.isLocked = 'false';
   mrTestEl.appendChild(mrDiscussionsEl);
 
   const discussionCounterEl = document.createElement('div');
@@ -31,9 +31,9 @@ export default function initVueMRPage() {
 
   const diffsAppEl = document.createElement('div');
   diffsAppEl.id = 'js-diffs-app';
-  diffsAppEl.setAttribute('data-endpoint', diffsAppEndpoint);
-  diffsAppEl.setAttribute('data-project-path', diffsAppProjectPath);
-  diffsAppEl.setAttribute('data-current-user-data', JSON.stringify(userDataMock));
+  diffsAppEl.dataset.endpoint = diffsAppEndpoint;
+  diffsAppEl.dataset.projectPath = diffsAppProjectPath;
+  diffsAppEl.dataset.currentUserData = JSON.stringify(userDataMock);
   mrTestEl.appendChild(diffsAppEl);
 
   const mock = new MockAdapter(axios);
diff --git a/spec/frontend/__helpers__/matchers/to_have_sprite_icon.js b/spec/frontend/__helpers__/matchers/to_have_sprite_icon.js
index bce9d93bea86f..45b9c31c4dbbf 100644
--- a/spec/frontend/__helpers__/matchers/to_have_sprite_icon.js
+++ b/spec/frontend/__helpers__/matchers/to_have_sprite_icon.js
@@ -9,7 +9,7 @@ export const toHaveSpriteIcon = (element, iconName) => {
 
   const iconReferences = [].slice.apply(element.querySelectorAll('svg use'));
   const matchingIcon = iconReferences.find(
-    (reference) => reference.parentNode.getAttribute('data-testid') === `${iconName}-icon`,
+    (reference) => reference.parentNode.dataset.testid === `${iconName}-icon`,
   );
 
   const pass = Boolean(matchingIcon);
diff --git a/spec/frontend/admin/users/index_spec.js b/spec/frontend/admin/users/index_spec.js
index 06dbadd6d3d62..961fa96acdd16 100644
--- a/spec/frontend/admin/users/index_spec.js
+++ b/spec/frontend/admin/users/index_spec.js
@@ -12,8 +12,8 @@ describe('initAdminUsersApp', () => {
 
   beforeEach(() => {
     el = document.createElement('div');
-    el.setAttribute('data-users', JSON.stringify(users));
-    el.setAttribute('data-paths', JSON.stringify(paths));
+    el.dataset.users = JSON.stringify(users);
+    el.dataset.paths = JSON.stringify(paths);
 
     wrapper = createWrapper(initAdminUsersApp(el));
   });
@@ -40,8 +40,8 @@ describe('initAdminUserActions', () => {
 
   beforeEach(() => {
     el = document.createElement('div');
-    el.setAttribute('data-user', JSON.stringify(user));
-    el.setAttribute('data-paths', JSON.stringify(paths));
+    el.dataset.user = JSON.stringify(user);
+    el.dataset.paths = JSON.stringify(paths);
 
     wrapper = createWrapper(initAdminUserActions(el));
   });
diff --git a/spec/frontend/authentication/two_factor_auth/index_spec.js b/spec/frontend/authentication/two_factor_auth/index_spec.js
index 0ff9d60f40944..f9a6b2df662ab 100644
--- a/spec/frontend/authentication/two_factor_auth/index_spec.js
+++ b/spec/frontend/authentication/two_factor_auth/index_spec.js
@@ -15,8 +15,8 @@ describe('initRecoveryCodes', () => {
   beforeEach(() => {
     el = document.createElement('div');
     el.setAttribute('class', 'js-2fa-recovery-codes');
-    el.setAttribute('data-codes', codesJsonString);
-    el.setAttribute('data-profile-account-path', profileAccountPath);
+    el.dataset.codes = codesJsonString;
+    el.dataset.profileAccountPath = profileAccountPath;
     document.body.appendChild(el);
 
     wrapper = createWrapper(initRecoveryCodes());
diff --git a/spec/frontend/blob/components/table_contents_spec.js b/spec/frontend/blob/components/table_contents_spec.js
index 358ac31819cd1..2cbac809a0d39 100644
--- a/spec/frontend/blob/components/table_contents_spec.js
+++ b/spec/frontend/blob/components/table_contents_spec.js
@@ -11,7 +11,7 @@ function createComponent() {
 }
 
 async function setLoaded(loaded) {
-  document.querySelector('.blob-viewer').setAttribute('data-loaded', loaded);
+  document.querySelector('.blob-viewer').dataset.loaded = loaded;
 
   await nextTick();
 }
@@ -53,7 +53,7 @@ describe('Markdown table of contents component', () => {
     it('does not show dropdown when viewing non-rich content', async () => {
       createComponent();
 
-      document.querySelector('.blob-viewer').setAttribute('data-type', 'simple');
+      document.querySelector('.blob-viewer').dataset.type = 'simple';
 
       await setLoaded(true);
 
diff --git a/spec/frontend/blob/viewer/index_spec.js b/spec/frontend/blob/viewer/index_spec.js
index 5f6baf3f63d62..b2559af182b93 100644
--- a/spec/frontend/blob/viewer/index_spec.js
+++ b/spec/frontend/blob/viewer/index_spec.js
@@ -80,9 +80,9 @@ describe('Blob viewer', () => {
     return asyncClick()
       .then(() => asyncClick())
       .then(() => {
-        expect(
-          document.querySelector('.blob-viewer[data-type="simple"]').getAttribute('data-loaded'),
-        ).toBe('true');
+        expect(document.querySelector('.blob-viewer[data-type="simple"]').dataset.loaded).toBe(
+          'true',
+        );
       });
   });
 
diff --git a/spec/frontend/cascading_settings/components/lock_popovers_spec.js b/spec/frontend/cascading_settings/components/lock_popovers_spec.js
index 585e6ac505bbc..182e3c1c8ff1f 100644
--- a/spec/frontend/cascading_settings/components/lock_popovers_spec.js
+++ b/spec/frontend/cascading_settings/components/lock_popovers_spec.js
@@ -21,12 +21,12 @@ describe('LockPopovers', () => {
     };
 
     if (lockedByApplicationSetting) {
-      popoverMountEl.setAttribute('data-popover-data', JSON.stringify(popoverData));
+      popoverMountEl.dataset.popoverData = JSON.stringify(popoverData);
     } else if (lockedByAncestor) {
-      popoverMountEl.setAttribute(
-        'data-popover-data',
-        JSON.stringify({ ...popoverData, ancestor_namespace: mockNamespace }),
-      );
+      popoverMountEl.dataset.popoverData = JSON.stringify({
+        ...popoverData,
+        ancestor_namespace: mockNamespace,
+      });
     }
 
     document.body.appendChild(popoverMountEl);
diff --git a/spec/frontend/code_navigation/store/actions_spec.js b/spec/frontend/code_navigation/store/actions_spec.js
index c47a9e697b6ee..8eee61d1342f4 100644
--- a/spec/frontend/code_navigation/store/actions_spec.js
+++ b/spec/frontend/code_navigation/store/actions_spec.js
@@ -195,8 +195,8 @@ describe('Code navigation actions', () => {
 
     it('commits SET_CURRENT_DEFINITION with LSIF data', () => {
       target.classList.add('js-code-navigation');
-      target.setAttribute('data-line-index', '0');
-      target.setAttribute('data-char-index', '0');
+      target.dataset.lineIndex = '0';
+      target.dataset.charIndex = '0';
 
       return testAction(
         actions.showDefinition,
@@ -218,8 +218,8 @@ describe('Code navigation actions', () => {
 
     it('adds hll class to target element', () => {
       target.classList.add('js-code-navigation');
-      target.setAttribute('data-line-index', '0');
-      target.setAttribute('data-char-index', '0');
+      target.dataset.lineIndex = '0';
+      target.dataset.charIndex = '0';
 
       return testAction(
         actions.showDefinition,
@@ -243,8 +243,8 @@ describe('Code navigation actions', () => {
 
     it('caches current target element', () => {
       target.classList.add('js-code-navigation');
-      target.setAttribute('data-line-index', '0');
-      target.setAttribute('data-char-index', '0');
+      target.dataset.lineIndex = '0';
+      target.dataset.charIndex = '0';
 
       return testAction(
         actions.showDefinition,
diff --git a/spec/frontend/confirm_modal_spec.js b/spec/frontend/confirm_modal_spec.js
index 53991349ee544..4224fb6be2a15 100644
--- a/spec/frontend/confirm_modal_spec.js
+++ b/spec/frontend/confirm_modal_spec.js
@@ -31,9 +31,9 @@ describe('ConfirmModal', () => {
     buttons.forEach((x) => {
       const button = document.createElement('button');
       button.setAttribute('class', 'js-confirm-modal-button');
-      button.setAttribute('data-path', x.path);
-      button.setAttribute('data-method', x.method);
-      button.setAttribute('data-modal-attributes', JSON.stringify(x.modalAttributes));
+      button.dataset.path = x.path;
+      button.dataset.method = x.method;
+      button.dataset.modalAttributes = JSON.stringify(x.modalAttributes);
       button.innerHTML = 'Action';
       buttonContainer.appendChild(button);
     });
diff --git a/spec/frontend/helpers/startup_css_helper_spec.js b/spec/frontend/helpers/startup_css_helper_spec.js
index 2236b5aa261c9..05161437c2257 100644
--- a/spec/frontend/helpers/startup_css_helper_spec.js
+++ b/spec/frontend/helpers/startup_css_helper_spec.js
@@ -59,9 +59,10 @@ describe('waitForCSSLoaded', () => {
         <link href="two.css" data-startupcss="loading">
       `);
       const events = waitForCSSLoaded(mockedCallback);
-      document
-        .querySelectorAll('[data-startupcss="loading"]')
-        .forEach((elem) => elem.setAttribute('data-startupcss', 'loaded'));
+      document.querySelectorAll('[data-startupcss="loading"]').forEach((elem) => {
+        // eslint-disable-next-line no-param-reassign
+        elem.dataset.startupcss = 'loaded';
+      });
       document.dispatchEvent(new CustomEvent('CSSStartupLinkLoaded'));
       await events;
 
diff --git a/spec/frontend/issues/create_merge_request_dropdown_spec.js b/spec/frontend/issues/create_merge_request_dropdown_spec.js
index 20b26f5abbaf4..cb7173c56a844 100644
--- a/spec/frontend/issues/create_merge_request_dropdown_spec.js
+++ b/spec/frontend/issues/create_merge_request_dropdown_spec.js
@@ -84,7 +84,7 @@ describe('CreateMergeRequestDropdown', () => {
     });
 
     it('enables when can create confidential issue', () => {
-      document.querySelector('.js-create-mr').setAttribute('data-is-confidential', 'true');
+      document.querySelector('.js-create-mr').dataset.isConfidential = 'true';
       confidentialState.selectedProject = { name: 'test' };
 
       dropdown.enable();
@@ -93,7 +93,7 @@ describe('CreateMergeRequestDropdown', () => {
     });
 
     it('does not enable when can not create confidential issue', () => {
-      document.querySelector('.js-create-mr').setAttribute('data-is-confidential', 'true');
+      document.querySelector('.js-create-mr').dataset.isConfidential = 'true';
 
       dropdown.enable();
 
diff --git a/spec/frontend/labels/delete_label_modal_spec.js b/spec/frontend/labels/delete_label_modal_spec.js
index 9804953894818..67220821fe083 100644
--- a/spec/frontend/labels/delete_label_modal_spec.js
+++ b/spec/frontend/labels/delete_label_modal_spec.js
@@ -25,11 +25,11 @@ describe('DeleteLabelModal', () => {
     buttons.forEach((x) => {
       const button = document.createElement('button');
       button.setAttribute('class', 'js-delete-label-modal-button');
-      button.setAttribute('data-label-name', x.labelName);
-      button.setAttribute('data-destroy-path', x.destroyPath);
+      button.dataset.labelName = x.labelName;
+      button.dataset.destroyPath = x.destroyPath;
 
       if (x.subjectName) {
-        button.setAttribute('data-subject-name', x.subjectName);
+        button.dataset.subjectName = x.subjectName;
       }
 
       button.innerHTML = 'Action';
diff --git a/spec/frontend/lazy_loader_spec.js b/spec/frontend/lazy_loader_spec.js
index 3d8b0d9c30770..e0b6c7119f956 100644
--- a/spec/frontend/lazy_loader_spec.js
+++ b/spec/frontend/lazy_loader_spec.js
@@ -27,7 +27,7 @@ describe('LazyLoader', () => {
   const createLazyLoadImage = () => {
     const newImg = document.createElement('img');
     newImg.className = 'lazy';
-    newImg.setAttribute('data-src', TEST_PATH);
+    newImg.dataset.src = TEST_PATH;
 
     document.body.appendChild(newImg);
     triggerChildMutation();
@@ -108,7 +108,7 @@ describe('LazyLoader', () => {
 
         expect(LazyLoader.loadImage).toHaveBeenCalledWith(img);
         expect(img.getAttribute('src')).toBe(TEST_PATH);
-        expect(img.getAttribute('data-src')).toBe(null);
+        expect(img.dataset.src).toBeUndefined();
         expect(img).toHaveClass('js-lazy-loaded');
       });
 
diff --git a/spec/frontend/members/index_spec.js b/spec/frontend/members/index_spec.js
index efabe54f23841..251a8b0b7740b 100644
--- a/spec/frontend/members/index_spec.js
+++ b/spec/frontend/members/index_spec.js
@@ -24,7 +24,7 @@ describe('initMembersApp', () => {
 
   beforeEach(() => {
     el = document.createElement('div');
-    el.setAttribute('data-members-data', dataAttribute);
+    el.dataset.membersData = dataAttribute;
 
     window.gon = { current_user_id: 123 };
   });
diff --git a/spec/frontend/members/utils_spec.js b/spec/frontend/members/utils_spec.js
index a157cfa1c1dab..b0c9459ff4ff2 100644
--- a/spec/frontend/members/utils_spec.js
+++ b/spec/frontend/members/utils_spec.js
@@ -256,7 +256,7 @@ describe('Members Utils', () => {
 
     beforeEach(() => {
       el = document.createElement('div');
-      el.setAttribute('data-members-data', dataAttribute);
+      el.dataset.membersData = dataAttribute;
     });
 
     afterEach(() => {
diff --git a/spec/frontend/notebook/cells/markdown_spec.js b/spec/frontend/notebook/cells/markdown_spec.js
index 7dc6f90d20223..de415b5bfe04c 100644
--- a/spec/frontend/notebook/cells/markdown_spec.js
+++ b/spec/frontend/notebook/cells/markdown_spec.js
@@ -78,8 +78,8 @@ describe('Markdown component', () => {
     });
 
     await nextTick();
-    expect(findLink().getAttribute('data-remote')).toBe(null);
-    expect(findLink().getAttribute('data-type')).toBe(null);
+    expect(findLink().dataset.remote).toBeUndefined();
+    expect(findLink().dataset.type).toBeUndefined();
   });
 
   describe('When parsing images', () => {
diff --git a/spec/frontend/notes/stores/actions_spec.js b/spec/frontend/notes/stores/actions_spec.js
index 4ecfbc5de1f46..38f29ac25596c 100644
--- a/spec/frontend/notes/stores/actions_spec.js
+++ b/spec/frontend/notes/stores/actions_spec.js
@@ -404,13 +404,13 @@ describe('Actions Notes Store', () => {
     beforeEach(() => {
       axiosMock.onDelete(endpoint).replyOnce(200, {});
 
-      document.body.setAttribute('data-page', '');
+      document.body.dataset.page = '';
     });
 
     afterEach(() => {
       axiosMock.restore();
 
-      document.body.setAttribute('data-page', '');
+      document.body.dataset.page = '';
     });
 
     it('commits DELETE_NOTE and dispatches updateMergeRequestWidget', () => {
@@ -440,7 +440,7 @@ describe('Actions Notes Store', () => {
     it('dispatches removeDiscussionsFromDiff on merge request page', () => {
       const note = { path: endpoint, id: 1 };
 
-      document.body.setAttribute('data-page', 'projects:merge_requests:show');
+      document.body.dataset.page = 'projects:merge_requests:show';
 
       return testAction(
         actions.removeNote,
@@ -473,13 +473,13 @@ describe('Actions Notes Store', () => {
     beforeEach(() => {
       axiosMock.onDelete(endpoint).replyOnce(200, {});
 
-      document.body.setAttribute('data-page', '');
+      document.body.dataset.page = '';
     });
 
     afterEach(() => {
       axiosMock.restore();
 
-      document.body.setAttribute('data-page', '');
+      document.body.dataset.page = '';
     });
 
     it('dispatches removeNote', () => {
diff --git a/spec/frontend/performance_bar/index_spec.js b/spec/frontend/performance_bar/index_spec.js
index 008961bf70937..2da176dbfe49b 100644
--- a/spec/frontend/performance_bar/index_spec.js
+++ b/spec/frontend/performance_bar/index_spec.js
@@ -17,11 +17,11 @@ describe('performance bar wrapper', () => {
     performance.getEntriesByType = jest.fn().mockReturnValue([]);
 
     peekWrapper.setAttribute('id', 'js-peek');
-    peekWrapper.setAttribute('data-env', 'development');
-    peekWrapper.setAttribute('data-request-id', '123');
-    peekWrapper.setAttribute('data-peek-url', '/-/peek/results');
-    peekWrapper.setAttribute('data-stats-url', 'https://log.gprd.gitlab.net/app/dashboards#/view/');
-    peekWrapper.setAttribute('data-profile-url', '?lineprofiler=true');
+    peekWrapper.dataset.env = 'development';
+    peekWrapper.dataset.requestId = '123';
+    peekWrapper.dataset.peekUrl = '/-/peek/results';
+    peekWrapper.dataset.statsUrl = 'https://log.gprd.gitlab.net/app/dashboards#/view/';
+    peekWrapper.dataset.profileUrl = '?lineprofiler=true';
 
     mock = new MockAdapter(axios);
 
diff --git a/spec/frontend/search_autocomplete_spec.js b/spec/frontend/search_autocomplete_spec.js
index 4639552b4d3ac..266f047e9dc91 100644
--- a/spec/frontend/search_autocomplete_spec.js
+++ b/spec/frontend/search_autocomplete_spec.js
@@ -53,7 +53,7 @@ describe('Search autocomplete dropdown', () => {
   };
 
   const disableProjectIssues = () => {
-    document.querySelector('.js-search-project-options').setAttribute('data-issues-disabled', true);
+    document.querySelector('.js-search-project-options').dataset.issuesDisabled = true;
   };
 
   // Mock `gl` object in window for dashboard specific page. App code will need it.
diff --git a/spec/frontend/user_popovers_spec.js b/spec/frontend/user_popovers_spec.js
index 2c3db36d7e630..1544fed5240f5 100644
--- a/spec/frontend/user_popovers_spec.js
+++ b/spec/frontend/user_popovers_spec.js
@@ -22,7 +22,7 @@ describe('User Popovers', () => {
     const link = document.createElement('a');
 
     link.classList.add('js-user-link');
-    link.setAttribute('data-user', '1');
+    link.dataset.user = '1';
 
     return link;
   };
diff --git a/spec/frontend/users_select/test_helper.js b/spec/frontend/users_select/test_helper.js
index 59edde48eab4c..9231e38ea9030 100644
--- a/spec/frontend/users_select/test_helper.js
+++ b/spec/frontend/users_select/test_helper.js
@@ -95,10 +95,10 @@ export const setAssignees = (...users) => {
       const input = document.createElement('input');
       input.name = 'merge_request[assignee_ids][]';
       input.value = user.id.toString();
-      input.setAttribute('data-avatar-url', user.avatar_url);
-      input.setAttribute('data-name', user.name);
-      input.setAttribute('data-username', user.username);
-      input.setAttribute('data-can-merge', user.can_merge);
+      input.dataset.avatarUrl = user.avatar_url;
+      input.dataset.name = user.name;
+      input.dataset.username = user.username;
+      input.dataset.canMerge = user.can_merge;
       return input;
     }),
   );
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js
index 8efc4d8462476..29ee7e0010fc1 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js
@@ -193,9 +193,7 @@ describe('MRWidgetMerged', () => {
 
   it('shows button to copy commit SHA to clipboard', () => {
     expect(selectors.copyMergeShaButton).not.toBe(null);
-    expect(selectors.copyMergeShaButton.getAttribute('data-clipboard-text')).toBe(
-      vm.mr.mergeCommitSha,
-    );
+    expect(selectors.copyMergeShaButton.dataset.clipboardText).toBe(vm.mr.mergeCommitSha);
   });
 
   it('hides button to copy commit SHA if SHA does not exist', async () => {
diff --git a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
index 298ac764d732a..bfd921d5e4e23 100644
--- a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
@@ -427,7 +427,7 @@ describe('MrWidgetOptions', () => {
         beforeEach(() => {
           const favicon = document.createElement('link');
           favicon.setAttribute('id', 'favicon');
-          favicon.setAttribute('data-original-href', faviconDataUrl);
+          favicon.dataset.originalHref = faviconDataUrl;
           document.body.appendChild(favicon);
 
           faviconElement = document.getElementById('favicon');
diff --git a/spec/frontend_integration/ide/helpers/ide_helper.js b/spec/frontend_integration/ide/helpers/ide_helper.js
index 5253560c6553c..20a1e5aceb207 100644
--- a/spec/frontend_integration/ide/helpers/ide_helper.js
+++ b/spec/frontend_integration/ide/helpers/ide_helper.js
@@ -46,14 +46,14 @@ export const findMonacoDiffEditor = () =>
 
 export const findAndSetEditorValue = async (value) => {
   const editor = await findMonacoEditor();
-  const uri = editor.getAttribute('data-uri');
+  const { uri } = editor.dataset;
 
   monacoEditor.getModel(uri).setValue(value);
 };
 
 export const getEditorValue = async () => {
   const editor = await findMonacoEditor();
-  const uri = editor.getAttribute('data-uri');
+  const { uri } = editor.dataset;
 
   return monacoEditor.getModel(uri).getValue();
 };
-- 
GitLab