mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
466 lines
18 KiB
TypeScript
466 lines
18 KiB
TypeScript
|
import {TelemetryLevel} from 'app/common/Telemetry';
|
||
|
import * as gu from 'test/nbrowser/playwrightGristUtils';
|
||
|
import {server, setupTestSuite} from 'test/nbrowser/playwrightTestUtils';
|
||
|
import {Defer, serveSomething, Serving} from 'test/server/customUtil';
|
||
|
import * as testUtils from 'test/server/testUtils';
|
||
|
import express from 'express';
|
||
|
import { expect, Locator, Page, test } from '@playwright/test';
|
||
|
|
||
|
test.describe('AdminPanel', function() {
|
||
|
setupTestSuite();
|
||
|
|
||
|
let oldEnv: testUtils.EnvironmentSnapshot;
|
||
|
let fakeServer: FakeUpdateServer;
|
||
|
|
||
|
test.afterEach(({ page }) => gu.checkForErrors(page));
|
||
|
|
||
|
test.beforeAll(async ({ browser }) => {
|
||
|
oldEnv = new testUtils.EnvironmentSnapshot();
|
||
|
process.env.GRIST_TEST_SERVER_DEPLOYMENT_TYPE = 'core';
|
||
|
process.env.GRIST_DEFAULT_EMAIL = gu.session(await browser.newContext()).email;
|
||
|
fakeServer = await startFakeServer();
|
||
|
process.env.GRIST_TEST_VERSION_CHECK_URL = `${fakeServer.url()}/version`;
|
||
|
await server.restart(true);
|
||
|
});
|
||
|
|
||
|
test.afterAll(async () => {
|
||
|
await fakeServer.close();
|
||
|
oldEnv.restore();
|
||
|
await server.restart(true);
|
||
|
});
|
||
|
|
||
|
const loginNonAdminSession = async (page: Page) => {
|
||
|
const session = gu.session(page.context()).user('user2').personalSite;
|
||
|
await session.login();
|
||
|
return session;
|
||
|
};
|
||
|
|
||
|
const loginAdminSession = async (page: Page) => {
|
||
|
const session = gu.session(page.context()).personalSite;
|
||
|
await session.login();
|
||
|
return session;
|
||
|
};
|
||
|
|
||
|
test('should show an explanation to non-managers', async ({ page }) => {
|
||
|
const session = await loginNonAdminSession(page);
|
||
|
|
||
|
await session.loadDocMenu(page, '/');
|
||
|
await gu.openAccountMenu(page);
|
||
|
|
||
|
await expect(page.locator('css=.test-usermenu-admin-panel')).not.toBeVisible();
|
||
|
await page.keyboard.press('Escape');
|
||
|
await expect(page.locator('css=.test-dm-admin-panel')).not.toBeVisible();
|
||
|
|
||
|
// Try loading the URL directly.
|
||
|
await page.goto(`${server.getHost()}/admin`);
|
||
|
await waitForAdminPanel(page);
|
||
|
await expect(page.locator('css=.test-admin-panel')).toBeVisible();
|
||
|
await expect(page.locator('css=.test-admin-panel')).toContainText(/Administrator Panel Unavailable/);
|
||
|
});
|
||
|
|
||
|
test('should be shown to managers', async ({ page }) => {
|
||
|
const session = await loginAdminSession(page);
|
||
|
console.log(process.env.GRIST_DEFAULT_EMAIL);
|
||
|
console.log(session.email);
|
||
|
|
||
|
await session.loadDocMenu(page, '/');
|
||
|
await expect(page.locator('css=.test-dm-admin-panel')).toBeVisible();
|
||
|
expect(await page.locator('css=.test-dm-admin-panel').getAttribute('href')).toMatch(/\/admin$/);
|
||
|
await gu.openAccountMenu(page);
|
||
|
await expect(page.locator('css=.test-usermenu-admin-panel')).toBeVisible();
|
||
|
expect(await page.locator('css=.test-usermenu-admin-panel').getAttribute('href')).toMatch(/\/admin$/);
|
||
|
await page.locator('css=.test-usermenu-admin-panel').click();
|
||
|
await waitForAdminPanel(page);
|
||
|
});
|
||
|
|
||
|
/*
|
||
|
it('should include support-grist section', async function() {
|
||
|
assert.match(page.locator('css=.test-admin-panel-item-sponsor').getText(), /Support Grist Labs on GitHub/);
|
||
|
await withExpandedItem('sponsor', async () => {
|
||
|
const button = page.locator('css=.test-support-grist-page-sponsorship-section');
|
||
|
assert.equal(await button.isDisplayed(), true);
|
||
|
assert.match(await button.getText(), /You can support Grist open-source/);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('supports opting in to telemetry from the page', async function() {
|
||
|
await assertTelemetryLevel('off');
|
||
|
|
||
|
let toggle = page.locator('css=.test-admin-panel-item-value-telemetry .widget_switch');
|
||
|
assert.equal(await isSwitchOn(toggle), false);
|
||
|
|
||
|
await withExpandedItem('telemetry', async () => {
|
||
|
assert.isFalse(page.locator('css=.test-support-grist-page-telemetry-section-message').isPresent());
|
||
|
await driver.findContentWait(
|
||
|
'.test-support-grist-page-telemetry-section button', /Opt in to Telemetry/, 2000).click();
|
||
|
await driver.findContentWait('.test-support-grist-page-telemetry-section button', /Opt out of Telemetry/, 2000);
|
||
|
assert.equal(
|
||
|
page.locator('css=.test-support-grist-page-telemetry-section-message').getText(),
|
||
|
'You have opted in to telemetry. Thank you! 🙏'
|
||
|
);
|
||
|
assert.equal(await isSwitchOn(toggle), true);
|
||
|
});
|
||
|
|
||
|
// Check it's still on after collapsing.
|
||
|
assert.equal(await isSwitchOn(toggle), true);
|
||
|
|
||
|
// Reload the page and check that the Grist config indicates telemetry is set to "limited".
|
||
|
await driver.navigate().refresh();
|
||
|
await waitForAdminPanel();
|
||
|
toggle = page.locator('css=.test-admin-panel-item-value-telemetry .widget_switch');
|
||
|
assert.equal(await isSwitchOn(toggle), true);
|
||
|
await toggleItem('telemetry');
|
||
|
await driver.findContentWait('.test-support-grist-page-telemetry-section button', /Opt out of Telemetry/, 2000);
|
||
|
assert.equal(
|
||
|
await driver.findWait('.test-support-grist-page-telemetry-section-message', 2000).getText(),
|
||
|
'You have opted in to telemetry. Thank you! 🙏'
|
||
|
);
|
||
|
await assertTelemetryLevel('limited');
|
||
|
});
|
||
|
|
||
|
it('supports opting out of telemetry from the page', async function() {
|
||
|
await driver.findContent('.test-support-grist-page-telemetry-section button', /Opt out of Telemetry/).click();
|
||
|
await driver.findContentWait('.test-support-grist-page-telemetry-section button', /Opt in to Telemetry/, 2000);
|
||
|
assert.isFalse(page.locator('css=.test-support-grist-page-telemetry-section-message').isPresent());
|
||
|
let toggle = page.locator('css=.test-admin-panel-item-value-telemetry .widget_switch');
|
||
|
assert.equal(await isSwitchOn(toggle), false);
|
||
|
|
||
|
// Reload the page and check that the Grist config indicates telemetry is set to "off".
|
||
|
await driver.navigate().refresh();
|
||
|
await waitForAdminPanel();
|
||
|
await toggleItem('telemetry');
|
||
|
await driver.findContentWait('.test-support-grist-page-telemetry-section button', /Opt in to Telemetry/, 2000);
|
||
|
assert.isFalse(page.locator('css=.test-support-grist-page-telemetry-section-message').isPresent());
|
||
|
await assertTelemetryLevel('off');
|
||
|
toggle = page.locator('css=.test-admin-panel-item-value-telemetry .widget_switch');
|
||
|
assert.equal(await isSwitchOn(toggle), false);
|
||
|
});
|
||
|
|
||
|
it('supports toggling telemetry from the toggle in the top line', async function() {
|
||
|
const toggle = page.locator('css=.test-admin-panel-item-value-telemetry .widget_switch');
|
||
|
assert.equal(await isSwitchOn(toggle), false);
|
||
|
await toggle.click();
|
||
|
await gu.waitForServer();
|
||
|
assert.equal(await isSwitchOn(toggle), true);
|
||
|
assert.match(page.locator('css=.test-support-grist-page-telemetry-section-message').getText(),
|
||
|
/You have opted in/);
|
||
|
await toggle.click();
|
||
|
await gu.waitForServer();
|
||
|
assert.equal(await isSwitchOn(toggle), false);
|
||
|
await withExpandedItem('telemetry', async () => {
|
||
|
assert.equal(page.locator('css=.test-support-grist-page-telemetry-section-message').isPresent(), false);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('shows telemetry opt-in status even when set via environment variable', async function() {
|
||
|
// Set the telemetry level to "limited" via environment variable and restart the server.
|
||
|
process.env.GRIST_TELEMETRY_LEVEL = 'limited';
|
||
|
await server.restart();
|
||
|
|
||
|
// Check that the Support Grist page reports telemetry is enabled.
|
||
|
await driver.get(`${server.getHost()}/admin`);
|
||
|
await waitForAdminPanel();
|
||
|
const toggle = page.locator('css=.test-admin-panel-item-value-telemetry .widget_switch');
|
||
|
assert.equal(await isSwitchOn(toggle), true);
|
||
|
await toggleItem('telemetry');
|
||
|
assert.equal(
|
||
|
await driver.findWait('.test-support-grist-page-telemetry-section-message', 2000).getText(),
|
||
|
'You have opted in to telemetry. Thank you! 🙏'
|
||
|
);
|
||
|
assert.isFalse(await driver.findContent('.test-support-grist-page-telemetry-section button',
|
||
|
/Opt out of Telemetry/).isPresent());
|
||
|
|
||
|
// Now set the telemetry level to "off" and restart the server.
|
||
|
process.env.GRIST_TELEMETRY_LEVEL = 'off';
|
||
|
await server.restart();
|
||
|
|
||
|
// Check that the Support Grist page reports telemetry is disabled.
|
||
|
await driver.get(`${server.getHost()}/admin`);
|
||
|
await waitForAdminPanel();
|
||
|
await toggleItem('telemetry');
|
||
|
assert.equal(
|
||
|
await driver.findWait('.test-support-grist-page-telemetry-section-message', 2000).getText(),
|
||
|
'You have opted out of telemetry.'
|
||
|
);
|
||
|
assert.isFalse(await driver.findContent('.test-support-grist-page-telemetry-section button',
|
||
|
/Opt in to Telemetry/).isPresent());
|
||
|
});
|
||
|
|
||
|
it('should show version', async function() {
|
||
|
await driver.get(`${server.getHost()}/admin`);
|
||
|
await waitForAdminPanel();
|
||
|
assert.equal(page.locator('css=.test-admin-panel-item-version').isDisplayed(), true);
|
||
|
assert.match(page.locator('css=.test-admin-panel-item-value-version').getText(), /^Version \d+\./);
|
||
|
});
|
||
|
|
||
|
it('should show sandbox', async function() {
|
||
|
await driver.get(`${server.getHost()}/admin`);
|
||
|
await waitForAdminPanel();
|
||
|
assert.equal(page.locator('css=.test-admin-panel-item-sandboxing').isDisplayed(), true);
|
||
|
await gu.waitToPass(
|
||
|
// unknown for grist-saas, unconfigured for grist-core.
|
||
|
async () => assert.match(page.locator('css=.test-admin-panel-item-value-sandboxing').getText(),
|
||
|
/^((unknown)|(unconfigured))/),
|
||
|
3000,
|
||
|
);
|
||
|
// It would be good to test other scenarios, but we are using
|
||
|
// a multi-server setup on grist-saas and the sandbox test isn't
|
||
|
// useful there yet.
|
||
|
});
|
||
|
|
||
|
it('should show various self checks', async function() {
|
||
|
await driver.get(`${server.getHost()}/admin`);
|
||
|
await waitForAdminPanel();
|
||
|
await gu.waitToPass(
|
||
|
async () => {
|
||
|
assert.equal(page.locator('css=.test-admin-panel-item-name-probe-reachable').isDisplayed(), true);
|
||
|
assert.match(page.locator('css=.test-admin-panel-item-value-probe-reachable').getText(), /✅/);
|
||
|
},
|
||
|
3000,
|
||
|
);
|
||
|
assert.equal(page.locator('css=.test-admin-panel-item-name-probe-system-user').isDisplayed(), true);
|
||
|
await gu.waitToPass(
|
||
|
async () => assert.match(page.locator('css=.test-admin-panel-item-value-probe-system-user').getText(), /✅/),
|
||
|
3000,
|
||
|
);
|
||
|
});
|
||
|
|
||
|
const upperCheckNow = () => page.locator('css=.test-admin-panel-updates-upper-check-now');
|
||
|
const lowerCheckNow = () => page.locator('css=.test-admin-panel-updates-lower-check-now');
|
||
|
const autoCheckToggle = () => page.locator('css=.test-admin-panel-updates-auto-check');
|
||
|
const updateMessage = () => page.locator('css=.test-admin-panel-updates-message');
|
||
|
const versionBox = () => page.locator('css=.test-admin-panel-updates-version');
|
||
|
function waitForStatus(message: RegExp) {
|
||
|
return gu.waitToPass(async () => {
|
||
|
assert.match(await updateMessage().getText(), message);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
it('should check for updates', async function() {
|
||
|
// Clear any cached settings.
|
||
|
await driver.executeScript('window.sessionStorage.clear(); window.localStorage.clear();');
|
||
|
await driver.navigate().refresh();
|
||
|
await waitForAdminPanel();
|
||
|
|
||
|
// By default don't have any info.
|
||
|
await waitForStatus(/No information available/);
|
||
|
|
||
|
// We see upper check-now button.
|
||
|
assert.isTrue(await upperCheckNow().isDisplayed());
|
||
|
|
||
|
// We can expand.
|
||
|
await toggleItem('updates');
|
||
|
|
||
|
// We see a toggle to update automatically.
|
||
|
assert.isTrue(await autoCheckToggle().isDisplayed());
|
||
|
assert.isFalse(await isSwitchOn(autoCheckToggle()));
|
||
|
|
||
|
// We can click it, Grist will turn on auto checks and do it right away.
|
||
|
fakeServer.pause();
|
||
|
await autoCheckToggle().click();
|
||
|
assert.isTrue(await isSwitchOn(autoCheckToggle()));
|
||
|
|
||
|
// It will first show "Checking for updates" message.
|
||
|
// (Request is blocked by fake server, so it will not complete until we resume it.)
|
||
|
await waitForStatus(/Checking for updates/);
|
||
|
|
||
|
// Upper check now button is removed.
|
||
|
assert.isFalse(await upperCheckNow().isPresent());
|
||
|
|
||
|
// Resume server and respond.
|
||
|
fakeServer.resume();
|
||
|
|
||
|
// It will show "New version available" message.
|
||
|
await waitForStatus(/Newer version available/);
|
||
|
// And a version number.
|
||
|
assert.isTrue(await versionBox().isDisplayed());
|
||
|
assert.match(await versionBox().getText(), /Version 9\.9\.9/);
|
||
|
|
||
|
// When we reload, we will auto check for updates.
|
||
|
fakeServer.pause();
|
||
|
fakeServer.latestVersion = await currentVersion();
|
||
|
await driver.navigate().refresh();
|
||
|
await waitForAdminPanel();
|
||
|
await waitForStatus(/Checking for updates/);
|
||
|
fakeServer.resume();
|
||
|
await waitForStatus(/Grist is up to date/);
|
||
|
|
||
|
// Disable auto-checks.
|
||
|
await toggleItem('updates');
|
||
|
assert.isTrue(await isSwitchOn(autoCheckToggle()));
|
||
|
await autoCheckToggle().click();
|
||
|
assert.isFalse(await isSwitchOn(autoCheckToggle()));
|
||
|
// Nothing should happen.
|
||
|
await waitForStatus(/Grist is up to date/);
|
||
|
assert.isTrue(await versionBox().isDisplayed());
|
||
|
assert.equal(await versionBox().getText(), `Version ${await currentVersion()}`);
|
||
|
|
||
|
// Refresh to see if we are disabled.
|
||
|
fakeServer.pause();
|
||
|
await driver.navigate().refresh();
|
||
|
await waitForAdminPanel();
|
||
|
await waitForStatus(/Last checked .+ ago/);
|
||
|
fakeServer.resume();
|
||
|
// Expand and see if the toggle is off.
|
||
|
await toggleItem('updates');
|
||
|
assert.isFalse(await isSwitchOn(autoCheckToggle()));
|
||
|
});
|
||
|
|
||
|
it('shows up-to-date message', async function() {
|
||
|
fakeServer.latestVersion = await currentVersion();
|
||
|
// Click upper check now.
|
||
|
await waitForStatus(/Last checked .+ ago/);
|
||
|
await upperCheckNow().click();
|
||
|
await waitForStatus(/Grist is up to date/);
|
||
|
|
||
|
// Update version once again.
|
||
|
fakeServer.latestVersion = '9.9.10';
|
||
|
// Click lower check now.
|
||
|
fakeServer.pause();
|
||
|
await lowerCheckNow().click();
|
||
|
await waitForStatus(/Checking for updates/);
|
||
|
fakeServer.resume();
|
||
|
await waitForStatus(/Newer version available/);
|
||
|
|
||
|
// Make sure we see the new version.
|
||
|
assert.isTrue(await versionBox().isDisplayed());
|
||
|
assert.match(await versionBox().getText(), /Version 9\.9\.10/);
|
||
|
});
|
||
|
|
||
|
it('shows error message', async function() {
|
||
|
fakeServer.failNext = true;
|
||
|
fakeServer.pause();
|
||
|
await lowerCheckNow().click();
|
||
|
await waitForStatus(/Checking for updates/);
|
||
|
fakeServer.resume();
|
||
|
await waitForStatus(/Error checking for updates/);
|
||
|
assert.match((await gu.getToasts())[0], /some error/);
|
||
|
await gu.wipeToasts();
|
||
|
});
|
||
|
|
||
|
it('should send telemetry data', async function() {
|
||
|
assert.deepEqual({...fakeServer.payload, installationId: 'test'}, {
|
||
|
installationId: 'test',
|
||
|
deploymentType: 'core',
|
||
|
currentVersion: await currentVersion(),
|
||
|
});
|
||
|
assert.isNotEmpty(fakeServer.payload.installationId);
|
||
|
});
|
||
|
|
||
|
it('should survive APP_HOME_URL misconfiguration', async function() {
|
||
|
process.env.APP_HOME_URL = 'http://misconfigured.invalid';
|
||
|
process.env.GRIST_BOOT_KEY = 'zig';
|
||
|
await server.restart(true);
|
||
|
await driver.get(`${server.getHost()}/admin`);
|
||
|
await waitForAdminPanel();
|
||
|
});
|
||
|
|
||
|
it('should honor GRIST_BOOT_KEY fallback', async function() {
|
||
|
await gu.removeLogin();
|
||
|
await driver.get(`${server.getHost()}/admin`);
|
||
|
await waitForAdminPanel();
|
||
|
assert.equal(page.locator('css=.test-admin-panel').isDisplayed(), true);
|
||
|
assert.match(page.locator('css=.test-admin-panel').getText(), /Administrator Panel Unavailable/);
|
||
|
|
||
|
process.env.GRIST_BOOT_KEY = 'zig';
|
||
|
await server.restart(true);
|
||
|
await driver.get(`${server.getHost()}/admin?boot-key=zig`);
|
||
|
await waitForAdminPanel();
|
||
|
assert.equal(page.locator('css=.test-admin-panel').isDisplayed(), true);
|
||
|
assert.notMatch(page.locator('css=.test-admin-panel').getText(), /Administrator Panel Unavailable/);
|
||
|
await driver.get(`${server.getHost()}/admin?boot-key=zig-wrong`);
|
||
|
await waitForAdminPanel();
|
||
|
assert.equal(page.locator('css=.test-admin-panel').isDisplayed(), true);
|
||
|
assert.match(page.locator('css=.test-admin-panel').getText(), /Administrator Panel Unavailable/);
|
||
|
});
|
||
|
|
||
|
*/
|
||
|
});
|
||
|
|
||
|
// @ts-ignore
|
||
|
async function assertTelemetryLevel(page: Page, level: TelemetryLevel) {
|
||
|
const telemetryLevel = await page.evaluate(() => (window as any).gristConfig.telemetry?.telemetryLevel);
|
||
|
expect(telemetryLevel).toEqual(level);
|
||
|
}
|
||
|
|
||
|
async function toggleItem(page: Page, itemId: string) {
|
||
|
const header = await page.locator(`css=.test-admin-panel-item-name-${itemId}`);
|
||
|
await header.click();
|
||
|
// Playwright should handle waiting for expand/collapse
|
||
|
return header;
|
||
|
}
|
||
|
|
||
|
// @ts-ignore
|
||
|
async function withExpandedItem(page: Page, itemId: string, callback: () => Promise<void>) {
|
||
|
const header = await toggleItem(page, itemId);
|
||
|
await callback();
|
||
|
await header.click();
|
||
|
// Playwright should wait for collapse.
|
||
|
}
|
||
|
|
||
|
// @ts-ignore
|
||
|
const isSwitchOn = async (switchElem: Locator) => await switchElem.locator('css=[class*=switch_on]').count() > 0;
|
||
|
const waitForAdminPanel = (page: Page) => page.locator('css=.test-admin-panel').waitFor();
|
||
|
|
||
|
interface FakeUpdateServer {
|
||
|
latestVersion: string;
|
||
|
failNext: boolean;
|
||
|
payload: any;
|
||
|
close: () => Promise<void>;
|
||
|
pause: () => void;
|
||
|
resume: () => void;
|
||
|
url: () => string;
|
||
|
}
|
||
|
|
||
|
async function startFakeServer() {
|
||
|
let mutex: Defer|null = null;
|
||
|
const API: FakeUpdateServer = {
|
||
|
latestVersion: '9.9.9',
|
||
|
failNext: false,
|
||
|
payload: null,
|
||
|
close: async () => {
|
||
|
mutex?.resolve();
|
||
|
mutex = null;
|
||
|
await server?.shutdown();
|
||
|
server = null;
|
||
|
},
|
||
|
pause: () => {
|
||
|
mutex = new Defer();
|
||
|
},
|
||
|
resume: () => {
|
||
|
mutex?.resolve();
|
||
|
mutex = null;
|
||
|
},
|
||
|
url: () => {
|
||
|
return server!.url;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
let server: Serving|null = await serveSomething((app) => {
|
||
|
app.use(express.json());
|
||
|
app.post('/version', async (req, res, next) => {
|
||
|
API.payload = req.body;
|
||
|
try {
|
||
|
await mutex;
|
||
|
if (API.failNext) {
|
||
|
res.status(500).json({error: 'some error'});
|
||
|
API.failNext = false;
|
||
|
return;
|
||
|
}
|
||
|
res.json({latestVersion: API.latestVersion});
|
||
|
} catch(ex) {
|
||
|
next(ex);
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
|
||
|
return API;
|
||
|
}
|
||
|
|
||
|
// @ts-ignore
|
||
|
async function currentVersion(page: Page) {
|
||
|
const currentVersionText = await page.locator("css=.test-admin-panel-item-value-version").textContent() ?? '';
|
||
|
return currentVersionText.match(/Version (.+)/)![1];
|
||
|
}
|
||
|
|