mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
Adds working Boot.playwright.ts
This commit is contained in:
parent
ffa81a78b1
commit
a2dd5292db
@ -1,8 +1,8 @@
|
|||||||
import {assert, driver} from 'mocha-webdriver';
|
import { assert } from 'mocha-webdriver';
|
||||||
import * as gu from 'test/nbrowser/playwrightGristUtils';
|
import * as gu from 'test/nbrowser/playwrightGristUtils';
|
||||||
import { server, setupTestSuite } from 'test/nbrowser/playwrightTestUtils';
|
import { server, setupTestSuite } from 'test/nbrowser/playwrightTestUtils';
|
||||||
import * as testUtils from 'test/server/testUtils';
|
import * as testUtils from 'test/server/testUtils';
|
||||||
import { test, expect, Page } from '@playwright/test';
|
import { expect, Page, test } from '@playwright/test';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The boot page functionality has been merged with the Admin Panel.
|
* The boot page functionality has been merged with the Admin Panel.
|
||||||
@ -21,16 +21,16 @@ test.describe('Boot', () => {
|
|||||||
expect(text).toContain('GRIST_BOOT_KEY=example-');
|
expect(text).toContain('GRIST_BOOT_KEY=example-');
|
||||||
}
|
}
|
||||||
|
|
||||||
test('tells user about /admin', async function() {
|
test('tells user about /admin', async function({ page }) {
|
||||||
await driver.get(`${server.getHost()}/boot`);
|
await page.goto(`${server.getHost()}/boot`);
|
||||||
assert.match(await driver.getPageSource(), /\/admin/);
|
assert.match(await page.content(), /\/admin/);
|
||||||
// Switch to a regular place to that gu.checkForErrors won't panic -
|
// Switch to a regular place to that gu.checkForErrors won't panic -
|
||||||
// it needs a Grist page.
|
// it needs a Grist page.
|
||||||
await driver.get(`${server.getHost()}`);
|
await page.goto(`${server.getHost()}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('gives prompt about how to enable boot page', async function({ page }) {
|
test('gives prompt about how to enable boot page', async function({ page }) {
|
||||||
await driver.get(`${server.getHost()}/admin`);
|
await page.goto(`${server.getHost()}/admin`);
|
||||||
await hasPrompt(page);
|
await hasPrompt(page);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -47,18 +47,18 @@ test.describe('Boot', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('gives prompt when key is missing', async function({ page }) {
|
test('gives prompt when key is missing', async function({ page }) {
|
||||||
await driver.get(`${server.getHost()}/admin`);
|
await page.goto(`${server.getHost()}/admin`);
|
||||||
await hasPrompt(page);
|
await hasPrompt(page);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('gives prompt when key is wrong', async function({ page }) {
|
test('gives prompt when key is wrong', async function({ page }) {
|
||||||
await driver.get(`${server.getHost()}/admin?boot-key=bilbo`);
|
await page.goto(`${server.getHost()}/admin?boot-key=bilbo`);
|
||||||
await hasPrompt(page);
|
await hasPrompt(page);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('gives page when key is right', async function() {
|
test('gives page when key is right', async function({ page }) {
|
||||||
await driver.get(`${server.getHost()}/admin?boot-key=lala`);
|
await page.goto(`${server.getHost()}/admin?boot-key=lala`);
|
||||||
await driver.findContentWait('div', /Is home page available/, 2000);
|
await expect(page.getByText(/Is home page available/)).toBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,9 @@
|
|||||||
import { expect, Page } from '@playwright/test';
|
import { expect, Page } from '@playwright/test';
|
||||||
|
import { HomeUtil } from "./playwrightHomeUtil";
|
||||||
|
import * as testUtils from "../server/testUtils";
|
||||||
|
import { server } from "./testServer";
|
||||||
|
|
||||||
|
export const homeUtil = new HomeUtil(testUtils.fixturesRoot, server);
|
||||||
|
|
||||||
export async function checkForErrors(page: Page) {
|
export async function checkForErrors(page: Page) {
|
||||||
const errors = await page.evaluate(() => (window as any).getAppErrors());
|
const errors = await page.evaluate(() => (window as any).getAppErrors());
|
||||||
|
156
test/nbrowser/playwrightHomeUtil.ts
Normal file
156
test/nbrowser/playwrightHomeUtil.ts
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
/**
|
||||||
|
* Contains some non-webdriver functionality needed by tests.
|
||||||
|
*/
|
||||||
|
import FormData from 'form-data';
|
||||||
|
import { WebDriver } from 'mocha-webdriver';
|
||||||
|
import fetch from 'node-fetch';
|
||||||
|
import { UserAPI, UserAPIImpl } from 'app/common/UserAPI';
|
||||||
|
import { HomeDBManager } from 'app/gen-server/lib/HomeDBManager';
|
||||||
|
import { TestingHooksClient } from 'app/server/lib/TestingHooks';
|
||||||
|
import { BrowserContext, Page } from "@playwright/test";
|
||||||
|
import EventEmitter = require('events');
|
||||||
|
|
||||||
|
export interface Server extends EventEmitter {
|
||||||
|
driver: WebDriver;
|
||||||
|
getTestingHooks(): Promise<TestingHooksClient>;
|
||||||
|
getHost(): string;
|
||||||
|
getUrl(team: string, relPath: string): string;
|
||||||
|
getDatabase(): Promise<HomeDBManager>;
|
||||||
|
isExternalServer(): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HomeUtil {
|
||||||
|
// Cache api keys of test users. It is often convenient to have various instances
|
||||||
|
// of the home api available while making browser tests.
|
||||||
|
private _apiKey = new Map<string, string>();
|
||||||
|
|
||||||
|
constructor(public fixturesRoot: string, public server: Server) {
|
||||||
|
server.on('stop', () => {
|
||||||
|
this._apiKey.clear();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove any simulated login from the current session (for the given org, if specified).
|
||||||
|
* For testing against an external server, all logins are removed, since there's no way
|
||||||
|
* to be more nuanced.
|
||||||
|
*/
|
||||||
|
public async removeLogin(page: Page, org: string = "") {
|
||||||
|
// If cursor is on field editor, escape before remove login
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
if (!this.server.isExternalServer()) {
|
||||||
|
const testingHooks = await this.server.getTestingHooks();
|
||||||
|
const sid = await this.getGristSid(page.context());
|
||||||
|
if (sid) { await testingHooks.setLoginSessionProfile(sid, null, org); }
|
||||||
|
} else {
|
||||||
|
await page.goto(`${this.server.getHost()}/logout`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current Grist session-id (for the selenium browser accessing this server),
|
||||||
|
* or null if there is no session.
|
||||||
|
*/
|
||||||
|
public async getGristSid(context: BrowserContext): Promise<string|null> {
|
||||||
|
// Load a cheap page on our server to get the session-id cookie from browser.
|
||||||
|
const newPage = await context.newPage();
|
||||||
|
await newPage.goto(`${this.server.getHost()}/test/session`);
|
||||||
|
const cookie = (await context.cookies())
|
||||||
|
.find(cookie => cookie.name === (process.env.GRIST_SESSION_COOKIE || 'grist_sid'));
|
||||||
|
if (!cookie) { return null; }
|
||||||
|
return decodeURIComponent(cookie.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new document.
|
||||||
|
*/
|
||||||
|
public async createNewDoc(username: string, org: string, workspace: string, docName: string,
|
||||||
|
options: {email?: string} = {}) {
|
||||||
|
const homeApi = this.createHomeApi(username, org, options.email);
|
||||||
|
const workspaceId = await this.getWorkspaceId(homeApi, workspace);
|
||||||
|
return await homeApi.newDoc({name: docName}, workspaceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a copy of a doc. Similar to importFixturesDoc, but starts with an existing docId.
|
||||||
|
*/
|
||||||
|
public async copyDoc(username: string, org: string, workspace: string,
|
||||||
|
docId: string, options: {newName?: string} = {}) {
|
||||||
|
const homeApi = this.createHomeApi(username, org);
|
||||||
|
const docWorker = await homeApi.getWorkerAPI('import');
|
||||||
|
const workspaceId = await this.getWorkspaceId(homeApi, workspace);
|
||||||
|
const uploadId = await docWorker.copyDoc(docId);
|
||||||
|
return docWorker.importDocToWorkspace(uploadId, workspaceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A helper that find a workspace id by name for a given username and org.
|
||||||
|
public async getWorkspaceId(homeApi: UserAPIImpl, workspace: string): Promise<number> {
|
||||||
|
return (await homeApi.getOrgWorkspaces('current')).find((w) => w.name === workspace)!.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A helper that returns the list of names of all documents within a workspace.
|
||||||
|
public async listDocs(homeApi: UserAPI, wid: number): Promise<string[]> {
|
||||||
|
const workspace = await homeApi.getWorkspace(wid);
|
||||||
|
return workspace.docs.map(d => d.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A helper to create a UserAPI instance for a given useranme and org, that targets the home server
|
||||||
|
// Username can be null for anonymous access.
|
||||||
|
public createHomeApi(username: string|null, org: string, email?: string): UserAPIImpl {
|
||||||
|
const apiKey = this.getApiKey(username, email);
|
||||||
|
return this._createHomeApiUsingApiKey(apiKey, org);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getApiKey(username: string|null, email?: string): string | null {
|
||||||
|
const name = (username || '').toLowerCase();
|
||||||
|
const apiKey = username && ((email && this._apiKey.get(email)) || `api_key_for_${name}`);
|
||||||
|
return apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether we are currently on any login page (including the test page).
|
||||||
|
*/
|
||||||
|
public async isOnLoginPage(page: Page) {
|
||||||
|
return await this.isOnGristLoginPage(page) || await this.isOnTestLoginPage(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether we are currently on a Grist login page.
|
||||||
|
*/
|
||||||
|
public async isOnGristLoginPage(page: Page) {
|
||||||
|
const isOnSignupPage = await page.locator('css=.test-sp-heading').count() > 0;
|
||||||
|
const isOnLoginPage = await page.locator('css=.test-lp-heading').count() > 0;
|
||||||
|
return isOnSignupPage || isOnLoginPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether we are currently on the test login page.
|
||||||
|
*/
|
||||||
|
public async isOnTestLoginPage(page: Page) {
|
||||||
|
return await page.getByText('A Very Credulous Login Page').count() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
private async _getApiKey(page: Page): Promise<string> {
|
||||||
|
return page.evaluate(() => {
|
||||||
|
const app = (window as any).gristApp;
|
||||||
|
if (!app) { return ""; }
|
||||||
|
const api: UserAPI = app.topAppModel.api;
|
||||||
|
return api.fetchApiKey().then(key => {
|
||||||
|
if (key) { return key; }
|
||||||
|
return api.createApiKey();
|
||||||
|
}).catch(() => "");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make a home api instance with the given api key, for the specified org.
|
||||||
|
// If no api key given, work anonymously.
|
||||||
|
private _createHomeApiUsingApiKey(apiKey: string|null, org?: string): UserAPIImpl {
|
||||||
|
const headers = apiKey ? {Authorization: `Bearer ${apiKey}`} : undefined;
|
||||||
|
return new UserAPIImpl(org ? this.server.getUrl(org, '') : this.server.getHost(), {
|
||||||
|
headers,
|
||||||
|
fetch: fetch as any,
|
||||||
|
newFormData: () => new FormData() as any, // form-data isn't quite type compatible
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -57,7 +57,8 @@ export function setupTestSuite(options?: TestSuiteOptions) {
|
|||||||
// Also, log out, to avoid logins interacting, unless NO_CLEANUP is requested (useful for
|
// Also, log out, to avoid logins interacting, unless NO_CLEANUP is requested (useful for
|
||||||
// debugging tests).
|
// debugging tests).
|
||||||
if (!process.env.NO_CLEANUP) {
|
if (!process.env.NO_CLEANUP) {
|
||||||
test.afterAll(() => server.removeLogin());
|
// Don't think this is needed, each test runs in its own browser context.
|
||||||
|
//test.afterAll(() => server.removeLogin());
|
||||||
}
|
}
|
||||||
|
|
||||||
// If requested, clear user preferences for all test users after this suite.
|
// If requested, clear user preferences for all test users after this suite.
|
||||||
@ -153,7 +154,7 @@ export function setupRequirement(options: TestSuiteOptions) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
before(async function() {
|
test.beforeAll(async function() {
|
||||||
|
|
||||||
if (new URL(server.getHost()).hostname !== 'localhost') {
|
if (new URL(server.getHost()).hostname !== 'localhost') {
|
||||||
// Non-dev servers should already meet the requirements; in any case we should not
|
// Non-dev servers should already meet the requirements; in any case we should not
|
||||||
|
@ -27,6 +27,7 @@ import * as path from 'path';
|
|||||||
import {removeConnection} from 'test/gen-server/seed';
|
import {removeConnection} from 'test/gen-server/seed';
|
||||||
import {HomeUtil} from 'test/nbrowser/homeUtil';
|
import {HomeUtil} from 'test/nbrowser/homeUtil';
|
||||||
import {getDatabase} from 'test/testUtils';
|
import {getDatabase} from 'test/testUtils';
|
||||||
|
import { expect } from "@playwright/test";
|
||||||
|
|
||||||
export class TestServerMerged extends EventEmitter implements IMochaServer {
|
export class TestServerMerged extends EventEmitter implements IMochaServer {
|
||||||
public testDir: string;
|
public testDir: string;
|
||||||
@ -253,10 +254,10 @@ export class TestServerMerged extends EventEmitter implements IMochaServer {
|
|||||||
* Wait for the server to be up and responsitve, for up to `ms` milliseconds.
|
* Wait for the server to be up and responsitve, for up to `ms` milliseconds.
|
||||||
*/
|
*/
|
||||||
public async waitServerReady(ms: number): Promise<void> {
|
public async waitServerReady(ms: number): Promise<void> {
|
||||||
await this.driver.wait(() => Promise.race([
|
await expect.poll(() => Promise.race([
|
||||||
this.isServerReady(),
|
this.isServerReady(),
|
||||||
this._exitPromise.then(() => { throw new Error("Server exited while waiting for it"); }),
|
this._exitPromise.then(() => { throw new Error("Server exited while waiting for it"); }),
|
||||||
]), ms);
|
]), { timeout: ms }).toBe(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
Reference in New Issue
Block a user