(core) Preserving cursor position when linked state is removed.

Summary:
- Preserving cursor position when linked state is removed.
- Moving linking tests to grist core.
- Disabling yarn offline mirror for grist-core. This helps testing grist-core when it is imported as a submodule.
- Moving one test for linked section from ReferenceColumns.ts to RightPanelSelectBy.ts.

Test Plan: Updated

Reviewers: paulfitz

Reviewed By: paulfitz

Subscribers: paulfitz

Differential Revision: https://phab.getgrist.com/D3795
pull/426/head
Jarosław Sadziński 1 year ago
parent 6e3f0f2b35
commit e93dd4bc6f

@ -0,0 +1,5 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
yarn-offline-mirror false

@ -140,8 +140,12 @@ function BaseView(gristDoc, viewSectionModel, options) {
return linking && linking.cursorPos ? linking.cursorPos() : null;
}).extend({deferred: true}));
// Update the cursor whenever linkedRowId() changes.
this.autoDispose(this.linkedRowId.subscribe(rowId => this.setCursorPos({rowId: rowId || 'new'})));
// Update the cursor whenever linkedRowId() changes (but only if we have any linking).
this.autoDispose(this.linkedRowId.subscribe(rowId => {
if (this.viewSection.linkingState.peek()) {
this.setCursorPos({rowId: rowId || 'new'});
}
}));
// Indicated whether editing the section should be disabled given the current linking state.
this.disableEditing = this.autoDispose(ko.computed(() => {
@ -693,7 +697,11 @@ BaseView.prototype.onRowResize = function(rowModels) {
* Called when user selects a different row which drives the link-filtering of this section.
*/
BaseView.prototype.onLinkFilterChange = function(rowId) {
this.setCursorPos({rowIndex: 0});
// If this section is linked, go to the first row as the row previously selected may no longer
// be visible.
if (this.viewSection.linkingState.peek()) {
this.setCursorPos({rowIndex: 0});
}
};
/**

@ -11,7 +11,7 @@ describe('ReferenceColumns', function() {
describe('rendering', function() {
before(async function() {
session = await gu.session().teamSite.login();
await session.tempDoc(cleanup, 'Favorite_Films_With_Linked_Ref.grist');
await session.tempDoc(cleanup, 'Favorite_Films.grist');
await gu.toggleSidePanel('right');
await driver.find('.test-config-data').click();
@ -147,23 +147,6 @@ describe('ReferenceColumns', function() {
]
);
});
it('should have linked card for friends', async () => {
// Open the All page.
await driver.findContentWait('.test-treeview-itemHeader', /Linked Friends/, 2000).click();
await gu.waitForDocToLoad();
await driver.findContentWait('.field_clip', /Mary/, 2000).click();
await gu.waitForServer();
await driver.findContentWait('.g_record_detail_label', /Title/, 2000).click();
assert.equal(await gu.getActiveCell().getText(), 'Alien');
await driver.findContentWait('.field_clip', /Jarek/, 2000).click();
await gu.waitForServer();
await driver.findContentWait('.g_record_detail_label', /Title/, 2000).click();
assert.equal(await gu.getActiveCell().getText(), '');
});
});
describe('autocomplete', function() {

@ -0,0 +1,295 @@
import {assert, driver, Key} from 'mocha-webdriver';
import * as gu from 'test/nbrowser/gristUtils';
import {server, setupTestSuite} from 'test/nbrowser/testUtils';
describe('RightPanel', function() {
this.timeout(20000);
setupTestSuite();
afterEach(() => gu.checkForErrors());
it('should open/close panel, and reflect the current section', async function() {
// Open a document with multiple views and multiple sections.
await server.simulateLogin("Chimpy", "chimpy@getgrist.com", 'nasa');
const doc = await gu.importFixturesDoc('chimpy', 'nasa', 'Horizon', 'World.grist', false);
await driver.get(`${server.getHost()}/o/nasa/doc/${doc.id}`);
// Check current view and section name.
assert.equal(await gu.getActiveSectionTitle(6000), 'CITY');
assert.equal(await driver.find('.test-bc-page').getAttribute('value'), 'City');
// Open side pane, and check it shows the right section.
await gu.toggleSidePanel('right');
await driver.find('.test-config-widget').click();
assert.equal(await gu.isSidePanelOpen('right'), true);
assert.equal(await driver.find('.test-right-widget-title').value(), 'CITY');
// Check that the tab's name reflects suitable text
assert.equal(await driver.find('.test-right-tab-pagewidget').getText(), 'Table');
assert.equal(await driver.find('.test-right-tab-field').getText(), 'Column');
// Switch to Field tab, check that it shows the right field.
await driver.find('.test-right-tab-field').click();
assert.equal(await driver.find('.test-field-label').value(), "Name");
// Check to a different field, check a different field is shown.
await driver.sendKeys(Key.RIGHT);
assert.equal(await driver.find('.test-field-label').value(), "Country");
// Click to a different section, check a different field is shown.
await gu.getSection('CITY Card List').find('.detail_row_num').click();
assert.equal(await driver.find('.test-field-label').value(), "Name");
// Check that the tab's name reflects suitable text
assert.equal(await driver.find('.test-right-tab-pagewidget').getText(), 'Card List');
assert.equal(await driver.find('.test-right-tab-field').getText(), 'Field');
// Close panel, check it's hidden.
await gu.toggleSidePanel('right');
assert.equal(await gu.isSidePanelOpen('right'), false);
assert.equal(await driver.find('.config_item').isPresent(), false);
// Reopen panel, check it's still right.
await gu.toggleSidePanel('right');
assert.equal(await driver.find('.test-field-label').value(), "Name");
// Switch to the section tab, check the new section is reflected.
await driver.find('.test-right-tab-pagewidget').click();
assert.equal(await driver.find('.test-right-widget-title').value(), 'CITY Card List');
// Switch to a different view, check the new section is reflected.
await driver.findContent('.test-treeview-itemHeader', /Country/).click();
assert.equal(await driver.find('.test-right-widget-title').value(), 'COUNTRY');
// Switch to field tab; check the new field is reflected.
await driver.find('.test-right-tab-field').click();
assert.equal(await driver.find('.test-field-label').value(), "Code");
});
it('should not cause errors when switching pages with Field tab open', async () => {
// There was an error ("this.calcSize is not a function") switching between pages when the
// active section changes type and Field tab is open, triggered by an unnecessary rebuilding
// of FieldConfigTab.
// Check that the active field tab is called "Column" (since the active section is "Table")
// and is open to column "Code".
assert.equal(await driver.find('.test-right-tab-field').getText(), 'Column');
assert.equal(await driver.find('.test-field-label').value(), "Code");
// Switch to the "City" page. Check that the tab is now called "Field" (since the active section is of
// type "CardList"), and open to the field "Name".
await driver.findContent('.test-treeview-itemHeader', /City/).click();
assert.equal(await driver.find('.test-right-tab-field').getText(), 'Field');
assert.equal(await driver.find('.test-field-label').value(), "Name");
// Check that this did not cause client-side errors.
await gu.checkForErrors();
// Now switch back, and check for errors again.
await driver.findContent('.test-treeview-itemHeader', /Country/).click();
await gu.checkForErrors();
});
it('should show tools when requested', async function() {
// Select specific view/section/field. Close side-pane.
await gu.getCell({col: "Name", rowNum: 3}).click();
assert.equal(await driver.find('.test-field-label').value(), "Name");
await gu.toggleSidePanel('right');
assert.equal(await gu.isSidePanelOpen('right'), false);
// Click Activity Log.
assert.equal(await driver.find('.action_log').isPresent(), false);
await driver.find('.test-tools-log').click();
await gu.waitToPass(() => // Click might not work while panel is sliding out to open.
driver.findContentWait('.test-doc-history-tabs .test-select-button', 'Activity', 500).click());
// Check that panel is shown, and correct.
assert.equal(await gu.isSidePanelOpen('right'), true);
assert.equal(await driver.find('.test-right-tab-field').isPresent(), false);
assert.equal(await driver.find('.action_log').isDisplayed(), true);
// Click "x", Check expected section config shown.
await driver.find('.test-right-tool-close').click();
assert.equal(await driver.find('.test-right-tab-field').getText(), 'Column');
assert.equal(await driver.find('.test-field-label').value(), "Name");
// TODO: polish data validation and then uncomment
/*
// Click Validations. Check it's shown and correct.
await driver.find('.test-tools-validate').click();
assert.equal(await driver.findContent('.config_item', /Validations/).isDisplayed(), true);
// Close panel. Switch to another view.
await gu.toggleSidePanel('right');
assert.equal(await gu.isSidePanelOpen('right'), false);
assert.equal(await driver.findContent('.config_item', /Validations/).isPresent(), false);
await driver.findContent('.test-treeview-itemHeader', /Country/).click();
// Open panel. Check Validations are still shown.
await gu.toggleSidePanel('right');
assert.equal(await driver.findContent('.config_item', /Validations/).isDisplayed(), true);
await driver.find('.test-right-tool-close').click();
*/
});
it('should keep panel state on reload', async function() {
// Check the panel is currently open and showing Field options.
assert.equal(await gu.isSidePanelOpen('right'), true);
assert.equal(await driver.find('.test-field-label').value(), "Name");
// Reload the page, and click the same cell as before.
await driver.navigate().refresh();
assert.equal(await gu.getActiveSectionTitle(3000), 'COUNTRY');
await gu.waitForServer();
await gu.getCell({col: "Name", rowNum: 3}).click();
// Check the panel is still open and showing the same Field options.
assert.equal(await gu.isSidePanelOpen('right'), true);
assert.equal(await driver.find('.test-field-label').value(), "Name");
});
it('\'SELECTOR FOR\' should work correctly', async function() {
// open the Data tab
await driver.find('.test-right-tab-pagewidget').click();
await driver.find('.test-config-data').click();
// open a page that has linked section
await driver.findContent('.test-treeview-itemHeader', /Country/).click();
// wait for data to load
assert(await gu.getActiveSectionTitle(3000));
await gu.waitForServer();
// select a view section that does not select other section
await gu.getSection('COUNTRY Card List').click();
// check that selector-for is not present
assert.equal(await driver.find('.test-selector-for').isPresent(), false);
// select a view section that does select other section
await gu.getSection('COUNTRY').click();
// check that selector-of is present and that all selected section are listed
assert.equal(await driver.find('.test-selector-for').isPresent(), true);
assert.deepEqual(await driver.findAll('.test-selector-for-entry', (e) => e.getText()), [
"CITY",
"COUNTRYLANGUAGE",
"COUNTRY Card List",
]);
});
it('\'Edit Data Selection\' should allow to change link', async () => {
// select COUNTRY DETAIL
await gu.getSection('CITY').click();
// open page widget picker
await driver.find('.test-pwc-editDataSelection').click();
// remove link
await driver.find('.test-wselect-selectby').doClick();
await driver.findContent('.test-wselect-selectby option', /Select Widget/).doClick();
// click save
await driver.find('.test-wselect-addBtn').doClick();
await gu.waitForServer();
// Go to the first record.
await gu.sendKeys(Key.chord(await gu.modKey(), Key.UP));
// Check that link was removed, by going to Aruba.
await gu.getSection('COUNTRY').click();
await gu.getCell(0, 1).click();
// City section should stay where it was
assert.equal(await gu.getCell(0, 1, 'CITY').getText(), 'Kabul');
// re-set the link
await gu.getSection('CITY').click();
await driver.find('.test-pwc-editDataSelection').click();
await driver.find('.test-wselect-selectby').click();
await driver.findContent('.test-wselect-selectby option', /Country$/).click();
await driver.find('.test-wselect-addBtn').doClick();
await gu.waitForServer();
// check link is set
await gu.getSection('COUNTRY').click();
await gu.getCell(0, 1).click();
assert.equal(await gu.getCell(0, 1, 'CITY').getText(), 'Oranjestad');
});
it('should not cause errors when switching pages with Table tab open', async () => {
// There were an error doing eigher one of 1) switching to `Code View`, or 2) removing the
// active page, when the Table tab was open, because: both caused the activeView to be set to an
// empty model causes some computed property of the ViewSectionRec to fail. This is what this
// test is aiming at catching.
await gu.toggleSidePanel('right', 'open');
await driver.find('.test-config-widget').click();
assert.deepEqual(await gu.getPageNames(), ['City', 'Country', 'CountryLanguage']);
// adds a new page
await gu.addNewPage(/Table/, /City/);
assert.deepEqual(await gu.getPageNames(), ['City', 'Country', 'CountryLanguage', 'New page']);
// remove that page
await gu.openPageMenu(/New page/);
await driver.find('.grist-floating-menu .test-docpage-remove').click();
await gu.waitForServer();
// check pages were removed and nothing break
assert.deepEqual(await gu.getPageNames(), ['City', 'Country', 'CountryLanguage']);
await gu.checkForErrors();
// now switch to `Code View`
await driver.find('.test-tools-code').click();
assert.equal(await driver.findWait('.g-code-viewer', 1000).isPresent(), true);
// check nothing broke
await gu.checkForErrors();
// switch back to City
await gu.getPageItem(/City/).click();
});
it('should not cause errors when editing summary table with `Change Widget` button', async () => {
// Changing the grouped by columns using the `Change Widget` used to throw `TypeError: Cannot
// read property `toUpperCase` of undefined`. The goal of this test is to prevent future
// regression.
// Create a summary table of City groupbed by country
await gu.addNewPage(/Table/, /City/, {summarize: [/Country/]});
// open right panel Widget
await gu.toggleSidePanel('right', 'open');
await driver.find('.test-right-tab-pagewidget').click();
await driver.find('.test-config-widget').click();
// click `Change Widget`
await driver.findContent('.test-right-panel button', /Change Widget/).click();
// remove column `Country` and save
await gu.selectWidget(/Table/, /City/, {summarize: []});
// check there were no error
await gu.checkForErrors();
});
it('should not raise errors when opening with table\'s `Widget Options`', async function() {
// Open right panel and select 'Column'
await gu.toggleSidePanel('right', 'open');
await driver.find('.test-right-tab-field').click();
// Close the right panel
await gu.toggleSidePanel('right', 'close');
// Open the right panel using the table's `Widget option`
await gu.openSectionMenu('viewLayout');
await driver.find('.test-widget-options').click();
await gu.checkForErrors();
});
});

