blob: f393b0d2076b26520f1ca92b1ea346ab2494b41e [file] [edit]
import './index';
import { expect } from 'chai';
import fetchMock from 'fetch-mock';
import { $, $$ } from '../../../infra-sk/modules/dom';
import { setUpElementUnderTest } from '../../../infra-sk/modules/test_util';
import {
ActiveFailureClass,
FailureClass,
flattenFailureClass,
FailureClassesSk,
} from './failure-classes-sk';
describe('failure-classes-sk', () => {
const newInstance = setUpElementUnderTest<FailureClassesSk>('failure-classes-sk');
afterEach(() => {
fetchMock.restore();
});
const testClasses: { failureClass: FailureClass; taskIds: string[] }[] = [
{
failureClass: {
id: 'class-1',
updates: [
{
timestamp: '2026-09-30T10:00:00Z',
user: 'autogardener',
analysis: 'Compiler error in SkCanvas.cpp',
errorMessage: 'SkCanvas.cpp:42: undefined symbol foo',
},
],
},
taskIds: ['task-1', 'task-2'],
},
{
failureClass: {
id: 'class-2',
updates: [
{
timestamp: '2026-09-30T10:00:00Z',
user: 'autogardener',
analysis: 'Timeout running dm on Android GPU',
errorMessage: 'Step dm timed out after 3600s',
},
],
},
taskIds: ['task-3'],
},
];
it('fetches and displays failure classes when taskIds are set', async () => {
fetchMock.postOnce('/json/failure-classes', testClasses);
const el = newInstance();
el.taskIds = ['task-1', 'task-2', 'task-3'];
await fetchMock.flush(true);
const items = $('.failure-class-item', el);
expect(items).to.have.length(2);
expect($$('.analysis', items[0])!.textContent).to.equal('Compiler error in SkCanvas.cpp');
expect($$('.value', items[0])!.textContent!.trim()).to.equal('2');
expect($$('.analysis', items[1])!.textContent).to.equal('Timeout running dm on Android GPU');
expect($$('.value', items[1])!.textContent!.trim()).to.equal('1');
});
it('dispatches highlight-tasks on mouseenter and mouseleave', async () => {
fetchMock.postOnce('/json/failure-classes', testClasses);
const el = newInstance();
el.taskIds = ['task-1', 'task-2', 'task-3'];
await fetchMock.flush(true);
const events: string[][] = [];
el.addEventListener('highlight-tasks', (e: Event) => {
events.push((e as CustomEvent).detail.taskIds);
});
const firstItem = $$('.failure-class-item', el) as HTMLElement;
firstItem.dispatchEvent(new MouseEvent('mouseenter'));
expect(events).to.deep.equal([['task-1', 'task-2']]);
expect(firstItem.classList.contains('hovered')).to.be.true;
firstItem.dispatchEvent(new MouseEvent('mouseleave'));
expect(events).to.deep.equal([['task-1', 'task-2'], []]);
expect(firstItem.classList.contains('hovered')).to.be.false;
});
it('highlights matching class and dispatches highlight-tasks when setHoveredTask is called', async () => {
fetchMock.postOnce('/json/failure-classes', testClasses);
const el = newInstance();
el.taskIds = ['task-1', 'task-2', 'task-3'];
await fetchMock.flush(true);
const events: string[][] = [];
el.addEventListener('highlight-tasks', (e: Event) => {
events.push((e as CustomEvent).detail.taskIds);
});
el.setHoveredTask('task-2', true);
expect(events).to.deep.equal([['task-1', 'task-2']]);
const firstItem = $$('.failure-class-item', el) as HTMLElement;
expect(firstItem.classList.contains('hovered')).to.be.true;
el.setHoveredTask('task-2', false);
expect(events).to.deep.equal([['task-1', 'task-2'], []]);
expect(firstItem.classList.contains('hovered')).to.be.false;
});
it('dispatches select-failure-class when clicked', async () => {
fetchMock.postOnce('/json/failure-classes', testClasses);
const el = newInstance();
el.taskIds = ['task-1', 'task-2', 'task-3'];
await fetchMock.flush(true);
let selected: ActiveFailureClass | null = null;
el.addEventListener('select-failure-class', (e: Event) => {
selected = (e as CustomEvent<ActiveFailureClass>).detail;
});
const firstItem = $$('.failure-class-item', el) as HTMLElement;
firstItem.click();
expect(selected).to.deep.equal({
failureClass: flattenFailureClass(testClasses[0].failureClass),
taskIds: testClasses[0].taskIds,
});
});
it('flattens updates in chronological order', () => {
const flattened = flattenFailureClass({
id: 'class-1',
updates: [
{
timestamp: '2026-09-30T10:00:00Z',
user: 'autogardener',
title: 'Initial title',
analysis: 'Initial analysis',
errorMessage: 'SkCanvas.cpp:42: undefined symbol foo\nextra noise',
flaky: true,
culprits: ['commit-a', 'commit-b'],
resolvedBy: null,
bugs: null,
},
{
timestamp: '2026-09-30T10:05:00Z',
user: 'dev@google.com',
message: 'Not flaky, introduced by commit-b',
title: 'Updated title',
analysis: 'Compiler error in SkCanvas.cpp',
errorMessage: 'SkCanvas.cpp:42: undefined symbol foo',
flaky: false,
culprits: ['commit-b'],
bugs: ['b/12345'],
duplicateOf: 'class-0',
duplicates: ['class-2', 'class-3'],
},
{
timestamp: '2026-09-30T10:15:00Z',
user: 'autogardener',
title: '',
resolvedBy: ['commit-d'],
bugs: [],
duplicateOf: '',
},
],
});
expect(flattened.title).to.equal('');
expect(flattened.analysis).to.equal('Compiler error in SkCanvas.cpp');
expect(flattened.errorMessage).to.equal('SkCanvas.cpp:42: undefined symbol foo');
expect(flattened.flaky).to.be.false;
expect(flattened.culprits).to.deep.equal(['commit-b']);
expect(flattened.resolvedBy).to.deep.equal(['commit-d']);
expect(flattened.bugs).to.deep.equal([]);
expect(flattened.duplicateOf).to.equal('');
expect(flattened.duplicates).to.deep.equal(['class-2', 'class-3']);
});
it('merges duplicate failure classes into canonical class on the client', async () => {
const classesWithDuplicate: { failureClass: FailureClass; taskIds: string[] }[] = [
{
failureClass: {
id: 'class-dup',
updates: [
{
timestamp: '2026-09-30T10:00:00Z',
user: 'autogardener',
analysis: 'Duplicate failure class',
duplicateOf: 'class-canonical',
},
],
},
taskIds: ['task-1', 'task-2'],
},
{
failureClass: {
id: 'class-canonical',
updates: [
{
timestamp: '2026-09-30T10:00:00Z',
user: 'autogardener',
analysis: 'Canonical failure class',
duplicates: ['class-dup'],
},
],
},
taskIds: ['task-3'],
},
];
fetchMock.postOnce('/json/failure-classes', classesWithDuplicate);
const el = newInstance();
el.taskIds = ['task-1', 'task-2', 'task-3'];
await fetchMock.flush(true);
const items = $('.failure-class-item', el);
expect(items).to.have.length(1);
expect($$('.analysis', items[0])!.textContent).to.equal('Canonical failure class');
expect($$('.value', items[0])!.textContent!.trim()).to.equal('3');
const events: string[][] = [];
el.addEventListener('highlight-tasks', (e: Event) => {
events.push((e as CustomEvent).detail.taskIds);
});
el.setHoveredTask('task-1', true);
expect(events).to.deep.equal([['task-1', 'task-2', 'task-3']]);
});
});