From 0dc06b1d77e979010cf33f2ce3b17436da33a06e Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Fri, 15 May 2026 11:04:30 -0300 Subject: [PATCH 1/9] [ref-ui] refactor the ui and design to feel more up-to-date - Updated tests along --- package.json | 1 + playwright.config.ts | 2 +- public/locales/de.json | 8 +- public/locales/en.json | 6 + public/locales/fr-FR.json | 8 +- public/locales/it.json | 8 +- public/locales/ja.json | 8 +- public/locales/pt-BR.json | 8 +- src/components/modal/component.tsx | 24 +- .../modal/picked-user-view/styles.tsx | 18 +- .../modal/presenter-view/component.tsx | 415 +++++++++++------- .../modal/presenter-view/styles.tsx | 339 +++++++++++--- src/components/modal/styles.tsx | 68 ++- tests/README.md | 10 +- tests/behavioral/helpers.ts | 20 + tests/behavioral/multi-user.spec.ts | 40 +- tests/behavioral/single-user.spec.ts | 61 +-- tests/structural/test.spec.ts | 37 +- 18 files changed, 742 insertions(+), 339 deletions(-) diff --git a/package.json b/package.json index 863aa3e..de88cd6 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "lint:watch": "watch 'yarn lint'", "publish-plugin:dev": "bash scripts/copy-plugin-to-container.sh", "test": "npx playwright test", + "test:isolated": "TEST_MEETINGS=isolated npx playwright test", "test-chromium-ci": "export CI='true' && npx playwright test --project=chromium" }, "browserslist": { diff --git a/playwright.config.ts b/playwright.config.ts index 49bf569..dd276dd 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -9,7 +9,7 @@ export default defineConfig({ fullyParallel: true, forbidOnly: CI, reporter: CI - ? [['list'], ['blob']] + ? [['blob'], ['./core/setup/customReporter.ts']] : [['list'], ['html', { open: 'never' }]], use: { baseURL: server, diff --git a/public/locales/de.json b/public/locales/de.json index f9e7c1d..e163cf4 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -18,5 +18,11 @@ "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickUser": "Wähle {0}", "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickAgain": "Wähle erneut", "pickRandomUserPlugin.actionsButtonDropdown.label.pickUser": "Wähle zufälligen Teilnehmer", - "pickRandomUserPlugin.actionsButtonDropdown.label.viewLastPickedUser": "Zeige zuletzt gewählten Teilnehmer" + "pickRandomUserPlugin.actionsButtonDropdown.label.viewLastPickedUser": "Zeige zuletzt gewählten Teilnehmer", + "pickRandomUserPlugin.modal.title": "Zufälligen Teilnehmer wählen", + "pickRandomUserPlugin.modal.closeButton.ariaLabel": "Schließen", + "pickRandomUserPlugin.modal.presenterView.availableSection.emptyState": "Keine {0} zur Auswahl verfügbar", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.emptyState": "Noch kein Teilnehmer ausgewählt", + "pickRandomUserPlugin.modal.presenterView.roleLabel.moderator": "Moderator", + "pickRandomUserPlugin.modal.presenterView.roleLabel.presenter": "Präsentator" } diff --git a/public/locales/en.json b/public/locales/en.json index 383e974..3aac237 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -19,6 +19,12 @@ "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickAgain": "Pick again", "pickRandomUserPlugin.actionsButtonDropdown.label.pickUser": "Pick random user", "pickRandomUserPlugin.actionsButtonDropdown.label.viewLastPickedUser": "Display last randomly picked user", + "pickRandomUserPlugin.modal.title": "Pick random user", + "pickRandomUserPlugin.modal.closeButton.ariaLabel": "Close", + "pickRandomUserPlugin.modal.presenterView.availableSection.emptyState": "No {0} available for selection", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.emptyState": "No user selected yet", + "pickRandomUserPlugin.modal.presenterView.roleLabel.moderator": "moderator", + "pickRandomUserPlugin.modal.presenterView.roleLabel.presenter": "presenter", "pickRandomUserPlugin.modal.closeDelayMessage": "You can close this modal in {seconds} seconds", "pickRandomUserPlugin.modal.closeDelayMessageSingular": "You can close this modal in {seconds} second" } diff --git a/public/locales/fr-FR.json b/public/locales/fr-FR.json index 716933a..04ad3cd 100644 --- a/public/locales/fr-FR.json +++ b/public/locales/fr-FR.json @@ -17,5 +17,11 @@ "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickUser": "Sélectionner {0}", "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickAgain": "Sélectionner à nouveau", "pickRandomUserPlugin.actionsButtonDropdown.label.pickUser": "Sélectionner un participant au hasard", - "pickRandomUserPlugin.actionsButtonDropdown.label.viewLastPickedUser": "Afficher le dernier participant sélectionné au hasard" + "pickRandomUserPlugin.actionsButtonDropdown.label.viewLastPickedUser": "Afficher le dernier participant sélectionné au hasard", + "pickRandomUserPlugin.modal.title": "Sélectionner un participant au hasard", + "pickRandomUserPlugin.modal.closeButton.ariaLabel": "Fermer", + "pickRandomUserPlugin.modal.presenterView.availableSection.emptyState": "Aucun {0} disponible pour la sélection", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.emptyState": "Aucun participant sélectionné pour l'instant", + "pickRandomUserPlugin.modal.presenterView.roleLabel.moderator": "modérateur", + "pickRandomUserPlugin.modal.presenterView.roleLabel.presenter": "présentateur" } diff --git a/public/locales/it.json b/public/locales/it.json index 4a5591c..7f312ab 100644 --- a/public/locales/it.json +++ b/public/locales/it.json @@ -17,5 +17,11 @@ "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickUser": "Scegli {0}", "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickAgain": "Scegli di nuovo", "pickRandomUserPlugin.actionsButtonDropdown.label.pickUser": "Scegli un utente casuale", - "pickRandomUserPlugin.actionsButtonDropdown.label.viewLastPickedUser": "Visualizza l'ultimo utente scelto casualmente" + "pickRandomUserPlugin.actionsButtonDropdown.label.viewLastPickedUser": "Visualizza l'ultimo utente scelto casualmente", + "pickRandomUserPlugin.modal.title": "Scegli un utente casuale", + "pickRandomUserPlugin.modal.closeButton.ariaLabel": "Chiudi", + "pickRandomUserPlugin.modal.presenterView.availableSection.emptyState": "Nessun {0} disponibile per la selezione", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.emptyState": "Nessun utente selezionato ancora", + "pickRandomUserPlugin.modal.presenterView.roleLabel.moderator": "moderatore", + "pickRandomUserPlugin.modal.presenterView.roleLabel.presenter": "presentatore" } diff --git a/public/locales/ja.json b/public/locales/ja.json index d21e691..fcc10c0 100644 --- a/public/locales/ja.json +++ b/public/locales/ja.json @@ -18,5 +18,11 @@ "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickUser": "{0}から指名する", "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickAgain": "もう一度指名する", "pickRandomUserPlugin.actionsButtonDropdown.label.pickUser": "ランダムに指名する", - "pickRandomUserPlugin.actionsButtonDropdown.label.viewLastPickedUser": "最後に指名されたユーザーを表示" + "pickRandomUserPlugin.actionsButtonDropdown.label.viewLastPickedUser": "最後に指名されたユーザーを表示", + "pickRandomUserPlugin.modal.title": "ランダムにユーザーを選択", + "pickRandomUserPlugin.modal.closeButton.ariaLabel": "閉じる", + "pickRandomUserPlugin.modal.presenterView.availableSection.emptyState": "選択可能な{0}はいません", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.emptyState": "まだユーザーが選択されていません", + "pickRandomUserPlugin.modal.presenterView.roleLabel.moderator": "モデレーター", + "pickRandomUserPlugin.modal.presenterView.roleLabel.presenter": "発表者" } diff --git a/public/locales/pt-BR.json b/public/locales/pt-BR.json index 4cceabe..c2c239c 100644 --- a/public/locales/pt-BR.json +++ b/public/locales/pt-BR.json @@ -18,5 +18,11 @@ "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickUser": "Selecionar {0}", "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickAgain": "Selecionar novamente", "pickRandomUserPlugin.actionsButtonDropdown.label.pickUser": "Selecionar usuário aleatório", - "pickRandomUserPlugin.actionsButtonDropdown.label.viewLastPickedUser": "Exibir último usuário selecionado aleatoriamente" + "pickRandomUserPlugin.actionsButtonDropdown.label.viewLastPickedUser": "Exibir último usuário selecionado aleatoriamente", + "pickRandomUserPlugin.modal.title": "Selecionar usuário aleatório", + "pickRandomUserPlugin.modal.closeButton.ariaLabel": "Fechar", + "pickRandomUserPlugin.modal.presenterView.availableSection.emptyState": "Nenhum {0} disponível para seleção", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.emptyState": "Nenhum usuário selecionado ainda", + "pickRandomUserPlugin.modal.presenterView.roleLabel.moderator": "moderador", + "pickRandomUserPlugin.modal.presenterView.roleLabel.presenter": "apresentador" } diff --git a/src/components/modal/component.tsx b/src/components/modal/component.tsx index 9d91194..6de75a8 100644 --- a/src/components/modal/component.tsx +++ b/src/components/modal/component.tsx @@ -13,6 +13,16 @@ const intlMessages = defineMessages({ description: 'Title to show that current user has been picked', defaultMessage: 'You have been randomly picked', }, + modalTitle: { + id: 'pickRandomUserPlugin.modal.title', + description: 'Title of the pick random user modal', + defaultMessage: 'Pick random user', + }, + closeButtonAriaLabel: { + id: 'pickRandomUserPlugin.modal.closeButton.ariaLabel', + description: 'Aria label for the modal close button', + defaultMessage: 'Close', + }, }); export function PickUserModal(props: PickUserModalProps) { @@ -80,18 +90,19 @@ export function PickUserModal(props: PickUserModalProps) { shouldCloseOnOverlayClick={canClose} shouldCloseOnEsc={canClose} > - + + + {intl.formatMessage(intlMessages.modalTitle)} + - + - + { showPresenterView ? ( @@ -121,7 +132,6 @@ export function PickUserModal(props: PickUserModalProps) { }} /> ) - } ); diff --git a/src/components/modal/picked-user-view/styles.tsx b/src/components/modal/picked-user-view/styles.tsx index 8ab2364..6e74782 100644 --- a/src/components/modal/picked-user-view/styles.tsx +++ b/src/components/modal/picked-user-view/styles.tsx @@ -11,7 +11,7 @@ const PickedUserViewWrapper = styled.div` const PickedUserViewTitle = styled.h1` font-weight: 600; - font-size: 20px; + font-size: 1.25rem; `; const PickedUserAvatarInitials = styled.div` @@ -39,17 +39,18 @@ const PickedUserAvatarImage = styled.img` `; const PickedUserName = styled.p` - font-size: 30px; + font-size: 1.875rem; font-weight: 500; `; const BackButton = styled.button` - border: 3px solid transparent; + border: 0.1875rem solid transparent; + margin-bottom: 1rem; overflow: visible; display: inline-block; background-color: var(--btn-primary-bg, var(--color-primary, #0F70D7)); color: var(--btn-primary-color, var(--color-white, #FFF)); - border-radius: 2px; + border-radius: 0.125rem; font-weight: 600; line-height: 1; text-align: center; @@ -57,7 +58,7 @@ const BackButton = styled.button` vertical-align: middle; cursor: pointer; user-select: none; - padding: 8px 15px; + padding: 0.5rem 0.9375rem; &:hover { color: var(--btn-primary-color, var(--color-white, #FFF)); background-color: var(--btn-primary-hover-bg, #0C57A7) !important; @@ -79,11 +80,10 @@ const CountdownMessage = styled.div` const CountdownBarContainer = styled.div` width: 100%; - height: 4px; + height: 0.25rem; background-color: #e9ecef; - border-radius: 2px; + border-radius: 0.125rem; overflow: hidden; - margin-top: 1rem; `; const CountdownBar = styled.div<{ progress: number }>` @@ -91,7 +91,7 @@ const CountdownBar = styled.div<{ progress: number }>` background: linear-gradient(90deg, #0F70D7 0%, #0C57A7 100%); width: ${({ progress }) => progress}%; transition: width linear 0.1s; - border-radius: 2px; + border-radius: 0.125rem; `; export { diff --git a/src/components/modal/presenter-view/component.tsx b/src/components/modal/presenter-view/component.tsx index b771eab..f9c5760 100644 --- a/src/components/modal/presenter-view/component.tsx +++ b/src/components/modal/presenter-view/component.tsx @@ -23,7 +23,7 @@ const intlMessages = defineMessages({ includePresenterLabel: { id: 'pickRandomUserPlugin.modal.presenterView.optionSection.includePresenterLabel', description: 'Label of skip presenter`s option on modal`s presenter view', - defaultMessage: 'Skip presenter', + defaultMessage: 'Include presenter', }, includePickedUsersLabel: { id: 'pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel', @@ -80,37 +80,82 @@ const intlMessages = defineMessages({ description: 'Label of the button to pick another user', defaultMessage: 'Pick again', }, + emptyState: { + id: 'pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.emptyState', + description: 'Empty state text shown when no user has been picked yet', + defaultMessage: 'No user selected yet', + }, + availableEmptyState: { + id: 'pickRandomUserPlugin.modal.presenterView.availableSection.emptyState', + description: 'Empty state text shown when no user is available for selection', + defaultMessage: 'No {0} available for selection', + }, + moderatorRoleLabel: { + id: 'pickRandomUserPlugin.modal.presenterView.roleLabel.moderator', + description: 'Role badge label for moderators', + defaultMessage: 'moderator', + }, + presenterRoleLabel: { + id: 'pickRandomUserPlugin.modal.presenterView.roleLabel.presenter', + description: 'Role badge label for presenters', + defaultMessage: 'presenter', + }, }); -const MAX_NAMES_TO_SHOW = 3; +const FALLBACK_AVATAR_COLORS = ['#4E7FF8', '#2BA084', '#E07A3A', '#7B61D9', '#D4733B']; -const makeHorizontalListOfNames = (list?: PickedUser[]) => { - const length = list?.length; - const formattedList = list?.filter((_, index) => { - if (length > MAX_NAMES_TO_SHOW) return index < MAX_NAMES_TO_SHOW; - return true; - }).reduce((accumulator, currentValue) => ((accumulator !== '') ? `${accumulator}, ${currentValue.name}` : currentValue.name), ''); - if (length > MAX_NAMES_TO_SHOW) return `${formattedList}...`; - return formattedList; -}; +function getAvatarColorFallback(name: string): string { + const hash = name.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0); + return FALLBACK_AVATAR_COLORS[hash % FALLBACK_AVATAR_COLORS.length]; +} + +function getInitials(name: string): string { + return name.slice(0, 2); +} -const makeVerticalListOfNames = ( - list?: DataChannelEntryResponseType[], -) => list?.filter((u) => !!u.payloadJson).map((u) => { - const time = new Date(u.createdAt); - const timeMiliseconds = time.getTime(); +function ShieldIcon() { return ( -
  • - {u.payloadJson.name} - {' '} - ( - {time.getHours().toString().padStart(2, '0')} - : - {time.getMinutes().toString().padStart(2, '0')} - ) -
  • + + + ); -}); +} + +function PresentationIcon() { + return ( + + + + ); +} + +function UserCheckIcon() { + return ( + + + + ); +} + +function makePickedUserRows(list?: DataChannelEntryResponseType[]) { + return list?.filter((u) => !!u.payloadJson).map((u) => { + const time = new Date(u.createdAt); + const { avatar, color, name } = u.payloadJson; + const initials = getInitials(name); + return ( + + {avatar ? ( + + ) : ( + + {initials} + + )} + {name} + + ); + }); +} export function PresenterViewComponent(props: PresenterViewComponentProps) { const { @@ -122,144 +167,202 @@ export function PresenterViewComponent(props: PresenterViewComponentProps) { users, } = props; - const { - filterOptions, - setFilterOptions, - } = useContext(FilterOptionsContext); + const { filterOptions, setFilterOptions } = useContext(FilterOptionsContext); + const { includeModerators, includePresenter, includePickedUsers } = filterOptions; - const { - includeModerators, - includePresenter, - includePickedUsers, - } = filterOptions; + const usersCount = users?.length ?? 0; + const userRoleLabel = (() => { + if (!includeModerators) { + return usersCount !== 1 + ? intl.formatMessage(intlMessages.viewerLabelPlural, { 0: usersCount }) + : intl.formatMessage(intlMessages.viewerLabel, { 0: usersCount }); + } + return usersCount !== 1 + ? intl.formatMessage(intlMessages.userLabelPlural, { 0: usersCount }) + : intl.formatMessage(intlMessages.userLabel, { 0: usersCount }); + })(); + + const hasPickedUsers = dataChannelPickedUsers?.some((u) => !!u.payloadJson); - let userRoleLabel: string; - const usersCountVariable = { 0: users?.length }; - if (!includeModerators) { - userRoleLabel = (users?.length !== 1) - ? intl.formatMessage(intlMessages.viewerLabelPlural, usersCountVariable) - : intl.formatMessage(intlMessages.viewerLabel, usersCountVariable); - } else { - userRoleLabel = (users?.length !== 1) - ? intl.formatMessage(intlMessages.userLabelPlural, usersCountVariable) - : intl.formatMessage(intlMessages.userLabel, usersCountVariable); - } return ( - - - - {intl.formatMessage(intlMessages.optionsTitle)} - - - - { - setFilterOptions((filterOptionsPrevious) => ({ - ...filterOptionsPrevious, - includeModerators: !filterOptionsPrevious.includeModerators, - })); - }} - name="options" - value="includeModerators" - /> - - {intl.formatMessage(intlMessages.includeModeratorsLabel)} - - - - { - setFilterOptions((filterOptionsPrevious) => ({ - ...filterOptionsPrevious, - includePresenter: !filterOptionsPrevious.includePresenter, - })); - }} - name="options" - value="includePresenter" - /> - - {intl.formatMessage(intlMessages.includePresenterLabel)} - - - - { - setFilterOptions((filterOptionsPrevious) => ({ - ...filterOptionsPrevious, - includePickedUsers: !filterOptionsPrevious.includePickedUsers, - })); - }} - name="options" - value="includePickedUsers" - /> - - {intl.formatMessage(intlMessages.includePickedUsersLabel)} - - - - - - - {intl.formatMessage(intlMessages.availableTitle)} - - - {`${users?.length} ${userRoleLabel}: `} - {makeHorizontalListOfNames(users)} - - - - - - {intl.formatMessage(intlMessages.previouslyPickedTitle)} - - { - deletionFunction([RESET_DATA_CHANNEL]); - }} - > - {intl.formatMessage(intlMessages.clearButtonLabel)} - - - - - { - makeVerticalListOfNames(dataChannelPickedUsers) - } - - - - { - users?.length > 0 ? ( - + + + {/* OPTIONS SECTION */} + + + {intl.formatMessage(intlMessages.optionsTitle)} + + + + + + + + + {intl.formatMessage(intlMessages.includeModeratorsLabel)} + + + { + setFilterOptions((prev) => ({ + ...prev, + includeModerators: !prev.includeModerators, + })); + }} + /> + + + + + + + + + {intl.formatMessage(intlMessages.includePresenterLabel)} + + + { + setFilterOptions((prev) => ({ + ...prev, + includePresenter: !prev.includePresenter, + })); + }} + /> + + + + + + + + + {intl.formatMessage(intlMessages.includePickedUsersLabel)} + + + { + setFilterOptions((prev) => ({ + ...prev, + includePickedUsers: !prev.includePickedUsers, + })); + }} + /> + + + + + {/* AVAILABLE USERS SECTION */} + + + + {intl.formatMessage(intlMessages.availableTitle)} + + + {usersCount} + {' '} + {userRoleLabel} + + + {usersCount === 0 ? ( + + + {intl.formatMessage(intlMessages.availableEmptyState, { 0: userRoleLabel })} + + + ) : ( + + {users?.map((user) => { + const initials = getInitials(user.name); + let roleBadgeLabel: string | null = null; + if (user.role === 'MODERATOR') { + roleBadgeLabel = intl.formatMessage(intlMessages.moderatorRoleLabel); + } else if (user.presenter) { + roleBadgeLabel = intl.formatMessage(intlMessages.presenterRoleLabel); + } + return ( + + {user.avatar ? ( + + ) : ( + + {initials} + + )} + {user.name} + {roleBadgeLabel && ( + {roleBadgeLabel} + )} + + ); + })} + + )} + + + {/* PREVIOUSLY PICKED SECTION */} + + + + {intl.formatMessage(intlMessages.previouslyPickedTitle)} + + deletionFunction([RESET_DATA_CHANNEL])} + > + {intl.formatMessage(intlMessages.clearButtonLabel)} + + + {hasPickedUsers ? ( + + + {makePickedUserRows(dataChannelPickedUsers)} + + + ) : ( + <> + + + {intl.formatMessage(intlMessages.emptyState)} + + + {/* Empty list kept in DOM so [data-test] li selectors resolve correctly */} + + + )} + + + + + {/* FOOTER */} + + {usersCount > 0 ? ( + { - handlePickRandomUser(); - }} + onClick={handlePickRandomUser} > - { - (pickedUserWithEntryId) + {pickedUserWithEntryId ? intl.formatMessage(intlMessages.pickAgainButtonLabel) - : intl.formatMessage(intlMessages.pickButtonLabel, { 0: userRoleLabel }) - } - + : intl.formatMessage(intlMessages.pickButtonLabel, { 0: userRoleLabel })} + ) : ( -

    + {intl.formatMessage(intlMessages.noUsersWarning, { 0: userRoleLabel })} -

    - ) - } -
    + + )} + + ); } diff --git a/src/components/modal/presenter-view/styles.tsx b/src/components/modal/presenter-view/styles.tsx index fb2b8d7..bc440d9 100644 --- a/src/components/modal/presenter-view/styles.tsx +++ b/src/components/modal/presenter-view/styles.tsx @@ -1,108 +1,313 @@ import styled from 'styled-components'; -const PresenterViewContentWrapper = styled.div` - width: '100%'; - height: '100%'; - align-items: 'flex-start'; - display: 'flex'; - flex-direction: 'column'; +// ── Section labels ──────────────────────────────────────────────────────────── + +const SectionLabel = styled.span` + font-size: 0.6875rem; + font-weight: 600; + color: #8B9AAF; + text-transform: uppercase; + letter-spacing: 0.6px; +`; + +// ── Options section (toggle rows) ───────────────────────────────────────────── + +const OptionsContainer = styled.div` + margin-top: 0.5rem; `; -const PresenterViewSectionWrapper = styled.div` +const ToggleRow = styled.label` display: flex; - align-items: flex-start; - flex-direction: column; - max-height: 30%; + align-items: center; + justify-content: space-between; + padding: 0.4375rem 0; + cursor: pointer; + border-bottom: 1px solid #F2F4F7; + + &:last-child { + border-bottom: none; + } `; -const PresenterViewSectionTitle = styled.div` - margin: 5px 0px; - font-weight: 600; - font-size: 20px; +const ToggleRowLeft = styled.div` + display: flex; + align-items: center; + gap: 0.5rem; `; -const PresenterViewSectionClearAllButton = styled.button` - padding: 1px 10px; - margin-left: 8px; - font-size: 15px; - background: #efefef; - border: none; - color: inherit; - border-radius: 8px; +const IconCircle = styled.span` + width: 1.5rem; + height: 1.5rem; + border-radius: 50%; + background: #EEF2F8; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + color: #4E7FF8; +`; + +const ToggleLabelText = styled.span` + font-size: 1rem; + color: #1C2B3A; +`; + +// Checkbox styled as a toggle switch. +// Keeps the real in the DOM so Playwright selectors +// (#includeModerators etc.) and isChecked() / toBeVisible() all keep working. +const ToggleCheckbox = styled.input` + appearance: none; + -webkit-appearance: none; + width: 2.25rem; + height: 1.25rem; + border-radius: 0.625rem; + background: #D1D9E3; cursor: pointer; - &:hover { - background-color: #ddd; + outline: none; + border: none; + transition: background 0.2s ease; + position: relative; + flex-shrink: 0; + margin: 0; + + &::before { + content: ''; + position: absolute; + width: 1rem; + height: 1rem; + background: #fff; + border-radius: 50%; + top: 0.125rem; + left: 0.125rem; + transition: left 0.2s ease; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15); + } + + &:checked { + background: #4E7FF8; + } + + &:checked::before { + left: 1.125rem; } `; -const PresenterViewSectionTitleWrapper = styled.div` +// ── Section header row (label + count/action) ───────────────────────────────── + +const SectionHeaderRow = styled.div` display: flex; - flex-direction: row; align-items: center; + justify-content: space-between; + margin-bottom: 0.5rem; `; -const PresenterViewSectionListWrapper = styled.div` - overflow-y: auto; +// ── Available users section ─────────────────────────────────────────────────── + +const CountBadge = styled.span` + font-size: 0.75rem; + color: #4E7FF8; + font-weight: 600; +`; + +const UserListContainer = styled.div` + background: #F7F9FB; + border-radius: 0.375rem; + padding: 0.625rem 0.75rem; + display: flex; + flex-direction: column; + gap: 0.375rem; max-height: 10rem; - margin-bottom: .75rem; - width: 100%; + overflow-y: auto; `; -const PresenterViewSectionList = styled.ul` - font-size: 18px; - margin: 0; - list-style-type: none; +const UserRow = styled.div` + display: flex; + align-items: center; + gap: 0.5rem; +`; + +const UserAvatar = styled.div<{ $color: string }>` + width: 1.625rem; + height: 1.625rem; + border-radius: 50%; + background: ${({ $color }) => $color}; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.625rem; + font-weight: 600; + color: #fff; + flex-shrink: 0; +`; + +const UserAvatarImage = styled.img` + width: 1.625rem; + height: 1.625rem; + border-radius: 50%; + object-fit: cover; + flex-shrink: 0; +`; + +const UserNameText = styled.span` + font-size: 0.8125rem; + color: #1C2B3A; + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +const RoleBadge = styled.span` + font-size: 0.625rem; + color: #8B9AAF; + background: #E8EDF2; + padding: 0.0625rem 0.375rem; + border-radius: 0.1875rem; + margin-left: auto; + flex-shrink: 0; + white-space: nowrap; +`; + +// ── Previously picked section ───────────────────────────────────────────────── + +const ClearAllButton = styled.button` + font-size: 0.6875rem; + color: #8B9AAF; + background: none; + border: none; + text-decoration: underline; + text-underline-offset: 2px; + cursor: pointer; + padding: 0; + + &:hover { + color: #6b7d92; + } +`; + +const EmptyStateContainer = styled.div` + background: #F7F9FB; + border-radius: 0.375rem; + padding: 0.875rem; + text-align: center; `; -const PresenterViewSectionContent = styled.div` +const EmptyStateText = styled.span` + font-size: 0.8125rem; + color: #A7B3C3; +`; + +const PickedUserListContainer = styled.div` + background: #F7F9FB; + border-radius: 0.375rem; + padding: 0.625rem 0.75rem; display: flex; flex-direction: column; - font-size: 18px; - padding-left: 40px; + gap: 0.375rem; + max-height: 7.5rem; + overflow-y: auto; `; -// Section Content related: +//
      that test selectors target with [data-test="pickRandomUserPreviouslyPickedList"] +const PickedList = styled.ul` + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 0.375rem; +`; -const CheckboxLabelWrapper = styled.label` +const PickedUserRow = styled.li` display: flex; align-items: center; + gap: 0.5rem; + list-style: none; `; -const CheckboxLabel = styled.span` - margin-left: 5px; +// ── Footer / action button ──────────────────────────────────────────────────── + +const FooterContainer = styled.div` + padding: 0 1.25rem 1rem; `; -const PickUserButton = styled.button` - border: 3px solid transparent; - overflow: visible; - display: inline-block; - background-color: var(--btn-primary-bg, var(--color-primary, #0F70D7)); - color: var(--btn-primary-color, var(--color-white, #FFF)); - border-radius: 2px; +const PickButton = styled.button` + width: 100%; + padding: 0.625rem 0; + background: #4E7FF8; + color: #fff; + border: none; + border-radius: 0.375rem; + font-size: 0.875rem; font-weight: 600; - line-height: 1; - text-align: center; - white-space: nowrap; - vertical-align: middle; + font-family: inherit; cursor: pointer; - user-select: none; - padding: 8px 15px; + display: flex; + align-items: center; + justify-content: center; + gap: 0.375rem; + &:hover { - color: var(--btn-primary-color, var(--color-white, #FFF)); - background-color: var(--btn-primary-hover-bg, #0C57A7) !important; + background: #3D6DE0; } `; +const NoUsersWarning = styled.p` + font-size: 0.8125rem; + color: #8B9AAF; + text-align: center; + margin: 0; + padding: 0.5rem 0; +`; + +// ── Outer wrappers ──────────────────────────────────────────────────────────── + +const PresenterViewWrapper = styled.div` + font-family: 'Source Sans Pro', Arial, sans-serif; +`; + +const ContentPadding = styled.div` + padding: 1rem 1.25rem 0.75rem; + display: flex; + flex-direction: column; + gap: 1rem; +`; + +const OptionsSection = styled.div``; + +const AvailableSection = styled.div``; + +const PreviouslyPickedSection = styled.div``; + export { - PresenterViewContentWrapper, - PresenterViewSectionWrapper, - PresenterViewSectionTitle, - PresenterViewSectionTitleWrapper, - PresenterViewSectionList, - PresenterViewSectionListWrapper, - PresenterViewSectionContent, - CheckboxLabelWrapper, - CheckboxLabel, - PickUserButton, - PresenterViewSectionClearAllButton, + SectionLabel, + OptionsContainer, + ToggleRow, + ToggleRowLeft, + IconCircle, + ToggleLabelText, + ToggleCheckbox, + SectionHeaderRow, + CountBadge, + UserListContainer, + UserRow, + UserAvatar, + UserAvatarImage, + UserNameText, + RoleBadge, + ClearAllButton, + EmptyStateContainer, + EmptyStateText, + PickedUserListContainer, + PickedList, + PickedUserRow, + FooterContainer, + PickButton, + NoUsersWarning, + PresenterViewWrapper, + ContentPadding, + OptionsSection, + AvailableSection, + PreviouslyPickedSection, }; diff --git a/src/components/modal/styles.tsx b/src/components/modal/styles.tsx index cfb9af3..352a1be 100644 --- a/src/components/modal/styles.tsx +++ b/src/components/modal/styles.tsx @@ -9,18 +9,13 @@ const PluginModal = styled(ReactModal)` outline-style: solid; display: flex; flex-direction: column; - padding: 2rem; - box-shadow: 0px 0px 20px rgba(0, 0, 0, 0.7); background-color: #fff !important; - max-width: 60vw; - max-height: 80vh; - border-radius: 0.2rem; - overflow: auto; - overflow-y: hidden; - background-repeat: no-repeat; - background-color: transparent; - background-size: 100% 40px, 100% 40px, 100% 14px, 100% 14px; - background-attachment: local, local, scroll, scroll; + width: 25rem; + max-width: 95vw; + border-radius: 0.5rem; + box-shadow: 0 0.5rem 2rem rgba(0, 0, 0, 0.4); + overflow: hidden; + font-family: 'Source Sans Pro', Arial, sans-serif; &::-webkit-scrollbar { width: 5px; @@ -38,47 +33,50 @@ const PluginModal = styled(ReactModal)` &::-webkit-scrollbar-thumb:hover { background: rgba(0, 0, 0, 0.5); } - &::-webkit-scrollbar-thumb:active { - background: rgba(0, 0, 0, 0.25); - } &::-webkit-scrollbar-track { - background: rgba(0, 0, 0, 0.25); + background: rgba(0, 0, 0, 0.1); border: none; border-radius: 50px; } &::-webkit-scrollbar-corner { background: transparent; } +`; - @media only screen and (max-width: 40em) { - max-width: 95vw; - } +const ModalHeader = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem 1.25rem 0.875rem; + border-bottom: 1px solid #E8EDF2; + flex-shrink: 0; +`; - @media only screen and (min-width: 40.063em) { - max-width: 80vw; - } +const ModalTitle = styled.span` + font-weight: 600; + font-size: 1.15rem; + color: #1C2B3A; `; const CloseButton = styled.button` - font-size: 40px; + font-size: 1rem; background: none; - color: inherit; - font: inherit; - cursor: pointer; - outline: inherit; + color: #8B9AAF; border: none; + cursor: pointer; + padding: 0.25rem; + display: flex; + align-items: center; + justify-content: center; + border-radius: 0.25rem; + line-height: 1; + &:hover { - background-color: #EEE; + background-color: #EEF2F8; + color: #1C2B3A; } `; -const CloseButtonWrapper = styled.div` - width: 100%; - align-items: flex-end; - display: flex; - flex-direction: column; -`; - const CountdownMessage = styled.div` width: 100%; text-align: center; @@ -93,5 +91,5 @@ const CountdownMessage = styled.div` `; export { - PluginModal, CloseButton, CloseButtonWrapper, CountdownMessage, + PluginModal, ModalHeader, ModalTitle, CloseButton, CountdownMessage, }; diff --git a/tests/README.md b/tests/README.md index 193358e..8b112ab 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,8 +1,7 @@ # Pick Random User Plugin – Automated Tests End-to-end tests for the **Pick Random User Plugin** written with [Playwright](https://playwright.dev/). -They reuse the shared test infrastructure from the -[BigBlueButton HTML Plugin SDK](https://github.com/bigbluebutton/bigbluebutton-html-plugin-sdk). +The test infrastructure lives entirely inside `tests/core/`. --- @@ -35,13 +34,6 @@ npm install npx playwright install --with-deps chromium ``` -The SDK test utilities are resolved via relative paths to the sibling -`bigbluebutton-html-plugin-sdk/` directory: - -```bash -cd ../bigbluebutton-html-plugin-sdk && npm install && cd - -``` - ### 2 – Configure environment variables ```bash diff --git a/tests/behavioral/helpers.ts b/tests/behavioral/helpers.ts index 657df3b..7220d64 100644 --- a/tests/behavioral/helpers.ts +++ b/tests/behavioral/helpers.ts @@ -1,7 +1,27 @@ +import { test } from '@playwright/test'; import { ELEMENT_WAIT_LONGER_TIME, ELEMENT_WAIT_TIME } from '../core/constants'; import { elements as e } from '../elements'; import { SessionPage as Page } from '../core/sessionPage'; +/** Click a toggle/checkbox and verify it reaches :checked state, retrying once if needed. */ +export async function clickToggleOnWithRetry( + page: Page, + selector: string, + description: string, +): Promise { + await page.page.click(selector); + try { + await page.hasElement(`${selector}:checked`, `${description} toggle should be on`, ELEMENT_WAIT_LONGER_TIME); + } catch { + test.info().annotations.push({ + type: 'toggle-retry', + description: `"${description}" toggle didn't register on first click and was retried`, + }); + await page.page.click(selector); + await page.hasElement(`${selector}:checked`, `${description} toggle should be on (retry)`, ELEMENT_WAIT_LONGER_TIME); + } +} + export async function openModal(modPage: Page): Promise { await modPage.page.waitForSelector(e.whiteboard, { timeout: ELEMENT_WAIT_LONGER_TIME }); await modPage.page.click(e.actions); diff --git a/tests/behavioral/multi-user.spec.ts b/tests/behavioral/multi-user.spec.ts index 8923896..d7762bf 100644 --- a/tests/behavioral/multi-user.spec.ts +++ b/tests/behavioral/multi-user.spec.ts @@ -1,7 +1,9 @@ /** * Behavioural tests – multi-user (moderator/presenter + attendee/viewer). */ -import { test, BrowserContext } from '@playwright/test'; +import { + test, BrowserContext, Browser, APIRequestContext, TestInfo, +} from '@playwright/test'; import { checkPluginAvailability } from '../core/fixtures/pluginBeforeAll'; import { ELEMENT_WAIT_LONGER_TIME, ELEMENT_WAIT_TIME, ELEMENT_WAIT_EXTRA_LONG_TIME } from '../core/constants'; import { elements as e } from '../elements'; @@ -42,16 +44,18 @@ async function cleanupAfterTest(modPage: Page, attendeePage: Page): Promise { - test.describe.configure({ mode: 'serial' }); + test.describe.configure({ mode: ISOLATED ? 'default' : 'serial' }); let modPage: Page; let attendeePage: Page; let modContext: BrowserContext; let attendeeContext: BrowserContext; - test.beforeAll(async ({ browser, request }, testInfo) => { + async function setupMeeting(browser: Browser, request: APIRequestContext, testInfo: TestInfo) { await checkPluginAvailability({ pluginName: PLUGIN_NAME, envVarName: ENV_VAR_NAME, @@ -99,16 +103,28 @@ test.describe('Pick Random User Plugin - Behavioural (multi-user)', () => { content: "body { font-family: 'Liberation Sans', Arial, sans-serif; }", }); attendeePage.meetingId = modPage.meetingId; - }); + } - test.afterAll(async () => { - await modContext?.close(); - await attendeeContext?.close(); - }); - - test.afterEach(async () => { - if (modPage && attendeePage) await cleanupAfterTest(modPage, attendeePage); - }); + if (ISOLATED) { + test.beforeEach(async ({ browser, request }, testInfo) => { + await setupMeeting(browser, request, testInfo); + }); + test.afterEach(async () => { + await modContext?.close(); + await attendeeContext?.close(); + }); + } else { + test.beforeAll(async ({ browser, request }, testInfo) => { + await setupMeeting(browser, request, testInfo); + }); + test.afterAll(async () => { + await modContext?.close(); + await attendeeContext?.close(); + }); + test.afterEach(async () => { + if (modPage && attendeePage) await cleanupAfterTest(modPage, attendeePage); + }); + } test('should show the same picked user name on both the presenter page and the attendee page', async (): Promise => { await waitForAttendeeMeeting(attendeePage); diff --git a/tests/behavioral/single-user.spec.ts b/tests/behavioral/single-user.spec.ts index b2ff80c..6e09f72 100644 --- a/tests/behavioral/single-user.spec.ts +++ b/tests/behavioral/single-user.spec.ts @@ -1,14 +1,18 @@ /** * Behavioural tests – single user (moderator / presenter only). */ -import { test, BrowserContext } from '@playwright/test'; +import { + test, BrowserContext, Browser, APIRequestContext, TestInfo, +} from '@playwright/test'; import { checkPluginAvailability } from '../core/fixtures/pluginBeforeAll'; import { ELEMENT_WAIT_LONGER_TIME, ELEMENT_WAIT_TIME } from '../core/constants'; import { elements as e } from '../elements'; import { SessionPage as ModPage } from '../core/sessionPage'; import { Plugin } from '../core/plugin'; import { encodeCustomParams } from '../core/helpers'; -import { goBackToPresenterView, openModal, moderatorCleanupAfterTest } from './helpers'; +import { + goBackToPresenterView, openModal, moderatorCleanupAfterTest, clickToggleOnWithRetry, +} from './helpers'; const PLUGIN_NAME = 'pick-random-user-plugin'; const ENV_VAR_NAME = 'PICK_RANDOM_USER_PLUGIN_URL'; @@ -19,25 +23,15 @@ const getPluginUrl = () => pluginUrl; /** Enable both inclusion filters so the single moderator/presenter is eligible. */ async function enableInclusionFilters(modPage: ModPage): Promise { - await modPage.page.click(e.includeModeratorsCheckbox); - await modPage.page.click(e.includePresenterCheckbox); - await modPage.hasElement( - e.pickRandomUserPickButton, - 'pick button should appear after enabling both inclusion filters', - ELEMENT_WAIT_LONGER_TIME, - ); + await clickToggleOnWithRetry(modPage, e.includeModeratorsCheckbox, 'includeModerators'); + await clickToggleOnWithRetry(modPage, e.includePresenterCheckbox, 'includePresenter'); } /** Enable all three filters (include picked users too, so "Pick again" is reachable). */ async function enableAllFilters(modPage: ModPage): Promise { - await modPage.page.click(e.includeModeratorsCheckbox); - await modPage.page.click(e.includePresenterCheckbox); - await modPage.page.click(e.includePickedUsersCheckbox); - await modPage.hasElement( - e.pickRandomUserPickButton, - 'pick button should appear after enabling all filters', - ELEMENT_WAIT_LONGER_TIME, - ); + await clickToggleOnWithRetry(modPage, e.includeModeratorsCheckbox, 'includeModerators'); + await clickToggleOnWithRetry(modPage, e.includePresenterCheckbox, 'includePresenter'); + await clickToggleOnWithRetry(modPage, e.includePickedUsersCheckbox, 'includePickedUsers'); } /** Pick a user and wait for the picked-user view to appear. */ @@ -54,14 +48,16 @@ async function cleanupAfterTest(modPage: ModPage) { await moderatorCleanupAfterTest(modPage); } +const ISOLATED = process.env.TEST_MEETINGS === 'isolated'; + // ── Tests ───────────────────────────────────────────────────────────────────── test.describe('Pick Random User Plugin - Behavioural (single user)', () => { - test.describe.configure({ mode: 'serial' }); + test.describe.configure({ mode: ISOLATED ? 'default' : 'serial' }); let modPage: ModPage; let sharedContext: BrowserContext; - test.beforeAll(async ({ browser, request }, testInfo) => { + async function setupMeeting(browser: Browser, request: APIRequestContext, testInfo: TestInfo) { await checkPluginAvailability({ pluginName: PLUGIN_NAME, envVarName: ENV_VAR_NAME, @@ -83,15 +79,26 @@ test.describe('Pick Random User Plugin - Behavioural (single user)', () => { const plugin = new Plugin({ browser, context: sharedContext }); await plugin.initModPage(page, { createParameter }); modPage = plugin.modPage; - }); + } - test.afterAll(async () => { - await sharedContext?.close(); - }); - - test.afterEach(async () => { - if (modPage) await cleanupAfterTest(modPage); - }); + if (ISOLATED) { + test.beforeEach(async ({ browser, request }, testInfo) => { + await setupMeeting(browser, request, testInfo); + }); + test.afterEach(async () => { + await sharedContext?.close(); + }); + } else { + test.beforeAll(async ({ browser, request }, testInfo) => { + await setupMeeting(browser, request, testInfo); + }); + test.afterAll(async () => { + await sharedContext?.close(); + }); + test.afterEach(async () => { + if (modPage) await cleanupAfterTest(modPage); + }); + } test('should show "Pick again" button (not "Pick user") after navigating back from picked-user view', async (): Promise => { // With "Include already picked users" ON the presenter stays in the pool diff --git a/tests/structural/test.spec.ts b/tests/structural/test.spec.ts index e8c55b4..f6a71a7 100644 --- a/tests/structural/test.spec.ts +++ b/tests/structural/test.spec.ts @@ -1,5 +1,7 @@ // eslint-disable-next-line import/no-extraneous-dependencies -import { test, BrowserContext } from '@playwright/test'; +import { + test, BrowserContext, Browser, APIRequestContext, TestInfo, +} from '@playwright/test'; import { checkPluginAvailability } from '../core/fixtures/pluginBeforeAll'; import { ELEMENT_WAIT_LONGER_TIME, ELEMENT_WAIT_TIME } from '../core/constants'; import { elements as e } from '../elements'; @@ -50,15 +52,17 @@ async function cleanupAfterTest(modPage: ModPage): Promise { } } +const ISOLATED = process.env.TEST_MEETINGS === 'isolated'; + // ── Tests ───────────────────────────────────────────────────────────────────── test.describe('Pick Random User Plugin - Structural', () => { - test.describe.configure({ mode: 'serial' }); + test.describe.configure({ mode: ISOLATED ? 'default' : 'serial' }); let modPage: ModPage; let sharedContext: BrowserContext; - test.beforeAll(async ({ browser, request }, testInfo) => { + async function setupMeeting(browser: Browser, request: APIRequestContext, testInfo: TestInfo) { await checkPluginAvailability({ pluginName: PLUGIN_NAME, envVarName: ENV_VAR_NAME, @@ -80,15 +84,26 @@ test.describe('Pick Random User Plugin - Structural', () => { const plugin = new Plugin({ browser, context: sharedContext }); await plugin.initModPage(page, { createParameter }); modPage = plugin.modPage; - }); - - test.afterAll(async () => { - await sharedContext?.close(); - }); + } - test.afterEach(async () => { - if (modPage) await cleanupAfterTest(modPage); - }); + if (ISOLATED) { + test.beforeEach(async ({ browser, request }, testInfo) => { + await setupMeeting(browser, request, testInfo); + }); + test.afterEach(async () => { + await sharedContext?.close(); + }); + } else { + test.beforeAll(async ({ browser, request }, testInfo) => { + await setupMeeting(browser, request, testInfo); + }); + test.afterAll(async () => { + await sharedContext?.close(); + }); + test.afterEach(async () => { + if (modPage) await cleanupAfterTest(modPage); + }); + } test('should show "Pick random user" label in the actions dropdown for a presenter', async (): Promise => { await modPage.page.waitForSelector(e.whiteboard, { timeout: ELEMENT_WAIT_LONGER_TIME }); From ab5ff32ba5e9674e139c6d3dac6acbf833308395 Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Fri, 15 May 2026 11:32:45 -0300 Subject: [PATCH 2/9] [ref-ui] added test:isolated documentation on the tests/readme file --- tests/README.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/tests/README.md b/tests/README.md index 8b112ab..11a64be 100644 --- a/tests/README.md +++ b/tests/README.md @@ -49,6 +49,7 @@ cp .env.template .env | `LOCAL_CONTAINER_NAME` | no | Docker container name for the local deployment script | | `TIMEOUT_MULTIPLIER` | no | Multiply all timeouts (default 1 locally, 2 in CI) | | `CI` | no | `"true"` enables CI reporter and single-worker mode | +| `TEST_MEETINGS` | no | Set to `"isolated"` to give each test its own meeting (see [Meeting isolation](#meeting-isolation)) | ### 3 – Build and deploy the plugin @@ -66,12 +67,28 @@ npm run publish-plugin:dev --- +## Meeting isolation + +By default every test suite (`test.describe` block) shares **one BBB meeting** for all its tests — the meeting is created once in `beforeAll` and torn down in `afterAll`. This is fast (~3 meetings total) but tests within a suite depend on the cleanup performed between them. + +Setting `TEST_MEETINGS=isolated` switches every suite to **one meeting per test**: the meeting is created in `beforeEach` and destroyed in `afterEach`. Tests become fully independent and can run in parallel, at the cost of more meeting setups (~15 meetings total). + +| Mode | Meetings created | Tests run | When to use | +|------|-----------------|-----------|-------------| +| default (`npm test`) | ~3 (one per suite) | serially within each suite | Normal development | +| isolated (`npm run test:isolated`) | ~1 per test | can run in parallel | Debugging flaky state, CI full isolation | + +--- + ## Running the tests ```bash -# All suites +# All suites – shared meetings (default) npm test +# All suites – one meeting per test +npm run test:isolated + # Only structural tests npm test -- tests/structural From 4fcc1ad834b8a6c26dbdc633a3243f2cd36e1f4b Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Thu, 21 May 2026 09:23:56 -0300 Subject: [PATCH 3/9] [ref-ui] fix labels based on review --- public/locales/en.json | 2 +- public/locales/fr-FR.json | 2 +- public/locales/it.json | 2 +- public/locales/pt-BR.json | 2 +- src/components/modal/presenter-view/component.tsx | 8 ++++++-- 5 files changed, 10 insertions(+), 6 deletions(-) diff --git a/public/locales/en.json b/public/locales/en.json index 3aac237..967d1be 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -6,7 +6,7 @@ "pickRandomUserPlugin.modal.presenterView.optionSection.title": "Options", "pickRandomUserPlugin.modal.presenterView.optionSection.includeModeratorsLabel": "Include moderators", "pickRandomUserPlugin.modal.presenterView.optionSection.includePresenterLabel": "Include presenter", - "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Include already picked user", + "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Include already picked users", "pickRandomUserPlugin.modal.presenterView.availableSection.title": "Available for selection", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "user", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabelPlural": "users", diff --git a/public/locales/fr-FR.json b/public/locales/fr-FR.json index 04ad3cd..ffc9734 100644 --- a/public/locales/fr-FR.json +++ b/public/locales/fr-FR.json @@ -5,7 +5,7 @@ "pickRandomUserPlugin.modal.presenterView.optionSection.title": "Options", "pickRandomUserPlugin.modal.presenterView.optionSection.includeModeratorsLabel": "Inclure les modérateurs", "pickRandomUserPlugin.modal.presenterView.optionSection.includePresenterLabel": "Inclure le présentateur", - "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Inclure un participant déjà sélectionné", + "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Inclure des participants déjà sélectionnés", "pickRandomUserPlugin.modal.presenterView.availableSection.title": "Disponible pour la sélection", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "participant", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabelPlural": "participants", diff --git a/public/locales/it.json b/public/locales/it.json index 7f312ab..13fe116 100644 --- a/public/locales/it.json +++ b/public/locales/it.json @@ -5,7 +5,7 @@ "pickRandomUserPlugin.modal.presenterView.optionSection.title": "Opzioni", "pickRandomUserPlugin.modal.presenterView.optionSection.includeModeratorsLabel": "Includi moderatori", "pickRandomUserPlugin.modal.presenterView.optionSection.includePresenterLabel": "Includi il presentatore", - "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Includi l'utente già selezionato", + "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Includi gli utenti già selezionati", "pickRandomUserPlugin.modal.presenterView.availableSection.title": "Disponibile per la selezione", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "utente", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabelPlural": "utenti", diff --git a/public/locales/pt-BR.json b/public/locales/pt-BR.json index c2c239c..b5e79fb 100644 --- a/public/locales/pt-BR.json +++ b/public/locales/pt-BR.json @@ -6,7 +6,7 @@ "pickRandomUserPlugin.modal.presenterView.optionSection.title": "Opções", "pickRandomUserPlugin.modal.presenterView.optionSection.includeModeratorsLabel": "Incluir moderadores", "pickRandomUserPlugin.modal.presenterView.optionSection.includePresenterLabel": "Incluir apresentador", - "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Incluir usuário já selecionado", + "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Incluir usuários já selecionados", "pickRandomUserPlugin.modal.presenterView.availableSection.title": "Disponíveis para seleção", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "usuário", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabelPlural": "usuários", diff --git a/src/components/modal/presenter-view/component.tsx b/src/components/modal/presenter-view/component.tsx index f9c5760..a323e33 100644 --- a/src/components/modal/presenter-view/component.tsx +++ b/src/components/modal/presenter-view/component.tsx @@ -28,7 +28,7 @@ const intlMessages = defineMessages({ includePickedUsersLabel: { id: 'pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel', description: 'Label of include picked users option on modal`s presenter view', - defaultMessage: 'Include already picked user', + defaultMessage: 'Include already picked users', }, availableTitle: { id: 'pickRandomUserPlugin.modal.presenterView.availableSection.title', @@ -182,6 +182,10 @@ export function PresenterViewComponent(props: PresenterViewComponentProps) { : intl.formatMessage(intlMessages.userLabel, { 0: usersCount }); })(); + const userRoleLabelSingular = (!includeModerators) + ? intl.formatMessage(intlMessages.viewerLabel, { 0: usersCount }) + : intl.formatMessage(intlMessages.userLabel, { 0: usersCount }); + const hasPickedUsers = dataChannelPickedUsers?.some((u) => !!u.payloadJson); return ( @@ -355,7 +359,7 @@ export function PresenterViewComponent(props: PresenterViewComponentProps) { > {pickedUserWithEntryId ? intl.formatMessage(intlMessages.pickAgainButtonLabel) - : intl.formatMessage(intlMessages.pickButtonLabel, { 0: userRoleLabel })} + : intl.formatMessage(intlMessages.pickButtonLabel, { 0: userRoleLabelSingular })} ) : ( From 6f3c373649a697b66d90689fbdd4f0c8654a624e Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Thu, 21 May 2026 09:56:00 -0300 Subject: [PATCH 4/9] [ref-ui] change options to improve user experience. --- public/locales/de.json | 4 + public/locales/en.json | 4 + public/locales/fr-FR.json | 4 + public/locales/it.json | 4 + public/locales/ja.json | 4 + public/locales/pt-BR.json | 4 + .../modal/presenter-view/component.tsx | 193 ++++++++---------- .../modal/presenter-view/styles.tsx | 113 ++++------ 8 files changed, 152 insertions(+), 178 deletions(-) diff --git a/public/locales/de.json b/public/locales/de.json index e163cf4..486a3e2 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -7,6 +7,10 @@ "pickRandomUserPlugin.modal.presenterView.optionSection.includeModeratorsLabel": "Moderatoren einbeziehen", "pickRandomUserPlugin.modal.presenterView.optionSection.includePresenterLabel": "Präsentator einbeziehen", "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Wähle bereits gewählte Teilnehmer", + "pickRandomUserPlugin.modal.presenterView.filterChips.label": "Auch einbeziehen:", + "pickRandomUserPlugin.modal.presenterView.filterChips.moderators": "Moderatoren", + "pickRandomUserPlugin.modal.presenterView.filterChips.presenter": "Präsentator", + "pickRandomUserPlugin.modal.presenterView.filterChips.pickedUsers": "Bereits gewählt", "pickRandomUserPlugin.modal.presenterView.availableSection.title": "Zur Auswahl stehen", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "Teilnehmer", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabelPlural": "Teilnehmer", diff --git a/public/locales/en.json b/public/locales/en.json index 967d1be..544d2ab 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -7,6 +7,10 @@ "pickRandomUserPlugin.modal.presenterView.optionSection.includeModeratorsLabel": "Include moderators", "pickRandomUserPlugin.modal.presenterView.optionSection.includePresenterLabel": "Include presenter", "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Include already picked users", + "pickRandomUserPlugin.modal.presenterView.filterChips.label": "Also include:", + "pickRandomUserPlugin.modal.presenterView.filterChips.moderators": "Moderators", + "pickRandomUserPlugin.modal.presenterView.filterChips.presenter": "Presenter", + "pickRandomUserPlugin.modal.presenterView.filterChips.pickedUsers": "Already picked", "pickRandomUserPlugin.modal.presenterView.availableSection.title": "Available for selection", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "user", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabelPlural": "users", diff --git a/public/locales/fr-FR.json b/public/locales/fr-FR.json index ffc9734..0580acb 100644 --- a/public/locales/fr-FR.json +++ b/public/locales/fr-FR.json @@ -6,6 +6,10 @@ "pickRandomUserPlugin.modal.presenterView.optionSection.includeModeratorsLabel": "Inclure les modérateurs", "pickRandomUserPlugin.modal.presenterView.optionSection.includePresenterLabel": "Inclure le présentateur", "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Inclure des participants déjà sélectionnés", + "pickRandomUserPlugin.modal.presenterView.filterChips.label": "Inclure aussi :", + "pickRandomUserPlugin.modal.presenterView.filterChips.moderators": "Modérateurs", + "pickRandomUserPlugin.modal.presenterView.filterChips.presenter": "Présentateur", + "pickRandomUserPlugin.modal.presenterView.filterChips.pickedUsers": "Déjà sélectionnés", "pickRandomUserPlugin.modal.presenterView.availableSection.title": "Disponible pour la sélection", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "participant", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabelPlural": "participants", diff --git a/public/locales/it.json b/public/locales/it.json index 13fe116..4867a5a 100644 --- a/public/locales/it.json +++ b/public/locales/it.json @@ -6,6 +6,10 @@ "pickRandomUserPlugin.modal.presenterView.optionSection.includeModeratorsLabel": "Includi moderatori", "pickRandomUserPlugin.modal.presenterView.optionSection.includePresenterLabel": "Includi il presentatore", "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Includi gli utenti già selezionati", + "pickRandomUserPlugin.modal.presenterView.filterChips.label": "Includi anche:", + "pickRandomUserPlugin.modal.presenterView.filterChips.moderators": "Moderatori", + "pickRandomUserPlugin.modal.presenterView.filterChips.presenter": "Presentatore", + "pickRandomUserPlugin.modal.presenterView.filterChips.pickedUsers": "Già selezionati", "pickRandomUserPlugin.modal.presenterView.availableSection.title": "Disponibile per la selezione", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "utente", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabelPlural": "utenti", diff --git a/public/locales/ja.json b/public/locales/ja.json index fcc10c0..e6d65b6 100644 --- a/public/locales/ja.json +++ b/public/locales/ja.json @@ -7,6 +7,10 @@ "pickRandomUserPlugin.modal.presenterView.optionSection.includeModeratorsLabel": "モデレーターを含める", "pickRandomUserPlugin.modal.presenterView.optionSection.includePresenterLabel": "発表者を含める", "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "既に指名した人を含める", + "pickRandomUserPlugin.modal.presenterView.filterChips.label": "含める:", + "pickRandomUserPlugin.modal.presenterView.filterChips.moderators": "モデレーター", + "pickRandomUserPlugin.modal.presenterView.filterChips.presenter": "発表者", + "pickRandomUserPlugin.modal.presenterView.filterChips.pickedUsers": "指名済みの人", "pickRandomUserPlugin.modal.presenterView.availableSection.title": "指名可能な人", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "人のユーザー", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabelPlural": "人のユーザー", diff --git a/public/locales/pt-BR.json b/public/locales/pt-BR.json index b5e79fb..4f79f17 100644 --- a/public/locales/pt-BR.json +++ b/public/locales/pt-BR.json @@ -7,6 +7,10 @@ "pickRandomUserPlugin.modal.presenterView.optionSection.includeModeratorsLabel": "Incluir moderadores", "pickRandomUserPlugin.modal.presenterView.optionSection.includePresenterLabel": "Incluir apresentador", "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Incluir usuários já selecionados", + "pickRandomUserPlugin.modal.presenterView.filterChips.label": "Incluir também:", + "pickRandomUserPlugin.modal.presenterView.filterChips.moderators": "Moderadores", + "pickRandomUserPlugin.modal.presenterView.filterChips.presenter": "Apresentador", + "pickRandomUserPlugin.modal.presenterView.filterChips.pickedUsers": "Já selecionados", "pickRandomUserPlugin.modal.presenterView.availableSection.title": "Disponíveis para seleção", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "usuário", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabelPlural": "usuários", diff --git a/src/components/modal/presenter-view/component.tsx b/src/components/modal/presenter-view/component.tsx index a323e33..c058261 100644 --- a/src/components/modal/presenter-view/component.tsx +++ b/src/components/modal/presenter-view/component.tsx @@ -10,25 +10,25 @@ import { PresenterViewComponentProps } from './types'; import { FilterOptionsContext } from '../../pick-random-user/context'; const intlMessages = defineMessages({ - optionsTitle: { - id: 'pickRandomUserPlugin.modal.presenterView.optionSection.title', - description: 'Title of the options section on modal`s presenter view', - defaultMessage: 'Options', + filterChipsLabel: { + id: 'pickRandomUserPlugin.modal.presenterView.filterChips.label', + description: 'Label preceding the filter chip group', + defaultMessage: 'Also include:', }, - includeModeratorsLabel: { - id: 'pickRandomUserPlugin.modal.presenterView.optionSection.includeModeratorsLabel', - description: 'Label to include moderator`s option on modal`s presenter view', - defaultMessage: 'Include moderators', + moderatorsChipLabel: { + id: 'pickRandomUserPlugin.modal.presenterView.filterChips.moderators', + description: 'Chip label to include moderators', + defaultMessage: 'Moderators', }, - includePresenterLabel: { - id: 'pickRandomUserPlugin.modal.presenterView.optionSection.includePresenterLabel', - description: 'Label of skip presenter`s option on modal`s presenter view', - defaultMessage: 'Include presenter', + presenterChipLabel: { + id: 'pickRandomUserPlugin.modal.presenterView.filterChips.presenter', + description: 'Chip label to include presenter', + defaultMessage: 'Presenter', }, - includePickedUsersLabel: { - id: 'pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel', - description: 'Label of include picked users option on modal`s presenter view', - defaultMessage: 'Include already picked users', + pickedUsersChipLabel: { + id: 'pickRandomUserPlugin.modal.presenterView.filterChips.pickedUsers', + description: 'Chip label to include already picked users', + defaultMessage: 'Already picked', }, availableTitle: { id: 'pickRandomUserPlugin.modal.presenterView.availableSection.title', @@ -113,27 +113,35 @@ function getInitials(name: string): string { return name.slice(0, 2); } -function ShieldIcon() { +function CheckboxSquare({ active }: { active: boolean }) { + const style: React.CSSProperties = active ? { + width: '0.875rem', + height: '0.875rem', + borderRadius: '3px', + background: '#4E7FF8', + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + flexShrink: 0, + } : { + width: '0.875rem', + height: '0.875rem', + borderRadius: '3px', + background: '#fff', + border: '1.5px solid #C0C8D4', + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + flexShrink: 0, + }; return ( - - - - ); -} - -function PresentationIcon() { - return ( - - - - ); -} - -function UserCheckIcon() { - return ( - - - + + {active && ( + + + + )} + ); } @@ -192,78 +200,53 @@ export function PresenterViewComponent(props: PresenterViewComponentProps) { - {/* OPTIONS SECTION */} + {/* FILTER CHIPS */} - - {intl.formatMessage(intlMessages.optionsTitle)} - - - - - - - - - {intl.formatMessage(intlMessages.includeModeratorsLabel)} - - - { - setFilterOptions((prev) => ({ - ...prev, - includeModerators: !prev.includeModerators, - })); - }} - /> - + + + {intl.formatMessage(intlMessages.filterChipsLabel)} + + + + setFilterOptions((prev) => ({ + ...prev, includeModerators: !prev.includeModerators, + }))} + /> + + {intl.formatMessage(intlMessages.moderatorsChipLabel)} + - - - - - - - {intl.formatMessage(intlMessages.includePresenterLabel)} - - - { - setFilterOptions((prev) => ({ - ...prev, - includePresenter: !prev.includePresenter, - })); - }} - /> - + + setFilterOptions((prev) => ({ + ...prev, includePresenter: !prev.includePresenter, + }))} + /> + + {intl.formatMessage(intlMessages.presenterChipLabel)} + - - - - - - - {intl.formatMessage(intlMessages.includePickedUsersLabel)} - - - { - setFilterOptions((prev) => ({ - ...prev, - includePickedUsers: !prev.includePickedUsers, - })); - }} - /> - - + + setFilterOptions((prev) => ({ + ...prev, includePickedUsers: !prev.includePickedUsers, + }))} + /> + + {intl.formatMessage(intlMessages.pickedUsersChipLabel)} + + + {/* AVAILABLE USERS SECTION */} diff --git a/src/components/modal/presenter-view/styles.tsx b/src/components/modal/presenter-view/styles.tsx index bc440d9..89e8dcf 100644 --- a/src/components/modal/presenter-view/styles.tsx +++ b/src/components/modal/presenter-view/styles.tsx @@ -10,86 +10,54 @@ const SectionLabel = styled.span` letter-spacing: 0.6px; `; -// ── Options section (toggle rows) ───────────────────────────────────────────── +// ── Filter chips section ────────────────────────────────────────────────────── -const OptionsContainer = styled.div` - margin-top: 0.5rem; -`; - -const ToggleRow = styled.label` +const FilterRow = styled.div` display: flex; - align-items: center; - justify-content: space-between; - padding: 0.4375rem 0; - cursor: pointer; - border-bottom: 1px solid #F2F4F7; - - &:last-child { - border-bottom: none; - } + flex-direction: column; + gap: 0.375rem; `; -const ToggleRowLeft = styled.div` - display: flex; - align-items: center; - gap: 0.5rem; +const FilterLabel = styled.span` + font-size: 0.8125rem; + font-weight: 600; + color: #8B9AAF; + white-space: nowrap; `; -const IconCircle = styled.span` - width: 1.5rem; - height: 1.5rem; - border-radius: 50%; - background: #EEF2F8; +const ChipGroup = styled.div` display: flex; align-items: center; - justify-content: center; - flex-shrink: 0; - color: #4E7FF8; + gap: 0.375rem; + flex-wrap: wrap; `; -const ToggleLabelText = styled.span` - font-size: 1rem; - color: #1C2B3A; +const ChipInput = styled.input` + display: none; `; -// Checkbox styled as a toggle switch. -// Keeps the real in the DOM so Playwright selectors -// (#includeModerators etc.) and isChecked() / toBeVisible() all keep working. -const ToggleCheckbox = styled.input` - appearance: none; - -webkit-appearance: none; - width: 2.25rem; - height: 1.25rem; - border-radius: 0.625rem; - background: #D1D9E3; +const FilterChip = styled.label<{ $active: boolean }>` + display: inline-flex; + align-items: center; + gap: 0.3125rem; + padding: 0.25rem 0.625rem; + border-radius: 999px; + font-size: 0.75rem; cursor: pointer; - outline: none; - border: none; - transition: background 0.2s ease; - position: relative; - flex-shrink: 0; - margin: 0; - - &::before { - content: ''; - position: absolute; - width: 1rem; - height: 1rem; - background: #fff; - border-radius: 50%; - top: 0.125rem; - left: 0.125rem; - transition: left 0.2s ease; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15); - } - - &:checked { - background: #4E7FF8; - } - - &:checked::before { - left: 1.125rem; - } + user-select: none; + transition: all 0.15s; + + ${({ $active }) => ($active ? ` + background: #EBF1FF; + border: 1.5px solid #4E7FF8; + color: #4E7FF8; + font-weight: 600; + ` : ` + background: #F0F2F6; + border: 1.5px solid #D1D9E3; + color: #8B9AAF; + font-weight: 500; + `)} `; // ── Section header row (label + count/action) ───────────────────────────────── @@ -282,12 +250,11 @@ const PreviouslyPickedSection = styled.div``; export { SectionLabel, - OptionsContainer, - ToggleRow, - ToggleRowLeft, - IconCircle, - ToggleLabelText, - ToggleCheckbox, + FilterRow, + FilterLabel, + ChipGroup, + ChipInput, + FilterChip, SectionHeaderRow, CountBadge, UserListContainer, From bde47062e43647874c02d5f98e438f83b227522a Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Thu, 21 May 2026 10:14:25 -0300 Subject: [PATCH 5/9] [ref-ui] Fixed tests due to change in the UI --- .../modal/presenter-view/component.tsx | 18 ++++++++++-- tests/behavioral/helpers.ts | 28 +++++++++++-------- tests/behavioral/multi-user.spec.ts | 2 +- tests/behavioral/single-user.spec.ts | 10 +++---- tests/elements.ts | 7 ++++- tests/structural/test.spec.ts | 10 +++---- 6 files changed, 49 insertions(+), 26 deletions(-) diff --git a/src/components/modal/presenter-view/component.tsx b/src/components/modal/presenter-view/component.tsx index c058261..2060101 100644 --- a/src/components/modal/presenter-view/component.tsx +++ b/src/components/modal/presenter-view/component.tsx @@ -207,7 +207,11 @@ export function PresenterViewComponent(props: PresenterViewComponentProps) { {intl.formatMessage(intlMessages.filterChipsLabel)} - + - + - +