@ -0,0 +1,201 @@
import { addToRepl, assert, driver} from 'mocha-webdriver';
import * as gu from 'test/nbrowser/gristUtils';
import {server, setupTestSuite} from 'test/nbrowser/testUtils';
describe('RightPanelSelectBy', function() {
this.timeout(20000);
setupTestSuite();
addToRepl('gu2', gu);
async function setup() {
await server.simulateLogin("Chimpy", "chimpy@getgrist.com", 'nasa');
const doc = await gu.importFixturesDoc('chimpy', 'nasa', 'Horizon', 'Favorite_Films_With_Linked_Ref.grist', false);
await driver.get(`${server.getHost()}/o/nasa/doc/${doc.id}`);
await gu.waitForDocToLoad();
await gu.toggleSidePanel('right', 'open');
await driver.find('.test-config-data').click();
}
it('should allow linking section with same table', async function() {
await setup();
// open page `All`
await driver.findContentWait('.test-treeview-itemHeader', /All/, 2000).click();
await gu.waitForDocToLoad();
await openSelectByForSection('PERFORMANCES DETAIL');
// the dollar in /...record$/ makes sure we match against the table main node and not a ref
// columns such as '...record.Film'
assert.equal(await driver.findContent('.test-select-row', /Performances record$/).isPresent(), true);
});
it('should not allow linking same section', async function() {
assert.equal(await driver.findContent('.test-select-row', /Performances detail/i).isPresent(), false);
});
it('should allow linking to/from a ref column', async function() {
// Performance record.Film links both from a ref column and to a ref column
assert.equal(await driver.findContent('.test-select-row', /Performances record.*Film/i).isPresent(), true);
});
it('should successfully link on select', async function() {
// select a link
await driver.findContent('.test-select-row', /Performances record$/).click();
await gu.waitForServer();
// Check that selections in 1st section are mirrored by the 2nd section.
await gu.getSection('PERFORMANCES RECORD').click();
await gu.getCell(0, 3).click();
assert.equal(await driver.find('.g_record_detail_value').getText(), 'Don Rickles');
});
it('should allow to remove link', async function() {
await openSelectByForSection('PERFORMANCES DETAIL');
await driver.findContent('.test-select-row', /Select Widget/).click();
await gu.waitForServer();
// Check that selections in 1st section are NOT mirrored by the 2nd section.
await gu.getSection('PERFORMANCES RECORD').click();
await gu.getCell(0, 1).click();
assert.equal(await driver.find('.g_record_detail_value').getText(), 'Don Rickles');
// undo, link is expected to be set for next test
await gu.undo();
});
it('should disallow creating cycles', async function() {
await openSelectByForSection('PERFORMANCES RECORD');
assert.equal(await driver.findContent('.test-select-row', /Performances detail/).isPresent(), false);
});
it('should not allow selecting from a chart or custom sections', async function() {
// open the 'Films' page
await driver.findContent('.test-treeview-itemHeader', /Films/).click();
await gu.waitForDocToLoad();
// Adds a chart widget
await gu.addNewSection(/Chart/, /Films/);
// open `SELECT BY`
await openSelectByForSection('FILMS');
// check that there is a chart and we cannot link from it
assert.equal(await gu.getSection('FILMS CHART').isPresent(), true);
assert.equal(await driver.findContent('.test-select-row', /Films chart/).isPresent(), false);
// undo
await gu.undo();
});
it('should update filter-linking tied to reference when value changes', async function() {
// Add a filter-linked section (of Performances) tied to a Ref column (FRIENDS.Favorite_Film).
await gu.getPageItem('Friends').click();
await gu.waitForServer();
await gu.addNewSection(/Table/, /Performances/);
await openSelectByForSection('Performances');
assert.equal(await driver.findContent('.test-select-row', /FRIENDS.*Favorite Film/).isPresent(), true);
await driver.findContent('.test-select-row', /FRIENDS.*Favorite Film/).click();
await gu.waitForServer();
// Select a row in FRIENDS.
const cell = await gu.getCell({section: 'Friends', col: 'Favorite Film', rowNum: 6});
assert.equal(await cell.getText(), 'Alien');
await cell.click();
// Check that the linked table reflects the selected row.
assert.deepEqual(await gu.getVisibleGridCells(
{section: 'Performances', cols: ['Actor', 'Film'], rowNums: [1, 2]}), [
'Sigourney Weaver', 'Alien',
'', '',
]);
// Change a value in FRIENDS.Favorite_Film column.
await gu.sendKeys('Toy');
await driver.findContent('.test-ref-editor-item', /Toy Story/).click();
await gu.waitForServer();
// Check that the linked table of Performances got updated.
assert.deepEqual(await gu.getVisibleGridCells(
{section: 'Performances', cols: ['Actor', 'Film'], rowNums: [1, 2, 3, 4]}), [
'Tom Hanks', 'Toy Story',
'Tim Allen', 'Toy Story',
'Don Rickles', 'Toy Story',
'', ''
]);
await gu.undo(2);
});
it('should update cursor-linking tied to reference when value changes', async function() {
// Add a cursor-linked card widget (of Films) tied to a Ref column (FRIENDS.Favorite_Film).
await gu.getPageItem('Friends').click();
await gu.waitForServer();
await gu.addNewSection(/Card/, /Films/);
await openSelectByForSection('Films Card');
assert.equal(await driver.findContent('.test-select-row', /FRIENDS.*Favorite Film/).isPresent(), true);
await driver.findContent('.test-select-row', /FRIENDS.*Favorite Film/).click();
await gu.waitForServer();
// Select a row in FRIENDS.
const cell = await gu.getCell({section: 'Friends', col: 'Favorite Film', rowNum: 6});
assert.equal(await cell.getText(), 'Alien');
await cell.click();
// Check that the linked card reflects the selected row.
assert.equal(await driver.find('.g_record_detail_value').getText(), 'Alien');
assert.equal(await driver.findContent('.g_record_detail_value', /19/).getText(), 'May 25th, 1979');
// Change the value in FRIENDS.Favorite_Film column.
await gu.sendKeys('Toy');
await driver.findContent('.test-ref-editor-item', /Toy Story/).click();
await gu.waitForServer();
// Check that the linked card of Films got updated.
assert.equal(await driver.find('.g_record_detail_value').getText(), 'Toy Story');
assert.equal(await driver.findContent('.g_record_detail_value', /19/).getText(), 'November 22nd, 1995');
// Select the 'new' row in FRIENDS.
const newCell = await gu.getCell({section: 'Friends', col: 'Favorite Film', rowNum: 7});
assert.equal(await newCell.getText(), '');
await newCell.click();
// Card should have also moved to the 'new' record
const cardFields = await driver.findAll('.g_record_detail_value');
for (const cardField of cardFields) {
assert.equal(await cardField.getText(), '');
}
await gu.undo(2);
});
it('should have linked card for friends', async () => {
// Open the All page.
await driver.findContentWait('.test-treeview-itemHeader', /Linked Friends/, 2000).click();
await gu.waitForDocToLoad();
await driver.findContentWait('.field_clip', /Mary/, 2000).click();
await gu.waitForServer();
await driver.findContentWait('.g_record_detail_label', /Title/, 2000).click();
assert.equal(await gu.getActiveCell().getText(), 'Alien');
await driver.findContentWait('.field_clip', /Jarek/, 2000).click();
await gu.waitForServer();
await driver.findContentWait('.g_record_detail_label', /Title/, 2000).click();
assert.equal(await gu.getActiveCell().getText(), '');
});
xit('should list options following the order of the section in the view layout', async function() {
// TODO
});
});
export async function openSelectByForSection(section: string) {
await gu.getSection(section).click();
await driver.find('.test-right-select-by').click();
}

