-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathinvitations.spec.ts
More file actions
115 lines (98 loc) · 3.85 KB
/
Copy pathinvitations.spec.ts
File metadata and controls
115 lines (98 loc) · 3.85 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
import { type Browser } from '@playwright/test';
import { expect, test } from 'e2e/utils';
import {
ADMIN_FILE,
INVITED_EMAIL,
INVITED_FILE,
ORG_SLUG,
} from 'e2e/utils/constants';
type Invitation = { id: string; email: string; status: string };
type Member = { user: { email: string } };
/**
* Sets up a fresh pending invitation for INVITED_EMAIL in the default org.
*
* - Cancels any existing pending invitation for that email.
* - Removes the user from the org if they are already a member.
* - Creates a new invitation and returns its ID.
*/
async function ensureInvitation(browser: Browser): Promise<string> {
const adminContext = await browser.newContext({ storageState: ADMIN_FILE });
try {
// 1. Get current org state
const orgResp = await adminContext.request.get(
`/api/rest/organizations/active?slug=${ORG_SLUG}`
);
const org = await orgResp.json();
// 2. Cancel any existing invitation for INVITED_EMAIL
const existingInvitation = (
org.invitations as Invitation[] | undefined
)?.find((i) => i.email === INVITED_EMAIL);
if (existingInvitation) {
await adminContext.request.post(
'/api/rest/organizations/cancel-invitation',
{ data: { invitationId: existingInvitation.id } }
);
}
// 3. Remove INVITED_EMAIL from org members if present
const existingMember = (org.members as Member[] | undefined)?.find(
(m) => m.user.email === INVITED_EMAIL
);
if (existingMember) {
await adminContext.request.post('/api/rest/organizations/remove-member', {
data: { memberIdOrEmail: INVITED_EMAIL },
});
}
// 4. Create a new invitation
await adminContext.request.post('/api/rest/organizations/invite-bulk', {
data: { emails: [INVITED_EMAIL], role: 'member' },
});
// 5. Get the new invitation ID
const orgResp2 = await adminContext.request.get(
`/api/rest/organizations/active?slug=${ORG_SLUG}`
);
const org2 = await orgResp2.json();
const invitation = (org2.invitations as Invitation[] | undefined)?.find(
(i) => i.email === INVITED_EMAIL
);
if (!invitation?.id) {
throw new Error(
`Failed to find invitation for ${INVITED_EMAIL} after creation`
);
}
return invitation.id;
} finally {
await adminContext.close();
}
}
test.describe('Invitation flow', () => {
// The invited user is already authenticated via INVITED_FILE storage state.
// Visiting /invitations/<id> while authenticated triggers the auto-accept
// directly, avoiding the redirect-after-login flow.
test.use({ storageState: INVITED_FILE });
test('Accept an organization invitation', async ({
browser,
page,
invitationPage,
}) => {
const invitationId = await ensureInvitation(browser);
await page.goto(`/invitations/${invitationId}`);
// The page auto-accepts once the session is available
await invitationPage.expectAccepted();
// Navigate to the app — the "Go to app" button calls navigate({ to: '/app' }).
// Depending on parallel test interference the user may need onboarding or may
// land on a "no organization" page, so we handle several outcomes.
await invitationPage.goToApp();
// Happy path: user is onboarded and has an org → OrgRedirect fires → layout-app.
// Onboarding path: user was re-created without onboardedAt → "Welcome" heading.
const layoutApp = page.getByTestId('layout-app');
const onboardingHeading = page.getByRole('heading', { name: 'Welcome' });
await expect(layoutApp.or(onboardingHeading)).toBeVisible({
timeout: 15_000,
});
if (await onboardingHeading.isVisible().catch(() => false)) {
await page.getByRole('textbox').first().fill('Invited User');
await page.getByRole('button', { name: 'Continue' }).click();
await expect(layoutApp).toBeVisible({ timeout: 15_000 });
}
});
});