chrome-devtools-frontend
Version:
Chrome DevTools UI
296 lines (258 loc) • 11.9 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 Root from '../../core/root/root.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as Protocol from '../../generated/protocol.js';
import * as AutofillManager from '../../models/autofill_manager/autofill_manager.js';
import {assertGridContents} from '../../testing/DataGridHelpers.js';
import {renderElementIntoDOM} from '../../testing/DOMHelpers.js';
import {createTarget, stubNoopSettings} from '../../testing/EnvironmentHelpers.js';
import {describeWithMockConnection} from '../../testing/MockConnection.js';
import {getMainFrame, navigate} from '../../testing/ResourceTreeHelpers.js';
import * as RenderCoordinator from '../../ui/components/render_coordinator/render_coordinator.js';
import * as UI from '../../ui/legacy/legacy.js';
import * as Autofill from './autofill.js';
const addressFormFilledEvent = {
addressUi: {
addressFields: [
{
fields: [
{name: 'NAME_FULL', value: 'Crocodile Middle Dundee'},
],
},
{
fields: [
{name: 'COMPANY_NAME', value: 'Uluru Tours'},
],
},
{
fields: [
{name: 'ADDRESS_HOME_STREET_ADDRESS', value: 'Outback Road 1'},
],
},
{
fields: [
{name: 'ADDRESS_HOME_CITY', value: 'Bundaberg'},
{name: 'ADDRESS_HOME_STATE', value: 'Queensland'},
{name: 'ADDRESS_HOME_ZIP', value: '12345'},
],
},
],
},
filledFields: [
{
htmlType: 'text',
id: 'input1',
name: '',
value: 'Crocodile',
autofillType: 'First name',
fillingStrategy: Protocol.Autofill.FillingStrategy.AutofillInferred,
fieldId: 1 as Protocol.DOM.BackendNodeId,
frameId: '1' as Protocol.Page.FrameId,
},
{
htmlType: 'text',
id: '',
name: 'input2',
value: 'Dundee',
autofillType: 'Last name',
fillingStrategy: Protocol.Autofill.FillingStrategy.AutofillInferred,
fieldId: 2 as Protocol.DOM.BackendNodeId,
frameId: '1' as Protocol.Page.FrameId,
},
{
htmlType: 'text',
id: 'input3',
name: '',
value: 'Australia',
autofillType: 'Country',
fillingStrategy: Protocol.Autofill.FillingStrategy.AutofillInferred,
fieldId: 3 as Protocol.DOM.BackendNodeId,
frameId: '1' as Protocol.Page.FrameId,
},
{
htmlType: 'text',
id: 'input4',
name: '',
value: '12345',
autofillType: 'Zip code',
fillingStrategy: Protocol.Autofill.FillingStrategy.AutocompleteAttribute,
fieldId: 4 as Protocol.DOM.BackendNodeId,
frameId: '1' as Protocol.Page.FrameId,
},
],
};
describeWithMockConnection('AutofillView', () => {
let target: SDK.Target.Target;
let autofillModel: SDK.AutofillModel.AutofillModel;
let showViewStub: sinon.SinonStub;
beforeEach(() => {
Root.Runtime.experiments.register('apca', '');
target = createTarget();
SDK.TargetManager.TargetManager.instance().setScopeTarget(target);
const maybeAutofillModel = target.model(SDK.AutofillModel.AutofillModel);
assert.exists(maybeAutofillModel);
autofillModel = maybeAutofillModel;
showViewStub = sinon.stub(UI.ViewManager.ViewManager.instance(), 'showView').resolves();
AutofillManager.AutofillManager.AutofillManager.instance({forceNew: true});
});
afterEach(() => {
showViewStub.restore();
});
const renderAutofillView = async () => {
const view = new Autofill.AutofillView.AutofillView();
view.style.display = 'block';
view.style.width = '640px';
view.style.height = '480px';
renderElementIntoDOM(view);
await view.render();
await RenderCoordinator.done({waitForWork: true});
return view;
};
const assertViewShowsEventData = (view: Autofill.AutofillView.AutofillView) => {
const addressSpans = view.shadowRoot!.querySelectorAll('.address span');
const addressText = [...addressSpans].map(div => div.textContent);
assert.deepEqual(
addressText, ['Crocodile', ' Middle ', 'Dundee', 'Uluru ToursOutback Road 1Bundaberg Queensland ', '12345']);
const expectedHeaders = ['Form field', 'Predicted autofill value', 'Value'];
const expectedRows = [
['#input1 (text)', 'First name\nheur', '"Crocodile"'],
['input2 (text)', 'Last name\nheur', '"Dundee"'],
['#input3 (text)', 'Country\nheur', '"Australia"'],
['#input4 (text)', 'Zip code\nattr', '"12345"'],
];
assertGridContents(view, expectedHeaders, expectedRows);
};
it('renders autofilled address and filled fields and clears content on navigation', async () => {
const expectedPlaceholderHeader = 'No autofill detected';
const expectedPlaceholder = 'To start debugging autofill, use Chrome\'s autofill menu to fill an address form.';
const view = await renderAutofillView();
let placeholderHeader = view.shadowRoot!.querySelector('.empty-state-header')!.textContent!.trim();
assert.strictEqual(placeholderHeader, expectedPlaceholderHeader);
let placeholderText = view.shadowRoot!.querySelector('.empty-state-description > span')!.textContent!.trim();
assert.strictEqual(placeholderText, expectedPlaceholder);
autofillModel.addressFormFilled(addressFormFilledEvent);
await RenderCoordinator.done({waitForWork: true});
assertViewShowsEventData(view);
navigate(getMainFrame(target));
await RenderCoordinator.done();
placeholderHeader = view.shadowRoot!.querySelector('.empty-state-header')!.textContent!.trim();
assert.strictEqual(placeholderHeader, expectedPlaceholderHeader);
placeholderText = view.shadowRoot!.querySelector('.empty-state-description > span')!.textContent!.trim();
assert.strictEqual(placeholderText, expectedPlaceholder);
});
it('shows content if the view is created after the event was received', async () => {
autofillModel.addressFormFilled(addressFormFilledEvent);
sinon.assert.calledOnceWithExactly(showViewStub, 'autofill-view');
const view = await renderAutofillView();
assert.isNotNull(view.shadowRoot);
assertViewShowsEventData(view);
await RenderCoordinator.done();
});
it('auto-open can be turned off/on', async () => {
const view = await renderAutofillView();
autofillModel.addressFormFilled(addressFormFilledEvent);
sinon.assert.calledOnceWithExactly(showViewStub, 'autofill-view');
showViewStub.reset();
// The auto-opening checkbox is the second one.
const checkbox = view.shadowRoot!.querySelectorAll('devtools-checkbox')[1];
assert.isNotNull(checkbox);
assert.isTrue(checkbox.checked);
checkbox.checked = false;
let event = new Event('change');
checkbox.dispatchEvent(event);
autofillModel.addressFormFilled(addressFormFilledEvent);
sinon.assert.notCalled(showViewStub);
checkbox.checked = true;
event = new Event('change');
checkbox.dispatchEvent(event);
autofillModel.addressFormFilled(addressFormFilledEvent);
sinon.assert.calledOnceWithExactly(showViewStub, 'autofill-view');
await RenderCoordinator.done();
});
it('showing test addresses in autofill menu can be turned off/on', async () => {
const view = await renderAutofillView();
autofillModel.addressFormFilled(addressFormFilledEvent);
sinon.assert.calledOnceWithExactly(showViewStub, 'autofill-view');
showViewStub.reset();
// The show test addresses checkbox is the first one.
const checkbox = view.shadowRoot!.querySelectorAll('devtools-checkbox')[0];
assert.isNotNull(checkbox);
assert.isFalse(checkbox.checked);
const setAddressSpy = sinon.spy(autofillModel!.agent, 'invoke_setAddresses');
sinon.assert.notCalled(setAddressSpy);
checkbox.checked = true;
const event = new Event('change');
checkbox.dispatchEvent(event);
sinon.assert.calledOnce(setAddressSpy);
await RenderCoordinator.done();
});
it('highlights corresponding grid row when hovering over address span', async () => {
const monospaceStyles = 'font-family:var(--monospace-font-family);font-size:var(--monospace-font-size);';
autofillModel.addressFormFilled(addressFormFilledEvent);
sinon.assert.calledOnceWithExactly(showViewStub, 'autofill-view');
const view = await renderAutofillView();
assertViewShowsEventData(view);
const addressSpans = view.shadowRoot!.querySelectorAll('.address span');
const crocodileSpan = addressSpans[0];
assert.strictEqual(crocodileSpan.textContent, 'Crocodile');
assert.isFalse(crocodileSpan.classList.contains('highlighted'));
const grid = view.shadowRoot!.querySelector('devtools-data-grid')!;
assert.isNotNull(grid.shadowRoot);
const firstGridRow = grid.shadowRoot.querySelector('tbody tr[jslog]')!;
let styles = firstGridRow.getAttribute('style') || '';
assert.strictEqual(styles.replace(/\s/g, ''), monospaceStyles);
crocodileSpan.dispatchEvent(new MouseEvent('mouseenter'));
await RenderCoordinator.done({waitForWork: true});
assert.isTrue(crocodileSpan.classList.contains('highlighted'));
styles = firstGridRow.getAttribute('style') || '';
assert.strictEqual(
styles.replace(/\s/g, ''), monospaceStyles + 'background-color:var(--sys-color-state-hover-on-subtle);');
crocodileSpan.dispatchEvent(new MouseEvent('mouseleave'));
await RenderCoordinator.done({waitForWork: true});
assert.isFalse(crocodileSpan.classList.contains('highlighted'));
styles = firstGridRow.getAttribute('style') || '';
assert.strictEqual(styles.replace(/\s/g, ''), monospaceStyles);
});
it('highlights corresponding address span and DOM node when hovering over grid row', async () => {
stubNoopSettings();
const mockFrame = {
resourceTreeModel: () => ({
target: () => target,
}),
} as SDK.ResourceTreeModel.ResourceTreeFrame;
const getFrameStub = sinon.stub(SDK.FrameManager.FrameManager.instance(), 'getFrame').callsFake(frameId => {
return frameId === addressFormFilledEvent.filledFields[3].frameId ? mockFrame : null;
});
autofillModel.addressFormFilled(addressFormFilledEvent);
sinon.assert.calledOnceWithExactly(showViewStub, 'autofill-view');
const view = await renderAutofillView();
assertViewShowsEventData(view);
const domModel = target.model(SDK.DOMModel.DOMModel);
const overlayModel = domModel?.overlayModel();
assert.exists(overlayModel);
const overlaySpy = sinon.spy(overlayModel, 'highlightInOverlay');
const hideOverlaySpy = sinon.spy(SDK.OverlayModel.OverlayModel, 'hideDOMNodeHighlight');
const addressSpans = view.shadowRoot!.querySelectorAll('.address span');
const zipCodeSpan = addressSpans[4];
assert.strictEqual(zipCodeSpan.textContent, '12345');
assert.isFalse(zipCodeSpan.classList.contains('highlighted'));
const grid = view.shadowRoot!.querySelector('devtools-data-grid')!;
assert.isNotNull(grid.shadowRoot);
const fourthGridRow = grid.shadowRoot.querySelector('tbody tr[jslog]:nth-child(5)')!;
fourthGridRow.dispatchEvent(new MouseEvent('mouseenter'));
await RenderCoordinator.done({waitForWork: true});
assert.isTrue(zipCodeSpan.classList.contains('highlighted'));
sinon.assert.calledOnce(overlaySpy);
const deferredNode =
(overlaySpy.getCall(0).args[0] as unknown as SDK.OverlayModel.HighlightDeferredNode).deferredNode;
assert.strictEqual(deferredNode.backendNodeId(), 4);
sinon.assert.notCalled(hideOverlaySpy);
fourthGridRow.dispatchEvent(new MouseEvent('mouseleave'));
await RenderCoordinator.done({waitForWork: true});
assert.isFalse(zipCodeSpan.classList.contains('highlighted'));
sinon.assert.calledOnce(hideOverlaySpy);
getFrameStub.restore();
});
});