@ -0,0 +1,215 @@
import mapValues = require('lodash/mapValues');
import { assert, driver, Key } from 'mocha-webdriver';
import * as gu from 'test/nbrowser/gristUtils';
import { server, setupTestSuite } from 'test/nbrowser/testUtils';
describe("SelectBy", function() {
this.timeout(20000);
setupTestSuite();
let doc: any;
function formatOption(main: string, srcColumn?: string, tgtColumn?: string) {
let ret = main;
ret += srcColumn ? ' \u2022 ' + srcColumn : '';
ret += tgtColumn ? ' \u2192 ' + tgtColumn : '';
return ret;
}
it("should offer correct options", async () => {
await server.simulateLogin("Chimpy", "chimpy@getgrist.com", "nasa");
doc = await gu.importFixturesDoc('chimpy', 'nasa', 'Horizon', 'selectBy.grist', false);
// check tables
const api = gu.createHomeApi('Chimpy', 'nasa');
assert.deepInclude(await api.getTable(doc.id, '_grist_Tables'), {
id: [1, 2, 3, 4],
tableId: ['Table1', 'Table2', 'Table3', 'Table3_summary_A'],
summarySourceTable: [0, 0, 0, 3],
});
// check visible columns (no manualSort columns)
const allColumns = await api.getTable(doc.id, '_grist_Tables_column');
const visibleColumns = mapValues(allColumns, (vals) => vals.filter((v, i) => allColumns.colId[i] !== 'manualSort'));
assert.deepInclude(visibleColumns, {
id: [2, 3, 6, 10, 12, 13, 14, 15, 16],
colId: ['table2_ref', 'table3_ref', 'table3_ref', 'A', 'A', 'table3_ref_2', 'A', 'group', 'count'],
parentId: [1, 1, 2, 3, 1, 1, 4, 4, 4],
type: ['Ref:Table2', 'Ref:Table3', 'Ref:Table3', 'Numeric', 'Text', 'Ref:Table3', 'Numeric',
'RefList:Table3', 'Int'],
label: ['table2_ref', 'table3_ref', 'table3_ref', 'A', 'A', 'table3_ref_2', 'A', 'group', 'count'],
});
// open document
await driver.get(`${server.getHost()}/o/nasa/doc/${doc.id}`);
// create a new page with table1 and table2 as 2 tables
await gu.addNewPage(/Table/, /Table1/);
await gu.addNewSection(/Table/, /Table2/);
// beginning adding a new widget to page
await driver.findWait('.test-dp-add-new', 2000).doClick();
await driver.findWait('.test-dp-add-widget-to-page', 2000).doClick();
// select /Table/ /Table1/ and check options of `SELECT BY` drop down
await driver.findContent('.test-wselect-table', /Table1/).doClick();
await driver.findContent('.test-wselect-type', /Table/).doClick();
await driver.find('.test-wselect-selectby').doClick();
assert.deepEqual(
// let's ignore the first option which is an internal added by grainjs
await driver.findAll('.test-wselect-selectby option:not(:first-of-type)', (e) => e.getText()), [
// note: this is a very contrived example to test various possible links. Real world use
// cases are expected to be simpler, resulting in simpler list of options that are easier to
// navigate for the user than this one (in particular the `->` separator might rarely show
// up).
formatOption('Select Widget'),
formatOption("TABLE1"),
formatOption("TABLE1", 'table2_ref'),
formatOption('TABLE1', 'table3_ref', 'table3_ref'),
formatOption('TABLE1', 'table3_ref', 'table3_ref_2'),
formatOption('TABLE1', 'table3_ref_2', 'table3_ref'),
formatOption('TABLE1', 'table3_ref_2', 'table3_ref_2'),
formatOption('TABLE2'),
formatOption('TABLE2', 'table3_ref', 'table3_ref'),
formatOption('TABLE2', 'table3_ref', 'table3_ref_2'),
]
);
// select Table2 and check options of `SELECT BY` drop down
await driver.findContent('.test-wselect-table', /Table2/).doClick();
await driver.find('.test-wselect-selectby').doClick();
assert.deepEqual(
await driver.findAll('.test-wselect-selectby option:not(:first-of-type)', (e) => e.getText()), [
formatOption('Select Widget'),
formatOption('TABLE1', 'table2_ref'),
formatOption('TABLE1', 'table3_ref'),
formatOption('TABLE1', 'table3_ref_2'),
formatOption('TABLE2'),
formatOption('TABLE2', 'table3_ref')
]
);
// Selecting "New Table" should show no options.
await driver.findContent('.test-wselect-table', /New Table/).doClick();
assert.equal(await driver.find('.test-wselect-selectby').isPresent(), false);
assert.lengthOf(await driver.findAll('.test-wselect-selectby option'), 0);
// Selecting a regular table should show options again.
await driver.findContent('.test-wselect-table', /Table2/).doClick();
assert.equal(await driver.find('.test-wselect-selectby').isPresent(), true);
assert.lengthOf(await driver.findAll('.test-wselect-selectby option'), 7);
// Create a page with with charts and custom widget and then check that no linking is offered
await gu.addNewPage(/Chart/, /Table1/);
await gu.addNewSection(/Custom/, /Table2/);
// open add widget to page
await driver.findWait('.test-dp-add-new', 2000).doClick();
await driver.findWait('.test-dp-add-widget-to-page', 2000).doClick();
// select /Table/ /Table1/ and check no options are available
await driver.findContent('.test-wselect-table', /Table1/).doClick();
await driver.findContent('.test-wselect-type', /Table/).doClick();
assert.equal(await driver.find('.test-wselect-selectby').isPresent(), false);
// select Table2 and check no options are available
await driver.findContent('.test-wselect-table', /Table2/).doClick();
assert.equal(await driver.find('.test-wselect-selectby').isPresent(), false);
});
it('should handle summary table correctly', async () => {
// Notice that table of view 4 is a summary of Table3
const api = gu.createHomeApi('Chimpy', 'nasa');
assert.deepInclude((await api.getTable(doc.id, '_grist_Tables')), {
id: [1, 2, 3, 4],
tableId: ['Table1', 'Table2', 'Table3', 'Table3_summary_A'],
summarySourceTable: [0, 0, 0, 3],
});
// open Summary page
await driver.get(`${server.getHost()}/o/nasa/doc/${doc.id}/p/4`);
// add new widget to page
await driver.findWait('.test-dp-add-new', 2000).doClick();
await driver.findWait('.test-dp-add-widget-to-page', 2000).doClick();
// select Table3 and summarize
await driver.findContent('.test-wselect-table', /Table3/).find('.test-wselect-pivot').doClick();
// check selectBy options
assert.deepEqual(
await driver.findAll('.test-wselect-selectby option:not(:first-of-type)', (e) => e.getText()),
[],
);
await driver.sendKeys(Key.ESCAPE);
});
it('should show nav buttons for card view linked to its summary', async function() {
// Still on the page with summary of Table3, add a new Card widget linked to the summary
await gu.addNewSection(/Card$/, /Table3/, {selectBy: /TABLE3.*by A/});
// Check that we have a card view.
await gu.getCell({section: 'TABLE3 [by A]', rowNum: 1, col: 'A'}).click();
const section = await gu.getSection('TABLE3 Card');
assert.equal(await gu.getDetailCell({section, rowNum: 1, col: 'A'}).getText(), '1');
// Check there are nav buttons in the card view.
assert.equal(await section.find('.btn.detail-left').isPresent(), true);
assert.equal(await section.find('.btn.detail-right').isPresent(), true);
assert.equal(await section.find('.grist-single-record__menu__count').getText(), '1 OF 1');
// Now add a record to the source table using the card view.
await section.find('.btn.detail-add-btn').click();
assert.equal(await gu.getDetailCell({section, rowNum: 1, col: 'A'}).getText(), '');
await gu.getDetailCell({section, rowNum: 1, col: 'A'}).click();
await gu.sendKeys('1', Key.ENTER);
await gu.waitForServer();
// Check that this group now has 2 records.
assert.equal(await section.find('.grist-single-record__menu__count').getText(), '2 OF 2');
// There is another group that still has one record.
await gu.getCell({section: 'TABLE3 [by A]', rowNum: 2, col: 'A'}).click();
assert.equal(await section.find('.grist-single-record__menu__count').getText(), '1 OF 1');
});
it('should save link correctly', async () => {
// create new page with table2 as a table
await gu.addNewPage(/Table/, /Table2/);
// begin adding table1 as a table to page
await driver.findWait('.test-dp-add-new', 2000).doClick();
await driver.findWait('.test-dp-add-widget-to-page', 2000).doClick();
await driver.findContent('.test-wselect-table', /Table1/).doClick();
// select link
await driver.find('.test-wselect-selectby').doClick();
await driver.findContent('.test-wselect-selectby option', /Table2/i).doClick();
// click `add to page` btn
await driver.find('.test-wselect-addBtn').doClick();
await gu.waitForServer();
// check new section added and check content
assert.deepEqual(await gu.getVisibleGridCells(1, [1, 2]), ['a', 'b']);
// select other row in selector section
await gu.getSection('Table2').doClick();
await gu.getCell({col: 0, rowNum: 2}).doClick();
// check that linked section was filterd
await gu.getSection('Table1').doClick();
assert.deepEqual(await gu.getVisibleGridCells(1, [1, 2]), ['c', 'd']);
// check that an single undo remove the section
await gu.undo();
assert.equal(await gu.getSection('Table1').isPresent(), false);
// check that a single redo add and link the section
await gu.redo();
await gu.getSection('Table1').doClick();
assert.deepEqual(await gu.getVisibleGridCells(1, [1, 2]), ['a', 'b']);
});
});

