email-designer.ts 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
  1. import { expect, type FrameLocator, type Page } from '@playwright/test'
  2. import { settle } from './hydration'
  3. /**
  4. * Driving the email designer.
  5. *
  6. * The designer opens in its own tab from the gallery, on a route that takes
  7. * either a preset (`&preset=1`) or a saved template (`&template=<id>`), so a
  8. * spec can go straight to the one it means. The preview is an iframe rendered
  9. * from `srcDoc`: the mail as a mail client would see it, with a `data-block`
  10. * mark on every row so the rail and the preview can point at each other.
  11. */
  12. /** Opens the designer on a kind's built-in preset. */
  13. export async function openPreset(page: Page, kind: string): Promise<void> {
  14. await page.goto(`/email-designer?kind=${kind}&preset=1`)
  15. await expect(page.locator('[data-rail-block]').first()).toBeVisible({ timeout: 30_000 })
  16. await settle(page)
  17. }
  18. /** The mail itself, inside the preview frame. */
  19. export function preview(page: Page): FrameLocator {
  20. return page.frameLocator('iframe')
  21. }
  22. /**
  23. * The subject and theme fields.
  24. *
  25. * The inspector shows one thing at a time: a block's own fields when a block
  26. * is selected, and the subject with the theme when none is. The rail's first
  27. * button is the way back to the whole mail.
  28. */
  29. export async function openSubjectAndTheme(page: Page): Promise<void> {
  30. await expect(async () => {
  31. await page.getByRole('button', { name: 'Subject and theme' }).click()
  32. await expect(subjectField(page)).toBeVisible({ timeout: 2_000 })
  33. }).toPass({ timeout: 30_000 })
  34. }
  35. /** The subject line. Its id carries the tag-field prefix; the label does not. */
  36. export function subjectField(page: Page) {
  37. return page.getByLabel('Subject', { exact: true })
  38. }
  39. /** The rail row for a block. */
  40. export function railBlock(page: Page, id: string) {
  41. return page.locator(`[data-rail-block="${id}"]`)
  42. }
  43. /** The ids of the blocks the mail is made of, in the order the rail lists them. */
  44. export async function railOrder(page: Page): Promise<string[]> {
  45. return page
  46. .locator('[data-rail-block]')
  47. .evaluateAll((rows) => rows.map((row) => row.getAttribute('data-rail-block') ?? ''))
  48. }
  49. /**
  50. * Saves the design, naming it when the designer asks.
  51. *
  52. * A preset has no name yet, so the first save opens a dialog; a saved
  53. * template updates in place. The dialog stays open on a refusal, which is
  54. * what the duplicate-name test reads.
  55. */
  56. export async function saveDesign(page: Page, name?: string): Promise<void> {
  57. await page.getByRole('button', { name: 'Save', exact: true }).click()
  58. if (name !== undefined) {
  59. const dialog = page.getByRole('dialog', { name: 'Save this template' })
  60. await expect(dialog).toBeVisible({ timeout: 10_000 })
  61. await dialog.getByPlaceholder('Template name').fill(name)
  62. await dialog.getByRole('button', { name: 'Save template', exact: true }).click()
  63. }
  64. await expect(
  65. page.locator('[data-sonner-toast]').filter({ hasText: 'Saved' }).first()
  66. ).toBeVisible({ timeout: 30_000 })
  67. }