validation.spec.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. import { expect, test } from '@playwright/test'
  2. import {
  3. addLabor,
  4. addPart,
  5. laborRows,
  6. newWorkOrder,
  7. partRowOf,
  8. partRows,
  9. saveWorkOrder,
  10. seededVehicleUrl,
  11. totalsRow,
  12. } from '../../support/work-order'
  13. /**
  14. * What the editor refuses to save, and what it says when it refuses.
  15. *
  16. * Each of these was silent once. A priced row with no name was dropped on the
  17. * way to the server, so the save succeeded and the money left the invoice
  18. * without a word; a negative figure made the browser refuse the submit on a
  19. * field it would not show, so Save did nothing at all and said nothing
  20. * either. The point of these tests is the message.
  21. */
  22. test.describe.configure({ mode: 'serial' })
  23. const stamp = Date.now()
  24. let vehicleUrl = ''
  25. /** The one job these tests keep trying to break; each test opens it afresh. */
  26. let jobUrl = ''
  27. test.beforeAll(async ({ browser }) => {
  28. const page = await browser.newPage({ storageState: 'e2e/.auth/owner.json' })
  29. vehicleUrl = await seededVehicleUrl(page)
  30. await page.close()
  31. })
  32. /** Clicks Save and expects it to be refused with one sentence. */
  33. async function expectRefused(page: import('@playwright/test').Page, message: RegExp) {
  34. await page.getByRole('button', { name: 'Save', exact: true }).click()
  35. await expect(page.getByText(message).filter({ visible: true }).first()).toBeVisible()
  36. await expect(page.getByText('Saved', { exact: true })).toHaveCount(0)
  37. }
  38. test.describe('a work order that cannot be saved says why', () => {
  39. test('a part with a price but no name is refused, and its money stays on screen', async ({
  40. page,
  41. }) => {
  42. jobUrl = await newWorkOrder(page, vehicleUrl, `E2E validation ${stamp}`)
  43. await addPart(page, { name: `E2E gasket ${stamp}`, quantity: 1, unitPrice: 500 })
  44. await saveWorkOrder(page)
  45. // A second row, priced, that nobody has named.
  46. await addPart(page, { name: 'to be emptied', quantity: 2, unitPrice: 300 })
  47. await partRows(page).first().fill('')
  48. await expect(totalsRow(page, 'Parts')).toContainText('$1,100.00')
  49. // The row says so on its own, before anybody reaches for Save.
  50. await expect(
  51. page.getByText('Rows without a part name are not saved.').filter({ visible: true })
  52. ).toBeVisible()
  53. await expectRefused(page, /every priced part needs a name/i)
  54. // Nothing was thrown away: the row and its money are still there to fix.
  55. await expect(totalsRow(page, 'Parts')).toContainText('$1,100.00')
  56. await expect(partRows(page)).toHaveCount(2)
  57. })
  58. test('naming it lets the save through with both lines', async ({ page }) => {
  59. await page.goto(jobUrl)
  60. await addPart(page, { name: 'to be named', quantity: 2, unitPrice: 300 })
  61. await partRows(page).first().fill('')
  62. await expect(async () => {
  63. await partRows(page).first().fill(`E2E hose ${stamp}`)
  64. await expect(totalsRow(page, 'Parts')).toContainText('$1,100.00', { timeout: 2_000 })
  65. }).toPass({ timeout: 30_000 })
  66. await saveWorkOrder(page)
  67. await page.reload()
  68. await expect(partRows(page)).toHaveCount(2)
  69. await expect(totalsRow(page, 'Parts')).toContainText('$1,100.00')
  70. })
  71. test('labour with hours but nothing said about it is refused too', async ({ page }) => {
  72. await page.goto(jobUrl)
  73. await addLabor(page, { description: 'to be emptied', hours: 3, rate: 900 })
  74. await laborRows(page).first().fill('')
  75. await expect(totalsRow(page, 'Labor')).toContainText('$2,700.00')
  76. await expect(
  77. page.getByText('Rows without a description are not saved.').filter({ visible: true })
  78. ).toBeVisible()
  79. await expectRefused(page, /needs a description/i)
  80. await laborRows(page).first().fill(`E2E fit the hose ${stamp}`)
  81. await saveWorkOrder(page)
  82. await expect(totalsRow(page, 'Labor')).toContainText('$2,700.00')
  83. })
  84. test('a negative quantity is refused in words, not by a dead button', async ({ page }) => {
  85. await page.goto(jobUrl)
  86. const quantity = partRowOf(partRows(page).first()).locator('input[type="number"]').first()
  87. await expect(async () => {
  88. await quantity.fill('-2')
  89. await expect(totalsRow(page, 'Parts')).toContainText('-', { timeout: 2_000 })
  90. }).toPass({ timeout: 30_000 })
  91. await expectRefused(page, /cannot be negative/i)
  92. })
  93. test('a negative unit price is refused as well', async ({ page }) => {
  94. await page.goto(jobUrl)
  95. const row = partRowOf(partRows(page).first())
  96. const unitPrice = row.locator('input[type="number"]').nth(3)
  97. await expect(async () => {
  98. await unitPrice.fill('-10')
  99. // The row's own total, not the parts subtotal: a small negative line is
  100. // swallowed by the other rows and the sum stays positive, which is
  101. // exactly how this reaches a customer unnoticed.
  102. await expect(row.getByText(/-\$/)).toBeVisible({ timeout: 2_000 })
  103. }).toPass({ timeout: 30_000 })
  104. await expectRefused(page, /cannot be negative/i)
  105. })
  106. test('the job is left as it was found, saved and correct', async ({ page }) => {
  107. await page.goto(jobUrl)
  108. await expect(totalsRow(page, 'Parts')).toContainText('$1,100.00')
  109. await expect(totalsRow(page, 'Labor')).toContainText('$2,700.00')
  110. })
  111. })