695 lines · 23.3 KB · JavaScript
RawDownload
import {mount, flushPromises} from '@vue/test-utils';import {toAbsoluteUrl} from '../utils.js';import RepoActionView from './RepoActionView.vue'; const testLocale = {  approve: 'Locale Approve',  cancel: 'Locale Cancel',  rerun: 'Locale Re-run',  artifactsTitle: 'artifactTitleHere',  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: '',  preExecutionError: 'pre-execution error',  status: {    unknown: '',    waiting: '',    running: '',    success: '',    failure: '',    cancelled: '',    skipped: '',    blocked: '',  },};const minimalInitialJobData = {  state: {    run: {      status: 'success',      commit: {        pusher: {},      },    },    currentJob: {      steps: [        {          summary: 'Test Job',          duration: '1s',          status: 'success',        },      ],    },  },  logs: {    stepsLog: [],  },};const minimalInitialArtifactData = {  artifacts: [],};const defaultTestProps = {  actionsURL: 'https://example.com/example-org/example-repo/actions',  jobIndex: '1',  attemptNumber: '1',  runIndex: '10',  runID: '1001',  initialJobData: minimalInitialJobData,  initialArtifactData: minimalInitialArtifactData,  locale: testLocale,  workflowName: 'workflow name',  workflowURL: 'https://example.com/example-org/example-repo/actions?workflow=test.yml',}; 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: defaultTestProps,  });  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: defaultTestProps,  });  wrapper.vm.loadJob(); // simulate intermittent reload immediately so UI switches from minimalInitialJobData to the mock data from the test's fetch spy.  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'});  const myJobState = {    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'},      ],    },  };  vi.spyOn(global, 'fetch').mockImplementation((url, opts) => {    const artifacts_value = {      artifacts: [],    };    const stepsLog_value = [      {        step: 0,        cursor: 0,        lines: [],      },    ];    const jobs_value = {      state: myJobState,      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: {      ...defaultTestProps,      runIndex: '123',      attemptNumber: viewHistorical ? '1' : '2',      actionsURL: toAbsoluteUrl('/user1/repo2/actions'),      initialJobData: {...minimalInitialJobData, state: myJobState},    },  });  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: defaultTestProps,  });  wrapper.vm.loadJob(); // simulate intermittent reload immediately so UI switches from minimalInitialJobData to the mock data from the test's fetch spy.  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');}); test('initial load schedules refresh when job is not done', async () => {  Object.defineProperty(document.documentElement, 'lang', {value: 'en'});  vi.spyOn(global, 'fetch').mockImplementation((url, _opts) => {    return Promise.resolve({      ok: true,      json: vi.fn().mockResolvedValue(        url.endsWith('/artifacts') ? minimalInitialArtifactData : minimalInitialJobData,      ),    });  });   // Provide a job that is "done" so that the component doesn't start incremental refresh...  {    const doneInitialJobData = structuredClone(minimalInitialJobData);    doneInitialJobData.state.run.done = true;    const wrapper = mount(RepoActionView, {      props: {        ...defaultTestProps,        initialJobData: doneInitialJobData,      },    });    await flushPromises();    const container = wrapper.find('.action-view-container');    expect(container.exists()).toBe(true);    expect(container.classes()).not.toContain('interval-pending');    wrapper.unmount();

Lines 1–500 of 695.