closures.spec.ts 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. import { expect, test } from '@playwright/test'
  2. import {
  3. absencesOf,
  4. closuresNamed,
  5. deleteClosuresNamed,
  6. deleteTechnicians,
  7. insertTechnician,
  8. ownerOrganizationId,
  9. } from '../../support/db'
  10. import { fillSettled, settle } from '../../support/hydration'
  11. /**
  12. * Leave and closed days, from where they are entered to the work board.
  13. *
  14. * A technician set on leave from the board's own Leave button shows as away
  15. * in their lane, leave set from the calendar shows there and can be removed
  16. * from its card, and a closed day added under Settings → Opening hours and
  17. * holidays is named in the board's day header. Both are what a service
  18. * advisor looks at before handing out work; nothing here stops a booking.
  19. */
  20. test.describe.configure({ mode: 'serial' })
  21. const stamp = Date.now()
  22. const TECH = `E2E Leave ${stamp}`
  23. const CLOSURE = `E2E Closed ${stamp}`
  24. /** A Wednesday a week or two out: always a working day, never today. */
  25. function nextWednesday(): string {
  26. const d = new Date()
  27. d.setUTCDate(d.getUTCDate() + 7 + ((3 - d.getUTCDay() + 7) % 7))
  28. return d.toISOString().slice(0, 10)
  29. }
  30. const DAY = nextWednesday()
  31. /** The board's week starts on Monday unless the workshop says otherwise. */
  32. const WEEK = new Date(Date.parse(`${DAY}T12:00:00Z`) - 2 * 86_400_000).toISOString().slice(0, 10)
  33. const BOARD = `/work-board?view=week&week=${WEEK}&date=${DAY}&group=technician`
  34. let technicianId = ''
  35. test.beforeAll(async () => {
  36. technicianId = await insertTechnician(await ownerOrganizationId(), TECH)
  37. })
  38. test.afterAll(async () => {
  39. await deleteClosuresNamed(CLOSURE)
  40. // Time off goes with the technician.
  41. if (technicianId) await deleteTechnicians([technicianId])
  42. })
  43. test('a technician set on leave from the board shows as away in their lane', async ({ page }) => {
  44. await page.goto(BOARD)
  45. await settle(page)
  46. await expect(async () => {
  47. await page.getByRole('button', { name: 'Leave', exact: true }).click()
  48. await expect(page.getByRole('dialog', { name: 'Leave and out of office' })).toBeVisible()
  49. }).toPass()
  50. await page.getByRole('button', { name: 'Set on leave or out of office' }).click()
  51. const dialog = page.getByRole('dialog', { name: 'Set on leave or out of office' })
  52. await dialog.getByLabel('Technician').click()
  53. await page.getByRole('option', { name: TECH }).click()
  54. await fillSettled(dialog.locator('#time-off-from'), DAY)
  55. await dialog.locator('#time-off-from').press('Tab')
  56. await fillSettled(dialog.locator('#time-off-to'), DAY)
  57. await dialog.locator('#time-off-to').press('Tab')
  58. await dialog.getByRole('button', { name: 'Save' }).click()
  59. await expect(dialog).toBeHidden()
  60. await expect
  61. .poll(() => absencesOf(technicianId))
  62. .toEqual([{ startDate: DAY, endDate: DAY, reason: 'holiday', startTime: null }])
  63. // The manager lists it, then the board says so in the lane.
  64. const manager = page.getByRole('dialog', { name: 'Leave and out of office' })
  65. await expect(manager.getByTestId('time-off-row').filter({ hasText: TECH })).toBeVisible()
  66. await page.keyboard.press('Escape')
  67. await expect(
  68. page.getByTestId('lane-away').filter({ hasText: 'On leave' }).filter({ visible: true }).first()
  69. ).toBeVisible()
  70. })
  71. test('leave set from the calendar shows there and can be taken back', async ({ page }) => {
  72. // The Thursday after the board's leave, so the two never meet.
  73. const day = new Date(Date.parse(`${DAY}T12:00:00Z`) + 86_400_000).toISOString().slice(0, 10)
  74. await page.goto(`/calendar?view=week&date=${day}`)
  75. await settle(page)
  76. // The Create menu starts it on the day the calendar is on.
  77. await expect(async () => {
  78. await page.getByRole('button', { name: 'Create', exact: true }).click()
  79. await expect(
  80. page.getByRole('menuitem', { name: 'Set on leave or out of office' })
  81. ).toBeVisible()
  82. }).toPass()
  83. await page.getByRole('menuitem', { name: 'Set on leave or out of office' }).click()
  84. const dialog = page.getByRole('dialog', { name: 'Set on leave or out of office' })
  85. await dialog.getByLabel('Technician').click()
  86. await page.getByRole('option', { name: TECH }).click()
  87. await dialog.getByLabel('Reason').click()
  88. await page.getByRole('option', { name: 'Sick leave' }).click()
  89. await dialog.getByRole('button', { name: 'Save' }).click()
  90. await expect(dialog).toBeHidden()
  91. await expect
  92. .poll(async () => (await absencesOf(technicianId)).filter((a) => a.startDate === day))
  93. .toEqual([{ startDate: day, endDate: day, reason: 'sick', startTime: null }])
  94. const entry = page.getByRole('button', { name: `${TECH}: Sick leave` }).first()
  95. await expect(entry).toBeVisible()
  96. // Its card offers Edit, and the dialog it opens removes it.
  97. await entry.click()
  98. await page.getByRole('button', { name: 'Edit', exact: true }).click()
  99. const edit = page.getByRole('dialog', { name: 'Edit leave or out of office' })
  100. await edit.getByRole('button', { name: 'Delete' }).click()
  101. await page
  102. .getByRole('alertdialog', { name: 'Remove this leave?' })
  103. .getByRole('button', { name: 'Delete' })
  104. .click()
  105. await expect
  106. .poll(async () => (await absencesOf(technicianId)).filter((a) => a.startDate === day))
  107. .toEqual([])
  108. await expect(page.getByRole('button', { name: `${TECH}: Sick leave` })).toHaveCount(0)
  109. })
  110. test('a closed day added in settings is named on the board', async ({ page }) => {
  111. await page.goto('/settings/opening-hours')
  112. await settle(page)
  113. await expect(async () => {
  114. await page.getByRole('button', { name: 'Add closed day' }).click()
  115. await expect(page.getByRole('dialog', { name: 'Add closed day' })).toBeVisible()
  116. }).toPass()
  117. const dialog = page.getByRole('dialog', { name: 'Add closed day' })
  118. await fillSettled(dialog.getByLabel('Name'), CLOSURE)
  119. await fillSettled(dialog.locator('#closure-from'), DAY)
  120. await dialog.locator('#closure-from').press('Tab')
  121. await dialog.getByRole('button', { name: 'Save' }).click()
  122. await expect(dialog).toBeHidden()
  123. await expect
  124. .poll(() => closuresNamed(CLOSURE))
  125. .toEqual([{ startDate: DAY, endDate: DAY, repeatsYearly: false }])
  126. await expect(page.getByTestId('closure-row').filter({ hasText: CLOSURE })).toBeVisible()
  127. await page.goto(BOARD)
  128. await settle(page)
  129. await expect(
  130. page
  131. .getByTestId('day-closed')
  132. .filter({ hasText: `Closed: ${CLOSURE}` })
  133. .first()
  134. ).toBeVisible()
  135. })