stale-tab.spec.ts 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  1. import { expect, type Page, test } from '@playwright/test'
  2. import { settle } from '../../support/hydration'
  3. /**
  4. * A tab left open for days. Found in PostHog on 27 Oct 2026: a workshop came
  5. * back to a dashboard from before a deploy, with its session gone, and clicked
  6. * Work orders five times over 16 seconds with nothing on screen before Next
  7. * reloaded the page and the layout sent them to sign-in. They then landed on
  8. * the dashboard rather than the page they wanted.
  9. *
  10. * The session is ended by dropping this context's session cookie, which is
  11. * what the server sees once a session expires, and leaves the shared owner
  12. * session in e2e/.auth/owner.json alone.
  13. */
  14. const email = process.env.E2E_USER_EMAIL ?? 'demo@torqvoice.com'
  15. const password = process.env.E2E_USER_PASSWORD ?? 'demo-e2e-pass'
  16. /** What a browser does when someone switches back to the tab. */
  17. async function bringTabBack(page: Page) {
  18. await page.evaluate(() => document.dispatchEvent(new Event('visibilitychange')))
  19. }
  20. async function signIn(page: Page) {
  21. await page.locator('#email').fill(email)
  22. await page.locator('#password').fill(password)
  23. await page.getByRole('button', { name: 'Sign In', exact: true }).click()
  24. }
  25. test.describe('a tab that went stale', () => {
  26. test('offers a reload once the server runs a newer build', async ({ page }) => {
  27. await page.route('**/api/public/version', (route) =>
  28. route.fulfill({ json: { version: 'a-newer-build' } })
  29. )
  30. await page.goto('/work-orders')
  31. await settle(page)
  32. const toast = page
  33. .locator('[data-sonner-toast]')
  34. .filter({ hasText: 'A new version is available' })
  35. await expect(async () => {
  36. await bringTabBack(page)
  37. await expect(toast).toBeVisible({ timeout: 2_000 })
  38. }).toPass()
  39. const reloaded = page.waitForEvent('load')
  40. await toast.getByRole('button', { name: 'Reload' }).click()
  41. await reloaded
  42. await expect(page).toHaveURL(/\/work-orders$/)
  43. })
  44. test('says nothing while the build is the same', async ({ page }) => {
  45. const asked = page.waitForRequest('**/api/public/version')
  46. await page.goto('/work-orders')
  47. await settle(page)
  48. await bringTabBack(page)
  49. await asked
  50. await expect(page.locator('[data-sonner-toast]')).toHaveCount(0)
  51. })
  52. test('goes to sign-in when the tab comes back without a session, and returns', async ({
  53. page,
  54. context,
  55. }) => {
  56. await page.goto('/work-orders?status=pending')
  57. await settle(page)
  58. await context.clearCookies({ name: /session_token/ })
  59. await bringTabBack(page)
  60. await page.waitForURL(/\/auth\/sign-in\?redirect=%2Fwork-orders%3Fstatus%3Dpending/)
  61. await settle(page)
  62. await expect(async () => {
  63. await signIn(page)
  64. await page.waitForURL(/\/work-orders\?status=pending$/, { timeout: 5_000 })
  65. }).toPass()
  66. })
  67. test('a page opened without a session comes back to itself after sign-in', async ({
  68. browser,
  69. }) => {
  70. const context = await browser.newContext({ storageState: { cookies: [], origins: [] } })
  71. const page = await context.newPage()
  72. await page.goto('/work-orders?status=pending')
  73. await page.waitForURL(/\/auth\/sign-in\?redirect=%2Fwork-orders%3Fstatus%3Dpending/)
  74. await settle(page)
  75. await expect(async () => {
  76. await signIn(page)
  77. await page.waitForURL(/\/work-orders\?status=pending$/, { timeout: 5_000 })
  78. }).toPass()
  79. await context.close()
  80. })
  81. })