Skip to content

Commit d05d369

Browse files
authored
Merge pull request #59 from AppFlowy-IO/update_profile_tst
test: add update profile test, fix UI refresh timing issue
2 parents b15748b + 45319f2 commit d05d369

File tree

6 files changed

+190
-30
lines changed

6 files changed

+190
-30
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,5 @@ coverage
3434

3535
cypress/snapshots/**/__diff_output__/
3636
cypress/screenshots
37+
38+
.serena
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { v4 as uuidv4 } from 'uuid';
2+
import { AuthTestUtils } from '../../support/auth-utils';
3+
4+
describe('Update User Profile', () => {
5+
const generateRandomEmail = () => `${uuidv4()}@appflowy.io`;
6+
7+
beforeEach(() => {
8+
cy.on('uncaught:exception', (err) => {
9+
if (err.message.includes('Minified React error') ||
10+
err.message.includes('View not found') ||
11+
err.message.includes('No workspace or service found')) {
12+
return false;
13+
}
14+
return true;
15+
});
16+
17+
cy.viewport(1280, 720);
18+
});
19+
20+
it('should update user profile settings through Account Settings', () => {
21+
const testEmail = generateRandomEmail();
22+
23+
// Login
24+
cy.log('Step 1: Logging in to the application');
25+
cy.visit('/login', { failOnStatusCode: false });
26+
cy.wait(2000);
27+
28+
const authUtils = new AuthTestUtils();
29+
authUtils.signInWithTestUrl(testEmail).then(() => {
30+
// Wait for app to load
31+
cy.log('Step 2: Waiting for application to load');
32+
cy.url({ timeout: 30000 }).should('include', '/app');
33+
cy.wait(3000);
34+
35+
// Open workspace dropdown
36+
cy.log('Step 3: Opening workspace dropdown');
37+
cy.get('[data-testid="workspace-dropdown-trigger"]', { timeout: 10000 }).should('be.visible').click();
38+
39+
// Wait for dropdown to open
40+
cy.get('[data-testid="workspace-dropdown-content"]', { timeout: 5000 }).should('be.visible');
41+
42+
// Click on Account Settings
43+
cy.log('Step 4: Opening Account Settings');
44+
cy.get('[data-testid="account-settings-button"]').should('be.visible').click();
45+
46+
// Add a wait to ensure the dialog has time to open
47+
cy.wait(1000);
48+
49+
// Wait for Account Settings dialog to open
50+
cy.log('Step 5: Verifying Account Settings dialog opened');
51+
cy.get('[data-testid="account-settings-dialog"]', { timeout: 10000 }).should('be.visible');
52+
53+
// Check initial date format (should be Local/default)
54+
cy.log('Step 6: Checking initial date format');
55+
cy.get('[data-testid="date-format-dropdown"]').should('be.visible');
56+
57+
// Test Date Format change - select US format (Month/Day/Year)
58+
cy.log('Step 7: Testing Date Format change to US format');
59+
cy.get('[data-testid="date-format-dropdown"]').click();
60+
cy.wait(500);
61+
62+
// Select US format (value 1) which is Month/Day/Year
63+
cy.get('[data-testid="date-format-1"]').should('be.visible').click();
64+
cy.wait(3000); // Wait for API call to complete
65+
66+
// Verify the dropdown now shows US format
67+
cy.get('[data-testid="date-format-dropdown"]').should('contain.text', 'Month/Day/Year');
68+
69+
// Test Time Format change
70+
cy.log('Step 8: Testing Time Format change');
71+
cy.get('[data-testid="time-format-dropdown"]').should('be.visible').click();
72+
cy.wait(500);
73+
74+
// Select 24-hour format (value 1)
75+
cy.get('[data-testid="time-format-1"]').should('be.visible').click();
76+
cy.wait(3000); // Wait for API call to complete
77+
78+
// Verify the dropdown now shows 24-hour format
79+
cy.get('[data-testid="time-format-dropdown"]').should('contain.text', '24');
80+
81+
// Test Start Week On change
82+
cy.log('Step 9: Testing Start Week On change');
83+
cy.get('[data-testid="start-week-on-dropdown"]').should('be.visible').click();
84+
cy.wait(500);
85+
86+
// Select Monday (value 1)
87+
cy.get('[data-testid="start-week-1"]').should('be.visible').click();
88+
cy.wait(3000); // Wait for API call to complete
89+
90+
cy.get('[data-testid="start-week-on-dropdown"]').should('contain.text', 'Monday');
91+
92+
// The settings should remain selected in the current session
93+
cy.log('Step 10: Verifying all settings are showing correctly');
94+
95+
// Verify all dropdowns still show the selected values
96+
cy.get('[data-testid="date-format-dropdown"]').should('contain.text', 'Month/Day/Year');
97+
cy.get('[data-testid="time-format-dropdown"]').should('contain.text', '24');
98+
cy.get('[data-testid="start-week-on-dropdown"]').should('contain.text', 'Monday');
99+
100+
cy.log('Test completed: User profile settings updated successfully');
101+
});
102+
});
103+
});

