roles.spec.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. import { expect, type Page, test } from '@playwright/test'
  2. import { linkIn, waitForMail } from '../../support/mail'
  3. import { settle } from '../../support/hydration'
  4. /**
  5. * What a role grants, and what it does not.
  6. *
  7. * A member's permissions come from the role they were given, and a member
  8. * given none "cannot do anything" — the team page says so in as many words.
  9. * That promise is worth a test, because the failure mode is silent in both
  10. * directions: a member who can reach the billing page can also change what
  11. * the workshop pays, and a member who can reach nothing sees a product that
  12. * looks broken rather than one that is waiting for an admin.
  13. *
  14. * The invitation goes out through the harness's mail sink, so the colleague
  15. * arrives the way a real one does: by following a link they were sent.
  16. */
  17. test.describe.configure({ mode: 'serial' })
  18. // The colleague starts as a stranger with no session.
  19. test.use({ storageState: { cookies: [], origins: [] } })
  20. const stamp = Date.now()
  21. const COLLEAGUE = `e2e-roleless-${stamp}@example.com`
  22. const PASSWORD = `E2e-pass-${stamp}`
  23. async function signInAsColleague(page: Page) {
  24. await page.goto('/auth/sign-in')
  25. await page.locator('#email').fill(COLLEAGUE)
  26. await page.locator('#password').fill(PASSWORD)
  27. await page.getByRole('button', { name: 'Sign In', exact: true }).click()
  28. await page.waitForURL((url) => !url.pathname.startsWith('/auth'), { timeout: 30_000 })
  29. }
  30. test.describe('a member with no role', () => {
  31. test('is invited by the owner and signs up from the mail', async ({ page, browser }) => {
  32. const owner = await browser.newContext({ storageState: 'e2e/.auth/owner.json' })
  33. const ownerPage = await owner.newPage()
  34. await ownerPage.goto('/settings/team')
  35. await settle(ownerPage)
  36. // "Someone in the office": invited by email, picks their own password, and
  37. // is given no role, which the dialog warns leaves them unable to do
  38. // anything until an admin says otherwise.
  39. await expect(async () => {
  40. await ownerPage.getByRole('button', { name: 'Add', exact: true }).first().click()
  41. await expect(ownerPage.getByText('Someone in the office')).toBeVisible({ timeout: 2_000 })
  42. }).toPass({ timeout: 30_000 })
  43. await ownerPage.getByText('Someone in the office').click()
  44. await ownerPage.locator('#member-email').fill(COLLEAGUE)
  45. // No role is a choice the owner has to make: the form starts with none
  46. // chosen and will not send the invitation until one is.
  47. const dialog = ownerPage.getByRole('dialog').filter({ has: ownerPage.locator('#member-email') })
  48. await dialog.getByRole('combobox').click()
  49. await ownerPage.getByRole('option', { name: 'No role', exact: true }).click()
  50. await ownerPage.getByRole('button', { name: 'Invite', exact: true }).click()
  51. await expect(ownerPage.getByText(COLLEAGUE).first()).toBeVisible({ timeout: 30_000 })
  52. await owner.close()
  53. const invitation = await waitForMail(COLLEAGUE)
  54. await page.goto(linkIn(invitation, /\/auth\/sign-up\?invite=/))
  55. await page.locator('#name').fill('E2E Roleless Colleague')
  56. await page.locator('#email').fill(COLLEAGUE)
  57. await page.locator('#password').fill(PASSWORD)
  58. await page.locator('#terms').click()
  59. await page.getByRole('button', { name: /create account/i }).click()
  60. // No onboarding: they joined a workshop that already exists.
  61. await page.waitForURL((url) => !/^\/(auth|onboarding)/.test(url.pathname), { timeout: 30_000 })
  62. })
  63. test('is told so, once and plainly, instead of a sidebar of refusals', async ({ page }) => {
  64. await signInAsColleague(page)
  65. // The whole application used to open, with every page inside it answering
  66. // "your role does not allow this" one at a time.
  67. await expect(page.getByRole('heading', { name: 'No access yet' })).toBeVisible()
  68. await expect(page.getByText(/Demo Auto Workshop/)).toBeVisible()
  69. // And it names who can fix it, because the reader cannot.
  70. await expect(page.getByText(/ask an owner or an admin/i)).toBeVisible()
  71. })
  72. test('reaches none of the workshop’s screens by their addresses', async ({ page }) => {
  73. await signInAsColleague(page)
  74. for (const url of ['/work-orders', '/customers', '/billing', '/settings/team', '/inventory']) {
  75. await page.goto(url)
  76. // The same one screen, wherever they point the browser.
  77. await expect(
  78. page.getByRole('heading', { name: 'No access yet' }),
  79. `${url} is refused`
  80. ).toBeVisible()
  81. }
  82. })
  83. test('can sign out from where they are', async ({ page }) => {
  84. // The only thing the screen offers, and the only thing they can do: an
  85. // account with nowhere to go still has to be able to leave.
  86. await signInAsColleague(page)
  87. await page.getByRole('button', { name: /sign out/i }).click()
  88. await expect(page).toHaveURL(/\/auth\/sign-in/, { timeout: 30_000 })
  89. })
  90. test('the owner is not affected by any of it', async ({ browser }) => {
  91. // The other half of the rule: the pages refused above are refused because
  92. // of the role, not because they are broken.
  93. const owner = await browser.newContext({ storageState: 'e2e/.auth/owner.json' })
  94. const ownerPage = await owner.newPage()
  95. for (const url of ['/work-orders', '/billing', '/settings/team']) {
  96. await ownerPage.goto(url)
  97. await expect(
  98. ownerPage.getByRole('heading', { name: 'No access yet' }),
  99. `${url} opens for the owner`
  100. ).toHaveCount(0)
  101. }
  102. await owner.close()
  103. })
  104. })