@ -0,0 +1,245 @@
import * as _ from 'lodash';
import {addToRepl, assert, driver} from 'mocha-webdriver';
import * as gu from 'test/nbrowser/gristUtils';
import {server, setupTestSuite} from 'test/nbrowser/testUtils';
import {openSelectByForSection} from "./RightPanelSelectBy";
describe('SelectByRefList', function() {
this.timeout(60000);
setupTestSuite();
addToRepl('gu2', gu);
gu.bigScreen();
async function setup() {
await server.simulateLogin("Chimpy", "chimpy@getgrist.com", 'nasa');
const doc = await gu.importFixturesDoc('chimpy', 'nasa', 'Horizon',
'SelectByRefList.grist', false);
await driver.get(`${server.getHost()}/o/nasa/doc/${doc.id}`);
await gu.waitForDocToLoad();
await gu.toggleSidePanel('right');
await driver.find('.test-config-data').click();
}
it('should filter a table selected by ref and reflist columns', async function() {
await setup();
/*
The fixture document contains the following tables:
- LINKTARGET is the table we 'select by' another table, filtering it.
It has 3 columns: rownum, ref, and reflist
- REFTARGET is the target of almost all ref/reflist columns in the doc,
especially ref and reflist in LINKTARGET.
It has 3 rows and 1 column, the values are just a, b, and c.
- INDIRECTREF has matching rows referencing the rows in REFTARGET.
- REFLISTS has 2 reflist columns:
- reflist points to REFTARGET, similar to LINKTARGET
- LinkTarget_reflist points to LINKTARGET
checkSelectingRecords selects each of the 3 records in a table, one at a time,
and checks that LINKTARGET is filtered to the corresponding subarray.
First we test selecting by the ref column of LINKTARGET (2nd column).
Selecting by REFTARGET or INDIRECTREF should give the same result.
Because the values of REFTARGET are [a,b,c], selecting those rows
gives the rows in LINKTARGET with `a` and then `b` in the 2nd column in the first two subarrays.
LINKTARGET doesn't have any references to the last row of REFTARGET (c)
so the last group is empty.
All these groups include the new record row at the end.
*/
let sourceData = [
[
'1', 'a', 'a',
'2', 'a', 'b',
'', '', '',
],
[
'3', 'b', 'a\nb',
'4', 'b', '',
'', '', '',
],
[
'', '', '',
],
];
// The last row selected has value `c`, so that's the default value for the ref column
let newRow = ['99', 'c', ''];
await checkSelectingRecords('INDIRECTREF • A → ref', sourceData, newRow);
await checkSelectingRecords('REFTARGET → ref', sourceData, newRow);
// Now selecting based on the reflist column (3rd column)
// gives groups where that column *contains* `a`, then contains `b`, then
// nothing because again LINKTARGET doesn't have references to `c`
sourceData = [
[
'1', 'a', 'a',
'3', 'b', 'a\nb',
'', '', '',
],
[
'2', 'a', 'b',
'3', 'b', 'a\nb',
'', '', '',
],
[
'', '', '',
],
];
// The last row selected has value `c`, so that's the default value for the reflist column
newRow = ['99', '', 'c'];
await checkSelectingRecords('INDIRECTREF • A → reflist', sourceData, newRow);
await checkSelectingRecords('REFTARGET → reflist', sourceData, newRow);
// This case is quite simple and direct: LINKTARGET should show the rows listed
// in the REFLISTS.LinkTarget_reflist column. The values there are [1], [2], and [3, 4],
// which you can see in the first column below.
sourceData = [
[
'1', 'a', 'a',
'', '', '',
],
[
'2', 'a', 'b',
'', '', '',
],
[
'3', 'b', 'a\nb',
'4', 'b', '',
'', '', '',
],
];
// LINKTARGET is being filtered by the `id` column
// There's no column to set a default value for that would help
// The newly added row disappears immediately
// TODO should we be appending the new row ID to the reflist in the source table?
newRow = ['', '', ''];
await checkSelectingRecords('REFLISTS • LinkTarget_reflist', sourceData, newRow);
// Similar to the above but indirect. We connect LINKTARGET.ref and REFLISTS.reflist,
// which both point to REFTARGET. This gives rows where LINKTARGET.ref is contained in REFLISTS.reflist
// (in contrast to LINKTARGET.row_id contained in REFLISTS.LinkTarget_reflist).
// The values of REFLISTS.reflist are [a], [b], and [a, b],
// so the values in the second column must be in there.
sourceData = [
[
'1', 'a', 'a',
'2', 'a', 'b',
'', '', '',
],
[
'3', 'b', 'a\nb',
'4', 'b', '',
'', '', '',
],
[
'1', 'a', 'a',
'2', 'a', 'b',
'3', 'b', 'a\nb',
'4', 'b', '',
'', '', '',
],
];
// The last row selected has value [a,b] in REFLISTS.reflist
// LINKTARGET.ref can only take one reference, so it defaults to the first
newRow = ['99', 'a', ''];
await checkSelectingRecords('REFLISTS • reflist → ref', sourceData, newRow);
// Taking it one step further, connect LINKTARGET.reflist and REFLISTS.reflist.
// Gives rows where the two reflists *intersect*.
// The values of REFLISTS.reflist are [a], [b], and [a, b],
// so the values in the third column must be in there.
sourceData = [
[
'1', 'a', 'a',
'3', 'b', 'a\nb',
'', '', '',
],
[
'2', 'a', 'b',
'3', 'b', 'a\nb',
'', '', '',
],
[
'1', 'a', 'a',
'2', 'a', 'b',
'3', 'b', 'a\nb',
'', '', '',
],
];
// The last row selected has value [a,b] in REFLISTS.reflist
// LINKTARGET.reflist gets that as a default value
newRow = ['99', '', 'a\nb'];
await checkSelectingRecords('REFLISTS • reflist → reflist', sourceData, newRow);
});
});
/**
* Makes LINKTARGET select by selectBy.
* Asserts that clicking each row in the driving table filters LINKTARGET
* to the corresponding subarray of sourceData.
* Then creates a new row in LINKTARGET and asserts that it has values equal to newRow.
* The values will depend on the link and the last row selected in the driving table.
*/
async function checkSelectingRecords(selectBy: string, sourceData: string[][], newRow: string[]) {
await openSelectByForSection('LINKTARGET');
await driver.findContent('.test-select-row', new RegExp(selectBy + '$')).click();
await gu.waitForServer();
const selectByTable = selectBy.split(' ')[0];
await gu.getCell({section: selectByTable, col: 0, rowNum: 3}).click();
let numSourceRows = 0;
async function checkSourceGroup(sourceRowIndex: number) {
const sourceGroup = sourceData[sourceRowIndex];
numSourceRows = sourceGroup.length / 3;
assert.deepEqual(
await gu.getVisibleGridCells({
section: 'LINKTARGET',
cols: ['rownum', 'ref', 'reflist'],
rowNums: _.range(1, numSourceRows + 1),
}),
sourceGroup
);
}
for (let i = 0; i < sourceData.length; i++) {
await gu.getCell({section: selectByTable, col: 0, rowNum: i + 1}).click();
await checkSourceGroup(i);
}
// Create a new record with rownum=99
await gu.getCell({section: 'LINKTARGET', col: 'rownum', rowNum: numSourceRows}).click();
await gu.enterCell('99');
assert.deepEqual(
await gu.getVisibleGridCells({
section: 'LINKTARGET',
cols: ['rownum', 'ref', 'reflist'],
rowNums: [numSourceRows],
}),
newRow,
);
await gu.undo();
// Check recursiveMoveToCursorPos
// TODO row number 4 is not tested because sometimes there are no matching source records
// to move the cursor to and we don't have a solution for that case yet
for (let rowNum = 1; rowNum <= 3; rowNum++) {
// Click an anchor link
const anchorCell = gu.getCell({section: "Anchors", rowNum, col: 1});
await anchorCell.find('.test-tb-link').click();
// Check that navigation to the link target worked
assert.equal(await gu.getActiveSectionTitle(), "LINKTARGET");
assert.equal(await gu.getActiveCell().getText(), String(rowNum));
// Check that the link target is still filtered correctly by the link source,
// which should imply that the link source cursor is in the right place
await gu.selectSectionByTitle(selectByTable);
const srcRowNum = await gu.getSelectedRowNum();
await checkSourceGroup(srcRowNum - 1);
}
}

