Skip to content
Snippets Groups Projects
Commit 1ece510a authored by Mireya Andres's avatar Mireya Andres :red_circle:
Browse files

Merge branch 'copy-failed-spec-names' into 'master'

Copy failed spec names to clipboard from MR widget

See merge request !91552
parents 030a99b7 5419abe3
No related branches found
No related tags found
No related merge requests found
Pipeline #587594519 passed
Pipeline: GitLab

#587607665

    <script>
    import { GlBadge, GlFriendlyWrap, GlLink, GlModal } from '@gitlab/ui';
    import { GlBadge, GlFriendlyWrap, GlLink, GlModal, GlTooltipDirective } from '@gitlab/ui';
    import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
    import { __, n__, s__, sprintf } from '~/locale';
    import CodeBlock from '~/vue_shared/components/code_block.vue';
    ......@@ -11,6 +12,10 @@ export default {
    GlFriendlyWrap,
    GlLink,
    GlModal,
    ModalCopyButton,
    },
    directives: {
    GlTooltip: GlTooltipDirective,
    },
    props: {
    modalId: {
    ......@@ -57,6 +62,7 @@ export default {
    history: __('History'),
    trace: __('System output'),
    attachment: s__('TestReports|Attachment'),
    copyTestName: s__('TestReports|Copy test name to rerun locally'),
    },
    modalCloseButton: {
    text: __('Close'),
    ......@@ -85,6 +91,13 @@ export default {
    {{ testCase.file }}
    </gl-link>
    <span v-else>{{ testCase.file }}</span>
    <modal-copy-button
    :title="$options.text.copyTestName"
    :text="testCase.file"
    :modal-id="modalId"
    category="tertiary"
    class="gl-ml-1"
    />
    </div>
    </div>
    ......
    <script>
    import { GlButton, GlDropdown, GlDropdownItem } from '@gitlab/ui';
    import { GlButton, GlDropdown, GlDropdownItem, GlTooltipDirective } from '@gitlab/ui';
    import { sprintf, __ } from '~/locale';
    export default {
    ......@@ -8,6 +8,9 @@ export default {
    GlDropdown,
    GlDropdownItem,
    },
    directives: {
    GlTooltip: GlTooltipDirective,
    },
    props: {
    widget: {
    type: String,
    ......@@ -19,6 +22,12 @@ export default {
    default: () => [],
    },
    },
    data: () => {
    return {
    timeout: null,
    updatingTooltip: false,
    };
    },
    computed: {
    dropdownLabel() {
    return sprintf(__('%{widget} options'), { widget: this.widget });
    ......@@ -27,9 +36,29 @@ export default {
    methods: {
    onClickAction(action) {
    this.$emit('clickedAction', action);
    if (action.onClick) {
    action.onClick();
    }
    if (action.tooltipOnClick) {
    this.updatingTooltip = true;
    this.$root.$emit('bv::show::tooltip', action.id);
    clearTimeout(this.timeout);
    this.timeout = setTimeout(() => {
    this.updatingTooltip = false;
    this.$root.$emit('bv::hide::tooltip', action.id);
    }, 1000);
    }
    },
    setTooltip(btn) {
    if (this.updatingTooltip && btn.tooltipOnClick) {
    return btn.tooltipOnClick;
    }
    return btn.tooltipText;
    },
    },
    };
    ......@@ -55,6 +84,7 @@ export default {
    :key="index"
    :href="btn.href"
    :target="btn.target"
    :data-clipboard-text="btn.dataClipboardText"
    @click="onClickAction(btn)"
    >
    {{ btn.text }}
    ......@@ -63,15 +93,20 @@ export default {
    <template v-if="tertiaryButtons.length">
    <gl-button
    v-for="(btn, index) in tertiaryButtons"
    :id="btn.id"
    :key="index"
    v-gl-tooltip.hover
    :title="setTooltip(btn)"
    :href="btn.href"
    :target="btn.target"
    :class="{ 'gl-mr-3': index !== tertiaryButtons.length - 1 }"
    :data-clipboard-text="btn.dataClipboardText"
    :icon="btn.icon"
    :data-testid="btn.testId || 'extension-actions-button'"
    :variant="btn.variant || 'confirm'"
    category="tertiary"
    variant="confirm"
    size="small"
    class="gl-display-none gl-md-display-block gl-float-left"
    data-testid="extension-actions-button"
    @click="onClickAction(btn)"
    >
    {{ btn.text }}
    ......
    ......@@ -7,6 +7,8 @@ export const TESTS_FAILED_STATUS = 'failed';
    export const ERROR_STATUS = 'error';
    export const i18n = {
    copyFailedSpecs: s__('Reports|Copy failed tests'),
    copyFailedSpecsTooltip: s__('Reports|Copy failed test names to run locally'),
    label: s__('Reports|Test summary'),
    loading: s__('Reports|Test summary results are loading'),
    error: s__('Reports|Test summary failed to load results'),
    ......
    import { uniqueId } from 'lodash';
    import { __ } from '~/locale';
    import axios from '~/lib/utils/axios_utils';
    import TestCaseDetails from '~/pipelines/components/test_reports/test_case_details.vue';
    import { EXTENSION_ICONS } from '../../constants';
    ......@@ -19,6 +20,20 @@ export default {
    props: ['testResultsPath', 'headBlobPath', 'pipeline'],
    modalComponent: TestCaseDetails,
    computed: {
    failedTestNames() {
    if (!this.collapsedData?.suites) {
    return '';
    }
    const newFailures = this.collapsedData?.suites.flatMap((suite) => [suite.new_failures || []]);
    const fileNames = newFailures.flatMap((newFailure) => {
    return newFailure.map((failure) => {
    return failure.file;
    });
    });
    return fileNames.join(' ');
    },
    summary(data) {
    if (data.parsingInProgress) {
    return this.$options.i18n.loading;
    ......@@ -41,14 +56,29 @@ export default {
    return EXTENSION_ICONS.success;
    },
    tertiaryButtons() {
    return [
    {
    text: this.$options.i18n.fullReport,
    href: `${this.pipeline.path}/test_report`,
    target: '_blank',
    fullReport: true,
    },
    ];
    const actionButtons = [];
    if (this.failedTestNames().length > 0) {
    actionButtons.push({
    dataClipboardText: this.failedTestNames(),
    id: uniqueId('copy-to-clipboard'),
    icon: 'copy-to-clipboard',
    testId: 'copy-failed-specs-btn',
    text: this.$options.i18n.copyFailedSpecs,
    tooltipText: this.$options.i18n.copyFailedSpecsTooltip,
    tooltipOnClick: __('Copied'),
    });
    }
    actionButtons.push({
    text: this.$options.i18n.fullReport,
    href: `${this.pipeline.path}/test_report`,
    target: '_blank',
    fullReport: true,
    testId: 'full-report-link',
    });
    return actionButtons;
    },
    },
    methods: {
    ......
    ......@@ -32543,6 +32543,12 @@ msgstr ""
    msgid "Reports|Classname"
    msgstr ""
     
    msgid "Reports|Copy failed test names to run locally"
    msgstr ""
    msgid "Reports|Copy failed tests"
    msgstr ""
    msgid "Reports|Execution time"
    msgstr ""
     
    ......@@ -38429,6 +38435,9 @@ msgstr ""
    msgid "TestReports|Attachment"
    msgstr ""
     
    msgid "TestReports|Copy test name to rerun locally"
    msgstr ""
    msgid "TestReports|Job artifacts are expired"
    msgstr ""
     
    ......@@ -3,6 +3,7 @@ import { shallowMount } from '@vue/test-utils';
    import { extendedWrapper } from 'helpers/vue_test_utils_helper';
    import TestCaseDetails from '~/pipelines/components/test_reports/test_case_details.vue';
    import CodeBlock from '~/vue_shared/components/code_block.vue';
    import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
    describe('Test case details', () => {
    let wrapper;
    ......@@ -19,6 +20,7 @@ describe('Test case details', () => {
    system_output: 'Line 42 is broken',
    };
    const findCopyFileBtn = () => wrapper.findComponent(ModalCopyButton);
    const findModal = () => wrapper.findComponent(GlModal);
    const findName = () => wrapper.findByTestId('test-case-name');
    const findFile = () => wrapper.findByTestId('test-case-file');
    ......@@ -66,6 +68,10 @@ describe('Test case details', () => {
    expect(findFileLink().attributes('href')).toBe(defaultTestCase.filePath);
    });
    it('renders copy button for test case file', () => {
    expect(findCopyFileBtn().attributes('text')).toBe(defaultTestCase.file);
    });
    it('renders the test case duration', () => {
    expect(findDuration().text()).toBe(defaultTestCase.formattedTime);
    });
    ......
    ......@@ -8,12 +8,14 @@
    {
    "result": "failure",
    "name": "Test#sum when a is 1 and b is 2 returns summary",
    "file": "spec/file_1.rb",
    "execution_time": 0.009411,
    "system_output": "Failure/Error: is_expected.to eq(3)\n\n expected: 3\n got: -1\n\n (compared using ==)\n./spec/test_spec.rb:12:in `block (4 levels) in <top (required)>'"
    },
    {
    "result": "failure",
    "name": "Test#sum when a is 100 and b is 200 returns summary",
    "file": "spec/file_2.rb",
    "execution_time": 0.000162,
    "system_output": "Failure/Error: is_expected.to eq(300)\n\n expected: 300\n got: -100\n\n (compared using ==)\n./spec/test_spec.rb:21:in `block (4 levels) in <top (required)>'"
    }
    ......
    import { GlButton } from '@gitlab/ui';
    import { nextTick } from 'vue';
    import MockAdapter from 'axios-mock-adapter';
    import testReportExtension from '~/vue_merge_request_widget/extensions/test_report';
    import { i18n } from '~/vue_merge_request_widget/extensions/test_report/constants';
    ......@@ -38,7 +38,8 @@ describe('Test report extension', () => {
    };
    const findToggleCollapsedButton = () => wrapper.findByTestId('toggle-button');
    const findTertiaryButton = () => wrapper.find(GlButton);
    const findFullReportLink = () => wrapper.findByTestId('full-report-link');
    const findCopyFailedSpecsBtn = () => wrapper.findByTestId('copy-failed-specs-btn');
    const findAllExtensionListItems = () => wrapper.findAllByTestId('extension-list-item');
    const findModal = () => wrapper.find(TestCaseDetails);
    ......@@ -130,8 +131,57 @@ describe('Test report extension', () => {
    await waitForPromises();
    expect(findTertiaryButton().text()).toBe('Full report');
    expect(findTertiaryButton().attributes('href')).toBe('pipeline/path/test_report');
    expect(findFullReportLink().text()).toBe('Full report');
    expect(findFullReportLink().attributes('href')).toBe('pipeline/path/test_report');
    });
    it('hides copy failed tests button when there are no failing tests', async () => {
    mockApi(httpStatusCodes.OK);
    createComponent();
    await waitForPromises();
    expect(findCopyFailedSpecsBtn().exists()).toBe(false);
    });
    it('displays copy failed tests button when there are failing tests', async () => {
    mockApi(httpStatusCodes.OK, newFailedTestReports);
    createComponent();
    await waitForPromises();
    expect(findCopyFailedSpecsBtn().exists()).toBe(true);
    expect(findCopyFailedSpecsBtn().text()).toBe(i18n.copyFailedSpecs);
    expect(findCopyFailedSpecsBtn().attributes('data-clipboard-text')).toBe(
    'spec/file_1.rb spec/file_2.rb',
    );
    });
    it('copy failed tests button updates tooltip text when clicked', async () => {
    mockApi(httpStatusCodes.OK, newFailedTestReports);
    createComponent();
    await waitForPromises();
    // original tooltip shows up
    expect(findCopyFailedSpecsBtn().attributes()).toMatchObject({
    title: i18n.copyFailedSpecsTooltip,
    });
    await findCopyFailedSpecsBtn().trigger('click');
    // tooltip text is replaced for 1 second
    expect(findCopyFailedSpecsBtn().attributes()).toMatchObject({
    title: 'Copied',
    });
    jest.runAllTimers();
    await nextTick();
    // tooltip reverts back to original string
    expect(findCopyFailedSpecsBtn().attributes()).toMatchObject({
    title: i18n.copyFailedSpecsTooltip,
    });
    });
    it('shows an error when a suite has a parsing error', async () => {
    ......
    0% Loading or .
    You are about to add 0 people to the discussion. Proceed with caution.
    Finish editing this message first!
    Please register or to comment