1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-11 13:53:34 +00:00

PM-21651 [For Automation Purposes] add test IDs to notification bar (#14863)

* PM-21651 [For Automation Purposes] Please add IDs to some of the main components

* option items

* dynamic test id

* mitigate feedback

* clean up logic
This commit is contained in:
Daniel Riera
2025-05-23 11:48:23 -04:00
committed by GitHub
parent 0e0be0a3de
commit c6af80f3eb
12 changed files with 70 additions and 15 deletions

View File

@@ -10,6 +10,7 @@ import { AngleUp, AngleDown } from "../icons";
export type OptionSelectionButtonProps = { export type OptionSelectionButtonProps = {
disabled: boolean; disabled: boolean;
icon?: Option["icon"]; icon?: Option["icon"];
id: string;
text?: string; text?: string;
theme: Theme; theme: Theme;
toggledOn: boolean; toggledOn: boolean;
@@ -19,6 +20,7 @@ export type OptionSelectionButtonProps = {
export function OptionSelectionButton({ export function OptionSelectionButton({
disabled, disabled,
icon, icon,
id,
text, text,
theme, theme,
toggledOn, toggledOn,
@@ -31,6 +33,7 @@ export function OptionSelectionButton({
return html` return html`
<button <button
class=${selectionButtonStyles({ disabled, toggledOn, theme })} class=${selectionButtonStyles({ disabled, toggledOn, theme })}
data-testid="${id}-option-selection"
title=${text} title=${text}
type="button" type="button"
aria-haspopup="menu" aria-haspopup="menu"

View File

@@ -12,6 +12,7 @@ export default {
argTypes: { argTypes: {
disabled: { control: "boolean" }, disabled: { control: "boolean" },
handleButtonClick: { control: false }, handleButtonClick: { control: false },
id: { control: "text" },
text: { control: "text" }, text: { control: "text" },
theme: { control: "select", options: [...Object.values(ThemeTypes)] }, theme: { control: "select", options: [...Object.values(ThemeTypes)] },
toggledOn: { control: "boolean" }, toggledOn: { control: "boolean" },
@@ -19,6 +20,7 @@ export default {
args: { args: {
disabled: false, disabled: false,
handleButtonClick: () => alert("Clicked"), handleButtonClick: () => alert("Clicked"),
id: "example-id",
text: "Click Me", text: "Click Me",
theme: ThemeTypes.Light, theme: ThemeTypes.Light,
toggledOn: false, toggledOn: false,

View File

@@ -3,7 +3,10 @@ import { Meta, StoryObj } from "@storybook/web-components";
import { ThemeTypes } from "@bitwarden/common/platform/enums"; import { ThemeTypes } from "@bitwarden/common/platform/enums";
import { NotificationTypes } from "../../../../../notification/abstractions/notification-bar"; import { NotificationTypes } from "../../../../../notification/abstractions/notification-bar";
import { getConfirmationHeaderMessage } from "../../../../../notification/bar"; import {
getConfirmationHeaderMessage,
getNotificationTestId,
} from "../../../../../notification/bar";
import { import {
NotificationConfirmationContainer, NotificationConfirmationContainer,
NotificationConfirmationContainerProps, NotificationConfirmationContainerProps,
@@ -38,7 +41,8 @@ export default {
const Template = (args: NotificationConfirmationContainerProps) => { const Template = (args: NotificationConfirmationContainerProps) => {
const headerMessage = getConfirmationHeaderMessage(args.i18n, args.type, args.error); const headerMessage = getConfirmationHeaderMessage(args.i18n, args.type, args.error);
return NotificationConfirmationContainer({ ...args, headerMessage }); const notificationTestId = getNotificationTestId(args.type, true);
return NotificationConfirmationContainer({ ...args, headerMessage, notificationTestId });
}; };
export const Default: StoryObj<NotificationConfirmationContainerProps> = { export const Default: StoryObj<NotificationConfirmationContainerProps> = {

View File

@@ -5,7 +5,7 @@ import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
import { NotificationTypes } from "../../../../notification/abstractions/notification-bar"; import { NotificationTypes } from "../../../../notification/abstractions/notification-bar";
import { getNotificationHeaderMessage } from "../../../../notification/bar"; import { getNotificationHeaderMessage, getNotificationTestId } from "../../../../notification/bar";
import { NotificationContainer, NotificationContainerProps } from "../../notification/container"; import { NotificationContainer, NotificationContainerProps } from "../../notification/container";
import { mockBrowserI18nGetMessage, mockI18n } from "../mock-data"; import { mockBrowserI18nGetMessage, mockI18n } from "../mock-data";
@@ -49,7 +49,8 @@ export default {
const Template = (args: NotificationContainerProps) => { const Template = (args: NotificationContainerProps) => {
const headerMessage = getNotificationHeaderMessage(args.i18n, args.type); const headerMessage = getNotificationHeaderMessage(args.i18n, args.type);
return NotificationContainer({ ...args, headerMessage }); const notificationTestId = getNotificationTestId(args.type);
return NotificationContainer({ ...args, headerMessage, notificationTestId });
}; };
export const Default: StoryObj<NotificationContainerProps> = { export const Default: StoryObj<NotificationContainerProps> = {

View File

@@ -16,6 +16,7 @@ const mockOptions: Option[] = [
type ComponentProps = { type ComponentProps = {
disabled?: boolean; disabled?: boolean;
id: string;
label?: string; label?: string;
options: Option[]; options: Option[];
theme: Theme; theme: Theme;
@@ -31,16 +32,18 @@ export default {
}, },
args: { args: {
disabled: false, disabled: false,
id: "example-id",
label: undefined, label: undefined,
options: mockOptions, options: mockOptions,
theme: ThemeTypes.Light, theme: ThemeTypes.Light,
}, },
} as Meta<ComponentProps>; } as Meta<ComponentProps>;
const BaseComponent = ({ disabled, label, options, theme }: ComponentProps) => { const BaseComponent = ({ disabled, id, label, options, theme }: ComponentProps) => {
return html` return html`
<option-selection <option-selection
.disabled=${disabled} .disabled=${disabled}
.id=${id}
.label="${label}" .label="${label}"
.options=${options} .options=${options}
theme=${theme} theme=${theme}

View File

@@ -28,6 +28,7 @@ export type NotificationConfirmationContainerProps = NotificationBarIframeInitDa
headerMessage?: string; headerMessage?: string;
i18n: I18n; i18n: I18n;
itemName: string; itemName: string;
notificationTestId: string;
task?: NotificationTaskInfo; task?: NotificationTaskInfo;
type: NotificationType; type: NotificationType;
}; };
@@ -40,6 +41,7 @@ export function NotificationConfirmationContainer({
headerMessage, headerMessage,
i18n, i18n,
itemName, itemName,
notificationTestId,
task, task,
theme = ThemeTypes.Light, theme = ThemeTypes.Light,
type, type,
@@ -68,7 +70,7 @@ export function NotificationConfirmationContainer({
} }
return html` return html`
<div class=${notificationContainerStyles(theme)}> <div data-testid="${notificationTestId}" class=${notificationContainerStyles(theme)}>
${NotificationHeader({ ${NotificationHeader({
handleCloseNotification, handleCloseNotification,
i18n, i18n,

View File

@@ -31,6 +31,7 @@ export type NotificationContainerProps = NotificationBarIframeInitData & {
i18n: I18n; i18n: I18n;
organizations?: OrgView[]; organizations?: OrgView[];
personalVaultIsAllowed?: boolean; personalVaultIsAllowed?: boolean;
notificationTestId: string;
type: NotificationType; // @TODO typing override for generic `NotificationBarIframeInitData.type` type: NotificationType; // @TODO typing override for generic `NotificationBarIframeInitData.type`
}; };
@@ -45,13 +46,14 @@ export function NotificationContainer({
i18n, i18n,
organizations, organizations,
personalVaultIsAllowed = true, personalVaultIsAllowed = true,
notificationTestId,
theme = ThemeTypes.Light, theme = ThemeTypes.Light,
type, type,
}: NotificationContainerProps) { }: NotificationContainerProps) {
const showBody = type !== NotificationTypes.Unlock; const showBody = type !== NotificationTypes.Unlock;
return html` return html`
<div class=${notificationContainerStyles(theme)}> <div data-testid="${notificationTestId}" class=${notificationContainerStyles(theme)}>
${NotificationHeader({ ${NotificationHeader({
handleCloseNotification, handleCloseNotification,
i18n, i18n,

View File

@@ -13,6 +13,7 @@ const { css } = createEmotion({
}); });
export type OptionItemProps = Option & { export type OptionItemProps = Option & {
id: string;
contextLabel?: string; contextLabel?: string;
theme: Theme; theme: Theme;
handleSelection: () => void; handleSelection: () => void;
@@ -20,6 +21,7 @@ export type OptionItemProps = Option & {
export function OptionItem({ export function OptionItem({
contextLabel, contextLabel,
id,
icon, icon,
text, text,
theme, theme,
@@ -44,6 +46,7 @@ export function OptionItem({
return html`<div return html`<div
class=${optionItemStyles} class=${optionItemStyles}
data-testid="${id}-option-item"
key=${value} key=${value}
tabindex="0" tabindex="0"
title=${text} title=${text}

View File

@@ -15,6 +15,7 @@ const { css } = createEmotion({
}); });
export type OptionItemsProps = { export type OptionItemsProps = {
id: string;
theme: Theme; theme: Theme;
topOffset: number; topOffset: number;
label?: string; label?: string;
@@ -23,6 +24,7 @@ export type OptionItemsProps = {
}; };
export function OptionItems({ export function OptionItems({
id,
theme, theme,
topOffset, topOffset,
label, label,
@@ -42,6 +44,7 @@ export function OptionItems({
<div class=${optionsWrapper({ isSafari, theme })}> <div class=${optionsWrapper({ isSafari, theme })}>
${options.map((option) => ${options.map((option) =>
OptionItem({ OptionItem({
id,
...option, ...option,
theme, theme,
contextLabel: label, contextLabel: label,

View File

@@ -20,6 +20,9 @@ export class OptionSelection extends LitElement {
@property() @property()
disabled: boolean = false; disabled: boolean = false;
@property()
id: string = "";
@property() @property()
label?: string; label?: string;
@@ -130,6 +133,7 @@ export class OptionSelection extends LitElement {
${OptionSelectionButton({ ${OptionSelectionButton({
disabled: this.disabled, disabled: this.disabled,
icon: this.selection?.icon, icon: this.selection?.icon,
id: this.id,
text: this.selection?.text, text: this.selection?.text,
theme: this.theme, theme: this.theme,
toggledOn: this.showMenu, toggledOn: this.showMenu,
@@ -137,6 +141,7 @@ export class OptionSelection extends LitElement {
})} })}
${this.showMenu ${this.showMenu
? OptionItems({ ? OptionItems({
id: this.id,
label: this.label, label: this.label,
options: this.options, options: this.options,
theme: this.theme, theme: this.theme,

View File

@@ -38,6 +38,7 @@ export function ButtonRow({ theme, primaryButton, selectButtons }: ButtonRowProp
<option-selection <option-selection
key=${id} key=${id}
theme=${theme} theme=${theme}
.id=${id}
.label=${label} .label=${label}
.options=${options} .options=${options}
.handleSelectionUpdate=${handleSelectionUpdate} .handleSelectionUpdate=${handleSelectionUpdate}

View File

@@ -175,6 +175,27 @@ function resolveNotificationType(initData: NotificationBarIframeInitData): Notif
return initData.type as NotificationType; return initData.type as NotificationType;
} }
/**
* Returns the appropriate test ID based on the resolved notification type.
*
* @param type - The resolved NotificationType.
* @param isConfirmation - Optional flag for confirmation vs. notification container.
*/
export function getNotificationTestId(
notificationType: NotificationType,
isConfirmation = false,
): string {
if (isConfirmation) {
return "confirmation-notification-bar";
}
return {
[NotificationTypes.Unlock]: "unlock-notification-bar",
[NotificationTypes.Add]: "save-notification-bar",
[NotificationTypes.Change]: "update-notification-bar",
}[notificationType];
}
/** /**
* Sets the text content of an element identified by ID within a template's content. * Sets the text content of an element identified by ID within a template's content.
* *
@@ -212,6 +233,7 @@ async function initNotificationBar(message: NotificationBarWindowMessage) {
if (useComponentBar) { if (useComponentBar) {
const resolvedType = resolveNotificationType(notificationBarIframeInitData); const resolvedType = resolveNotificationType(notificationBarIframeInitData);
const headerMessage = getNotificationHeaderMessage(i18n, resolvedType); const headerMessage = getNotificationHeaderMessage(i18n, resolvedType);
const notificationTestId = getNotificationTestId(resolvedType);
appendHeaderMessageToTitle(headerMessage); appendHeaderMessageToTitle(headerMessage);
document.body.innerHTML = ""; document.body.innerHTML = "";
@@ -224,6 +246,7 @@ async function initNotificationBar(message: NotificationBarWindowMessage) {
...notificationBarIframeInitData, ...notificationBarIframeInitData,
headerMessage, headerMessage,
type: resolvedType, type: resolvedType,
notificationTestId,
theme: resolvedTheme, theme: resolvedTheme,
personalVaultIsAllowed: !personalVaultDisallowed, personalVaultIsAllowed: !personalVaultDisallowed,
handleCloseNotification, handleCloseNotification,
@@ -269,6 +292,7 @@ async function initNotificationBar(message: NotificationBarWindowMessage) {
headerMessage, headerMessage,
type: resolvedType, type: resolvedType,
theme: resolvedTheme, theme: resolvedTheme,
notificationTestId,
personalVaultIsAllowed: !personalVaultDisallowed, personalVaultIsAllowed: !personalVaultDisallowed,
handleCloseNotification, handleCloseNotification,
handleSaveAction, handleSaveAction,
@@ -499,23 +523,25 @@ function handleSaveCipherConfirmation(message: NotificationBarWindowMessage) {
const resolvedTheme = getResolvedTheme(theme ?? ThemeTypes.Light); const resolvedTheme = getResolvedTheme(theme ?? ThemeTypes.Light);
const resolvedType = resolveNotificationType(notificationBarIframeInitData); const resolvedType = resolveNotificationType(notificationBarIframeInitData);
const headerMessage = getConfirmationHeaderMessage(i18n, resolvedType, error); const headerMessage = getConfirmationHeaderMessage(i18n, resolvedType, error);
const notificationTestId = getNotificationTestId(resolvedType, true);
globalThis.setTimeout(() => sendPlatformMessage({ command: "bgCloseNotificationBar" }), 5000); globalThis.setTimeout(() => sendPlatformMessage({ command: "bgCloseNotificationBar" }), 5000);
return render( return render(
NotificationConfirmationContainer({ NotificationConfirmationContainer({
...notificationBarIframeInitData, ...notificationBarIframeInitData,
type: type as NotificationType,
theme: resolvedTheme,
handleCloseNotification,
headerMessage,
i18n,
error, error,
itemName: itemName ?? i18n.typeLogin, handleCloseNotification,
task, handleOpenTasks: () => sendPlatformMessage({ command: "bgOpenAtRisksPasswords" }),
handleOpenVault: (e: Event) => handleOpenVault: (e: Event) =>
cipherId ? openViewVaultItemPopout(cipherId) : openAddEditVaultItemPopout(e, {}), cipherId ? openViewVaultItemPopout(cipherId) : openAddEditVaultItemPopout(e, {}),
handleOpenTasks: () => sendPlatformMessage({ command: "bgOpenAtRisksPasswords" }), headerMessage,
i18n,
itemName: itemName ?? i18n.typeLogin,
notificationTestId,
task,
theme: resolvedTheme,
type: type as NotificationType,
}), }),
document.body, document.body,
); );