@ -0,0 +1,65 @@
import { getColValues } from 'app/common/DocActions';
import { UserAPI } from 'app/common/UserAPI';
import { assert, driver } from 'mocha-webdriver';
import * as gu from 'test/nbrowser/gristUtils';
import { server, setupTestSuite } from 'test/nbrowser/testUtils';
/**
* This is test for a bug that was on the Right Panel. [Select by] dropdown wasn't updated
* properly when summary tables (or linking in general) were updated.
*/
describe("SelectByRightPanel", function() {
this.timeout(20000);
setupTestSuite();
let docId: string;
let api: UserAPI;
before(async () => {
await server.simulateLogin("Chimpy", "chimpy@getgrist.com", "nasa");
docId = await gu.createNewDoc('chimpy', 'nasa', 'Horizon', 'Test22.grist');
api = gu.createHomeApi('Chimpy', 'nasa');
await driver.get(`${server.getHost()}/o/nasa/doc/${docId}`);
await gu.waitForDocToLoad();
await api.applyUserActions(docId, [
['UpdateRecord', '_grist_Tables_column', 2, { label: 'Company' }],
['UpdateRecord', '_grist_Tables_column', 3, { label: 'Category' }],
['UpdateRecord', '_grist_Tables_column', 4, { label: 'Month' }],
['AddVisibleColumn', 'Table1', 'Date', {}],
['AddVisibleColumn', 'Table1', 'Value', {}],
]);
// Add some dummy data.
await api.applyUserActions(docId, [
['BulkAddRecord', 'Table1', new Array(7).fill(null), getColValues([
{ Company: 'Mic', Category: 'Sales', Month: 1, Date: 1, Value: 100 },
{ Company: 'Mic', Category: 'Sales', Month: 1, Date: 2, Value: 100 },
{ Company: 'Mic', Category: 'Cloud', Month: 1, Date: 3, Value: 300 },
{ Company: 'Gog', Category: 'Sales', Month: 4, Date: 4, Value: 100 },
{ Company: 'Gog', Category: 'Adv', Month: 4, Date: 4, Value: 100 },
{ Company: 'Gog', Category: 'Adv', Month: 3, Date: 5, Value: 100 },
{ Company: 'Tes', Category: 'Sales', Month: 2, Date: 6, Value: 100 },
])],
]);
});
it("selects by right panel for", async () => {
// Add first summary table by Company
await gu.addNewSection('Table', 'Table1', { summarize: ['Company'] });
// Add second one by Category, we will update selection later using data selection on right panel
await gu.addNewSection('Table', 'Table1', { summarize: ['Category'] });
// Add Company to this table, select by should be filled with the new option.
await gu.toggleSidePanel('right', 'open');
await driver.find('.test-right-tab-pagewidget').click();
await driver.find('.test-config-data').click();
await driver.find('.test-pwc-editDataSelection').click();
await driver.findContent('.test-wselect-column', /Company/).doClick();
await driver.find('.test-wselect-addBtn').click();
await gu.waitForServer();
// Test that we have new option.
await driver.find('.test-right-select-by').click();
await driver.findContentWait('.test-select-menu li', "TABLE1 [by Company]", 200).click();
await gu.waitForServer();
assert.deepEqual(await gu.getVisibleGridCells('Company', [1, 2]), ['Mic', 'Mic']);
assert.deepEqual(await gu.getVisibleGridCells('Category', [1, 2]), ['Sales', 'Cloud']);
});
});

