Skip to content

Commit 00c5880

Browse files
committed
Correct the live specs against what the stack actually does
Seed: a usage limit, chat modes and transcripts, search history, a connected administrator. Sessions persist across Playwright's worker restarts so the sign-in rate limit is not the failure. Specs: not-found is a streamed 200, the login callback is the zone root as on legacy, account deletion is blocked for a member, and a withheld directory comes from the default group. Adds Home's connected-account states and a parity audit.
1 parent f492734 commit 00c5880

11 files changed

Lines changed: 448 additions & 108 deletions

File tree

‎.github/scripts/http-e2e.sh‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,6 @@ platform_live_environment() {
202202
export PLATFORM_LIVE_CONNECTED_EMAIL=cory@platform-live.test
203203
export PLATFORM_LIVE_ROLE_TARGET_EMAIL=rae@platform-live.test
204204
export PLATFORM_LIVE_REMOVABLE_EMAIL=remy@platform-live.test
205-
export PLATFORM_LIVE_NO_DIRECTORY_EMAIL=noor@platform-live.test
206205
export PLATFORM_LIVE_REFUSED_INVITE_EMAIL=otto@platform-live.test
207206
export PLATFORM_LIVE_ORCHESTRATOR=1
208207
bun --no-env-file scripts/platform-live-seed.ts

‎apps/sim/platform/checks/live/chat.spec.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ test('A chat outside the viewer’s access is not found rather than an empty cha
2424
page,
2525
}) => {
2626
await signInAsAdmin(page, '/org/home')
27-
const response = await page.goto('/org/chat/00000000-0000-4000-8000-000000000000')
28-
expect(response?.status()).toBe(404)
27+
await page.goto('/org/chat/00000000-0000-4000-8000-000000000000')
28+
await expect(page.getByRole('heading', { name: 'Page not found' })).toBeVisible()
29+
await expect(page.getByRole('region', { name: 'Conversation' })).toHaveCount(0)
2930
})
3031

