Skip to content
Draft
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
165 changes: 165 additions & 0 deletions src/__tests__/CompareResults/ProfileCompare.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import fetchMock from '@fetch-mock/jest';
import userEvent from '@testing-library/user-event';

import { ProfileCompareButton } from '../../components/CompareResults/ProfileCompare/ProfileCompareButton';
import type { CompareResultsItem } from '../../types/state';
import getTestData from '../utils/fixtures';
import { render, screen, waitFor, within } from '../utils/test-utils';

// A minimal speedometer3 row derived from an existing fixture. We only need
// the fields that ProfileCompareButton / Dialog reads.
function makeSpeedometer3Row(
overrides: Partial<CompareResultsItem> = {},
): CompareResultsItem {
const base = getTestData().testCompareData[0];
return {
...base,
suite: 'speedometer3',
header_name: 'browsertime speedometer3 opt',
base_repository_name: 'try',
new_repository_name: 'try',
base_rev: 'b45e818c8db40353dae549cd7235c8210c58802b',
new_rev: 'f00ba7f00ba7f00ba7f00ba7f00ba7f00ba7f00b',
base_retriggerable_job_ids: [111, 222],
new_retriggerable_job_ids: [333, 444],
base_runs: [412.3, 415.8],
new_runs: [425.5, 431.7],
...overrides,
};
}

function mockJobInfo(repo: string, jobId: number, taskId: string, retryId = 0) {
fetchMock.get(
`begin:https://treeherder.mozilla.org/api/project/${repo}/jobs/${jobId}/`,
{ taskcluster_metadata: { task_id: taskId, retry_id: retryId } },
);
}

function mockArtifacts(taskId: string, runId: number, names: string[]) {
fetchMock.get(
`https://firefox-ci-tc.services.mozilla.com/api/queue/v1/task/${taskId}/runs/${runId}/artifacts`,
{ artifacts: names.map((name) => ({ name })) },
);
}

function mockTaskStatus(taskId: string, workerId: string, runId = 0) {
fetchMock.get(
`https://firefox-ci-tc.services.mozilla.com/api/queue/v1/task/${taskId}/status`,
// Wrap in `body` so fetch-mock doesn't interpret the response's top-level
// `status` key as an HTTP status code.
{ body: { status: { runs: [{ runId, state: 'completed', workerId }] } } },
);
}

