diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000000..91aac327de --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,26 @@ +# Testing Instructions + +When generating test files inside the "/app/web" path, follow these rules: + +- Use vitest +- Ensure 100% code coverage +- Add as few comments as possible +- The test file should be located in the same folder as the original file +- Use the `test` function instead of `it` +- Follow the same test pattern used for other files in the package where the file is located +- All imports should be at the top of the file, not inside individual tests +- For mocking inside "test" blocks use "vi.mocked" +- Add the original file path to the "test.coverage.include"array in the "apps/web/vite.config.mts" file +- Don't mock functions that are already mocked in the "apps/web/vitestSetup.ts" file + +If it's a test for a ".tsx" file, follow these extra instructions: + +- Add this code inside the "describe" block and before any test: + +afterEach(() => { + cleanup(); +}); + +- the "afterEach" function should only have "cleanup()" inside it and should be adde to the "vitest" imports +- For click events, import userEvent from "@testing-library/user-event" +- Mock other components that can make the text more complex and but at the same time mocking it wouldn't make the test flaky. It's ok to leave basic and simple components. diff --git a/.vscode/settings.json b/.vscode/settings.json index 22650b0892..10bac75fe3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,9 +1,4 @@ { - "github.copilot.chat.codeGeneration.instructions": [ - { - "text": "When generating tests, always use vitest and use the `test` function instead of `it`." - } - ], "javascript.updateImportsOnFileMove.enabled": "always", "sonarlint.connectedMode.project": { "connectionId": "formbricks", diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/components/ManageIntegration.test.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/components/ManageIntegration.test.tsx new file mode 100644 index 0000000000..df1a9130c9 --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/components/ManageIntegration.test.tsx @@ -0,0 +1,151 @@ +import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions"; +import { cleanup, render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { afterEach, describe, expect, test, vi } from "vitest"; +import { TEnvironment } from "@formbricks/types/environment"; +import { TIntegrationAirtable, TIntegrationAirtableConfig } from "@formbricks/types/integration/airtable"; +import { ManageIntegration } from "./ManageIntegration"; + +vi.mock("@/app/(app)/environments/[environmentId]/integrations/actions", () => ({ + deleteIntegrationAction: vi.fn(), +})); +vi.mock( + "@/app/(app)/environments/[environmentId]/integrations/airtable/components/AddIntegrationModal", + () => ({ + AddIntegrationModal: ({ open, setOpenWithStates }) => + open ? ( +
| + | {columns.map((column) => ( |
|
+ |
|
))}
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MultipleChoiceSummary.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MultipleChoiceSummary.test.tsx
new file mode 100644
index 0000000000..5793f8d1d9
--- /dev/null
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MultipleChoiceSummary.test.tsx
@@ -0,0 +1,275 @@
+import { cleanup, render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { afterEach, describe, expect, test, vi } from "vitest";
+import { MultipleChoiceSummary } from "./MultipleChoiceSummary";
+
+vi.mock("@/modules/ui/components/avatars", () => ({
+ PersonAvatar: ({ personId }: any) => {rowLabel}
@@ -94,7 +94,7 @@ export const MatrixQuestionSummary = ({ questionSummary, survey, setFilter }: Ma
)
}>
{percentage}
-
+
{personId} ,
+}));
+vi.mock("./QuestionSummaryHeader", () => ({ QuestionSummaryHeader: () => }));
+
+describe("MultipleChoiceSummary", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ const baseSurvey = { id: "s1" } as any;
+ const envId = "env";
+
+ test("renders header and choice button", async () => {
+ const setFilter = vi.fn();
+ const q = {
+ question: {
+ id: "q",
+ headline: "H",
+ type: "multipleChoiceSingle",
+ choices: [{ id: "c", label: { default: "C" } }],
+ },
+ choices: { C: { value: "C", count: 1, percentage: 100, others: [] } },
+ type: "multipleChoiceSingle",
+ selectionCount: 0,
+ } as any;
+ render(
+
{results.map((result, resultsIdx) => (
-
- setFilter(
- questionSummary.question.id,
- questionSummary.question.headline,
- questionSummary.question.type,
- questionSummary.type === "multipleChoiceSingle" || otherValue === result.value
- ? t("environments.surveys.summary.includes_either")
- : t("environments.surveys.summary.includes_all"),
- [result.value]
- )
- }>
-
-
- - {results.length - resultsIdx} - {result.value} - -
-
-
- {convertFloatToNDecimal(result.percentage, 2)}%
+
-
+
+
+
{result.others && result.others.length > 0 && (
- e.stopPropagation()}>
+
{t("environments.surveys.summary.other_values_found")}
@@ -124,11 +130,9 @@ export const MultipleChoiceSummary = ({
.filter((otherValue) => otherValue.value !== "")
.slice(0, visibleOtherResponses)
.map((otherValue, idx) => (
-
+
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/NPSSummary.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/NPSSummary.test.tsx
new file mode 100644
index 0000000000..125c4e6754
--- /dev/null
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/NPSSummary.test.tsx
@@ -0,0 +1,60 @@
+import { cleanup, render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { afterEach, describe, expect, test, vi } from "vitest";
+import { TSurveyQuestionSummaryNps } from "@formbricks/types/surveys/types";
+import { NPSSummary } from "./NPSSummary";
+
+vi.mock("@/modules/ui/components/progress-bar", () => ({
+ ProgressBar: ({ progress, barColor }: { progress: number; barColor: string }) => (
+
{surveyType === "link" && (
-
+
+
))}
{otherValue.value}
)}
@@ -139,7 +143,6 @@ export const MultipleChoiceSummary = ({
? `/environments/${environmentId}/contacts/${otherValue.contact.id}`
: { pathname: null }
}
- key={idx}
className="m-2 grid h-16 grid-cols-2 items-center rounded-lg text-sm hover:bg-slate-100">
{otherValue.value}
@@ -163,7 +166,7 @@ export const MultipleChoiceSummary = ({
)}
)}
- {`${progress}-${barColor}`}
+ ),
+ HalfCircle: ({ value }: { value: number }) => {value} ,
+}));
+vi.mock("./QuestionSummaryHeader", () => ({
+ QuestionSummaryHeader: () => ,
+}));
+
+describe("NPSSummary", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ const baseQuestion = { id: "q1", headline: "Question?", type: "nps" as const };
+ const summary = {
+ question: baseQuestion,
+ promoters: { count: 2, percentage: 50 },
+ passives: { count: 1, percentage: 25 },
+ detractors: { count: 1, percentage: 25 },
+ dismissed: { count: 0, percentage: 0 },
+ score: 25,
+ } as unknown as TSurveyQuestionSummaryNps;
+ const survey = {} as any;
+
+ test("renders header, groups, ProgressBar and HalfCircle", () => {
+ render(
+
{["promoters", "passives", "detractors", "dismissed"].map((group) => (
- applyFilter(group)}>
+
}
/>
-
+
{questionSummary.dismissed && questionSummary.dismissed.count > 0 && (
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryMetadata.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryMetadata.test.tsx
new file mode 100644
index 0000000000..6c7b0b63bf
--- /dev/null
+++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryMetadata.test.tsx
@@ -0,0 +1,135 @@
+import { cleanup, render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { useState } from "react";
+import { afterEach, describe, expect, test, vi } from "vitest";
+import { SummaryMetadata } from "./SummaryMetadata";
+
+vi.mock("lucide-react", () => ({
+ ChevronDownIcon: () => ,
+ ChevronUpIcon: () => ,
+}));
+vi.mock("@/modules/ui/components/tooltip", () => ({
+ TooltipProvider: ({ children }) => <>{children}>,
+ Tooltip: ({ children }) => <>{children}>,
+ TooltipTrigger: ({ children }) => <>{children}>,
+ TooltipContent: ({ children }) => <>{children}>,
+}));
+
+const baseSummary = {
+ completedPercentage: 50,
+ completedResponses: 2,
+ displayCount: 3,
+ dropOffPercentage: 25,
+ dropOffCount: 1,
+ startsPercentage: 75,
+ totalResponses: 4,
+ ttcAverage: 65000,
+};
+
+describe("SummaryMetadata", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ test("renders loading skeletons when isLoading=true", () => {
+ const { container } = render(
+
{questionSummary.choices.map((result) => (
-
+
))}
setFilter(
@@ -85,7 +85,7 @@ export const RatingSummary = ({ questionSummary, survey, setFilter }: RatingSumm
@@ -99,9 +101,7 @@ export const SummaryMetadata = ({
setShowDropOffs(!showDropOffs)}
- className="group flex h-full w-full cursor-pointer flex-col justify-between space-y-2 rounded-lg border border-slate-200 bg-white p-4 text-left shadow-sm">
+
@@ -135,6 +135,7 @@ export const SummaryMetadata = ({
{t("environments.surveys.summary.drop_offs")}
{`${Math.round(dropOffPercentage)}%` !== "NaN%" && !isLoading && (
@@ -112,20 +112,20 @@ export const SummaryMetadata = ({
{isLoading ? (
- ) : dropOffCount === 0 ? (
- -
) : (
- dropOffCount
+ displayCountValue
)}
{!isLoading && (
-
+
+
)}
{filterComboBoxValue} + ) : ( +
+ {typeof filterComboBoxValue !== "string" &&
+ filterComboBoxValue?.map((o, index) => (
+
+ ))}
+
+ );
+
+ const commandItemOnSelect = (o: string) => {
+ if (!isMultiple) {
+ onChangeFilterComboBoxValue(typeof o === "object" ? getLocalizedValue(o, defaultLanguageCode) : o);
+ } else {
+ onChangeFilterComboBoxValue(
+ Array.isArray(filterComboBoxValue)
+ ? [...filterComboBoxValue, typeof o === "object" ? getLocalizedValue(o, defaultLanguageCode) : o]
+ : [typeof o === "object" ? getLocalizedValue(o, defaultLanguageCode) : o]
+ );
+ }
+ if (!isMultiple) {
+ setOpen(false);
+ }
+ };
+
return (
{filterOptions && filterOptions?.length <= 1 ? (
@@ -130,39 +163,37 @@ export const QuestionFilterComboBox = ({
)}
!disabled && !isDisabledComboBox && filterValue && setOpen(true)}
className={clsx(
- "group flex items-center justify-between rounded-md rounded-l-none bg-white px-3 py-2 text-sm",
- disabled || isDisabledComboBox || !filterValue ? "opacity-50" : "cursor-pointer"
+ "group flex items-center justify-between rounded-md rounded-l-none bg-white px-3 py-2 text-sm"
)}>
- {filterComboBoxValue && filterComboBoxValue?.length > 0 ? (
- !Array.isArray(filterComboBoxValue) ? (
-
{filterComboBoxValue} - ) : ( -
- {typeof filterComboBoxValue !== "string" &&
- filterComboBoxValue?.map((o, index) => (
-
- ))}
-
- )
+ {filterComboBoxValue && filterComboBoxValue.length > 0 ? (
+ filterComboBoxItem
) : (
- {t("common.select")}... + )} -
+
+
{open && (
@@ -183,21 +214,7 @@ export const QuestionFilterComboBox = ({
{filteredOptions?.map((o, index) => (
+
setOpen(true)}
className="group flex cursor-pointer items-center justify-between rounded-md bg-white px-3 py-2 text-sm">
{!open && selected.hasOwnProperty("label") && (
@@ -174,14 +174,14 @@ export const QuestionsComboBox = ({ options, selected, onChangeValue }: Question
-
{open && (
)}
diff --git a/apps/web/modules/ui/components/progress-bar/index.test.tsx b/apps/web/modules/ui/components/progress-bar/index.test.tsx
new file mode 100644
index 0000000000..6fddfba0b7
--- /dev/null
+++ b/apps/web/modules/ui/components/progress-bar/index.test.tsx
@@ -0,0 +1,105 @@
+import { cleanup, render } from "@testing-library/react";
+import { afterEach, describe, expect, test } from "vitest";
+import { HalfCircle, ProgressBar } from ".";
+
+describe("ProgressBar", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ test("renders with default height and correct progress", () => {
+ const { container } = render( {
@@ -36,7 +36,7 @@ export const LanguageDropdown = ({ survey, setLanguage, locale }: LanguageDropdo
setShowLanguageSelect(false);
}}>
{getLanguageLabel(surveyLanguage.language.code, locale)}
-
+
))}
+
);
};
diff --git a/apps/web/vite.config.mts b/apps/web/vite.config.mts
index 4f41c02d5e..5abfa0b2e9 100644
--- a/apps/web/vite.config.mts
+++ b/apps/web/vite.config.mts
@@ -35,6 +35,7 @@ export default defineConfig({
"modules/organization/settings/teams/components/edit-memberships/organization-actions.tsx",
"modules/ui/components/alert/*.tsx",
"modules/ui/components/environmentId-base-layout/*.tsx",
+ "modules/ui/components/progress-bar/index.tsx",
"app/(app)/environments/**/layout.tsx",
"app/(app)/environments/**/settings/(organization)/general/page.tsx",
"app/(app)/environments/**/components/PosthogIdentify.tsx",
@@ -47,6 +48,20 @@ export default defineConfig({
"app/intercom/*.tsx",
"app/sentry/*.tsx",
"app/(app)/environments/**/surveys/**/(analysis)/summary/components/SurveyAnalysisCTA.tsx",
+ "app/(app)/environments/**/surveys/**/(analysis)/summary/components/ConsentSummary.tsx",
+ "app/(app)/environments/**/surveys/**/(analysis)/summary/components/MatrixQuestionSummary.tsx",
+ "app/(app)/environments/**/surveys/**/(analysis)/summary/components/MultipleChoiceSummary.tsx",
+ "app/(app)/environments/**/surveys/**/(analysis)/summary/components/NPSSummary.tsx",
+ "app/(app)/environments/**/surveys/**/(analysis)/summary/components/PictureChoiceSummary.tsx",
+ "app/(app)/environments/**/surveys/**/(analysis)/summary/components/RatingSummary.tsx",
+ "app/(app)/environments/**/surveys/**/(analysis)/summary/components/SummaryMetadata.tsx",
+ "app/(app)/environments/**/surveys/**/components/QuestionFilterComboBox.tsx",
+ "app/(app)/environments/**/surveys/**/components/QuestionsComboBox.tsx",
+ "app/(app)/environments/**/integrations/airtable/components/ManageIntegration.tsx",
+ "app/(app)/environments/**/integrations/google-sheets/components/ManageIntegration.tsx",
+ "apps/web/app/(app)/environments/**/integrations/notion/components/ManageIntegration.tsx",
+ "app/(app)/environments/**/integrations/slack/components/ManageIntegration.tsx",
+ "app/(app)/environments/**/surveys/**/(analysis)/responses/components/ResponseTableCell.tsx",
"modules/ee/sso/lib/**/*.ts",
"app/lib/**/*.ts",
"app/api/(internal)/insights/lib/**/*.ts",
@@ -75,8 +90,9 @@ export default defineConfig({
"modules/analysis/**/*.tsx",
"modules/analysis/**/*.ts",
"modules/survey/editor/components/end-screen-form.tsx",
+ "lib/utils/billing.ts",
"lib/crypto.ts",
- "lib/utils/billing.ts"
+ "lib/utils/billing.ts",
],
exclude: [
"**/.next/**",
+ style={{ width: `${maxWidth}%`, transition: "width 0.5s ease-out" }}>
|
|---|