Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 66 additions & 13 deletions resources/js/vue/components/shared/CodeBox.vue
Original file line number Diff line number Diff line change
@@ -1,25 +1,45 @@
<template>
<pre
class="tw-font-mono tw-bg-gray-100 tw-p-2 tw-whitespace-pre-wrap tw-break-all tw-overflow-hidden"
:class="{ 'tw-border': bordered, 'tw-rounded': bordered }"
><template
v-for="segment in textSegments"
><a
v-if="segment.href"
class="tw-link tw-link-hover tw-link-info"
:href="segment.href"
>{{ segment.text }}</a><span
v-else
v-html="segment.text"
/></template></pre>
<div class="tw-relative">
<button
v-if="showCopyButton && String(text).length > 0"
type="button"
class="tw-btn tw-btn-ghost tw-btn-sm tw-absolute tw-top-1 tw-right-1"
:title="copied ? 'Copied!' : 'Copy'"
data-test="copy-button"
@click="copyText"
>
<FontAwesomeIcon :icon="copied ? FA.faCheck : FA.faCopy" />
</button>

<!-- Must be formatted like this to avoid extra whitespace -->
<pre
class="tw-font-mono tw-bg-gray-100 tw-p-2 tw-whitespace-pre-wrap tw-break-all tw-overflow-hidden"
:class="{ 'tw-border': bordered, 'tw-rounded': bordered }"
><template
v-for="segment in textSegments"
><a
v-if="segment.href"
class="tw-link tw-link-hover tw-link-info"
:href="segment.href"
>{{ segment.text }}</a><span
v-else
v-html="segment.text"
/></template></pre>
</div>
</template>

<script>
import { AnsiUp } from 'ansi_up';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import { faCopy, faCheck } from '@fortawesome/free-solid-svg-icons';

export default {
name: 'CodeBox',

components: {
FontAwesomeIcon,
},

props: {
text: {
type: String,
Expand All @@ -39,9 +59,27 @@ export default {
required: false,
default: undefined,
},

showCopyButton: {
type: Boolean,
default: true,
},
},

data() {
return {
copied: false,
};
},

computed: {
FA() {
return {
faCopy,
faCheck,
};
},

ansiText() {
const escapedText = String(this.text).replace(/\[NON-XML-CHAR-0x1B\]/g, '\x1B') ?? '';
return (new AnsiUp).ansi_to_html(escapedText);
Expand Down Expand Up @@ -97,5 +135,20 @@ export default {
return segments;
},
},

methods: {
async copyText() {
try {
await navigator.clipboard.writeText(String(this.text));
this.copied = true;
setTimeout(() => {
this.copied = false;
}, 2000);
}
catch (err) {
console.error('Failed to copy: ', err);
}
},
},
};
</script>
61 changes: 61 additions & 0 deletions tests/cypress/component/code-box.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,65 @@ describe('CodeBox', () => {
cy.get('a').eq(0).should('have.attr', 'href', 'https://example.com');
cy.get('a').eq(1).should('have.attr', 'href', 'https://example.com');
});

it('shows the copy button by default when text is present', () => {
cy.mount(CodeBox, {
props: {
text: 'some text',
},
});

cy.get('[data-test="copy-button"]').should('exist');
});

it('hides the copy button when showCopyButton is false', () => {
cy.mount(CodeBox, {
props: {
text: 'some text',
showCopyButton: false,
},
});

cy.get('[data-test="copy-button"]').should('not.exist');
});

it('hides the copy button when the text is empty', () => {
cy.mount(CodeBox, {
props: {
text: '',
},
});

cy.get('[data-test="copy-button"]').should('not.exist');
});

it('hides the copy button when the text becomes empty', () => {
cy.mount(CodeBox, {
props: {
text: 'some text',
},
}).then(({ wrapper }) => {
cy.get('[data-test="copy-button"]').should('exist').then(() => {
wrapper.setProps({ text: '' });
cy.get('[data-test="copy-button"]').should('not.exist');
});
});
});

it('copies the text to the clipboard when the copy button is clicked', () => {
const code = 'const message = "Hello, World!";';

cy.mount(CodeBox, {
props: {
text: code,
},
}).then(() => {
cy.window().then((win) => {
cy.stub(win.navigator.clipboard, 'writeText').resolves().as('writeText');
});

cy.get('[data-test="copy-button"]').click();
cy.get('@writeText').should('have.been.calledWith', code);
});
});
});
4 changes: 2 additions & 2 deletions tests/cypress/e2e/tests.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,13 @@ describe('the test page', () => {
// expand the test command line
cy.get('a#commandlinelink').should('contain', 'Show Command Line').click();
cy.get('a#commandlinelink').should('contain', 'Hide Command Line');
cy.get('pre#commandline').should('contain', '/a/path/to/test/nap --run-test .');
cy.get('#commandline').should('contain', '/a/path/to/test/nap --run-test .');
// toggle it back
cy.get('a#commandlinelink').click();
cy.get('a#commandlinelink').should('contain', 'Show Command Line');

// verify the test output field
cy.get('pre#test_output').should('contain', 'PASS');
cy.get('#test_output').should('contain', 'PASS');
});


Expand Down
Loading