mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
6460c22a89
Summary: New shortcuts for removing and adding rows. For adding a row we now have Mod+(Shift)+Enter For removing rows we now have Mod+Delete/Mod+Backspace Before removing rows, the user is prompted to confirm, this prompt can be dismissed and this setting can be remembered. User needs to confirm only when using shortcut. Old shortcuts are still active and shows information about this change. This information is shown only once, after this shortcuts have default behavior (zooming). New users don't see this explanation. Test Plan: Updated Reviewers: georgegevoian Reviewed By: georgegevoian Differential Revision: https://phab.getgrist.com/D3655
283 lines
10 KiB
TypeScript
283 lines
10 KiB
TypeScript
/**
|
|
* Exports `server`, set up to start using setupTestSuite(), e.g.
|
|
*
|
|
* import {assert, driver} from 'mocha-webdriver';
|
|
* import {server, setupTestSuite} from 'test/nbrowser/testUtils';
|
|
*
|
|
* describe("MyTest", function() {
|
|
* this.timeout(20000); // Needed because we wait for server for up to 15s.
|
|
* setupTestSuite();
|
|
* });
|
|
*
|
|
* Run with VERBOSE=1 in the environment to see the server log on the console. Normally it goes
|
|
* into a file whose path is printed when server starts.
|
|
*
|
|
* Run `bin/mocha 'test/nbrowser/*.ts' -b --no-exit` to open a command-line prompt on
|
|
* first-failure for debugging and quick reruns.
|
|
*/
|
|
import log from 'app/server/lib/log';
|
|
import {addToRepl, assert, driver, enableDebugCapture, Key, setOptionsModifyFunc, useServer} from 'mocha-webdriver';
|
|
import * as gu from 'test/nbrowser/gristUtils';
|
|
import {server} from 'test/nbrowser/testServer';
|
|
|
|
// Exports the server object with useful methods such as getHost(), waitServerReady(),
|
|
// simulateLogin(), etc.
|
|
export {server};
|
|
|
|
setOptionsModifyFunc(({chromeOpts, firefoxOpts}) => {
|
|
// Set "kiosk" printing that saves to PDF without offering any dialogs. This applies to regular
|
|
// (non-headless) Chrome. On headless Chrome, no dialog or output occurs regardless.
|
|
chromeOpts.addArguments("--kiosk-printing");
|
|
|
|
chromeOpts.setUserPreferences({
|
|
// Don't show popups to save passwords, which are shown when running against a deployment when
|
|
// we use a login form.
|
|
"credentials_enable_service": false,
|
|
"profile.password_manager_enabled" : false,
|
|
|
|
// These preferences are my best effort to set up "print to pdf" that saves into the test's temp
|
|
// dir, based on discussion here: https://bugs.chromium.org/p/chromedriver/issues/detail?id=2821.
|
|
// On headless, it's ignored (no files are saved). When run manually, it would work EXCEPT with
|
|
// kiosk-printing (i.e. also ignored), so look for your downloaded PDFs elsewhere (perhaps
|
|
// ~/Downloads). Leaving it here in case it works better some day.
|
|
"printing.default_destination_selection_rules": JSON.stringify({
|
|
kind: "local",
|
|
namePattern: "Save as PDF",
|
|
}),
|
|
"printing.print_preview_sticky_settings.appState": JSON.stringify({
|
|
recentDestinations: [{
|
|
id: 'Save as PDF',
|
|
origin: 'local',
|
|
account: '',
|
|
}],
|
|
version: 2
|
|
}),
|
|
"download.default_directory": server.testDir,
|
|
"savefile.default_directory": server.testDir,
|
|
});
|
|
});
|
|
|
|
interface TestSuiteOptions {
|
|
samples?: boolean;
|
|
team?: boolean;
|
|
|
|
// If set, clear user preferences for all test users at the end of the suite. It should be used
|
|
// for suites that modify preferences. Not that it only works in dev, not in deployment tests.
|
|
clearUserPrefs?: boolean;
|
|
}
|
|
|
|
// Sets up the test suite to use the Grist server, and also to record logs and screenshots after
|
|
// failed tests (if MOCHA_WEBDRIVER_LOGDIR var is set).
|
|
//
|
|
// Returns a Cleanup instance as a convenience, for use scheduling any clean-up that would have
|
|
// the same scope as the test suite.
|
|
export function setupTestSuite(options?: TestSuiteOptions) {
|
|
useServer(server);
|
|
enableDebugCapture();
|
|
addToRepl('gu', gu, 'gristUtils, grist-specific helpers');
|
|
addToRepl('Key', Key, 'key values such as Key.ENTER');
|
|
addToRepl('server', server, 'test server');
|
|
|
|
// After every suite, assert it didn't leave new browser windows open.
|
|
checkForExtraWindows();
|
|
|
|
// After every suite, clear sessionStorage and localStorage to avoid affecting other tests.
|
|
if (!process.env.NO_CLEANUP) {
|
|
after(clearCurrentWindowStorage);
|
|
}
|
|
// Also, log out, to avoid logins interacting, unless NO_CLEANUP is requested (useful for
|
|
// debugging tests).
|
|
if (!process.env.NO_CLEANUP) {
|
|
after(async () => {
|
|
try {
|
|
await server.removeLogin();
|
|
} catch(err) {
|
|
// If there are any alerts open, close them as it might be blocking other tests.
|
|
if (err.name && err.name === 'UnexpectedAlertOpenError') {
|
|
await driver.switchTo().alert().accept();
|
|
assert.fail("Unexpected alert open");
|
|
}
|
|
throw err;
|
|
}
|
|
});
|
|
}
|
|
|
|
// If requested, clear user preferences for all test users after this suite.
|
|
if (options?.clearUserPrefs) {
|
|
after(clearTestUserPreferences);
|
|
}
|
|
|
|
// Though unlikely it is possible that the server was left paused by a previous test, so let's
|
|
// always call resume.
|
|
afterEach(() => server.resume());
|
|
|
|
// Close database until next test explicitly needs it, to avoid conflicts
|
|
// with tests that don't use the same server.
|
|
after(async () => server.closeDatabase());
|
|
|
|
return setupRequirement({team: true, ...options});
|
|
}
|
|
|
|
// Clean up any browser windows after the test suite that didn't exist at its start.
|
|
function checkForExtraWindows() {
|
|
let origHandles: string[];
|
|
before(async () => {
|
|
origHandles = await driver.getAllWindowHandles();
|
|
});
|
|
after(async () => {
|
|
assert.deepEqual(await driver.getAllWindowHandles(), origHandles);
|
|
});
|
|
}
|
|
|
|
// Clean up any browser windows after the test suite that didn't exist at its start.
|
|
// Call this BEFORE setupTestSuite() when the test is expected to create new windows, so that they
|
|
// may get cleaned up before the check for extraneous windows runs.
|
|
export function cleanupExtraWindows() {
|
|
let origHandles: string[];
|
|
before(async () => {
|
|
origHandles = await driver.getAllWindowHandles();
|
|
});
|
|
after(async () => {
|
|
const newHandles = await driver.getAllWindowHandles();
|
|
for (const w of newHandles) {
|
|
if (!origHandles.includes(w)) {
|
|
await driver.switchTo().window(w);
|
|
await driver.close();
|
|
}
|
|
}
|
|
await driver.switchTo().window(newHandles[0]);
|
|
});
|
|
}
|
|
|
|
|
|
async function clearCurrentWindowStorage() {
|
|
if ((await driver.getCurrentUrl()).startsWith('http')) {
|
|
try {
|
|
await driver.executeScript('window.sessionStorage.clear(); window.localStorage.clear();');
|
|
} catch (err) {
|
|
log.info("Could not clear window storage after the test ended: %s", err.message);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function clearTestUserPreferences() {
|
|
// After every suite, clear user preferences for all test users.
|
|
const dbManager = await server.getDatabase();
|
|
let emails = Object.keys(gu.TestUserEnum).map(user => gu.translateUser(user as any).email);
|
|
emails = [...new Set(emails)]; // Remove duplicates.
|
|
await dbManager.testClearUserPrefs(emails);
|
|
}
|
|
|
|
export type CleanupFunc = (() => void|Promise<void>);
|
|
|
|
/**
|
|
* Helper to run cleanup callbacks created in a test case. See setupCleanup() below for usage.
|
|
*/
|
|
export class Cleanup {
|
|
private _callbacksAfterAll: CleanupFunc[] = [];
|
|
private _callbacksAfterEach: CleanupFunc[] = [];
|
|
|
|
public addAfterAll(cleanupFunc: CleanupFunc) {
|
|
this._callbacksAfterAll.push(cleanupFunc);
|
|
}
|
|
public addAfterEach(cleanupFunc: CleanupFunc) {
|
|
this._callbacksAfterEach.push(cleanupFunc);
|
|
}
|
|
|
|
public async runCleanup(which: 'all'|'each') {
|
|
const callbacks = which === 'all' ? this._callbacksAfterAll : this._callbacksAfterEach;
|
|
const list = callbacks.splice(0); // Get a copy of the list AND clear it out.
|
|
for (const f of list) {
|
|
await f();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper to run cleanup callbacks created in the course of running a test.
|
|
* Usage:
|
|
* const cleanup = setupCleanup();
|
|
* it("should do stuff", function() {
|
|
* cleanup.addAfterAll(() => { ...doSomething1()... });
|
|
* cleanup.addAfterEach(() => { ...doSomething2()... });
|
|
* });
|
|
*
|
|
* Here, doSomething1() is called at the end of a suite, while doSomething2() is called at the end
|
|
* of the current test case.
|
|
*/
|
|
export function setupCleanup() {
|
|
const cleanup = new Cleanup();
|
|
after(() => cleanup.runCleanup('all'));
|
|
afterEach(() => cleanup.runCleanup('each'));
|
|
return cleanup;
|
|
}
|
|
|
|
/**
|
|
* Implement some optional requirements for a test, such as having an example document
|
|
* present, or a team site to run tests in. These requirements should be automatically
|
|
* satisfied by staging/prod deployments, and only need doing in self-contained tests
|
|
* or tests against dev servers.
|
|
*
|
|
* Returns a Cleanup instance for any cleanup that would have the same scope as the
|
|
* requirement.
|
|
*/
|
|
export function setupRequirement(options: TestSuiteOptions) {
|
|
const cleanup = setupCleanup();
|
|
if (options.samples) {
|
|
if (process.env.TEST_ADD_SAMPLES || !server.isExternalServer()) {
|
|
gu.shareSupportWorkspaceForSuite(); // TODO: Remove after the support workspace is removed from the backend.
|
|
gu.addSamplesForSuite();
|
|
}
|
|
}
|
|
|
|
before(async function() {
|
|
|
|
if (new URL(server.getHost()).hostname !== 'localhost') {
|
|
// Non-dev servers should already meet the requirements; in any case we should not
|
|
// fiddle with them here.
|
|
return;
|
|
}
|
|
|
|
// Optionally ensure that a team site is available for tests.
|
|
if (options.team) {
|
|
await gu.addSupportUserIfPossible();
|
|
const api = gu.createHomeApi('support', 'docs');
|
|
for (const suffix of ['', '2'] as const) {
|
|
let orgName = `test${suffix}-grist`;
|
|
const deployment = process.env.GRIST_ID_PREFIX;
|
|
if (deployment) { orgName = `${orgName}-${deployment}`; }
|
|
let isNew: boolean = false;
|
|
try {
|
|
await api.newOrg({name: `Test${suffix} Grist`, domain: orgName});
|
|
isNew = true;
|
|
} catch (e) {
|
|
// Assume the org already exists.
|
|
}
|
|
if (isNew) {
|
|
await api.updateOrgPermissions(orgName, {
|
|
users: {
|
|
'gristoid+chimpy@gmail.com': 'owners',
|
|
}
|
|
});
|
|
// Recreate the api for the correct org, then update billing.
|
|
const api2 = gu.createHomeApi('support', orgName);
|
|
const billing = api2.getBillingAPI();
|
|
try {
|
|
await billing.updateBillingManagers({
|
|
users: {
|
|
'gristoid+chimpy@gmail.com': 'managers',
|
|
}
|
|
});
|
|
} catch (e) {
|
|
// ignore if no billing endpoint
|
|
if (!String(e).match('404: Not Found')) {
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
return cleanup;
|
|
}
|