mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(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
This commit is contained in:
parent
6e3f0f2b35
commit
e93dd4bc6f
5
.yarnrc
Normal file
5
.yarnrc
Normal file
@ -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() {
|
||||
|
295
test/nbrowser/RightPanel.ts
Normal file
295
test/nbrowser/RightPanel.ts
Normal file
@ -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();
|
||||
});
|
||||
|
||||
|
||||
});
|
201
test/nbrowser/RightPanelSelectBy.ts
Normal file
201
test/nbrowser/RightPanelSelectBy.ts
Normal file
@ -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();
|
||||
}
|
215
test/nbrowser/SelectBy.ts
Normal file
215
test/nbrowser/SelectBy.ts
Normal file
@ -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']);
|
||||
});
|
||||
|
||||
});
|
245
test/nbrowser/SelectByRefList.ts
Normal file
245
test/nbrowser/SelectByRefList.ts
Normal file
@ -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);
|
||||
}
|
||||
}
|
65
test/nbrowser/SelectByRightPanel.ts
Normal file
65
test/nbrowser/SelectByRightPanel.ts
Normal file
@ -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']);
|
||||
});
|
||||
});
|
275
test/nbrowser/SelectBySummary.ts
Normal file
275
test/nbrowser/SelectBySummary.ts
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
192
test/nbrowser/SelectBySummaryRef.ts
Normal file
192
test/nbrowser/SelectBySummaryRef.ts
Normal file
@ -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);
|
||||
}
|
204
test/nbrowser/aclTestUtils.ts
Normal file
204
test/nbrowser/aclTestUtils.ts
Normal file
@ -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…
Reference in New Issue
Block a user