gristlabs_grist-core/test/nbrowser/LinkingBidirectional.ts
Jarosław Sadziński 083a20417e (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
2023-10-10 15:31:48 +02:00

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),
};
}