feat(reporter): add copy button for annotations (#31790)

Adds a copy-to-clipboard button for each annotation so that text can be
copied easily.
This re-uses the existing `CopyToClipboard` component and adds a `small`
variant that can be used inline. The icon size and colour have been
chosen to avoid being overwhelming when used inline.

Related to #30141 
I opted not to introduce the hover behaviour from #30749 as it's less
discoverable, but can understand why that might be favourable. Certainly
open to suggestions 😄

<img width="379" alt="Screenshot 2024-07-22 at 3 23 53 PM"
src="https://github.com/user-attachments/assets/3b9998cf-2e8d-40c9-9c8a-64eab3a9ed2e">
This commit is contained in:
Anthony Roberts 2024-09-17 00:57:11 +10:00 committed by GitHub
parent 762e954599
commit 71c43693ac
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 57 additions and 6 deletions

View file

@ -20,15 +20,31 @@
width: 24px;
border: none;
outline: none;
color: var(--color-fg-default);
color: var(--color-fg-muted);
background: transparent;
padding: 4px;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 4px;
}
.copy-icon svg {
margin: 0;
}
.copy-icon:not(:disabled):hover {
background-color: var(--color-border-default);
}
.copy-button-container {
visibility: hidden;
display: inline-flex;
margin-left: 8px;
vertical-align: bottom;
}
.copy-value-container:hover .copy-button-container {
visibility: visible;
}

View file

@ -18,9 +18,14 @@ import * as React from 'react';
import * as icons from './icons';
import './copyToClipboard.css';
export const CopyToClipboard: React.FunctionComponent<{
value: string,
}> = ({ value }) => {
type CopyToClipboardProps = {
value: string;
};
/**
* A copy to clipboard button.
*/
export const CopyToClipboard: React.FunctionComponent<CopyToClipboardProps> = ({ value }) => {
type IconType = 'copy' | 'check' | 'cross';
const [icon, setIcon] = React.useState<IconType>('copy');
const handleCopy = React.useCallback(() => {
@ -34,5 +39,21 @@ export const CopyToClipboard: React.FunctionComponent<{
});
}, [value]);
const iconElement = icon === 'check' ? icons.check() : icon === 'cross' ? icons.cross() : icons.copy();
return <button className='copy-icon' onClick={handleCopy}>{iconElement}</button>;
return <button className='copy-icon' aria-label='Copy to clipboard' onClick={handleCopy}>{iconElement}</button>;
};
type CopyToClipboardContainerProps = CopyToClipboardProps & {
children: React.ReactNode
};
/**
* Container for displaying a copy to clipboard button alongside children.
*/
export const CopyToClipboardContainer: React.FunctionComponent<CopyToClipboardContainerProps> = ({ children, value }) => {
return <span className='copy-value-container'>
{children}
<span className='copy-button-container'>
<CopyToClipboard value={value} />
</span>
</span>;
};

View file

@ -76,6 +76,19 @@ test('should render test case', async ({ mount }) => {
await expect(component.getByText('My test')).toBeVisible();
});
test('should render copy buttons for annotations', async ({ mount, page, context }) => {
await context.grantPermissions(['clipboard-read', 'clipboard-write']);
const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} test={testCase} run={0} anchor=''></TestCaseView>);
await expect(component.getByText('Annotation text', { exact: false }).first()).toBeVisible();
component.getByText('Annotation text', { exact: false }).first().hover();
await expect(component.getByLabel('Copy to clipboard').first()).toBeVisible();
await component.getByLabel('Copy to clipboard').first().click();
const handle = await page.evaluateHandle(() => navigator.clipboard.readText());
const clipboardContent = await handle.jsonValue();
expect(clipboardContent).toBe('Annotation text');
});
const annotationLinkRenderingTestCase: TestCase = {
testId: 'testid',
title: 'My test',

View file

@ -26,6 +26,7 @@ import { TestResultView } from './testResultView';
import { linkifyText } from '@web/renderUtils';
import { hashStringToInt, msToString } from './utils';
import { clsx } from '@web/uiUtils';
import { CopyToClipboardContainer } from './copyToClipboard';
export const TestCaseView: React.FC<{
projectNames: string[],
@ -73,7 +74,7 @@ function TestCaseAnnotationView({ annotation: { type, description } }: { annotat
return (
<div className='test-case-annotation'>
<span style={{ fontWeight: 'bold' }}>{type}</span>
{description && <span>: {linkifyText(description)}</span>}
{description && <CopyToClipboardContainer value={description}>: {linkifyText(description)}</CopyToClipboardContainer>}
</div>
);
}