mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) Tests and bug fixes for bidirectional linking
Summary: - Adding tests for bidirectional linking - Fixing loop bug for bidirectional linking in custom widgets which use row filtering Test Plan: New tests Reviewers: JakubSerafin Reviewed By: JakubSerafin Differential Revision: https://phab.getgrist.com/D4070
This commit is contained in:
parent
a8e0f96813
commit
083a20417e
@ -286,7 +286,7 @@ export class LinkingState extends Disposable {
|
|||||||
|
|
||||||
// Get previous linkingstate's info, if applicable (2 or more hops back)
|
// Get previous linkingstate's info, if applicable (2 or more hops back)
|
||||||
const prevLink = this._srcSection.linkingState?.();
|
const prevLink = this._srcSection.linkingState?.();
|
||||||
const prevLinkHasCursor = prevLink &&
|
const prevLinkHasCursor = prevLink?.incomingCursorPos &&
|
||||||
(prevLink.linkTypeDescription() === "Cursor:Same-Table" ||
|
(prevLink.linkTypeDescription() === "Cursor:Same-Table" ||
|
||||||
prevLink.linkTypeDescription() === "Cursor:Reference");
|
prevLink.linkTypeDescription() === "Cursor:Reference");
|
||||||
const [prevLinkedPos, prevLinkedVersion] = prevLinkHasCursor ? prevLink.incomingCursorPos() :
|
const [prevLinkedPos, prevLinkedVersion] = prevLinkHasCursor ? prevLink.incomingCursorPos() :
|
||||||
|
@ -156,6 +156,11 @@ function isValidLink(source: LinkNode, target: LinkNode) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If one of the section has custom row filter, we can't make cycles.
|
||||||
|
if (target.section.selectedRowsActive()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// We know our ancestors cycle back around to ourselves
|
// We know our ancestors cycle back around to ourselves
|
||||||
// - lets walk back along the cyclic portion of the ancestor chain and verify that each link in that chain is
|
// - lets walk back along the cyclic portion of the ancestor chain and verify that each link in that chain is
|
||||||
// a cursor-link
|
// a cursor-link
|
||||||
@ -426,6 +431,18 @@ export class LinkConfig {
|
|||||||
assert(srcTableId, "srcCol not a valid reference");
|
assert(srcTableId, "srcCol not a valid reference");
|
||||||
}
|
}
|
||||||
assert(srcTableId === tgtTableId, "mismatched tableIds");
|
assert(srcTableId === tgtTableId, "mismatched tableIds");
|
||||||
|
|
||||||
|
// If this section has a custom link filter, it can't create cycles.
|
||||||
|
if (this.tgtSection.selectedRowsActive()) {
|
||||||
|
// Make sure we don't have a cycle.
|
||||||
|
let src = this.tgtSection.linkSrcSection();
|
||||||
|
while (!src.isDisposed() && src.getRowId()) {
|
||||||
|
assert(src.getRowId() !== this.srcSection.getRowId(),
|
||||||
|
"Sections with filter linking can't be part of a cycle (same record linking)'");
|
||||||
|
src = src.linkSrcSection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`LinkConfig invalid: ` +
|
throw new Error(`LinkConfig invalid: ` +
|
||||||
`${this.srcSection.getRowId()}:${this.srcCol?.getRowId()}[${srcTableId}] -> ` +
|
`${this.srcSection.getRowId()}:${this.srcCol?.getRowId()}[${srcTableId}] -> ` +
|
||||||
|
@ -77,11 +77,12 @@ export const allowSelectBy = viewApi.allowSelectBy;
|
|||||||
*/
|
*/
|
||||||
export const setSelectedRows = viewApi.setSelectedRows;
|
export const setSelectedRows = viewApi.setSelectedRows;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the cursor position in a linked section.
|
||||||
|
*/
|
||||||
export const setCursorPos = viewApi.setCursorPos;
|
export const setCursorPos = viewApi.setCursorPos;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches data backing the widget as for [[GristView.fetchSelectedTable]],
|
* Fetches data backing the widget as for [[GristView.fetchSelectedTable]],
|
||||||
* but decoding data by default, replacing e.g. ['D', timestamp] with
|
* but decoding data by default, replacing e.g. ['D', timestamp] with
|
||||||
|
@ -57,27 +57,6 @@ async function getListItems(col: string) {
|
|||||||
.findAll(`.test-config-widget-map-list-for-${col} .test-config-widget-ref-select-label`, el => el.getText());
|
.findAll(`.test-config-widget-map-list-for-${col} .test-config-widget-ref-select-label`, el => el.getText());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gets or sets access level
|
|
||||||
async function givenAccess(level?: AccessLevel) {
|
|
||||||
const text = {
|
|
||||||
[AccessLevel.none]: 'No document access',
|
|
||||||
[AccessLevel.read_table]: 'Read selected table',
|
|
||||||
[AccessLevel.full]: 'Full document access',
|
|
||||||
};
|
|
||||||
if (!level) {
|
|
||||||
const currentAccess = await driver.find('.test-config-widget-access .test-select-open').getText();
|
|
||||||
return Object.entries(text).find(e => e[1] === currentAccess)![0];
|
|
||||||
} else {
|
|
||||||
await driver.find('.test-config-widget-access .test-select-open').click();
|
|
||||||
await driver.findContent('.test-select-menu li', text[level]).click();
|
|
||||||
await gu.waitForServer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checks if access prompt is visible.
|
|
||||||
const hasPrompt = () => driver.find('.test-config-widget-access-accept').isPresent();
|
|
||||||
// Accepts new access level.
|
|
||||||
const accept = () => driver.find('.test-config-widget-access-accept').click();
|
|
||||||
// When refreshing, we need to make sure widget repository is enabled once again.
|
// When refreshing, we need to make sure widget repository is enabled once again.
|
||||||
async function refresh() {
|
async function refresh() {
|
||||||
await driver.navigate().refresh();
|
await driver.navigate().refresh();
|
||||||
@ -88,19 +67,6 @@ async function refresh() {
|
|||||||
await gu.selectSectionByTitle('Widget');
|
await gu.selectSectionByTitle('Widget');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function selectAccess(access: string) {
|
|
||||||
// if the current access is ok do nothing
|
|
||||||
if ((await givenAccess()) === access) {
|
|
||||||
// unless we need to confirm it
|
|
||||||
if (await hasPrompt()) {
|
|
||||||
await accept();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// else switch access level
|
|
||||||
await givenAccess(access as AccessLevel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checks if active section has option in the menu to open configuration
|
// Checks if active section has option in the menu to open configuration
|
||||||
async function hasSectionOption() {
|
async function hasSectionOption() {
|
||||||
const menu = await gu.openSectionMenu('viewLayout');
|
const menu = await gu.openSectionMenu('viewLayout');
|
||||||
@ -158,7 +124,7 @@ async function checkSortMenu(state: 'empty' | 'modified' | 'customized' | 'empty
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('CustomWidgetsConfig', function () {
|
describe('CustomWidgetsConfig', function () {
|
||||||
this.timeout(60000);
|
this.timeout('60s');
|
||||||
const cleanup = setupTestSuite();
|
const cleanup = setupTestSuite();
|
||||||
let mainSession: gu.Session;
|
let mainSession: gu.Session;
|
||||||
gu.bigScreen();
|
gu.bigScreen();
|
||||||
@ -371,7 +337,7 @@ describe('CustomWidgetsConfig', function () {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
await accept();
|
await gu.acceptAccessRequest();
|
||||||
|
|
||||||
// Get the drop for M2 mappings.
|
// Get the drop for M2 mappings.
|
||||||
const mappingsForM2 = () => driver.find(pickerDrop('M2'));
|
const mappingsForM2 = () => driver.find(pickerDrop('M2'));
|
||||||
@ -421,7 +387,7 @@ describe('CustomWidgetsConfig', function () {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
await accept();
|
await gu.acceptAccessRequest();
|
||||||
|
|
||||||
// Get the drop for M2 mappings.
|
// Get the drop for M2 mappings.
|
||||||
const mappingsForM2 = () => driver.find(pickerDrop('M2'));
|
const mappingsForM2 = () => driver.find(pickerDrop('M2'));
|
||||||
@ -461,7 +427,7 @@ describe('CustomWidgetsConfig', function () {
|
|||||||
// Select widget that has single column configuration.
|
// Select widget that has single column configuration.
|
||||||
await clickOption(COLUMN_WIDGET);
|
await clickOption(COLUMN_WIDGET);
|
||||||
await widget.waitForFrame();
|
await widget.waitForFrame();
|
||||||
await accept();
|
await gu.acceptAccessRequest();
|
||||||
// Visible columns section should be hidden.
|
// Visible columns section should be hidden.
|
||||||
assert.isFalse(await driver.find('.test-vfc-visible-fields-select-all').isPresent());
|
assert.isFalse(await driver.find('.test-vfc-visible-fields-select-all').isPresent());
|
||||||
// Record event should be fired.
|
// Record event should be fired.
|
||||||
@ -496,7 +462,7 @@ describe('CustomWidgetsConfig', function () {
|
|||||||
requiredAccess: 'read table',
|
requiredAccess: 'read table',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
await accept();
|
await gu.acceptAccessRequest();
|
||||||
const empty = {M1: null, M2: null, M3: null, M4: null};
|
const empty = {M1: null, M2: null, M3: null, M4: null};
|
||||||
await widget.waitForFrame();
|
await widget.waitForFrame();
|
||||||
assert.isNull(await widget.onRecordsMappings());
|
assert.isNull(await widget.onRecordsMappings());
|
||||||
@ -562,7 +528,7 @@ describe('CustomWidgetsConfig', function () {
|
|||||||
|
|
||||||
await toggleWidgetMenu();
|
await toggleWidgetMenu();
|
||||||
await clickOption(COLUMN_WIDGET);
|
await clickOption(COLUMN_WIDGET);
|
||||||
await accept();
|
await gu.acceptAccessRequest();
|
||||||
|
|
||||||
// Make sure columns are there to pick.
|
// Make sure columns are there to pick.
|
||||||
|
|
||||||
@ -584,7 +550,7 @@ describe('CustomWidgetsConfig', function () {
|
|||||||
assert.isTrue(await driver.find('.test-vfc-visible-fields-select-all').isPresent());
|
assert.isTrue(await driver.find('.test-vfc-visible-fields-select-all').isPresent());
|
||||||
assert.isFalse(await driver.find('.test-config-widget-label-for-Column').isPresent());
|
assert.isFalse(await driver.find('.test-config-widget-label-for-Column').isPresent());
|
||||||
|
|
||||||
await selectAccess(AccessLevel.read_table);
|
await gu.changeWidgetAccess(AccessLevel.read_table);
|
||||||
// Widget should receive full records.
|
// Widget should receive full records.
|
||||||
assert.deepEqual(await widget.onRecords(), [
|
assert.deepEqual(await widget.onRecords(), [
|
||||||
{id: 1, A: 'A'},
|
{id: 1, A: 'A'},
|
||||||
@ -594,7 +560,7 @@ describe('CustomWidgetsConfig', function () {
|
|||||||
// Now go back to the widget with mappings.
|
// Now go back to the widget with mappings.
|
||||||
await toggleWidgetMenu();
|
await toggleWidgetMenu();
|
||||||
await clickOption(COLUMN_WIDGET);
|
await clickOption(COLUMN_WIDGET);
|
||||||
await accept();
|
await gu.acceptAccessRequest();
|
||||||
assert.equal(await driver.find(pickerDrop('Column')).getText(), 'Pick a column');
|
assert.equal(await driver.find(pickerDrop('Column')).getText(), 'Pick a column');
|
||||||
assert.isFalse(await driver.find('.test-vfc-visible-fields-select-all').isPresent());
|
assert.isFalse(await driver.find('.test-vfc-visible-fields-select-all').isPresent());
|
||||||
assert.isTrue(await driver.find('.test-config-widget-label-for-Column').isPresent());
|
assert.isTrue(await driver.find('.test-config-widget-label-for-Column').isPresent());
|
||||||
@ -614,7 +580,7 @@ describe('CustomWidgetsConfig', function () {
|
|||||||
requiredAccess: 'read table',
|
requiredAccess: 'read table',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
await accept();
|
await gu.acceptAccessRequest();
|
||||||
const empty = {M1: [], M2: []};
|
const empty = {M1: [], M2: []};
|
||||||
await widget.waitForFrame();
|
await widget.waitForFrame();
|
||||||
// Add some columns, numeric B and any C.
|
// Add some columns, numeric B and any C.
|
||||||
@ -691,7 +657,7 @@ describe('CustomWidgetsConfig', function () {
|
|||||||
requiredAccess: 'read table',
|
requiredAccess: 'read table',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
await accept();
|
await gu.acceptAccessRequest();
|
||||||
await widget.waitForFrame();
|
await widget.waitForFrame();
|
||||||
// Add B=Date, C=DateTime, D=Numeric
|
// Add B=Date, C=DateTime, D=Numeric
|
||||||
await gu.sendActions([
|
await gu.sendActions([
|
||||||
@ -753,7 +719,7 @@ describe('CustomWidgetsConfig', function () {
|
|||||||
requiredAccess: 'read table',
|
requiredAccess: 'read table',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
await accept();
|
await gu.acceptAccessRequest();
|
||||||
await widget.waitForFrame();
|
await widget.waitForFrame();
|
||||||
await gu.sendActions([
|
await gu.sendActions([
|
||||||
['AddVisibleColumn', 'Table1', 'Any', {type: 'Any'}],
|
['AddVisibleColumn', 'Table1', 'Any', {type: 'Any'}],
|
||||||
@ -801,7 +767,7 @@ describe('CustomWidgetsConfig', function () {
|
|||||||
await gu.sendActions([
|
await gu.sendActions([
|
||||||
['AddVisibleColumn', 'Table1', 'Choice', {type: 'Choice', widgetOptions: JSON.stringify(widgetOptions)}]
|
['AddVisibleColumn', 'Table1', 'Choice', {type: 'Choice', widgetOptions: JSON.stringify(widgetOptions)}]
|
||||||
]);
|
]);
|
||||||
await accept();
|
await gu.acceptAccessRequest();
|
||||||
await widget.waitForFrame();
|
await widget.waitForFrame();
|
||||||
|
|
||||||
await gu.selectSectionByTitle('Widget');
|
await gu.selectSectionByTitle('Widget');
|
||||||
@ -837,7 +803,7 @@ describe('CustomWidgetsConfig', function () {
|
|||||||
requiredAccess: 'read table',
|
requiredAccess: 'read table',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
await accept();
|
await gu.acceptAccessRequest();
|
||||||
await widget.waitForFrame();
|
await widget.waitForFrame();
|
||||||
// Add some columns, to remove later
|
// Add some columns, to remove later
|
||||||
await gu.selectSectionByTitle('Table');
|
await gu.selectSectionByTitle('Table');
|
||||||
@ -918,7 +884,7 @@ describe('CustomWidgetsConfig', function () {
|
|||||||
requiredAccess: 'read table',
|
requiredAccess: 'read table',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
await accept();
|
await gu.acceptAccessRequest();
|
||||||
await widget.waitForFrame();
|
await widget.waitForFrame();
|
||||||
assert.deepEqual(await widget.onRecordsMappings(), null);
|
assert.deepEqual(await widget.onRecordsMappings(), null);
|
||||||
assert.deepEqual(await widget.onRecords(), [
|
assert.deepEqual(await widget.onRecords(), [
|
||||||
@ -1059,7 +1025,7 @@ describe('CustomWidgetsConfig', function () {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
it(`should get null options`, async () => {
|
it(`should get null options`, async () => {
|
||||||
await selectAccess(access);
|
await gu.changeWidgetAccess(access);
|
||||||
await widget.waitForFrame();
|
await widget.waitForFrame();
|
||||||
assert.equal(await widget.onOptions(), null);
|
assert.equal(await widget.onOptions(), null);
|
||||||
assert.equal(await widget.access(), access);
|
assert.equal(await widget.access(), access);
|
||||||
@ -1067,7 +1033,7 @@ describe('CustomWidgetsConfig', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it(`should save config options and inform about it the main widget`, async () => {
|
it(`should save config options and inform about it the main widget`, async () => {
|
||||||
await selectAccess(access);
|
await gu.changeWidgetAccess(access);
|
||||||
await widget.waitForFrame();
|
await widget.waitForFrame();
|
||||||
// Save config and check if normal widget received new configuration
|
// Save config and check if normal widget received new configuration
|
||||||
const config = {key: 1} as const;
|
const config = {key: 1} as const;
|
||||||
@ -1084,7 +1050,7 @@ describe('CustomWidgetsConfig', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it(`should save and read options`, async () => {
|
it(`should save and read options`, async () => {
|
||||||
await selectAccess(access);
|
await gu.changeWidgetAccess(access);
|
||||||
await widget.waitForFrame();
|
await widget.waitForFrame();
|
||||||
// Make sure get options returns null.
|
// Make sure get options returns null.
|
||||||
assert.equal(await widget.getOptions(), null);
|
assert.equal(await widget.getOptions(), null);
|
||||||
@ -1096,7 +1062,7 @@ describe('CustomWidgetsConfig', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it(`should save and read options by keys`, async () => {
|
it(`should save and read options by keys`, async () => {
|
||||||
await selectAccess(access);
|
await gu.changeWidgetAccess(access);
|
||||||
await widget.waitForFrame();
|
await widget.waitForFrame();
|
||||||
// Should support key operations
|
// Should support key operations
|
||||||
const set = async (key: string, value: any) => {
|
const set = async (key: string, value: any) => {
|
||||||
@ -1117,7 +1083,7 @@ describe('CustomWidgetsConfig', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it(`should call configure method`, async () => {
|
it(`should call configure method`, async () => {
|
||||||
await selectAccess(access);
|
await gu.changeWidgetAccess(access);
|
||||||
await widget.waitForFrame();
|
await widget.waitForFrame();
|
||||||
// Make sure configure wasn't called yet.
|
// Make sure configure wasn't called yet.
|
||||||
assert.isFalse(await widget.wasConfigureCalled());
|
assert.isFalse(await widget.wasConfigureCalled());
|
||||||
@ -1158,26 +1124,26 @@ describe('CustomWidgetsConfig', function () {
|
|||||||
// Select widget without request
|
// Select widget without request
|
||||||
await toggleWidgetMenu();
|
await toggleWidgetMenu();
|
||||||
await clickOption(NORMAL_WIDGET);
|
await clickOption(NORMAL_WIDGET);
|
||||||
assert.isFalse(await hasPrompt());
|
assert.isFalse(await gu.hasAccessPrompt());
|
||||||
assert.equal(await givenAccess(), AccessLevel.none);
|
assert.equal(await gu.widgetAccess(), AccessLevel.none);
|
||||||
assert.equal(await widget.access(), AccessLevel.none);
|
assert.equal(await widget.access(), AccessLevel.none);
|
||||||
// Select widget that requests read access.
|
// Select widget that requests read access.
|
||||||
await toggleWidgetMenu();
|
await toggleWidgetMenu();
|
||||||
await clickOption(READ_WIDGET);
|
await clickOption(READ_WIDGET);
|
||||||
assert.isTrue(await hasPrompt());
|
assert.isTrue(await gu.hasAccessPrompt());
|
||||||
assert.equal(await givenAccess(), AccessLevel.none);
|
assert.equal(await gu.widgetAccess(), AccessLevel.none);
|
||||||
assert.equal(await widget.access(), AccessLevel.none);
|
assert.equal(await widget.access(), AccessLevel.none);
|
||||||
await accept();
|
await gu.acceptAccessRequest();
|
||||||
assert.equal(await givenAccess(), AccessLevel.read_table);
|
assert.equal(await gu.widgetAccess(), AccessLevel.read_table);
|
||||||
assert.equal(await widget.access(), AccessLevel.read_table);
|
assert.equal(await widget.access(), AccessLevel.read_table);
|
||||||
// Select widget that requests full access.
|
// Select widget that requests full access.
|
||||||
await toggleWidgetMenu();
|
await toggleWidgetMenu();
|
||||||
await clickOption(FULL_WIDGET);
|
await clickOption(FULL_WIDGET);
|
||||||
assert.isTrue(await hasPrompt());
|
assert.isTrue(await gu.hasAccessPrompt());
|
||||||
assert.equal(await givenAccess(), AccessLevel.none);
|
assert.equal(await gu.widgetAccess(), AccessLevel.none);
|
||||||
assert.equal(await widget.access(), AccessLevel.none);
|
assert.equal(await widget.access(), AccessLevel.none);
|
||||||
await accept();
|
await gu.acceptAccessRequest();
|
||||||
assert.equal(await givenAccess(), AccessLevel.full);
|
assert.equal(await gu.widgetAccess(), AccessLevel.full);
|
||||||
assert.equal(await widget.access(), AccessLevel.full);
|
assert.equal(await widget.access(), AccessLevel.full);
|
||||||
await gu.undo(5);
|
await gu.undo(5);
|
||||||
});
|
});
|
||||||
|
280
test/nbrowser/LinkingBidirectional.ts
Normal file
280
test/nbrowser/LinkingBidirectional.ts
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
import * as gu from 'test/nbrowser/gristUtils';
|
||||||
|
import {setupTestSuite} from 'test/nbrowser/testUtils';
|
||||||
|
import {assert, driver, Key} from 'mocha-webdriver';
|
||||||
|
|
||||||
|
describe('LinkingBidirectional', function() {
|
||||||
|
this.timeout('60s');
|
||||||
|
|
||||||
|
const cleanup = setupTestSuite({team: true});
|
||||||
|
let session: gu.Session;
|
||||||
|
|
||||||
|
afterEach(() => gu.checkForErrors());
|
||||||
|
|
||||||
|
before(async function() {
|
||||||
|
session = await gu.session().login();
|
||||||
|
await session.tempDoc(cleanup, 'Class Enrollment.grist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update cursor when sections cursor-linked together', async function() {
|
||||||
|
// Setup new page with "Classes1"
|
||||||
|
await gu.addNewPage('Table', 'Classes', {});
|
||||||
|
await gu.renameActiveSection('Classes1');
|
||||||
|
|
||||||
|
// Set its cursor to row 2
|
||||||
|
await gu.getCell('Semester', 2, 'Classes1').click();
|
||||||
|
|
||||||
|
// Add Classes2 & 3
|
||||||
|
await gu.addNewSection('Table', 'Classes', { selectBy: 'Classes1' });
|
||||||
|
await gu.renameActiveSection('Classes2');
|
||||||
|
await gu.addNewSection('Table', 'Classes', { selectBy: 'Classes2' });
|
||||||
|
await gu.renameActiveSection('Classes3');
|
||||||
|
|
||||||
|
// Make sure that linking put them all on row 2
|
||||||
|
assert.deepEqual(await getCursorAndArrow('Classes1'), {arrow: null, cursor: {rowNum: 2, col: 0}});
|
||||||
|
assert.deepEqual(await getCursorAndArrow('Classes2'), {arrow: 2, cursor: {rowNum: 2, col: 0}});
|
||||||
|
assert.deepEqual(await getCursorAndArrow('Classes3'), {arrow: 2, cursor: {rowNum: 2, col: 0}});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should propagate cursor-links downstream', async function() {
|
||||||
|
// Cursor link from upstream should drive a given section, but we can still move the downstream cursor
|
||||||
|
// To other positions. The downstream linking should use whichever of its ancestors was most recently clicked
|
||||||
|
|
||||||
|
// Set cursors: Sec1 on row 1, Sec2 on row 2
|
||||||
|
await gu.getCell('Semester', 1, 'Classes1').click();
|
||||||
|
await gu.getCell('Semester', 2, 'Classes2').click();
|
||||||
|
|
||||||
|
// Sec1 should be on row 1. Sec2 should be on 2 even though link is telling it to be on 1. Sec3 should be on 2
|
||||||
|
assert.deepEqual(await getCursorAndArrow('Classes1'), {arrow: null, cursor: {rowNum: 1, col: 0}});
|
||||||
|
assert.deepEqual(await getCursorAndArrow('Classes2'), {arrow: 1, cursor: {rowNum: 2, col: 0}});
|
||||||
|
assert.deepEqual(await getCursorAndArrow('Classes3'), {arrow: 2, cursor: {rowNum: 2, col: 0}});
|
||||||
|
|
||||||
|
// click on Sec3 row 3
|
||||||
|
await gu.getCell('Semester', 3, 'Classes3').click();
|
||||||
|
|
||||||
|
// Cursors should be on 1, 2, 3, with arrows lagging 1 step behind
|
||||||
|
assert.deepEqual(await getCursorAndArrow('Classes1'), {arrow: null, cursor: {rowNum: 1, col: 0}});
|
||||||
|
assert.deepEqual(await getCursorAndArrow('Classes2'), {arrow: 1, cursor: {rowNum: 2, col: 0}});
|
||||||
|
assert.deepEqual(await getCursorAndArrow('Classes3'), {arrow: 2, cursor: {rowNum: 3, col: 0}});
|
||||||
|
|
||||||
|
// When clicking on oldest ancestor, it should now take priority over all downstream cursors again
|
||||||
|
// Click on S1 row 4
|
||||||
|
await gu.getCell('Semester', 4, 'Classes1').click();
|
||||||
|
// assert that all are on 4
|
||||||
|
assert.deepEqual(await getCursorAndArrow('Classes1'), {arrow: null, cursor: {rowNum: 4, col: 0}});
|
||||||
|
assert.deepEqual(await getCursorAndArrow('Classes2'), {arrow: 4, cursor: {rowNum: 4, col: 0}});
|
||||||
|
assert.deepEqual(await getCursorAndArrow('Classes3'), {arrow: 4, cursor: {rowNum: 4, col: 0}});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work correctly when linked into a cycle', async function() {
|
||||||
|
// Link them all into a cycle.
|
||||||
|
await gu.selectBy('Classes3');
|
||||||
|
|
||||||
|
// Now, any section should drive all the other sections
|
||||||
|
|
||||||
|
// Click on row 1 in Sec1
|
||||||
|
await gu.getCell('Semester', 1, 'Classes1').click();
|
||||||
|
assert.deepEqual(await getCursorAndArrow('Classes1'), {arrow: 1, cursor: {rowNum: 1, col: 0}});
|
||||||
|
assert.deepEqual(await getCursorAndArrow('Classes2'), {arrow: 1, cursor: {rowNum: 1, col: 0}});
|
||||||
|
assert.deepEqual(await getCursorAndArrow('Classes3'), {arrow: 1, cursor: {rowNum: 1, col: 0}});
|
||||||
|
|
||||||
|
// Click on row 2 in Sec2
|
||||||
|
await gu.getCell('Semester', 2, 'Classes2').click();
|
||||||
|
assert.deepEqual(await getCursorAndArrow('Classes1'), {arrow: 2, cursor: {rowNum: 2, col: 0}});
|
||||||
|
assert.deepEqual(await getCursorAndArrow('Classes2'), {arrow: 2, cursor: {rowNum: 2, col: 0}});
|
||||||
|
assert.deepEqual(await getCursorAndArrow('Classes3'), {arrow: 2, cursor: {rowNum: 2, col: 0}});
|
||||||
|
|
||||||
|
// Click on row 3 in Sec3
|
||||||
|
await gu.getCell('Semester', 3, 'Classes3').click();
|
||||||
|
assert.deepEqual(await getCursorAndArrow('Classes1'), {arrow: 3, cursor: {rowNum: 3, col: 0}});
|
||||||
|
assert.deepEqual(await getCursorAndArrow('Classes2'), {arrow: 3, cursor: {rowNum: 3, col: 0}});
|
||||||
|
assert.deepEqual(await getCursorAndArrow('Classes3'), {arrow: 3, cursor: {rowNum: 3, col: 0}});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep position after refresh', async function() {
|
||||||
|
// Nothing should change after a refresh
|
||||||
|
await gu.reloadDoc();
|
||||||
|
|
||||||
|
assert.deepEqual(await getCursorAndArrow('Classes1'), {arrow: 3, cursor: {rowNum: 3, col: 0}});
|
||||||
|
assert.deepEqual(await getCursorAndArrow('Classes2'), {arrow: 3, cursor: {rowNum: 3, col: 0}});
|
||||||
|
assert.deepEqual(await getCursorAndArrow('Classes3'), {arrow: 3, cursor: {rowNum: 3, col: 0}});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update linking when filters change', async function() {
|
||||||
|
// Let's filter out the current row ("Spring 2019") from Classes2
|
||||||
|
await gu.selectSectionByTitle('Classes2');
|
||||||
|
await gu.sortAndFilter()
|
||||||
|
.then(x => x.addColumn())
|
||||||
|
.then(x => x.clickColumn('Semester'))
|
||||||
|
.then(x => x.close())
|
||||||
|
.then(x => x.click());
|
||||||
|
|
||||||
|
// Open the pinned filter, and filter out the date.
|
||||||
|
await gu.openPinnedFilter('Semester')
|
||||||
|
.then(x => x.toggleValue('Spring 2019'))
|
||||||
|
.then(x => x.close());
|
||||||
|
|
||||||
|
// Classes2 got its cursor moved when we filtered out its current row, so all sections should now
|
||||||
|
// have moved to row 1
|
||||||
|
assert.deepEqual(await getCursorAndArrow('Classes1'), {arrow: 1, cursor: {rowNum: 1, col: 0}});
|
||||||
|
assert.deepEqual(await getCursorAndArrow('Classes2'), {arrow: 1, cursor: {rowNum: 1, col: 0}});
|
||||||
|
assert.deepEqual(await getCursorAndArrow('Classes3'), {arrow: 1, cursor: {rowNum: 1, col: 0}});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should propagate cursor linking even through a filtered-out section', async function() {
|
||||||
|
// Row 3 (from Classes1 and Classes3) is no longer present in Classes2
|
||||||
|
// Make sure it can still get the value through the link from Classes1 -> 2 -> 3
|
||||||
|
|
||||||
|
// Click on row 3 in Sec1
|
||||||
|
await gu.getCell('Semester', 3, 'Classes1').click();
|
||||||
|
|
||||||
|
// Classes1 and 3 should be on row3
|
||||||
|
// Classes2 should still be on row 1 (from the last test case), and should have no arrow (since it's desynced)
|
||||||
|
assert.deepEqual(await getCursorAndArrow('Classes1'), {arrow: 3, cursor: {rowNum: 3, col: 0}});
|
||||||
|
assert.deepEqual(await getCursorAndArrow('Classes2'), {arrow: null, cursor: {rowNum: 1, col: 0}});
|
||||||
|
assert.deepEqual(await getCursorAndArrow('Classes3'), {arrow: 3, cursor: {rowNum: 3, col: 0}});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep position after refresh when section is filtered out', async function() {
|
||||||
|
// Save the filter in Classes2
|
||||||
|
await gu.selectSectionByTitle('Classes2');
|
||||||
|
await gu.sortAndFilter()
|
||||||
|
.then(x => x.save());
|
||||||
|
|
||||||
|
// Go to Classes1, and refresh (its curser will be restored).
|
||||||
|
await gu.selectSectionByTitle('Classes1');
|
||||||
|
await gu.reloadDoc();
|
||||||
|
|
||||||
|
// Nothing should change after a refresh
|
||||||
|
assert.deepEqual(await getCursorAndArrow('Classes1'), {arrow: 3, cursor: {rowNum: 3, col: 0}});
|
||||||
|
assert.deepEqual(await getCursorAndArrow('Classes2'), {arrow: null, cursor: {rowNum: 1, col: 0}});
|
||||||
|
assert.deepEqual(await getCursorAndArrow('Classes3'), {arrow: 3, cursor: {rowNum: 3, col: 0}});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should navigate to correct cells and sections', async function() {
|
||||||
|
await gu.getCell('Semester', 3, 'Classes1').click();
|
||||||
|
const anchor = await gu.getAnchor();
|
||||||
|
await gu.wipeToasts();
|
||||||
|
|
||||||
|
await gu.onNewTab(async () => {
|
||||||
|
await driver.get(anchor);
|
||||||
|
await gu.waitForDocToLoad();
|
||||||
|
|
||||||
|
assert.deepEqual(await getCursorAndArrow('Classes1'), {arrow: 3, cursor: {rowNum: 3, col: 0}});
|
||||||
|
assert.deepEqual(await getCursorAndArrow('Classes2'), {arrow: null, cursor: {rowNum: 1, col: 0}});
|
||||||
|
assert.deepEqual(await getCursorAndArrow('Classes3'), {arrow: 3, cursor: {rowNum: 3, col: 0}});
|
||||||
|
});
|
||||||
|
|
||||||
|
await gu.getCell('Semester', 3, 'Classes3').click();
|
||||||
|
const anchor2 = await gu.getAnchor();
|
||||||
|
await gu.wipeToasts();
|
||||||
|
|
||||||
|
await gu.onNewTab(async () => {
|
||||||
|
await driver.get(anchor2);
|
||||||
|
await gu.waitForDocToLoad();
|
||||||
|
|
||||||
|
assert.deepEqual(await getCursorAndArrow('Classes1'), {arrow: 3, cursor: {rowNum: 3, col: 0}});
|
||||||
|
assert.deepEqual(await getCursorAndArrow('Classes2'), {arrow: null, cursor: {rowNum: 1, col: 0}});
|
||||||
|
assert.deepEqual(await getCursorAndArrow('Classes3'), {arrow: 3, cursor: {rowNum: 3, col: 0}});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update cursor-linking when clicking on a cell, even if the position doesn't change", async function() {
|
||||||
|
// Classes2 should still be on row 1, but the other 2 sections have their cursors on row 3
|
||||||
|
// If we click on Classes2, even if the cursor position doesn't change, we should still record
|
||||||
|
// Classes2 as now being the most recently-clicked section and have it drive the linking of the other 2 sections
|
||||||
|
// (i.e. they should change to match it)
|
||||||
|
|
||||||
|
// Click on row 1 in Classes2 (it's got filtered-out-rows, and so is desynced from the other 2)
|
||||||
|
await gu.getCell('Semester', 1, 'Classes2').click();
|
||||||
|
|
||||||
|
// All sections should now jump to join it
|
||||||
|
assert.deepEqual(await getCursorAndArrow('Classes1'), {arrow: 1, cursor: {rowNum: 1, col: 0}});
|
||||||
|
assert.deepEqual(await getCursorAndArrow('Classes2'), {arrow: 1, cursor: {rowNum: 1, col: 0}});
|
||||||
|
assert.deepEqual(await getCursorAndArrow('Classes3'), {arrow: 1, cursor: {rowNum: 1, col: 0}});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update cursor linking correctly when the selected row is deleted', async function() {
|
||||||
|
// When deleting a row, the current section moves down 1 row (preserving the cursorpos), but other
|
||||||
|
// sections with the same table jump to row 1
|
||||||
|
|
||||||
|
// If we're cursor-linked, make sure that doesn't cause a desync.
|
||||||
|
|
||||||
|
// Click on row 2 in Classes1 and delete it
|
||||||
|
await gu.getCell('Semester', 2, 'Classes1').click();
|
||||||
|
await gu.removeRow(2);
|
||||||
|
|
||||||
|
// Classes1 and Classes3 should both have moved to the next row (previously rowNum3, but now is rowNum 2)
|
||||||
|
// Classes2 has this row filtered out, so it should have jumped to row1 instead (desynced from the others)
|
||||||
|
assert.deepEqual(await getCursorAndArrow('Classes1'), {arrow: 2, cursor: {rowNum: 2, col: 0}});
|
||||||
|
assert.deepEqual(await getCursorAndArrow('Classes2'), {arrow: null, cursor: {rowNum: 1, col: 0}});
|
||||||
|
assert.deepEqual(await getCursorAndArrow('Classes3'), {arrow: 2, cursor: {rowNum: 2, col: 0}});
|
||||||
|
|
||||||
|
// Undo the row-deletion
|
||||||
|
await gu.undo();
|
||||||
|
|
||||||
|
// The first 2 sections will still be on row2, but verify that Classes2 also joins them
|
||||||
|
assert.deepEqual(await getCursorAndArrow('Classes1'), {arrow: 2, cursor: {rowNum: 2, col: 0}});
|
||||||
|
assert.deepEqual(await getCursorAndArrow('Classes2'), {arrow: 2, cursor: {rowNum: 2, col: 0}});
|
||||||
|
assert.deepEqual(await getCursorAndArrow('Classes3'), {arrow: 2, cursor: {rowNum: 2, col: 0}});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support custom filters', async function() {
|
||||||
|
// Add a new custom section with a widget.
|
||||||
|
await gu.addNewPage('Table', 'Classes', {});
|
||||||
|
|
||||||
|
// Rename this section as Data.
|
||||||
|
await gu.renameActiveSection('Data');
|
||||||
|
|
||||||
|
// Add new custom section with a widget.
|
||||||
|
await gu.addNewSection('Custom', 'Classes', { selectBy: 'Data' });
|
||||||
|
|
||||||
|
// Rename this section as Custom.
|
||||||
|
await gu.renameActiveSection('Custom');
|
||||||
|
|
||||||
|
// Make sure it can be used as a filter.
|
||||||
|
await gu.changeWidgetAccess('read table');
|
||||||
|
// Tell grist that we can be linked to.
|
||||||
|
await gu.customCode(grist => grist.sectionApi.configure({allowSelectBy: true}));
|
||||||
|
|
||||||
|
// Now link them together.
|
||||||
|
await gu.selectSectionByTitle('Data');
|
||||||
|
await gu.selectBy('Custom');
|
||||||
|
|
||||||
|
// And now lets filter some rows in the Data by using custom widget API.
|
||||||
|
await gu.selectSectionByTitle('Custom');
|
||||||
|
await gu.customCode(grist => grist.setSelectedRows([1, 2, 3, 4]).catch(() => {}));
|
||||||
|
|
||||||
|
// Check for the error. It was easy to create an infinite loop with the call above, and checking
|
||||||
|
// for errors here was catching it.
|
||||||
|
await gu.checkForErrors();
|
||||||
|
|
||||||
|
// This link was not valid, so custom section wasn't filtered, but it managed to filter Data.
|
||||||
|
// But in the process, the configuration was cleared.
|
||||||
|
|
||||||
|
// Switch section, so that UI is refreshed.
|
||||||
|
await gu.selectSectionByTitle('Data');
|
||||||
|
await gu.selectSectionByTitle('Custom');
|
||||||
|
|
||||||
|
// Make sure we can't select Data by Custom anymore.
|
||||||
|
await gu.openSelectByForSection('Custom');
|
||||||
|
|
||||||
|
// Make sure it is empty.
|
||||||
|
assert.deepEqual(await driver.findAll('.test-select-menu .test-select-row', e => e.getText()), ['Select Widget']);
|
||||||
|
await gu.sendKeys(Key.ESCAPE);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
interface CursorArrowInfo {
|
||||||
|
arrow: null | number;
|
||||||
|
cursor: {rowNum: number, col: number};
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: this differs from getCursorSelectorInfo in LinkingSelector.ts
|
||||||
|
// That function only gives the cursorPos if that section is actively selected (false otherwise), whereas this
|
||||||
|
// function returns it always
|
||||||
|
async function getCursorAndArrow(sectionName: string): Promise<CursorArrowInfo> {
|
||||||
|
return {
|
||||||
|
arrow: await gu.getArrowPosition(sectionName),
|
||||||
|
cursor: await gu.getCursorPosition(sectionName),
|
||||||
|
};
|
||||||
|
}
|
@ -207,9 +207,8 @@ interface CursorSelectorInfo {
|
|||||||
|
|
||||||
async function getCursorSelectorInfo(section: WebElement): Promise<CursorSelectorInfo> {
|
async function getCursorSelectorInfo(section: WebElement): Promise<CursorSelectorInfo> {
|
||||||
const hasCursor = await section.find('.active_cursor').isPresent();
|
const hasCursor = await section.find('.active_cursor').isPresent();
|
||||||
const hasSelector = await section.find('.link_selector_row').isPresent();
|
|
||||||
return {
|
return {
|
||||||
linkSelector: hasSelector && Number(await section.find('.link_selector_row .gridview_data_row_num').getText()),
|
linkSelector: await gu.getSelectorPosition(section).then(r => r ?? false),
|
||||||
cursor: hasCursor && await gu.getCursorPosition(section),
|
cursor: hasCursor && await gu.getCursorPosition(section),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -7,11 +7,13 @@ import * as fse from 'fs-extra';
|
|||||||
import escapeRegExp = require('lodash/escapeRegExp');
|
import escapeRegExp = require('lodash/escapeRegExp');
|
||||||
import noop = require('lodash/noop');
|
import noop = require('lodash/noop');
|
||||||
import startCase = require('lodash/startCase');
|
import startCase = require('lodash/startCase');
|
||||||
import { assert, driver as driverOrig, error, Key, WebElement, WebElementPromise } from 'mocha-webdriver';
|
import { assert, By, driver as driverOrig, error, Key, WebElement, WebElementPromise } from 'mocha-webdriver';
|
||||||
import { stackWrapFunc, stackWrapOwnMethods, WebDriver } from 'mocha-webdriver';
|
import { stackWrapFunc, stackWrapOwnMethods, WebDriver } from 'mocha-webdriver';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import * as PluginApi from 'app/plugin/grist-plugin-api';
|
||||||
|
|
||||||
import {csvDecodeRow} from 'app/common/csvFormat';
|
import {csvDecodeRow} from 'app/common/csvFormat';
|
||||||
|
import { AccessLevel } from 'app/common/CustomWidget';
|
||||||
import { decodeUrl } from 'app/common/gristUrls';
|
import { decodeUrl } from 'app/common/gristUrls';
|
||||||
import { FullUser, UserProfile } from 'app/common/LoginSessionAPI';
|
import { FullUser, UserProfile } from 'app/common/LoginSessionAPI';
|
||||||
import { resetOrg } from 'app/common/resetOrg';
|
import { resetOrg } from 'app/common/resetOrg';
|
||||||
@ -567,6 +569,31 @@ export async function rightClick(cell: WebElement) {
|
|||||||
await driver.withActions((actions) => actions.contextClick(cell));
|
await driver.withActions((actions) => actions.contextClick(cell));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the selector position in the Grid view section (or null if not present).
|
||||||
|
* Selector is the black box around the row number.
|
||||||
|
*/
|
||||||
|
export async function getSelectorPosition(section?: WebElement|string) {
|
||||||
|
if (typeof section === 'string') { section = await getSection(section); }
|
||||||
|
section = section ?? await driver.findWait('.active_section', 4000);
|
||||||
|
const hasSelector = await section.find('.link_selector_row').isPresent();
|
||||||
|
return hasSelector && Number(await section.find('.link_selector_row .gridview_data_row_num').getText());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the arrow position in the Grid view section (or null if no arrow is present).
|
||||||
|
*/
|
||||||
|
export async function getArrowPosition(section?: WebElement|string) {
|
||||||
|
if (typeof section === 'string') { section = await getSection(section); }
|
||||||
|
section = section ?? await driver.findWait('.active_section', 4000);
|
||||||
|
const arrow = section.find('.gridview_data_row_info.linked_dst');
|
||||||
|
const hasArrow = await arrow.isPresent();
|
||||||
|
return hasArrow ? Number(
|
||||||
|
await arrow.findElement(By.xpath("./..")) //Get its parent
|
||||||
|
.getText()
|
||||||
|
) : null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns {rowNum, col} object representing the position of the cursor in the active view
|
* Returns {rowNum, col} object representing the position of the cursor in the active view
|
||||||
* section. RowNum is a 1-based number as in the row headers, and col is a 0-based index for
|
* section. RowNum is a 1-based number as in the row headers, and col is a 0-based index for
|
||||||
@ -859,18 +886,18 @@ export async function importUrlDialog(url: string): Promise<void> {
|
|||||||
await driver.switchTo().defaultContent();
|
await driver.switchTo().defaultContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executed passed function in the context of given iframe, and then switching back to original context
|
* Executed passed function in the context of given iframe, and then switching back to original context
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export async function doInIframe<T>(iframe: WebElement, func: () => Promise<T>) {
|
export async function doInIframe<T>(iframe: WebElement, func: () => Promise<T>) {
|
||||||
try {
|
try {
|
||||||
await driver.switchTo().frame(iframe);
|
await driver.switchTo().frame(iframe);
|
||||||
return await func();
|
return await func();
|
||||||
} finally {
|
} finally {
|
||||||
await driver.switchTo().defaultContent();
|
await driver.switchTo().defaultContent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts or resets the collections of UserActions. This should be followed some time later by
|
* Starts or resets the collections of UserActions. This should be followed some time later by
|
||||||
@ -3178,6 +3205,75 @@ export async function setGristTheme(options: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes custom code inside active custom widget.
|
||||||
|
*/
|
||||||
|
export async function customCode(fn: (grist: typeof PluginApi) => void) {
|
||||||
|
const section = await driver.findWait('.active_section iframe', 4000);
|
||||||
|
return await doInIframe(section, async () => {
|
||||||
|
return await driver.executeScript(`(${fn})(grist)`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets or sets widget access level (doesn't deal with prompts).
|
||||||
|
*/
|
||||||
|
export async function widgetAccess(level?: AccessLevel) {
|
||||||
|
const text = {
|
||||||
|
[AccessLevel.none]: 'No document access',
|
||||||
|
[AccessLevel.read_table]: 'Read selected table',
|
||||||
|
[AccessLevel.full]: 'Full document access',
|
||||||
|
};
|
||||||
|
if (!level) {
|
||||||
|
const currentAccess = await driver.find('.test-config-widget-access .test-select-open').getText();
|
||||||
|
return Object.entries(text).find(e => e[1] === currentAccess)![0];
|
||||||
|
} else {
|
||||||
|
await driver.find('.test-config-widget-access .test-select-open').click();
|
||||||
|
await driver.findContent('.test-select-menu li', text[level]).click();
|
||||||
|
await waitForServer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if access prompt is visible.
|
||||||
|
*/
|
||||||
|
export async function hasAccessPrompt() {
|
||||||
|
return await driver.find('.test-config-widget-access-accept').isPresent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accepts new access level.
|
||||||
|
*/
|
||||||
|
export async function acceptAccessRequest() {
|
||||||
|
await driver.find('.test-config-widget-access-accept').click();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rejects new access level.
|
||||||
|
*/
|
||||||
|
export async function rejectAccessRequest() {
|
||||||
|
await driver.find('.test-config-widget-access-reject').click();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets widget access level (deals with requests).
|
||||||
|
*/
|
||||||
|
export async function changeWidgetAccess(access: 'read table'|'full'|'none') {
|
||||||
|
await openWidgetPanel();
|
||||||
|
|
||||||
|
// if the current access is ok do nothing
|
||||||
|
if ((await widgetAccess()) === access) {
|
||||||
|
// unless we need to confirm it
|
||||||
|
if (await hasAccessPrompt()) {
|
||||||
|
await acceptAccessRequest();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// else switch access level
|
||||||
|
await widgetAccess(access as AccessLevel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
} // end of namespace gristUtils
|
} // end of namespace gristUtils
|
||||||
|
|
||||||
stackWrapOwnMethods(gristUtils);
|
stackWrapOwnMethods(gristUtils);
|
||||||
|
Loading…
Reference in New Issue
Block a user