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
2 changes: 1 addition & 1 deletion src/actions/speaker-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ export const getSpeakers =
createAction(RECEIVE_SPEAKERS),
`${window.API_BASE_URL}/api/v1/speakers`,
authErrorHandler,
{ order, orderDir, page, term }
{ order, orderDir, page, term, perPage }
)(params)(dispatch).then(() => {
dispatch(stopLoading());
});
Expand Down
3 changes: 2 additions & 1 deletion src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -699,7 +699,8 @@
"member_id": "Member Id",
"registration_code": "Registration Code",
"on_site_phone": "On Site Phone",
"delete_speaker_warning": "Are you sure you want to delete speaker"
"no_results": "No items found for this search criteria.",
"delete_speaker_warning": "Are you sure you want to delete speaker {name}"
},
"edit_speaker": {
"registration_code": "Registration Code",
Expand Down
225 changes: 225 additions & 0 deletions src/pages/speakers/__tests__/summit-speakers-list-page.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
import React from "react";
import { screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { renderWithRedux } from "../../../utils/test-utils";
import SummitSpeakerListPage from "../summit-speakers-list-page";
import * as speakerActions from "../../../actions/speaker-actions";

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

jest.mock("openstack-uicore-foundation/lib/components/mui/table", () => ({
__esModule: true,
default: ({ data, onEdit, onDelete }) => (
<div data-testid="mui-table">
{data.map((row) => (
<div key={row.id} data-testid={`row-${row.id}`}>
<span>{row.name}</span>
{onEdit && (
<button type="button" onClick={() => onEdit(row)}>
edit-row
</button>
)}
{onDelete && (
<button type="button" onClick={() => onDelete(row.id)}>
delete-row
</button>
)}
</div>
))}
</div>
)
}));

jest.mock("openstack-uicore-foundation/lib/components/mui/search-input", () => {
const React = require("react");
return {
__esModule: true,
default: ({ onSearch, term }) => {
const [value, setValue] = React.useState(term || "");
return (
<input
data-testid="search-input"
value={value}
onChange={(e) => setValue(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") onSearch(value);
}}
/>
);
}
};
});

jest.mock("../../../actions/speaker-actions", () => ({
...jest.requireActual("../../../actions/speaker-actions"),
getSpeakers: jest.fn(() => () => Promise.resolve()),
deleteSpeaker: jest.fn(() => () => Promise.resolve())
}));

// Mock Member so permissions are easy to control per test
jest.mock("../../../models/member", () => ({
__esModule: true,
default: jest.fn().mockImplementation(() => ({
canAddSpeakers: jest.fn(() => true),
canEditSpeakers: jest.fn(() => true),
canDeleteSpeakers: jest.fn(() => true)
}))
}));

const mockHistory = { push: jest.fn() };

const SAMPLE_SPEAKERS = [
{ id: 1, name: "Alice Smith", email: "alice@example.com", member_id: 10 },
{ id: 2, name: "Bob Jones", email: "bob@example.com", member_id: 20 }
];

const buildInitialState = (listOverrides = {}) => ({
currentSpeakerListState: {
speakers: [],
term: "",
order: "id",
orderDir: 1,
currentPage: 1,
lastPage: 1,
perPage: 10,
totalSpeakers: 0,
...listOverrides
},
loggedUserState: { member: { groups: [] } }
});

const allPermissions = {
canAddSpeakers: jest.fn(() => true),
canEditSpeakers: jest.fn(() => true),
canDeleteSpeakers: jest.fn(() => true)
};

describe("SummitSpeakerListPage", () => {
beforeEach(() => {
jest.clearAllMocks();
const Member = require("../../../models/member").default;
Member.mockImplementation(() => ({ ...allPermissions }));
});

test("should call getSpeakers on mount", async () => {
renderWithRedux(<SummitSpeakerListPage history={mockHistory} />, {
initialState: buildInitialState()
});

await waitFor(() => {
expect(speakerActions.getSpeakers).toHaveBeenCalledTimes(1);
});
});

test("should show empty state message when no speakers", () => {
renderWithRedux(<SummitSpeakerListPage history={mockHistory} />, {
initialState: buildInitialState()
});

expect(screen.getByText("speaker_list.no_results")).toBeInTheDocument();
expect(screen.queryByTestId("mui-table")).not.toBeInTheDocument();
});

test("should show table and total count when speakers are present", () => {
renderWithRedux(<SummitSpeakerListPage history={mockHistory} />, {
initialState: buildInitialState({
speakers: SAMPLE_SPEAKERS,
totalSpeakers: 2
})
});

expect(screen.getByTestId("mui-table")).toBeInTheDocument();
expect(screen.getByText(/2\sspeaker_list\.speakers/)).toBeInTheDocument();
expect(
screen.queryByText("speaker_list.no_results")
).not.toBeInTheDocument();
});

test("should show Add button when canAddSpeakers is true", () => {
renderWithRedux(<SummitSpeakerListPage history={mockHistory} />, {
initialState: buildInitialState()
});

expect(screen.getByText("speaker_list.add_speaker")).toBeInTheDocument();
});

test("should hide Add button when canAddSpeakers is false", () => {
const Member = require("../../../models/member").default;
Member.mockImplementation(() => ({
canAddSpeakers: jest.fn(() => false),
canEditSpeakers: jest.fn(() => true),
canDeleteSpeakers: jest.fn(() => true)
}));

renderWithRedux(<SummitSpeakerListPage history={mockHistory} />, {
initialState: buildInitialState()
});

expect(
screen.queryByText("speaker_list.add_speaker")
).not.toBeInTheDocument();
});

test("should navigate to new speaker page on Add button click", async () => {
const user = userEvent.setup();
renderWithRedux(<SummitSpeakerListPage history={mockHistory} />, {
initialState: buildInitialState()
});

await user.click(screen.getByText("speaker_list.add_speaker"));

expect(mockHistory.push).toHaveBeenCalledWith("/app/speakers/new");
});

test("should navigate to speaker edit page on row edit", async () => {
const user = userEvent.setup();
renderWithRedux(<SummitSpeakerListPage history={mockHistory} />, {
initialState: buildInitialState({
speakers: SAMPLE_SPEAKERS,
totalSpeakers: 2
})
});

await user.click(screen.getAllByText("edit-row")[0]);

expect(mockHistory.push).toHaveBeenCalledWith("/app/speakers/1");
});

test("should call deleteSpeaker and refreshes on row delete", async () => {
const user = userEvent.setup();
renderWithRedux(<SummitSpeakerListPage history={mockHistory} />, {
initialState: buildInitialState({
speakers: SAMPLE_SPEAKERS,
totalSpeakers: 2
})
});

await user.click(screen.getAllByText("delete-row")[0]);

await waitFor(() => {
expect(speakerActions.deleteSpeaker).toHaveBeenCalledWith(1);
});
});

test("should call getSpeakers with search term on Enter", async () => {
const user = userEvent.setup();
renderWithRedux(<SummitSpeakerListPage history={mockHistory} />, {
initialState: buildInitialState()
});

await user.type(screen.getByTestId("search-input"), "alice{Enter}");

await waitFor(() => {
expect(speakerActions.getSpeakers).toHaveBeenCalledWith(
"alice",
1,
10,
"id",
1
);
});
});
});
Loading
Loading