describe('ProfileCompareButton', () => {
afterEach(() => {
fetchMock.mockReset();
});

it('shows runs with worker IDs, sorted by score, with the median preselected', async () => {
// Base has three profiled runs (out-of-order scores) so we can verify
// both sort-by-score and median preselection. New has two profiled runs
// (one job with no profile is filtered out).
const row = makeSpeedometer3Row({
base_retriggerable_job_ids: [111, 222, 555],
new_retriggerable_job_ids: [333, 444],
base_runs: [415.8, 412.3, 418.2],
new_runs: [425.5, 431.7],
});

mockJobInfo('try', 111, 'TASKBASE1');
mockJobInfo('try', 222, 'TASKBASE2');
mockJobInfo('try', 555, 'TASKBASE3');
mockJobInfo('try', 333, 'TASKNEW1');
mockJobInfo('try', 444, 'TASKNEW2');

const profile = 'public/test_info/profile_speedometer3_compact.jslb.gz';
mockArtifacts('TASKBASE1', 0, [profile, 'public/logs/live.log']);
mockArtifacts('TASKBASE2', 0, [profile]);
mockArtifacts('TASKBASE3', 0, [profile]);
mockArtifacts('TASKNEW1', 0, [profile]);
// No profile on this run — should be filtered out.
mockArtifacts('TASKNEW2', 0, ['public/logs/live.log']);

mockTaskStatus('TASKBASE1', 'worker-b1');
mockTaskStatus('TASKBASE2', 'worker-b2');
mockTaskStatus('TASKBASE3', 'worker-b3');
mockTaskStatus('TASKNEW1', 'worker-n1');
mockTaskStatus('TASKNEW2', 'worker-n2');

render(<ProfileCompareButton result={row} />);

const openButton = await screen.findByTitle(
'open profile comparison for this result',
);
const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
await user.click(openButton);

const dialog = await screen.findByRole('dialog');
// Wait for the dialog to be populated.
await waitFor(() =>
expect(
within(dialog).getByText(/Run 1.*score.*412\.3/),
).toBeInTheDocument(),
);

// Base runs should appear sorted by score ascending:
// 412.3 (worker-b2), 415.8 (worker-b1), 418.2 (worker-b3).
// New runs should appear sorted: 425.5 (worker-n1), 431.7 — actually
// the only profiled new runs are TASKNEW1 (425.5). TASKNEW2 has no
// profile artifact so isn't shown.
const allLabels = within(dialog).getAllByText(/Run \d.*score/);
// 3 base + 1 new = 4
expect(allLabels).toHaveLength(4);

// Worker IDs should be visible.
expect(within(dialog).getByText(/worker-b2/)).toBeInTheDocument();
expect(within(dialog).getByText(/worker-n1/)).toBeInTheDocument();

// Median of base is index 1 (415.8, worker-b1). Only one new run so
// that's preselected. The compare button should be enabled immediately.
const activeCompareBtn = within(dialog).getByRole('link', {
name: 'Open profile comparison',
});
expect(activeCompareBtn).toHaveAttribute(
'href',
expect.stringContaining('TASKBASE1'),
);
expect(activeCompareBtn).toHaveAttribute(
'href',
expect.stringContaining('TASKNEW1'),
);
});

it('shows an empty-state message when no runs have profile artifacts', async () => {
mockJobInfo('try', 111, 'TASKBASE1');
mockJobInfo('try', 222, 'TASKBASE2');
mockJobInfo('try', 333, 'TASKNEW1');
mockJobInfo('try', 444, 'TASKNEW2');

mockArtifacts('TASKBASE1', 0, ['public/logs/live.log']);
mockArtifacts('TASKBASE2', 0, ['public/logs/live.log']);
mockArtifacts('TASKNEW1', 0, ['public/logs/live.log']);
mockArtifacts('TASKNEW2', 0, ['public/logs/live.log']);

mockTaskStatus('TASKBASE1', 'worker-b1');
mockTaskStatus('TASKBASE2', 'worker-b2');
mockTaskStatus('TASKNEW1', 'worker-n1');
mockTaskStatus('TASKNEW2', 'worker-n2');

render(<ProfileCompareButton result={makeSpeedometer3Row()} />);

const openButton = await screen.findByTitle(
'open profile comparison for this result',
);
const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
await user.click(openButton);

const dialog = await screen.findByRole('dialog');
const emptyMessages = await within(dialog).findAllByText(
'No profile artifacts found.',
);
// Both sides show the empty message.
expect(emptyMessages).toHaveLength(2);
});
});
41 changes: 41 additions & 0 deletions src/__tests__/CompareResults/ProfileCompareUrls.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {
buildCompareBenchmarkUrl,
buildSingleProfileUrl,
buildTaskArtifactUrl,
SPEEDOMETER3_PROFILE_ARTIFACT,
} from '../../components/CompareResults/ProfileCompare/urls';

