|
1 | | -import { expect, test } from '@playwright/test' |
2 | | -import { organizationId, signInAsAdmin } from '@/platform/checks/live/session' |
| 1 | +import { expect, type Page, test } from '@playwright/test' |
| 2 | +import { |
| 3 | + memberEmail, |
| 4 | + organizationId, |
| 5 | + requireEnv, |
| 6 | + signIn, |
| 7 | + signInAsAdmin, |
| 8 | +} from '@/platform/checks/live/session' |
3 | 9 |
|
4 | 10 | test.describe('Home on real data', () => { |
5 | 11 | test.beforeEach(async ({ page }) => { |
@@ -83,3 +89,222 @@ test.describe('Home on real data', () => { |
83 | 89 | await expect(email).toBeVisible() |
84 | 90 | }) |
85 | 91 | }) |
| 92 | + |
| 93 | +test('a member who cannot build lands on Search, not an error', async ({ page }) => { |
| 94 | + await signIn(page, memberEmail(), '/org/home') |
| 95 | + await expect(page).toHaveURL(/\/org\/search/) |
| 96 | + await expect(page.getByRole('textbox', { name: 'Search' })).toBeVisible() |
| 97 | +}) |
| 98 | + |
| 99 | +test.describe('Home for the seeded administrator', () => { |
| 100 | + test('greets the member by first name', async ({ page }) => { |
| 101 | + await signInAsAdmin(page, '/org/home') |
| 102 | + await expect(page.getByRole('heading', { level: 1 })).toContainText(/, Ada$/) |
| 103 | + }) |
| 104 | + |
| 105 | + test('Recents list the viewer’s real history and × removes it for good', async ({ page }) => { |
| 106 | + await signInAsAdmin(page, '/org/home') |
| 107 | + const recents = page.getByRole('region', { name: 'Recents', exact: true }) |
| 108 | + await expect(recents.getByRole('link', { name: 'Onboarding guide' })).toBeVisible() |
| 109 | + const renewal = recents.getByRole('link', { name: 'Renewal terms' }) |
| 110 | + await renewal.hover() |
| 111 | + await recents.getByRole('button', { name: 'Remove Renewal terms from recents' }).click() |
| 112 | + await expect(renewal).toHaveCount(0) |
| 113 | + await page.reload() |
| 114 | + await expect(recents.getByRole('link', { name: 'Onboarding guide' })).toBeVisible() |
| 115 | + await expect(renewal).toHaveCount(0) |
| 116 | + }) |
| 117 | + |
| 118 | + test('an unread chat needs attention and opens from there', async ({ page }) => { |
| 119 | + await signInAsAdmin(page, '/org/home') |
| 120 | + const attention = page.getByRole('region', { name: 'Needs your attention', exact: true }) |
| 121 | + await expect(attention.getByText('Vendor renewals')).toBeVisible() |
| 122 | + await expect(attention.getByText('Quarterly review')).toHaveCount(0) |
| 123 | + await attention.getByText('Vendor renewals').click() |
| 124 | + await expect(page).toHaveURL(/\/org\/search\/chat\/22222222-/) |
| 125 | + await expect(page.getByText('Here is what I found about Vendor renewals.')).toBeVisible() |
| 126 | + }) |
| 127 | + |
| 128 | + test('Search mode drops the activity chart and offers Search’s Get started', async ({ page }) => { |
| 129 | + await signInAsAdmin(page, '/org/home') |
| 130 | + await expect(page.getByRole('region', { name: 'Tasks run', exact: true })).toBeVisible() |
| 131 | + await page.getByRole('radio', { name: 'Search' }).click() |
| 132 | + await expect(page.getByRole('region', { name: 'Tasks run', exact: true })).toHaveCount(0) |
| 133 | + await expect(page.getByRole('region', { name: 'Searches', exact: true })).toHaveCount(0) |
| 134 | + }) |
| 135 | + |
| 136 | + test('connecting Gmail starts the provider’s real authorization', async ({ page, context }) => { |
| 137 | + await context.route('https://accounts.google.com/**', (route) => |
| 138 | + route.fulfill({ status: 200, contentType: 'text/html', body: 'Google stand-in' }) |
| 139 | + ) |
| 140 | + await signInAsAdmin(page, '/org/home') |
| 141 | + const authorization = page.waitForRequest((request) => |
| 142 | + request.url().startsWith('https://accounts.google.com/') |
| 143 | + ) |
| 144 | + await page |
| 145 | + .getByRole('region', { name: 'Email', exact: true }) |
| 146 | + .getByRole('button', { name: 'Gmail', exact: true }) |
| 147 | + .click() |
| 148 | + const url = new URL((await authorization).url()) |
| 149 | + expect(url.searchParams.get('client_id')).toBe(process.env.GOOGLE_CLIENT_ID) |
| 150 | + expect(url.searchParams.get('redirect_uri')).toContain('/api/') |
| 151 | + }) |
| 152 | +}) |
| 153 | + |
| 154 | +/** |
| 155 | + * The seeded member holds real Gmail and Calendar grants with fixture tokens. Google cannot honor |
| 156 | + * those, so these replace the widget reads at the browser's network boundary: they prove what Home |
| 157 | + * does with each answer. `home-widgets.integration.ts` proves the reads behind them. |
| 158 | + */ |
| 159 | +test.describe('Home with connected accounts', () => { |
| 160 | + const GRANT = 'platform-live-grant-gmail' |
| 161 | + const CALENDAR_GRANT = 'platform-live-grant-google-calendar' |
| 162 | + |
| 163 | + function account(credentialId: string, state: string) { |
| 164 | + return { |
| 165 | + credentialId, |
| 166 | + displayName: requireEnv('PLATFORM_LIVE_CONNECTED_EMAIL'), |
| 167 | + state, |
| 168 | + } |
| 169 | + } |
| 170 | + |
| 171 | + const THREADS = [ |
| 172 | + { |
| 173 | + id: 'thread-1', |
| 174 | + credentialId: GRANT, |
| 175 | + sender: 'Grace Hopper', |
| 176 | + subject: 'Compiler review', |
| 177 | + excerpt: 'The draft is ready', |
| 178 | + receivedAt: new Date(Date.now() - 3_600_000).toISOString(), |
| 179 | + messageCount: 3, |
| 180 | + url: 'https://mail.google.com/mail/u/0/#inbox/thread-1', |
| 181 | + }, |
| 182 | + ] |
| 183 | + |
| 184 | + function calendarFor(date: string) { |
| 185 | + const startAt = `${date}T15:00:00.000Z` |
| 186 | + return { |
| 187 | + accounts: [account(CALENDAR_GRANT, 'ready')], |
| 188 | + window: { startAt: `${date}T00:00:00.000Z`, endAt: `${date}T23:59:59.000Z` }, |
| 189 | + events: [ |
| 190 | + { |
| 191 | + id: `event-${date}`, |
| 192 | + credentialId: CALENDAR_GRANT, |
| 193 | + title: `Standup ${date}`, |
| 194 | + allDay: false, |
| 195 | + startAt, |
| 196 | + endAt: `${date}T15:30:00.000Z`, |
| 197 | + people: [{ name: 'Grace Hopper' }], |
| 198 | + }, |
| 199 | + ], |
| 200 | + } |
| 201 | + } |
| 202 | + |
| 203 | + async function serveWidgets(page: Page) { |
| 204 | + await page.route('**/home/email', (route) => |
| 205 | + route.fulfill({ json: { accounts: [account(GRANT, 'ready')], threads: THREADS } }) |
| 206 | + ) |
| 207 | + await page.route('**/home/calendar?*', (route) => { |
| 208 | + const date = new URL(route.request().url()).searchParams.get('date') as string |
| 209 | + return route.fulfill({ json: calendarFor(date) }) |
| 210 | + }) |
| 211 | + } |
| 212 | + |
| 213 | + const open = (page: Page) => |
| 214 | + signIn(page, requireEnv('PLATFORM_LIVE_CONNECTED_EMAIL'), '/org/home') |
| 215 | + |
| 216 | + test('a connected member reads their mail, opens a thread, and keeps a chosen day', async ({ |
| 217 | + page, |
| 218 | + context, |
| 219 | + }) => { |
| 220 | + await serveWidgets(page) |
| 221 | + await context.route('https://mail.google.com/**', (route) => |
| 222 | + route.fulfill({ status: 200, contentType: 'text/html', body: 'Gmail stand-in' }) |
| 223 | + ) |
| 224 | + await open(page) |
| 225 | + const email = page.getByRole('region', { name: 'Email', exact: true }) |
| 226 | + await expect(email.getByText('Grace Hopper')).toBeVisible() |
| 227 | + await expect(email.getByText('Compiler review')).toBeVisible() |
| 228 | + const opened = context.waitForEvent('page') |
| 229 | + await email.getByText('Grace Hopper').click() |
| 230 | + expect((await opened).url()).toContain('mail.google.com/mail/u/0/#inbox/thread-1') |
| 231 | + |
| 232 | + const calendar = page.getByRole('region', { name: 'Calendar', exact: true }) |
| 233 | + await expect(calendar.getByText(/^Standup \d{4}-\d{2}-\d{2}$/)).toBeVisible() |
| 234 | + const next = page.waitForRequest((request) => request.url().includes('/home/calendar?')) |
| 235 | + await calendar.getByRole('button', { name: 'Next day' }).click() |
| 236 | + const chosen = new URL((await next).url()).searchParams.get('date') as string |
| 237 | + await expect(page).toHaveURL(new RegExp(`day=${chosen}`)) |
| 238 | + await expect(calendar.getByText(`Standup ${chosen}`)).toBeVisible() |
| 239 | + await page.reload() |
| 240 | + await expect(calendar.getByText(`Standup ${chosen}`)).toBeVisible() |
| 241 | + }) |
| 242 | + |
| 243 | + test('a failed read offers a retry that recovers, with nothing else on Home affected', async ({ |
| 244 | + page, |
| 245 | + }) => { |
| 246 | + let failing = true |
| 247 | + await serveWidgets(page) |
| 248 | + await page.route('**/home/email', (route) => |
| 249 | + failing |
| 250 | + ? route.fulfill({ status: 500, json: { error: 'Provider down' } }) |
| 251 | + : route.fulfill({ json: { accounts: [account(GRANT, 'ready')], threads: THREADS } }) |
| 252 | + ) |
| 253 | + await open(page) |
| 254 | + const email = page.getByRole('region', { name: 'Email', exact: true }) |
| 255 | + await expect(email.getByText('Your email could not load')).toBeVisible({ timeout: 30_000 }) |
| 256 | + await expect( |
| 257 | + page.getByRole('region', { name: 'Calendar', exact: true }).getByText(/^Standup /) |
| 258 | + ).toBeVisible() |
| 259 | + failing = false |
| 260 | + await email.getByRole('button', { name: 'Retry' }).click() |
| 261 | + await expect(email.getByText('Grace Hopper')).toBeVisible() |
| 262 | + }) |
| 263 | + |
| 264 | + test('an account Google stopped honoring asks to reconnect, here and in attention', async ({ |
| 265 | + page, |
| 266 | + }) => { |
| 267 | + await serveWidgets(page) |
| 268 | + await page.route('**/home/email', (route) => |
| 269 | + route.fulfill({ json: { accounts: [account(GRANT, 'needs_reauth')], threads: [] } }) |
| 270 | + ) |
| 271 | + await open(page) |
| 272 | + await expect( |
| 273 | + page |
| 274 | + .getByRole('region', { name: 'Email', exact: true }) |
| 275 | + .getByText('Reconnect your email', { exact: true }) |
| 276 | + ).toBeVisible() |
| 277 | + await expect( |
| 278 | + page.getByRole('region', { name: 'Needs your attention', exact: true }).getByText(/Reconnect/) |
| 279 | + ).toBeVisible() |
| 280 | + }) |
| 281 | + |
| 282 | + test('a connected member with an empty mailbox sees that, not a prompt to connect', async ({ |
| 283 | + page, |
| 284 | + }) => { |
| 285 | + await serveWidgets(page) |
| 286 | + await page.route('**/home/email', (route) => |
| 287 | + route.fulfill({ json: { accounts: [account(GRANT, 'ready')], threads: [] } }) |
| 288 | + ) |
| 289 | + await open(page) |
| 290 | + const email = page.getByRole('region', { name: 'Email', exact: true }) |
| 291 | + await expect(email.getByText('No email yet', { exact: true })).toBeVisible() |
| 292 | + await expect(email.getByText('Connect your email', { exact: true })).toHaveCount(0) |
| 293 | + }) |
| 294 | + |
| 295 | + test('disconnecting removes the account for real and returns to the connect prompt', async ({ |
| 296 | + page, |
| 297 | + }) => { |
| 298 | + await serveWidgets(page) |
| 299 | + await open(page) |
| 300 | + const email = page.getByRole('region', { name: 'Email', exact: true }) |
| 301 | + await email.getByRole('button', { name: 'Email options' }).click() |
| 302 | + await page.getByRole('menuitem', { name: 'Disconnect Email' }).click() |
| 303 | + const dialog = page.getByRole('dialog', { name: 'Disconnect Email?' }) |
| 304 | + await dialog.getByRole('button', { name: 'Disconnect', exact: true }).click() |
| 305 | + await expect(dialog).toBeHidden() |
| 306 | + await expect(email.getByText('Connect your email', { exact: true })).toBeVisible() |
| 307 | + await page.reload() |
| 308 | + await expect(email.getByText('Connect your email', { exact: true })).toBeVisible() |
| 309 | + }) |
| 310 | +}) |
0 commit comments