From de2cb68b571c1085fe6660563992b758f449664c Mon Sep 17 00:00:00 2001 From: lihbr Date: Thu, 5 Dec 2024 17:43:41 +0100 Subject: [PATCH] refactor!: --- src/PrismicLink.vue | 192 +++++++ test/components-PrismicLink.test.ts | 807 ++++++++++++++++------------ 2 files changed, 657 insertions(+), 342 deletions(-) create mode 100644 src/PrismicLink.vue diff --git a/src/PrismicLink.vue b/src/PrismicLink.vue new file mode 100644 index 0000000..acb6d28 --- /dev/null +++ b/src/PrismicLink.vue @@ -0,0 +1,192 @@ + + + diff --git a/test/components-PrismicLink.test.ts b/test/components-PrismicLink.test.ts index 168542e..40df2d0 100644 --- a/test/components-PrismicLink.test.ts +++ b/test/components-PrismicLink.test.ts @@ -1,7 +1,5 @@ -import { expect, it, vi } from "vitest" +import { describe, expect, it, vi } from "vitest" -import * as mock from "@prismicio/mock" -import type { LinkField } from "@prismicio/client" import { LinkType } from "@prismicio/client" import { mount } from "@vue/test-utils" import { markRaw } from "vue" @@ -12,437 +10,563 @@ import { } from "./__fixtures__/WrapperComponent" import router from "./__fixtures__/router" -import { createPrismic } from "../src" -import { PrismicLinkImpl } from "../src/components" +import { PrismicLink, createPrismic } from "../src" -it("renders empty link field", () => { - const wrapper = mount(PrismicLinkImpl, { - props: { - field: mock.value.link({ seed: 1, state: "empty", type: LinkType.Any }), - }, - slots: { default: "foo" }, - }) - - expect(wrapper.html()).toBe('foo') -}) - -it("renders link to web field", () => { - const wrapper = mount(PrismicLinkImpl, { - props: { - field: { - ...mock.value.link({ - seed: 2, - type: LinkType.Web, - withTargetBlank: false, - }), - url: "https://example.com", +describe("renders a link field", () => { + it("empty", (ctx) => { + const wrapper = mount(PrismicLink, { + props: { + field: ctx.mock.value.link({ state: "empty", type: LinkType.Any }), }, - }, - slots: { default: "foo" }, - }) + slots: { default: "foo" }, + }) - expect(wrapper.html()).toBe('foo') -}) + expect(wrapper.html()).toBe("foo") + }) -it("renders link to media field", () => { - const wrapper = mount(PrismicLinkImpl, { - props: { - field: { - ...mock.value.link({ seed: 3, type: LinkType.Media }), - url: "https://example.com/image.png", + it("link to web", (ctx) => { + const wrapper = mount(PrismicLink, { + props: { + field: { + ...ctx.mock.value.link({ + type: LinkType.Web, + withTargetBlank: false, + }), + url: "https://example.com", + }, }, - }, - slots: { default: "foo" }, - }) + slots: { default: "foo" }, + }) - expect(wrapper.html()).toBe('foo') -}) + expect(wrapper.html()).toBe( + 'foo', + ) + }) -it("renders link to document field", () => { - const wrapper = mount(PrismicLinkImpl, { - props: { - field: { - ...mock.value.link({ seed: 4, type: LinkType.Document }), - url: "/bar", + it("link to web (blank target)", (ctx) => { + const wrapper = mount(PrismicLink, { + props: { + field: { + ...ctx.mock.value.link({ + type: LinkType.Web, + withTargetBlank: true, + }), + url: "https://example.com", + }, }, - }, - slots: { default: "foo" }, - global: { - plugins: [router], - }, - }) + slots: { default: "foo" }, + }) - expect(wrapper.html()).toBe('foo') -}) + expect(wrapper.html()).toBe( + 'foo', + ) + }) -it("renders document as link", (ctx) => { - const wrapper = mount(PrismicLinkImpl, { - props: { - field: { - ...mock.value.document({ seed: ctx.task.name }), - url: "/bar", + it("link to media", (ctx) => { + const wrapper = mount(PrismicLink, { + props: { + field: { + ...ctx.mock.value.link({ type: LinkType.Media }), + url: "https://example.com/image.png", + }, }, - }, - slots: { default: "foo" }, - global: { - plugins: [router], - }, - }) + slots: { default: "foo" }, + }) - expect(wrapper.html()).toBe('foo') -}) + expect(wrapper.html()).toBe( + 'foo', + ) + }) -it("renders link text when slot is not provided", () => { - const wrapper = mount(PrismicLinkImpl, { - props: { - field: { - ...mock.value.link({ - seed: 2, - type: LinkType.Web, - withTargetBlank: false, - model: { type: "Link", config: { allowText: true } }, - withText: true, - }), - url: "https://example.com", - text: "bar", + it("content relationship", (ctx) => { + const wrapper = mount(PrismicLink, { + props: { + field: { + ...ctx.mock.value.link({ type: LinkType.Document }), + url: "/bar", + }, }, - }, - }) + slots: { default: "foo" }, + global: { + plugins: [router], + }, + }) - expect(wrapper.html()).toBe('bar') + expect(wrapper.html()).toBe('foo') + }) }) -it("renders slot over link text when slot is provided", () => { - const wrapper = mount(PrismicLinkImpl, { - props: { - field: { - ...mock.value.link({ - seed: 2, - type: LinkType.Web, - withTargetBlank: false, - model: { type: "Link", config: { allowText: true } }, - withText: true, - }), - url: "https://example.com", - text: "bar", +describe("renders a document as link", () => { + it("resolvable", (ctx) => { + const wrapper = mount(PrismicLink, { + props: { + document: { + ...ctx.mock.value.document(), + url: "/bar", + }, }, - }, - slots: { default: "foo" }, - }) + slots: { default: "foo" }, + global: { + plugins: [router], + }, + }) - expect(wrapper.html()).toBe('foo') -}) + expect(wrapper.html()).toBe('foo') + }) -it("renders non-resolvable document as link", (ctx) => { - const wrapper = mount(PrismicLinkImpl, { - props: { - field: { - ...mock.value.document({ seed: ctx.task.name }), - url: null, + it("non-resolvable", (ctx) => { + const wrapper = mount(PrismicLink, { + props: { + document: { + ...ctx.mock.value.document(), + url: null, + }, }, - }, - slots: { default: "foo" }, - global: { - plugins: [router], - }, - }) + slots: { default: "foo" }, + }) - expect(wrapper.html()).toBe('foo') + expect(wrapper.html()).toBe("foo") + }) }) -it("uses plugin provided link resolver", () => { - const spiedLinkResolver = vi.fn(() => "/bar") +describe("renders link content", () => { + it("with link text", (ctx) => { + const wrapper = mount(PrismicLink, { + props: { + field: { + ...ctx.mock.value.link({ + type: LinkType.Web, + withTargetBlank: false, + model: { type: "Link", config: { allowText: true } }, + withText: true, + }), + url: "https://example.com", + text: "bar", + }, + }, + }) - const prismic = createPrismic({ - endpoint: "test", - linkResolver: spiedLinkResolver, + expect(wrapper.html()).toBe( + 'bar', + ) }) - const wrapper = mount(PrismicLinkImpl, { - props: { - field: { - ...mock.value.link({ seed: 5, type: LinkType.Document }), - url: undefined, + it("with slot (priority over link text)", (ctx) => { + const wrapper = mount(PrismicLink, { + props: { + field: { + ...ctx.mock.value.link({ + type: LinkType.Web, + withTargetBlank: false, + model: { type: "Link", config: { allowText: true } }, + withText: true, + }), + url: "https://example.com", + text: "bar", + }, }, - }, - slots: { default: "foo" }, - global: { - plugins: [router, prismic], - }, - }) + slots: { default: "foo" }, + }) - expect(spiedLinkResolver).toHaveBeenCalledOnce() - expect(wrapper.html()).toBe('foo') + expect(wrapper.html()).toBe( + 'foo', + ) + }) }) -it("uses provided link resolver over plugin provided", () => { - const spiedLinkResolver1 = vi.fn(() => "/bar") - const spiedLinkResolver2 = vi.fn(() => "/baz") +describe("uses link resolver", () => { + it("from plugin", (ctx) => { + const spiedLinkResolver = vi.fn(() => "/bar") + + const prismic = createPrismic({ + endpoint: "test", + linkResolver: spiedLinkResolver, + }) + + const wrapper = mount(PrismicLink, { + props: { + field: { + ...ctx.mock.value.link({ type: LinkType.Document }), + url: undefined, + }, + }, + slots: { default: "foo" }, + global: { + plugins: [router, prismic], + }, + }) - const prismic = createPrismic({ - endpoint: "test", - linkResolver: spiedLinkResolver1, + expect(spiedLinkResolver).toHaveBeenCalledOnce() + expect(wrapper.html()).toBe('foo') }) - const wrapper = mount(PrismicLinkImpl, { - props: { - field: { - ...mock.value.link({ seed: 6, type: LinkType.Document }), - url: undefined, + it("from props (priority over plugin)", (ctx) => { + const spiedLinkResolver1 = vi.fn(() => "/bar") + const spiedLinkResolver2 = vi.fn(() => "/baz") + + const prismic = createPrismic({ + endpoint: "test", + linkResolver: spiedLinkResolver1, + }) + + const wrapper = mount(PrismicLink, { + props: { + field: { + ...ctx.mock.value.link({ type: LinkType.Document }), + url: undefined, + }, + linkResolver: spiedLinkResolver2, }, - linkResolver: spiedLinkResolver2, - }, - slots: { default: "foo" }, - global: { - plugins: [router, prismic], - }, - }) + slots: { default: "foo" }, + global: { + plugins: [router, prismic], + }, + }) - expect(spiedLinkResolver1).not.toHaveBeenCalled() - expect(spiedLinkResolver2).toHaveBeenCalledOnce() - expect(wrapper.html()).toBe('foo') + expect(spiedLinkResolver1).not.toHaveBeenCalled() + expect(spiedLinkResolver2).toHaveBeenCalledOnce() + expect(wrapper.html()).toBe('foo') + }) }) -it("renders link with blank target", () => { - const wrapper = mount(PrismicLinkImpl, { - props: { - field: { - ...mock.value.link({ - seed: 7, - type: LinkType.Web, - withTargetBlank: true, - }), - url: "https://example.com", +describe("renders rel attribute", () => { + it("omitted on internal links", (ctx) => { + const wrapper = mount(PrismicLink, { + props: { + field: { + ...ctx.mock.value.link({ type: LinkType.Document }), + url: "/bar", + }, }, - }, - slots: { default: "foo" }, + global: { + plugins: [router], + }, + }) + + expect(wrapper.html()).not.toContain('rel="') }) - expect(wrapper.html()).toBe( - 'foo', - ) -}) + it("with default value on external links", (ctx) => { + const wrapper = mount(PrismicLink, { + props: { + field: { + ...ctx.mock.value.link({ + type: LinkType.Web, + withTargetBlank: false, + }), + url: "https://example.com", + }, + }, + }) -it("renders link with blank target using plugin provided default rel attribute", () => { - const prismic = createPrismic({ - endpoint: "test", - components: { - linkBlankTargetRelAttribute: "bar", - }, + expect(wrapper.html()).toContain('rel="noreferrer"') }) - const wrapper = mount(PrismicLinkImpl, { - props: { - field: { - ...mock.value.link({ - seed: 8, - type: LinkType.Web, - withTargetBlank: true, - }), - url: "https://example.com", + it("with plugin options", (ctx) => { + const prismic = createPrismic({ + endpoint: "test", + components: { + linkRel: () => "plugin", }, - }, - slots: { default: "foo" }, - global: { - plugins: [router, prismic], - }, + }) + + const wrapper = mount(PrismicLink, { + props: { + field: { + ...ctx.mock.value.link({ + type: LinkType.Web, + withTargetBlank: false, + }), + url: "https://example.com", + }, + }, + global: { + plugins: [router, prismic], + }, + }) + + expect(wrapper.html()).toContain(`rel="plugin"`) }) - expect(wrapper.html()).toBe( - 'foo', - ) -}) + it("with props function (priority over plugin)", (ctx) => { + const prismic = createPrismic({ + endpoint: "test", + components: { + linkRel: () => "plugin", + }, + }) + + const wrapper = mount(PrismicLink, { + props: { + field: { + ...ctx.mock.value.link({ + type: LinkType.Web, + withTargetBlank: false, + }), + url: "https://example.com", + }, + rel: () => "props", + }, + global: { + plugins: [router, prismic], + }, + }) -it("renders link with blank target using provided default rel attribute over plugin provided", () => { - const prismic = createPrismic({ - endpoint: "test", - components: { - linkBlankTargetRelAttribute: "bar", - }, + expect(wrapper.html()).toContain(`rel="props"`) }) - const wrapper = mount(PrismicLinkImpl, { - props: { - field: { - ...mock.value.link({ - seed: 9, - type: LinkType.Web, - withTargetBlank: true, - }), - url: "https://example.com", + it("with props string value (priority over plugin)", (ctx) => { + const prismic = createPrismic({ + endpoint: "test", + components: { + linkRel: () => "plugin", }, - blankTargetRelAttribute: "baz", - }, - slots: { default: "foo" }, - global: { - plugins: [router, prismic], - }, - }) + }) + + const wrapper = mount(PrismicLink, { + props: { + field: { + ...ctx.mock.value.link({ + type: LinkType.Web, + withTargetBlank: false, + }), + url: "https://example.com", + }, + rel: "props", + }, + global: { + plugins: [router, prismic], + }, + }) - expect(wrapper.html()).toBe( - 'foo', - ) + expect(wrapper.html()).toContain(`rel="props"`) + }) }) -it("uses provided blank and rel attribute", () => { - const wrapper = mount(PrismicLinkImpl, { - props: { - field: { - ...mock.value.link({ seed: 10, type: LinkType.Web }), - url: "https://example.com", +describe("renders external links using component", () => { + it("from plugin", (ctx) => { + const prismic = createPrismic({ + endpoint: "test", + components: { + linkExternalComponent: WrapperComponent, }, - target: "bar", - rel: "baz", - }, - slots: { default: "foo" }, - }) + }) + + const wrapper = mount(PrismicLink, { + props: { + field: { + ...ctx.mock.value.link({ + type: LinkType.Web, + withTargetBlank: false, + }), + url: "https://example.com", + }, + }, + global: { plugins: [prismic] }, + }) - expect(wrapper.html()).toBe( - 'foo', - ) -}) + expect(wrapper.html()).toBe( + '
Cursus sit
', + ) + }) -it("forwards blank and rel attribute to component links", () => { - const wrapper = mount(PrismicLinkImpl, { - props: { - field: { - ...mock.value.link({ - seed: 10, - type: LinkType.Web, - withTargetBlank: true, - }), - url: "https://example.com", + it("from props (priority over plugin)", (ctx) => { + const prismic = createPrismic({ + endpoint: "test", + components: { + linkExternalComponent: createWrapperComponent(1), }, - externalComponent: markRaw(WrapperComponent), - }, - slots: { default: "foo" }, + }) + + const wrapper = mount(PrismicLink, { + props: { + field: { + ...ctx.mock.value.link({ + type: LinkType.Web, + withTargetBlank: false, + }), + url: "https://example.com", + }, + externalComponent: markRaw(createWrapperComponent(2)), + }, + global: { plugins: [prismic] }, + }) + + expect(wrapper.html()).toBe( + '
', + ) }) - expect(wrapper.html()).toBe( - '
foo
', - ) -}) + it("forwards attributes", (ctx) => { + const wrapper = mount(PrismicLink, { + props: { + field: { + ...ctx.mock.value.link({ + type: LinkType.Web, + withTargetBlank: true, + }), + url: "https://example.com", + }, + externalComponent: markRaw(WrapperComponent), + }, + slots: { default: "foo" }, + }) -it("uses plugin provided external link component on external link", () => { - const prismic = createPrismic({ - endpoint: "test", - components: { - linkExternalComponent: WrapperComponent, - }, + expect(wrapper.html()).toBe( + '
foo
', + ) }) +}) - const wrapper = mount(PrismicLinkImpl, { - props: { - field: { - ...mock.value.link({ - seed: 11, - type: LinkType.Web, - withTargetBlank: false, - }), - url: "https://example.com", +describe("renders internal links using component", () => { + it("from plugin", (ctx) => { + const prismic = createPrismic({ + endpoint: "test", + components: { + linkInternalComponent: WrapperComponent, }, - }, - global: { plugins: [prismic] }, + }) + + const wrapper = mount(PrismicLink, { + props: { + field: { + ...ctx.mock.value.link({ type: LinkType.Document }), + url: "/bar", + }, + }, + global: { plugins: [prismic] }, + }) + + expect(wrapper.html()).toBe( + '
', + ) }) - expect(wrapper.html()).toBe( - '
', - ) -}) + it("from props (priority over plugin)", (ctx) => { + const prismic = createPrismic({ + endpoint: "test", + components: { + linkInternalComponent: createWrapperComponent(1), + }, + }) + + const wrapper = mount(PrismicLink, { + props: { + field: { + ...ctx.mock.value.link({ type: LinkType.Document }), + url: "/bar", + }, + internalComponent: markRaw(createWrapperComponent(2)), + }, + global: { plugins: [prismic] }, + }) -it("uses provided external link component over plugin provided on external link", () => { - const prismic = createPrismic({ - endpoint: "test", - components: { - linkExternalComponent: createWrapperComponent(1), - }, + expect(wrapper.html()).toBe( + '
', + ) }) +}) - const wrapper = mount(PrismicLinkImpl, { - props: { - field: { - ...mock.value.link({ - seed: 12, - type: LinkType.Web, - withTargetBlank: false, - }), - url: "https://example.com", +it("throws when trying to render a non-link field", () => { + // The warning only logs in "development". + const originalNodeEnv = process.env.NODE_ENV + process.env.NODE_ENV = "development" + const consoleErrorSpy = vi + .spyOn(console, "error") + .mockImplementation(() => void 0) + + expect(() => + mount(PrismicLink, { + props: { + // @ts-expect-error - Purposely giving incompatible props. + field: {}, }, - externalComponent: markRaw(createWrapperComponent(2)), - }, - global: { plugins: [prismic] }, - }) + }), + ).toThrowError(/missing-link-properties/i) - expect(wrapper.html()).toBe( - '
', - ) + consoleErrorSpy.mockRestore() + process.env.NODE_ENV = originalNodeEnv }) -it("uses plugin provided internal link component on internal link", () => { - const prismic = createPrismic({ - endpoint: "test", - components: { - linkInternalComponent: WrapperComponent, - }, - }) +it("warns when trying to render an invalid link field", () => { + // The warning only logs in "development". + const originalNodeEnv = process.env.NODE_ENV + process.env.NODE_ENV = "development" + const consoleWarnSpy = vi + .spyOn(console, "warn") + .mockImplementation(() => void 0) - const wrapper = mount(PrismicLinkImpl, { + mount(PrismicLink, { props: { - field: { - ...mock.value.link({ seed: 13, type: LinkType.Document }), - url: "/bar", - }, + field: { link_type: "Any", foo: "bar" }, }, - global: { plugins: [prismic] }, }) - expect(wrapper.html()).toBe('
') -}) + expect(consoleWarnSpy).toHaveBeenCalledWith( + expect.stringMatching(/The provided field is missing required properties/i), + expect.anything(), + ) + expect(consoleWarnSpy).toHaveBeenCalledWith( + expect.stringMatching(/missing-link-properties/i), + expect.anything(), + ) -it("uses provided internal link component over plugin provided on internal link", () => { - const prismic = createPrismic({ - endpoint: "test", - components: { - linkInternalComponent: createWrapperComponent(1), - }, - }) + consoleWarnSpy.mockClear() - const wrapper = mount(PrismicLinkImpl, { + mount(PrismicLink, { props: { - field: { - ...mock.value.link({ seed: 14, type: LinkType.Document }), - url: "/bar", - }, - internalComponent: markRaw(createWrapperComponent(2)), + field: { link_type: "Any", text: "foo", foo: "bar" }, }, - global: { plugins: [prismic] }, }) - expect(wrapper.html()).toBe('
') + expect(consoleWarnSpy).toHaveBeenCalledWith( + expect.stringMatching(/The provided field is missing required properties/i), + expect.anything(), + ) + expect(consoleWarnSpy).toHaveBeenCalledWith( + expect.stringMatching(/missing-link-properties/i), + expect.anything(), + ) + + consoleWarnSpy.mockRestore() + process.env.NODE_ENV = originalNodeEnv }) -it("renders nothing when invalid", () => { - vi.stubGlobal("console", { warn: vi.fn() }) +it("warns when trying to render an invalid document", () => { + // The warning only logs in "development". + const originalNodeEnv = process.env.NODE_ENV + process.env.NODE_ENV = "development" + const consoleWarnSpy = vi + .spyOn(console, "warn") + .mockImplementation(() => void 0) - const wrapper = mount(PrismicLinkImpl, { - props: { field: null as unknown as LinkField }, - slots: { default: "foo" }, + mount(PrismicLink, { + props: { + // @ts-expect-error - Purposely giving incompatible props. + document: {}, + }, }) - expect(wrapper.html()).toBe("") - expect(console.warn).toHaveBeenCalledOnce() - expect(vi.mocked(console.warn).mock.calls[0][0]).toMatch( - /Invalid prop: type check failed for prop/i, + expect(consoleWarnSpy).toHaveBeenCalledWith( + expect.stringMatching( + /The provided document is missing required properties/i, + ), + expect.anything(), + ) + expect(consoleWarnSpy).toHaveBeenCalledWith( + expect.stringMatching(/missing-link-properties/i), + expect.anything(), ) - vi.resetAllMocks() + consoleWarnSpy.mockRestore() + process.env.NODE_ENV = originalNodeEnv }) -it("reacts to changes properly", async () => { - const wrapper = mount(PrismicLinkImpl, { +it("reacts to changes properly", async (ctx) => { + const wrapper = mount(PrismicLink, { props: { field: { - ...mock.value.link({ seed: 15, type: LinkType.Web }), + ...ctx.mock.value.link({ type: LinkType.Web }), url: "https://example.com", }, }, @@ -453,8 +577,7 @@ it("reacts to changes properly", async () => { await wrapper.setProps({ field: { - ...mock.value.link({ - seed: 16, + ...ctx.mock.value.link({ type: LinkType.Web, withTargetBlank: false, }), @@ -467,6 +590,6 @@ it("reacts to changes properly", async () => { expect(secondRender).not.toBe(firstRender) expect(secondRender).toMatchInlineSnapshot( - `"foo"`, + `"foo"`, ) })