From 3ce07be46bb57f2d55a44316c039d0317d3d3f5e Mon Sep 17 00:00:00 2001 From: Benjamin Frueh Date: Thu, 25 Sep 2025 17:59:23 +0200 Subject: [PATCH 1/6] feat: Save a checksum for documents and use it to detect conflicts Signed-off-by: Benjamin Frueh --- composer/composer/autoload_classmap.php | 1 + composer/composer/autoload_static.php | 1 + lib/Db/Document.php | 7 +++- .../Version070000Date20250925110024.php | 32 +++++++++++++++ lib/Service/DocumentService.php | 41 +++++++++++++++---- 5 files changed, 73 insertions(+), 9 deletions(-) create mode 100644 lib/Migration/Version070000Date20250925110024.php diff --git a/composer/composer/autoload_classmap.php b/composer/composer/autoload_classmap.php index a05cc44b183..daa77de7338 100644 --- a/composer/composer/autoload_classmap.php +++ b/composer/composer/autoload_classmap.php @@ -62,6 +62,7 @@ 'OCA\\Text\\Migration\\Version030701Date20230207131313' => $baseDir . '/../lib/Migration/Version030701Date20230207131313.php', 'OCA\\Text\\Migration\\Version030901Date20231114150437' => $baseDir . '/../lib/Migration/Version030901Date20231114150437.php', 'OCA\\Text\\Migration\\Version040100Date20240611165300' => $baseDir . '/../lib/Migration/Version040100Date20240611165300.php', + 'OCA\\Text\\Migration\\Version070000Date20250925110024' => $baseDir . '/../lib/Migration/Version070000Date20250925110024.php', 'OCA\\Text\\Notification\\Notifier' => $baseDir . '/../lib/Notification/Notifier.php', 'OCA\\Text\\Service\\ApiService' => $baseDir . '/../lib/Service/ApiService.php', 'OCA\\Text\\Service\\AttachmentService' => $baseDir . '/../lib/Service/AttachmentService.php', diff --git a/composer/composer/autoload_static.php b/composer/composer/autoload_static.php index 76acef1e6ae..a71fb7b3fbc 100644 --- a/composer/composer/autoload_static.php +++ b/composer/composer/autoload_static.php @@ -77,6 +77,7 @@ class ComposerStaticInitText 'OCA\\Text\\Migration\\Version030701Date20230207131313' => __DIR__ . '/..' . '/../lib/Migration/Version030701Date20230207131313.php', 'OCA\\Text\\Migration\\Version030901Date20231114150437' => __DIR__ . '/..' . '/../lib/Migration/Version030901Date20231114150437.php', 'OCA\\Text\\Migration\\Version040100Date20240611165300' => __DIR__ . '/..' . '/../lib/Migration/Version040100Date20240611165300.php', + 'OCA\\Text\\Migration\\Version070000Date20250925110024' => __DIR__ . '/..' . '/../lib/Migration/Version070000Date20250925110024.php', 'OCA\\Text\\Notification\\Notifier' => __DIR__ . '/..' . '/../lib/Notification/Notifier.php', 'OCA\\Text\\Service\\ApiService' => __DIR__ . '/..' . '/../lib/Service/ApiService.php', 'OCA\\Text\\Service\\AttachmentService' => __DIR__ . '/..' . '/../lib/Service/AttachmentService.php', diff --git a/lib/Db/Document.php b/lib/Db/Document.php index b3dff8656f5..dd188a22d37 100644 --- a/lib/Db/Document.php +++ b/lib/Db/Document.php @@ -23,6 +23,8 @@ * @method setLastSavedVersionEtag(string $etag): void * @method getBaseVersionEtag(): string * @method setBaseVersionEtag(string $etag): void + * @method getChecksum(): ?string + * @method setChecksum(?string $checksum): void */ class Document extends Entity implements \JsonSerializable { public $id = null; @@ -33,6 +35,7 @@ class Document extends Entity implements \JsonSerializable { protected int $lastSavedVersionTime = 0; protected string $lastSavedVersionEtag = ''; protected string $baseVersionEtag = ''; + protected ?string $checksum = null; public function __construct() { $this->addType('id', 'integer'); @@ -40,6 +43,7 @@ public function __construct() { $this->addType('lastSavedVersion', 'integer'); $this->addType('lastSavedVersionTime', 'integer'); $this->addType('initialVersion', 'integer'); + $this->addType('checksum', 'string'); } public function jsonSerialize(): array { @@ -48,7 +52,8 @@ public function jsonSerialize(): array { 'lastSavedVersion' => $this->lastSavedVersion, 'lastSavedVersionTime' => $this->lastSavedVersionTime, 'baseVersionEtag' => $this->baseVersionEtag, - 'initialVersion' => $this->initialVersion + 'initialVersion' => $this->initialVersion, + 'checksum' => $this->checksum ]; } } diff --git a/lib/Migration/Version070000Date20250925110024.php b/lib/Migration/Version070000Date20250925110024.php new file mode 100644 index 00000000000..82eed759869 --- /dev/null +++ b/lib/Migration/Version070000Date20250925110024.php @@ -0,0 +1,32 @@ +getTable('text_documents'); + if (!$table->hasColumn('checksum')) { + $table->addColumn('checksum', Types::STRING, [ + 'notnull' => false, + 'length' => 8, + ]); + return $schema; + } + + return null; + } +} diff --git a/lib/Service/DocumentService.php b/lib/Service/DocumentService.php index e37dfd70dca..bf142e979cd 100644 --- a/lib/Service/DocumentService.php +++ b/lib/Service/DocumentService.php @@ -143,6 +143,7 @@ public function createDocument(File $file): Document { $document->setLastSavedVersionTime($file->getMTime()); $document->setLastSavedVersionEtag($file->getEtag()); $document->setBaseVersionEtag(uniqid()); + $document->setChecksum($this->computeCheckSum($file->getContent())); try { /** @var Document $document */ $document = $this->documentMapper->insert($document); @@ -310,6 +311,8 @@ public function getSteps(int $documentId, int $lastVersion): array { return $this->stepMapper->find($documentId, $lastVersion); } + + /** * @throws DocumentSaveConflictException * @throws InvalidPathException @@ -317,18 +320,39 @@ public function getSteps(int $documentId, int $lastVersion): array { */ public function assertNoOutsideConflict(Document $document, File $file, bool $force = false, ?string $shareToken = null): void { $documentId = $document->getId(); - $savedEtag = $file->getEtag(); $lastMTime = $document->getLastSavedVersionTime(); + $lastEtag = $document->getLastSavedVersionEtag(); + + if ($lastMTime <= 0 || $force || $this->isReadOnly($file, $shareToken) || $this->cache->get('document-save-lock-' . $documentId)) { + return; + } + + $fileMtime = $file->getMtime(); + $fileEtag = $file->getEtag(); + + if ($lastEtag === $fileEtag && $lastMTime === $fileMtime) { + return; + } + + $storedChecksum = $document->getChecksum(); + $fileContent = $file->getContent(); + $fileChecksum = $this->computeChecksum($fileContent); - if ($lastMTime > 0 - && $force === false - && !$this->isReadOnly($file, $shareToken) - && $savedEtag !== $document->getLastSavedVersionEtag() - && $lastMTime !== $file->getMtime() - && !$this->cache->get('document-save-lock-' . $documentId) - ) { + if ($storedChecksum !== $fileChecksum) { throw new DocumentSaveConflictException('File changed in the meantime from outside'); } + + $document->setLastSavedVersionTime($fileMtime); + $document->setLastSavedVersionEtag($fileEtag); + $this->documentMapper->update($document); + } + + /** + * @param string $content + * @return string + */ + private function computeCheckSum(string $content): string { + return hash('crc32', $content); } /** @@ -414,6 +438,7 @@ public function autosave(Document $document, ?File $file, int $version, ?string $document->setLastSavedVersion($version); $document->setLastSavedVersionTime($file->getMTime()); $document->setLastSavedVersionEtag($file->getEtag()); + $document->setChecksum($this->computeCheckSum($autoSaveDocument)); $this->documentMapper->update($document); } catch (LockedException $e) { // Ignore lock since it might occur when multiple people save at the same time From 103bf7e2b70bfdc835005c2998b798d24061ae8f Mon Sep 17 00:00:00 2001 From: Benjamin Frueh Date: Thu, 25 Sep 2025 18:16:07 +0200 Subject: [PATCH 2/6] chore: Add SPDX information Signed-off-by: Benjamin Frueh --- lib/Migration/Version070000Date20250925110024.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/Migration/Version070000Date20250925110024.php b/lib/Migration/Version070000Date20250925110024.php index 82eed759869..c48856c9d26 100644 --- a/lib/Migration/Version070000Date20250925110024.php +++ b/lib/Migration/Version070000Date20250925110024.php @@ -2,6 +2,11 @@ declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + namespace OCA\Text\Migration; use Closure; From 5984900722e0fbc100fd9504ae8741e9fed022eb Mon Sep 17 00:00:00 2001 From: Benjamin Frueh Date: Sun, 28 Sep 2025 14:31:48 +0200 Subject: [PATCH 3/6] test: fix conflict spec to cover conflict and non-conflict uploads Signed-off-by: Benjamin Frueh --- cypress/e2e/conflict.spec.js | 27 ++++++++++++++++++++++----- cypress/fixtures/edited-lines.txt | 9 +++++++++ cypress/fixtures/edited-test.md | 2 ++ 3 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 cypress/fixtures/edited-lines.txt create mode 100644 cypress/fixtures/edited-test.md diff --git a/cypress/e2e/conflict.spec.js b/cypress/e2e/conflict.spec.js index f6b430b5531..81596bbddf3 100644 --- a/cypress/e2e/conflict.spec.js +++ b/cypress/e2e/conflict.spec.js @@ -59,8 +59,12 @@ variants.forEach(function ({ fixture, mime }) { 'contain', 'The file was overwritten.', ) - getWrapper().find('#read-only-editor').should('contain', 'Hello world') - getWrapper().find('.text-editor__main').should('contain', 'Hello world') + getWrapper().find('#read-only-editor').should('contain', 'edited') + getWrapper() + .find('#read-only-editor') + .should('not.contain', 'cruel conflicting') + + getWrapper().find('.text-editor__main').should('contain', 'edited') getWrapper() .find('.text-editor__main') .should('contain', 'cruel conflicting') @@ -78,7 +82,7 @@ variants.forEach(function ({ fixture, mime }) { getWrapper().should('not.exist') cy.get('[data-cy="resolveThisVersion"]').should('not.exist') - cy.getContent().should('contain', 'Hello world') + cy.getContent().should('contain', 'edited') cy.getContent().should('contain', 'cruel conflicting') }, ) @@ -92,7 +96,7 @@ variants.forEach(function ({ fixture, mime }) { getWrapper().should('not.exist') cy.get('[data-cy="resolveThisVersion"]').should('not.exist') cy.get('[data-cy="resolveServerVersion"]').should('not.exist') - cy.getContent().should('contain', 'Hello world') + cy.getContent().should('contain', 'edited') cy.getContent().should('not.contain', 'cruel conflicting') }) @@ -107,6 +111,19 @@ variants.forEach(function ({ fixture, mime }) { cy.getContent().should('contain', 'cruel conflicting') getWrapper().should('not.exist') }) + + it.only(prefix + ': no conflict when uploading same file content', function () { + cy.testName().then((testName) => { + cy.uploadFile(fileName, mime, `${testName}/${fileName}`) + cy.visitTestFolder() + cy.openFile(fileName) + cy.uploadFile(fileName, mime, `${testName}/${fileName}`) + + cy.get('.text-editor .document-status').should('not.exist') + getWrapper().should('not.exist') + cy.getContent().should('not.contain', 'edited') + }) + }) }) }) @@ -156,7 +173,7 @@ function createConflict(fileName, mime) { .should('have.attr', 'contenteditable', 'true') cy.getContent().type('Hello you cruel conflicting world') cy.testName().then((testName) => { - cy.uploadFile(fileName, mime, testName + '/' + fileName) + cy.uploadFile('edited-' + fileName, mime, testName + '/' + fileName) }) cy.get('#viewer .modal-header button.header-close').click() cy.get('#viewer').should('not.exist') diff --git a/cypress/fixtures/edited-lines.txt b/cypress/fixtures/edited-lines.txt new file mode 100644 index 00000000000..42e4ff329a7 --- /dev/null +++ b/cypress/fixtures/edited-lines.txt @@ -0,0 +1,9 @@ +This file contains multiple lines + +Hello world + +It's a text file so it should not be parsed as markdown + +But when it is these would turn into paragraphs. + +edited \ No newline at end of file diff --git a/cypress/fixtures/edited-test.md b/cypress/fixtures/edited-test.md new file mode 100644 index 00000000000..a90390b9c4c --- /dev/null +++ b/cypress/fixtures/edited-test.md @@ -0,0 +1,2 @@ +## Hello world +edited From 35dd13b08f8e1fe591a5c4701b49bf30c70e700a Mon Sep 17 00:00:00 2001 From: Benjamin Frueh Date: Sun, 28 Sep 2025 14:40:34 +0200 Subject: [PATCH 4/6] chore: apply prettier formatting Signed-off-by: Benjamin Frueh --- cypress/e2e/conflict.spec.js | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/cypress/e2e/conflict.spec.js b/cypress/e2e/conflict.spec.js index 81596bbddf3..976165c0ada 100644 --- a/cypress/e2e/conflict.spec.js +++ b/cypress/e2e/conflict.spec.js @@ -112,18 +112,21 @@ variants.forEach(function ({ fixture, mime }) { getWrapper().should('not.exist') }) - it.only(prefix + ': no conflict when uploading same file content', function () { - cy.testName().then((testName) => { - cy.uploadFile(fileName, mime, `${testName}/${fileName}`) - cy.visitTestFolder() - cy.openFile(fileName) - cy.uploadFile(fileName, mime, `${testName}/${fileName}`) - - cy.get('.text-editor .document-status').should('not.exist') - getWrapper().should('not.exist') - cy.getContent().should('not.contain', 'edited') - }) - }) + it.only( + prefix + ': no conflict when uploading same file content', + function () { + cy.testName().then((testName) => { + cy.uploadFile(fileName, mime, `${testName}/${fileName}`) + cy.visitTestFolder() + cy.openFile(fileName) + cy.uploadFile(fileName, mime, `${testName}/${fileName}`) + + cy.get('.text-editor .document-status').should('not.exist') + getWrapper().should('not.exist') + cy.getContent().should('not.contain', 'edited') + }) + }, + ) }) }) From fc059b825622658c7726124cea707fcafb818f9b Mon Sep 17 00:00:00 2001 From: Benjamin Frueh Date: Sun, 28 Sep 2025 15:40:55 +0200 Subject: [PATCH 5/6] test: fix scroll behaviour test and remove .only Signed-off-by: Benjamin Frueh --- cypress/e2e/conflict.spec.js | 27 ++++++++++++--------------- cypress/fixtures/edited-long.md | 23 +++++++++++++++++++++++ 2 files changed, 35 insertions(+), 15 deletions(-) create mode 100644 cypress/fixtures/edited-long.md diff --git a/cypress/e2e/conflict.spec.js b/cypress/e2e/conflict.spec.js index 976165c0ada..c9836083897 100644 --- a/cypress/e2e/conflict.spec.js +++ b/cypress/e2e/conflict.spec.js @@ -112,21 +112,18 @@ variants.forEach(function ({ fixture, mime }) { getWrapper().should('not.exist') }) - it.only( - prefix + ': no conflict when uploading same file content', - function () { - cy.testName().then((testName) => { - cy.uploadFile(fileName, mime, `${testName}/${fileName}`) - cy.visitTestFolder() - cy.openFile(fileName) - cy.uploadFile(fileName, mime, `${testName}/${fileName}`) - - cy.get('.text-editor .document-status').should('not.exist') - getWrapper().should('not.exist') - cy.getContent().should('not.contain', 'edited') - }) - }, - ) + it(prefix + ': no conflict when uploading same file content', function () { + cy.testName().then((testName) => { + cy.uploadFile(fileName, mime, `${testName}/${fileName}`) + cy.visitTestFolder() + cy.openFile(fileName) + cy.uploadFile(fileName, mime, `${testName}/${fileName}`) + + cy.get('.text-editor .document-status').should('not.exist') + getWrapper().should('not.exist') + cy.getContent().should('not.contain', 'edited') + }) + }) }) }) diff --git a/cypress/fixtures/edited-long.md b/cypress/fixtures/edited-long.md new file mode 100644 index 00000000000..251ee450f22 --- /dev/null +++ b/cypress/fixtures/edited-long.md @@ -0,0 +1,23 @@ +# Hello world + +## First subheading + +Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos. + +Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos. + +Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos. + +## Second subheading + +Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos. + +Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos. + +Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos. + +## Third subheading + +Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos. + +edited \ No newline at end of file From 295c61f6a9cdd82415bab5686addb0405b8ecb76 Mon Sep 17 00:00:00 2001 From: Benjamin Frueh Date: Tue, 30 Sep 2025 14:33:25 +0200 Subject: [PATCH 6/6] test: update cypress test for conflicts Signed-off-by: Benjamin Frueh --- cypress/e2e/conflict.spec.js | 48 ++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/cypress/e2e/conflict.spec.js b/cypress/e2e/conflict.spec.js index c9836083897..da4b2199d6c 100644 --- a/cypress/e2e/conflict.spec.js +++ b/cypress/e2e/conflict.spec.js @@ -51,7 +51,7 @@ variants.forEach(function ({ fixture, mime }) { }) it(prefix + ': displays conflicts', function () { - createConflict(fileName, mime) + createConflict(fileName, 'edited-' + fileName, mime) cy.openFile(fileName) @@ -59,12 +59,12 @@ variants.forEach(function ({ fixture, mime }) { 'contain', 'The file was overwritten.', ) - getWrapper().find('#read-only-editor').should('contain', 'edited') + getWrapper().find('#read-only-editor').should('contain', 'Hello world') getWrapper() .find('#read-only-editor') .should('not.contain', 'cruel conflicting') - getWrapper().find('.text-editor__main').should('contain', 'edited') + getWrapper().find('.text-editor__main').should('contain', 'Hello world') getWrapper() .find('.text-editor__main') .should('contain', 'cruel conflicting') @@ -73,7 +73,7 @@ variants.forEach(function ({ fixture, mime }) { it( prefix + ': resolves conflict using current editing session', function () { - createConflict(fileName, mime) + createConflict(fileName, 'edited-' + fileName, mime) cy.openFile(fileName) cy.intercept({ method: 'POST', url: '**/session/*/push' }).as('push') @@ -82,13 +82,12 @@ variants.forEach(function ({ fixture, mime }) { getWrapper().should('not.exist') cy.get('[data-cy="resolveThisVersion"]').should('not.exist') - cy.getContent().should('contain', 'edited') cy.getContent().should('contain', 'cruel conflicting') }, ) it(prefix + ': resolves conflict using server version', function () { - createConflict(fileName, mime) + createConflict(fileName, 'edited-' + fileName, mime) cy.openFile(fileName) cy.get('[data-cy="resolveServerVersion"]').click() @@ -96,12 +95,12 @@ variants.forEach(function ({ fixture, mime }) { getWrapper().should('not.exist') cy.get('[data-cy="resolveThisVersion"]').should('not.exist') cy.get('[data-cy="resolveServerVersion"]').should('not.exist') - cy.getContent().should('contain', 'edited') + cy.getContent().should('contain', 'Hello world') cy.getContent().should('not.contain', 'cruel conflicting') }) it(prefix + ': hides conflict in read only session', function () { - createConflict(fileName, mime) + createConflict(fileName, 'edited-' + fileName, mime) cy.testName().then((testName) => { cy.shareFile(`/${testName}/${fileName}`).then((token) => { cy.logout() @@ -113,16 +112,10 @@ variants.forEach(function ({ fixture, mime }) { }) it(prefix + ': no conflict when uploading same file content', function () { - cy.testName().then((testName) => { - cy.uploadFile(fileName, mime, `${testName}/${fileName}`) - cy.visitTestFolder() - cy.openFile(fileName) - cy.uploadFile(fileName, mime, `${testName}/${fileName}`) - - cy.get('.text-editor .document-status').should('not.exist') - getWrapper().should('not.exist') - cy.getContent().should('not.contain', 'edited') - }) + createConflict(fileName, fileName, mime) + cy.openFile(fileName) + cy.getContent().should('contain', 'Hello world') + getWrapper().should('not.exist') }) }) }) @@ -139,7 +132,7 @@ describe('conflict dialog scroll behaviour', function () { cy.login(user) cy.createTestFolder() - createConflict(fileName, 'text/markdown') + createConflict(fileName, 'edited-' + fileName, 'text/markdown') cy.openFile(fileName) @@ -158,23 +151,30 @@ describe('conflict dialog scroll behaviour', function () { }) /** - * @param {string} fileName - filename + * @param {string} fileName1 - filename1 + * @param {string} fileName2 - filename2 * @param {string} mime - mimetype */ -function createConflict(fileName, mime) { +function createConflict(fileName1, fileName2, mime) { cy.testName().then((testName) => { - cy.uploadFile(fileName, mime, `${testName}/${fileName}`) + cy.uploadFile(fileName1, mime, `${testName}/${fileName1}`) }) cy.visitTestFolder() - cy.openFile(fileName) + cy.openFile(fileName1) cy.log('Inspect editor') cy.getEditor() .find('.ProseMirror') .should('have.attr', 'contenteditable', 'true') + cy.getContent().type('Hello you cruel conflicting world') + cy.testName().then((testName) => { - cy.uploadFile('edited-' + fileName, mime, testName + '/' + fileName) + cy.uploadFile(fileName2, mime, testName + '/' + fileName1) }) + + cy.intercept('POST', '**/session/*/sync').as('sync') + cy.wait('@sync', { timeout: 10000 }) + cy.get('#viewer .modal-header button.header-close').click() cy.get('#viewer').should('not.exist') }