modern-layout.spec.ts 7.7 KB

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