describe('ProfileCompare url helpers', () => {
it('builds a Taskcluster artifact URL', () => {
expect(
buildTaskArtifactUrl(
'eSFQ0OC9R665QYfdgtWgKA',
0,
SPEEDOMETER3_PROFILE_ARTIFACT,
),
).toBe(
'https://firefox-ci-tc.services.mozilla.com/api/queue/v1/task/eSFQ0OC9R665QYfdgtWgKA/runs/0/artifacts/public/test_info/profile_speedometer3_compact.jslb.gz',
);
});

it('builds a single-profile URL that decodes to the raw artifact URL', () => {
const url = buildSingleProfileUrl('eSFQ0OC9R665QYfdgtWgKA', 0);
expect(url).toBe(
'https://profiler.firefox.com/from-url/https%3A%2F%2Ffirefox-ci-tc.services.mozilla.com%2Fapi%2Fqueue%2Fv1%2Ftask%2FeSFQ0OC9R665QYfdgtWgKA%2Fruns%2F0%2Fartifacts%2Fpublic%2Ftest_info%2Fprofile_speedometer3_compact.jslb.gz',
);
});

it('builds a compare-benchmark URL with both profiles', () => {
const base = buildSingleProfileUrl('CVdpkviXTEyAJH37VBMTGQ', 0);
const cmp = buildSingleProfileUrl('IUYZmFShTXSjbSgQqRQ0JQ', 0);
const url = buildCompareBenchmarkUrl(base, cmp);
// The `profiles[]` parameter should appear twice and the inner URLs
// should be double-encoded (the outer URLSearchParams encoding wrapping
// the from-url URL, which itself contains an encoded task artifact URL).
expect(url).toBe(
'https://deploy-preview-6012--perf-html.netlify.app/compare-benchmark/?' +
'profiles%5B%5D=https%3A%2F%2Fprofiler.firefox.com%2Ffrom-url%2Fhttps%253A%252F%252Ffirefox-ci-tc.services.mozilla.com%252Fapi%252Fqueue%252Fv1%252Ftask%252FCVdpkviXTEyAJH37VBMTGQ%252Fruns%252F0%252Fartifacts%252Fpublic%252Ftest_info%252Fprofile_speedometer3_compact.jslb.gz' +
'&profiles%5B%5D=https%3A%2F%2Fprofiler.firefox.com%2Ffrom-url%2Fhttps%253A%252F%252Ffirefox-ci-tc.services.mozilla.com%252Fapi%252Fqueue%252Fv1%252Ftask%252FIUYZmFShTXSjbSgQqRQ0JQ%252Fruns%252F0%252Fartifacts%252Fpublic%252Ftest_info%252Fprofile_speedometer3_compact.jslb.gz',
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ exports[`Results View The table should match snapshot and other elements should
</div>
</div>
<div
class="fdtnfac f1l733lh"
class="ftvoz88 f1l733lh"
data-testid="table-header"
role="row"
>
Expand Down Expand Up @@ -742,7 +742,7 @@ exports[`Results View The table should match snapshot and other elements should
class="revision-block fw0pvlu"
>
<div
class="revisionRow f1wmgkbg f1dlt78d MuiBox-root css-1woqe0l"
class="revisionRow f1wmgkbg f1dlt78d MuiBox-root css-1v96cc6"
role="row"
>
<div
Expand Down Expand Up @@ -940,7 +940,7 @@ exports[`Results View The table should match snapshot and other elements should
</div>
</div>
<div
class="revisionRow f1wmgkbg f1dlt78d MuiBox-root css-1woqe0l"
class="revisionRow f1wmgkbg f1dlt78d MuiBox-root css-1v96cc6"
role="row"
>
<div
Expand Down Expand Up @@ -1139,7 +1139,7 @@ exports[`Results View The table should match snapshot and other elements should
</div>
</div>
<div
class="revisionRow f1wmgkbg f1dlt78d MuiBox-root css-1woqe0l"
class="revisionRow f1wmgkbg f1dlt78d MuiBox-root css-1v96cc6"
role="row"
>
<div
Expand Down Expand Up @@ -1338,7 +1338,7 @@ exports[`Results View The table should match snapshot and other elements should
</div>
</div>
<div
class="revisionRow f1wmgkbg f1dlt78d MuiBox-root css-1woqe0l"
class="revisionRow f1wmgkbg f1dlt78d MuiBox-root css-1v96cc6"
role="row"
>
<div
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1319,7 +1319,7 @@ exports[`Results Table Should match snapshot 1`] = `
</div>
</div>
<div
class="fdtnfac f1l733lh"
class="ftvoz88 f1l733lh"
data-testid="table-header"
role="row"
>
Expand Down Expand Up @@ -1715,7 +1715,7 @@ exports[`Results Table Should match snapshot 1`] = `
class="revision-block fw0pvlu"
>
<div
class="revisionRow f1wmgkbg f1dlt78d MuiBox-root css-1woqe0l"
class="revisionRow f1wmgkbg f1dlt78d MuiBox-root css-1v96cc6"
role="row"
>
<div
Expand Down Expand Up @@ -1913,7 +1913,7 @@ exports[`Results Table Should match snapshot 1`] = `
</div>
</div>
<div
class="revisionRow f1wmgkbg f1dlt78d MuiBox-root css-1woqe0l"
class="revisionRow f1wmgkbg f1dlt78d MuiBox-root css-1v96cc6"
role="row"
>
<div
Expand Down Expand Up @@ -2112,7 +2112,7 @@ exports[`Results Table Should match snapshot 1`] = `
</div>
</div>
<div
class="revisionRow f1wmgkbg f1dlt78d MuiBox-root css-1woqe0l"
class="revisionRow f1wmgkbg f1dlt78d MuiBox-root css-1v96cc6"
role="row"
>
<div
Expand Down Expand Up @@ -2398,7 +2398,7 @@ exports[`Results Table Should match snapshot 1`] = `
class="revision-block fw0pvlu"
>
<div
class="revisionRow f1wmgkbg f1dlt78d MuiBox-root css-1woqe0l"
class="revisionRow f1wmgkbg f1dlt78d MuiBox-root css-1v96cc6"
role="row"
>
<div
Expand Down Expand Up @@ -4202,7 +4202,7 @@ exports[`Results Table for MannWhitneyResultsItem for mann-whitney-u testVersion
</div>
</div>
<div
class="fdtnfac f1l733lh"
class="ftvoz88 f1l733lh"
data-testid="table-header"
role="row"
>
Expand Down Expand Up @@ -4598,7 +4598,7 @@ exports[`Results Table for MannWhitneyResultsItem for mann-whitney-u testVersion
class="revision-block fw0pvlu"
>
<div
class="revisionRow f1wmgkbg f1dlt78d MuiBox-root css-1woqe0l"
class="revisionRow f1wmgkbg f1dlt78d MuiBox-root css-1v96cc6"
role="row"
>
<div
Expand Down Expand Up @@ -4809,7 +4809,7 @@ exports[`Results Table for MannWhitneyResultsItem for mann-whitney-u testVersion
</div>
</div>
<div
class="revisionRow f1wmgkbg f1dlt78d MuiBox-root css-1woqe0l"
class="revisionRow f1wmgkbg f1dlt78d MuiBox-root css-1v96cc6"
role="row"
>
<div
Expand Down Expand Up @@ -5021,7 +5021,7 @@ exports[`Results Table for MannWhitneyResultsItem for mann-whitney-u testVersion
</div>
</div>
<div
class="revisionRow f1wmgkbg f1dlt78d MuiBox-root css-1woqe0l"
class="revisionRow f1wmgkbg f1dlt78d MuiBox-root css-1v96cc6"
role="row"
>
<div
Expand Down Expand Up @@ -5320,7 +5320,7 @@ exports[`Results Table for MannWhitneyResultsItem for mann-whitney-u testVersion
class="revision-block fw0pvlu"
>
<div
class="revisionRow f1wmgkbg f1dlt78d MuiBox-root css-1woqe0l"
class="revisionRow f1wmgkbg f1dlt78d MuiBox-root css-1v96cc6"
role="row"
>
<div
Expand Down Expand Up @@ -6135,7 +6135,7 @@ exports[`Results Table for MannWhitneyResultsItem for mann-whitney-u testVersion
</span>
</div>
<div
class="revisionRow f1wmgkbg f1dlt78d MuiBox-root css-1woqe0l"
class="revisionRow f1wmgkbg f1dlt78d MuiBox-root css-1v96cc6"
role="row"
>
<div
Expand Down Expand Up @@ -6386,7 +6386,7 @@ exports[`Results Table for MannWhitneyResultsItem for mann-whitney-u testVersion
</span>
</div>
<div
class="revisionRow f1wmgkbg f1dlt78d MuiBox-root css-1woqe0l"
class="revisionRow f1wmgkbg f1dlt78d MuiBox-root css-1v96cc6"
role="row"
>
<div
Expand Down Expand Up @@ -6700,7 +6700,7 @@ exports[`Results Table should render different blocks when rendering several rev
</span>
</div>
<div
class="revisionRow f1wmgkbg f1dlt78d MuiBox-root css-1auw4i3"
class="revisionRow f1wmgkbg f1dlt78d MuiBox-root css-5lef8y"
role="row"
>
<div
Expand Down Expand Up @@ -6946,7 +6946,7 @@ exports[`Results Table should render different blocks when rendering several rev
</span>
</div>
<div
class="revisionRow f1wmgkbg f1dlt78d MuiBox-root css-1auw4i3"
class="revisionRow f1wmgkbg f1dlt78d MuiBox-root css-5lef8y"
role="row"
>
<div
Expand Down
Loading