(core) Revealing hidden pages with visible children.

Summary:
When a page is hidden, all its nested pages are shown as children of
a different page that happens to be before (as in pagePos) that page.

This diff shows those pages as CENSORED.

Test Plan: Updated

Reviewers: alexmojaki

Reviewed By: alexmojaki

Subscribers: alexmojaki

Differential Revision: https://phab.getgrist.com/D3670
This commit is contained in:
Jarosław Sadziński
2022-10-28 10:04:59 +02:00
parent b263d83122
commit 7c9cb9843e
9 changed files with 295 additions and 25 deletions

View File

@@ -7,7 +7,7 @@ import {server, setupTestSuite} from 'test/nbrowser/testUtils';
import values = require('lodash/values');
describe('Pages', function() {
this.timeout(20000);
this.timeout(30000);
setupTestSuite();
let doc: DocCreationInfo;
let api: UserAPI;
@@ -20,6 +20,124 @@ describe('Pages', function() {
api = session.createHomeApi();
});
it('should show censor pages', async () => {
// Make a 3 level hierarchy.
assert.deepEqual(await gu.getPageTree(), [
{
label: 'Interactions', children: [
{ label: 'Documents' },
]
},
{
label: 'People', children: [
{ label: 'User & Leads' },
{ label: 'Overview' },
]
},
]);
await insertPage(/Overview/, /User & Leads/);
assert.deepEqual(await gu.getPageTree(), [
{
label: 'Interactions', children: [
{ label: 'Documents' },
]
},
{
label: 'People', children: [
{ label: 'User & Leads', children: [{ label: 'Overview' }] },
]
},
]);
const revertAcl = await gu.beginAclTran(api, doc.id);
// Update ACL, hide People table from all users.
await hideTable("People");
// We will be reloaded, but it's not easy to wait for it, so do the refresh manually.
await gu.reloadDoc();
assert.deepEqual(await gu.getPageTree(), [
{
label: 'Interactions', children: [
{ label: 'Documents'},
]
},
{
label: 'CENSORED', children: [
{ label: 'User & Leads', children: [{ label: 'Overview' }] },
]
},
]);
// Test that we can't click this page.
await driver.findContent('.test-treeview-itemHeader', /CENSORED/).click();
await gu.waitForServer();
assert.equal(await gu.getSectionTitle(), 'INTERACTIONS');
// Test that we don't have move handler.
assert.isFalse(
await driver.findContent('.test-treeview-itemHeaderWrapper', /CENSORED/)
.find('.test-treeview-handle').isPresent()
);
// Now hide User_Leads
await hideTable("User_Leads");
await gu.reloadDoc();
assert.deepEqual(await gu.getPageTree(), [
{
label: 'Interactions', children: [
{ label: 'Documents'},
]
},
{
label: 'CENSORED', children: [
{ label: 'CENSORED', children: [{ label: 'Overview' }] },
]
},
]);
// Now hide Overview, and test that whole node is hidden.
await hideTable("Overview");
await gu.reloadDoc();
assert.deepEqual(await gu.getPageTree(), [
{
label: 'Interactions', children: [
{ label: 'Documents'},
]
}
]);
// Now hide Documents, this is a leaf, so it should be hidden from the start
await hideTable("Documents");
await gu.reloadDoc();
assert.deepEqual(await gu.getPageTree(), [
{
label: 'Interactions'
}
]);
// Now hide Interactions, we should have a blank treeview
await hideTable("Interactions");
// We can wait for doc to load, because it waits for section.
await driver.findWait(".test-treeview-container", 1000);
assert.deepEqual(await gu.getPageTree(), []);
// Rollback
await revertAcl();
await gu.reloadDoc();
assert.deepEqual(await gu.getPageTree(), [
{
label: 'Interactions', children: [
{ label: 'Documents' },
]
},
{
label: 'People', children: [
{ label: 'User & Leads', children: [{ label: 'Overview' }] },
]
},
]);
await gu.undo();
});
it('should list all pages in document', async () => {
// check content of _girst_Pages and _grist_Views
@@ -122,8 +240,7 @@ describe('Pages', function() {
// revert changes
await gu.undo(2);
assert.deepEqual(await gu.getPageNames(), ['Interactions', 'Documents', 'People', 'User & Leads', 'Overview']);
})
});
it('should not allow blank page name', async () => {
// Begin renaming of People page
@@ -403,7 +520,7 @@ describe('Pages', function() {
it('should not throw JS errors when removing the current page without a slug', async () => {
// Create and open new document
const docId = await session.tempNewDoc(cleanup, "test-page-removal-js-error")
const docId = await session.tempNewDoc(cleanup, "test-page-removal-js-error");
await driver.get(`${server.getHost()}/o/test-grist/doc/${docId}`);
await gu.waitForUrl('test-page-removal-js-error');
@@ -442,7 +559,7 @@ describe('Pages', function() {
it('should offer a way to delete last tables', async () => {
// Create and open new document
const docId = await session.tempNewDoc(cleanup, "prompts")
const docId = await session.tempNewDoc(cleanup, "prompts");
await driver.get(`${server.getHost()}/o/test-grist/doc/${docId}`);
await gu.waitForUrl('prompts');
@@ -482,9 +599,18 @@ describe('Pages', function() {
assert.deepEqual(await gu.getSectionTitles(), ['TABLE C', 'TABLE D', 'TABLE1' ]);
});
async function hideTable(tableId: string) {
await api.applyUserActions(doc.id, [
['AddRecord', '_grist_ACLResources', -1, {tableId, colIds: '*'}],
['AddRecord', '_grist_ACLRules', null, {
resource: -1, aclFormula: '', permissionsText: '-R',
}],
]);
}
});
async function movePage(page: RegExp, target: {before: RegExp}|{after: RegExp}) {
async function movePage(page: RegExp, target: {before: RegExp}|{after: RegExp}|{into: RegExp}) {
const targetReg = values(target)[0];
await driver.withActions(actions => actions
.move({origin: driver.findContent('.test-treeview-itemHeader', page)})
@@ -496,3 +622,19 @@ async function movePage(page: RegExp, target: {before: RegExp}|{after: RegExp})
})
.release());
}
async function insertPage(page: RegExp, into: RegExp) {
await driver.withActions(actions => actions
.move({origin: driver.findContent('.test-treeview-itemHeader', page)})
.move({origin: driver.findContent('.test-treeview-itemHeaderWrapper', page)
.find('.test-treeview-handle')})
.press()
.move({origin: driver.findContent('.test-treeview-itemHeader', into),
y: 5
})
.pause(1500) // wait for a target to be highlighted
.release()
);
await gu.waitForServer();
}

View File

@@ -16,7 +16,8 @@ import { FullUser, UserProfile } from 'app/common/LoginSessionAPI';
import { resetOrg } from 'app/common/resetOrg';
import { UserAction } from 'app/common/DocActions';
import { TestState } from 'app/common/TestState';
import { Organization as APIOrganization, DocStateComparison, UserAPIImpl, Workspace } from 'app/common/UserAPI';
import { Organization as APIOrganization, DocStateComparison,
UserAPI, UserAPIImpl, Workspace } from 'app/common/UserAPI';
import { Organization } from 'app/gen-server/entity/Organization';
import { Product } from 'app/gen-server/entity/Product';
import { create } from 'app/server/lib/create';
@@ -688,6 +689,11 @@ export async function waitForDocToLoad(timeoutMs: number = 10000): Promise<void>
await waitForServer();
}
export async function reloadDoc() {
await driver.navigate().refresh();
await waitForDocToLoad();
}
/**
* Wait for the doc list to show, to know that workspaces are fetched, and imports enabled.
*/
@@ -930,6 +936,51 @@ export function getPageNames(): Promise<string[]> {
return driver.findAll('.test-docpage-label', (e) => e.getText());
}
export interface PageTree {
label: string;
children?: PageTree[];
}
/**
* Returns a current page tree as a JSON object.
*/
export async function getPageTree(): Promise<PageTree[]> {
const allPages = await driver.findAll('.test-docpage-label');
const root: PageTree = {label: 'root', children: []};
const stack: PageTree[] = [root];
let current = 0;
for(const page of allPages) {
const label = await page.getText();
const offset = await page.findClosest('.test-treeview-itemHeader').find('.test-treeview-offset');
const level = parseInt((await offset.getCssValue('width')).replace("px", "")) / 10;
if (level === current) {
const parent = stack.pop()!;
parent.children ??= [];
parent.children.push({label});
stack.push(parent);
} else if (level > current) {
current = level;
const child = {label};
const grandFather = stack.pop()!;
grandFather.children ??= [];
const father = grandFather.children[grandFather.children.length - 1];
father.children ??= [];
father.children.push(child);
stack.push(grandFather);
stack.push(father);
} else {
while (level < current) {
stack.pop();
current--;
}
const parent = stack.pop()!;
parent.children ??= [];
parent.children.push({label});
stack.push(parent);
}
}
return root.children!;
}
/**
* Adds a new empty table using the 'Add New' menu.
*/
@@ -2685,6 +2736,32 @@ export function withComments() {
});
}
/**
* Helper to revert ACL changes. It first saves the current ACL data, and
* then removes everything and adds it back.
*/
export async function beginAclTran(api: UserAPI, docId: string) {
const oldRes = await api.getTable(docId, '_grist_ACLResources');
const oldRules = await api.getTable(docId, '_grist_ACLRules');
return async () => {
const newRes = await api.getTable(docId, '_grist_ACLResources');
const newRules = await api.getTable(docId, '_grist_ACLRules');
const restoreRes = {tableId: oldRes.tableId, colIds: oldRes.colIds};
const restoreRules = {
resource: oldRules.resource,
aclFormula: oldRules.aclFormula,
permissionsText: oldRules.permissionsText
};
await api.applyUserActions(docId, [
['BulkRemoveRecord', '_grist_ACLRules', newRules.id],
['BulkRemoveRecord', '_grist_ACLResources', newRes.id],
['BulkAddRecord', '_grist_ACLResources', oldRes.id, restoreRes],
['BulkAddRecord', '_grist_ACLRules', oldRules.id, restoreRules],
]);
};
}
} // end of namespace gristUtils
stackWrapOwnMethods(gristUtils);