src/@types/translations/en.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1539,8 +1539,8 @@
15391539
"isRange": "End date",
15401540
"dateFormatFriendly": "Month Day, Year",
15411541
"dateFormatISO": "Year-Month-Day",
1542-
"dateFormatLocal": "Month/Day/Year",
1543-
"dateFormatUS": "Year/Month/Day",
1542+
"dateFormatLocal": "Local",
1543+
"dateFormatUS": "Month/Day/Year",
15441544
"dateFormatDayMonthYear": "Day/Month/Year",
15451545
"timeFormat": "Time format",
15461546
"invalidTimeFormat": "Invalid format",

src/application/services/js-services/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,7 @@ export class AFClientService implements AFService {
361361
const token = getTokenParsed();
362362
const userId = token?.user?.id;
363363

364-
const user = await getUser(() => APIService.getCurrentUser(), userId, StrategyType.CACHE_AND_NETWORK);
364+
const user = await getUser(() => APIService.getCurrentUser(), userId, StrategyType.NETWORK_ONLY);
365365

366366
if (!user) {
367367
return Promise.reject(new Error('User not found'));

src/components/app/workspaces/AccountSettings.tsx

Lines changed: 72 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import dayjs from 'dayjs';
2-
import { useCallback, useMemo, useState, useEffect } from 'react';
2+
import { debounce } from 'lodash-es';
3+
import { useCallback, useMemo, useState, useEffect, useRef } from 'react';
34
import { useTranslation } from 'react-i18next';
45

56
import { DateFormat, TimeFormat } from '@/application/types';
67
import { MetadataKey } from '@/application/user-metadata';
78
import { ReactComponent as ChevronDownIcon } from '@/assets/icons/alt_arrow_down.svg';
89
import { useCurrentUser, useService } from '@/components/main/app.hooks';
9-
import { Dialog, DialogContent, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
10+
import { Dialog, DialogContent, DialogDescription, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
1011
import {
1112
DropdownMenu,
1213
DropdownMenuContent,
@@ -20,6 +21,7 @@ export function AccountSettings({ children }: { children?: React.ReactNode }) {
2021
const { t } = useTranslation();
2122
const currentUser = useCurrentUser();
2223
const service = useService();
24+
const [open, setIsOpen] = useState(false);
2325

2426
const [dateFormat, setDateFormat] = useState(
2527
() => Number(currentUser?.metadata?.[MetadataKey.DateFormat] as DateFormat) || DateFormat.Local
@@ -29,54 +31,86 @@ export function AccountSettings({ children }: { children?: React.ReactNode }) {
2931
);
3032
const [startWeekOn, setStartWeekOn] = useState(() => Number(currentUser?.metadata?.[MetadataKey.StartWeekOn]) || 0);
3133

34+
const metadataUpdateRef = useRef<Record<string, unknown> | null>(null);
35+
36+
const debounceUpdateProfile = useMemo(() => {
37+
return debounce(async () => {
38+
if (!service || !currentUser?.metadata || !metadataUpdateRef.current) return;
39+
40+
await service?.updateUserProfile(metadataUpdateRef.current);
41+
}, 300);
42+
}, [service, currentUser]);
43+
3244
useEffect(() => {
33-
setDateFormat(Number(currentUser?.metadata?.[MetadataKey.DateFormat] as DateFormat) || DateFormat.Local);
34-
setTimeFormat(Number(currentUser?.metadata?.[MetadataKey.TimeFormat] as TimeFormat) || TimeFormat.TwelveHour);
35-
setStartWeekOn(Number(currentUser?.metadata?.[MetadataKey.StartWeekOn]) || 0);
36-
}, [currentUser]);
45+
return () => {
46+
debounceUpdateProfile.cancel();
47+
};
48+
}, [debounceUpdateProfile]);
3749

3850
const handleSelectDateFormat = useCallback(
3951
async (dateFormat: number) => {
4052
setDateFormat(dateFormat);
41-
if (!service || !currentUser?.metadata) return;
53+
if (!currentUser?.metadata) return;
4254

43-
await service?.updateUserProfile({ ...currentUser.metadata, [MetadataKey.DateFormat]: dateFormat });
44-
await service?.getCurrentUser();
55+
metadataUpdateRef.current = { ...currentUser.metadata, [MetadataKey.DateFormat]: dateFormat };
56+
await debounceUpdateProfile();
4557
},
46-
[currentUser, service]
58+
[currentUser, debounceUpdateProfile]
4759
);
4860

4961
const handleSelectTimeFormat = useCallback(
5062
async (timeFormat: number) => {
5163
setTimeFormat(timeFormat);
52-
if (!service || !currentUser?.metadata) return;
64+
if (!currentUser?.metadata) return;
5365

54-
await service?.updateUserProfile({ ...currentUser.metadata, [MetadataKey.TimeFormat]: timeFormat });
55-
await service?.getCurrentUser();
66+
metadataUpdateRef.current = { ...currentUser.metadata, [MetadataKey.TimeFormat]: timeFormat };
67+
await debounceUpdateProfile();
5668
},
57-
[currentUser, service]
69+
[currentUser, debounceUpdateProfile]
5870
);
5971

6072
const handleSelectStartWeekOn = useCallback(
6173
async (startWeekOn: number) => {
6274
setStartWeekOn(startWeekOn);
63-
if (!service || !currentUser?.metadata) return;
6475

65-
await service?.updateUserProfile({ ...currentUser.metadata, [MetadataKey.StartWeekOn]: startWeekOn });
66-
await service?.getCurrentUser();
76+
if (!currentUser?.metadata) return;
77+
78+
metadataUpdateRef.current = { ...currentUser.metadata, [MetadataKey.StartWeekOn]: startWeekOn };
79+
await debounceUpdateProfile();
80+
},
81+
[currentUser, debounceUpdateProfile]
82+
);
83+
84+
const onOpenChange = useCallback(
85+
async (isOpen: boolean) => {
86+
if (isOpen) {
87+
const user = await service?.getCurrentUser();
88+
89+
setDateFormat(Number(user?.metadata?.[MetadataKey.DateFormat] as DateFormat) || DateFormat.Local);
90+
setTimeFormat(Number(user?.metadata?.[MetadataKey.TimeFormat] as TimeFormat) || TimeFormat.TwelveHour);
91+
setStartWeekOn(Number(user?.metadata?.[MetadataKey.StartWeekOn]) || 0);
92+
}
93+
94+
setIsOpen(isOpen);
6795
},
68-
[currentUser, service]
96+
[service]
6997
);
7098

7199
if (!currentUser || !service) {
72100
return <></>;
73101
}
74102

75103
return (
76-
<Dialog>
104+
<Dialog open={open} onOpenChange={onOpenChange}>
77105
<DialogTrigger asChild>{children}</DialogTrigger>
78-
<DialogContent className='flex h-[300px] min-h-0 w-[400px] flex-col gap-3 sm:max-w-[calc(100%-2rem)]'>
106+
<DialogContent
107+
data-testid='account-settings-dialog'
108+
className='flex h-[300px] min-h-0 w-[400px] flex-col gap-3 sm:max-w-[calc(100%-2rem)]'
109+
>
79110
<DialogTitle className='text-md font-bold text-text-primary'>{t('web.accountSettings')}</DialogTitle>
111+
<DialogDescription className='sr-only'>
112+
Configure your account preferences including date format, time format, and week start day
113+
</DialogDescription>
80114
<div className='flex min-h-0 w-full flex-1 flex-col items-start gap-3 py-4'>
81115
<DateFormatDropdown dateFormat={dateFormat} onSelect={handleSelectDateFormat} />
82116
<TimeFormatDropdown timeFormat={timeFormat} onSelect={handleSelectTimeFormat} />
@@ -126,6 +160,7 @@ function DateFormatDropdown({ dateFormat, onSelect }: { dateFormat: number; onSe
126160
<div className='relative'>
127161
<DropdownMenu open={isOpen} onOpenChange={setIsOpen} modal={false}>
128162
<DropdownMenuTrigger
163+
data-testid='date-format-dropdown'
129164
asChild
130165
onPointerDown={(e) => {
131166
e.preventDefault();
@@ -148,7 +183,11 @@ function DateFormatDropdown({ dateFormat, onSelect }: { dateFormat: number; onSe
148183
<DropdownMenuContent align='start'>
149184
<DropdownMenuRadioGroup value={dateFormat.toString()} onValueChange={(value) => onSelect(Number(value))}>
150185
{dateFormats.map((item) => (
151-
<DropdownMenuRadioItem key={item.value} value={item.value.toString()}>
186+
<DropdownMenuRadioItem
187+
data-testid={`date-format-${item.value}`}
188+
key={item.value}
189+
value={item.value.toString()}
190+
>
152191
{item.label}
153192
</DropdownMenuRadioItem>
154193
))}
@@ -187,6 +226,7 @@ function TimeFormatDropdown({ timeFormat, onSelect }: { timeFormat: number; onSe
187226
<div className='relative'>
188227
<DropdownMenu open={isOpen} onOpenChange={setIsOpen} modal={false}>
189228
<DropdownMenuTrigger
229+
data-testid='time-format-dropdown'
190230
asChild
191231
onPointerDown={(e) => {
192232
e.preventDefault();
@@ -209,7 +249,11 @@ function TimeFormatDropdown({ timeFormat, onSelect }: { timeFormat: number; onSe
209249
<DropdownMenuContent align='start'>
210250
<DropdownMenuRadioGroup value={timeFormat.toString()} onValueChange={(value) => onSelect(Number(value))}>
211251
{timeFormats.map((item) => (
212-
<DropdownMenuRadioItem key={item.value} value={item.value.toString()}>
252+
<DropdownMenuRadioItem
253+
data-testid={`time-format-${item.value}`}
254+
key={item.value}
255+
value={item.value.toString()}
256+
>
213257
{item.label}
214258
</DropdownMenuRadioItem>
215259
))}
@@ -251,6 +295,7 @@ function StartWeekOnDropdown({
251295
<div className='relative'>
252296
<DropdownMenu open={isOpen} onOpenChange={setIsOpen} modal={false}>
253297
<DropdownMenuTrigger
298+
data-testid='start-week-on-dropdown'
254299
asChild
255300
onPointerDown={(e) => {
256301
e.preventDefault();
@@ -273,7 +318,11 @@ function StartWeekOnDropdown({
273318
<DropdownMenuContent align='start'>
274319
<DropdownMenuRadioGroup value={startWeekOn.toString()} onValueChange={(value) => onSelect(Number(value))}>
275320
{daysOfWeek.map((item) => (
276-
<DropdownMenuRadioItem key={item.value} value={item.value.toString()}>
321+
<DropdownMenuRadioItem
322+
data-testid={`start-week-${item.value}`}
323+
key={item.value}
324+
value={item.value.toString()}
325+
>
277326
{item.label}
278327
</DropdownMenuRadioItem>
279328
))}

src/components/app/workspaces/Workspaces.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ export function Workspaces() {
134134
<DropdownMenuTrigger asChild>
135135
<div
136136
ref={ref}
137-
data-testid="workspace-dropdown-trigger"
137+
data-testid='workspace-dropdown-trigger'
138138
onMouseLeave={() => setHoveredHeader(false)}
139139
onMouseEnter={() => setHoveredHeader(true)}
140140
className={dropdownMenuItemVariants({ variant: 'default', className: 'w-full overflow-hidden' })}
@@ -157,11 +157,17 @@ export function Workspaces() {
157157
</div>
158158
</DropdownMenuTrigger>
159159

160-
<DropdownMenuContent data-testid="workspace-dropdown-content" className='min-w-[300px] max-w-[300px] overflow-hidden'>
160+
<DropdownMenuContent
161+
data-testid='workspace-dropdown-content'
162+
className='min-w-[300px] max-w-[300px] overflow-hidden'
163+
>
161164
<DropdownMenuLabel className='w-full overflow-hidden'>
162165
<span className='truncate'>{currentUser?.email}</span>
163166
</DropdownMenuLabel>
164-
<DropdownMenuGroup data-testid="workspace-list" className={'appflowy-scroller max-h-[200px] flex-1 overflow-y-auto overflow-x-hidden'}>
167+
<DropdownMenuGroup
168+
data-testid='workspace-list'
169+
className={'appflowy-scroller max-h-[200px] flex-1 overflow-y-auto overflow-x-hidden'}
170+
>
165171
<WorkspaceList
166172
defaultWorkspaces={userWorkspaceInfo?.workspaces}
167173
currentWorkspaceId={currentWorkspaceId}
@@ -217,7 +223,7 @@ export function Workspaces() {
217223
<DropdownMenuSeparator />
218224
<DropdownMenuGroup>
219225
<AccountSettings>
220-
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
226+
<DropdownMenuItem data-testid='account-settings-button' onSelect={(e) => e.preventDefault()}>
221227
<SettingsIcon />
222228
<div className={'flex-1 text-left'}>{t('web.accountSettings')}</div>
223229
<ChevronRightIcon className='text-icon-tertiary' />

0 commit comments

Comments
 (0)