inspection.spec.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. import { expect, type Page, test } from '@playwright/test'
  2. import {
  3. addMark,
  4. certificatePdf,
  5. closeEditor,
  6. conditionMap,
  7. earlierSection,
  8. markEditor,
  9. openDropoff,
  10. ownMarks,
  11. printsMap,
  12. } from '../../support/condition-map'
  13. import {
  14. conditionMarksOf,
  15. forgetWorkshopSetting,
  16. ownerOrganizationId,
  17. plantConditionInspection,
  18. plantConditionJob,
  19. plantConditionMark,
  20. plantJobOnVehicle,
  21. setWorkshopSetting,
  22. userIdFor,
  23. workshopSetting,
  24. } from '../../support/db'
  25. import { settle } from '../../support/hydration'
  26. /**
  27. * The condition map as an inspection check, and how it meets the work order.
  28. *
  29. * Marks drawn on the inspection are this visit on the job it is linked to
  30. * (shown there in colour, changed only on the inspection), and a job's
  31. * drop-off marks are grey on the inspection. A completed inspection is a
  32. * finished document: its map can no longer be changed, and its certificate
  33. * keeps what it said when a mark is later cleared as repaired.
  34. *
  35. * The certificate only prints the map from a designed certificate, so the
  36. * file gives the workshop one and takes it away again afterwards.
  37. */
  38. const stamp = Date.now()
  39. const DESIGN_KEY = 'certificate.layoutConfig'
  40. let organizationId = ''
  41. let userId = ''
  42. let designBefore: string | null = null
  43. test.beforeAll(async () => {
  44. organizationId = await ownerOrganizationId()
  45. userId = await userIdFor('demo@torqvoice.com')
  46. designBefore = await setWorkshopSetting(
  47. organizationId,
  48. DESIGN_KEY,
  49. JSON.stringify({ version: 2, documentType: 'certificate' })
  50. )
  51. })
  52. test.afterAll(async () => {
  53. if (designBefore === null) await forgetWorkshopSetting(organizationId, DESIGN_KEY)
  54. else await setWorkshopSetting(organizationId, DESIGN_KEY, designBefore)
  55. })
  56. const jobUrl = (job: { vehicleId: string; serviceRecordId: string }) =>
  57. `/vehicles/${job.vehicleId}/service/${job.serviceRecordId}`
  58. async function openInspection(page: Page, inspectionId: string) {
  59. await page.goto(`/inspections/${inspectionId}`)
  60. await settle(page)
  61. const map = conditionMap(page)
  62. await expect(map).toBeVisible({ timeout: 30_000 })
  63. return map
  64. }
  65. async function complete(page: Page) {
  66. await expect(async () => {
  67. await page.getByRole('button', { name: 'Complete', exact: true }).first().click()
  68. await expect(page.getByRole('alertdialog', { name: 'Complete this inspection?' })).toBeVisible({
  69. timeout: 2_000,
  70. })
  71. }).toPass({ timeout: 30_000 })
  72. const confirm = page.getByRole('alertdialog', { name: 'Complete this inspection?' })
  73. await confirm.getByRole('button', { name: 'Complete', exact: true }).click()
  74. await expect(page.getByText('Inspection completed').first()).toBeVisible()
  75. // Completing offers to share the certificate straight away; not today.
  76. const share = page.getByRole('dialog').filter({ hasText: 'Share a read-only view' })
  77. const offered = await share
  78. .waitFor({ state: 'visible', timeout: 5_000 })
  79. .then(() => true)
  80. .catch(() => false)
  81. if (offered) {
  82. await page.keyboard.press('Escape')
  83. await expect(share).toBeHidden()
  84. }
  85. }
  86. test('a mark drawn on the inspection is on the linked job, where it cannot be changed', async ({
  87. page,
  88. }) => {
  89. const job = await plantConditionJob(organizationId, userId, `E2E linked ${stamp}`)
  90. const inspection = await plantConditionInspection(organizationId, job.vehicleId, `E2E ${stamp}`, {
  91. serviceRecordId: job.serviceRecordId,
  92. })
  93. const map = await openInspection(page, inspection.inspectionId)
  94. const editor = await addMark(page, map, 'right', 'right_front_door')
  95. // The inspection offers the workshop's kinds, and names the mark by them.
  96. await expect(editor.getByRole('heading')).toContainText('Dent on the Right front door')
  97. const kinds = editor.getByRole('group', { name: 'Type' }).getByRole('button')
  98. await expect(kinds).toHaveCount(8)
  99. await expect(kinds.filter({ hasText: 'Scratch' })).toHaveCount(1)
  100. await closeEditor(page)
  101. await expect(ownMarks(map)).toHaveCount(1)
  102. const [row] = await conditionMarksOf(job.vehicleId)
  103. expect(row).toMatchObject({
  104. inspectionId: inspection.inspectionId,
  105. serviceRecordId: null,
  106. panel: 'right_front_door',
  107. })
  108. // On the job it is this visit: in colour, said to come from the inspection.
  109. const onJob = await openDropoff(page, jobUrl(job))
  110. await expect(ownMarks(onJob)).toHaveCount(1)
  111. await expect(onJob).toContainText('From the linked inspection, changed there')
  112. await ownMarks(onJob).first().click()
  113. const onJobEditor = markEditor(page)
  114. await expect(onJobEditor.getByRole('button', { name: 'Scratch' })).toBeDisabled()
  115. await expect(onJobEditor.getByRole('button', { name: 'Remove mark' })).toHaveCount(0)
  116. // And says where it can be changed, rather than asking to reopen the job.
  117. await expect(onJobEditor).toContainText('Drawn on the linked inspection. Change it there.')
  118. await expect(onJobEditor).not.toContainText('Reopen to change the condition map')
  119. })
  120. test('a job’s drop-off mark is grey on the inspection', async ({ page }) => {
  121. const job = await plantConditionJob(organizationId, userId, `E2E dropoff grey ${stamp}`)
  122. await plantConditionMark(
  123. organizationId,
  124. job.vehicleId,
  125. { serviceRecordId: job.serviceRecordId },
  126. { view: 'top', panel: 'hood', recordedAt: new Date(Date.now() - 60_000) }
  127. )
  128. const inspection = await plantConditionInspection(organizationId, job.vehicleId, `E2E ${stamp}`, {
  129. serviceRecordId: job.serviceRecordId,
  130. })
  131. const map = await openInspection(page, inspection.inspectionId)
  132. await expect(ownMarks(map)).toHaveCount(0)
  133. await expect(earlierSection(map)).toContainText('Bonnet')
  134. })
  135. test('a second map check on the inspection is the same visit, changed on its own check', async ({
  136. page,
  137. }) => {
  138. const job = await plantConditionJob(organizationId, userId, `E2E two checks ${stamp}`)
  139. const inspection = await plantConditionInspection(organizationId, job.vehicleId, `E2E ${stamp}`, {
  140. secondCheck: true,
  141. })
  142. await plantConditionMark(
  143. organizationId,
  144. job.vehicleId,
  145. { inspectionId: inspection.inspectionId, inspectionItemId: inspection.secondItemId! },
  146. { view: 'rear', panel: 'rear_bumper' }
  147. )
  148. await openInspection(page, inspection.inspectionId)
  149. const maps = page.getByTestId('condition-map')
  150. await expect(maps).toHaveCount(2)
  151. const first = maps.first()
  152. // On the first check it is this visit, in colour, said to come from the other check.
  153. await expect(ownMarks(first)).toHaveCount(1)
  154. await expect(earlierSection(first)).toHaveCount(0)
  155. await expect(first).toContainText('From another check on this inspection')
  156. await ownMarks(first).first().click()
  157. const editor = markEditor(page)
  158. await expect(editor).toContainText('Drawn on another check of this inspection. Change it there.')
  159. await expect(editor.getByRole('button', { name: 'Remove mark' })).toHaveCount(0)
  160. await page.keyboard.press('Escape')
  161. // On its own check it can be changed.
  162. await ownMarks(maps.last()).first().click()
  163. await expect(editor.getByRole('button', { name: 'Remove mark' })).toBeVisible()
  164. })
  165. test('a completed inspection’s map cannot be changed until it is reopened', async ({ page }) => {
  166. const job = await plantConditionJob(organizationId, userId, `E2E completed ${stamp}`)
  167. const inspection = await plantConditionInspection(organizationId, job.vehicleId, `E2E ${stamp}`)
  168. await plantConditionMark(organizationId, job.vehicleId, inspection, {
  169. view: 'left',
  170. panel: 'left_rear_door',
  171. })
  172. const map = await openInspection(page, inspection.inspectionId)
  173. await complete(page)
  174. await expect(map).toContainText('Reopen to change the condition map.')
  175. // A click on the drawing adds nothing.
  176. await map.locator('path[data-view="top"][data-panel="roof"]').click()
  177. await expect(markEditor(page)).toBeHidden()
  178. expect(await conditionMarksOf(job.vehicleId)).toHaveLength(1)
  179. })
  180. test.describe('the certificate', () => {
  181. test('prints the inspection’s marks', async ({ page }) => {
  182. const job = await plantConditionJob(organizationId, userId, `E2E cert ${stamp}`)
  183. const inspection = await plantConditionInspection(organizationId, job.vehicleId, `E2E ${stamp}`)
  184. await plantConditionMark(organizationId, job.vehicleId, inspection, {
  185. view: 'left',
  186. panel: 'left_front_door',
  187. note: 'Dent by the handle',
  188. })
  189. const pdf = await certificatePdf(page, inspection.inspectionId)
  190. expect(printsMap(pdf)).toBe(true)
  191. expect(pdf.flat).toContain('Left front door')
  192. expect(pdf.flat).toContain('Dent by the handle')
  193. })
  194. test('prints from the default Regulatory design when the workshop designed none', async ({
  195. page,
  196. }) => {
  197. const job = await plantConditionJob(organizationId, userId, `E2E cert default ${stamp}`)
  198. const inspection = await plantConditionInspection(organizationId, job.vehicleId, `E2E ${stamp}`)
  199. await plantConditionMark(organizationId, job.vehicleId, inspection, {
  200. view: 'left',
  201. panel: 'left_front_door',
  202. })
  203. // No certificate design at all: before, this printed the built-in sheet,
  204. // which has no condition map.
  205. const planted = await workshopSetting(organizationId, DESIGN_KEY)
  206. await forgetWorkshopSetting(organizationId, DESIGN_KEY)
  207. try {
  208. const pdf = await certificatePdf(page, inspection.inspectionId)
  209. expect(printsMap(pdf), 'the designed certificate prints the map').toBe(true)
  210. expect(pdf.flat).toContain('Left front door')
  211. } finally {
  212. if (planted) await setWorkshopSetting(organizationId, DESIGN_KEY, planted)
  213. }
  214. })
  215. test('prints no map when the inspection recorded no mark of its own', async ({ page }) => {
  216. const job = await plantConditionJob(organizationId, userId, `E2E cert bare ${stamp}`)
  217. await plantConditionMark(
  218. organizationId,
  219. job.vehicleId,
  220. { serviceRecordId: job.serviceRecordId },
  221. { view: 'top', panel: 'hood', recordedAt: new Date(Date.now() - 60_000) }
  222. )
  223. const inspection = await plantConditionInspection(organizationId, job.vehicleId, `E2E ${stamp}`)
  224. const pdf = await certificatePdf(page, inspection.inspectionId)
  225. expect(printsMap(pdf)).toBe(false)
  226. expect(pdf.flat).not.toContain('Bonnet')
  227. })
  228. test('keeps what it said when a mark is later cleared as repaired', async ({ page }) => {
  229. const job = await plantConditionJob(organizationId, userId, `E2E cert frozen ${stamp}`)
  230. const inspection = await plantConditionInspection(organizationId, job.vehicleId, `E2E ${stamp}`)
  231. await plantConditionMark(organizationId, job.vehicleId, inspection, {
  232. view: 'left',
  233. panel: 'left_front_door',
  234. recordedAt: new Date(Date.now() - 60_000),
  235. })
  236. await openInspection(page, inspection.inspectionId)
  237. await complete(page)
  238. expect((await certificatePdf(page, inspection.inspectionId)).flat).toContain('Left front door')
  239. // The next job finds it repaired and clears it: allowed, and the history keeps it.
  240. const nextJob = await plantJobOnVehicle(organizationId, job.vehicleId, `E2E next ${stamp}`)
  241. const next = await openDropoff(page, jobUrl(nextJob))
  242. const grey = earlierSection(next)
  243. await grey.getByRole('button', { name: 'Clear as repaired' }).click()
  244. await page
  245. .getByRole('alertdialog', { name: 'Clear as repaired' })
  246. .getByRole('button', { name: 'Clear as repaired' })
  247. .click()
  248. await expect(earlierSection(next)).toHaveCount(0)
  249. const after = await certificatePdf(page, inspection.inspectionId)
  250. expect(printsMap(after), 'the completed certificate still has its map').toBe(true)
  251. expect(after.flat).toContain('Left front door')
  252. })
  253. })