Skip to content

Commit 65ae5ed

Browse files
authored
chore: show duplicating loading indicator (#237)
1 parent 34a659c commit 65ae5ed

File tree

2 files changed

+168
-17
lines changed

2 files changed

+168
-17
lines changed
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import { AuthTestUtils } from '../../support/auth-utils';
2+
import { TestTool } from '../../support/page-utils';
3+
import {
4+
AddPageSelectors,
5+
DropdownSelectors,
6+
EditorSelectors,
7+
HeaderSelectors,
8+
PageSelectors,
9+
SpaceSelectors,
10+
waitForReactUpdate,
11+
} from '../../support/selectors';
12+
import { generateRandomEmail } from '../../support/test-config';
13+
import { testLog } from '../../support/test-helpers';
14+
15+
describe('Duplicate Page', () => {
16+
let testEmail: string;
17+
18+
beforeEach(function () {
19+
testEmail = generateRandomEmail();
20+
});
21+
22+
it('should create a document, type hello world, duplicate it, and verify content in duplicated document', () => {
23+
cy.on('uncaught:exception', (err: Error) => {
24+
if (
25+
err.message.includes('No workspace or service found') ||
26+
err.message.includes('View not found') ||
27+
err.message.includes('Minified React error')
28+
) {
29+
return false;
30+
}
31+
32+
return true;
33+
});
34+
35+
const pageName = `Test Page ${Date.now()}`;
36+
37+
// Step 1: Sign in
38+
testLog.step(1, 'Signing in');
39+
cy.visit('/login', { failOnStatusCode: false });
40+
cy.wait(2000);
41+
42+
const authUtils = new AuthTestUtils();
43+
authUtils.signInWithTestUrl(testEmail);
44+
45+
cy.url().should('include', '/app');
46+
TestTool.waitForPageLoad(3000);
47+
TestTool.waitForSidebarReady();
48+
cy.wait(2000);
49+
50+
// Step 2: Create a new document page
51+
testLog.step(2, 'Creating a new document page');
52+
53+
SpaceSelectors.itemByName('General').first().click();
54+
waitForReactUpdate(500);
55+
56+
SpaceSelectors.itemByName('General').first().within(() => {
57+
AddPageSelectors.inlineAddButton().first().should('be.visible').click();
58+
});
59+
waitForReactUpdate(1000);
60+
61+
DropdownSelectors.menuItem().first().click();
62+
waitForReactUpdate(2000);
63+
64+
testLog.info('New document page created (in modal mode)');
65+
66+
// Step 3: Exit modal mode by pressing Escape
67+
testLog.step(3, 'Exiting modal mode');
68+
cy.get('body').type('{esc}');
69+
waitForReactUpdate(1000);
70+
testLog.info('Exited modal mode');
71+
72+
// Step 4: Open the created page from sidebar (it should be "Untitled")
73+
testLog.step(4, 'Opening the created page from sidebar');
74+
PageSelectors.nameContaining('Untitled').first().click({ force: true });
75+
waitForReactUpdate(2000);
76+
testLog.info('Opened the page');
77+
78+
// Step 5: Type "hello world" in the document
79+
testLog.step(5, 'Typing hello world in the document');
80+
81+
EditorSelectors.firstEditor().should('exist', { timeout: 15000 });
82+
EditorSelectors.firstEditor().click({ force: true }).type('hello world', { force: true });
83+
cy.wait(2000);
84+
85+
cy.contains('hello world').should('exist');
86+
testLog.info('Content added: "hello world"');
87+
88+
// Step 6: Duplicate the document from the header
89+
testLog.step(6, 'Duplicating the document');
90+
91+
HeaderSelectors.moreActionsButton().should('be.visible').click({ force: true });
92+
waitForReactUpdate(500);
93+
94+
DropdownSelectors.content().within(() => {
95+
cy.contains('Duplicate').click();
96+
});
97+
testLog.info('Clicked Duplicate');
98+
99+
// Verify toast appears
100+
cy.get('[data-sonner-toast]', { timeout: 5000 }).should('exist');
101+
testLog.info('Toast notification appeared');
102+
103+
// Wait for duplication to complete and toast to dismiss
104+
// The toast should be dismissed after the operation completes
105+
cy.get('[data-sonner-toast]', { timeout: 15000 }).should('not.exist');
106+
testLog.info('Toast dismissed - duplication completed successfully');
107+
108+
// Verify no error toast appeared
109+
cy.get('[data-sonner-toast][data-type="error"]').should('not.exist');
110+
111+
// Step 7: Find and open the duplicated document
112+
testLog.step(7, 'Opening the duplicated document');
113+
114+
// The duplicated page should have "(copy)" suffix
115+
// Look for it in the sidebar
116+
PageSelectors.names().then(($pages: JQuery<HTMLElement>) => {
117+
const duplicatedPage = Array.from($pages).find((el) => {
118+
const text = el.textContent || '';
119+
return text.includes('Untitled') && text.includes('(copy)');
120+
});
121+
122+
if (duplicatedPage) {
123+
testLog.info(`Found duplicated page: ${duplicatedPage.textContent}`);
124+
cy.wrap(duplicatedPage).click({ force: true });
125+
} else {
126+
// If no "(copy)" suffix, look for second Untitled page
127+
testLog.info('Looking for duplicated page');
128+
const untitledPages = Array.from($pages).filter((el) =>
129+
el.textContent?.includes('Untitled')
130+
);
131+
if (untitledPages.length > 1) {
132+
cy.wrap(untitledPages[1]).click({ force: true });
133+
} else {
134+
// Just click the first Untitled
135+
PageSelectors.nameContaining('Untitled').first().click({ force: true });
136+
}
137+
}
138+
});
139+
140+
waitForReactUpdate(2000);
141+
142+
// Step 8: Verify the duplicated document contains "hello world"
143+
testLog.step(8, 'Verifying content in duplicated document');
144+
145+
cy.contains('hello world', { timeout: 10000 }).should('exist');
146+
testLog.info('Duplicated document contains "hello world"');
147+
148+
// Step 9: Modify the content in the duplicated document
149+
testLog.step(9, 'Modifying content in duplicated document');
150+
151+
EditorSelectors.firstEditor().should('exist', { timeout: 15000 });
152+
EditorSelectors.firstEditor().click({ force: true }).type(' - modified in copy', { force: true });
153+
cy.wait(2000);
154+
155+
cy.contains('hello world - modified in copy').should('exist');
156+
testLog.success('Duplicated document modified successfully - test passed!');
157+
});
158+
});

src/components/app/header/MoreActionsContent.tsx

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useMemo, useState } from 'react';
1+
import { useCallback, useMemo, useState } from 'react';
22
import { useTranslation } from 'react-i18next';
33
import { toast } from 'sonner';
44

