From 53b135ffbac0a5b9aefcaa1decb46c25c5eaf567 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Castillo?= Date: Tue, 1 Sep 2026 18:27:49 -0300 Subject: [PATCH 1/3] fix: adjust extra questions types on sponsor badge scan save or add MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Castillo --- src/components/mui/mui-qr-badge-popup.js | 22 ++++++++++++------- .../edit-badge-scan-popup.js | 18 +++++++++------ src/utils/extra-questions.js | 8 +++++++ 3 files changed, 33 insertions(+), 15 deletions(-) diff --git a/src/components/mui/mui-qr-badge-popup.js b/src/components/mui/mui-qr-badge-popup.js index 694932bfe..55d81917b 100644 --- a/src/components/mui/mui-qr-badge-popup.js +++ b/src/components/mui/mui-qr-badge-popup.js @@ -36,7 +36,11 @@ import CloseIcon from "@mui/icons-material/Close"; import { useSnackbarMessage } from "openstack-uicore-foundation/lib/components/mui/snackbar-notification"; import MuiFormikTextField from "openstack-uicore-foundation/lib/components/mui/formik-inputs/textfield"; import QrReader from "../qr-reader"; -import { getTypeValue, toSlug } from "../../utils/extra-questions"; +import { + getTypeValue, + toSlug, + formatAnswerForSubmit +} from "../../utils/extra-questions"; import MuiFormikAsyncAutocomplete from "./formik-inputs/mui-formik-async-select"; import { queryAttendeesWithTickets } from "../../actions/attendee-actions"; @@ -68,13 +72,15 @@ const MuiQrBadgePopup = ({ const { attendee_email, notes, ...extraValues } = values; const extra_questions = Object.entries(extraValues) - .map(([slug, value]) => ({ - question_id: parseInt(slug.split("_").pop()), - answer: Array.isArray(value) - ? value.filter((v) => v !== "").join(",") - : value - })) - .filter((q) => q.answer); + .map(([slug, value]) => { + const question_id = parseInt(slug.split("_").pop()); + const question = extraQuestions.find((q) => q.id === question_id); + return { + question_id, + answer: formatAnswerForSubmit(value, question?.type) + }; + }) + .filter((q) => q.answer !== ""); const entity = { ...(scanMode === BADGE_SCAN_MODE_QR diff --git a/src/pages/sponsors/sponsor-page/tabs/sponsor-badge-scans/edit-badge-scan-popup.js b/src/pages/sponsors/sponsor-page/tabs/sponsor-badge-scans/edit-badge-scan-popup.js index 6aac999b6..096173188 100644 --- a/src/pages/sponsors/sponsor-page/tabs/sponsor-badge-scans/edit-badge-scan-popup.js +++ b/src/pages/sponsors/sponsor-page/tabs/sponsor-badge-scans/edit-badge-scan-popup.js @@ -21,7 +21,11 @@ import ExtraQuestionsMUI from "openstack-uicore-foundation/lib/components/extra- import MuiFormikTextField from "openstack-uicore-foundation/lib/components/mui/formik-inputs/textfield"; import useScrollToError from "../../../../../hooks/useScrollToError"; -import { getTypeValue, toSlug } from "../../../../../utils/extra-questions"; +import { + getTypeValue, + toSlug, + formatAnswerForSubmit +} from "../../../../../utils/extra-questions"; const formatExtraQuestions = (extraQuestions, sponsorQuestions) => { const values = {}; @@ -67,17 +71,17 @@ const EditBadgeScanPopup = ({ badgeScan, onClose, onSubmit }) => { // formatting extra questions before submit, and omit empty answers const extra_questions = Object.entries(extraValues) .map(([slug, value]) => { - const parts = slug.split("_").pop(); - const question_id = parseInt(parts); + const question_id = parseInt(slug.split("_").pop()); + const question = badgeScan.sponsor_extra_questions.find( + (q) => q.id === question_id + ); return { question_id, - answer: Array.isArray(value) - ? value.filter((v) => v !== "").join(",") - : value + answer: formatAnswerForSubmit(value, question?.type) }; }) - .filter((q) => q.answer); + .filter((q) => q.answer !== ""); onSubmit({ id, diff --git a/src/utils/extra-questions.js b/src/utils/extra-questions.js index c9c545dbe..008f424f3 100644 --- a/src/utils/extra-questions.js +++ b/src/utils/extra-questions.js @@ -28,3 +28,11 @@ export const getTypeValue = (ans, type) => { return ans; } }; + +// reverse of getTypeValue: formats a typed form value back into the string +// the API expects for extra_questions[].answer +export const formatAnswerForSubmit = (value, type) => { + if (Array.isArray(value)) return value.filter((v) => v !== "").join(","); + if (type === QuestionType_Checkbox) return value ? "true" : "false"; + return value === null || value === undefined ? "" : `${value}`; +}; From 77ababf4c2d036eb132aa6756bcf33ec33aa18b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Castillo?= Date: Wed, 2 Sep 2026 01:45:19 -0300 Subject: [PATCH 2/3] fix: add regression tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Castillo --- .../mui/__tests__/mui-qr-badge-popup.test.js | 64 +++++++++++++++++-- src/utils/__tests__/extra-questions.test.js | 34 ++++++++++ 2 files changed, 94 insertions(+), 4 deletions(-) create mode 100644 src/utils/__tests__/extra-questions.test.js diff --git a/src/components/mui/__tests__/mui-qr-badge-popup.test.js b/src/components/mui/__tests__/mui-qr-badge-popup.test.js index 923a7dd30..a60bfe278 100644 --- a/src/components/mui/__tests__/mui-qr-badge-popup.test.js +++ b/src/components/mui/__tests__/mui-qr-badge-popup.test.js @@ -55,10 +55,35 @@ jest.mock("../formik-inputs/mui-formik-async-select", () => { jest.mock( "openstack-uicore-foundation/lib/components/extra-questions-mui", - () => ({ - __esModule: true, - default: () =>
- }) + () => { + const React = require("react"); + const { toSlug } = require("../../../utils/extra-questions"); + return { + __esModule: true, + default: ({ extraQuestions, formik }) => ( +
+ {extraQuestions + .filter((q) => q.type === "CheckBox") + .map((q) => { + const slug = toSlug(q.name, q.id); + return ( + + ); + })} +
+ ) + }; + } ); const mockErrorMessage = jest.fn(); @@ -361,4 +386,35 @@ describe("MuiQrBadgePopup", () => { expect(screen.queryByTestId("extra-questions")).not.toBeInTheDocument(); }); }); + + describe("Extra questions submission", () => { + const checkboxQuestion = { id: 5, name: "Opted In", type: "CheckBox" }; + + it("should submit an unchecked CheckBox answer as the string 'false' instead of dropping it", async () => { + const onSave = jest.fn(); + renderComponent({ + isAdmin: true, + extraQuestions: [checkboxQuestion], + onSave + }); + + await userEvent.click( + screen.getByRole("radio", { + name: "sponsor_badge_scans.scan_popup.scan_qr" + }) + ); + await userEvent.click(screen.getByText("Simulate Scan")); + await userEvent.click( + screen.getByRole("button", { name: "general.save" }) + ); + + await waitFor(() => { + expect(onSave).toHaveBeenCalledWith( + expect.objectContaining({ + extra_questions: [{ question_id: 5, answer: "false" }] + }) + ); + }); + }); + }); }); diff --git a/src/utils/__tests__/extra-questions.test.js b/src/utils/__tests__/extra-questions.test.js new file mode 100644 index 000000000..4153a432a --- /dev/null +++ b/src/utils/__tests__/extra-questions.test.js @@ -0,0 +1,34 @@ +import { formatAnswerForSubmit } from "../extra-questions"; + +describe("formatAnswerForSubmit", () => { + describe("CheckBox questions", () => { + it("should format an unchecked (false) answer as the string 'false' instead of dropping it", () => { + expect(formatAnswerForSubmit(false, "CheckBox")).toBe("false"); + }); + + it("should format a checked (true) answer as the string 'true'", () => { + expect(formatAnswerForSubmit(true, "CheckBox")).toBe("true"); + }); + }); + + describe("CheckBoxList questions", () => { + it("should join selected values, filtering out empty entries", () => { + expect(formatAnswerForSubmit(["1", "", "3"], "CheckBoxList")).toBe("1,3"); + }); + + it("should return an empty string for no selection", () => { + expect(formatAnswerForSubmit([], "CheckBoxList")).toBe(""); + }); + }); + + describe("other question types", () => { + it("should stringify a numeric 0 answer instead of treating it as empty", () => { + expect(formatAnswerForSubmit(0, "ComboBox")).toBe("0"); + }); + + it("should return an empty string for a null or undefined answer", () => { + expect(formatAnswerForSubmit(null, "RadioButtonList")).toBe(""); + expect(formatAnswerForSubmit(undefined, "Text")).toBe(""); + }); + }); +}); From 01de57f426c43a62e6a5343fea464c64cf0c868d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Castillo?= Date: Wed, 2 Sep 2026 01:53:13 -0300 Subject: [PATCH 3/3] fix: add test file for edit-badge-scan-popup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Castillo --- .../__tests__/edit-badge-scan-popup.test.js | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 src/pages/sponsors/sponsor-page/tabs/sponsor-badge-scans/__tests__/edit-badge-scan-popup.test.js diff --git a/src/pages/sponsors/sponsor-page/tabs/sponsor-badge-scans/__tests__/edit-badge-scan-popup.test.js b/src/pages/sponsors/sponsor-page/tabs/sponsor-badge-scans/__tests__/edit-badge-scan-popup.test.js new file mode 100644 index 000000000..2f65b3730 --- /dev/null +++ b/src/pages/sponsors/sponsor-page/tabs/sponsor-badge-scans/__tests__/edit-badge-scan-popup.test.js @@ -0,0 +1,91 @@ +// edit-badge-scan-popup.test.js +import React from "react"; +import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import "@testing-library/jest-dom"; +import EditBadgeScanPopup from "../edit-badge-scan-popup"; + +jest.mock("i18n-react/dist/i18n-react", () => ({ + __esModule: true, + default: { translate: (key) => key } +})); + +jest.mock( + "openstack-uicore-foundation/lib/components/extra-questions-mui", + () => { + const React = require("react"); + const { toSlug } = require("../../../../../../utils/extra-questions"); + return { + __esModule: true, + default: ({ extraQuestions, formik }) => ( +
+ {extraQuestions + .filter((q) => q.type === "CheckBox") + .map((q) => { + const slug = toSlug(q.name, q.id); + return ( + + ); + })} +
+ ) + }; + } +); + +const buildBadgeScan = (overrides = {}) => ({ + id: 1, + attendee_full_name: "John Doe", + attendee_company: "Acme", + notes: "", + extra_questions: [], + sponsor_extra_questions: [ + { id: 5, name: "Opted In", type: "CheckBox", order: 1 } + ], + ...overrides +}); + +const renderComponent = (props = {}) => + render( + + ); + +describe("EditBadgeScanPopup", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe("Extra questions submission", () => { + it("should submit an unchecked CheckBox answer as the string 'false' instead of dropping it", async () => { + const onSubmit = jest.fn(); + renderComponent({ onSubmit }); + + await userEvent.click( + screen.getByRole("button", { name: "general.save" }) + ); + + await waitFor(() => { + expect(onSubmit).toHaveBeenCalledWith( + expect.objectContaining({ + extra_questions: [{ question_id: 5, answer: "false" }] + }) + ); + }); + }); + }); +});