import {mount, flushPromises} from '@vue/test-utils'; import {toAbsoluteUrl} from '../utils.js'; import RepoActionView from './RepoActionView.vue'; test('processes ##[group] and ##[endgroup]', async () => { Object.defineProperty(document.documentElement, 'lang', {value: 'en'}); vi.spyOn(global, 'fetch').mockImplementation((url, opts) => { const artifacts_value = { artifacts: [], }; const stepsLog_value = [ { step: 0, cursor: 0, lines: [ {index: 1, message: '##[group]Test group', timestamp: 0}, {index: 2, message: 'A test line', timestamp: 0}, {index: 3, message: '##[endgroup]', timestamp: 0}, {index: 4, message: 'A line outside the group', timestamp: 0}, ], }, ]; const jobs_value = { state: { run: { status: 'success', commit: { pusher: {}, }, }, currentJob: { steps: [ { summary: 'Test Job', duration: '1s', status: 'success', }, ], allAttempts: [{number: 1, time_since_started_html: '', status: 'success'}], }, }, logs: { stepsLog: opts.body?.includes('"cursor":null') ? stepsLog_value : [], }, }; return Promise.resolve({ ok: true, json: vi.fn().mockResolvedValue( url.endsWith('/artifacts') ? artifacts_value : jobs_value, ), }); }); const wrapper = mount(RepoActionView, { props: { jobIndex: '1', attemptNumber: '1', locale: { approve: '', cancel: '', rerun: '', artifactsTitle: '', areYouSure: '', confirmDeleteArtifact: '', rerun_all: '', showTimeStamps: '', showLogSeconds: '', showFullScreen: '', downloadLogs: '', runAttemptLabel: '', viewingOutOfDateRun: '', viewMostRecentRun: '', status: { unknown: '', waiting: '', running: '', success: '', failure: '', cancelled: '', skipped: '', blocked: '', }, }, }, }); await flushPromises(); await wrapper.get('.job-step-summary').trigger('click'); await flushPromises(); // Test if header was loaded correctly expect(wrapper.get('.step-summary-msg').text()).toEqual('Test Job'); // Check if 3 lines where rendered expect(wrapper.findAll('.job-log-line').length).toEqual(3); // Check if line 1 contains the group header expect(wrapper.get('.job-log-line:nth-of-type(1) > details.log-msg').text()).toEqual('Test group'); // Check if right after the header line exists a log list expect(wrapper.find('.job-log-line:nth-of-type(1) + .job-log-list.hidden').exists()).toBe(true); // Check if inside the loglist exist exactly one log line expect(wrapper.findAll('.job-log-list > .job-log-line').length).toEqual(1); // Check if inside the loglist is an logline with our second logline expect(wrapper.get('.job-log-list > .job-log-line > .log-msg').text()).toEqual('A test line'); // Check if after the log list exists another log line expect(wrapper.get('.job-log-list + .job-log-line > .log-msg').text()).toEqual('A line outside the group'); }); test('load multiple steps on a finished action', async () => { Object.defineProperty(document.documentElement, 'lang', {value: 'en'}); vi.spyOn(global, 'fetch').mockImplementation((url, opts) => { if (url.endsWith('/artifacts')) { return Promise.resolve({ ok: true, json: vi.fn().mockResolvedValue( { artifacts: [], }, ), }); } const postBody = JSON.parse(opts.body); const stepsLog_value = []; for (const cursor of postBody.logCursors) { if (cursor.expanded) { stepsLog_value.push( { step: cursor.step, cursor: 0, lines: [ {index: 1, message: `Step #${cursor.step + 1} Log #1`, timestamp: 0}, {index: 1, message: `Step #${cursor.step + 1} Log #2`, timestamp: 0}, {index: 1, message: `Step #${cursor.step + 1} Log #3`, timestamp: 0}, ], }, ); } } const jobs_value = { state: { run: { status: 'success', commit: { pusher: {}, }, }, currentJob: { steps: [ { summary: 'Test Step #1', duration: '1s', status: 'success', }, { summary: 'Test Step #2', duration: '1s', status: 'success', }, ], allAttempts: [{number: 1, time_since_started_html: '', status: 'success'}], }, }, logs: { stepsLog: opts.body?.includes('"cursor":null') ? stepsLog_value : [], }, }; return Promise.resolve({ ok: true, json: vi.fn().mockResolvedValue( jobs_value, ), }); }); const wrapper = mount(RepoActionView, { props: { actionsURL: 'https://example.com/example-org/example-repo/actions', runIndex: '1', jobIndex: '2', attemptNumber: '1', locale: { approve: '', cancel: '', rerun: '', artifactsTitle: '', areYouSure: '', confirmDeleteArtifact: '', rerun_all: '', showTimeStamps: '', showLogSeconds: '', showFullScreen: '', downloadLogs: '', runAttemptLabel: '', viewingOutOfDateRun: '', viewMostRecentRun: '', status: { unknown: '', waiting: '', running: '', success: '', failure: '', cancelled: '', skipped: '', blocked: '', }, }, }, }); await flushPromises(); // Click on both steps to start their log loading in fast succession... await wrapper.get('.job-step-section:nth-of-type(1) .job-step-summary').trigger('click'); await wrapper.get('.job-step-section:nth-of-type(2) .job-step-summary').trigger('click'); await flushPromises(); // Verify both step's logs were loaded expect(wrapper.get('.job-step-section:nth-of-type(1) .job-log-line:nth-of-type(1) .log-msg').text()).toEqual('Step #1 Log #1'); expect(wrapper.get('.job-step-section:nth-of-type(1) .job-log-line:nth-of-type(2) .log-msg').text()).toEqual('Step #1 Log #2'); expect(wrapper.get('.job-step-section:nth-of-type(1) .job-log-line:nth-of-type(3) .log-msg').text()).toEqual('Step #1 Log #3'); expect(wrapper.get('.job-step-section:nth-of-type(2) .job-log-line:nth-of-type(1) .log-msg').text()).toEqual('Step #2 Log #1'); expect(wrapper.get('.job-step-section:nth-of-type(2) .job-log-line:nth-of-type(2) .log-msg').text()).toEqual('Step #2 Log #2'); expect(wrapper.get('.job-step-section:nth-of-type(2) .job-log-line:nth-of-type(3) .log-msg').text()).toEqual('Step #2 Log #3'); }); function configureForMultipleAttemptTests({viewHistorical}) { Object.defineProperty(document.documentElement, 'lang', {value: 'en'}); vi.spyOn(global, 'fetch').mockImplementation((url, opts) => { const artifacts_value = { artifacts: [], }; const stepsLog_value = [ { step: 0, cursor: 0, lines: [], }, ]; const jobs_value = { state: { run: { canApprove: true, canCancel: true, canRerun: true, status: 'success', commit: { pusher: {}, }, }, currentJob: { steps: [ { summary: 'Test Job', duration: '1s', status: 'success', }, ], allAttempts: [ {number: 2, time_since_started_html: 'yesterday', status: 'success'}, {number: 1, time_since_started_html: 'two days ago', status: 'failure'}, ], }, }, logs: { stepsLog: opts.body?.includes('"cursor":null') ? stepsLog_value : [], }, }; return Promise.resolve({ ok: true, json: vi.fn().mockResolvedValue( url.endsWith('/artifacts') ? artifacts_value : jobs_value, ), }); }); const wrapper = mount(RepoActionView, { props: { runIndex: '123', jobIndex: '1', attemptNumber: viewHistorical ? '1' : '2', actionsURL: toAbsoluteUrl('/user1/repo2/actions'), locale: { approve: 'Locale Approve', cancel: 'Locale Cancel', rerun: 'Locale Re-run', artifactsTitle: '', areYouSure: '', confirmDeleteArtifact: '', rerun_all: '', showTimeStamps: '', showLogSeconds: '', showFullScreen: '', downloadLogs: '', runAttemptLabel: 'Run attempt %[1]s %[2]s', viewingOutOfDateRun: 'oh no, out of date since %[1]s give or take or so', viewMostRecentRun: '', status: { unknown: '', waiting: '', running: '', success: '', failure: '', cancelled: '', skipped: '', blocked: '', }, }, }, }); return wrapper; } test('display baseline with most-recent attempt', async () => { const wrapper = configureForMultipleAttemptTests({viewHistorical: false}); await flushPromises(); // Warning dialog for viewing an out-of-date attempt... expect(wrapper.findAll('.job-out-of-date-warning').length).toEqual(0); // Approve button should be visible; can't have all three at once but at least this verifies the inverse of the // historical attempt test below. expect(wrapper.findAll('button').filter((button) => button.text() === 'Locale Approve').length).toEqual(1); // Job list will be visible... expect(wrapper.findAll('.job-group-section').length).toEqual(1); // Attempt selector dropdown... expect(wrapper.findAll('.job-attempt-dropdown').length).toEqual(1); expect(wrapper.findAll('.job-attempt-dropdown .svg.octicon-check-circle-fill.text.green').length).toEqual(1); expect(wrapper.get('.job-attempt-dropdown .ui.dropdown').text()).toEqual('Run attempt 2 yesterday'); }); test('display reconfigured for historical attempt', async () => { const wrapper = configureForMultipleAttemptTests({viewHistorical: true}); await flushPromises(); // Warning dialog for viewing an out-of-date attempt... expect(wrapper.findAll('.job-out-of-date-warning').length).toEqual(1); expect(wrapper.get('.job-out-of-date-warning').text()).toEqual('oh no, out of date since two days ago give or take or so'); await wrapper.get('.job-out-of-date-warning button').trigger('click'); expect(window.location.href).toEqual(toAbsoluteUrl('/user1/repo2/actions/runs/123/jobs/1')); // eslint-disable-next-line no-restricted-globals history.back(); await flushPromises(); // Approve, Cancel, Re-run all buttons should all be suppressed... expect(wrapper.findAll('button').filter((button) => button.text() === 'Locale Approve').length).toEqual(0); expect(wrapper.findAll('button').filter((button) => button.text() === 'Locale Cancel').length).toEqual(0); expect(wrapper.findAll('button').filter((button) => button.text() === 'Locale Re-run').length).toEqual(0); // Job list will be suppressed... expect(wrapper.findAll('.job-group-section').length).toEqual(0); // Attempt selector dropdown... expect(wrapper.findAll('.job-attempt-dropdown').length).toEqual(1); expect(wrapper.findAll('.job-attempt-dropdown .svg.octicon-x-circle-fill.text.red').length).toEqual(1); expect(wrapper.get('.job-attempt-dropdown .ui.dropdown').text()).toEqual('Run attempt 1 two days ago'); }); test('historical attempt dropdown interactions', async () => { const wrapper = configureForMultipleAttemptTests({viewHistorical: true}); await flushPromises(); // Check dropdown exists, but isn't expanded. const attemptsNotExpanded = () => { expect(wrapper.findAll('.job-attempt-dropdown').length).toEqual(1); expect(wrapper.findAll('.job-attempt-dropdown .action-job-menu').length).toEqual(0, 'dropdown content not yet visible'); }; attemptsNotExpanded(); // Click on attempt dropdown wrapper.get('.job-attempt-dropdown .ui.dropdown').trigger('click'); await flushPromises(); // Check dropdown is expanded and both options are displayed const attemptsExpanded = () => { expect(wrapper.findAll('.job-attempt-dropdown .action-job-menu').length).toEqual(1); expect(wrapper.get('.job-attempt-dropdown .action-job-menu').isVisible()).toBe(true); expect(wrapper.findAll('.job-attempt-dropdown .action-job-menu a').filter((a) => a.text() === 'Run attempt 2 yesterday').length).toEqual(1); expect(wrapper.findAll('.job-attempt-dropdown .action-job-menu a').filter((a) => a.text() === 'Run attempt 1 two days ago').length).toEqual(1); }; attemptsExpanded(); // Normally dismiss occurs on a body click event; simulate that by calling `closeDropdown()` wrapper.vm.closeDropdown(); await flushPromises(); // Should return to not expanded. attemptsNotExpanded(); // Click on the gear dropdown wrapper.get('.job-gear-dropdown').trigger('click'); await flushPromises(); // Check that gear's menu is expanded, and attempt dropdown isn't. expect(wrapper.findAll('.job-gear-dropdown .action-job-menu').length).toEqual(1); expect(wrapper.get('.job-gear-dropdown .action-job-menu').isVisible()).toBe(true); attemptsNotExpanded(); // Click on attempt dropdown wrapper.get('.job-attempt-dropdown .ui.dropdown').trigger('click'); await flushPromises(); // Check that attempt dropdown expanded again, gear dropdown disappeared (mutually exclusive) expect(wrapper.findAll('.job-gear-dropdown .action-job-menu').length).toEqual(0); attemptsExpanded(); // Click on the other option in the dropdown to verify it navigates to the target attempt wrapper.findAll('.job-attempt-dropdown .action-job-menu a').find((a) => a.text() === 'Run attempt 2 yesterday').trigger('click'); expect(window.location.href).toEqual(toAbsoluteUrl('/user1/repo2/actions/runs/123/jobs/1/attempt/2')); }); test('artifacts download links', async () => { Object.defineProperty(document.documentElement, 'lang', {value: 'en'}); vi.spyOn(global, 'fetch').mockImplementation((url, opts) => { if (url.endsWith('/artifacts')) { return Promise.resolve({ ok: true, json: vi.fn().mockResolvedValue( { artifacts: [ {name: 'artifactname1', size: 111, status: 'completed'}, {name: 'artifactname2', size: 222, status: 'expired'}, ], }, ), }); } const postBody = JSON.parse(opts.body); const stepsLog_value = []; for (const cursor of postBody.logCursors) { if (cursor.expanded) { stepsLog_value.push( { step: cursor.step, cursor: 0, lines: [ {index: 1, message: `Step #${cursor.step + 1} Log #1`, timestamp: 0}, ], }, ); } } const jobs_value = { state: { run: { status: 'success', commit: { pusher: {}, }, }, currentJob: { steps: [ { summary: 'Test Step #1', duration: '1s', status: 'success', }, ], allAttempts: [{number: 1, time_since_started_html: '', status: 'success'}], }, }, logs: { stepsLog: opts.body?.includes('"cursor":null') ? stepsLog_value : [], }, }; return Promise.resolve({ ok: true, json: vi.fn().mockResolvedValue( jobs_value, ), }); }); const wrapper = mount(RepoActionView, { props: { actionsURL: 'https://example.com/example-org/example-repo/actions', runIndex: '10', runID: '1001', jobIndex: '2', attemptNumber: '1', locale: { approve: '', cancel: '', rerun: '', artifactsTitle: 'artifactTitleHere', areYouSure: '', confirmDeleteArtifact: '', rerun_all: '', showTimeStamps: '', showLogSeconds: '', showFullScreen: '', downloadLogs: '', status: { unknown: '', waiting: '', running: '', success: '', failure: '', cancelled: '', skipped: '', blocked: '', }, }, }, }); await flushPromises(); expect(wrapper.get('.job-artifacts .job-artifacts-title').text()).toEqual('artifactTitleHere'); expect(wrapper.get('.job-artifacts .job-artifacts-item:nth-of-type(1) .job-artifacts-link').attributes('href')).toEqual('https://example.com/example-org/example-repo/actions/runs/1001/artifacts/artifactname1'); expect(wrapper.get('.job-artifacts .job-artifacts-item:nth-of-type(2) .job-artifacts-link').attributes('href')).toEqual('https://example.com/example-org/example-repo/actions/runs/1001/artifacts/artifactname2'); });