forked from calcom/cal.diy
-
Notifications
You must be signed in to change notification settings - Fork 0
test: Add QA documentation, unit tests, coverage report and traceability matrix #58
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
e0f7f1e
Add comprehensive QA documentation for Cal.com
devin-ai-integration[bot] d5e5232
docs: Expand BRD with detailed business context and add test case sum…
devin-ai-integration[bot] b4b8c9e
test: Add unit tests, coverage report, and traceability matrix
devin-ai-integration[bot] File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
48 changes: 48 additions & 0 deletions
48
packages/features/eventtypes/lib/getDefinedBufferTimes.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| import { describe, expect, it } from "vitest"; | ||
|
|
||
| import { getDefinedBufferTimes } from "./getDefinedBufferTimes"; | ||
|
|
||
| describe("getDefinedBufferTimes", () => { | ||
| it("should return an array of predefined buffer times", () => { | ||
| const bufferTimes = getDefinedBufferTimes(); | ||
| expect(Array.isArray(bufferTimes)).toBe(true); | ||
| expect(bufferTimes.length).toBeGreaterThan(0); | ||
| }); | ||
|
|
||
| it("should contain all expected buffer time values", () => { | ||
| const bufferTimes = getDefinedBufferTimes(); | ||
| expect(bufferTimes).toEqual([5, 10, 15, 20, 30, 45, 60, 90, 120]); | ||
| }); | ||
|
|
||
| it("should return buffer times in ascending order", () => { | ||
| const bufferTimes = getDefinedBufferTimes(); | ||
| for (let i = 1; i < bufferTimes.length; i++) { | ||
| expect(bufferTimes[i]).toBeGreaterThan(bufferTimes[i - 1]); | ||
| } | ||
| }); | ||
|
|
||
| it("should include common meeting buffer durations (5, 10, 15, 30 mins)", () => { | ||
| const bufferTimes = getDefinedBufferTimes(); | ||
| expect(bufferTimes).toContain(5); | ||
| expect(bufferTimes).toContain(10); | ||
| expect(bufferTimes).toContain(15); | ||
| expect(bufferTimes).toContain(30); | ||
| }); | ||
|
|
||
| it("should have maximum buffer time of 120 minutes", () => { | ||
| const bufferTimes = getDefinedBufferTimes(); | ||
| expect(bufferTimes[bufferTimes.length - 1]).toBe(120); | ||
| }); | ||
|
|
||
| it("should have minimum buffer time of 5 minutes", () => { | ||
| const bufferTimes = getDefinedBufferTimes(); | ||
| expect(bufferTimes[0]).toBe(5); | ||
| }); | ||
|
|
||
| it("should return all positive numbers", () => { | ||
| const bufferTimes = getDefinedBufferTimes(); | ||
| bufferTimes.forEach((time) => { | ||
| expect(time).toBeGreaterThan(0); | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| import { describe, expect, it } from "vitest"; | ||
|
|
||
| import { | ||
| DEFAULT_SCHEDULE, | ||
| getAvailabilityFromSchedule, | ||
| MINUTES_DAY_END, | ||
| MINUTES_DAY_START, | ||
| MINUTES_IN_DAY, | ||
| } from "./availability"; | ||
|
|
||
| describe("availability", () => { | ||
| describe("DEFAULT_SCHEDULE", () => { | ||
| it("should have 7 days (Sunday-Saturday)", () => { | ||
| expect(DEFAULT_SCHEDULE).toHaveLength(7); | ||
| }); | ||
|
|
||
| it("should have empty arrays for Sunday (index 0) and Saturday (index 6)", () => { | ||
| expect(DEFAULT_SCHEDULE[0]).toEqual([]); | ||
| expect(DEFAULT_SCHEDULE[6]).toEqual([]); | ||
| }); | ||
|
|
||
| it("should have working hours for Monday-Friday", () => { | ||
| for (let i = 1; i <= 5; i++) { | ||
| expect(DEFAULT_SCHEDULE[i]).toHaveLength(1); | ||
| expect(DEFAULT_SCHEDULE[i][0]).toHaveProperty("start"); | ||
| expect(DEFAULT_SCHEDULE[i][0]).toHaveProperty("end"); | ||
| } | ||
| }); | ||
| }); | ||
|
|
||
| describe("constants", () => { | ||
| it("should define MINUTES_IN_DAY as 1440", () => { | ||
| expect(MINUTES_IN_DAY).toBe(1440); | ||
| }); | ||
|
|
||
| it("should define MINUTES_DAY_END as 1439", () => { | ||
| expect(MINUTES_DAY_END).toBe(1439); | ||
| }); | ||
|
|
||
| it("should define MINUTES_DAY_START as 0", () => { | ||
| expect(MINUTES_DAY_START).toBe(0); | ||
| }); | ||
| }); | ||
|
|
||
| describe("getAvailabilityFromSchedule", () => { | ||
| it("should return empty array for empty schedule", () => { | ||
| const schedule = [[], [], [], [], [], [], []]; | ||
| const result = getAvailabilityFromSchedule(schedule); | ||
| expect(result).toEqual([]); | ||
| }); | ||
|
|
||
| it("should merge days with same time ranges", () => { | ||
| const timeRange = { | ||
| start: new Date("2024-01-01T09:00:00Z"), | ||
| end: new Date("2024-01-01T17:00:00Z"), | ||
| }; | ||
| const schedule = [[], [timeRange], [timeRange], [timeRange], [timeRange], [timeRange], []]; | ||
| const result = getAvailabilityFromSchedule(schedule); | ||
| expect(result).toHaveLength(1); | ||
| expect(result[0].days).toEqual([1, 2, 3, 4, 5]); | ||
| }); | ||
|
|
||
| it("should keep separate entries for different time ranges", () => { | ||
| const morningRange = { | ||
| start: new Date("2024-01-01T08:00:00Z"), | ||
| end: new Date("2024-01-01T12:00:00Z"), | ||
| }; | ||
| const afternoonRange = { | ||
| start: new Date("2024-01-01T13:00:00Z"), | ||
| end: new Date("2024-01-01T17:00:00Z"), | ||
| }; | ||
| const schedule = [[], [morningRange, afternoonRange], [], [], [], [], []]; | ||
| const result = getAvailabilityFromSchedule(schedule); | ||
| expect(result).toHaveLength(2); | ||
| }); | ||
|
|
||
| it("should handle schedule with only weekend availability", () => { | ||
| const timeRange = { | ||
| start: new Date("2024-01-01T10:00:00Z"), | ||
| end: new Date("2024-01-01T14:00:00Z"), | ||
| }; | ||
| const schedule = [[timeRange], [], [], [], [], [], [timeRange]]; | ||
| const result = getAvailabilityFromSchedule(schedule); | ||
| expect(result).toHaveLength(1); | ||
| expect(result[0].days).toEqual([0, 6]); | ||
| }); | ||
|
|
||
| it("should handle single day availability", () => { | ||
| const timeRange = { | ||
| start: new Date("2024-01-01T09:00:00Z"), | ||
| end: new Date("2024-01-01T17:00:00Z"), | ||
| }; | ||
| const schedule = [[], [timeRange], [], [], [], [], []]; | ||
| const result = getAvailabilityFromSchedule(schedule); | ||
| expect(result).toHaveLength(1); | ||
| expect(result[0].days).toEqual([1]); | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| import { describe, expect, it } from "vitest"; | ||
|
|
||
| import { contructEmailFromPhoneNumber } from "./contructEmailFromPhoneNumber"; | ||
|
|
||
| describe("contructEmailFromPhoneNumber", () => { | ||
| it("should construct email from phone number with country code", () => { | ||
| expect(contructEmailFromPhoneNumber("+1234567890")).toBe("1234567890@sms.cal.com"); | ||
| }); | ||
|
|
||
| it("should construct email from phone number without plus sign", () => { | ||
| expect(contructEmailFromPhoneNumber("1234567890")).toBe("1234567890@sms.cal.com"); | ||
| }); | ||
|
|
||
| it("should remove multiple plus signs", () => { | ||
| expect(contructEmailFromPhoneNumber("++1234567890")).toBe("1234567890@sms.cal.com"); | ||
| }); | ||
|
|
||
| it("should handle international phone numbers", () => { | ||
| expect(contructEmailFromPhoneNumber("+44123456789")).toBe("44123456789@sms.cal.com"); | ||
| expect(contructEmailFromPhoneNumber("+919876543210")).toBe("919876543210@sms.cal.com"); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| import { describe, expect, it } from "vitest"; | ||
|
|
||
| import convertToNewDurationType from "./convertToNewDurationType"; | ||
|
|
||
| describe("convertToNewDurationType", () => { | ||
| describe("minutes conversions", () => { | ||
| it("should return same value for minutes to minutes", () => { | ||
| expect(convertToNewDurationType("minutes", "minutes", 30)).toBe(30); | ||
| expect(convertToNewDurationType("minutes", "minutes", 60)).toBe(60); | ||
| }); | ||
|
|
||
| it("should convert minutes to hours", () => { | ||
| expect(convertToNewDurationType("minutes", "hours", 60)).toBe(1); | ||
| expect(convertToNewDurationType("minutes", "hours", 120)).toBe(2); | ||
| expect(convertToNewDurationType("minutes", "hours", 90)).toBe(2); | ||
| }); | ||
|
|
||
| it("should convert minutes to days", () => { | ||
| expect(convertToNewDurationType("minutes", "days", 1440)).toBe(1); | ||
| expect(convertToNewDurationType("minutes", "days", 2880)).toBe(2); | ||
| expect(convertToNewDurationType("minutes", "days", 1500)).toBe(2); | ||
| }); | ||
| }); | ||
|
|
||
| describe("hours conversions", () => { | ||
| it("should convert hours to minutes", () => { | ||
| expect(convertToNewDurationType("hours", "minutes", 1)).toBe(60); | ||
| expect(convertToNewDurationType("hours", "minutes", 2)).toBe(120); | ||
| }); | ||
|
|
||
| it("should return same value for hours to hours", () => { | ||
| expect(convertToNewDurationType("hours", "hours", 5)).toBe(5); | ||
| }); | ||
|
|
||
| it("should convert hours to days (multiplies by HOURS_IN_DAY)", () => { | ||
| expect(convertToNewDurationType("hours", "days", 1)).toBe(24); | ||
| expect(convertToNewDurationType("hours", "days", 2)).toBe(48); | ||
| }); | ||
| }); | ||
|
|
||
| describe("days conversions", () => { | ||
| it("should convert days to minutes", () => { | ||
| expect(convertToNewDurationType("days", "minutes", 1)).toBe(1440); | ||
| expect(convertToNewDurationType("days", "minutes", 2)).toBe(2880); | ||
| }); | ||
|
|
||
| it("should convert days to hours", () => { | ||
| expect(convertToNewDurationType("days", "hours", 1)).toBe(24); | ||
| expect(convertToNewDurationType("days", "hours", 2)).toBe(48); | ||
| }); | ||
|
|
||
| it("should return same value for days to days", () => { | ||
| expect(convertToNewDurationType("days", "days", 3)).toBe(3); | ||
| }); | ||
| }); | ||
|
|
||
| describe("ceiling behavior", () => { | ||
| it("should ceil fractional results", () => { | ||
| expect(convertToNewDurationType("minutes", "hours", 45)).toBe(1); | ||
| expect(convertToNewDurationType("minutes", "hours", 61)).toBe(2); | ||
| expect(convertToNewDurationType("minutes", "days", 1)).toBe(1); | ||
| }); | ||
| }); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| import { describe, expect, it } from "vitest"; | ||
|
|
||
| import { objectsToCsv, sanitizeValue } from "./csvUtils"; | ||
|
|
||
| describe("csvUtils", () => { | ||
| describe("sanitizeValue", () => { | ||
| it("should return plain values unchanged", () => { | ||
| expect(sanitizeValue("hello")).toBe("hello"); | ||
| expect(sanitizeValue("123")).toBe("123"); | ||
| }); | ||
|
|
||
| it("should wrap values with commas in quotes", () => { | ||
| expect(sanitizeValue("hello,world")).toBe('"hello,world"'); | ||
| }); | ||
|
|
||
| it("should wrap values with newlines in quotes", () => { | ||
| expect(sanitizeValue("hello\nworld")).toBe('"hello\nworld"'); | ||
| }); | ||
|
|
||
| it("should double-escape quotes and wrap in quotes", () => { | ||
| expect(sanitizeValue('say "hello"')).toBe('"say ""hello"""'); | ||
| }); | ||
|
|
||
| it("should handle empty string", () => { | ||
| expect(sanitizeValue("")).toBe(""); | ||
| }); | ||
| }); | ||
|
|
||
| describe("objectsToCsv", () => { | ||
| it("should convert array of objects to CSV string", () => { | ||
| const data = [ | ||
| { name: "John", age: "30", city: "NYC" }, | ||
| { name: "Jane", age: "25", city: "LA" }, | ||
| ]; | ||
| const result = objectsToCsv(data); | ||
| expect(result).toBe("name,age,city\nJohn,30,NYC\nJane,25,LA"); | ||
| }); | ||
|
|
||
| it("should return empty string for empty array", () => { | ||
| expect(objectsToCsv([])).toBe(""); | ||
| }); | ||
|
|
||
| it("should handle values with special characters", () => { | ||
| const data = [{ name: 'John "Jr"', note: "hello,world" }]; | ||
| const result = objectsToCsv(data); | ||
| expect(result).toContain('"John ""Jr"""'); | ||
| expect(result).toContain('"hello,world"'); | ||
| }); | ||
|
|
||
| it("should handle single row", () => { | ||
| const data = [{ id: "1", status: "active" }]; | ||
| const result = objectsToCsv(data); | ||
| expect(result).toBe("id,status\n1,active"); | ||
| }); | ||
|
|
||
| it("should handle undefined values", () => { | ||
| const data = [{ name: "John", value: undefined }] as Record<string, unknown>[]; | ||
| const result = objectsToCsv(data); | ||
| expect(result).toContain("name,value"); | ||
| expect(result).toContain("John,"); | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| import { describe, expect, it } from "vitest"; | ||
|
|
||
| import { getPlaceholderAvatar } from "./defaultAvatarImage"; | ||
|
|
||
| describe("getPlaceholderAvatar", () => { | ||
| it("should return the avatar URL if provided", () => { | ||
| const avatar = "https://example.com/avatar.png"; | ||
| expect(getPlaceholderAvatar(avatar, "John Doe")).toBe(avatar); | ||
| }); | ||
|
|
||
| it("should return placeholder URL when avatar is null", () => { | ||
| const result = getPlaceholderAvatar(null, "John Doe"); | ||
| expect(result).toContain("ui-avatars.com"); | ||
| expect(result).toContain("John%20Doe"); | ||
| }); | ||
|
|
||
| it("should return placeholder URL when avatar is undefined", () => { | ||
| const result = getPlaceholderAvatar(undefined, "Jane"); | ||
| expect(result).toContain("ui-avatars.com"); | ||
| expect(result).toContain("Jane"); | ||
| }); | ||
|
|
||
| it("should handle null name with placeholder", () => { | ||
| const result = getPlaceholderAvatar(null, null); | ||
| expect(result).toContain("ui-avatars.com"); | ||
| expect(result).toContain("name="); | ||
| }); | ||
|
|
||
| it("should handle undefined name with placeholder", () => { | ||
| const result = getPlaceholderAvatar(null, undefined); | ||
| expect(result).toContain("ui-avatars.com"); | ||
| }); | ||
|
|
||
| it("should encode special characters in name", () => { | ||
| const result = getPlaceholderAvatar(null, "John & Jane"); | ||
| expect(result).toContain("ui-avatars.com"); | ||
| expect(result).toContain(encodeURIComponent("John & Jane")); | ||
| }); | ||
|
|
||
| it("should return avatar URL even if it is an empty string", () => { | ||
| expect(getPlaceholderAvatar("", "John")).toContain("ui-avatars.com"); | ||
| }); | ||
| }); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Test encodes incorrect hours-to-days conversion logic (pre-existing source bug)
The test at lines 35-38 asserts that converting 1 hour to days yields 24, and 2 hours to days yields 48. This is mathematically wrong: 1 hour = 1/24 of a day ≈ 0.042 days (ceil → 1 day), not 24 days.
Root Cause: Source code multiplies instead of dividing for hours→days
The source code at
packages/lib/convertToNewDurationType.ts:19defines:This multiplies hours by 24 to get "days", but the correct conversion is to divide hours by 24 (or equivalently, convert hours to minutes first, then divide by minutes-per-day):
For comparison, the reverse conversion
days_hoursat line 21 also usesprevValue * HOURS_IN_DAY, which IS correct (1 day = 24 hours). The asymmetry reveals the bug:hours_daysanddays_hoursshould be inverses of each other, but both multiply.The test documents this buggy behavior rather than the correct mathematical conversion. In practice, the
hours_dayspath is not exercised by the current UI code (which always converts through "minutes" as the canonical unit), so the bug has no visible impact today. However, the test cements incorrect behavior that would cause problems if thehours→dayspath is ever used directly.Impact: If
convertToNewDurationType("hours", "days", ...)is ever called directly, it will return wildly incorrect values (e.g., 24 days instead of ~0.042 days for 1 hour input).Prompt for agents
Was this helpful? React with 👍 or 👎 to provide feedback.