printing.spec.ts 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. import { expect, test } from '@playwright/test'
  2. import {
  3. addMark,
  4. closeEditor,
  5. invoicePdf,
  6. openDropoff,
  7. printsMap,
  8. workOrderPdf,
  9. } from '../../support/condition-map'
  10. import {
  11. conditionMapOnInvoice,
  12. ownerOrganizationId,
  13. plantConditionInspection,
  14. plantConditionJob,
  15. plantConditionMark,
  16. plantJobOnVehicle,
  17. userIdFor,
  18. } from '../../support/db'
  19. import { shareLink } from '../../support/work-order'
  20. /**
  21. * Where the condition map prints, and what it prints there.
  22. *
  23. * - The work order prints the job's own marks, earlier ones grey beside them,
  24. * and nothing at all when the job recorded none of its own.
  25. * - The invoice prints this visit only (the drop-off and the linked
  26. * inspection), is off unless the design or the job's own switch turns it on,
  27. * and once sent it is the copy the customer holds: a later change on the job
  28. * reaches it only when it is sent again.
  29. *
  30. * Read from the PDFs themselves, as text. Panel names are the app's English
  31. * ("Bonnet" for the hood).
  32. */
  33. const stamp = Date.now()
  34. const DAY = 24 * 60 * 60 * 1000
  35. let organizationId = ''
  36. let userId = ''
  37. test.beforeAll(async () => {
  38. organizationId = await ownerOrganizationId()
  39. userId = await userIdFor('demo@torqvoice.com')
  40. })
  41. const jobUrl = (job: { vehicleId: string; serviceRecordId: string }) =>
  42. `/vehicles/${job.vehicleId}/service/${job.serviceRecordId}`
  43. /** A car with a dent on the bonnet from a visit a month ago, and today's job on it. */
  44. async function carWithHistory(label: string) {
  45. const earlier = await plantConditionJob(
  46. organizationId,
  47. userId,
  48. `${label} earlier`,
  49. new Date(Date.now() - 30 * DAY)
  50. )
  51. await plantConditionMark(
  52. organizationId,
  53. earlier.vehicleId,
  54. { serviceRecordId: earlier.serviceRecordId },
  55. { view: 'top', panel: 'hood', recordedAt: new Date(Date.now() - 30 * DAY) }
  56. )
  57. const today = await plantJobOnVehicle(organizationId, earlier.vehicleId, label)
  58. return today
  59. }
  60. test.describe('the work order', () => {
  61. test('prints the job’s own marks, with earlier ones marked as such', async ({ page }) => {
  62. const job = await carWithHistory(`E2E WO print ${stamp}`)
  63. await plantConditionMark(
  64. organizationId,
  65. job.vehicleId,
  66. { serviceRecordId: job.serviceRecordId },
  67. { view: 'left', panel: 'left_front_door', kind: 'scratch', note: 'Keyed along the door' }
  68. )
  69. const pdf = await workOrderPdf(page, job.serviceRecordId)
  70. expect(printsMap(pdf), 'the section is there').toBe(true)
  71. expect(pdf.flat).toContain('Left front door')
  72. expect(pdf.flat).toContain('Keyed along the door')
  73. expect(pdf.flat).toContain('Bonnet (recorded earlier)')
  74. })
  75. test('prints nothing when the job recorded no mark of its own', async ({ page }) => {
  76. const job = await carWithHistory(`E2E WO bare ${stamp}`)
  77. const pdf = await workOrderPdf(page, job.serviceRecordId)
  78. expect(printsMap(pdf)).toBe(false)
  79. expect(pdf.flat).not.toContain('Bonnet')
  80. })
  81. test('prints no mark from a later visit on an older job', async ({ page }) => {
  82. const older = await plantConditionJob(
  83. organizationId,
  84. userId,
  85. `E2E WO older ${stamp}`,
  86. new Date(Date.now() - 60 * DAY)
  87. )
  88. await plantConditionMark(
  89. organizationId,
  90. older.vehicleId,
  91. { serviceRecordId: older.serviceRecordId },
  92. { view: 'left', panel: 'left_front_door', recordedAt: new Date(Date.now() - 60 * DAY) }
  93. )
  94. const later = await plantJobOnVehicle(organizationId, older.vehicleId, `E2E WO later ${stamp}`)
  95. await plantConditionMark(
  96. organizationId,
  97. older.vehicleId,
  98. { serviceRecordId: later.serviceRecordId },
  99. { view: 'rear', panel: 'rear_bumper' }
  100. )
  101. const pdf = await workOrderPdf(page, older.serviceRecordId)
  102. expect(printsMap(pdf)).toBe(true)
  103. expect(pdf.flat).toContain('Left front door')
  104. expect(pdf.flat, 'a dent found later was not there when this job was done').not.toContain(
  105. 'Rear bumper'
  106. )
  107. })
  108. })
  109. test.describe('the invoice', () => {
  110. test('leaves the map off until the job’s switch puts it on, and then prints this visit only', async ({
  111. page,
  112. }) => {
  113. const job = await carWithHistory(`E2E invoice map ${stamp}`)
  114. await plantConditionMark(
  115. organizationId,
  116. job.vehicleId,
  117. { serviceRecordId: job.serviceRecordId },
  118. { view: 'left', panel: 'left_front_door' }
  119. )
  120. // The linked inspection's marks are this visit too.
  121. const inspection = await plantConditionInspection(
  122. organizationId,
  123. job.vehicleId,
  124. `E2E ${stamp}`,
  125. {
  126. serviceRecordId: job.serviceRecordId,
  127. }
  128. )
  129. await plantConditionMark(organizationId, job.vehicleId, inspection, {
  130. view: 'top',
  131. panel: 'roof',
  132. })
  133. expect(printsMap(await invoicePdf(page, job.serviceRecordId)), 'off by default').toBe(false)
  134. await openDropoff(page, jobUrl(job))
  135. const onInvoice = page.getByTestId('condition-map-on-invoice')
  136. await expect(onInvoice).not.toBeChecked()
  137. await expect(async () => {
  138. await onInvoice.click()
  139. await expect(onInvoice).toBeChecked({ timeout: 2_000 })
  140. }).toPass({ timeout: 30_000 })
  141. await expect.poll(() => conditionMapOnInvoice(job.serviceRecordId)).toBe(true)
  142. const pdf = await invoicePdf(page, job.serviceRecordId)
  143. expect(printsMap(pdf)).toBe(true)
  144. expect(pdf.flat).toContain('Left front door')
  145. expect(pdf.flat, 'the linked inspection’s mark').toContain('Roof')
  146. expect(pdf.flat, 'an earlier visit is not billed here').not.toContain('Bonnet')
  147. expect(pdf.flat).not.toContain('recorded earlier')
  148. })
  149. test('once sent, keeps the map it was sent with until it is sent again', async ({ page }) => {
  150. const job = await plantConditionJob(organizationId, userId, `E2E sent map ${stamp}`)
  151. await plantConditionMark(
  152. organizationId,
  153. job.vehicleId,
  154. { serviceRecordId: job.serviceRecordId },
  155. { view: 'left', panel: 'left_front_door' }
  156. )
  157. await openDropoff(page, jobUrl(job))
  158. const onInvoice = page.getByTestId('condition-map-on-invoice')
  159. await expect(async () => {
  160. await onInvoice.click()
  161. await expect(onInvoice).toBeChecked({ timeout: 2_000 })
  162. }).toPass({ timeout: 30_000 })
  163. await expect.poll(() => conditionMapOnInvoice(job.serviceRecordId)).toBe(true)
  164. // Sent: the share link issues the invoice.
  165. await shareLink(page)
  166. await page.keyboard.press('Escape')
  167. // A dent found after it went out is the job's, not the sent invoice's.
  168. const again = await openDropoff(page, jobUrl(job))
  169. await addMark(page, again, 'top', 'trunk')
  170. await closeEditor(page)
  171. const sent = await invoicePdf(page, job.serviceRecordId)
  172. expect(printsMap(sent)).toBe(true)
  173. expect(sent.flat).toContain('Left front door')
  174. expect(sent.flat, 'added after it was sent').not.toContain('Boot lid')
  175. // The switch stays usable, and says where its change will land.
  176. await expect(onInvoice).toBeEnabled()
  177. await expect(page.getByTestId('sent-copy-note')).toContainText(
  178. 'Changes here go on the invoice when it is sent again'
  179. )
  180. // Unticked on the sent invoice, it asks first: the sent copy keeps its map.
  181. const asked = page.getByRole('alertdialog', { name: 'This invoice has already been sent' })
  182. await onInvoice.click()
  183. await expect(asked).toBeVisible()
  184. await asked.getByRole('button', { name: 'Cancel' }).click()
  185. await expect(asked).toBeHidden()
  186. await expect(onInvoice).toBeChecked()
  187. expect(await conditionMapOnInvoice(job.serviceRecordId)).toBe(true)
  188. await onInvoice.click()
  189. await asked.getByRole('button', { name: 'Save for the next send' }).click()
  190. await expect(onInvoice).not.toBeChecked()
  191. await expect.poll(() => conditionMapOnInvoice(job.serviceRecordId)).toBe(false)
  192. // Saved for the job; the invoice the customer holds still has its map.
  193. expect(printsMap(await invoicePdf(page, job.serviceRecordId))).toBe(true)
  194. })
  195. })