Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions src/components/forms/__tests__/badge-settings-form.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React from "react";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import "@testing-library/jest-dom";
import BadgeSettingsForm from "../badge-settings-form";

jest.mock("i18n-react/dist/i18n-react", () => ({
__esModule: true,
default: { translate: (key) => key }
}));

jest.mock("sweetalert2", () => ({
__esModule: true,
default: { fire: jest.fn() }
}));

const mockSummit = { id: 1, badge_features_types: [], badge_types: [] };

const renderForm = (onSubmit) =>
render(
<BadgeSettingsForm
entity={{}}
currentSummit={mockSummit}
errors={{}}
onSubmit={onSubmit}
onDeleteImage={jest.fn()}
onDeleteBadgeTypeImage={jest.fn()}
/>
);

it("should call onSubmit only once when Save is clicked twice while saving", async () => {
const pendingPromise = new Promise(() => {});
const onSubmit = jest.fn(() => pendingPromise);
const { container } = renderForm(onSubmit);

fireEvent.change(container.querySelector("#BADGE_TEMPLATE_WIDTH"), {
target: { value: "100" }
});

const saveButton = screen.getByRole("button", { name: "general.save" });
fireEvent.click(saveButton);
fireEvent.click(saveButton);

expect(onSubmit).toHaveBeenCalledTimes(1);
});

it("should re-enable Save and not throw an unhandled rejection when onSubmit rejects", async () => {
const onSubmit = jest.fn(() => Promise.reject(new Error("412")));
const { container } = renderForm(onSubmit);

fireEvent.change(container.querySelector("#BADGE_TEMPLATE_WIDTH"), {
target: { value: "100" }
});

fireEvent.click(screen.getByRole("button", { name: "general.save" }));

await waitFor(() => {
expect(
screen.getByRole("button", { name: "general.save" })
).not.toBeDisabled();
});
});
40 changes: 26 additions & 14 deletions src/components/forms/badge-settings-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@
* */
import React from "react";
import T from "i18n-react/dist/i18n-react";
import UploadInput from "openstack-uicore-foundation/lib/components/inputs/upload-input"
import Input from "openstack-uicore-foundation/lib/components/inputs/text-input"
import TextArea from "openstack-uicore-foundation/lib/components/inputs/textarea-input"
import Panel from "openstack-uicore-foundation/lib/components/sections/panel"
import UploadInput from "openstack-uicore-foundation/lib/components/inputs/upload-input";
import Input from "openstack-uicore-foundation/lib/components/inputs/text-input";
import TextArea from "openstack-uicore-foundation/lib/components/inputs/textarea-input";
import Panel from "openstack-uicore-foundation/lib/components/sections/panel";
import Dropdown from "openstack-uicore-foundation/lib/components/inputs/dropdown";
import Switch from "react-switch";
import Swal from "sweetalert2";
Expand All @@ -30,7 +30,8 @@ class BadgeSettingsForm extends React.Component {
this.state = {
entity: { ...props.entity },
errors: props.errors,
showSection: null
showSection: null,
isSaving: false
};

this.handleChange = this.handleChange.bind(this);
Expand Down Expand Up @@ -159,26 +160,36 @@ class BadgeSettingsForm extends React.Component {
handleSubmit(ev) {
ev.preventDefault();

if (this.state.isSaving) return;

// save only the settings with the following conditions
const settingsToSave = Object.fromEntries(
Object.entries(this.state.entity).filter(
([, values]) => values.updated === true
)
);

this.props.onSubmit(settingsToSave).then(() => {
const success_message = {
title: T.translate("general.done"),
html: T.translate("badge_settings.badge_template_settings_updated"),
type: "success"
};
this.setState({ isSaving: true });

Swal.fire(success_message);
});
this.props
.onSubmit(settingsToSave)
.then(() => {
const success_message = {
title: T.translate("general.done"),
html: T.translate("badge_settings.badge_template_settings_updated"),
type: "success"
};

Swal.fire(success_message);
})
.catch(() => {})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tomrndom .catch(() => {}) on this line sits after .then(), so it swallows errors thrown inside the success handler (the block above), not just rejections from onSubmit. If Swal.fire(success_message) throws for any reason (bad message shape, a sweetalert2 internal error), that error is silently absorbed here: isSaving still resets via .finally(), but the user sees neither a success nor an error message, and the bug never surfaces in the console — a real save success ends up looking identical to a swallowed exception.

Suggested fix — use the two-argument form of .then() so the reject handler only covers onSubmit's own rejection, letting a success-handler error propagate instead of being masked:

this.props
  .onSubmit(settingsToSave)
  .then(
    () => {
      Swal.fire({
        title: T.translate("general.done"),
        html: T.translate("badge_settings.badge_template_settings_updated"),
        type: "success"
      });
    },
    () => {} // only swallows onSubmit's own rejection
  )
  .finally(() => {
    this.setState({ isSaving: false });
  });

Regression test to add alongside it in badge-settings-form.test.js — proves the success-handler error is no longer swallowed (fails on the current .then().catch() chain, passes after the fix above):

import Swal from "sweetalert2";

it("should not swallow an error thrown by the success handler", async () => {
  const onSubmit = jest.fn(() => Promise.resolve());
  Swal.fire.mockImplementationOnce(() => {
    throw new Error("Swal render error");
  });

  const rejections = [];
  const onUnhandledRejection = (event) => rejections.push(event.reason);
  window.addEventListener("unhandledrejection", onUnhandledRejection);

  const { container } = renderForm(onSubmit);
  fireEvent.change(container.querySelector("#BADGE_TEMPLATE_WIDTH"), {
    target: { value: "100" }
  });
  fireEvent.click(screen.getByRole("button", { name: "general.save" }));

  await waitFor(() => expect(rejections).toHaveLength(1));
  window.removeEventListener("unhandledrejection", onUnhandledRejection);
});

.finally(() => {
this.setState({ isSaving: false });
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

render() {
const { entity, showSection } = this.state;
const { entity, showSection, isSaving } = this.state;
const { currentSummit } = this.props;

const ddlAlignOptions = [
Expand Down Expand Up @@ -1580,6 +1591,7 @@ class BadgeSettingsForm extends React.Component {
onClick={this.handleSubmit}
className="btn btn-primary pull-right"
value={T.translate("general.save")}
disabled={isSaving}
/>
</div>
</div>
Expand Down
Loading