import {DocCreationInfo} from 'app/common/DocListAPI'; import {UserAPI} from 'app/common/UserAPI'; import {assert, driver, Key} from 'mocha-webdriver'; import * as gu from 'test/nbrowser/gristUtils'; import {server, setupTestSuite} from 'test/nbrowser/testUtils'; import uuidv4 from "uuid/v4"; describe("Fork", function() { // this is a relatively slow test in staging. this.timeout(40000); const cleanup = setupTestSuite(); let doc: DocCreationInfo; let api: UserAPI; let personal: gu.Session; let team: gu.Session; before(async function() { personal = gu.session().personalSite; team = gu.session().teamSite; }); async function makeDocIfAbsent() { if (doc && doc.id) { return; } doc = await team.tempDoc(cleanup, 'Hello.grist', {load: false}); } async function getForks(api: UserAPI) { const wss = await api.getOrgWorkspaces('current'); const forks = wss .find(ws => ws.name === team.workspace) ?.docs .find(d => d.id === doc.id) ?.forks; return forks ?? []; } async function testForksOfForks(isLoggedIn: boolean = true) { const session = isLoggedIn ? await team.login() : await team.anon.login(); await session.loadDoc(`/doc/${doc.id}/m/fork`); await gu.getCell({rowNum: 1, col: 0}).click(); await gu.enterCell('1'); await gu.waitForServer(); assert.equal(await gu.getCell({rowNum: 1, col: 0}).getText(), '1'); const forkUrl = await driver.getCurrentUrl(); assert.match(forkUrl, isLoggedIn ? /^[^~]*~[^~]+~\d+$/ : /^[^~]*~[^~]*$/); await session.loadDoc((new URL(forkUrl)).pathname + '/m/fork'); await gu.getCell({rowNum: 1, col: 0}).click(); await gu.enterCell('2'); await gu.waitForServer(); await driver.findContentWait( '.test-notifier-toast-wrapper', /Cannot fork a document that's already a fork.*Report a problem/s, 2000 ); assert.equal(await gu.getCell({rowNum: 1, col: 0}).getText(), '1'); assert.equal(await driver.getCurrentUrl(), `${forkUrl}/m/fork`); await gu.wipeToasts(); } // Run tests with both regular docId and a custom urlId in URL, to make sure // ids are kept straight during forking. for (const idType of ['urlId', 'docId'] as Array<'docId'|'urlId'>) { describe(`with ${idType} in url`, function() { before(async function() { // Chimpy imports a document await team.login(); await makeDocIfAbsent(); // Chimpy invites anon to view this document as a viewer, and charon as an owner api = team.createHomeApi(); const user2 = gu.session().user('user2').email; const user3 = gu.session().user('user3').email; await api.updateDocPermissions(doc.id, {users: {'anon@getgrist.com': 'viewers', [user3]: 'viewers', [user2]: 'owners'}}); // Optionally set a urlId if (idType === 'urlId') { await api.updateDoc(doc.id, {urlId: 'doc-ula'}); } }); afterEach(() => gu.checkForErrors()); for (const mode of ['anonymous', 'logged in']) { for (const content of ['empty', 'imported']) { it(`can create an ${content} unsaved document when ${mode}`, async function() { let name: string; if (mode === 'anonymous') { name = '@Guest'; await personal.anon.login(); } else { name = `@${personal.name}`; await personal.login(); } const anonApi = personal.anon.createHomeApi(); const activeApi = (mode === 'anonymous') ? anonApi : api; const id = await (content === 'empty' ? activeApi.newUnsavedDoc() : activeApi.importUnsavedDoc(Buffer.from('A,B\n999,2\n'), {filename: 'foo.csv'})); await personal.loadDoc(`/doc/${id}`); await gu.dismissWelcomeTourIfNeeded(); // check that the tag is there assert.equal(await driver.find('.test-unsaved-tag').isPresent(), true); // check that the org name area is showing the user (not @Support). assert.equal(await driver.find('.test-dm-org').getText(), name); if (content === 'imported') { assert.equal(await gu.getCell({rowNum: 1, col: 0}).getText(), '999'); } else { assert.equal(await gu.getCell({rowNum: 1, col: 0}).getText(), ''); } // editing should work await gu.getCell({rowNum: 1, col: 0}).click(); await gu.enterCell('123'); await gu.waitForServer(); assert.equal(await gu.getCell({rowNum: 1, col: 0}).getText(), '123'); // edits should persist across reloads await personal.loadDoc(`/doc/${id}`); assert.equal(await gu.getCell({rowNum: 1, col: 0}).getText(), '123'); // edits should still work await gu.getCell({rowNum: 1, col: 0}).click(); await gu.enterCell('234'); await gu.waitForServer(); assert.equal(await gu.getCell({rowNum: 1, col: 0}).getText(), '234'); if (mode !== 'anonymous') { // if we log out, access should now be denied const anonSession = await personal.anon.login(); await anonSession.loadDoc(`/doc/${id}`, false); assert.match(await driver.findWait('.test-error-header', 2000).getText(), /Access denied/); // if we log in as a different user, access should be denied const altSession = await personal.user('user2').login(); await altSession.loadDoc(`/doc/${id}`, false); assert.match(await driver.findWait('.test-error-header', 2000).getText(), /Access denied/); } }); } } it('allows a document to be forked anonymously', async function() { // Anon loads the document, tries to modify, and fails - no write access const anonSession = await team.anon.login(); await anonSession.loadDoc(`/doc/${doc.id}`); await gu.getCell({rowNum: 1, col: 0}).click(); await gu.enterCell('123'); await gu.waitForServer(); assert.notEqual(await gu.getCell({rowNum: 1, col: 0}).value(), '123'); // check that there is no tag assert.equal(await driver.find('.test-unsaved-tag').isPresent(), false); // Anon forks the document, tries to modify, and succeeds await anonSession.loadDoc(`/doc/${doc.id}/m/fork`); await gu.getCell({rowNum: 1, col: 0}).click(); await gu.enterCell('123'); // Check that we show some indicators of what's happening to the user in notification toasts // (but allow for possibility that things are changing fast). await driver.findContentWait('.test-notifier-toast-message', /(Preparing your copy)|(You are now.*your own copy)/, 2000); await gu.waitForServer(); await driver.findContentWait('.test-notifier-toast-message', /You are now.*your own copy/, 2000); assert.equal(await driver.findContent('.test-notifier-toast-message', /Preparing/).isPresent(), false); assert.equal(await gu.getCell({rowNum: 1, col: 0}).getText(), '123'); await gu.getCell({rowNum: 2, col: 0}).click(); await gu.enterCell('234'); await gu.waitForServer(); assert.equal(await gu.getCell({rowNum: 2, col: 0}).getText(), '234'); // The url of the doc should now be that of a fork const forkUrl = await driver.getCurrentUrl(); assert.match(forkUrl, /~/); // check that the tag is there assert.equal(await driver.find('.test-unsaved-tag').isPresent(), true); // Open the original url and make sure the change we made is not there await anonSession.loadDoc(`/doc/${doc.id}`); assert.notEqual(await gu.getCell({rowNum: 1, col: 0}).value(), '123'); assert.notEqual(await gu.getCell({rowNum: 2, col: 0}).value(), '234'); assert.equal(await driver.find('.test-unsaved-tag').isPresent(), false); // Open the fork url and make sure the change we made is persisted there await anonSession.loadDoc((new URL(forkUrl)).pathname); assert.equal(await gu.getCell({rowNum: 1, col: 0}).getText(), '123'); assert.equal(await gu.getCell({rowNum: 2, col: 0}).getText(), '234'); assert.equal(await driver.find('.test-unsaved-tag').isPresent(), true); }); it('allows a document to be forked anonymously multiple times', async function() { // Anon forks the document, tries to modify, and succeeds const anonSession = await team.anon.login(); await anonSession.loadDoc(`/doc/${doc.id}/m/fork`); await gu.getCell({rowNum: 1, col: 0}).click(); await gu.enterCell('1'); await gu.waitForServer(); assert.equal(await gu.getCell({rowNum: 1, col: 0}).getText(), '1'); const fork1 = await driver.getCurrentUrl(); await anonSession.loadDoc(`/doc/${doc.id}/m/fork`); await gu.getCell({rowNum: 1, col: 0}).click(); await gu.enterCell('2'); await gu.waitForServer(); assert.equal(await gu.getCell({rowNum: 1, col: 0}).getText(), '2'); const fork2 = await driver.getCurrentUrl(); await anonSession.loadDoc((new URL(fork1)).pathname); assert.equal(await gu.getCell({rowNum: 1, col: 0}).getText(), '1'); await anonSession.loadDoc((new URL(fork2)).pathname); assert.equal(await gu.getCell({rowNum: 1, col: 0}).getText(), '2'); }); it('does not allow an anonymous fork to be forked', () => testForksOfForks(false)); it('shows the right page item after forking', async function() { const anonSession = await team.anon.login(); await anonSession.loadDoc(`/doc/${doc.id}/m/fork`); assert.match(await driver.find('.test-treeview-itemHeader.selected').getText(), /Table1/); // Add a new page; this immediately triggers a fork, AND selects the new page in it. await gu.addNewPage(/Table/, /New Table/, {dismissTips: true}); const urlId1 = await gu.getCurrentUrlId(); assert.match(urlId1!, /~/); assert.match(await driver.find('.test-treeview-itemHeader.selected').getText(), /Table2/); }); for (const user of [{access: 'viewers', name: 'user3'}, {access: 'editors', name: 'user2'}]) { it(`allows a logged in user with ${user.access} permissions to fork`, async function() { const userSession = await gu.session().teamSite.user(user.name as any).login(); await userSession.loadDoc(`/doc/${doc.id}/m/fork`); assert.equal(await gu.getEmail(), userSession.email); assert.equal(await driver.find('.test-unsaved-tag').isPresent(), false); await gu.getCell({rowNum: 1, col: 0}).click(); await gu.enterCell('123'); await gu.waitForServer(); assert.equal(await driver.findWait('.test-unsaved-tag', 4000).isPresent(), true); await gu.getCell({rowNum: 2, col: 0}).click(); await gu.enterCell('234'); await gu.waitForServer(); assert.equal(await gu.getCell({rowNum: 2, col: 0}).getText(), '234'); // The url of the doc should now be that of a fork const forkUrl = await driver.getCurrentUrl(); assert.match(forkUrl, /~/); // Check that the fork was saved to the home db const api = userSession.createHomeApi(); const forks = await getForks(api); const forkId = forkUrl.match(/\/[\w-]+~(\w+)(?:~\w)?/)?.[1]; const fork = forks.find(f => f.id === forkId); assert.isDefined(fork); assert.equal(fork!.trunkId, doc.id); // Open the original url and make sure the change we made is not there await userSession.loadDoc(`/doc/${doc.id}`); assert.notEqual(await gu.getCell({rowNum: 1, col: 0}).value(), '123'); assert.notEqual(await gu.getCell({rowNum: 2, col: 0}).value(), '234'); // Open the fork url and make sure the change we made is persisted there await userSession.loadDoc((new URL(forkUrl)).pathname); assert.notEqual(await gu.getCell({rowNum: 2, col: 0}).value(), '234'); assert.equal(await gu.getCell({rowNum: 1, col: 0}).getText(), '123'); assert.equal(await gu.getCell({rowNum: 2, col: 0}).getText(), '234'); // Check we still have editing rights await gu.getCell({rowNum: 1, col: 0}).click(); await gu.enterCell('1234'); await gu.waitForServer(); await userSession.loadDoc((new URL(forkUrl)).pathname); assert.equal(await gu.getCell({rowNum: 1, col: 0}).getText(), '1234'); // Check others with write access to trunk can view but not edit our // fork await team.login(); await team.loadDoc((new URL(forkUrl)).pathname); assert.equal(await gu.getEmail(), team.email); assert.equal(await gu.getCell({rowNum: 1, col: 0}).getText(), '1234'); await gu.getCell({rowNum: 1, col: 0}).click(); await gu.enterCell('12345'); await gu.waitForServer(); await team.loadDoc((new URL(forkUrl)).pathname); assert.equal(await gu.getCell({rowNum: 1, col: 0}).getText(), '1234'); const anonSession = await team.anon.login(); await anonSession.loadDoc((new URL(forkUrl)).pathname); assert.equal(await gu.getEmail(), 'anon@getgrist.com'); assert.equal(await gu.getCell({rowNum: 1, col: 0}).getText(), '1234'); await gu.getCell({rowNum: 1, col: 0}).click(); await gu.enterCell('12345'); await gu.waitForServer(); await anonSession.loadDoc((new URL(forkUrl)).pathname); assert.equal(await gu.getCell({rowNum: 1, col: 0}).getText(), '1234'); }); } it('controls access to forks of a logged in user correctly', async function() { await gu.removeLogin(); const doc2 = await team.tempDoc(cleanup, 'Hello.grist', {load: false}); await api.updateDocPermissions(doc2.id, {maxInheritedRole: null}); await team.login(); await team.loadDoc(`/doc/${doc2.id}/m/fork`); await gu.getCell({rowNum: 1, col: 0}).click(); await gu.enterCell('123'); await gu.waitForServer(); // The url of the doc should now be that of a fork const forkUrl = await driver.getCurrentUrl(); assert.match(forkUrl, /~/); // Open the original url and make sure the change we made is not there await team.loadDoc(`/doc/${doc2.id}`); assert.notEqual(await gu.getCell({rowNum: 1, col: 0}).value(), '123'); // Open the fork url and make sure the change we made is persisted there await team.loadDoc((new URL(forkUrl)).pathname); assert.equal(await gu.getCell({rowNum: 1, col: 0}).getText(), '123'); // Check we still have editing rights await gu.getCell({rowNum: 1, col: 0}).click(); await gu.enterCell('1234'); await gu.waitForServer(); await team.loadDoc((new URL(forkUrl)).pathname); assert.equal(await gu.getCell({rowNum: 1, col: 0}).getText(), '1234'); // Check others without view access to trunk cannot see fork await team.user('user2').login(); await driver.get(forkUrl); assert.equal(await driver.findWait('.test-dm-logo', 2000).isDisplayed(), true); assert.match(await driver.find('.test-error-header').getText(), /Access denied/); await server.removeLogin(); await driver.get(forkUrl); assert.equal(await driver.findWait('.test-dm-logo', 2000).isDisplayed(), true); assert.match(await driver.find('.test-error-header').getText(), /Access denied/); }); it('fails to create forks with inconsistent user id', async function() { // Note: this test is also valuable for triggering an error during openDoc flow even though // the initial page load succeeds, and checking how such an error is shown. const forkId = `${idType}fork${uuidv4()}`; await team.login(); const userId = await team.getUserId(); const altSession = await team.user('user2').login(); await altSession.loadDoc(`/doc/${doc.id}~${forkId}~${userId}`, false); assert.equal(await driver.findWait('.test-modal-dialog', 2000).isDisplayed(), true); assert.match(await driver.find('.test-modal-dialog').getText(), /Document fork not found/); // Ensure the user has a way to report the problem (although including the report button in // the modal might be better). assert.match(await driver.find('.test-notifier-toast-wrapper').getText(), /Document fork not found.*Report a problem/s); // A new doc cannot be created either (because of access // mismatch - for forks of the doc used in these tests, user2 // would have some access to fork via acls on trunk, but for a // new doc user2 has no access granted via the doc, or // workspace, or org). await altSession.loadDoc(`/doc/new~${forkId}~${userId}`, false); assert.equal(await driver.findWait('.test-dm-logo', 2000).isDisplayed(), true); assert.match(await driver.find('.test-error-header').getText(), /Access denied/); // Same, but as an anonymous user. const anonSession = await altSession.anon.login(); await anonSession.loadDoc(`/doc/${doc.id}~${forkId}~${userId}`, false); assert.equal(await driver.findWait('.test-modal-dialog', 2000).isDisplayed(), true); assert.match(await driver.find('.test-modal-dialog').getText(), /Document fork not found/); // A new doc cannot be created either (because of access mismatch). await altSession.loadDoc(`/doc/new~${forkId}~${userId}`, false); assert.equal(await driver.findWait('.test-dm-logo', 2000).isDisplayed(), true); assert.match(await driver.find('.test-error-header').getText(), /Access denied/); // Now as a user who *is* allowed to create the fork. // But doc forks cannot be casually created this way anymore, so it still doesn't work. await team.login(); await team.loadDoc(`/doc/${doc.id}~${forkId}~${userId}`, false); assert.equal(await driver.findWait('.test-modal-dialog', 2000).isDisplayed(), true); assert.match(await driver.find('.test-modal-dialog').getText(), /Document fork not found/); // New document can no longer be casually created this way anymore either. await team.loadDoc(`/doc/new~${forkId}~${userId}`, false); assert.equal(await driver.findWait('.test-modal-dialog', 2000).isDisplayed(), true); assert.match(await driver.find('.test-modal-dialog').getText(), /Document fork not found/); await gu.wipeToasts(); }); it("should include the unsaved tags", async function() { await team.login(); // open a document const trunk = await team.tempDoc(cleanup, 'World.grist'); // make a fork await team.loadDoc(`/doc/${trunk.id}/m/fork`); await gu.getCell({rowNum: 1, col: 0}).click(); await gu.enterCell('123'); await gu.waitForServer(); const forkUrl = await driver.getCurrentUrl(); assert.match(forkUrl, /~/); // check that there is no tag on trunk await team.loadDoc(`/doc/${trunk.id}`); assert.equal(await driver.find('.test-unsaved-tag').isPresent(), false); // open same document with the fork bit in the URL await team.loadDoc((new URL(forkUrl)).pathname); // check that the tag is there assert.equal(await driver.find('.test-unsaved-tag').isPresent(), true); }); it('handles url history correctly', async function() { await team.login(); await makeDocIfAbsent(); await team.loadDoc(`/doc/${doc.id}/m/fork`); const initialUrl = await driver.getCurrentUrl(); assert.equal(await driver.find('.test-unsaved-tag').isPresent(), false); await gu.getCell({rowNum: 1, col: 0}).click(); await gu.enterCell('2'); await gu.waitForServer(); assert.equal(await gu.getCell({rowNum: 1, col: 0}).getText(), '2'); const forkUrl = await driver.getCurrentUrl(); assert.equal(await driver.findWait('.test-unsaved-tag', 4000).isPresent(), true); await driver.executeScript('history.back()'); await gu.waitForUrl(/\/m\/fork/); assert.equal(await driver.getCurrentUrl(), initialUrl); await gu.waitForDocToLoad(); assert.equal(await gu.getCell({rowNum: 1, col: 0}).getText(), 'hello'); assert.equal(await driver.find('.test-unsaved-tag').isPresent(), false); await driver.executeScript('history.forward()'); await gu.waitForUrl(/~/); assert.equal(await driver.getCurrentUrl(), forkUrl); await gu.waitForDocToLoad(); assert.equal(await gu.getCell({rowNum: 1, col: 0}).getText(), '2'); assert.equal(await driver.find('.test-unsaved-tag').isPresent(), true); }); it('disables document renaming for forks', async function() { await team.login(); await team.loadDoc(`/doc/${doc.id}/m/fork`); assert.equal(await driver.find('.test-bc-doc').getAttribute('disabled'), null); await gu.getCell({rowNum: 1, col: 0}).click(); await gu.enterCell('2'); await gu.waitForServer(); await gu.waitToPass(async () => { assert.equal(await driver.find('.test-bc-doc').getAttribute('disabled'), 'true'); }); }); it('navigating browser history play well with the add new menu', async function() { await team.login(); await makeDocIfAbsent(); await team.loadDoc(`/doc/${doc.id}/m/fork`); const count = await getAddNewEntryCount(); // edit one cell await gu.getCell({rowNum: 1, col: 0}).click(); await gu.enterCell('2'); await gu.waitForServer(); // check we're on a fork await gu.waitForUrl(/~/); // navigate back history await driver.navigate().back(); await gu.waitForDocToLoad(); // check number of entries in add new menu are the same assert.equal(await getAddNewEntryCount(), count); // helper that get the number of items in the add new menu async function getAddNewEntryCount() { await driver.find('.test-dp-add-new').click(); const items = await driver.findAll('.grist-floating-menu li', e => e.getText()); assert.include(items, "Import from file"); await driver.sendKeys(Key.ESCAPE); return items.length; } }); it('can replace a trunk document with a fork via api', async function() { await team.login(); await makeDocIfAbsent(); await team.loadDoc(`/doc/${doc.id}/m/fork`); // edit one cell await gu.getCell({rowNum: 2, col: 0}).click(); const v1 = await gu.getCell({rowNum: 2, col: 0}).getText(); const v2 = `${v1}_tweaked`; await gu.enterCell(v2); await gu.waitForServer(); // check we're on a fork await gu.waitForUrl(/~/); const urlId = await gu.getCurrentUrlId(); // open trunk again, to test that page is reloaded after replacement await team.loadDoc(`/doc/${doc.id}`); // replace the trunk with the fork assert.notEqual(urlId, doc.id); assert.equal(await gu.getCell({rowNum: 2, col: 0}).getText(), v1); const docApi = team.createHomeApi().getDocAPI(doc.id); await docApi.replace({sourceDocId: urlId!}); // check that replacement worked (giving a little time for page reload) await gu.waitToPass(async () => { assert.equal(await gu.getCell({rowNum: 2, col: 0}).getText(), v2); }, 4000); // have a different user make a doc we don't have access to const altSession = await personal.user('user2').login(); const altDoc = await altSession.tempDoc(cleanup, 'Hello.grist'); await gu.dismissWelcomeTourIfNeeded(); await gu.getCell({rowNum: 2, col: 0}).click(); await gu.enterCell('altDoc'); await gu.waitForServer(); // replacement should fail for document not found // (error is "not found" for document in a different team site // or team site vs personal site) await assert.isRejected(docApi.replace({sourceDocId: altDoc.id}), /not found/); // replacement should fail for document not accessible await assert.isRejected(personal.createHomeApi().getDocAPI(doc.id).replace({sourceDocId: altDoc.id}), /access denied/); // check cell content does not change await altSession.loadDoc(`/doc/${altDoc.id}`); assert.equal(await gu.getCell({rowNum: 2, col: 0}).getText(), 'altDoc'); await team.login(); await team.loadDoc(`/doc/${doc.id}`); assert.equal(await gu.getCell({rowNum: 2, col: 0}).getText(), v2); }); it('can replace a trunk document with a fork via UI', async function() { await team.login(); await makeDocIfAbsent(); await team.loadDoc(`/doc/${doc.id}/m/fork`); // edit one cell. await gu.getCell({rowNum: 2, col: 0}).click(); const v1 = await gu.getCell({rowNum: 2, col: 0}).getText(); const v2 = `${v1}_tweaked`; await gu.enterCell(v2); await gu.waitForServer(); // check we're on a fork. await gu.waitForUrl(/~/); const forkUrlId = await gu.getCurrentUrlId(); assert.equal(await driver.findWait('.test-unsaved-tag', 4000).isPresent(), true); // check Replace Original gives expected button, and press it. await driver.find('.test-tb-share').click(); await driver.find('.test-replace-original').click(); let confirmButton = driver.findWait('.test-modal-confirm', 3000); assert.equal(await confirmButton.getText(), 'Update'); await confirmButton.click(); // check we're no longer on a fork, but still have the change made on the fork. await gu.waitForUrl(/^[^~]*$/, 6000); await gu.waitForDocToLoad(); assert.equal(await gu.getCell({rowNum: 2, col: 0}).getText(), v2); // edit the cell again. await gu.getCell({rowNum: 2, col: 0}).click(); const v3 = `${v2}_tweaked`; await gu.enterCell(v3); await gu.waitForServer(); // revisit the fork. await team.loadDoc(`/doc/${forkUrlId}`); assert.equal(await driver.findWait('.test-unsaved-tag', 4000).isPresent(), true); // check Replace Original gives a scarier button, and press it anyway. await driver.find('.test-tb-share').click(); await driver.find('.test-replace-original').click(); confirmButton = driver.findWait('.test-modal-confirm', 3000); assert.equal(await confirmButton.getText(), 'Overwrite'); await confirmButton.click(); // check we're no longer on a fork, but have the fork's content. await gu.waitForUrl(/^[^~]*$/, 6000); await gu.waitForDocToLoad(); assert.equal(await gu.getCell({rowNum: 2, col: 0}).getText(), v2); // revisit the fork. await team.loadDoc(`/doc/${forkUrlId}`); assert.equal(await driver.findWait('.test-unsaved-tag', 4000).isPresent(), true); // check Replace Original mentions that the document is the same as the trunk. await driver.find('.test-tb-share').click(); await driver.find('.test-replace-original').click(); confirmButton = driver.findWait('.test-modal-confirm', 3000); assert.equal(await confirmButton.getText(), 'Update'); assert.match(await driver.find('.test-modal-dialog').getText(), /already identical/); }); it('gives an error when replacing without write access via UI', async function() { await team.login(); await makeDocIfAbsent(); // Give view access to a friend. const altSession = await team.user('user2').login(); await api.updateDocPermissions(doc.id, {users: {[altSession.email]: 'viewers'}}); try { await team.loadDoc(`/doc/${doc.id}/m/fork`); // edit one cell. await gu.getCell({rowNum: 2, col: 0}).click(); const v1 = await gu.getCell({rowNum: 2, col: 0}).getText(); const v2 = `${v1}_tweaked`; await gu.enterCell(v2); await gu.waitForServer(); // check we're on a fork. await gu.waitForUrl(/~/); await gu.waitForDocToLoad(); assert.equal(await driver.findWait('.test-unsaved-tag', 4000).isPresent(), true); // check Replace Original does not let us proceed because we don't have // editing rights on trunk. await driver.find('.test-tb-share').click(); assert.equal(await driver.find('.test-replace-original').matches('.disabled'), true); // Clicking the disabled element does nothing. await driver.find('.test-replace-original').click(); assert.equal(await driver.find('.grist-floating-menu').isDisplayed(), true); await assert.isRejected(driver.findWait('.test-modal-dialog', 500), /Waiting for element/); } finally { await api.updateDocPermissions(doc.id, {users: {[altSession.email]: null}}); } }); it('does not allow a fork to be forked', testForksOfForks); }); } });