chrome-devtools-frontend
Version:
Chrome DevTools UI
1,040 lines (879 loc) • 39.8 kB
text/typescript
// Copyright 2023 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as Common from '../../core/common/common.js';
import * as Host from '../../core/host/host.js';
import {renderElementIntoDOM} from '../../testing/DOMHelpers.js';
import {expectCall, expectCalled} from '../../testing/ExpectStubCall.js';
import {getVeId} from '../../testing/VisualLoggingHelpers.js';
import * as VisualLoggingTesting from './visual_logging-testing.js';
describe('LoggingDriver', () => {
let recordImpression: sinon.SinonStub;
let throttler: Common.Throttler.Throttler;
let throttle: sinon.SinonStub;
let onerror: OnErrorEventHandler;
before(() => {
onerror = window.onerror;
window.onerror = (message, url, lineNumber, column, error) => {
if (message !== 'ResizeObserver loop completed with undelivered notifications.' && onerror) {
onerror.apply(window, [message, url, lineNumber, column, error]);
}
};
});
after(() => {
window.onerror = onerror;
});
beforeEach(() => {
throttler = new Common.Throttler.Throttler(1000000000);
throttle = sinon.stub(throttler, 'schedule');
recordImpression = sinon.stub(
Host.InspectorFrontendHost.InspectorFrontendHostInstance,
'recordImpression',
);
});
afterEach(async () => {
await VisualLoggingTesting.LoggingDriver.stopLogging();
});
function addLoggableElements() {
const parent = document.createElement('div') as HTMLElement;
parent.id = 'parent';
parent.setAttribute('jslog', 'TreeItem; track: hover');
parent.style.width = '300px';
parent.style.height = '300px';
const element = document.createElement('div') as HTMLElement;
element.id = 'element';
element.setAttribute('jslog', 'TreeItem; context:42; track: click, keydown, hover, drag, resize, change');
element.style.width = '300px';
element.style.height = '300px';
parent.appendChild(element);
renderElementIntoDOM(parent);
}
it('logs impressions on startLogging', async () => {
addLoggableElements();
await VisualLoggingTesting.LoggingDriver.startLogging();
sinon.assert.calledOnce(recordImpression);
assert.sameDeepMembers(recordImpression.firstCall.firstArg.impressions, [
{id: getVeId('#element'), type: 1, context: 42, parent: getVeId('#parent'), width: 300, height: 300},
{id: getVeId('#parent'), type: 1, width: 300, height: 300},
]);
});
async function assertImpressionRecordedDeferred() {
const [work] = await expectCalled(throttle);
sinon.assert.notCalled(recordImpression);
await work();
sinon.assert.called(recordImpression);
}
it('does not log impressions when document hidden', async () => {
addLoggableElements();
sinon.stub(document, 'hidden').value(true);
await VisualLoggingTesting.LoggingDriver.startLogging({processingThrottler: throttler});
sinon.assert.notCalled(recordImpression);
});
it('does not log impressions when parent hidden', async () => {
addLoggableElements();
const parent = document.getElementById('parent') as HTMLElement;
parent.style.height = '0';
await VisualLoggingTesting.LoggingDriver.startLogging({processingThrottler: throttler});
sinon.assert.notCalled(recordImpression);
});
it('logs impressions when visibility changes', async () => {
let hidden = true;
addLoggableElements();
sinon.stub(document, 'hidden').get(() => hidden);
await VisualLoggingTesting.LoggingDriver.startLogging({processingThrottler: throttler});
hidden = false;
const event = document.createEvent('Event');
event.initEvent('visibilitychange', true, true);
document.dispatchEvent(event);
await assertImpressionRecordedDeferred();
});
it('logs impressions on scroll', async () => {
addLoggableElements();
const parent = document.getElementById('parent') as HTMLElement;
parent.style.marginTop = '2000px';
await VisualLoggingTesting.LoggingDriver.startLogging({processingThrottler: throttler});
const scrollend = sinon.stub();
window.addEventListener('scrollend', scrollend);
window.scrollTo({
top: 2000,
left: 0,
behavior: 'instant',
});
await expectCalled(scrollend);
await assertImpressionRecordedDeferred();
scrollend.resetHistory();
window.scrollTo({
top: 0,
left: 0,
behavior: 'instant',
});
await expectCalled(scrollend);
});
it('logs impressions on mutation', async () => {
await VisualLoggingTesting.LoggingDriver.startLogging({processingThrottler: throttler});
addLoggableElements();
await assertImpressionRecordedDeferred();
});
it('logs impressions on mutation in shadow DOM', async () => {
const parent = document.createElement('div') as HTMLElement;
renderElementIntoDOM(parent);
const shadow = parent.attachShadow({mode: 'open'});
const shadowContent = document.createElement('div');
shadow.appendChild(shadowContent);
await VisualLoggingTesting.LoggingDriver.startLogging({processingThrottler: throttler});
shadowContent.innerHTML = '<div jslog="TreeItem" style="width:300px;height:300px"></div>';
await assertImpressionRecordedDeferred();
});
it('logs impressions on mutation in additional document', async () => {
const iframe = document.createElement('iframe');
renderElementIntoDOM(iframe);
await VisualLoggingTesting.LoggingDriver.startLogging({processingThrottler: throttler});
const iframeDocument = iframe.contentDocument;
assert.exists(iframeDocument);
await VisualLoggingTesting.LoggingDriver.addDocument(iframeDocument);
iframeDocument.body.innerHTML = '<div jslog="TreeItem" style="width:300px;height:300px"></div>';
await assertImpressionRecordedDeferred();
});
it('correctly determines visibility in additional document', async () => {
const iframe = document.createElement('iframe');
renderElementIntoDOM(iframe);
iframe.style.width = '100px';
iframe.style.height = '100px';
iframe.width = '100';
iframe.height = '100';
const iframeDocument = iframe.contentDocument;
assert.exists(iframeDocument);
iframeDocument.body.innerHTML = // Second div should not be out of viewport and not logged
`<div style="width:150px;height:150px"></div>
<div jslog="TreeItem" style="width:150px;height:150px"></div>`;
await VisualLoggingTesting.LoggingDriver.startLogging({processingThrottler: throttler});
await VisualLoggingTesting.LoggingDriver.addDocument(iframeDocument);
sinon.assert.notCalled(recordImpression);
});
it('hashes a string context', async () => {
const element = document.createElement('div') as HTMLElement;
element.setAttribute('jslog', 'TreeItem; track: hover; context: foobar');
element.style.width = '300px';
element.style.height = '300px';
renderElementIntoDOM(element);
await VisualLoggingTesting.LoggingDriver.startLogging();
sinon.assert.calledOnce(recordImpression);
assert.strictEqual(recordImpression.firstCall.firstArg.impressions[0]?.context, -103332984);
});
it('logs clicks', async () => {
addLoggableElements();
await VisualLoggingTesting.LoggingDriver.startLogging();
const recordClick = sinon.stub(
Host.InspectorFrontendHost.InspectorFrontendHostInstance,
'recordClick',
);
const element = document.getElementById('element') as HTMLElement;
element.click();
await expectCalled(recordClick);
});
it('logs right clicks', async () => {
addLoggableElements();
await VisualLoggingTesting.LoggingDriver.startLogging();
const recordClick = sinon.stub(
Host.InspectorFrontendHost.InspectorFrontendHostInstance,
'recordClick',
);
const element = document.getElementById('element') as HTMLElement;
element.dispatchEvent(new MouseEvent('contextmenu'));
await expectCalled(recordClick);
});
it('logs middle clicks', async () => {
addLoggableElements();
await VisualLoggingTesting.LoggingDriver.startLogging();
const recordClick = sinon.stub(
Host.InspectorFrontendHost.InspectorFrontendHostInstance,
'recordClick',
);
const element = document.getElementById('element') as HTMLElement;
element.dispatchEvent(new MouseEvent('auxclick'));
await expectCalled(recordClick);
});
it('does not log clicks if not configured', async () => {
addLoggableElements();
await VisualLoggingTesting.LoggingDriver.startLogging();
const recordClick = sinon.stub(
Host.InspectorFrontendHost.InspectorFrontendHostInstance,
'recordClick',
);
const parent = document.getElementById('parent') as HTMLElement;
parent.click();
await new Promise(resolve => setTimeout(resolve, 0));
sinon.assert.notCalled(recordClick);
});
it('does not log click on double click', async () => {
addLoggableElements();
const element = document.getElementById('element') as HTMLElement;
element.setAttribute('jslog', 'TreeItem; context:42; track: click, dblclick');
await VisualLoggingTesting.LoggingDriver.startLogging({clickLogThrottler: throttler});
const recordClick = sinon.stub(
Host.InspectorFrontendHost.InspectorFrontendHostInstance,
'recordClick',
);
element.dispatchEvent(new MouseEvent('click'));
element.dispatchEvent(new MouseEvent('dblclick'));
const [logging] = await expectCalled(throttle);
sinon.assert.calledTwice(throttle);
sinon.assert.notCalled(recordClick);
await logging();
sinon.assert.calledOnce(recordClick);
assert.isTrue(recordClick.firstCall.firstArg.doubleClick);
});
it('does not log click on parent when clicked on child', async () => {
addLoggableElements();
const parent = document.getElementById('parent') as HTMLElement;
parent.setAttribute('jslog', 'TreeItem; track: click');
await VisualLoggingTesting.LoggingDriver.startLogging({clickLogThrottler: throttler});
const recordClick = sinon.stub(
Host.InspectorFrontendHost.InspectorFrontendHostInstance,
'recordClick',
);
const element = document.getElementById('element') as HTMLElement;
element.click();
const [logging] = await expectCalled(throttle);
sinon.assert.notCalled(recordClick);
await logging();
sinon.assert.calledOnce(recordClick);
assert.strictEqual(recordClick.firstCall.firstArg.veid, getVeId(element));
});
const logsSelectOptions = (event: Event) => async () => {
const parent = document.createElement('div') as HTMLElement;
parent.innerHTML = `
<select jslog="TreeItem; context: 0" id="select" style="width: 30px; height: 20px">
<option jslog="TreeItem; context: 1">1</option>
<option jslog="TreeItem; context: 2">2</option>
</select>`;
renderElementIntoDOM(parent);
const select = document.getElementById('select')!;
const recordClick = sinon.stub(
Host.InspectorFrontendHost.InspectorFrontendHostInstance,
'recordClick',
);
await VisualLoggingTesting.LoggingDriver.startLogging(
{processingThrottler: throttler, clickLogThrottler: throttler});
sinon.assert.calledOnce(recordImpression);
const impressions = recordImpression.firstCall.firstArg.impressions;
assert.sameDeepMembers(impressions, [
{id: getVeId(select), type: 1, width: 30, height: 20, context: 0},
]);
recordImpression.resetHistory();
throttle.callsArg(0);
select.dispatchEvent(event);
await expectCalled(recordClick);
sinon.assert.calledOnce(recordClick);
assert.strictEqual(recordClick.firstCall.firstArg.veid, getVeId(select));
await expectCalled(recordImpression);
sinon.assert.calledOnce(recordImpression);
assert.sameDeepMembers(recordImpression.firstCall.firstArg.impressions, [
{id: getVeId('option:first-child'), type: 1, parent: getVeId(select), context: 1, width: 0, height: 0},
{id: getVeId('option:last-child'), type: 1, parent: getVeId(select), context: 2, width: 0, height: 0},
]);
};
it('logs impressions on select options on click', logsSelectOptions(new MouseEvent('click')));
it('logs impressions on select options on space press', logsSelectOptions(new KeyboardEvent('keypress', {key: ' '})));
it('logs impressions on select options on F4', logsSelectOptions(new KeyboardEvent('keydown', {code: 'F4'})));
it('logs option click on select change', async () => {
const parent = document.createElement('div') as HTMLElement;
parent.innerHTML = `
<select jslog="TreeItem; context: 0" id="select">
<option jslog="TreeItem; context: 1; track: click">1</option>
<option jslog="TreeItem; context: 2; track: click">2</option>
</select>`;
renderElementIntoDOM(parent);
await VisualLoggingTesting.LoggingDriver.startLogging({clickLogThrottler: throttler});
const recordClick = sinon.stub(
Host.InspectorFrontendHost.InspectorFrontendHostInstance,
'recordClick',
);
const select = document.getElementById('select') as HTMLSelectElement;
assert.exists(select);
select.selectedIndex = 1;
select.dispatchEvent(new Event('change'));
await expectCalled(throttle).then(([logging]) => logging());
sinon.assert.calledOnce(recordClick);
assert.deepEqual(recordClick.firstCall.firstArg, {veid: getVeId(select.selectedOptions[0]), doubleClick: false});
});
it('logs keydown', async () => {
addLoggableElements();
await VisualLoggingTesting.LoggingDriver.startLogging({keyboardLogThrottler: throttler});
const recordKeyDown = sinon.stub(
Host.InspectorFrontendHost.InspectorFrontendHostInstance,
'recordKeyDown',
);
const element = document.getElementById('element') as HTMLElement;
element.dispatchEvent(new KeyboardEvent('keydown', {key: 'a'}));
element.dispatchEvent(new KeyboardEvent('keydown', {key: 'b'}));
const [logging] = await expectCalled(throttle);
sinon.assert.calledTwice(throttle);
sinon.assert.notCalled(recordKeyDown);
await logging();
sinon.assert.calledOnce(recordKeyDown);
});
it('logs keydown for specific codes', async () => {
addLoggableElements();
const element = document.getElementById('element') as HTMLElement;
element.setAttribute('jslog', 'TreeItem; context:42; track: keydown: KeyA|KeyB');
await VisualLoggingTesting.LoggingDriver.startLogging({keyboardLogThrottler: throttler});
const recordKeyDown = sinon.stub(
Host.InspectorFrontendHost.InspectorFrontendHostInstance,
'recordKeyDown',
);
element.dispatchEvent(new KeyboardEvent('keydown', {code: 'KeyC'}));
await new Promise(resolve => setTimeout(resolve, 0));
sinon.assert.notCalled(throttle);
element.dispatchEvent(new KeyboardEvent('keydown', {code: 'KeyA'}));
let [logging] = await expectCalled(throttle);
sinon.assert.notCalled(recordKeyDown);
await logging();
sinon.assert.calledOnce(recordKeyDown);
recordKeyDown.resetHistory();
element.dispatchEvent(new KeyboardEvent('keydown', {code: 'KeyB'}));
[logging] = await expectCalled(throttle);
sinon.assert.notCalled(recordKeyDown);
await logging();
sinon.assert.calledOnce(recordKeyDown);
});
it('logs change', async () => {
addLoggableElements();
await VisualLoggingTesting.LoggingDriver.startLogging();
const recordChange = sinon.stub(
Host.InspectorFrontendHost.InspectorFrontendHostInstance,
'recordChange',
);
const element = document.getElementById('element') as HTMLElement;
element.dispatchEvent(new Event('change'));
sinon.assert.calledOnce(recordChange);
});
it('logs change for each input type', async () => {
addLoggableElements();
await VisualLoggingTesting.LoggingDriver.startLogging();
const recordChange = sinon.stub(
Host.InspectorFrontendHost.InspectorFrontendHostInstance,
'recordChange',
);
const element = document.getElementById('element') as HTMLElement;
element.dispatchEvent(new InputEvent('input', {inputType: 'insertText'}));
await new Promise(resolve => setTimeout(resolve, 0));
sinon.assert.notCalled(recordChange);
element.dispatchEvent(new InputEvent('input', {inputType: 'insertText'}));
await new Promise(resolve => setTimeout(resolve, 0));
sinon.assert.notCalled(recordChange);
let logging = expectCalled(recordChange);
element.dispatchEvent(new InputEvent('input', {inputType: 'inserFromPaste'}));
await logging;
logging = expectCalled(recordChange);
element.dispatchEvent(new InputEvent('input', {inputType: 'inserFromDrop'}));
await logging;
logging = expectCalled(recordChange);
element.dispatchEvent(new Event('change'));
await logging;
});
it('logs change on focus out after input', async () => {
addLoggableElements();
await VisualLoggingTesting.LoggingDriver.startLogging();
const recordChange = sinon.stub(
Host.InspectorFrontendHost.InspectorFrontendHostInstance,
'recordChange',
);
const element = document.getElementById('element') as HTMLElement;
element.dispatchEvent(new InputEvent('input', {inputType: 'insertText'}));
element.dispatchEvent(new Event('focusout'));
await expectCalled(recordChange);
});
it('logs change on new impressions', async () => {
addLoggableElements();
await VisualLoggingTesting.LoggingDriver.startLogging({processingThrottler: throttler});
const recordChange = sinon.stub(
Host.InspectorFrontendHost.InspectorFrontendHostInstance,
'recordChange',
);
const element = document.getElementById('element') as HTMLElement;
const parent = document.getElementById('parent') as HTMLElement;
element.dispatchEvent(new InputEvent('input', {inputType: 'insertText'}));
throttle.callsArg(0);
parent.appendChild(element.cloneNode());
await expectCalled(recordChange);
});
it('logs change on resize', async () => {
addLoggableElements();
await VisualLoggingTesting.LoggingDriver.startLogging({resizeLogThrottler: throttler});
const recordChange = sinon.stub(
Host.InspectorFrontendHost.InspectorFrontendHostInstance,
'recordChange',
);
const element = document.getElementById('element') as HTMLElement;
element.dispatchEvent(new InputEvent('input', {inputType: 'insertText'}));
throttle.callsArg(0);
element.style.width = '400px';
await expectCalled(recordChange);
});
it('does not log change on focus out without input', async () => {
addLoggableElements();
await VisualLoggingTesting.LoggingDriver.startLogging();
const recordChange = sinon.stub(
Host.InspectorFrontendHost.InspectorFrontendHostInstance,
'recordChange',
);
const element = document.getElementById('element') as HTMLElement;
element.dispatchEvent(new Event('focusout'));
await new Promise(resolve => setTimeout(resolve, 0));
assert.isFalse(recordChange.calledOnce);
});
it('logs state with change of a checkbox', async () => {
const recordChange = sinon.stub(
Host.InspectorFrontendHost.InspectorFrontendHostInstance,
'recordChange',
);
const element = document.createElement('input');
element.setAttribute('jslog', 'TreeItem; track: change');
element.type = 'checkbox';
element.checked = true;
renderElementIntoDOM(element);
await VisualLoggingTesting.LoggingDriver.startLogging();
let logging = expectCall(recordChange);
element.dispatchEvent(new Event('change'));
let [event] = await logging;
assert.strictEqual(event.context, 1530936795);
element.checked = false;
logging = expectCall(recordChange);
element.dispatchEvent(new Event('change'));
[event] = await logging;
assert.strictEqual(event.context, 1936227034);
});
it('logs state with change of a radio', async () => {
const recordChange = sinon.stub(
Host.InspectorFrontendHost.InspectorFrontendHostInstance,
'recordChange',
);
const element = document.createElement('input');
element.setAttribute('jslog', 'TreeItem; track: change');
element.type = 'radio';
element.checked = true;
renderElementIntoDOM(element);
await VisualLoggingTesting.LoggingDriver.startLogging();
let logging = expectCall(recordChange);
element.dispatchEvent(new Event('change'));
let [event] = await logging;
assert.strictEqual(event.context, 1530936795);
element.checked = false;
logging = expectCall(recordChange);
element.dispatchEvent(new Event('change'));
[event] = await logging;
assert.strictEqual(event.context, 1936227034);
});
it('logs state with change of a label`s control', async () => {
const recordChange = sinon.stub(
Host.InspectorFrontendHost.InspectorFrontendHostInstance,
'recordChange',
);
const label = document.createElement('label');
label.setAttribute('jslog', 'TreeItem; track: change');
const input = document.createElement('input');
input.type = 'radio';
input.checked = true;
input.style.display = 'none';
label.appendChild(input);
renderElementIntoDOM(label);
await VisualLoggingTesting.LoggingDriver.startLogging();
let logging = expectCall(recordChange);
input.dispatchEvent(new Event('change'));
let [event] = await logging;
assert.strictEqual(event.context, 1530936795);
input.checked = false;
logging = expectCall(recordChange);
input.dispatchEvent(new Event('change'));
[event] = await logging;
assert.strictEqual(event.context, 1936227034);
});
it('logs hover', async () => {
addLoggableElements();
await VisualLoggingTesting.LoggingDriver.startLogging({hoverLogThrottler: throttler});
const recordHover = sinon.stub(
Host.InspectorFrontendHost.InspectorFrontendHostInstance,
'recordHover',
);
const element = document.getElementById('element') as HTMLElement;
element.dispatchEvent(new MouseEvent('mouseover'));
const [logging] = await expectCalled(throttle);
sinon.assert.notCalled(recordHover);
await logging();
sinon.assert.calledOnce(recordHover);
});
it('does not log hover if too short', async () => {
addLoggableElements();
await VisualLoggingTesting.LoggingDriver.startLogging({hoverLogThrottler: throttler});
const recordHover = sinon.stub(
Host.InspectorFrontendHost.InspectorFrontendHostInstance,
'recordHover',
);
const element = document.getElementById('element') as HTMLElement;
element.dispatchEvent(new MouseEvent('mouseover'));
await expectCalled(throttle);
sinon.assert.notCalled(recordHover);
element.dispatchEvent(new MouseEvent('mouseout'));
await expectCalled(throttle).then(([work]) => work());
sinon.assert.notCalled(recordHover);
});
it('does not log hover if in descendent', async () => {
addLoggableElements();
await VisualLoggingTesting.LoggingDriver.startLogging({hoverLogThrottler: throttler});
const recordHover = sinon.stub(
Host.InspectorFrontendHost.InspectorFrontendHostInstance,
'recordHover',
);
const parent = document.getElementById('parent') as HTMLElement;
const element = document.getElementById('element') as HTMLElement;
parent.dispatchEvent(new MouseEvent('mouseover'));
await expectCalled(throttle);
throttle.resetHistory();
element.dispatchEvent(new MouseEvent('mouseover'));
await expectCalled(throttle).then(([work]) => work());
sinon.assert.called(recordHover);
assert.deepEqual(recordHover.firstCall.firstArg, {veid: getVeId(element)});
});
it('logs drag', async () => {
const dragLogThrottler = new Common.Throttler.Throttler(1000000000);
addLoggableElements();
await VisualLoggingTesting.LoggingDriver.startLogging({dragLogThrottler});
const recordDrag = sinon.stub(
Host.InspectorFrontendHost.InspectorFrontendHostInstance,
'recordDrag',
);
const element = document.getElementById('element') as HTMLElement;
element.dispatchEvent(new MouseEvent('pointerdown'));
assert.exists(dragLogThrottler.process);
sinon.assert.notCalled(recordDrag);
await dragLogThrottler.process?.();
sinon.assert.called(recordDrag);
sinon.assert.calledOnce(recordDrag);
});
it('does not log drag if too short in time', async () => {
const dragLogThrottler = new Common.Throttler.Throttler(1000000000);
addLoggableElements();
await VisualLoggingTesting.LoggingDriver.startLogging({dragLogThrottler});
const recordDrag = sinon.stub(
Host.InspectorFrontendHost.InspectorFrontendHostInstance,
'recordDrag',
);
const element = document.getElementById('element') as HTMLElement;
element.dispatchEvent(new MouseEvent('pointerdown'));
assert.exists(dragLogThrottler.process);
sinon.assert.notCalled(recordDrag);
element.dispatchEvent(new MouseEvent('pointerup'));
await dragLogThrottler.process?.();
sinon.assert.notCalled(recordDrag);
});
it('logs drag if short in time but long in distance', async () => {
addLoggableElements();
await VisualLoggingTesting.LoggingDriver.startLogging({dragLogThrottler: throttler});
const recordDrag = sinon.stub(
Host.InspectorFrontendHost.InspectorFrontendHostInstance,
'recordDrag',
);
const element = document.getElementById('element') as HTMLElement;
element.dispatchEvent(new MouseEvent('pointerdown', {screenX: 0, screenY: 0}));
await expectCalled(throttle);
sinon.assert.notCalled(recordDrag);
element.dispatchEvent(new MouseEvent('pointerup', {screenX: 100, screenY: 100}));
await throttler.process?.();
sinon.assert.notCalled(recordDrag);
});
it('logs resize', async () => {
addLoggableElements();
await VisualLoggingTesting.LoggingDriver.startLogging({resizeLogThrottler: throttler});
const recordResize = sinon.stub(
Host.InspectorFrontendHost.InspectorFrontendHostInstance,
'recordResize',
);
const element = document.getElementById('element') as HTMLElement;
element.style.height = '400px';
const [logging] = await expectCall(throttle, {callCount: 2});
sinon.assert.notCalled(recordResize);
await logging();
sinon.assert.calledOnce(recordResize);
});
it('does not log resize if too small', async () => {
addLoggableElements();
await VisualLoggingTesting.LoggingDriver.startLogging({resizeLogThrottler: throttler});
const recordResize = sinon.stub(
Host.InspectorFrontendHost.InspectorFrontendHostInstance,
'recordResize',
);
const element = document.getElementById('element') as HTMLElement;
element.style.height = '301px';
sinon.assert.notCalled(recordResize);
});
it('logs resize on visibility change', async () => {
addLoggableElements();
await VisualLoggingTesting.LoggingDriver.startLogging({resizeLogThrottler: throttler});
const recordResize = sinon.stub(
Host.InspectorFrontendHost.InspectorFrontendHostInstance,
'recordResize',
);
const element = document.getElementById('element') as HTMLElement;
element.style.display = 'none';
const [logging] = await expectCall(throttle, {callCount: 2});
sinon.assert.notCalled(recordResize);
logging();
await expectCalled(recordResize);
sinon.assert.calledOnce(recordResize);
assert.deepEqual(recordResize.firstCall.firstArg, {veid: getVeId(element), width: 0, height: 0});
recordResize.resetHistory();
element.style.display = 'block';
sinon.assert.notCalled(recordResize);
throttle.callsArg(0);
await expectCall(recordResize);
sinon.assert.calledOnce(recordResize);
assert.deepEqual(recordResize.firstCall.firstArg, {veid: getVeId(element), width: 300, height: 300});
});
it('throttles resize per element', async () => {
addLoggableElements();
const element1 = document.getElementById('element') as HTMLElement;
const element2 = element1.cloneNode() as HTMLElement;
document.getElementById('parent')?.appendChild(element2);
await VisualLoggingTesting.LoggingDriver.startLogging({resizeLogThrottler: throttler});
const recordResize = sinon.stub(
Host.InspectorFrontendHost.InspectorFrontendHostInstance,
'recordResize',
);
element1.style.height = '200px';
await expectCall(throttle, {callCount: 2});
element2.style.height = '200px';
await expectCall(throttle, {callCount: 2});
element1.style.height = '10px';
await expectCall(throttle, {callCount: 2});
element2.style.height = '10px';
const [work] = await expectCall(throttle, {callCount: 2});
sinon.assert.notCalled(recordResize);
await work();
sinon.assert.calledTwice(recordResize);
assert.strictEqual(recordResize.firstCall.firstArg.height, 10);
assert.strictEqual(recordResize.lastCall.firstArg.height, 10);
assert.notStrictEqual(recordResize.firstCall.firstArg.veid, recordResize.lastCall.firstArg.veid);
});
it('only logs resize of the outer element', async () => {
addLoggableElements();
const element = document.getElementById('element') as HTMLElement;
const child = document.createElement('div');
child.setAttribute('jslog', 'TreeItem; track: resize');
child.style.width = '100%';
child.style.height = '100%';
element.appendChild(child);
await VisualLoggingTesting.LoggingDriver.startLogging({resizeLogThrottler: throttler});
const recordResize = sinon.stub(
Host.InspectorFrontendHost.InspectorFrontendHostInstance,
'recordResize',
);
element.style.width = '400px';
const [work] = await expectCall(throttle, {callCount: 2});
sinon.assert.notCalled(recordResize);
await work();
await expectCalled(recordResize);
sinon.assert.calledOnce(recordResize);
assert.deepEqual(recordResize.firstCall.firstArg, {veid: getVeId(element), width: 400, height: 300});
});
it('does not log resize intial impressions due to visibility change', async () => {
addLoggableElements();
const element = document.getElementById('element') as HTMLElement;
element.style.display = 'none';
await VisualLoggingTesting.LoggingDriver.startLogging(
{processingThrottler: throttler, resizeLogThrottler: throttler});
const recordResize = sinon.stub(
Host.InspectorFrontendHost.InspectorFrontendHostInstance,
'recordResize',
);
recordImpression.resetHistory();
element.style.display = 'block';
await expectCalled(throttle).then(([work]) => work());
sinon.assert.calledOnce(throttle);
sinon.assert.calledOnce(recordImpression);
sinon.assert.notCalled(recordResize);
await new Promise(resolve => setTimeout(resolve, 0));
sinon.assert.notCalled(recordResize);
});
it('properly handles the switch between visible elements', async () => {
addLoggableElements();
const element1 = document.getElementById('element') as HTMLElement;
const child = document.createElement('div');
child.id = 'child';
child.setAttribute('jslog', 'TreeItem; track: resize');
child.style.width = '100%';
child.style.height = '100%';
element1.appendChild(child);
const element2 = element1.cloneNode(/* deep=*/ true) as HTMLElement;
element2.id = 'element2';
document.getElementById('parent')?.appendChild(element2);
// First ensure both top level elements have impressions logged
await VisualLoggingTesting.LoggingDriver.startLogging({resizeLogThrottler: throttler});
const recordResize = sinon.stub(
Host.InspectorFrontendHost.InspectorFrontendHostInstance,
'recordResize',
);
// Now hide one and wait for logging to finish
throttle.callsArg(0);
element2.style.display = 'none';
await expectCalled(recordResize, {callCount: 1});
throttle.reset();
recordResize.reset();
// Now the actual test: hiding one element and show the other one
element1.style.display = 'none';
element2.style.display = 'block';
// Throttler is called by both resize and intersection observer for each element
await expectCalled(throttle, {callCount: 4}).then(([work]) => work());
sinon.assert.calledTwice(recordResize);
assert.sameDeepMembers(recordResize.getCalls().map(c => c.firstArg), [
{veid: VisualLoggingTesting.LoggingState.getLoggingState(element1)?.veid, width: 0, height: 0},
{veid: VisualLoggingTesting.LoggingState.getLoggingState(element2)?.veid, width: 300, height: 300},
]);
});
it('logs resize when removed from DOM', async () => {
addLoggableElements();
await VisualLoggingTesting.LoggingDriver.startLogging({resizeLogThrottler: throttler});
const recordResize = sinon.stub(
Host.InspectorFrontendHost.InspectorFrontendHostInstance,
'recordResize',
);
const element = document.getElementById('element') as HTMLElement;
const parent = document.getElementById('parent') as HTMLElement;
parent.removeChild(element);
const [logging] = await expectCall(throttle, {callCount: 2});
sinon.assert.notCalled(recordResize);
await logging();
sinon.assert.calledOnce(recordResize);
assert.deepEqual(recordResize.firstCall.firstArg, {veid: getVeId(element), width: 0, height: 0});
});
it('logs click, then resize, then impressions', async () => {
addLoggableElements();
const processingThrottler = new Common.Throttler.Throttler(10);
const clickLogThrottler = new Common.Throttler.Throttler(100);
const keyboardLogThrottler = new Common.Throttler.Throttler(100);
const resizeLogThrottler = new Common.Throttler.Throttler(100);
await VisualLoggingTesting.LoggingDriver.startLogging({
processingThrottler,
clickLogThrottler,
keyboardLogThrottler,
resizeLogThrottler,
});
const recordResize = sinon.stub(
Host.InspectorFrontendHost.InspectorFrontendHostInstance,
'recordResize',
);
const recordClick = sinon.stub(
Host.InspectorFrontendHost.InspectorFrontendHostInstance,
'recordClick',
);
recordImpression.resetHistory();
const element = document.getElementById('element') as HTMLElement;
const parent = document.getElementById('parent') as HTMLElement;
parent.removeChild(element);
parent.appendChild(element.cloneNode());
element.click();
await Promise.all([
expectCalled(recordImpression),
expectCalled(recordResize),
expectCalled(recordClick),
]);
assert.isTrue(recordClick.calledBefore(recordResize));
assert.isTrue(recordResize.calledBefore(recordImpression));
});
it('logs keydown, then resize, then impressions', async () => {
addLoggableElements();
const element = document.getElementById('element') as HTMLElement;
element.setAttribute('jslog', 'TreeItem; context:42; track: keydown: KeyA, resize');
const keyboardLogThrottler = new Common.Throttler.Throttler(100);
const resizeLogThrottler = new Common.Throttler.Throttler(100);
await VisualLoggingTesting.LoggingDriver.startLogging({
processingThrottler: throttler,
keyboardLogThrottler,
resizeLogThrottler,
});
const recordResize = sinon.stub(
Host.InspectorFrontendHost.InspectorFrontendHostInstance,
'recordResize',
);
const recordKeyDown = sinon.stub(
Host.InspectorFrontendHost.InspectorFrontendHostInstance,
'recordKeyDown',
);
recordImpression.resetHistory();
throttle.callsArg(0);
const parent = document.getElementById('parent') as HTMLElement;
parent.removeChild(element);
parent.appendChild(element.cloneNode());
element.dispatchEvent(new KeyboardEvent('keydown', {code: 'KeyA', key: 'a'}));
await Promise.all([
expectCalled(recordImpression),
expectCalled(recordResize),
expectCalled(recordKeyDown),
]);
assert.isTrue(recordKeyDown.calledBefore(recordResize));
assert.isTrue(recordResize.calledBefore(recordImpression));
});
it('logs non-DOM impressions', async () => {
addLoggableElements();
const loggable = {};
const parent = document.getElementById('parent')!;
VisualLoggingTesting.NonDomState.registerLoggable(loggable, {ve: 1, context: '123'}, parent);
await VisualLoggingTesting.LoggingDriver.startLogging();
sinon.assert.calledOnce(recordImpression);
assert.sameDeepMembers(recordImpression.firstCall.firstArg.impressions, [
{id: getVeId(loggable), type: 1, context: 123, parent: getVeId(parent), width: 0, height: 0},
{id: getVeId('#element'), type: 1, context: 42, parent: getVeId(parent), width: 300, height: 300},
{id: getVeId(parent), type: 1, width: 300, height: 300},
]);
});
it('logs non-DOM impressions after parent was logged', async () => {
addLoggableElements();
const loggable1 = {};
const parent = document.getElementById('parent')!;
await VisualLoggingTesting.LoggingDriver.startLogging();
sinon.assert.calledOnce(recordImpression);
VisualLoggingTesting.NonDomState.registerLoggable(loggable1, {ve: 1, context: '123'}, parent);
recordImpression.resetHistory();
await VisualLoggingTesting.LoggingDriver.scheduleProcessing();
await expectCalled(recordImpression);
assert.sameDeepMembers(recordImpression.lastCall.firstArg.impressions, [
{id: getVeId(loggable1), type: 1, context: 123, parent: getVeId(parent), width: 0, height: 0},
]);
recordImpression.resetHistory();
const loggable2 = {};
VisualLoggingTesting.NonDomState.registerLoggable(loggable2, {ve: 1, context: '345'}, parent);
await VisualLoggingTesting.LoggingDriver.scheduleProcessing();
await expectCalled(recordImpression);
assert.sameDeepMembers(recordImpression.lastCall.firstArg.impressions, [
{id: getVeId(loggable2), type: 1, context: 345, parent: getVeId(parent), width: 0, height: 0},
]);
});
it('logs root non-DOM impressions', async () => {
addLoggableElements();
const loggable = {};
VisualLoggingTesting.NonDomState.registerLoggable(loggable, {ve: 1, context: '123'}, undefined);
await VisualLoggingTesting.LoggingDriver.startLogging();
sinon.assert.calledOnce(recordImpression);
assert.sameDeepMembers(recordImpression.firstCall.firstArg.impressions, [
{id: getVeId(loggable), type: 1, context: 123, width: 0, height: 0},
{id: getVeId('#element'), type: 1, context: 42, parent: getVeId('#parent'), width: 300, height: 300},
{id: getVeId('#parent'), type: 1, width: 300, height: 300},
]);
assert.isEmpty(VisualLoggingTesting.NonDomState.getNonDomLoggables());
});
it('postpones logging non-DOM impressions with detached parent', async () => {
addLoggableElements();
const loggable = {};
const parent = document.createElement('div');
VisualLoggingTesting.NonDomState.registerLoggable(loggable, {ve: 1, context: '123'}, parent);
await VisualLoggingTesting.LoggingDriver.startLogging();
sinon.assert.calledOnce(recordImpression);
assert.sameDeepMembers(recordImpression.firstCall.firstArg.impressions, [
{id: getVeId('#element'), type: 1, context: 42, parent: getVeId('#parent'), width: 300, height: 300},
{id: getVeId('#parent'), type: 1, width: 300, height: 300},
]);
assert.deepInclude(
VisualLoggingTesting.NonDomState.getNonDomLoggables(parent),
{loggable, config: {ve: 1, context: '123'}, parent, size: undefined});
});
});