Files
formbricks-formbricks/apps/web/lib/response/utils.test.ts
2025-05-08 18:44:09 +00:00

558 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Prisma } from "@prisma/client";
import { describe, expect, test, vi } from "vitest";
import { TResponse } from "@formbricks/types/responses";
import { TSurvey, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
import {
buildWhereClause,
calculateTtcTotal,
extracMetadataKeys,
extractSurveyDetails,
generateAllPermutationsOfSubsets,
getResponseContactAttributes,
getResponseHiddenFields,
getResponseMeta,
getResponsesFileName,
getResponsesJson,
} from "./utils";
describe("Response Utils", () => {
describe("calculateTtcTotal", () => {
test("should calculate total time correctly", () => {
const ttc = {
question1: 10,
question2: 20,
question3: 30,
};
const result = calculateTtcTotal(ttc);
expect(result._total).toBe(60);
});
test("should handle empty ttc object", () => {
const ttc = {};
const result = calculateTtcTotal(ttc);
expect(result._total).toBe(0);
});
});
describe("buildWhereClause", () => {
const mockSurvey: Partial<TSurvey> = {
id: "survey1",
name: "Test Survey",
questions: [
{
id: "q1",
type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
headline: { default: "Question 1" },
required: true,
choices: [
{ id: "1", label: { default: "Option 1" } },
{ id: "other", label: { default: "Other" } },
],
shuffleOption: "none",
isDraft: false,
},
],
type: "app",
hiddenFields: { enabled: true, fieldIds: [] },
createdAt: new Date(),
updatedAt: new Date(),
environmentId: "env1",
createdBy: "user1",
status: "draft",
};
test("should build where clause with finished filter", () => {
const filterCriteria = { finished: true };
const result = buildWhereClause(mockSurvey as TSurvey, filterCriteria);
expect(result.AND).toContainEqual({ finished: true });
});
test("should build where clause with date range", () => {
const filterCriteria = {
createdAt: {
min: new Date("2024-01-01"),
max: new Date("2024-12-31"),
},
};
const result = buildWhereClause(mockSurvey as TSurvey, filterCriteria);
expect(result.AND).toContainEqual({
createdAt: {
gte: new Date("2024-01-01"),
lte: new Date("2024-12-31"),
},
});
});
test("should build where clause with tags", () => {
const filterCriteria = {
tags: {
applied: ["tag1", "tag2"],
notApplied: ["tag3"],
},
};
const result = buildWhereClause(mockSurvey as TSurvey, filterCriteria);
expect(result.AND).toHaveLength(1);
});
test("should build where clause with contact attributes", () => {
const filterCriteria = {
contactAttributes: {
email: { op: "equals" as const, value: "test@example.com" },
},
};
const result = buildWhereClause(mockSurvey as TSurvey, filterCriteria);
expect(result.AND).toHaveLength(1);
});
});
describe("buildWhereClause others & meta filters", () => {
const baseSurvey: Partial<TSurvey> = {
id: "s1",
name: "Survey",
questions: [],
type: "app",
hiddenFields: { enabled: false, fieldIds: [] },
createdAt: new Date(),
updatedAt: new Date(),
environmentId: "e1",
createdBy: "u1",
status: "inProgress",
};
test("others: equals & notEquals", () => {
const criteria = {
others: {
Language: { op: "equals" as const, value: "en" },
Region: { op: "notEquals" as const, value: "APAC" },
},
};
const result = buildWhereClause(baseSurvey as TSurvey, criteria);
expect(result.AND).toEqual([
{
AND: [{ language: "en" }, { region: { not: "APAC" } }],
},
]);
});
test("meta: equals & notEquals map to userAgent paths", () => {
const criteria = {
meta: {
browser: { op: "equals" as const, value: "Chrome" },
os: { op: "notEquals" as const, value: "Windows" },
},
};
const result = buildWhereClause(baseSurvey as TSurvey, criteria);
expect(result.AND).toEqual([
{
AND: [
{ meta: { path: ["userAgent", "browser"], equals: "Chrome" } },
{ meta: { path: ["userAgent", "os"], not: "Windows" } },
],
},
]);
});
});
describe("buildWhereClause datafield filter operations", () => {
const textSurvey: Partial<TSurvey> = {
id: "s2",
name: "TextSurvey",
questions: [
{
id: "qText",
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "Text Q" },
required: false,
isDraft: false,
charLimit: {},
inputType: "text",
},
{
id: "qNum",
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "Num Q" },
required: false,
isDraft: false,
charLimit: {},
inputType: "number",
},
],
type: "app",
hiddenFields: { enabled: false, fieldIds: [] },
createdAt: new Date(),
updatedAt: new Date(),
environmentId: "e2",
createdBy: "u2",
status: "inProgress",
};
const ops: Array<[keyof TSurveyQuestionTypeEnum | string, any, any]> = [
["submitted", { op: "submitted" }, { path: ["qText"], not: Prisma.DbNull }],
["filledOut", { op: "filledOut" }, { path: ["qText"], not: [] }],
["skipped", { op: "skipped" }, "OR"],
["equals", { op: "equals", value: "foo" }, { path: ["qText"], equals: "foo" }],
["notEquals", { op: "notEquals", value: "bar" }, "NOT"],
["lessThan", { op: "lessThan", value: 5 }, { path: ["qNum"], lt: 5 }],
["lessEqual", { op: "lessEqual", value: 10 }, { path: ["qNum"], lte: 10 }],
["greaterThan", { op: "greaterThan", value: 1 }, { path: ["qNum"], gt: 1 }],
["greaterEqual", { op: "greaterEqual", value: 2 }, { path: ["qNum"], gte: 2 }],
[
"includesAll",
{ op: "includesAll", value: ["a", "b"] },
{ path: ["qText"], array_contains: ["a", "b"] },
],
];
ops.forEach(([name, filter, expected]) => {
test(name as string, () => {
const result = buildWhereClause(textSurvey as TSurvey, {
data: {
[["submitted", "filledOut", "equals", "includesAll"].includes(name as string) ? "qText" : "qNum"]:
filter,
},
});
// for OR/NOT cases we just ensure the operator key exists
if (expected === "OR" || expected === "NOT") {
expect(JSON.stringify(result)).toMatch(
new RegExp(name === "skipped" ? `"OR":\\s*\\[` : `"not":"${filter.value}"`)
);
} else {
expect(result.AND).toEqual([
{
AND: [{ data: expected }],
},
]);
}
});
});
test("uploaded & notUploaded", () => {
const res1 = buildWhereClause(textSurvey as TSurvey, { data: { qText: { op: "uploaded" } } });
expect(res1.AND).toContainEqual({
AND: [{ data: { path: ["qText"], not: "skipped" } }],
});
const res2 = buildWhereClause(textSurvey as TSurvey, { data: { qText: { op: "notUploaded" } } });
expect(JSON.stringify(res2)).toMatch(/"equals":"skipped"/);
expect(JSON.stringify(res2)).toMatch(/"equals":{}/);
});
test("clicked, accepted & booked", () => {
["clicked", "accepted", "booked"].forEach((status) => {
const key = status as "clicked" | "accepted" | "booked";
const res = buildWhereClause(textSurvey as TSurvey, { data: { qText: { op: key } } });
expect(res.AND).toEqual([{ AND: [{ data: { path: ["qText"], equals: status } }] }]);
});
});
test("matrix", () => {
const matrixSurvey: Partial<TSurvey> = {
id: "s3",
name: "MatrixSurvey",
questions: [
{
id: "qM",
type: TSurveyQuestionTypeEnum.Matrix,
headline: { default: "Matrix" },
required: false,
rows: [{ default: "R1" }],
columns: [{ default: "C1" }],
shuffleOption: "none",
isDraft: false,
},
],
type: "app",
hiddenFields: { enabled: false, fieldIds: [] },
createdAt: new Date(),
updatedAt: new Date(),
environmentId: "e3",
createdBy: "u3",
status: "inProgress",
};
const res = buildWhereClause(matrixSurvey as TSurvey, {
data: { qM: { op: "matrix", value: { R1: "foo" } } },
});
expect(res.AND).toEqual([
{
AND: [
{
data: { path: ["qM", "R1"], equals: "foo" },
},
],
},
]);
});
});
describe("getResponsesFileName", () => {
test("should generate correct filename", () => {
const surveyName = "Test Survey";
const extension = "csv";
const result = getResponsesFileName(surveyName, extension);
expect(result).toContain("export-test_survey-");
});
});
describe("extracMetadataKeys", () => {
test("should extract metadata keys correctly", () => {
const meta = {
userAgent: { browser: "Chrome", os: "Windows", device: "Desktop" },
country: "US",
source: "direct",
};
const result = extracMetadataKeys(meta);
expect(result).toContain("userAgent - browser");
expect(result).toContain("userAgent - os");
expect(result).toContain("userAgent - device");
expect(result).toContain("country");
expect(result).toContain("source");
});
test("should handle empty metadata", () => {
const result = extracMetadataKeys({});
expect(result).toEqual([]);
});
});
describe("extractSurveyDetails", () => {
const mockSurvey: Partial<TSurvey> = {
id: "survey1",
name: "Test Survey",
questions: [
{
id: "q1",
type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
headline: { default: "Question 1" },
required: true,
choices: [
{ id: "1", label: { default: "Option 1" } },
{ id: "2", label: { default: "Option 2" } },
],
shuffleOption: "none",
isDraft: false,
},
{
id: "q2",
type: TSurveyQuestionTypeEnum.Matrix,
headline: { default: "Matrix Question" },
required: true,
rows: [{ default: "Row 1" }, { default: "Row 2" }],
columns: [{ default: "Column 1" }, { default: "Column 2" }],
shuffleOption: "none",
isDraft: false,
},
],
type: "app",
hiddenFields: { enabled: true, fieldIds: ["hidden1"] },
createdAt: new Date(),
updatedAt: new Date(),
environmentId: "env1",
createdBy: "user1",
status: "draft",
};
const mockResponses: Partial<TResponse>[] = [
{
id: "response1",
surveyId: "survey1",
data: {},
meta: { userAgent: { browser: "Chrome" } },
contactAttributes: { email: "test@example.com" },
finished: true,
createdAt: new Date(),
updatedAt: new Date(),
notes: [],
tags: [],
},
];
test("should extract survey details correctly", () => {
const result = extractSurveyDetails(mockSurvey as TSurvey, mockResponses as TResponse[]);
expect(result.metaDataFields).toContain("userAgent - browser");
expect(result.questions).toHaveLength(2); // 1 regular question + 2 matrix rows
expect(result.hiddenFields).toContain("hidden1");
expect(result.userAttributes).toContain("email");
});
});
describe("getResponsesJson", () => {
const mockSurvey: Partial<TSurvey> = {
id: "survey1",
name: "Test Survey",
questions: [
{
id: "q1",
type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
headline: { default: "Question 1" },
required: true,
choices: [
{ id: "1", label: { default: "Option 1" } },
{ id: "2", label: { default: "Option 2" } },
],
shuffleOption: "none",
isDraft: false,
},
],
type: "app",
hiddenFields: { enabled: true, fieldIds: [] },
createdAt: new Date(),
updatedAt: new Date(),
environmentId: "env1",
createdBy: "user1",
status: "draft",
};
const mockResponses: Partial<TResponse>[] = [
{
id: "response1",
surveyId: "survey1",
data: { q1: "answer1" },
meta: { userAgent: { browser: "Chrome" } },
contactAttributes: { email: "test@example.com" },
finished: true,
createdAt: new Date(),
updatedAt: new Date(),
notes: [],
tags: [],
},
];
test("should generate correct JSON data", () => {
const questionsHeadlines = [["1. Question 1"]];
const userAttributes = ["email"];
const hiddenFields: string[] = [];
const result = getResponsesJson(
mockSurvey as TSurvey,
mockResponses as TResponse[],
questionsHeadlines,
userAttributes,
hiddenFields
);
expect(result[0]["Response ID"]).toBe("response1");
expect(result[0]["userAgent - browser"]).toBe("Chrome");
expect(result[0]["1. Question 1"]).toBe("answer1");
expect(result[0]["email"]).toBe("test@example.com");
});
});
describe("getResponseContactAttributes", () => {
test("should extract contact attributes correctly", () => {
const responses = [
{
contactAttributes: { email: "test1@example.com", name: "Test 1" },
data: {},
meta: {},
},
{
contactAttributes: { email: "test2@example.com", name: "Test 2" },
data: {},
meta: {},
},
];
const result = getResponseContactAttributes(
responses as Pick<TResponse, "contactAttributes" | "data" | "meta">[]
);
expect(result.email).toContain("test1@example.com");
expect(result.email).toContain("test2@example.com");
expect(result.name).toContain("Test 1");
expect(result.name).toContain("Test 2");
});
test("should handle empty responses", () => {
const result = getResponseContactAttributes([]);
expect(result).toEqual({});
});
});
describe("getResponseMeta", () => {
test("should extract meta data correctly", () => {
const responses = [
{
contactAttributes: {},
data: {},
meta: {
userAgent: { browser: "Chrome", os: "Windows" },
country: "US",
},
},
{
contactAttributes: {},
data: {},
meta: {
userAgent: { browser: "Firefox", os: "MacOS" },
country: "UK",
},
},
];
const result = getResponseMeta(responses as Pick<TResponse, "contactAttributes" | "data" | "meta">[]);
expect(result.browser).toContain("Chrome");
expect(result.browser).toContain("Firefox");
expect(result.os).toContain("Windows");
expect(result.os).toContain("MacOS");
});
test("should handle empty responses", () => {
const result = getResponseMeta([]);
expect(result).toEqual({});
});
});
describe("getResponseHiddenFields", () => {
const mockSurvey: Partial<TSurvey> = {
id: "survey1",
name: "Test Survey",
questions: [],
type: "app",
hiddenFields: { enabled: true, fieldIds: ["hidden1", "hidden2"] },
createdAt: new Date(),
updatedAt: new Date(),
environmentId: "env1",
createdBy: "user1",
status: "draft",
};
test("should extract hidden fields correctly", () => {
const responses = [
{
contactAttributes: {},
data: { hidden1: "value1", hidden2: "value2" },
meta: {},
},
{
contactAttributes: {},
data: { hidden1: "value3", hidden2: "value4" },
meta: {},
},
];
const result = getResponseHiddenFields(
mockSurvey as TSurvey,
responses as Pick<TResponse, "contactAttributes" | "data" | "meta">[]
);
expect(result.hidden1).toContain("value1");
expect(result.hidden1).toContain("value3");
expect(result.hidden2).toContain("value2");
expect(result.hidden2).toContain("value4");
});
test("should handle empty responses", () => {
const result = getResponseHiddenFields(mockSurvey as TSurvey, []);
expect(result).toEqual({
hidden1: [],
hidden2: [],
});
});
});
describe("generateAllPermutationsOfSubsets", () => {
test("with empty array returns empty", () => {
expect(generateAllPermutationsOfSubsets([])).toEqual([]);
});
test("with two elements returns 4 permutations", () => {
const out = generateAllPermutationsOfSubsets(["x", "y"]);
expect(out).toEqual(expect.arrayContaining([["x"], ["y"], ["x", "y"], ["y", "x"]]));
expect(out).toHaveLength(4);
});
});
});