Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,20 @@ module.exports = {
"\\.(css|less)$": "<rootDir>/__mocks__/styleMock.js"
},
collectCoverageFrom: [
"**/*.{js,jsx}",
"**/*.{js,jsx,ts,tsx}",
"!**/node_modules/**",
"!**/__test__/**",
"!**/deploy/**",
"!**/coverage/**"
],
coverageThreshold: {
"src/lib/": {
branches: 30,
functions: 45,
lines: 45,
statements: 45
}
},
setupFilesAfterEnv: ["<rootDir>/__test__/setup-framework.js"],
testPathIgnorePatterns: ["<rootDir>/node_modules/", "<rootDir>/__test__/e2e/"]
};
111 changes: 111 additions & 0 deletions src/lib/attributes.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import {
camelCase,
nameComponents,
recordToCamelCase,
snakeToTitleCase,
titleCase
} from "./attributes";

describe("camelCase", () => {
it("converts a space-separated string", () => {
expect(camelCase("hello world")).toBe("helloWorld");
});

it("converts a multi-word string", () => {
expect(camelCase("the quick brown fox")).toBe("theQuickBrownFox");
});

it("handles a single word", () => {
expect(camelCase("hello")).toBe("hello");
});

it("handles an already camelCase string", () => {
expect(camelCase("helloWorld")).toBe("helloWorld");
});
});

describe("titleCase", () => {
it("capitalizes the first letter and lowercases the rest", () => {
expect(titleCase("hello")).toBe("Hello");
});

it("converts an all-uppercase word", () => {
expect(titleCase("HELLO")).toBe("Hello");
});

// Note: this is a single-word titleCase, unlike scripts.ts titleCase
// which handles multiple words.
it("only capitalizes the first character of multi-word input", () => {
expect(titleCase("hello world")).toBe("Hello world");
});
});

describe("snakeToTitleCase", () => {
it("converts snake_case to Title Case with spaces", () => {
expect(snakeToTitleCase("hello_world")).toBe("Hello World");
});

it("converts a single word", () => {
expect(snakeToTitleCase("hello")).toBe("Hello");
});

it("handles multiple underscores", () => {
expect(snakeToTitleCase("one_two_three")).toBe("One Two Three");
});
});

describe("nameComponents", () => {
it("splits a two-word name into first and last", () => {
const result = nameComponents("Jane Doe");
expect(result.firstName).toBe("Jane");
expect(result.lastName).toBe("Doe");
expect(result.cellNumber).toBeUndefined();
});

it("treats a single word as firstName only", () => {
const result = nameComponents("Jane");
expect(result.firstName).toBe("Jane");
expect(result.lastName).toBeUndefined();
expect(result.cellNumber).toBeUndefined();
});

it("puts everything after the first word into lastName", () => {
const result = nameComponents("Jane van der Berg");
expect(result.firstName).toBe("Jane");
expect(result.lastName).toBe("van der Berg");
});

it("extracts a phone number from the name string", () => {
const result = nameComponents("Jane Doe 2025551234");
expect(result.firstName).toBe("Jane");
expect(result.lastName).toBe("Doe");
expect(result.cellNumber).toBe("+12025551234");
});

it("returns undefined fields for an empty string", () => {
const result = nameComponents("");
expect(result.firstName).toBeUndefined();
expect(result.lastName).toBeUndefined();
expect(result.cellNumber).toBeUndefined();
});
});

describe("recordToCamelCase", () => {
it("converts snake_case keys to camelCase", () => {
const result = recordToCamelCase({
first_name: "Jane",
last_name: "Doe"
});
expect(result).toEqual({ firstName: "Jane", lastName: "Doe" });
});

it("preserves already camelCase keys", () => {
const result = recordToCamelCase({ firstName: "Jane" });
expect(result).toEqual({ firstName: "Jane" });
});

it("does not modify values", () => {
const result = recordToCamelCase({ some_key: "some_value" });
expect(result).toEqual({ someKey: "some_value" });
});
});
66 changes: 66 additions & 0 deletions src/lib/datetime.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { DateTime, deprecatedTimezoneMap, parseIanaZone } from "./datetime";

