Skip to content
Merged
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
105 changes: 105 additions & 0 deletions src/entrypoints/content/unfurlers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { beforeEach, describe, expect, it, vi } from "vitest";

const { mockReplaceUrlInTextNodes } = vi.hoisted(() => ({
mockReplaceUrlInTextNodes: vi.fn(),
}));

vi.mock("./fetch", () => ({
client: vi.fn().mockResolvedValue({ name: "TestSpace", spaceKey: "test" }),
}));

vi.mock("./replaceUrlInTextNodes", () => ({
replaceUrlInTextNodes: mockReplaceUrlInTextNodes,
}));

import { gitCommitUnfurler, gitFileUnfurler } from "./unfurlers";

describe("gitFileUnfurler", () => {
beforeEach(() => {
mockReplaceUrlInTextNodes.mockClear();
});

const createAnchor = (href: string) => {
const anchor = document.createElement("a");
anchor.href = href;
return anchor;
};

it("should build title for blob URL with filename", async () => {
const anchor = createAnchor(
"https://nulab.backlog.jp/git/NAKAMURA/hackz-nulab-26/blob/main/CLAUDE.md",
);
await gitFileUnfurler(anchor);
expect(mockReplaceUrlInTextNodes).toHaveBeenCalledWith(
anchor,
"[TestSpace][NAKAMURA/hackz-nulab-26] CLAUDE.md | Git",
);
});

it("should build title for tree URL", async () => {
const anchor = createAnchor(
"https://nulab.backlog.jp/git/PROJ/my-repo/tree/main/src/components",
);
await gitFileUnfurler(anchor);
expect(mockReplaceUrlInTextNodes).toHaveBeenCalledWith(
anchor,
"[TestSpace][PROJ/my-repo] components | Git",
);
});

it("should handle nested file paths by showing only filename", async () => {
const anchor = createAnchor(
"https://nulab.backlog.jp/git/NAKAMURA/hackz-nulab-26/blob/main/src/components/App.tsx",
);
await gitFileUnfurler(anchor);
expect(mockReplaceUrlInTextNodes).toHaveBeenCalledWith(
anchor,
"[TestSpace][NAKAMURA/hackz-nulab-26] App.tsx | Git",
);
});

it("should not match non-git URLs", async () => {
const anchor = createAnchor("https://nulab.backlog.jp/view/TEST-123");
await gitFileUnfurler(anchor);
expect(mockReplaceUrlInTextNodes).not.toHaveBeenCalled();
});

it("should not match git URLs without blob or tree", async () => {
const anchor = createAnchor(
"https://nulab.backlog.jp/git/PROJ/my-repo/commits",
);
await gitFileUnfurler(anchor);
expect(mockReplaceUrlInTextNodes).not.toHaveBeenCalled();
});
});

describe("gitCommitUnfurler", () => {
beforeEach(() => {
mockReplaceUrlInTextNodes.mockClear();
});

const createAnchor = (href: string) => {
const anchor = document.createElement("a");
anchor.href = href;
return anchor;
};

it("should build title with truncated commit hash", async () => {
const anchor = createAnchor(
"https://nulab.backlog.jp/git/NAKAMURA/hackz-nulab-26/commit/d883cf51748ad8d4d864d1143657a10a27f73e17",
);
await gitCommitUnfurler(anchor);
expect(mockReplaceUrlInTextNodes).toHaveBeenCalledWith(
anchor,
"[TestSpace][NAKAMURA/hackz-nulab-26] リビジョン : d883cf5174 | Git",
);
});

it("should not match non-commit git URLs", async () => {
const anchor = createAnchor(
"https://nulab.backlog.jp/git/NAKAMURA/hackz-nulab-26/blob/main/CLAUDE.md",
);
await gitCommitUnfurler(anchor);
expect(mockReplaceUrlInTextNodes).not.toHaveBeenCalled();
});
});
23 changes: 23 additions & 0 deletions src/entrypoints/content/unfurlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ const DOCUMENT_ID_REGEX = "(?<documentId>[a-f0-9]{32})" as const;
const PROJECT_KEY_REGEX = "(?<projectKey>[A-Z0-9_]+)" as const;
const REPOSITORY_REGEX = "(?<repository>[a-z0-9-]+)" as const;
const PULL_REQUEST_NUMBER_REGEX = "(?<number>[0-9]+)" as const;
const GIT_FILE_PATH_REGEX = "(?<filePath>.+)" as const;
const GIT_COMMIT_HASH_REGEX = "(?<commitHash>[a-f0-9]+)" as const;

export const issueUnfurler = defineUnfurler({
parseUrl: (url) =>
Expand Down Expand Up @@ -127,3 +129,24 @@ export const pullRequestUnfurler = defineUnfurler({
return `[${params.projectKey}/${params.repository}#${params.number}][${pullRequest.status.name}] ${pullRequest.summary} | Pull Request`;
},
});

export const gitFileUnfurler = defineUnfurler({
parseUrl: (url) =>
regex(
`^/git/${PROJECT_KEY_REGEX}/${REPOSITORY_REGEX}/(?:blob|tree)/${GIT_FILE_PATH_REGEX}$`,
).exec(url.pathname)?.groups,
buildTitle: (params) => {
const fileName = params.filePath.split("/").pop();
return `[${params.projectKey}/${params.repository}] ${fileName} | Git`;
},
});

export const gitCommitUnfurler = defineUnfurler({
parseUrl: (url) =>
regex(
`^/git/${PROJECT_KEY_REGEX}/${REPOSITORY_REGEX}/commit/${GIT_COMMIT_HASH_REGEX}$`,
).exec(url.pathname)?.groups,
buildTitle: (params) => {
return `[${params.projectKey}/${params.repository}] リビジョン : ${params.commitHash.slice(0, 10)} | Git`;
},
});