Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import sponsorCustomizedFormItemsListReducer from "../sponsor-customized-form-items-list-reducer";
import {
RECEIVE_SPONSOR_CUSTOMIZED_FORM_ITEM,
SPONSOR_FORM_MANAGED_ITEM_UPDATED
} from "../../../actions/sponsor-forms-actions";

const DEFAULT_STATE = {
items: [],
showArchived: false,
term: "",
order: "name",
orderDir: 1,
currentPage: 1,
lastPage: 1,
perPage: 10,
totalCount: 0,
currentItem: {
code: "",
name: "",
description: "",
early_bird_rate: 0,
standard_rate: 0,
onsite_rate: 0,
quantity_limit_per_show: 0,
quantity_limit_per_sponsor: 0,
default_quantity: 0,
images: [],
meta_fields: []
}
};

const buildItem = (overrides = {}) => ({
id: 1,
code: "ITEM1",
name: "Item One",
description: "desc",
early_bird_rate: 100,
standard_rate: 200,
onsite_rate: 300,
default_quantity: 5,
is_archived: false,
images: [],
meta_fields: [],
...overrides
});

describe("sponsorCustomizedFormItemsListReducer", () => {
describe("RECEIVE_SPONSOR_CUSTOMIZED_FORM_ITEM", () => {
it("maps file_url to file_path on each image — the edit-form image fix", () => {
const result = sponsorCustomizedFormItemsListReducer(DEFAULT_STATE, {
type: RECEIVE_SPONSOR_CUSTOMIZED_FORM_ITEM,
payload: {
response: buildItem({
images: [
{ id: 10, file_url: "https://cdn/a.png" },
{ id: 11, file_url: "https://cdn/b.png" }
]
})
}
});

expect(result.currentItem.images).toEqual([
{
id: 10,
file_url: "https://cdn/a.png",
file_path: "https://cdn/a.png"
},
{
id: 11,
file_url: "https://cdn/b.png",
file_path: "https://cdn/b.png"
}
]);
});

it("handles absent images without throwing", () => {
const result = sponsorCustomizedFormItemsListReducer(DEFAULT_STATE, {
type: RECEIVE_SPONSOR_CUSTOMIZED_FORM_ITEM,
payload: { response: buildItem({ images: undefined }) }
});

expect(result.currentItem.images).toEqual([]);
});

it("handles absent meta_fields without throwing — guards the .length access", () => {
const result = sponsorCustomizedFormItemsListReducer(DEFAULT_STATE, {
type: RECEIVE_SPONSOR_CUSTOMIZED_FORM_ITEM,
payload: { response: buildItem({ meta_fields: undefined }) }
});

expect(result.currentItem.meta_fields).toEqual([]);
});
});

describe("SPONSOR_FORM_MANAGED_ITEM_UPDATED", () => {
it("replaces the matching list item and preserves its images as-is", () => {
const images = [{ id: 20, file_url: "https://cdn/img.png" }];
const state = {
...DEFAULT_STATE,
items: [
buildItem({ id: 1, name: "Before", images }),
buildItem({ id: 2, name: "Other" })
]
};

const result = sponsorCustomizedFormItemsListReducer(state, {
type: SPONSOR_FORM_MANAGED_ITEM_UPDATED,
payload: {
response: buildItem({
id: 1,
name: "After",
early_bird_rate: 500,
standard_rate: 600,
onsite_rate: 700,
images
})
}
});

expect(result.items[0].name).toBe("After");
expect(result.items[0].images).toBe(images);
expect(result.items[1].name).toBe("Other");
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import sponsorCustomizedFormReducer from "../sponsor-customized-form-reducer";
import {
RECEIVE_SPONSOR_CUSTOMIZED_FORM,
RESET_SPONSOR_CUSTOMIZED_FORM
} from "../../../actions/sponsor-forms-actions";

const DEFAULT_ENTITY = {
id: 0,
code: "",
name: "",
allowed_add_ons: [],
opens_at: "",
expires_at: "",
instructions: "",
meta_fields: [],
items: []
};

const DEFAULT_STATE = { entity: DEFAULT_ENTITY };

describe("sponsorCustomizedFormReducer", () => {
describe("RECEIVE_SPONSOR_CUSTOMIZED_FORM", () => {
it("maps file_url to file_path for images on each item", () => {
const result = sponsorCustomizedFormReducer(DEFAULT_STATE, {
type: RECEIVE_SPONSOR_CUSTOMIZED_FORM,
payload: {
response: {
id: 1,
code: "FORM1",
items: [
{
id: 10,
name: "Item A",
images: [
{ id: 100, file_url: "https://example.com/img1.png" },
{ id: 101, file_url: "https://example.com/img2.png" }
]
}
]
}
}
});

expect(result.entity.items[0].images).toEqual([
{
id: 100,
file_url: "https://example.com/img1.png",
file_path: "https://example.com/img1.png"
},
{
id: 101,
file_url: "https://example.com/img2.png",
file_path: "https://example.com/img2.png"
}
]);
});

it("handles items with no images", () => {
const result = sponsorCustomizedFormReducer(DEFAULT_STATE, {
type: RECEIVE_SPONSOR_CUSTOMIZED_FORM,
payload: {
response: {
id: 1,
code: "FORM1",
items: [{ id: 10, name: "Item A", images: [] }]
}
}
});

expect(result.entity.items[0].images).toEqual([]);
});

it("handles empty items array", () => {
const result = sponsorCustomizedFormReducer(DEFAULT_STATE, {
type: RECEIVE_SPONSOR_CUSTOMIZED_FORM,
payload: {
response: { id: 1, code: "FORM1", items: [] }
}
});

expect(result.entity.items).toEqual([]);
});

it("handles missing items field", () => {
const result = sponsorCustomizedFormReducer(DEFAULT_STATE, {
type: RECEIVE_SPONSOR_CUSTOMIZED_FORM,
payload: {
response: { id: 1, code: "FORM1" }
}
});

expect(result.entity.items).toEqual([]);
});
Comment on lines +58 to +93

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a test for items where images is missing (not just empty).

You already validate images: [], but the reducer also handles images being absent. A direct test for that case will lock the intended fallback contract.

Suggested test case
   it("handles items with no images", () => {
@@
     expect(result.entity.items[0].images).toEqual([]);
   });

+  it("handles items with missing images field", () => {
+    const result = sponsorCustomizedFormReducer(DEFAULT_STATE, {
+      type: RECEIVE_SPONSOR_CUSTOMIZED_FORM,
+      payload: {
+        response: {
+          id: 1,
+          code: "FORM1",
+          items: [{ id: 10, name: "Item A" }]
+        }
+      }
+    });
+
+    expect(result.entity.items[0].images).toEqual([]);
+  });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it("handles items with no images", () => {
const result = sponsorCustomizedFormReducer(DEFAULT_STATE, {
type: RECEIVE_SPONSOR_CUSTOMIZED_FORM,
payload: {
response: {
id: 1,
code: "FORM1",
items: [{ id: 10, name: "Item A", images: [] }]
}
}
});
expect(result.entity.items[0].images).toEqual([]);
});
it("handles empty items array", () => {
const result = sponsorCustomizedFormReducer(DEFAULT_STATE, {
type: RECEIVE_SPONSOR_CUSTOMIZED_FORM,
payload: {
response: { id: 1, code: "FORM1", items: [] }
}
});
expect(result.entity.items).toEqual([]);
});
it("handles missing items field", () => {
const result = sponsorCustomizedFormReducer(DEFAULT_STATE, {
type: RECEIVE_SPONSOR_CUSTOMIZED_FORM,
payload: {
response: { id: 1, code: "FORM1" }
}
});
expect(result.entity.items).toEqual([]);
});
it("handles items with no images", () => {
const result = sponsorCustomizedFormReducer(DEFAULT_STATE, {
type: RECEIVE_SPONSOR_CUSTOMIZED_FORM,
payload: {
response: {
id: 1,
code: "FORM1",
items: [{ id: 10, name: "Item A", images: [] }]
}
}
});
expect(result.entity.items[0].images).toEqual([]);
});
it("handles items with missing images field", () => {
const result = sponsorCustomizedFormReducer(DEFAULT_STATE, {
type: RECEIVE_SPONSOR_CUSTOMIZED_FORM,
payload: {
response: {
id: 1,
code: "FORM1",
items: [{ id: 10, name: "Item A" }]
}
}
});
expect(result.entity.items[0].images).toEqual([]);
});
it("handles empty items array", () => {
const result = sponsorCustomizedFormReducer(DEFAULT_STATE, {
type: RECEIVE_SPONSOR_CUSTOMIZED_FORM,
payload: {
response: { id: 1, code: "FORM1", items: [] }
}
});
expect(result.entity.items).toEqual([]);
});
it("handles missing items field", () => {
const result = sponsorCustomizedFormReducer(DEFAULT_STATE, {
type: RECEIVE_SPONSOR_CUSTOMIZED_FORM,
payload: {
response: { id: 1, code: "FORM1" }
}
});
expect(result.entity.items).toEqual([]);
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/reducers/sponsors/__tests__/sponsor-customized-form-reducer.test.js`
around lines 58 - 93, Add a new unit test that dispatches
RECEIVE_SPONSOR_CUSTOMIZED_FORM to sponsorCustomizedFormReducer with a payload
whose response.items contains an item object that omits the images property
(e.g., { id: 10, name: "Item A" }), and assert that
result.entity.items[0].images is an empty array; use the same DEFAULT_STATE
setup and naming pattern as the existing tests to ensure the reducer's fallback
for missing images is validated.

});

describe("RESET_SPONSOR_CUSTOMIZED_FORM", () => {
it("resets to default state", () => {
const dirty = { entity: { id: 99, items: [{ id: 1 }] } };
const result = sponsorCustomizedFormReducer(dirty, {
type: RESET_SPONSOR_CUSTOMIZED_FORM
});
expect(result).toEqual(DEFAULT_STATE);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,11 @@ const sponsorCustomizedFormItemsListReducer = (

const currentItem = {
...item,
meta_fields: item.meta_fields.length > 0 ? item.meta_fields : []
images: (item.images || []).map((img) => ({

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This block (the RECEIVE_SPONSOR_CUSTOMIZED_FORM_ITEM path) has no tests, unlike the form reducer which got full coverage in this PR.

The file_path normalization here is the path that populates currentItem, which is what SponsorFormItemForm receives as initialValues — it's the edit-form image fix. Consider adding a test that asserts currentItem.images[].file_path === file_url, covering at least the images-present and images-absent cases.

...img,
file_path: img.file_url
})),
meta_fields: (item.meta_fields ?? []).length > 0 ? item.meta_fields : []
};
return { ...state, currentItem };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No test file was added for this reducer, while the form reducer received full coverage in this PR. The three cases most worth adding:

  • RECEIVE_SPONSOR_CUSTOMIZED_FORM_ITEM with images — asserts currentItem.images[].file_path === file_url (the actual edit-form path this PR fixes)
  • RECEIVE_SPONSOR_CUSTOMIZED_FORM_ITEM with meta_fields: undefined — proves the unguarded .length access on line 117 does not throw
  • SPONSOR_FORM_MANAGED_ITEM_UPDATED — verifies that list images survive a save round-trip (currently stores updatedItem.images without file_path normalization, which is fine for table display but worth locking in)

}
Expand Down
21 changes: 17 additions & 4 deletions src/reducers/sponsors/sponsor-customized-form-reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ const DEFAULT_ENTITY = {
opens_at: "",
expires_at: "",
instructions: "",
meta_fields: []
meta_fields: [],
items: []
};

const DEFAULT_STATE = {
Expand All @@ -43,10 +44,22 @@ const sponsorCustomizedFormReducer = (state = DEFAULT_STATE, action) => {
return DEFAULT_STATE;

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
DEFAULT_ENTITY missing items field
fix: Add items: [] to DEFAULT_ENTITY

}
case RECEIVE_SPONSOR_CUSTOMIZED_FORM: {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Two different action creators dispatch RECEIVE_SPONSOR_CUSTOMIZED_FORM into this case with different API expand contracts:

  • getSponsorManagedForm expands items.images, so the normalization below runs correctly.
  • getSponsorCustomizedForm does not expand items, so payload.response.items is absent and falls through the || [] guard safely.

This is safe today, but worth documenting — a future developer adding item-level display to the customized-form popup may not notice that items are empty in that path. Consider a comment here or splitting the action types to make the contract explicit.

return {
...state,
entity: payload.response
// this actions is dispatched by getSponsorManagedForm and getSponsorCustomizedForm
// getSponsorManagedForm expands items.images, getSponsorCustomizedForm not,
// so items is absent here for the customized path.
// Add expand=items,items.images to that action if item display is ever needed in
// the customized-form popup.
const entity = {

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
The fix addresses the bug for one data path (entity-level items from getSponsorManagedForm) but leaves the same bug active on the individual-item edit path (currentItem from getSponsorCustomizedFormItem), meaning images will still be silently dropped in the most common editing scenario. The missing regression test also means the fix is unverified.

...payload.response,
items: (payload.response.items || []).map((it) => ({

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
Add a unit test for RECEIVE_SPONSOR_CUSTOMIZED_FORM in sponsor-customized-form-reducer.js covering: items with images (file_path mapped), items with no images, and empty items array

...it,
images: (it.images || []).map((img) => ({
...img,
file_path: img.file_url
}))
}))
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return { ...state, entity };
}
default:
return state;
Expand Down
Loading