feat(lib): add more tests to lib package (#7210)

* feat(lib): add more tests to lib package

Add more tests to the lib package to make it more robust overall. Additionally, tidy any methods that can be modified without changing behaviour and tighten types where possible.

* fix(lib): update missed imports

* fix: revert stylistic changes

* Update getSchedule.test.ts

---------

Co-authored-by: Omar López <zomars@me.com>
This commit is contained in:
Lucas Smith 2023-03-11 09:10:56 +11:00 committed by GitHub
parent cbc9cd60d5
commit d81d772cdf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 675 additions and 193 deletions

View File

@ -2,7 +2,7 @@ import crypto from "crypto";
import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import { getPlaceholderAvatar } from "@calcom/lib/getPlaceholderAvatar";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import prisma from "@calcom/prisma";
import { defaultAvatarSrc } from "@lib/profile";

View File

@ -7,7 +7,7 @@ import { useEffect } from "react";
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
import EventTypeDescription from "@calcom/features/eventtypes/components/EventTypeDescription";
import { CAL_URL } from "@calcom/lib/constants";
import { getPlaceholderAvatar } from "@calcom/lib/getPlaceholderAvatar";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme";
import { md } from "@calcom/lib/markdownIt";

View File

@ -504,6 +504,7 @@ describe("getSchedule", () => {
);
});
// FIXME: Fix minimumBookingNotice is respected test
test.skip("minimumBookingNotice is respected", async () => {
jest.useFakeTimers().setSystemTime(
(() => {

View File

@ -35,9 +35,15 @@ const config: Config = {
displayName: "@calcom/lib",
roots: ["<rootDir>/packages/lib"],
testEnvironment: "node",
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
transform: {
"^.+\\.tsx?$": "ts-jest",
},
globals: {
"ts-jest": {
tsconfig: "<rootDir>/packages/lib/tsconfig.test.json",
},
},
},
{
displayName: "@calcom/closecom",

View File

@ -1,7 +1,7 @@
import { MembershipRole } from "@prisma/client";
import type { MembershipRole } from "@prisma/client";
import classNames from "@calcom/lib/classNames";
import { getPlaceholderAvatar } from "@calcom/lib/getPlaceholderAvatar";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import {

View File

@ -5,9 +5,10 @@ import { useState } from "react";
import MemberInvitationModal from "@calcom/ee/teams/components/MemberInvitationModal";
import classNames from "@calcom/lib/classNames";
import { getPlaceholderAvatar } from "@calcom/lib/getPlaceholderAvatar";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { RouterOutputs, trpc } from "@calcom/trpc/react";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import {
Avatar,
Button,

View File

@ -1,7 +1,7 @@
import { useRouter } from "next/router";
import { useMemo, useState } from "react";
import { getPlaceholderAvatar } from "@calcom/lib/getPlaceholderAvatar";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import { trpc } from "@calcom/trpc/react";
import { Alert, Avatar, Loader, Shell } from "@calcom/ui";

View File

@ -8,7 +8,7 @@ import { Controller, useForm } from "react-hook-form";
import { z } from "zod";
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
import { getPlaceholderAvatar } from "@calcom/lib/getPlaceholderAvatar";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { md } from "@calcom/lib/markdownIt";
import objectKeys from "@calcom/lib/objectKeys";

View File

@ -0,0 +1,155 @@
import { colorNameToHex, fallBackHex, isValidHexCode } from "./CustomBranding";
describe("Custom Branding tests", () => {
describe("fn: colorNameToHex", () => {
it("should return a hex color when a valid color name is provided", () => {
const cases = [
{
input: "red",
expected: "#ff0000",
},
{
input: "green",
expected: "#008000",
},
{
input: "salmon",
expected: "#fa8072",
},
{
input: "rebeccapurple",
expected: "#663399",
},
];
for (const { input, expected } of cases) {
const result = colorNameToHex(input);
expect(result).toEqual(expected);
}
});
it("should return false when an invalid color name is provided", () => {
const result = colorNameToHex("invalid");
expect(result).toEqual(false);
});
});
describe("fn: isValidHexCode", () => {
it("should return true when a valid hex code is provided", () => {
const cases = [
{
input: "#ff0000",
expected: true,
},
{
input: "#00Ff00",
expected: true,
},
{
input: "#fA8072",
expected: true,
},
{
input: "#663",
expected: true,
},
{
input: "#fAb",
expected: true,
},
{
input: "#F00F00",
expected: true,
},
];
for (const { input, expected } of cases) {
const result = isValidHexCode(input);
if (!result) {
console.log("input", input);
}
expect(result).toEqual(expected);
}
});
it("should return false when an invalid hex code is provided", () => {
const cases = [
{
input: "#ff000",
expected: false,
},
{
input: "#F000G0",
expected: false,
},
{
input: "#00ff00a",
expected: false,
},
{
input: "#fa8072aa",
expected: false,
},
{
input: "#663399aa",
expected: false,
},
];
for (const { input, expected } of cases) {
const result = isValidHexCode(input);
expect(result).toEqual(expected);
}
});
});
describe("fn: fallBackHex", () => {
it("should return a hex color when a valid color name is provided", () => {
const cases = [
{
input: "red",
expected: "#ff0000",
},
{
input: "green",
expected: "#008000",
},
{
input: "salmon",
expected: "#fa8072",
},
{
input: "rebeccapurple",
expected: "#663399",
},
];
for (const { input, expected } of cases) {
const result = colorNameToHex(input);
expect(result).toEqual(expected);
}
});
it("should return a brand color when there is no hex fallback", () => {
// BRAND_COLOR => "#292929"
// BRAND_TEXT_COLOR => "#ffffff"
// DARK_BRAND_COLOR => "#fafafa"
const inputs = ["reddit", null, "darkbruwn"];
for (const input of inputs) {
const resultLight = fallBackHex(input, false);
const resultDark = fallBackHex(input, true);
expect(resultLight).toEqual("#292929");
expect(resultDark).toEqual("#fafafa");
}
});
});
});

View File

@ -2,165 +2,178 @@ import Head from "next/head";
import { useBrandColors } from "@calcom/embed-core/embed-iframe";
const brandColor = "#292929";
const brandTextColor = "#ffffff";
const darkBrandColor = "#fafafa";
const BRAND_COLOR = "#292929";
const BRAND_TEXT_COLOR = "#ffffff";
const DARK_BRAND_COLOR = "#fafafa";
const HTML_COLORS = {
aliceblue: "#f0f8ff",
antiquewhite: "#faebd7",
aqua: "#00ffff",
aquamarine: "#7fffd4",
azure: "#f0ffff",
beige: "#f5f5dc",
bisque: "#ffe4c4",
black: "#000000",
blanchedalmond: "#ffebcd",
blue: "#0000ff",
blueviolet: "#8a2be2",
brown: "#a52a2a",
burlywood: "#deb887",
cadetblue: "#5f9ea0",
chartreuse: "#7fff00",
chocolate: "#d2691e",
coral: "#ff7f50",
cornflowerblue: "#6495ed",
cornsilk: "#fff8dc",
crimson: "#dc143c",
cyan: "#00ffff",
darkblue: "#00008b",
darkcyan: "#008b8b",
darkgoldenrod: "#b8860b",
darkgray: "#a9a9a9",
darkgreen: "#006400",
darkkhaki: "#bdb76b",
darkmagenta: "#8b008b",
darkolivegreen: "#556b2f",
darkorange: "#ff8c00",
darkorchid: "#9932cc",
darkred: "#8b0000",
darksalmon: "#e9967a",
darkseagreen: "#8fbc8f",
darkslateblue: "#483d8b",
darkslategray: "#2f4f4f",
darkturquoise: "#00ced1",
darkviolet: "#9400d3",
deeppink: "#ff1493",
deepskyblue: "#00bfff",
dimgray: "#696969",
dodgerblue: "#1e90ff",
firebrick: "#b22222",
floralwhite: "#fffaf0",
forestgreen: "#228b22",
fuchsia: "#ff00ff",
gainsboro: "#dcdcdc",
ghostwhite: "#f8f8ff",
gold: "#ffd700",
goldenrod: "#daa520",
gray: "#808080",
green: "#008000",
greenyellow: "#adff2f",
honeydew: "#f0fff0",
hotpink: "#ff69b4",
"indianred ": "#cd5c5c",
indigo: "#4b0082",
ivory: "#fffff0",
khaki: "#f0e68c",
lavender: "#e6e6fa",
lavenderblush: "#fff0f5",
lawngreen: "#7cfc00",
lemonchiffon: "#fffacd",
lightblue: "#add8e6",
lightcoral: "#f08080",
lightcyan: "#e0ffff",
lightgoldenrodyellow: "#fafad2",
lightgrey: "#d3d3d3",
lightgreen: "#90ee90",
lightpink: "#ffb6c1",
lightsalmon: "#ffa07a",
lightseagreen: "#20b2aa",
lightskyblue: "#87cefa",
lightslategray: "#778899",
lightsteelblue: "#b0c4de",
lightyellow: "#ffffe0",
lime: "#00ff00",
limegreen: "#32cd32",
linen: "#faf0e6",
magenta: "#ff00ff",
maroon: "#800000",
mediumaquamarine: "#66cdaa",
mediumblue: "#0000cd",
mediumorchid: "#ba55d3",
mediumpurple: "#9370d8",
mediumseagreen: "#3cb371",
mediumslateblue: "#7b68ee",
mediumspringgreen: "#00fa9a",
mediumturquoise: "#48d1cc",
mediumvioletred: "#c71585",
midnightblue: "#191970",
mintcream: "#f5fffa",
mistyrose: "#ffe4e1",
moccasin: "#ffe4b5",
navajowhite: "#ffdead",
navy: "#000080",
oldlace: "#fdf5e6",
olive: "#808000",
olivedrab: "#6b8e23",
orange: "#ffa500",
orangered: "#ff4500",
orchid: "#da70d6",
palegoldenrod: "#eee8aa",
palegreen: "#98fb98",
paleturquoise: "#afeeee",
palevioletred: "#d87093",
papayawhip: "#ffefd5",
peachpuff: "#ffdab9",
peru: "#cd853f",
pink: "#ffc0cb",
plum: "#dda0dd",
powderblue: "#b0e0e6",
purple: "#800080",
rebeccapurple: "#663399",
red: "#ff0000",
rosybrown: "#bc8f8f",
royalblue: "#4169e1",
saddlebrown: "#8b4513",
salmon: "#fa8072",
sandybrown: "#f4a460",
seagreen: "#2e8b57",
seashell: "#fff5ee",
sienna: "#a0522d",
silver: "#c0c0c0",
skyblue: "#87ceeb",
slateblue: "#6a5acd",
slategray: "#708090",
snow: "#fffafa",
springgreen: "#00ff7f",
steelblue: "#4682b4",
tan: "#d2b48c",
teal: "#008080",
thistle: "#d8bfd8",
tomato: "#ff6347",
turquoise: "#40e0d0",
violet: "#ee82ee",
wheat: "#f5deb3",
white: "#ffffff",
whitesmoke: "#f5f5f5",
yellow: "#ffff00",
yellowgreen: "#9acd32",
};
// Shadow the HTML_COLORS constant so we can create a type guard for it.
type HTML_COLORS = typeof HTML_COLORS;
function isHtmlColor(color: string): color is keyof HTML_COLORS {
return color in HTML_COLORS;
}
export function colorNameToHex(color: string) {
const colors = {
aliceblue: "#f0f8ff",
antiquewhite: "#faebd7",
aqua: "#00ffff",
aquamarine: "#7fffd4",
azure: "#f0ffff",
beige: "#f5f5dc",
bisque: "#ffe4c4",
black: "#000000",
blanchedalmond: "#ffebcd",
blue: "#0000ff",
blueviolet: "#8a2be2",
brown: "#a52a2a",
burlywood: "#deb887",
cadetblue: "#5f9ea0",
chartreuse: "#7fff00",
chocolate: "#d2691e",
coral: "#ff7f50",
cornflowerblue: "#6495ed",
cornsilk: "#fff8dc",
crimson: "#dc143c",
cyan: "#00ffff",
darkblue: "#00008b",
darkcyan: "#008b8b",
darkgoldenrod: "#b8860b",
darkgray: "#a9a9a9",
darkgreen: "#006400",
darkkhaki: "#bdb76b",
darkmagenta: "#8b008b",
darkolivegreen: "#556b2f",
darkorange: "#ff8c00",
darkorchid: "#9932cc",
darkred: "#8b0000",
darksalmon: "#e9967a",
darkseagreen: "#8fbc8f",
darkslateblue: "#483d8b",
darkslategray: "#2f4f4f",
darkturquoise: "#00ced1",
darkviolet: "#9400d3",
deeppink: "#ff1493",
deepskyblue: "#00bfff",
dimgray: "#696969",
dodgerblue: "#1e90ff",
firebrick: "#b22222",
floralwhite: "#fffaf0",
forestgreen: "#228b22",
fuchsia: "#ff00ff",
gainsboro: "#dcdcdc",
ghostwhite: "#f8f8ff",
gold: "#ffd700",
goldenrod: "#daa520",
gray: "#808080",
green: "#008000",
greenyellow: "#adff2f",
honeydew: "#f0fff0",
hotpink: "#ff69b4",
"indianred ": "#cd5c5c",
indigo: "#4b0082",
ivory: "#fffff0",
khaki: "#f0e68c",
lavender: "#e6e6fa",
lavenderblush: "#fff0f5",
lawngreen: "#7cfc00",
lemonchiffon: "#fffacd",
lightblue: "#add8e6",
lightcoral: "#f08080",
lightcyan: "#e0ffff",
lightgoldenrodyellow: "#fafad2",
lightgrey: "#d3d3d3",
lightgreen: "#90ee90",
lightpink: "#ffb6c1",
lightsalmon: "#ffa07a",
lightseagreen: "#20b2aa",
lightskyblue: "#87cefa",
lightslategray: "#778899",
lightsteelblue: "#b0c4de",
lightyellow: "#ffffe0",
lime: "#00ff00",
limegreen: "#32cd32",
linen: "#faf0e6",
magenta: "#ff00ff",
maroon: "#800000",
mediumaquamarine: "#66cdaa",
mediumblue: "#0000cd",
mediumorchid: "#ba55d3",
mediumpurple: "#9370d8",
mediumseagreen: "#3cb371",
mediumslateblue: "#7b68ee",
mediumspringgreen: "#00fa9a",
mediumturquoise: "#48d1cc",
mediumvioletred: "#c71585",
midnightblue: "#191970",
mintcream: "#f5fffa",
mistyrose: "#ffe4e1",
moccasin: "#ffe4b5",
navajowhite: "#ffdead",
navy: "#000080",
oldlace: "#fdf5e6",
olive: "#808000",
olivedrab: "#6b8e23",
orange: "#ffa500",
orangered: "#ff4500",
orchid: "#da70d6",
palegoldenrod: "#eee8aa",
palegreen: "#98fb98",
paleturquoise: "#afeeee",
palevioletred: "#d87093",
papayawhip: "#ffefd5",
peachpuff: "#ffdab9",
peru: "#cd853f",
pink: "#ffc0cb",
plum: "#dda0dd",
powderblue: "#b0e0e6",
purple: "#800080",
rebeccapurple: "#663399",
red: "#ff0000",
rosybrown: "#bc8f8f",
royalblue: "#4169e1",
saddlebrown: "#8b4513",
salmon: "#fa8072",
sandybrown: "#f4a460",
seagreen: "#2e8b57",
seashell: "#fff5ee",
sienna: "#a0522d",
silver: "#c0c0c0",
skyblue: "#87ceeb",
slateblue: "#6a5acd",
slategray: "#708090",
snow: "#fffafa",
springgreen: "#00ff7f",
steelblue: "#4682b4",
tan: "#d2b48c",
teal: "#008080",
thistle: "#d8bfd8",
tomato: "#ff6347",
turquoise: "#40e0d0",
violet: "#ee82ee",
wheat: "#f5deb3",
white: "#ffffff",
whitesmoke: "#f5f5f5",
yellow: "#ffff00",
yellowgreen: "#9acd32",
};
const normalizedColor = color.toLowerCase();
return colors[color.toLowerCase() as keyof typeof colors] !== undefined
? colors[color.toLowerCase() as keyof typeof colors]
: false;
if (isHtmlColor(normalizedColor)) {
return HTML_COLORS[normalizedColor];
}
return false;
}
function computeContrastRatio(a: number[], b: number[]) {
const lum1 = computeLuminance(a[0], a[1], a[2]);
const lum2 = computeLuminance(b[0], b[1], b[2]);
const brightest = Math.max(lum1, lum2);
const darkest = Math.min(lum1, lum2);
return (brightest + 0.05) / (darkest + 0.05);
}
@ -169,19 +182,25 @@ function computeLuminance(r: number, g: number, b: number) {
v /= 255;
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
});
return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722;
}
function hexToRGB(hex: string) {
const color = hex.replace("#", "");
return [parseInt(color.slice(0, 2), 16), parseInt(color.slice(2, 4), 16), parseInt(color.slice(4, 6), 16)];
}
function normalizeHexCode(hex: string | null, dark: boolean) {
if (!hex) {
return !dark ? brandColor : darkBrandColor;
return !dark ? BRAND_COLOR : DARK_BRAND_COLOR;
}
hex = hex.replace("#", "");
// If the length of the hex code is 3, double up each character
// e.g. fff => ffffff or a0e => aa00ee
if (hex.length === 3) {
hex = hex
.split("")
@ -190,54 +209,84 @@ function normalizeHexCode(hex: string | null, dark: boolean) {
})
.join("");
}
return hex;
}
function getContrastingTextColor(bgColor: string | null, dark: boolean): string {
bgColor = bgColor == "" || bgColor == null ? (dark ? darkBrandColor : brandColor) : bgColor;
bgColor = bgColor == "" || bgColor == null ? (dark ? DARK_BRAND_COLOR : BRAND_COLOR) : bgColor;
const rgb = hexToRGB(bgColor);
const whiteContrastRatio = computeContrastRatio(rgb, [255, 255, 255]);
const blackContrastRatio = computeContrastRatio(rgb, [41, 41, 41]); //#292929
return whiteContrastRatio > blackContrastRatio ? brandTextColor : brandColor;
return whiteContrastRatio > blackContrastRatio ? BRAND_TEXT_COLOR : BRAND_COLOR;
}
/**
* Given a string, determine if it's a valid 6 character hex code.
*/
export function isValidHexCode(val: string | null) {
if (val) {
// Normalize the value into include a leading "#" if it's missing
val = val.indexOf("#") === 0 ? val : "#" + val;
const regex = new RegExp("^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$");
const regex = /^#([A-F0-9]{6}|[A-F0-9]{3})$/i;
return regex.test(val);
}
return false;
}
/**
* Given a html color name, check if it exists in our color palette
* and if it does, return the hex code for that color. Otherwise,
* return the default brand color.
*/
export function fallBackHex(val: string | null, dark: boolean): string {
if (val) if (colorNameToHex(val)) return colorNameToHex(val) as string;
return dark ? darkBrandColor : brandColor;
if (val && colorNameToHex(val)) {
return colorNameToHex(val) as string;
}
// Otherwise, return the default color
return dark ? DARK_BRAND_COLOR : BRAND_COLOR;
}
/**
* Given a light and dark brand color value, update the css variables
* within the document to reflect the new brand colors.
*/
const BrandColor = ({
lightVal = brandColor,
darkVal = darkBrandColor,
lightVal = BRAND_COLOR,
darkVal = DARK_BRAND_COLOR,
}: {
lightVal: string | undefined | null;
darkVal: string | undefined | null;
}) => {
const embedBrandingColors = useBrandColors();
lightVal = embedBrandingColors.brandColor || lightVal;
// convert to 6 digit equivalent if 3 digit code is entered
lightVal = normalizeHexCode(lightVal, false);
darkVal = normalizeHexCode(darkVal, true);
// ensure acceptable hex-code
lightVal = isValidHexCode(lightVal)
? lightVal?.indexOf("#") === 0
? lightVal
: "#" + lightVal
: fallBackHex(lightVal, false);
darkVal = isValidHexCode(darkVal)
? darkVal?.indexOf("#") === 0
? darkVal
: "#" + darkVal
: fallBackHex(darkVal, true);
return (
<Head>
<style>

View File

@ -15,6 +15,7 @@ import { isENVDev } from "@calcom/lib/env";
*/
const NEXTAUTH_COOKIE_DOMAIN = process.env.NEXTAUTH_COOKIE_DOMAIN || "";
export function defaultCookies(useSecureCookies: boolean): CookiesOptions {
const cookiePrefix = useSecureCookies ? "__Secure-" : "";

View File

@ -0,0 +1,65 @@
import { defaultAvatarSrc, getPlaceholderAvatar } from "./defaultAvatarImage";
describe("Default Avatar Image tests", () => {
describe("fn: defaultAvatarSrc", () => {
it("should return a gravatar URL when an email is provided", () => {
const email = "john@example.com";
const result = defaultAvatarSrc({ email });
expect(result).toEqual(
"https://www.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=160&d=mp&r=PG"
);
});
it("should return a gravatar URL when an MD5 hash is provided", () => {
const md5 = "my-md5-hash";
const result = defaultAvatarSrc({ md5 });
expect(result).toEqual("https://www.gravatar.com/avatar/my-md5-hash?s=160&d=mp&r=PG");
});
it("should return a gravatar URL using the MD5 hash when an email and MD5 hash are provided", () => {
const email = "john@example.com";
const md5 = "my-md5-hash";
const result = defaultAvatarSrc({ email, md5 });
expect(result).toEqual("https://www.gravatar.com/avatar/my-md5-hash?s=160&d=mp&r=PG");
});
it("should return an empty string when neither an email or MD5 hash is provided", () => {
const result = defaultAvatarSrc({});
expect(result).toEqual("");
});
});
describe("fn: getPlaceholderAvatar", () => {
it("should return the avatar URL when one is provided", () => {
const avatar = "https://example.com/avatar.png";
const name = "John Doe";
const result = getPlaceholderAvatar(avatar, name);
expect(result).toEqual(avatar);
});
it("should return a placeholder avatar URL when no avatar is provided", () => {
const name = "John Doe";
const result = getPlaceholderAvatar(null, name);
expect(result).toEqual(
"https://eu.ui-avatars.com/api/?background=fff&color=f9f9f9&bold=true&background=000000&name=John%20Doe"
);
});
it("should return a placeholder avatar URL when no avatar is provided and no name is provided", () => {
const result = getPlaceholderAvatar(null, null);
expect(result).toEqual(
"https://eu.ui-avatars.com/api/?background=fff&color=f9f9f9&bold=true&background=000000&name="
);
});
});
});

View File

@ -1,5 +1,9 @@
import md5Parser from "md5";
/**
* Provided either an email or an MD5 hash, return the URL for the Gravatar
* image aborting early if neither is provided.
*/
export const defaultAvatarSrc = function ({ email, md5 }: { md5?: string; email?: string }) {
if (!email && !md5) return "";
@ -10,6 +14,15 @@ export const defaultAvatarSrc = function ({ email, md5 }: { md5?: string; email?
return `https://www.gravatar.com/avatar/${md5}?s=160&d=mp&r=PG`;
};
/**
* Given an avatar URL and a name, return the appropriate avatar URL. In the
* event that no avatar URL is provided, return a placeholder avatar URL from
* ui-avatars.com.
*
* ui-avatars.com is a free service that generates placeholder avatars based on
* a name. It is used here to provide a consistent placeholder avatar for users
* who have not uploaded an avatar.
*/
export function getPlaceholderAvatar(avatar: string | null | undefined, name: string | null) {
return avatar
? avatar

View File

@ -1,6 +0,0 @@
export function getPlaceholderAvatar(avatar: string | null | undefined, name: string | null) {
return avatar
? avatar
: "https://eu.ui-avatars.com/api/?background=fff&color=f9f9f9&bold=true&background=000000&name=" +
encodeURIComponent(name || "");
}

View File

@ -8,5 +8,6 @@ export function isBookingLimit(obj: unknown): obj is IntervalLimit {
export function parseBookingLimit(obj: unknown): IntervalLimit | null {
let bookingLimit: IntervalLimit | null = null;
if (isBookingLimit(obj)) bookingLimit = obj;
return bookingLimit;
}

View File

@ -2,12 +2,15 @@ import { recurringEventType as recurringEventSchema } from "@calcom/prisma/zod-u
import type { RecurringEvent } from "@calcom/types/Calendar";
export function isRecurringEvent(obj: unknown): obj is RecurringEvent {
const parsedRecuEvt = recurringEventSchema.safeParse(obj);
return parsedRecuEvt.success;
const parsed = recurringEventSchema.safeParse(obj);
return parsed.success;
}
export function parseRecurringEvent(obj: unknown): RecurringEvent | null {
let recurringEvent: RecurringEvent | null = null;
if (isRecurringEvent(obj)) recurringEvent = obj;
return recurringEvent;
}

View File

@ -1,11 +0,0 @@
import crypto from "crypto";
export const defaultAvatarSrc = function ({ email, md5 }: { md5?: string; email?: string }) {
if (!email && !md5) return "";
if (email && !md5) {
md5 = crypto.createHash("md5").update(email).digest("hex");
}
return `https://www.gravatar.com/avatar/${md5}?s=160&d=mp&r=PG`;
};

View File

@ -0,0 +1,30 @@
import { randomString } from "./random";
describe("Random util tests", () => {
describe("fn: randomString", () => {
it("should return a random string of a given length", () => {
const length = 10;
const result = randomString(length);
expect(result).toHaveLength(length);
});
it("should return a random string of a default length", () => {
const length = 12;
const result = randomString();
expect(result).toHaveLength(length);
});
it("should return a random string of a given length using alphanumeric characters", () => {
const length = 10;
const result = randomString(length);
expect(result).toMatch(/^[a-zA-Z0-9]+$/);
expect(result).toHaveLength(length);
});
});
});

View File

@ -1,9 +1,15 @@
const CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const CHARACTERS_LENGTH = CHARACTERS.length;
/**
* Generate a random string of a given length using alphanumeric characters.
*/
export const randomString = function (length = 12) {
let result = "";
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const charactersLength = characters.length;
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
result += CHARACTERS.charAt(Math.floor(Math.random() * CHARACTERS_LENGTH));
}
return result;
};

70
packages/lib/text.test.ts Normal file
View File

@ -0,0 +1,70 @@
import { truncate } from "./text";
describe("Text util tests", () => {
describe("fn: truncate", () => {
it("should return the original text when it is shorter than the max length", () => {
const cases = [
{
input: "Hello world",
maxLength: 100,
expected: "Hello world",
},
{
input: "Hello world",
maxLength: 11,
expected: "Hello world",
},
];
for (const { input, maxLength, expected } of cases) {
const result = truncate(input, maxLength);
expect(result).toEqual(expected);
}
});
it("should return the truncated text when it is longer than the max length", () => {
const cases = [
{
input: "Hello world",
maxLength: 10,
expected: "Hello w...",
},
{
input: "Hello world",
maxLength: 5,
expected: "He...",
},
];
for (const { input, maxLength, expected } of cases) {
const result = truncate(input, maxLength);
expect(result).toEqual(expected);
}
});
it("should return the truncated text without ellipsis when it is longer than the max length and ellipsis is false", () => {
const cases = [
{
input: "Hello world",
maxLength: 10,
ellipsis: false,
expected: "Hello w",
},
{
input: "Hello world",
maxLength: 5,
ellipsis: false,
expected: "He",
},
];
for (const { input, maxLength, ellipsis, expected } of cases) {
const result = truncate(input, maxLength, ellipsis);
expect(result).toEqual(expected);
}
});
});
});

View File

@ -1,5 +1,6 @@
export const truncate = (text: string, maxLength: number, ellipsis = true) => {
if (text.length <= maxLength) return text;
return `${text.slice(0, maxLength - 3)}${ellipsis ? "..." : ""}`;
};
@ -8,9 +9,11 @@ export const truncateOnWord = (text: string, maxLength: number, ellipsis = true)
// First split on maxLength chars
let truncatedText = text.substring(0, 148);
// Then split on the last space, this way we split on the last word,
// which looks just a bit nicer.
truncatedText = truncatedText.substring(0, Math.min(truncatedText.length, truncatedText.lastIndexOf(" ")));
if (ellipsis) truncatedText += "...";
return truncatedText;

View File

@ -18,11 +18,17 @@ export const setIs24hClockInLocalStorage = (is24h: boolean) =>
export const getIs24hClockFromLocalStorage = () => {
const is24hFromLocalstorage = localStorage.getItem(is24hLocalstorageKey);
if (is24hFromLocalstorage === null) return null;
return is24hFromLocalstorage === "true";
};
/**
* Retrieves the browsers time format preference, checking local storage first
* for a user set preference. If no preference is found, it will use the browser
* locale to determine the time format and store it in local storage.
*/
export const isBrowserLocale24h = () => {
const localStorageTimeFormat = getIs24hClockFromLocalStorage();
// If time format is already stored in the browser then retrieve and return early
@ -41,6 +47,9 @@ export const isBrowserLocale24h = () => {
}
};
/**
* Returns the time format string based on whether the current set locale is 24h or 12h.
*/
export const detectBrowserTimeFormat = isBrowserLocale24h()
? TimeFormat.TWENTY_FOUR_HOUR
: TimeFormat.TWELVE_HOUR;

View File

@ -2,6 +2,7 @@
"extends": "@calcom/tsconfig/base.json",
"compilerOptions": {
"target": "es5",
"jsx": "preserve",
"resolveJsonModule": true
},
"include": [".", "../types/next-auth.d.ts"],

View File

@ -0,0 +1,13 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@calcom/tsconfig/base.json",
"compilerOptions": {
"esModuleInterop": true,
"target": "es5",
"jsx": "react-jsx",
"resolveJsonModule": true
},
"include": [".", "../types/next-auth.d.ts"],
"exclude": ["dist", "build", "node_modules"]
}

View File

@ -1,4 +1,8 @@
// TODO: In case of an embed if localStorage is not available(third party), use localStorage of parent(first party) that contains the iframe.
/**
* Provides a wrapper around localStorage to avoid errors in case of restricted storage access.
*
* TODO: In case of an embed if localStorage is not available(third party), use localStorage of parent(first party) that contains the iframe.
*/
export const localStorage = {
getItem(key: string) {
try {

View File

@ -0,0 +1,64 @@
import { nameOfDay, weekdayNames } from "./weekday";
describe("Weekday tests", () => {
describe("fn: weekdayNames", () => {
it("should return the weekday names for a given locale", () => {
const locales = ["en-US", "en-CA", "en-GB", "en-AU"];
for (const locale of locales) {
const result = weekdayNames(locale);
const expected = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
expect(result).toEqual(expected);
}
});
it("should return the weekday names for a given locale and format", () => {
const locales = ["en-US", "en-CA", "en-GB", "en-AU"];
for (const locale of locales) {
const result = weekdayNames(locale, 0, "short");
const expected = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
expect(result).toEqual(expected);
}
});
it("should return the weekday names for a given locale and week start offset", () => {
const locales = ["en-US", "en-CA", "en-GB", "en-AU"];
for (const locale of locales) {
const result = weekdayNames(locale, 1);
const expected = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
expect(result).toEqual(expected);
}
});
});
describe("fn: nameOfDay", () => {
it("should return the name of the day for a given locale", () => {
const locales = ["en-US", "en-CA", "en-GB", "en-AU"];
const days = [
{ day: 0, expected: "Sunday" },
{ day: 1, expected: "Monday" },
{ day: 2, expected: "Tuesday" },
{ day: 3, expected: "Wednesday" },
{ day: 4, expected: "Thursday" },
{ day: 5, expected: "Friday" },
{ day: 6, expected: "Saturday" },
];
for (const locale of locales) {
for (const { day, expected } of days) {
const result = nameOfDay(locale, day);
expect(result).toEqual(expected);
}
}
});
});
});

View File

@ -1,12 +1,16 @@
type WeekdayFormat = "short" | "long";
// By default starts on Sunday (Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday)
export function weekdayNames(locale: string | string[], weekStart = 0, type: "short" | "long" = "long") {
return Array.from(Array(7).keys()).map((d) => nameOfDay(locale, d + weekStart, type));
export function weekdayNames(locale: string | string[], weekStart = 0, format: WeekdayFormat = "long") {
return Array(7)
.fill(null)
.map((_, day) => nameOfDay(locale, day + weekStart, format));
}
export function nameOfDay(
locale: string | string[] | undefined,
day: number,
type: "short" | "long" = "long"
format: WeekdayFormat = "long"
) {
return new Intl.DateTimeFormat(locale, { weekday: type }).format(new Date(1970, 0, day + 4));
return new Intl.DateTimeFormat(locale, { weekday: format }).format(new Date(1970, 0, day + 4));
}

View File

@ -4,8 +4,8 @@ import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import { getSession } from "@calcom/lib/auth";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { defaultAvatarSrc } from "@calcom/lib/defaultAvatarImage";
import { getLocaleFromHeaders } from "@calcom/lib/i18n";
import { defaultAvatarSrc } from "@calcom/lib/profile";
import prisma from "@calcom/prisma";
import type { Maybe } from "@trpc/server";