diff --git a/blocks/edit/edit.js b/blocks/edit/edit.js index fdfb0688..fa4da365 100644 --- a/blocks/edit/edit.js +++ b/blocks/edit/edit.js @@ -6,7 +6,7 @@ let wsProvider; let startPreviewing; let stopPreviewing; -async function setUI(el, utils) { +async function setUI(el, utils, guid) { const details = getPathDetails(); if (!details) return; @@ -36,7 +36,7 @@ async function setUI(el, utils) { } const { daFetch } = await utils; - const { permissions } = await daFetch(details.sourceUrl, { method: 'HEAD' }); + const { permissions, headers } = await daFetch(details.sourceUrl, { method: 'HEAD' }); daTitle.permissions = permissions; daContent.permissions = permissions; @@ -45,12 +45,17 @@ async function setUI(el, utils) { daContent.wsProvider = undefined; } + const docGUID = guid ?? headers?.get('x-da-id'); ({ proseEl, wsProvider, startPreviewing, stopPreviewing, - } = prose.default({ path: details.sourceUrl, permissions })); + } = prose.default({ + path: details.sourceUrl, + permissions, + docGUID, + }, (updatedGuid) => setUI(el, utils, updatedGuid))); daContent.proseEl = proseEl; daContent.wsProvider = wsProvider; diff --git a/blocks/edit/prose/index.js b/blocks/edit/prose/index.js index 2a4d3601..6a31a6af 100644 --- a/blocks/edit/prose/index.js +++ b/blocks/edit/prose/index.js @@ -196,7 +196,13 @@ function addSyncedListener(wsProvider, canWrite) { wsProvider.on('synced', handleSynced); } -export default function initProse({ path, permissions }) { +function getNewestGuid(guidArray) { + const guids = [...guidArray]; + guids.sort((a, b) => a.ts - b.ts); + return guids.pop(); +} + +export default function initProse({ path, permissions, docGUID }, resetFunc) { // Destroy ProseMirror if it already exists - GH-212 if (window.view) delete window.view; const editor = document.createElement('div'); @@ -223,7 +229,33 @@ export default function initProse({ path, permissions }) { createAwarenessStatusWidget(wsProvider, window); registerErrorHandler(ydoc); - const yXmlFragment = ydoc.getXmlFragment('prosemirror'); + const guidArray = ydoc.getArray('prosemirror-guids'); + let curGuid; + if (docGUID) { + curGuid = docGUID; + + if (getNewestGuid(guidArray) !== curGuid) { + guidArray.push([{ ts: Date.now(), guid: curGuid }]); + } + } else { + curGuid = crypto.randomUUID(); + guidArray.push([{ ts: Date.now(), guid: curGuid, newDoc: true }]); + } + + ydoc.on('update', () => { + // If the document has been replaced by a another document (it has been first deleted + // and then a new document has been created), reset the window to connect to the new doc. + const guids = [...guidArray]; + if (guids.length === 0) { + return; + } + const latestGuid = getNewestGuid(guids); + if (latestGuid.guid !== curGuid) { + resetFunc(latestGuid.guid); + } + }); + + const yXmlFragment = ydoc.getXmlFragment(`prosemirror-${curGuid}`); if (window.adobeIMS?.isSignedInUser()) { window.adobeIMS.getProfile().then( diff --git a/test/e2e/tests/browse.spec.js b/test/e2e/tests/browse.spec.js index 05d64510..809232c1 100644 --- a/test/e2e/tests/browse.spec.js +++ b/test/e2e/tests/browse.spec.js @@ -1,8 +1,10 @@ import { test, expect } from '@playwright/test'; import ENV from '../utils/env.js'; +import { getQuery } from '../utils/page.js'; + test('Get Main Page', async ({ page }) => { - await page.goto(ENV); + await page.goto(`${ENV}/${getQuery()}`); const html = await page.content(); expect(html).toContain('Browse - DA'); diff --git a/test/e2e/tests/copy_rename.spec.js b/test/e2e/tests/copy_rename.spec.js index 7625c5d8..0bc1a027 100644 --- a/test/e2e/tests/copy_rename.spec.js +++ b/test/e2e/tests/copy_rename.spec.js @@ -1,5 +1,5 @@ /* - * Copyright 2024 Adobe. All rights reserved. + * Copyright 2025 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 @@ -16,7 +16,7 @@ import { getQuery, getTestFolderURL, getTestPageURL } from '../utils/page.js'; test('Copy and Rename with Versioned document', async ({ page }, workerInfo) => { // This test has a fairly high timeout because it waits for the document to be saved // a number of times - test.setTimeout(60000); + test.setTimeout(1200000); const pageURL = getTestPageURL('copyrename', workerInfo); const orgPageName = pageURL.split('/').pop(); @@ -49,6 +49,7 @@ test('Copy and Rename with Versioned document', async ({ page }, workerInfo) => // Go back to the directory view await page.goto(`${ENV}/${getQuery()}#/da-sites/da-status/tests`); + // Copy the document const copyFolderURL = getTestFolderURL('copy', workerInfo); const copyFolderName = copyFolderURL.split('/').pop(); await page.getByRole('button', { name: 'New' }).click(); @@ -65,6 +66,7 @@ test('Copy and Rename with Versioned document', async ({ page }, workerInfo) => await page.getByRole('link', { name: copyFolderName }).click(); await page.waitForURL(`**/da-sites/da-status/tests/${copyFolderName}`); + // Paste it in the new folder await page.getByRole('button', { name: 'Paste' }).click(); await page.waitForTimeout(3000); /* TODO REMOVE once #233 is fixed */ await page.reload(); @@ -72,7 +74,7 @@ test('Copy and Rename with Versioned document', async ({ page }, workerInfo) => const href = await link.getAttribute('href'); await expect(href).toEqual(`/edit#/da-sites/da-status/tests/${copyFolderName}/${orgPageName}`); - // go back to the original to rename it + // Go back to the original to rename it // Go to the directory view await page.goto(`${ENV}/${getQuery()}#/da-sites/da-status/tests`); await page.reload(); // Clears any leftover selection, if any @@ -92,8 +94,18 @@ test('Copy and Rename with Versioned document', async ({ page }, workerInfo) => await page.waitForTimeout(3000); await page.goto(`${pageURL}ren`); + // Ensure it has the latest text await page.waitForTimeout(3000); await expect(page.locator('div.ProseMirror')).toContainText('After versioned'); + + // Make some edits, ensure that they work + await page.locator('div.ProseMirror').fill('Now its renamed'); + await page.waitForTimeout(3000); + await page.reload(); + await page.waitForTimeout(3000); + await expect(page.locator('div.ProseMirror')).toContainText('Now its renamed'); + + // Restore a previous version await page.getByRole('button', { name: 'Versions' }).click(); await page.getByText('myver', { exact: false }).click(); await page.locator('li').filter({ hasText: 'myver' }).getByRole('button').click(); @@ -102,6 +114,13 @@ test('Copy and Rename with Versioned document', async ({ page }, workerInfo) => // Ensure that the original text is still there await expect(page.locator('div.ProseMirror')).toContainText('Versioned text'); + // Make some further edits + await page.locator('div.ProseMirror').fill('Renamed again'); + await page.waitForTimeout(3000); + await page.reload(); + await page.waitForTimeout(3000); + await expect(page.locator('div.ProseMirror')).toContainText('Renamed again'); + // now go to the copy await page.goto(`${ENV}/edit${getQuery()}#/da-sites/da-status/tests/${copyFolderName}/${orgPageName}`); await page.reload(); // Resets the versions view, shouldn't be needed TODO @@ -109,4 +128,11 @@ test('Copy and Rename with Versioned document', async ({ page }, workerInfo) => await page.getByRole('button', { name: 'Versions' }).click(); await expect(page.getByText('Now')).toBeVisible(); await expect(page.getByText('myver')).not.toBeVisible(); // The version shouldn't be there on the copy + + // Make some edits on the copy + await page.locator('div.ProseMirror').fill('Renamed copy'); + await page.waitForTimeout(3000); + await page.reload(); + await page.waitForTimeout(3000); + await expect(page.locator('div.ProseMirror')).toContainText('Renamed copy'); }); diff --git a/test/e2e/tests/delete.spec.js b/test/e2e/tests/delete.spec.js index 9a9359b6..d0082815 100644 --- a/test/e2e/tests/delete.spec.js +++ b/test/e2e/tests/delete.spec.js @@ -100,6 +100,9 @@ test('Empty out open editors on deleted documents', async ({ browser, page }, wo const enteredText = `Some content entered at ${new Date()}`; await page.locator('div.ProseMirror').fill(enteredText); + // Wait for the content to be stored in the backend + await page.waitForTimeout(3000); + // Create a second window on the same document const page2 = await browser.newPage(); await page2.goto(url); @@ -131,8 +134,16 @@ test('Empty out open editors on deleted documents', async ({ browser, page }, wo await list.locator('sl-button.negative').locator('visible=true').click(); // Give the second window a chance to update itself - await list.waitForTimeout(10000); + await page2.waitForTimeout(3000); // The open window should be cleared out now await expect(page2.locator('div.ProseMirror')).not.toContainText(enteredText); + + // Add some text to the second window with the stale document, it should not be saved + await page2.locator('div.ProseMirror').fill('Some new content'); + await page2.waitForTimeout(5000); + + list.reload(); + // The document should still not be in the list, even though edits were made on the stale doc + await expect(list.locator(`a[href="/edit#/da-sites/da-status/tests/${pageName}"]`)).not.toBeVisible(); }); diff --git a/test/e2e/utils/env.js b/test/e2e/utils/env.js index 0662164f..90f92585 100644 --- a/test/e2e/utils/env.js +++ b/test/e2e/utils/env.js @@ -7,7 +7,7 @@ function getEnv() { if (!owner) { owner = 'adobe'; } - if (branch === 'local') { + if (branch === 'local' || branch === 'localstg') { return 'http://localhost:3000'; } if (branch === 'local-https') { diff --git a/test/e2e/utils/page.js b/test/e2e/utils/page.js index c6ac0e79..94bb38a3 100644 --- a/test/e2e/utils/page.js +++ b/test/e2e/utils/page.js @@ -16,6 +16,9 @@ export function getQuery() { if (branch === 'local') { return '?da-admin=local&da-collab=local'; } + if (branch === 'localstg') { + return '?da-admin=stage&da-collab=stage'; + } return ''; }