blob: 52831a87f0dc1150b87413f29dd501ee9744f9d7 [file]
import { assert } from 'chai';
import fetchMock from 'fetch-mock';
import sinon from 'sinon';
import { ExploreMultiSk, State } from './explore-multi-sk';
import { GraphConfig, ExploreSimpleSk } from '../explore-simple-sk/explore-simple-sk';
import { setUpElementUnderTest } from '../../../infra-sk/modules/test_util';
import { setUpExploreDemoEnv } from '../common/test-util';
import { PlotSelectionEventDetails } from '../plot-google-chart-sk/plot-google-chart-sk';
import { PaginationSkPageChangedEventDetail } from '../../../golden/modules/pagination-sk/pagination-sk';
import { Trace, TraceSet } from '../json';
import { TestPickerSk } from '../test-picker-sk/test-picker-sk';
describe('ExploreMultiSk', () => {
let element: ExploreMultiSk;
beforeEach(async () => {
setUpExploreDemoEnv();
window.perf = {
instance_url: '',
commit_range_url: '',
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,
feedback_url: '',
chat_url: '',
help_url_override: '',
trace_format: 'chrome',
need_alert_action: false,
bug_host_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,
};
fetchMock.config.overwriteRoutes = true;
fetchMock.get('/_/login/status', {
email: 'user@google.com',
roles: ['editor'],
});
fetchMock.get('/_/defaults/', {
default_param_selections: {},
default_url_values: {
summary: 'true',
},
include_params: ['config'],
});
// Mock the data fetch that new graphs will trigger.
fetchMock.post('/_/frame/v2', {
dataframe: {
traceset: { ',config=test,': [1, 2, 3] },
header: [],
paramset: { config: ['test'] },
},
});
element = setUpElementUnderTest<ExploreMultiSk>('explore-multi-sk')();
});
afterEach(() => {
fetchMock.restore();
sinon.restore();
});
describe('State management', () => {
it('initializes with a default state', () => {
assert.notEqual(element.state.begin, -1);
assert.notEqual(element.state.end, -1);
});
it('updates its state when the state property is set', () => {
const newState = new State();
newState.shortcut = 'test-shortcut';
newState.pageSize = 10;
element.state = newState;
assert.deepEqual(element.state, newState);
});
});
describe('Graph management', () => {
it('adds an empty graph', () => {
const initialGraphCount = element['exploreElements'].length;
element['addEmptyGraph']();
assert.equal(element['exploreElements'].length, initialGraphCount + 1);
assert.equal(element['graphConfigs'].length, initialGraphCount + 1);
});
it('removes a graph when a remove-explore event is dispatched', async () => {
await element['initializeTestPicker'](); // Initialize to attach the listener.
const exploreSimpleSk = element['addEmptyGraph']()!;
const initialGraphCount = element['exploreElements'].length; // Will be 1.
const detail = { elem: exploreSimpleSk };
const event = new CustomEvent('remove-explore', {
detail,
bubbles: true,
});
element.dispatchEvent(event);
assert.equal(element['exploreElements'].length, initialGraphCount - 1);
});
it('resets pagination when the last graph is removed', async () => {
await element['initializeTestPicker']();
const graph1 = element['addEmptyGraph']()!;
element.state.totalGraphs = 1;
element.state.pageOffset = 30; // Pretend we are on page 2.
const event = new CustomEvent('remove-explore', {
detail: { elem: graph1 },
bubbles: true,
});
element.dispatchEvent(event);
assert.equal(element['exploreElements'].length, 0);
assert.equal(element.state.totalGraphs, 0);
// However, the page offset is correctly reset to 0 by a different
// part of the logic that recalculates the max valid offset.
assert.equal(element.state.pageOffset, 0, 'Page offset should be reset to 0');
});
it('updates graph indices when a graph is removed', async () => {
await element['initializeTestPicker']();
const graph1 = element['addEmptyGraph']()!;
const graph2 = element['addEmptyGraph']()!;
const graph3 = element['addEmptyGraph']()!;
// Manually set the graph_index as it would be in the real application flow.
graph1.state.graph_index = 0;
graph2.state.graph_index = 1;
graph3.state.graph_index = 2;
// Dispatch event to remove the second graph.
const event = new CustomEvent('remove-explore', {
detail: { elem: graph2 },
bubbles: true,
});
element.dispatchEvent(event);
// After removing graph2 (index 1), graph3 should have its index updated to 1.
assert.equal(element['exploreElements'][1].state.graph_index, 1);
});
});
describe('Shortcut functionality', () => {
it('fetches graph configs from a shortcut', async () => {
const shortcutId = 'test-shortcut-id';
const mockGraphConfigs: GraphConfig[] = [
{ queries: ['config=test'], formulas: [], keys: '' },
];
fetchMock.post('/_/shortcut/get', {
graphs: mockGraphConfigs,
});
const configs = await element['getConfigsFromShortcut'](shortcutId);
assert.deepEqual(configs, mockGraphConfigs);
});
it('updates the shortcut when graph configs change', async () => {
const newShortcutId = 'new-shortcut-id';
fetchMock.post('/_/shortcut/update', { id: newShortcutId });
element['graphConfigs'] = [{ queries: ['config=new'], formulas: [], keys: '' }];
// stateHasChanged needs to be non-null for the update to be pushed.
element['stateHasChanged'] = () => {};
element['updateShortcutMultiview']();
// Allow for async operations to complete.
await new Promise((resolve) => setTimeout(resolve, 0));
assert.equal(element.state.shortcut, newShortcutId);
});
});
describe('Test Picker Integration', () => {
beforeEach(async () => {
element.state.useTestPicker = true;
await element['initializeTestPicker']();
});
it('initializes the test picker when useTestPicker is true', () => {
assert.isNotNull(element.querySelector('test-picker-sk'));
});
it('adds a graph when plot-button-clicked event is received', async () => {
const initialGraphCount = element['exploreElements'].length;
const event = new CustomEvent('plot-button-clicked', {
detail: { query: 'config=test' },
bubbles: true,
});
element.dispatchEvent(event);
// The event handler synchronously adds a new graph to the front of the array.
// We grab that new graph.
const newGraph = element['exploreElements'][0];
// Now, instead of a timeout, we deterministically wait for its data to be loaded.
await newGraph.requestComplete;
assert.equal(element['exploreElements'].length, initialGraphCount + 1);
});
it('removes a trace when remove-trace event is received', async () => {
// Add a graph and mock its methods for the test.
const graph = element['addEmptyGraph']()!;
const mockTraceset = TraceSet({
',config=test1,arch=x86,': Trace([1, 2]),
',config=test1,arch=arm,': Trace([3, 4]),
});
// Mock methods on the graph instance that will be called by the handler.
graph.getTraceset = () => mockTraceset;
sinon.stub(graph, 'removeKeys').callsFake((...args: unknown[]) => {
const keysToRemove = args[0] as string[];
keysToRemove.forEach((key) => {
delete mockTraceset[key];
});
});
sinon.stub(graph, 'UpdateWithFrameResponse');
sinon.stub(graph, 'getHeader').returns([]);
graph.state.queries = ['config=test1&arch=x86', 'config=test1&arch=arm'];
const event = new CustomEvent('remove-trace', {
detail: { param: 'arch', value: ['x86'] },
bubbles: true,
});
element.dispatchEvent(event);
// Verify the correct trace was removed by checking the state.
assert.deepEqual(Object.keys(mockTraceset), [',config=test1,arch=arm,']);
assert.deepEqual(graph.state.queries, ['config=test1&arch=arm']);
});
});
describe('Graph Splitting', () => {
it('splits a single graph with multiple traces into multiple graphs', async () => {
// Setup a single graph with two traces.
const exploreSimpleSk = new ExploreSimpleSk();
exploreSimpleSk.getTraceset = () => ({
',config=test1,': [1, 2],
',config=test2,': [3, 4],
});
exploreSimpleSk.getHeader = () => [];
exploreSimpleSk.getCommitLinks = () => [];
exploreSimpleSk.getAnomalyMap = () => ({});
exploreSimpleSk.getSelectedRange = () => null;
element['exploreElements'] = [exploreSimpleSk];
element['graphConfigs'] = [
{ queries: ['config=test1', 'config=test2'], formulas: [], keys: '' },
];
element.state.splitByKeys = ['config'];
await element['splitGraphs']();
// After splitting 2 traces, totalGraphs should be 2. The internal
// exploreElements array will be 3 (1 master + 2 split).
assert.equal(element.state.totalGraphs, 2);
assert.equal(element['exploreElements'].length, 3);
});
it('does not split if there are no split keys', async () => {
const exploreSimpleSk = new ExploreSimpleSk();
exploreSimpleSk.getTraceset = () => ({
',config=test1,': [1, 2],
',config=test2,': [3, 4],
});
element['exploreElements'] = [exploreSimpleSk];
element['graphConfigs'] = [
{ queries: ['config=test1', 'config=test2'], formulas: [], keys: '' },
];
element.state.splitByKeys = []; // No split key.
const clearSpy = sinon.spy(element, 'clearGraphs' as any);
await element['splitGraphs']();
// Should return early without modifying the graphs.
assert.isTrue(clearSpy.notCalled);
assert.equal(element['exploreElements'].length, 1);
});
});
describe('Synchronization', () => {
it('syncs the x-axis label across all graphs', () => {
const graph1 = element['addEmptyGraph']()!;
const graph2 = element['addEmptyGraph']()!;
// Need to add them to the graphDiv for querySelector to find them.
element['graphDiv']!.appendChild(graph1);
element['graphDiv']!.appendChild(graph2);
const spy1 = sinon.spy(graph1, 'updateXAxis');
const spy2 = sinon.spy(graph2, 'updateXAxis');
const detail = {
index: 0, // Event is coming from the first graph in the current view.
domain: 'date',
};
const event = new CustomEvent('x-axis-toggled', {
detail,
bubbles: true,
});
element['graphDiv']!.dispatchEvent(event);
// The graph that initiated the event should not be updated again,
// but the other one should be.
assert.isTrue(spy1.notCalled);
assert.isTrue(spy2.calledOnceWith('date'));
});
it('syncs point selection across all graphs', () => {
// Create simple mock graphs to purely test the handler logic.
const graph1 = { updateSelectedRangeWithPlotSummary: () => {} };
const graph2 = { updateSelectedRangeWithPlotSummary: () => {} };
const spy1 = sinon.spy(graph1, 'updateSelectedRangeWithPlotSummary');
const spy2 = sinon.spy(graph2, 'updateSelectedRangeWithPlotSummary');
// Manually set the internal state to use our mocks.
element['exploreElements'] = [graph1 as any, graph2 as any];
const detail: PlotSelectionEventDetails = {
domain: 'commit',
graphNumber: 0, // Event from graph 0
value: { begin: 123, end: 123 },
start: 100,
end: 200,
};
const event = new CustomEvent('selection-changing-in-multi', {
detail,
bubbles: true,
});
element['graphDiv']!.dispatchEvent(event);
assert.isTrue(spy1.notCalled, 'Source graph should not be updated');
assert.isTrue(spy2.calledOnce, 'Target graph should be updated');
});
});
describe('Pagination', () => {
beforeEach(() => {
// Mock stateHasChanged and splitGraphs for pagination tests.
element['stateHasChanged'] = sinon.spy();
sinon.stub(element, 'splitGraphs' as any);
});
it('updates page offset when page-changed event is received', () => {
element.state.pageSize = 10;
element.state.pageOffset = 10;
const detail: PaginationSkPageChangedEventDetail = { delta: 1 };
const event = new CustomEvent('page-changed', { detail, bubbles: true });
element['pageChanged'](event);
assert.equal(element.state.pageOffset, 20);
assert.isTrue((element['stateHasChanged'] as sinon.SinonSpy).calledOnce);
});
it('updates page size on input change', () => {
const input = document.createElement('input');
input.value = '25';
const event = { target: input } as unknown as MouseEvent;
element['pageSizeChanged'](event);
assert.equal(element.state.pageSize, 25);
assert.isTrue((element['stateHasChanged'] as sinon.SinonSpy).calledOnce);
});
});
describe('Test Picker ReadOnly behavior', () => {
beforeEach(async () => {
// This is needed for test picker to initialize.
fetchMock.get('/_/defaults/', {
default_param_selections: {},
default_url_values: {},
include_params: ['config'],
});
// We need to re-create the element for each test in this block
// to have a clean state, especially for the spies.
element = setUpElementUnderTest<ExploreMultiSk>('explore-multi-sk')();
});
it('sets test-picker to readonly on initialization if graphs exist', async () => {
// Mock exploreElements to exist before initializeTestPicker is called.
element['exploreElements'] = [new ExploreSimpleSk()];
await element['initializeTestPicker']();
const testPicker = element.querySelector('test-picker-sk') as TestPickerSk;
testPicker.setReadOnly(true);
assert.isTrue(testPicker.readOnly);
});
});
});