Emdash source with visual editor image upload fix
Fixes: 1. media.ts: wrap placeholder generation in try-catch 2. toolbar.ts: check r.ok, display error message in popover
This commit is contained in:
305
e2e/tests/widgets.spec.ts
Normal file
305
e2e/tests/widgets.spec.ts
Normal file
@@ -0,0 +1,305 @@
|
||||
/**
|
||||
* Widgets E2E Tests
|
||||
*
|
||||
* Tests widget area management at /widgets.
|
||||
* Covers creating widget areas, adding widgets, and deleting areas.
|
||||
*
|
||||
* The fixture starts with no widget areas, so tests create their own.
|
||||
*/
|
||||
|
||||
import { test, expect } from "../fixtures";
|
||||
|
||||
// API helper
|
||||
function apiHeaders(token: string, baseUrl: string) {
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
"X-EmDash-Request": "1",
|
||||
Origin: baseUrl,
|
||||
};
|
||||
}
|
||||
|
||||
test.describe("Widgets", () => {
|
||||
let headers: Record<string, string>;
|
||||
let baseUrl: string;
|
||||
|
||||
test.beforeEach(async ({ admin, serverInfo }) => {
|
||||
await admin.devBypassAuth();
|
||||
baseUrl = serverInfo.baseUrl;
|
||||
headers = apiHeaders(serverInfo.token, baseUrl);
|
||||
});
|
||||
|
||||
test.describe("Widget Areas Page", () => {
|
||||
test("renders the widgets page", async ({ admin, page }) => {
|
||||
await admin.goto("/widgets");
|
||||
await admin.waitForShell();
|
||||
await admin.waitForLoading();
|
||||
|
||||
// Should show the page title
|
||||
await expect(page.locator("h1").first()).toContainText("Widgets");
|
||||
|
||||
// Should show the "Add Widget Area" button
|
||||
await expect(page.getByRole("button", { name: "Add Widget Area" })).toBeVisible();
|
||||
|
||||
// Should show the available widgets palette
|
||||
await expect(page.locator("h2", { hasText: "Available Widgets" })).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
});
|
||||
|
||||
test("shows empty state when no widget areas exist", async ({ admin, page }) => {
|
||||
// Clean up any existing areas first
|
||||
const res = await fetch(`${baseUrl}/_emdash/api/widget-areas`, { headers });
|
||||
if (res.ok) {
|
||||
const data: any = await res.json();
|
||||
const areas = data.data?.areas ?? [];
|
||||
for (const area of areas) {
|
||||
await fetch(`${baseUrl}/_emdash/api/widget-areas/${area.name}`, {
|
||||
method: "DELETE",
|
||||
headers,
|
||||
}).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
await admin.goto("/widgets");
|
||||
await admin.waitForShell();
|
||||
await admin.waitForLoading();
|
||||
|
||||
// Should show empty state
|
||||
await expect(page.locator("text=No widget areas yet")).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
test("shows built-in widget types in the palette", async ({ admin, page }) => {
|
||||
await admin.goto("/widgets");
|
||||
await admin.waitForShell();
|
||||
await admin.waitForLoading();
|
||||
|
||||
// Should show Content Block and Menu in the palette
|
||||
await expect(page.locator("text=Content Block").first()).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.locator("text=Menu").first()).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Create Widget Area", () => {
|
||||
test("opens and closes the create widget area dialog", async ({ admin, page }) => {
|
||||
await admin.goto("/widgets");
|
||||
await admin.waitForShell();
|
||||
await admin.waitForLoading();
|
||||
|
||||
// Click "Add Widget Area"
|
||||
await page.getByRole("button", { name: "Add Widget Area" }).click();
|
||||
|
||||
// Dialog should appear
|
||||
const dialog = page.getByRole("dialog", { name: "Create Widget Area" });
|
||||
await expect(dialog).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Should have Name, Label, Description fields
|
||||
await expect(dialog.getByLabel("Name")).toBeVisible();
|
||||
await expect(dialog.getByLabel("Label")).toBeVisible();
|
||||
|
||||
// Cancel should close the dialog
|
||||
await dialog.getByRole("button", { name: "Cancel" }).click();
|
||||
await expect(dialog).not.toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
test("creates a new widget area", async ({ admin, page }) => {
|
||||
const areaName = `e2e-area-${Date.now()}`;
|
||||
const areaLabel = "E2E Test Area";
|
||||
|
||||
await admin.goto("/widgets");
|
||||
await admin.waitForShell();
|
||||
await admin.waitForLoading();
|
||||
|
||||
// Open create dialog
|
||||
await page.getByRole("button", { name: "Add Widget Area" }).click();
|
||||
|
||||
const dialog = page.getByRole("dialog", { name: "Create Widget Area" });
|
||||
await expect(dialog).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Fill form
|
||||
await dialog.getByLabel("Name").fill(areaName);
|
||||
await dialog.getByLabel("Label").fill(areaLabel);
|
||||
await dialog.getByLabel("Description").fill("Area for E2E testing");
|
||||
|
||||
// Submit
|
||||
await dialog.getByRole("button", { name: "Create" }).click();
|
||||
|
||||
// Dialog should close
|
||||
await expect(dialog).not.toBeVisible({ timeout: 10000 });
|
||||
|
||||
// New area should appear in the list
|
||||
await expect(page.locator("h3", { hasText: areaLabel })).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Clean up via API
|
||||
await fetch(`${baseUrl}/_emdash/api/widget-areas/${areaName}`, {
|
||||
method: "DELETE",
|
||||
headers,
|
||||
}).catch(() => {});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Add Widget to Area", () => {
|
||||
test("adds a content widget to an area via API and verifies in UI", async ({ admin, page }) => {
|
||||
const areaName = `e2e-widget-${Date.now()}`;
|
||||
const areaLabel = "Widget Test Area";
|
||||
|
||||
// Create area via API
|
||||
await fetch(`${baseUrl}/_emdash/api/widget-areas`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({ name: areaName, label: areaLabel }),
|
||||
});
|
||||
|
||||
// Add a content widget via API
|
||||
await fetch(`${baseUrl}/_emdash/api/widget-areas/${areaName}/widgets`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({ type: "content", title: "Test Content Widget" }),
|
||||
});
|
||||
|
||||
// Navigate to widgets page
|
||||
await admin.goto("/widgets");
|
||||
await admin.waitForShell();
|
||||
await admin.waitForLoading();
|
||||
|
||||
// Area should be visible
|
||||
await expect(page.locator("h3", { hasText: areaLabel })).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Widget should be visible in the area
|
||||
await expect(page.locator("text=Test Content Widget").first()).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
// Clean up
|
||||
await fetch(`${baseUrl}/_emdash/api/widget-areas/${areaName}`, {
|
||||
method: "DELETE",
|
||||
headers,
|
||||
}).catch(() => {});
|
||||
});
|
||||
|
||||
test("deletes a widget from an area", async ({ admin, page }) => {
|
||||
const areaName = `e2e-del-widget-${Date.now()}`;
|
||||
const areaLabel = "Delete Widget Area";
|
||||
|
||||
// Create area and widget via API
|
||||
await fetch(`${baseUrl}/_emdash/api/widget-areas`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({ name: areaName, label: areaLabel }),
|
||||
});
|
||||
|
||||
const widgetRes = await fetch(`${baseUrl}/_emdash/api/widget-areas/${areaName}/widgets`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({ type: "content", title: "Widget To Delete" }),
|
||||
});
|
||||
const widgetData: any = await widgetRes.json();
|
||||
const widgetTitle = widgetData.data?.widget?.title ?? "Widget To Delete";
|
||||
|
||||
// Navigate to widgets page
|
||||
await admin.goto("/widgets");
|
||||
await admin.waitForShell();
|
||||
await admin.waitForLoading();
|
||||
|
||||
// Widget should be visible
|
||||
await expect(page.locator(`text=${widgetTitle}`).first()).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Click the delete button on the widget
|
||||
const deleteButton = page.getByRole("button", { name: `Delete ${widgetTitle}` });
|
||||
if (await deleteButton.isVisible({ timeout: 3000 }).catch(() => false)) {
|
||||
await deleteButton.click();
|
||||
await admin.waitForLoading();
|
||||
|
||||
// Widget should disappear
|
||||
await expect(page.locator(`text=${widgetTitle}`).first()).not.toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
// Clean up area
|
||||
await fetch(`${baseUrl}/_emdash/api/widget-areas/${areaName}`, {
|
||||
method: "DELETE",
|
||||
headers,
|
||||
}).catch(() => {});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Delete Widget Area", () => {
|
||||
test("deletes a widget area with confirmation", async ({ admin, page }) => {
|
||||
const areaName = `e2e-del-area-${Date.now()}`;
|
||||
const areaLabel = "Area To Delete";
|
||||
|
||||
// Create area via API
|
||||
await fetch(`${baseUrl}/_emdash/api/widget-areas`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({ name: areaName, label: areaLabel }),
|
||||
});
|
||||
|
||||
await admin.goto("/widgets");
|
||||
await admin.waitForShell();
|
||||
await admin.waitForLoading();
|
||||
|
||||
// Area should be visible
|
||||
await expect(page.locator("h3", { hasText: areaLabel })).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Click the delete button on the area header
|
||||
const deleteAreaButton = page.getByRole("button", {
|
||||
name: `Delete ${areaLabel} widget area`,
|
||||
});
|
||||
await deleteAreaButton.click();
|
||||
|
||||
// ConfirmDialog should appear
|
||||
const confirmDialog = page.getByRole("dialog", { name: "Delete Widget Area" });
|
||||
await expect(confirmDialog).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Confirm deletion
|
||||
await confirmDialog.getByRole("button", { name: "Delete" }).click();
|
||||
|
||||
// Area should disappear
|
||||
await expect(page.locator("h3", { hasText: areaLabel })).not.toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
});
|
||||
|
||||
test("cancel delete keeps the widget area", async ({ admin, page }) => {
|
||||
const areaName = `e2e-keep-area-${Date.now()}`;
|
||||
const areaLabel = "Area To Keep";
|
||||
|
||||
// Create area via API
|
||||
await fetch(`${baseUrl}/_emdash/api/widget-areas`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({ name: areaName, label: areaLabel }),
|
||||
});
|
||||
|
||||
await admin.goto("/widgets");
|
||||
await admin.waitForShell();
|
||||
await admin.waitForLoading();
|
||||
|
||||
await expect(page.locator("h3", { hasText: areaLabel })).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Click delete
|
||||
await page.getByRole("button", { name: `Delete ${areaLabel} widget area` }).click();
|
||||
|
||||
// Dialog appears
|
||||
const confirmDialog = page.getByRole("dialog", { name: "Delete Widget Area" });
|
||||
await expect(confirmDialog).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Cancel
|
||||
await confirmDialog.getByRole("button", { name: "Cancel" }).click();
|
||||
|
||||
// Dialog closes
|
||||
await expect(confirmDialog).not.toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Area should still be there
|
||||
await expect(page.locator("h3", { hasText: areaLabel })).toBeVisible();
|
||||
|
||||
// Clean up
|
||||
await fetch(`${baseUrl}/_emdash/api/widget-areas/${areaName}`, {
|
||||
method: "DELETE",
|
||||
headers,
|
||||
}).catch(() => {});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user