Skip to content

Commit

Permalink
refactor(web): migrate components/questions to TypeScript
Browse files Browse the repository at this point in the history
  • Loading branch information
dgdavid committed Jul 31, 2024
1 parent f42aa70 commit 560f953
Show file tree
Hide file tree
Showing 11 changed files with 111 additions and 91 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) [2022] SUSE LLC
* Copyright (c) [2022-2024] SUSE LLC
*
* All Rights Reserved.
*
Expand All @@ -23,8 +23,9 @@ import React from "react";
import { screen } from "@testing-library/react";
import { plainRender } from "~/test-utils";
import { GenericQuestion } from "~/components/questions";
import { Question } from "~/types/questions";

const question = {
const question: Question = {
id: 1,
text: "Do you write unit tests?",
options: ["always", "sometimes", "never"],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) [2022] SUSE LLC
* Copyright (c) [2022-2024] SUSE LLC
*
* All Rights Reserved.
*
Expand All @@ -23,10 +23,23 @@ import React from "react";
import { Text } from "@patternfly/react-core";
import { Popup } from "~/components/core";
import { QuestionActions } from "~/components/questions";
import { AnswerCallback, Question } from "~/types/questions";
import { _ } from "~/i18n";

export default function GenericQuestion({ question, answerCallback }) {
const actionCallback = (option) => {
/**
* Component for rendering generic questions
*
* @param question - the question to be answered
* @param answerCallback - the callback to be triggered on answer
*/
export default function GenericQuestion({
question,
answerCallback,
}: {
question: Question;
answerCallback: AnswerCallback;
}): React.ReactNode {
const actionCallback = (option: string) => {
question.answer = option;
answerCallback(question);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) [2022] SUSE LLC
* Copyright (c) [2022-2024] SUSE LLC
*
* All Rights Reserved.
*
Expand All @@ -23,23 +23,25 @@ import React from "react";
import { screen } from "@testing-library/react";
import { plainRender } from "~/test-utils";
import { LuksActivationQuestion } from "~/components/questions";

let question;
const answerFn = jest.fn();
import { AnswerCallback, Question } from "~/types/questions";

let question: Question;
const questionMock: Question = {
id: 1,
class: "storage.luks_activation",
text: "A Luks device found. Do you want to open it?",
options: ["decrypt", "skip"],
defaultOption: "decrypt",
data: { attempt: "1" },
};
const answerFn: AnswerCallback = jest.fn();

const renderQuestion = () =>
plainRender(<LuksActivationQuestion question={question} answerCallback={answerFn} />);

describe("LuksActivationQuestion", () => {
beforeEach(() => {
question = {
id: 1,
class: "storage.luks_activation",
text: "A Luks device found. Do you want to open it?",
options: ["decrypt", "skip"],
defaultOption: "decrypt",
data: { attempt: "1" },
};
question = { ...questionMock };
});

it("renders the question text", async () => {
Expand All @@ -48,13 +50,6 @@ describe("LuksActivationQuestion", () => {
await screen.findByText(question.text);
});

it("contains a textinput for entering the password", async () => {
renderQuestion();

const passwordInput = await screen.findByLabelText("Encryption Password");
expect(passwordInput).not.toBeNull();
});

describe("when it is the first attempt", () => {
it("does not contain a warning", async () => {
renderQuestion();
Expand All @@ -66,14 +61,7 @@ describe("LuksActivationQuestion", () => {

describe("when it is not the first attempt", () => {
beforeEach(() => {
question = {
id: 1,
class: "storage.luks_activation",
text: "A Luks device found. Do you want to open it?",
options: ["decrypt", "skip"],
defaultOption: "decrypt",
data: { attempt: "3" },
};
question = { ...questionMock, data: { attempt: "2" } };
});

it("contains a warning", async () => {
Expand All @@ -84,17 +72,6 @@ describe("LuksActivationQuestion", () => {
});

describe("when the user selects one of the options", () => {
beforeEach(() => {
question = {
id: 1,
class: "storage.luks_activation",
text: "A Luks device found. Do you want to open it?",
options: ["decrypt", "skip"],
defaultOption: "decrypt",
data: { attempt: "1" },
};
});

describe("by clicking on 'Skip'", () => {
it("calls the callback after setting both, answer and password", async () => {
const { user } = renderQuestion();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) [2022] SUSE LLC
* Copyright (c) [2022-2024] SUSE LLC
*
* All Rights Reserved.
*
Expand All @@ -20,27 +20,41 @@
*/

import React, { useState } from "react";
import { Alert, Form, FormGroup, Text } from "@patternfly/react-core";
import { Alert as PFAlert, Form, FormGroup, Text } from "@patternfly/react-core";
import { Icon } from "~/components/layout";
import { PasswordInput, Popup } from "~/components/core";
import { QuestionActions } from "~/components/questions";
import { _ } from "~/i18n";

const renderAlert = (attempt) => {
if (!attempt || attempt === 1) return null;
/**
* Internal component for rendering an alert if given password failed
*/
const Alert = ({ attempt }: { attempt: string | undefined }): React.ReactNode => {
if (!attempt || parseInt(attempt) === 1) return null;

return (
// TRANSLATORS: error message, user entered a wrong password
<Alert variant="warning" isInline isPlain title={_("Given encryption password didn't work")} />
<PFAlert
variant="warning"
isInline
isPlain
title={_("Given encryption password didn't work")}
/>
);
};

/**
* Component for rendering questions related to LUKS activation
*
* @param question - the question to be answered
* @param answerCallback - the callback to be triggered on answer
*/
export default function LuksActivationQuestion({ question, answerCallback }) {
const [password, setPassword] = useState(question.password || "");
const conditions = { disable: { decrypt: password === "" } };
const defaultAction = "decrypt";

const actionCallback = (option) => {
const actionCallback = (option: string) => {
question.password = password;
question.answer = option;
answerCallback(question);
Expand All @@ -60,7 +74,7 @@ export default function LuksActivationQuestion({ question, answerCallback }) {
aria-label={_("Question")}
titleIconVariant={() => <Icon name="lock" size="s" />}
>
{renderAlert(parseInt(question.data.attempt))}
<Alert attempt={question.data.attempt} />
<Text>{question.text}</Text>
<Form onSubmit={triggerDefaultAction}>
{/* TRANSLATORS: field label */}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) [2022] SUSE LLC
* Copyright (c) [2022-2024] SUSE LLC
*
* All Rights Reserved.
*
Expand All @@ -23,10 +23,11 @@ import React from "react";
import { screen } from "@testing-library/react";
import { installerRender } from "~/test-utils";
import { QuestionActions } from "~/components/questions";
import { Question } from "~/types/questions";

let defaultOption = "sure";

let question = {
let question: Question = {
id: 1,
text: "Should we use a component for rendering actions?",
options: ["no", "maybe", "sure"],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) [2022] SUSE LLC
* Copyright (c) [2022-2024] SUSE LLC
*
* All Rights Reserved.
*
Expand Down Expand Up @@ -27,11 +27,8 @@ import { Popup } from "~/components/core";
* Returns given text capitalized
*
* TODO: make it work with i18n
*
* @param {String} text - string to be capitalized
* @return {String} capitalized text
*/
const label = (text) => `${text[0].toUpperCase()}${text.slice(1)}`;
const label = (text: string): string => `${text[0].toUpperCase()}${text.slice(1)}`;

/**
* A component for building a Question actions, using the defaultAction
Expand All @@ -42,13 +39,23 @@ const label = (text) => `${text[0].toUpperCase()}${text.slice(1)}`;
* React.Fragment (aka <>) here for wrapping the actions instead of directly using the Popup.Actions.
*
* @param {object} props - component props
* @param {Array.<String>} props.actions - the actions to be shown
* @param {String} [props.defaultAction] - the action to be shown as primary
* @param {function} props.actionCallback - the function to be called when user clicks on action
* @param {Object} [props.conditions={}] - an object holding conditions, like when an action is disabled
* @param props.actions - the actions to be shown
* @param props.defaultAction - the action to be shown as primary
* @param props.actionCallback - the function to be called when user clicks on action
* @param props.conditions={} - an object holding conditions, like when an action is disabled
*/
export default function QuestionActions({ actions, defaultAction, actionCallback, conditions }) {
let [[primaryAction], secondaryActions] = partition(actions, (a) => a === defaultAction);
export default function QuestionActions({
actions,
defaultAction,
actionCallback,
conditions = {},
}: {
actions: string[];
defaultAction?: string;
actionCallback: (action: string) => void;
conditions?: { disable?: { [key: string]: boolean } };
}): React.ReactNode {
let [[primaryAction], secondaryActions] = partition(actions, (a: string) => a === defaultAction);

// Ensure there is always a primary action
if (!primaryAction) [primaryAction, ...secondaryActions] = secondaryActions;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,25 +23,21 @@ import React from "react";
import { screen } from "@testing-library/react";
import { plainRender } from "~/test-utils";
import { QuestionWithPassword } from "~/components/questions";
import { Question } from "~/types/questions";

let question;
const answerFn = jest.fn();
const question: Question = {
id: 1,
class: "question.password",
text: "Random question. Will you provide random password?",
options: ["ok", "cancel"],
defaultOption: "cancel",
};

const renderQuestion = () =>
plainRender(<QuestionWithPassword question={question} answerCallback={answerFn} />);

describe("QuestionWithPassword", () => {
beforeEach(() => {
question = {
id: 1,
class: "question.password",
text: "Random question. Will you provide random password?",
options: ["ok", "cancel"],
defaultOption: "cancel",
data: {},
};
});

it("renders the question text", () => {
renderQuestion();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,30 @@
*/

import React, { useState } from "react";
import { Alert, Form, FormGroup, Text } from "@patternfly/react-core";
import { Form, FormGroup, Text } from "@patternfly/react-core";
import { Icon } from "~/components/layout";
import { PasswordInput, Popup } from "~/components/core";
import { QuestionActions } from "~/components/questions";
import { AnswerCallback, Question } from "~/types/questions";
import { _ } from "~/i18n";

export default function QuestionWithPassword({ question, answerCallback }) {
/**
* Component for rendering questions asking for password
*
* @param question - the question to be answered
* @param answerCallback - the callback to be triggered on answer
*/
export default function QuestionWithPassword({
question,
answerCallback,
}: {
question: Question;
answerCallback: AnswerCallback;
}): React.ReactNode {
const [password, setPassword] = useState(question.password || "");
const defaultAction = question.defaultOption;

const actionCallback = (option) => {
const actionCallback = (option: string) => {
question.password = password;
question.answer = option;
answerCallback(question);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) [2022] SUSE LLC
* Copyright (c) [2022-2024] SUSE LLC
*
* All Rights Reserved.
*
Expand All @@ -23,10 +23,10 @@ import React from "react";
import { screen } from "@testing-library/react";
import { installerRender, plainRender } from "~/test-utils";
import { Questions } from "~/components/questions";
import { QuestionType } from "~/types/questions";
import { Question, QuestionType } from "~/types/questions";
import * as GenericQuestionComponent from "~/components/questions/GenericQuestion";

let mockQuestions;
let mockQuestions: Question[];
const mockMutation = jest.fn();

jest.mock("~/components/questions/LuksActivationQuestion", () => () => (
Expand All @@ -43,15 +43,15 @@ jest.mock("~/queries/questions", () => ({
useQuestionsConfig: () => ({ mutate: mockMutation }),
}));

const genericQuestion = {
const genericQuestion: Question = {
id: 1,
type: QuestionType.generic,
text: "Do you write unit tests?",
options: ["always", "sometimes", "never"],
defaultOption: "sometimes",
};
const passwordQuestion = { id: 1, type: QuestionType.withPassword };
const luksActivationQuestion = { id: 1, class: "storage.luks_activation" };
const passwordQuestion: Question = { id: 1, type: QuestionType.withPassword };
const luksActivationQuestion: Question = { id: 2, class: "storage.luks_activation" };

describe("Questions", () => {
afterEach(() => {
Expand Down
Loading

0 comments on commit 560f953

Please sign in to comment.