modern-layout.spec.ts 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  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 [/^Photos/, /^Documents/, /^Diagnostics/, /^Video/, /^Status Reports/]) {
  66. await expect(files.getByRole('tab', { name })).toBeVisible()
  67. }
  68. await expect(files.getByTestId('media-add')).toContainText('Add photos')
  69. await files.getByRole('tab', { name: /^Status Reports/ }).click()
  70. await expect(files.getByTestId('status-reports-section')).toBeVisible()
  71. // The money stays on screen along the bottom, with what is done about it.
  72. const bar = page.getByTestId('money-bar')
  73. await expect(bar).toBeInViewport()
  74. await expect(bar.getByTestId('money-balance')).toContainText('300')
  75. await expect(bar.getByRole('button', { name: 'Take payment' })).toBeVisible()
  76. await expect(bar.getByRole('button', { name: /Preview/ })).toBeVisible()
  77. await expect(bar.getByRole('button', { name: /Send/ })).toBeVisible()
  78. })
  79. test('saves what is on screen', async ({ page }) => {
  80. await page.goto(jobUrl)
  81. await settle(page)
  82. await expect(page.getByTestId('service-layout-modern')).toBeVisible()
  83. // The part saved when the job was made is here, with its figures.
  84. await expect(page.locator('textarea[placeholder="Name *"]')).toHaveValue(partName)
  85. // The stepper is the status control: walk the job on and save.
  86. const stepper = page.getByTestId('status-stepper')
  87. await stepper.getByRole('button', { name: /In Progress/i }).click()
  88. await expect(stepper.getByRole('button', { name: /In Progress/i })).toHaveAttribute(
  89. 'aria-current',
  90. 'step'
  91. )
  92. // The title is edited in the header, behind its pencil; the form carries
  93. // it as a hidden field, so Save takes it with everything else.
  94. const title = `E2E modern layout ${stamp} renamed`
  95. await page.getByTestId('edit-title').click()
  96. await page.getByTestId('title-input').fill(title)
  97. await page.getByTestId('title-input').press('Enter')
  98. await expect(page.getByTestId('service-title')).toHaveText(title)
  99. await expect(page.locator('input[name="title"]')).toHaveValue(title)
  100. await saveWorkOrder(page)
  101. await page.reload()
  102. await settle(page)
  103. await expect(page.getByTestId('service-title').first()).toHaveText(title)
  104. await expect(
  105. page.getByTestId('status-stepper').getByRole('button', { name: /In Progress/i })
  106. ).toHaveAttribute('aria-current', 'step')
  107. await expect(page.locator('textarea[placeholder="Name *"]')).toHaveValue(partName)
  108. })
  109. test('stacks into one column on a phone without overflowing sideways', async ({ page }) => {
  110. await page.setViewportSize({ width: 390, height: 844 })
  111. await page.goto(jobUrl)
  112. await settle(page)
  113. const layout = page.getByTestId('service-layout-modern')
  114. await expect(layout).toBeVisible()
  115. // The money bar is a desktop thing: the page already carries its totals,
  116. // payment form, preview and send, and a phone has no height to spare.
  117. await expect(page.getByTestId('money-bar')).toBeHidden()
  118. // What it leaves behind is the running total, in the flow under the parts,
  119. // so adding a line on a small screen still shows what it did to the bill.
  120. await expect(page.getByTestId('money-line')).toBeVisible()
  121. // The header's second line is which job this is; when it was opened is
  122. // left for a wider screen.
  123. await expect(page.getByTestId('service-opened')).toBeHidden()
  124. // Each control of the status stepper stays in its own space: a label is
  125. // cut short rather than running into the next. Three stages, and the
  126. // arrow beside "In Progress" that opens the statuses filed under it
  127. // (always there, for "Waiting Parts"): four buttons in all on a workshop
  128. // that has defined no statuses of its own.
  129. const stages = await page
  130. .getByTestId('status-stepper')
  131. .getByRole('button')
  132. .evaluateAll((buttons) => buttons.map((b) => b.getBoundingClientRect().toJSON()))
  133. expect(stages).toHaveLength(4)
  134. for (let i = 1; i < stages.length; i++) {
  135. expect(
  136. stages[i - 1].right,
  137. `stage ${i} ends before stage ${i + 1} starts`
  138. ).toBeLessThanOrEqual(stages[i].left)
  139. }
  140. const overflow = await layout.evaluate((el) => el.scrollWidth - el.clientWidth)
  141. expect(overflow, 'sideways overflow in pixels').toBeLessThanOrEqual(1)
  142. const main = await page.getByTestId('service-main').boundingBox()
  143. const sidebar = await page.getByTestId('service-sidebar').boundingBox()
  144. expect(sidebar?.y ?? 0, 'the sidebar sits under the job').toBeGreaterThan(
  145. (main?.y ?? 0) + (main?.height ?? 0) - 1
  146. )
  147. })
  148. })