Skip to content

Commit feb21f6

Browse files
OrKoNDevtools-frontend LUCI CQ
authored andcommitted
Add assertScreenshot to unit tests
This CL adds a custom browser wrapper that starts the browser using Puppeteer instead of letting Karma to directly start the browser. The wrapper also exposes a binding that allows the test code to capture a screenshot of the test DOM. The helper reuses existing screenshot assertions and reports the result back to the test code. This CL also adds args to improve stability of screenshots and fixes the font to be a Roboto font loaded from Google Fonts. To test: `npm run test -- front_end/panels/ai_assistance/components/UserActionRow.test.ts` Bug: 401489541 Change-Id: I04889d0f0caa5c468fd35f98c66b0e6ac393de18 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6330749 Auto-Submit: Alex Rudenko <[email protected]> Commit-Queue: Danil Somsikov <[email protected]> Reviewed-by: Danil Somsikov <[email protected]>
1 parent 10ed841 commit feb21f6

File tree

9 files changed

+297
-185
lines changed

9 files changed

+297
-185
lines changed

front_end/panels/ai_assistance/ai_assistance.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export * from './ChangeManager.js';
1717
export * from './components/ChatView.js';
1818
export * from './components/MarkdownRendererWithCodeBlock.js';
1919
export * from './SelectWorkspaceDialog.js';
20-
export * from './components/UserActionRow.js';
20+
export * as UserActionRow from './components/UserActionRow.js';
2121
export * from './EvaluateAction.js';
2222
export * from './ExtensionScope.js';
2323
export * as PatchWidget from './PatchWidget.js';

front_end/panels/ai_assistance/components/UserActionRow.test.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,18 @@
33
// found in the LICENSE file.
44

55
import * as Host from '../../../core/host/host.js';
6+
import {assertScreenshot, renderElementIntoDOM} from '../../../testing/DOMHelpers.js';
67
import {
78
describeWithEnvironment,
89
} from '../../../testing/EnvironmentHelpers.js';
910
import {createViewFunctionStub, type ViewFunctionStub} from '../../../testing/ViewFunctionHelpers.js';
1011
import * as AiAssistance from '../ai_assistance.js';
1112

