mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
083a20417e
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
281 lines
13 KiB
TypeScript
281 lines
13 KiB
TypeScript
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),
|
|
};
|
|
}
|