mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
680 lines
22 KiB
JavaScript
680 lines
22 KiB
JavaScript
|
import * as _ from 'lodash';
|
||
|
import { assert, driver, Key, stackWrapFunc, WebElement,
|
||
|
WebElementPromise } from 'mocha-webdriver';
|
||
|
import { driverCompanion, findOldTimey, waitImpl,
|
||
|
webdriverjqWrapper } from 'test/nbrowser/webdriverjq-nbrowser';
|
||
|
import * as guBase from 'test/nbrowser/gristUtils';
|
||
|
import { setupTestSuite } from 'test/nbrowser/testUtils';
|
||
|
import { server } from 'test/nbrowser/testServer';
|
||
|
|
||
|
// Simulate the old "$" object.
|
||
|
|
||
|
const _webdriverjqFactory = webdriverjqWrapper(driver);
|
||
|
function $(key) {
|
||
|
if (typeof key !== 'string') {
|
||
|
return key;
|
||
|
}
|
||
|
return _webdriverjqFactory(key);
|
||
|
}
|
||
|
|
||
|
function rep(n, value) {
|
||
|
return _.times(n, () => value);
|
||
|
}
|
||
|
|
||
|
// The "$" object needs some setup done asynchronously.
|
||
|
// We do that later, during test initialization.
|
||
|
async function applyPatchesToJquerylikeObject($) {
|
||
|
$.MOD = await guBase.modKey();
|
||
|
$.COPY = await guBase.copyKey();
|
||
|
$.CUT = await guBase.cutKey();
|
||
|
$.PASTE = await guBase.pasteKey();
|
||
|
$.SELECT_ALL = await guBase.selectAllKey();
|
||
|
const capabilities = await driver.getCapabilities();
|
||
|
if (capabilities.getBrowserName() === 'chrome' && await guBase.isMac()) {
|
||
|
$.SELECT_ALL_LINES = (n) => [...rep(n, $.UP), $.HOME, $.SHIFT, ...rep(n, $.DOWN), $.END]
|
||
|
} else {
|
||
|
$.SELECT_ALL_LINES = () => $.SELECT_ALL;
|
||
|
}
|
||
|
$.getPage = async (url) => {
|
||
|
return driver.get(url);
|
||
|
};
|
||
|
$.wait = (timeoutMs, conditionFunc) => {
|
||
|
return waitImpl(timeoutMs, conditionFunc);
|
||
|
}
|
||
|
for (const key of Object.keys(Key)) {
|
||
|
$[key] = Key[key];
|
||
|
}
|
||
|
// We need to tweak driver object a bit too (really?)
|
||
|
driverCompanion.$ = $;
|
||
|
// driver.testHost = server.getHost();
|
||
|
// driver.waitImpl = waitImpl;
|
||
|
}
|
||
|
|
||
|
// Adapt common old setup.
|
||
|
const test = {
|
||
|
setupTestSuite(self, ...args) {
|
||
|
self.timeout(20000);
|
||
|
return setupTestSuite(...args);
|
||
|
},
|
||
|
};
|
||
|
|
||
|
// Add some methods to the grist utils that are used by old tests.
|
||
|
// This could be cleaned up further, but translating to newer ways
|
||
|
// of doing things or, if the method is really useful, adding the
|
||
|
// method to the new grist utils.
|
||
|
|
||
|
const waitForServer = guBase.waitForServer;
|
||
|
let patchesApplied = false;
|
||
|
let session;
|
||
|
|
||
|
const gu = {
|
||
|
...guBase,
|
||
|
|
||
|
// Apply all needed patches, async initialization, and log in.
|
||
|
async supportOldTimeyTestCode() {
|
||
|
if (!patchesApplied) {
|
||
|
applyPatchesToWebElements();
|
||
|
applyPatchesToAssert();
|
||
|
applyPatchesToJquerylikeObject($);
|
||
|
}
|
||
|
patchesApplied = true;
|
||
|
// Login as someone so old code doesn't have to be upgraded to do it.
|
||
|
session = await gu.session().user('userz');
|
||
|
const dbManager = await server.getDatabase();
|
||
|
const profile = {email: session.email, name: session.name};
|
||
|
await dbManager.getUserByLogin(session.email, {profile});
|
||
|
await gu.setApiKey(session.name);
|
||
|
await session.login();
|
||
|
},
|
||
|
|
||
|
// getCell with old-timey arguments.
|
||
|
getCellRC(r, c) {
|
||
|
return gu.getCell(c, r + 1);
|
||
|
},
|
||
|
|
||
|
// clickCell with old-timey arguments.
|
||
|
async clickCellRC(r, c) {
|
||
|
const cell = gu.getCell(c, r + 1);
|
||
|
await cell.click();
|
||
|
return cell;
|
||
|
},
|
||
|
|
||
|
// sendKeys variant that accepts arrays in place of Key.chord.
|
||
|
sendKeys(...args) {
|
||
|
return guBase.sendKeys(...args.map(
|
||
|
a => Array.isArray(a) ? Key.chord(...a) : a
|
||
|
));
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* When doing type conversion in the side pane, this clicks the 'Apply' button and waits for the
|
||
|
* conversion to complete.
|
||
|
*/
|
||
|
async applyTypeConversion() {
|
||
|
await $('.test-type-transform-apply').wait().scrollIntoView({
|
||
|
block: 'nearest',
|
||
|
inline: 'nearest',
|
||
|
}).click();
|
||
|
await $('.test-type-transform-apply').waitDrop(assert.isPresent, false);
|
||
|
return gu.waitForServer();
|
||
|
},
|
||
|
|
||
|
async clickColumnMenuItem(colName, itemText, optRightClick) {
|
||
|
await gu.openColumnMenu(colName);
|
||
|
return gu.actions.selectFloatingOption(itemText);
|
||
|
},
|
||
|
|
||
|
getOpenEditingLabel(parentElem) {
|
||
|
return driver.find('.test-column-title-label');
|
||
|
},
|
||
|
|
||
|
enterGridValues(startRowIndex, startColIndex, dataMatrix) {
|
||
|
const transpose = dataMatrix[0].map(
|
||
|
(_, colIndex) => dataMatrix.map(row => row[colIndex])
|
||
|
);
|
||
|
return gu.enterGridRows({col: startColIndex,
|
||
|
rowNum: startRowIndex + 1}, transpose);
|
||
|
},
|
||
|
|
||
|
async openSidePane(tabName) {
|
||
|
if (['log', 'validate', 'repl', 'code'].includes(tabName)) {
|
||
|
await guBase.toggleSidePanel('right', 'open');
|
||
|
return $(`.test-tools-${tabName}`).wait().click();
|
||
|
} else if (tabName === 'field') {
|
||
|
await guBase.toggleSidePanel('right', 'open');
|
||
|
return $('.test-right-tab-field').click();
|
||
|
} else if (tabName === 'view') {
|
||
|
await guBase.toggleSidePanel('right', 'open');
|
||
|
return $('.test-right-tab-pagewidget').wait().click();
|
||
|
}
|
||
|
},
|
||
|
|
||
|
getGridValues(...options) {
|
||
|
return gu.getVisibleGridCells(...options);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Returns the text in the row header of the last row in a Grid section, scrolling to the
|
||
|
* bottom, but not moving the cursor.
|
||
|
* @param {String} options.section: Optional section name to use instead of the active section.
|
||
|
*/
|
||
|
async getGridLastRowText(options) {
|
||
|
if (options?.section) {
|
||
|
await gu.actions.viewSection(options.section).selectSection();
|
||
|
}
|
||
|
return String(await gu.getGridRowCount());
|
||
|
},
|
||
|
|
||
|
getAddRowNumber() {
|
||
|
return gu.getGridRowCount();
|
||
|
},
|
||
|
|
||
|
async getGridLabels(sectionName) {
|
||
|
return gu.getSection(sectionName).findAll('[data-test-id="GridView_columnLabel"] .kf_elabel_text',
|
||
|
label => label.getText());
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Given a cell in grist GridView, returns whether it contains the cursor. You may use it as
|
||
|
* hasCursor(getCell(...)) or getCell(...).waitFor(hasCursor, optTimeout).
|
||
|
*/
|
||
|
hasCursor(cellElem) {
|
||
|
return cellElem.find('.selected_cursor').isDisplayed();
|
||
|
},
|
||
|
|
||
|
async useFixtureDoc(cleanup, fname, flag) {
|
||
|
return session.tempDoc(cleanup, fname);
|
||
|
},
|
||
|
|
||
|
async copyDoc(docId, flag) {
|
||
|
const result = await guBase.copyDoc(session.name, 'docs', 'Home', docId);
|
||
|
await session.loadDoc(`/doc/${result.id}`);
|
||
|
return result;
|
||
|
},
|
||
|
|
||
|
async clickCell(rowIndexOrPosOrCell, colIndex) {
|
||
|
if (typeof rowIndexOrPosOrCell === 'object' && 'driver_' in rowIndexOrPosOrCell) {
|
||
|
return rowIndexOrPosOrCell.click();
|
||
|
}
|
||
|
// Best just to force a rewrite of clickCell, e.g. to clickCellRC,
|
||
|
// since newer gristUtils interprets arguments entirely differently.
|
||
|
if (typeof rowIndexOrPosOrCell === 'number') {
|
||
|
throw new Error("ambiguous row/col");
|
||
|
}
|
||
|
const cell = guBase.getCell(rowIndexOrPosOrCell, colIndex);
|
||
|
await cell.click();
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Sets the visibleCol of the currently selected field to value.
|
||
|
*/
|
||
|
async setVisibleCol(value) {
|
||
|
await gu.openSidePane('field');
|
||
|
await $('.test-fbuilder-ref-col-select').click();
|
||
|
await $(`.test-select-menu .test-select-row:contains(${value})`).wait().click();
|
||
|
return waitForServer();
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Asserts the type of the currently selected field.
|
||
|
*/
|
||
|
async assertType(value) {
|
||
|
await gu.openSidePane('field');
|
||
|
assert.equal(await $('.test-fbuilder-type-select .test-select-row').getText(), value);
|
||
|
},
|
||
|
|
||
|
closeSidePane() {
|
||
|
return gu.toggleSidePanel('right', 'close');
|
||
|
},
|
||
|
|
||
|
clickVisibleDetailCells(column, rowNums) {
|
||
|
return gu.getDetailCell(column, rowNums[0]).click();
|
||
|
},
|
||
|
|
||
|
async clickRowMenuItem(rowNum, item) {
|
||
|
await (await gu.openRowMenu(rowNum)).findContent('li', item).click();
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Selects rows starting from rowStart and ending at rowEnd (1-based) by clicking and dragging or
|
||
|
* shift clicking (Defaults to dragging)
|
||
|
* @param {int} rowStart: 1-based row number
|
||
|
* @param {int} rowEnd: 1-based row number.
|
||
|
* @param {String} optMethod: if 'shift' then shift clicking is used to select rows otherwise it
|
||
|
* defaults to drag to select.
|
||
|
*/
|
||
|
async selectRows(rowStart, rowEnd, optMethod) {
|
||
|
let start = await driver.findContent('.active_section .gridview_data_row_num', gu.exactMatch(rowStart.toString()));
|
||
|
let end = await driver.findContent('.active_section .gridview_data_row_num', gu.exactMatch(rowEnd.toString()));
|
||
|
if (optMethod === 'shift') {
|
||
|
await driver.withActions(a => a.click(start).keyDown($.SHIFT).click(end).keyUp($.SHIFT));
|
||
|
} else {
|
||
|
await driver.withActions(a => a.move({origin: start}).press().move({origin: end}).release());
|
||
|
}
|
||
|
},
|
||
|
|
||
|
async _fieldSettingsClickOption(isCommonToSeparate, optionSubstring) {
|
||
|
assert.include(await $('.fieldbuilder_settings_button').text(), isCommonToSeparate ? 'Common' : 'Separate');
|
||
|
await $('.fieldbuilder_settings_button').click();
|
||
|
await gu.actions.selectFloatingOption(optionSubstring);
|
||
|
await waitForServer();
|
||
|
assert.include(await $('.fieldbuilder_settings_button').text(), isCommonToSeparate ? 'Separate' : 'Common');
|
||
|
},
|
||
|
fieldSettingsUseSeparate: () => gu._fieldSettingsClickOption(true, 'Use separate'),
|
||
|
fieldSettingsSaveAsCommon: () => gu._fieldSettingsClickOption(false, 'Save as common'),
|
||
|
fieldSettingsRevertToCommon: () => gu._fieldSettingsClickOption(false, 'Revert to common'),
|
||
|
|
||
|
/**
|
||
|
* Changes date format for date and datetime editor or returns current format
|
||
|
* @param {string} value Date format
|
||
|
*/
|
||
|
async dateFormat(value) {
|
||
|
if (!value) {
|
||
|
return $('$Widget_dateFormat .test-select-row').text();
|
||
|
}
|
||
|
await $('$Widget_dateFormat').wait().click();
|
||
|
await $(`.test-select-menu .test-select-row:contains(${value})`).wait().click();
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Changes time format for datetime editor or returns current format
|
||
|
* @param {string} value Time format
|
||
|
*/
|
||
|
async timeFormat(value) {
|
||
|
if (!value) {
|
||
|
return $('$Widget_timeFormat .test-select-row').getText();
|
||
|
}
|
||
|
await $('$Widget_timeFormat').wait().click();
|
||
|
await $(`.test-select-menu .test-select-row:contains(${value})`).wait().click();
|
||
|
},
|
||
|
|
||
|
async getDetailValues(...options) {
|
||
|
return gu.getVisibleDetailCells(...options);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Selects all cells in a GridView between and including startCell and endCell
|
||
|
* @param {Array} startCell:
|
||
|
* startCell[0]: 1-based row index.
|
||
|
* startCell[1]: 0-based column index.
|
||
|
* @param {Array} endCell:
|
||
|
* endCell[0]: 1-based row index.
|
||
|
* endCell[1]: 0-based column index.
|
||
|
**/
|
||
|
async selectGridArea(startCell, endCell) {
|
||
|
let start = await gu.getCell({rowNum: startCell[0], col: startCell[1]});
|
||
|
let end = await gu.getCell({rowNum: endCell[0], col: endCell[1]});
|
||
|
await driver.withActions(a => a.click(start).keyDown($.SHIFT).click(end).keyUp($.SHIFT));
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Returns text of the cells for the given rows and columns of a viewSection.
|
||
|
* @param {String} option.section: Optional section name instead of active.
|
||
|
* @param {Array<Number>} option.rowNums: Array of row numbers (1-based)
|
||
|
* @param {Array<Number>} option.cols: Array of column indices (0-based) or labels.
|
||
|
* @param [Number: Function] option.cellFunc: a function that returns cells given an array of
|
||
|
* columns, rows and optionally a viewsection (defaults to the currently active section)
|
||
|
* @param [Number: Function] option.valueFunc: Optional function, or an object mapping column
|
||
|
* index or label (as in options.cols) to function, with the function mapping a cell to
|
||
|
* its value (by default, cell => cell.text()).
|
||
|
* @returns {Promise<Array>} Returns array of values for each requested cell, as all values from
|
||
|
* the first row, followed by values from the second, etc.
|
||
|
*/
|
||
|
async getSectionValues(options) {
|
||
|
var opts = { section: options.section };
|
||
|
var defaultValueFunc = (cell => cell.text());
|
||
|
var valueFunc;
|
||
|
if (options.valueFunc && !_.isFunction(options.valueFunc)) {
|
||
|
valueFunc = (col => options.valueFunc[col] || defaultValueFunc);
|
||
|
} else {
|
||
|
valueFunc = _.constant(options.valueFunc || defaultValueFunc);
|
||
|
}
|
||
|
const colValues = [];
|
||
|
for (const col of options.cols) {
|
||
|
const colValue = await valueFunc(col)(options.cellFunc(col, options.rowNums, opts).array());
|
||
|
colValues.push(colValue);
|
||
|
}
|
||
|
return _.flatten(_.zip.apply(_, colValues), true);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Asserts the widget of the currently selected field.
|
||
|
*/
|
||
|
async assertWidget(value) {
|
||
|
await gu.openSidePane('field');
|
||
|
assert.equal(await $('.test-fbuilder-widget-select .test-select-row').getText(), value);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Sets the widget of the currently selected field to value.
|
||
|
*/
|
||
|
async setWidget(value) {
|
||
|
await gu.openSidePane('field');
|
||
|
const selector = $('.test-fbuilder-widget-select');
|
||
|
const btnChildren = await selector.elem().findAll('.test-select-button');
|
||
|
if (btnChildren.length > 0) {
|
||
|
// This is a button select.
|
||
|
await selector.findOldTimey(`.test-select-button:contains(${value})`).click();
|
||
|
} else {
|
||
|
// This is a dropdown select.
|
||
|
await selector.click();
|
||
|
await $(`.test-select-menu .test-select-row:contains(${value})`).click();
|
||
|
}
|
||
|
await gu.waitForServer();
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Adds a new record to the grid. Takes an array of values that matches column positions.
|
||
|
*/
|
||
|
async addRecord(values) {
|
||
|
await gu.sendKeys([$.MOD, $.UP]);
|
||
|
await gu.sendKeys([$.MOD, $.DOWN]);
|
||
|
await gu.sendKeys([$.LEFT]);
|
||
|
await gu.sendKeys([$.LEFT]);
|
||
|
await gu.sendKeys([$.LEFT]);
|
||
|
await gu.sendKeys([$.LEFT]);
|
||
|
await gu.sendKeys([$.LEFT]);
|
||
|
await driver.sleep(1000);
|
||
|
// For each value, type it, followed by Tab.
|
||
|
for (const [i, value] of values.entries()) {
|
||
|
await gu.waitAppFocus(true);
|
||
|
await gu.sendKeys(value, $.TAB);
|
||
|
await gu.waitForServer();
|
||
|
if (i === 0) {
|
||
|
// The very first value triggers add-record, but the creation of the new row isn't
|
||
|
// immediate, so give it a moment.
|
||
|
await driver.sleep(250);
|
||
|
}
|
||
|
}
|
||
|
// Return a promise that can be awaited; it will wait for all the previously queued ones.
|
||
|
return driver.sleep(0);
|
||
|
},
|
||
|
|
||
|
actions: {
|
||
|
createNewDoc: async (optDocName) => {
|
||
|
await gu.simulateLogin("Chimpy", "chimpy@getgrist.com", "nasa");
|
||
|
const docId = await gu.createNewDoc('chimpy', 'nasa', 'Horizon', optDocName || 'Untitled');
|
||
|
await gu.loadDoc(`/o/nasa/doc/${docId}`);
|
||
|
},
|
||
|
getDocTitle: () => {
|
||
|
return $('.test-bc-doc').val();
|
||
|
},
|
||
|
getActiveTab: () => {
|
||
|
return $('.test-treeview-itemHeader.selected .test-docpage-label').wait();
|
||
|
},
|
||
|
getTabs: () => {
|
||
|
return $('.test-docpage-label');
|
||
|
},
|
||
|
renameDoc: (newName) => {
|
||
|
$('.test-bc-doc').click();
|
||
|
$.driver.sendKeys(newName, $.ENTER);
|
||
|
return $.wait(1000, () => $.driver.getTitle().startsWith(newName + ' - '));
|
||
|
},
|
||
|
selectTabView: async (viewTitle) => {
|
||
|
const isOpen = await gu.isSidePanelOpen('left');
|
||
|
if (!isOpen) {
|
||
|
await gu.toggleSidePanel('left', 'open');
|
||
|
}
|
||
|
await gu.openPage(viewTitle);
|
||
|
if (!isOpen) {
|
||
|
await gu.toggleSidePanel('left', 'close');
|
||
|
}
|
||
|
},
|
||
|
addNewTable: async () => {
|
||
|
await $('.test-dp-add-new').wait().click();
|
||
|
await $('.test-dp-empty-table').click();
|
||
|
// if we selected a new table, there will be a popup for a name
|
||
|
const prompts = await $(".test-modal-prompt");
|
||
|
const prompt = prompts[0];
|
||
|
if (prompt) {
|
||
|
await await $(".test-modal-confirm").click();
|
||
|
}
|
||
|
return gu.waitForServer();
|
||
|
},
|
||
|
addNewSection: (tableId, sectionType) => {
|
||
|
return gu.addNewSection(sectionType, tableId);
|
||
|
},
|
||
|
addNewSummarySection: async (tableId, groupByArr, sectionType, sectionName) => {
|
||
|
await gu.addNewSection(sectionType, tableId, {summarize: groupByArr});
|
||
|
await gu.waitForServer();
|
||
|
await gu.renameActiveSection(sectionName);
|
||
|
await gu.waitForServer();
|
||
|
},
|
||
|
addNewView: (tableId, sectionType) => {
|
||
|
return gu.addNewPage(sectionType, tableId);
|
||
|
},
|
||
|
selectFloatingOption: async (optionName) => {
|
||
|
// Sometimes the element is there but "not interactable". Work around that.
|
||
|
await gu.waitToPass(async () => {
|
||
|
await $(`.grist-floating-menu li:contains(${optionName})`).click();
|
||
|
});
|
||
|
},
|
||
|
/**
|
||
|
* Actions related to view section. To use, pass in the section name.
|
||
|
* @param {string} sectionName - Title of the view section
|
||
|
* @return Object<string, function>} Collection of methods for the view section.
|
||
|
*
|
||
|
* @example
|
||
|
* gu.actions.viewSection('Table1 record').selectMenuOption('Insert section');
|
||
|
*/
|
||
|
viewSection: (sectionName) => {
|
||
|
let section = gu.getSection(sectionName);
|
||
|
return {
|
||
|
/**
|
||
|
* Clicks inside to make the current section active.
|
||
|
*/
|
||
|
selectSection: function () {
|
||
|
return gu.selectSectionByTitle(sectionName);
|
||
|
},
|
||
|
/**
|
||
|
* Opens the view section drop-down menu.
|
||
|
* @param {string} which - Which menu to open, coud be: 'sortAndFilter' or 'viewLayout'
|
||
|
*/
|
||
|
openMenu: async function (which) {
|
||
|
await driver.withActions(a => a.move({origin: section.find('.viewsection_title')})); // to display menu buttons on hover
|
||
|
const item = section.find(`.test-section-menu-${which}`);
|
||
|
await gu.waitToPass(() => item.click());
|
||
|
},
|
||
|
/**
|
||
|
* Opens the section drop-down menu and select option matching param.
|
||
|
* @param {string} which - Which menu to open, coud be: 'sortAndFilter' or 'viewLayout'
|
||
|
* @param {string} optionName
|
||
|
*/
|
||
|
selectMenuOption: function (which, optionName) {
|
||
|
this.openMenu(which);
|
||
|
return gu.actions.selectFloatingOption(optionName);
|
||
|
}
|
||
|
};
|
||
|
},
|
||
|
tableView: (tableName, viewName) => {
|
||
|
return {
|
||
|
select: () => {
|
||
|
return gu.getPageItem(tableName).click();
|
||
|
},
|
||
|
selectOption: async optionName => {
|
||
|
await gu.openPageMenu(tableName);
|
||
|
return gu.actions.selectFloatingOption(optionName);
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* This monkey-patches the WebElement class to make it look enough like
|
||
|
* jquery that a lot of old test code can be used without modification.
|
||
|
*/
|
||
|
function applyPatchesToWebElements() {
|
||
|
|
||
|
WebElement.prototype.wait = function(fn, ...args) {
|
||
|
if (fn) {
|
||
|
return gu.waitToPass(async () => {
|
||
|
return fn.apply(null, [this, ...args]);
|
||
|
}).then(() => true);
|
||
|
} else {
|
||
|
return new WebElementPromise(
|
||
|
driver,
|
||
|
gu.waitToPass(async () => {
|
||
|
if (!(await this.isPresent())) {
|
||
|
throw new Error('not present');
|
||
|
}
|
||
|
}).then(() => this));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
WebElement.prototype.selected = function(val) {
|
||
|
return driver.executeScript((elem, val) => {
|
||
|
elem.selected = val;
|
||
|
}, this, val);
|
||
|
}
|
||
|
|
||
|
WebElement.prototype.attr = function(key, val) {
|
||
|
if (val !== undefined) {
|
||
|
return driver.executeScript((elem, key, val) => {
|
||
|
elem.setAttribute(key, val);
|
||
|
}, this, key, val);
|
||
|
}
|
||
|
return this.getAttribute(key);
|
||
|
}
|
||
|
|
||
|
WebElement.prototype.classList = async function() {
|
||
|
return (await this.getAttribute('className')).split(' ');
|
||
|
}
|
||
|
|
||
|
// Lists of WebElements work differently - if we did a find() we
|
||
|
// already have just the first match.
|
||
|
WebElement.prototype.first = function() {
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
WebElement.prototype.text = function() {
|
||
|
return this.getText();
|
||
|
}
|
||
|
|
||
|
WebElement.prototype.val = function(newVal) {
|
||
|
if (newVal === undefined) {
|
||
|
return this.getAttribute('value');
|
||
|
}
|
||
|
return gu.setValue(this, newVal);
|
||
|
}
|
||
|
|
||
|
WebElement.prototype.css = function(key, val) {
|
||
|
if (val === undefined) {
|
||
|
return this.getCssValue(key);
|
||
|
}
|
||
|
return new WebElementPromise(
|
||
|
driver,
|
||
|
driver.executeScript(elem => {
|
||
|
elem.style[key] = val;
|
||
|
}, this)
|
||
|
);
|
||
|
}
|
||
|
|
||
|
WebElement.prototype.is = function(selector) {
|
||
|
return this.matches(selector);
|
||
|
}
|
||
|
|
||
|
WebElement.prototype.hasClass = async function(className) {
|
||
|
return (await this.classList()).includes(className);
|
||
|
}
|
||
|
|
||
|
WebElement.prototype.scrollIntoView = function(opts) {
|
||
|
opts = opts || {behavior: 'auto'};
|
||
|
return new WebElementPromise(
|
||
|
driver,
|
||
|
driver.executeScript((elem, opts) => elem.scrollIntoView(opts),
|
||
|
this, opts).then(() => this));
|
||
|
}
|
||
|
|
||
|
WebElement.prototype.parent = function() {
|
||
|
return new WebElementPromise(
|
||
|
driver,
|
||
|
driver.executeScript(elem => {
|
||
|
return elem.parentNode.closest('*');
|
||
|
}, this)
|
||
|
);
|
||
|
}
|
||
|
|
||
|
WebElement.prototype.closest = function(key) {
|
||
|
return this.findClosest(key);
|
||
|
}
|
||
|
|
||
|
WebElement.prototype.children = async function(mapper) {
|
||
|
// Collect children.
|
||
|
let result = await driver.executeScript(elem => {
|
||
|
return [...elem.children].map(c => c.closest('*'));
|
||
|
}, this);
|
||
|
// Fix up type.
|
||
|
result = result.map(v => new WebElementPromise(
|
||
|
driver,
|
||
|
Promise.resolve(v),
|
||
|
));
|
||
|
// Apply mapper if available.
|
||
|
if (mapper) {
|
||
|
result = result.map(mapper);
|
||
|
}
|
||
|
// Result is a single promise.
|
||
|
return Promise.all(result);
|
||
|
}
|
||
|
|
||
|
WebElement.prototype.trimmedText = async function() {
|
||
|
const text = await this.getText();
|
||
|
return text.trim();
|
||
|
}
|
||
|
|
||
|
// A version of find() that supports some old timey syntax.
|
||
|
WebElement.prototype.findOldTimey = function(key) {
|
||
|
return findOldTimey(this, key);
|
||
|
}
|
||
|
|
||
|
WebElement.prototype.findAllOldTimey = function(key, mapper) {
|
||
|
return findOldTimey(this, key, true, mapper);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* This monkey-patches assert to add some methods that are very
|
||
|
* commonly used.
|
||
|
*/
|
||
|
function applyPatchesToAssert() {
|
||
|
|
||
|
assert.hasClass = stackWrapFunc(async function(elem, className, present) {
|
||
|
if (present === undefined) {
|
||
|
present = true;
|
||
|
}
|
||
|
const c = await elem.getAttribute('class');
|
||
|
if (present) {
|
||
|
await assert.include(c.split(' '), className);
|
||
|
} else {
|
||
|
await assert.notInclude(c.split(' '), className);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
assert.isPresent = stackWrapFunc(async function(elem, present) {
|
||
|
if (present === undefined) {
|
||
|
present = true;
|
||
|
}
|
||
|
let current = false;
|
||
|
try {
|
||
|
current = await elem.isPresent();
|
||
|
} catch (e) {
|
||
|
// $ object may fail if elem is non-existent.
|
||
|
}
|
||
|
await assert.equal(current, present);
|
||
|
return true;
|
||
|
});
|
||
|
|
||
|
assert.isDisplayed = stackWrapFunc(async function(elem, displayed) {
|
||
|
if (displayed === undefined) {
|
||
|
displayed = true;
|
||
|
}
|
||
|
await assert.equal(await elem.isDisplayed(), displayed);
|
||
|
return true;
|
||
|
});
|
||
|
}
|
||
|
|
||
|
exports.$ = $;
|
||
|
exports.gu = gu;
|
||
|
exports.server = server;
|
||
|
exports.test = test;
|