describe("parseIanaZone", () => {
it("converts a deprecated US timezone to its IANA equivalent", () => {
expect(parseIanaZone("US/Eastern")).toBe("America/New_York");
});

it("is case-insensitive", () => {
expect(parseIanaZone("us/eastern")).toBe("America/New_York");
expect(parseIanaZone("US/EASTERN")).toBe("America/New_York");
});

it("passes through an already-IANA timezone unchanged", () => {
expect(parseIanaZone("America/New_York")).toBe("America/New_York");
});

it("passes through an unknown timezone unchanged", () => {
expect(parseIanaZone("Europe/Berlin")).toBe("Europe/Berlin");
});

it("returns empty string for empty string input", () => {
expect(parseIanaZone("")).toBe("");
});
});

describe("deprecatedTimezoneMap", () => {
it("contains all expected US timezone mappings", () => {
expect(Object.keys(deprecatedTimezoneMap)).toHaveLength(12);
expect(deprecatedTimezoneMap["us/eastern"]).toBe("America/New_York");
expect(deprecatedTimezoneMap["us/pacific"]).toBe("America/Los_Angeles");
expect(deprecatedTimezoneMap["us/central"]).toBe("America/Chicago");
expect(deprecatedTimezoneMap["us/mountain"]).toBe("America/Denver");
expect(deprecatedTimezoneMap["us/hawaii"]).toBe("Pacific/Honolulu");
});
});

describe("DateTime", () => {
it("extends luxon DateTime", () => {
const dt = DateTime.fromISO("2020-01-01T12:00:00Z");
expect(dt.isValid).toBe(true);
expect(dt.year).toBe(2020);
});

describe("setZone", () => {
it("resolves a deprecated timezone name without invalidating", () => {
const dt = DateTime.fromISO("2020-01-01T12:00:00Z");
const rezoned = dt.setZone("US/Eastern");
expect(rezoned.isValid).toBe(true);
// The offset matches America/New_York (-5 in January)
expect(rezoned.offset).toBe(-300);
});

it("works with standard IANA timezone names", () => {
const dt = DateTime.fromISO("2020-01-01T12:00:00Z");
const rezoned = dt.setZone("America/Chicago");
expect(rezoned.isValid).toBe(true);
expect(rezoned.zoneName).toBe("America/Chicago");
});

it("works with UTC", () => {
const dt = DateTime.fromISO("2020-01-01T12:00:00-05:00");
const rezoned = dt.setZone("utc");
expect(rezoned.hour).toBe(17);
});
});
});
44 changes: 43 additions & 1 deletion src/lib/interaction-step-helpers.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import type { InteractionStep } from "../api/interaction-step";
import { makeTree } from "./interaction-step-helpers";
import {
getTopMostParent,
interactionStepForId,
makeTree
} from "./interaction-step-helpers";

const baseEmptyStep: Omit<InteractionStep, "id" | "parentInteractionId"> = {
questionText: "",
Expand All @@ -22,6 +26,44 @@ const step = (
...overrides
});

describe("interactionStepForId", () => {
it("returns the step matching the given id", () => {
const steps = [step("1", null), step("2", "1"), step("3", "1")];
expect(interactionStepForId("2", steps)).toEqual(steps[1]);
});

it("returns null when no step matches", () => {
const steps = [step("1", null)];
expect(interactionStepForId("999", steps)).toBeNull();
});

// Documents known behavior: uses forEach instead of find, so returns
// the *last* match if there are duplicates.
it("returns the last match when duplicate ids exist", () => {
const first = step("1", null, { questionText: "first" });
const duplicate = step("1", null, { questionText: "duplicate" });
expect(interactionStepForId("1", [first, duplicate])).toEqual(duplicate);
});
});

