mirror of
https://github.com/different-ai/openwork
synced 2026-04-26 01:25:10 +02:00
272 lines
11 KiB
TypeScript
272 lines
11 KiB
TypeScript
import { writeFile } from "node:fs/promises";
|
|
|
|
import { expect, test, type Page } from "@playwright/test";
|
|
|
|
import {
|
|
buildPngDataUrl,
|
|
buildSocialPreviewGalleryHtml,
|
|
capturePixelPerfectScreenshot,
|
|
getSocialPreviewGalleryViewport,
|
|
getScenarioTitleRegion,
|
|
SOCIAL_PREVIEW_SCENARIOS,
|
|
} from "./social-preview-simulator.ts";
|
|
|
|
const initialBody = `---
|
|
name: agent-creator
|
|
description: Create new OpenCode agents with a gpt-5.2-codex default.
|
|
---
|
|
|
|
# Agent Creator
|
|
|
|
Any markdown body is acceptable here.
|
|
`;
|
|
|
|
async function publishSkill(page: Page) {
|
|
await page.goto("/");
|
|
|
|
await page.locator('input[type="file"]').setInputFiles({
|
|
name: "AGENTS.md",
|
|
mimeType: "text/markdown",
|
|
buffer: Buffer.from(initialBody, "utf8"),
|
|
});
|
|
|
|
await Promise.all([
|
|
page.waitForURL(/\/b\/[0-9A-HJKMNP-TV-Z]{26}$/),
|
|
page.getByRole("button", { name: /generate share link/i }).click(),
|
|
]);
|
|
|
|
return page.url();
|
|
}
|
|
|
|
test("shows a read-only shared skill page with OpenWork import actions", async ({ page }) => {
|
|
const shareUrl = await publishSkill(page);
|
|
|
|
const jsonResponse = await page.request.get(shareUrl, {
|
|
headers: { Accept: "application/json" },
|
|
});
|
|
expect(jsonResponse.ok()).toBeTruthy();
|
|
expect(jsonResponse.headers()["content-type"] ?? "").toContain("application/json");
|
|
const bundleJson = await jsonResponse.json();
|
|
expect(bundleJson).toMatchObject({
|
|
schemaVersion: 1,
|
|
type: "skill",
|
|
name: "agent-creator",
|
|
});
|
|
|
|
await expect(page.getByText("Bundle details")).toHaveCount(0);
|
|
await expect(page.getByText("Raw endpoints")).toHaveCount(0);
|
|
await expect(page.getByRole("button", { name: /save changes/i })).toHaveCount(0);
|
|
await expect(page.getByLabel("Skill name")).toHaveCount(0);
|
|
await expect(page.getByLabel("Skill description")).toHaveCount(0);
|
|
await expect(page.getByRole("link", { name: /open in web app/i })).toHaveCount(0);
|
|
await expect(page.getByRole("button", { name: /copy share link/i })).toHaveCount(0);
|
|
await expect(page.getByText("Preview", { exact: true })).toHaveCount(0);
|
|
await expect(page.locator(".preview-eyebrow")).toContainText("Agent Creator");
|
|
await expect(page.locator(".preview-filename")).toContainText("agent-creator.md");
|
|
await expect(page.locator(".preview-highlight")).toContainText("Any markdown body is acceptable here.");
|
|
|
|
const openInAppHref = await page.getByRole("link", { name: /^open in openwork$/i }).getAttribute("href");
|
|
expect(openInAppHref).toBeTruthy();
|
|
expect(openInAppHref ?? "").toContain("openwork://import-bundle?");
|
|
|
|
const openInAppLink = page.getByRole("link", { name: /^open in openwork$/i });
|
|
await openInAppLink.dispatchEvent("pointerdown");
|
|
const refreshedOpenInAppHref = await openInAppLink.getAttribute("href");
|
|
expect(refreshedOpenInAppHref ?? "").toContain("ow_nonce=");
|
|
|
|
const deepLinkQuery = new URL((openInAppHref ?? "").replace("openwork://import-bundle?", "https://example.test/?"));
|
|
expect(deepLinkQuery.searchParams.get("ow_bundle")).toBe(shareUrl);
|
|
expect(deepLinkQuery.searchParams.get("ow_label")).toBe("agent-creator");
|
|
});
|
|
|
|
test("publishes a share page with a valid OG preview card for link unfurls", async ({ page }) => {
|
|
const shareUrl = await publishSkill(page);
|
|
const ogImageUrls = await page.locator('meta[property="og:image"]').evaluateAll((elements) =>
|
|
elements.map((element) => element.getAttribute("content") ?? "").filter(Boolean),
|
|
);
|
|
const ogImageUrl = ogImageUrls[0];
|
|
const ogTitle = await page.locator('meta[property="og:title"]').getAttribute("content");
|
|
const ogDescription = await page.locator('meta[property="og:description"]').getAttribute("content");
|
|
const twitterCard = await page.locator('meta[name="twitter:card"]').getAttribute("content");
|
|
const twitterImageUrl = await page.locator('meta[name="twitter:image"]').getAttribute("content");
|
|
|
|
expect(ogImageUrls).toHaveLength(4);
|
|
expect(ogImageUrl).toBeTruthy();
|
|
expect(ogImageUrl).toContain("/og/");
|
|
expect(ogImageUrl).not.toContain("variant=");
|
|
expect(ogImageUrls).toContain(`${ogImageUrl!}?variant=linkedin`);
|
|
expect(ogImageUrls).toContain(`${ogImageUrl!}?variant=slack`);
|
|
expect(ogImageUrls).toContain(`${ogImageUrl!}?variant=whatsapp`);
|
|
expect(ogTitle).toBe("agent-creator");
|
|
expect(ogDescription).toBe("Create new OpenCode agents with a gpt-5.2-codex default.");
|
|
expect(twitterCard).toBe("summary_large_image");
|
|
expect(twitterImageUrl).toBe(`${ogImageUrl!}?variant=twitter`);
|
|
|
|
const pngResponse = await page.request.get(ogImageUrl!);
|
|
expect(pngResponse.ok()).toBeTruthy();
|
|
expect(pngResponse.headers()["content-type"] ?? "").toContain("image/png");
|
|
|
|
const imageMetrics = await page.evaluate(async (url) => {
|
|
const img = new Image();
|
|
img.src = url;
|
|
await img.decode();
|
|
|
|
const canvas = document.createElement("canvas");
|
|
canvas.width = img.naturalWidth;
|
|
canvas.height = img.naturalHeight;
|
|
const ctx = canvas.getContext("2d");
|
|
if (!ctx) throw new Error("Canvas context unavailable");
|
|
|
|
ctx.drawImage(img, 0, 0);
|
|
const titleRegion = ctx.getImageData(170, 210, 420, 150).data;
|
|
let darkPixels = 0;
|
|
|
|
for (let index = 0; index < titleRegion.length; index += 4) {
|
|
const r = titleRegion[index] ?? 255;
|
|
const g = titleRegion[index + 1] ?? 255;
|
|
const b = titleRegion[index + 2] ?? 255;
|
|
if (r < 70 && g < 90 && b < 110) darkPixels += 1;
|
|
}
|
|
|
|
return {
|
|
width: img.naturalWidth,
|
|
height: img.naturalHeight,
|
|
darkPixels,
|
|
};
|
|
}, ogImageUrl!);
|
|
|
|
expect(imageMetrics.width).toBe(1200);
|
|
expect(imageMetrics.height).toBe(630);
|
|
expect(imageMetrics.darkPixels).toBeGreaterThan(2200);
|
|
|
|
const twitterPngResponse = await page.request.get(twitterImageUrl!);
|
|
expect(twitterPngResponse.ok()).toBeTruthy();
|
|
const twitterMetrics = await page.evaluate(async (url) => {
|
|
const img = new Image();
|
|
img.src = url;
|
|
await img.decode();
|
|
return {
|
|
width: img.naturalWidth,
|
|
height: img.naturalHeight,
|
|
};
|
|
}, twitterImageUrl!);
|
|
expect(twitterMetrics.width).toBe(1200);
|
|
expect(twitterMetrics.height).toBe(600);
|
|
|
|
const linkedinImageUrl = `${ogImageUrl!}?variant=linkedin`;
|
|
const linkedinMetrics = await page.evaluate(async (url) => {
|
|
const img = new Image();
|
|
img.src = url;
|
|
await img.decode();
|
|
return {
|
|
width: img.naturalWidth,
|
|
height: img.naturalHeight,
|
|
};
|
|
}, linkedinImageUrl);
|
|
expect(linkedinMetrics.width).toBe(1200);
|
|
expect(linkedinMetrics.height).toBe(627);
|
|
|
|
const svgResponse = await page.request.get(`${ogImageUrl!}?format=svg`);
|
|
expect(svgResponse.ok()).toBeTruthy();
|
|
expect(svgResponse.headers()["content-type"] ?? "").toContain("image/svg+xml");
|
|
|
|
const svg = await svgResponse.text();
|
|
expect(svg).toContain("Agent Creator");
|
|
expect(svg).not.toContain("agent-creator.md");
|
|
expect(svg).toContain("SKILL.md");
|
|
expect(svg).toContain("share.openworklabs.com");
|
|
expect(svg).not.toContain("Any markdown body is acceptable here.");
|
|
|
|
const pastePreviewHtml = await page.content();
|
|
expect(pastePreviewHtml).toContain(shareUrl);
|
|
});
|
|
|
|
test("keeps the OG title legible across simulated social preview sizes", async ({ page }, testInfo) => {
|
|
await publishSkill(page);
|
|
const ogImageUrl = await page.locator('meta[property="og:image"]').first().getAttribute("content");
|
|
const twitterImageUrl = await page.locator('meta[name="twitter:image"]').getAttribute("content");
|
|
|
|
expect(ogImageUrl).toBeTruthy();
|
|
expect(twitterImageUrl).toBeTruthy();
|
|
|
|
const variantUrls = {
|
|
facebook: ogImageUrl!,
|
|
linkedin: `${ogImageUrl!}?variant=linkedin`,
|
|
slack: `${ogImageUrl!}?variant=slack`,
|
|
whatsapp: `${ogImageUrl!}?variant=whatsapp`,
|
|
twitter: twitterImageUrl!,
|
|
};
|
|
const variantImageUrls = {
|
|
facebook: buildPngDataUrl(Buffer.from(await (await page.request.get(variantUrls.facebook)).body())),
|
|
linkedin: buildPngDataUrl(Buffer.from(await (await page.request.get(variantUrls.linkedin)).body())),
|
|
slack: buildPngDataUrl(Buffer.from(await (await page.request.get(variantUrls.slack)).body())),
|
|
whatsapp: buildPngDataUrl(Buffer.from(await (await page.request.get(variantUrls.whatsapp)).body())),
|
|
twitter: buildPngDataUrl(Buffer.from(await (await page.request.get(variantUrls.twitter)).body())),
|
|
};
|
|
const socialPreviewPage = await page.context().newPage();
|
|
await socialPreviewPage.setContent(
|
|
buildSocialPreviewGalleryHtml({
|
|
images: variantImageUrls,
|
|
}),
|
|
{ waitUntil: "load" },
|
|
);
|
|
await socialPreviewPage.waitForFunction(() =>
|
|
Array.from(document.images).every((image) => image.complete && image.naturalWidth > 0),
|
|
);
|
|
const galleryViewport = getSocialPreviewGalleryViewport();
|
|
await capturePixelPerfectScreenshot(socialPreviewPage, {
|
|
path: testInfo.outputPath("social-preview-gallery.png"),
|
|
width: galleryViewport.width,
|
|
height: galleryViewport.height,
|
|
});
|
|
|
|
for (const scenario of SOCIAL_PREVIEW_SCENARIOS) {
|
|
const metrics = await socialPreviewPage.evaluate((currentScenario) => {
|
|
const tile = document.querySelector<HTMLElement>(`[data-scenario="${currentScenario.key}"]`);
|
|
const image = tile?.querySelector<HTMLImageElement>("img");
|
|
if (!image) throw new Error(`Missing image for ${currentScenario.key}`);
|
|
|
|
const canvas = document.createElement("canvas");
|
|
canvas.width = currentScenario.previewWidth;
|
|
canvas.height = currentScenario.previewHeight;
|
|
const ctx = canvas.getContext("2d");
|
|
if (!ctx) throw new Error("Canvas context unavailable");
|
|
|
|
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
const region = {
|
|
left: Math.round(currentScenario.previewWidth * (170 / 1200)),
|
|
top: Math.round(currentScenario.previewHeight * (210 / 630)),
|
|
width: Math.max(1, Math.round(currentScenario.previewWidth * (420 / 1200))),
|
|
height: Math.max(1, Math.round(currentScenario.previewHeight * (150 / 630))),
|
|
};
|
|
const titleRegion = ctx.getImageData(region.left, region.top, region.width, region.height).data;
|
|
|
|
let darkPixels = 0;
|
|
for (let index = 0; index < titleRegion.length; index += 4) {
|
|
const r = titleRegion[index] ?? 255;
|
|
const g = titleRegion[index + 1] ?? 255;
|
|
const b = titleRegion[index + 2] ?? 255;
|
|
if (r < 70 && g < 90 && b < 110) darkPixels += 1;
|
|
}
|
|
|
|
return {
|
|
darkPixels,
|
|
totalPixels: region.width * region.height,
|
|
ratio: darkPixels / Math.max(1, region.width * region.height),
|
|
};
|
|
}, scenario);
|
|
|
|
const region = getScenarioTitleRegion(scenario);
|
|
await writeFile(
|
|
testInfo.outputPath(`${scenario.key}.json`),
|
|
JSON.stringify({ scenario, region, metrics }, null, 2),
|
|
"utf8",
|
|
);
|
|
|
|
expect(metrics.darkPixels).toBeGreaterThan(100);
|
|
expect(metrics.ratio).toBeGreaterThan(scenario.minDarkPixelRatio);
|
|
}
|
|
|
|
await socialPreviewPage.close();
|
|
});
|