@ -0,0 +1,275 @@
import * as _ from 'lodash';
import {addToRepl, assert, driver} from 'mocha-webdriver';
import {enterRulePart, findDefaultRuleSet} from 'test/nbrowser/aclTestUtils';
import * as gu from 'test/nbrowser/gristUtils';
import {server, setupTestSuite} from 'test/nbrowser/testUtils';
import {openSelectByForSection} from "./RightPanelSelectBy";
describe('SelectBySummary', function() {
this.timeout(50000);
setupTestSuite();
addToRepl('gu2', gu);
gu.bigScreen();
before(async function() {
await server.simulateLogin("Chimpy", "chimpy@getgrist.com", 'nasa');
const doc = await gu.importFixturesDoc('chimpy', 'nasa', 'Horizon',
'SelectBySummary.grist', false);
await driver.get(`${server.getHost()}/o/nasa/doc/${doc.id}`);
await gu.waitForDocToLoad();
await gu.toggleSidePanel('right', 'open');
await driver.find('.test-config-data').click();
});
it('should filter a source table selected by a summary table', async function() {
await checkSelectingRecords(
['onetwo'],
[
'1', '16',
'2', '20',
],
[
[
'1', 'a', '1',
'1', 'b', '3',
'1', 'a\nb', '5',
'1', '', '7',
],
[
'2', 'a', '2',
'2', 'b', '4',
'2', 'a\nb', '6',
'2', '', '8',
],
],
);
await checkSelectingRecords(
['choices'],
[
'a', '14',
'b', '18',
'', '15',
],
[
[
'1', 'a', '1',
'2', 'a', '2',
'1', 'a\nb', '5',
'2', 'a\nb', '6',
],
[
'1', 'b', '3',
'2', 'b', '4',
'1', 'a\nb', '5',
'2', 'a\nb', '6',
],
[
'1', '', '7',
'2', '', '8',
],
],
);
await checkSelectingRecords(
['onetwo', 'choices'],
[
'1', 'a', '6',
'2', 'a', '8',
'1', 'b', '8',
'2', 'b', '10',
'1', '', '7',
'2', '', '8',
],
[
[
'1', 'a', '1',
'1', 'a\nb', '5',
],
[
'2', 'a', '2',
'2', 'a\nb', '6',
],
[
'1', 'b', '3',
'1', 'a\nb', '5',
],
[
'2', 'b', '4',
'2', 'a\nb', '6',
],
[
'1', '', '7',
],
[
'2', '', '8',
],
],
);
});
it('should create new rows in the source table (link target) with correct default values',
gu.revertChanges(async function() {
// Select the record with ['2', 'a'] in the summary table
// so those values will be used as defaults in the source table
await gu.getCell({section: 'TABLE1 [by onetwo, choices]', col: 'rownum', rowNum: 2}).click();
// Create a new record with rownum=99
await gu.getCell({section: 'TABLE1', col: 'rownum', rowNum: 3}).click();
await gu.enterCell('99');
assert.deepEqual(
await gu.getVisibleGridCells({
section: 'TABLE1',
cols: ['onetwo', 'choices', 'rownum'],
rowNums: [3],
}),
['2', 'a', '99'],
);
})
);
it('should filter a summary table selected by a less detailed summary table', async function() {
// Delete the Table1 widget so that we can hide the table in ACL without hiding the whole page.
const menu = await gu.openSectionMenu('viewLayout', 'TABLE1');
await menu.findContent('.test-cmd-name', 'Delete widget').click();
await gu.waitForServer();
// Open the ACL UI
await driver.find('.test-tools-access-rules').click();
await driver.findWait('.test-rule-set', 2000); // Wait for initialization fetch to complete.
// Deny all access to Table1.
await driver.findContentWait('button', /Add Table Rules/, 2000).click();
await driver.findContentWait('.grist-floating-menu li', /Table1/, 3000).click();
const ruleSet = findDefaultRuleSet(/Table1/);
await enterRulePart(ruleSet, 1, null, 'Deny All');
await driver.find('.test-rules-save').click();
await gu.waitForServer();
// Go back to the main page.
await gu.getPageItem('Table1').click();
// Now check filter linking, but with the detailed summary 'TABLE1 [by onetwo, choices]' as the target,
// selecting by the two less detailed summaries.
// There was a bug previously that this would not work while the summary source table (Table1) was hidden.
await checkSelectingRecords(
['onetwo'],
[
'1', '16',
'2', '20',
],
[
[
'1', 'a', '6',
'1', 'b', '8',
'1', '', '7',
],
[
'2', 'a', '8',
'2', 'b', '10',
'2', '', '8',
],
],
// This argument was not used in the previous test, as TABLE1 is the default.
'TABLE1 [by onetwo, choices]',
);
await checkSelectingRecords(
['choices'],
[
'a', '14',
'b', '18',
'', '15',
],
[
[
'1', 'a', '6',
'2', 'a', '8',
],
[
'1', 'b', '8',
'2', 'b', '10',
],
[
'1', '', '7',
'2', '', '8',
],
],
'TABLE1 [by onetwo, choices]',
);
});
});
/**
* Makes `targetSection` select by the existing summary table grouped by groubyColumns.
* Asserts that the summary table has the data summaryData under groubyColumns and rownum.
* Asserts that clicking each row in the summary table filters the target section
* to the corresponding subarray of `targetData`.
*/
async function checkSelectingRecords(
groubyColumns: string[],
summaryData: string[],
targetData: string[][],
targetSection: string = 'TABLE1',
) {
const summarySection = `TABLE1 [by ${groubyColumns.join(', ')}]`;
await openSelectByForSection(targetSection);
await driver.findContent('.test-select-row', summarySection).click();
await gu.waitForServer();
assert.deepEqual(
await gu.getVisibleGridCells({
section: summarySection,
cols: [...groubyColumns, 'rownum'],
rowNums: _.range(1, targetData.length + 1)
}),
summaryData,
);
async function checkTargetGroup(targetGroupIndex: number) {
const targetGroup = targetData[targetGroupIndex];
const countCell = await gu.getCell({section: summarySection, col: 'count', rowNum: targetGroupIndex + 1});
const numTargetRows = targetGroup.length / 3;
if (targetSection === 'TABLE1') {
assert.equal(await countCell.getText(), numTargetRows.toString());
}
await countCell.click();
assert.deepEqual(
await gu.getVisibleGridCells({
section: targetSection,
cols: ['onetwo', 'choices', 'rownum'],
rowNums: _.range(1, numTargetRows + 1),
}),
targetGroup
);
}
for (let i = 0; i < targetData.length; i++) {
await checkTargetGroup(i);
}
if (targetSection === 'TABLE1') {
// Check recursiveMoveToCursorPos
for (let rowNum = 1; rowNum <= 8; rowNum++) {
// Click an anchor link
const anchorCell = gu.getCell({section: "Anchors", rowNum, col: 1});
await anchorCell.find('.test-tb-link').click();
// Check that navigation to the link target worked
assert.equal(await gu.getActiveSectionTitle(), "TABLE1");
assert.equal(await gu.getActiveCell().getText(), String(rowNum));
// Check that the link target is still filtered correctly by the link source,
// which should imply that the link source cursor is in the right place
await gu.selectSectionByTitle(summarySection);
const summaryRowNum = await gu.getSelectedRowNum();
await checkTargetGroup(summaryRowNum - 1);
}
}
}