3132
test('A resource in the URL survives a reload of the chat route', async ({ page }) => {

‎apps/sim/platform/checks/live/home.spec.ts‎

Lines changed: 227 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
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'
39

410
test.describe('Home on real data', () => {
511
test.beforeEach(async ({ page }) => {
@@ -83,3 +89,222 @@ test.describe('Home on real data', () => {
8389
await expect(email).toBeVisible()
8490
})
8591
})
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+
})

‎apps/sim/platform/checks/live/members.spec.ts‎

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ async function signInAsAdmin(page: Page) {
2222
function memberRow(page: Page, email: string) {
2323
return page
2424
.getByText(email)
25+
.first()
2526
.locator('xpath=ancestor::div[.//button[starts-with(@aria-label, "Actions for")]][1]')
2627
}
2728

@@ -49,26 +50,38 @@ test('a partial invite failure keeps only the refused address, with its reason',
4950
test('a role change persists and a failed one is reported', async ({ page }) => {
5051
await signInAsAdmin(page)
5152
const target = requireEnv('PLATFORM_LIVE_ROLE_TARGET_EMAIL')
52-
await memberRow(page, target).getByRole('combobox').click()
53+
await memberRow(page, target)
54+
.getByRole('combobox', { name: /^Role for/ })
55+
.click()
5356
await page.getByRole('option', { name: 'Admin' }).click()
5457
await page.reload()
55-
await expect(memberRow(page, target).getByRole('combobox')).toHaveText(/Admin/)
58+
await expect(memberRow(page, target).getByRole('combobox', { name: /^Role for/ })).toHaveText(
59+
/Admin/
60+
)
5661

5762
await page.route('**/api/organizations/*/members/*', (route) =>
5863
route.request().method() === 'PUT'
5964
? route.fulfill({ status: 500, json: { error: 'Refused by the live check' } })
6065
: route.continue()
6166
)
62-
await memberRow(page, target).getByRole('combobox').click()
67+
await memberRow(page, target)
68+
.getByRole('combobox', { name: /^Role for/ })
69+
.click()
6370
await page.getByRole('option', { name: 'Member' }).click()
6471
await expect(page.getByText("Couldn't change the role")).toBeVisible()
65-
await expect(memberRow(page, target).getByRole('combobox')).toHaveText(/Admin/)
72+
await expect(memberRow(page, target).getByRole('combobox', { name: /^Role for/ })).toHaveText(
73+
/Admin/
74+
)
6675
await page.unroute('**/api/organizations/*/members/*')
6776

68-
await memberRow(page, target).getByRole('combobox').click()
77+
await memberRow(page, target)
78+
.getByRole('combobox', { name: /^Role for/ })
79+
.click()
6980
await page.getByRole('option', { name: 'Member' }).click()
7081
await page.reload()
71-
await expect(memberRow(page, target).getByRole('combobox')).toHaveText(/Member/)
82+
await expect(memberRow(page, target).getByRole('combobox', { name: /^Role for/ })).toHaveText(
83+
/Member/
84+
)
7285
})
7386

7487
test('removal holds on its disclosure, keeps the dialog on failure, then removes', async ({

‎apps/sim/platform/checks/live/people.spec.ts‎

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { expect, test } from '@playwright/test'
2-
import { adminEmail, requireEnv, signIn, signInAsAdmin } from '@/platform/checks/live/session'
2+
import {
3+
adminEmail,
4+
memberEmail,
5+
organizationId,
6+
signIn,
7+
signInAsAdmin,
8+
} from '@/platform/checks/live/session'
39

4-
/**
5-
* `PLATFORM_LIVE_EMAIL` is an administrator in an organization with the `people` flag on.
6-
* `PLATFORM_LIVE_NO_DIRECTORY_EMAIL` belongs to a permission group withholding
7-
* `organization.member_directory`.
8-
*/
10+
/** `PLATFORM_LIVE_EMAIL` is an administrator in an organization with the `people` flag on. */
911

1012
test('the directory filters, pages and opens a profile that returns to the same view', async ({
1113
page,
@@ -36,10 +38,26 @@ test('the chart reads without a canvas and an unknown focus says so', async ({ p
3638

3739
test('a member withheld the directory gets no People, from the sidebar or a saved link', async ({
3840
page,
41+
browser,
42+
baseURL,
3943
}) => {
40-
await signIn(page, requireEnv('PLATFORM_LIVE_NO_DIRECTORY_EMAIL'), '/org')
41-
await expect(page.getByRole('button', { name: 'Organization menu' })).toBeVisible()
42-
await expect(page.getByRole('link', { name: 'People', exact: true })).toHaveCount(0)
43-
const response = await page.goto('/org/people')
44-
expect(response?.status()).toBe(404)
44+
await signInAsAdmin(page, '/org/home')
45+
const groups = `/api/organizations/${organizationId()}/permission-groups`
46+
const created = await page.request.post(groups, {
47+
data: { name: 'Withhold directory', isDefault: true, config: { hideOrgMemberDirectory: true } },
48+
})
49+
expect(created.status(), await created.text()).toBeLessThan(300)
50+
const { id } = (await created.json()) as { id: string }
51+
const context = await browser.newContext({ baseURL })
52+
try {
53+
const member = await context.newPage()
54+
await signIn(member, memberEmail(), '/org')
55+
await expect(member.getByRole('button', { name: 'Organization menu' })).toBeVisible()
56+
await expect(member.getByRole('link', { name: 'People', exact: true })).toHaveCount(0)
57+
await member.goto('/org/people')
58+
await expect(member.getByRole('heading', { name: 'Page not found' })).toBeVisible()
59+
} finally {
60+
await context.close()
61+
expect((await page.request.delete(`${groups}/${id}`)).ok()).toBe(true)
62+
}
4563
})

‎apps/sim/platform/checks/live/playwright.config.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export default defineConfig({
1919
outputDir: path.join(path.dirname(report), 'artifacts'),
2020
use: {
2121
baseURL: process.env.PLATFORM_LIVE_ORIGIN ?? 'http://localhost:3000',
22+
actionTimeout: 20_000,
2223
screenshot: 'only-on-failure',
2324
trace: 'retain-on-failure',
2425
},

0 commit comments

Comments
 (0)