import type { PullRequestReaction, PullRequestReactionContent } from "@t3tools/contracts"; import { describe, expect, it } from "vite-plus/test"; import { applyPendingPullRequestReactions, PULL_REQUEST_REACTION_ORDER, pullRequestReactionEmoji, pullRequestReactionName, pullRequestReactionTooltip, } from "./pullRequestReactions.logic"; function reaction(overrides: Partial = {}): PullRequestReaction { return { content: "heart", count: 1, actors: ["octocat"], viewerHasReacted: false, ...overrides, }; } describe("reaction presentation", () => { it("names and draws all eight, in GitHub's picker order", () => { expect(PULL_REQUEST_REACTION_ORDER).toEqual([ "thumbs-up", "thumbs-down", "laugh", "hooray", "confused", "heart", "rocket", "eyes", ]); expect(PULL_REQUEST_REACTION_ORDER.map(pullRequestReactionEmoji)).toEqual([ "👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀", ]); expect(pullRequestReactionName("thumbs-up")).toBe("thumbs up"); expect(pullRequestReactionName("eyes")).toBe("eyes"); }); }); describe("reaction tooltip", () => { it("reads as GitHub's sentence for one, two and three names", () => { expect( pullRequestReactionTooltip(reaction({ content: "thumbs-up", count: 1, actors: ["Bil0000"] })), ).toBe("Bil0000 reacted with thumbs up emoji"); expect(pullRequestReactionTooltip(reaction({ count: 2, actors: ["Bil0000", "octocat"] }))).toBe( "Bil0000 and octocat reacted with heart emoji", ); expect( pullRequestReactionTooltip( reaction({ content: "eyes", count: 3, actors: ["Bil0000", "octocat"], viewerHasReacted: true, }), ), ).toBe("You, Bil0000, and octocat reacted with eyes emoji"); }); it("counts everyone past the third name, including the ones the host never named", () => { expect( pullRequestReactionTooltip( reaction({ content: "rocket", count: 15, actors: ["a", "b", "c", "d"], viewerHasReacted: true, }), ), ).toBe("You, a, b, and 12 others reacted with rocket emoji"); // A host that counted more than it named still says who is missing. expect(pullRequestReactionTooltip(reaction({ count: 2, actors: ["octocat"] }))).toBe( "octocat and 1 other reacted with heart emoji", ); // Nothing named at all leaves the count to speak for itself, and nobody to be "other" than. expect(pullRequestReactionTooltip(reaction({ count: 4, actors: [] }))).toBe( "4 people reacted with heart emoji", ); expect(pullRequestReactionTooltip(reaction({ count: 1, actors: [] }))).toBe( "1 person reacted with heart emoji", ); }); it("names the viewer as You, ahead of the other people who reacted", () => { // The host already leaves the viewer's own login out of `actors`. expect( pullRequestReactionTooltip( reaction({ count: 3, actors: ["Bil0000", "octocat"], viewerHasReacted: true }), ), ).toBe("You, Bil0000, and octocat reacted with heart emoji"); }); it("names nobody as You when the viewer has not reacted", () => { expect( pullRequestReactionTooltip( reaction({ count: 2, actors: ["Bil0000", "octocat"], viewerHasReacted: false }), ), ).toBe("Bil0000 and octocat reacted with heart emoji"); }); it("names actors as given, and leaves off You, for a host with no room for the viewer", () => { // `count` says two and `actors` already lists two logins, one of them the viewer's own — // there is no slot left for "You" that wouldn't invent or hide a real reactor. expect( pullRequestReactionTooltip( reaction({ count: 2, actors: ["Bil0000", "octocat"], viewerHasReacted: true }), ), ).toBe("Bil0000 and octocat reacted with heart emoji"); // Same shape past the naming cap: the display limit still leaves an honest remainder. expect( pullRequestReactionTooltip( reaction({ count: 4, actors: ["Bil0000", "octocat", "hubot", "zzz"], viewerHasReacted: true, }), ), ).toBe("Bil0000, octocat, hubot, and 1 other reacted with heart emoji"); }); it("keeps naming the viewer You when the host does leave them room", () => { // `actors` has fewer logins than `count`, so the viewer fits without being counted twice. expect( pullRequestReactionTooltip( reaction({ count: 2, actors: ["octocat"], viewerHasReacted: true }), ), ).toBe("You and octocat reacted with heart emoji"); }); }); describe("reaction tooltip, with a reaction in flight", () => { it("still says You after an optimistic react, even at full capacity", () => { // The host's last snapshot had every reactor named and nobody left over; the optimistic // bump grows `count` without touching `actors`, so there is now room for "You" and the // sentence should use it rather than reading as a non-compliant host. const applied = applyPendingPullRequestReactions( [reaction({ count: 2, actors: ["a", "b"], viewerHasReacted: false })], new Map([["heart", true] as const]), ); expect(pullRequestReactionTooltip(applied[0]!)).toBe("You, a, and b reacted with heart emoji"); }); it("drops You after an optimistic un-react, without treating the host as non-compliant", () => { const applied = applyPendingPullRequestReactions( [reaction({ count: 2, actors: ["octocat"], viewerHasReacted: true })], new Map([["heart", false] as const]), ); expect(pullRequestReactionTooltip(applied[0]!)).toBe("octocat reacted with heart emoji"); }); }); describe("pending reactions", () => { const pending = ( entries: ReadonlyArray, ): ReadonlyMap => new Map(entries); it("returns the host's list untouched while nothing is in flight", () => { const reactions = [reaction()]; expect(applyPendingPullRequestReactions(reactions, pending([]))).toBe(reactions); }); it("adds a reaction nobody had yet, in picker order", () => { const applied = applyPendingPullRequestReactions( [reaction({ content: "rocket", count: 2, actors: ["a", "b"] })], pending([["thumbs-up", true]]), ); expect(applied).toEqual([ { content: "thumbs-up", count: 1, actors: [], viewerHasReacted: true }, { content: "rocket", count: 2, actors: ["a", "b"], viewerHasReacted: false }, ]); }); it("joins and leaves an existing reaction, and drops the pill nobody is left on", () => { expect( applyPendingPullRequestReactions( [reaction({ count: 2, actors: ["a", "b"] })], pending([["heart", true]]), ), ).toEqual([{ content: "heart", count: 3, actors: ["a", "b"], viewerHasReacted: true }]); expect( applyPendingPullRequestReactions( [reaction({ count: 2, actors: ["a", "b"], viewerHasReacted: true })], pending([["heart", false]]), ), ).toEqual([{ content: "heart", count: 1, actors: ["a", "b"], viewerHasReacted: false }]); expect( applyPendingPullRequestReactions( [reaction({ count: 1, viewerHasReacted: true })], pending([["heart", false]]), ), ).toEqual([]); }); it("ignores a pending state the host has already caught up with", () => { const reactions = [reaction({ count: 3, viewerHasReacted: true })]; expect(applyPendingPullRequestReactions(reactions, pending([["heart", true]]))).toEqual( reactions, ); expect(applyPendingPullRequestReactions([], pending([["heart", false]]))).toEqual([]); }); });