Skip to content

Commit ccbb103

Browse files
authored
fix: share test (#319)
1 parent 22f9639 commit ccbb103

3 files changed

Lines changed: 103 additions & 11 deletions

File tree

playwright/e2e/page/share-page.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,8 +381,9 @@ test.describe('Share Page Test', () => {
381381

382382
const userCEmail = generateRandomEmail();
383383

384-
// Given: user B account exists and user A is signed in
384+
// Given: user B and C accounts exist and user A is signed in
385385
await createUserAccount(request, userBEmail);
386+
await createUserAccount(request, userCEmail);
386387
await signInAndWaitForApp(page, request, testEmail);
387388
testLog.info('User A signed in');
388389

playwright/e2e/page/shared-view-workspace-routing.spec.ts

Lines changed: 94 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { test, expect, Page, APIRequestContext, Browser } from '@playwright/test';
1+
import { test, expect, Page, APIRequestContext, APIResponse, Browser } from '@playwright/test';
22
import { PageSelectors, SidebarSelectors } from '../../support/selectors';
33
import { generateRandomEmail, setupPageErrorHandling, TestConfig } from '../../support/test-config';
44
import { signInAndWaitForApp } from '../../support/auth-flow-helpers';
@@ -35,6 +35,82 @@ async function getAuthToken(page: Page): Promise<string> {
3535
return page.evaluate(() => localStorage.getItem('af_auth_token') || '');
3636
}
3737

38+
async function expectApiSuccess<T = unknown>(response: APIResponse, action: string): Promise<T> {
39+
let body: { code?: number; data?: T; message?: string } | null = null;
40+
41+
try {
42+
body = await response.json();
43+
} catch {
44+
const text = await response.text().catch(() => '');
45+
46+
throw new Error(`${action} failed: HTTP ${response.status()} ${text}`);
47+
}
48+
49+
if (!response.ok() || body?.code !== 0) {
50+
throw new Error(`${action} failed: HTTP ${response.status()} ${JSON.stringify(body)}`);
51+
}
52+
53+
return body.data as T;
54+
}
55+
56+
async function guestCanAccessSharedPage(
57+
request: APIRequestContext,
58+
guestAuthToken: string,
59+
workspaceId: string,
60+
viewId: string,
61+
): Promise<boolean> {
62+
const response = await request.get(`${TestConfig.apiUrl}/api/workspace/${workspaceId}/page-view/${viewId}`, {
63+
headers: { Authorization: `Bearer ${guestAuthToken}` },
64+
failOnStatusCode: false,
65+
});
66+
const body = await response.json().catch(() => null) as { code?: number; data?: { view?: unknown } } | null;
67+
68+
return response.ok() && body?.code === 0 && Boolean(body?.data?.view);
69+
}
70+
71+
async function joinSharedWorkspaceIfInviteCodeRequired(
72+
request: APIRequestContext,
73+
guestAuthToken: string,
74+
workspaceId: string,
75+
viewId: string,
76+
guestEmail: string,
77+
) {
78+
if (await guestCanAccessSharedPage(request, guestAuthToken, workspaceId, viewId)) {
79+
return;
80+
}
81+
82+
const codesResponse = await request.get(
83+
`${TestConfig.apiUrl}/api/sharing/workspace/${workspaceId}/view/${viewId}/guest-invite-codes`,
84+
{ headers: { Authorization: `Bearer ${guestAuthToken}` } },
85+
);
86+
const codesData = await expectApiSuccess<{ codes: Array<{ code: string; email: string }> }>(
87+
codesResponse,
88+
'List guest invite codes',
89+
);
90+
const inviteCode = codesData.codes.find((item) => item.email.toLowerCase() === guestEmail.toLowerCase())?.code;
91+
92+
if (!inviteCode) {
93+
throw new Error(`Guest cannot access shared page and no guest invite code was found for ${guestEmail}`);
94+
}
95+
96+
const joinResponse = await request.post(
97+
`${TestConfig.apiUrl}/api/sharing/workspace/${workspaceId}/join-by-guest-invite-code`,
98+
{
99+
headers: { Authorization: `Bearer ${guestAuthToken}`, 'Content-Type': 'application/json' },
100+
data: { code: inviteCode },
101+
},
102+
);
103+
104+
await expectApiSuccess(joinResponse, 'Join shared workspace by guest invite code');
105+
106+
await expect
107+
.poll(
108+
() => guestCanAccessSharedPage(request, guestAuthToken, workspaceId, viewId),
109+
{ timeout: 30000, intervals: [1000, 2000, 3000] },
110+
)
111+
.toBe(true);
112+
}
113+
38114
async function createPageWithContent(page: Page, title: string, content: string): Promise<string> {
39115
const viewId = await createDocumentPageAndNavigate(page);
40116

@@ -86,7 +162,7 @@ async function shareViewWithGuest(
86162
data: { view_id: viewId, emails: [guestEmail], access_level: 50, auto_confirm: true },
87163
},
88164
);
89-
if (!response.ok()) throw new Error(`Failed to share view: ${response.status()}`);
165+
await expectApiSuccess(response, 'Share view with guest');
90166
}
91167

92168
async function getUserProfile(request: APIRequestContext, authToken: string) {
@@ -119,7 +195,11 @@ async function waitForEditorContent(page: Page, maxAttempts = 3): Promise<void>
119195
// ── Setup: Annie creates and shares a page ───────────────────────────
120196

121197
async function annieCreatesAndSharesPage(
122-
browser: Browser, request: APIRequestContext, ownerEmail: string, guestEmail: string,
198+
browser: Browser,
199+
request: APIRequestContext,
200+
ownerEmail: string,
201+
guestEmail: string,
202+
guestAuthToken: string,
123203
): Promise<{ ownerWorkspaceId: string; sharedViewId: string; directLink: string }> {
124204
const ownerContext = await browser.newContext();
125205
const ownerPage = await ownerContext.newPage();
@@ -147,6 +227,14 @@ async function annieCreatesAndSharesPage(
147227

148228
await shareViewWithGuest(request, ownerToken, ownerWorkspaceId, sharedViewId, guestEmail);
149229
testLog.info(`Annie shared page with guest (auto_confirm=true)`);
230+
await joinSharedWorkspaceIfInviteCodeRequired(
231+
request,
232+
guestAuthToken,
233+
ownerWorkspaceId,
234+
sharedViewId,
235+
guestEmail,
236+
);
237+
testLog.info('Guest access to Annie workspace confirmed');
150238

151239
const directLink = `/app/${ownerWorkspaceId}/${sharedViewId}`;
152240
await ownerContext.close();
@@ -191,7 +279,7 @@ test.describe('Shared View Cross-Workspace Routing', () => {
191279

192280
// And: Annie creates a page in her workspace and shares it with Nathan
193281
const { ownerWorkspaceId, directLink } = await annieCreatesAndSharesPage(
194-
browser, request, ownerEmail, guestEmail,
282+
browser, request, ownerEmail, guestEmail, guestToken,
195283
);
196284
expect(ownerWorkspaceId).not.toBe(guestWorkspaceId);
197285
testLog.info(`Direct link to Annie's page: ${directLink}`);
@@ -257,7 +345,7 @@ test.describe('Shared View Cross-Workspace Routing', () => {
257345

258346
// And: Annie creates a page and shares it with Nathan
259347
const { ownerWorkspaceId, directLink } = await annieCreatesAndSharesPage(
260-
browser, request, ownerEmail, guestEmail,
348+
browser, request, ownerEmail, guestEmail, guestToken,
261349
);
262350
expect(guestWorkspaceId).not.toBe(ownerWorkspaceId);
263351
testLog.info(`Annie's direct link: ${directLink}`);
@@ -301,7 +389,7 @@ test.describe('Shared View Cross-Workspace Routing', () => {
301389

302390
// And: Annie creates a page and shares it with Nathan
303391
const { sharedViewId } = await annieCreatesAndSharesPage(
304-
browser, request, ownerEmail, guestEmail,
392+
browser, request, ownerEmail, guestEmail, guestToken,
305393
);
306394

307395
// When: Nathan navigates using his OWN workspace ID with the shared view ID

src/components/app/share/SharePanel.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback, useEffect, useMemo, useState } from 'react';
1+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
22

33
import { AccessLevel, IPeopleWithAccessType, MentionablePerson, Role, SubscriptionPlan } from '@/application/types';
44
import { notify } from '@/components/_shared/notify';
@@ -25,6 +25,7 @@ function SharePanel({ viewId }: { viewId: string }) {
2525
const [mentionable, setMentionable] = useState<MentionablePerson[]>([]);
2626
const [isLoadingMentionable, setIsLoadingMentionable] = useState(false);
2727
const [mentionableError, setMentionableError] = useState<string | null>(null);
28+
const loadPeopleRequestSeq = useRef(0);
2829
const outline = useAppOutline();
2930
const hasFullAccess = useMemo(() => {
3031
return people.find((p) => p.email === currentUser?.email)?.access_level === AccessLevel.FullAccess;
@@ -45,18 +46,20 @@ function SharePanel({ viewId }: { viewId: string }) {
4546

4647
const ancestorViewIds = findAncestors(outline || [], viewId)?.map((item) => item.view_id) || [];
4748

49+
const requestSeq = ++loadPeopleRequestSeq.current;
50+
4851
setIsLoading(true);
4952
try {
5053
const detail = await AccessService.getShareDetail(currentWorkspaceId, viewId, ancestorViewIds, signal);
5154

52-
if (signal?.aborted) return;
55+
if (signal?.aborted || requestSeq !== loadPeopleRequestSeq.current) return;
5356
setPeople(detail.shared_with);
5457
} catch (error) {
55-
if (signal?.aborted) return;
58+
if (signal?.aborted || requestSeq !== loadPeopleRequestSeq.current) return;
5659
console.error(error);
5760
setPeople([]);
5861
} finally {
59-
if (!signal?.aborted) {
62+
if (!signal?.aborted && requestSeq === loadPeopleRequestSeq.current) {
6063
setIsLoading(false);
6164
}
6265
}

0 commit comments

Comments
 (0)