import { expect, Page, test } from "@playwright/test"; import { applyDeterministicPage, e2eHeaders, useE2EUser } from "./helpers.ts"; test("landlord can generate a tenant invite email", async ({ page }: { page: Page }) => { await applyDeterministicPage(page); await useE2EUser(page, "landlord"); await page.goto("/landlord/tenants/invite"); await expect( page.getByRole("heading", { name: /invite a tenant/i }), ).toBeVisible(); const generateButton = page.getByRole("button", { name: /generate email template/i, }); await page.getByLabel(/tenant name/i).fill("Sam Tenant"); await page.getByLabel(/tenant email address/i).fill( "sam.tenant@example.com", ); await page.getByLabel(/select a property/i).selectOption("e2e-prop-1"); await generateButton.click(); // Wait for the template to be generated (API call to complete) await page.waitForFunction(() => { const textarea = document.querySelector("textarea"); return textarea && textarea.value && !textarea.value.includes("INVITE_ID"); }, { timeout: 10000 }); const templateArea = page.locator("textarea"); await expect(templateArea).toHaveValue(/sam\.tenant@example\.com/i); await expect(templateArea).toHaveValue( /tenant\/invite\/.+/i, ); const firstTemplate = await templateArea.inputValue(); const firstInviteMatch = firstTemplate.match(/tenant\/invite\/([^\s#]+)/i); const firstInviteId = firstInviteMatch?.[1]?.replace(/[^a-z0-9-]/gi, "") || ""; expect(firstInviteId).toBeTruthy(); expect(firstInviteId).not.toBe("INVITE_ID"); await expect( page.getByRole("link", { name: /open email app/i }), ).toHaveAttribute("href", /mailto:/i); await expect( page.getByRole("button", { name: /^copy$/i }), ).toBeEnabled(); await page.goto("/landlord/tenants/invite"); await page.getByLabel(/tenant name/i).fill("Sam Tenant"); await page.getByLabel(/tenant email address/i).fill( "jamie.roommate@example.com", ); await page.getByLabel(/select a property/i).selectOption("e2e-prop-1"); await generateButton.click(); // Wait for the template to be generated (API call to complete) await page.waitForFunction(() => { const textarea = document.querySelector("textarea"); return textarea && textarea.value && !textarea.value.includes("INVITE_ID"); }, { timeout: 10000 }); const secondTemplate = await templateArea.inputValue(); const secondInviteMatch = secondTemplate.match(/tenant\/invite\/([^\s#]+)/i); const secondInviteId = secondInviteMatch?.[1]?.replace(/[^a-z0-9-]/gi, "") || ""; expect(secondInviteId).toBe(firstInviteId); expect(secondInviteId).not.toBe("INVITE_ID"); await page.goto(`/landlord/tenants/invite?invite_id=${firstInviteId}`); const emailInput = page.getByLabel(/tenant email address/i); await expect(emailInput).toHaveValue(/sam\.tenant@example\.com/i); await expect(emailInput).toHaveValue(/jamie\.roommate@example\.com/i); }); test("tenant invite preview shows property details and actions", async ({ page }: { page: Page }) => { await applyDeterministicPage(page); const inviteResponse = await page.request.post("/api/update/tenant_invite", { headers: { "Content-Type": "application/json", ...e2eHeaders("landlord") }, data: { property_id: "e2e-prop-1", invited_emails: ["taylor.tenant@example.com"], }, }); const invitePayload = await inviteResponse.json(); const inviteId = invitePayload?.invite?.id || ""; expect(inviteId).toBeTruthy(); await useE2EUser(page, "tenant"); await page.goto(`/tenant/invite/${inviteId}`); await expect(page.getByRole("heading", { name: /skyline lofts/i })) .toBeVisible(); await expect(page.getByText(/invite sent to taylor\.tenant@example\.com/i)) .toBeVisible(); await expect(page.getByRole("img", { name: /skyline lofts photo/i })) .toBeVisible(); await expect(page.getByRole("button", { name: /start lease/i })) .toBeVisible(); await expect(page.getByRole("button", { name: /decline/i })).toBeVisible(); await expect(page.getByRole("link", { name: /cancel/i })).toBeVisible(); });