modern-layout.spec.ts 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. import { expect, test } from '@playwright/test'
  2. import { settle } from '../../support/hydration'
  3. import { addPart, newWorkOrder, saveWorkOrder, seededVehicleUrl } from '../../support/work-order'
  4. /**
  5. * The work order page every browser opens. It is one form, so what these
  6. * tests hold it to is each field once, and a save that keeps what was on
  7. * screen. The classic page is still offered in the menu but is not tested.
  8. */
  9. test.describe.configure({ mode: 'serial' })
  10. const stamp = Date.now()
  11. const partName = `E2E wiper blade ${stamp}`
  12. let jobUrl = ''
  13. test.beforeAll(async ({ browser }) => {
  14. const page = await browser.newPage({ storageState: 'e2e/.auth/owner.json' })
  15. const vehicleUrl = await seededVehicleUrl(page)
  16. jobUrl = await newWorkOrder(page, vehicleUrl, `E2E modern layout ${stamp}`)
  17. await addPart(page, { name: partName, quantity: 2, unitPrice: 150 })
  18. await saveWorkOrder(page)
  19. await page.close()
  20. })
  21. test.describe('the work order page', () => {
  22. test('opens for a browser that has not chosen a layout', async ({ page }) => {
  23. await page.goto(jobUrl)
  24. await settle(page)
  25. await expect(page.getByTestId('service-layout-modern')).toBeVisible()
  26. await expect(page.getByTestId('service-layout')).toHaveCount(0)
  27. // The top of the page is the work order's number and its status, then
  28. // what the job is called.
  29. const hero = page.getByTestId('service-hero')
  30. await expect(hero.getByTestId('service-number')).not.toBeEmpty()
  31. await expect(hero.getByTestId('service-status')).toHaveText('Pending')
  32. await expect(hero).toContainText(`E2E modern layout ${stamp}`)
  33. })
  34. test('holds one copy of each field and keeps photos on the job', async ({ page }) => {
  35. await page.goto(jobUrl)
  36. await settle(page)
  37. await expect(page.getByTestId('service-layout-modern')).toBeVisible()
  38. for (const selector of [
  39. 'input[name="title"]',
  40. '#invoiceNumber',
  41. '#invoiceDate',
  42. '#mileage',
  43. 'input[name="techName"]',
  44. 'input[name="serviceDate"]',
  45. 'textarea[placeholder="Name *"]',
  46. ]) {
  47. await expect(page.locator(selector), `${selector} appears once`).toHaveCount(1)
  48. }
  49. const duplicates = await page.evaluate(() => {
  50. const form = document.querySelector('form')
  51. if (!form) return ['no form']
  52. const seen = new Map<string, number>()
  53. for (const el of form.querySelectorAll<HTMLInputElement>('input[name], textarea[name]')) {
  54. seen.set(el.name, (seen.get(el.name) ?? 0) + 1)
  55. }
  56. return [...seen.entries()].filter(([, count]) => count > 1).map(([name]) => name)
  57. })
  58. expect(duplicates, 'field names used twice in the form').toEqual([])
  59. // No tab bar at the top of the page: photos, documents, diagnostics,
  60. // video and status reports are tabs of one card on the job itself.
  61. const files = page.getByTestId('files-media')
  62. await expect(files).toBeVisible()
  63. await expect(page.getByRole('button', { name: /^Images/ })).toHaveCount(0)
  64. await expect(page.getByRole('button', { name: /^Status Reports/ })).toHaveCount(0)
  65. for (const name of [
  66. /^All/,
  67. /^Photos/,
  68. /^Documents/,
  69. /^Diagnostics/,
  70. /^Video/,
  71. /^Status Reports/,
  72. ]) {
  73. await expect(files.getByRole('tab', { name })).toBeVisible()
  74. }
  75. // It opens on everything, and each kind has its own way in.
  76. await expect(files.getByRole('tab', { name: /^All/ })).toHaveAttribute('aria-selected', 'true')
  77. await expect(files.getByTestId('media-add')).toContainText('Add files')
  78. await files.getByRole('tab', { name: /^Photos/ }).click()
  79. await expect(files.getByTestId('media-add')).toContainText('Add photos')
  80. await files.getByRole('tab', { name: /^Status Reports/ }).click()
  81. await expect(files.getByTestId('status-reports-section')).toBeVisible()
  82. // The money stays on screen along the bottom, with what is done about it.
  83. const bar = page.getByTestId('money-bar')
  84. await expect(bar).toBeInViewport()
  85. await expect(bar.getByTestId('money-balance')).toContainText('300')
  86. await expect(bar.getByRole('button', { name: 'Take payment' })).toBeVisible()
  87. await expect(bar.getByRole('button', { name: /Preview/ })).toBeVisible()
  88. await expect(bar.getByRole('button', { name: /Send/ })).toBeVisible()
  89. })
  90. test('saves what is on screen', async ({ page }) => {
  91. await page.goto(jobUrl)
  92. await settle(page)
  93. await expect(page.getByTestId('service-layout-modern')).toBeVisible()
  94. // The part saved when the job was made is here, with its figures.
  95. await expect(page.locator('textarea[placeholder="Name *"]')).toHaveValue(partName)
  96. // The stepper is the status control: walk the job on and save.
  97. const stepper = page.getByTestId('status-stepper')
  98. await stepper.getByRole('button', { name: /In Progress/i }).click()
  99. await expect(stepper.getByRole('button', { name: /In Progress/i })).toHaveAttribute(
  100. 'aria-current',
  101. 'step'
  102. )
  103. // The title is edited in the header, behind its pencil; the form carries
  104. // it as a hidden field, so Save takes it with everything else.
  105. const title = `E2E modern layout ${stamp} renamed`
  106. await page.getByTestId('edit-title').click()
  107. await page.getByTestId('title-input').fill(title)
  108. await page.getByTestId('title-input').press('Enter')
  109. await expect(page.getByTestId('service-title')).toHaveText(title)
  110. await expect(page.locator('input[name="title"]')).toHaveValue(title)
  111. await saveWorkOrder(page)
  112. await page.reload()
  113. await settle(page)
  114. await expect(page.getByTestId('service-title').first()).toHaveText(title)
  115. await expect(
  116. page.getByTestId('status-stepper').getByRole('button', { name: /In Progress/i })
  117. ).toHaveAttribute('aria-current', 'step')
  118. await expect(page.locator('textarea[placeholder="Name *"]')).toHaveValue(partName)
  119. })
  120. test('stacks into one column on a phone without overflowing sideways', async ({ page }) => {
  121. await page.setViewportSize({ width: 390, height: 844 })
  122. await page.goto(jobUrl)
  123. await settle(page)
  124. const layout = page.getByTestId('service-layout-modern')
  125. await expect(layout).toBeVisible()
  126. // The money bar is a desktop thing: the page already carries its totals,
  127. // payment form, preview and send, and a phone has no height to spare.
  128. await expect(page.getByTestId('money-bar')).toBeHidden()
  129. // What it leaves behind is the running total, in the flow under the parts,
  130. // so adding a line on a small screen still shows what it did to the bill.
  131. await expect(page.getByTestId('money-line')).toBeVisible()
  132. // The header's second line is which job this is; when it was opened is
  133. // left for a wider screen.
  134. await expect(page.getByTestId('service-opened')).toBeHidden()
  135. // Each control of the status stepper stays in its own space: a label is
  136. // cut short rather than running into the next. Three stages, and the
  137. // arrow beside "In Progress" that opens the statuses filed under it
  138. // (always there, for "Waiting Parts"): four buttons in all on a workshop
  139. // that has defined no statuses of its own.
  140. const stages = await page
  141. .getByTestId('status-stepper')
  142. .getByRole('button')
  143. .evaluateAll((buttons) => buttons.map((b) => b.getBoundingClientRect().toJSON()))
  144. expect(stages).toHaveLength(4)
  145. for (let i = 1; i < stages.length; i++) {
  146. expect(
  147. stages[i - 1].right,
  148. `stage ${i} ends before stage ${i + 1} starts`
  149. ).toBeLessThanOrEqual(stages[i].left)
  150. }
  151. const overflow = await layout.evaluate((el) => el.scrollWidth - el.clientWidth)
  152. expect(overflow, 'sideways overflow in pixels').toBeLessThanOrEqual(1)
  153. const main = await page.getByTestId('service-main').boundingBox()
  154. const sidebar = await page.getByTestId('service-sidebar').boundingBox()
  155. expect(sidebar?.y ?? 0, 'the sidebar sits under the job').toBeGreaterThan(
  156. (main?.y ?? 0) + (main?.height ?? 0) - 1
  157. )
  158. })
  159. })