Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 68 additions & 52 deletions app/e2e/widget-lab.spec.ts → app/e2e/widget-library.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,28 +37,38 @@ async function addBarWidgetToDashboard(page: import("@playwright/test").Page) {
// Suite
// ---------------------------------------------------------------------------

test.describe("Widget Lab", () => {
test.describe("Widget Library", () => {
test.beforeEach(async ({ authPage }) => {
await authPage.login(ALICE.email, ALICE.password);
});

// ── Sidebar navigation ──────────────────────────────────────────────

test("sidebar has Widget Lab item that navigates to /widget-lab", async ({
test("sidebar has Widget Library item that navigates to /widget-library", async ({
page,
}) => {
await page.goto("/");
await page.getByRole("button", { name: "Widget Lab" }).click();
await expect(page).toHaveURL("/widget-lab");
await page.getByRole("button", { name: "Widget Library" }).click();
await expect(page).toHaveURL("/widget-library");
await expect(
page.getByRole("heading", { name: "Widget Lab" }),
page.getByRole("heading", { name: "Widget Library" }),
).toBeVisible();
});

test("Widget Lab page shows empty state when no templates exist", async ({
test("legacy /widget-lab URL redirects to /widget-library (#914)", async ({
page,
}) => {
// Next.js permanent redirect from /widget-lab → /widget-library.
// Playwright follows the redirect automatically; landing on the new path
// is enough proof the redirects() entry in next.config.ts is wired.
await page.goto("/widget-lab");
await expect(page).toHaveURL("/widget-library");
});

test("Widget Library page shows empty state when no templates exist", async ({
page,
}) => {
await page.goto("/widget-library");
// Either the empty-state copy or template cards should render
await expect(
page
Expand All @@ -68,7 +78,7 @@ test.describe("Widget Lab", () => {
).toBeVisible({ timeout: 10_000 });
});

// ── Save to Widget Lab flow ─────────────────────────────────────────
// ── Save to Widget Library flow ─────────────────────────────────────────

test.describe("Save / browse / delete template flow", () => {
let dashboardCleanup: (() => Promise<void>) | undefined;
Expand All @@ -77,7 +87,7 @@ test.describe("Widget Lab", () => {
test.beforeEach(async ({ page }) => {
const { id, cleanup } = await createTestDashboard(
page.request,
`Widget Lab Test ${Date.now()}`,
`Widget Library Test ${Date.now()}`,
);
dashboardCleanup = cleanup;
await page.goto(`/${id}/edit`);
Expand All @@ -96,18 +106,20 @@ test.describe("Widget Lab", () => {
await dashboardCleanup?.();
});

test("can save a widget as a template and see it in Widget Lab", async ({
test("can save a widget as a template and see it in Widget Library", async ({
page,
}) => {
// Open widget actions menu → "Save to Widget Lab"
// Open widget actions menu → "Save to Widget Library"
const widgetCard = page.locator("[data-testid='widget-card']").first();
await widgetCard.hover();
await widgetCard.getByRole("button", { name: "Widget actions" }).click();
await page.getByRole("menuitem", { name: "Save to Widget Lab" }).click();
await page
.getByRole("menuitem", { name: "Save to Widget Library" })
.click();

// Save Template dialog should appear
const saveDialog = page.getByRole("dialog", {
name: "Save to Widget Lab",
name: "Save to Widget Library",
});
await expect(saveDialog).toBeVisible();

Expand All @@ -120,8 +132,8 @@ test.describe("Widget Lab", () => {
await saveDialog.getByRole("button", { name: "Save Template" }).click();
await expect(saveDialog).not.toBeVisible();

// Navigate to Widget Lab and verify the template appears
await page.goto("/widget-lab");
// Navigate to Widget Library and verify the template appears
await page.goto("/widget-library");
await expect(page.getByText(templateName)).toBeVisible({
timeout: 10_000,
});
Expand All @@ -134,7 +146,7 @@ test.describe("Widget Lab", () => {
templateId = saved?.id;
});

test("can delete a template from Widget Lab", async ({ page }) => {
test("can delete a template from Widget Library", async ({ page }) => {
// First save a template via the API so we don't depend on the UI flow
const templateName = `E2E Delete ${Date.now()}`;
const createRes = await page.request.post("/api/widget-templates", {
Expand All @@ -149,8 +161,8 @@ test.describe("Widget Lab", () => {
const { id } = (await createRes.json()).data;
templateId = id;

// Go to Widget Lab
await page.goto("/widget-lab");
// Go to Widget Library
await page.goto("/widget-library");
await expect(page.getByText(templateName)).toBeVisible({
timeout: 10_000,
});
Expand Down Expand Up @@ -188,7 +200,7 @@ test.describe("Widget Lab", () => {
templateName = `E2E Tmpl ${Date.now()}`;
const { id, cleanup } = await createTestDashboard(
page.request,
`Widget Lab From Template ${Date.now()}`,
`Widget Library From Template ${Date.now()}`,
);
dashboardCleanup = cleanup;

Expand Down Expand Up @@ -288,9 +300,9 @@ test.describe("Widget Lab", () => {
});
});

// ── Create / Edit templates directly in Widget Lab ──────────────────
// ── Create / Edit templates directly in Widget Library ──────────────────

test.describe("Widget Lab editor — create and edit templates", () => {
test.describe("Widget Library editor — create and edit templates", () => {
let templateId: string | undefined;

test.afterEach(async ({ page }) => {
Expand All @@ -300,13 +312,13 @@ test.describe("Widget Lab", () => {
}
});

test("can create a new template directly from Widget Lab", async ({
test("can create a new template directly from Widget Library", async ({
page,
}) => {
test.setTimeout(60_000);
await page.goto("/widget-lab");
await page.goto("/widget-library");
await expect(
page.getByRole("heading", { name: "Widget Lab" }),
page.getByRole("heading", { name: "Widget Library" }),
).toBeVisible();

// Click "New Template" button
Expand All @@ -319,7 +331,7 @@ test.describe("Widget Lab", () => {
await dialog.locator("#lab-template-name").fill(templateName);
await dialog
.locator("#lab-template-desc")
.fill("Created directly in Widget Lab");
.fill("Created directly in Widget Library");
await dialog.locator("#lab-template-tags").fill("e2e, test");

// Select a connection
Expand All @@ -345,7 +357,7 @@ test.describe("Widget Lab", () => {
await dialog.getByRole("button", { name: "Create Template" }).click();
await expect(dialog).not.toBeVisible({ timeout: 10_000 });

// Verify it appears in the Widget Lab list
// Verify it appears in the Widget Library list
await expect(page.getByText(templateName)).toBeVisible({
timeout: 10_000,
});
Expand All @@ -359,7 +371,9 @@ test.describe("Widget Lab", () => {
expect(templateId).toBeDefined();
});

test("can edit an existing template in Widget Lab", async ({ page }) => {
test("can edit an existing template in Widget Library", async ({
page,
}) => {
test.setTimeout(60_000);

// Create a template via API first
Expand All @@ -378,8 +392,8 @@ test.describe("Widget Lab", () => {
const { id } = (await createRes.json()).data;
templateId = id;

// Go to Widget Lab and click edit on the template card
await page.goto("/widget-lab");
// Go to Widget Library and click edit on the template card
await page.goto("/widget-library");
await expect(page.getByText(origName)).toBeVisible({ timeout: 10_000 });

const card = page
Expand Down Expand Up @@ -428,8 +442,8 @@ test.describe("Widget Lab", () => {
const { id } = (await createRes.json()).data;
templateId = id;

// Navigate to Widget Lab
await page.goto("/widget-lab");
// Navigate to Widget Library
await page.goto("/widget-library");
await expect(page.getByText(templateName)).toBeVisible({
timeout: 10_000,
});
Expand Down Expand Up @@ -471,8 +485,8 @@ test.describe("Widget Lab", () => {
);

try {
// Go to Widget Lab
await page.goto("/widget-lab");
// Go to Widget Library
await page.goto("/widget-library");
await expect(page.getByText(templateName)).toBeVisible({
timeout: 10_000,
});
Expand Down Expand Up @@ -580,8 +594,8 @@ test.describe("Widget Lab", () => {
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(1_000);

// 3. Edit the template in Widget Lab — change its name
await page.goto("/widget-lab");
// 3. Edit the template in Widget Library — change its name
await page.goto("/widget-library");
await expect(page.getByText(templateName)).toBeVisible({
timeout: 10_000,
});
Expand Down Expand Up @@ -621,9 +635,9 @@ test.describe("Widget Lab", () => {
});
});

// ── Save to Widget Lab from view mode ─────────────────────────────
// ── Save to Widget Library from view mode ─────────────────────────────

test.describe("Save to Widget Lab from view mode", () => {
test.describe("Save to Widget Library from view mode", () => {
test("action is visible on widget menu in view mode", async ({
authPage,
page,
Expand All @@ -646,11 +660,11 @@ test.describe("Widget Lab", () => {
await widgetCard.getByRole("button", { name: "Widget actions" }).click();

await expect(
page.getByRole("menuitem", { name: "Save to Widget Lab" }),
page.getByRole("menuitem", { name: "Save to Widget Library" }),
).toBeVisible();
});

test("can save a widget from view mode and see it in Widget Lab", async ({
test("can save a widget from view mode and see it in Widget Library", async ({
authPage,
page,
}) => {
Expand All @@ -665,16 +679,18 @@ test.describe("Widget Lab", () => {
expect(movieAnalytics).toBeTruthy();
await page.goto(`/${movieAnalytics!.id}`);

// Open widget actions → Save to Widget Lab
// Open widget actions → Save to Widget Library
const widgetCard = page.locator("[data-testid='widget-card']").first();
await expect(widgetCard).toBeVisible({ timeout: 15_000 });
await widgetCard.hover();
await widgetCard.getByRole("button", { name: "Widget actions" }).click();
await page.getByRole("menuitem", { name: "Save to Widget Lab" }).click();
await page
.getByRole("menuitem", { name: "Save to Widget Library" })
.click();

// Fill and submit
const saveDialog = page.getByRole("dialog", {
name: "Save to Widget Lab",
name: "Save to Widget Library",
});
await expect(saveDialog).toBeVisible();

Expand All @@ -683,8 +699,8 @@ test.describe("Widget Lab", () => {
await saveDialog.getByRole("button", { name: "Save Template" }).click();
await expect(saveDialog).not.toBeVisible();

// Verify in Widget Lab
await page.goto("/widget-lab");
// Verify in Widget Library
await page.goto("/widget-library");
await expect(page.getByText(templateName)).toBeVisible({
timeout: 10_000,
});
Expand All @@ -699,7 +715,7 @@ test.describe("Widget Lab", () => {
}
});

test("reader role does not see Save to Widget Lab action", async ({
test("reader role does not see Save to Widget Library action", async ({
authPage,
page,
}) => {
Expand All @@ -720,19 +736,19 @@ test.describe("Widget Lab", () => {
await widgetCard.hover();
await widgetCard.getByRole("button", { name: "Widget actions" }).click();

// Export CSV should be visible, but Save to Widget Lab should NOT
// Export CSV should be visible, but Save to Widget Library should NOT
await expect(
page.getByRole("menuitem", { name: "Export CSV" }),
).toBeVisible();
await expect(
page.getByRole("menuitem", { name: "Save to Widget Lab" }),
page.getByRole("menuitem", { name: "Save to Widget Library" }),
).not.toBeVisible();
});
});

// ── Widget Lab consumption: duplicate, filter, search ───────────────
// ── Widget Library consumption: duplicate, filter, search ───────────────

test.describe("Widget Lab consumption", () => {
test.describe("Widget Library consumption", () => {
// Tests in this block share the same template names ("Neo4j Bar Template",
// "PostgreSQL Table Template") in their beforeEach. With fullyParallel and
// 2 CI workers, two tests' beforeEach can race → two templates with the
Expand Down Expand Up @@ -773,7 +789,7 @@ test.describe("Widget Lab", () => {
});

test("can duplicate a template", async ({ page }) => {
await page.goto("/widget-lab");
await page.goto("/widget-library");
const card = page
.locator("[data-testid='template-card']")
.filter({ hasText: "Neo4j Bar Template" })
Expand All @@ -798,7 +814,7 @@ test.describe("Widget Lab", () => {
});

test("can filter templates by chart type", async ({ page }) => {
await page.goto("/widget-lab");
await page.goto("/widget-library");
const neo4jCard = page
.locator("[data-testid='template-card']")
.filter({ hasText: "Neo4j Bar Template" })
Expand All @@ -819,7 +835,7 @@ test.describe("Widget Lab", () => {
});

test("can filter templates by connector type", async ({ page }) => {
await page.goto("/widget-lab");
await page.goto("/widget-library");
const neo4jCard = page
.locator("[data-testid='template-card']")
.filter({ hasText: "Neo4j Bar Template" })
Expand All @@ -839,7 +855,7 @@ test.describe("Widget Lab", () => {
});

test("can search templates by name", async ({ page }) => {
await page.goto("/widget-lab");
await page.goto("/widget-library");
await expect(
page.getByText("Neo4j Bar Template", { exact: true }),
).toBeVisible({ timeout: 10_000 });
Expand Down
11 changes: 11 additions & 0 deletions app/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,17 @@ const nextConfig: NextConfig = {

return config;
},
async redirects() {
return [
{
// #914 — Widget Lab renamed to Widget Library. Permanent 308 so
// bookmarked URLs survive and SEO carries to the new path.
source: "/widget-lab",
destination: "/widget-library",
permanent: true,
},
];
},
};

export default nextConfig;
2 changes: 1 addition & 1 deletion app/src/app/(dashboard)/__tests__/layout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ describe("DashboardLayout", () => {
expect(screen.getByTestId("sidebar-item-Dashboards")).toBeDefined();
expect(screen.getByTestId("sidebar-item-Connections")).toBeDefined();
expect(screen.getByTestId("sidebar-item-Users")).toBeDefined();
expect(screen.getByTestId("sidebar-item-Widget Lab")).toBeDefined();
expect(screen.getByTestId("sidebar-item-Widget Library")).toBeDefined();
expect(screen.getByTestId("sidebar-item-Settings")).toBeDefined();
expect(screen.getByTestId("sidebar-item-Sign out")).toBeDefined();
expect(screen.getByTestId("sidebar-item-Theme")).toBeDefined();
Expand Down
Loading
Loading