@ -0,0 +1,192 @@
import {addToRepl, assert, driver, Key} from 'mocha-webdriver';
import * as gu from 'test/nbrowser/gristUtils';
import {server, setupTestSuite} from 'test/nbrowser/testUtils';
import {openSelectByForSection} from "./RightPanelSelectBy";
describe('SelectBySummaryRef', function() {
this.timeout(20000);
setupTestSuite();
addToRepl('gu2', gu);
before(async function(){
await server.simulateLogin("Chimpy", "chimpy@getgrist.com", 'nasa');
const doc = await gu.importFixturesDoc('chimpy', 'nasa', 'Horizon',
'SelectBySummaryRef.grist', false);
await driver.get(`${server.getHost()}/o/nasa/doc/${doc.id}`);
await gu.waitForDocToLoad();
await gu.toggleSidePanel('right', 'open');
await driver.find('.test-config-data').click();
});
it('should give correct options when linking with a summary table with ref/reflist columns', async () => {
/*
The doc has 3 tables on one page with these columns:
1. Source:
- 'Other ref' is a reflist to 'Other'
2. Summary (a summary table of Source):
- 'Other ref' is the groupby column, so now it's a *ref* to 'Other', hence the column name in Source
- 'Source ref' is a ref to Source
- 'Source reflist' is a reflist to Source
- 'group' is the usual group column in summary tables (a reflist to Source) which is hidden from the options.
3. Other:
- 'Text' which won't be mentioned again since it's not a reference or anything.
- 'Source ref' is a ref to Source
*/
const sourceOptions = [
'Other',
'Other • Source ref',
'Summary',
'Summary • Other ref',
'Summary • Source ref',
'Summary • Source reflist',
];
const summaryOptions = [
'Source → Source ref',
'Source → Source reflist',
'Source • Other ref',
'Other',
'Other • Source ref → Source ref',
'Other • Source ref → Source reflist',
];
const otherOptions = [
'Source',
'Source • Other ref',
'Summary • Other ref',
'Summary → Source ref',
'Summary • Source ref',
'Summary • Source reflist',
];
await checkRightPanelSelectByOptions('Source', sourceOptions);
await checkRightPanelSelectByOptions('Other', otherOptions);
await checkRightPanelSelectByOptions('Summary', summaryOptions);
// Detach the summary table
await driver.find('.test-detach-button').click();
await gu.waitForServer();
// Each widget now has an option to select by the `group` reflist column of Summary
// in place of selecting by 'summaryness'.
const sourceOptionsWithGroup = [...sourceOptions, 'Summary • group'];
assert.deepEqual(sourceOptionsWithGroup.splice(2, 1), ['Summary']);
const otherOptionsWithGroup = [...otherOptions, 'Summary • group'];
assert.deepEqual(otherOptionsWithGroup.splice(3, 1), ['Summary → Source ref']);
// The summary table has also gained new options to select by the group column.
// There were no corresponding 'summaryness' options before because a summary table can't select by its source table
// (based purely on summaryness), only the other way around.
// Same for selecting by a reference to the source table.
// Such options are theoretically possible but are disabled because they're a bit weird,
// usually filter linking to a single row when cursor linking would make more sense and still not be very useful.
const summaryOptionsWithGroup = [...summaryOptions, 'Other • Source ref → group'];
summaryOptionsWithGroup.splice(2, 0, 'Source → group');
await checkRightPanelSelectByOptions('Source', sourceOptionsWithGroup);
await checkRightPanelSelectByOptions('Other', otherOptionsWithGroup);
await checkRightPanelSelectByOptions('Summary', summaryOptionsWithGroup);
// Undo detaching the summary table
await gu.undo();
});
it('should give correct options when adding a new summary table', async () => {
// Go to the second page in the document, which only has a widget for the 'Other' table
await gu.getPageItem("Other").click();
await gu.openAddWidgetToPage();
// Sanity check for the select by options of the plain table
await gu.selectWidget('Table', 'Other', {dontAdd: true});
await checkAddWidgetSelectByOptions([
'Other',
'Other • Source ref',
]);
// Select by options for summary tables of Other only exist when grouping by Source ref
await gu.selectWidget('Table', 'Other', {dontAdd: true, summarize: []});
await checkAddWidgetSelectByOptions(null);
await gu.selectWidget('Table', 'Other', {dontAdd: true, summarize: ['Text']});
await checkAddWidgetSelectByOptions(null);
await gu.selectWidget('Table', 'Other', {dontAdd: true, summarize: ['Source ref']});
// Note that in this case we are inferring options for a table that doesn't exist anywhere yet
await checkAddWidgetSelectByOptions([
'Other • Source ref',
]);
// Actually add the summary table in the last case above, selected by the only option
await gu.selectWidget('Table', 'Other',
{selectBy: 'Other • Source ref', summarize: ['Source ref']});
// Check that the link is actually there in the right panel and that the options are the same as when adding.
await checkCurrentSelectBy('Other • Source ref');
await checkRightPanelSelectByOptions('OTHER [by Source ref]', [
'Other • Source ref',
]);
// Undo adding the summary table
await gu.undo();
});
it('should give correct options when adding an existing summary table', async () => {
// Go to the second page in the document, which only has a widget for the 'Other' table
await gu.getPageItem("Other").click();
await gu.openAddWidgetToPage();
// Sanity check for the select by options of the plain table
await gu.selectWidget('Table', 'Source', {dontAdd: true});
await checkAddWidgetSelectByOptions([
'Other',
'Other • Source ref',
]);
// No select by options for summary table without groupby columns
await gu.selectWidget('Table', 'Source', {dontAdd: true, summarize: []});
await checkAddWidgetSelectByOptions(null);
// This summary table already exists on the first page.
// '→ Source ref' and '→ Source reflist' refer to formula columns in the summary table that
// don't exist by default.
await gu.selectWidget('Table', 'Source', {dontAdd: true, summarize: ['Other ref']});
await checkAddWidgetSelectByOptions([
'Other',
'Other • Source ref → Source ref',
'Other • Source ref → Source reflist',
]);
// Actually add the summary table in the last case above, selected by the second option
await gu.selectWidget('Table', 'Source',
{selectBy: 'Other • Source ref → Source ref', summarize: ['Other ref']});
// Check that the link is actually there in the right panel and that the options are the same as when adding.
await checkCurrentSelectBy('Other • Source ref → Source ref');
await checkRightPanelSelectByOptions('SOURCE [by Other ref]', [
'Other',
'Other • Source ref → Source ref',
'Other • Source ref → Source reflist',
]);
});
});
// Check that the 'Select by' menu in the right panel for the section has the expected options
async function checkRightPanelSelectByOptions(section: string, expected: string[]) {
await openSelectByForSection(section);
const actual = await driver.findAll('.test-select-menu .test-select-row', (e) => e.getText());
assert.deepEqual(actual, ['Select Widget', ...expected]);
await gu.sendKeys(Key.ESCAPE);
}
async function checkAddWidgetSelectByOptions(expected: string[]|null) {
const actual = await driver.findAll('.test-wselect-selectby option', (e) => e.getText());
assert.deepEqual(actual, expected === null ? [] : ['', 'Select Widget', ...expected]);
}
async function checkCurrentSelectBy(expected: string) {
const actual = await driver.find('.test-right-select-by').getText();
assert.equal(actual, expected);
}

