blob: f8ba20a5ad7dd753c0f16e6a66376de43e83d3ca [file]
import './explore-multi-v2-sk';
import { ExploreMultiV2Sk } from './explore-multi-v2-sk';
import { UNSET_TIME } from '../const/const';
import { expect } from 'chai';
import sinon from 'sinon';
import { DataService } from '../data-service';
import { TraceDatabase } from './db';
describe('explore-multi-v2-sk', () => {
let element: ExploreMultiV2Sk;
let globalOldSubtle: any;
let globalOldGet: any;
let globalOldSet: any;
beforeEach(async () => {
window.history.replaceState(null, '', window.location.pathname);
(window as any).WORKER_URL =
'data:application/javascript,self.postMessage({ type: "LOADED" }); self.onmessage = (e) => { if (e.data.type === "INIT") { self.postMessage({ type: "READY" }); } };';
element = document.createElement('explore-multi-v2-sk') as ExploreMultiV2Sk;
element['_debounceDelay'] = 0;
(element as any)._fetchMetadata = async () => {};
document.body.appendChild(element);
await element.updateComplete;
// Mock crypto.subtle for hashRequest
globalOldSubtle = window.crypto.subtle;
Object.defineProperty(window.crypto, 'subtle', {
get: () => ({
digest: async () => new ArrayBuffer(32),
}),
configurable: true,
});
// Mock TraceDatabase to avoid real IndexedDB calls
globalOldGet = TraceDatabase.prototype.get;
globalOldSet = TraceDatabase.prototype.set;
TraceDatabase.prototype.get = async () => null; // Default to cache miss
TraceDatabase.prototype.set = async () => {};
});
afterEach(() => {
document.body.removeChild(element);
// Restore mocks
Object.defineProperty(window.crypto, 'subtle', {
get: () => globalOldSubtle,
configurable: true,
});
TraceDatabase.prototype.get = globalOldGet;
TraceDatabase.prototype.set = globalOldSet;
});
it('uses custom selects', () => {
const toolbar = element.shadowRoot!.querySelector('explore-toolbar-sk');
const selects = toolbar!.shadowRoot!.querySelectorAll('select.custom-select');
expect(selects.length).to.be.greaterThan(0);
});
it('uses custom checkboxes', () => {
const toolbar = element.shadowRoot!.querySelector('explore-toolbar-sk');
const checkboxes = toolbar!.shadowRoot!.querySelectorAll(
'.custom-checkbox input[type="checkbox"]'
);
expect(checkboxes.length).to.be.greaterThan(0);
});
it('uses custom sliders for numeric inputs', async () => {
element['_hoverMode'] = 'smoothed';
await element.updateComplete;
const toolbar = element.shadowRoot!.querySelector('explore-toolbar-sk');
await (toolbar as any).updateComplete;
const sliders = toolbar!.shadowRoot!.querySelectorAll('input.custom-slider[type="range"]');
expect(sliders.length).to.be.greaterThan(0);
});
it('updates edge slider max and adds outlier slider', async () => {
element['_hoverMode'] = 'smoothed';
await element.updateComplete;
const toolbar = element.shadowRoot!.querySelector('explore-toolbar-sk');
await (toolbar as any).updateComplete;
const edgeSlider = toolbar!.shadowRoot!.querySelector(
'input.custom-slider[type="range"][max="1"]'
);
expect(edgeSlider).to.not.be.null;
const outlierSlider = toolbar!.shadowRoot!.querySelector(
'input.custom-slider[type="range"][max="5"]'
);
expect(outlierSlider).to.not.be.null;
});
it('uses custom buttons', async () => {
const toolbar = element.shadowRoot!.querySelector('explore-toolbar-sk');
await (toolbar as any).updateComplete;
const buttons = toolbar!.shadowRoot!.querySelectorAll('button.custom-btn');
expect(buttons.length).to.be.greaterThan(0);
});
it('smooth is unchecked by default', () => {
expect((element as any)._smooth).to.be.false;
});
it('syncs all toolbar checkboxes to URL', async () => {
(element as any)._showSparklines = true;
(element as any)._showRegressions = false;
(element as any)._tooltipDiffs = true;
(element as any).dateMode = true;
(element as any)._tracePage = 3;
(element as any)._pageSize = 25;
(element as any)._showAllTraces = true;
(element as any)._selectedSubrepo = 'Skia';
(element as any)._edgeDetectionFactor = 0.75;
(element as any)._edgeLookahead = 4;
(element as any)._stateHasChanged();
await new Promise((resolve) => setTimeout(resolve, 0));
const url = new URL(window.location.href);
expect(url.searchParams.get('sparklines')).to.equal('true');
expect(url.searchParams.get('regressions')).to.equal('false');
expect(url.searchParams.get('tooltipDiffs')).to.equal('true');
expect(url.searchParams.get('dateMode')).to.equal('true');
expect(url.searchParams.get('page')).to.equal('3');
expect(url.searchParams.get('pageSize')).to.equal('25');
expect(url.searchParams.get('showAll')).to.equal('true');
expect(url.searchParams.get('subrepo')).to.equal('Skia');
expect(url.searchParams.get('edgeFactor')).to.equal('0.75');
expect(url.searchParams.get('outlier')).to.equal('4');
});
it('triggers state update on dateMode, page, pageSize, showAll, subrepo, edgeFactor or outlier changes', async () => {
let stateChangedCalled = false;
(element as any)._stateHasChanged = () => {
stateChangedCalled = true;
};
stateChangedCalled = false;
element['dateMode'] = true;
await element.updateComplete;
expect(stateChangedCalled).to.be.true;
stateChangedCalled = false;
element['_tracePage'] = 2;
await element.updateComplete;
expect(stateChangedCalled).to.be.true;
stateChangedCalled = false;
element['_pageSize'] = 50;
await element.updateComplete;
expect(stateChangedCalled).to.be.true;
stateChangedCalled = false;
element['_showAllTraces'] = true;
await element.updateComplete;
expect(stateChangedCalled).to.be.true;
stateChangedCalled = false;
element['_selectedSubrepo'] = 'V8';
await element.updateComplete;
expect(stateChangedCalled).to.be.true;
stateChangedCalled = false;
element['_edgeDetectionFactor'] = 0.8;
await element.updateComplete;
expect(stateChangedCalled).to.be.true;
stateChangedCalled = false;
element['_edgeLookahead'] = 2;
await element.updateComplete;
expect(stateChangedCalled).to.be.true;
});
it('syncs begin and end to URL', async () => {
(element as any).begin = 1680000000;
(element as any).end = 1680100000;
(element as any)._stateHasChanged();
await new Promise((resolve) => setTimeout(resolve, 0));
const url = new URL(window.location.href);
expect(url.searchParams.get('begin')).to.equal('1680000000');
expect(url.searchParams.get('end')).to.equal('1680100000');
});
it('resolves time range correctly using explicit bounds', () => {
(element as any).begin = 1680000000;
(element as any).end = 1680100000;
const range1 = (element as any)._resolveTimeRange();
expect(range1.begin).to.equal(1680000000);
expect(range1.end).to.equal(1680100000);
});
it('immediately writes back resolved default bounds to keep state deterministic', () => {
(element as any).begin = UNSET_TIME;
(element as any).end = UNSET_TIME;
const range = (element as any)._resolveTimeRange();
expect((element as any).begin).to.equal(range.begin);
expect((element as any).end).to.equal(range.end);
expect((element as any).begin).to.not.equal(UNSET_TIME);
expect((element as any).end).to.not.equal(UNSET_TIME);
});
it('syncs dateMode to URL', async () => {
(element as any).dateMode = true;
(element as any)._stateHasChanged();
await new Promise((resolve) => setTimeout(resolve, 0));
const url = new URL(window.location.href);
expect(url.searchParams.get('dateMode')).to.equal('true');
});
it('deserializes dateMode from URL', async () => {
// Clear state
window.history.replaceState(null, '', window.location.pathname);
// Set dateMode in URL
const url = new URL(window.location.href);
url.searchParams.set('dateMode', 'true');
window.history.pushState({}, '', url.toString());
// Create a new element to see if it picks up the state from URL
const newElement = document.createElement('explore-multi-v2-sk') as ExploreMultiV2Sk;
document.body.appendChild(newElement);
await newElement.updateComplete;
expect((newElement as any).dateMode).to.be.true;
document.body.removeChild(newElement);
});
it('sends all queries to worker', async () => {
let filterArgs: any = null;
const mockController = {
isReady: () => true,
filter: (queries: any, numUserQueries: number, requestId?: number) => {
filterArgs = { queries, numUserQueries, requestId };
return requestId || 0;
},
terminate: () => {},
};
(element as any)['_workerController'] = mockController;
element['queries'] = [{ bot: ['linux32'], test: ['binary_size'] }, { bot: ['mac64'] }];
element['_triggerWorkerFilter']();
expect(filterArgs).to.not.be.null;
expect(filterArgs.queries.length).to.equal(4); // 2 user queries + 2 facet removed queries
expect(filterArgs.numUserQueries).to.equal(2);
});
it('does not set globalBounds to loadedBounds on trace selection', async () => {
const mockDataService = {
getLinksBatch: async () => ({}),
sendFrameRequest: async () => ({
dataframe: {
header: [{ offset: 10, timestamp: 1000 }],
traceset: { t1: [1.0] },
},
}),
};
const oldInstance = (DataService as any).instance;
(DataService as any).instance = mockDataService;
const oldSubtle = window.crypto.subtle;
Object.defineProperty(window.crypto, 'subtle', {
get: () => ({
digest: async () => new ArrayBuffer(32),
}),
configurable: true,
});
const oldGet = TraceDatabase.prototype.get;
const oldSet = TraceDatabase.prototype.set;
TraceDatabase.prototype.get = async () => null;
TraceDatabase.prototype.set = async () => {};
try {
element['_seriesData'] = [];
element['_matchingTraceIds'] = ['t1'];
element['_tracePage'] = 0;
element['_pageSize'] = 10;
await element['_fetchData'](element['_latestRequestId']);
expect(element['_globalBounds']).to.deep.equal({});
} finally {
(DataService as any).instance = oldInstance;
Object.defineProperty(window.crypto, 'subtle', {
get: () => oldSubtle,
configurable: true,
});
TraceDatabase.prototype.get = oldGet;
TraceDatabase.prototype.set = oldSet;
}
});
it('calls fetchTraceValues with integer commits when given floats', async () => {
let fetchTraceValuesArg: any = null;
const mockDataService = {
getLinksBatch: async () => ({}),
fetchTraceValues: async (arg: any) => {
fetchTraceValuesArg = arg;
return { results: {} };
},
};
const oldInstance = (DataService as any).instance;
(DataService as any).instance = mockDataService;
try {
element['_matchingTraceIds'] = ['t1'];
element['_tracePage'] = 0;
element['_pageSize'] = 10;
element['_seriesData'] = [{ id: 't1', rows: [], color: '#fff' }];
element['_loadedBounds'] = { t1: { min: 100000, max: 104000 } };
element['_globalBounds'] = { t1: { min: 90000, max: 110000 } };
await element['_doHandleViewportChanged']({
detail: { minCommit: 103700.01463834244, maxCommit: 105000.5 },
});
expect(fetchTraceValuesArg).to.not.be.null;
expect(Number.isInteger(fetchTraceValuesArg.min_commit)).to.be.true;
expect(Number.isInteger(fetchTraceValuesArg.max_commit)).to.be.true;
} finally {
(DataService as any).instance = oldInstance;
}
});
it('calls fetchTraceValues with begin and end in Date Mode', async () => {
let fetchTraceValuesArg: any = null;
const mockDataService = {
getLinksBatch: async () => ({}),
fetchTraceValues: async (arg: any) => {
fetchTraceValuesArg = arg;
return { results: {} };
},
};
const oldInstance = (DataService as any).instance;
(DataService as any).instance = mockDataService;
try {
element['_matchingTraceIds'] = ['t1'];
element['_tracePage'] = 0;
element['_pageSize'] = 10;
element['_seriesData'] = [];
element['_loadedBounds'] = {};
element['_globalBounds'] = {};
element['dateMode'] = true;
await element['_doHandleViewportChanged']({
detail: { minCommit: 1700000000.5, maxCommit: 1700005000.7 },
});
expect(fetchTraceValuesArg).to.not.be.null;
expect(fetchTraceValuesArg.begin).to.equal(1700000000);
expect(fetchTraceValuesArg.end).to.equal(1700005001);
expect(fetchTraceValuesArg.min_commit).to.equal(0);
expect(fetchTraceValuesArg.max_commit).to.equal(0);
} finally {
(DataService as any).instance = oldInstance;
}
});
it('updates globalBounds when fetch returns no new data on the left', async () => {
let fetchCalled = false;
const mockDataService = {
getLinksBatch: async () => ({}),
fetchTraceValues: async (_arg: any) => {
fetchCalled = true;
return { results: { t1: [] } }; // Return empty rows for t1
},
};
const oldInstance = (DataService as any).instance;
(DataService as any).instance = mockDataService;
try {
element['_matchingTraceIds'] = ['t1'];
element['_tracePage'] = 0;
element['_pageSize'] = 10;
element['_seriesData'] = [
{
id: 't1',
rows: [{ commit_number: 1000, createdat: 0, val: 1, smoothedVal: 1 }],
color: '#fff',
},
];
element['_loadedBounds'] = { t1: { min: 1000, max: 2000 } };
element['_globalBounds'] = {}; // Empty global bounds
// Trigger viewport change that requires left fetch
await element['_doHandleViewportChanged']({
detail: { minCommit: 500, maxCommit: 1500 },
});
expect(fetchCalled).to.be.true;
// Since it returned no new data, globalBounds.min should be set to loadedBounds.min
expect(element['_globalBounds']['t1']).to.not.be.undefined;
expect(element['_globalBounds']['t1'].min).to.equal(1000);
} finally {
(DataService as any).instance = oldInstance;
}
});
it('determines Y axis title correctly', () => {
const title1 = element['_determineYAxisTitle'](['benchmark=A,unit=ms', 'benchmark=B,unit=ms']);
expect(title1).to.equal('ms');
const title2 = element['_determineYAxisTitle'](['benchmark=A,unit=ms', 'benchmark=B,unit=s']);
expect(title2).to.equal('');
const title3 = element['_determineYAxisTitle']([
'benchmark=A,unit=ms,improvement_dir=up',
'benchmark=B,unit=ms,improvement_dir=up',
]);
expect(title3).to.equal('ms - up');
const title4 = element['_determineYAxisTitle']([]);
expect(title4).to.equal('');
});
it('returns primary key exactly as given without removing stat', () => {
const key1 = ',benchmark=A,stat=min,test=B,';
const primary1 = element['_getPrimaryKey'](key1);
expect(primary1).to.equal(',benchmark=A,stat=min,test=B,');
const key2 = ',benchmark=A,test=B,';
const primary2 = element['_getPrimaryKey'](key2);
expect(primary2).to.equal(',benchmark=A,test=B,');
});
it('translates traceset keys into independent series without grouping', () => {
const df = {
header: [{ offset: 10, timestamp: 1000 }],
traceset: {
',benchmark=A,test=B,': [1.0],
',benchmark=A,test=B,stat=min,': [0.5],
',benchmark=A,test=B,stat=max,': [1.5],
},
};
const series = element['_translateDataFrame'](df);
expect(series.length).to.equal(3);
expect(series.map((s: any) => s.id)).to.deep.equal([
',benchmark=A,test=B,',
',benchmark=A,stat=min,test=B,',
',benchmark=A,stat=max,test=B,',
]);
expect(series[0].rows[0].val).to.equal(1.0);
expect(series[1].rows[0].val).to.equal(0.5);
expect(series[2].rows[0].val).to.equal(1.5);
});
it('merges series data correctly accumulating stats', () => {
const existing = [
{
id: ',benchmark=A,test=B,',
color: 'red',
rows: [{ commit_number: 10, val: 1.0 }],
allStats: {},
},
];
const newSeries = [
{
id: ',benchmark=A,test=B,',
color: 'blue',
rows: [],
allStats: { min: [{ commit_number: 10, val: 0.5 }] },
},
];
const merged = (element as any)['_mergeSeriesWithStats'](existing, newSeries);
expect(merged.length).to.equal(1);
expect(merged[0].rows.length).to.equal(1);
expect(merged[0].rows[0].val).to.equal(1.0); // Kept existing rows
expect(merged[0].allStats['min']).to.not.be.undefined;
expect(merged[0].allStats['min'][0].val).to.equal(0.5); // Accumulated stats
});
it('toggles showAllTraces when Show All button is clicked', async () => {
element['_showAllTraces'] = false;
await element.updateComplete;
const toolbar = element.shadowRoot!.querySelector('explore-toolbar-sk');
const showAllBtn = Array.from(toolbar!.shadowRoot!.querySelectorAll('button')).find((b) =>
b.textContent?.trim().includes('Show All')
);
expect(showAllBtn).to.not.be.undefined;
showAllBtn!.click();
await element.updateComplete;
expect(element['_showAllTraces']).to.be.true;
expect(showAllBtn!.textContent?.trim()).to.include('Show Paged');
showAllBtn!.click();
await element.updateComplete;
expect(element['_showAllTraces']).to.be.false;
expect(showAllBtn!.textContent?.trim()).to.include('Show All');
});
it('triggers fetch and fetches all traces when showAllTraces changes', async () => {
let fetchCount = 0;
let fetchTraceIdsArg: string[] = [];
const mockDataService = {
getLinksBatch: async () => ({}),
sendFrameRequest: async (req: any) => {
fetchCount++;
fetchTraceIdsArg = req.trace_ids;
return {
dataframe: {
header: [{ offset: 10, timestamp: 1000 }],
traceset: { t1: [1.0] },
},
};
},
};
const oldInstance = (DataService as any).instance;
(DataService as any).instance = mockDataService;
const oldGet = TraceDatabase.prototype.get;
const oldSet = TraceDatabase.prototype.set;
TraceDatabase.prototype.get = async () => null;
TraceDatabase.prototype.set = async () => {};
const oldSubtle = window.crypto.subtle;
Object.defineProperty(window.crypto, 'subtle', {
get: () => ({
digest: async () => new ArrayBuffer(32),
}),
configurable: true,
});
try {
element['_matchingTraceIds'] = Array.from({ length: 20 }, (_, i) => `t${i}`);
element['_tracePage'] = 0;
element['_pageSize'] = 10;
element['_seriesData'] = [];
await element.updateComplete;
await element['_fetchData'](element['_latestRequestId']);
expect(fetchCount).to.be.greaterThan(0);
const countBefore = fetchCount;
expect(fetchTraceIdsArg.length).to.equal(10);
element['_showAllTraces'] = true;
await element.updateComplete;
await element['_fetchData'](element['_latestRequestId']);
expect(fetchCount).to.be.greaterThan(countBefore);
expect(fetchTraceIdsArg.length).to.equal(20);
} finally {
(DataService as any).instance = oldInstance;
TraceDatabase.prototype.get = oldGet;
TraceDatabase.prototype.set = oldSet;
Object.defineProperty(window.crypto, 'subtle', {
get: () => oldSubtle,
configurable: true,
});
}
});
it('loads worker via Blob URL when fetch succeeds', async () => {
const newElement = document.createElement('explore-multi-v2-sk') as ExploreMultiV2Sk;
const mockWorker = {
postMessage: () => {},
terminate: () => {},
};
let workerCreatedWithUrl = '';
const originalWorker = window.Worker;
(window as any).Worker = function (url: string) {
workerCreatedWithUrl = url;
return mockWorker;
} as any;
const originalWorkerUrl = (window as any).WORKER_URL;
delete (window as any).WORKER_URL; // Use default path (/dist/explore-multi-v2-sk/filter.worker.js)
const originalFetch = window.fetch;
window.fetch = async () =>
({
ok: true,
text: async () => 'console.log("mock worker code")',
}) as any;
try {
document.body.appendChild(newElement);
// Wait a bit for the fetch promise to resolve and worker to be created
await new Promise((resolve) => setTimeout(resolve, 50));
expect(workerCreatedWithUrl).to.match(/^blob:/);
} finally {
window.Worker = originalWorker;
window.fetch = originalFetch;
(window as any).WORKER_URL = originalWorkerUrl;
document.body.removeChild(newElement);
}
});
it('applies default param selections on load if queries empty', async () => {
const mockDataService = {
getInitPage: async () => ({ dataframe: { paramset: {} } }),
getDefaults: async () => ({
default_param_selections: { branch_name: ['aosp-androidx-main'] },
}),
};
const oldInstance = (DataService as any).instance;
(DataService as any).instance = mockDataService as any;
try {
const newElement = document.createElement('explore-multi-v2-sk') as ExploreMultiV2Sk;
document.body.appendChild(newElement);
await newElement.updateComplete;
// Wait for _fetchMetadata to complete!
await new Promise((resolve) => setTimeout(resolve, 50));
expect((newElement as any).queries.length).to.equal(1);
expect((newElement as any).queries[0]['branch_name']).to.deep.equal(['aosp-androidx-main']);
document.body.removeChild(newElement);
} finally {
(DataService as any).instance = oldInstance;
}
});
it('has sophisticated tour steps', () => {
expect((element as any)._tourSteps.length).to.equal(11);
expect((element as any)._tourSteps[0].title).to.equal('Dynamic Setup');
});
it('applies conditional defaults on selection', async () => {
(element as any)._conditionalDefaults = [
{
trigger: { param: 'metric', values: ['timeNs'] },
apply: [{ param: 'stat', values: ['min'], select_only_first: true }],
},
];
(element as any).queries = [{ metric: [] }];
(element as any)._handleSetSelected(
0,
new CustomEvent('set-selected', {
detail: { key: 'metric', values: ['timeNs'] },
})
);
expect((element as any).queries[0]['stat']).to.deep.equal(['min']);
});
it('applies conditional defaults on add query', async () => {
(element as any)._conditionalDefaults = [
{
trigger: { param: 'metric', values: ['timeNs'] },
apply: [{ param: 'stat', values: ['min'], select_only_first: true }],
},
];
(element as any).queries = [{ metric: [] }];
(element as any)._handleAddQuery(
0,
new CustomEvent('add-query', {
detail: { key: 'metric', value: 'timeNs' },
})
);
expect((element as any).queries[0]['stat']).to.deep.equal(['min']);
});
it('applies multiple values when select_only_first is false', async () => {
(element as any)._conditionalDefaults = [
{
trigger: { param: 'metric', values: ['frameDurationCpuMs'] },
apply: [{ param: 'stat', values: ['P50', 'P90'], select_only_first: false }],
},
];
(element as any).queries = [{ metric: [] }];
(element as any)._handleAddQuery(
0,
new CustomEvent('add-query', {
detail: { key: 'metric', value: 'frameDurationCpuMs' },
})
);
expect((element as any).queries[0]['stat']).to.deep.equal(['P50', 'P90']);
});
it('applies only first value when select_only_first is true', async () => {
(element as any)._conditionalDefaults = [
{
trigger: { param: 'metric', values: ['frameDurationCpuMs'] },
apply: [{ param: 'stat', values: ['P50', 'P90'], select_only_first: true }],
},
];
(element as any).queries = [{ metric: [] }];
(element as any)._handleAddQuery(
0,
new CustomEvent('add-query', {
detail: { key: 'metric', value: 'frameDurationCpuMs' },
})
);
expect((element as any).queries[0]['stat']).to.deep.equal(['P50']);
});
it('does not apply conditional defaults when trigger does not match', async () => {
(element as any)._conditionalDefaults = [
{
trigger: { param: 'metric', values: ['timeNs'] },
apply: [{ param: 'stat', values: ['min'], select_only_first: true }],
},
];
(element as any).queries = [{ metric: [] }];
(element as any)._handleAddQuery(
0,
new CustomEvent('add-query', {
detail: { key: 'metric', value: 'otherMetric' },
})
);
expect((element as any).queries[0]['stat']).to.be.undefined;
});
it('does not re-apply conditional defaults if the trigger value was already present', async () => {
(element as any)._conditionalDefaults = [
{
trigger: { param: 'metric', values: ['timeNs'] },
apply: [{ param: 'stat', values: ['min'], select_only_first: true }],
},
];
(element as any).queries = [{ metric: ['timeNs'], stat: ['max'] }];
(element as any)._handleAddQuery(
0,
new CustomEvent('add-query', {
detail: { key: 'metric', value: 'otherMetric' },
})
);
expect((element as any).queries[0]['stat']).to.deep.equal(['max']);
});
it('clears suggestions list on query modification events', () => {
element['_suggestionsForQueryBar'] = [[{ params: [], score: 100 }]];
element['_handleAddQuery'](
0,
new CustomEvent('add-query', {
detail: { key: 'metric', value: 'time' },
})
);
expect(element['_suggestionsForQueryBar'][0]).to.deep.equal([]);
element['_suggestionsForQueryBar'] = [[{ params: [], score: 100 }]];
element['_handleRemoveQuery'](
0,
new CustomEvent('remove-query', {
detail: { key: 'metric', value: 'time' },
})
);
expect(element['_suggestionsForQueryBar'][0]).to.deep.equal([]);
element['_suggestionsForQueryBar'] = [[{ params: [], score: 100 }]];
element['_handleSetSelected'](
0,
new CustomEvent('set-selected', {
detail: { key: 'metric', values: ['time'] },
})
);
expect(element['_suggestionsForQueryBar'][0]).to.deep.equal([]);
element['_suggestionsForQueryBar'] = [[{ params: [], score: 100 }]];
element['_handleRemoveKey'](
0,
new CustomEvent('remove-key', {
detail: { key: 'metric' },
})
);
expect(element['_suggestionsForQueryBar'][0]).to.deep.equal([]);
});
it('merges anomalymap from fetchTraceValues response correctly under the uncollapsed trace ID', async () => {
const mockDataService = {
getLinksBatch: async () => ({}),
fetchTraceValues: async (_arg: any) => {
return {
results: {
',benchmark=A,test=B,stat=value,': [{ commit_number: 50, createdat: 0, val: 1.5 }],
},
anomalymap: {
',benchmark=A,test=B,stat=value,': {
100: {
id: 'a1',
is_improvement: true,
state: 'untriaged',
bug_id: 0,
recovered: false,
median_before_anomaly: 5.0,
median_after_anomaly: 10.0,
},
},
},
};
},
};
const oldInstance = (DataService as any).instance;
(DataService as any).instance = mockDataService;
const oldGet = TraceDatabase.prototype.get;
const oldSet = TraceDatabase.prototype.set;
TraceDatabase.prototype.get = async () => null; // Force cache miss
TraceDatabase.prototype.set = async () => {};
const oldSubtle = window.crypto.subtle;
Object.defineProperty(window.crypto, 'subtle', {
get: () => ({
digest: async () => new ArrayBuffer(32),
}),
configurable: true,
});
try {
element['_matchingTraceIds'] = [',benchmark=A,test=B,stat=value,'];
element['_tracePage'] = 0;
element['_pageSize'] = 10;
element['_seriesData'] = [
{
id: ',benchmark=A,test=B,stat=value,',
rows: [{ commit_number: 150, createdat: 0, val: 1.0 }],
color: '#fff',
},
];
element['_loadedBounds'] = { ',benchmark=A,test=B,stat=value,': { min: 100, max: 200 } };
element['_globalBounds'] = {};
// Trigger viewport change that requires left fetch
await element['_doHandleViewportChanged']({
detail: { minCommit: 50, maxCommit: 150 },
});
expect(element['_regressions']).to.not.be.empty;
const reg = element['_regressions'][',benchmark=A,test=B,stat=value,'];
expect(reg).to.not.be.undefined;
expect(reg[100]).to.not.be.undefined;
expect(reg[100].is_improvement).to.be.true;
expect((reg[100] as any).status).to.equal('untriaged');
expect((reg[100] as any).median_before).to.equal(5.0);
expect((reg[100] as any).median_after).to.equal(10.0);
} finally {
(DataService as any).instance = oldInstance;
TraceDatabase.prototype.get = oldGet;
TraceDatabase.prototype.set = oldSet;
Object.defineProperty(window.crypto, 'subtle', {
get: () => oldSubtle,
configurable: true,
});
}
});
it('uses cache in _doHandleViewportChanged and skips fetch', async () => {
let fetchCalled = false;
const mockDataService = {
getLinksBatch: async () => ({}),
fetchTraceValues: async (_arg: any) => {
fetchCalled = true;
return { results: {} };
},
};
const oldInstance = (DataService as any).instance;
(DataService as any).instance = mockDataService;
const oldGet = TraceDatabase.prototype.get;
const oldSet = TraceDatabase.prototype.set;
const cachedData = {
results: {
',benchmark=A,test=B,': [{ commit_number: 50, createdat: 0, val: 1.5 }],
},
};
TraceDatabase.prototype.get = async () => cachedData;
TraceDatabase.prototype.set = async () => {};
const oldSubtle = window.crypto.subtle;
Object.defineProperty(window.crypto, 'subtle', {
get: () => ({
digest: async () => new ArrayBuffer(32),
}),
configurable: true,
});
try {
element['_matchingTraceIds'] = [',benchmark=A,test=B,'];
element['_tracePage'] = 0;
element['_pageSize'] = 10;
element['_seriesData'] = [{ id: ',benchmark=A,test=B,', rows: [], color: '#fff' }];
element['_loadedBounds'] = { ',benchmark=A,test=B,': { min: 100, max: 200 } };
element['_globalBounds'] = {};
// Trigger viewport change that requires left fetch
await element['_doHandleViewportChanged']({
detail: { minCommit: 50, maxCommit: 150 },
});
expect(fetchCalled).to.be.false; // Verified fetch skipped!
// Verify data was merged from cache
const series = element['_seriesData'][0];
expect(series.rows.length).to.equal(1);
expect(series.rows[0].commit_number).to.equal(50);
expect(series.rows[0].val).to.equal(1.5);
} finally {
(DataService as any).instance = oldInstance;
TraceDatabase.prototype.get = oldGet;
TraceDatabase.prototype.set = oldSet;
Object.defineProperty(window.crypto, 'subtle', {
get: () => oldSubtle,
configurable: true,
});
}
});
it('merges stat=median rows cleanly as independent series', async () => {
const mockDataService = {
getLinksBatch: async () => ({}),
fetchTraceValues: async (_arg: any) => {
return {
results: {
',benchmark=A,test=B,stat=median,': [{ commit_number: 50, createdat: 0, val: 1.5 }],
},
};
},
};
const oldInstance = (DataService as any).instance;
(DataService as any).instance = mockDataService;
const oldGet = TraceDatabase.prototype.get;
const oldSet = TraceDatabase.prototype.set;
TraceDatabase.prototype.get = async () => null;
TraceDatabase.prototype.set = async () => {};
const oldSubtle = window.crypto.subtle;
Object.defineProperty(window.crypto, 'subtle', {
get: () => ({
digest: async () => new ArrayBuffer(32),
}),
configurable: true,
});
try {
element['_matchingTraceIds'] = [',benchmark=A,test=B,stat=median,'];
element['_tracePage'] = 0;
element['_pageSize'] = 10;
element['_seriesData'] = [
{ id: ',benchmark=A,test=B,stat=median,', rows: [], color: '#fff' },
];
element['_loadedBounds'] = { ',benchmark=A,test=B,stat=median,': { min: 100, max: 200 } };
element['_globalBounds'] = {};
element['_defaultParamSelections'] = { stat: ['median'] };
await element['_doHandleViewportChanged']({
detail: { minCommit: 50, maxCommit: 150 },
});
const series = element['_seriesData'][0];
expect(series.id).to.equal(',benchmark=A,test=B,stat=median,');
expect(series.rows.length).to.equal(1);
expect(series.rows[0].val).to.equal(1.5);
} finally {
(DataService as any).instance = oldInstance;
TraceDatabase.prototype.get = oldGet;
TraceDatabase.prototype.set = oldSet;
Object.defineProperty(window.crypto, 'subtle', {
get: () => oldSubtle,
configurable: true,
});
}
});
it('renders config pills when diffBase or splitKeys is set', async () => {
element['_diffBase'] = { key: 'test', value: 'Score' };
element['splitKeys'] = new Set(['bot']);
await element.updateComplete;
const pills = element.shadowRoot!.querySelectorAll('.config-pill');
expect(pills.length).to.equal(2);
const diffPill = Array.from(pills).find((p) => p.textContent?.includes('Diff Base:'));
expect(diffPill).to.not.be.undefined;
expect(diffPill!.textContent).to.include('Score');
const splitPill = Array.from(pills).find((p) => p.textContent?.includes('Split by:'));
expect(splitPill).to.not.be.undefined;
expect(splitPill!.textContent).to.include('bot');
});
it('adds and removes split keys when split event is fired', async () => {
expect(element['splitKeys'].has('bot')).to.be.false;
const toolbar = element.shadowRoot!.querySelector('explore-toolbar-sk');
expect(toolbar).to.not.be.null;
// Fire split event to add 'bot'
toolbar!.dispatchEvent(
new CustomEvent('split', { detail: { key: 'bot' }, bubbles: true, composed: true })
);
await element.updateComplete;
expect(element['splitKeys'].has('bot')).to.be.true;
// Fire split event to remove 'bot'
toolbar!.dispatchEvent(
new CustomEvent('split', { detail: { key: 'bot' }, bubbles: true, composed: true })
);
await element.updateComplete;
expect(element['splitKeys'].has('bot')).to.be.false;
});
it('removes split keys when clicking remove button on the config pill', async () => {
element['splitKeys'] = new Set(['bot']);
await element.updateComplete;
const splitPill = Array.from(element.shadowRoot!.querySelectorAll('.config-pill')).find((p) =>
p.textContent?.includes('Split by:')
);
expect(splitPill).to.not.be.undefined;
const removeBtn = splitPill!.querySelector('.config-pill-remove') as HTMLButtonElement;
expect(removeBtn).to.not.be.null;
removeBtn.click();
await element.updateComplete;
expect(element['splitKeys'].has('bot')).to.be.false;
});
it('loads queries from a shortcut ID', async () => {
const mockShortcutConfigs = [
{ queries: ['test=A&stat=value'], formulas: [], keys: '' },
{ queries: ['benchmark=B'], formulas: [], keys: '' },
];
let getShortcutId = '';
const originalGetShortcut = DataService.prototype.getShortcut;
DataService.prototype.getShortcut = async (id: string) => {
getShortcutId = id;
return mockShortcutConfigs as any;
};
try {
await element['_loadShortcut']('mock-shortcut-123');
expect(getShortcutId).to.equal('mock-shortcut-123');
expect(element['queries'].length).to.equal(2);
expect(element['queries'][0]).to.deep.equal({ test: ['A'], stat: ['value'] });
expect(element['queries'][1]).to.deep.equal({ benchmark: ['B'] });
expect(element['_lastLoadedShortcut']).to.equal('mock-shortcut-123');
} finally {
DataService.prototype.getShortcut = originalGetShortcut;
}
});
it('updates the shortcut ID in state and URL when queries change', async () => {
const originalUpdateShortcut = DataService.prototype.updateShortcut;
let updateConfigs: any = null;
DataService.prototype.updateShortcut = async (configs: any) => {
updateConfigs = configs;
return 'new-shortcut-id-456';
};
try {
element['queries'] = [{ test: ['X'] }];
await element['_updateShortcut']();
expect(updateConfigs).to.not.be.null;
expect(updateConfigs.length).to.equal(1);
expect(updateConfigs[0].queries).to.deep.equal(['test=X']);
expect(element['_shortcut']).to.equal('new-shortcut-id-456');
const url = new URL(window.location.href);
expect(url.searchParams.get('shortcut')).to.equal('new-shortcut-id-456');
} finally {
DataService.prototype.updateShortcut = originalUpdateShortcut;
}
});
it('clears shortcut ID from state and URL when queries are emptied', async () => {
const originalUpdateShortcut = DataService.prototype.updateShortcut;
let updateCalled = false;
DataService.prototype.updateShortcut = async () => {
updateCalled = true;
return 'test-id';
};
try {
// Start with a valid query and shortcut
element['queries'] = [{ test: ['Z'] }];
element['_shortcut'] = 'some-existing-id';
element['_lastQueriesJson'] = JSON.stringify([{ test: ['Z'] }]);
// Now clear the query
element['queries'] = [{}];
await element['_updateShortcut']();
expect(updateCalled).to.be.false; // Should not call updateShortcut when empty
expect(element['_shortcut']).to.equal('');
const url = new URL(window.location.href);
expect(url.searchParams.get('shortcut')).to.be.null;
} finally {
DataService.prototype.updateShortcut = originalUpdateShortcut;
}
});
it('handles network errors when loading a shortcut gracefully', async () => {
const originalGetShortcut = DataService.prototype.getShortcut;
DataService.prototype.getShortcut = async () => {
throw new Error('Network failure');
};
try {
element['queries'] = [{ existing: ['true'] }];
element['_lastLoadedShortcut'] = '';
await element['_loadShortcut']('bad-shortcut-id');
// Queries should remain unchanged from the existing queries
expect(element['queries']).to.deep.equal([{ existing: ['true'] }]);
} finally {
DataService.prototype.getShortcut = originalGetShortcut;
}
});
it('populates optionsByKeyPerQuery[0] with overridden options for active facets', async () => {
element['queries'] = [{ benchmark: ['v8'] }];
element['_latestActiveFacets'] = ['benchmark'];
const payload = {
queryResults: [
{ paramsByKey: { benchmark: [{ value: 'v8', count: 10 }] } }, // result for full query
{
paramsByKey: {
benchmark: [
{ value: 'v8', count: 10 },
{ value: 'v8.infra', count: 5 },
],
},
}, // result for query with benchmark removed
],
};
element['_handleFilterResult'](payload);
expect(element['_optionsByKeyPerQuery'][0]['benchmark']).to.deep.equal([
{ value: 'v8', count: 10 },
{ value: 'v8.infra', count: 5 },
]);
});
describe('expand/collapse query bars', () => {
it('collapses query bars to first 3 by default when there are more than 3', async () => {
element.queries = [{}, {}, {}, {}, {}];
await element.updateComplete;
const queryBars = element.shadowRoot!.querySelectorAll('query-bar-sk');
expect(queryBars.length).to.equal(3);
const expandBtn = element.shadowRoot!.querySelector(
'.expand-queries-btn'
) as HTMLButtonElement;
expect(expandBtn).to.not.be.null;
expect(expandBtn.textContent?.trim()).to.equal('Expand (2 more)');
});
it('expands query bars and changes label when expand button clicked', async () => {
element.queries = [{}, {}, {}, {}, {}];
await element.updateComplete;
const expandBtn = element.shadowRoot!.querySelector(
'.expand-queries-btn'
) as HTMLButtonElement;
expandBtn.click();
await element.updateComplete;
const queryBars = element.shadowRoot!.querySelectorAll('query-bar-sk');
expect(queryBars.length).to.equal(5);
expect(expandBtn.textContent?.trim()).to.equal('Collapse');
});
it('collapses query bars back to 3 when collapse button clicked', async () => {
element.queries = [{}, {}, {}, {}, {}];
(element as any)._queriesExpanded = true;
await element.updateComplete;
const expandBtn = element.shadowRoot!.querySelector(
'.expand-queries-btn'
) as HTMLButtonElement;
expect(expandBtn.textContent?.trim()).to.equal('Collapse');
expandBtn.click();
await element.updateComplete;
const queryBars = element.shadowRoot!.querySelectorAll('query-bar-sk');
expect(queryBars.length).to.equal(3);
expect(expandBtn.textContent?.trim()).to.equal('Expand (2 more)');
});
it('auto-expands query bars when add query button is clicked and total becomes > 3', async () => {
element.queries = [{}, {}, {}];
await element.updateComplete;
const addBtn = element.shadowRoot!.querySelector(
'.add-query-circle-btn'
) as HTMLButtonElement;
addBtn.click();
await element.updateComplete;
expect((element as any)._queriesExpanded).to.be.true;
const queryBars = element.shadowRoot!.querySelectorAll('query-bar-sk');
expect(queryBars.length).to.equal(4);
});
});
describe('cloning query bars', () => {
it('duplicates query parameters and inserts the new query next to the cloned one', async () => {
element.queries = [{ benchmark: ['blink_perf'] }, { device: ['m1'] }];
await element.updateComplete;
// Simulate clone-query event on the first query bar
const firstQueryBar = element.shadowRoot!.querySelectorAll('query-bar-sk')[0];
expect(firstQueryBar).to.not.be.undefined;
firstQueryBar.dispatchEvent(new CustomEvent('clone-query', { bubbles: true }));
await element.updateComplete;
expect(element.queries.length).to.equal(3);
expect(element.queries[0]).to.deep.equal({ benchmark: ['blink_perf'] });
expect(element.queries[1]).to.deep.equal({ benchmark: ['blink_perf'] }); // inserted copy next to it
expect(element.queries[2]).to.deep.equal({ device: ['m1'] });
});
it('auto-expands query bars if total count becomes > 3 after cloning', async () => {
element.queries = [{ benchmark: ['blink_perf'] }, { device: ['m1'] }, { arch: ['x86'] }];
(element as any)._queriesExpanded = false;
await element.updateComplete;
const firstQueryBar = element.shadowRoot!.querySelectorAll('query-bar-sk')[0];
firstQueryBar.dispatchEvent(new CustomEvent('clone-query', { bubbles: true }));
await element.updateComplete;
expect((element as any)._queriesExpanded).to.be.true;
const queryBars = element.shadowRoot!.querySelectorAll('query-bar-sk');
expect(queryBars.length).to.equal(4);
});
});
describe('V2 Toggle', () => {
beforeEach(() => {
localStorage.removeItem('perf:use-explore-v2');
});
afterEach(() => {
localStorage.removeItem('perf:use-explore-v2');
});
it('_toggleV2Mode should save preference and redirect to /m', async () => {
const redirectStub = sinon.stub(element, 'redirect');
await element['_toggleV2Mode']();
expect(localStorage.getItem('perf:use-explore-v2')).to.equal('false');
expect(redirectStub.calledOnce).to.be.true;
expect(redirectStub.firstCall.args[0]).to.include('/m');
});
});
describe('request ID tracking (race conditions)', () => {
it('discards out-of-order fetch results', async () => {
let resolveFetch1: (value: any) => void = () => {};
let resolveFetch2: (value: any) => void = () => {};
const fetch1Promise = new Promise((resolve) => {
resolveFetch1 = resolve;
});
const fetch2Promise = new Promise((resolve) => {
resolveFetch2 = resolve;
});
let resolveFetch1Started: (value: any) => void = () => {};
let resolveFetch2Started: (value: any) => void = () => {};
const fetch1StartedPromise = new Promise((resolve) => {
resolveFetch1Started = resolve;
});
const fetch2StartedPromise = new Promise((resolve) => {
resolveFetch2Started = resolve;
});
const mockDataService = {
getLinksBatch: async () => ({}),
sendFrameRequest: async (req: any) => {
if (req.trace_ids.includes('t1')) {
resolveFetch1Started(null);
await fetch1Promise;
return {
dataframe: {
header: [{ offset: 10, timestamp: 1000 }],
traceset: { t1: [1.0] },
},
};
} else if (req.trace_ids.includes('t2')) {
resolveFetch2Started(null);
await fetch2Promise;
return {
dataframe: {
header: [{ offset: 10, timestamp: 1000 }],
traceset: { t2: [2.0] },
},
};
}
throw new Error('Unexpected request: ' + JSON.stringify(req));
},
};
const oldInstance = (DataService as any).instance;
(DataService as any).instance = mockDataService;
const oldGet = TraceDatabase.prototype.get;
const oldSet = TraceDatabase.prototype.set;
TraceDatabase.prototype.get = async () => null;
TraceDatabase.prototype.set = async () => {};
const oldSubtle = window.crypto.subtle;
Object.defineProperty(window.crypto, 'subtle', {
get: () => ({
digest: async () => new ArrayBuffer(32),
}),
configurable: true,
});
try {
element['_tracePage'] = 0;
element['_pageSize'] = 10;
element['_seriesData'] = [];
// Start Fetch 1 (requestId = 1)
element['_matchingTraceIds'] = ['t1'];
element['_latestRequestId'] = 1;
const p1 = element['_fetchData'](1);
// Wait for Fetch 1 to reach sendFrameRequest
await fetch1StartedPromise;
// Start Fetch 2 (requestId = 2)
element['_matchingTraceIds'] = ['t2'];
element['_latestRequestId'] = 2;
const p2 = element['_fetchData'](2);
// Wait for Fetch 2 to reach sendFrameRequest
await fetch2StartedPromise;
// Resolve Fetch 2 first
resolveFetch2(null);
await p2;
// Verify Fetch 2 data is applied
expect(element['_seriesData'].map((s: any) => s.id)).to.deep.equal(['t2']);
// Resolve Fetch 1 later
resolveFetch1(null);
await p1;
// Verify Fetch 1 data is DISCARDED (we still have Fetch 2 data)
expect(element['_seriesData'].map((s: any) => s.id)).to.deep.equal(['t2']);
} finally {
(DataService as any).instance = oldInstance;
TraceDatabase.prototype.get = oldGet;
TraceDatabase.prototype.set = oldSet;
Object.defineProperty(window.crypto, 'subtle', {
get: () => oldSubtle,
configurable: true,
});
}
});
});
describe('trace color assignment', () => {
it('assigns unique and stable colors based on position in _seriesData', async () => {
// Set seriesData using the helper method
(element as any)['_updateSeriesData']([
{ id: 't1', rows: [], color: 'red' },
{ id: 't2', rows: [], color: 'red' },
]);
// They should have been reassigned unique colors
const c1 = element['_seriesData'][0].color;
const c2 = element['_seriesData'][1].color;
expect(c1).to.not.equal(c2);
expect(c1).to.equal('hsl(0, 70%, 50%)');
expect(c2).to.equal('hsl(137.5, 70%, 50%)');
// Now add a third series
(element as any)['_updateSeriesData']([
...element['_seriesData'],
{ id: 't3', rows: [], color: 'red' },
]);
const c3 = element['_seriesData'][2].color;
expect(element['_seriesData'][0].color).to.equal(c1); // stable
expect(element['_seriesData'][1].color).to.equal(c2); // stable
expect(c3).to.equal('hsl(275, 70%, 50%)'); // unique
});
});
it('shows loading overlay during worker initialization and hides it when ready', async () => {
const newElement = document.createElement('explore-multi-v2-sk') as ExploreMultiV2Sk;
const WORKER_LOADED_DELAY = 5;
const WORKER_STATUS_DELAY = 10;
const WORKER_READY_DELAY = 20;
const STEP1_WAIT = 2; // Before WORKER_LOADED_DELAY
const STEP2_WAIT = 15; // After WORKER_LOADED_DELAY + WORKER_STATUS_DELAY (5 + 10 = 15)
const STEP3_WAIT = 15; // Wait another 15ms (total 32ms), ensuring we are past the READY event (at 25ms)
let workerOnMessage: ((e: MessageEvent) => void) | null = null;
const mockWorker = {
postMessage: (msg: any) => {
if (msg.type === 'INIT') {
setTimeout(() => {
workerOnMessage!({
data: { type: 'STATUS', payload: { message: 'Loading database...' } },
} as any);
}, WORKER_STATUS_DELAY);
setTimeout(() => {
workerOnMessage!({ data: { type: 'READY' } } as any);
}, WORKER_READY_DELAY);
}
},
terminate: () => {},
};
const originalWorker = window.Worker;
(window as any).Worker = function (_: string) {
const w = Object.create(mockWorker);
Object.defineProperty(w, 'onmessage', {
set: (handler) => {
workerOnMessage = handler;
},
get: () => workerOnMessage,
});
setTimeout(() => {
workerOnMessage!({ data: { type: 'LOADED' } } as any);
}, WORKER_LOADED_DELAY);
return w;
} as any;
try {
document.body.appendChild(newElement);
// 1. Initial connection status
await new Promise((resolve) => setTimeout(resolve, STEP1_WAIT));
let overlay = newElement.shadowRoot!.querySelector('.worker-init-overlay');
let status = newElement.shadowRoot!.querySelector('.worker-init-status');
expect(overlay).to.not.be.null;
expect(status!.textContent).to.equal('Starting worker...');
// 2. Updated status from worker
await new Promise((resolve) => setTimeout(resolve, STEP2_WAIT));
status = newElement.shadowRoot!.querySelector('.worker-init-status');
expect(status!.textContent).to.equal('Loading database...');
// 3. Hidden when READY
await new Promise((resolve) => setTimeout(resolve, STEP3_WAIT));
overlay = newElement.shadowRoot!.querySelector('.worker-init-overlay');
expect(overlay).to.be.null;
} finally {
window.Worker = originalWorker;
newElement.parentNode?.removeChild(newElement);
}
});
it('dismisses loading overlay on worker initialization failure', async () => {
const newElement = document.createElement('explore-multi-v2-sk') as ExploreMultiV2Sk;
let workerOnMessage: ((e: MessageEvent) => void) | null = null;
const mockWorker = {
postMessage: (msg: any) => {
if (msg.type === 'INIT') {
setTimeout(() => {
workerOnMessage!({
data: { type: 'ERROR', payload: { message: 'Failed to fetch Wasm' } },
} as any);
}, 10);
}
},
terminate: () => {},
};
const originalWorker = window.Worker;
(window as any).Worker = function (_: string) {
const w = Object.create(mockWorker);
Object.defineProperty(w, 'onmessage', {
set: (handler) => {
workerOnMessage = handler;
},
get: () => workerOnMessage,
});
setTimeout(() => {
workerOnMessage!({ data: { type: 'LOADED' } } as any);
}, 5);
return w;
} as any;
try {
document.body.appendChild(newElement);
// Poll until overlay is dismissed (should happen after ERROR)
const startTime = Date.now();
while (
newElement.shadowRoot!.querySelector('.worker-init-overlay') &&
Date.now() - startTime < 2000
) {
await new Promise((resolve) => setTimeout(resolve, 10));
await newElement.updateComplete;
}
const overlay = newElement.shadowRoot!.querySelector('.worker-init-overlay');
expect(overlay).to.be.null;
} finally {
window.Worker = originalWorker;
newElement.parentNode?.removeChild(newElement);
}
});
});