Skip to content

Commit fec75fc

Browse files
committed
various improvements
1 parent c5d041d commit fec75fc

11 files changed

+228
-41
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
@import '../../styles/theming';
2+
3+
.button {
4+
background-color: transparent;
5+
font-size: inherit;
6+
padding: 0;
7+
border: none;
8+
color: $anchorColor;
9+
cursor: pointer;
10+
11+
&:disabled {
12+
cursor: unset;
13+
}
14+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { render, screen, waitFor } from '@testing-library/react'
2+
import { userEvent } from '@testing-library/user-event'
3+
import { expect } from 'chai'
4+
import React from 'react'
5+
import sinon from 'sinon'
6+
7+
import { CopyButton } from './CopyButton.js'
8+
9+
describe('<CopyButton/>', () => {
10+
const originalClipboard = navigator.clipboard
11+
12+
before(() => {
13+
// @ts-expect-error overriding navigator.clipboard
14+
navigator.clipboard = {
15+
writeText: sinon.stub().resolves(),
16+
}
17+
})
18+
19+
after(() => {
20+
// @ts-expect-error overriding navigator.clipboard
21+
navigator.clipboard = originalClipboard
22+
})
23+
24+
it('should copy text to clipboard, then disable and show checkmark', async () => {
25+
render(<CopyButton text="test text" />)
26+
27+
await userEvent.click(screen.getByRole('button', { name: 'Copy' }))
28+
29+
await waitFor(() => {
30+
expect(screen.getByRole('button', { name: 'Copied' })).to.be.visible
31+
expect(screen.getByRole('button', { name: 'Copied' })).to.have.property('disabled', true)
32+
expect(navigator.clipboard.writeText).to.have.been.calledOnceWithExactly('test text')
33+
})
34+
})
35+
})

src/components/app/CopyButton.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { faCheck, faCopy } from '@fortawesome/free-solid-svg-icons'
2+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
3+
import React, { FC, useState } from 'react'
4+
5+
import styles from './CopyButton.module.scss'
6+
7+
export const CopyButton: FC<{ text: string }> = ({ text }) => {
8+
const [copied, setCopied] = useState(false)
9+
10+
const handleCopy = () => {
11+
navigator.clipboard
12+
.writeText(text)
13+
.then(() => {
14+
setCopied(true)
15+
setTimeout(() => {
16+
setCopied(false)
17+
}, 3000)
18+
})
19+
.catch((err) => {
20+
console.error('Failed to copy', err)
21+
})
22+
}
23+
24+
return (
25+
<button
26+
onClick={handleCopy}
27+
aria-label={copied ? 'Copied' : 'Copy'}
28+
disabled={copied}
29+
className={styles.button}
30+
>
31+
<FontAwesomeIcon icon={copied ? faCheck : faCopy} />
32+
</button>
33+
)
34+
}

src/components/app/ExecutionSummary.module.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22

33
.conjunction {
44
color: $codeTextColor;
5-
}
5+
}

src/components/app/ExecutionSummary.spec.tsx

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { Meta } from '@cucumber/messages'
2-
import { render } from '@testing-library/react'
2+
import { render, screen } from '@testing-library/react'
3+
import { userEvent } from '@testing-library/user-event'
34
import { expect } from 'chai'
45
import React from 'react'
6+
import sinon from 'sinon'
57

68
import examplesTablesFeature from '../../../acceptance/examples-tables/examples-tables.feature.js'
79
import { EnvelopesWrapper } from './EnvelopesWrapper.js'
@@ -25,30 +27,75 @@ const meta: Meta = {
2527
},
2628
}
2729

