From faee8cad95b72ad96e2541a0b40e09d28645393b Mon Sep 17 00:00:00 2001 From: Wilhelm Behncke Date: Tue, 28 May 2024 14:47:04 +0200 Subject: [PATCH] TASK: Handle edge case when automatic syncing during "publish document" removes the document This also adds two more E2E test cases for publishing with automatic syncing. --- .../Fixtures/1Dimension/syncing.e2e.js | 352 ++++++++++++------ packages/neos-ui-sagas/src/Publish/index.ts | 18 +- 2 files changed, 259 insertions(+), 111 deletions(-) diff --git a/Tests/IntegrationTests/Fixtures/1Dimension/syncing.e2e.js b/Tests/IntegrationTests/Fixtures/1Dimension/syncing.e2e.js index 13aaee6fe6..0a591b2b70 100644 --- a/Tests/IntegrationTests/Fixtures/1Dimension/syncing.e2e.js +++ b/Tests/IntegrationTests/Fixtures/1Dimension/syncing.e2e.js @@ -14,153 +14,240 @@ fixture`Syncing` const contentIframeSelector = Selector('[name="neos-content-main"]', {timeout: 2000}); test('Syncing: Create a conflict state between two editors and choose "Discard all" as a resolution strategy during rebase', async t => { - await prepareConflictBetweenAdminAndEditor(t); + await prepareContentElementConflictBetweenAdminAndEditor(t); await chooseDiscardAllAsResolutionStrategy(t); await confirmAndPerformDiscardAll(t); await finishSynchronization(t); - await assertThatSynchronizationWasSuccessful(t); + + await assertThatWeAreOnPage(t, 'Home'); + await assertThatWeCannotSeePageInTree(t, 'Sync Demo #1'); + await assertThatWeCannotSeePageInTree(t, 'Sync Demo #2'); + await assertThatWeCannotSeePageInTree(t, 'Sync Demo #3'); }); test('Syncing: Create a conflict state between two editors and choose "Drop conflicting changes" as a resolution strategy during rebase', async t => { - await prepareConflictBetweenAdminAndEditor(t); + await prepareContentElementConflictBetweenAdminAndEditor(t); await chooseDropConflictingChangesAsResolutionStrategy(t); await confirmDropConflictingChanges(t); await finishSynchronization(t); - await assertThatSynchronizationWasSuccessful(t); + + await assertThatWeAreOnPage(t, 'Home'); + await assertThatWeCannotSeePageInTree(t, 'Sync Demo #1'); + await assertThatWeCannotSeePageInTree(t, 'Sync Demo #2'); + await assertThatWeCannotSeePageInTree(t, 'Sync Demo #3'); }); test('Syncing: Create a conflict state between two editors, start and cancel resolution, then restart and choose "Drop conflicting changes" as a resolution strategy during rebase', async t => { - await prepareConflictBetweenAdminAndEditor(t); + await prepareContentElementConflictBetweenAdminAndEditor(t); await cancelResolutionDuringStrategyChoice(t); await startSynchronization(t); await assertThatConflictResolutionHasStarted(t); await chooseDropConflictingChangesAsResolutionStrategy(t); await confirmDropConflictingChanges(t); await finishSynchronization(t); - await assertThatSynchronizationWasSuccessful(t); + + await assertThatWeAreOnPage(t, 'Home'); + await assertThatWeCannotSeePageInTree(t, 'Sync Demo #1'); + await assertThatWeCannotSeePageInTree(t, 'Sync Demo #2'); + await assertThatWeCannotSeePageInTree(t, 'Sync Demo #3'); }); test('Syncing: Create a conflict state between two editors and choose "Drop conflicting changes" as a resolution strategy, then cancel and choose "Discard all" as a resolution strategy during rebase', async t => { - await prepareConflictBetweenAdminAndEditor(t); + await prepareContentElementConflictBetweenAdminAndEditor(t); await chooseDropConflictingChangesAsResolutionStrategy(t); await cancelDropConflictingChanges(t); await chooseDiscardAllAsResolutionStrategy(t); await confirmAndPerformDiscardAll(t); await finishSynchronization(t); - await assertThatSynchronizationWasSuccessful(t); + + await assertThatWeAreOnPage(t, 'Home'); + await assertThatWeCannotSeePageInTree(t, 'Sync Demo #1'); + await assertThatWeCannotSeePageInTree(t, 'Sync Demo #2'); + await assertThatWeCannotSeePageInTree(t, 'Sync Demo #3'); }); -async function prepareConflictBetweenAdminAndEditor(t) { - // - // Login as "editor" once, to initialize a content stream for their workspace - // in case there isn't one already - // - await switchToRole(t, editorUserOnOneDimensionTestSite); - await Page.waitForIframeLoading(); - await t.wait(2000); +test('Publish + Syncing: Create a conflict state between two editors, then try to publish and choose "Drop conflicting changes" as a resolution strategy during automatic rebase', async t => { + await prepareDocumentConflictBetweenAdminAndEditor(t); + await startPublishAll(t); + await assertThatConflictResolutionHasStarted(t); + await chooseDropConflictingChangesAsResolutionStrategy(t); + await confirmDropConflictingChanges(t); + await finishPublish(t); + + await assertThatWeAreOnPage(t, 'Home'); + await assertThatWeCannotSeePageInTree(t, 'This page will be deleted during sync'); +}); + +test('Publish + Syncing: Create a conflict state between two editors, then try to publish the document only and choose "Drop conflicting changes" as a resolution strategy during automatic rebase', async t => { + await prepareDocumentConflictBetweenAdminAndEditor(t); + await startPublishDocument(t); + await assertThatConflictResolutionHasStarted(t); + await chooseDropConflictingChangesAsResolutionStrategy(t); + await confirmDropConflictingChanges(t); + await finishPublish(t); + + await assertThatWeAreOnPage(t, 'Home'); + await assertThatWeCannotSeePageInTree(t, 'This page will be deleted during sync'); +}); + +async function prepareContentElementConflictBetweenAdminAndEditor(t) { + await loginAsEditorOnceToInitializeAContentStreamForTheirWorkspaceIfNeeded(t); // // Login as "admin" // - await switchToRole(t, adminUserOnOneDimensionTestSite); - await PublishDropDown.discardAll(); + await as(t, adminUserOnOneDimensionTestSite, async () => { + await PublishDropDown.discardAll(); - // - // Create a hierarchy of document nodes - // - async function createDocumentNode(pageTitleToCreate) { - await t - .click(Selector('#neos-PageTree-AddNode')) - .click(ReactSelector('InsertModeSelector').find('#into')) - .click(ReactSelector('NodeTypeItem').find('button>span>span').withText('Page_Test')) - .typeText(Selector('#neos-NodeCreationDialog-Body input'), pageTitleToCreate) - .click(Selector('#neos-NodeCreationDialog-CreateNew')); - await Page.waitForIframeLoading(); - } - await createDocumentNode('Sync Demo #1'); - await createDocumentNode('Sync Demo #2'); - await createDocumentNode('Sync Demo #3'); + // + // Create a hierarchy of document nodes + // + await createDocumentNode(t, 'Home', 'into', 'Sync Demo #1'); + await createDocumentNode(t, 'Sync Demo #1', 'into', 'Sync Demo #2'); + await createDocumentNode(t, 'Sync Demo #2', 'into', 'Sync Demo #3'); - // - // Publish everything - // - await PublishDropDown.publishAll(); + // + // Publish everything + // + await PublishDropDown.publishAll(); + }); // // Login as "editor" // - await switchToRole(t, editorUserOnOneDimensionTestSite); - - // - // Sync changes from "admin" - // - await t.wait(2000); - await t.eval(() => location.reload(true)); - await waitForReact(30000); - await Page.waitForIframeLoading(); - await startSynchronization(t); - await t.wait(1000); + await as(t, editorUserOnOneDimensionTestSite, async () => { + // + // Sync changes from "admin" + // + await t.wait(2000); + await t.eval(() => location.reload(true)); + await waitForReact(30000); + await Page.waitForIframeLoading(); + await startSynchronization(t); + await t.wait(1000); + + // + // Assert that all 3 documents are now visible in the document tree + // + await t.expect(Page.treeNode.withExactText('Sync Demo #1').exists) + .ok('[🗋 Sync Demo #1] cannot be found in the document tree of user "editor".'); + await t.expect(Page.treeNode.withExactText('Sync Demo #2').exists) + .ok('[🗋 Sync Demo #2] cannot be found in the document tree of user "editor".'); + await t.expect(Page.treeNode.withExactText('Sync Demo #3').exists) + .ok('[🗋 Sync Demo #3] cannot be found in the document tree of user "editor".'); + }); - // - // Assert that all 3 documents are now visible in the document tree - // - await t.expect(Page.treeNode.withExactText('Sync Demo #1').exists) - .ok('[🗋 Sync Demo #1] cannot be found in the document tree of user "editor".'); - await t.expect(Page.treeNode.withExactText('Sync Demo #2').exists) - .ok('[🗋 Sync Demo #2] cannot be found in the document tree of user "editor".'); - await t.expect(Page.treeNode.withExactText('Sync Demo #3').exists) - .ok('[🗋 Sync Demo #3] cannot be found in the document tree of user "editor".'); // // Login as "admin" again // - await switchToRole(t, adminUserOnOneDimensionTestSite); + await as(t, adminUserOnOneDimensionTestSite, async () => { + // + // Create a headline node in [🗋 Sync Demo #3] + // + await Page.goToPage('Sync Demo #3'); + await t + .switchToIframe(contentIframeSelector) + .click(Selector('.neos-contentcollection')) + .click(Selector('#neos-InlineToolbar-AddNode')) + .switchToMainWindow() + .click(Selector('button#into')) + .click(ReactSelector('NodeTypeItem').withProps({nodeType: {label: 'Headline_Test'}})) + .switchToIframe(contentIframeSelector) + .typeText(Selector('.test-headline h1'), 'Hello from Page "Sync Demo #3"!') + .wait(2000) + .switchToMainWindow(); + }); - // - // Create a headline node in [🗋 Sync Demo #3] - // - await Page.goToPage('Sync Demo #3'); - await t - .switchToIframe(contentIframeSelector) - .click(Selector('.neos-contentcollection')) - .click(Selector('#neos-InlineToolbar-AddNode')) - .switchToMainWindow() - .click(Selector('button#into')) - .click(ReactSelector('NodeTypeItem').withProps({nodeType: {label: 'Headline_Test'}})) - .switchToIframe(contentIframeSelector) - .typeText(Selector('.test-headline h1'), 'Hello from Page "Sync Demo #3"!') - .wait(2000) - .switchToMainWindow(); // // Login as "editor" again // - await switchToRole(t, editorUserOnOneDimensionTestSite); + await as(t, editorUserOnOneDimensionTestSite, async () => { + // + // Delete page [🗋 Sync Demo #1] + // + await deleteDocumentNode(t, 'Sync Demo #1'); - // - // Delete page [🗋 Sync Demo #1] - // - await Page.goToPage('Sync Demo #1'); - await t.click(Selector('#neos-PageTree-DeleteSelectedNode')); - await t.click(Selector('#neos-DeleteNodeModal-Confirm')); - await Page.waitForIframeLoading(); + // + // Publish everything + // + await PublishDropDown.publishAll(); + }); - // - // Publish everything - // - await PublishDropDown.publishAll(); // // Login as "admin" again and visit [🗋 Sync Demo #3] // + await as(t, adminUserOnOneDimensionTestSite, async () => { + await Page.goToPage('Sync Demo #3'); + + // + // Sync changes from "editor" + // + await startSynchronization(t); + await assertThatConflictResolutionHasStarted(t); + }); +} + +async function prepareDocumentConflictBetweenAdminAndEditor(t) { + await loginAsEditorOnceToInitializeAContentStreamForTheirWorkspaceIfNeeded(t); + + await as(t, adminUserOnOneDimensionTestSite, async () => { + await PublishDropDown.discardAll(); + await createDocumentNode(t, 'Home', 'into', 'This page will be deleted during sync'); + await PublishDropDown.publishAll(); + + await t + .switchToIframe(contentIframeSelector) + .click(Selector('.neos-contentcollection')) + .click(Selector('#neos-InlineToolbar-AddNode')) + .switchToMainWindow() + .click(Selector('button#into')) + .click(ReactSelector('NodeTypeItem').withProps({nodeType: {label: 'Headline_Test'}})) + .switchToIframe(contentIframeSelector) + .doubleClick(Selector('.test-headline h1')) + .typeText(Selector('.test-headline h1'), 'This change will not be published.') + .wait(2000) + .switchToMainWindow(); + }); + + await as(t, editorUserOnOneDimensionTestSite, async () => { + await t.wait(2000); + await t.eval(() => location.reload(true)); + await waitForReact(30000); + await Page.waitForIframeLoading(); + await startSynchronization(t); + await t.wait(1000); + await finishSynchronization(t); + + await t.expect(Page.treeNode.withExactText('This page will be deleted during sync').exists) + .ok('[🗋 This page will be deleted during sync] cannot be found in the document tree of user "editor".'); + + await deleteDocumentNode(t, 'This page will be deleted during sync'); + await PublishDropDown.publishAll(); + }); + await switchToRole(t, adminUserOnOneDimensionTestSite); - await Page.goToPage('Sync Demo #3'); + await Page.goToPage('This page will be deleted during sync'); +} - // - // Sync changes from "editor" - // - await startSynchronization(t); - await assertThatConflictResolutionHasStarted(t); +let editHasLoggedInAtLeastOnce = false; +async function loginAsEditorOnceToInitializeAContentStreamForTheirWorkspaceIfNeeded(t) { + if (editHasLoggedInAtLeastOnce) { + return; + } + + await as(t, editorUserOnOneDimensionTestSite, async () => { + await Page.waitForIframeLoading(); + await t.wait(2000); + editHasLoggedInAtLeastOnce = true; + }); +} + +async function as(t, role, asyncCallback) { + await switchToRole(t, role); + await asyncCallback(); } async function switchToRole(t, role) { @@ -173,6 +260,41 @@ async function switchToRole(t, role) { await Page.goToPage('Home'); } +async function createDocumentNode(t, referencePageTitle, insertMode, pageTitleToCreate) { + await Page.goToPage(referencePageTitle); + await t + .click(Selector('#neos-PageTree-AddNode')) + .click(ReactSelector('InsertModeSelector').find('#' + insertMode)) + .click(ReactSelector('NodeTypeItem').find('button>span>span').withText('Page_Test')) + .typeText(Selector('#neos-NodeCreationDialog-Body input'), pageTitleToCreate) + .click(Selector('#neos-NodeCreationDialog-CreateNew')); + await Page.waitForIframeLoading(); +} + +async function deleteDocumentNode(t, pageTitleToDelete) { + await Page.goToPage(pageTitleToDelete); + await t.click(Selector('#neos-PageTree-DeleteSelectedNode')); + await t.click(Selector('#neos-DeleteNodeModal-Confirm')); + await Page.waitForIframeLoading(); +} + +async function startPublishAll(t) { + await t.click(PublishDropDown.publishDropdown) + await t.click(PublishDropDown.publishDropdownPublishAll); + await t.click(Selector('#neos-PublishDialog-Confirm')); +} + +async function startPublishDocument(t) { + await t.click(Selector('#neos-PublishDropDown-Publish')) + await t.click(Selector('#neos-PublishDialog-Confirm')); +} + +async function finishPublish(t) { + await assertThatPublishingHasFinishedWithoutError(t); + await t.click(Selector('#neos-PublishDialog-Acknowledge')); + await t.wait(2000); +} + async function startSynchronization(t) { await t.click(Selector('#neos-workspace-rebase')); await t.click(Selector('#neos-SyncWorkspace-Confirm')); @@ -215,10 +337,7 @@ async function cancelDropConflictingChanges(t) { } async function finishSynchronization(t) { - await t.expect(Selector('#neos-SyncWorkspace-Acknowledge').exists) - .ok('Acknowledge button for "Sync Workspace" is not available.', { - timeout: 30000 - }); + await assertThatSynchronizationHasFinishedWithoutError(t); await t.click(Selector('#neos-SyncWorkspace-Acknowledge')); } @@ -229,22 +348,35 @@ async function assertThatConflictResolutionHasStarted(t) { }); } -async function assertThatSynchronizationWasSuccessful(t) { - // - // Assert that we have been redirected to the home page by checking if - // the currently focused document tree node is "Home". - // +async function assertThatSynchronizationHasFinishedWithoutError(t) { + await t.expect(Selector('#neos-SyncWorkspace-Acknowledge').exists) + .ok('Acknowledge button for "Sync Workspace" is not available.', { + timeout: 30000 + }); + await t.expect(Selector('#neos-SyncWorkspace-Retry').exists) + .notOk('An error occurred during "Sync Workspace".', { + timeout: 30000 + }); +} + +async function assertThatPublishingHasFinishedWithoutError(t) { + await t.expect(Selector('#neos-PublishDialog-Acknowledge').exists) + .ok('Acknowledge button for "Publishing" is not available.', { + timeout: 30000 + }); + await t.expect(Selector('#neos-PublishDialog-Retry').exists) + .notOk('An error occurred during "Publishing".', { + timeout: 30000 + }); +} + +async function assertThatWeAreOnPage(t, pageTitle) { await t .expect(Selector('[role="treeitem"] [role="button"][class*="isFocused"]').textContent) - .eql('Home'); + .eql(pageTitle); +} - // - // Assert that all 3 documents are not visible anymore in the document tree - // - await t.expect(Page.treeNode.withExactText('Sync Demo #1').exists) - .notOk('[🗋 Sync Demo #1] can still be found in the document tree of user "admin".'); - await t.expect(Page.treeNode.withExactText('Sync Demo #2').exists) - .notOk('[🗋 Sync Demo #2] can still be found in the document tree of user "admin".'); - await t.expect(Page.treeNode.withExactText('Sync Demo #3').exists) - .notOk('[🗋 Sync Demo #3] can still be found in the document tree of user "admin".'); +async function assertThatWeCannotSeePageInTree(t, pageTitle) { + await t.expect(Page.treeNode.withExactText(pageTitle).exists) + .notOk(`[🗋 ${pageTitle}] can still be found in the document tree of user "admin".`); } diff --git a/packages/neos-ui-sagas/src/Publish/index.ts b/packages/neos-ui-sagas/src/Publish/index.ts index ceb0673d5e..df2c098ee4 100644 --- a/packages/neos-ui-sagas/src/Publish/index.ts +++ b/packages/neos-ui-sagas/src/Publish/index.ts @@ -108,7 +108,23 @@ export function * watchPublishing({routes}: {routes: Routes}) { if (conflictsWereResolved) { yield put(actions.CR.Publishing.resolveConflicts()); - yield * attemptToPublishOrDiscard(); + + // + // It may happen that after conflicts are resolved, the + // document we're trying to publish no longer exists. + // + // We need to finish the publishing operation in this + // case, otherwise it'll lead to an error. + // + const publishingShouldContinue = scope === PublishingScope.DOCUMENT + ? Boolean(yield select(selectors.CR.Nodes.byContextPathSelector(ancestorId))) + : true; + + if (publishingShouldContinue) { + yield * attemptToPublishOrDiscard(); + } else { + yield put(actions.CR.Publishing.succeed(0)); + } } else { yield put(actions.CR.Publishing.cancel()); yield call(updateWorkspaceInfo);