Skip to content

Commit 3257998

Browse files
Nancy LiDevtools-frontend LUCI CQ
authored andcommitted
[RPP Icicle blowtorch] Show the invalid status of current input
Bug: 382272853 Change-Id: Ib759477c02d97503634732f14da9fc4b7cb60de8 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6070382 Reviewed-by: Jack Franklin <[email protected]> Commit-Queue: Nancy Li <[email protected]>
1 parent 1c8b80e commit 3257998

File tree

3 files changed

+166
-28
lines changed

3 files changed

+166
-28
lines changed

front_end/panels/timeline/components/IgnoreListSetting.test.ts

Lines changed: 83 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ import * as Coordinator from '../../../ui/components/render_coordinator/render_c
1717

1818
import * as TimelineComponents from './components.js';
1919

20+
const coordinator = Coordinator.RenderCoordinator.RenderCoordinator.instance();
21+
2022
describeWithEnvironment('Ignore List Setting', () => {
2123
async function renderIgnoreListSetting(): Promise<HTMLElement> {
22-
const coordinator = Coordinator.RenderCoordinator.RenderCoordinator.instance();
2324
const component = new TimelineComponents.IgnoreListSetting.IgnoreListSetting();
2425
renderElementIntoDOM(component);
2526
await coordinator.done();
@@ -48,6 +49,14 @@ describeWithEnvironment('Ignore List Setting', () => {
4849
return newRegexInput;
4950
}
5051

52+
function getValidationResultElement(component: HTMLElement): HTMLDivElement {
53+
assert.isNotNull(component.shadowRoot);
54+
const validationResultElement = component.shadowRoot.querySelector<HTMLDivElement>('.input-validation');
55+
56+
assert.exists(validationResultElement);
57+
return validationResultElement;
58+
}
59+
5160
before(() => {
5261
const targetManager = SDK.TargetManager.TargetManager.instance();
5362
const workspace = Workspace.Workspace.WorkspaceImpl.instance({forceNew: true});
@@ -169,6 +178,74 @@ describeWithEnvironment('Ignore List Setting', () => {
169178
assert.isFalse(isIgnoreRegexDisabled('rule 1'));
170179
});
171180

181+
it('Do not show warning message for valid rule', async () => {
182+
const component = await renderIgnoreListSetting();
183+
const newRegexInput = getNewRegexInput(component);
184+
185+
dispatchFocusEvent(newRegexInput);
186+
newRegexInput.value = 'rule 2';
187+
dispatchInputEvent(newRegexInput);
188+
await coordinator.done();
189+
190+
const validationResultElement = component.shadowRoot?.querySelector<HTMLDivElement>('.input-validation');
191+
assert.notExists(validationResultElement);
192+
193+
// We need this to simulate the 'finish editing' with empty input, so it can remove the temp regex. Otherwise the
194+
// future tests will be messed up.
195+
// The 'finish editing' part will be tested later
196+
newRegexInput.value = '';
197+
dispatchBlurEvent(newRegexInput);
198+
});
199+
200+
it('Show error message for invalid rule', async () => {
201+
// One example of invalid rule is duplicate input.
202+
assert.isTrue(isRegexInIgnoredList('rule 1'));
203+
204+
const component = await renderIgnoreListSetting();
205+
const newRegexInput = getNewRegexInput(component);
206+
207+
dispatchFocusEvent(newRegexInput);
208+
newRegexInput.value = 'rule 1';
209+
dispatchInputEvent(newRegexInput);
210+
await coordinator.done();
211+
212+
const validationResultElement = getValidationResultElement(component);
213+
assert.isFalse(validationResultElement.hidden);
214+
assert.isTrue(validationResultElement.classList.contains('input-validation-error'));
215+
assert.isNotEmpty(validationResultElement.textContent);
216+
217+
// We need this to simulate the 'finish editing' with empty input, so it can remove the temp regex. Otherwise the
218+
// future tests will be messed up.
219+
// The 'finish editing' part will be tested later
220+
newRegexInput.value = '';
221+
dispatchBlurEvent(newRegexInput);
222+
});
223+
224+
it('Show warning message for valid rule with warning message', async () => {
225+
// One example of valid rule with warning message is when a rule is disabled and it is added again.
226+
disableIgnoreRegex('rule 1');
227+
assert.isTrue(isIgnoreRegexDisabled('rule 1'));
228+
229+
const component = await renderIgnoreListSetting();
230+
const newRegexInput = getNewRegexInput(component);
231+
232+
dispatchFocusEvent(newRegexInput);
233+
newRegexInput.value = 'rule 1';
234+
dispatchInputEvent(newRegexInput);
235+
await coordinator.done();
236+
237+
const validationResultElement = getValidationResultElement(component);
238+
assert.isFalse(validationResultElement.hidden);
239+
assert.isFalse(validationResultElement.classList.contains('input-validation-error'));
240+
assert.isNotEmpty(validationResultElement.textContent);
241+
242+
// We need this to simulate the 'finish editing' with empty input, so it can remove the temp regex. Otherwise the
243+
// future tests will be messed up.
244+
// The 'finish editing' part will be tested later
245+
newRegexInput.value = '';
246+
dispatchBlurEvent(newRegexInput);
247+
});
248+
172249
describe('preview the result', () => {
173250
it('Add an empty regex when focusing on the input', async () => {
174251
const regexPatterns = getIgnoredRegexes();
@@ -261,7 +338,7 @@ describeWithEnvironment('Pattern validator', () => {
261338
const emptyPattern = '';
262339
const result = TimelineComponents.IgnoreListSetting.patternValidator([], emptyPattern);
263340
assert.isFalse(result.valid);
264-
assert.strictEqual(result.errorMessage, 'Rule can\'t be empty');
341+
assert.strictEqual(result.message, 'Rule can\'t be empty');
265342
});
266343

267344
it('Returns the reason for the existed pattern', () => {
@@ -270,7 +347,7 @@ describeWithEnvironment('Pattern validator', () => {
270347

271348
const result = TimelineComponents.IgnoreListSetting.patternValidator([existedRegex], duplicatePattern);
272349
assert.isFalse(result.valid);
273-
assert.strictEqual(result.errorMessage, 'Rule already exists');
350+
assert.strictEqual(result.message, 'Rule already exists');
274351
});
275352

276353
it('Returns true for the disabled existed pattern', () => {
@@ -279,13 +356,15 @@ describeWithEnvironment('Pattern validator', () => {
279356

280357
const result = TimelineComponents.IgnoreListSetting.patternValidator([existedRegex], duplicatePattern);
281358
assert.isTrue(result.valid);
359+
assert.strictEqual(
360+
result.message, 'This rule already exists but is disabled. Saving this value will re-enable the rule');
282361
});
283362

284363
it('Returns the reason for the invalid pattern', () => {
285364
const invalidPattern = '[';
286365
const result = TimelineComponents.IgnoreListSetting.patternValidator([], invalidPattern);
287366
assert.isFalse(result.valid);
288-
assert.strictEqual(result.errorMessage, 'Rule must be a valid regular expression');
367+
assert.strictEqual(result.message, 'Rule must be a valid regular expression');
289368
});
290369
});
291370

front_end/panels/timeline/components/IgnoreListSetting.ts

Lines changed: 53 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ const UIStrings = {
6262
*@description Error message in Framework Ignore List settings pane that declares pattern must be a valid regular expression
6363
*/
6464
patternMustBeAValidRegular: 'Rule must be a valid regular expression',
65+
/**
66+
*@description Error message in Framework Ignore List settings pane that declares pattern already exits
67+
*/
68+
patternAlreadyExistsWillBeEnables:
69+
'This rule already exists but is disabled. Saving this value will re-enable the rule',
6570
};
6671

6772
const str_ = i18n.i18n.registerUIStrings('panels/timeline/components/IgnoreListSetting.ts', UIStrings);
@@ -74,11 +79,13 @@ export class IgnoreListSetting extends HTMLElement {
7479
Common.Settings.Settings.instance().moduleSetting('enable-ignore-listing');
7580
readonly #regexPatterns = this.#getSkipStackFramesPatternSetting().getAsArray();
7681

77-
#newItemCheckbox = UI.UIUtils.CheckboxLabel.create(
82+
#newRegexCheckbox = UI.UIUtils.CheckboxLabel.create(
7883
/* title*/ undefined, /* checked*/ false, /* subtitle*/ undefined,
7984
/* jslogContext*/ 'timeline.ignore-list-new-regex.checkbox');
80-
#newItemInput = UI.UIUtils.createInput(
85+
#newRegexInput = UI.UIUtils.createInput(
8186
/* className*/ 'new-regex-text-input', /* type*/ 'text', /* jslogContext*/ 'timeline.ignore-list-new-regex.text');
87+
#newRegexIsValid = true;
88+
#newRegexValidationMessage?: string;
8289

8390
#editingRegexSetting: Common.Settings.RegExpSettingItem|null = null;
8491

@@ -110,7 +117,7 @@ export class IgnoreListSetting extends HTMLElement {
110117
}
111118

112119
#startEditing(): void {
113-
this.#editingRegexSetting = {pattern: this.#newItemInput.value, disabled: false, disabledForUrl: undefined};
120+
this.#editingRegexSetting = {pattern: this.#newRegexInput.value, disabled: false, disabledForUrl: undefined};
114121
// We need to push the temp regex here to update the flame chart.
115122
// We are using the "skip-stack-frames-pattern" setting to determine which is rendered on flame chart. And the push
116123
// here will update the setting's value.
@@ -135,12 +142,12 @@ export class IgnoreListSetting extends HTMLElement {
135142
}
136143

137144
#resetInput(): void {
138-
this.#newItemCheckbox.checkboxElement.checked = false;
139-
this.#newItemInput.value = '';
145+
this.#newRegexCheckbox.checkboxElement.checked = false;
146+
this.#newRegexInput.value = '';
140147
}
141148

142149
#onNewRegexAdded(): void {
143-
const newRegex = this.#newItemInput.value.trim();
150+
const newRegex = this.#newRegexInput.value.trim();
144151

145152
this.#finishEditing();
146153
const {valid} = patternValidator(this.#getExistingRegexes(), newRegex);
@@ -179,25 +186,45 @@ export class IgnoreListSetting extends HTMLElement {
179186
}
180187

181188
#handleInputChange(): void {
189+
const newRegex = this.#newRegexInput.value.trim();
182190
// Enable the rule if the text input field is not empty.
183-
this.#newItemCheckbox.checkboxElement.checked = Boolean(this.#newItemInput.value.trim());
191+
this.#newRegexCheckbox.checkboxElement.checked = Boolean(newRegex);
192+
const {valid, message} = patternValidator(this.#getExistingRegexes(), newRegex);
193+
194+
this.#newRegexInput.classList.toggle('error-input', !valid);
195+
UI.ARIAUtils.setInvalid(this.#newRegexInput, !valid);
196+
this.#newRegexIsValid = valid;
197+
this.#newRegexValidationMessage = message;
184198

185199
if (this.#editingRegexSetting) {
186-
this.#editingRegexSetting.pattern = this.#newItemInput.value.trim();
200+
this.#editingRegexSetting.pattern = this.#newRegexInput.value.trim();
187201
this.#getSkipStackFramesPatternSetting().setAsArray(this.#regexPatterns);
188202
}
189203
}
190204

191205
#initAddNewItem(): void {
192206
const checkboxHelpText = i18nString(UIStrings.ignoreScriptsWhoseNamesMatchNewRegex);
193207
const inputHelpText = i18nString(UIStrings.addNewRegex);
194-
UI.Tooltip.Tooltip.install(this.#newItemCheckbox, checkboxHelpText);
195-
UI.Tooltip.Tooltip.install(this.#newItemInput, inputHelpText);
208+
UI.Tooltip.Tooltip.install(this.#newRegexCheckbox, checkboxHelpText);
209+
UI.Tooltip.Tooltip.install(this.#newRegexInput, inputHelpText);
196210

197-
this.#newItemInput.addEventListener('blur', this.#onNewRegexAdded.bind(this), false);
198-
this.#newItemInput.addEventListener('keydown', this.#handleKeyDown.bind(this), false);
199-
this.#newItemInput.addEventListener('input', this.#handleInputChange.bind(this), false);
200-
this.#newItemInput.addEventListener('focus', this.#startEditing.bind(this), false);
211+
this.#newRegexInput.addEventListener('blur', this.#onNewRegexAdded.bind(this), false);
212+
this.#newRegexInput.addEventListener('keydown', this.#handleKeyDown.bind(this), false);
213+
this.#newRegexInput.addEventListener('input', this.#handleInputChange.bind(this), false);
214+
this.#newRegexInput.addEventListener('focus', this.#startEditing.bind(this), false);
215+
}
216+
217+
#renderNewRegexRow(): LitHtml.TemplateResult {
218+
const classes = LitHtml.Directives.classMap({
219+
'input-validation': true,
220+
'input-validation-error': !this.#newRegexIsValid,
221+
});
222+
return html`
223+
<div class='new-regex-row'>${this.#newRegexCheckbox}${this.#newRegexInput}</div>
224+
${
225+
this.#newRegexValidationMessage ? html`<div class=${classes}>${this.#newRegexValidationMessage}</div>` :
226+
LitHtml.nothing}
227+
`;
201228
}
202229

203230
#onRegexEnableToggled(regex: Common.Settings.RegExpSettingItem, checkbox: UI.UIUtils.CheckboxLabel): void {
@@ -262,7 +289,7 @@ export class IgnoreListSetting extends HTMLElement {
262289
<div class='ignore-list-setting-content'>
263290
<div class='ignore-list-setting-description'>${i18nString(UIStrings.ignoreListDescription)}</div>
264291
${this.#getExistingRegexes().map(this.#renderItem.bind(this))}
265-
<div class='new-regex-row'>${this.#newItemCheckbox}${this.#newItemInput}</div>
292+
${this.#renderNewRegexRow()}
266293
</div>
267294
</devtools-button-dialog>
268295
`;
@@ -287,21 +314,23 @@ declare global {
287314
* @param inputValue new regex in string format
288315
* @returns if the regex is valid and if not, why it is invalid
289316
*/
290-
export function patternValidator(
291-
existingRegexes: Common.Settings.RegExpSettingItem[], inputValue: string): ({valid: true}|{
292-
valid: false,
293-
errorMessage: string,
294-
}) {
317+
export function patternValidator(existingRegexes: Common.Settings.RegExpSettingItem[], inputValue: string): {
318+
valid: boolean,
319+
message?: string,
320+
} {
295321
const pattern = inputValue.trim();
296322

297323
if (!pattern.length) {
298-
return {valid: false, errorMessage: i18nString(UIStrings.patternCannotBeEmpty)};
324+
return {valid: false, message: i18nString(UIStrings.patternCannotBeEmpty)};
299325
}
300326

301327
for (let i = 0; i < existingRegexes.length; ++i) {
302328
const regexPattern = existingRegexes[i];
303-
if (regexPattern.pattern === pattern && !regexPattern.disabled && regexPattern.disabledForUrl === undefined) {
304-
return {valid: false, errorMessage: i18nString(UIStrings.patternAlreadyExists)};
329+
if (regexPattern.pattern === pattern) {
330+
if (regexPattern.disabled || regexPattern.disabledForUrl) {
331+
return {valid: true, message: i18nString(UIStrings.patternAlreadyExistsWillBeEnables)};
332+
}
333+
return {valid: false, message: i18nString(UIStrings.patternAlreadyExists)};
305334
}
306335
}
307336

@@ -311,7 +340,7 @@ export function patternValidator(
311340
} catch (e) {
312341
}
313342
if (!regex) {
314-
return {valid: false, errorMessage: i18nString(UIStrings.patternMustBeAValidRegular)};
343+
return {valid: false, message: i18nString(UIStrings.patternMustBeAValidRegular)};
315344
}
316345
return {valid: true};
317346
}

front_end/panels/timeline/components/ignoreListSetting.css

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,34 @@
3030
.new-regex-text-input {
3131
flex: auto;
3232
}
33+
34+
.harmony-input[type="text"] {
35+
/* padding: 3px 6px; */
36+
/* height: 24px; */
37+
border: 1px solid var(--sys-color-neutral-outline);
38+
border-radius: 4px;
39+
outline: none;
40+
41+
&.error-input,
42+
&:invalid {
43+
border-color: var(--sys-color-error);
44+
}
45+
46+
&:not(.error-input):not(:invalid):focus {
47+
border-color: var(--sys-color-state-focus-ring);
48+
}
49+
50+
&:not(.error-input):not(:invalid):hover:not(:focus) {
51+
background: var(--sys-color-state-hover-on-subtle);
52+
}
53+
}
54+
}
55+
56+
.input-validation {
57+
/* 24px is the size of the checkbox, add this margin-left so the text can align with the input */
58+
margin: 5px 0 5px 24px;
59+
60+
&.input-validation-error {
61+
color: var(--sys-color-error);
62+
}
3363
}

0 commit comments

Comments
 (0)