links.spec.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. import { expect, type Page, test } from '@playwright/test'
  2. import {
  3. deleteMessagesWithBody,
  4. deleteNotifications,
  5. jobWithCustomer,
  6. linkTelegramChat,
  7. ownerOrganizationId,
  8. type PlantedNotification,
  9. plantInboundMessage,
  10. plantNotification,
  11. } from '../../support/db'
  12. import { settle } from '../../support/hydration'
  13. /**
  14. * Every notification lands on the thing it is about.
  15. *
  16. * An audit of the bell (15 Sep 2026) found links that opened the right page
  17. * and showed nothing: an inbound SMS, Telegram or WhatsApp message opened the
  18. * inbox with no conversation selected, a customer's payment opened the
  19. * vehicle instead of the invoice, and feedback on a status report did not
  20. * switch tab when the job was already open.
  21. *
  22. * The notifications are planted with the address the code builds, and in the
  23. * old shape where the pages now have to honour links already stored in
  24. * people's bells. The triggers themselves (a Twilio webhook, a Telegram
  25. * update) need providers the harness cannot play.
  26. */
  27. test.describe.configure({ mode: 'serial' })
  28. const stamp = Date.now()
  29. const planted: string[] = []
  30. const bodies: string[] = []
  31. let organizationId = ''
  32. let job: Awaited<ReturnType<typeof jobWithCustomer>>
  33. /** The customer's Telegram chat before the spec linked one, put back afterwards. */
  34. let previousTelegramChat: string | null | undefined
  35. test.beforeAll(async () => {
  36. organizationId = await ownerOrganizationId()
  37. job = await jobWithCustomer(organizationId)
  38. })
  39. test.afterAll(async () => {
  40. await deleteNotifications(planted)
  41. if (previousTelegramChat !== undefined) {
  42. await linkTelegramChat(job.customerId, previousTelegramChat)
  43. }
  44. for (const body of bodies) await deleteMessagesWithBody(body)
  45. })
  46. async function plant(notification: PlantedNotification): Promise<void> {
  47. planted.push(await plantNotification(organizationId, notification))
  48. }
  49. /** Opens the bell and clicks the notification with this title. */
  50. async function openFromBell(page: Page, title: string): Promise<void> {
  51. const panel = page.getByRole('dialog', { name: 'Notifications' })
  52. await expect(async () => {
  53. await page.getByRole('button', { name: 'Open notifications' }).click()
  54. await expect(panel).toBeVisible({ timeout: 2_000 })
  55. }).toPass({ timeout: 30_000 })
  56. await panel.getByRole('button').filter({ hasText: title }).first().click()
  57. await expect(panel).toBeHidden()
  58. }
  59. test('an inbound SMS opens that conversation, not an empty inbox', async ({ page }) => {
  60. const body = `E2E SMS from the customer ${stamp}`
  61. const title = `E2E new SMS ${stamp}`
  62. bodies.push(body)
  63. await plantInboundMessage('sms', organizationId, job.customerId, body)
  64. await plant({
  65. type: 'sms_inbound',
  66. title,
  67. message: `${job.customerName}: ${body}`,
  68. entityType: 'sms_message',
  69. entityId: job.customerId,
  70. entityUrl: `/messages?customerId=${job.customerId}`,
  71. })
  72. await page.goto('/')
  73. await settle(page)
  74. await openFromBell(page, title)
  75. await page.waitForURL(/\/messages\?customerId=/)
  76. await expect(page.getByText('Pick a conversation')).toHaveCount(0)
  77. // Once in the list's preview and once in the open conversation.
  78. await expect(page.getByText(body).filter({ visible: true })).toHaveCount(2)
  79. })
  80. test('a Telegram message stored with the old link opens its conversation', async ({ page }) => {
  81. const body = `E2E Telegram from the customer ${stamp}`
  82. const title = `E2E new Telegram ${stamp}`
  83. bodies.push(body)
  84. previousTelegramChat = await linkTelegramChat(job.customerId, '777000')
  85. await plantInboundMessage('telegram', organizationId, job.customerId, body)
  86. await plant({
  87. type: 'telegram_inbound',
  88. title,
  89. message: `${job.customerName}: ${body}`,
  90. entityType: 'telegram_message',
  91. entityId: job.customerId,
  92. entityUrl: `/messages?tab=telegram&customerId=${job.customerId}`,
  93. })
  94. await page.goto('/')
  95. await settle(page)
  96. await openFromBell(page, title)
  97. await page.waitForURL(/\/messages\?tab=telegram/)
  98. await expect(page.getByText('Pick a conversation')).toHaveCount(0)
  99. await expect(page.getByText(body).filter({ visible: true })).toHaveCount(2)
  100. })
  101. test('a payment stored with the old vehicle link opens the invoice', async ({ page }) => {
  102. const title = `E2E payment received ${stamp}`
  103. await plant({
  104. type: 'invoice_payment',
  105. title,
  106. message: `${job.customerName} paid 100.00`,
  107. entityType: 'invoice',
  108. entityId: job.serviceRecordId,
  109. entityUrl: `/vehicles/${job.vehicleId}?tab=service&record=${job.serviceRecordId}`,
  110. })
  111. await page.goto('/')
  112. await settle(page)
  113. await openFromBell(page, title)
  114. await page.waitForURL(`**/vehicles/${job.vehicleId}/service/${job.serviceRecordId}`)
  115. })
  116. test('feedback on a status report switches the open job to its reports', async ({ page }) => {
  117. const title = `E2E status report feedback ${stamp}`
  118. await plant({
  119. type: 'status_report_feedback',
  120. title,
  121. message: `${job.customerName} responded to the status report`,
  122. entityType: 'ServiceRecord',
  123. entityId: job.serviceRecordId,
  124. entityUrl: `/vehicles/${job.vehicleId}/service/${job.serviceRecordId}?tab=statusReports`,
  125. })
  126. // The job is already open on its details when the notification is clicked.
  127. await page.goto(`/vehicles/${job.vehicleId}/service/${job.serviceRecordId}`)
  128. await settle(page)
  129. await expect(page.getByRole('button', { name: 'New Status Report' })).toHaveCount(0)
  130. await openFromBell(page, title)
  131. await page.waitForURL(/tab=statusReports/)
  132. await expect(page.getByRole('button', { name: 'New Status Report' })).toBeVisible()
  133. })