diff --git a/web/tests/e2e/lexicon-delete.spec.ts b/web/tests/e2e/lexicon-delete.spec.ts new file mode 100644 index 0000000..17479dc --- /dev/null +++ b/web/tests/e2e/lexicon-delete.spec.ts @@ -0,0 +1,79 @@ +import { test, expect } from "@playwright/test" +import { loginAsTestAdmin } from "./auth-helper" + +const TEST_LEXICON = { + lexicon: 1, + id: "test.e2e.lexicondelete.item", + defs: { + main: { + type: "record", + key: "tid", + record: { + type: "object", + properties: { + title: { type: "string" }, + }, + }, + }, + }, +} + +async function seedLexicon( + request: import("@playwright/test").APIRequestContext, +) { + const resp = await request.post("/admin/lexicons", { + data: { + lexicon_json: TEST_LEXICON, + backfill: false, + }, + }) + if (!resp.ok()) { + const text = await resp.text() + if (!text.includes("already exists")) { + throw new Error(`Failed to seed lexicon: ${resp.status()} ${text}`) + } + } +} + +async function cleanupLexicon( + request: import("@playwright/test").APIRequestContext, +) { + await request.delete(`/admin/lexicons/${TEST_LEXICON.id}`) +} + +test.describe("Lexicon Delete", () => { + test.beforeEach(async ({ page }) => { + await loginAsTestAdmin(page) + await seedLexicon(page.request) + }) + + test.afterEach(async ({ page }) => { + await cleanupLexicon(page.request) + }) + + test("delete shows AlertDialog confirmation and succeeds", async ({ page }) => { + await page.goto("/dashboard/lexicons") + + const row = page.locator("table tbody tr", { hasText: TEST_LEXICON.id }) + await expect(row).toBeVisible({ timeout: 5000 }) + + const deleteButton = row.getByRole("button", { name: /delete lexicon/i }) + await deleteButton.click() + + const dialog = page.getByRole("alertdialog") + await expect(dialog).toBeVisible({ timeout: 3000 }) + await expect(dialog.getByText("Delete lexicon?")).toBeVisible() + await expect(dialog.getByText(/permanently remove/i)).toBeVisible() + + await dialog.getByRole("button", { name: "Cancel" }).click() + await expect(dialog).not.toBeVisible() + await expect(row).toBeVisible() + + await deleteButton.click() + await expect(dialog).toBeVisible() + await dialog.getByRole("button", { name: "Delete" }).click() + + await expect(page.getByText("Lexicon deleted")).toBeVisible({ timeout: 5000 }) + await expect(row).not.toBeVisible({ timeout: 5000 }) + }) +}) diff --git a/web/tests/e2e/record-delete.spec.ts b/web/tests/e2e/record-delete.spec.ts new file mode 100644 index 0000000..bd5598b --- /dev/null +++ b/web/tests/e2e/record-delete.spec.ts @@ -0,0 +1,136 @@ +import { test, expect } from "@playwright/test" +import { loginAsTestAdmin } from "./auth-helper" +import pg from "pg" + +const DB_URL = "postgres://happyview:happyview@localhost:5434/happyview_test" +const COLLECTION = "test.e2e.recorddelete.item" + +const TEST_LEXICON = { + lexicon: 1, + id: COLLECTION, + defs: { + main: { + type: "record", + key: "tid", + record: { + type: "object", + properties: { + title: { type: "string" }, + }, + }, + }, + }, +} + +async function seedRecords( + request: import("@playwright/test").APIRequestContext, +) { + const lexResp = await request.post("/admin/lexicons", { + data: { lexicon_json: TEST_LEXICON, backfill: false }, + }) + if (!lexResp.ok()) { + const text = await lexResp.text() + if (!text.includes("already exists")) { + throw new Error(`Failed to seed lexicon: ${lexResp.status()} ${text}`) + } + } + + const client = new pg.Client(DB_URL) + await client.connect() + try { + for (let i = 1; i <= 3; i++) { + const uri = `at://did:plc:e2e-test-admin/${COLLECTION}/record-${i}` + const now = new Date().toISOString() + await client.query( + `INSERT INTO records (uri, did, collection, rkey, record, cid, indexed_at) + VALUES ($1, $2, $3, $4, $5, $6, $7) + ON CONFLICT (uri) DO NOTHING`, + [ + uri, + "did:plc:e2e-test-admin", + COLLECTION, + `record-${i}`, + JSON.stringify({ title: `Test Record ${i}` }), + `cid-e2e-${i}`, + now, + ], + ) + } + } finally { + await client.end() + } +} + +async function cleanupRecords( + request: import("@playwright/test").APIRequestContext, +) { + const client = new pg.Client(DB_URL) + await client.connect() + try { + await client.query("DELETE FROM records WHERE collection = $1", [COLLECTION]) + } finally { + await client.end() + } + await request.delete(`/admin/lexicons/${COLLECTION}`) +} + +test.describe("Record Delete", () => { + test.beforeEach(async ({ page }) => { + await loginAsTestAdmin(page) + await seedRecords(page.request) + }) + + test.afterEach(async ({ page }) => { + await cleanupRecords(page.request) + }) + + test("single record delete shows AlertDialog and toasts on success", async ({ page }) => { + await page.goto(`/dashboard/records?collection=${encodeURIComponent(COLLECTION)}`) + + const firstRow = page.locator("table tbody tr").first() + await expect(firstRow).toBeVisible({ timeout: 5000 }) + await firstRow.click() + + const sheet = page.locator("[data-slot='sheet-content']") + await expect(sheet).toBeVisible({ timeout: 5000 }) + + const deleteRecordButton = sheet.getByRole("button", { name: /delete record/i }) + await deleteRecordButton.click() + + const dialog = page.getByRole("alertdialog") + await expect(dialog).toBeVisible({ timeout: 3000 }) + await expect(dialog.getByText("Delete record?")).toBeVisible() + + await dialog.getByRole("button", { name: "Cancel" }).click() + await expect(dialog).not.toBeVisible() + + await deleteRecordButton.click() + await expect(dialog).toBeVisible() + await dialog.getByRole("button", { name: "Delete" }).click() + + await expect(page.getByText("Record deleted")).toBeVisible({ timeout: 5000 }) + }) + + test("bulk delete uses Promise.allSettled with success toast", async ({ page }) => { + await page.goto(`/dashboard/records?collection=${encodeURIComponent(COLLECTION)}`) + + await expect(page.locator("table tbody tr").first()).toBeVisible({ timeout: 5000 }) + + const selectAll = page.locator("table thead").getByRole("checkbox", { name: /select all/i }) + await selectAll.click() + + const actionsButton = page.getByRole("button", { name: "Actions" }) + await actionsButton.click() + + const deleteMenuItem = page.getByRole("menuitem", { name: /delete/i }) + await deleteMenuItem.click() + + const dialog = page.locator("[data-slot='dialog-content'], [role='dialog']").first() + await expect(dialog).toBeVisible({ timeout: 3000 }) + + const confirmButton = dialog.getByRole("button", { name: /^delete$/i }) + await confirmButton.click() + + await expect(page.getByText(/deleted 3 records/i)).toBeVisible({ timeout: 10000 }) + }) +}) diff --git a/web/tests/e2e/script-delete.spec.ts b/web/tests/e2e/script-delete.spec.ts new file mode 100644 index 0000000..642588f --- /dev/null +++ b/web/tests/e2e/script-delete.spec.ts @@ -0,0 +1,94 @@ +import { test, expect } from "@playwright/test" +import { loginAsTestAdmin } from "./auth-helper" + +const TEST_LEXICON = { + lexicon: 1, + id: "test.e2e.scriptdelete.item", + defs: { + main: { + type: "record", + key: "tid", + record: { + type: "object", + properties: { + title: { type: "string" }, + }, + }, + }, + }, +} + +const TEST_SCRIPT_SUFFIX = "test.e2e.scriptdelete.item" +const TEST_SCRIPT_ID = `before_create:${TEST_SCRIPT_SUFFIX}` + +async function seedScript( + request: import("@playwright/test").APIRequestContext, +) { + const lexResp = await request.post("/admin/lexicons", { + data: { lexicon_json: TEST_LEXICON, backfill: false }, + }) + if (!lexResp.ok()) { + const text = await lexResp.text() + if (!text.includes("already exists")) { + throw new Error(`Failed to seed lexicon: ${lexResp.status()} ${text}`) + } + } + + const resp = await request.post("/admin/scripts", { + data: { + id: TEST_SCRIPT_ID, + code: 'return record', + language: "lua", + }, + }) + if (!resp.ok()) { + const text = await resp.text() + if (!text.includes("already exists")) { + throw new Error(`Failed to seed script: ${resp.status()} ${text}`) + } + } +} + +async function cleanupScript( + request: import("@playwright/test").APIRequestContext, +) { + await request.delete(`/admin/scripts/${encodeURIComponent(TEST_SCRIPT_ID)}`) + await request.delete(`/admin/lexicons/${TEST_LEXICON.id}`) +} + +test.describe("Script Delete", () => { + test.beforeEach(async ({ page }) => { + await loginAsTestAdmin(page) + await seedScript(page.request) + }) + + test.afterEach(async ({ page }) => { + await cleanupScript(page.request) + }) + + test("delete shows AlertDialog instead of window.confirm", async ({ page }) => { + await page.goto("/dashboard/settings/scripts") + + const row = page.locator("table tbody tr", { hasText: TEST_SCRIPT_SUFFIX }) + await expect(row).toBeVisible({ timeout: 5000 }) + + const deleteButton = row.getByRole("button", { name: /delete script/i }) + await deleteButton.click() + + const dialog = page.getByRole("alertdialog") + await expect(dialog).toBeVisible({ timeout: 3000 }) + await expect(dialog.getByText("Delete script?")).toBeVisible() + await expect(dialog.getByText(/permanently remove/i)).toBeVisible() + + await dialog.getByRole("button", { name: "Cancel" }).click() + await expect(dialog).not.toBeVisible() + await expect(row).toBeVisible() + + await deleteButton.click() + await expect(dialog).toBeVisible() + await dialog.getByRole("button", { name: "Delete" }).click() + + await expect(page.getByText("Script deleted")).toBeVisible({ timeout: 5000 }) + await expect(row).not.toBeVisible({ timeout: 5000 }) + }) +})