describe("getTopMostParent", () => {
it("returns the root step (parentInteractionId === null)", () => {
const root = step("1", null);
const child = step("2", "1");
expect(getTopMostParent([root, child], false)).toEqual(root);
});

it("returns the newest root when multiple roots exist", () => {
const olderRoot = step("1", null);
const newerRoot = step("2", null);
expect(getTopMostParent([olderRoot, newerRoot], false)).toEqual(newerRoot);
});

it("returns undefined for an empty array", () => {
expect(getTopMostParent([], false)).toBeUndefined();
});
});

describe("makeTree", () => {
it("returns a wrapper with empty children for an empty input", () => {
const tree = makeTree([]);
Expand Down
98 changes: 98 additions & 0 deletions src/lib/permissions.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { UserRoleType } from "../api/organization-membership";
import {
hasRole,
hasRoleAtLeast,
isRoleGreater,
ROLE_HIERARCHY
} from "./permissions";

describe("ROLE_HIERARCHY", () => {
it("orders roles from least to most privileged", () => {
expect(ROLE_HIERARCHY).toEqual([
UserRoleType.SUSPENDED,
UserRoleType.TEXTER,
UserRoleType.SUPERVOLUNTEER,
UserRoleType.ADMIN,
UserRoleType.OWNER,
UserRoleType.SUPERADMIN
]);
});
});

describe("isRoleGreater", () => {
it("returns true when first role outranks second", () => {
expect(isRoleGreater(UserRoleType.ADMIN, UserRoleType.TEXTER)).toBe(true);
});

it("returns false when first role is lower than second", () => {
expect(isRoleGreater(UserRoleType.TEXTER, UserRoleType.ADMIN)).toBe(false);
});

it("returns false when roles are equal", () => {
expect(isRoleGreater(UserRoleType.ADMIN, UserRoleType.ADMIN)).toBe(false);
});

it("returns true for SUPERADMIN over OWNER", () => {
expect(isRoleGreater(UserRoleType.SUPERADMIN, UserRoleType.OWNER)).toBe(
true
);
});
});

describe("hasRoleAtLeast", () => {
it("returns true when role meets the requirement", () => {
expect(hasRoleAtLeast(UserRoleType.ADMIN, UserRoleType.ADMIN)).toBe(true);
});

it("returns true when role exceeds the requirement", () => {
expect(hasRoleAtLeast(UserRoleType.OWNER, UserRoleType.ADMIN)).toBe(true);
});

it("returns false when role is below the requirement", () => {
expect(hasRoleAtLeast(UserRoleType.TEXTER, UserRoleType.ADMIN)).toBe(false);
});

it("returns true for SUSPENDED meeting SUSPENDED", () => {
expect(hasRoleAtLeast(UserRoleType.SUSPENDED, UserRoleType.SUSPENDED)).toBe(
true
);
});
});

describe("hasRole", () => {
it("returns true when user has a role at or above the required level", () => {
expect(hasRole(UserRoleType.ADMIN, [UserRoleType.OWNER])).toBe(true);
});

it("returns false when user only has a lower role", () => {
expect(hasRole(UserRoleType.ADMIN, [UserRoleType.TEXTER])).toBe(false);
});

it("returns true when one of multiple roles meets the requirement", () => {
expect(
hasRole(UserRoleType.ADMIN, [UserRoleType.TEXTER, UserRoleType.ADMIN])
).toBe(true);
});

it("picks the highest from a multi-role list", () => {
expect(
hasRole(UserRoleType.SUPERADMIN, [
UserRoleType.TEXTER,
UserRoleType.SUPERVOLUNTEER,
UserRoleType.ADMIN,
UserRoleType.OWNER,
UserRoleType.SUPERADMIN
])
).toBe(true);
});

// Documents known bug: getHighestRole mutates its input array via .sort()
// This test will be updated when the bug is fixed in PR 4.
it("mutates the input array (known bug)", () => {
const roles = [UserRoleType.ADMIN, UserRoleType.TEXTER];
hasRole(UserRoleType.TEXTER, roles);
// After calling hasRole, the array has been sorted in place
expect(roles[0]).toBe(UserRoleType.TEXTER);
expect(roles[1]).toBe(UserRoleType.ADMIN);
});
});
Loading
Loading