Skip to content

Commit 23e9189

Browse files
committed
Add "copy" button to all output boxes
1 parent 442b574 commit 23e9189

3 files changed

Lines changed: 129 additions & 15 deletions

File tree

resources/js/vue/components/shared/CodeBox.vue

Lines changed: 66 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,45 @@
11
<template>
2-
<pre
3-
class="tw-font-mono tw-bg-gray-100 tw-p-2 tw-whitespace-pre-wrap tw-break-all tw-overflow-hidden"
4-
:class="{ 'tw-border': bordered, 'tw-rounded': bordered }"
5-
><template
6-
v-for="segment in textSegments"
7-
><a
8-
v-if="segment.href"
9-
class="tw-link tw-link-hover tw-link-info"
10-
:href="segment.href"
11-
>{{ segment.text }}</a><span
12-
v-else
13-
v-html="segment.text"
14-
/></template></pre>
2+
<div class="tw-relative">
3+
<button
4+
v-if="showCopyButton && String(text).length > 0"
5+
type="button"
6+
class="tw-btn tw-btn-ghost tw-btn-sm tw-absolute tw-top-1 tw-right-1"
7+
:title="copied ? 'Copied!' : 'Copy'"
8+
data-test="copy-button"
9+
@click="copyText"
10+
>
11+
<FontAwesomeIcon :icon="copied ? FA.faCheck : FA.faCopy" />
12+
</button>
13+
14+
<!-- Must be formatted like this to avoid extra whitespace -->
15+
<pre
16+
class="tw-font-mono tw-bg-gray-100 tw-p-2 tw-whitespace-pre-wrap tw-break-all tw-overflow-hidden"
17+
:class="{ 'tw-border': bordered, 'tw-rounded': bordered }"
18+
><template
19+
v-for="segment in textSegments"
20+
><a
21+
v-if="segment.href"
22+
class="tw-link tw-link-hover tw-link-info"
23+
:href="segment.href"
24+
>{{ segment.text }}</a><span
25+
v-else
26+
v-html="segment.text"
27+
/></template></pre>
28+
</div>
1529
</template>
1630

1731
<script>
1832
import { AnsiUp } from 'ansi_up';
33+
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
34+
import { faCopy, faCheck } from '@fortawesome/free-solid-svg-icons';
1935
2036
export default {
2137
name: 'CodeBox',
2238
39+
components: {
40+
FontAwesomeIcon,
41+
},
42+
2343
props: {
2444
text: {
2545
type: String,
@@ -39,9 +59,27 @@ export default {
3959
required: false,
4060
default: undefined,
4161
},
62+
63+
showCopyButton: {
64+
type: Boolean,
65+
default: true,
66+
},
67+
},
68+
69+
data() {
70+
return {
71+
copied: false,
72+
};
4273
},
4374
4475
computed: {
76+
FA() {
77+
return {
78+
faCopy,
79+
faCheck,
80+
};
81+
},
82+
4583
ansiText() {
4684
const escapedText = String(this.text).replace(/\[NON-XML-CHAR-0x1B\]/g, '\x1B') ?? '';
4785
return (new AnsiUp).ansi_to_html(escapedText);
@@ -97,5 +135,20 @@ export default {
97135
return segments;
98136
},
99137
},
138+
139+
methods: {
140+
async copyText() {
141+
try {
142+
await navigator.clipboard.writeText(String(this.text));
143+
this.copied = true;
144+
setTimeout(() => {
145+
this.copied = false;
146+
}, 2000);
147+
}
148+
catch (err) {
149+
console.error('Failed to copy: ', err);
150+
}
151+
},
152+
},
100153
};
101154
</script>

tests/cypress/component/code-box.cy.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,65 @@ describe('CodeBox', () => {
7777
cy.get('a').eq(0).should('have.attr', 'href', 'https://example.com');
7878
cy.get('a').eq(1).should('have.attr', 'href', 'https://example.com');
7979
});
80+
81+
it('shows the copy button by default when text is present', () => {
82+
cy.mount(CodeBox, {
83+
props: {
84+
text: 'some text',
85+
},
86+
});
87+
88+
cy.get('[data-test="copy-button"]').should('exist');
89+
});
90+
91+
it('hides the copy button when showCopyButton is false', () => {
92+
cy.mount(CodeBox, {
93+
props: {
94+
text: 'some text',
95+
showCopyButton: false,
96+
},
97+
});
98+
99+
cy.get('[data-test="copy-button"]').should('not.exist');
100+
});
101+
102+
it('hides the copy button when the text is empty', () => {
103+
cy.mount(CodeBox, {
104+
props: {
105+
text: '',
106+
},
107+
});
108+
109+
cy.get('[data-test="copy-button"]').should('not.exist');
110+
});
111+
112+
it('hides the copy button when the text becomes empty', () => {
113+
cy.mount(CodeBox, {
114+
props: {
115+
text: 'some text',
116+
},
117+
}).then(({ wrapper }) => {
118+
cy.get('[data-test="copy-button"]').should('exist').then(() => {
119+
wrapper.setProps({ text: '' });
120+
cy.get('[data-test="copy-button"]').should('not.exist');
121+
});
122+
});
123+
});
124+
125+
it('copies the text to the clipboard when the copy button is clicked', () => {
126+
const code = 'const message = "Hello, World!";';
127+
128+
cy.mount(CodeBox, {
129+
props: {
130+
text: code,
131+
},
132+
}).then(() => {
133+
cy.window().then((win) => {
134+
cy.stub(win.navigator.clipboard, 'writeText').resolves().as('writeText');
135+
});
136+
137+
cy.get('[data-test="copy-button"]').click();
138+
cy.get('@writeText').should('have.been.calledWith', code);
139+
});
140+
});
80141
});

tests/cypress/e2e/tests.cy.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,13 @@ describe('the test page', () => {
6363
// expand the test command line
6464
cy.get('a#commandlinelink').should('contain', 'Show Command Line').click();
6565
cy.get('a#commandlinelink').should('contain', 'Hide Command Line');
66-
cy.get('pre#commandline').should('contain', '/a/path/to/test/nap --run-test .');
66+
cy.get('#commandline').should('contain', '/a/path/to/test/nap --run-test .');
6767
// toggle it back
6868
cy.get('a#commandlinelink').click();
6969
cy.get('a#commandlinelink').should('contain', 'Show Command Line');
7070

7171
// verify the test output field
72-
cy.get('pre#test_output').should('contain', 'PASS');
72+
cy.get('#test_output').should('contain', 'PASS');
7373
});
7474

7575

0 commit comments

Comments
 (0)