Skip to content

Commit

Permalink
refactor(web): adapt Questions component to queries
Browse files Browse the repository at this point in the history
  • Loading branch information
dgdavid committed Jul 30, 2024
1 parent e12f481 commit ded2d52
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 112 deletions.
65 changes: 17 additions & 48 deletions web/src/components/questions/Questions.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,73 +19,42 @@
* find current contact information at www.suse.com.
*/

import React, { useCallback, useEffect, useState } from "react";
import { useInstallerClient } from "~/context/installer";
import { useCancellablePromise } from "~/utils";
import { QUESTION_TYPES } from "~/client/questions";

import React from "react";
import {
GenericQuestion,
QuestionWithPassword,
LuksActivationQuestion,
} from "~/components/questions";
import { useQuestionMutation, useQuestions, useQuestionsChanges } from "~/queries/questions";
import { QuestionType } from "~/types/questions";

export default function Questions() {
const client = useInstallerClient();
const { cancellablePromise } = useCancellablePromise();

const [pendingQuestions, setPendingQuestions] = useState([]);

const addQuestion = useCallback((question) => {
setPendingQuestions((pending) => [...pending, question]);
}, []);

const removeQuestion = useCallback(
(id) => setPendingQuestions((pending) => pending.filter((q) => q.id !== id)),
[],
);

const answerQuestion = useCallback(
(question) => {
client.questions.answer(question);
removeQuestion(question.id);
},
[client.questions, removeQuestion],
);

useEffect(() => {
client.questions.listenQuestions();
}, [client.questions, cancellablePromise]);

useEffect(() => {
cancellablePromise(client.questions.getQuestions())
.then(setPendingQuestions)
.catch((e) => console.error("Something went wrong retrieving pending questions", e));
}, [client.questions, cancellablePromise]);

useEffect(() => {
const unsubscribeCallbacks = [];
unsubscribeCallbacks.push(client.questions.onQuestionAdded(addQuestion));
unsubscribeCallbacks.push(client.questions.onQuestionRemoved(removeQuestion));

return () => {
unsubscribeCallbacks.forEach((cb) => cb());
};
}, [client.questions, addQuestion, removeQuestion]);
useQuestionsChanges();
const pendingQuestions = useQuestions();
const answerQuestion = useQuestionMutation();

if (pendingQuestions.length === 0) return null;

// Renders the first pending question
const [currentQuestion] = pendingQuestions;

let QuestionComponent = GenericQuestion;

// show specialized popup for question which need password
if (currentQuestion.type === QUESTION_TYPES.withPassword) {
if (currentQuestion.type === QuestionType.withPassword) {
QuestionComponent = QuestionWithPassword;
}

// show specialized popup for luks activation question
// more can follow as it will be needed
if (currentQuestion.class === "storage.luks_activation") {
QuestionComponent = LuksActivationQuestion;
}
return <QuestionComponent question={currentQuestion} answerCallback={answerQuestion} />;

return (
<QuestionComponent
question={currentQuestion}
answerCallback={() => answerQuestion.mutate(currentQuestion)}
/>
);
}
125 changes: 61 additions & 64 deletions web/src/components/questions/Questions.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,107 +20,104 @@
*/

import React from "react";

import { act, waitFor, within } from "@testing-library/react";
import { installerRender } from "~/test-utils";
import { createClient } from "~/client";

import { screen } from "@testing-library/react";
import { installerRender, plainRender } from "~/test-utils";
import { Questions } from "~/components/questions";
import { QuestionType } from "~/types/questions";
import * as GenericQuestionComponent from "~/components/questions/GenericQuestion";

let mockQuestions;
const mockMutation = jest.fn();

jest.mock("~/client");
jest.mock("~/components/questions/GenericQuestion", () => () => <div>A Generic question mock</div>);
jest.mock("~/components/questions/LuksActivationQuestion", () => () => (
<div>A LUKS activation question mock</div>
));
jest.mock("~/components/questions/QuestionWithPassword", () => () => (
<div>A question with password mock</div>
));

const handlers = {};
const genericQuestion = { id: 1, type: "generic" };
jest.mock("~/queries/questions", () => ({
...jest.requireActual("~/queries/software"),
useQuestions: () => mockQuestions,
useQuestionsChanges: () => jest.fn(),
useQuestionMutation: () => ({ mutate: mockMutation }),
}));

const genericQuestion = {
id: 1,
type: QuestionType.generic,
options: ["yes", "no"],
defaultOption: "no",
text: "A generic question",
};
const passwordQuestion = { id: 1, type: QuestionType.withPassword };
const luksActivationQuestion = { id: 1, class: "storage.luks_activation" };
let pendingQuestions = [];

beforeEach(() => {
createClient.mockImplementation(() => {
return {
questions: {
getQuestions: () => Promise.resolve(pendingQuestions),
// Capture the handler for the onQuestionAdded signal for triggering it manually
onQuestionAdded: (onAddHandler) => {
handlers.onAdd = onAddHandler;
return jest.fn;
},
// Capture the handler for the onQuestionREmoved signal for triggering it manually
onQuestionRemoved: (onRemoveHandler) => {
handlers.onRemove = onRemoveHandler;
return jest.fn;
},
listenQuestions: jest.fn(),
},
};
});
});

describe("Questions", () => {
afterEach(() => {
jest.restoreAllMocks();
});

describe("when there are no pending questions", () => {
beforeEach(() => {
pendingQuestions = [];
mockQuestions = [];
});

it("renders nothing", async () => {
const { container } = installerRender(<Questions />);
await waitFor(() => expect(container).toBeEmptyDOMElement());
it("renders nothing", () => {
const { container } = plainRender(<Questions />);
expect(container).toBeEmptyDOMElement();
});
});

describe("when a new question is added", () => {
it("push it into the pending queue", async () => {
const { container } = installerRender(<Questions />);
await waitFor(() => expect(container).toBeEmptyDOMElement());

// Manually triggers the handler given for the onQuestionAdded signal
act(() => handlers.onAdd(genericQuestion));
describe("when a question is answered", () => {
beforeEach(() => {
mockQuestions = [genericQuestion];
});

await within(container).findByText("A Generic question mock");
it("triggers the useQuestionMutationk", async () => {
const { user } = plainRender(<Questions />);
const button = screen.getByRole("button", { name: "Yes" });
await user.click(button);
expect(mockMutation).toHaveBeenCalled();
});
});

describe("when a question is removed", () => {
describe("when there is a generic question pending", () => {
beforeEach(() => {
pendingQuestions = [genericQuestion];
mockQuestions = [genericQuestion];
// Not using jest.mock at the top like for the other question components
// because the original implementation was needed for testing that
// mutation is triggered when proceed.
jest
.spyOn(GenericQuestionComponent, "default")
.mockReturnValue(<div>A generic question mock</div>);
});

it("removes it from the queue", async () => {
const { container } = installerRender(<Questions />);
await within(container).findByText("A Generic question mock");

// Manually triggers the handler given for the onQuestionRemoved signal
act(() => handlers.onRemove(genericQuestion.id));

const content = within(container).queryByText("A Generic question mock");
expect(content).toBeNull();
it("renders a GenericQuestion component", () => {
plainRender(<Questions />);
screen.getByText("A generic question mock");
});
});

describe("when there is a generic question pending", () => {
beforeEach(() => {
pendingQuestions = [genericQuestion];
mockQuestions = [passwordQuestion];
});

it("renders a GenericQuestion component", async () => {
const { container } = installerRender(<Questions />);

await within(container).findByText("A Generic question mock");
it("renders a QuestionWithPassword component", () => {
plainRender(<Questions />);
screen.getByText("A question with password mock");
});
});

describe("when there is a LUKS activation question pending", () => {
beforeEach(() => {
pendingQuestions = [luksActivationQuestion];
mockQuestions = [luksActivationQuestion];
});

it("renders a LuksActivationQuestion component", async () => {
const { container } = installerRender(<Questions />);

await within(container).findByText("A LUKS activation question mock");
it("renders a LuksActivationQuestion component", () => {
installerRender(<Questions />);
screen.getByText("A LUKS activation question mock");
});
});
});

0 comments on commit ded2d52

Please sign in to comment.