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/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/__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" }]
+ })
+ );
+ });
+ });
+ });
+});
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/__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("");
+ });
+ });
+});
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}`;
+};