From 0f4af88bf13a17345b484fb0881acf199f91fa48 Mon Sep 17 00:00:00 2001 From: Jeremy Davis Date: Thu, 10 Oct 2024 17:14:10 +0200 Subject: [PATCH] SONAR-22298 Fix clipboard a11y --- .../src/components/DropdownMenu.tsx | 28 +- .../__snapshots__/CodeSnippet-test.tsx.snap | 565 ++++++++++-------- .../components/__tests__/clipboard-test.tsx | 8 +- .../src/components/clipboard.tsx | 207 +++---- .../components/RuleDetailsHeader.tsx | 5 +- .../apps/issues/__tests__/IssueHeader-it.tsx | 4 - .../js/apps/issues/components/IssueHeader.tsx | 4 +- .../components/HotspotHeader.tsx | 4 +- .../encryption/__tests__/EncryptionApp-it.tsx | 8 +- .../components/__tests__/SystemApp-it.tsx | 10 +- .../__tests__/SourceViewer-it.tsx | 16 +- .../__tests__/AzurePipelinesTutorial-it.tsx | 44 +- .../BitbucketPipelinesTutorial-it.tsx | 63 +- .../BitbucketPipelinesTutorial-it.tsx.snap | 6 +- .../__tests__/GithubActionTutorial-it.tsx | 65 +- .../GithubActionTutorial-it.tsx.snap | 59 +- .../__tests__/GitLabCITutorial-it.tsx | 49 +- .../jenkins/__tests__/JenkinsTutorial-it.tsx | 70 ++- .../other/__tests__/OtherTutorial-it.tsx | 158 +++-- .../js/components/tutorials/test-utils.ts | 26 +- 20 files changed, 764 insertions(+), 635 deletions(-) diff --git a/server/sonar-web/design-system/src/components/DropdownMenu.tsx b/server/sonar-web/design-system/src/components/DropdownMenu.tsx index e3be15c7a1a..1251298bfcb 100644 --- a/server/sonar-web/design-system/src/components/DropdownMenu.tsx +++ b/server/sonar-web/design-system/src/components/DropdownMenu.tsx @@ -29,7 +29,7 @@ import { InputSizeKeys, ThemedProps } from '../types/theme'; import { BaseLink, LinkProps } from './Link'; import NavLink from './NavLink'; import { Tooltip } from './Tooltip'; -import { ClipboardBase } from './clipboard'; +import { useCopyClipboardEffect } from './clipboard'; import { Checkbox } from './input/Checkbox'; interface Props extends React.HtmlHTMLAttributes { @@ -223,23 +223,17 @@ interface ItemCopyProps { export function ItemCopy(props: ItemCopyProps) { const { children, className, copyValue, tooltipOverlay } = props; + + const [copySuccess, handleCopy] = useCopyClipboardEffect(copyValue); + return ( - - {({ setCopyButton, copySuccess }) => ( - -
  • - - {children} - -
  • -
    - )} -
    + +
  • + + {children} + +
  • +
    ); } diff --git a/server/sonar-web/design-system/src/components/__tests__/__snapshots__/CodeSnippet-test.tsx.snap b/server/sonar-web/design-system/src/components/__tests__/__snapshots__/CodeSnippet-test.tsx.snap index 1d7d505e179..ac2ea24e199 100644 --- a/server/sonar-web/design-system/src/components/__tests__/__snapshots__/CodeSnippet-test.tsx.snap +++ b/server/sonar-web/design-system/src/components/__tests__/__snapshots__/CodeSnippet-test.tsx.snap @@ -16,16 +16,8 @@ exports[`should highlight code content correctly 1`] = ` border-radius: 0.25rem; } -.emotion-4 { +.emotion-3 { box-sizing: border-box; - -webkit-text-decoration: none; - text-decoration: none; - outline: none; - border: var(--border); - color: var(--color); - background-color: var(--background); - -webkit-transition: background-color 0.2s ease; - transition: background-color 0.2s ease; display: -webkit-inline-box; display: -webkit-inline-flex; display: -ms-inline-flexbox; @@ -34,19 +26,28 @@ exports[`should highlight code content correctly 1`] = ` -webkit-box-align: center; -ms-flex-align: center; align-items: center; - height: 2.25rem; - font: var(--echoes-typography-text-default-semi-bold); - padding-left: 1rem; - padding-right: 1rem; - padding-top: 0.5rem; - padding-bottom: 0.5rem; - border-radius: 0.5rem; + padding: var(--echoes-dimension-space-0) var(--button-padding); + height: var(--button-height); + min-height: var(--button-height); + overflow: hidden; + font: var(--echoes-typography-others-label); + color: var(--button-color); + -webkit-text-decoration: none; + text-decoration: none; + background-color: var(--button-background); + border: var(--button-border); + border-radius: var(--echoes-border-radius-400); + outline: none; cursor: pointer; - --background: rgb(255,255,255); - --backgroundHover: rgb(239,242,249); - --color: rgb(62,67,87); - --focus: rgba(197,205,223,0.2); - --border: 1px solid rgb(197,205,223); + --button-color: var(--echoes-color-text-default); + --button-border: var(--echoes-color-border-bold) solid var(--echoes-border-width-default); + --button-background: var(--echoes-color-background-default); + --button-background-hover: var(--echoes-color-background-default-hover); + --button-background-active: var(--echoes-color-background-default-active); + --button-background-focus: var(--echoes-color-background-default); + --button-background-disabled: var(--echoes-color-background-disabled); + --button-padding: var(--echoes-dimension-space-150); + --button-height: var(--echoes-sizes-buttons-large); -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; @@ -57,90 +58,115 @@ exports[`should highlight code content correctly 1`] = ` position: absolute; } -.emotion-4:hover, -.emotion-4:active { - color: var(--color); - background-color: var(--backgroundHover); +.emotion-3:focus, +.emotion-3:focus-visible { + background-color: var(--button-background-focus); + outline: var(--echoes-color-focus-default) solid var(--echoes-focus-border-width-default); + outline-offset: var(--echoes-focus-border-offset-default); } -.emotion-4:focus, -.emotion-4:active, -.emotion-4:focus-visible { - color: var(--color); +.emotion-3:hover { + background-color: var(--button-background-hover); } -.emotion-4:focus-visible { - outline: var(--echoes-focus-border-width-default) solid var(--echoes-color-focus-default); - outline-offset: var(--echoes-focus-border-offset-default); +.emotion-3:active { + background-color: var(--button-background-active); } -.emotion-4:disabled, -.emotion-4:disabled:hover { +.emotion-3:disabled, +.emotion-3:disabled:has(:hover, :active, :focus, :focus-visible) { color: var(--echoes-color-text-disabled); - background-color: rgb(239,242,249); - border: 1px solid rgb(197,205,223); + background-color: var(--button-background-disabled); + border: none; cursor: not-allowed; + pointer-events: none; } -.emotion-4>svg { - margin-right: 0.25rem; +.code-snippet-highlighted-oneline .emotion-3 { + bottom: 0.5rem; } -.emotion-4 [disabled] { - pointer-events: none; +.emotion-5 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + gap: var(--echoes-dimension-space-75); + overflow: hidden; } -.code-snippet-highlighted-oneline .emotion-4 { - bottom: 0.5rem; +.emotion-7 { + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + display: inline-block; + font-size: calc(1em + 4px); + font-style: normal; + font-weight: normal; + height: calc(2em - 16px); + line-height: calc(2em - 16px); + text-align: center; + vertical-align: bottom; + width: calc(2em - 16px); + font-family: 'Material Symbols Rounded'; +} + +.emotion-9 { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } -.emotion-6 code { +.emotion-11 code { font: var(--echoes-typography-code-default); background: rgb(252,252,253); color: rgb(51,53,60); } -.emotion-6 code.hljs { +.emotion-11 code.hljs { padding: unset; } -.emotion-6 .hljs-meta, -.emotion-6 .hljs-variable { +.emotion-11 .hljs-meta, +.emotion-11 .hljs-variable { color: rgb(51,53,60); } -.emotion-6 .hljs-doctag, -.emotion-6 .hljs-title, -.emotion-6 .hljs-title.class_, -.emotion-6 .hljs-title.function_ { +.emotion-11 .hljs-doctag, +.emotion-11 .hljs-title, +.emotion-11 .hljs-title.class_, +.emotion-11 .hljs-title.function_ { color: rgb(34,84,192); } -.emotion-6 .hljs-comment { +.emotion-11 .hljs-comment { font: var(--echoes-typography-code-comment); color: rgb(109,111,119); } -.emotion-6 .hljs-keyword, -.emotion-6 .hljs-tag, -.emotion-6 .hljs-type { +.emotion-11 .hljs-keyword, +.emotion-11 .hljs-tag, +.emotion-11 .hljs-type { color: rgb(152,29,150); } -.emotion-6 .hljs-literal, -.emotion-6 .hljs-number { +.emotion-11 .hljs-literal, +.emotion-11 .hljs-number { color: rgb(126,83,5); } -.emotion-6 .hljs-string { +.emotion-11 .hljs-string { color: rgb(32,105,31); } -.emotion-6 .hljs-meta .hljs-keyword { +.emotion-11 .hljs-meta .hljs-keyword { color: rgb(47,103,48); } -.emotion-6 .sonar-underline { +.emotion-11 .sonar-underline { -webkit-text-decoration: underline rgb(253,162,155); text-decoration: underline rgb(253,162,155); -webkit-text-decoration: underline rgb(253,162,155) wavy; @@ -149,17 +175,17 @@ exports[`should highlight code content correctly 1`] = ` text-decoration-skip-ink: none; } -.emotion-6.code-wrap { +.emotion-11.code-wrap { white-space: pre-wrap; word-break: break-all; } -.emotion-6.code-wrap.wrap-words { +.emotion-11.code-wrap.wrap-words { word-break: normal; overflow-wrap: break-word; } -.emotion-6 mark { +.emotion-11 mark { font-weight: 400; padding: 0.25rem; border-radius: 0.25rem; @@ -172,32 +198,28 @@ exports[`should highlight code content correctly 1`] = ` class="sw-code fs-mask emotion-0 emotion-1" >
             <prop>foobar<prop>
    @@ -223,16 +245,8 @@ exports[`should show full size when multiline with no editing 1`] = `
       border-radius: 0.25rem;
     }
     
    -.emotion-4 {
    +.emotion-3 {
       box-sizing: border-box;
    -  -webkit-text-decoration: none;
    -  text-decoration: none;
    -  outline: none;
    -  border: var(--border);
    -  color: var(--color);
    -  background-color: var(--background);
    -  -webkit-transition: background-color 0.2s ease;
    -  transition: background-color 0.2s ease;
       display: -webkit-inline-box;
       display: -webkit-inline-flex;
       display: -ms-inline-flexbox;
    @@ -241,19 +255,28 @@ exports[`should show full size when multiline with no editing 1`] = `
       -webkit-box-align: center;
       -ms-flex-align: center;
       align-items: center;
    -  height: 2.25rem;
    -  font: var(--echoes-typography-text-default-semi-bold);
    -  padding-left: 1rem;
    -  padding-right: 1rem;
    -  padding-top: 0.5rem;
    -  padding-bottom: 0.5rem;
    -  border-radius: 0.5rem;
    +  padding: var(--echoes-dimension-space-0) var(--button-padding);
    +  height: var(--button-height);
    +  min-height: var(--button-height);
    +  overflow: hidden;
    +  font: var(--echoes-typography-others-label);
    +  color: var(--button-color);
    +  -webkit-text-decoration: none;
    +  text-decoration: none;
    +  background-color: var(--button-background);
    +  border: var(--button-border);
    +  border-radius: var(--echoes-border-radius-400);
    +  outline: none;
       cursor: pointer;
    -  --background: rgb(255,255,255);
    -  --backgroundHover: rgb(239,242,249);
    -  --color: rgb(62,67,87);
    -  --focus: rgba(197,205,223,0.2);
    -  --border: 1px solid rgb(197,205,223);
    +  --button-color: var(--echoes-color-text-default);
    +  --button-border: var(--echoes-color-border-bold) solid var(--echoes-border-width-default);
    +  --button-background: var(--echoes-color-background-default);
    +  --button-background-hover: var(--echoes-color-background-default-hover);
    +  --button-background-active: var(--echoes-color-background-default-active);
    +  --button-background-focus: var(--echoes-color-background-default);
    +  --button-background-disabled: var(--echoes-color-background-disabled);
    +  --button-padding: var(--echoes-dimension-space-150);
    +  --button-height: var(--echoes-sizes-buttons-large);
       -webkit-user-select: none;
       -moz-user-select: none;
       -ms-user-select: none;
    @@ -264,90 +287,115 @@ exports[`should show full size when multiline with no editing 1`] = `
       position: absolute;
     }
     
    -.emotion-4:hover,
    -.emotion-4:active {
    -  color: var(--color);
    -  background-color: var(--backgroundHover);
    +.emotion-3:focus,
    +.emotion-3:focus-visible {
    +  background-color: var(--button-background-focus);
    +  outline: var(--echoes-color-focus-default) solid var(--echoes-focus-border-width-default);
    +  outline-offset: var(--echoes-focus-border-offset-default);
     }
     
    -.emotion-4:focus,
    -.emotion-4:active,
    -.emotion-4:focus-visible {
    -  color: var(--color);
    +.emotion-3:hover {
    +  background-color: var(--button-background-hover);
     }
     
    -.emotion-4:focus-visible {
    -  outline: var(--echoes-focus-border-width-default) solid var(--echoes-color-focus-default);
    -  outline-offset: var(--echoes-focus-border-offset-default);
    +.emotion-3:active {
    +  background-color: var(--button-background-active);
     }
     
    -.emotion-4:disabled,
    -.emotion-4:disabled:hover {
    +.emotion-3:disabled,
    +.emotion-3:disabled:has(:hover, :active, :focus, :focus-visible) {
       color: var(--echoes-color-text-disabled);
    -  background-color: rgb(239,242,249);
    -  border: 1px solid rgb(197,205,223);
    +  background-color: var(--button-background-disabled);
    +  border: none;
       cursor: not-allowed;
    +  pointer-events: none;
     }
     
    -.emotion-4>svg {
    -  margin-right: 0.25rem;
    +.code-snippet-highlighted-oneline .emotion-3 {
    +  bottom: 0.5rem;
     }
     
    -.emotion-4 [disabled] {
    -  pointer-events: none;
    +.emotion-5 {
    +  display: -webkit-box;
    +  display: -webkit-flex;
    +  display: -ms-flexbox;
    +  display: flex;
    +  -webkit-align-items: center;
    +  -webkit-box-align: center;
    +  -ms-flex-align: center;
    +  align-items: center;
    +  gap: var(--echoes-dimension-space-75);
    +  overflow: hidden;
     }
     
    -.code-snippet-highlighted-oneline .emotion-4 {
    -  bottom: 0.5rem;
    +.emotion-7 {
    +  -moz-osx-font-smoothing: grayscale;
    +  -webkit-font-smoothing: antialiased;
    +  display: inline-block;
    +  font-size: calc(1em + 4px);
    +  font-style: normal;
    +  font-weight: normal;
    +  height: calc(2em - 16px);
    +  line-height: calc(2em - 16px);
    +  text-align: center;
    +  vertical-align: bottom;
    +  width: calc(2em - 16px);
    +  font-family: 'Material Symbols Rounded';
     }
     
    -.emotion-6 code {
    +.emotion-9 {
    +  overflow: hidden;
    +  text-overflow: ellipsis;
    +  white-space: nowrap;
    +}
    +
    +.emotion-11 code {
       font: var(--echoes-typography-code-default);
       background: rgb(252,252,253);
       color: rgb(51,53,60);
     }
     
    -.emotion-6 code.hljs {
    +.emotion-11 code.hljs {
       padding: unset;
     }
     
    -.emotion-6 .hljs-meta,
    -.emotion-6 .hljs-variable {
    +.emotion-11 .hljs-meta,
    +.emotion-11 .hljs-variable {
       color: rgb(51,53,60);
     }
     
    -.emotion-6 .hljs-doctag,
    -.emotion-6 .hljs-title,
    -.emotion-6 .hljs-title.class_,
    -.emotion-6 .hljs-title.function_ {
    +.emotion-11 .hljs-doctag,
    +.emotion-11 .hljs-title,
    +.emotion-11 .hljs-title.class_,
    +.emotion-11 .hljs-title.function_ {
       color: rgb(34,84,192);
     }
     
    -.emotion-6 .hljs-comment {
    +.emotion-11 .hljs-comment {
       font: var(--echoes-typography-code-comment);
       color: rgb(109,111,119);
     }
     
    -.emotion-6 .hljs-keyword,
    -.emotion-6 .hljs-tag,
    -.emotion-6 .hljs-type {
    +.emotion-11 .hljs-keyword,
    +.emotion-11 .hljs-tag,
    +.emotion-11 .hljs-type {
       color: rgb(152,29,150);
     }
     
    -.emotion-6 .hljs-literal,
    -.emotion-6 .hljs-number {
    +.emotion-11 .hljs-literal,
    +.emotion-11 .hljs-number {
       color: rgb(126,83,5);
     }
     
    -.emotion-6 .hljs-string {
    +.emotion-11 .hljs-string {
       color: rgb(32,105,31);
     }
     
    -.emotion-6 .hljs-meta .hljs-keyword {
    +.emotion-11 .hljs-meta .hljs-keyword {
       color: rgb(47,103,48);
     }
     
    -.emotion-6 .sonar-underline {
    +.emotion-11 .sonar-underline {
       -webkit-text-decoration: underline rgb(253,162,155);
       text-decoration: underline rgb(253,162,155);
       -webkit-text-decoration: underline rgb(253,162,155) wavy;
    @@ -356,17 +404,17 @@ exports[`should show full size when multiline with no editing 1`] = `
       text-decoration-skip-ink: none;
     }
     
    -.emotion-6.code-wrap {
    +.emotion-11.code-wrap {
       white-space: pre-wrap;
       word-break: break-all;
     }
     
    -.emotion-6.code-wrap.wrap-words {
    +.emotion-11.code-wrap.wrap-words {
       word-break: normal;
       overflow-wrap: break-word;
     }
     
    -.emotion-6 mark {
    +.emotion-11 mark {
       font-weight: 400;
       padding: 0.25rem;
       border-radius: 0.25rem;
    @@ -379,33 +427,28 @@ exports[`should show full size when multiline with no editing 1`] = `
         class="sw-code fs-mask emotion-0 emotion-1"
       >
         
         
           
             foo
    @@ -432,16 +475,8 @@ exports[`should show reduced size when single line with no editing 1`] = `
       border-radius: 0.25rem;
     }
     
    -.emotion-4 {
    +.emotion-3 {
       box-sizing: border-box;
    -  -webkit-text-decoration: none;
    -  text-decoration: none;
    -  outline: none;
    -  border: var(--border);
    -  color: var(--color);
    -  background-color: var(--background);
    -  -webkit-transition: background-color 0.2s ease;
    -  transition: background-color 0.2s ease;
       display: -webkit-inline-box;
       display: -webkit-inline-flex;
       display: -ms-inline-flexbox;
    @@ -450,19 +485,28 @@ exports[`should show reduced size when single line with no editing 1`] = `
       -webkit-box-align: center;
       -ms-flex-align: center;
       align-items: center;
    -  height: 2.25rem;
    -  font: var(--echoes-typography-text-default-semi-bold);
    -  padding-left: 1rem;
    -  padding-right: 1rem;
    -  padding-top: 0.5rem;
    -  padding-bottom: 0.5rem;
    -  border-radius: 0.5rem;
    +  padding: var(--echoes-dimension-space-0) var(--button-padding);
    +  height: var(--button-height);
    +  min-height: var(--button-height);
    +  overflow: hidden;
    +  font: var(--echoes-typography-others-label);
    +  color: var(--button-color);
    +  -webkit-text-decoration: none;
    +  text-decoration: none;
    +  background-color: var(--button-background);
    +  border: var(--button-border);
    +  border-radius: var(--echoes-border-radius-400);
    +  outline: none;
       cursor: pointer;
    -  --background: rgb(255,255,255);
    -  --backgroundHover: rgb(239,242,249);
    -  --color: rgb(62,67,87);
    -  --focus: rgba(197,205,223,0.2);
    -  --border: 1px solid rgb(197,205,223);
    +  --button-color: var(--echoes-color-text-default);
    +  --button-border: var(--echoes-color-border-bold) solid var(--echoes-border-width-default);
    +  --button-background: var(--echoes-color-background-default);
    +  --button-background-hover: var(--echoes-color-background-default-hover);
    +  --button-background-active: var(--echoes-color-background-default-active);
    +  --button-background-focus: var(--echoes-color-background-default);
    +  --button-background-disabled: var(--echoes-color-background-disabled);
    +  --button-padding: var(--echoes-dimension-space-150);
    +  --button-height: var(--echoes-sizes-buttons-large);
       -webkit-user-select: none;
       -moz-user-select: none;
       -ms-user-select: none;
    @@ -475,90 +519,115 @@ exports[`should show reduced size when single line with no editing 1`] = `
       top: 1rem;
     }
     
    -.emotion-4:hover,
    -.emotion-4:active {
    -  color: var(--color);
    -  background-color: var(--backgroundHover);
    +.emotion-3:focus,
    +.emotion-3:focus-visible {
    +  background-color: var(--button-background-focus);
    +  outline: var(--echoes-color-focus-default) solid var(--echoes-focus-border-width-default);
    +  outline-offset: var(--echoes-focus-border-offset-default);
     }
     
    -.emotion-4:focus,
    -.emotion-4:active,
    -.emotion-4:focus-visible {
    -  color: var(--color);
    +.emotion-3:hover {
    +  background-color: var(--button-background-hover);
     }
     
    -.emotion-4:focus-visible {
    -  outline: var(--echoes-focus-border-width-default) solid var(--echoes-color-focus-default);
    -  outline-offset: var(--echoes-focus-border-offset-default);
    +.emotion-3:active {
    +  background-color: var(--button-background-active);
     }
     
    -.emotion-4:disabled,
    -.emotion-4:disabled:hover {
    +.emotion-3:disabled,
    +.emotion-3:disabled:has(:hover, :active, :focus, :focus-visible) {
       color: var(--echoes-color-text-disabled);
    -  background-color: rgb(239,242,249);
    -  border: 1px solid rgb(197,205,223);
    +  background-color: var(--button-background-disabled);
    +  border: none;
       cursor: not-allowed;
    +  pointer-events: none;
     }
     
    -.emotion-4>svg {
    -  margin-right: 0.25rem;
    +.code-snippet-highlighted-oneline .emotion-3 {
    +  bottom: 0.5rem;
     }
     
    -.emotion-4 [disabled] {
    -  pointer-events: none;
    +.emotion-5 {
    +  display: -webkit-box;
    +  display: -webkit-flex;
    +  display: -ms-flexbox;
    +  display: flex;
    +  -webkit-align-items: center;
    +  -webkit-box-align: center;
    +  -ms-flex-align: center;
    +  align-items: center;
    +  gap: var(--echoes-dimension-space-75);
    +  overflow: hidden;
     }
     
    -.code-snippet-highlighted-oneline .emotion-4 {
    -  bottom: 0.5rem;
    +.emotion-7 {
    +  -moz-osx-font-smoothing: grayscale;
    +  -webkit-font-smoothing: antialiased;
    +  display: inline-block;
    +  font-size: calc(1em + 4px);
    +  font-style: normal;
    +  font-weight: normal;
    +  height: calc(2em - 16px);
    +  line-height: calc(2em - 16px);
    +  text-align: center;
    +  vertical-align: bottom;
    +  width: calc(2em - 16px);
    +  font-family: 'Material Symbols Rounded';
    +}
    +
    +.emotion-9 {
    +  overflow: hidden;
    +  text-overflow: ellipsis;
    +  white-space: nowrap;
     }
     
    -.emotion-6 code {
    +.emotion-11 code {
       font: var(--echoes-typography-code-default);
       background: rgb(252,252,253);
       color: rgb(51,53,60);
     }
     
    -.emotion-6 code.hljs {
    +.emotion-11 code.hljs {
       padding: unset;
     }
     
    -.emotion-6 .hljs-meta,
    -.emotion-6 .hljs-variable {
    +.emotion-11 .hljs-meta,
    +.emotion-11 .hljs-variable {
       color: rgb(51,53,60);
     }
     
    -.emotion-6 .hljs-doctag,
    -.emotion-6 .hljs-title,
    -.emotion-6 .hljs-title.class_,
    -.emotion-6 .hljs-title.function_ {
    +.emotion-11 .hljs-doctag,
    +.emotion-11 .hljs-title,
    +.emotion-11 .hljs-title.class_,
    +.emotion-11 .hljs-title.function_ {
       color: rgb(34,84,192);
     }
     
    -.emotion-6 .hljs-comment {
    +.emotion-11 .hljs-comment {
       font: var(--echoes-typography-code-comment);
       color: rgb(109,111,119);
     }
     
    -.emotion-6 .hljs-keyword,
    -.emotion-6 .hljs-tag,
    -.emotion-6 .hljs-type {
    +.emotion-11 .hljs-keyword,
    +.emotion-11 .hljs-tag,
    +.emotion-11 .hljs-type {
       color: rgb(152,29,150);
     }
     
    -.emotion-6 .hljs-literal,
    -.emotion-6 .hljs-number {
    +.emotion-11 .hljs-literal,
    +.emotion-11 .hljs-number {
       color: rgb(126,83,5);
     }
     
    -.emotion-6 .hljs-string {
    +.emotion-11 .hljs-string {
       color: rgb(32,105,31);
     }
     
    -.emotion-6 .hljs-meta .hljs-keyword {
    +.emotion-11 .hljs-meta .hljs-keyword {
       color: rgb(47,103,48);
     }
     
    -.emotion-6 .sonar-underline {
    +.emotion-11 .sonar-underline {
       -webkit-text-decoration: underline rgb(253,162,155);
       text-decoration: underline rgb(253,162,155);
       -webkit-text-decoration: underline rgb(253,162,155) wavy;
    @@ -567,17 +636,17 @@ exports[`should show reduced size when single line with no editing 1`] = `
       text-decoration-skip-ink: none;
     }
     
    -.emotion-6.code-wrap {
    +.emotion-11.code-wrap {
       white-space: pre-wrap;
       word-break: break-all;
     }
     
    -.emotion-6.code-wrap.wrap-words {
    +.emotion-11.code-wrap.wrap-words {
       word-break: normal;
       overflow-wrap: break-word;
     }
     
    -.emotion-6 mark {
    +.emotion-11 mark {
       font-weight: 400;
       padding: 0.25rem;
       border-radius: 0.25rem;
    @@ -590,32 +659,28 @@ exports[`should show reduced size when single line with no editing 1`] = `
         class="sw-code sw-py-6 code-snippet-highlighted-oneline fs-mask emotion-0 emotion-1"
       >
         
         
           
             foobar
    diff --git a/server/sonar-web/design-system/src/components/__tests__/clipboard-test.tsx b/server/sonar-web/design-system/src/components/__tests__/clipboard-test.tsx
    index 027b990fff6..e073810552e 100644
    --- a/server/sonar-web/design-system/src/components/__tests__/clipboard-test.tsx
    +++ b/server/sonar-web/design-system/src/components/__tests__/clipboard-test.tsx
    @@ -43,9 +43,9 @@ describe('ClipboardButton', () => {
     
         await user.click(screen.getByRole('button', { name: 'Copy' }));
     
    -    expect(await screen.findByText('Copied')).toBeVisible();
    +    expect(await screen.findByRole('tooltip', { name: 'Copied' })).toBeInTheDocument();
     
    -    await waitForElementToBeRemoved(() => screen.queryByText('Copied'));
    +    await waitForElementToBeRemoved(() => screen.queryByRole('tooltip', { name: 'Copied' }));
         jest.runAllTimers();
       });
     
    @@ -74,9 +74,9 @@ describe('ClipboardIconButton', () => {
     
         await user.click(screen.getByRole('button', { name: 'Copy to clipboard' }));
     
    -    expect(await screen.findByText('Copied')).toBeVisible();
    +    expect(await screen.findByRole('tooltip', { name: 'Copied' })).toBeInTheDocument();
     
    -    await waitForElementToBeRemoved(() => screen.queryByText('Copied'));
    +    await waitForElementToBeRemoved(() => screen.queryByRole('tooltip', { name: 'Copied' }));
         jest.runAllTimers();
       });
     });
    diff --git a/server/sonar-web/design-system/src/components/clipboard.tsx b/server/sonar-web/design-system/src/components/clipboard.tsx
    index 1745e74f2fe..530880b86c6 100644
    --- a/server/sonar-web/design-system/src/components/clipboard.tsx
    +++ b/server/sonar-web/design-system/src/components/clipboard.tsx
    @@ -17,91 +17,21 @@
      * along with this program; if not, write to the Free Software Foundation,
      * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
      */
    +import {
    +  Button,
    +  ButtonIcon,
    +  ButtonSize,
    +  ButtonVariety,
    +  IconCopy,
    +  Tooltip,
    +  TooltipProvider,
    +} from '@sonarsource/echoes-react';
     import classNames from 'classnames';
    -import Clipboard from 'clipboard';
    -import React from 'react';
    -import { INTERACTIVE_TOOLTIP_DELAY } from '../helpers/constants';
    -import { ButtonSecondary } from '../sonar-aligned/components/buttons';
    -import { DiscreetInteractiveIcon, InteractiveIcon, InteractiveIconSize } from './InteractiveIcon';
    -import { Tooltip } from './Tooltip';
    -import { CopyIcon } from './icons/CopyIcon';
    -import { IconProps } from './icons/Icon';
    +import { copy } from 'clipboard';
    +import React, { ComponentProps, useCallback, useState } from 'react';
     
     const COPY_SUCCESS_NOTIFICATION_LIFESPAN = 1000;
     
    -export interface State {
    -  copySuccess: boolean;
    -}
    -
    -interface RenderProps {
    -  copySuccess: boolean;
    -  setCopyButton: (node: HTMLElement | null) => void;
    -}
    -
    -interface BaseProps {
    -  children: (props: RenderProps) => React.ReactNode;
    -}
    -
    -export class ClipboardBase extends React.PureComponent {
    -  private clipboard?: Clipboard;
    -  private copyButton?: HTMLElement | null;
    -  mounted = false;
    -  state: State = { copySuccess: false };
    -
    -  componentDidMount() {
    -    this.mounted = true;
    -    if (this.copyButton) {
    -      this.clipboard = new Clipboard(this.copyButton, {
    -        container: this.copyButton.parentElement ?? undefined,
    -      });
    -      this.clipboard.on('success', this.handleSuccessCopy);
    -    }
    -  }
    -
    -  componentDidUpdate(props: BaseProps) {
    -    if (this.props.children !== props.children) {
    -      if (this.clipboard) {
    -        this.clipboard.destroy();
    -      }
    -      if (this.copyButton) {
    -        this.clipboard = new Clipboard(this.copyButton, {
    -          container: this.copyButton.parentElement ?? undefined,
    -        });
    -        this.clipboard.on('success', this.handleSuccessCopy);
    -      }
    -    }
    -  }
    -
    -  componentWillUnmount() {
    -    this.mounted = false;
    -    if (this.clipboard) {
    -      this.clipboard.destroy();
    -    }
    -  }
    -
    -  setCopyButton = (node: HTMLElement | null) => {
    -    this.copyButton = node;
    -  };
    -
    -  handleSuccessCopy = () => {
    -    if (this.mounted) {
    -      this.setState({ copySuccess: true });
    -      setTimeout(() => {
    -        if (this.mounted) {
    -          this.setState({ copySuccess: false });
    -        }
    -      }, COPY_SUCCESS_NOTIFICATION_LIFESPAN);
    -    }
    -  };
    -
    -  render() {
    -    return this.props.children({
    -      setCopyButton: this.setCopyButton,
    -      copySuccess: this.state.copySuccess,
    -    });
    -  }
    -}
    -
     interface ButtonProps {
       children?: React.ReactNode;
       className?: string;
    @@ -111,41 +41,42 @@ interface ButtonProps {
       icon?: React.ReactNode;
     }
     
    -export function ClipboardButton({
    -  icon = ,
    -  className,
    -  children,
    -  copyValue,
    -  copiedLabel = 'Copied',
    -  copyLabel = 'Copy',
    -}: ButtonProps) {
    +export function ClipboardButton(props: ButtonProps) {
    +  const {
    +    icon = ,
    +    className,
    +    children,
    +    copyValue,
    +    copiedLabel = 'Copied',
    +    copyLabel = 'Copy',
    +  } = props;
    +  const [copySuccess, handleCopy] = useCopyClipboardEffect(copyValue);
    +
       return (
    -    
    -      {({ setCopyButton, copySuccess }) => (
    -        
    -          
    -            {children ?? copyLabel}
    -          
    -        
    -      )}
    -    
    +    
    +      {/* TODO ^ Remove TooltipProvider after design-system is reintegrated into sonar-web */}
    +      
    +        
    +      
    +    
       );
     }
     
     interface IconButtonProps {
    -  Icon?: React.ComponentType>;
    +  Icon?: ComponentProps['Icon'];
       'aria-label'?: string;
       className?: string;
       copiedLabel?: string;
       copyLabel?: string;
       copyValue: string;
       discreet?: boolean;
    -  size?: InteractiveIconSize;
    +  size?: ButtonSize;
     }
     
     export function ClipboardIconButton(props: IconButtonProps) {
    @@ -153,37 +84,49 @@ export function ClipboardIconButton(props: IconButtonProps) {
         className,
         copyValue,
         discreet,
    -    size = 'small',
    -    Icon = CopyIcon,
    +    size = ButtonSize.Medium,
    +    Icon = IconCopy,
         copiedLabel = 'Copied',
         copyLabel = 'Copy to clipboard',
       } = props;
    -  const InteractiveIconComponent = discreet ? DiscreetInteractiveIcon : InteractiveIcon;
    +
    +  const [copySuccess, handleCopy] = useCopyClipboardEffect(copyValue);
     
       return (
    -    
    -      {({ setCopyButton, copySuccess }) => {
    -        return (
    -          
    -                {copySuccess ? copiedLabel : copyLabel}
    -              
    -            }
    -            mouseEnterDelay={INTERACTIVE_TOOLTIP_DELAY}
    -            {...(copySuccess ? { visible: copySuccess } : undefined)}
    -          >
    -            
    -          
    -        );
    -      }}
    -    
    +    
    +      {/* TODO ^ Remove TooltipProvider after design-system is reintegrated into sonar-web */}
    +      
    +    
       );
     }
    +
    +export function useCopyClipboardEffect(copyValue: string) {
    +  const [copySuccess, setCopySuccess] = useState(false);
    +
    +  const handleCopy = useCallback(
    +    ({ currentTarget }: React.MouseEvent) => {
    +      const isSuccess = copy(copyValue) === copyValue;
    +      setCopySuccess(isSuccess);
    +
    +      if (isSuccess) {
    +        setTimeout(() => {
    +          setCopySuccess(false);
    +        }, COPY_SUCCESS_NOTIFICATION_LIFESPAN);
    +      }
    +
    +      currentTarget.focus();
    +    },
    +    [copyValue],
    +  );
    +
    +  return [copySuccess, handleCopy] as const;
    +}
    diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsHeader.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsHeader.tsx
    index 5172425030d..abfad7d1414 100644
    --- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsHeader.tsx
    +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsHeader.tsx
    @@ -17,7 +17,8 @@
      * along with this program; if not, write to the Free Software Foundation,
      * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
      */
    -import { ClipboardIconButton, IssueMessageHighlighting, LinkIcon, Title } from 'design-system';
    +import { IconLink } from '@sonarsource/echoes-react';
    +import { ClipboardIconButton, IssueMessageHighlighting, Title } from 'design-system';
     import * as React from 'react';
     import { translate } from '../../../helpers/l10n';
     import { getPathUrlAsString, getRuleUrl } from '../../../helpers/urls';
    @@ -44,7 +45,7 @@ export default function RuleDetailsMeta(props: Readonly) {
             
               <IssueMessageHighlighting message={ruleDetails.name} />
               <ClipboardIconButton
    -            Icon={LinkIcon}
    +            Icon={IconLink}
                 aria-label={translate('permalink')}
                 className="sw-ml-1 sw-align-bottom"
                 copyValue={getPathUrlAsString(ruleUrl, ruleDetails.isExternal)}
    diff --git a/server/sonar-web/src/main/js/apps/issues/__tests__/IssueHeader-it.tsx b/server/sonar-web/src/main/js/apps/issues/__tests__/IssueHeader-it.tsx
    index f249c8f48e4..e16b6bb0182 100644
    --- a/server/sonar-web/src/main/js/apps/issues/__tests__/IssueHeader-it.tsx
    +++ b/server/sonar-web/src/main/js/apps/issues/__tests__/IssueHeader-it.tsx
    @@ -42,10 +42,6 @@ it('renders correctly', async () => {
     
       // Title
       expect(byRole('heading', { name: issue.message }).get()).toBeInTheDocument();
    -  expect(byRole('button', { name: 'permalink' }).get()).toHaveAttribute(
    -    'data-clipboard-text',
    -    'http://localhost/project/issues?issues=AVsae-CQS-9G3txfbFN2&open=AVsae-CQS-9G3txfbFN2&id=myproject',
    -  );
     
       // CCT attribute
       const cctBadge = byText(
    diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssueHeader.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssueHeader.tsx
    index c9d1aa14aa0..452e18f2faa 100644
    --- a/server/sonar-web/src/main/js/apps/issues/components/IssueHeader.tsx
    +++ b/server/sonar-web/src/main/js/apps/issues/components/IssueHeader.tsx
    @@ -17,13 +17,13 @@
      * along with this program; if not, write to the Free Software Foundation,
      * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
      */
    +import { IconLink } from '@sonarsource/echoes-react';
     import {
       Badge,
       BasicSeparator,
       ClipboardIconButton,
       IssueMessageHighlighting,
       Link,
    -  LinkIcon,
       Note,
       PageContentFontWrapper,
     } from 'design-system';
    @@ -172,7 +172,7 @@ export default class IssueHeader extends React.PureComponent<Props, State> {
                       messageFormattings={issue.messageFormattings}
                     />
                     <ClipboardIconButton
    -                  Icon={LinkIcon}
    +                  Icon={IconLink}
                       aria-label={translate('permalink')}
                       className="sw-ml-1 sw-align-bottom"
                       copyValue={getPathUrlAsString(issueUrl, false)}
    diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotHeader.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotHeader.tsx
    index 34fd6605f0f..efa7c66293e 100644
    --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotHeader.tsx
    +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotHeader.tsx
    @@ -17,13 +17,13 @@
      * along with this program; if not, write to the Free Software Foundation,
      * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
      */
    +import { IconLink } from '@sonarsource/echoes-react';
     import {
       ClipboardIconButton,
       IssueMessageHighlighting,
       LightLabel,
       LightPrimary,
       Link,
    -  LinkIcon,
       StyledPageTitle,
     } from 'design-system';
     import React from 'react';
    @@ -73,7 +73,7 @@ export function HotspotHeader(props: HotspotHeaderProps) {
                   <IssueMessageHighlighting message={message} messageFormattings={messageFormattings} />
                 </LightPrimary>
                 <ClipboardIconButton
    -              Icon={LinkIcon}
    +              Icon={IconLink}
                   copiedLabel={translate('copied_action')}
                   copyLabel={translate('copy_to_clipboard')}
                   className="sw-ml-2"
    diff --git a/server/sonar-web/src/main/js/apps/settings/encryption/__tests__/EncryptionApp-it.tsx b/server/sonar-web/src/main/js/apps/settings/encryption/__tests__/EncryptionApp-it.tsx
    index fca8c5f30a1..cc652ed6323 100644
    --- a/server/sonar-web/src/main/js/apps/settings/encryption/__tests__/EncryptionApp-it.tsx
    +++ b/server/sonar-web/src/main/js/apps/settings/encryption/__tests__/EncryptionApp-it.tsx
    @@ -19,7 +19,7 @@
      */
     import userEvent from '@testing-library/user-event';
     import React from 'react';
    -import { byRole } from '~sonar-aligned/helpers/testSelector';
    +import { byRole, byText } from '~sonar-aligned/helpers/testSelector';
     import SettingsServiceMock from '../../../../api/mocks/SettingsServiceMock';
     import { renderComponent } from '../../../../helpers/testReactTestingUtils';
     import EncryptionApp from '../EncryptionApp';
    @@ -50,7 +50,7 @@ it('should be able to generate new key', async () => {
     
       expect(await ui.appHeading.find()).toBeInTheDocument();
       await user.click(ui.generateSecretButton.get());
    -  expect(ui.copyToClipboard.get()).toHaveAttribute('data-clipboard-text', 'secretKey');
    +  expect(byText('secretKey').get()).toBeInTheDocument();
     });
     
     it('should be able to encrypt property value when secret is registered', async () => {
    @@ -61,11 +61,11 @@ it('should be able to encrypt property value when secret is registered', async (
       expect(await ui.appHeading.find()).toBeInTheDocument();
       await user.type(ui.encryptTextarea.get(), 'sonar.announcement.message');
       await user.click(ui.encryptButton.get());
    -  expect(ui.copyToClipboard.get()).toHaveAttribute('data-clipboard-text', 'encryptedValue');
    +  expect(byText('encryptedValue').get()).toBeInTheDocument();
     
       // can generate new secret in view
       await user.click(ui.generateNewSecretButton.get());
    -  expect(ui.copyToClipboard.get()).toHaveAttribute('data-clipboard-text', 'secretKey');
    +  expect(byText('secretKey').get()).toBeInTheDocument();
     });
     
     function renderEncryptionApp() {
    diff --git a/server/sonar-web/src/main/js/apps/system/components/__tests__/SystemApp-it.tsx b/server/sonar-web/src/main/js/apps/system/components/__tests__/SystemApp-it.tsx
    index e0e59af446c..756edf56855 100644
    --- a/server/sonar-web/src/main/js/apps/system/components/__tests__/SystemApp-it.tsx
    +++ b/server/sonar-web/src/main/js/apps/system/components/__tests__/SystemApp-it.tsx
    @@ -39,10 +39,7 @@ describe('System Info Standalone', () => {
         renderSystemApp();
         await ui.appIsLoaded();
     
    -    expect(ui.copyIdInformation.get()).toHaveAttribute(
    -      'data-clipboard-text',
    -      expect.stringContaining(`Server ID: asd564-asd54a-5dsfg45`),
    -    );
    +    expect(byText('asd564-asd54a-5dsfg45').get()).toBeInTheDocument();
     
         expect(ui.sectionButton('System').get()).toBeInTheDocument();
         expect(screen.queryByRole('cell', { name: 'High Availability' })).not.toBeInTheDocument();
    @@ -103,10 +100,7 @@ describe('System Info Cluster', () => {
         expect(ui.downloadLogsButton.query()).not.toBeInTheDocument();
         expect(ui.downloadSystemInfoButton.get()).toBeInTheDocument();
     
    -    expect(ui.copyIdInformation.get()).toHaveAttribute(
    -      'data-clipboard-text',
    -      expect.stringContaining(`Server ID: asd564-asd54a-5dsfg45`),
    -    );
    +    expect(byText('asd564-asd54a-5dsfg45').get()).toBeInTheDocument();
     
         // Renders health checks
         expect(ui.healthCauseWarning.get()).toBeInTheDocument();
    diff --git a/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx b/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx
    index c649604f45b..e5ea1b7114a 100644
    --- a/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx
    +++ b/server/sonar-web/src/main/js/components/SourceViewer/__tests__/SourceViewer-it.tsx
    @@ -17,7 +17,7 @@
      * along with this program; if not, write to the Free Software Foundation,
      * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
      */
    -import { queryHelpers, screen, within } from '@testing-library/react';
    +import { screen, within } from '@testing-library/react';
     import userEvent from '@testing-library/user-event';
     import * as React from 'react';
     import { byLabelText } from '~sonar-aligned/helpers/testSelector';
    @@ -79,23 +79,13 @@ it('should show a permalink on line number', async () => {
       );
     
       expect(
    -    /* eslint-disable-next-line testing-library/prefer-presence-queries */
    -    queryHelpers.queryByAttribute(
    -      'data-clipboard-text',
    -      row,
    -      'http://localhost/code?id=foo&selected=foo%3Atest1.js&line=1',
    -    ),
    +    rowScreen.getByRole('menuitem', { name: 'source_viewer.copy_permalink' }),
       ).toBeInTheDocument();
     
       await user.keyboard('[Escape]');
     
       expect(
    -    /* eslint-disable-next-line testing-library/prefer-presence-queries */
    -    queryHelpers.queryByAttribute(
    -      'data-clipboard-text',
    -      row,
    -      'http://localhost/code?id=foo&selected=foo%3Atest1.js&line=1',
    -    ),
    +    rowScreen.queryByRole('menuitem', { name: 'source_viewer.copy_permalink' }),
       ).not.toBeInTheDocument();
     
       row = await screen.findByRole('row', { name: / \* 6$/ });
    diff --git a/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/AzurePipelinesTutorial-it.tsx b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/AzurePipelinesTutorial-it.tsx
    index 13b851236a7..7837ca6c18f 100644
    --- a/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/AzurePipelinesTutorial-it.tsx
    +++ b/server/sonar-web/src/main/js/components/tutorials/azure-pipelines/__tests__/AzurePipelinesTutorial-it.tsx
    @@ -150,7 +150,9 @@ function assertServiceEndpointStepIsCorrectlyRendered() {
           name: 'onboarding.tutorial.with.azure_pipelines.ServiceEndpoint.title',
         }),
       ).toBeInTheDocument();
    -  expect(getCopyToClipboardValue(0, 'Copy to clipboard')).toBe('https://sonarqube.example.com/');
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy to clipboard', inlineSnippet: true })).toBe(
    +    'https://sonarqube.example.com/',
    +  );
       expect(
         screen.getByRole('button', { name: 'onboarding.token.generate.long' }),
       ).toBeInTheDocument();
    @@ -163,26 +165,34 @@ function assertDotNetStepIsCorrectlyRendered() {
         }),
       ).toBeInTheDocument();
     
    -  expect(getCopyToClipboardValue(1, 'Copy to clipboard')).toBe('foo');
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy to clipboard', inlineSnippet: true })).toBe(
    +    'foo',
    +  );
     }
     
     function assertMavenStepIsCorrectlyRendered() {
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot('maven, copy additional properties');
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot(
    +    'maven, copy additional properties',
    +  );
     }
     
     function assertGradleStepIsCorrectlyRendered() {
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot('gradle, copy additional properties');
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot(
    +    'gradle, copy additional properties',
    +  );
     }
     
     function assertObjCStepIsCorrectlyRendered(os: string, arch: string = 'x86_64') {
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot(
         `objectivec ${os} ${arch}, copy shell script`,
       );
    -  expect(getCopyToClipboardValue(1, 'Copy to clipboard')).toBe('foo');
    -  expect(getCopyToClipboardValue(2, 'Copy to clipboard')).toMatchSnapshot(
    -    `objectivec ${os} ${arch}, copy additional properties`,
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy to clipboard', inlineSnippet: true })).toBe(
    +    'foo',
       );
    -  expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot(
    +  expect(
    +    getCopyToClipboardValue({ i: 2, name: 'Copy to clipboard', inlineSnippet: true }),
    +  ).toMatchSnapshot(`objectivec ${os} ${arch}, copy additional properties`);
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy' })).toMatchSnapshot(
         `objectivec ${os} ${arch}, copy build-wrapper command`,
       );
     }
    @@ -192,20 +202,24 @@ function assertAutomaticCppStepIsCorrectlyRendered() {
     }
     
     function assertManualCppStepIsCorrectlyRendered(os: string, arch: string = 'x86_64') {
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot(
         `manual-cpp ${os} ${arch}, copy shell script`,
       );
    -  expect(getCopyToClipboardValue(1, 'Copy to clipboard')).toBe('foo');
    -  expect(getCopyToClipboardValue(2, 'Copy to clipboard')).toMatchSnapshot(
    -    `manual-cpp ${os} ${arch}, copy additional properties`,
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy to clipboard', inlineSnippet: true })).toBe(
    +    'foo',
       );
    -  expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot(
    +  expect(
    +    getCopyToClipboardValue({ i: 2, name: 'Copy to clipboard', inlineSnippet: true }),
    +  ).toMatchSnapshot(`manual-cpp ${os} ${arch}, copy additional properties`);
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy' })).toMatchSnapshot(
         `manual-cpp ${os} ${arch}, copy build-wrapper command`,
       );
     }
     
     function assertOtherStepIsCorrectlyRendered() {
    -  expect(getCopyToClipboardValue(1, 'Copy to clipboard')).toBe('foo');
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy to clipboard', inlineSnippet: true })).toBe(
    +    'foo',
    +  );
     }
     
     function assertFinishStepIsCorrectlyRendered() {
    diff --git a/server/sonar-web/src/main/js/components/tutorials/bitbucket-pipelines/__tests__/BitbucketPipelinesTutorial-it.tsx b/server/sonar-web/src/main/js/components/tutorials/bitbucket-pipelines/__tests__/BitbucketPipelinesTutorial-it.tsx
    index 54ecf78154a..e4a6c586fde 100644
    --- a/server/sonar-web/src/main/js/components/tutorials/bitbucket-pipelines/__tests__/BitbucketPipelinesTutorial-it.tsx
    +++ b/server/sonar-web/src/main/js/components/tutorials/bitbucket-pipelines/__tests__/BitbucketPipelinesTutorial-it.tsx
    @@ -30,6 +30,7 @@ import { AlmKeys } from '../../../../types/alm-settings';
     import { Feature } from '../../../../types/features';
     import {
       getCommonNodes,
    +  getCopyToClipboardHostURLValue,
       getCopyToClipboardValue,
       getTutorialActionButtons,
       getTutorialBuildButtons,
    @@ -64,81 +65,97 @@ it('should follow and complete all steps', async () => {
       expect(await ui.secretsStepTitle.find()).toBeInTheDocument();
     
       // Env variables step
    -  expect(getCopyToClipboardValue(0, 'Copy to clipboard')).toMatchSnapshot('sonar token key');
    -  expect(getCopyToClipboardValue(1, 'Copy to clipboard')).toMatchSnapshot('sonarqube host url key');
    -  expect(getCopyToClipboardValue(2, 'Copy to clipboard')).toMatchSnapshot(
    +  expect(
    +    getCopyToClipboardValue({ i: 0, name: 'Copy to clipboard', inlineSnippet: true }),
    +  ).toMatchSnapshot('sonar token key');
    +  expect(
    +    getCopyToClipboardValue({ i: 1, name: 'Copy to clipboard', inlineSnippet: true }),
    +  ).toMatchSnapshot('sonarqube host url key');
    +  expect(getCopyToClipboardHostURLValue({ i: 2, name: 'Copy to clipboard' })).toMatchSnapshot(
         'sonarqube host url value',
       );
     
       // Create/update configuration file step
       // Maven
       await user.click(ui.mavenBuildButton.get());
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot('Maven: bitbucket-pipelines.yml');
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot(
    +    'Maven: bitbucket-pipelines.yml',
    +  );
     
       // Gradle
       await user.click(ui.gradleBuildButton.get());
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot('Groovy: build.gradle');
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot('Groovy: build.gradle');
       await user.click(ui.gradleDSLButton(GradleBuildDSL.Kotlin).get());
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot('Kotlin: build.gradle.kts');
    -  expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot('Gradle: bitbucket-pipelines.yml');
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot(
    +    'Kotlin: build.gradle.kts',
    +  );
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy' })).toMatchSnapshot(
    +    'Gradle: bitbucket-pipelines.yml',
    +  );
     
       // .NET
       await user.click(ui.dotnetBuildButton.get());
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot('.NET: bitbucket-pipelines.yml');
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot(
    +    '.NET: bitbucket-pipelines.yml',
    +  );
     
       // Cpp
       await user.click(ui.cppBuildButton.get());
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot(
         'C++ (automatic) and other: sonar-project.properties',
       );
    -  expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy' })).toMatchSnapshot(
         'C++ (automatic) and other: bitbucket-pipelines.yml',
       );
     
       // Cpp (manual)
       await user.click(ui.autoConfigManual.get());
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot(
         'C++ (manual) and Objective-C: sonar-project.properties',
       );
    -  expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy' })).toMatchSnapshot(
         'C++ (manual) and Objective-C: bitbucket-pipelines.yml',
       );
       await user.click(ui.arm64Button.get());
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot(
         'C++ (manual arm64) and Objective-C: sonar-project.properties',
       );
    -  expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy' })).toMatchSnapshot(
         'C++ (manual arm64) and Objective-C: bitbucket-pipelines.yml',
       );
     
       // Objective-C
       await user.click(ui.objCBuildButton.get());
       await user.click(ui.x86_64Button.get());
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot(
         'C++ (manual) and Objective-C: bitbucket-pipelines.yml',
       );
    -  expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy' })).toMatchSnapshot(
         'C++ (manual) and Objective-C: sonar-project.properties',
       );
       await user.click(ui.arm64Button.get());
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot(
         'C++ (manual arm64) and Objective-C: bitbucket-pipelines.yml',
       );
    -  expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy' })).toMatchSnapshot(
         'C++ (manual arm64) and Objective-C: sonar-project.properties',
       );
     
       // Dart
       await user.click(ui.dartBuildButton.get());
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot('Dart: sonar-project.properties');
    -  expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot('Dart: bitbucket-pipelines.yml');
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot(
    +    'Dart: sonar-project.properties',
    +  );
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy' })).toMatchSnapshot(
    +    'Dart: bitbucket-pipelines.yml',
    +  );
     
       // Other
       await user.click(ui.otherBuildButton.get());
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot(
         'C++ (automatic) and other: sonar-project.properties',
       );
    -  expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy' })).toMatchSnapshot(
         'C++ (automatic) and other: .github/workflows/build.yml',
       );
     
    @@ -154,7 +171,7 @@ it('should generate/delete a new token or use existing one', async () => {
       // Generate token
       await user.click(ui.genTokenDialogButton.get());
       await user.click(ui.generateTokenButton.get());
    -  expect(getCopyToClipboardValue()).toEqual('generatedtoken2');
    +  expect(getCopyToClipboardValue({ inlineSnippet: true })).toEqual('generatedtoken2');
     
       // Revoke current token and create new one
       await user.click(ui.deleteTokenButton.get());
    diff --git a/server/sonar-web/src/main/js/components/tutorials/bitbucket-pipelines/__tests__/__snapshots__/BitbucketPipelinesTutorial-it.tsx.snap b/server/sonar-web/src/main/js/components/tutorials/bitbucket-pipelines/__tests__/__snapshots__/BitbucketPipelinesTutorial-it.tsx.snap
    index f4549f87087..797e6fd2b08 100644
    --- a/server/sonar-web/src/main/js/components/tutorials/bitbucket-pipelines/__tests__/__snapshots__/BitbucketPipelinesTutorial-it.tsx.snap
    +++ b/server/sonar-web/src/main/js/components/tutorials/bitbucket-pipelines/__tests__/__snapshots__/BitbucketPipelinesTutorial-it.tsx.snap
    @@ -30,8 +30,7 @@ pipelines:
     `;
     
     exports[`should follow and complete all steps: C++ (automatic) and other: .github/workflows/build.yml 1`] = `
    -"
    -definitions:
    +"definitions:
       steps:
         - step: &build-step
             name: SonarQube analysis
    @@ -58,8 +57,7 @@ pipelines:
     `;
     
     exports[`should follow and complete all steps: C++ (automatic) and other: bitbucket-pipelines.yml 1`] = `
    -"
    -definitions:
    +"definitions:
       steps:
         - step: &build-step
             name: SonarQube analysis
    diff --git a/server/sonar-web/src/main/js/components/tutorials/github-action/__tests__/GithubActionTutorial-it.tsx b/server/sonar-web/src/main/js/components/tutorials/github-action/__tests__/GithubActionTutorial-it.tsx
    index 562ef915d5e..12c20815c99 100644
    --- a/server/sonar-web/src/main/js/components/tutorials/github-action/__tests__/GithubActionTutorial-it.tsx
    +++ b/server/sonar-web/src/main/js/components/tutorials/github-action/__tests__/GithubActionTutorial-it.tsx
    @@ -30,6 +30,7 @@ import { AlmKeys } from '../../../../types/alm-settings';
     import { Feature } from '../../../../types/features';
     import {
       getCommonNodes,
    +  getCopyToClipboardHostURLValue,
       getCopyToClipboardValue,
       getTutorialActionButtons,
       getTutorialBuildButtons,
    @@ -62,90 +63,106 @@ it('should follow and complete all steps', async () => {
       expect(await ui.secretsStepTitle.find()).toBeInTheDocument();
     
       // Env variables step
    -  expect(getCopyToClipboardValue(0, 'Copy to clipboard')).toMatchSnapshot('sonar token key');
    -  expect(getCopyToClipboardValue(1, 'Copy to clipboard')).toMatchSnapshot('sonarqube host url key');
    -  expect(getCopyToClipboardValue(2, 'Copy to clipboard')).toMatchSnapshot(
    +  expect(
    +    getCopyToClipboardValue({ i: 0, name: 'Copy to clipboard', inlineSnippet: true }),
    +  ).toMatchSnapshot('sonar token key');
    +  expect(
    +    getCopyToClipboardValue({ i: 1, name: 'Copy to clipboard', inlineSnippet: true }),
    +  ).toMatchSnapshot('sonarqube host url key');
    +  expect(getCopyToClipboardHostURLValue({ i: 2, name: 'Copy to clipboard' })).toMatchSnapshot(
         'sonarqube host url value',
       );
     
       // Create/update configuration file step
       // Maven
       await user.click(ui.mavenBuildButton.get());
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot('Maven: .github/workflows/build.yml');
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot(
    +    'Maven: .github/workflows/build.yml',
    +  );
     
       // Gradle
       await user.click(ui.gradleBuildButton.get());
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot('Groovy: build.gradle');
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot('Groovy: build.gradle');
       await user.click(ui.gradleDSLButton(GradleBuildDSL.Kotlin).get());
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot('Kotlin: build.gradle.kts');
    -  expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot('Gradle: .github/workflows/build.yml');
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot(
    +    'Kotlin: build.gradle.kts',
    +  );
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy' })).toMatchSnapshot(
    +    'Gradle: .github/workflows/build.yml',
    +  );
     
       // .NET
       await user.click(ui.dotnetBuildButton.get());
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot('.NET: .github/workflows/build.yml');
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot(
    +    '.NET: .github/workflows/build.yml',
    +  );
     
       // Cpp
       await user.click(ui.cppBuildButton.get());
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot(
         'C++ (automatic) and other: sonar-project.properties',
       );
    -  expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy' })).toMatchSnapshot(
         'C++ (automatic) and other: .github/workflows/build.yml',
       );
     
       await user.click(ui.autoConfigManual.get());
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot('C++: sonar-project.properties');
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot(
    +    'C++: sonar-project.properties',
    +  );
       await user.click(ui.linuxButton.get());
    -  expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy' })).toMatchSnapshot(
         'C++ Linux: .github/workflows/build.yml',
       );
       await user.click(ui.arm64Button.get());
    -  expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy' })).toMatchSnapshot(
         'C++ Linux arm64: .github/workflows/build.yml',
       );
       await user.click(ui.windowsButton.get());
    -  expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy' })).toMatchSnapshot(
         'C++ Windows: .github/workflows/build.yml',
       );
       await user.click(ui.macosButton.get());
    -  expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy' })).toMatchSnapshot(
         'C++ MacOS: .github/workflows/build.yml',
       );
     
       // Objective-C
       await user.click(ui.objCBuildButton.get());
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot(
         'Objective-C: sonar-project.properties',
       );
     
       await user.click(ui.linuxButton.get());
       await user.click(ui.x86_64Button.get());
    -  expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy' })).toMatchSnapshot(
         'Objective-C Linux: .github/workflows/build.yml',
       );
       await user.click(ui.arm64Button.get());
    -  expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy' })).toMatchSnapshot(
         'Objective-C Linux arm64: .github/workflows/build.yml',
       );
       await user.click(ui.windowsButton.get());
    -  expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy' })).toMatchSnapshot(
         'Objective-C Windows: .github/workflows/build.yml',
       );
       await user.click(ui.macosButton.get());
    -  expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy' })).toMatchSnapshot(
         'Objective-C MacOS: .github/workflows/build.yml',
       );
     
       // Dart
       await user.click(ui.dartBuildButton.get());
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot('Dart: .github/workflows/build.yml');
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot(
    +    'Dart: .github/workflows/build.yml',
    +  );
     
       // Other
       await user.click(ui.otherBuildButton.get());
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot(
         'C++ (automatic) and other: sonar-project.properties',
       );
    -  expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy' })).toMatchSnapshot(
         'C++ (automatic) and other: .github/workflows/build.yml',
       );
     
    @@ -161,7 +178,7 @@ it('should generate/delete a new token or use existing one', async () => {
       // Generate token
       await user.click(ui.genTokenDialogButton.get());
       await user.click(ui.generateTokenButton.get());
    -  expect(getCopyToClipboardValue()).toEqual('generatedtoken2');
    +  expect(getCopyToClipboardValue({ inlineSnippet: true })).toEqual('generatedtoken2');
     
       // Revoke current token and create new one
       await user.click(ui.deleteTokenButton.get());
    diff --git a/server/sonar-web/src/main/js/components/tutorials/github-action/__tests__/__snapshots__/GithubActionTutorial-it.tsx.snap b/server/sonar-web/src/main/js/components/tutorials/github-action/__tests__/__snapshots__/GithubActionTutorial-it.tsx.snap
    index bc5a71eb7fc..e3cd4f02c53 100644
    --- a/server/sonar-web/src/main/js/components/tutorials/github-action/__tests__/__snapshots__/GithubActionTutorial-it.tsx.snap
    +++ b/server/sonar-web/src/main/js/components/tutorials/github-action/__tests__/__snapshots__/GithubActionTutorial-it.tsx.snap
    @@ -1,35 +1,5 @@
     // Jest Snapshot v1, https://goo.gl/fbAQLP
     
    -exports[`should follow and complete all steps: Dart: .github/workflows/build.yml 1`] = `
    -"
    -name: Build
    -
    -on:
    -  push:
    -    branches:
    -      - main
    -  pull_request:
    -    types: [opened, synchronize, reopened]
    -
    -jobs:
    -  build:
    -    name: Build and analyze
    -    runs-on: ubuntu-latest
    -    steps:
    -      - <commands to build your project>
    -      - name: Download sonar-scanner
    -        run: |
    -          curl --create-dirs -sSLo $HOME/.sonar/sonar-scanner.zip https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-6.1.0.4477-linux-x64.zip
    -          unzip $HOME/.sonar/sonar-scanner.zip -o -d $HOME/.sonar/
    -      - name: Run sonar-scanner
    -        env:
    -          SONAR_TOKEN: \${{ secrets.SONAR_TOKEN }}
    -          SONAR_HOST_URL: \${{ secrets.SONAR_HOST_URL }}
    -        run: |
    -          sonar-scanner-6.1.0.4477-linux-x64/bin/sonar-scanner \\
    -            -Dsonar.projectKey=my-project"
    -`;
    -
     exports[`should follow and complete all steps: .NET: .github/workflows/build.yml 1`] = `
     "name: Build
     
    @@ -297,6 +267,35 @@ jobs:
     
     exports[`should follow and complete all steps: C++: sonar-project.properties 1`] = `"sonar.projectKey=my-project"`;
     
    +exports[`should follow and complete all steps: Dart: .github/workflows/build.yml 1`] = `
    +"name: Build
    +
    +on:
    +  push:
    +    branches:
    +      - main
    +  pull_request:
    +    types: [opened, synchronize, reopened]
    +
    +jobs:
    +  build:
    +    name: Build and analyze
    +    runs-on: ubuntu-latest
    +    steps:
    +      - <commands to build your project>
    +      - name: Download sonar-scanner
    +        run: |
    +          curl --create-dirs -sSLo $HOME/.sonar/sonar-scanner.zip https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-6.1.0.4477-linux-x64.zip
    +          unzip $HOME/.sonar/sonar-scanner.zip -o -d $HOME/.sonar/
    +      - name: Run sonar-scanner
    +        env:
    +          SONAR_TOKEN: \${{ secrets.SONAR_TOKEN }}
    +          SONAR_HOST_URL: \${{ secrets.SONAR_HOST_URL }}
    +        run: |
    +          sonar-scanner-6.1.0.4477-linux-x64/bin/sonar-scanner \\
    +            -Dsonar.projectKey=my-project"
    +`;
    +
     exports[`should follow and complete all steps: Gradle: .github/workflows/build.yml 1`] = `
     "name: Build
     
    diff --git a/server/sonar-web/src/main/js/components/tutorials/gitlabci/__tests__/GitLabCITutorial-it.tsx b/server/sonar-web/src/main/js/components/tutorials/gitlabci/__tests__/GitLabCITutorial-it.tsx
    index 26fec42cb08..93c062e5fa6 100644
    --- a/server/sonar-web/src/main/js/components/tutorials/gitlabci/__tests__/GitLabCITutorial-it.tsx
    +++ b/server/sonar-web/src/main/js/components/tutorials/gitlabci/__tests__/GitLabCITutorial-it.tsx
    @@ -26,6 +26,7 @@ import { RenderContext, renderApp } from '../../../../helpers/testReactTestingUt
     import { byRole } from '../../../../sonar-aligned/helpers/testSelector';
     import {
       getCommonNodes,
    +  getCopyToClipboardHostURLValue,
       getCopyToClipboardValue,
       getTutorialActionButtons,
       getTutorialBuildButtons,
    @@ -56,47 +57,61 @@ it('should follow and complete all steps', async () => {
       expect(await ui.secretsStepTitle.find()).toBeInTheDocument();
     
       // Env variables step
    -  expect(getCopyToClipboardValue(0, 'Copy to clipboard')).toMatchSnapshot('sonar token key');
    -  expect(getCopyToClipboardValue(1, 'Copy to clipboard')).toMatchSnapshot('sonarqube host url key');
    -  expect(getCopyToClipboardValue(2, 'Copy to clipboard')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy to clipboard' })).toMatchSnapshot(
    +    'sonar token key',
    +  );
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy to clipboard' })).toMatchSnapshot(
    +    'sonarqube host url key',
    +  );
    +  expect(getCopyToClipboardHostURLValue({ i: 2, name: 'Copy to clipboard' })).toMatchSnapshot(
         'sonarqube host url value',
       );
     
       // Create/update configuration file step
       // Maven
       await user.click(ui.mavenBuildButton.get());
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot('Maven: pom.xml');
    -  expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot('Maven: gitlab-ci.yml');
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot('Maven: pom.xml');
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy' })).toMatchSnapshot('Maven: gitlab-ci.yml');
     
       // Gradle
       await user.click(ui.gradleBuildButton.get());
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot('Groovy: build.gradle');
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot('Groovy: build.gradle');
       await user.click(ui.gradleDSLButton(GradleBuildDSL.Kotlin).get());
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot('Kotlin: build.gradle.kts');
    -  expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot('Gradle: gitlab-ci.yml');
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot(
    +    'Kotlin: build.gradle.kts',
    +  );
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy' })).toMatchSnapshot('Gradle: gitlab-ci.yml');
     
       // .NET
       await user.click(ui.dotnetBuildButton.get());
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot('.NET: gitlab-ci.yml');
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot('.NET: gitlab-ci.yml');
     
       // C++/Objective-C
       await user.click(ui.cppBuildButton.get());
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot('CPP: sonar-project.properties');
    -  expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot('CPP: gitlab-ci.yml');
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot(
    +    'CPP: sonar-project.properties',
    +  );
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy' })).toMatchSnapshot('CPP: gitlab-ci.yml');
     
       // c++ manual config
       await user.click(ui.autoConfigManual.get());
    -  expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot('CPP - manual: gitlab-ci.yml');
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy' })).toMatchSnapshot(
    +    'CPP - manual: gitlab-ci.yml',
    +  );
     
       // Dart
       await user.click(ui.dartBuildButton.get());
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot('Dart: sonar-project.properties');
    -  expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot('Dart: gitlab-ci.yml');
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot(
    +    'Dart: sonar-project.properties',
    +  );
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy' })).toMatchSnapshot('Dart: gitlab-ci.yml');
     
       // Other
       await user.click(ui.otherBuildButton.get());
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot('Other: sonar-project.properties');
    -  expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot('Other: gitlab-ci.yml');
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot(
    +    'Other: sonar-project.properties',
    +  );
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy' })).toMatchSnapshot('Other: gitlab-ci.yml');
     
       expect(ui.allSetSentence.get()).toBeInTheDocument();
     });
    @@ -110,7 +125,7 @@ it('should generate/delete a new token or use existing one', async () => {
       // Generate token
       await user.click(ui.genTokenDialogButton.get());
       await user.click(ui.generateTokenButton.get());
    -  expect(getCopyToClipboardValue()).toEqual('generatedtoken2');
    +  expect(getCopyToClipboardValue({ inlineSnippet: true })).toEqual('generatedtoken2');
     
       // Revoke current token and create new one
       await user.click(ui.deleteTokenButton.get());
    diff --git a/server/sonar-web/src/main/js/components/tutorials/jenkins/__tests__/JenkinsTutorial-it.tsx b/server/sonar-web/src/main/js/components/tutorials/jenkins/__tests__/JenkinsTutorial-it.tsx
    index 61ccedae4bd..00da86bece4 100644
    --- a/server/sonar-web/src/main/js/components/tutorials/jenkins/__tests__/JenkinsTutorial-it.tsx
    +++ b/server/sonar-web/src/main/js/components/tutorials/jenkins/__tests__/JenkinsTutorial-it.tsx
    @@ -99,101 +99,121 @@ it.each([AlmKeys.BitbucketCloud, AlmKeys.BitbucketServer, AlmKeys.GitHub, AlmKey
     
         // 3. Multibranch Pipeline Job
         expect(ui.multiBranchPipelineSecondListItem(alm).get()).toBeInTheDocument();
    -    expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot(`ref spec`);
    +    expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot(`ref spec`);
     
         // 4. Create DevOps platform webhook
         expect(ui.webhookStepTitle(alm).get()).toBeInTheDocument();
    -    expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot(`jenkins url`);
    +    expect(getCopyToClipboardValue({ i: 1, name: 'Copy' })).toMatchSnapshot(`jenkins url`);
     
         // 5. Create jenkinsfile
         // Maven
         await user.click(ui.mavenBuildButton.get());
    -    expect(getCopyToClipboardValue(2, 'Copy')).toMatchSnapshot(`maven jenkinsfile`);
    +    expect(getCopyToClipboardValue({ i: 2, name: 'Copy' })).toMatchSnapshot(`maven jenkinsfile`);
     
         // Gradle (Groovy)
         await user.click(ui.gradleBuildButton.get());
    -    expect(getCopyToClipboardValue(2, 'Copy')).toMatchSnapshot(`Groovy: build.gradle file`);
    +    expect(getCopyToClipboardValue({ i: 2, name: 'Copy' })).toMatchSnapshot(
    +      `Groovy: build.gradle file`,
    +    );
         // Gradle(Kotlin)
         await user.click(ui.gradleDSLButton(GradleBuildDSL.Kotlin).get());
    -    expect(getCopyToClipboardValue(2, 'Copy')).toMatchSnapshot(`Kotlin: build.gradle.kts file`);
    -    expect(getCopyToClipboardValue(3, 'Copy')).toMatchSnapshot(`gradle jenkinsfile`);
    +    expect(getCopyToClipboardValue({ i: 2, name: 'Copy' })).toMatchSnapshot(
    +      `Kotlin: build.gradle.kts file`,
    +    );
    +    expect(getCopyToClipboardValue({ i: 3, name: 'Copy' })).toMatchSnapshot(`gradle jenkinsfile`);
     
         // .NET
         await user.click(ui.dotnetBuildButton.get());
         await user.click(ui.windowsDotnetCoreButton.get());
    -    expect(getCopyToClipboardValue(2, 'Copy')).toMatchSnapshot(`windows dotnet core jenkinsfile`);
    +    expect(getCopyToClipboardValue({ i: 2, name: 'Copy' })).toMatchSnapshot(
    +      `windows dotnet core jenkinsfile`,
    +    );
     
         await user.click(ui.windowsDotnetFrameworkButton.get());
    -    expect(getCopyToClipboardValue(2, 'Copy')).toMatchSnapshot(
    +    expect(getCopyToClipboardValue({ i: 2, name: 'Copy' })).toMatchSnapshot(
           `windows dotnet framework jenkinsfile`,
         );
     
         await user.click(ui.linuxDotnetCoreButton.get());
    -    expect(getCopyToClipboardValue(2, 'Copy')).toMatchSnapshot(`linux dotnet core jenkinsfile`);
    +    expect(getCopyToClipboardValue({ i: 2, name: 'Copy' })).toMatchSnapshot(
    +      `linux dotnet core jenkinsfile`,
    +    );
     
         // C++ (automatic)
         await user.click(ui.cppBuildButton.get());
    -    expect(getCopyToClipboardValue(2, 'Copy')).toMatchSnapshot(
    +    expect(getCopyToClipboardValue({ i: 2, name: 'Copy' })).toMatchSnapshot(
           `c++ (automatic and other): build tools sonar-project.properties code`,
         );
    -    expect(getCopyToClipboardValue(3, 'Copy')).toMatchSnapshot(
    +    expect(getCopyToClipboardValue({ i: 3, name: 'Copy' })).toMatchSnapshot(
           `c++ (automatic and other): build tools jenkinsfile`,
         );
     
         // C++ (manual)
         await user.click(ui.autoConfigManual.get());
    -    expect(getCopyToClipboardValue(2, 'Copy')).toMatchSnapshot(`sonar-project.properties code`);
    +    expect(getCopyToClipboardValue({ i: 2, name: 'Copy' })).toMatchSnapshot(
    +      `sonar-project.properties code`,
    +    );
     
         await user.click(ui.linuxButton.get());
    -    expect(getCopyToClipboardValue(3, 'Copy')).toMatchSnapshot(
    +    expect(getCopyToClipboardValue({ i: 3, name: 'Copy' })).toMatchSnapshot(
           `c++ (manual) and objectivec: linux jenkinsfile`,
         );
     
         await user.click(ui.arm64Button.get());
    -    expect(getCopyToClipboardValue(3, 'Copy')).toMatchSnapshot(
    +    expect(getCopyToClipboardValue({ i: 3, name: 'Copy' })).toMatchSnapshot(
           `c++ (manual) and objectivec: linux arm64 jenkinsfile`,
         );
     
         await user.click(ui.windowsButton.get());
    -    expect(getCopyToClipboardValue(3, 'Copy')).toMatchSnapshot(
    +    expect(getCopyToClipboardValue({ i: 3, name: 'Copy' })).toMatchSnapshot(
           `c++ (manual) and objectivec: windows jenkinsfile`,
         );
     
         await user.click(ui.macosButton.get());
    -    expect(getCopyToClipboardValue(3, 'Copy')).toMatchSnapshot(
    +    expect(getCopyToClipboardValue({ i: 3, name: 'Copy' })).toMatchSnapshot(
           `c++ (manual) and objectivec: macos jenkinsfile`,
         );
     
         // Objective-C
         await user.click(ui.objCBuildButton.get());
    -    expect(getCopyToClipboardValue(2, 'Copy')).toMatchSnapshot(`sonar-project.properties code`);
    +    expect(getCopyToClipboardValue({ i: 2, name: 'Copy' })).toMatchSnapshot(
    +      `sonar-project.properties code`,
    +    );
     
         await user.click(ui.linuxButton.get());
         await user.click(ui.x86_64Button.get());
    -    expect(getCopyToClipboardValue(3, 'Copy')).toMatchSnapshot(`objectivec: linux jenkinsfile`);
    +    expect(getCopyToClipboardValue({ i: 3, name: 'Copy' })).toMatchSnapshot(
    +      `objectivec: linux jenkinsfile`,
    +    );
     
         await user.click(ui.arm64Button.get());
    -    expect(getCopyToClipboardValue(3, 'Copy')).toMatchSnapshot(
    +    expect(getCopyToClipboardValue({ i: 3, name: 'Copy' })).toMatchSnapshot(
           `objectivec: linux arm64 jenkinsfile`,
         );
     
         await user.click(ui.windowsButton.get());
    -    expect(getCopyToClipboardValue(3, 'Copy')).toMatchSnapshot(`objectivec: windows jenkinsfile`);
    +    expect(getCopyToClipboardValue({ i: 3, name: 'Copy' })).toMatchSnapshot(
    +      `objectivec: windows jenkinsfile`,
    +    );
     
         await user.click(ui.macosButton.get());
    -    expect(getCopyToClipboardValue(3, 'Copy')).toMatchSnapshot(`objectivec: macos jenkinsfile`);
    +    expect(getCopyToClipboardValue({ i: 3, name: 'Copy' })).toMatchSnapshot(
    +      `objectivec: macos jenkinsfile`,
    +    );
     
         // Dart
         await user.click(ui.dartBuildButton.get());
    -    expect(getCopyToClipboardValue(2, 'Copy')).toMatchSnapshot(`Dart: sonar-project.properties`);
    -    expect(getCopyToClipboardValue(3, 'Copy')).toMatchSnapshot(`Dart: jenkinsfile`);
    +    expect(getCopyToClipboardValue({ i: 2, name: 'Copy' })).toMatchSnapshot(
    +      `Dart: sonar-project.properties`,
    +    );
    +    expect(getCopyToClipboardValue({ i: 3, name: 'Copy' })).toMatchSnapshot(`Dart: jenkinsfile`);
     
         // Other
         await user.click(ui.otherBuildButton.get());
    -    expect(getCopyToClipboardValue(2, 'Copy')).toMatchSnapshot(
    +    expect(getCopyToClipboardValue({ i: 2, name: 'Copy' })).toMatchSnapshot(
           `c++ (automatic and other): build tools sonar-project.properties code`,
         );
    -    expect(getCopyToClipboardValue(3, 'Copy')).toMatchSnapshot(
    +    expect(getCopyToClipboardValue({ i: 3, name: 'Copy' })).toMatchSnapshot(
           `c++ (automatic and other): build tools jenkinsfile`,
         );
     
    diff --git a/server/sonar-web/src/main/js/components/tutorials/other/__tests__/OtherTutorial-it.tsx b/server/sonar-web/src/main/js/components/tutorials/other/__tests__/OtherTutorial-it.tsx
    index 905eb73b915..b6f15c19ae8 100644
    --- a/server/sonar-web/src/main/js/components/tutorials/other/__tests__/OtherTutorial-it.tsx
    +++ b/server/sonar-web/src/main/js/components/tutorials/other/__tests__/OtherTutorial-it.tsx
    @@ -35,6 +35,10 @@ jest.mock('../../../../api/settings', () => ({
       getAllValues: jest.fn().mockResolvedValue([]),
     }));
     
    +jest.mock('clipboard', () => ({
    +  copy: jest.fn(),
    +}));
    +
     const tokenMock = new UserTokensMock();
     
     afterEach(() => {
    @@ -100,50 +104,66 @@ it('can choose build tools and copy provided settings', async () => {
     
       // Maven
       await user.click(ui.mavenBuildButton.get());
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot('maven: execute scanner');
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot('maven: execute scanner');
     
       // Gradle
       await user.click(ui.gradleBuildButton.get());
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot('gradle: sonarqube plugin');
    -  expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot('gradle: execute scanner');
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot(
    +    'gradle: sonarqube plugin',
    +  );
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy' })).toMatchSnapshot(
    +    'gradle: execute scanner',
    +  );
     
       // Dotnet - Core
       await user.click(ui.dotnetBuildButton.get());
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot(
         'dotnet core: install scanner globally',
       );
    -  expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot('dotnet core: execute command 1');
    -  expect(getCopyToClipboardValue(2, 'Copy')).toMatchSnapshot('dotnet core: execute command 2');
    -  expect(getCopyToClipboardValue(3, 'Copy')).toMatchSnapshot('dotnet core: execute command 3');
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy' })).toMatchSnapshot(
    +    'dotnet core: execute command 1',
    +  );
    +  expect(getCopyToClipboardValue({ i: 2, name: 'Copy' })).toMatchSnapshot(
    +    'dotnet core: execute command 2',
    +  );
    +  expect(getCopyToClipboardValue({ i: 3, name: 'Copy' })).toMatchSnapshot(
    +    'dotnet core: execute command 3',
    +  );
     
       // Dotnet - Framework
       await user.click(ui.dotnetFrameworkButton.get());
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot('dotnet framework: execute command 1');
    -  expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot('dotnet framework: execute command 2');
    -  expect(getCopyToClipboardValue(2, 'Copy')).toMatchSnapshot('dotnet framework: execute command 3');
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot(
    +    'dotnet framework: execute command 1',
    +  );
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy' })).toMatchSnapshot(
    +    'dotnet framework: execute command 2',
    +  );
    +  expect(getCopyToClipboardValue({ i: 2, name: 'Copy' })).toMatchSnapshot(
    +    'dotnet framework: execute command 3',
    +  );
     
       // C++ - Automatic
       await user.click(ui.cppBuildButton.get());
       await user.click(ui.linuxButton.get());
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot(
         'c++ (automatic) and other linux: download scanner',
       );
    -  expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy' })).toMatchSnapshot(
         'c++ (automatic) and other linux: execute scanner',
       );
       await user.click(ui.arm64Button.get());
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot(
         'c++ (automatic) and other linux arm64: download scanner',
       );
    -  expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy' })).toMatchSnapshot(
         'c++ (automatic) and other linux arm64: execute scanner',
       );
       await user.click(ui.windowsButton.get());
    -  expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy' })).toMatchSnapshot(
         'c++ (automatic) and other windows: execute scanner',
       );
       await user.click(ui.macosButton.get());
    -  expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy' })).toMatchSnapshot(
         'c++ (automatic) and other macos: execute scanner',
       );
     
    @@ -151,146 +171,170 @@ it('can choose build tools and copy provided settings', async () => {
       await user.click(ui.autoConfigManual.get());
       await user.click(ui.linuxButton.get());
       await user.click(ui.x86_64Button.get());
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot(
         'c++ (manual) linux: download build wrapper',
       );
    -  expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy' })).toMatchSnapshot(
         'c++ (manual) linux: download scanner',
       );
    -  expect(getCopyToClipboardValue(2, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 2, name: 'Copy' })).toMatchSnapshot(
         'c++ (manual) linux: execute build wrapper',
       );
    -  expect(getCopyToClipboardValue(3, 'Copy')).toMatchSnapshot('c++ (manual) linux: execute scanner');
    +  expect(getCopyToClipboardValue({ i: 3, name: 'Copy' })).toMatchSnapshot(
    +    'c++ (manual) linux: execute scanner',
    +  );
     
       // C++ - Linux (ARM64)
       await user.click(ui.arm64Button.get());
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot(
         'c++ (manual) linux arm64: download build wrapper',
       );
    -  expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy' })).toMatchSnapshot(
         'c++ (manual) linux arm64: download scanner',
       );
    -  expect(getCopyToClipboardValue(2, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 2, name: 'Copy' })).toMatchSnapshot(
         'c++ (manual) linux arm64: execute build wrapper',
       );
    -  expect(getCopyToClipboardValue(3, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 3, name: 'Copy' })).toMatchSnapshot(
         'c++ (manual) linux arm64: execute scanner',
       );
     
       // C++ - Windows
       await user.click(ui.windowsButton.get());
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot(
         'c++ (manual) windows: download build wrapper',
       );
    -  expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy' })).toMatchSnapshot(
         'c++ (manual) windows: download scanner',
       );
    -  expect(getCopyToClipboardValue(2, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 2, name: 'Copy' })).toMatchSnapshot(
         'c++ (manual) windows: execute build wrapper',
       );
    -  expect(getCopyToClipboardValue(3, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 3, name: 'Copy' })).toMatchSnapshot(
         'c++ (manual) windows: execute scanner',
       );
     
       // C++ - MacOS
       await user.click(ui.macosButton.get());
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot(
         'c++ (manual) macos: download build wrapper',
       );
    -  expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy' })).toMatchSnapshot(
         'c++ (manual) macos: download scanner',
       );
    -  expect(getCopyToClipboardValue(2, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 2, name: 'Copy' })).toMatchSnapshot(
         'c++ (manual) macos: execute build wrapper',
       );
    -  expect(getCopyToClipboardValue(3, 'Copy')).toMatchSnapshot('c++ (manual) macos: execute scanner');
    +  expect(getCopyToClipboardValue({ i: 3, name: 'Copy' })).toMatchSnapshot(
    +    'c++ (manual) macos: execute scanner',
    +  );
     
       // Objective-C - Linux (x86_64)
       await user.click(ui.objCBuildButton.get());
       await user.click(ui.linuxButton.get());
       await user.click(ui.x86_64Button.get());
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot(
         'objective-c linux: download build wrapper',
       );
    -  expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot('objective-c linux: download scanner');
    -  expect(getCopyToClipboardValue(2, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy' })).toMatchSnapshot(
    +    'objective-c linux: download scanner',
    +  );
    +  expect(getCopyToClipboardValue({ i: 2, name: 'Copy' })).toMatchSnapshot(
         'objective-c linux: execute build wrapper',
       );
    -  expect(getCopyToClipboardValue(3, 'Copy')).toMatchSnapshot('objective-c linux: execute scanner');
    +  expect(getCopyToClipboardValue({ i: 3, name: 'Copy' })).toMatchSnapshot(
    +    'objective-c linux: execute scanner',
    +  );
     
       // Objective-C - Linux (ARM64)
       await user.click(ui.arm64Button.get());
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot(
         'objective-c linux arm64: download build wrapper',
       );
    -  expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy' })).toMatchSnapshot(
         'objective-c linux arm64: download scanner',
       );
    -  expect(getCopyToClipboardValue(2, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 2, name: 'Copy' })).toMatchSnapshot(
         'objective-c linux arm64: execute build wrapper',
       );
    -  expect(getCopyToClipboardValue(3, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 3, name: 'Copy' })).toMatchSnapshot(
         'objective-c linux arm64: execute scanner',
       );
     
       // Objective-C - Windows
       await user.click(ui.windowsButton.get());
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot(
         'objective-c windows: download build wrapper',
       );
    -  expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy' })).toMatchSnapshot(
         'objective-c windows: download scanner',
       );
    -  expect(getCopyToClipboardValue(2, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 2, name: 'Copy' })).toMatchSnapshot(
         'objective-c windows: execute build wrapper',
       );
    -  expect(getCopyToClipboardValue(3, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 3, name: 'Copy' })).toMatchSnapshot(
         'objective-c windows: execute scanner',
       );
     
       // Objective-C - MacOS
       await user.click(ui.macosButton.get());
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot(
         'objective-c macos: download build wrapper',
       );
    -  expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot('objective-c macos: download scanner');
    -  expect(getCopyToClipboardValue(2, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy' })).toMatchSnapshot(
    +    'objective-c macos: download scanner',
    +  );
    +  expect(getCopyToClipboardValue({ i: 2, name: 'Copy' })).toMatchSnapshot(
         'objective-c macos: execute build wrapper',
       );
    -  expect(getCopyToClipboardValue(3, 'Copy')).toMatchSnapshot('objective-c macos: execute scanner');
    +  expect(getCopyToClipboardValue({ i: 3, name: 'Copy' })).toMatchSnapshot(
    +    'objective-c macos: execute scanner',
    +  );
     
       // Dart - Linux
       await user.click(ui.dartBuildButton.get());
       await user.click(ui.linuxButton.get());
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot('Dart linux: download scanner');
    -  expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot('Dart linux: execute scanner');
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot(
    +    'Dart linux: download scanner',
    +  );
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy' })).toMatchSnapshot(
    +    'Dart linux: execute scanner',
    +  );
     
       // Dart - Windows
       await user.click(ui.windowsButton.get());
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot('Dart windows: download scanner');
    -  expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot('Dart windows: execute scanner');
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot(
    +    'Dart windows: download scanner',
    +  );
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy' })).toMatchSnapshot(
    +    'Dart windows: execute scanner',
    +  );
     
       // Dart - MacOS
       await user.click(ui.macosButton.get());
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot('Dart macos: download scanner');
    -  expect(getCopyToClipboardValue(1, 'Copy')).toMatchSnapshot('Dart macos: execute scanner');
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot(
    +    'Dart macos: download scanner',
    +  );
    +  expect(getCopyToClipboardValue({ i: 1, name: 'Copy' })).toMatchSnapshot(
    +    'Dart macos: execute scanner',
    +  );
     
       // Other - Linux
       await user.click(ui.otherBuildButton.get());
       await user.click(ui.linuxButton.get());
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot(
         'c++ (automatic) and other linux: execute scanner',
       );
     
       // Other - Windows
       await user.click(ui.windowsButton.get());
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot(
         'c++ (automatic) and other windows: execute scanner',
       );
     
       // Other - MacOS
       await user.click(ui.macosButton.get());
    -  expect(getCopyToClipboardValue(0, 'Copy')).toMatchSnapshot(
    +  expect(getCopyToClipboardValue({ i: 0, name: 'Copy' })).toMatchSnapshot(
         'c++ (automatic) and other macos: execute scanner',
       );
     });
    diff --git a/server/sonar-web/src/main/js/components/tutorials/test-utils.ts b/server/sonar-web/src/main/js/components/tutorials/test-utils.ts
    index c2f727cd252..2aff448489b 100644
    --- a/server/sonar-web/src/main/js/components/tutorials/test-utils.ts
    +++ b/server/sonar-web/src/main/js/components/tutorials/test-utils.ts
    @@ -27,8 +27,30 @@ const CI_TRANSLATE_MAP: Partial<Record<TutorialModes, string>> = {
       [TutorialModes.GitLabCI]: 'gitlab_ci',
     };
     
    -export function getCopyToClipboardValue(i = 0, name = 'copy_to_clipboard') {
    -  return screen.getAllByRole('button', { name })[i].getAttribute('data-clipboard-text');
    +interface GetCopyToClipboardValueArgs {
    +  i?: number;
    +  inlineSnippet?: boolean;
    +  name?: string;
    +}
    +
    +export function getCopyToClipboardValue({
    +  i = 0,
    +  inlineSnippet = false,
    +  name = 'copy_to_clipboard',
    +}: GetCopyToClipboardValueArgs = {}) {
    +  const button = screen.getAllByRole('button', { name })[i];
    +
    +  return inlineSnippet
    +    ? button.previousSibling?.firstChild?.textContent
    +    : button.nextSibling?.firstChild?.textContent;
    +}
    +
    +export function getCopyToClipboardHostURLValue({
    +  i = 0,
    +  name = 'copy_to_clipboard',
    +}: Omit<GetCopyToClipboardValueArgs, 'inlineSnippet'> = {}) {
    +  return screen.getAllByRole('button', { name })[i].nextSibling?.nextSibling?.firstChild
    +    ?.textContent;
     }
     
     export function getCommonNodes(ci: TutorialModes) {
    -- 
    2.39.5