${image
@@ -32,7 +52,13 @@ export function externalLinkTemplate({
/>
${isVideoLink(url) ? playIconTemplate() : ""}
`
- : ""}
+ : isVideoLink(url)
+ ? html`
${title || url}
diff --git a/src/js/templates/postEmbed.template.js b/src/js/templates/postEmbed.template.js
index b591fd8e..8df1ad7a 100644
--- a/src/js/templates/postEmbed.template.js
+++ b/src/js/templates/postEmbed.template.js
@@ -414,16 +414,17 @@ function youtubeEmbedTemplate({ youtubeVideo, external }) {
? YOUTUBE_SHORT_ASPECT_RATIO
: YOUTUBE_VIDEO_ASPECT_RATIO;
return html` {
e.stopPropagation();
- e.preventDefault();
}}
>`;
}
diff --git a/tests/e2e/specs/concerns/postEmbeds.test.js b/tests/e2e/specs/concerns/postEmbeds.test.js
index 41dd9c59..a637cef6 100644
--- a/tests/e2e/specs/concerns/postEmbeds.test.js
+++ b/tests/e2e/specs/concerns/postEmbeds.test.js
@@ -373,11 +373,18 @@ test.describe("Post embeds view — YouTube", () => {
const embed = view.locator('[data-testid="youtube-embed"]');
await expect(embed).toBeVisible({ timeout: 10000 });
await expect(embed).toHaveAttribute("data-teststate", "preview");
+ const card = embed.locator('[data-testid="external-link"]');
+ await expect(
+ card.locator('[data-testid="external-link-title"]'),
+ ).toHaveText("A video");
+ await expect(
+ card.locator('[data-testid="external-link-domain"]'),
+ ).toHaveText("www.youtube.com");
await expect(
view.locator('[data-testid="youtube-embed-iframe"]'),
).toHaveCount(0);
- await view.locator('[data-testid="youtube-embed-play"]').click();
+ await card.locator("a").click();
const iframe = view.locator('[data-testid="youtube-embed-iframe"]');
await expect(iframe).toBeVisible();
diff --git a/tests/unit/specs/components/youtube-embed.test.js b/tests/unit/specs/components/youtube-embed.test.js
index 7c318f6d..777a6cdd 100644
--- a/tests/unit/specs/components/youtube-embed.test.js
+++ b/tests/unit/specs/components/youtube-embed.test.js
@@ -8,55 +8,97 @@ t.beforeEach(() => {
document.body.innerHTML = "";
});
-function createEmbed({ start = "0", thumb = "" } = {}) {
+function createEmbed({
+ start = "0",
+ thumb = "",
+ aspectRatio = String(9 / 16),
+} = {}) {
const element = document.createElement("youtube-embed");
element.setAttribute("video-id", "dQw4w9WgXcQ");
element.setAttribute("start", start);
element.setAttribute("thumb", thumb);
element.setAttribute("video-title", "Test video");
+ element.setAttribute("url", "https://www.youtube.com/watch?v=dQw4w9WgXcQ");
+ element.setAttribute("description", "Test description");
+ if (aspectRatio !== null) {
+ element.setAttribute("aspect-ratio", aspectRatio);
+ }
document.body.appendChild(element);
return element;
}
+function getCardLink(element) {
+ return element.querySelector("[data-testid='external-link'] a");
+}
+
t.describe("YoutubeEmbed - preview state", (it) => {
- it("renders a play button and no iframe", () => {
+ it("renders an external link card and no iframe", () => {
const element = createEmbed();
- assert(
- element.querySelector("[data-testid='youtube-embed-play']") !== null,
- );
+ assert(element.querySelector("[data-testid='external-link']") !== null);
assertEquals(element.querySelector("iframe"), null);
assertEquals(element.dataset.teststate, "preview");
});
- it("renders the thumbnail when provided", () => {
- const element = createEmbed({ thumb: "https://example.com/thumb.jpg" });
- const thumb = element.querySelector(".youtube-embed-thumb");
- assert(thumb !== null);
- assertEquals(thumb.getAttribute("src"), "https://example.com/thumb.jpg");
+ it("links the card to the original video URL", () => {
+ const element = createEmbed();
+ assertEquals(
+ getCardLink(element).getAttribute("href"),
+ "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
+ );
});
- it("renders no thumbnail image when thumb is empty", () => {
+ it("labels the card link with the video title", () => {
const element = createEmbed();
- assertEquals(element.querySelector(".youtube-embed-thumb"), null);
- assert(
- element.querySelector("[data-testid='youtube-embed-play']") !== null,
+ assertEquals(
+ getCardLink(element).getAttribute("aria-label"),
+ "Play YouTube video: Test video",
);
});
- it("labels the play button with the video title", () => {
+ it("renders the title, description, and domain", () => {
const element = createEmbed();
- const button = element.querySelector("[data-testid='youtube-embed-play']");
assertEquals(
- button.getAttribute("aria-label"),
- "Play YouTube video: Test video",
+ element
+ .querySelector("[data-testid='external-link-title']")
+ .textContent.trim(),
+ "Test video",
+ );
+ assertEquals(
+ element
+ .querySelector("[data-testid='external-link-description']")
+ .textContent.trim(),
+ "Test description",
+ );
+ assertEquals(
+ element
+ .querySelector("[data-testid='external-link-domain']")
+ .textContent.trim(),
+ "www.youtube.com",
+ );
+ });
+
+ it("renders the thumbnail with a play icon when provided", () => {
+ const element = createEmbed({ thumb: "https://example.com/thumb.jpg" });
+ const thumb = element.querySelector(".external-link-image");
+ assert(thumb !== null);
+ assertEquals(thumb.getAttribute("src"), "https://example.com/thumb.jpg");
+ assert(element.querySelector(".play-icon") !== null);
+ });
+
+ it("renders a play icon placeholder instead of an image when thumb is empty", () => {
+ const element = createEmbed();
+ assertEquals(element.querySelector(".external-link-image"), null);
+ assert(
+ element.querySelector(".external-link-video-placeholder .play-icon") !==
+ null,
);
});
});
t.describe("YoutubeEmbed - playing state", (it) => {
- it("swaps in the player iframe when the play button is clicked", () => {
+ it("swaps in the player iframe when the card is clicked", () => {
const element = createEmbed({ start: "32" });
- element.querySelector("[data-testid='youtube-embed-play']").click();
+ getCardLink(element).click();
const iframe = element.querySelector(
"[data-testid='youtube-embed-iframe']",
);
@@ -66,15 +108,38 @@ t.describe("YoutubeEmbed - playing state", (it) => {
"https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ?autoplay=1&start=32&rel=0&playsinline=1",
);
assertEquals(element.dataset.teststate, "playing");
- assertEquals(
- element.querySelector("[data-testid='youtube-embed-play']"),
- null,
- );
+ assert(element.classList.contains("is-playing"));
+ assertEquals(element.style.aspectRatio, String(9 / 16));
+ assertEquals(element.querySelector("[data-testid='external-link']"), null);
+ });
+
+ it("applies no aspect ratio in the preview state", () => {
+ const element = createEmbed();
+ assertEquals(element.style.aspectRatio, "");
+ });
+
+ it("falls back to a 16:9 aspect ratio when the attribute is missing", () => {
+ const element = createEmbed({ aspectRatio: null });
+ getCardLink(element).click();
+ assertEquals(element.style.aspectRatio, String(16 / 9));
+ });
+
+ it("does not play on a modified click so the link can open normally", () => {
+ const element = createEmbed();
+ const event = new window.MouseEvent("click", {
+ bubbles: true,
+ cancelable: true,
+ metaKey: true,
+ });
+ getCardLink(element).dispatchEvent(event);
+ assertEquals(event.defaultPrevented, false);
+ assertEquals(element.dataset.teststate, "preview");
+ assertEquals(element.querySelector("iframe"), null);
});
- it("moves focus to the iframe when the play button is clicked", () => {
+ it("moves focus to the iframe when the card is clicked", () => {
const element = createEmbed();
- element.querySelector("[data-testid='youtube-embed-play']").click();
+ getCardLink(element).click();
assertEquals(
document.activeElement,
element.querySelector("[data-testid='youtube-embed-iframe']"),
@@ -83,7 +148,7 @@ t.describe("YoutubeEmbed - playing state", (it) => {
it("falls back to start=0 for a non-numeric start attribute", () => {
const element = createEmbed({ start: "1m30s" });
- element.querySelector("[data-testid='youtube-embed-play']").click();
+ getCardLink(element).click();
const iframe = element.querySelector(
"[data-testid='youtube-embed-iframe']",
);
diff --git a/tests/unit/specs/templates/externalLink.template.test.js b/tests/unit/specs/templates/externalLink.template.test.js
index cc65fe1e..11b0b155 100644
--- a/tests/unit/specs/templates/externalLink.template.test.js
+++ b/tests/unit/specs/templates/externalLink.template.test.js
@@ -198,7 +198,7 @@ t.describe("externalLinkTemplate", (it) => {
assertEquals(container.querySelector(".play-icon"), null);
});
- it("should not render a play icon for a video link without an image", () => {
+ it("should render a play icon placeholder for a video link without an image", () => {
const result = externalLinkTemplate({
url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
title: "Example Video",
@@ -206,7 +206,132 @@ t.describe("externalLinkTemplate", (it) => {
});
const container = document.createElement("div");
render(result, container);
- assertEquals(container.querySelector(".play-icon"), null);
+ const placeholder = container.querySelector(
+ ".external-link-video-placeholder",
+ );
+ assert(placeholder !== null);
+ assert(placeholder.querySelector(".play-icon") !== null);
+ assertEquals(container.querySelector(".external-link-image"), null);
+ });
+
+ it("should not render the video placeholder for a non-video link without an image", () => {
+ const result = externalLinkTemplate({
+ url: "https://example.com/article",
+ title: "Example",
+ description: "Test",
+ });
+ const container = document.createElement("div");
+ render(result, container);
+ assertEquals(
+ container.querySelector(".external-link-video-placeholder"),
+ null,
+ );
+ });
+
+ it("should call onClick and prevent navigation on a plain click", () => {
+ let clicked = false;
+ const result = externalLinkTemplate({
+ url: "https://example.com",
+ title: "Example",
+ description: "Test",
+ onClick: () => {
+ clicked = true;
+ },
+ });
+ const container = document.createElement("div");
+ render(result, container);
+ const link = container.querySelector("a");
+ const event = new window.MouseEvent("click", {
+ cancelable: true,
+ bubbles: true,
+ });
+ link.dispatchEvent(event);
+ assertEquals(clicked, true);
+ assertEquals(event.defaultPrevented, true);
+ });
+
+ it("should not call onClick on a modified click so the link opens normally", () => {
+ for (const modifier of ["metaKey", "ctrlKey", "shiftKey", "altKey"]) {
+ let clicked = false;
+ const result = externalLinkTemplate({
+ url: "https://example.com",
+ title: "Example",
+ description: "Test",
+ onClick: () => {
+ clicked = true;
+ },
+ });
+ const container = document.createElement("div");
+ render(result, container);
+ const link = container.querySelector("a");
+ const event = new window.MouseEvent("click", {
+ cancelable: true,
+ bubbles: true,
+ [modifier]: true,
+ });
+ link.dispatchEvent(event);
+ assert(clicked === false, `onClick fired on a ${modifier} click`);
+ assertEquals(event.defaultPrevented, false);
+ }
+ });
+
+ it("should label the link with ariaLabel when provided", () => {
+ const result = externalLinkTemplate({
+ url: "https://example.com",
+ title: "Example",
+ description: "Test",
+ ariaLabel: "Play Example",
+ });
+ const container = document.createElement("div");
+ render(result, container);
+ assertEquals(
+ container.querySelector("a").getAttribute("aria-label"),
+ "Play Example",
+ );
+ });
+
+ it("should default the aria-label to the title", () => {
+ const result = externalLinkTemplate({
+ url: "https://example.com",
+ title: "Example",
+ description: "Test",
+ });
+ const container = document.createElement("div");
+ render(result, container);
+ assertEquals(
+ container.querySelector("a").getAttribute("aria-label"),
+ "Example",
+ );
+ });
+
+ it("should default the aria-label to the url when there is no title", () => {
+ const result = externalLinkTemplate({
+ url: "https://example.com",
+ title: "",
+ description: "Test",
+ });
+ const container = document.createElement("div");
+ render(result, container);
+ assertEquals(
+ container.querySelector("a").getAttribute("aria-label"),
+ "https://example.com",
+ );
+ });
+
+ it("should render an empty domain instead of throwing for an unparseable url", () => {
+ const result = externalLinkTemplate({
+ url: "not a url",
+ title: "Example",
+ description: "Test",
+ });
+ const container = document.createElement("div");
+ render(result, container);
+ assertEquals(
+ container
+ .querySelector("[data-testid='external-link-domain']")
+ .textContent.trim(),
+ "",
+ );
});
it("should prevent navigation when disableNavigation is true", () => {
diff --git a/tests/unit/specs/templates/postEmbed.template.test.js b/tests/unit/specs/templates/postEmbed.template.test.js
index f19ec739..15ee5ead 100644
--- a/tests/unit/specs/templates/postEmbed.template.test.js
+++ b/tests/unit/specs/templates/postEmbed.template.test.js
@@ -405,9 +405,10 @@ t.describe("postEmbedTemplate - external YouTube", (it) => {
"https://example.com/thumb.jpg",
);
assertEquals(
- container.querySelector("[data-testid='external-link']"),
- null,
+ embedElement.getAttribute("url"),
+ "https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=32s",
);
+ assertEquals(embedElement.getAttribute("description"), "A description");
});
it("uses a 16:9 aspect ratio for regular videos", () => {
@@ -417,7 +418,7 @@ t.describe("postEmbedTemplate - external YouTube", (it) => {
const embedElement = container.querySelector(
"[data-testid='youtube-embed']",
);
- assertEquals(embedElement.style.aspectRatio, String(16 / 9));
+ assertEquals(embedElement.getAttribute("aspect-ratio"), String(16 / 9));
});
it("uses a portrait aspect ratio for shorts", () => {
@@ -427,7 +428,7 @@ t.describe("postEmbedTemplate - external YouTube", (it) => {
const embedElement = container.querySelector(
"[data-testid='youtube-embed']",
);
- assertEquals(embedElement.style.aspectRatio, String(9 / 16));
+ assertEquals(embedElement.getAttribute("aspect-ratio"), String(9 / 16));
});
it("renders watch URLs on all YouTube hostnames", () => {
@@ -470,7 +471,7 @@ t.describe("postEmbedTemplate - external YouTube", (it) => {
);
assert(embedElement !== null);
assertEquals(embedElement.getAttribute("video-id"), "dQw4w9WgXcQ");
- assertEquals(embedElement.style.aspectRatio, String(16 / 9));
+ assertEquals(embedElement.getAttribute("aspect-ratio"), String(16 / 9));
});
it("converts hour/minute/second start times to seconds", () => {