1213
describeWithEnvironment('UserActionRow', () => {
13-
function createComponent(props: AiAssistance.UserActionRowWidgetParams):
14-
[ViewFunctionStub<typeof AiAssistance.UserActionRow>, AiAssistance.UserActionRow] {
15-
const view = createViewFunctionStub(AiAssistance.UserActionRow);
16-
const component = new AiAssistance.UserActionRow(undefined, view);
14+
function createComponent(props: AiAssistance.UserActionRow.UserActionRowWidgetParams):
15+
[ViewFunctionStub<typeof AiAssistance.UserActionRow.UserActionRow>, AiAssistance.UserActionRow.UserActionRow] {
16+
const view = createViewFunctionStub(AiAssistance.UserActionRow.UserActionRow);
17+
const component = new AiAssistance.UserActionRow.UserActionRow(undefined, view);
1718
Object.assign(component, props);
1819
component.wasShown();
1920
return [view, component];
@@ -92,4 +93,27 @@ describeWithEnvironment('UserActionRow', () => {
9293
expect(view.input.isSubmitButtonDisabled).equals(true);
9394
}
9495
});
96+
97+
describe('view', () => {
98+
it('looks fine', async () => {
99+
const target = document.createElement('div');
100+
renderElementIntoDOM(target);
101+
AiAssistance.UserActionRow.DEFAULT_VIEW(
102+
{
103+
onRatingClick: () => {},
104+
onReportClick: () => {},
105+
scrollSuggestionsScrollContainer: () => {},
106+
onSuggestionsScrollOrResize: () => {},
107+
onSuggestionClick: () => {},
108+
onSubmit: () => {},
109+
onClose: () => {},
110+
onInputChange: () => {},
111+
showRateButtons: true,
112+
isSubmitButtonDisabled: false,
113+
isShowingFeedbackForm: true,
114+
},
115+
{}, target);
116+
await assertScreenshot('ai_assistance/user_action_row.png');
117+
});
118+
});
95119
});

front_end/panels/ai_assistance/components/UserActionRow.ts

Lines changed: 149 additions & 151 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,152 @@ export interface UserActionRowWidgetParams {
111111
canShowFeedbackForm: boolean;
112112
}
113113

114-
export type View = (input: UserActionRowViewInput, output: ViewOutput, target: HTMLElement) => void;
114+
export const DEFAULT_VIEW = (input: UserActionRowViewInput, output: ViewOutput, target: HTMLElement): void => {
115+
// clang-format off
116+
Lit.render(html`
117+
<style>${Input.textInputStylesRaw.cssContent}</style>
118+
<style>${userActionRowStyles.cssContent}</style>
119+
<div class="ai-assistance-feedback-row">
120+
<div class="rate-buttons">
121+
${input.showRateButtons ? html`
122+
<devtools-button
123+
.data=${{
124+
variant: Buttons.Button.Variant.ICON,
125+
size: Buttons.Button.Size.SMALL,
126+
iconName: 'thumb-up',
127+
toggledIconName: 'thumb-up-filled',
128+
toggled: input.currentRating === Host.AidaClient.Rating.POSITIVE,
129+
toggleType: Buttons.Button.ToggleType.PRIMARY,
130+
title: lockedString(UIStringsNotTranslate.thumbsUp),
131+
jslogContext: 'thumbs-up',
132+
} as Buttons.Button.ButtonData}
133+
@click=${() => input.onRatingClick(Host.AidaClient.Rating.POSITIVE)}
134+
></devtools-button>
135+
<devtools-button
136+
.data=${{
137+
variant: Buttons.Button.Variant.ICON,
138+
size: Buttons.Button.Size.SMALL,
139+
iconName: 'thumb-down',
140+
toggledIconName: 'thumb-down-filled',
141+
toggled: input.currentRating === Host.AidaClient.Rating.NEGATIVE,
142+
toggleType: Buttons.Button.ToggleType.PRIMARY,
143+
title: lockedString(UIStringsNotTranslate.thumbsDown),
144+
jslogContext: 'thumbs-down',
145+
} as Buttons.Button.ButtonData}
146+
@click=${() => input.onRatingClick(Host.AidaClient.Rating.NEGATIVE)}
147+
></devtools-button>
148+
<div class="vertical-separator"></div>
149+
`: Lit.nothing}
150+
<devtools-button
151+
.data=${
152+
{
153+
variant: Buttons.Button.Variant.ICON,
154+
size: Buttons.Button.Size.SMALL,
155+
title: lockedString(UIStringsNotTranslate.report),
156+
iconName: 'report',
157+
jslogContext: 'report',
158+
} as Buttons.Button.ButtonData
159+
}
160+
@click=${input.onReportClick}
161+
></devtools-button>
162+
</div>
163+
${input.suggestions ? html`<div class="suggestions-container">
164+
<div class="scroll-button-container left hidden" ${ref(element => { output.suggestionsLeftScrollButtonContainer = element; } )}>
165+
<devtools-button
166+
class='scroll-button'
167+
.data=${{
168+
variant: Buttons.Button.Variant.ICON,
169+
size: Buttons.Button.Size.SMALL,
170+
iconName: 'chevron-left',
171+
title: lockedString(UIStringsNotTranslate.scrollToPrevious),
172+
jslogContext: 'chevron-left',
173+
} as Buttons.Button.ButtonData}
174+
@click=${() => input.scrollSuggestionsScrollContainer('left')}
175+
></devtools-button>
176+
</div>
177+
<div class="suggestions-scroll-container" @scroll=${input.onSuggestionsScrollOrResize} ${ref(element => { output.suggestionsScrollContainer = element; })}>
178+
${input.suggestions.map(suggestion => html`<devtools-button
179+
class='suggestion'
180+
.data=${{
181+
variant: Buttons.Button.Variant.OUTLINED,
182+
title: suggestion,
183+
jslogContext: 'suggestion',
184+
} as Buttons.Button.ButtonData}
185+
@click=${() => input.onSuggestionClick(suggestion)}
186+
>${suggestion}</devtools-button>`)}
187+
</div>
188+
<div class="scroll-button-container right hidden" ${ref(element => { output.suggestionsRightScrollButtonContainer = element; })}>
189+
<devtools-button
190+
class='scroll-button'
191+
.data=${{
192+
variant: Buttons.Button.Variant.ICON,
193+
size: Buttons.Button.Size.SMALL,
194+
iconName: 'chevron-right',
195+
title: lockedString(UIStringsNotTranslate.scrollToNext),
196+
jslogContext: 'chevron-right',
197+
} as Buttons.Button.ButtonData}
198+
@click=${() => input.scrollSuggestionsScrollContainer('right')}
199+
></devtools-button>
200+
</div>
201+
</div>` : Lit.nothing}
202+
</div>
203+
${input.isShowingFeedbackForm ? html`
204+
<form class="feedback-form" @submit=${input.onSubmit}>
205+
<div class="feedback-header">
206+
<h4 class="feedback-title">${lockedString(
207+
UIStringsNotTranslate.whyThisRating,
208+
)}</h4>
209+
<devtools-button
210+
aria-label=${lockedString(UIStringsNotTranslate.close)}
211+
@click=${input.onClose}
212+
.data=${
213+
{
214+
variant: Buttons.Button.Variant.ICON,
215+
iconName: 'cross',
216+
size: Buttons.Button.Size.SMALL,
217+
title: lockedString(UIStringsNotTranslate.close),
218+
jslogContext: 'close',
219+
} as Buttons.Button.ButtonData
220+
}
221+
></devtools-button>
222+
</div>
223+
<input
224+
type="text"
225+
class="devtools-text-input feedback-input"
226+
@input=${(event: KeyboardEvent) => input.onInputChange((event.target as HTMLInputElement).value)}
227+
placeholder=${lockedString(
228+
UIStringsNotTranslate.provideFeedbackPlaceholder,
229+
)}
230+
jslog=${VisualLogging.textField('feedback').track({ keydown: 'Enter' })}
231+
>
232+
<span class="feedback-disclaimer">${
233+
lockedString(UIStringsNotTranslate.disclaimer)
234+
}</span>
235+
<div>
236+
<devtools-button
237+
aria-label=${lockedString(UIStringsNotTranslate.submit)}
238+
.data=${
239+
{
240+
type: 'submit',
241+
disabled: input.isSubmitButtonDisabled,
242+
variant: Buttons.Button.Variant.OUTLINED,
243+
size: Buttons.Button.Size.SMALL,
244+
title: lockedString(UIStringsNotTranslate.submit),
245+
jslogContext: 'send',
246+
} as Buttons.Button.ButtonData
247+
}
248+
>${
249+
lockedString(UIStringsNotTranslate.submit)
250+
}</devtools-button>
251+
</div>
252+
</div>
253+
</form>
254+
` : Lit.nothing}
255+
`, target, {host: target});
256+
// clang-format on
257+
};
258+
259+
export type View = typeof DEFAULT_VIEW;
115260

116261
/**
117262
* This presenter has too many responsibilities (rating buttons, feedback
@@ -132,159 +277,12 @@ export class UserActionRow extends UI.Widget.Widget implements UserActionRowWidg
132277
#isShowingFeedbackForm = false;
133278
#isSubmitButtonDisabled = true;
134279

135-
#view: View;
280+
view: View;
136281
#viewOutput: ViewOutput = {};
137282

138283
constructor(element?: HTMLElement, view?: View) {
139284
super(false, false, element);
140-
this.registerRequiredCSS(Input.textInputStylesRaw);
141-
this.registerRequiredCSS(userActionRowStyles);
142-
// clang-format off
143-
this.#view = view ?? ((input, output, target) => {
144-
Lit.render(
145-
html`
146-
<div class="ai-assistance-feedback-row">
147-
<div class="rate-buttons">
148-
${input.showRateButtons ? html`
149-
<devtools-button
150-
.data=${{
151-
variant: Buttons.Button.Variant.ICON,
152-
size: Buttons.Button.Size.SMALL,
153-
iconName: 'thumb-up',
154-
toggledIconName: 'thumb-up-filled',
155-
toggled: input.currentRating === Host.AidaClient.Rating.POSITIVE,
156-
toggleType: Buttons.Button.ToggleType.PRIMARY,
157-
title: lockedString(UIStringsNotTranslate.thumbsUp),
158-
jslogContext: 'thumbs-up',
159-
} as Buttons.Button.ButtonData}
160-
@click=${() => input.onRatingClick(Host.AidaClient.Rating.POSITIVE)}
161-
></devtools-button>
162-
<devtools-button
163-
.data=${{
164-
variant: Buttons.Button.Variant.ICON,
165-
size: Buttons.Button.Size.SMALL,
166-
iconName: 'thumb-down',
167-
toggledIconName: 'thumb-down-filled',
168-
toggled: input.currentRating === Host.AidaClient.Rating.NEGATIVE,
169-
toggleType: Buttons.Button.ToggleType.PRIMARY,
170-
title: lockedString(UIStringsNotTranslate.thumbsDown),
171-
jslogContext: 'thumbs-down',
172-
} as Buttons.Button.ButtonData}
173-
@click=${() => input.onRatingClick(Host.AidaClient.Rating.NEGATIVE)}
174-
></devtools-button>
175-
<div class="vertical-separator"></div>
176-
`: Lit.nothing}
177-
<devtools-button
178-
.data=${
179-
{
180-
variant: Buttons.Button.Variant.ICON,
181-
size: Buttons.Button.Size.SMALL,
182-
title: lockedString(UIStringsNotTranslate.report),
183-
iconName: 'report',
184-
jslogContext: 'report',
185-
} as Buttons.Button.ButtonData
186-
}
187-
@click=${input.onReportClick}
188-
></devtools-button>
189-
</div>
190-
${input.suggestions ? html`<div class="suggestions-container">
191-
<div class="scroll-button-container left hidden" ${ref(element => { output.suggestionsLeftScrollButtonContainer = element; } )}>
192-
<devtools-button
193-
class='scroll-button'
194-
.data=${{
195-
variant: Buttons.Button.Variant.ICON,
196-
size: Buttons.Button.Size.SMALL,
197-
iconName: 'chevron-left',
198-
title: lockedString(UIStringsNotTranslate.scrollToPrevious),
199-
jslogContext: 'chevron-left',
200-
} as Buttons.Button.ButtonData}
201-
@click=${() => input.scrollSuggestionsScrollContainer('left')}
202-
></devtools-button>
203-
</div>
204-
<div class="suggestions-scroll-container" @scroll=${input.onSuggestionsScrollOrResize} ${ref(element => { output.suggestionsScrollContainer = element; })}>
205-
${input.suggestions.map(suggestion => html`<devtools-button
206-
class='suggestion'
207-
.data=${{
208-
variant: Buttons.Button.Variant.OUTLINED,
209-
title: suggestion,
210-
jslogContext: 'suggestion',
211-
} as Buttons.Button.ButtonData}
212-
@click=${() => input.onSuggestionClick(suggestion)}
213-
>${suggestion}</devtools-button>`)}
214-
</div>
215-
<div class="scroll-button-container right hidden" ${ref(element => { output.suggestionsRightScrollButtonContainer = element; })}>
216-
<devtools-button
217-
class='scroll-button'
218-
.data=${{
219-
variant: Buttons.Button.Variant.ICON,
220-
size: Buttons.Button.Size.SMALL,
221-
iconName: 'chevron-right',
222-
title: lockedString(UIStringsNotTranslate.scrollToNext),
223-
jslogContext: 'chevron-right',
224-
} as Buttons.Button.ButtonData}
225-
@click=${() => input.scrollSuggestionsScrollContainer('right')}
226-
></devtools-button>
227-
</div>
228-
</div>` : Lit.nothing}
229-
</div>
230-
${input.isShowingFeedbackForm ? html`
231-
<form class="feedback-form" @submit=${input.onSubmit}>
232-
<div class="feedback-header">
233-
<h4 class="feedback-title">${lockedString(
234-
UIStringsNotTranslate.whyThisRating,
235-
)}</h4>
236-
<devtools-button
237-
aria-label=${lockedString(UIStringsNotTranslate.close)}
238-
@click=${input.onClose}
239-
.data=${
240-
{
241-
variant: Buttons.Button.Variant.ICON,
242-
iconName: 'cross',
243-
size: Buttons.Button.Size.SMALL,
244-
title: lockedString(UIStringsNotTranslate.close),
245-
jslogContext: 'close',
246-
} as Buttons.Button.ButtonData
247-
}
248-
></devtools-button>
249-
</div>
250-
<input
251-
type="text"
252-
class="devtools-text-input feedback-input"
253-
@input=${(event: KeyboardEvent) => input.onInputChange((event.target as HTMLInputElement).value)}
254-
placeholder=${lockedString(
255-
UIStringsNotTranslate.provideFeedbackPlaceholder,
256-
)}
257-
jslog=${VisualLogging.textField('feedback').track({ keydown: 'Enter' })}
258-
>
259-
<span class="feedback-disclaimer">${
260-
lockedString(UIStringsNotTranslate.disclaimer)
261-
}</span>
262-
<div>
263-
<devtools-button
264-
aria-label=${lockedString(UIStringsNotTranslate.submit)}
265-
.data=${
266-
{
267-
type: 'submit',
268-
disabled: input.isSubmitButtonDisabled,
269-
variant: Buttons.Button.Variant.OUTLINED,
270-
size: Buttons.Button.Size.SMALL,
271-
title: lockedString(UIStringsNotTranslate.submit),
272-
jslogContext: 'send',
273-
} as Buttons.Button.ButtonData
274-
}
275-
>${
276-
lockedString(UIStringsNotTranslate.submit)
277-
}</devtools-button>
278-
</div>
279-
</div>
280-
</form>
281-
` : Lit.nothing}
282-
`,
283-
target,
284-
{host: target}
285-
);
286-
}) as View;
287-
// clang-format on
285+
this.view = view ?? DEFAULT_VIEW;
288286
}
289287

290288
override wasShown(): void {
@@ -298,7 +296,7 @@ export class UserActionRow extends UI.Widget.Widget implements UserActionRowWidg
298296
}
299297

300298
override performUpdate(): Promise<void>|void {
301-
this.#view(
299+
this.view(
302300
{
303301
onSuggestionClick: this.onSuggestionClick,
304302
onRatingClick: this.#handleRateClick.bind(this),

front_end/panels/ai_assistance/components/userActionRow.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77

88
.ai-assistance-feedback-row {
9+
font-family: var(--default-font-family);
910
width: 100%;
1011
display: flex;
1112
gap: var(--sys-size-8);

0 commit comments

Comments
 (0)