Files
formbricks-formbricks/apps/web/lib/utils/recall.test.ts
2025-10-17 05:47:17 +00:00

509 lines
17 KiB
TypeScript

import { describe, expect, test, vi } from "vitest";
import { TResponseData, TResponseVariables } from "@formbricks/types/responses";
import { TSurvey, TSurveyQuestion, TSurveyRecallItem } from "@formbricks/types/surveys/types";
import { structuredClone } from "@/lib/pollyfills/structuredClone";
import {
checkForEmptyFallBackValue,
extractFallbackValue,
extractId,
extractIds,
extractRecallInfo,
fallbacks,
findRecallInfoById,
getFallbackValues,
getRecallItems,
headlineToRecall,
parseRecallInfo,
recallToHeadline,
replaceHeadlineRecall,
replaceRecallInfoWithUnderline,
} from "./recall";
// Mock dependencies
vi.mock("@/lib/i18n/utils", () => ({
getLocalizedValue: (obj: any, lang: string) => {
if (typeof obj === "string") return obj;
if (!obj) return "";
return obj[lang] || obj["default"] || "";
},
}));
vi.mock("@/lib/pollyfills/structuredClone", () => ({
structuredClone: vi.fn((obj) => JSON.parse(JSON.stringify(obj))),
}));
vi.mock("@/lib/utils/datetime", () => ({
isValidDateString: vi.fn((value) => {
try {
return !isNaN(new Date(value as string).getTime());
} catch {
return false;
}
}),
formatDateWithOrdinal: vi.fn((date) => {
return "January 1st, 2023";
}),
}));
describe("recall utility functions", () => {
describe("extractId", () => {
test("extracts ID correctly from a string with recall pattern", () => {
const text = "This is a #recall:question123 example";
const result = extractId(text);
expect(result).toBe("question123");
});
test("returns null when no ID is found", () => {
const text = "This has no recall pattern";
const result = extractId(text);
expect(result).toBeNull();
});
test("returns null for malformed recall pattern", () => {
const text = "This is a #recall: malformed pattern";
const result = extractId(text);
expect(result).toBeNull();
});
});
describe("extractIds", () => {
test("extracts multiple IDs from a string with multiple recall patterns", () => {
const text = "This has #recall:id1 and #recall:id2 and #recall:id3";
const result = extractIds(text);
expect(result).toEqual(["id1", "id2", "id3"]);
});
test("returns empty array when no IDs are found", () => {
const text = "This has no recall patterns";
const result = extractIds(text);
expect(result).toEqual([]);
});
test("handles mixed content correctly", () => {
const text = "Text #recall:id1 more text #recall:id2";
const result = extractIds(text);
expect(result).toEqual(["id1", "id2"]);
});
});
describe("extractFallbackValue", () => {
test("extracts fallback value correctly", () => {
const text = "Text #recall:id1/fallback:defaultValue# more text";
const result = extractFallbackValue(text);
expect(result).toBe("defaultValue");
});
test("returns empty string when no fallback value is found", () => {
const text = "Text with no fallback";
const result = extractFallbackValue(text);
expect(result).toBe("");
});
test("handles empty fallback value", () => {
const text = "Text #recall:id1/fallback:# more text";
const result = extractFallbackValue(text);
expect(result).toBe("");
});
});
describe("extractRecallInfo", () => {
test("extracts complete recall info from text", () => {
const text = "This is #recall:id1/fallback:default# text";
const result = extractRecallInfo(text);
expect(result).toBe("#recall:id1/fallback:default#");
});
test("returns null when no recall info is found", () => {
const text = "This has no recall info";
const result = extractRecallInfo(text);
expect(result).toBeNull();
});
test("extracts recall info for a specific ID when provided", () => {
const text = "This has #recall:id1/fallback:default1# and #recall:id2/fallback:default2#";
const result = extractRecallInfo(text, "id2");
expect(result).toBe("#recall:id2/fallback:default2#");
});
});
describe("findRecallInfoById", () => {
test("finds recall info by ID", () => {
const text = "Text #recall:id1/fallback:value1# and #recall:id2/fallback:value2#";
const result = findRecallInfoById(text, "id2");
expect(result).toBe("#recall:id2/fallback:value2#");
});
test("returns null when ID is not found", () => {
const text = "Text #recall:id1/fallback:value1#";
const result = findRecallInfoById(text, "id2");
expect(result).toBeNull();
});
});
describe("recallToHeadline", () => {
test("converts recall pattern to headline format without slash", () => {
const headline = { en: "How do you like #recall:product/fallback:ournbspproduct#?" };
const survey = {
id: "test-survey",
questions: [{ id: "product", headline: { en: "Product Question" } }],
hiddenFields: { fieldIds: [] },
variables: [],
} as any;
const result = recallToHeadline(headline, survey, false, "en");
expect(result.en).toBe("How do you like @Product Question?");
});
test("converts recall pattern to headline format with slash", () => {
const headline = { en: "Rate #recall:product/fallback:ournbspproduct#" };
const survey = {
id: "test-survey",
questions: [{ id: "product", headline: { en: "Product Question" } }],
hiddenFields: { fieldIds: [] },
variables: [],
} as any;
const result = recallToHeadline(headline, survey, true, "en");
expect(result.en).toBe("Rate /Product Question\\");
});
test("handles hidden fields in recall", () => {
const headline = { en: "Your email is #recall:email/fallback:notnbspprovided#" };
const survey: TSurvey = {
id: "test-survey",
questions: [],
hiddenFields: { fieldIds: ["email"] },
variables: [],
} as unknown as TSurvey;
const result = recallToHeadline(headline, survey, false, "en");
expect(result.en).toBe("Your email is @email");
});
test("handles variables in recall", () => {
const headline = { en: "Your plan is #recall:plan/fallback:unknown#" };
const survey: TSurvey = {
id: "test-survey",
questions: [],
hiddenFields: { fieldIds: [] },
variables: [{ id: "plan", name: "Subscription Plan" }],
} as unknown as TSurvey;
const result = recallToHeadline(headline, survey, false, "en");
expect(result.en).toBe("Your plan is @Subscription Plan");
});
test("returns unchanged headline when no recall pattern is found", () => {
const headline = { en: "Regular headline with no recall" };
const survey = {} as TSurvey;
const result = recallToHeadline(headline, survey, false, "en");
expect(result).toEqual(headline);
});
test("handles nested recall patterns", () => {
const headline = {
en: "This is #recall:inner/fallback:fallback2#",
};
const survey = {
id: "test-survey",
questions: [{ id: "inner", headline: { en: "Inner with @outer" } }],
hiddenFields: { fieldIds: [] },
variables: [],
} as any;
const result = recallToHeadline(headline, survey, false, "en");
expect(result.en).toBe("This is @Inner with @outer");
});
});
describe("replaceRecallInfoWithUnderline", () => {
test("replaces recall info with underline", () => {
const text = "This is a #recall:id1/fallback:default# example";
const result = replaceRecallInfoWithUnderline(text);
expect(result).toBe("This is a ___ example");
});
test("replaces multiple recall infos with underlines", () => {
const text = "This #recall:id1/fallback:v1# has #recall:id2/fallback:v2# multiple recalls";
const result = replaceRecallInfoWithUnderline(text);
expect(result).toBe("This ___ has ___ multiple recalls");
});
test("returns unchanged text when no recall info is present", () => {
const text = "This has no recall info";
const result = replaceRecallInfoWithUnderline(text);
expect(result).toBe(text);
});
});
describe("checkForEmptyFallBackValue", () => {
test("identifies question with empty fallback value", () => {
const questionHeadline = { en: "Question with #recall:id1/fallback:# empty fallback" };
const survey = {
questions: [
{
id: "q1",
headline: questionHeadline,
},
],
} as any;
const result = checkForEmptyFallBackValue(survey, "en");
expect(result).toBe(survey.questions[0]);
});
test("identifies question with empty fallback in subheader", () => {
const questionSubheader = { en: "Subheader with #recall:id1/fallback:# empty fallback" };
const survey = {
questions: [
{
id: "q1",
headline: { en: "Normal question" },
subheader: questionSubheader,
},
],
} as any;
const result = checkForEmptyFallBackValue(survey, "en");
expect(result).toBe(survey.questions[0]);
});
test("returns null when no empty fallback values are found", () => {
const questionHeadline = { en: "Question with #recall:id1/fallback:default# valid fallback" };
const survey = {
questions: [
{
id: "q1",
headline: questionHeadline,
},
],
} as any;
const result = checkForEmptyFallBackValue(survey, "en");
expect(result).toBeNull();
});
});
describe("replaceHeadlineRecall", () => {
test("processes all questions in a survey", () => {
const survey: TSurvey = {
questions: [
{
id: "q1",
headline: { en: "Question with #recall:id1/fallback:default#" },
},
{
id: "q2",
headline: { en: "Another with #recall:id2/fallback:other#" },
},
] as unknown as TSurveyQuestion[],
hiddenFields: { fieldIds: [] },
variables: [],
} as unknown as TSurvey;
vi.mocked(structuredClone).mockImplementation((obj) => JSON.parse(JSON.stringify(obj)));
const result = replaceHeadlineRecall(survey, "en");
// Verify recallToHeadline was called for each question
expect(result).not.toBe(survey); // Should be a clone
expect(result.questions[0].headline).not.toEqual(survey.questions[0].headline);
expect(result.questions[1].headline).not.toEqual(survey.questions[1].headline);
});
});
describe("getRecallItems", () => {
test("extracts recall items from text", () => {
const text = "Text with #recall:id1/fallback:val1# and #recall:id2/fallback:val2#";
const survey: TSurvey = {
questions: [
{ id: "id1", headline: { en: "Question One" } },
{ id: "id2", headline: { en: "Question Two" } },
] as unknown as TSurveyQuestion[],
hiddenFields: { fieldIds: [] },
variables: [],
} as unknown as TSurvey;
const result = getRecallItems(text, survey, "en");
expect(result).toHaveLength(2);
expect(result[0].id).toBe("id1");
expect(result[0].label).toBe("Question One");
expect(result[0].type).toBe("question");
expect(result[1].id).toBe("id2");
expect(result[1].label).toBe("Question Two");
expect(result[1].type).toBe("question");
});
test("handles hidden fields in recall items", () => {
const text = "Text with #recall:hidden1/fallback:val1#";
const survey: TSurvey = {
questions: [],
hiddenFields: { fieldIds: ["hidden1"] },
variables: [],
} as unknown as TSurvey;
const result = getRecallItems(text, survey, "en");
expect(result).toHaveLength(1);
expect(result[0].id).toBe("hidden1");
expect(result[0].type).toBe("hiddenField");
});
test("handles variables in recall items", () => {
const text = "Text with #recall:var1/fallback:val1#";
const survey: TSurvey = {
questions: [],
hiddenFields: { fieldIds: [] },
variables: [{ id: "var1", name: "Variable One" }],
} as unknown as TSurvey;
const result = getRecallItems(text, survey, "en");
expect(result).toHaveLength(1);
expect(result[0].id).toBe("var1");
expect(result[0].label).toBe("Variable One");
expect(result[0].type).toBe("variable");
});
test("returns empty array when no recall items are found", () => {
const text = "Text with no recall items";
const survey: TSurvey = {} as TSurvey;
const result = getRecallItems(text, survey, "en");
expect(result).toEqual([]);
});
});
describe("getFallbackValues", () => {
test("extracts fallback values from text", () => {
const text = "Text #recall:id1/fallback:value1# and #recall:id2/fallback:value2#";
const result = getFallbackValues(text);
expect(result).toEqual({
id1: "value1",
id2: "value2",
});
});
test("returns empty object when no fallback values are found", () => {
const text = "Text with no fallback values";
const result = getFallbackValues(text);
expect(result).toEqual({});
});
});
describe("headlineToRecall", () => {
test("transforms headlines to recall info", () => {
const text = "What do you think of @Product?";
const recallItems: TSurveyRecallItem[] = [{ id: "product", label: "Product", type: "question" }];
const fallbacks: fallbacks = {
product: "our product",
};
const result = headlineToRecall(text, recallItems, fallbacks);
expect(result).toBe("What do you think of #recall:product/fallback:our product#?");
});
test("transforms multiple headlines", () => {
const text = "Rate @Product made by @Company";
const recallItems: TSurveyRecallItem[] = [
{ id: "product", label: "Product", type: "question" },
{ id: "company", label: "Company", type: "question" },
];
const fallbacks: fallbacks = {
product: "our product",
company: "our company",
};
const result = headlineToRecall(text, recallItems, fallbacks);
expect(result).toBe(
"Rate #recall:product/fallback:our product# made by #recall:company/fallback:our company#"
);
});
});
describe("parseRecallInfo", () => {
test("replaces recall info with response data", () => {
const text = "Your answer was #recall:q1/fallback:not-provided#";
const responseData: TResponseData = {
q1: "Yes definitely",
};
const result = parseRecallInfo(text, responseData);
expect(result).toBe("Your answer was Yes definitely");
});
test("uses fallback when response data is missing", () => {
const text = "Your answer was #recall:q1/fallback:notnbspprovided#";
const responseData: TResponseData = {
q2: "Some other answer",
};
const result = parseRecallInfo(text, responseData);
expect(result).toBe("Your answer was not provided");
});
test("formats date values", () => {
const text = "You joined on #recall:joinDate/fallback:an-unknown-date#";
const responseData: TResponseData = {
joinDate: "2023-01-01",
};
const result = parseRecallInfo(text, responseData);
expect(result).toBe("You joined on January 1st, 2023");
});
test("formats array values as comma-separated list", () => {
const text = "Your selections: #recall:preferences/fallback:none#";
const responseData: TResponseData = {
preferences: ["Option A", "Option B", "Option C"],
};
const result = parseRecallInfo(text, responseData);
expect(result).toBe("Your selections: Option A, Option B, Option C");
});
test("uses variables when available", () => {
const text = "Welcome back, #recall:username/fallback:user#";
const variables: TResponseVariables = {
username: "John Doe",
};
const result = parseRecallInfo(text, {}, variables);
expect(result).toBe("Welcome back, John Doe");
});
test("prioritizes variables over response data", () => {
const text = "Your email is #recall:email/fallback:no-email#";
const responseData: TResponseData = {
email: "response@example.com",
};
const variables: TResponseVariables = {
email: "variable@example.com",
};
const result = parseRecallInfo(text, responseData, variables);
expect(result).toBe("Your email is variable@example.com");
});
test("handles withSlash parameter", () => {
const text = "Your name is #recall:name/fallback:anonymous#";
const variables: TResponseVariables = {
name: "John Doe",
};
const result = parseRecallInfo(text, {}, variables, true);
expect(result).toBe("Your name is #/John Doe\\#");
});
test("handles 'nbsp' in fallback values", () => {
const text = "Default spacing: #recall:space/fallback:nonnbspbreaking#";
const result = parseRecallInfo(text);
expect(result).toBe("Default spacing: non breaking");
});
});
});