28-
describe('ExecutionSummary', () => {
29-
const envelopes = [...examplesTablesFeature, { meta }]
30+
const envelopes = [...examplesTablesFeature, { meta }]
31+
32+
describe('<ExecutionSummary/>', () => {
33+
const originalClipboard = navigator.clipboard
34+
35+
before(() => {
36+
// @ts-expect-error overriding navigator.clipboard
37+
navigator.clipboard = {
38+
writeText: sinon.stub().resolves(),
39+
}
40+
})
41+
42+
after(() => {
43+
// @ts-expect-error overriding navigator.clipboard
44+
navigator.clipboard = originalClipboard
45+
})
3046

3147
describe('meta', () => {
32-
it('should include the implementation name and version', () => {
33-
const { getByText } = render(
48+
it('should include a phrase for the setup details', () => {
49+
render(
3450
<EnvelopesWrapper envelopes={envelopes}>
3551
<ExecutionSummary />
3652
</EnvelopesWrapper>
3753
)
3854

39-
expect(getByText('cucumber-js 8.0.0-rc.1')).to.be.visible
55+
expect(screen.getByTestId('setup.phrase')).to.contain.text(
56+
57+
)
58+
})
59+
60+
it('should copy the setup details on request', async () => {
61+
render(
62+
<EnvelopesWrapper envelopes={envelopes}>
63+
<ExecutionSummary />
64+
</EnvelopesWrapper>
65+
)
66+
67+
await userEvent.click(screen.getByRole('button', { name: 'Copy' }))
68+
69+
expect(navigator.clipboard.writeText).to.have.been
70+
.calledOnceWithExactly(`Implementation: [email protected]
71+
72+
Platform: [email protected]`)
4073
})
4174

4275
it('should include the job link', () => {
43-
const { getByText } = render(
76+
render(
77+
<EnvelopesWrapper envelopes={envelopes}>
78+
<ExecutionSummary />
79+
</EnvelopesWrapper>
80+
)
81+
82+
expect(screen.getByRole('link', { name: 'GitHub Actions' }))
83+
.attr('href')
84+
.to.eq(meta?.ci?.url)
85+
})
86+
87+
it('should include the commit link', () => {
88+
render(
4489
<EnvelopesWrapper envelopes={envelopes}>
4590
<ExecutionSummary />
4691
</EnvelopesWrapper>
4792
)
4893

49-
const jobLinkElement = getByText(meta?.ci?.name as string)
50-
expect(jobLinkElement).to.be.visible
51-
expect(jobLinkElement.getAttribute('href')).to.eq(meta?.ci?.url)
94+
expect(screen.getByRole('link', { name: 'b53d820' }))
95+
.attr('href')
96+
.to.eq(
97+
'https://github.com/cucumber/cucumber-js/commit/b53d820504b31c8e4d44234dc5eaa58d6b7fdd4c'
98+
)
5299
})
53100
})
54101
})

src/components/app/ExecutionSummary.tsx

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ import { useQueries } from '../../hooks/index.js'
1010
import { useResultStatistics } from '../../hooks/useResultStatistics.js'
1111
import { CICommitLink } from './CICommitLink.js'
1212
import { CIJobLink } from './CIJobLink.js'
13+
import { CopyButton } from './CopyButton.js'
1314
import styles from './ExecutionSummary.module.scss'
1415
import { HeaderItem, HeaderSection, HeaderSubItem } from './Header.js'
1516
import { HealthChart } from './HealthChart.js'
1617
import { ImplementationIcon } from './ImplementationIcon.js'
18+
import { makeSetupString } from './makeSetupString.js'
1719
import { OSIcon } from './OSIcon.js'
1820
import { RuntimeIcon } from './RuntimeIcon.js'
1921

@@ -41,13 +43,14 @@ export const ExecutionSummary: FC = () => {
4143
<RuntimeIcon runtime={meta.runtime} />
4244
<OSIcon os={meta.os} />
4345
</HeaderSubItem>
44-
<HeaderSubItem>
46+
<span data-testid="setup.phrase">
4547
<VersionedTool {...meta.implementation} fallback="unknown tool" />
46-
<em className={styles.conjunction}>with</em>
48+
<em className={styles.conjunction}> with </em>
4749
<VersionedTool {...meta.runtime} fallback="unknown runtime" />
48-
<em className={styles.conjunction}>on</em>
50+
<em className={styles.conjunction}> on </em>
4951
<VersionedTool {...meta.os} fallback="unknown platform" />
50-
</HeaderSubItem>
52+
</span>
53+
<CopyButton text={makeSetupString(meta)} />
5154
</HeaderItem>
5255
</HeaderSection>
5356
)}
@@ -59,21 +62,26 @@ export const ExecutionSummary: FC = () => {
5962
{formatStatusRate(
6063
scenarioCountByStatus[TestStepResultStatus.PASSED],
6164
totalScenarioCount
62-
)}{' passed'}
65+
)}
66+
{' passed'}
6367
</span>
6468
</HeaderSubItem>
6569
</HeaderItem>
6670
{startDate && (
6771
<HeaderItem>
6872
<HeaderSubItem>
6973
<FontAwesomeIcon aria-hidden="true" style={{ opacity: 0.75 }} icon={faStopwatch} />
70-
<time title={startDate.toString()} dateTime={startDate.toISOString()}>{formatExecutionDistance(startDate)}</time>
71-
{finishDate && (
72-
<>
73-
<em className={styles.conjunction}>in</em>
74-
<span>{formatExecutionDuration(startDate, finishDate)}</span>
75-
</>
76-
)}
74+
<span>
75+
<time title={startDate.toString()} dateTime={startDate.toISOString()}>
76+
{formatExecutionDistance(startDate)}
77+
</time>
78+
{finishDate && (
79+
<>
80+
<em className={styles.conjunction}> in </em>
81+
<span>{formatExecutionDuration(startDate, finishDate)}</span>
82+
</>
83+
)}
84+
</span>
7785
</HeaderSubItem>
7886
</HeaderItem>
7987
)}

src/components/app/Header.module.scss

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515

1616
.item {
1717
display: flex;
18-
align-items: center;
18+
align-items: flex-start;
19+
gap: 0.5em;
1920
font-size: 1.125em;
2021
padding: 1rem;
2122

@@ -43,8 +44,4 @@
4344
> svg {
4445
height: 1.25em;
4546
}
46-
47-
& + & {
48-
margin-left: 0.5em;
49-
}
5047
}

src/components/app/Header.tsx

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,14 @@ import React, { FC, PropsWithChildren } from 'react'
22

33
import styles from './Header.module.scss'
44

5-
export const HeaderSection: FC<PropsWithChildren> = ({
6-
children,
7-
}) => {
5+
export const HeaderSection: FC<PropsWithChildren> = ({ children }) => {
86
return <div className={styles.section}>{children}</div>
97
}
108

11-
export const HeaderItem: FC<PropsWithChildren> = ({
12-
children,
13-
}) => {
9+
export const HeaderItem: FC<PropsWithChildren> = ({ children }) => {
1410
return <div className={styles.item}>{children}</div>
1511
}
1612

17-
export const HeaderSubItem: FC<PropsWithChildren> = ({
18-
children,
19-
}) => {
13+
export const HeaderSubItem: FC<PropsWithChildren> = ({ children }) => {
2014
return <div className={styles.subItem}>{children}</div>
2115
}

src/components/app/OSIcon.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@ import { MacOS } from './icons/MacOS.js'
66
import { Windows } from './icons/Windows.js'
77

88
export const OSIcon: FC<{ os: Meta['os'] }> = ({ os }) => {
9-
const { name, version } = os
9+
const { name } = os
1010
if (name.match(/windows|win32/i)) {
11-
return <Windows version={version} />
11+
return <Windows />
1212
}
1313
if (name.match(/(darwin|mac)/i)) {
14-
return <MacOS version={version} />
14+
return <MacOS />
1515
}
1616
if (name.match(/linux/i)) {
17-
return <Linux version={version} />
17+
return <Linux />
1818
}
1919
return null
2020
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { expect } from 'chai'
2+
3+
import { makeSetupString } from './makeSetupString.js'
4+
5+
describe('makeSetupString', () => {
6+
it('returns a multi-line string including impl, runtime and os', () => {
7+
const result = makeSetupString({
8+
protocolVersion: '17.1.1',
9+
implementation: { version: '8.0.0-rc.1', name: 'cucumber-js' },
10+
cpu: { name: 'x64' },
11+
os: { name: 'linux', version: '5.11.0-1022-azure' },
12+
runtime: { name: 'node.js', version: '16.13.1' },
13+
ci: {
14+
name: 'GitHub Actions',
15+
url: 'https://github.com/cucumber/cucumber-js/actions/runs/1592557391',
16+
buildNumber: '1592557391',
17+
git: {
18+
revision: 'b53d820504b31c8e4d44234dc5eaa58d6b7fdd4c',
19+
remote: 'https://github.com/cucumber/cucumber-js.git',
20+
branch: 'main',
21+
},
22+
},
23+
})
24+
25+
expect(result).to.eq(
26+
`Implementation: [email protected]
27+
28+
29+
)
30+
})
31+
})

0 commit comments

Comments
 (0)