gristlabs_grist-core/test/nbrowser/testUtils.ts
George Gevoian 37eed2d3c2 (core) Fix staging test and tweak template line height
Summary:
Staging tests were using an organization named Test Grist,
while local was using one named test-grist. Both are now
named Test Grist, which should help keep things more consistent
between local and deployment test runs.

The inherited line height of template docs in icon view was
different on Windows, cutting off part of the last line of the
description. The description line height should now be fixed
to a reasonable value.

Test Plan: Manually tested CSS fix on a Windows machine.

Reviewers: paulfitz

Reviewed By: paulfitz

Subscribers: paulfitz

Differential Revision: https://phab.getgrist.com/D2948
2021-07-29 08:16:19 -07:00

251 lines
8.8 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 * as 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.
after(clearCurrentWindowStorage);
// 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());
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 (!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) {
const api = gu.createHomeApi('support', 'docs');
let orgName = 'test-grist';
const deployment = process.env.GRIST_ID_PREFIX;
if (deployment) { orgName = `${orgName}-${deployment}`; }
let isNew: boolean = false;
try {
await api.newOrg({name: 'Test 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();
await billing.updateBillingManagers({
users: {
'gristoid+chimpy@gmail.com': 'managers',
}
});
}
}
});
return cleanup;
}