mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
273 lines
9.7 KiB
TypeScript
273 lines
9.7 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();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
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 at least one example document is present.
|
||
|
if (options.samples) {
|
||
|
const homeApi = gu.createHomeApi('support', 'docs');
|
||
|
const wss = await homeApi.getOrgWorkspaces('current');
|
||
|
const exampleWs = wss.find(ws => ws.name === 'Examples & Templates');
|
||
|
if (!exampleWs) {
|
||
|
throw new Error('missing example workspace');
|
||
|
}
|
||
|
// Only add the example if one isn't already there.
|
||
|
if (!exampleWs.docs.some((doc) => (doc.name === 'My Lightweight CRM'))) {
|
||
|
|
||
|
const exampleDocId = (await gu.importFixturesDoc('support', 'docs', 'Examples & Templates',
|
||
|
'video/Lightweight CRM.grist', {load: false, newName: 'My Lightweight CRM.grist'})).id;
|
||
|
|
||
|
// Remove it after the suite.
|
||
|
cleanup.addAfterAll(() => homeApi.deleteDoc(exampleDocId));
|
||
|
}
|
||
|
await homeApi.updateWorkspacePermissions(exampleWs.id, {users: {
|
||
|
'everyone@getgrist.com': 'viewers',
|
||
|
'anon@getgrist.com': 'viewers',
|
||
|
}});
|
||
|
}
|
||
|
|
||
|
// 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: orgName, 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;
|
||
|
}
|