218 lines · 8.2 KB · TypeScript
RawDownload
import { describe, expect, it, vi } from "vite-plus/test";import {  DEFAULT_MOBILE_THEME_ID,  getMobileThemeVariables,  MOBILE_THEME_IDS,  type MobileThemeAppearance,  type MobileThemeId,} from "../../lib/mobileTheme";import { readDefaultMobileThemeVariables } from "../../lib/mobileTheme.test-support"; import {  buildNativeReviewDiffData,  createNativeReviewDiffTheme,  getCachedNativeReviewDiffData,  type BuildNativeReviewDiffDataInput,} from "./nativeReviewDiffAdapter";import type { ReviewInlineComment } from "./reviewCommentSelection";import { buildReviewParsedDiff } from "./reviewModel";import * as ReviewWordDiffs from "./reviewWordDiffs"; const parsedDiff = buildReviewParsedDiff(  [    "diff --git a/example.ts b/example.ts",    "--- a/example.ts",    "+++ b/example.ts",    "@@ -1 +1 @@",    "-const before = 1;",    "+const after = 2;",  ].join("\n"),  "native-review-cache-test",); function makeComment(text: string): ReviewInlineComment {  return {    id: "comment-1",    sectionId: "git:working-tree",    sectionTitle: "Dirty worktree",    filePath: "example.ts",    startIndex: 0,    endIndex: 0,    rangeLabel: "-1",    text,    diff: "@@ -1,1 +1,0 @@\n-const before = 1;",  };} function buildInput(comments: BuildNativeReviewDiffDataInput["comments"]) {  return { parsedDiff, comments } satisfies BuildNativeReviewDiffDataInput;} function filesPatch(paths: ReadonlyArray<string>) {  return paths    .map((path) =>      [        `diff --git a/${path} b/${path}`,        `--- a/${path}`,        `+++ b/${path}`,        "@@ -1,2 +1,2 @@",        '-const first = renderPanel({ label: "before", enabled: true });',        '-const second = renderPanel({ label: "before", enabled: true });',        '+const first = renderPanel({ label: "after", enabled: true });',        '+const second = renderPanel({ label: "after", enabled: true });',      ].join("\n"),    )    .join("\n");} function appTheme(themeId: MobileThemeId, appearance: MobileThemeAppearance) {  return themeId === DEFAULT_MOBILE_THEME_ID || themeId === "material-you"    ? readDefaultMobileThemeVariables(appearance)    : getMobileThemeVariables(themeId, appearance);} describe("getCachedNativeReviewDiffData", () => {  it("reuses the row model for equivalent empty comment arrays", () => {    const first = getCachedNativeReviewDiffData(buildInput([]));    const second = getCachedNativeReviewDiffData(buildInput([]));     expect(second).toBe(first);  });   it("reuses equivalent comment contents and invalidates changed comments", () => {    const first = getCachedNativeReviewDiffData(buildInput([makeComment("First")]));    const equivalent = getCachedNativeReviewDiffData(buildInput([makeComment("First")]));    const changed = getCachedNativeReviewDiffData(buildInput([makeComment("Changed")]));     expect(equivalent).toBe(first);    expect(changed).not.toBe(first);    expect(changed.rows.find((row) => row.kind === "comment")?.commentText).toBe("Changed");  });   it("reuses source rows and word matching when a file comment changes", () => {    const diff = buildReviewParsedDiff(filesPatch(["example.ts", "second.ts"]), "comment-reuse");    const matchWords = vi.spyOn(ReviewWordDiffs, "computeWordAltDiffRanges");    try {      const base = getCachedNativeReviewDiffData({ parsedDiff: diff });      expect(matchWords).toHaveBeenCalledTimes(4);      expect(base.rows.filter((row) => row.wordDiffRanges?.length)).toHaveLength(8);      const firstComment = makeComment("First file comment");      const secondComment = {        ...makeComment("Second file comment"),        id: "comment-2",        filePath: "second.ts",      };      const first = getCachedNativeReviewDiffData({        parsedDiff: diff,        comments: [firstComment, secondComment],      });      const changed = getCachedNativeReviewDiffData({        parsedDiff: diff,        comments: [{ ...firstComment, text: "Changed first comment" }, secondComment],      });       expect(matchWords).toHaveBeenCalledTimes(4);      expect(changed.files).toBe(base.files);      expect(changed.commentTargetsByRowId).toBe(base.commentTargetsByRowId);      expect(changed.rowIdByCommentLineId).toBe(base.rowIdByCommentLineId);      expect(changed.rows.find((row) => row.id === secondComment.id)).toBe(        first.rows.find((row) => row.id === secondComment.id),      );      const sourceRows = changed.rows.filter((row) => row.kind !== "comment");      for (const [index, row] of sourceRows.entries()) {        expect(row).toBe(base.rows[index]);      }      const removed = getCachedNativeReviewDiffData({ parsedDiff: diff, comments: [] });      expect(removed.rows).toEqual(base.rows);      expect(matchWords).toHaveBeenCalledTimes(4);      expect(first.rows.find((row) => row.id === firstComment.id)?.commentText).toBe(        "First file comment",      );    } finally {      matchWords.mockRestore();    }  });   it("updates comment titles and locations without changing source coordinates", () => {    const diff = buildReviewParsedDiff(filesPatch(["example.ts", "second.ts"]), "comment-metadata");    const comment = makeComment("Review this line");    const first = getCachedNativeReviewDiffData({ parsedDiff: diff, comments: [comment] });    const renamed = getCachedNativeReviewDiffData({      parsedDiff: diff,      comments: [{ ...comment, sectionTitle: "Branch comparison" }],    });    expect(renamed.rows.find((row) => row.id === comment.id)?.commentSectionTitle).toBe(      "Branch comparison",    );    const moved = getCachedNativeReviewDiffData({      parsedDiff: diff,      comments: [{ ...comment, filePath: "second.ts", endIndex: 99, rangeLabel: "+2" }],    });    const commentIndex = moved.rows.findIndex((row) => row.id === comment.id);    expect(moved.rows[commentIndex]).toMatchObject({      kind: "comment",      filePath: "second.ts",      commentRangeLabel: "+2",    });    expect(moved.rows[commentIndex - 1]).toMatchObject({      kind: "line",      change: "add",      newLineNumber: 2,    });    expect(moved.rows.filter((row) => row.kind !== "comment")).toEqual(      first.rows.filter((row) => row.kind !== "comment"),    );    expect(first.rows.find((row) => row.id === comment.id)?.commentSectionTitle).toBe(      "Dirty worktree",    );  });   it("keeps cached IDs and targets local to each parsed section and file layout", () => {    const original = filesPatch(["example.ts", "second.ts"]);    const variants = [      buildReviewParsedDiff(original, "first-section"),      buildReviewParsedDiff(original, "second-section"),      buildReviewParsedDiff(        filesPatch(["inserted.ts", "example.ts", "second.ts"]),        "first-section",      ),      buildReviewParsedDiff(filesPatch(["second.ts", "example.ts"]), "first-section"),      buildReviewParsedDiff(filesPatch(["example.ts"]), "first-section"),      buildReviewParsedDiff(        original.replaceAll("@@ -1,2 +1,2 @@", "@@ -7,2 +12,2 @@"),        "first-section",      ),    ];    for (const diff of [...variants, variants[0]!]) {      const input = { parsedDiff: diff, comments: [makeComment("Coordinates")] };      expect(getCachedNativeReviewDiffData(input)).toEqual(buildNativeReviewDiffData(input));    }  });}); describe("createNativeReviewDiffTheme", () => {  it("serializes every native color as cross-platform opaque hex", () => {    for (const themeId of MOBILE_THEME_IDS) {      for (const appearance of ["light", "dark"] as const) {        const theme = createNativeReviewDiffTheme(          appearance,          themeId,          appTheme(themeId, appearance),        );        for (const color of Object.values(theme)) {          expect(color, `${themeId}/${appearance}`).toMatch(/^#[\da-f]{6}$/i);        }      }    }  });   it("uses the selected app palette for native code surfaces", () => {    const standard = createNativeReviewDiffTheme("dark", "t3-code", appTheme("t3-code", "dark"));    const iris = createNativeReviewDiffTheme("dark", "iris", appTheme("iris", "dark"));     expect(iris.background).not.toBe(standard.background);    expect(iris.hunkText).not.toBe(standard.hunkText);    expect(iris.addBar).toBe(standard.addBar);    expect(iris.deleteBar).toBe(standard.deleteBar);  });});