From d0200ce355c937d3fd65c8129f48a90e5336779b 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 e57b29dc9c6..d7550e08853 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($stepsVersion); $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 0cccac697777f5d6b535f3c5871acaa071dc76a4 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 9bc8a72d0559c42432fe0d47e0aa6ff2b3cc8720 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 297e69bd518ded753f8fa702d01eb0ff8fd60711 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 e5ec03fce81e8851cd5d6bda4ce5eca780f96318 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 c4d8dfd7b6aa8c935f213c671fc46d21862f5527 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') }