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
134 changes: 134 additions & 0 deletions tests/ui/integration/job-view/failure_summary.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/**
* Integration tests for the Failure Summary tab: bug suggestions
* rendered for a failed job, and classifying a job from a suggested
* bug through the pinboard.
*
* The bug suggestions themselves come from the bug_suggestions.json
* fixture; generating them is backend logic covered by pytest
* (tests/model/test_error_summary.py). These tests cover the wiring:
* selected job -> failure summary tab -> pinboard -> save.
*/

const { test, expect } = require('@playwright/test');

const {
mockJobsViewApi,
seedLoggedInSession,
BUILD_JOB,
SHERIFF_USER,
BUG_SUGGESTIONS,
} = require('./mockJobsApi');

// The open, recent bug on the first suggestion in bug_suggestions.json.
const SUGGESTED_BUG = BUG_SUGGESTIONS[0].bugs.open_recent[0];

const selectBuildJob = async (page) => {
await page.getByTestId('job-btn').filter({ hasText: /^B$/ }).first().click();
await expect(
page.locator('#push-list .job-btn.selected-job').first(),
).toBeVisible();
};

// The pin button on the suggested bug's row in the failure summary.
const suggestedBugPinButton = (page, bugId) =>
page
.getByTestId('bug-list-item')
.filter({ hasText: `bug ${bugId}` })
.first()
.getByTitle('Add to list of bugs to associate with all pinned jobs');

test.describe('Failure summary tab', () => {
test('shows bug suggestions for a failed job', async ({ page }) => {
await mockJobsViewApi(page, { bugSuggestions: BUG_SUGGESTIONS });
await page.goto('/jobs?repo=autoland');
await selectBuildJob(page);

// The failure summary is the default tab for a failed job.
await expect(
page.getByRole('tab', { name: 'Failure Summary', selected: true }),
).toBeVisible();

// The failure line and its suggested bug are rendered.
const detailsPanel = page.locator('#details-panel');
await expect(detailsPanel).toContainText(BUG_SUGGESTIONS[0].search);
await expect(
detailsPanel.getByRole('link', {
name: new RegExp(`bug ${SUGGESTED_BUG.id}`),
}),
).toBeVisible();
});

test('pinning a suggested bug pins the job with the bug attached', async ({
page,
}) => {
await mockJobsViewApi(page, { bugSuggestions: BUG_SUGGESTIONS });
await page.goto('/jobs?repo=autoland');
await selectBuildJob(page);

await suggestedBugPinButton(page, SUGGESTED_BUG.id).click();

const pinboard = page.locator('#pinboard-panel');
await expect(pinboard).toBeVisible();
await expect(pinboard.locator('.pinned-job')).toHaveText('B');
await expect(
page.getByTestId(`pinboard-bug-${SUGGESTED_BUG.id}`),
).toBeVisible();
});

test('a logged-in user can classify from a suggested bug', async ({
page,
}) => {
await seedLoggedInSession(page);
const captured = await mockJobsViewApi(page, {
user: [SHERIFF_USER],
bugSuggestions: BUG_SUGGESTIONS,
});
await page.goto('/jobs?repo=autoland');
await expect(page.locator('#th-global-navbar')).toContainText(
'Sheriff Tester',
);

await selectBuildJob(page);
await suggestedBugPinButton(page, SUGGESTED_BUG.id).click();

const pinboard = page.locator('#pinboard-panel');
await expect(
page.getByTestId(`pinboard-bug-${SUGGESTED_BUG.id}`),
).toBeVisible();

await pinboard.locator('.save-btn').click();

await expect
.poll(() => captured.notes.length, { message: 'note POST sent' })
.toBe(1);
expect(captured.notes[0]).toMatchObject({
job_id: BUILD_JOB.id,
failure_classification_id: 4,
});

await expect
.poll(() => captured.bugJobMaps.length, {
message: 'bug-job-map POST sent',
})
.toBe(1);
expect(captured.bugJobMaps[0]).toMatchObject({
job_id: BUILD_JOB.id,
bug_id: SUGGESTED_BUG.id,
type: 'annotation',
});

// The pinboard is cleared after a successful save.
await expect(pinboard.locator('.pinned-job')).toHaveCount(0);

// The job now renders as classified in the push list: its button
// on its own push gains the star icon and the classified marker.
// (The star svg's <title> adds "classified" to the button text,
// so a /^B$/ text filter would miss the classified button.)
const classifiedB = page
.getByTestId(`push-${BUILD_JOB.push_id}`)
.locator('[data-testid="job-btn"][data-classified="true"]')
.filter({ hasText: /^B/ });
await expect(classifiedB).toHaveCount(1);
await expect(classifiedB.locator('.classified-icon')).toBeVisible();
});
});
138 changes: 138 additions & 0 deletions tests/ui/integration/job-view/job_selection.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/**
* Integration tests for job selection in the Jobs view: deep links,
* keyboard navigation between jobs, and clearing the selection.
*
* Job selection is URL-first (the selectedTaskRun param is the source
* of truth), so these tests assert both the visible selection and the
* URL staying in sync — a recurring regression area.
*/

