diff --git a/manifest.json b/manifest.json index 2713334..14885a8 100644 --- a/manifest.json +++ b/manifest.json @@ -1,5 +1,5 @@ { - "requiredSdkVersion": "~0.0.65", + "requiredSdkVersion": "~0.0.92", "name": "BbbPluginPickRandomUser", "version": "0.0.8", "javascriptEntrypointUrl": "BbbPluginPickRandomUser.js", diff --git a/package-lock.json b/package-lock.json index fd397e6..9f90882 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "@types/react": "^18.2.13", "@types/react-dom": "^18.2.6", "babel-plugin-syntax-dynamic-import": "^6.18.0", - "bigbluebutton-html-plugin-sdk": "0.0.89", + "bigbluebutton-html-plugin-sdk": "0.0.100", "path": "^0.12.7", "react": "^18.2.0", "react-chat-elements": "^12.0.14", @@ -3678,9 +3678,10 @@ "license": "MIT" }, "node_modules/bigbluebutton-html-plugin-sdk": { - "version": "0.0.89", - "resolved": "https://registry.npmjs.org/bigbluebutton-html-plugin-sdk/-/bigbluebutton-html-plugin-sdk-0.0.89.tgz", - "integrity": "sha512-KD09jHDaANHkDOFn3dNZtVrgP0OY5RK03m3Nw+LOyuXfg2KTjR9DWgmxU55k6JD5F8bYJ6uw0Zr6MB7jIWusDw==", + "version": "0.0.100", + "resolved": "https://registry.npmjs.org/bigbluebutton-html-plugin-sdk/-/bigbluebutton-html-plugin-sdk-0.0.100.tgz", + "integrity": "sha512-Ge/CXQBCMwdfR6qjx4VI4SuCmEoH7rMW0t4d01ENiYi+mxLHi2HW8hiZvsEnlaw1D8KTN7ETSjhiHzL6J5070A==", + "license": "LGPL-3.0", "dependencies": { "@apollo/client": "^3.8.7", "@browser-bunyan/console-formatted-stream": "^1.8.0", diff --git a/package.json b/package.json index 863aa3e..e4c99ad 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "@types/react": "^18.2.13", "@types/react-dom": "^18.2.6", "babel-plugin-syntax-dynamic-import": "^6.18.0", - "bigbluebutton-html-plugin-sdk": "0.0.89", + "bigbluebutton-html-plugin-sdk": "0.0.100", "react-intl": "^6.6.8", "path": "^0.12.7", "react": "^18.2.0", @@ -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..a9b6c72 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -1,4 +1,5 @@ { + "pickRandomUserPlugin.modal.pickedUserView.resultSectionLabel": "Ergebnis", "pickRandomUserPlugin.modal.pickedUserView.title.currentUserPicked": "Sie wurden zufällig ausgewählt", "pickRandomUserPlugin.modal.pickedUserView.title.randomUserPicked": "Zufällig gewählter Teilnehmer", "pickRandomUserPlugin.modal.pickedUserView.backButton.label": "zurück", @@ -7,6 +8,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", @@ -15,8 +20,15 @@ "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.title": "Zuvor gewählt", "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.clearButtonLabel": "Alle löschen", "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.noUsersWarning": "Keine {0} zum Auswählen verfügbar", - "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickUser": "Wähle {0}", - "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickAgain": "Wähle erneut", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickUser": "Wähle zufälligen Teilnehmer", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickNext": "Nächsten zufälligen Teilnehmer wählen", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickAnother": "Anderen zufälligen Teilnehmer wählen", "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..526201e 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -1,4 +1,5 @@ { + "pickRandomUserPlugin.modal.pickedUserView.resultSectionLabel": "Result", "pickRandomUserPlugin.modal.pickedUserView.title.currentUserPicked": "You have been randomly picked", "pickRandomUserPlugin.modal.pickedUserView.title.randomUserPicked": "Randomly picked user", "pickRandomUserPlugin.modal.pickedUserView.backButton.label": "back", @@ -6,7 +7,11 @@ "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.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", @@ -15,10 +20,17 @@ "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.title": "Previously picked", "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.clearButtonLabel": "Clear All", "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.noUsersWarning": "No {0} available to randomly pick from", - "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickUser": "Pick {0}", - "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickAgain": "Pick again", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickUser": "Pick random user", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickNext": "Pick next random user", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickAnother": "Pick another random user", "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..b862585 100644 --- a/public/locales/fr-FR.json +++ b/public/locales/fr-FR.json @@ -1,11 +1,16 @@ { + "pickRandomUserPlugin.modal.pickedUserView.resultSectionLabel": "Résultat", "pickRandomUserPlugin.modal.pickedUserView.title.currentUserPicked": "Vous avez été sélectionné au hasard", "pickRandomUserPlugin.modal.pickedUserView.title.randomUserPicked": "Participant sélectionné au hasard", "pickRandomUserPlugin.modal.pickedUserView.backButton.label": "retour", "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.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", @@ -14,8 +19,15 @@ "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.title": "Sélectionné précédemment", "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.clearButtonLabel": "Vider la sélection", "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.noUsersWarning": "Aucun {0} disponible pour être sélectionné au hasard", - "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickUser": "Sélectionner {0}", - "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickAgain": "Sélectionner à nouveau", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickUser": "Sélectionner un participant au hasard", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickNext": "Sélectionner le prochain participant au hasard", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickAnother": "Sélectionner un autre participant au hasard", "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..68e1bb9 100644 --- a/public/locales/it.json +++ b/public/locales/it.json @@ -1,11 +1,16 @@ { + "pickRandomUserPlugin.modal.pickedUserView.resultSectionLabel": "Risultato", "pickRandomUserPlugin.modal.pickedUserView.title.currentUserPicked": "Sei stato scelto a caso", "pickRandomUserPlugin.modal.pickedUserView.title.randomUserPicked": "Utente scelto a caso", "pickRandomUserPlugin.modal.pickedUserView.backButton.label": "indietro", "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.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", @@ -14,8 +19,15 @@ "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.title": "Precedentemente selezionato", "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.clearButtonLabel": "Cancella tutto", "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.noUsersWarning": "Nessun {0} disponibile da cui scegliere casualmente", - "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickUser": "Scegli {0}", - "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickAgain": "Scegli di nuovo", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickUser": "Scegli un utente casuale", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickNext": "Scegli il prossimo utente casuale", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickAnother": "Scegli un altro utente casuale", "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..7533564 100644 --- a/public/locales/ja.json +++ b/public/locales/ja.json @@ -1,4 +1,5 @@ { + "pickRandomUserPlugin.modal.pickedUserView.resultSectionLabel": "結果", "pickRandomUserPlugin.modal.pickedUserView.title.currentUserPicked": "あなたが指名されました", "pickRandomUserPlugin.modal.pickedUserView.title.randomUserPicked": "指名された人", "pickRandomUserPlugin.modal.pickedUserView.backButton.label": "戻る", @@ -7,6 +8,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": "人のユーザー", @@ -15,8 +20,15 @@ "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.title": "既に指名された人", "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.clearButtonLabel": "全消去", "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.noUsersWarning": "{0}の中で指名できる人はいません", - "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickUser": "{0}から指名する", - "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickAgain": "もう一度指名する", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickUser": "ランダムに指名する", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickNext": "次のランダムユーザーを指名する", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickAnother": "別のランダムユーザーを指名する", "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..24fea30 100644 --- a/public/locales/pt-BR.json +++ b/public/locales/pt-BR.json @@ -1,4 +1,5 @@ { + "pickRandomUserPlugin.modal.pickedUserView.resultSectionLabel": "Resultado", "pickRandomUserPlugin.modal.pickedUserView.title.currentUserPicked": "Você foi selecionado aleatoriamente", "pickRandomUserPlugin.modal.pickedUserView.title.randomUserPicked": "Usuário selecionado aleatoriamente", "pickRandomUserPlugin.modal.pickedUserView.backButton.label": "voltar", @@ -6,7 +7,11 @@ "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.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", @@ -15,8 +20,15 @@ "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.title": "Selecionados anteriormente", "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.clearButtonLabel": "Limpar tudo", "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.noUsersWarning": "Não há disponibilidade de {0} para seleção", - "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickUser": "Selecionar {0}", - "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickAgain": "Selecionar novamente", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickUser": "Selecionar usuário aleatório", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickNext": "Selecionar próximo usuário aleatório", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickAnother": "Selecionar outro usuário aleatório", "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/component.tsx b/src/components/modal/picked-user-view/component.tsx index b4d8a1c..3993226 100644 --- a/src/components/modal/picked-user-view/component.tsx +++ b/src/components/modal/picked-user-view/component.tsx @@ -6,16 +6,16 @@ import * as Styled from './styles'; import { hasCurrentUserSeenPickedUser } from '../../../commons/utils'; const intlMessages = defineMessages({ + resultSectionLabel: { + id: 'pickRandomUserPlugin.modal.pickedUserView.resultSectionLabel', + description: 'Section label shown above the picked user result', + defaultMessage: 'Result', + }, currentUserPicked: { id: 'pickRandomUserPlugin.modal.pickedUserView.title.currentUserPicked', description: 'Title to show that current user has been picked', defaultMessage: 'You have been randomly picked', }, - randomUserPicked: { - id: 'pickRandomUserPlugin.modal.pickedUserView.title.randomUserPicked', - description: 'Title to show that random user has been picked', - defaultMessage: 'Randomly picked user', - }, backButtonLabel: { id: 'pickRandomUserPlugin.modal.pickedUserView.backButton.label', description: 'Label of back button in picked-user view on the modal', @@ -71,62 +71,62 @@ export function PickedUserViewComponent(props: PickedUserViewComponentProps) { }); } }, [pickedUserWithEntryId]); - const title = (pickedUserWithEntryId?.pickedUser?.userId === currentUser?.userId) - ? intl.formatMessage(intlMessages.currentUserPicked) - : intl.formatMessage(intlMessages.randomUserPicked); - const avatarAltDescriptor = intl.formatMessage(intlMessages.currentUserPicked, { 0: pickedUserWithEntryId?.pickedUser?.name, }); return ( - {title} - { - (pickedUserWithEntryId) ? ( - <> - {avatarUrl ? ( - - ) : ( - - {pickedUserWithEntryId?.pickedUser?.name.slice(0, 2)} - + + + {intl.formatMessage(intlMessages.resultSectionLabel)} + + { + (pickedUserWithEntryId) ? ( + <> + {avatarUrl ? ( + + ) : ( + + {pickedUserWithEntryId?.pickedUser?.name.slice(0, 2)} + + )} + {pickedUserWithEntryId?.pickedUser?.name} + + ) : null + } + {!canClose && remainingSeconds > 0 && !currentUser?.presenter && ( + + {intl.formatMessage( + remainingSeconds === 1 + ? intlMessages.modalCloseDelayMessageSingular + : intlMessages.modalCloseDelayMessage, + { seconds: Math.ceil(remainingSeconds) }, )} - {pickedUserWithEntryId?.pickedUser?.name} - - ) : null - } - {!canClose && remainingSeconds > 0 && !currentUser?.presenter && ( - - {intl.formatMessage( - remainingSeconds === 1 - ? intlMessages.modalCloseDelayMessageSingular - : intlMessages.modalCloseDelayMessage, - { seconds: Math.ceil(remainingSeconds) }, - )} - - )} - { - (currentUser?.presenter) ? ( + + )} + + {currentUser?.presenter && ( + {intl.formatMessage(intlMessages.backButtonLabel)} - ) : null - } - {!canClose && remainingSeconds > 0 && currentUser?.presenter && ( - - - + {!canClose && remainingSeconds > 0 && ( + + + + )} + )} ); diff --git a/src/components/modal/picked-user-view/styles.tsx b/src/components/modal/picked-user-view/styles.tsx index 8ab2364..0945a89 100644 --- a/src/components/modal/picked-user-view/styles.tsx +++ b/src/components/modal/picked-user-view/styles.tsx @@ -3,15 +3,31 @@ import { ModalAvatarProps } from './types'; const PickedUserViewWrapper = styled.div` width: 100%; - height: 100%; + display: flex; + flex-direction: column; +`; + +const PickedUserViewBody = styled.div` + display: flex; + flex-direction: column; align-items: center; + padding: 1.5rem 1.25rem 1rem; +`; + +const PickedUserViewFooter = styled.div` + padding: 0 1.25rem 1rem; display: flex; flex-direction: column; + gap: 0.5rem; `; -const PickedUserViewTitle = styled.h1` - font-weight: 600; - font-size: 20px; +const ResultSectionLabel = styled.span` + font-size: 1rem; + font-weight: 800; + color: #8B9AAF; + text-transform: uppercase; + letter-spacing: 0.6px; + margin: 1rem 0; `; const PickedUserAvatarInitials = styled.div` @@ -25,7 +41,6 @@ const PickedUserAvatarInitials = styled.div` color: white; font-size: 2.75rem; font-weight: 400; - margin-bottom: .25rem; text-transform: capitalize; `; @@ -39,28 +54,27 @@ const PickedUserAvatarImage = styled.img` `; const PickedUserName = styled.p` - font-size: 30px; + font-size: 1.875rem; font-weight: 500; + margin: 1rem 0; `; const BackButton = 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; + 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; &:hover { - color: var(--btn-primary-color, var(--color-white, #FFF)); - background-color: var(--btn-primary-hover-bg, #0C57A7) !important; + background: #3D6DE0; } `; @@ -79,11 +93,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,12 +104,14 @@ 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 { PickedUserViewWrapper, - PickedUserViewTitle, + PickedUserViewBody, + PickedUserViewFooter, + ResultSectionLabel, PickedUserAvatarInitials, PickedUserAvatarImage, PickedUserName, diff --git a/src/components/modal/presenter-view/component.tsx b/src/components/modal/presenter-view/component.tsx index b771eab..52f899b 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: 'Skip 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 user', + 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', @@ -73,44 +73,105 @@ const intlMessages = defineMessages({ pickButtonLabel: { id: 'pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickUser', description: 'Label of the button to pick another user', - defaultMessage: 'Pick {0}', + defaultMessage: 'Pick random user', }, - pickAgainButtonLabel: { - id: 'pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickAgain', - description: 'Label of the button to pick another user', - defaultMessage: 'Pick again', + pickNextRandomUserButtonLabel: { + id: 'pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickNext', + description: 'Label of the button to pick next random user (when already picked users are included in pool)', + defaultMessage: 'Pick next random user', + }, + pickAnotherRandomUserButtonLabel: { + id: 'pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickAnother', + description: 'Label of the button to pick another random user (when already picked users are excluded)', + defaultMessage: 'Pick another random user', + }, + 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]; +} -const makeVerticalListOfNames = ( - list?: DataChannelEntryResponseType[], -) => list?.filter((u) => !!u.payloadJson).map((u) => { - const time = new Date(u.createdAt); - const timeMiliseconds = time.getTime(); +function getInitials(name: string): string { + return name.slice(0, 2); +} + +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 ( -
  • - {u.payloadJson.name} - {' '} - ( - {time.getHours().toString().padStart(2, '0')} - : - {time.getMinutes().toString().padStart(2, '0')} - ) -
  • + + {active && ( + + + + )} + ); -}); +} + +function makePickedUserRows(list?: DataChannelEntryResponseType[]) { + return list?.filter((u) => !!u.payloadJson).map((u) => { + const time = new Date(u.createdAt); + const hh = String(time.getHours()).padStart(2, '0'); + const mm = String(time.getMinutes()).padStart(2, '0'); + const { avatar, color, name } = u.payloadJson; + const initials = getInitials(name); + return ( + + {avatar ? ( + + ) : ( + + {initials} + + )} + {name} + {`${hh}:${mm}`} + + ); + }); +} export function PresenterViewComponent(props: PresenterViewComponentProps) { const { @@ -122,144 +183,191 @@ 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 ? ( - + + + {/* FILTER CHIPS */} + + + + {intl.formatMessage(intlMessages.filterChipsLabel)} + + + + setFilterOptions((prev) => ({ + ...prev, includeModerators: !prev.includeModerators, + }))} + /> + + {intl.formatMessage(intlMessages.moderatorsChipLabel)} + + + + setFilterOptions((prev) => ({ + ...prev, includePresenter: !prev.includePresenter, + }))} + /> + + {intl.formatMessage(intlMessages.presenterChipLabel)} + + + + setFilterOptions((prev) => ({ + ...prev, includePickedUsers: !prev.includePickedUsers, + }))} + /> + + {intl.formatMessage(intlMessages.pickedUsersChipLabel)} + + + + + + {/* 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) - ? intl.formatMessage(intlMessages.pickAgainButtonLabel) - : intl.formatMessage(intlMessages.pickButtonLabel, { 0: userRoleLabel }) - } - + {pickedUserWithEntryId + ? intl.formatMessage(includePickedUsers + ? intlMessages.pickNextRandomUserButtonLabel + : intlMessages.pickAnotherRandomUserButtonLabel) + : intl.formatMessage(intlMessages.pickButtonLabel)} + ) : ( -

    + {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..5e8e772 100644 --- a/src/components/modal/presenter-view/styles.tsx +++ b/src/components/modal/presenter-view/styles.tsx @@ -1,108 +1,319 @@ 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; `; -const PresenterViewSectionWrapper = styled.div` +// ── Filter chips section ────────────────────────────────────────────────────── + +const FilterRow = styled.div` display: flex; - align-items: flex-start; flex-direction: column; - max-height: 30%; + gap: 0.375rem; `; -const PresenterViewSectionTitle = styled.div` - margin: 5px 0px; +const FilterLabel = styled.span` + font-size: 0.8125rem; font-weight: 600; - font-size: 20px; + color: #8B9AAF; + white-space: nowrap; `; -const PresenterViewSectionClearAllButton = styled.button` - padding: 1px 10px; - margin-left: 8px; - font-size: 15px; - background: #efefef; - border: none; - color: inherit; - border-radius: 8px; +const ChipGroup = styled.div` + display: flex; + align-items: center; + gap: 0.375rem; + flex-wrap: wrap; +`; + +const ChipInput = styled.input` + display: none; +`; + +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; - &:hover { - background-color: #ddd; - } + 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; + `)} `; -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; +`; + +// ── Available users section ─────────────────────────────────────────────────── + +const CountBadge = styled.span` + font-size: 0.75rem; + color: #4E7FF8; + font-weight: 600; `; -const PresenterViewSectionListWrapper = styled.div` +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: 12rem; overflow-y: auto; - max-height: 10rem; - margin-bottom: .75rem; - width: 100%; + + &::-webkit-scrollbar { + width: 4px; + } + &::-webkit-scrollbar-track { + background: transparent; + } + &::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.2); + border-radius: 999px; + } + &::-webkit-scrollbar-button { + display: none; + } + scrollbar-width: thin; + scrollbar-color: rgba(0, 0, 0, 0.2) transparent; `; -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 PresenterViewSectionContent = styled.div` +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 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; + overflow-y: auto; + + &::-webkit-scrollbar { + width: 4px; + } + &::-webkit-scrollbar-track { + background: transparent; + } + &::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.2); + border-radius: 999px; + } + &::-webkit-scrollbar-button { + display: none; + } + scrollbar-width: thin; + scrollbar-color: rgba(0, 0, 0, 0.2) transparent; `; -// 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 PickedTimeText = styled.span` + font-size: 0.6875rem; + color: #A7B3C3; + margin-left: auto; + flex-shrink: 0; `; -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, + FilterRow, + FilterLabel, + ChipGroup, + ChipInput, + FilterChip, + SectionHeaderRow, + CountBadge, + UserListContainer, + UserRow, + UserAvatar, + UserAvatarImage, + UserNameText, + RoleBadge, + ClearAllButton, + EmptyStateContainer, + EmptyStateText, + PickedUserListContainer, + PickedList, + PickedUserRow, + PickedTimeText, + 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/src/components/pick-random-user/component.tsx b/src/components/pick-random-user/component.tsx index 74b6f94..e1cdb49 100644 --- a/src/components/pick-random-user/component.tsx +++ b/src/components/pick-random-user/component.tsx @@ -13,10 +13,8 @@ import { PickRandomUserPluginProps, PickedUserSeenEntryDataChannel, PickedUser, - UsersMoreInformationGraphqlResponse, } from './types'; import { FilterOptionsContext } from './context'; -import { USERS_MORE_INFORMATION } from './queries'; import { PickUserModal } from '../modal/component'; import ActionButtonDropdownManager from '../extensible-areas/action-button-dropdown/component'; import { filterPossibleUsersToBePicked } from './utils'; @@ -38,8 +36,9 @@ function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { const currentUserInfo = pluginApi.useCurrentUser(); const shouldUnmountPlugin = pluginApi.useShouldUnmountPlugin(); const { data: currentUser } = currentUserInfo; - const allUsersInfo = pluginApi - .useCustomSubscription(USERS_MORE_INFORMATION); + const allUsersInfo = pluginApi?.useUsersBasicInfo + ? pluginApi?.useUsersBasicInfo() + : { data: undefined as undefined }; const { data: allUsers } = allUsersInfo; const { diff --git a/src/components/pick-random-user/queries.ts b/src/components/pick-random-user/queries.ts deleted file mode 100644 index 45d35a9..0000000 --- a/src/components/pick-random-user/queries.ts +++ /dev/null @@ -1,14 +0,0 @@ -// TODO: Replace this query with the useUsersBasicInfo once it's merged in -export const USERS_MORE_INFORMATION = ` -subscription usersMoreInformation { - user(where: { bot: { _eq: false } }) { - color - name - userId - role - presenter - avatar - bot - } -} -`; diff --git a/src/components/pick-random-user/types.ts b/src/components/pick-random-user/types.ts index f314959..5fc600c 100644 --- a/src/components/pick-random-user/types.ts +++ b/src/components/pick-random-user/types.ts @@ -18,10 +18,6 @@ export interface PickRandomUserPluginProps { pluginUuid: string, } -export interface UsersMoreInformationGraphqlResponse { - user: PickedUser[]; -} - export interface PickedUserSeenEntryDataChannel { pickedUserId: string; seenByUserId: string; diff --git a/src/components/pick-random-user/utils.ts b/src/components/pick-random-user/utils.ts index 3ea3912..921ed74 100644 --- a/src/components/pick-random-user/utils.ts +++ b/src/components/pick-random-user/utils.ts @@ -1,13 +1,12 @@ -import { DataChannelEntryResponseType } from 'bigbluebutton-html-plugin-sdk'; +import { DataChannelEntryResponseType, UsersBasicInfoResponseFromGraphqlWrapper } from 'bigbluebutton-html-plugin-sdk'; import { FilterOptionsType, PickedUser, - UsersMoreInformationGraphqlResponse, } from './types'; import { Role } from './enums'; export const filterPossibleUsersToBePicked = ( - allUsers: UsersMoreInformationGraphqlResponse, + allUsers: UsersBasicInfoResponseFromGraphqlWrapper | undefined, pickedUserFromDataChannel: DataChannelEntryResponseType[], filterOptions: FilterOptionsType, ) => ({ @@ -24,5 +23,5 @@ export const filterPossibleUsersToBePicked = ( ) === -1; } return true; - }), + }) || [], }); diff --git a/tests/README.md b/tests/README.md index 193358e..11a64be 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 @@ -57,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 @@ -74,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 diff --git a/tests/behavioral/helpers.ts b/tests/behavioral/helpers.ts index 657df3b..26865c1 100644 --- a/tests/behavioral/helpers.ts +++ b/tests/behavioral/helpers.ts @@ -1,7 +1,33 @@ +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 chip and verify the underlying checkbox reaches checked state, retrying once. + * @param clickSelector - the visible chip label to click + * @param checkSelector - the hidden input whose checked state to assert (defaults to clickSelector) + */ +export async function clickToggleOnWithRetry( + page: Page, + clickSelector: string, + description: string, + checkSelector: string = clickSelector, +): Promise { + const checkLocator = page.page.locator(checkSelector); + await page.page.click(clickSelector); + try { + await test.expect(checkLocator, `${description} toggle should be on`).toBeChecked({ timeout: 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(clickSelector); + await test.expect(checkLocator, `${description} toggle should be on (retry)`).toBeChecked({ timeout: 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); @@ -13,7 +39,7 @@ export async function goBackToPresenterView(modPage: Page): Promise { await modPage.hasElement(e.pickRandomUserBackButton, 'back button should be visible'); await modPage.page.click(e.pickRandomUserBackButton); await modPage.hasElement( - e.includeModeratorsCheckbox, + e.includeModeratorsChip, 'presenter view should be restored after clicking back', ELEMENT_WAIT_TIME, ); @@ -27,13 +53,13 @@ export async function moderatorCleanupAfterTest(modPage: Page): Promise { const modCloseBtn = modPage.page.locator(e.pickRandomUserModalCloseButton); if (await modCloseBtn.isVisible()) { - // Uncheck any filter checkboxes that were left enabled. + // Uncheck any filter chips that were left enabled. const includePickedUsers = modPage.page.locator(e.includePickedUsersCheckbox); - if (await includePickedUsers.isChecked()) await includePickedUsers.click(); + if (await includePickedUsers.isChecked()) await modPage.page.click(e.includePickedUsersChip); const includeModerators = modPage.page.locator(e.includeModeratorsCheckbox); - if (await includeModerators.isChecked()) await includeModerators.click(); + if (await includeModerators.isChecked()) await modPage.page.click(e.includeModeratorsChip); const includePresenter = modPage.page.locator(e.includePresenterCheckbox); - if (await includePresenter.isChecked()) await includePresenter.click(); + if (await includePresenter.isChecked()) await modPage.page.click(e.includePresenterChip); // Clear the previously-picked history. const clearBtn = modPage.page.locator(e.pickRandomUserClearAllButton); diff --git a/tests/behavioral/multi-user.spec.ts b/tests/behavioral/multi-user.spec.ts index 8923896..fd92244 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); @@ -168,26 +184,24 @@ test.describe('Pick Random User Plugin - Behavioural (multi-user)', () => { ); }); - test('should show "You have been randomly picked" to the picked attendee and "Randomly picked user" to the presenter', async (): Promise => { + test('should show the "Result" section label to both the picked attendee and the presenter', async (): Promise => { await waitForAttendeeMeeting(attendeePage); await openModal(modPage); await modPage.hasElement(e.pickRandomUserPickButton, 'pick button should be visible', ELEMENT_WAIT_LONGER_TIME); await modPage.page.click(e.pickRandomUserPickButton); - // Attendee is the picked user → sees "You have been randomly picked". await attendeePage.hasElement(e.pickRandomUserPickedUserViewTitle, 'attendee modal should open', ELEMENT_WAIT_LONGER_TIME); await attendeePage.hasText( e.pickRandomUserPickedUserViewTitle, - 'You have been randomly picked', - 'picked attendee should see the "You have been randomly picked" title', + 'Result', + 'picked attendee should see the "Result" section label', ); - // Presenter sees "Randomly picked user" (different user was picked). await modPage.hasElement(e.pickRandomUserPickedUserViewTitle, 'presenter modal should transition to picked-user view', ELEMENT_WAIT_LONGER_TIME); await modPage.hasText( e.pickRandomUserPickedUserViewTitle, - 'Randomly picked user', - 'presenter should see the "Randomly picked user" title (not their own name)', + 'Result', + 'presenter should see the "Result" section label', ); }); @@ -253,7 +267,7 @@ test.describe('Pick Random User Plugin - Behavioural (multi-user)', () => { await openModal(modPage); // Enable "Include already picked users" so the viewer stays eligible after being picked. - await modPage.page.click(e.includePickedUsersCheckbox); + await modPage.page.click(e.includePickedUsersChip); await modPage.hasElement( e.pickRandomUserPickButton, 'pick button should be visible once the attendee is an eligible viewer', @@ -293,8 +307,8 @@ test.describe('Pick Random User Plugin - Behavioural (multi-user)', () => { ); await modPage.hasText( e.pickRandomUserPickButton, - 'Pick again', - 'pick button should read "Pick again" because a user has already been picked this session', + 'Pick next random user', + 'pick button should read "Pick next random user" because a user has already been picked and includePickedUsers is ON', ); // ── Assertion 3: re-picking selects the same (and only) eligible user ───── diff --git a/tests/behavioral/single-user.spec.ts b/tests/behavioral/single-user.spec.ts index b2ff80c..ed55049 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.includeModeratorsChip, 'includeModerators', e.includeModeratorsCheckbox); + await clickToggleOnWithRetry(modPage, e.includePresenterChip, 'includePresenter', e.includePresenterCheckbox); } /** 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.includeModeratorsChip, 'includeModerators', e.includeModeratorsCheckbox); + await clickToggleOnWithRetry(modPage, e.includePresenterChip, 'includePresenter', e.includePresenterCheckbox); + await clickToggleOnWithRetry(modPage, e.includePickedUsersChip, 'includePickedUsers', e.includePickedUsersCheckbox); } /** 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,17 +79,28 @@ 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 => { + test('should show "Pick next random user" button (not "Pick random user") after navigating back from picked-user view when includePickedUsers is enabled', async (): Promise => { // With "Include already picked users" ON the presenter stays in the pool // after being picked, so the pick button remains visible on return. await openModal(modPage); @@ -107,8 +114,8 @@ test.describe('Pick Random User Plugin - Behavioural (single user)', () => { ); await modPage.hasText( e.pickRandomUserPickButton, - 'Pick again', - 'button label should read "Pick again" after a user has already been picked', + 'Pick next random user', + 'button label should read "Pick next random user" after a user has already been picked (includePickedUsers is enabled)', ); }); diff --git a/tests/elements.ts b/tests/elements.ts index 1943b1f..d5b57e0 100644 --- a/tests/elements.ts +++ b/tests/elements.ts @@ -10,7 +10,12 @@ export const elements = { // Modal close button pickRandomUserModalCloseButton: '[data-test="pickRandomUserModalCloseButton"]', - // Presenter view – filter checkboxes (identified by their HTML id attributes) + // Presenter view – filter chips (label elements, for click and visibility checks) + includeModeratorsChip: '[data-test="includeModeratorsChip"]', + includePresenterChip: '[data-test="includePresenterChip"]', + includePickedUsersChip: '[data-test="includePickedUsersChip"]', + + // Presenter view – filter checkboxes (hidden inputs, for isChecked / not.toBeChecked only) includeModeratorsCheckbox: '#includeModerators', includePresenterCheckbox: '#includePresenter', includePickedUsersCheckbox: '#includePickedUsers', diff --git a/tests/structural/test.spec.ts b/tests/structural/test.spec.ts index e8c55b4..5738d6a 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 }); @@ -104,8 +119,8 @@ test.describe('Pick Random User Plugin - Structural', () => { test('should open the presenter modal when clicking the action-button option', async (): Promise => { await openPickRandomUserModal(modPage); await modPage.hasElement( - e.includeModeratorsCheckbox, - 'should show the modal presenter view with the "Include moderators" checkbox', + e.includeModeratorsChip, + 'should show the modal presenter view with the "Include moderators" chip', ); await modPage.hasElement( e.pickRandomUserAvailableContent, @@ -115,9 +130,9 @@ test.describe('Pick Random User Plugin - Structural', () => { test('should display all three filter checkboxes in the presenter view', async (): Promise => { await openPickRandomUserModal(modPage); - await modPage.hasElement(e.includeModeratorsCheckbox, 'should display the "Include moderators" checkbox'); - await modPage.hasElement(e.includePresenterCheckbox, 'should display the "Include presenter" checkbox'); - await modPage.hasElement(e.includePickedUsersCheckbox, 'should display the "Include already picked user" checkbox'); + await modPage.hasElement(e.includeModeratorsChip, 'should display the "Include moderators" chip'); + await modPage.hasElement(e.includePresenterChip, 'should display the "Include presenter" chip'); + await modPage.hasElement(e.includePickedUsersChip, 'should display the "Include already picked" chip'); }); test('should have all three filter checkboxes unchecked by default', async (): Promise => {