Skip to content

Commit 1b147b5

Browse files
Victor TanDevtools-frontend LUCI CQ
authored andcommitted
[Client Hints] add support override form factors
Add option to override the form factors in devtool. devtool backend:https://crrev.com/c/6634640 Bug: 422218341 Change-Id: I66bae9371ad0ca52b722eaa263f87a5742959913 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6653397 Commit-Queue: Alex Rudenko <[email protected]> Reviewed-by: Nancy Li <[email protected]> Commit-Queue: Victor Tan <[email protected]> Reviewed-by: Alex Rudenko <[email protected]>
1 parent c0ca1d3 commit 1b147b5

File tree

7 files changed

+261
-3
lines changed

7 files changed

+261
-3
lines changed

front_end/panels/settings/emulation/components/UserAgentClientHintsForm.test.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright 2021 The Chromium Authors. All rights reserved.
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
4+
import type * as Protocol from '../../../../generated/protocol.js';
45
import {
56
getElementsWithinComponent,
67
getElementWithinComponent,
@@ -30,6 +31,7 @@ describeWithLocale('UserAgentClientHintsForm', () => {
3031
version: '1.2.3',
3132
},
3233
],
34+
formFactors: [] as string[],
3335
fullVersion: '',
3436
platform: '',
3537
platformVersion: '',
@@ -66,6 +68,65 @@ describeWithLocale('UserAgentClientHintsForm', () => {
6668
assert.deepEqual(brandInputValues, ['Brand3']);
6769
});
6870

71+
it('renders form factors checkboxes with initial values and updates on change', () => {
72+
const component = renderUserAgentClientHintsForm();
73+
const initialFormFactors: string[] = [
74+
'Desktop',
75+
'Mobile',
76+
];
77+
component.value = {
78+
metaData: {
79+
...testMetaData,
80+
formFactors: initialFormFactors,
81+
},
82+
};
83+
84+
const checkboxLabels = getElementsWithinComponent(component, '.form-factor-checkbox-label', HTMLLabelElement);
85+
assert.strictEqual(
86+
checkboxLabels.length, EmulationComponents.UserAgentClientHintsForm.ALL_PROTOCOL_FORM_FACTORS.length,
87+
'Should render all form factor checkboxes');
88+
89+
for (const ff of EmulationComponents.UserAgentClientHintsForm.ALL_PROTOCOL_FORM_FACTORS) {
90+
const checkbox = getElementWithinComponent(component, `input[value="${ff}"]`, HTMLInputElement);
91+
assert.isNotNull(checkbox, `Checkbox for ${ff} should exist`);
92+
if (initialFormFactors.includes(ff)) {
93+
assert.isTrue(checkbox.checked, `Checkbox for ${ff} should be checked initially`);
94+
} else {
95+
assert.isFalse(checkbox.checked, `Checkbox for ${ff} should be unchecked initially`);
96+
}
97+
}
98+
99+
// Simulate changing a checkbox
100+
const tabletCheckbox = getElementWithinComponent(component, 'input[value=Tablet]', HTMLInputElement);
101+
assert.isFalse(tabletCheckbox.checked, 'Tablet checkbox should be unchecked before click');
102+
tabletCheckbox.click();
103+
104+
const expectedFormFactorsAfterClick: string[] = [
105+
'Desktop',
106+
'Mobile',
107+
'Tablet',
108+
];
109+
const currentMetaData =
110+
component.value.metaData as (Protocol.Emulation.UserAgentMetadata & {formFactors?: string[]});
111+
assert.deepEqual(
112+
currentMetaData.formFactors?.sort(), expectedFormFactorsAfterClick.sort(),
113+
'Form factors in component value should be updated after checkbox click');
114+
115+
// Simulate unchecking a checkbox
116+
const mobileCheckbox = getElementWithinComponent(component, 'input[value="Mobile"]', HTMLInputElement);
117+
assert.isTrue(mobileCheckbox.checked, 'Mobile checkbox should be checked before unchecking');
118+
mobileCheckbox.click();
119+
120+
const expectedFormFactorsAfterUncheck: string[] = [
121+
'Desktop',
122+
'Tablet',
123+
];
124+
const finalMetaData = component.value.metaData as (Protocol.Emulation.UserAgentMetadata & {formFactors?: string[]});
125+
assert.deepEqual(
126+
finalMetaData.formFactors?.sort(), expectedFormFactorsAfterUncheck.sort(),
127+
'Form factors in component value should be updated after unchecking a checkbox');
128+
});
129+
69130
it('Submitting the form returns metaData', async () => {
70131
const component = renderUserAgentClientHintsForm();
71132
component.value = {

front_end/panels/settings/emulation/components/UserAgentClientHintsForm.ts

Lines changed: 125 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,47 @@ const UIStrings = {
8181
*/
8282
brandFullVersionListDelete: 'Delete brand from full version list',
8383

84+
/**
85+
* @description Heading for the form factors section.
86+
*/
87+
formFactorsTitle: 'Form Factors (Sec-CH-UA-Form-Factors)',
88+
/**
89+
* @description ARIA label for the group of form factor checkboxes.
90+
*/
91+
formFactorsGroupAriaLabel: 'Available Form Factors',
92+
/**
93+
* @description Form factor option: Desktop.
94+
*/
95+
formFactorDesktop: 'Desktop',
96+
/**
97+
* @description Form factor option: Automotive.
98+
*/
99+
formFactorAutomotive: 'Automotive',
100+
/**
101+
* @description Form factor option: Mobile.
102+
*/
103+
formFactorMobile: 'Mobile',
104+
/**
105+
* @description Form factor option: Tablet.
106+
*/
107+
formFactorTablet: 'Tablet',
108+
/**
109+
* @description Form factor option: XR.
110+
*/
111+
formFactorXR: 'XR',
112+
/**
113+
* @description Form factor option: EInk.
114+
*/
115+
formFactorEInk: 'EInk',
116+
/**
117+
* @description Form factor option: Watch.
118+
*/
119+
formFactorWatch: 'Watch',
120+
84121
/**
85122
* @description Label for full browser version input field.
86123
*/
87-
fullBrowserVersion: 'Full browser version (Sec-CH-UA-Full-Browser-Version)',
124+
fullBrowserVersion: 'Full browser version (Sec-CH-UA-Full-Version)',
88125
/**
89126
* @description Placeholder for full browser version input field.
90127
*/
@@ -198,8 +235,19 @@ const DEFAULT_METADATA = {
198235
architecture: '',
199236
model: '',
200237
mobile: false,
238+
formFactors: []
201239
};
202240

241+
export const ALL_PROTOCOL_FORM_FACTORS: string[] = [
242+
UIStrings.formFactorDesktop,
243+
UIStrings.formFactorAutomotive,
244+
UIStrings.formFactorMobile,
245+
UIStrings.formFactorTablet,
246+
UIStrings.formFactorXR,
247+
UIStrings.formFactorEInk,
248+
UIStrings.formFactorWatch,
249+
] as const;
250+
203251
/**
204252
* Component for user agent client hints form, it is used in device settings panel
205253
* and network conditions panel. It is customizable through showMobileCheckbox and showSubmitButton.
@@ -412,6 +460,23 @@ export class UserAgentClientHintsForm extends HTMLElement {
412460
}
413461
};
414462

463+
#handleFormFactorCheckboxChange = (formFactorValue: string, isChecked: boolean): void => {
464+
let currentFormFactors = [...(this.#metaData.formFactors || [])];
465+
if (isChecked) {
466+
if (!currentFormFactors.includes(formFactorValue)) {
467+
currentFormFactors.push(formFactorValue);
468+
}
469+
} else {
470+
currentFormFactors = currentFormFactors.filter(ff => ff !== formFactorValue);
471+
}
472+
this.#metaData = {
473+
...this.#metaData,
474+
formFactors: currentFormFactors,
475+
};
476+
this.dispatchEvent(new ClientHintsChangeEvent());
477+
this.#render();
478+
};
479+
415480
#handleInputChange = (stateKey: keyof Protocol.Emulation.UserAgentMetadata, value: string|boolean): void => {
416481
if (stateKey in this.#metaData) {
417482
this.#metaData = {
@@ -743,13 +808,48 @@ export class UserAgentClientHintsForm extends HTMLElement {
743808
`;
744809
}
745810

811+
#renderFormFactorsSection(): Lit.TemplateResult {
812+
const checkboxElements = ALL_PROTOCOL_FORM_FACTORS.map(ffValue => {
813+
const isChecked = this.#metaData.formFactors?.includes(ffValue) || false;
814+
const labelStringId = `formFactor${ffValue}` as keyof typeof UIStrings;
815+
const label = i18nString(UIStrings[labelStringId]);
816+
817+
return html`
818+
<label class="form-factor-checkbox-label">
819+
<input
820+
type="checkbox"
821+
.checked=${isChecked}
822+
value=${ffValue}
823+
jslog=${VisualLogging.toggle(Platform.StringUtilities.toKebabCase(ffValue)).track({
824+
click: true
825+
})}
826+
@change=${
827+
(e: Event) => this.#handleFormFactorCheckboxChange(ffValue, (e.target as HTMLInputElement).checked)}
828+
/>
829+
${label}
830+
</label>
831+
`;
832+
});
833+
834+
return html`
835+
<span class="full-row label" jslog=${VisualLogging.sectionHeader('form-factors')}>
836+
${i18nString(UIStrings.formFactorsTitle)}
837+
</span>
838+
<div class="full-row form-factors-checkbox-group" role="group" aria-label=${
839+
i18nString(UIStrings.formFactorsGroupAriaLabel)}>
840+
${checkboxElements}
841+
</div>
842+
`;
843+
}
844+
746845
#render(): void {
747846
const {fullVersion, architecture} = this.#metaData;
748847
const useragentSection = this.#renderUseragent();
749848
const fullVersionListSection = this.#renderFullVersionList();
750849
const fullBrowserInput = this.#renderInputWithLabel(
751850
i18nString(UIStrings.fullBrowserVersion), i18nString(UIStrings.fullBrowserVersionPlaceholder),
752851
fullVersion || '', 'fullVersion');
852+
const formFactorsSection = this.#renderFormFactorsSection();
753853
const platformSection = this.#renderPlatformSection();
754854
const architectureInput = this.#renderInputWithLabel(
755855
i18nString(UIStrings.architecture), i18nString(UIStrings.architecturePlaceholder), architecture,
@@ -805,10 +905,17 @@ export class UserAgentClientHintsForm extends HTMLElement {
805905
@submit=${this.#handleSubmit}
806906
>
807907
${useragentSection}
908+
<hr class="section-separator">
808909
${fullVersionListSection}
910+
<hr class="section-separator">
809911
${fullBrowserInput}
912+
<hr class="section-separator">
913+
${formFactorsSection}
914+
<hr class="section-separator">
810915
${platformSection}
916+
<hr class="section-separator">
811917
${architectureInput}
918+
<hr class="section-separator">
812919
${deviceModelSection}
813920
${submitButton}
814921
</form>
@@ -833,6 +940,23 @@ export class UserAgentClientHintsForm extends HTMLElement {
833940
if (!isBrandValid) {
834941
return {valid: false, errorMessage: i18nString(UIStrings.notRepresentable)};
835942
}
943+
} else if (metaDataKey === 'formFactors') {
944+
const formFactors = metaDataValue as string[] | undefined;
945+
if (formFactors) {
946+
for (const ff of formFactors) {
947+
if (!ALL_PROTOCOL_FORM_FACTORS.includes(ff)) {
948+
return {
949+
valid: false,
950+
errorMessage: i18nString(UIStrings.notRepresentable) + ` (Invalid form factor: ${ff})`,
951+
};
952+
}
953+
const ffError = EmulationUtils.UserAgentMetadata.validateAsStructuredHeadersString(
954+
ff, i18nString(UIStrings.notRepresentable));
955+
if (!ffError.valid) {
956+
return ffError;
957+
}
958+
}
959+
}
836960
} else {
837961
// otherwise, validate the value as a string
838962
const metaDataError = EmulationUtils.UserAgentMetadata.validateAsStructuredHeadersString(

front_end/panels/settings/emulation/components/userAgentClientHintsForm.css

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,25 @@
4545
grid-column: 1 / 5;
4646
}
4747

48+
.form-factors-checkbox-group {
49+
display: grid;
50+
grid-template-columns: repeat(2, 1fr);
51+
gap: 6px 10px;
52+
}
53+
54+
.form-factor-checkbox-label {
55+
display: flex;
56+
align-items: center;
57+
gap: 6px;
58+
white-space: nowrap;
59+
}
60+
61+
hr.section-separator {
62+
grid-column: 1 / 5; /* Ensures the separator spans all columns */
63+
border: none;
64+
margin-top: 1px;
65+
}
66+
4867
.half-row {
4968
grid-column: span 2;
5069
}

front_end/ui/visual_logging/KnownContextValues.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,7 @@ export const knownContextValues = new Set([
501501
'automatic-workspace-folders',
502502
'automatic-workspace-folders.connect',
503503
'automatically-ignore-list-known-third-party-scripts',
504+
'automotive',
504505
'auxclick',
505506
'avif-format-disabled',
506507
'avif-format-disabled-true',
@@ -1295,6 +1296,7 @@ export const knownContextValues = new Set([
12951296
'dynamic-local-setting',
12961297
'dynamic-range-limit',
12971298
'dynamic-synced-setting',
1299+
'e-ink',
12981300
'early-hints-headers',
12991301
'edit',
13001302
'edit-and-resend',
@@ -1597,6 +1599,7 @@ export const knownContextValues = new Set([
15971599
'forced-color-adjust',
15981600
'forced-reflow',
15991601
'form-data',
1602+
'form-factors',
16001603
'fourth',
16011604
'fr',
16021605
'fr-ca',
@@ -3527,6 +3530,7 @@ export const knownContextValues = new Set([
35273530
'tab-5',
35283531
'tab-size',
35293532
'table-layout',
3533+
'tablet',
35303534
'tag-name',
35313535
'take-screenshot',
35323536
'target',
@@ -3954,6 +3958,7 @@ export const knownContextValues = new Set([
39543958
'wasm',
39553959
'wasm-auto-stepping',
39563960
'wasm-auto-stepping-false',
3961+
'watch',
39573962
'watch-test-expression',
39583963
'watch-test-object',
39593964
'waterfall',
@@ -4002,6 +4007,7 @@ export const knownContextValues = new Set([
40024007
'x-offset',
40034008
'x-small',
40044009
'xhr',
4010+
'xr',
40054011
'xx-large',
40064012
'xx-small',
40074013
'xy',

test/e2e/emulation/custom-devices_test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,15 @@ describe('Custom devices', () => {
9898
await tabForward(); // Focus full version.
9999
await typeText('1.1.2345');
100100

101+
// Focus on form factors checkbox
102+
for (let i = 0; i < 7; ++i) {
103+
await tabForward();
104+
// Enable form factors Desktop and XR
105+
if (i === 0 || i === 4) {
106+
await pressKey('Space');
107+
}
108+
}
109+
101110
await tabForward(); // Focus platform.
102111
await typeText('Cyborg');
103112

@@ -141,6 +150,12 @@ describe('Custom devices', () => {
141150
assert.strictEqual(await targetTextContent('#res-architecture'), 'Bipedal');
142151
assert.strictEqual(await targetTextContent('#res-model'), 'C-1-Gardener');
143152
assert.strictEqual(await targetTextContent('#res-ua-full-version'), '1.1.2345');
153+
assert.strictEqual(await targetTextContent('#res-num-full-version-list'), '1');
154+
assert.strictEqual(await targetTextContent('#res-full-version-list-0-name'), 'Ready Rover');
155+
assert.strictEqual(await targetTextContent('#res-full-version-list-0-version'), '2.4.9');
156+
assert.strictEqual(await targetTextContent('#res-num-form-factors'), '2');
157+
assert.strictEqual(await targetTextContent('#res-form-factors-0'), 'Desktop');
158+
assert.strictEqual(await targetTextContent('#res-form-factors-1'), 'XR');
144159

145160
// Focus the first item in the device list, which should be the custom entry,
146161
// and then click the edit button that should appear.
@@ -164,6 +179,10 @@ describe('Custom devices', () => {
164179
// Change the value.
165180
await typeText('1.1.5');
166181

182+
// Move to form factor Desktop checkbox, uncheck it.
183+
await tabForward();
184+
await pressKey('Space');
185+
167186
// Save the changes.
168187
await pressKey('Enter');
169188

@@ -183,6 +202,11 @@ describe('Custom devices', () => {
183202
assert.strictEqual(await targetTextContent('#res-architecture'), 'Bipedal');
184203
assert.strictEqual(await targetTextContent('#res-model'), 'C-1-Gardener');
185204
assert.strictEqual(await targetTextContent('#res-ua-full-version'), '1.1.5');
205+
assert.strictEqual(await targetTextContent('#res-num-full-version-list'), '1');
206+
assert.strictEqual(await targetTextContent('#res-full-version-list-0-name'), 'Ready Rover');
207+
assert.strictEqual(await targetTextContent('#res-full-version-list-0-version'), '2.4.9');
208+
assert.strictEqual(await targetTextContent('#res-num-form-factors'), '1');
209+
assert.strictEqual(await targetTextContent('#res-form-factors-0'), 'XR');
186210
});
187211

188212
it('can add and properly display a device with a custom resolution', async () => {

0 commit comments

Comments
 (0)