const { test, expect } = require('@playwright/test');

const {
mockJobsViewApi,
jobBySymbol,
taskRunStr,
BUILD_JOB,
} = require('./mockJobsApi');

// The two unclassified failures on each push: B (busted) and Meh
// (testfailed). Cpp is testfailed but already classified.
const MEH_JOB = jobBySymbol('Meh');

// The selected job's button on the push list. The same fixture job
// list is served for every push, so a selected job lights up its copy
// on each push — assert on the first copy.
const selectedJob = (page) =>
page.locator('#push-list .job-btn.selected-job').first();

const clickJob = (page, symbol) =>
page
.getByTestId('job-btn')
.filter({ hasText: new RegExp(`^${symbol}$`) })
.first()
.click();

test.describe('Job selection', () => {
test('deep link with selectedTaskRun selects the job on load', async ({
page,
}) => {
await mockJobsViewApi(page);
await page.goto(
`/jobs?repo=autoland&selectedTaskRun=${taskRunStr(BUILD_JOB)}`,
);

await expect(selectedJob(page)).toBeVisible();

const detailsPanel = page.locator('#details-panel');
await expect(detailsPanel).toBeVisible();
await expect(detailsPanel).toContainText(BUILD_JOB.job_type_name);
});

test.describe('with the default view loaded', () => {
test.beforeEach(async ({ page }) => {
await mockJobsViewApi(page);
await page.goto('/jobs?repo=autoland');
await expect(page.getByTestId('push-header').first()).toBeVisible();
// Keyboard navigation needs the job buttons rendered.
await expect(page.getByTestId('job-btn').first()).toBeVisible();
});

test('"n" and "p" step through unclassified failures and update the URL', async ({
page,
}) => {
// First "n" selects the first unclassified failure: the busted B job.
await page.keyboard.press('n');
await expect(selectedJob(page)).toHaveText('B');
await expect(page).toHaveURL(
new RegExp(`selectedTaskRun=${taskRunStr(BUILD_JOB)}`),
);

// Next unclassified failure on the same push is the Meh job
// (Cpp is skipped because it is already classified).
await page.keyboard.press('n');
await expect(selectedJob(page)).toHaveText('Meh');
await expect(page).toHaveURL(
new RegExp(`selectedTaskRun=${taskRunStr(MEH_JOB)}`),
);

// "p" steps back to the previous unclassified failure.
await page.keyboard.press('p');
await expect(selectedJob(page)).toHaveText('B');
await expect(page).toHaveURL(
new RegExp(`selectedTaskRun=${taskRunStr(BUILD_JOB)}`),
);
});

test('arrow keys step through all jobs regardless of status', async ({
page,
}) => {
await clickJob(page, 'B');
await expect(selectedJob(page)).toHaveText('B');

// Right arrow moves to the next job of any result status — the
// already-classified Cpp job, which "n"/"p" would skip.
await page.keyboard.press('ArrowRight');
await expect(selectedJob(page)).toHaveText(/^Cpp/);

await page.keyboard.press('ArrowLeft');
await expect(selectedJob(page)).toHaveText('B');
});

test('escape clears the selected job and the URL param', async ({
page,
}) => {
await clickJob(page, 'B');
await expect(selectedJob(page)).toBeVisible();
await expect(page).toHaveURL(/selectedTaskRun=/);

await page.keyboard.press('Escape');

await expect(
page.locator('#push-list .job-btn.selected-job'),
).toHaveCount(0);
await expect(page).not.toHaveURL(/selectedTaskRun=/);
});

test('browser back after selecting a job clears the selection', async ({
page,
}) => {
await clickJob(page, 'B');
await expect(selectedJob(page)).toBeVisible();
await expect(page).toHaveURL(/selectedTaskRun=/);

// Selecting a job can push more than one (identical) history
// entry, so step back until we reach the pre-selection entry.
let backSteps = 0;
do {
await page.goBack();
backSteps += 1;
} while (/selectedTaskRun=/.test(page.url()) && backSteps < 5);

await expect(page).not.toHaveURL(/selectedTaskRun=/);
await expect(
page.locator('#push-list .job-btn.selected-job'),
).toHaveCount(0);
});
});
});
Loading