@ -0,0 +1,204 @@
import { assert, driver, Key, stackWrapOwnMethods, WebElement } from 'mocha-webdriver';
import * as gu from 'test/nbrowser/gristUtils';
// tslint:disable:no-namespace
// Wrap in a namespace so that we can apply stackWrapOwnMethods to all the exports together.
namespace gristUtils {
/**
* Find .test-rule-table element for the given tableId.
*/
export function findTable(tableId: RegExp|'*'): WebElement {
const header = driver.findContent('.test-rule-table-header', tableId === '*' ? 'Default Rules' : tableId);
return header.findClosest('.test-rule-table');
}
/**
* Remove any rules within a .test-rule-table element, by hitting the trash buttons.
*/
export async function removeTable(tableId: RegExp|'*'): Promise<void> {
const header = driver.findContent('.test-rule-table-header', tableId === '*' ? 'Default Rules' : tableId);
if (await header.isPresent()) {
const table = header.findClosest('.test-rule-table');
await removeRules(table);
}
}
/**
* Remove any rules within an element, by hitting the trash button.
*/
export async function removeRules(el: WebElement): Promise<void> {
while (true) { // eslint-disable-line no-constant-condition
const remove = el.find('.test-rule-remove');
if (!await remove.isPresent()) { break; }
await remove.click();
}
}
/**
* Find .test-rule-set for the default rule set of the given tableId.
*/
export function findDefaultRuleSet(tableId: RegExp|'*'): WebElement {
const table = findTable(tableId);
const cols = table.findContent('.test-rule-resource', /All/);
return cols.findClosest('.test-rule-set');
}
/**
* Find a .test-rule-set at the given 1-based index, among the rule sets for the given tableId.
*/
export function findRuleSet(tableId: RegExp|'*', ruleNum: number): WebElement {
const table = findTable(tableId);
// Add one to skip table header element.
return table.find(`.test-rule-set:nth-child(${ruleNum + 1})`);
}
/**
* PartNum should be 1-based. Permissions is either the text of an option in the permission
* widget's dropdown menu (e.g. "Allow All") or a mapping of single-character bit to desired
* state, e.g. {R: 'deny', U: 'allow', C: ''}.
*/
export async function enterRulePart(
ruleSet: WebElement,
partNum: number,
aclFormula: string|null,
permissions: string|{[bit: string]: string},
memo?: string
) {
const part = ruleSet.find(`.test-rule-part-and-memo:nth-child(${partNum}) .test-rule-part`);
if (aclFormula !== null) {
await part.findWait('.test-rule-acl-formula .ace_editor', 500);
await part.find('.test-rule-acl-formula').doClick();
await driver.findWait('.test-rule-acl-formula .ace_focus', 500);
await gu.sendKeys(Key.HOME, Key.chord(Key.SHIFT, Key.END), Key.DELETE); // Clear formula
await gu.sendKeys(aclFormula, Key.ENTER);
}
if (typeof permissions === 'string') {
await part.find('.test-rule-permissions .test-permissions-dropdown').click();
await driver.findContent('.grist-floating-menu li', permissions).click();
} else {
for (const [bit, desired] of Object.entries(permissions)) {
const elem = await part.findContent('.test-rule-permissions div', bit);
if (!await elem.matches(`[class$=-${desired}]`)) {
await elem.click();
if (!await elem.matches(`[class$=-${desired}]`)) {
await elem.click();
if (!await elem.matches(`[class$=-${desired}]`)) {
throw new Error(`Can't set permission bit ${bit} to ${desired}`);
}
}
}
}
}
if (memo) {
const memoEditorPromise = ruleSet.find(`.test-rule-part-and-memo:nth-child(${partNum}) .test-rule-memo-editor`);
if (await memoEditorPromise.isPresent()) {
await memoEditorPromise.click();
await gu.clearInput();
} else {
await part.find('.test-rule-memo-add').click();
}
await gu.sendKeys(memo, Key.ENTER);
}
}
/**
* Enters formula in the ACL condition editor to trigger the autocomplete dropdown.
* @param ruleSet Rule set dom (for a table or default)
* @param partNum Index of the condition
* @param aclFormula Formula to enter
*/
export async function triggerAutoComplete(
ruleSet: WebElement, partNum: number, aclFormula: string
) {
const part = ruleSet.find(`.test-rule-part-and-memo:nth-child(${partNum}) .test-rule-part`);
if (aclFormula !== null) {
await part.findWait('.test-rule-acl-formula .ace_editor', 500);
await part.find('.test-rule-acl-formula').doClick();
await driver.findWait('.test-rule-acl-formula .ace_focus', 500);
await gu.sendKeys(Key.HOME, Key.chord(Key.SHIFT, Key.END), Key.DELETE); // Clear formula
await gu.sendKeys(aclFormula);
}
}
/**
* Fetch rule text from an element. Uses Ace text if that is non-empty, in order
* to get complete text of long rules. If Ace text is empty, returns any plain
* text (e.g. "Everyone Else").
*/
export async function getRuleText(el: WebElement) {
const plainText = await el.getText();
const aceText = await gu.getAceText(el);
return aceText || plainText;
}
/**
* Read the rules within an element in a format that is easy to
* compare with.
*/
export async function getRules(el: WebElement): Promise<Array<{
formula: string, perm: string,
res?: string,
memo?: string}>> {
const ruleSets = await el.findAll('.test-rule-set');
const results: Array<{formula: string, perm: string,
res?: string,
memo?: string}> = [];
for (const ruleSet of ruleSets) {
const scope = ruleSet.find('.test-rule-resource');
const res = (await scope.isPresent()) ? (await scope.getText()) : undefined;
const parts = await ruleSet.findAll('.test-rule-part-and-memo');
for (const part of parts) {
const formula = await getRuleText(await part.find('.test-rule-acl-formula'));
const perms = await part.find('.test-rule-permissions').findAll('div');
const permParts: Array<string> = [];
for (const perm of perms) {
const content = await perm.getText();
if (content.length !== 1) { continue; }
const classes = await perm.getAttribute('class');
const prefix = classes.includes('-deny') ? '-' :
(classes.includes('-allow') ? '+' : '');
permParts.push(prefix ? (prefix + content) : '');
}
const hasMemo = await part.find('.test-rule-memo').isPresent();
const memo = hasMemo ? await part.find('.test-rule-memo input').value() : undefined;
results.push({formula, perm: permParts.join(''),
...(memo ? {memo} : {}),
...(res ? {res} : {})
});
}
}
return results;
}
/**
* Check if there is an extra "add" button compared to the number of rules
* within an element.
*/
export async function hasExtraAdd(el: WebElement): Promise<boolean> {
const parts = await el.findAll('.test-rule-part-and-memo');
const adds = await el.findAll('.test-rule-add');
return adds.length === parts.length + 1;
}
/**
* Assert that the Save button is currently disabled because the rules are
* saved.
*/
export async function assertSaved() {
assert.equal(await driver.find('.test-rules-non-save').getText(), 'Saved');
assert.equal(await driver.find('.test-rules-save').getText(), '');
}
/**
* Assert that the Save button is currently enabled because the rules have
* changed.
*/
export async function assertChanged() {
assert.equal(await driver.find('.test-rules-save').getText(), 'Save');
assert.equal(await driver.find('.test-rules-non-save').getText(), '');
}
} // end of namespace aclTestUtils
stackWrapOwnMethods(gristUtils);
export = gristUtils;
Loading…
Cancel
Save