deletion.spec.ts 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. import { expect, type Page, test } from '@playwright/test'
  2. import { settle } from '../../support/hydration'
  3. import {
  4. ownerOrganizationId,
  5. type PlantedVehicle,
  6. plantVehicleWithFiles,
  7. removePlantedVehicle,
  8. userIdFor,
  9. } from '../../support/db'
  10. import {
  11. onDisk,
  12. type PlantedFile,
  13. plantFile,
  14. removePlanted,
  15. sharesDiskWithServer,
  16. } from '../../support/files'
  17. /**
  18. * What deleting something does to the files on disk. Every delete goes
  19. * through the file manager (src/lib/files/manager.ts), which removes a file
  20. * only once no row points at it, only inside the workshop's own folder, and
  21. * only after the rows are gone. These specs plant real files and the rows
  22. * that use them, delete through the page the way a workshop does, and read
  23. * the disk:
  24. *
  25. * - what went with the deleted rows is gone;
  26. * - what something else still uses stays: a tire set's photo copied onto a
  27. * job (the set outlives the job and the vehicle), a quote's document (a
  28. * quote outlives its vehicle);
  29. * - another workshop's file stays, whatever URL a row holds.
  30. */
  31. test.describe.configure({ mode: 'serial' })
  32. const stamp = Date.now()
  33. let organizationId = ''
  34. let userId = ''
  35. const foreignOrg = `e2e-foreign-${stamp}`
  36. const planted: PlantedVehicle[] = []
  37. const files: PlantedFile[] = []
  38. async function plantAll(label: string, withInspection = true) {
  39. const f = {
  40. vehicleImage: await plantFile(organizationId, 'vehicles'),
  41. jobPhoto: await plantFile(organizationId, 'services'),
  42. tireSetPhoto: await plantFile(organizationId, 'tire-hotel'),
  43. statusVideo: await plantFile(organizationId, 'services', 'mp4'),
  44. inspectionPhoto: await plantFile(organizationId, 'services'),
  45. quoteDocument: await plantFile(organizationId, 'quotes', 'pdf'),
  46. foreignPhoto: await plantFile(foreignOrg, 'services'),
  47. }
  48. files.push(...Object.values(f))
  49. const vehicle = await plantVehicleWithFiles(
  50. organizationId,
  51. userId,
  52. {
  53. vehicleImage: f.vehicleImage.url,
  54. jobPhoto: f.jobPhoto.url,
  55. tireSetPhoto: f.tireSetPhoto.url,
  56. statusVideo: f.statusVideo.url,
  57. inspectionPhoto: withInspection ? f.inspectionPhoto.url : undefined,
  58. quoteDocument: f.quoteDocument.url,
  59. foreignPhoto: f.foreignPhoto.url,
  60. },
  61. `${label} ${stamp}`
  62. )
  63. planted.push(vehicle)
  64. return { f, vehicle }
  65. }
  66. async function expectGone(file: PlantedFile, what: string) {
  67. await expect.poll(() => onDisk(file), { message: `${what} is deleted` }).toBe(false)
  68. }
  69. function expectKept(file: PlantedFile, what: string) {
  70. expect(onDisk(file), `${what} is kept`).toBe(true)
  71. }
  72. async function confirmIn(page: Page, dialogTitle: RegExp) {
  73. const dialog = page.getByRole('alertdialog', { name: dialogTitle })
  74. await expect(dialog).toBeVisible()
  75. await dialog.getByRole('button', { name: 'Delete', exact: true }).click()
  76. }
  77. test.beforeAll(async () => {
  78. organizationId = await ownerOrganizationId()
  79. userId = await userIdFor('demo@torqvoice.com')
  80. })
  81. test.beforeEach(() => {
  82. test.skip(!sharesDiskWithServer(), 'the suite does not share a disk with the app server')
  83. })
  84. test.afterAll(async () => {
  85. for (const vehicle of planted) await removePlantedVehicle(vehicle).catch(() => undefined)
  86. await removePlanted(files)
  87. })
  88. test('deleting a vehicle removes its files and keeps what outlives it', async ({ page }) => {
  89. const { f, vehicle } = await plantAll('E2E files vehicle')
  90. for (const file of Object.values(f)) expect(onDisk(file)).toBe(true)
  91. await page.goto(`/vehicles/${vehicle.vehicleId}`)
  92. await settle(page)
  93. await expect(async () => {
  94. await page.getByRole('button', { name: 'Open menu' }).click()
  95. await page.getByRole('menuitem', { name: 'Delete' }).click({ timeout: 2_000 })
  96. }).toPass({ timeout: 30_000 })
  97. await confirmIn(page, /Delete vehicle/)
  98. await page.waitForURL((url) => !url.pathname.includes(vehicle.vehicleId))
  99. // Went with the vehicle.
  100. await expectGone(f.vehicleImage, "the vehicle's image")
  101. await expectGone(f.jobPhoto, "its job's photo")
  102. await expectGone(f.statusVideo, "its job's status report video")
  103. if (vehicle.inspected) await expectGone(f.inspectionPhoto, "its inspection's photo")
  104. // Still used: the tire set and the quote are not deleted with the vehicle.
  105. expectKept(f.tireSetPhoto, "the tire set's photo, which was also on the job")
  106. expectKept(f.quoteDocument, "the quote's document")
  107. // Not this workshop's to delete, whatever the job's row said.
  108. expectKept(f.foreignPhoto, "another workshop's file")
  109. })
  110. test("deleting a work order keeps the tire set's photo that was on it", async ({ page }) => {
  111. const { f, vehicle } = await plantAll('E2E files job', false)
  112. await page.goto(`/vehicles/${vehicle.vehicleId}/service/${vehicle.serviceRecordId}`)
  113. await settle(page)
  114. await expect(async () => {
  115. await page.getByRole('button', { name: 'More actions' }).click()
  116. await page.getByRole('menuitem', { name: 'Delete' }).click({ timeout: 2_000 })
  117. }).toPass({ timeout: 30_000 })
  118. await confirmIn(page, /Delete Service Record/)
  119. await page.waitForURL((url) => !url.pathname.includes(vehicle.serviceRecordId))
  120. await expectGone(f.jobPhoto, "the job's own photo")
  121. await expectGone(f.statusVideo, "the job's status report video")
  122. // This is the file the old delete took from the tire set.
  123. expectKept(f.tireSetPhoto, "the tire set's photo")
  124. expectKept(f.vehicleImage, "the vehicle's image")
  125. expectKept(f.foreignPhoto, "another workshop's file")
  126. })
  127. test("deleting a photo on the work order removes its file, and a tire set's copy leaves it", async ({
  128. page,
  129. }) => {
  130. const { f, vehicle } = await plantAll('E2E files photo', false)
  131. await page.goto(`/vehicles/${vehicle.vehicleId}/service/${vehicle.serviceRecordId}`)
  132. await settle(page)
  133. const tiles = page.getByTestId('files-media').getByTestId('media-tile')
  134. const tileOf = (file: PlantedFile) =>
  135. tiles.filter({ has: page.locator(`img[src="${file.url}"]`) })
  136. await expect(tileOf(f.jobPhoto)).toHaveCount(1)
  137. await tileOf(f.jobPhoto).getByRole('button', { name: 'Delete' }).click()
  138. await expect(tileOf(f.jobPhoto)).toHaveCount(0)
  139. await expectGone(f.jobPhoto, 'the deleted photo')
  140. // The tire set's photo is on the job as a copy; deleting the copy leaves
  141. // the set its file.
  142. await expect(tileOf(f.tireSetPhoto)).toHaveCount(1)
  143. await tileOf(f.tireSetPhoto).getByRole('button', { name: 'Delete' }).click()
  144. await expect(tileOf(f.tireSetPhoto)).toHaveCount(0)
  145. expectKept(f.tireSetPhoto, "the tire set's photo")
  146. })