@@ -13,7 +13,6 @@ import { useAppHandlers, useAppOutline, useAppView, useCurrentWorkspaceId } from
1313
import MovePagePopover from '@/components/app/view-actions/MovePagePopover';
1414
import { useService } from '@/components/main/app.hooks';
1515
import { DropdownMenuGroup, DropdownMenuItem } from '@/components/ui/dropdown-menu';
16-
import { Progress } from '@/components/ui/progress';
1716

1817

1918
function MoreActionsContent ({ itemClicked, viewId }: {
@@ -38,44 +37,38 @@ function MoreActionsContent ({ itemClicked, viewId }: {
3837
return findView(outline, parentViewId) ?? null;
3938
}, [outline, parentViewId]);
4039

41-
const [duplicateLoading, setDuplicateLoading] = useState(false);
4240
const {
4341
refreshOutline,
4442
} = useAppHandlers();
4543
const handleDuplicateClick = async () => {
4644
if (!workspaceId || !service) return;
47-
setDuplicateLoading(true);
45+
itemClicked?.();
46+
toast.loading(`${t('moreAction.duplicateView')}...`);
4847
try {
4948
await service.duplicateAppPage(workspaceId, viewId);
50-
49+
toast.dismiss();
5150
void refreshOutline?.();
5251
// eslint-disable-next-line
5352
} catch (e: any) {
53+
toast.dismiss();
5454
toast.error(e.message);
55-
} finally {
56-
setDuplicateLoading(false);
5755
}
58-
59-
itemClicked?.();
6056
};
6157

6258
const [container, setContainer] = useState<HTMLElement | null>(null);
59+
const containerRef = useCallback((el: HTMLElement | null) => {
60+
setContainer(el);
61+
}, []);
6362

6463
return (
6564
<DropdownMenuGroup
6665
>
67-
<div
68-
ref={el => {
69-
70-
setContainer(el);
71-
}}
72-
/>
66+
<div ref={containerRef} />
7367
<DropdownMenuItem
7468
className={`${layout === ViewLayout.AIChat ? 'hidden' : ''}`}
7569
onSelect={handleDuplicateClick}
76-
disabled={duplicateLoading}
7770
>
78-
{duplicateLoading ? <Progress /> : <DuplicateIcon />}
71+
<DuplicateIcon />
7972
{t('button.duplicate')}
8073
</DropdownMenuItem>
8174
{container && <MovePagePopover

0 commit comments

Comments
 (0)