| import './index'; |
| import sinon from 'sinon'; |
| import { assert } from 'chai'; |
| import { AnomaliesTableSk } from './anomalies-table-sk'; |
| |
| import { setUpElementUnderTest } from '../../../infra-sk/modules/test_util'; |
| import { Anomaly } from '../json'; |
| import fetchMock from 'fetch-mock'; |
| |
| describe('anomalies-table-sk', () => { |
| const newInstance = setUpElementUnderTest<AnomaliesTableSk>('anomalies-table-sk'); |
| fetchMock.config.overwriteRoutes = false; |
| |
| let element: AnomaliesTableSk; |
| beforeEach(() => { |
| window.perf = { |
| dev_mode: false, |
| instance_url: '', |
| instance_name: 'chrome-perf-test', |
| header_image_url: '', |
| commit_range_url: 'http://example.com/range/{begin}/{end}', |
| key_order: ['config'], |
| demo: true, |
| radius: 7, |
| num_shift: 10, |
| interesting: 25, |
| step_up_only: false, |
| display_group_by: true, |
| hide_list_of_commits_on_explore: false, |
| notifications: 'none', |
| fetch_chrome_perf_anomalies: false, |
| // TODO(b/454590264) For now, sql anomalies are disabled. Change to true in the future. |
| fetch_anomalies_from_sql: false, |
| feedback_url: '', |
| chat_url: '', |
| help_url_override: '', |
| trace_format: '', |
| need_alert_action: false, |
| bug_host_url: 'https://example.bug.url', |
| git_repo_url: '', |
| keys_for_commit_range: [], |
| keys_for_useful_links: [], |
| skip_commit_detail_display: false, |
| image_tag: 'fake-tag', |
| remove_default_stat_value: false, |
| enable_skia_bridge_aggregation: false, |
| show_json_file_display: false, |
| always_show_commit_info: false, |
| show_triage_link: true, |
| show_bisect_btn: true, |
| app_version: 'test-version', |
| enable_v2_ui: false, |
| }; |
| |
| fetchMock.post('begin:/_/anomalies/group_report', { |
| sid: 'test_sid', |
| timerange_map: { |
| '123': { |
| begin: 100, |
| end: 200, |
| }, |
| }, |
| }); |
| |
| element = newInstance(); |
| }); |
| |
| afterEach(() => { |
| // TODO(b/454590264) For now, sql anomalies are disabled. Change to true in the future. |
| window.perf.fetch_anomalies_from_sql = false; |
| fetchMock.restore(); |
| sinon.restore(); |
| }); |
| |
| const dummyAnomaly = ( |
| id: string, |
| bugId: number, |
| start: number, |
| end: number, |
| testPath: string |
| ): Anomaly => ({ |
| id: id, |
| test_path: testPath, |
| bug_id: bugId, |
| start_revision: start, |
| end_revision: end, |
| is_improvement: false, |
| recovered: true, |
| state: '', |
| statistic: '', |
| units: '', |
| degrees_of_freedom: 0, |
| median_before_anomaly: 75.209091, |
| median_after_anomaly: 100.5023, |
| p_value: 0, |
| segment_size_after: 0, |
| segment_size_before: 0, |
| std_dev_before_anomaly: 0, |
| t_statistic: 0, |
| subscription_name: '', |
| bug_component: 'Test>Component', |
| bug_labels: ['TestLabel1', 'TestLabel2'], |
| bug_cc_emails: [], |
| bisect_ids: [], |
| }); |
| |
| describe('populate table', () => { |
| it('populates the table with anomalies', async () => { |
| const anomalies = [dummyAnomaly('1', 12345, 100, 200, 'master/bot/suite/test')]; |
| // Mock shortcut update call to prevent console errors in test |
| fetchMock.post('/_/shortcut/update', { id: 'test_shortcut' }); |
| await element.populateTable(anomalies); |
| assert.equal(element.anomalyList.length, 1); |
| }); |
| }); |
| |
| describe('open report', () => { |
| it('opens a new window with the correct url', async () => { |
| const spy = sinon.spy(window, 'open'); |
| const anomalies = [dummyAnomaly('1', 12345, 100, 200, 'master/bot/suite/test')]; |
| fetchMock.post('/_/shortcut/update', { id: 'test_shortcut' }); |
| |
| await element.populateTable(anomalies); |
| await element.checkSelectedAnomalies(anomalies); |
| await element.openReport(); |
| sinon.assert.calledOnce(spy); |
| sinon.assert.calledWith(spy, '/u/?anomalyIDs=1', '_blank'); |
| }); |
| }); |
| |
| describe('open anomaly group report page', () => { |
| it('navigates to anomaly group report page when sql anoms are disabled', async () => { |
| const openSpy = sinon.spy(window, 'open'); |
| fetchMock.post('/_/shortcut/update', { id: 'test_shortcut' }); |
| |
| const anomalies = [ |
| dummyAnomaly('1', 12345, 100, 200, 'master/bot/suite/test1'), |
| dummyAnomaly('2', 12345, 150, 250, 'master/bot/suite/test2'), |
| dummyAnomaly('3', 0, 300, 400, 'master/bot/suite/test3'), |
| dummyAnomaly('4', 0, 350, 450, 'master/bot/suite/test4'), |
| dummyAnomaly('5', 0, 500, 600, 'master/bot/suite2/test5'), |
| dummyAnomaly('6', 0, 700, 800, 'master/bot/suite2/test6'), |
| ]; |
| element.anomalyList = anomalies; |
| await element.populateTable(anomalies); |
| |
| for (const group of element.anomalyGroups) { |
| const summaryRowCheckboxId = element.getGroupId(group); |
| const groupCheckbox = element.querySelector<HTMLInputElement>( |
| `input[id^="anomaly-row-"][id$="-${summaryRowCheckboxId}"]` |
| ); |
| groupCheckbox!.checked = true; |
| } |
| |
| await element.openAnomalyGroupReportPage(); |
| |
| assert.isTrue(openSpy.calledWith('/u/?sid=test_sid', '_blank')); |
| assert.isTrue(openSpy.calledThrice); |
| }); |
| |
| it('navigates to anomaly group report page when sql anomalies are enabled', async () => { |
| window.perf.fetch_anomalies_from_sql = true; |
| |
| const openSpy = sinon.spy(window, 'open'); |
| fetchMock.post('/_/shortcut/update', { id: 'test_shortcut' }); |
| |
| const anomalies = [ |
| dummyAnomaly('1', 12345, 100, 200, 'master/bot/suite/test1'), |
| dummyAnomaly('2', 12345, 150, 250, 'master/bot/suite/test2'), |
| dummyAnomaly('3', 0, 300, 400, 'master/bot/suite/test3'), |
| dummyAnomaly('4', 0, 350, 450, 'master/bot/suite/test4'), |
| dummyAnomaly('5', 0, 500, 600, 'master/bot/suite2/test5'), |
| dummyAnomaly('6', 0, 700, 800, 'master/bot/suite2/test6'), |
| ]; |
| element.anomalyList = anomalies; |
| await element.populateTable(anomalies); |
| |
| for (const group of element.anomalyGroups) { |
| const summaryRowCheckboxId = element.getGroupId(group); |
| const groupCheckbox = element.querySelector<HTMLInputElement>( |
| `input[id^="anomaly-row-"][id$="-${summaryRowCheckboxId}"]` |
| ); |
| groupCheckbox!.checked = true; |
| } |
| |
| await element.openAnomalyGroupReportPage(); |
| |
| assert.isTrue(openSpy.calledWith(`/u/?anomalyIDs=${encodeURIComponent('1,2')}`, '_blank')); |
| assert.isTrue(openSpy.calledWith(`/u/?anomalyIDs=${encodeURIComponent('3,4')}`, '_blank')); |
| assert.isTrue(openSpy.calledWith(`/u/?anomalyIDs=${encodeURIComponent('5,6')}`, '_blank')); |
| assert.isTrue(openSpy.calledThrice); |
| }); |
| |
| it('opens single anomaly group with anomaly id', async () => { |
| const openSpy = sinon.spy(window, 'open'); |
| fetchMock.post('/_/shortcut/update', { id: 'test_shortcut' }); |
| |
| const anomalies = [dummyAnomaly('1', 12345, 100, 200, 'master/bot/suite/test1')]; |
| element.anomalyList = anomalies; |
| await element.populateTable(anomalies); |
| |
| const anomalyCheckbox = element.querySelector<HTMLInputElement>( |
| `input[id^="anomaly-row-"][id$="-1"]` |
| ); |
| anomalyCheckbox!.checked = true; |
| |
| await element.openAnomalyGroupReportPage(); |
| |
| assert.isTrue(openSpy.calledWith('/u/?anomalyIDs=1', '_blank')); |
| assert.isTrue(openSpy.calledOnce); |
| }); |
| |
| it('opens multi anomaly group with sid when sql anoms are disabled', async () => { |
| const openSpy = sinon.spy(window, 'open'); |
| fetchMock.post('/_/shortcut/update', { id: 'test_shortcut' }); |
| |
| const anomalies = [ |
| dummyAnomaly('1', 12345, 100, 200, 'master/bot/suite/test1'), |
| dummyAnomaly('2', 12345, 150, 250, 'master/bot/suite/test2'), |
| ]; |
| element.anomalyList = anomalies; |
| await element.populateTable(anomalies); |
| |
| const group = element.anomalyGroups[0]; |
| const summaryRowCheckboxId = element.getGroupId(group); |
| const groupCheckbox = element.querySelector<HTMLInputElement>( |
| `input[id^="anomaly-row-"][id$="-${summaryRowCheckboxId}"]` |
| ); |
| groupCheckbox!.checked = true; |
| |
| await element.openAnomalyGroupReportPage(); |
| |
| assert.isTrue(openSpy.calledWith('/u/?sid=test_sid', '_blank')); |
| assert.isTrue(openSpy.calledOnce); |
| }); |
| |
| it('opens short multi anomaly group with anomalyIDs using sql anomalies', async () => { |
| window.perf.fetch_anomalies_from_sql = true; |
| |
| const openSpy = sinon.spy(window, 'open'); |
| fetchMock.post('/_/shortcut/update', { id: 'test_shortcut' }); |
| |
| const anomalies = [ |
| dummyAnomaly('1', 12345, 100, 200, 'master/bot/suite/test1'), |
| dummyAnomaly('2', 12345, 150, 250, 'master/bot/suite/test2'), |
| ]; |
| element.anomalyList = anomalies; |
| await element.populateTable(anomalies); |
| |
| const group = element.anomalyGroups[0]; |
| const summaryRowCheckboxId = element.getGroupId(group); |
| const groupCheckbox = element.querySelector<HTMLInputElement>( |
| `input[id^="anomaly-row-"][id$="-${summaryRowCheckboxId}"]` |
| ); |
| groupCheckbox!.checked = true; |
| |
| await element.openAnomalyGroupReportPage(); |
| |
| assert.isTrue(openSpy.calledWith(`/u/?anomalyIDs=${encodeURIComponent('1,2')}`, '_blank')); |
| assert.isTrue(openSpy.calledOnce); |
| }); |
| |
| it('opens both single and multi anomaly groups when sql anoms are disabled', async () => { |
| const openSpy = sinon.spy(window, 'open'); |
| fetchMock.post('/_/shortcut/update', { id: 'test_shortcut' }); |
| |
| const anomalies = [ |
| dummyAnomaly('1', 12345, 100, 200, 'master/bot/suite/test1'), |
| dummyAnomaly('2', 12345, 150, 250, 'master/bot/suite/test2'), |
| dummyAnomaly('3', 54321, 100, 200, 'master/bot/suite/test3'), |
| ]; |
| element.anomalyList = anomalies; |
| await element.populateTable(anomalies); |
| |
| // Check multi-anomaly group. |
| const multiAnomalyGroup = element.anomalyGroups.find((g) => g.anomalies.length > 1)!; |
| const summaryRowCheckboxId = element.getGroupId(multiAnomalyGroup); |
| const groupCheckbox = element.querySelector<HTMLInputElement>( |
| `input[id^="anomaly-row-"][id$="-${summaryRowCheckboxId}"]` |
| )!; |
| groupCheckbox.checked = true; |
| |
| // Check single-anomaly group. |
| const singleAnomalyCheckbox = element.querySelector<HTMLInputElement>( |
| `input[id^="anomaly-row-"][id$="-3"]` |
| )!; |
| singleAnomalyCheckbox.checked = true; |
| |
| await element.openAnomalyGroupReportPage(); |
| |
| assert.isTrue(openSpy.calledTwice); |
| assert.isTrue(openSpy.calledWith('/u/?sid=test_sid', '_blank')); |
| assert.isTrue(openSpy.calledWith('/u/?anomalyIDs=3', '_blank')); |
| }); |
| |
| it('opens both single and multi anomaly groups using sql anomalies', async () => { |
| window.perf.fetch_anomalies_from_sql = true; |
| |
| const openSpy = sinon.spy(window, 'open'); |
| fetchMock.post('/_/shortcut/update', { id: 'test_shortcut' }); |
| |
| const anomalies = [ |
| dummyAnomaly('1', 12345, 100, 200, 'master/bot/suite/test1'), |
| dummyAnomaly('2', 12345, 150, 250, 'master/bot/suite/test2'), |
| dummyAnomaly('3', 54321, 100, 200, 'master/bot/suite/test3'), |
| ]; |
| element.anomalyList = anomalies; |
| await element.populateTable(anomalies); |
| |
| // Check multi-anomaly group. |
| const multiAnomalyGroup = element.anomalyGroups.find((g) => g.anomalies.length > 1)!; |
| const summaryRowCheckboxId = element.getGroupId(multiAnomalyGroup); |
| const groupCheckbox = element.querySelector<HTMLInputElement>( |
| `input[id^="anomaly-row-"][id$="-${summaryRowCheckboxId}"]` |
| )!; |
| groupCheckbox.checked = true; |
| |
| // Check single-anomaly group. |
| const singleAnomalyCheckbox = element.querySelector<HTMLInputElement>( |
| `input[id^="anomaly-row-"][id$="-3"]` |
| )!; |
| singleAnomalyCheckbox.checked = true; |
| |
| await element.openAnomalyGroupReportPage(); |
| |
| assert.isTrue(openSpy.calledTwice); |
| assert.isTrue(openSpy.calledWith(`/u/?anomalyIDs=${encodeURIComponent('1,2')}`, '_blank')); |
| assert.isTrue(openSpy.calledWith('/u/?anomalyIDs=3', '_blank')); |
| }); |
| }); |
| |
| describe('toggle popup', () => { |
| it('toggles the showPopup property', () => { |
| assert.isFalse(element.showPopup); |
| element.togglePopup(); |
| assert.isTrue(element.showPopup); |
| element.togglePopup(); |
| assert.isFalse(element.showPopup); |
| }); |
| }); |
| |
| describe('group anomalies', () => { |
| it('groups anomalies by bug id, revision overlap, and benchmark', () => { |
| const anomalies = [ |
| dummyAnomaly('1', 12345, 100, 200, 'master/bot/suite/test1'), |
| dummyAnomaly('2', 12345, 150, 250, 'master/bot/suite/test2'), |
| dummyAnomaly('3', 0, 300, 400, 'master/bot/suite/test3'), |
| dummyAnomaly('4', 0, 350, 450, 'master/bot/suite/test4'), |
| dummyAnomaly('5', 0, 500, 600, 'master/bot/suite2/test5'), |
| dummyAnomaly('6', 0, 700, 800, 'master/bot/suite2/test6'), |
| ]; |
| element.anomalyList = anomalies; |
| element.groupAnomalies(); |
| assert.lengthOf(element.anomalyGroups, 3); |
| assert.lengthOf(element.anomalyGroups[0].anomalies, 2); |
| assert.lengthOf(element.anomalyGroups[1].anomalies, 2); |
| assert.lengthOf(element.anomalyGroups[2].anomalies, 2); |
| }); |
| }); |
| |
| describe('do ranges overlap', () => { |
| it('returns true if ranges overlap', () => { |
| const a = dummyAnomaly('1', 0, 100, 200, ''); |
| const b = dummyAnomaly('2', 0, 150, 250, ''); |
| assert.isTrue(element.doRangesOverlap(a, b)); |
| }); |
| |
| it('returns false if ranges do not overlap', () => { |
| const a = dummyAnomaly('1', 0, 100, 200, ''); |
| const b = dummyAnomaly('2', 0, 300, 400, ''); |
| assert.isFalse(element.doRangesOverlap(a, b)); |
| }); |
| }); |
| |
| describe('is same revision', () => { |
| it('returns true if revisions are the same', () => { |
| const a = dummyAnomaly('1', 0, 100, 200, ''); |
| const b = dummyAnomaly('2', 0, 100, 200, ''); |
| assert.isTrue(element.isSameRevision(a, b)); |
| }); |
| |
| it('returns false if revisions are different', () => { |
| const a = dummyAnomaly('1', 0, 100, 200, ''); |
| const b = dummyAnomaly('2', 0, 100, 201, ''); |
| assert.isFalse(element.isSameRevision(a, b)); |
| }); |
| }); |
| |
| describe('is same benchmark', () => { |
| it('returns true if benchmarks are the same', () => { |
| const a = dummyAnomaly('1', 0, 0, 0, 'master/bot/suite/test1'); |
| const b = dummyAnomaly('2', 0, 0, 0, 'master/bot/suite/test2'); |
| assert.isTrue(element.isSameBenchmark(a, b)); |
| }); |
| |
| it('returns false if benchmarks are different', () => { |
| const a = dummyAnomaly('1', 0, 0, 0, 'master/bot/suite1/test1'); |
| const b = dummyAnomaly('2', 0, 0, 0, 'master/bot/suite2/test2'); |
| assert.isFalse(element.isSameBenchmark(a, b)); |
| }); |
| }); |
| |
| describe('find longest sub test path', () => { |
| it('returns the longest common sub test path', () => { |
| const anomalies = [ |
| dummyAnomaly('1', 0, 0, 0, 'master/bot/suite/test1/sub1'), |
| dummyAnomaly('2', 0, 0, 0, 'master/bot/suite/test1/sub2'), |
| ]; |
| assert.equal(element.findLongestSubTestPath(anomalies), 'test1/sub*'); |
| }); |
| }); |
| |
| describe('get report link for bug id', () => { |
| it('returns a link for a valid bug id', () => { |
| const link = element.getReportLinkForBugId(12345); |
| assert.isDefined(link); |
| }); |
| |
| it('returns an empty template for bug id 0', () => { |
| const link = element.getReportLinkForBugId(0); |
| assert.equal(link.strings.at(0), ''); |
| }); |
| |
| it('returns a message for bug id -1', () => { |
| const link = element.getReportLinkForBugId(-1); |
| assert.equal(link.strings[0], 'Invalid Alert'); |
| }); |
| |
| it('returns a message for bug id -2', () => { |
| const link = element.getReportLinkForBugId(-2); |
| assert.equal(link.strings[0], 'Ignored Alert'); |
| }); |
| }); |
| |
| describe('get report link for summary row bug id', () => { |
| it('returns the first anomaly with a bug id', () => { |
| const anomalyWithBug = dummyAnomaly('1', 12345, 0, 0, ''); |
| const anomalyWithoutBug = dummyAnomaly('2', 0, 0, 0, ''); |
| const group = { anomalies: [anomalyWithoutBug, anomalyWithBug], expanded: false }; |
| const anomaly = element.getReportLinkForSummaryRowBugId(group); |
| assert.deepEqual(anomaly, anomalyWithBug); |
| }); |
| |
| it('returns undefined if no anomalies have a bug id', () => { |
| const group = { anomalies: [dummyAnomaly('1', 0, 0, 0, '')], expanded: false }; |
| const anomaly = element.getReportLinkForSummaryRowBugId(group); |
| assert.isUndefined(anomaly); |
| }); |
| }); |
| |
| describe('get row class', () => { |
| it('returns the correct class for an expanded parent row', () => { |
| const group = { anomalies: [dummyAnomaly('1', 0, 0, 0, '')], expanded: true }; |
| const rowClass = element.getRowClass(0, group); |
| assert.equal(rowClass, 'parent-expanded-row'); |
| }); |
| |
| it('returns the correct class for an expanded child row', () => { |
| const group = { anomalies: [dummyAnomaly('1', 0, 0, 0, '')], expanded: true }; |
| const rowClass = element.getRowClass(1, group); |
| assert.equal(rowClass, 'child-expanded-row'); |
| }); |
| }); |
| |
| describe('expand group', () => { |
| it('toggles the expanded property of a group', () => { |
| const group = { anomalies: [], expanded: false }; |
| element.expandGroup(group); |
| assert.isTrue(group.expanded); |
| element.expandGroup(group); |
| assert.isFalse(group.expanded); |
| }); |
| }); |
| |
| describe('compute revision range', () => { |
| it('returns the correct range string', () => { |
| assert.equal(element.computeRevisionRange(100, 200), '100 - 200'); |
| }); |
| |
| it('returns a single number if start and end are the same', () => { |
| assert.equal(element.computeRevisionRange(100, 100), '100'); |
| }); |
| |
| it('returns an empty string if start or end is null', () => { |
| assert.equal(element.computeRevisionRange(null, 100), ''); |
| assert.equal(element.computeRevisionRange(100, null), ''); |
| }); |
| }); |
| |
| describe('check selected anomalies', () => { |
| it('checks the checkboxes for the given anomalies', async () => { |
| const anomalies = [dummyAnomaly('1', 12345, 100, 200, 'master/bot/suite/test')]; |
| // Mock shortcut update call to prevent console errors in test |
| fetchMock.post('/_/shortcut/update', { id: 'test_shortcut' }); |
| await element.populateTable(anomalies); |
| await element.checkSelectedAnomalies(anomalies); |
| await fetchMock.flush(true); |
| const checkbox = element.querySelector('[id^="anomaly-row-"][id$="-1"]') as HTMLInputElement; |
| assert.isTrue(checkbox.checked); |
| }); |
| }); |
| |
| describe('toggle children checkboxes', () => { |
| it('toggles the checkboxes of all children in a group', async () => { |
| const anomalies = [ |
| dummyAnomaly('1', 0, 100, 200, 'master/bot/suite/test1'), |
| dummyAnomaly('2', 0, 100, 200, 'master/bot/suite/test2'), |
| ]; |
| // Mock shortcut update call to prevent console errors in test |
| fetchMock.post('/_/shortcut/update', { id: 'test_shortcut' }); |
| await fetchMock.flush(true); |
| await element.populateTable(anomalies); |
| const group = element.anomalyGroups[0]; |
| const suummarycheckboxid = element.getGroupId(group); |
| const summarycheckbox = element.querySelector( |
| `input[id^="anomaly-row-"][id$="-${suummarycheckboxid}"]` |
| ) as HTMLInputElement; |
| summarycheckbox.checked = true; |
| element.toggleChildrenCheckboxes(group); |
| |
| const checkbox1 = element.querySelector('[id^="anomaly-row-"][id$="-1"]') as HTMLInputElement; |
| const checkbox2 = element.querySelector('[id^="anomaly-row-"][id$="-2"]') as HTMLInputElement; |
| assert.isTrue(checkbox1.checked); |
| assert.isTrue(checkbox2.checked); |
| }); |
| }); |
| |
| describe('toggle all checkboxes', () => { |
| it('toggles all checkboxes in the table', async () => { |
| const anomalies = [ |
| dummyAnomaly('1', 0, 100, 200, 'master/bot/suite/test1'), |
| dummyAnomaly('2', 0, 100, 200, 'master/bot/suite/test2'), |
| ]; |
| // Mock shortcut update call to prevent console errors in test |
| fetchMock.post('/_/shortcut/update', { id: 'test_shortcut' }); |
| await fetchMock.flush(true); |
| await element.populateTable(anomalies); |
| |
| const headerCheckbox = element.querySelector('[id^="header-checkbox-"]') as HTMLInputElement; |
| headerCheckbox.checked = true; |
| element.toggleAllCheckboxes(); |
| const checkbox1 = element.querySelector('[id^="anomaly-row-"][id$="-1"]') as HTMLInputElement; |
| const checkbox2 = element.querySelector('[id^="anomaly-row-"][id$="-2"]') as HTMLInputElement; |
| assert.isTrue(checkbox1.checked); |
| assert.isTrue(checkbox2.checked); |
| }); |
| }); |
| |
| describe('open multi graph url', () => { |
| it('fetches the url if it does not exist in the map', async () => { |
| // A stub is better here because we can control the return value. |
| const mockTab = { |
| document: { write: () => {} }, |
| location: { href: '' }, |
| }; |
| const openStub = sinon.stub(window, 'open').returns(mockTab as any); |
| |
| const anomaly = dummyAnomaly('123', 0, 0, 0, 'master/bot/suite/test'); |
| fetchMock.post('/_/shortcut/update', { id: 'test_shortcut' }); |
| await fetchMock.flush(true); |
| |
| window.history.pushState({}, '', '/a/'); |
| |
| // CORRECT: Call the function with both arguments. |
| // The call to window.open() is now conceptually part of the "user action" |
| // that the test is simulating. |
| const newTab = window.open('', '_blank'); |
| await element.openMultiGraphUrl(anomaly, newTab); |
| |
| // Assert that window.open was called as expected. |
| assert.isTrue(openStub.calledOnce); |
| |
| // Assert that the tab was navigated to the correct URL. |
| |
| const weekInSeconds = 604800; // 7 * 24 * 60 * 60 |
| const expectedBegin = 100 - weekInSeconds; |
| const expectedEnd = 200 + weekInSeconds; |
| const expectedUrl = `/m/?begin=${expectedBegin}&end=${expectedEnd}&request_type=0&shortcut=test_shortcut&totalGraphs=1`; |
| assert.include(mockTab.location.href, expectedUrl); |
| }); |
| }); |
| |
| describe('generate summary row', () => { |
| it('correctly summarizes a group of anomalies', async () => { |
| const anomalies = [ |
| dummyAnomaly('1', 12345, 100, 200, 'master/bot/suite/test1/sub1'), |
| dummyAnomaly('2', 12345, 150, 250, 'master/bot/suite/test1/sub2'), |
| dummyAnomaly('3', 12345, 120, 220, 'master/bot/suite/test2/sub1'), |
| ]; |
| anomalies[0].is_improvement = true; |
| anomalies[0].median_before_anomaly = 100; |
| anomalies[0].median_after_anomaly = 150; // +50% improvement |
| anomalies[1].is_improvement = false; |
| anomalies[1].median_before_anomaly = 100; |
| anomalies[1].median_after_anomaly = 80; // -20% regression |
| anomalies[2].is_improvement = false; |
| anomalies[2].median_before_anomaly = 100; |
| anomalies[2].median_after_anomaly = 90; // -10% regression |
| |
| await element.populateTable(anomalies); |
| |
| const summaryRow = element.querySelector('tr[data-bugid="12345"]'); |
| assert.isNotNull(summaryRow); |
| |
| const cells = summaryRow!.querySelectorAll('td'); |
| assert.equal(cells[5].textContent?.trim(), 'bot'); |
| assert.equal(cells[6].textContent?.trim(), 'suite'); |
| assert.equal(cells[7].textContent?.trim(), 'test*'); |
| assert.equal(cells[8].textContent?.trim(), '-20%'); |
| assert.include(cells[8].className, 'regression'); |
| }); |
| }); |
| |
| describe('get checked anomalies', () => { |
| it('returns the currently checked anomalies', async () => { |
| const anomalies = [dummyAnomaly('1', 12345, 100, 200, 'master/bot/suite/test')]; |
| await element.populateTable(anomalies); |
| element.checkSelectedAnomalies(anomalies); |
| const checked = element.getCheckedAnomalies(); |
| assert.deepEqual(checked, anomalies); |
| }); |
| }); |
| |
| describe('fetch group report api', () => { |
| it('fetches the group report', async () => { |
| fetchMock.post('begin:/_/anomalies/group_report', { sid: 'test_sid' }); |
| await element.fetchGroupReportApi('1,2,3'); |
| assert.equal(element.getGroupReportResponse!.sid, 'test_sid'); |
| }); |
| }); |
| |
| describe('generate multi graph url', () => { |
| it('generates the correct multi graph url', async () => { |
| const anomalies = [dummyAnomaly('1', 0, 0, 0, 'master/bot/suite/test')]; |
| const timerangeMap = { '1': { begin: 1, end: 2 } }; |
| fetchMock.post('/_/shortcut/update', { id: 'test_shortcut' }); |
| const urls = await element.generateMultiGraphUrl(anomalies, timerangeMap); |
| assert.isNotEmpty(urls); |
| }); |
| }); |
| |
| describe('calculate time range', () => { |
| it('calculates the correct time range', () => { |
| const timerange = { begin: 1000, end: 2000 }; |
| const newRange = element.calculateTimeRange(timerange); |
| const weekInSeconds = 7 * 24 * 60 * 60; |
| assert.equal(newRange[0], (1000 - weekInSeconds).toString()); |
| assert.equal(newRange[1], (2000 + weekInSeconds).toString()); |
| }); |
| }); |
| |
| describe('initial check all checkbox', () => { |
| it('checks all checkboxes', async () => { |
| const anomalies = [ |
| dummyAnomaly('1', 0, 100, 200, 'master/bot/suite/test1'), |
| dummyAnomaly('2', 0, 100, 200, 'master/bot/suite/test2'), |
| ]; |
| await element.populateTable(anomalies); |
| element.initialCheckAllCheckbox(); |
| const checkbox1 = element.querySelector('[id^="anomaly-row-"][id$="-1"]') as HTMLInputElement; |
| const checkbox2 = element.querySelector('[id^="anomaly-row-"][id$="-2"]') as HTMLInputElement; |
| assert.isTrue(checkbox1.checked); |
| assert.isTrue(checkbox2.checked); |
| }); |
| |
| it('initial all checkboxes including group summary rows', async () => { |
| const anomalies = [ |
| dummyAnomaly('1', 12345, 100, 200, 'master/bot/suite/test1'), |
| dummyAnomaly('2', 12345, 150, 250, 'master/bot/suite/test2'), |
| dummyAnomaly('3', 0, 300, 400, 'master/bot/suite/test3'), |
| ]; |
| fetchMock.post('/_/shortcut/update', { id: 'test_shortcut' }); |
| await element.populateTable(anomalies); |
| element.initialCheckAllCheckbox(); |
| |
| // Check individual anomaly checkboxes |
| assert.isTrue( |
| (element.querySelector('[id^="anomaly-row-"][id$="-1"]') as HTMLInputElement).checked |
| ); |
| assert.isTrue( |
| (element.querySelector('[id^="anomaly-row-"][id$="-2"]') as HTMLInputElement).checked |
| ); |
| assert.isTrue( |
| (element.querySelector('[id^="anomaly-row-"][id$="-3"]') as HTMLInputElement).checked |
| ); |
| |
| // Check group summary checkboxes (assuming grouping logic creates groups) |
| assert.isTrue( |
| (element.querySelector('[id^="anomaly-row-"][id$="-group-1-2"]') as HTMLInputElement) |
| .checked |
| ); |
| }); |
| }); |
| |
| describe('get group id', () => { |
| it('returns the correct group id', () => { |
| const group = { |
| anomalies: [dummyAnomaly('2', 0, 0, 0, ''), dummyAnomaly('1', 0, 0, 0, '')], |
| expanded: false, |
| }; |
| const groupId = element.getGroupId(group); |
| assert.equal(groupId, 'group-1-2'); |
| }); |
| }); |
| |
| describe('checkbox interaction', () => { |
| it('checks a single anomaly in an expanded group on first click', async () => { |
| const anomalies = [ |
| dummyAnomaly('1', 12345, 100, 200, 'master/bot/suite/test1'), |
| dummyAnomaly('2', 12345, 150, 250, 'master/bot/suite/test2'), |
| ]; |
| await element.populateTable(anomalies); |
| |
| // Expand the group to make individual anomaly rows visible. |
| const group = element.anomalyGroups[0]; |
| element.expandGroup(group); |
| |
| const checkbox1 = element.querySelector('[id^="anomaly-row-"][id$="-1"]') as HTMLInputElement; |
| assert.isNotNull(checkbox1, 'Checkbox for anomaly 1 should exist.'); |
| assert.isFalse(checkbox1.checked, 'Checkbox should not be checked initially.'); |
| |
| // Simulate a single click. |
| checkbox1.click(); |
| |
| assert.isTrue(checkbox1.checked, 'Checkbox should be checked after one click.'); |
| const checkedAnomalies = element.getCheckedAnomalies(); |
| assert.deepEqual(checkedAnomalies, [anomalies[0]]); |
| }); |
| }); |
| |
| describe('individual anomaly styling', () => { |
| const createAndTestAnomaly = async ( |
| id: string, |
| isImprovement: boolean, |
| before: number, |
| after: number, |
| expectedClass: 'improvement' | 'regression', |
| expectedDelta: string |
| ) => { |
| const anomaly = dummyAnomaly(id, 12345, 100, 200, 'master/bot/suite/test'); |
| anomaly.is_improvement = isImprovement; |
| anomaly.median_before_anomaly = before; |
| anomaly.median_after_anomaly = after; |
| |
| await element.populateTable([anomaly]); |
| |
| const row = element.querySelector(`[id^="anomaly-row-"][id$="-${id}"]`)!.closest('tr'); |
| assert.isNotNull(row, `Row for anomaly ${id} should exist.`); |
| |
| const deltaCell = row!.querySelector('td:last-child'); |
| assert.isNotNull(deltaCell, `Delta cell for anomaly ${id} should exist.`); |
| |
| assert.include( |
| deltaCell!.className, |
| expectedClass, |
| `Cell should have class '${expectedClass}'` |
| ); |
| assert.notInclude( |
| deltaCell!.className, |
| expectedClass === 'improvement' ? 'regression' : 'improvement', |
| `Cell should not have class '${ |
| expectedClass === 'improvement' ? 'regression' : 'improvement' |
| }'` |
| ); |
| assert.equal(deltaCell!.textContent?.trim(), expectedDelta); |
| }; |
| |
| it('correctly styles a regression where lower is better', async () => { |
| await createAndTestAnomaly('1', false, 100, 120, 'regression', '+20%'); |
| }); |
| |
| it('correctly styles an improvement where lower is better', async () => { |
| await createAndTestAnomaly('2', true, 100, 80, 'improvement', '-20%'); |
| }); |
| |
| it('correctly styles a regression where greater is better', async () => { |
| await createAndTestAnomaly('3', false, 100, 80, 'regression', '-20%'); |
| }); |
| |
| it('correctly styles an improvement where greater is better', async () => { |
| await createAndTestAnomaly('4', true, 100, 120, 'improvement', '+20%'); |
| }); |
| }); |
| |
| describe('summary row styling', () => { |
| const createAnomaly = ( |
| id: string, |
| isImprovement: boolean, |
| before: number, |
| after: number |
| ): Anomaly => { |
| const anomaly = dummyAnomaly(id, 12345, 100, 200, 'master/bot/suite/test'); |
| anomaly.is_improvement = isImprovement; |
| anomaly.median_before_anomaly = before; |
| anomaly.median_after_anomaly = after; |
| return anomaly; |
| }; |
| |
| it('shows largest improvement when there are no regressions', async () => { |
| const improvement1 = createAnomaly('1', true, 100, 150); // +50% |
| const improvement2 = createAnomaly('2', true, 100, 120); // +20% |
| |
| await element.populateTable([improvement1, improvement2]); |
| |
| const summaryRow = element.querySelector('tr[data-bugid="12345"]'); |
| assert.isNotNull(summaryRow); |
| const summaryCell = summaryRow!.querySelector('td:last-child'); |
| assert.isNotNull(summaryCell); |
| assert.include(summaryCell!.className, 'improvement'); |
| assert.notInclude(summaryCell!.className, 'regression'); |
| assert.equal(summaryCell!.textContent?.trim(), '+50%'); |
| }); |
| |
| it('shows largest regression when there are only regressions', async () => { |
| const regression1 = createAnomaly('1', false, 100, 90); // -10% |
| const regression2 = createAnomaly('2', false, 100, 80); // -20% |
| |
| await element.populateTable([regression1, regression2]); |
| |
| const summaryRow = element.querySelector('tr[data-bugid="12345"]'); |
| assert.isNotNull(summaryRow); |
| const summaryCell = summaryRow!.querySelector('td:last-child'); |
| assert.isNotNull(summaryCell); |
| assert.include(summaryCell!.className, 'regression'); |
| assert.notInclude(summaryCell!.className, 'improvement'); |
| assert.equal(summaryCell!.textContent?.trim(), '-20%'); |
| }); |
| |
| it('shows largest regression in a mixed group', async () => { |
| const improvement = createAnomaly('1', true, 100, 150); // +50% |
| const regression1 = createAnomaly('2', false, 100, 90); // -10% |
| const regression2 = createAnomaly('3', false, 100, 80); // -20% |
| |
| await element.populateTable([improvement, regression1, regression2]); |
| |
| const summaryRow = element.querySelector('tr[data-bugid="12345"]'); |
| assert.isNotNull(summaryRow); |
| const summaryCell = summaryRow!.querySelector('td:last-child'); |
| assert.isNotNull(summaryCell); |
| assert.include(summaryCell!.className, 'regression'); |
| assert.notInclude(summaryCell!.className, 'improvement'); |
| assert.equal(summaryCell!.textContent?.trim(), '-20%'); |
| }); |
| |
| it('shows largest positive regression in a mixed group when lower is better', async () => { |
| // Simulates "lower is better". A positive delta is a regression. |
| const regression1 = createAnomaly('1', false, 100, 120); // +20% regression |
| const regression2 = createAnomaly('2', false, 100, 110); // +10% regression |
| const improvement = createAnomaly('3', true, 100, 90); // -10% improvement |
| |
| await element.populateTable([regression1, regression2, improvement]); |
| |
| const summaryRow = element.querySelector('tr[data-bugid="12345"]'); |
| assert.isNotNull(summaryRow); |
| const summaryCell = summaryRow!.querySelector('td:last-child'); |
| assert.isNotNull(summaryCell); |
| |
| // The class should be 'regression' because regressions exist. |
| assert.include(summaryCell!.className, 'regression'); |
| assert.notInclude(summaryCell!.className, 'improvement'); |
| |
| // The value should be the largest regression by magnitude, which is +20%. |
| assert.equal(summaryCell!.textContent?.trim(), '+20%'); |
| }); |
| }); |
| }); |