Skip to content

Commit 0a9c27b

Browse files
authored
Merge pull request #148 from AppFlowy-IO/fix_user_avatar
Fix: display user avatar issue. Support display workspace profile avatar
2 parents 33276c1 + 9a0cd8a commit 0a9c27b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+5455
-1349
lines changed

.claude/agents/tester.md

Whitespace-only changes.

.github/workflows/integration-test.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,12 @@ jobs:
113113
exit 1
114114
)
115115
116+
# Run HTTP API integration tests
117+
- name: Run HTTP API integration tests
118+
run: |
119+
echo "Running HTTP API integration tests..."
120+
pnpm run test:http-api
121+
116122
# Cache build artifacts
117123
- name: Cache build
118124
id: cache-build
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import { avatarTestUtils } from './avatar-test-utils';
2+
3+
const { generateRandomEmail, setupBeforeEach, imports } = avatarTestUtils;
4+
const { updateUserMetadata, AuthTestUtils, AvatarSelectors, WorkspaceSelectors } = imports;
5+
6+
describe('Avatar API', () => {
7+
beforeEach(() => {
8+
setupBeforeEach();
9+
});
10+
11+
describe('Avatar Upload via API', () => {
12+
it('should update avatar URL via API and display in UI', () => {
13+
const testEmail = generateRandomEmail();
14+
const authUtils = new AuthTestUtils();
15+
const testAvatarUrl = 'https://api.dicebear.com/7.x/avataaars/svg?seed=test';
16+
17+
cy.task('log', 'Step 1: Visit login page');
18+
cy.visit('/login', { failOnStatusCode: false });
19+
cy.wait(2000);
20+
21+
cy.task('log', 'Step 2: Sign in with test account');
22+
authUtils.signInWithTestUrl(testEmail);
23+
24+
cy.url({ timeout: 30000 }).should('include', '/app');
25+
cy.wait(3000);
26+
27+
cy.task('log', 'Step 3: Update avatar via API');
28+
updateUserMetadata(testAvatarUrl).then((response) => {
29+
cy.task('log', `API Response: ${JSON.stringify(response)}`);
30+
expect(response.status).to.equal(200);
31+
});
32+
33+
cy.task('log', 'Step 4: Reload page to see updated avatar');
34+
cy.reload();
35+
cy.wait(3000);
36+
37+
cy.task('log', 'Step 5: Open Account Settings to verify avatar');
38+
WorkspaceSelectors.dropdownTrigger().click();
39+
cy.wait(1000);
40+
cy.get('[data-testid="account-settings-button"]').click();
41+
AvatarSelectors.accountSettingsDialog().should('be.visible');
42+
43+
cy.task('log', 'Step 6: Verify avatar image is displayed in Account Settings');
44+
// Note: Account Settings dialog may not display avatar directly
45+
// The avatar is displayed via getUserIconUrl which prioritizes workspace member avatar
46+
// Since we updated user metadata (icon_url), it should be available
47+
// But the actual display location might be in the workspace dropdown or elsewhere
48+
49+
// Wait for any avatar image to be present and loaded
50+
// The AvatarImage component loads asynchronously and sets opacity to 0 while loading
51+
cy.get('[data-testid="avatar-image"]', { timeout: 10000 })
52+
.should('exist')
53+
.should(($imgs) => {
54+
// Find the first visible avatar image (opacity not 0)
55+
let foundVisible = false;
56+
$imgs.each((index, img) => {
57+
const $img = Cypress.$(img);
58+
const opacity = $img.css('opacity');
59+
const src = $img.attr('src');
60+
if (opacity !== '0' && src && src.length > 0) {
61+
foundVisible = true;
62+
return false; // break
63+
}
64+
});
65+
expect(foundVisible, 'At least one avatar image should be visible').to.be.true;
66+
});
67+
68+
// Verify that the avatar image has loaded (check for non-empty src and visible state)
69+
cy.get('[data-testid="avatar-image"]').then(($imgs) => {
70+
let foundLoaded = false;
71+
$imgs.each((index, img) => {
72+
const $img = Cypress.$(img);
73+
const opacity = parseFloat($img.css('opacity') || '0');
74+
const src = $img.attr('src') || '';
75+
76+
if (opacity > 0 && src.length > 0) {
77+
foundLoaded = true;
78+
cy.task('log', `Found loaded avatar image with src: ${src.substring(0, 50)}...`);
79+
return false; // break
80+
}
81+
});
82+
expect(foundLoaded, 'At least one avatar image should be loaded and visible').to.be.true;
83+
});
84+
});
85+
86+
it('test direct API call', () => {
87+
const testEmail = generateRandomEmail();
88+
const authUtils = new AuthTestUtils();
89+
const testAvatarUrl = 'https://api.dicebear.com/7.x/avataaars/svg?seed=test';
90+
91+
cy.task('log', '========== Step 1: Visit login page ==========');
92+
cy.visit('/login', { failOnStatusCode: false });
93+
cy.wait(2000);
94+
95+
cy.task('log', '========== Step 2: Sign in with test account ==========');
96+
authUtils.signInWithTestUrl(testEmail);
97+
cy.url({ timeout: 30000 }).should('include', '/app');
98+
cy.wait(3000);
99+
100+
cy.task('log', '========== Step 3: Get token from localStorage ==========');
101+
cy.window()
102+
.its('localStorage')
103+
.invoke('getItem', 'token')
104+
.then((tokenStr) => {
105+
cy.task('log', `Token string: ${tokenStr ? 'Found' : 'Not found'}`);
106+
const token = JSON.parse(tokenStr);
107+
const accessToken = token.access_token;
108+
cy.task('log', `Access token: ${accessToken ? 'Present (length: ' + accessToken.length + ')' : 'Missing'}`);
109+
});
110+
111+
cy.task('log', '========== Step 4: Making API request ==========');
112+
cy.task('log', `URL: ${avatarTestUtils.APPFLOWY_BASE_URL}/api/user/update`);
113+
cy.task('log', `Avatar URL: ${testAvatarUrl}`);
114+
115+
updateUserMetadata(testAvatarUrl).then((response) => {
116+
cy.task('log', '========== Step 5: Checking response ==========');
117+
cy.task('log', `Response is null: ${response === null}`);
118+
cy.task('log', `Response type: ${typeof response}`);
119+
cy.task('log', `Response status: ${response?.status}`);
120+
cy.task('log', `Response body: ${JSON.stringify(response?.body)}`);
121+
cy.task('log', `Response headers: ${JSON.stringify(response?.headers)}`);
122+
123+
expect(response).to.not.be.null;
124+
expect(response.status).to.equal(200);
125+
126+
if (response.body) {
127+
cy.task('log', `Response body code: ${response.body.code}`);
128+
cy.task('log', `Response body message: ${response.body.message}`);
129+
}
130+
});
131+
});
132+
133+
it('should display emoji as avatar via API', () => {
134+
const testEmail = generateRandomEmail();
135+
const authUtils = new AuthTestUtils();
136+
const testEmoji = '🎨';
137+
138+
cy.task('log', 'Step 1: Visit login page');
139+
cy.visit('/login', { failOnStatusCode: false });
140+
cy.wait(2000);
141+
142+
cy.task('log', 'Step 2: Sign in with test account');
143+
authUtils.signInWithTestUrl(testEmail);
144+
145+
cy.url({ timeout: 30000 }).should('include', '/app');
146+
cy.wait(3000);
147+
148+
cy.task('log', 'Step 3: Update avatar to emoji via API');
149+
updateUserMetadata(testEmoji).then((response) => {
150+
expect(response).to.not.be.null;
151+
expect(response.status).to.equal(200);
152+
});
153+
154+
cy.task('log', 'Step 4: Reload page');
155+
cy.reload();
156+
cy.wait(3000);
157+
158+
cy.task('log', 'Step 5: Open Account Settings');
159+
WorkspaceSelectors.dropdownTrigger().click();
160+
cy.wait(1000);
161+
cy.get('[data-testid="account-settings-button"]').click();
162+
AvatarSelectors.accountSettingsDialog().should('be.visible');
163+
164+
cy.task('log', 'Step 6: Verify emoji is displayed in fallback');
165+
AvatarSelectors.avatarFallback().should('contain.text', testEmoji);
166+
});
167+
168+
it('should display fallback character when no avatar is set', () => {
169+
const testEmail = generateRandomEmail();
170+
const authUtils = new AuthTestUtils();
171+
172+
cy.task('log', 'Step 1: Visit login page');
173+
cy.visit('/login', { failOnStatusCode: false });
174+
cy.wait(2000);
175+
176+
cy.task('log', 'Step 2: Sign in with test account (no avatar set)');
177+
authUtils.signInWithTestUrl(testEmail).then(() => {
178+
cy.url({ timeout: 30000 }).should('include', '/app');
179+
cy.wait(3000);
180+
181+
cy.task('log', 'Step 3: Open workspace dropdown to see avatar');
182+
WorkspaceSelectors.dropdownTrigger().click();
183+
cy.wait(500);
184+
185+
cy.task('log', 'Step 4: Verify fallback is displayed in workspace dropdown avatar');
186+
AvatarSelectors.workspaceDropdownAvatar().within(() => {
187+
AvatarSelectors.avatarFallback().should('be.visible');
188+
});
189+
});
190+
});
191+
});
192+
});
193+
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { avatarTestUtils } from './avatar-test-utils';
2+
3+
const { generateRandomEmail, setupBeforeEach, imports } = avatarTestUtils;
4+
const { updateWorkspaceMemberAvatar, AuthTestUtils, dbUtils } = imports;
5+
6+
describe('Avatar Database', () => {
7+
beforeEach(() => {
8+
setupBeforeEach();
9+
});
10+
11+
describe('Database Verification', () => {
12+
it('should store avatar in workspace_member_profiles table', () => {
13+
const testEmail = generateRandomEmail();
14+
const authUtils = new AuthTestUtils();
15+
const testAvatarUrl = 'https://api.dicebear.com/7.x/avataaars/svg?seed=db-test';
16+
17+
cy.task('log', 'Step 1: Visit login page');
18+
cy.visit('/login', { failOnStatusCode: false });
19+
cy.wait(2000);
20+
21+
cy.task('log', 'Step 2: Sign in with test account');
22+
authUtils.signInWithTestUrl(testEmail).then(() => {
23+
cy.url({ timeout: 30000 }).should('include', '/app');
24+
cy.wait(3000);
25+
26+
cy.task('log', 'Step 3: Set avatar via API');
27+
dbUtils.getCurrentWorkspaceId().then((workspaceId) => {
28+
expect(workspaceId).to.not.be.null;
29+
30+
updateWorkspaceMemberAvatar(workspaceId!, testAvatarUrl).then((response) => {
31+
expect(response.status).to.equal(200);
32+
});
33+
34+
cy.wait(3000);
35+
36+
cy.task('log', 'Step 4: Verify avatar is stored in database');
37+
dbUtils.getCurrentUserUuid().then((userUuid) => {
38+
expect(userUuid).to.not.be.null;
39+
40+
dbUtils.getWorkspaceMemberProfile(workspaceId!, userUuid!).then((profile) => {
41+
expect(profile).to.not.be.null;
42+
expect(profile?.avatar_url).to.equal(testAvatarUrl);
43+
expect(profile?.workspace_id).to.equal(workspaceId);
44+
expect(profile?.user_uuid).to.equal(userUuid);
45+
});
46+
});
47+
});
48+
});
49+
});
50+
});
51+
});
52+

0 commit comments

Comments
 (0)