roles.spec.ts 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  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. await ownerPage.getByRole('button', { name: 'Invite', exact: true }).click()
  46. await expect(ownerPage.getByText(COLLEAGUE).first()).toBeVisible({ timeout: 30_000 })
  47. await owner.close()
  48. const invitation = await waitForMail(COLLEAGUE)
  49. await page.goto(linkIn(invitation, /\/auth\/sign-up\?invite=/))
  50. await page.locator('#name').fill('E2E Roleless Colleague')
  51. await page.locator('#email').fill(COLLEAGUE)
  52. await page.locator('#password').fill(PASSWORD)
  53. await page.locator('#terms').click()
  54. await page.getByRole('button', { name: /create account/i }).click()
  55. // No onboarding: they joined a workshop that already exists.
  56. await page.waitForURL((url) => !/^\/(auth|onboarding)/.test(url.pathname), { timeout: 30_000 })
  57. })
  58. test('is told so, once and plainly, instead of a sidebar of refusals', async ({ page }) => {
  59. await signInAsColleague(page)
  60. // The whole application used to open, with every page inside it answering
  61. // "your role does not allow this" one at a time.
  62. await expect(page.getByRole('heading', { name: 'No access yet' })).toBeVisible()
  63. await expect(page.getByText(/Demo Auto Workshop/)).toBeVisible()
  64. // And it names who can fix it, because the reader cannot.
  65. await expect(page.getByText(/ask an owner or an admin/i)).toBeVisible()
  66. })
  67. test('reaches none of the workshop’s screens by their addresses', async ({ page }) => {
  68. await signInAsColleague(page)
  69. for (const url of ['/work-orders', '/customers', '/billing', '/settings/team', '/inventory']) {
  70. await page.goto(url)
  71. // The same one screen, wherever they point the browser.
  72. await expect(
  73. page.getByRole('heading', { name: 'No access yet' }),
  74. `${url} is refused`
  75. ).toBeVisible()
  76. }
  77. })
  78. test('can sign out from where they are', async ({ page }) => {
  79. // The only thing the screen offers, and the only thing they can do: an
  80. // account with nowhere to go still has to be able to leave.
  81. await signInAsColleague(page)
  82. await page.getByRole('button', { name: /sign out/i }).click()
  83. await expect(page).toHaveURL(/\/auth\/sign-in/, { timeout: 30_000 })
  84. })
  85. test('the owner is not affected by any of it', async ({ browser }) => {
  86. // The other half of the rule: the pages refused above are refused because
  87. // of the role, not because they are broken.
  88. const owner = await browser.newContext({ storageState: 'e2e/.auth/owner.json' })
  89. const ownerPage = await owner.newPage()
  90. for (const url of ['/work-orders', '/billing', '/settings/team']) {
  91. await ownerPage.goto(url)
  92. await expect(
  93. ownerPage.getByRole('heading', { name: 'No access yet' }),
  94. `${url} opens for the owner`
  95. ).toHaveCount(0)
  96. }
  97. await owner.close()
  98. })
  99. })