diff --git a/__mocks__/obsidian.ts b/__mocks__/obsidian.ts index 5a64ed2..71eb0aa 100644 --- a/__mocks__/obsidian.ts +++ b/__mocks__/obsidian.ts @@ -3,6 +3,7 @@ import { PluginMock } from "./obsidian/Plugin"; import { ModalMock } from "./obsidian/Modal"; import { PluginSettingTabMock } from "./obsidian/PluginSettingTab"; import { NoticeMock } from "./obsidian/Notice"; +import { setIconMock } from "./obsidian/setIcon"; module.exports = { App: AppMock, @@ -10,4 +11,5 @@ module.exports = { Modal: ModalMock, PluginSettingTab: PluginSettingTabMock, Notice: NoticeMock, + setIcon: setIconMock, }; diff --git a/__mocks__/obsidian/setIcon.ts b/__mocks__/obsidian/setIcon.ts new file mode 100644 index 0000000..5ad2844 --- /dev/null +++ b/__mocks__/obsidian/setIcon.ts @@ -0,0 +1,5 @@ +export function setIconMock(parent: HTMLElement, iconId: string): void { + const icon = document.createElement("span"); + icon.innerText = iconId; + parent.appendChild(icon); +} diff --git a/src/inline/inline.spec.ts b/src/inline/inline.spec.ts new file mode 100644 index 0000000..410cd53 --- /dev/null +++ b/src/inline/inline.spec.ts @@ -0,0 +1,45 @@ +import { test, describe, expect, beforeAll } from "@jest/globals"; +import { elementTestSetup } from "../../test/element"; +import { createTag } from "./inline"; + +const selectors = { + org: ".github-link-inline-org", + repo: ".github-link-inline-repo", + issueTitle: ".github-link-inline-issue-title", + prTitle: ".github-link-inline-pr-title", +}; + +describe("createTag", () => { + beforeAll(() => { + elementTestSetup(); + }); + test.each([ + { url: "https://github.com/nathonius", user: "nathonius" }, + { url: "https://github.com/nathonius/", user: "nathonius" }, + { url: "https://github.com/sam.lake", user: "sam.lake" }, + { url: "https://github.com/abraham_lincoln", user: "abraham_lincoln" }, + ])("user only $user", ({ url, user }) => { + const tag = createTag(url); + expect(tag).toBeTruthy(); + const org = tag?.querySelector(selectors.org) as HTMLSpanElement; + expect(org.innerText).toEqual(user); + [selectors.repo, selectors.issueTitle, selectors.prTitle].forEach((s) => { + expect(tag?.querySelector(s)).toBeFalsy(); + }); + }); + + test.each([ + { url: "https://github.com/nathonius/obsidian-github-link", user: "nathonius", repo: "obsidian-github-link" }, + { url: "https://github.com/nathonius/obsidian.github.link", user: "nathonius", repo: "obsidian.github.link" }, + ])("repo $repo", ({ url, user, repo }) => { + const tag = createTag(url); + expect(tag).toBeTruthy(); + const org = tag?.querySelector(selectors.org) as HTMLSpanElement; + const repoEl = tag?.querySelector(selectors.repo) as HTMLSpanElement; + expect(org.innerText).toEqual(user); + expect(repoEl.innerText).toEqual(repo); + [selectors.issueTitle, selectors.prTitle].forEach((s) => { + expect(tag?.querySelector(s)).toBeFalsy(); + }); + }); +}); diff --git a/src/inline/view-plugin.spec.ts b/src/inline/view-plugin.spec.ts index cb15d77..fdc9048 100644 --- a/src/inline/view-plugin.spec.ts +++ b/src/inline/view-plugin.spec.ts @@ -1,123 +1,102 @@ import { expect, test, describe } from "@jest/globals"; -import { matchRegexp } from './view-plugin'; +import { matchRegexp } from "./view-plugin"; // Some potential URLs: const GITHUB_URLS = [ - // USER/ORG - 'https://github.com/nathonius', - // REPO - 'https://github.com/nathonius/obsidian-github-link', - 'https://github.com/nathonius/obsidian-github-link/tree/main', - 'https://github.com/joshleaves/obsidian-github-link/tree/fix/156-linking-to-file', - 'https://github.com/nathonius/obsidian-github-link/blob/main/src/github/url-parse.ts', - 'https://github.com/nathonius/obsidian-github-link/blob/main/src/inline/view-plugin.ts#L21', - 'https://github.com/nathonius/obsidian-github-link/blob/main/src/inline/view-plugin.ts#L13-32', - // ISSUES - 'https://github.com/nathonius/obsidian-github-link/issues', - 'https://github.com/nathonius/obsidian-github-link/issues/156', - // PULLS - 'https://github.com/nathonius/obsidian-github-link/pulls', - 'https://github.com/nathonius/obsidian-github-link/pull/157', - 'https://github.com/nathonius/obsidian-github-link/pull/157/commits', - 'https://github.com/nathonius/obsidian-github-link/pull/157/commits/42d35e4c7070d2ec9f3bacf7f4a0561d9d7346bb', - 'https://github.com/nathonius/obsidian-github-link/pull/157/files', - // MORE EDGE CASES - 'https://github.com/kwsch/pk3DS/blob/e40d3ce5548d75821f31785dc88cd465610530a6/pk3DS.Core/CTR/Images/BCLIM.cs', - 'https://github.com/kwsch/png2bclim/blob/master/png2bclim/BCLIM.cs', - 'https://github.com/kwsch/png2bclim', - 'https://github.com/ihaveamac/3DS-rom-tools/wiki/Extract-a-game-or-application-in-.3ds-or-.cci-format' -] + // USER/ORG + "https://github.com/nathonius", + // REPO + "https://github.com/nathonius/obsidian-github-link", + "https://github.com/nathonius/obsidian-github-link/tree/main", + "https://github.com/joshleaves/obsidian-github-link/tree/fix/156-linking-to-file", + "https://github.com/nathonius/obsidian-github-link/blob/main/src/github/url-parse.ts", + "https://github.com/nathonius/obsidian-github-link/blob/main/src/inline/view-plugin.ts#L21", + "https://github.com/nathonius/obsidian-github-link/blob/main/src/inline/view-plugin.ts#L13-32", + // ISSUES + "https://github.com/nathonius/obsidian-github-link/issues", + "https://github.com/nathonius/obsidian-github-link/issues/156", + // PULLS + "https://github.com/nathonius/obsidian-github-link/pulls", + "https://github.com/nathonius/obsidian-github-link/pull/157", + "https://github.com/nathonius/obsidian-github-link/pull/157/commits", + "https://github.com/nathonius/obsidian-github-link/pull/157/commits/42d35e4c7070d2ec9f3bacf7f4a0561d9d7346bb", + "https://github.com/nathonius/obsidian-github-link/pull/157/files", + // MORE EDGE CASES + "https://github.com/kwsch/pk3DS/blob/e40d3ce5548d75821f31785dc88cd465610530a6/pk3DS.Core/CTR/Images/BCLIM.cs", + "https://github.com/kwsch/png2bclim/blob/master/png2bclim/BCLIM.cs", + "https://github.com/kwsch/png2bclim", + "https://github.com/ihaveamac/3DS-rom-tools/wiki/Extract-a-game-or-application-in-.3ds-or-.cci-format", +]; -describe('matchRegexp', () => { - test('matches GitHub URLs on their own', () => { - GITHUB_URLS.forEach(url => { - const text = `${url}` // (...) - const match = text.match(matchRegexp); - expect(match).not.toBeNull(); - expect(match![0]).toBe(url); - }); - }); +describe("url regex match", () => { + test.each(GITHUB_URLS)("matches GitHub URLs on their own", (url) => { + const text = `${url}`; // (...) + const match = text.match(matchRegexp); + expect(match).not.toBeNull(); + expect(match![0]).toBe(url); + }); - test('does not match URLs inside markdown links', () => { - GITHUB_URLS.forEach(url => { - const text = `[A link to GitHub](${url})`; - const match = text.match(matchRegexp); - expect(match).toBeNull(); - }); - }); + test.each(GITHUB_URLS)("does not match URLs inside markdown links", (url) => { + const text = `[A link to GitHub](${url})`; + const match = text.match(matchRegexp); + expect(match).toBeNull(); + }); - test('matches URLs ending a sentence', () => { - GITHUB_URLS.forEach(url => { - const text = `That's the end of the line for ${url}.`; - const match = text.match(matchRegexp); - expect(match).not.toBeNull(); - expect(match![0]).toBe(url); - }); - }); + test.each(GITHUB_URLS)("matches URLs ending a sentence", (url) => { + const text = `That's the end of the line for ${url}.`; + const match = text.match(matchRegexp); + expect(match).not.toBeNull(); + expect(match![0]).toBe(url); + }); - test('matches URLs followed by punctuation', () => { - GITHUB_URLS.forEach(url => { - const text = `First ${url}, then some text.`; - const match = text.match(matchRegexp); - expect(match).not.toBeNull(); - expect(match![0]).toBe(url); - }); - }); + test.each(GITHUB_URLS)("matches URLs followed by punctuation", (url) => { + const text = `First ${url}, then some text.`; + const match = text.match(matchRegexp); + expect(match).not.toBeNull(); + expect(match![0]).toBe(url); + }); - test('matches URLs ending a sentence with an interrogative', () => { - GITHUB_URLS.forEach(url => { - const text = `You heard about ${url}?`; - const match = text.match(matchRegexp); - expect(match).not.toBeNull(); - expect(match![0]).toBe(url); - }); - }); + test.each(GITHUB_URLS)("matches URLs ending a sentence with an interrogative", (url) => { + const text = `You heard about ${url}?`; + const match = text.match(matchRegexp); + expect(match).not.toBeNull(); + expect(match![0]).toBe(url); + }); - test('matches URLs followed by multiple punctuation', () => { - GITHUB_URLS.forEach(url => { - const text = `A test for ${url}...`; - const match = text.match(matchRegexp); - expect(match).not.toBeNull(); - expect(match![0]).toBe(url); - }); - }); + test.each(GITHUB_URLS)("matches URLs followed by multiple punctuation", (url) => { + const text = `A test for ${url}...`; + const match = text.match(matchRegexp); + expect(match).not.toBeNull(); + expect(match![0]).toBe(url); + }); + test.each(GITHUB_URLS)("matches URLs ending a sentence that segues to another", (url) => { + const text = `That's the end of the line for ${url}. But not for this variable!`; + const match = text.match(matchRegexp); + expect(match).not.toBeNull(); + expect(match![0]).toBe(url); + }); - test('matches URLs ending a sentence that segues to another', () => { - GITHUB_URLS.forEach(url => { - const text = `That's the end of the line for ${url}. But not for this variable!`; - const match = text.match(matchRegexp); - expect(match).not.toBeNull(); - expect(match![0]).toBe(url); - }); - }); - - test('matches URLs in text with multiple lines', () => { - GITHUB_URLS.forEach(url => { - const text = `Some text + test.each(GITHUB_URLS)("matches URLs in text with multiple lines", (url) => { + const text = `Some text ${url} more text.`; - const match = text.match(matchRegexp); - expect(match).not.toBeNull(); - expect(match![0]).toBe(url); - }); - }); + const match = text.match(matchRegexp); + expect(match).not.toBeNull(); + expect(match![0]).toBe(url); + }); - test('matches URLs in text followed by another text', () => { - GITHUB_URLS.forEach(url => { - const text = `${url} is a cool plugin`; - const match = text.match(matchRegexp); - expect(match).not.toBeNull(); - expect(match![0]).toBe(url); - }) - }); + test.each(GITHUB_URLS)("matches URLs in text followed by another text", (url) => { + const text = `${url} is a cool plugin`; + const match = text.match(matchRegexp); + expect(match).not.toBeNull(); + expect(match![0]).toBe(url); + }); - test('matches URLs quoted', () => { - GITHUB_URLS.forEach(url => { - const text = `Obama chuckled: you mean "${url}" ?`; - const match = text.match(matchRegexp); - expect(match).not.toBeNull(); - expect(match![0]).toBe(url); - }) - }); + test.each(GITHUB_URLS)("matches URLs quoted", (url) => { + const text = `Obama chuckled: you mean "${url}" ?`; + const match = text.match(matchRegexp); + expect(match).not.toBeNull(); + expect(match![0]).toBe(url); + }); }); diff --git a/test/element.ts b/test/element.ts new file mode 100644 index 0000000..2498bfd --- /dev/null +++ b/test/element.ts @@ -0,0 +1,66 @@ +/** + * Local version of obsidian's 'createEl' function, copied from + * https://ryotaushio.github.io/the-hobbyist-dev/obsidian/api-&-internals/createel.html + */ +function createEl( + tag: K, + options?: string | DomElementInfo, + callback?: (el: HTMLElementTagNameMap[K]) => void, +): HTMLElementTagNameMap[K] { + const el = document.createElement(tag); + if (typeof options === "string") options = { cls: options }; + options = options || {}; + + if (options.cls) Array.isArray(options.cls) ? (el.className = options.cls.join(" ")) : (el.className = options.cls); + if (options.text) el.setText(options.text); + if (options.attr) { + for (const [k, v] of Object.entries(options.attr)) { + el.setAttribute(k, String(v)); + } + } + if (options.title !== undefined) el.title = options.title; + if ( + options.value !== undefined && + (el instanceof HTMLInputElement || el instanceof HTMLSelectElement || el instanceof HTMLOptionElement) + ) { + el.value = options.value; + } + if (options.type && el instanceof HTMLInputElement) el.type = options.type; + if (options.type && el instanceof HTMLStyleElement) el.setAttribute("type", options.type); + if (options.placeholder && el instanceof HTMLInputElement) el.placeholder = options.placeholder; + if ( + options.href && + (el instanceof HTMLAnchorElement || el instanceof HTMLLinkElement || el instanceof HTMLBaseElement) + ) + el.href = options.href; + callback?.(el); + if (options.parent) { + if (options.prepend) options.parent.insertBefore(el, options.parent.firstChild); + else options.parent.appendChild(el); + } + // add event listeners (e.g. { onclick: () => {} }) + for (const key in options) { + if (Object.prototype.hasOwnProperty.call(options, key) && key.startsWith("on")) { + const value = options[key as keyof DomElementInfo]; + if (typeof value === "function") el.addEventListener(key.substring(2), value); + } + } + return el; +} + +function createSpan(options?: string | DomElementInfo, callback?: (el: HTMLSpanElement) => void) { + return createEl("span", options, callback); +} + +function createDiv(options?: string | DomElementInfo, callback?: (el: HTMLDivElement) => void) { + return createEl("div", options, callback); +} + +export function elementTestSetup() { + HTMLElement.prototype.setText = function (text: string) { + this.innerText = text; + }; + window.createEl = createEl; + window.createSpan = createSpan; + window.createDiv = createDiv; +}