title-template.spec.ts 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. import { expect, type Page, test } from '@playwright/test'
  2. import {
  3. forgetWorkshopSetting,
  4. ownerOrganizationId,
  5. serviceRecordNames,
  6. vehicleFacts,
  7. workshopSetting,
  8. } from '../../support/db'
  9. import { settle } from '../../support/hydration'
  10. import { saveWorkOrder, seededVehicleUrl } from '../../support/work-order'
  11. /**
  12. * A new work order opens with a title the workshop chose.
  13. *
  14. * Every job used to start as "New Service Record" and wait for somebody to
  15. * type over it. A workshop asked for the title to fill itself in from the
  16. * job's number, the plate or the customer, so Settings → Workshop now holds a
  17. * template of tags, resolved on the server the moment the draft is made and
  18. * its number is known. The unit tests in
  19. * `src/__tests__/features/vehicles/work-order-title.test.ts` pin the
  20. * resolution rules; this file proves the chain from the settings page to the
  21. * title field of a job opened from a vehicle.
  22. *
  23. * The seeded workshop has never set a template, so the default applies until
  24. * this file saves one, and the setting is removed again at the end.
  25. */
  26. test.describe.configure({ mode: 'serial' })
  27. const KEY = 'workshop.workOrderTitleTemplate'
  28. let organizationId = ''
  29. let vehicleUrl = ''
  30. let vehicleId = ''
  31. let facts: Awaited<ReturnType<typeof vehicleFacts>>
  32. /** Types a template into Settings → Workshop and saves it. */
  33. async function saveTemplate(page: Page, template: string): Promise<void> {
  34. await page.goto('/settings/workshop')
  35. await settle(page)
  36. const field = page.locator('#workOrderTitleTemplate')
  37. await expect(field).toBeVisible()
  38. await field.fill(template)
  39. await page.getByRole('button', { name: 'Save Workshop Settings', exact: true }).click()
  40. await expect(page.getByText('Workshop settings saved', { exact: true })).toBeVisible()
  41. await expect.poll(() => workshopSetting(organizationId, KEY)).toBe(template)
  42. }
  43. /** When the last job was opened: `/service/new` reuses an untouched draft younger than five seconds. */
  44. let lastOpenedAt = 0
  45. /** Opens a fresh job on the seeded vehicle and returns what it was called and numbered. */
  46. async function openNewJob(page: Page) {
  47. const wait = lastOpenedAt + 5_500 - Date.now()
  48. if (wait > 0) await page.waitForTimeout(wait)
  49. lastOpenedAt = Date.now()
  50. await page.goto(`${vehicleUrl}/service/new`)
  51. await page.waitForURL(/\/vehicles\/[^/]+\/service\/[^/]+$/)
  52. await settle(page)
  53. const id = page.url().split('/').pop() ?? ''
  54. const row = await serviceRecordNames(id)
  55. return { id, ...row, field: page.locator('input[name="title"]') }
  56. }
  57. test.beforeAll(async ({ browser }) => {
  58. organizationId = await ownerOrganizationId()
  59. await forgetWorkshopSetting(organizationId, KEY)
  60. const page = await browser.newPage({ storageState: 'e2e/.auth/owner.json' })
  61. vehicleUrl = await seededVehicleUrl(page)
  62. vehicleId = vehicleUrl.split('/').pop() ?? ''
  63. facts = await vehicleFacts(vehicleId)
  64. expect(facts.licensePlate, 'the seeded vehicle has a plate to print').toBeTruthy()
  65. expect(facts.customerName, 'and an owner').toBeTruthy()
  66. await page.close()
  67. })
  68. test.afterAll(async () => {
  69. await forgetWorkshopSetting(organizationId, KEY)
  70. })
  71. test.describe('the default, before the workshop has chosen anything', () => {
  72. test('names a new job after its number and the plate', async ({ page }) => {
  73. const job = await openNewJob(page)
  74. expect(job.invoiceNumber, 'the job was numbered').toBeTruthy()
  75. expect(job.title).toBe(`${job.invoiceNumber} - ${facts.licensePlate}`)
  76. // And that is what the editor shows, ready to be typed over.
  77. await expect(job.field).toHaveValue(job.title)
  78. })
  79. test('is what the settings page offers to edit', async ({ page }) => {
  80. await page.goto('/settings/workshop')
  81. await settle(page)
  82. await expect(page.locator('#workOrderTitleTemplate')).toHaveValue(
  83. '{order_number} - {license_plate}'
  84. )
  85. await expect(page.getByTestId('work-order-title-preview')).toHaveText(
  86. 'Example: 2026-1042 - AB 12345'
  87. )
  88. })
  89. })
  90. test.describe('a template the workshop wrote', () => {
  91. test('is previewed as it is typed, tag by tag', async ({ page }) => {
  92. await page.goto('/settings/workshop')
  93. await settle(page)
  94. const field = page.locator('#workOrderTitleTemplate')
  95. await field.fill('')
  96. await page.getByTestId('work-order-title-tag-customer_name').click()
  97. await page.getByTestId('work-order-title-tag-vehicle').click()
  98. // Two tags added back to back are kept apart by a space.
  99. await expect(field).toHaveValue('{customer_name} {vehicle}')
  100. await expect(page.getByTestId('work-order-title-preview')).toHaveText(
  101. 'Example: Jane Cooper 2021 Toyota Camry'
  102. )
  103. // A tag nobody knows is pointed out, and left out of the example.
  104. await field.fill('{order_number} - {nope}')
  105. await expect(page.getByTestId('work-order-title-unknown')).toHaveText(
  106. 'Unknown tags are left out: {nope}'
  107. )
  108. await expect(page.getByTestId('work-order-title-preview')).toHaveText('Example: 2026-1042')
  109. })
  110. test('names every new job with the customer, the car and the number', async ({ page }) => {
  111. await saveTemplate(page, '{customer_name} · {license_plate} · WO#{order_number}')
  112. const job = await openNewJob(page)
  113. expect(job.title).toBe(
  114. `${facts.customerName} · ${facts.licensePlate} · WO#${job.invoiceNumber}`
  115. )
  116. await expect(job.field).toHaveValue(job.title)
  117. })
  118. test('leaves out a tag nobody knows, with its separator', async ({ page }) => {
  119. await saveTemplate(page, '{order_number} - {nope} - {license_plate}')
  120. const job = await openNewJob(page)
  121. expect(job.title).toBe(`${job.invoiceNumber} - ${facts.licensePlate}`)
  122. })
  123. test('prints the car as year, make and model', async ({ page }) => {
  124. await saveTemplate(page, '{vehicle} ({order_number})')
  125. const job = await openNewJob(page)
  126. expect(job.title).toBe(`${facts.year} ${facts.make} ${facts.model} (${job.invoiceNumber})`)
  127. })
  128. test('saved empty brings back the plain name', async ({ page }) => {
  129. await saveTemplate(page, '')
  130. const job = await openNewJob(page)
  131. expect(job.title).toBe('New Service Record')
  132. })
  133. test('can still be typed over on the job', async ({ page }) => {
  134. await saveTemplate(page, '{order_number} - {license_plate}')
  135. const job = await openNewJob(page)
  136. await job.field.fill('Brake pads, front')
  137. await saveWorkOrder(page)
  138. expect((await serviceRecordNames(job.id)).title).toBe('Brake pads, front')
  139. })
  140. })