blob: d2da628a7edea47b0f0a12b53dd3b4240b35f85e [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'chrome://os-settings/chromeos/lazy_load.js';
import {MediaDevicesProxy, PrivacyHubBrowserProxyImpl, SettingsPrivacyHubSubpage} from 'chrome://os-settings/chromeos/lazy_load.js';
import {MetricsConsentBrowserProxyImpl, OsSettingsPrivacyPageElement, Router, routes, SecureDnsMode, SettingsToggleButtonElement} from 'chrome://os-settings/chromeos/os_settings.js';
import {assert} from 'chrome://resources/js/assert_ts.js';
import {webUIListenerCallback} from 'chrome://resources/js/cr.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {getDeepActiveElement} from 'chrome://resources/js/util_ts.js';
import {DomRepeat, flush} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {assertEquals, assertFalse, assertNotReached, assertTrue} from 'chrome://webui-test/chai_assert.js';
import {waitAfterNextRender} from 'chrome://webui-test/polymer_test_util.js';
import {FakeMediaDevices} from '../fake_media_devices.js';
import {FakeMetricsPrivate} from '../fake_metrics_private.js';
import {DEVICE_METRICS_CONSENT_PREF_NAME, TestMetricsConsentBrowserProxy} from './test_metrics_consent_browser_proxy.js';
import {TestPrivacyHubBrowserProxy} from './test_privacy_hub_browser_proxy.js';
const USER_METRICS_CONSENT_PREF_NAME = 'metrics.user_consent';
const PrivacyHubVersion = {
Future: 'Privacy Hub with future (after MVP) features.',
MVP: 'Privacy Hub with MVP features.',
Dogfood: 'Privacy Hub with dogfooded features (camera and microphone only).',
};
function overridedValues(privacyHubVersion: string) {
switch (privacyHubVersion) {
case PrivacyHubVersion.Future: {
return {
showPrivacyHubPage: true,
showPrivacyHubMVPPage: true,
showPrivacyHubFuturePage: true,
};
}
case PrivacyHubVersion.Dogfood: {
return {
showPrivacyHubPage: true,
showPrivacyHubMVPPage: false,
showPrivacyHubFuturePage: false,
};
}
case PrivacyHubVersion.MVP: {
return {
showPrivacyHubPage: true,
showPrivacyHubMVPPage: true,
showPrivacyHubFuturePage: false,
};
}
default: {
assertNotReached(`Unsupported Privacy Hub version: {privacyHubVersion}`);
}
}
}
async function parametrizedPrivacyHubSubpageTestsuite(
privacyHubVersion: string) {
let privacyHubSubpage: SettingsPrivacyHubSubpage;
let privacyHubBrowserProxy: TestPrivacyHubBrowserProxy;
let mediaDevices: FakeMediaDevices;
setup(async () => {
loadTimeData.overrideValues(overridedValues(privacyHubVersion));
privacyHubBrowserProxy = new TestPrivacyHubBrowserProxy();
PrivacyHubBrowserProxyImpl.setInstanceForTesting(privacyHubBrowserProxy);
mediaDevices = new FakeMediaDevices();
MediaDevicesProxy.setMediaDevicesForTesting(mediaDevices);
privacyHubSubpage = document.createElement('settings-privacy-hub-subpage');
document.body.appendChild(privacyHubSubpage);
await waitAfterNextRender(privacyHubSubpage);
flush();
});
teardown(() => {
privacyHubSubpage.remove();
Router.getInstance().resetRouteForTesting();
});
test('Deep link to camera toggle on privacy hub', async () => {
const params = new URLSearchParams();
params.append('settingId', '1116');
Router.getInstance().navigateTo(routes.PRIVACY_HUB, params);
flush();
const deepLinkElement =
privacyHubSubpage.shadowRoot!.querySelector('#cameraToggle')!
.shadowRoot!.querySelector('cr-toggle');
assert(deepLinkElement);
await waitAfterNextRender(deepLinkElement);
assertEquals(
deepLinkElement, getDeepActiveElement(),
'Camera toggle should be focused for settingId=1116.');
});
test('Deep link to microphone toggle on privacy hub', async () => {
const params = new URLSearchParams();
params.append('settingId', '1117');
Router.getInstance().navigateTo(routes.PRIVACY_HUB, params);
flush();
const deepLinkElement =
privacyHubSubpage.shadowRoot!.querySelector('#microphoneToggle')!
.shadowRoot!.querySelector('cr-toggle');
assert(deepLinkElement);
await waitAfterNextRender(deepLinkElement);
assertEquals(
deepLinkElement, getDeepActiveElement(),
'Microphone toggle should be focused for settingId=1117.');
});
test('Microphone toggle disabled when the hw toggle is active', async () => {
const getMicrophoneList = () =>
privacyHubSubpage.shadowRoot!.querySelector('#micList');
const getMicrophoneCrToggle = () =>
privacyHubSubpage.shadowRoot!.querySelector('#microphoneToggle')!
.shadowRoot!.querySelector('cr-toggle');
const getMicrophoneTooltip = () =>
privacyHubSubpage.shadowRoot!.querySelector('#microphoneToggle')!
.querySelector('cr-tooltip-icon');
privacyHubBrowserProxy.microphoneToggleIsEnabled = false;
await privacyHubBrowserProxy.whenCalled(
'getInitialMicrophoneHardwareToggleState');
await waitAfterNextRender(privacyHubSubpage);
// There should be no MediaDevice connected initially. Microphone toggle
// should be disabled as no microphone is connected.
assertEquals(null, getMicrophoneList());
assertTrue(getMicrophoneCrToggle()!.disabled);
// TODO(b/259553116) Check how banshee handles the microphone hardware
// switch.
assertTrue(getMicrophoneTooltip()!.hidden);
// Add a microphone.
mediaDevices.addDevice('audioinput', 'Fake Microphone');
await waitAfterNextRender(privacyHubSubpage);
assert(getMicrophoneList());
// Microphone toggle should be enabled to click now as there is a microphone
// connected and the hw toggle is inactive.
assertFalse(getMicrophoneCrToggle()!.disabled);
// The tooltip should only show when the HW switch is engaged.
assertTrue(getMicrophoneTooltip()!.hidden);
// Activate the hw toggle.
webUIListenerCallback('microphone-hardware-toggle-changed', true);
await waitAfterNextRender(privacyHubSubpage);
// Microphone toggle should be disabled again due to the hw switch being
// active.
assertTrue(getMicrophoneCrToggle()!.disabled);
// With the HW switch being active the tooltip should be visible.
assertFalse(getMicrophoneTooltip()!.hidden);
// Ensure that the tooltip has the intended content.
assertEquals(
privacyHubSubpage.i18n('microphoneHwToggleTooltip'),
getMicrophoneTooltip()!.tooltipText.trim());
mediaDevices.popDevice();
});
test('Suggested content, pref disabled', () => {
privacyHubSubpage.remove();
privacyHubSubpage = document.createElement('settings-privacy-hub-subpage');
document.body.appendChild(privacyHubSubpage);
flush();
// The default state of the pref is disabled.
const suggestedContent =
privacyHubSubpage.shadowRoot!
.querySelector<SettingsToggleButtonElement>('#suggested-content');
assert(suggestedContent);
assertFalse(suggestedContent.checked);
});
test('Suggested content, pref enabled', () => {
// Update the backing pref to enabled.
privacyHubSubpage.prefs = {
'settings': {
'suggested_content_enabled': {
value: true,
},
},
};
flush();
// The checkbox reflects the updated pref state.
const suggestedContent =
privacyHubSubpage.shadowRoot!
.querySelector<SettingsToggleButtonElement>('#suggested-content');
assert(suggestedContent);
assertTrue(suggestedContent.checked);
});
test('Deep link to Geolocation toggle on privacy hub', async () => {
const params = new URLSearchParams();
params.append('settingId', '1118');
Router.getInstance().navigateTo(routes.PRIVACY_HUB, params);
flush();
const toggleElement =
privacyHubSubpage.shadowRoot!.querySelector('#geolocationToggle');
if (privacyHubVersion === PrivacyHubVersion.Dogfood) {
assertEquals(null, toggleElement);
} else {
assert(toggleElement);
const deepLinkElement =
toggleElement.shadowRoot!.querySelector('cr-toggle');
assert(deepLinkElement);
await waitAfterNextRender(deepLinkElement);
assertEquals(
deepLinkElement, getDeepActiveElement(),
'Geolocation toggle should be focused for settingId=1118.');
}
});
test('Camera, microphone toggle, their sublabel and their list', async () => {
const getNoCameraText = () =>
privacyHubSubpage.shadowRoot!.querySelector('#noCamera');
const getCameraList = () =>
privacyHubSubpage.shadowRoot!.querySelector<DomRepeat>('#cameraList');
const getCameraCrToggle = () =>
privacyHubSubpage.shadowRoot!.querySelector('#cameraToggle')!
.shadowRoot!.querySelector('cr-toggle');
const getCameraToggleSublabel = () =>
privacyHubSubpage.shadowRoot!.querySelector('#cameraToggle')!
.shadowRoot!.querySelector('#sub-label-text');
const getNoMicrophoneText = () =>
privacyHubSubpage.shadowRoot!.querySelector('#noMic');
const getMicrophoneList = () =>
privacyHubSubpage.shadowRoot!.querySelector<DomRepeat>('#micList');
const getMicrophoneCrToggle = () =>
privacyHubSubpage.shadowRoot!.querySelector('#microphoneToggle')!
.shadowRoot!.querySelector('cr-toggle');
const getMicrophoneToggleSublabel = () =>
privacyHubSubpage.shadowRoot!.querySelector('#microphoneToggle')!
.shadowRoot!.querySelector('#sub-label-text');
// Initially, the lists of media devices should be hidden and `#noMic` and
// `#noCamera` should be displayed.
assertEquals(null, getCameraList());
assert(getNoCameraText());
assertEquals(
privacyHubSubpage.i18n('noCameraConnectedText'),
getNoCameraText()!.textContent!.trim());
assertEquals(
privacyHubSubpage.i18n('cameraToggleSubtext'),
getCameraToggleSublabel()!.textContent!.trim());
assertEquals(null, getMicrophoneList());
assert(getNoMicrophoneText());
assertEquals(
privacyHubSubpage.i18n('noMicrophoneConnectedText'),
getNoMicrophoneText()!.textContent!.trim());
assertEquals(
privacyHubSubpage.i18n('microphoneToggleSubtext'),
getMicrophoneToggleSublabel()!.textContent!.trim());
const tests = [
{
device: {
kind: 'audiooutput',
label: 'Fake Speaker 1',
},
changes: {
cam: false,
mic: false,
},
},
{
device: {
kind: 'videoinput',
label: 'Fake Camera 1',
},
changes: {
mic: false,
cam: true,
},
},
{
device: {
kind: 'audioinput',
label: 'Fake Microphone 1',
},
changes: {
cam: false,
mic: true,
},
},
{
device: {
kind: 'videoinput',
label: 'Fake Camera 2',
},
changes: {
cam: true,
mic: false,
},
},
{
device: {
kind: 'audiooutput',
label: 'Fake Speaker 2',
},
changes: {
cam: false,
mic: false,
},
},
{
device: {
kind: 'audioinput',
label: 'Fake Microphone 2',
},
changes: {
cam: false,
mic: true,
},
},
];
let cams = 0;
let mics = 0;
// Adding a media device in each iteration.
for (const test of tests) {
mediaDevices.addDevice(test.device.kind, test.device.label);
await waitAfterNextRender(privacyHubSubpage);
if (test.changes.cam) {
cams++;
}
if (test.changes.mic) {
mics++;
}
const cameraList = getCameraList();
if (cams) {
assert(cameraList);
assertEquals(cams, cameraList.items!.length);
assertFalse(getCameraCrToggle()!.disabled);
} else {
assertEquals(null, cameraList);
assertTrue(getCameraCrToggle()!.disabled);
}
const microphoneList = getMicrophoneList();
if (mics) {
assert(microphoneList);
assertEquals(mics, microphoneList.items!.length);
assertFalse(getMicrophoneCrToggle()!.disabled);
} else {
assertEquals(null, microphoneList);
assertTrue(getMicrophoneCrToggle()!.disabled);
}
}
// Removing the most recently added media device in each iteration.
for (const test of tests.reverse()) {
mediaDevices.popDevice();
await waitAfterNextRender(privacyHubSubpage);
if (test.changes.cam) {
cams--;
}
if (test.changes.mic) {
mics--;
}
const cameraList = getCameraList();
if (cams) {
assert(cameraList);
assertEquals(cams, cameraList.items!.length);
assertFalse(getCameraCrToggle()!.disabled);
} else {
assertEquals(null, cameraList);
assertTrue(getCameraCrToggle()!.disabled);
}
const microphoneList = getMicrophoneList();
if (mics) {
assert(microphoneList);
assertEquals(mics, microphoneList.items!.length);
assertFalse(getMicrophoneCrToggle()!.disabled);
} else {
assertEquals(null, microphoneList);
assertTrue(getMicrophoneCrToggle()!.disabled);
}
}
});
test('Toggle camera button', async () => {
privacyHubSubpage.remove();
privacyHubSubpage = document.createElement('settings-privacy-hub-subpage');
privacyHubSubpage.prefs = {
'ash': {
'user': {
'camera_allowed': {
value: true,
},
},
},
};
document.body.appendChild(privacyHubSubpage);
const fakeMetricsPrivate = new FakeMetricsPrivate();
chrome.metricsPrivate =
fakeMetricsPrivate as unknown as typeof chrome.metricsPrivate;
flush();
mediaDevices.addDevice('videoinput', 'Fake Camera');
await waitAfterNextRender(privacyHubSubpage);
const cameraToggleControl =
privacyHubSubpage.shadowRoot!.querySelector('#cameraToggle')!
.shadowRoot!.querySelector('cr-toggle');
assert(cameraToggleControl);
// Pref and toggle should be in sync and not disabled
assertTrue(cameraToggleControl.checked);
assertTrue(privacyHubSubpage.prefs.ash.user.camera_allowed.value);
assertFalse(cameraToggleControl.disabled);
// Click the button
cameraToggleControl.click();
flush();
await waitAfterNextRender(cameraToggleControl);
assertFalse(privacyHubSubpage.prefs.ash.user.camera_allowed.value);
assertFalse(cameraToggleControl.checked);
assertEquals(
fakeMetricsPrivate.countBoolean(
'ChromeOS.PrivacyHub.Camera.Settings.Enabled', false),
1);
assertEquals(
fakeMetricsPrivate.countBoolean(
'ChromeOS.PrivacyHub.Camera.Settings.Enabled', true),
0);
// Click the button again
cameraToggleControl.click();
flush();
await waitAfterNextRender(cameraToggleControl);
assertTrue(privacyHubSubpage.prefs.ash.user.camera_allowed.value);
assertTrue(cameraToggleControl.checked);
assertEquals(
fakeMetricsPrivate.countBoolean(
'ChromeOS.PrivacyHub.Camera.Settings.Enabled', false),
1);
assertEquals(
fakeMetricsPrivate.countBoolean(
'ChromeOS.PrivacyHub.Camera.Settings.Enabled', true),
1);
});
test('Toggle microphone button', async () => {
privacyHubSubpage.remove();
privacyHubSubpage = document.createElement('settings-privacy-hub-subpage');
privacyHubSubpage.prefs = {
'ash': {
'user': {
'microphone_allowed': {
value: true,
},
},
},
};
document.body.appendChild(privacyHubSubpage);
const fakeMetricsPrivate = new FakeMetricsPrivate();
chrome.metricsPrivate =
fakeMetricsPrivate as unknown as typeof chrome.metricsPrivate;
flush();
mediaDevices.addDevice('audioinput', 'Fake Mic');
await waitAfterNextRender(privacyHubSubpage);
const microphoneToggleControl =
privacyHubSubpage.shadowRoot!.querySelector('#microphoneToggle')!
.shadowRoot!.querySelector('cr-toggle');
assert(microphoneToggleControl);
// Pref and toggle should be in sync and not disabled
assertTrue(microphoneToggleControl.checked);
assertTrue(privacyHubSubpage.prefs.ash.user.microphone_allowed.value);
assertFalse(microphoneToggleControl.disabled);
// Click the button
microphoneToggleControl.click();
flush();
await waitAfterNextRender(microphoneToggleControl);
assertFalse(privacyHubSubpage.prefs.ash.user.microphone_allowed.value);
assertFalse(microphoneToggleControl.checked);
assertEquals(
fakeMetricsPrivate.countBoolean(
'ChromeOS.PrivacyHub.Microphone.Settings.Enabled', false),
1);
assertEquals(
fakeMetricsPrivate.countBoolean(
'ChromeOS.PrivacyHub.Microphone.Settings.Enabled', true),
0);
// Click the button again
microphoneToggleControl.click();
flush();
await waitAfterNextRender(microphoneToggleControl);
assertTrue(privacyHubSubpage.prefs.ash.user.microphone_allowed.value);
assertTrue(microphoneToggleControl.checked);
assertEquals(
fakeMetricsPrivate.countBoolean(
'ChromeOS.PrivacyHub.Microphone.Settings.Enabled', false),
1);
assertEquals(
fakeMetricsPrivate.countBoolean(
'ChromeOS.PrivacyHub.Microphone.Settings.Enabled', true),
1);
});
}
suite(
'<settings-privacy-hub-subpage> Dogfood',
() => parametrizedPrivacyHubSubpageTestsuite(PrivacyHubVersion.Dogfood));
suite(
'<settings-privacy-hub-subpage> MVP',
() => parametrizedPrivacyHubSubpageTestsuite(PrivacyHubVersion.MVP));
suite(
'<settings-privacy-hub-subpage> Future',
() => parametrizedPrivacyHubSubpageTestsuite(PrivacyHubVersion.Future));
async function parametrizedTestsuiteForMetricsConsentToggle(
isPrivacyHubVisible: boolean) {
let settingsPage: SettingsPrivacyHubSubpage|OsSettingsPrivacyPageElement;
// Which settings page to run the tests on.
const pageId = isPrivacyHubVisible ? 'settings-privacy-hub-subpage' :
'os-settings-privacy-page';
const prefs_ = {
'cros': {
'device': {
'peripheral_data_access_enabled': {
value: true,
},
},
'metrics': {
'reportingEnabled': {
value: true,
},
},
},
'metrics': {
'user_consent': {
value: false,
},
},
'dns_over_https':
{'mode': {value: SecureDnsMode.AUTOMATIC}, 'templates': {value: ''}},
};
let metricsConsentBrowserProxy: TestMetricsConsentBrowserProxy;
setup(async () => {
loadTimeData.overrideValues({
showPrivacyHubPage: isPrivacyHubVisible,
});
metricsConsentBrowserProxy = new TestMetricsConsentBrowserProxy();
MetricsConsentBrowserProxyImpl.setInstanceForTesting(
metricsConsentBrowserProxy);
settingsPage = document.createElement(pageId);
});
async function setUpPage(prefName: string, isConfigurable: boolean) {
metricsConsentBrowserProxy.setMetricsConsentState(prefName, isConfigurable);
settingsPage = document.createElement(pageId);
settingsPage.prefs = Object.assign({}, prefs_);
document.body.appendChild(settingsPage);
flush();
await metricsConsentBrowserProxy.whenCalled('getMetricsConsentState');
await waitAfterNextRender(settingsPage);
flush();
}
teardown(() => {
settingsPage.remove();
Router.getInstance().resetRouteForTesting();
});
test(
'Send usage stats toggle visibility in os-settings-privacy-page',
async () => {
settingsPage = document.createElement('os-settings-privacy-page');
document.body.appendChild(settingsPage);
flush();
const element =
settingsPage.shadowRoot!.querySelector('#metricsConsentToggle');
assertEquals(
isPrivacyHubVisible, element === null,
'Send usage toggle should only be visible here when privacy hub' +
' is hidden.');
});
test(
'Send usage stats toggle visibility in settings-privacy-hub-subpage',
async () => {
if (isPrivacyHubVisible) {
settingsPage = document.createElement('settings-privacy-hub-subpage');
document.body.appendChild(settingsPage);
flush();
const element =
settingsPage.shadowRoot!.querySelector('#metricsConsentToggle');
assertFalse(
element === null,
'Send usage toggle should be visible in the privacy hub' +
' subpage.');
}
});
test('Deep link to send usage stats', async () => {
await setUpPage(DEVICE_METRICS_CONSENT_PREF_NAME, /*isConfigurable=*/ true);
const params = new URLSearchParams();
params.append('settingId', '1103');
Router.getInstance().navigateTo(
isPrivacyHubVisible ? routes.PRIVACY_HUB : routes.OS_PRIVACY, params);
flush();
const deepLinkElement =
settingsPage.shadowRoot!.querySelector(
'#metricsConsentToggle')!.shadowRoot!
.querySelector('#settingsToggle')!.shadowRoot!.querySelector(
'cr-toggle');
assert(deepLinkElement);
await waitAfterNextRender(deepLinkElement);
assertEquals(
deepLinkElement, getDeepActiveElement(),
'Send usage stats toggle should be focused for settingId=1103.');
});
test('Toggle disabled if metrics consent is not configurable', async () => {
await setUpPage(
DEVICE_METRICS_CONSENT_PREF_NAME, /*isConfigurable=*/ false);
const toggle =
settingsPage.shadowRoot!.querySelector(
'#metricsConsentToggle')!.shadowRoot!
.querySelector('#settingsToggle')!.shadowRoot!.querySelector(
'cr-toggle');
assert(toggle);
await waitAfterNextRender(toggle);
// The pref is true, so the toggle should be on.
assertTrue(toggle.checked);
// Not configurable, so toggle should be disabled.
assertTrue(toggle.disabled);
});
test('Toggle enabled if metrics consent is configurable', async () => {
await setUpPage(DEVICE_METRICS_CONSENT_PREF_NAME, /*isConfigurable=*/ true);
const toggle =
settingsPage.shadowRoot!.querySelector(
'#metricsConsentToggle')!.shadowRoot!
.querySelector('#settingsToggle')!.shadowRoot!.querySelector(
'cr-toggle');
assert(toggle);
await waitAfterNextRender(toggle);
// The pref is true, so the toggle should be on.
assertTrue(toggle.checked);
// Configurable, so toggle should be enabled.
assertFalse(toggle.disabled);
// Toggle.
toggle.click();
await metricsConsentBrowserProxy.whenCalled('updateMetricsConsent');
// Pref should be off now.
assertFalse(toggle.checked);
});
test('Correct pref displayed', async () => {
await setUpPage(USER_METRICS_CONSENT_PREF_NAME, /*isConfigurable=*/ true);
const toggle =
settingsPage.shadowRoot!.querySelector(
'#metricsConsentToggle')!.shadowRoot!
.querySelector('#settingsToggle')!.shadowRoot!.querySelector(
'cr-toggle');
assert(toggle);
await waitAfterNextRender(toggle);
// The user consent pref is false, so the toggle should not be checked.
assertFalse(toggle.checked);
// Configurable, so toggle should be enabled.
assertFalse(toggle.disabled);
// Toggle.
toggle.click();
await metricsConsentBrowserProxy.whenCalled('updateMetricsConsent');
// Pref should be on now.
assertTrue(toggle.checked);
});
}
suite(
'<os-settings-privacy-page> OfficialBuild PrivacyHubVisible',
() => parametrizedTestsuiteForMetricsConsentToggle(
/*isPrivacyHubVisible=*/ true));
suite(
'<os-settings-privacy-page> OfficialBuild PrivacyHubHidden',
() => parametrizedTestsuiteForMetricsConsentToggle(
/*isPrivacyHubVisible=*/ false));