Initial attempt

This commit is contained in:
Spoffy
2024-06-15 04:52:04 +01:00
parent 73e022b0c5
commit ffa81a78b1
7 changed files with 425 additions and 2 deletions

View File

@@ -0,0 +1,64 @@
import {assert, driver} from 'mocha-webdriver';
import * as gu from 'test/nbrowser/playwrightGristUtils';
import {server, setupTestSuite} from 'test/nbrowser/playwrightTestUtils';
import * as testUtils from 'test/server/testUtils';
import { test, expect, Page } from '@playwright/test';
/**
* The boot page functionality has been merged with the Admin Panel.
* Check that it behaves as a boot page did now.
*/
test.describe('Boot', () => {
setupTestSuite();
let oldEnv: testUtils.EnvironmentSnapshot;
test.afterEach(({ page }) => gu.checkForErrors(page));
async function hasPrompt(page: Page) {
// There is some glitchiness to when the text appears.
const text = await page.getByText(/GRIST_BOOT_KEY/).textContent();
expect(text).toContain('GRIST_BOOT_KEY=example-');
}
test('tells user about /admin', async function() {
await driver.get(`${server.getHost()}/boot`);
assert.match(await driver.getPageSource(), /\/admin/);
// Switch to a regular place to that gu.checkForErrors won't panic -
// it needs a Grist page.
await driver.get(`${server.getHost()}`);
});
test('gives prompt about how to enable boot page', async function({ page }) {
await driver.get(`${server.getHost()}/admin`);
await hasPrompt(page);
});
test.describe('with a GRIST_BOOT_KEY', function() {
test.beforeAll(async function() {
oldEnv = new testUtils.EnvironmentSnapshot();
process.env.GRIST_BOOT_KEY = 'lala';
await server.restart();
});
test.afterAll(async function() {
oldEnv.restore();
await server.restart();
});
test('gives prompt when key is missing', async function({ page }) {
await driver.get(`${server.getHost()}/admin`);
await hasPrompt(page);
});
test('gives prompt when key is wrong', async function({ page }) {
await driver.get(`${server.getHost()}/admin?boot-key=bilbo`);
await hasPrompt(page);
});
test('gives page when key is right', async function() {
await driver.get(`${server.getHost()}/admin?boot-key=lala`);
await driver.findContentWait('div', /Is home page available/, 2000);
});
});
});

View File

@@ -37,6 +37,7 @@ import * as testUtils from 'test/server/testUtils';
import type { AssertionError } from 'assert';
import axios from 'axios';
import { lock } from 'proper-lockfile';
import { test } from '@playwright/test';
// tslint:disable:no-namespace
// Wrap in a namespace so that we can apply stackWrapOwnMethods to all the exports together.
@@ -1989,6 +1990,33 @@ export function shareSupportWorkspaceForSuite() {
});
}
export function shareSupportWorkspaceForSuitePlaywright() {
let api: UserAPIImpl|undefined;
let wss: Workspace[]|undefined;
test.beforeAll(async function() {
// test/gen-server/seed.ts creates a support user with a personal org and an "Examples &
// Templates" workspace, but doesn't share it (to avoid impacting the many existing tests).
// Share that workspace with @everyone and @anon, and clean up after this suite.
await addSupportUserIfPossible();
api = createHomeApi('Support', 'docs'); // this uses an api key, so no need to log in.
wss = await api.getOrgWorkspaces('current');
await api.updateWorkspacePermissions(wss[0].id, {users: {
'everyone@getgrist.com': 'viewers',
'anon@getgrist.com': 'viewers',
}});
});
test.afterAll(async function() {
if (api && wss) {
await api.updateWorkspacePermissions(wss[0].id, {users: {
'everyone@getgrist.com': null,
'anon@getgrist.com': null,
}});
}
});
}
export async function clearTestState() {
await driver.executeScript("window.testGrist = {}");
}
@@ -2685,6 +2713,16 @@ export function addSamplesForSuite() {
});
}
export function addSamplesForSuitePlaywright() {
test.beforeAll(async function() {
await addSamples();
});
test.afterAll(async function() {
await removeTemplatesOrg();
});
}
export async function openAccountMenu() {
await driver.findWait('.test-dm-account', 1000).click();
// Since the AccountWidget loads orgs and the user data asynchronously, the menu

View File

@@ -0,0 +1,6 @@
import { expect, Page } from '@playwright/test';
export async function checkForErrors(page: Page) {
const errors = await page.evaluate(() => (window as any).getAppErrors());
expect(errors).toEqual([]);
}

View File

@@ -0,0 +1,205 @@
/**
* 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 gu from 'test/nbrowser/gristUtils';
import { server } from 'test/nbrowser/testServer';
import { test } from '@playwright/test';
// Exports the server object with useful methods such as getHost(), waitServerReady(),
// simulateLogin(), etc.
export {server};
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;
// Max milliseconds to wait for a page to finish loading. E.g. affects clicks that cause
// navigation, which wait for that. A navigation that takes longer will throw an exception.
pageLoadTimeout?: number;
}
// 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) {
test.beforeAll(async () => server.start());
test.afterAll(async () => server.stop());
// After every suite, assert it didn't leave new browser windows open.
// Don't know if we need this in playwright
//checkForExtraWindows();
// After every suite, clear sessionStorage and localStorage to avoid affecting other tests.
if (!process.env.NO_CLEANUP) {
// Not sure this works in playwright? or is needed?
//test.afterAll(({ page }) => clearCurrentWindowStorage(page));
}
// Also, log out, to avoid logins interacting, unless NO_CLEANUP is requested (useful for
// debugging tests).
if (!process.env.NO_CLEANUP) {
test.afterAll(() => server.removeLogin());
}
// If requested, clear user preferences for all test users after this suite.
if (options?.clearUserPrefs) {
test.afterAll(clearTestUserPreferences);
}
// Though unlikely it is possible that the server was left paused by a previous test, so let's
// always call resume.
test.afterEach(() => server.resume());
// Close database until next test explicitly needs it, to avoid conflicts
// with tests that don't use the same server.
test.afterAll(async () => server.closeDatabase());
// Not needed in playwright? Uses different timing model.
/*
if (options?.pageLoadTimeout) {
setDriverTimeoutsForSuite({pageLoad: options.pageLoadTimeout});
}
*/
return setupRequirement({team: true, ...options});
}
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();
test.afterAll(() => cleanup.runCleanup('all'));
test.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.shareSupportWorkspaceForSuitePlaywright(); // TODO: Remove after the support workspace is removed from the backend.
gu.addSamplesForSuitePlaywright();
}
}
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;
}